mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
test(site): replace api-docs-builder design doc with E2E spec tests (#1225)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,99 +1,8 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as ts from 'typescript';
|
||||
import * as tae from 'typescript-api-extractor';
|
||||
import { extractCore } from './core-handler.js';
|
||||
import { extractCSSVars } from './css-vars-handler.js';
|
||||
import { extractDataAttrs } from './data-attrs-handler.js';
|
||||
import { abbreviateType } from './formatter.js';
|
||||
import { extractHtml } from './html-handler.js';
|
||||
import { extractPartDescription, extractParts, extractSubPartProps } from './parts-handler.js';
|
||||
import {
|
||||
type ComponentReference,
|
||||
ComponentReferenceSchema,
|
||||
type ComponentSource,
|
||||
type CoreExtraction,
|
||||
type CSSVarDef,
|
||||
type CSSVarsExtraction,
|
||||
type DataAttrDef,
|
||||
type DataAttrsExtraction,
|
||||
type PartReference,
|
||||
type PartSource,
|
||||
type PropDef,
|
||||
type StateDef,
|
||||
} from './types.js';
|
||||
import { generateComponentReferences } from './pipeline.js';
|
||||
import { ComponentReferenceSchema } from './types.js';
|
||||
import { generateUtilReferences } from './util-handler.js';
|
||||
import { kebabToPascal, partKebabFromSource, sortProps } from './utils.js';
|
||||
|
||||
// Components whose PascalCase name doesn't match simple kebab-to-pascal conversion.
|
||||
const NAME_OVERRIDES: Record<string, string> = {
|
||||
'pip-button': 'PiPButton',
|
||||
};
|
||||
|
||||
// Parts whose HTML element file doesn't follow the `{component}-{part}-element.ts` convention.
|
||||
// Key: `{component}/{part-kebab}`, Value: element file basename (without `.ts`).
|
||||
const PART_ELEMENT_OVERRIDES: Record<string, string> = {
|
||||
'tooltip/provider': 'tooltip-group-element',
|
||||
};
|
||||
|
||||
function buildProps(coreData: CoreExtraction): Record<string, PropDef> {
|
||||
const props: Record<string, PropDef> = {};
|
||||
for (const prop of coreData.props) {
|
||||
props[prop.name] = {
|
||||
type: prop.type,
|
||||
detailedType: prop.detailedType,
|
||||
description: prop.description,
|
||||
default: coreData.defaultProps[prop.name] ?? prop.default,
|
||||
required: prop.required,
|
||||
};
|
||||
|
||||
if (props[prop.name]!.detailedType === undefined) delete props[prop.name]!.detailedType;
|
||||
if (props[prop.name]!.description === undefined) delete props[prop.name]!.description;
|
||||
if (props[prop.name]!.default === undefined) delete props[prop.name]!.default;
|
||||
if (!props[prop.name]!.required) delete props[prop.name]!.required;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
function buildState(coreData: CoreExtraction): Record<string, StateDef> {
|
||||
const state: Record<string, StateDef> = {};
|
||||
for (const s of coreData.state) {
|
||||
state[s.name] = {
|
||||
type: s.type,
|
||||
detailedType: s.detailedType,
|
||||
description: s.description,
|
||||
};
|
||||
if (state[s.name]!.detailedType === undefined) delete state[s.name]!.detailedType;
|
||||
if (state[s.name]!.description === undefined) delete state[s.name]!.description;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function buildDataAttrs(dataAttrsData: DataAttrsExtraction): Record<string, DataAttrDef> {
|
||||
const dataAttributes: Record<string, DataAttrDef> = {};
|
||||
for (const attr of dataAttrsData.attrs) {
|
||||
const def: DataAttrDef = { description: attr.description };
|
||||
if (attr.type) {
|
||||
const abbreviated = abbreviateType(attr.name, attr.type);
|
||||
if (abbreviated) {
|
||||
def.type = abbreviated;
|
||||
def.detailedType = attr.type;
|
||||
} else {
|
||||
def.type = attr.type;
|
||||
}
|
||||
}
|
||||
dataAttributes[attr.name] = def;
|
||||
}
|
||||
return dataAttributes;
|
||||
}
|
||||
|
||||
function buildCSSVars(cssVarsData: CSSVarsExtraction): Record<string, CSSVarDef> {
|
||||
const cssCustomProperties: Record<string, CSSVarDef> = {};
|
||||
for (const v of cssVarsData.vars) {
|
||||
cssCustomProperties[v.name] = { description: v.description };
|
||||
}
|
||||
return cssCustomProperties;
|
||||
}
|
||||
|
||||
// Magenta prefix - visible on both light and dark terminals
|
||||
const PREFIX = '\x1b[35m[api-docs-builder]\x1b[0m';
|
||||
@@ -107,488 +16,9 @@ const log = {
|
||||
|
||||
// Paths relative to the monorepo root
|
||||
const MONOREPO_ROOT = path.resolve(import.meta.dirname, '../../../../');
|
||||
const CORE_UI_PATH = path.join(MONOREPO_ROOT, 'packages/core/src/core/ui');
|
||||
const HTML_UI_PATH = path.join(MONOREPO_ROOT, 'packages/html/src/ui');
|
||||
const REACT_UI_PATH = path.join(MONOREPO_ROOT, 'packages/react/src/ui');
|
||||
const COMPONENT_OUTPUT_PATH = path.join(MONOREPO_ROOT, 'site/src/content/generated-component-reference');
|
||||
const UTIL_OUTPUT_PATH = path.join(MONOREPO_ROOT, 'site/src/content/generated-util-reference');
|
||||
|
||||
/**
|
||||
* Discover all components by scanning the core/ui directory.
|
||||
*/
|
||||
function discoverComponents(): ComponentSource[] {
|
||||
const components: ComponentSource[] = [];
|
||||
|
||||
if (!fs.existsSync(CORE_UI_PATH)) {
|
||||
log.error(`Core UI path not found: ${CORE_UI_PATH}`);
|
||||
return components;
|
||||
}
|
||||
|
||||
const dirs = fs.readdirSync(CORE_UI_PATH, { withFileTypes: true });
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (!dir.isDirectory()) continue;
|
||||
|
||||
const componentName = NAME_OVERRIDES[dir.name] ?? kebabToPascal(dir.name);
|
||||
const componentDir = path.join(CORE_UI_PATH, dir.name);
|
||||
|
||||
// Look for core file
|
||||
const coreFile = path.join(componentDir, `${dir.name}-core.ts`);
|
||||
const dataAttrsFile = path.join(componentDir, `${dir.name}-data-attrs.ts`);
|
||||
const cssVarsFile = path.join(componentDir, `${dir.name}-css-vars.ts`);
|
||||
|
||||
// Look for HTML element file
|
||||
const htmlFile = path.join(HTML_UI_PATH, dir.name, `${dir.name}-element.ts`);
|
||||
|
||||
const source: ComponentSource = {
|
||||
name: componentName,
|
||||
kebab: dir.name,
|
||||
};
|
||||
|
||||
if (fs.existsSync(coreFile)) {
|
||||
source.corePath = coreFile;
|
||||
}
|
||||
|
||||
if (fs.existsSync(dataAttrsFile)) {
|
||||
source.dataAttrsPath = dataAttrsFile;
|
||||
}
|
||||
|
||||
if (fs.existsSync(cssVarsFile)) {
|
||||
source.cssVarsPath = cssVarsFile;
|
||||
}
|
||||
|
||||
if (fs.existsSync(htmlFile)) {
|
||||
source.htmlPath = htmlFile;
|
||||
}
|
||||
|
||||
// Check for multi-part component (index.parts.ts in React package)
|
||||
const partsIndexFile = path.join(REACT_UI_PATH, dir.name, 'index.parts.ts');
|
||||
if (fs.existsSync(partsIndexFile)) {
|
||||
source.partsIndexPath = partsIndexFile;
|
||||
}
|
||||
|
||||
// Only include if we have at least a core file
|
||||
if (source.corePath) {
|
||||
components.push(source);
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TypeScript program for all relevant files.
|
||||
*/
|
||||
function createProgram(sources: ComponentSource[]): ts.Program {
|
||||
const files: string[] = [];
|
||||
|
||||
for (const source of sources) {
|
||||
if (source.corePath) files.push(source.corePath);
|
||||
if (source.dataAttrsPath) files.push(source.dataAttrsPath);
|
||||
if (source.cssVarsPath) files.push(source.cssVarsPath);
|
||||
if (source.htmlPath) files.push(source.htmlPath);
|
||||
if (source.partsIndexPath) files.push(source.partsIndexPath);
|
||||
|
||||
// For multi-part components, include all element files from the HTML directory
|
||||
// and React source files for JSDoc description extraction
|
||||
if (source.partsIndexPath) {
|
||||
const htmlDir = path.join(HTML_UI_PATH, source.kebab);
|
||||
if (fs.existsSync(htmlDir)) {
|
||||
const elementFiles = fs.readdirSync(htmlDir).filter((f) => f.endsWith('-element.ts'));
|
||||
for (const file of elementFiles) {
|
||||
const fullPath = path.join(htmlDir, file);
|
||||
if (!files.includes(fullPath)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include React component .tsx files for JSDoc description extraction
|
||||
const reactDir = path.dirname(source.partsIndexPath);
|
||||
const reactFiles = fs.readdirSync(reactDir).filter((f) => f.endsWith('.tsx'));
|
||||
for (const file of reactFiles) {
|
||||
const fullPath = path.join(reactDir, file);
|
||||
if (!files.includes(fullPath)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Include origin component files for re-exported parts
|
||||
const partsSource = fs.readFileSync(source.partsIndexPath, 'utf-8');
|
||||
const nonLocalImports = partsSource.match(/from\s+['"]([^.][^'"]*)['"]/g);
|
||||
if (nonLocalImports) {
|
||||
for (const match of nonLocalImports) {
|
||||
const importPath = match.replace(/from\s+['"]/, '').replace(/['"]$/, '');
|
||||
const originDir = path.resolve(path.dirname(source.partsIndexPath), importPath, '..');
|
||||
const originKebab = path.basename(originDir);
|
||||
|
||||
// Include origin HTML element files
|
||||
const originHtmlDir = path.join(HTML_UI_PATH, originKebab);
|
||||
if (fs.existsSync(originHtmlDir)) {
|
||||
const originElementFiles = fs.readdirSync(originHtmlDir).filter((f) => f.endsWith('-element.ts'));
|
||||
for (const file of originElementFiles) {
|
||||
const fullPath = path.join(originHtmlDir, file);
|
||||
if (!files.includes(fullPath)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include origin React .tsx files for JSDoc description extraction
|
||||
if (fs.existsSync(originDir)) {
|
||||
const originReactFiles = fs.readdirSync(originDir).filter((f) => f.endsWith('.tsx'));
|
||||
for (const file of originReactFiles) {
|
||||
const fullPath = path.join(originDir, file);
|
||||
if (!files.includes(fullPath)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load base tsconfig - works for all packages since we only need type resolution
|
||||
const tsconfigPath = path.join(MONOREPO_ROOT, 'tsconfig.base.json');
|
||||
const config = tae.loadConfig(tsconfigPath);
|
||||
|
||||
config.options.rootDir = MONOREPO_ROOT;
|
||||
|
||||
return ts.createProgram(files, config.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the API reference for a single-part component.
|
||||
*/
|
||||
function buildSingleComponentReference(source: ComponentSource, program: ts.Program): ComponentReference | null {
|
||||
// Extract from core
|
||||
const coreData = source.corePath ? extractCore(source.corePath, program, source.name) : null;
|
||||
|
||||
if (!coreData) {
|
||||
log.warn(`No core data found for ${source.name}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract data attributes
|
||||
const dataAttrsData = source.dataAttrsPath ? extractDataAttrs(source.dataAttrsPath, program, source.name) : null;
|
||||
|
||||
// Extract CSS custom properties
|
||||
const cssVarsData = source.cssVarsPath ? extractCSSVars(source.cssVarsPath, program, source.name) : null;
|
||||
|
||||
// Extract HTML element info
|
||||
const htmlData = source.htmlPath ? extractHtml(source.htmlPath, program, source.name) : null;
|
||||
|
||||
// Build result
|
||||
const result: ComponentReference = {
|
||||
name: source.name,
|
||||
description: coreData.description,
|
||||
props: buildProps(coreData),
|
||||
state: buildState(coreData),
|
||||
dataAttributes: dataAttrsData ? buildDataAttrs(dataAttrsData) : {},
|
||||
cssCustomProperties: cssVarsData ? buildCSSVars(cssVarsData) : {},
|
||||
platforms: {},
|
||||
};
|
||||
|
||||
// Add HTML platform info if available
|
||||
if (htmlData) {
|
||||
result.platforms.html = {
|
||||
tagName: htmlData.tagName,
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up undefined description
|
||||
if (result.description === undefined) delete result.description;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a React source file instantiates the component's own Core class
|
||||
* (matches `new {ComponentName}Core(`). This prevents auxiliary classes like
|
||||
* `TooltipGroupCore` from being mistaken for the primary Core.
|
||||
*/
|
||||
function instantiatesCore(filePath: string, componentName: string): boolean {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return new RegExp(`new ${componentName}Core\\b`).test(content);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** String search heuristic — a comment containing `stateAttrMap` would also match. */
|
||||
function usesDataAttrs(filePath: string): boolean {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8').includes('stateAttrMap');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover parts and match them to HTML element files.
|
||||
*
|
||||
* Matching algorithm:
|
||||
* 1. Parse `index.parts.ts` for named exports -> part names and source paths
|
||||
* 2. Filter out non-local re-exports (sources not starting with './')
|
||||
* 3. Derive kebab segment from source: `./time-value` -> strip `./time-` prefix -> `value`
|
||||
* 4. For each part, look for `{name}-{kebab}-element.ts` in HTML dir
|
||||
* 5. Primary part: the part whose React source file instantiates the Core class
|
||||
* 6. Primary part gets: shared core file, shared data-attrs/css-vars, main element
|
||||
*/
|
||||
function discoverParts(source: ComponentSource, program: ts.Program): PartSource[] {
|
||||
if (!source.partsIndexPath) return [];
|
||||
|
||||
const partExports = extractParts(source.partsIndexPath, program);
|
||||
if (partExports.length === 0) return [];
|
||||
|
||||
const localExports = partExports.filter((p) => p.source.startsWith('./'));
|
||||
const nonLocalExports = partExports.filter((p) => !p.source.startsWith('./'));
|
||||
|
||||
if (localExports.length === 0 && nonLocalExports.length === 0) return [];
|
||||
|
||||
const componentKebab = source.kebab;
|
||||
const htmlDir = path.join(HTML_UI_PATH, componentKebab);
|
||||
|
||||
const parts: PartSource[] = [];
|
||||
|
||||
// Process local exports
|
||||
for (const partExport of localExports) {
|
||||
const kebab = partKebabFromSource(partExport.source, componentKebab);
|
||||
|
||||
// Look for sub-part element file: {component}-{part}-element.ts (or override)
|
||||
const overrideKey = `${componentKebab}/${kebab}`;
|
||||
const elementBasename = PART_ELEMENT_OVERRIDES[overrideKey] ?? `${componentKebab}-${kebab}-element`;
|
||||
const subPartElementFile = path.join(htmlDir, `${elementBasename}.ts`);
|
||||
const hasSubPartElement = fs.existsSync(subPartElementFile);
|
||||
|
||||
// Resolve React source path for JSDoc description extraction
|
||||
const reactFile = path.join(path.dirname(source.partsIndexPath!), `${partExport.source.replace('./', '')}.tsx`);
|
||||
const reactPath = fs.existsSync(reactFile) ? reactFile : undefined;
|
||||
|
||||
// Primary detection: the part whose React source instantiates the Core class
|
||||
const isPrimary = !!reactPath && instantiatesCore(reactPath, source.name);
|
||||
|
||||
const subPartUsesDataAttrs = !isPrimary && !!reactPath && usesDataAttrs(reactPath);
|
||||
|
||||
const part: PartSource = {
|
||||
name: partExport.name,
|
||||
localName: partExport.localName,
|
||||
kebab,
|
||||
isPrimary,
|
||||
htmlPath: hasSubPartElement ? subPartElementFile : isPrimary ? source.htmlPath : undefined,
|
||||
reactPath,
|
||||
dataAttrsPath: subPartUsesDataAttrs ? source.dataAttrsPath : undefined,
|
||||
dataAttrsComponentName: subPartUsesDataAttrs ? source.name : undefined,
|
||||
};
|
||||
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
// Resolve re-exported parts from other components
|
||||
if (nonLocalExports.length > 0) {
|
||||
// Group by source module path
|
||||
const bySource = new Map<string, typeof nonLocalExports>();
|
||||
for (const exp of nonLocalExports) {
|
||||
const list = bySource.get(exp.source) ?? [];
|
||||
list.push(exp);
|
||||
bySource.set(exp.source, list);
|
||||
}
|
||||
|
||||
const partsDir = path.dirname(source.partsIndexPath!);
|
||||
|
||||
for (const [sourcePath, exports] of bySource) {
|
||||
// Resolve the origin index.parts.ts file
|
||||
const originPartsFile = path.resolve(partsDir, `${sourcePath}.ts`);
|
||||
if (!fs.existsSync(originPartsFile)) {
|
||||
log.warn(`${source.name}: Re-export source not found: ${originPartsFile}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Derive origin component kebab from directory name
|
||||
const originKebab = path.basename(path.dirname(originPartsFile));
|
||||
const originHtmlDir = path.join(HTML_UI_PATH, originKebab);
|
||||
const originReactDir = path.dirname(originPartsFile);
|
||||
|
||||
// Parse origin's exports to match re-exported names to original local exports
|
||||
const originExports = extractParts(originPartsFile, program);
|
||||
|
||||
for (const reExport of exports) {
|
||||
// Find the matching export in the origin file
|
||||
const originExport = originExports.find((o) => o.name === reExport.name);
|
||||
if (!originExport) {
|
||||
log.warn(`${source.name}: Re-exported part "${reExport.name}" not found in ${originPartsFile}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Derive kebab from the origin export's source path
|
||||
const kebab = partKebabFromSource(originExport.source, originKebab);
|
||||
|
||||
// Look for element file in origin's HTML directory
|
||||
const subPartElementFile = path.join(originHtmlDir, `${originKebab}-${kebab}-element.ts`);
|
||||
const hasSubPartElement = fs.existsSync(subPartElementFile);
|
||||
|
||||
// Resolve React source from origin component for JSDoc description
|
||||
const reactFile = path.join(originReactDir, `${originExport.source.replace('./', '')}.tsx`);
|
||||
const reactPath = fs.existsSync(reactFile) ? reactFile : undefined;
|
||||
|
||||
const reExportUsesDataAttrs = !!reactPath && usesDataAttrs(reactPath);
|
||||
const originDataAttrsFile = path.join(CORE_UI_PATH, originKebab, `${originKebab}-data-attrs.ts`);
|
||||
const originDataAttrsPath =
|
||||
reExportUsesDataAttrs && fs.existsSync(originDataAttrsFile) ? originDataAttrsFile : undefined;
|
||||
const originComponentName = originDataAttrsPath ? kebabToPascal(originKebab) : undefined;
|
||||
|
||||
const part: PartSource = {
|
||||
name: reExport.name,
|
||||
localName: originExport.localName,
|
||||
kebab,
|
||||
isPrimary: false, // Re-exported parts are never primary
|
||||
htmlPath: hasSubPartElement ? subPartElementFile : undefined,
|
||||
reactPath,
|
||||
dataAttrsPath: originDataAttrsPath,
|
||||
dataAttrsComponentName: originComponentName,
|
||||
};
|
||||
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const primaryCount = parts.filter((p) => p.isPrimary).length;
|
||||
if (primaryCount === 0) {
|
||||
log.warn(`${source.name}: No primary part identified (no part instantiates a Core class)`);
|
||||
} else if (primaryCount > 1) {
|
||||
log.warn(`${source.name}: Multiple parts instantiate Core — only the first will be treated as primary`);
|
||||
let foundFirst = false;
|
||||
for (const p of parts) {
|
||||
if (p.isPrimary) {
|
||||
if (foundFirst) p.isPrimary = false;
|
||||
foundFirst = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Primary part first so it appears first in the docs.
|
||||
return parts.sort((a, b) => Number(b.isPrimary) - Number(a.isPrimary));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the API reference for a multi-part component.
|
||||
*
|
||||
* Multi-part components have empty top-level props/state/dataAttributes/cssCustomProperties.
|
||||
* All data is in the `parts` record.
|
||||
*
|
||||
* For the primary part:
|
||||
* - Props and state come from the shared core file (`{name}-core.ts`)
|
||||
* - Data attributes come from the shared data-attrs file (`{name}-data-attrs.ts`)
|
||||
* - CSS custom properties come from the shared css-vars file (`{name}-css-vars.ts`)
|
||||
* - HTML tag comes from the main element file (`{name}-element.ts`)
|
||||
*
|
||||
* For non-primary parts:
|
||||
* - Props, state, data attributes, and CSS vars are empty (no dedicated core file)
|
||||
* - HTML tag comes from their sub-part element file (`{name}-{part}-element.ts`)
|
||||
*
|
||||
* All parts get `platforms.react` (they come from `index.parts.ts`).
|
||||
* Parts with matching HTML element files also get `platforms.html`.
|
||||
*/
|
||||
function buildMultiPartReference(
|
||||
source: ComponentSource,
|
||||
program: ts.Program,
|
||||
parts: PartSource[]
|
||||
): ComponentReference | null {
|
||||
const partsRecord: Record<string, PartReference> = {};
|
||||
|
||||
for (const part of parts) {
|
||||
// Extract JSDoc description from React component file
|
||||
const description = part.reactPath
|
||||
? (extractPartDescription(part.reactPath, program, part.localName) ??
|
||||
extractPartDescription(part.reactPath, program, part.name))
|
||||
: undefined;
|
||||
|
||||
if (part.isPrimary) {
|
||||
// Primary part: extract from shared core, data-attrs, and css-vars
|
||||
const coreData = source.corePath ? extractCore(source.corePath, program, source.name) : null;
|
||||
const dataAttrsData = source.dataAttrsPath ? extractDataAttrs(source.dataAttrsPath, program, source.name) : null;
|
||||
const cssVarsData = source.cssVarsPath ? extractCSSVars(source.cssVarsPath, program, source.name) : null;
|
||||
|
||||
const elementName = `${source.name}Element`;
|
||||
const htmlData = part.htmlPath ? extractHtml(part.htmlPath, program, source.name, elementName) : null;
|
||||
|
||||
const partRef: PartReference = {
|
||||
name: part.name,
|
||||
description,
|
||||
props: coreData ? sortProps(buildProps(coreData)) : {},
|
||||
state: coreData ? buildState(coreData) : {},
|
||||
dataAttributes: dataAttrsData ? buildDataAttrs(dataAttrsData) : {},
|
||||
cssCustomProperties: cssVarsData ? buildCSSVars(cssVarsData) : {},
|
||||
platforms: { react: {} },
|
||||
};
|
||||
|
||||
if (!partRef.description) delete partRef.description;
|
||||
if (htmlData) {
|
||||
partRef.platforms.html = { tagName: htmlData.tagName };
|
||||
}
|
||||
|
||||
partsRecord[part.kebab] = partRef;
|
||||
} else {
|
||||
// Non-primary part: extract HTML tag, shared data attributes, and custom React props
|
||||
const elementName = part.htmlPath ? kebabToPascal(path.basename(part.htmlPath, '.ts')) : undefined;
|
||||
const htmlData =
|
||||
part.htmlPath && elementName ? extractHtml(part.htmlPath, program, source.name, elementName) : null;
|
||||
|
||||
const dataAttrsData =
|
||||
part.dataAttrsPath && part.dataAttrsComponentName
|
||||
? extractDataAttrs(part.dataAttrsPath, program, part.dataAttrsComponentName)
|
||||
: null;
|
||||
|
||||
const subPartProps = part.reactPath ? extractSubPartProps(part.reactPath, program, part.localName) : {};
|
||||
|
||||
const partRef: PartReference = {
|
||||
name: part.name,
|
||||
description,
|
||||
props: sortProps(subPartProps),
|
||||
state: {},
|
||||
dataAttributes: dataAttrsData ? buildDataAttrs(dataAttrsData) : {},
|
||||
cssCustomProperties: {},
|
||||
platforms: { react: {} },
|
||||
};
|
||||
|
||||
if (!partRef.description) delete partRef.description;
|
||||
if (htmlData) {
|
||||
partRef.platforms.html = { tagName: htmlData.tagName };
|
||||
}
|
||||
|
||||
partsRecord[part.kebab] = partRef;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: source.name,
|
||||
props: {},
|
||||
state: {},
|
||||
dataAttributes: {},
|
||||
cssCustomProperties: {},
|
||||
platforms: {},
|
||||
parts: partsRecord,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the API reference for a single component.
|
||||
*/
|
||||
function buildComponentReference(source: ComponentSource, program: ts.Program): ComponentReference | null {
|
||||
if (source.partsIndexPath) {
|
||||
const parts = discoverParts(source, program);
|
||||
// Single-part fallback: when filtering leaves only 1 part, use single-part mode
|
||||
if (parts.length > 1) {
|
||||
return buildMultiPartReference(source, program, parts);
|
||||
}
|
||||
}
|
||||
|
||||
return buildSingleComponentReference(source, program);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point.
|
||||
*/
|
||||
@@ -597,7 +27,6 @@ function main() {
|
||||
// (or a few others like ESSymbol, TemplateLiteral). It falls back to `any`
|
||||
// and logs a warning for each occurrence. This is a known gap in the alpha
|
||||
// library — not a bug in our types. Suppress the noise here.
|
||||
// https://github.com/michaldudak/typescript-api-extractor/blob/main/src/parsers/typeResolver.ts
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args: unknown[]) => {
|
||||
if (typeof args[0] === 'string' && args[0].startsWith('Unable to handle a type with flag')) return;
|
||||
@@ -609,53 +38,37 @@ function main() {
|
||||
fs.mkdirSync(COMPONENT_OUTPUT_PATH, { recursive: true });
|
||||
}
|
||||
|
||||
// Discover components
|
||||
const components = discoverComponents();
|
||||
// Generate component references via pipeline
|
||||
const componentResults = generateComponentReferences(MONOREPO_ROOT);
|
||||
|
||||
if (components.length === 0) {
|
||||
if (componentResults.length === 0) {
|
||||
log.info('No components found.');
|
||||
return;
|
||||
} else {
|
||||
log.info(`Found ${componentResults.length} components. Processing...`);
|
||||
}
|
||||
log.info(`Found ${components.length} components. Processing...`);
|
||||
|
||||
// Create TypeScript program
|
||||
const program = createProgram(components);
|
||||
|
||||
// Process each component
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const source of components) {
|
||||
try {
|
||||
const apiRef = buildComponentReference(source, program);
|
||||
|
||||
if (apiRef) {
|
||||
// Sort props (top-level only for single-part)
|
||||
apiRef.props = sortProps(apiRef.props);
|
||||
|
||||
// Validate against schema before writing
|
||||
const validated = ComponentReferenceSchema.safeParse(apiRef);
|
||||
if (!validated.success) {
|
||||
log.error(`Schema validation failed for ${source.name}:`);
|
||||
for (const issue of validated.error.issues) {
|
||||
log.error(` - ${issue.path.join('.')}: ${issue.message}`);
|
||||
}
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write JSON file
|
||||
const outputFile = path.join(COMPONENT_OUTPUT_PATH, `${source.kebab}.json`);
|
||||
const json = `${JSON.stringify(validated.data, null, 2)}\n`;
|
||||
fs.writeFileSync(outputFile, json);
|
||||
|
||||
log.success(`✅ Generated ${path.basename(outputFile)}`);
|
||||
successCount++;
|
||||
for (const result of componentResults) {
|
||||
// Validate against schema before writing
|
||||
const validated = ComponentReferenceSchema.safeParse(result.reference);
|
||||
if (!validated.success) {
|
||||
log.error(`Schema validation failed for ${result.name}:`);
|
||||
for (const issue of validated.error.issues) {
|
||||
log.error(` - ${issue.path.join('.')}: ${issue.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`⚠️ Error processing ${source.name}:`, (error as Error).message);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write JSON file
|
||||
const outputFile = path.join(COMPONENT_OUTPUT_PATH, `${result.kebab}.json`);
|
||||
const json = `${JSON.stringify(validated.data, null, 2)}\n`;
|
||||
fs.writeFileSync(outputFile, json);
|
||||
|
||||
log.success(`✅ Generated ${path.basename(outputFile)}`);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
log.info(`Done! Generated ${successCount} component files.`);
|
||||
|
||||
Reference in New Issue
Block a user