UNPKG

kompendium

Version:

Documentation generator for Stencil components

482 lines (481 loc) 19.6 kB
import { Application, ReflectionKind, TypeDocReader, TSConfigReader, } from "typedoc"; import { existsSync, readFileSync } from "fs"; import ts from "typescript"; export function parseFile(filename, tsconfig) { if (!existsSync(filename)) { // eslint-disable-next-line no-console console.warn('typeRoot file does not exist', filename); return []; } const app = new Application(); // Use current working directory as TypeDoc's base path for clean relative paths const projectRoot = process.cwd(); const options = { entryPoints: [filename], skipErrorChecking: true, basePath: projectRoot, }; if (tsconfig) { options.tsconfig = tsconfig; } if (filename.endsWith('.d.ts')) { options.exclude = ['**/+(*test*|node_modules)/**']; } app.options.addReader(new TypeDocReader()); app.options.addReader(new TSConfigReader()); app.bootstrap(options); const project = app.convert(); if (!project) { // eslint-disable-next-line no-console console.warn('Could not find any type information'); return []; } const data = []; const allInterfaces = project.getReflectionsByKind(ReflectionKind.Interface); const allClasses = project.getReflectionsByKind(ReflectionKind.Class); const allTypeAliases = project.getReflectionsByKind(ReflectionKind.TypeAlias); const allEnums = project.getReflectionsByKind(ReflectionKind.Enum); const interfaces = allInterfaces.filter((r) => shouldIncludeType(r)); const classes = allClasses.filter((r) => shouldIncludeType(r)); const typeAliases = allTypeAliases.filter((r) => shouldIncludeType(r)); const enums = allEnums.filter((r) => shouldIncludeType(r)); interfaces.forEach((reflection) => addInterface(reflection, data)); classes.forEach((reflection) => addClass(reflection, data)); typeAliases.forEach((reflection) => addType(reflection, data)); enums.forEach((reflection) => { var _a; const members = []; const enumReflection = reflection; (_a = enumReflection.children) === null || _a === void 0 ? void 0 : _a.forEach((child) => { if (child.kind === ReflectionKind.EnumMember) { addEnumMember(child, members); } }); data.push({ type: 'enum', name: enumReflection.name, docs: getDocs(enumReflection), docsTags: getDocsTags(enumReflection), members: members, sources: getSources(enumReflection), }); }); return data; } /** * Determines if a type should be included in the documentation based on its source location * and documentation tags. * * Excludes types from: * - node_modules (third-party dependencies) * - examples directories * - test files (.test., .spec., /test/, /tests/) * - Types marked with @private or @internal tags * - Stencil auto-generated types (components.d.ts, Components namespace, CustomEvent wrappers) * @param {DeclarationReflection} reflection - The TypeDoc reflection to check * @returns {boolean} true if the type should be documented, false otherwise */ function shouldIncludeType(reflection) { var _a; if (!reflection.sources || reflection.sources.length === 0) { // No source information - include by default return true; } for (const source of reflection.sources) { const sourcePath = source.fullFileName || source.fileName || ''; if (shouldExcludeSource(sourcePath)) { return false; } } // Exclude CustomEvent wrapper types // These are generic wrappers around CustomEvent<T> that don't add useful // documentation if (reflection.name.endsWith('CustomEvent')) { return false; } // Exclude HTML element interface types // These are DOM element interfaces (HTMLLimelButtonElement, etc.) already // documented in the Components section if (reflection.name.startsWith('HTML') && reflection.name.endsWith('Element')) { return false; } // Exclude types in Stencil's Components namespace // Belt-and-suspenders: catches any component interfaces that were not // excluded based on source location if (isInComponentsNamespace(reflection)) { return false; } // Don't include anything marked @private or @internal if ((_a = reflection.comment) === null || _a === void 0 ? void 0 : _a.blockTags) { const hasPrivateTag = reflection.comment.blockTags.some((tag) => tag.tag === '@private' || tag.tag === '@internal'); if (hasPrivateTag) { return false; } } // Include this type return true; } /** * Checks if a source path should be excluded from documentation. * @param {string} sourcePath - The file path to check * @returns {boolean} true if the source should be excluded, false otherwise */ function shouldExcludeSource(sourcePath) { const normalizedPath = sourcePath.replaceAll('\\', '/'); if (normalizedPath.includes('node_modules/')) { return true; } if (normalizedPath.includes('/examples/') || normalizedPath.includes('/example/')) { return true; } if (normalizedPath.includes('.test.') || normalizedPath.includes('.spec.')) { return true; } // This file contains component prop interfaces and HTML element types that // are already documented in the Components section return normalizedPath.endsWith('components.d.ts'); } /** * Checks if a TypeDoc reflection is defined within Stencil's Components namespace. * * Stencil generates a Components namespace in components.d.ts containing interfaces * for component props. These types are already documented in the Components section * and should not be duplicated in the Types section. * @param {DeclarationReflection} reflection - The TypeDoc reflection to check * @returns {boolean} true if the reflection is in a Components namespace, false otherwise */ function isInComponentsNamespace(reflection) { let current = reflection.parent; while (current) { if (current.kind === ReflectionKind.Namespace && current.name === 'Components') { return true; } current = current.parent; } return false; } const fns = { [ReflectionKind.Interface]: addInterface, [ReflectionKind.Class]: addClass, [ReflectionKind.TypeAlias]: addType, [ReflectionKind.Enum]: addEnum, [ReflectionKind.EnumMember]: addEnumMember, }; const traverseCallback = (data) => (reflection) => { const fn = fns[reflection.kind]; if (fn) { fn(reflection, data); } else { reflection.traverse(traverseCallback(data)); } }; function addInterface(reflection, data) { var _a, _b; data.push({ type: 'interface', name: reflection.name, typeParams: getTypeParams(reflection), docs: getDocs(reflection), docsTags: getDocsTags(reflection), props: ((_a = reflection.children) === null || _a === void 0 ? void 0 : _a.filter(isProperty).map(getProperty)) || [], methods: ((_b = reflection.children) === null || _b === void 0 ? void 0 : _b.filter(isMethod).map(getMethod)) || [], sources: getSources(reflection), }); } function addClass(reflection, data) { var _a, _b; const decorators = getDecorators(reflection); const implementedInterfaces = getImplementedInterfaces(reflection); data.push({ type: 'class', name: reflection.name, typeParams: getTypeParams(reflection), docs: getDocs(reflection), docsTags: getDocsTags(reflection), props: ((_a = reflection.children) === null || _a === void 0 ? void 0 : _a.filter(isProperty).map((prop) => getPropertyWithInheritDoc(prop, implementedInterfaces))) || [], methods: ((_b = reflection.children) === null || _b === void 0 ? void 0 : _b.filter(isMethod).map((method) => getMethodWithInheritDoc(method, implementedInterfaces))) || [], sources: getSources(reflection), decorators: decorators, }); } function addType(reflection, data) { data.push({ type: 'alias', name: reflection.name, docs: getDocs(reflection), docsTags: getDocsTags(reflection), alias: reflection.type.toString(), sources: getSources(reflection), }); } function addEnum(reflection, data) { const members = []; reflection.traverse(traverseCallback(members)); data.push({ type: 'enum', name: reflection.name, docs: getDocs(reflection), docsTags: getDocsTags(reflection), members: members, sources: getSources(reflection), }); } function addEnumMember(reflection, data) { let value; if (reflection.type && reflection.type.type === 'literal') { const literalValue = reflection.type.value; if (typeof literalValue === 'string') { value = `"${literalValue}"`; } else { value = String(literalValue); } } else { value = reflection.defaultValue; } data.push({ name: reflection.name, docs: getDocs(reflection), docsTags: getDocsTags(reflection), value: value, }); } function getDocs(reflection) { var _a; if (!reflection.comment) { return ''; } const text = ((_a = reflection.comment.summary) === null || _a === void 0 ? void 0 : _a.map((part) => part.text).join('').trim()) || ''; // Normalize multiple newlines to single newlines return text.replaceAll(/\n\n+/g, '\n'); } function getDocsTags(reflection) { var _a, _b; return ((_b = (_a = reflection.comment) === null || _a === void 0 ? void 0 : _a.blockTags) === null || _b === void 0 ? void 0 : _b.map(getTag)) || []; } function getTag(tag) { var _a; // tag already has @ prefix, so remove it const tagName = tag.tag.replace(/^@+/, ''); return { name: tagName, text: ((_a = tag.content) === null || _a === void 0 ? void 0 : _a.map((part) => part.text).join('').trim()) || '', }; } function isProperty(reflection) { var _a; if (reflection.kind !== ReflectionKind.Property) { return false; } const type = reflection.type; return !((type === null || type === void 0 ? void 0 : type.type) === 'reflection' && ((_a = type.declaration) === null || _a === void 0 ? void 0 : _a.signatures)); } function isMethod(reflection) { var _a; if (reflection.kind !== ReflectionKind.Property) { return false; } const type = reflection.type; return (type === null || type === void 0 ? void 0 : type.type) === 'reflection' && ((_a = type.declaration) === null || _a === void 0 ? void 0 : _a.signatures); } function getProperty(reflection) { return { name: reflection.name, type: reflection.type.toString(), docs: getDocs(reflection), docsTags: getDocsTags(reflection), default: reflection.defaultValue, optional: reflection.flags.isOptional, }; } function getMethod(reflection) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; const type = reflection.type; const signature = (_b = (_a = type === null || type === void 0 ? void 0 : type.declaration) === null || _a === void 0 ? void 0 : _a.signatures) === null || _b === void 0 ? void 0 : _b[0]; if (!signature) { return { name: reflection.name, docs: getDocs(reflection), docsTags: [], parameters: [], returns: { type: 'void', docs: '' }, }; } let docs = ((_d = (_c = signature.comment) === null || _c === void 0 ? void 0 : _c.summary) === null || _d === void 0 ? void 0 : _d.map((part) => part.text).join('').trim()) || ''; // Normalize multiple newlines to single newlines docs = docs.replaceAll(/\n\n+/g, '\n'); const parameters = ((_e = signature.parameters) === null || _e === void 0 ? void 0 : _e.map((param) => { var _a, _b, _c, _d; return ({ name: param.name, type: ((_a = param.type) === null || _a === void 0 ? void 0 : _a.toString()) || 'any', docs: ((_c = (_b = param.comment) === null || _b === void 0 ? void 0 : _b.summary) === null || _c === void 0 ? void 0 : _c.map((part) => part.text).join('').trim()) || '', default: param.defaultValue, optional: ((_d = param.flags) === null || _d === void 0 ? void 0 : _d.isOptional) || false, }); })) || []; const returnsTag = (_g = (_f = signature.comment) === null || _f === void 0 ? void 0 : _f.blockTags) === null || _g === void 0 ? void 0 : _g.find((tag) => tag.tag === '@returns'); const returnsText = ((_h = returnsTag === null || returnsTag === void 0 ? void 0 : returnsTag.content) === null || _h === void 0 ? void 0 : _h.map((part) => part.text).join('').trim()) || ''; const returns = { type: ((_j = signature.type) === null || _j === void 0 ? void 0 : _j.toString()) || 'void', docs: returnsText, }; const docsTags = ((_l = (_k = signature.comment) === null || _k === void 0 ? void 0 : _k.blockTags) === null || _l === void 0 ? void 0 : _l.filter((tag) => tag.tag !== '@param' && tag.tag !== '@returns').map(getTag)) || []; return { name: reflection.name, docs: docs, docsTags: docsTags, parameters: parameters, returns: returns, }; } function getImplementedInterfaces(reflection) { const interfaces = []; const implemented = reflection.implementedTypes; if (implemented) { implemented.forEach((type) => { if (type.reflection) { interfaces.push(type.reflection); } }); } return interfaces; } function getPropertyWithInheritDoc(reflection, interfaces) { var _a, _b, _c; const prop = getProperty(reflection); const hasInheritDoc = (_b = (_a = reflection.comment) === null || _a === void 0 ? void 0 : _a.blockTags) === null || _b === void 0 ? void 0 : _b.some((tag) => tag.tag.toLowerCase() === '@inheritdoc'); if (hasInheritDoc && interfaces.length > 0) { // Try to find the property in implemented interfaces for (const iface of interfaces) { const interfaceProp = (_c = iface.children) === null || _c === void 0 ? void 0 : _c.find((child) => child.name === reflection.name); if (interfaceProp) { return { ...prop, docs: getDocs(interfaceProp), docsTags: getDocsTags(interfaceProp), }; } } } return prop; } function getMethodWithInheritDoc(reflection, interfaces) { var _a, _b, _c, _d, _e; const type = reflection.type; const signature = (_b = (_a = type === null || type === void 0 ? void 0 : type.declaration) === null || _a === void 0 ? void 0 : _a.signatures) === null || _b === void 0 ? void 0 : _b[0]; const hasInheritDoc = (_d = (_c = signature === null || signature === void 0 ? void 0 : signature.comment) === null || _c === void 0 ? void 0 : _c.blockTags) === null || _d === void 0 ? void 0 : _d.some((tag) => tag.tag.toLowerCase() === '@inheritdoc'); if (hasInheritDoc && interfaces.length > 0) { for (const iface of interfaces) { const interfaceMethod = (_e = iface.children) === null || _e === void 0 ? void 0 : _e.find((child) => child.name === reflection.name); if (interfaceMethod) { return getMethod(interfaceMethod); } } } return getMethod(reflection); } /** * Extract decorator information from TypeScript source code using AST parsing * * TypeDoc 0.23 removed the decorators property from DeclarationReflection, * which was present in 0.17. This function works around that limitation by * parsing the original TypeScript source files to extract decorator information. * @param {DeclarationReflection} reflection - The TypeDoc reflection to extract decorators from * @returns {DecoratorDescription[]} Array of decorator descriptions with names and arguments * @remarks * This is a workaround for incomplete TypeDoc 0.23 API. TypeDoc removed * decorator information from their reflection model, but decorators are * essential for documenting Stencil components (e.g., @Component, @Prop). * * Performance considerations: * - Reads and parses source files on demand (I/O + parsing overhead) * - Caches nothing - each call re-parses the file * - For large codebases, consider caching parsed source files * * Known limitations: * - Only extracts class-level decorators, not property/method decorators * - Decorator arguments are captured as raw text, not parsed * - Fails silently if source file is unavailable or parsing fails * - Depends on TypeScript AST structure (may break with TS version changes) * @example * ```typescript * // For a class like: * @Component({ tag: 'my-component' }) * class MyComponent { } * * // Returns: * [{ * name: 'Component', * arguments: { _config: "{ tag: 'my-component' }" } * }] * ``` */ function getDecorators(reflection) { if (!reflection.sources || reflection.sources.length === 0) { return []; } const source = reflection.sources[0]; const fileName = source.fullFileName || source.fileName; if (!existsSync(fileName)) { return []; } try { const sourceFile = ts.createSourceFile(fileName, readFileSync(fileName, 'utf8'), ts.ScriptTarget.Latest, true); let decorators = []; const visit = (node) => { var _a; if (ts.isClassDeclaration(node) && ((_a = node.name) === null || _a === void 0 ? void 0 : _a.getText()) === reflection.name) { let nodeDecorators; if (ts.canHaveDecorators(node)) { nodeDecorators = ts.getDecorators(node); } if (nodeDecorators) { decorators = nodeDecorators.map((decorator) => { const expression = decorator.expression; let name = ''; let args = {}; if (ts.isCallExpression(expression)) { name = expression.expression.getText(); if (expression.arguments.length > 0) { args = { _config: expression.arguments[0].getText(), }; } } else { name = expression.getText(); } return { name: name, arguments: args }; }); } } ts.forEachChild(node, visit); }; visit(sourceFile); return decorators; } catch (error) { // eslint-disable-next-line no-console console.warn('Failed to parse decorators:', error); return []; } } function getTypeParams(reflection) { var _a; return (((_a = reflection.typeParameters) === null || _a === void 0 ? void 0 : _a.map((param) => ({ name: param.name, }))) || []); } function getSources(reflection) { var _a; return ((_a = reflection.sources) === null || _a === void 0 ? void 0 : _a.map((source) => source.fileName)) || []; } //# sourceMappingURL=typedoc.js.map