Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"license": "MPL-2.0",
"author": "HashiCorp Design Systems <design-systems@hashicorp.com>",
"scripts": {
"build": "rollup --config",
"build": "rollup --config && node scripts/parse-docs.mjs",
"format": "prettier . --cache --write",
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm run format",
Expand Down Expand Up @@ -114,6 +114,7 @@
"stylelint": "^16.17.0",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard-scss": "^14.0.0",
"ts-morph": "^28.0.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.29.0",
"webpack": "^5.104.1"
Expand Down
82 changes: 82 additions & 0 deletions packages/components/scripts/parse-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: MPL-2.0
*/

/**
* Parse component docs metadata from signature types.
*
* Input: src/components.ts central public exports. Output:
* dist/manifest/components.json consumed by docs tooling. Invoked during `pnpm
* build` in this package.
*/

import { Project } from 'ts-morph';
import { existsSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';

import {
ENTRY_FILE_PATH,
OUTPUT_FILE_PATH,
TSCONFIG_PATH,
TYPE_TRACE_LIMITS,
} from './parse-docs/config.mjs';
import { createSourceFileResolver } from './parse-docs/source-files.mjs';
import { createTypeResolver } from './parse-docs/type-resolver.mjs';
import { extractDocData } from './parse-docs/doc-text.mjs';
import { parseComponentsFromEntry } from './parse-docs/component-parser.mjs';
import {
printSkippedComponentsSummary,
printSuccess,
sortDocPayloads,
writeManifest,
} from './parse-docs/output.mjs';

if (!existsSync(ENTRY_FILE_PATH)) {
console.error(`❌ Central entry file not found at: ${ENTRY_FILE_PATH}`);

process.exit(1);
}

const outputDir = dirname(OUTPUT_FILE_PATH);

if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}

const project = new Project({ tsConfigFilePath: TSCONFIG_PATH });
const entryFile = project.addSourceFileAtPath(ENTRY_FILE_PATH);
const missingFamilyTypes = [];
const missingSignatures = [];

const sourceFileResolver = createSourceFileResolver({ project, entryFile });
const typeResolver = createTypeResolver({
limits: TYPE_TRACE_LIMITS,
resolveImportSourceFile: sourceFileResolver.resolveImportSourceFile,
});

// walk only from the central components entrypoint so output order and coverage stay deterministic
console.log(`🔍 Crawling entry point via AST: ${ENTRY_FILE_PATH}\n`);

const allDocPayloads = parseComponentsFromEntry({
entryFile,
sourceFileResolver,
typeResolver,
extractDocData,
onMissingFamilyTypes: (moduleSpecifier, componentName) => {
missingFamilyTypes.push({ moduleSpecifier, componentName });
},
onMissingSignature: (moduleSpecifier, componentName, signatureName) => {
missingSignatures.push({ moduleSpecifier, componentName, signatureName });
},
});

const sortedDocPayloads = sortDocPayloads(allDocPayloads);

writeManifest(OUTPUT_FILE_PATH, sortedDocPayloads);

printSuccess(OUTPUT_FILE_PATH);
printSkippedComponentsSummary({
missingFamilyTypes,
missingSignatures,
});
64 changes: 64 additions & 0 deletions packages/components/scripts/parse-docs/ast-helpers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright IBM Corp. 2021, 2026
* SPDX-License-Identifier: MPL-2.0
*/

import { Node } from 'ts-morph';

export function findImportForLocalName(sourceFile, localName) {
for (const importDecl of sourceFile.getImportDeclarations()) {
const moduleSpecifier = importDecl.getModuleSpecifierValue();

const defaultImport = importDecl.getDefaultImport();

if (defaultImport && defaultImport.getText() === localName) {
return {
importDecl,
moduleSpecifier,
importedName: 'default',
isDefault: true,
};
}

for (const namedImport of importDecl.getNamedImports()) {
const candidateLocalName =
namedImport.getAliasNode()?.getText() || namedImport.getName();

if (candidateLocalName !== localName) {
continue;
}

return {
importDecl,
moduleSpecifier,
importedName: namedImport.getName(),
isDefault: false,
};
}
}

return null;
}

export function getContextualComponentTypeQuery(typeNode) {
if (!typeNode) {
return null;
}

if (Node.isTypeQuery(typeNode)) {
return typeNode;
}

if (
Node.isTypeReference(typeNode) &&
typeNode.getTypeName().getText() === 'WithBoundArgs'
) {
const firstTypeArg = typeNode.getTypeArguments()[0];

if (firstTypeArg && Node.isTypeQuery(firstTypeArg)) {
return firstTypeArg;
}
}

return null;
}
Loading
Loading