docs(site): complete menu radio group references with demos and options hooks (#1807)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-07-14 15:34:52 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 82b9e43bb5
commit a9a09a7e52
54 changed files with 1953 additions and 77 deletions
+71 -2
View File
@@ -23,12 +23,13 @@ import type {
CSSVarsExtraction,
DataAttrDef,
DataAttrsExtraction,
ExtraDataAttrsSource,
PartReference,
PartSource,
PropDef,
StateDef,
} from './types.js';
import { kebabToPascal, partKebabFromSource, sortProps } from './utils.js';
import { getJSDocTagValue, kebabToPascal, log, partKebabFromSource, sortProps } from './utils.js';
// ─── Overrides ─────────────────────────────────────────────────────
@@ -105,6 +106,48 @@ export function buildCSSVars(cssVarsData: CSSVarsExtraction): Record<string, CSS
// ─── Discovery ─────────────────────────────────────────────────────
// Extra data-attrs files in a component dir ({kebab}-{x}-data-attrs.ts)
// declare their target parts with a `@parts item, radio-item` JSDoc tag on
// the exported const. They cover attrs a DOM layer applies to part elements
// directly, which the per-part stateAttrMap heuristic can't see (e.g.
// menu-item-data-attrs applied by create-menu).
function dataAttrsComponentName(fileBasename: string): string {
return kebabToPascal(fileBasename.replace(/-data-attrs\.ts$/, ''));
}
function discoverExtraDataAttrs(componentDir: string, componentKebab: string): ExtraDataAttrsSource[] {
const extras: ExtraDataAttrsSource[] = [];
const mainFile = `${componentKebab}-data-attrs.ts`;
for (const file of fs.readdirSync(componentDir)) {
if (!file.endsWith('-data-attrs.ts') || file === mainFile) continue;
const filePath = path.join(componentDir, file);
const exportName = `${dataAttrsComponentName(file)}DataAttrs`;
const sourceFile = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf-8'), ts.ScriptTarget.Latest, true);
let tagValue: string | undefined;
ts.forEachChild(sourceFile, (node) => {
if (!ts.isVariableStatement(node)) return;
const declaresExport = node.declarationList.declarations.some(
(decl) => ts.isIdentifier(decl.name) && decl.name.text === exportName
);
if (declaresExport) tagValue = getJSDocTagValue(node, 'parts');
});
if (!tagValue) continue;
const parts = tagValue
.split(',')
.map((part) => part.trim())
.filter(Boolean);
if (parts.length === 0) continue;
extras.push({ path: filePath, parts });
}
return extras;
}
export function discoverComponents(monorepoRoot: string): ComponentSource[] {
const coreUiPath = path.join(monorepoRoot, 'packages/core/src/core/ui');
const htmlUiPath = path.join(monorepoRoot, 'packages/html/src/ui');
@@ -142,6 +185,9 @@ export function discoverComponents(monorepoRoot: string): ComponentSource[] {
const partsIndexFile = path.join(reactUiPath, dir.name, 'index.parts.ts');
if (fs.existsSync(partsIndexFile)) source.partsIndexPath = partsIndexFile;
const extraDataAttrs = discoverExtraDataAttrs(componentDir, dir.name);
if (extraDataAttrs.length > 0) source.extraDataAttrs = extraDataAttrs;
if (source.corePath) {
components.push(source);
}
@@ -154,7 +200,6 @@ export function discoverComponents(monorepoRoot: string): ComponentSource[] {
export function createComponentProgram(sources: ComponentSource[], monorepoRoot: string): ts.Program {
const htmlUiPath = path.join(monorepoRoot, 'packages/html/src/ui');
const coreUiPath = path.join(monorepoRoot, 'packages/core/src/core/ui');
const files: string[] = [];
for (const source of sources) {
@@ -163,6 +208,7 @@ export function createComponentProgram(sources: ComponentSource[], monorepoRoot:
if (source.cssVarsPath) files.push(source.cssVarsPath);
if (source.htmlPath) files.push(source.htmlPath);
if (source.partsIndexPath) files.push(source.partsIndexPath);
if (source.extraDataAttrs) files.push(...source.extraDataAttrs.map((extra) => extra.path));
if (source.partsIndexPath) {
const htmlDir = path.join(htmlUiPath, source.kebab);
@@ -458,6 +504,25 @@ function buildMultiPartReference(
}
}
for (const extra of source.extraDataAttrs ?? []) {
const componentName = dataAttrsComponentName(path.basename(extra.path));
const extraData = extractDataAttrs(extra.path, program, componentName);
if (!extraData) {
log.warn(`No ${componentName}DataAttrs export found in ${extra.path}; skipping @parts merge`);
continue;
}
const extraAttrs = buildDataAttrs(extraData);
for (const partKebab of extra.parts) {
const partRef = partsRecord[partKebab];
if (!partRef) {
log.warn(`@parts in ${extra.path} references unknown part "${partKebab}" on ${source.name}`);
continue;
}
partRef.dataAttributes = { ...partRef.dataAttributes, ...extraAttrs };
}
}
return {
name: source.name,
props: {},
@@ -481,6 +546,10 @@ export function buildComponentReference(
}
}
if (source.extraDataAttrs?.length) {
log.warn(`Ignoring @parts data-attrs in ${source.kebab}: ${source.name} is not a multi-part component`);
}
return buildSingleComponentReference(source, program);
}
@@ -23,7 +23,9 @@
* Core instantiation, sub-parts with/without HTML elements,
* React-only parts (no platforms.html), sub-part data-attr
* inheritance (stateAttrMap heuristic), non-boolean type
* inference (number, string literal union via type alias).
* inference (number, string literal union via type alias),
* extra @parts-tagged data-attrs files attaching to the
* listed parts (gauge-label-data-attrs.ts).
* slider/ Base multi-part component. Exercises: base component whose
* parts are re-exported by domain variants.
* volume-slider/ Domain variant. Exercises: re-exported parts from slider,
@@ -35,6 +37,13 @@
* create* factory, mixin display name stripping, selector discovery,
* @label overloads, slug collision (react vs html create-player),
* framework assignment.
* ui/rate-options/ Hook re-exported through a directory index
* (entry index ./ui/rate-options ./use-rate-options), with a
* namespace merged onto the function (Props/Result pattern).
* Exercises: recursive re-export resolution in util discovery,
* entry-visibility filtering (useRateInternals is scanned but never
* re-exported to the entry), and skipping re-exports that resolve to
* a directory with no index.ts (./legacy holds only compiled JS).
*
* Features (packages/core/src/dom/store/features/):
* playback.ts Simple feature. Exercises: boolean state properties,
@@ -343,6 +352,28 @@ describe('Component pipeline (end-to-end)', () => {
expect(label.platforms.react).toEqual({});
expect(label.platforms.html).toBeUndefined();
});
// Extra data-attrs files ({component}-{x}-data-attrs.ts, next to the
// main {component}-data-attrs.ts) declare their target parts with a
// @parts JSDoc tag. This covers attrs that a DOM layer applies to
// parts directly, invisible to the per-part stateAttrMap heuristic
// (e.g. menu-item-data-attrs.ts applied by create-menu.ts).
it('extra @parts-tagged data-attrs file attaches to listed parts', () => {
const parts = findComponent('Gauge')!.reference.parts!;
// label: no other attrs — gets the extra file's attrs
expect(parts.label!.dataAttributes['data-emphasized']).toMatchObject({
description: 'Present when the value is emphasized.',
});
// fill: extra attrs merge with attrs inherited via stateAttrMap
expect(parts.fill!.dataAttributes['data-emphasized']).toBeDefined();
expect(parts.fill!.dataAttributes['data-percentage']).toBeDefined();
// parts not listed in @parts are untouched
expect(parts.track!.dataAttributes).toEqual({});
expect(parts.indicator!.dataAttributes['data-emphasized']).toBeUndefined();
});
});
// ─────────────────────────────────────────────────────────────────
@@ -539,6 +570,39 @@ describe('Util pipeline (end-to-end)', () => {
expect(findByName('useFormat', 'react')).toBeDefined();
});
// Entry indexes often re-export hooks through a directory index
// (entry index → ./ui/rate-options → ./use-rate-options). Discovery
// must follow re-export hops to the declaring module so JSDoc and
// overload extraction read the real source file. The fixture also
// merges a namespace onto the hook (the repo's Props/Result pattern),
// which must not break FunctionNode detection.
it('discovers hooks re-exported through a directory index', () => {
const entry = findByName('useRateOptions', 'react');
expect(entry).toBeDefined();
expect(entry!.slug).toBe('use-rate-options');
expect(entry!.data.description).toContain('Create rate menu options');
const overload = entry!.data.overloads[0]!;
expect(overload.parameters.props).toBeDefined();
expect(overload.parameters.props!.description).toContain('formatRate');
});
// Whole modules are scanned, but only names visible from the entry
// point (through named re-exports and local `export *` chains) are
// public API. useRateInternals matches the use* convention and lives
// in a scanned file, but is never re-exported up to the entry.
it('excludes exports that are not visible from the entry point', () => {
expect(findByName('useRateInternals', 'react')).toBeUndefined();
});
// rate-options/index.ts re-exports './legacy', which resolves to a
// directory with no index.ts (only compiled index.js). Discovery must
// skip it — not crash reading a directory — and the unreachable export
// stays undocumented.
it('skips re-exports that resolve to a directory without index.ts', () => {
expect(findByName('useLegacyRate', 'react')).toBeUndefined();
});
it('discovers controllers from HTML entry points', () => {
expect(findByName('PlayerController', 'html')).toBeDefined();
expect(findByName('SnapshotController', 'html')).toBeDefined();
@@ -562,6 +626,13 @@ describe('Util pipeline (end-to-end)', () => {
expect(findByName('createPlayer', 'html')).toBeDefined();
expect(findByName('createSelector', null)).toBeDefined();
});
it('leaves external re-exports with their canonical entry point', () => {
const matches = entries.filter((entry) => entry.data.name === 'createSelector');
expect(matches).toHaveLength(1);
expect(matches[0]).toMatchObject({ slug: 'create-selector', framework: null });
});
});
// ─────────────────────────────────────────────────────────────────
@@ -0,0 +1,21 @@
/**
* Data attributes fixture for part-scoped attrs files.
*
* Exercises: extra data-attrs files ({component}-{x}-data-attrs.ts) declare
* their target parts with a @parts JSDoc tag on the exported const. Listed
* parts get the attrs merged into whatever they already have plain attach
* (label) and merge with attrs inherited via the stateAttrMap heuristic
* (fill). This header's own raw "@parts" mention is a deliberate hazard:
* the builder must bind to the tag in the JSDoc block closest to the export,
* not the first match anywhere in the file.
*/
/**
* Data attributes set on caption-like parts.
*
* @parts label, fill
*/
export const GaugeLabelDataAttrs = {
/** Present when the value is emphasized. */
emphasized: 'data-emphasized',
} as const;
@@ -1,4 +1,6 @@
export { usePlayer } from './player/context';
export { createPlayer } from './player/create-player';
export { useRateOptions } from './ui/rate-options';
export { createSelector } from './utils/external';
export { mergeProps } from './utils/merge-props';
export { useFormat } from './utils/use-format';
@@ -0,0 +1,8 @@
// Resolves to a directory with no index.ts — discovery must skip it, not crash.
export { useLegacyRate } from './legacy';
export {
type RateOption,
type RateOptionsProps,
type RateOptionsResult,
useRateOptions,
} from './use-rate-options';
@@ -0,0 +1,5 @@
// Compiled-only module (no TypeScript source): exercises discovery skipping
// a re-export specifier that resolves to a directory without index.ts/.tsx.
export function useLegacyRate() {
return 1;
}
@@ -0,0 +1,39 @@
export interface RateOptionsProps {
/** Custom formatter for visible rate labels. */
formatRate?: ((rate: number) => string) | undefined;
/** Whether rate selection is disabled. */
disabled?: boolean | undefined;
}
export interface RateOption {
rate: number;
label: string;
disabled: boolean;
}
export interface RateOptionsResult {
rate: number;
options: RateOption[];
setRate: (rate: number) => void;
}
/**
* Create rate menu options from the player rate state. Returns `null` when
* the rate feature is not configured.
*
* @param props - Optional `formatRate` and `disabled` overrides.
*/
export function useRateOptions(props?: RateOptionsProps): RateOptionsResult | null {
return props?.disabled ? null : { rate: 1, options: [], setRate: () => {} };
}
export namespace useRateOptions {
export type Props = RateOptionsProps;
export type Result = RateOptionsResult;
export type Option = RateOption;
}
/** Internal helper — matches the use* convention but is never re-exported to the entry point. */
export function useRateInternals(): number {
return 1;
}
@@ -0,0 +1 @@
export { createContext as createSelector } from 'react';
@@ -59,6 +59,14 @@ export interface ComponentSource {
htmlPath?: string;
/** Path to index.parts.ts (if multi-part) */
partsIndexPath?: string;
/** Extra part-scoped data-attrs files ({kebab}-{x}-data-attrs.ts with a `@parts` tag) */
extraDataAttrs?: ExtraDataAttrsSource[];
}
export interface ExtraDataAttrsSource {
path: string;
/** Part kebabs listed in the `@parts` JSDoc tag on the file's export */
parts: string[];
}
/**
+141 -44
View File
@@ -45,6 +45,7 @@ import {
} from '../../../src/types/util-reference.js';
import { utilReferenceSlug } from '../../../src/utils/utilReferenceSlug.js';
import { abbreviateType, formatDetailedType, formatType } from './formatter.js';
import { getJSDocTagValue, hasJSDocTag } from './utils.js';
const PREFIX = '\x1b[35m[api-docs-builder]\x1b[0m';
@@ -90,11 +91,12 @@ function resolveModulePath(fromFile: string, specifier: string): string {
const dir = path.dirname(fromFile);
const resolved = path.resolve(dir, specifier);
// Try exact match, then with extensions
// Try exact match, then with extensions. Require a file — a bare
// directory specifier must fall through to index resolution below.
const extensions = ['', '.ts', '.tsx'];
for (const ext of extensions) {
const full = resolved + ext;
if (fs.existsSync(full)) return full;
if (fs.existsSync(full) && fs.statSync(full).isFile()) return full;
}
// Try index files
@@ -106,21 +108,138 @@ function resolveModulePath(fromFile: string, specifier: string): string {
return resolved;
}
function resolveLocalModules(indexPath: string): string[] {
const sourceFile = ts.createSourceFile(indexPath, fs.readFileSync(indexPath, 'utf-8'), ts.ScriptTarget.Latest, true);
function isFile(filePath: string): boolean {
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
}
// Memoized: getUtilEntries resolves the same entry point twice (program
// creation + discovery), and each pass re-reads every module in the graph.
const localModulesCache = new Map<string, string[]>();
function resolveLocalModules(indexPath: string): string[] {
const cached = localModulesCache.get(indexPath);
if (cached) return cached;
const visited = new Set<string>([indexPath]);
const localPaths: string[] = [];
ts.forEachChild(sourceFile, (node) => {
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
const specifier = node.moduleSpecifier.text;
if (specifier.startsWith('.')) {
localPaths.push(resolveModulePath(indexPath, specifier));
// Post-order: a module's own re-exports are pushed before the module
// itself, so declaring files are scanned (and win seenKeys dedup) before
// the directory indexes that re-export them.
function collect(filePath: string): void {
const sourceFile = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf-8'), ts.ScriptTarget.Latest, true);
ts.forEachChild(sourceFile, (node) => {
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
const specifier = node.moduleSpecifier.text;
if (!specifier.startsWith('.')) return;
const resolved = resolveModulePath(filePath, specifier);
if (visited.has(resolved)) return;
visited.add(resolved);
// resolveModulePath falls back to the raw path when nothing matches;
// skip anything that isn't a readable file (e.g. a directory with no
// index.ts) instead of crashing on the read.
if (!isFile(resolved)) return;
collect(resolved);
localPaths.push(resolved);
}
});
}
collect(indexPath);
localModulesCache.set(indexPath, localPaths);
return localPaths;
}
// Names actually exported from an entry point, resolving local `export *`
// chains. External star re-exports (`@videojs/*`) are skipped — they can't
// make a locally-declared symbol visible. Discovery scans whole modules, so
// without this filter a deep-scanned file's internal exports (never
// re-exported up to the entry) would be documented as public API.
function collectVisibleExportNames(indexPath: string, visited = new Set<string>()): Set<string> {
const names = new Set<string>();
if (visited.has(indexPath) || !isFile(indexPath)) return names;
visited.add(indexPath);
const sourceFile = ts.createSourceFile(indexPath, fs.readFileSync(indexPath, 'utf-8'), ts.ScriptTarget.Latest, true);
ts.forEachChild(sourceFile, (node) => {
if (ts.isExportDeclaration(node)) {
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
for (const spec of node.exportClause.elements) {
names.add(spec.name.text);
if (spec.propertyName) names.add(spec.propertyName.text);
}
} else if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
names.add(node.exportClause.name.text);
} else if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
const specifier = node.moduleSpecifier.text;
if (!specifier.startsWith('.')) return;
for (const name of collectVisibleExportNames(resolveModulePath(indexPath, specifier), visited)) {
names.add(name);
}
}
return;
}
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
const isExported = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
if (!isExported) return;
if (ts.isVariableStatement(node)) {
for (const decl of node.declarationList.declarations) {
if (ts.isIdentifier(decl.name)) names.add(decl.name.text);
}
} else if (
(ts.isFunctionDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isEnumDeclaration(node)) &&
node.name
) {
names.add(node.name.text);
}
});
return localPaths;
return names;
}
function collectDeclaredExportNames(modulePath: string): Set<string> {
const names = new Set<string>();
const sourceFile = ts.createSourceFile(
modulePath,
fs.readFileSync(modulePath, 'utf-8'),
ts.ScriptTarget.Latest,
true
);
ts.forEachChild(sourceFile, (node) => {
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
const isExported = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
if (!isExported) return;
if (ts.isVariableStatement(node)) {
for (const declaration of node.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)) names.add(declaration.name.text);
}
} else if (
(ts.isFunctionDeclaration(node) ||
ts.isClassDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node) ||
ts.isEnumDeclaration(node)) &&
node.name
) {
names.add(node.name.text);
}
});
return names;
}
// ─── Phase 2: Convention Matching ──────────────────────────────────
@@ -550,39 +669,6 @@ function getJSDocParamDescription(node: ts.Node, paramName: string): string | un
return undefined;
}
function hasJSDocTag(node: ts.Node, tagName: string): boolean {
const jsDocNodes = (node as any).jsDoc as ts.JSDoc[] | undefined;
if (!jsDocNodes?.length) return false;
for (const doc of jsDocNodes) {
if (!doc.tags) continue;
for (const tag of doc.tags) {
if (tag.tagName.text === tagName) return true;
}
}
return false;
}
function getJSDocTagValue(node: ts.Node, tagName: string): string | undefined {
const jsDocNodes = (node as any).jsDoc as ts.JSDoc[] | undefined;
if (!jsDocNodes?.length) return undefined;
for (const doc of jsDocNodes) {
if (!doc.tags) continue;
for (const tag of doc.tags) {
if (tag.tagName.text === tagName) {
if (!tag.comment) return undefined;
if (typeof tag.comment === 'string') return tag.comment.trim();
return tag.comment
.map((c: ts.JSDocComment) => ('text' in c ? c.text : ''))
.join('')
.trim();
}
}
}
return undefined;
}
// ─── Shared AST Helpers ─────────────────────────────────────────────
function buildParamEntry(
@@ -961,6 +1047,7 @@ function discoverUtilExports(monorepoRoot: string, program: ts.Program): UtilEnt
const localModules = resolveLocalModules(indexPath);
// When the entry point is a leaf module (no re-exports), scan it directly
const modulesToScan = localModules.length > 0 ? localModules : [indexPath];
const visibleNames = collectVisibleExportNames(indexPath);
const failedModules: string[] = [];
// Collect all TAE exports for type resolution (formatDetailedType)
@@ -970,6 +1057,8 @@ function discoverUtilExports(monorepoRoot: string, program: ts.Program): UtilEnt
// utilities, contexts, and selectors (e.g., usePlayer, createPlayer, selectPlayback)
for (const modulePath of modulesToScan) {
if (!fs.existsSync(modulePath)) continue;
const declaredNames = collectDeclaredExportNames(modulePath);
if (declaredNames.size === 0) continue;
let ast: tae.ModuleNode;
try {
@@ -982,6 +1071,13 @@ function discoverUtilExports(monorepoRoot: string, program: ts.Program): UtilEnt
allExports.push(...ast.exports);
for (const exportNode of ast.exports) {
// Re-exported APIs are owned by their declaring module. Local
// declarations are scanned post-order, while external package
// re-exports are documented by that package's canonical entry point.
if (!declaredNames.has(exportNode.name)) continue;
// Whole modules are scanned, but only exports that are actually
// visible from the entry point are public API.
if (!visibleNames.has(exportNode.name)) continue;
processExport(exportNode, modulePath, entryPoint, program, seenKeys, seenSlugs, entries, allExports);
}
}
@@ -1010,6 +1106,7 @@ function discoverUtilExports(monorepoRoot: string, program: ts.Program): UtilEnt
for (const modulePath of failedModules) {
const rawExports = discoverExportsFromRawAST(modulePath, program);
for (const info of rawExports) {
if (!visibleNames.has(info.name)) continue;
processRawExport(info, entryPoint, program, seenKeys, seenSlugs, entries);
}
}
@@ -1021,7 +1118,7 @@ function discoverUtilExports(monorepoRoot: string, program: ts.Program): UtilEnt
const rawExports = discoverExportsFromRawAST(modulePath, program);
for (const info of rawExports) {
if (!info.isClass) continue;
if (!info.isClass || !visibleNames.has(info.name)) continue;
processRawExport(info, entryPoint, program, seenKeys, seenSlugs, entries);
}
}
@@ -1,5 +1,51 @@
import type * as ts from 'typescript';
import type { PropDef } from './types.js';
const PREFIX = '\x1b[35m[api-docs-builder]\x1b[0m';
export const log = {
info: (...args: unknown[]) => console.log(PREFIX, ...args),
warn: (...args: unknown[]) => console.warn(PREFIX, '\x1b[33mwarn:\x1b[0m', ...args),
error: (...args: unknown[]) => console.error(PREFIX, '\x1b[31merror:\x1b[0m', ...args),
};
export function hasJSDocTag(node: ts.Node, tagName: string): boolean {
const jsDocNodes = (node as any).jsDoc as ts.JSDoc[] | undefined;
if (!jsDocNodes?.length) return false;
for (const doc of jsDocNodes) {
if (!doc.tags) continue;
for (const tag of doc.tags) {
if (tag.tagName.text === tagName) return true;
}
}
return false;
}
export function getJSDocTagValue(node: ts.Node, tagName: string): string | undefined {
const jsDocNodes = (node as any).jsDoc as ts.JSDoc[] | undefined;
if (!jsDocNodes?.length) return undefined;
// TS attaches every leading JSDoc block to the node (file headers included)
// and parses @tags even mid-sentence — the block closest to the declaration
// is the binding one, so scan in reverse.
for (const doc of [...jsDocNodes].reverse()) {
if (!doc.tags) continue;
for (const tag of doc.tags) {
if (tag.tagName.text === tagName) {
if (!tag.comment) return undefined;
if (typeof tag.comment === 'string') return tag.comment.trim();
return tag.comment
.map((c: ts.JSDocComment) => ('text' in c ? c.text : ''))
.join('')
.trim();
}
}
}
return undefined;
}
export function kebabToPascal(str: string): string {
return str
.split('-')