mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
import * as ts from 'typescript';
|
|
import * as tae from 'typescript-api-extractor';
|
|
import type { PropDef } from './types.js';
|
|
|
|
export interface PartExport {
|
|
/** PascalCase exported part name (e.g., "Value", "Group", "Separator"). */
|
|
name: string;
|
|
/** Local symbol name in the source module before aliasing. */
|
|
localName: string;
|
|
/** Source path (e.g., "./time-value", "./time-group"). */
|
|
source: string;
|
|
}
|
|
|
|
/**
|
|
* Extract part definitions from a React `index.parts.ts` file.
|
|
*
|
|
* Discovery algorithm:
|
|
* 1. Parses named exports from `index.parts.ts` (filters out type-only exports)
|
|
* 2. Each value export becomes a part: `export { Group } from './time-group'` -> part "Group"
|
|
* 3. Source path is preserved for HTML element matching in the main builder
|
|
*
|
|
* If a part isn't appearing in the output:
|
|
* - Ensure it's exported as a value export (not `type`-only) in `index.parts.ts`
|
|
* - Ensure the source path follows `'./{component}-{part}'` naming
|
|
*/
|
|
export function extractParts(filePath: string, program: ts.Program): PartExport[] {
|
|
const sourceFile = program.getSourceFile(filePath);
|
|
if (!sourceFile) return [];
|
|
|
|
const parts: PartExport[] = [];
|
|
|
|
function visit(node: ts.Node) {
|
|
// Match: export { Name } from './source' or export { Name, type NameProps } from './source'
|
|
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
const source = node.moduleSpecifier.text;
|
|
|
|
// Skip type-only export declarations (export type { ... } from '...')
|
|
if (node.isTypeOnly) return;
|
|
|
|
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
for (const element of node.exportClause.elements) {
|
|
// Skip type-only specifiers (e.g., `type GroupProps`)
|
|
if (element.isTypeOnly) continue;
|
|
|
|
const localName = element.propertyName?.text ?? element.name.text;
|
|
parts.push({
|
|
name: element.name.text,
|
|
localName,
|
|
source,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
|
|
return parts;
|
|
}
|
|
|
|
/**
|
|
* Extract the JSDoc description from a React component export.
|
|
*
|
|
* Parses the file with `typescript-api-extractor`, finds the export matching
|
|
* `partName`, and returns its description (stripping `@example` blocks).
|
|
*/
|
|
export function extractPartDescription(filePath: string, program: ts.Program, partName: string): string | undefined {
|
|
const ast = tae.parseFromProgram(filePath, program);
|
|
const component = ast.exports.find((exp) => exp.name === partName);
|
|
let desc = component?.documentation?.description;
|
|
if (desc) desc = desc.replace(/\n*@example[\s\S]*$/, '').trim();
|
|
return desc || undefined;
|
|
}
|
|
|
|
/**
|
|
* Extract custom React-specific props from a sub-part's Props interface.
|
|
*
|
|
* Walks syntactic own members of `{localName}Props`, then also includes
|
|
* members from any extended interface declared within the project (i.e., not
|
|
* from `node_modules`). This picks up props from project types like
|
|
* `TooltipGroupProps` while excluding inherited React DOM attributes.
|
|
*/
|
|
export function extractSubPartProps(filePath: string, program: ts.Program, localName: string): Record<string, PropDef> {
|
|
const sourceFile = program.getSourceFile(filePath);
|
|
if (!sourceFile) return {};
|
|
const checker = program.getTypeChecker();
|
|
const props: Record<string, PropDef> = {};
|
|
|
|
const SKIP_PROPS = new Set(['children']);
|
|
|
|
function collectFromMembers(members: ts.NodeArray<ts.TypeElement>) {
|
|
for (const member of members) {
|
|
if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue;
|
|
const name = member.name.text;
|
|
if (SKIP_PROPS.has(name) || !member.type) continue;
|
|
|
|
let typeStr = checker.typeToString(checker.getTypeFromTypeNode(member.type));
|
|
if (member.questionToken) typeStr = typeStr.replace(/ \| undefined$/, '');
|
|
|
|
const propDef: PropDef = { type: typeStr };
|
|
|
|
const symbol = checker.getSymbolAtLocation(member.name);
|
|
if (symbol) {
|
|
const docs = symbol.getDocumentationComment(checker);
|
|
const desc = docs.map((d) => d.text).join('');
|
|
if (desc) propDef.description = desc;
|
|
}
|
|
|
|
props[name] = propDef;
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(sourceFile, function visit(node) {
|
|
if (ts.isInterfaceDeclaration(node) && node.name.text === `${localName}Props`) {
|
|
// Collect own syntactic members.
|
|
collectFromMembers(node.members);
|
|
|
|
// Walk extends clause and include members from project-local interfaces.
|
|
if (node.heritageClauses) {
|
|
for (const clause of node.heritageClauses) {
|
|
for (const expr of clause.types) {
|
|
const type = checker.getTypeAtLocation(expr);
|
|
const symbol = type.getSymbol();
|
|
const decl = symbol?.declarations?.[0];
|
|
if (!decl) continue;
|
|
|
|
// Only include if declared in project sources (not node_modules).
|
|
const declFile = decl.getSourceFile().fileName;
|
|
if (declFile.includes('node_modules')) continue;
|
|
|
|
if (ts.isInterfaceDeclaration(decl)) {
|
|
collectFromMembers(decl.members);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
});
|
|
|
|
return props;
|
|
}
|