From e3e1ed64dc42c9e19afdac691216c516cd363438 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sat, 23 May 2026 22:52:54 -0400 Subject: [PATCH 1/6] chore: improve output with metadata processing --- doc-kit.config.mjs | 4 +- .../{processor.mjs => processor/index.mjs} | 33 +- plugins/processor/metadata.mjs | 728 ++++++++++++++++++ plugins/processor/router.mjs | 340 ++++++++ plugins/shared/categories.mjs | 104 +++ plugins/shared/titles.mjs | 82 ++ plugins/theme/helpers/index.mjs | 16 +- plugins/theme/index.mjs | 3 - plugins/theme/partials/index.mjs | 394 +++++++--- plugins/theme/router.mjs | 72 -- scripts/markdown.mjs | 4 +- 11 files changed, 1595 insertions(+), 185 deletions(-) rename plugins/{processor.mjs => processor/index.mjs} (57%) create mode 100644 plugins/processor/metadata.mjs create mode 100644 plugins/processor/router.mjs create mode 100644 plugins/shared/categories.mjs create mode 100644 plugins/shared/titles.mjs delete mode 100644 plugins/theme/router.mjs diff --git a/doc-kit.config.mjs b/doc-kit.config.mjs index ba276858..6e3a2e35 100644 --- a/doc-kit.config.mjs +++ b/doc-kit.config.mjs @@ -9,7 +9,7 @@ export default { repository: 'webpack/webpack', // Input & Output - input: ['./pages/v5.x/**/*.md', './pages/v5.x/*.md'], + input: ['./pages/v5.x/**/*.md'], output: 'out', // Base URL, @@ -17,11 +17,13 @@ export default { ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000', }, + threads: 1, metadata: { typeMap: './pages/v5.x/type-map.json', }, web: { project: 'webpack', useAbsoluteURLs: true, + remoteConfigUrl: null, }, }; diff --git a/plugins/processor.mjs b/plugins/processor/index.mjs similarity index 57% rename from plugins/processor.mjs rename to plugins/processor/index.mjs index 0ea5f5c2..ce59d62f 100644 --- a/plugins/processor.mjs +++ b/plugins/processor/index.mjs @@ -1,13 +1,32 @@ import { Converter, ReflectionKind, Renderer } from 'typedoc'; import { writeFileSync } from 'node:fs'; import { join } from 'node:path'; +import { applySourceMetadata } from './metadata.mjs'; +import { DocKitRouter } from './router.mjs'; + +const typeMapKey = target => { + const name = target.getFullName(); + let root = target; + + while (root.parent) root = root.parent; + + if (!root.name || name === root.name || name.startsWith(`${root.name}.`)) { + return name; + } + + return `${root.name}.${name}`; +}; /** * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ export function load(app) { + // Keep router ownership in the processor plugin because routing depends on + // source metadata and the synthetic type pages created during conversion. + app.renderer.defineRouter('doc-kit', DocKitRouter); + app.converter.on(Converter.EVENT_RESOLVE_BEGIN, context => { - // Convert accessors to properties + // doc-kit has property metadata, not TypeDoc accessor metadata. context.project .getReflectionsByKind(ReflectionKind.Accessor) .forEach(accessor => { @@ -21,26 +40,32 @@ export function load(app) { } }); - // Remove re-exports + // Reference reflections duplicate the real declaration entries and confuse + // both routing and the custom type map. context.project .getReflectionsByKind(ReflectionKind.Reference) .forEach(ref => context.project.removeReflection(ref)); - // Merge `export=` namespaces into their parent + // types.d.ts models CommonJS `export = webpack` as a nested namespace. + // Collapse it so public names are emitted as webpack.*. context.project .getReflectionsByKind(ReflectionKind.Namespace) .filter(ref => ref.name === 'export=') .forEach(namespace => context.project.mergeReflections(namespace, namespace.parent) ); + + applySourceMetadata(context.project); }); app.renderer.on(Renderer.EVENT_END, () => { + // doc-kit resolves custom type annotations from this map while generating + // HTML, so use the final router URLs instead of recomputing paths here. const typeMap = Object.fromEntries( app.renderer.router .getLinkTargets() .map(target => [ - target.getFullName(), + typeMapKey(target), app.renderer.router.getAnchoredURL(target), ]) ); diff --git a/plugins/processor/metadata.mjs b/plugins/processor/metadata.mjs new file mode 100644 index 00000000..a577fe2a --- /dev/null +++ b/plugins/processor/metadata.mjs @@ -0,0 +1,728 @@ +import ts from 'typescript'; +import { Comment, CommentTag, ReflectionKind, ReflectionType } from 'typedoc'; +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { basename, dirname, extname, join, relative, resolve } from 'node:path'; +import { SOURCE_METADATA } from './router.mjs'; + +/** + * Source metadata is intentionally a documentation-only overlay: + * 1. types.d.ts decides which symbols exist and what their signatures are. + * 2. webpack/lib/index.js maps those public symbols back to implementation files. + * 3. all JS files under webpack/lib contribute summaries, params, returns, + * and stability/deprecation tags when that source comment has real content. + */ +const DOC_BLOCK_TAGS = new Set([ + '@deprecated', + '@example', + '@legacy', + '@remarks', + '@see', + '@since', + '@throws', +]); + +const MODIFIER_TAGS = new Set([ + '@alpha', + '@beta', + '@experimental', + '@internal', +]); + +const textPart = text => [{ kind: 'text', text }]; + +const normalizeText = value => + String(value ?? '') + .replace(/\r\n?/g, '\n') + .trim(); + +const jsDocText = value => { + if (!value) return ''; + if (Array.isArray(value)) { + return normalizeText( + value.map(part => part.text ?? part.getText?.() ?? '').join('') + ); + } + return normalizeText(value); +}; + +const hasCommentContent = comment => + Boolean( + comment?.summary || + comment?.returns || + comment?.params?.size || + comment?.blockTags?.length || + comment?.modifierTags?.size + ); + +const hasRenderableCommentContent = comment => + Boolean( + comment?.summary || + comment?.returns || + [...(comment?.params?.values() ?? [])].some(Boolean) || + comment?.blockTags?.some(tag => tag.content) || + comment?.modifierTags?.size + ); + +const mergeComments = comments => { + const merged = { + summary: '', + params: new Map(), + returns: '', + blockTags: [], + modifierTags: new Set(), + }; + + for (const comment of comments.filter(Boolean)) { + if (comment.summary) merged.summary = comment.summary; + if (comment.returns) merged.returns = comment.returns; + for (const [name, value] of comment.params) { + if (value) merged.params.set(name, value); + } + for (const tag of comment.blockTags) { + if (tag.content) merged.blockTags.push(tag); + } + for (const tag of comment.modifierTags) { + merged.modifierTags.add(tag); + } + } + + return hasCommentContent(merged) ? merged : undefined; +}; + +// TypeScript exposes JSDoc differently depending on whether a comment belongs +// to a declaration, assignment, or export wrapper. Walk up a tiny amount so +// comments attached to CommonJS assignment statements are still discovered. +const getJsDocs = node => { + let current = node; + + while (current && !ts.isSourceFile(current)) { + if (current.jsDoc?.length) return current.jsDoc; + if (ts.isBlock(current) || ts.isClassDeclaration(current)) break; + current = current.parent; + } + + return []; +}; + +const getJsDocComment = node => { + const jsDocs = getJsDocs(node); + const comments = jsDocs.map(jsDoc => { + const comment = { + summary: jsDocText(jsDoc.comment), + params: new Map(), + returns: '', + blockTags: [], + modifierTags: new Set(), + }; + + for (const tag of jsDoc.tags ?? []) { + const tagName = `@${tag.tagName?.escapedText ?? ''}`; + const content = jsDocText(tag.comment); + + if (ts.isJSDocParameterTag(tag) && tag.name) { + comment.params.set(tag.name.getText(), content); + continue; + } + + if (ts.isJSDocReturnTag(tag)) { + comment.returns = content; + continue; + } + + if (MODIFIER_TAGS.has(tagName)) { + comment.modifierTags.add(tagName); + continue; + } + + if (DOC_BLOCK_TAGS.has(tagName)) { + comment.blockTags.push({ tag: tagName, content }); + } + } + + return hasCommentContent(comment) ? comment : undefined; + }); + + return mergeComments(comments); +}; + +const toTypedocComment = sourceComment => { + if (!sourceComment || !hasRenderableCommentContent(sourceComment)) { + return undefined; + } + + const blockTags = []; + if (sourceComment.returns) { + blockTags.push(new CommentTag('@returns', textPart(sourceComment.returns))); + } + + for (const { tag, content } of sourceComment.blockTags ?? []) { + blockTags.push(new CommentTag(tag, textPart(content))); + } + + return new Comment( + sourceComment.summary ? textPart(sourceComment.summary) : [], + blockTags, + new Set(sourceComment.modifierTags ?? []) + ); +}; + +const toParameterComment = text => { + const content = normalizeText(text); + return content ? new Comment(textPart(content)) : undefined; +}; + +const assignComment = (target, sourceComment) => { + const typedocComment = toTypedocComment(sourceComment); + if (typedocComment) target.comment = typedocComment; +}; + +const applySignatureComment = (signature, sourceComment) => { + if (!signature || !hasRenderableCommentContent(sourceComment)) return; + + assignComment(signature, sourceComment); + + const params = [...sourceComment.params.values()].filter(Boolean); + + for (const parameter of signature.parameters ?? []) { + const comment = + sourceComment.params.get(parameter.name) ?? + (parameter.name.startsWith('__') && params.length === 1 + ? params[0] + : undefined); + const parameterComment = toParameterComment(comment); + if (parameterComment) parameter.comment = parameterComment; + } +}; + +const propertyNameText = name => { + if (!name) return undefined; + if ( + ts.isIdentifier(name) || + ts.isStringLiteral(name) || + ts.isNumericLiteral(name) + ) { + return name.text; + } + return undefined; +}; + +const getAssignedProperty = expression => { + if (!ts.isPropertyAccessExpression(expression)) return undefined; + + const property = expression.name.text; + const owner = expression.expression; + + if ( + ts.isPropertyAccessExpression(owner) && + owner.expression.getText() === 'module' && + owner.name.text === 'exports' + ) { + return property; + } + + if (owner.getText() === 'exports') return property; + + return undefined; +}; + +const isModuleExports = expression => + ts.isPropertyAccessExpression(expression) && + expression.expression.getText() === 'module' && + expression.name.text === 'exports'; + +const expressionLocalName = expression => + ts.isIdentifier(expression) ? expression.text : undefined; + +const resolveSourceRequest = (fromFile, request) => { + if (!request.startsWith('.')) return undefined; + + const base = resolve(dirname(fromFile), request); + const candidates = [base, `${base}.js`, join(base, 'index.js')]; + return candidates.find(candidate => existsSync(candidate)); +}; + +const firstRequire = expression => { + let found; + + const visit = node => { + if (found) return; + + if ( + ts.isCallExpression(node) && + node.expression.getText() === 'require' && + ts.isStringLiteral(node.arguments[0]) + ) { + found = { + request: node.arguments[0].text, + property: + node.parent && ts.isPropertyAccessExpression(node.parent) + ? node.parent.name.text + : undefined, + }; + return; + } + + ts.forEachChild(node, visit); + }; + + visit(expression); + return found; +}; + +const listJsFiles = directory => { + const entries = readdirSync(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...listJsFiles(path)); + } else if (entry.isFile() && extname(entry.name) === '.js') { + files.push(path); + } + } + + return files; +}; + +const sourceFileFor = filePath => + ts.createSourceFile( + filePath, + readFileSync(filePath, 'utf8'), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + +const sourceFileBase = filePath => basename(filePath, extname(filePath)); + +const createFileMetadata = (filePath, libDir) => { + const sourceFile = sourceFileFor(filePath); + const symbols = new Map(); + const classes = new Map(); + const namedExports = new Map(); + let defaultExport; + + const rememberSymbol = (name, data) => { + if (!name) return; + const existing = symbols.get(name) ?? {}; + symbols.set(name, { + ...existing, + ...data, + comment: data.comment ?? existing.comment, + members: data.members ?? existing.members, + }); + }; + + // Keep per-class member comments separate from file-level exports. A public + // class comes from types.d.ts, but method/constructor prose usually lives in + // the implementation file next to the code. + const classMembers = node => { + const members = new Map(); + + for (const member of node.members ?? []) { + if (ts.isConstructorDeclaration(member)) { + const comment = getJsDocComment(member); + if (comment) members.set('constructor', comment); + continue; + } + + const name = propertyNameText(member.name); + if (!name) continue; + + const comment = getJsDocComment(member); + if (comment) members.set(name, comment); + } + + return members; + }; + + const visit = node => { + if (ts.isClassDeclaration(node) && node.name) { + const data = { + kind: 'class', + localName: node.name.text, + comment: getJsDocComment(node), + members: classMembers(node), + }; + symbols.set(node.name.text, data); + classes.set(node.name.text, data); + } + + if (ts.isFunctionDeclaration(node) && node.name) { + rememberSymbol(node.name.text, { + kind: 'function', + localName: node.name.text, + comment: getJsDocComment(node), + }); + } + + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { + const comment = getJsDocComment(node); + if (comment) { + rememberSymbol(node.name.text, { + kind: 'variable', + localName: node.name.text, + comment, + }); + } + } + + if ( + ts.isExpressionStatement(node) && + ts.isBinaryExpression(node.expression) + ) { + const { left, right } = node.expression; + const comment = getJsDocComment(node) ?? getJsDocComment(right); + + if (isModuleExports(left)) { + defaultExport = { + localName: expressionLocalName(right), + comment, + }; + } else { + const property = getAssignedProperty(left); + if (property) { + namedExports.set(property, { + localName: expressionLocalName(right), + comment, + }); + } + } + + if ( + ts.isPropertyAccessExpression(left) && + ts.isIdentifier(left.expression) && + classes.has(left.expression.text) && + comment + ) { + classes.get(left.expression.text).members.set(left.name.text, comment); + } + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + + return { + filePath, + fileBase: sourceFileBase(filePath), + relativePath: relative(libDir, filePath).replace(/\\/g, '/'), + symbols, + classes, + namedExports, + defaultExport, + }; +}; + +const getExportObject = sourceFile => { + let exportObject; + + const visit = node => { + if (exportObject) return; + + if ( + ts.isExpressionStatement(node) && + ts.isBinaryExpression(node.expression) && + isModuleExports(node.expression.left) && + ts.isCallExpression(node.expression.right) + ) { + const objectArg = [...node.expression.right.arguments].find( + ts.isObjectLiteralExpression + ); + if (objectArg) exportObject = objectArg; + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return exportObject; +}; + +const buildExportMap = libDir => { + const indexPath = join(libDir, 'index.js'); + const sourceFile = sourceFileFor(indexPath); + const exportMap = new Map(); + const exportObject = getExportObject(sourceFile); + + const addExport = (parts, data) => { + exportMap.set(parts.join('.'), { + publicPath: parts.join('.'), + publicName: parts.at(-1), + ...data, + }); + }; + + // webpack's callable export is assigned outside lib/index.js, so seed the + // root namespace before walking the object tree of re-exported helpers. + addExport(['webpack'], { + sourcePath: join(libDir, 'webpack.js'), + localName: 'webpack', + }); + + const walkObject = (object, parts = []) => { + for (const property of object.properties) { + const name = propertyNameText(property.name); + if (!name) continue; + + if ( + ts.isPropertyAssignment(property) && + ts.isObjectLiteralExpression(property.initializer) + ) { + walkObject(property.initializer, [...parts, name]); + continue; + } + + const expression = ts.isGetAccessor(property) + ? property.body?.statements.filter(ts.isReturnStatement).at(-1) + ?.expression + : ts.isPropertyAssignment(property) + ? property.initializer + : undefined; + + if (!expression) continue; + + const requireCall = firstRequire(expression); + const sourcePath = requireCall?.request + ? resolveSourceRequest(indexPath, requireCall.request) + : undefined; + + addExport([...parts, name], { + sourcePath, + localName: requireCall?.property, + comment: getJsDocComment(property), + }); + } + }; + + if (exportObject) walkObject(exportObject); + return exportMap; +}; + +const buildSourceIndex = libDir => { + const sourceFiles = new Map(); + for (const file of listJsFiles(libDir)) { + sourceFiles.set(file, createFileMetadata(file, libDir)); + } + return sourceFiles; +}; + +const allReflections = project => Object.values(project.reflections); + +const publicName = reflection => reflection.getFullName?.(); + +const exportEntryFor = (name, exportMap) => { + const exact = exportMap.get(name); + if (exact) return exact; + + const parts = name.split('.'); + for (let i = parts.length - 1; i > 0; i--) { + const parent = exportMap.get(parts.slice(0, i).join('.')); + if (!parent?.sourcePath) continue; + + const memberPath = parts.slice(i); + // If lib/index.js exports an object, types.d.ts may expose its properties as + // public members. Reuse the parent source file and match the leaf locally. + return { + ...parent, + publicPath: name, + publicName: memberPath.at(-1), + localName: memberPath.at(-1), + parentPublicPath: parent.publicPath, + memberPath, + }; + } +}; + +const sourceEntryFor = (entry, sourceFiles) => { + const file = entry.sourcePath ? sourceFiles.get(entry.sourcePath) : undefined; + if (!file) return undefined; + + const exported = entry.publicName + ? file.namedExports.get(entry.publicName) + : undefined; + const localName = + entry.localName ?? + exported?.localName ?? + file.defaultExport?.localName ?? + entry.publicName; + const symbol = + file.symbols.get(localName) ?? file.symbols.get(entry.publicName); + + return { + file, + localName, + comment: + symbol?.comment ?? + entry.comment ?? + exported?.comment ?? + file.defaultExport?.comment, + symbol, + }; +}; + +const commentForSourceEntry = sourceEntry => + sourceEntry?.comment ?? sourceEntry?.symbol?.comment; + +const memberSourceComment = (sourceEntry, memberName) => + sourceEntry?.symbol?.members?.get(memberName) ?? + sourceEntry?.file.namedExports.get(memberName)?.comment ?? + sourceEntry?.file.symbols.get(memberName)?.comment; + +const namedExportComment = (sourceEntry, memberName) => { + const exported = sourceEntry?.file.namedExports.get(memberName); + if (!exported) return undefined; + return ( + exported.comment ?? + sourceEntry.file.symbols.get(exported.localName)?.comment ?? + sourceEntry.file.symbols.get(memberName)?.comment + ); +}; + +const ownSignatures = reflection => [ + ...(reflection.signatures ?? []), + ...(reflection.type instanceof ReflectionType + ? (reflection.type.declaration.signatures ?? []) + : []), +]; + +const applyFunctionLikeComment = (reflection, sourceComment) => { + if (!hasRenderableCommentContent(sourceComment)) return; + + const signatures = ownSignatures(reflection); + if (signatures.length) { + signatures.forEach(signature => + applySignatureComment(signature, sourceComment) + ); + } else { + assignComment(reflection, sourceComment); + } +}; + +// Exported object literals (for example helper namespaces) often document their +// individual properties as named exports in the implementation file. +const applyObjectMembers = (reflection, sourceEntry) => { + const declaration = + reflection.type instanceof ReflectionType + ? reflection.type.declaration + : undefined; + + for (const child of declaration?.children ?? []) { + const sourceComment = + namedExportComment(sourceEntry, child.name) ?? + sourceEntry?.file.symbols.get(child.name)?.comment; + + if (!hasRenderableCommentContent(sourceComment)) continue; + + const signatures = ownSignatures(child); + if (signatures.length) { + signatures.forEach(signature => + applySignatureComment(signature, sourceComment) + ); + } else { + assignComment(child, sourceComment); + } + } +}; + +const applyNamespaceMembers = (reflection, sourceEntry) => { + for (const child of reflection.children ?? []) { + const sourceComment = + namedExportComment(sourceEntry, child.name) ?? + sourceEntry?.file.symbols.get(child.name)?.comment; + + if (!hasRenderableCommentContent(sourceComment)) continue; + + applyFunctionLikeComment(child, sourceComment); + } +}; + +const applyClassMembers = (reflection, sourceEntry) => { + assignComment(reflection, commentForSourceEntry(sourceEntry)); + + for (const child of reflection.children ?? []) { + if (child.kindOf(ReflectionKind.Constructor)) { + const comment = memberSourceComment(sourceEntry, 'constructor'); + child.signatures?.forEach(signature => + applySignatureComment(signature, comment) + ); + continue; + } + + const comment = memberSourceComment(sourceEntry, child.name); + if (!hasRenderableCommentContent(comment)) continue; + + if (child.signatures?.length) { + child.signatures.forEach(signature => + applySignatureComment(signature, comment) + ); + } else { + assignComment(child, comment); + } + } +}; + +const attachSourceMetadata = (reflection, sourceEntry, exportEntry) => { + if (!sourceEntry?.file) return; + + const leaf = publicName(reflection)?.split('.').at(-1); + const fileBase = sourceEntry.file.fileBase; + + reflection[SOURCE_METADATA] = { + sourcePath: sourceEntry.file.filePath, + sourceRelativePath: sourceEntry.file.relativePath, + fileBase, + anchorName: fileBase !== leaf ? leaf : undefined, + publicPath: exportEntry.publicPath, + }; +}; + +export const applySourceMetadata = ( + project, + { libDir = './webpack/lib' } = {} +) => { + const absoluteLibDir = resolve(libDir); + if (!existsSync(absoluteLibDir) || !statSync(absoluteLibDir).isDirectory()) { + return; + } + + const sourceFiles = buildSourceIndex(absoluteLibDir); + const exportMap = buildExportMap(absoluteLibDir); + + // The declaration file remains authoritative for shape. This pass only + // enriches public reflections that can be matched back to webpack/lib source. + for (const reflection of allReflections(project)) { + if ( + !reflection.kindOf( + ReflectionKind.Class | + ReflectionKind.Namespace | + ReflectionKind.Function | + ReflectionKind.Variable + ) + ) { + continue; + } + + const name = publicName(reflection); + const exportEntry = name ? exportEntryFor(name, exportMap) : undefined; + if (!exportEntry?.sourcePath) continue; + + const sourceEntry = sourceEntryFor(exportEntry, sourceFiles); + attachSourceMetadata(reflection, sourceEntry, exportEntry); + + if (reflection.kindOf(ReflectionKind.Namespace)) { + applyNamespaceMembers(reflection, sourceEntry); + continue; + } + + if (reflection.kindOf(ReflectionKind.Class)) { + applyClassMembers(reflection, sourceEntry); + continue; + } + + applyFunctionLikeComment(reflection, commentForSourceEntry(sourceEntry)); + applyObjectMembers(reflection, sourceEntry); + } +}; diff --git a/plugins/processor/router.mjs b/plugins/processor/router.mjs new file mode 100644 index 00000000..6849b430 --- /dev/null +++ b/plugins/processor/router.mjs @@ -0,0 +1,340 @@ +import createNodeSlugger from '@node-core/doc-kit/src/generators/metadata/utils/slugger.mjs'; +import { + DeclarationReflection, + PageKind, + Reflection, + ReflectionKind, +} from 'typedoc'; +import { MemberRouter } from 'typedoc-plugin-markdown'; +import { categoryForReflection } from '../shared/categories.mjs'; +import { getMemberTitle } from '../shared/titles.mjs'; + +/** + * The router owns the public Markdown shape. It keeps one class per file, + * namespaces on index pages, namespace functions/constants as anchored entries, + * root plugin classes under plugins/, and structural types on category types + * pages instead of the root README. + */ +export const SOURCE_METADATA = Symbol.for('webpack-doc-kit.sourceMetadata'); +export const TYPE_PAGE_METADATA = Symbol.for( + 'webpack-doc-kit.typePageMetadata' +); + +const sluggers = new Map(); + +// Interfaces and type aliases are still public API, but doc-kit consumes them +// more cleanly when category pages collect them instead of leaving hundreds of +// structural entries on README.md. +const TYPE_PAGE_KINDS = ReflectionKind.Interface | ReflectionKind.TypeAlias; + +const fullNameParts = reflection => reflection.getFullName().split('.'); + +const pagePath = parts => parts.join('/'); + +const rootExportBaseName = reflection => { + const category = categoryForReflection(reflection); + return category ? `${category}/${reflection.name}` : reflection.name; +}; + +const namespaceBaseName = reflection => { + if (!reflection.kindOf(ReflectionKind.Namespace)) { + return; + } + + const parts = fullNameParts(reflection); + + if (parts.length === 1) { + const category = categoryForReflection(reflection); + + if (category) { + return `${category}/${reflection.name}`; + } + + // Lowercase root namespaces behave like source directories: their classes + // can live beside an index page, while uppercase namespace-like objects + // (for example RuntimeGlobals) remain a single file. + return /^[a-z]/.test(reflection.name) + ? `${reflection.name}/index` + : reflection.name; + } + + return /^[a-z]/.test(reflection.name) + ? pagePath([...parts, 'index']) + : pagePath(parts); +}; + +const classBaseName = reflection => { + const parts = fullNameParts(reflection); + + if (parts.length === 1) { + return rootExportBaseName(reflection); + } + + return pagePath(parts); +}; + +export const sourcePageBaseName = reflection => { + if ( + !(reflection instanceof Reflection) || + !reflection.kindOf(ReflectionKind.Class | ReflectionKind.Namespace) + ) { + return; + } + + if (reflection.kindOf(ReflectionKind.Class)) { + return classBaseName(reflection); + } + + if (reflection.kindOf(ReflectionKind.Namespace)) { + return namespaceBaseName(reflection); + } + + return; +}; + +export const hasSourcePage = reflection => + Boolean(sourcePageBaseName(reflection)); + +export const sourceAnchorName = reflection => { + if ( + !(reflection instanceof Reflection) || + !reflection.kindOf(ReflectionKind.Function | ReflectionKind.Variable) + ) { + return; + } + + const baseName = sourcePageBaseName(reflection); + if (!baseName || baseName.split('/').at(-1) === reflection.name) { + return; + } + + return reflection[SOURCE_METADATA]?.anchorName; +}; + +const typePageBaseName = reflection => { + const category = categoryForReflection(reflection); + return category ? `${category}/types` : 'types'; +}; + +const typePageTitle = baseName => + baseName === 'types' + ? 'webpack.types' + : `webpack.${baseName.replace(/\/types$/, '').replace(/\//g, '.')}.types`; + +const typePageName = baseName => baseName.replace(/\//g, '.'); + +const removeChildren = (items, moved) => + items?.filter(item => !moved.has(item)); + +const removeFromGroups = (groups, moved) => + groups + ?.map(group => ({ + ...group, + children: group.children.filter(child => !moved.has(child)), + categories: removeFromGroups(group.categories, moved), + })) + .filter(group => group.children.length || group.categories?.length); + +const makeTypeGroup = (title, children) => + children.length ? { title, children } : undefined; + +const createTypePage = (project, baseName, children) => { + const page = new DeclarationReflection( + typePageName(baseName), + ReflectionKind.Namespace, + project + ); + const interfaces = children.filter(child => + child.kindOf(ReflectionKind.Interface) + ); + const typeAliases = children.filter(child => + child.kindOf(ReflectionKind.TypeAlias) + ); + + page.children = children; + page.childrenIncludingDocuments = children; + page.groups = [ + makeTypeGroup('Interfaces', interfaces), + makeTypeGroup('Type Aliases', typeAliases), + ].filter(Boolean); + // Synthetic type pages are not TypeDoc declarations from the input file. This + // metadata gives the theme and router a stable title and output path for them. + page[TYPE_PAGE_METADATA] = { + baseName, + title: typePageTitle(baseName), + }; + + return page; +}; + +const compareByName = (a, b) => a.name.localeCompare(b.name); + +export class DocKitRouter extends MemberRouter { + /** @param {import('typedoc').ProjectReflection} project */ + buildPages(project) { + const typePages = this.prepareTypePages(project); + const pages = super.buildPages(project); + + for (const { baseName, children, model } of typePages) { + const url = this.getFileName(baseName); + this.fullUrls.set(model, url); + pages.push({ kind: PageKind.Reflection, model, url }); + + for (const child of children) { + this.buildAnchors(child, model); + } + } + + return pages; + } + + /** @param {import('typedoc').ProjectReflection} project */ + prepareTypePages(project) { + const movedTypes = (project.children ?? []) + .filter(child => child.kindOf(TYPE_PAGE_KINDS)) + .sort(compareByName); + const movedSet = new Set(movedTypes); + const byPage = new Map(); + + for (const reflection of movedTypes) { + const baseName = typePageBaseName(reflection); + const group = byPage.get(baseName) ?? []; + group.push(reflection); + byPage.set(baseName, group); + } + + // Remove moved structural types from the project root before TypeDoc builds + // README.md; their URLs are rebuilt below against the synthetic type pages. + project.children = removeChildren(project.children, movedSet); + project.childrenIncludingDocuments = removeChildren( + project.childrenIncludingDocuments, + movedSet + ); + project.groups = removeFromGroups(project.groups, movedSet); + project.categories = removeFromGroups(project.categories, movedSet); + + return [...byPage].map(([baseName, children]) => ({ + baseName, + children, + model: createTypePage(project, baseName, children), + })); + } + + /** + * @param {import('typedoc').DeclarationReflection} reflection + * @param {import('typedoc').MarkdownPageEvent[]} outPages + */ + buildChildPages(reflection, outPages) { + const kind = this.getPageKind(reflection); + + if (!kind) { + // Functions and constants are entries on their parent page, never files. + this.buildAnchors(reflection, reflection.parent); + return; + } + + if (hasSourcePage(reflection)) { + const shouldWritePage = this.shouldWritePage(reflection); + const idealName = this.getIdealBaseName(reflection); + const actualName = shouldWritePage + ? this.getFileName(idealName) + : `${idealName}${this.extension}`; + + this.fullUrls.set(reflection, actualName); + + if (shouldWritePage) { + outPages.push({ kind, model: reflection, url: actualName }); + } + } else if ( + !reflection.kindOf( + ReflectionKind.Module | + ReflectionKind.Namespace | + ReflectionKind.Document + ) + ) { + this.buildAnchors(reflection, reflection.parent); + } + + reflection.traverse(child => { + this.buildChildPages(child, outPages); + return true; + }); + } + + /** @param {import('typedoc').DeclarationReflection} reflection */ + getIdealBaseName(reflection) { + return sourcePageBaseName(reflection) ?? super.getIdealBaseName(reflection); + } + + /** @param {import('typedoc').RouterTarget} pageTarget */ + getSlugger(pageTarget) { + if (sluggers.has(pageTarget)) { + return sluggers.get(pageTarget); + } + + // Use doc-kit's slugger so type-map anchors match the parser's heading + // normalization instead of TypeDoc's default GitHub-style slugs. + const slugger = createNodeSlugger(); + sluggers.set(pageTarget, slugger); + return slugger; + } + + /** @param {import('typedoc').RouterTarget} target */ + getAnchoredURL(target) { + const fullUrl = this.getFullUrl(target); + const [page, routedAnchor] = fullUrl.split('#'); + const anchor = + routedAnchor ?? sourceAnchorName(target) ?? this.getAnchor(target); + const pageUrl = anchor ? page.replace(/\.md$/, '.html') : page; + + return anchor ? `${pageUrl}#${anchor}` : page; + } + + /** + * @param {import('typedoc').RouterTarget} target + * @param {import('typedoc').RouterTarget} pageTarget + */ + buildAnchors(target, pageTarget) { + if ( + !(target instanceof Reflection) || + !(pageTarget instanceof Reflection) + ) { + return; + } + + const pageUrl = this.fullUrls.get(pageTarget); + if (!pageUrl) return; + + if ( + !target.isDeclaration() && + !target.isSignature() && + !target.isTypeParameter() + ) { + return; + } + + if ( + target.kindOf(ReflectionKind.TypeLiteral) && + (!target.parent?.kindOf(ReflectionKind.SomeExport) || + target.parent.type?.type !== 'reflection') + ) { + return; + } + + if (!target.kindOf(ReflectionKind.TypeLiteral)) { + const title = getMemberTitle(target); + const anchor = this.getSlugger(pageTarget).slug(title); + + this.fullUrls.set( + target, + `${pageUrl.replace(/\.md$/, '.html')}#${anchor}` + ); + this.anchors.set(target, anchor); + } + + target.traverse(child => { + this.buildAnchors(child, pageTarget); + return true; + }); + } +} diff --git a/plugins/shared/categories.mjs b/plugins/shared/categories.mjs new file mode 100644 index 00000000..166dd4b7 --- /dev/null +++ b/plugins/shared/categories.mjs @@ -0,0 +1,104 @@ +import { ReflectionKind } from 'typedoc'; + +// First match wins. Keep more specific groups above broad API-family rules so +// adding a new category is usually a single RegExp entry rather than router code. +const CATEGORY_RULES = [ + { + category: 'plugins', + match: reflection => + reflection.kindOf(ReflectionKind.Class) && + reflection.name.endsWith('Plugin'), + pattern: /^WebpackPlugin/, + }, + { + category: 'cli', + pattern: /^(?:Argument|Colors|ColorsOptions|Problem)$/, + }, + { + category: 'assets', + pattern: /^Asset|AssetInfo$/, + }, + { + category: 'cache', + pattern: /Cache|Cached|Etag|ValueCache/, + }, + { + category: 'runtime', + pattern: /^Runtime.*/, + }, + { + category: 'stats', + pattern: /^(?:Multi)?Stats/, + }, + { + category: 'errors', + pattern: /(?:Error|ValidationError)$/, + }, + { + category: 'chunks', + pattern: /^(?:.*Chunk.*|Entrypoint)$/, + }, + { + category: 'compilation', + pattern: + /^(?:Compilation|Compiler|MultiCompiler|Watching|PathData|CodeGenerationResults?)$/, + }, + { + category: 'dependencies', + pattern: /Dependency/, + }, + { + category: 'entries', + pattern: /^Entry/, + }, + { + category: 'externals', + pattern: /^External|Externals/, + }, + { + category: 'filesystem', + pattern: /FileSystem$/, + }, + { + category: 'library', + pattern: /Library/, + }, + { + category: 'loaders', + pattern: /Loader/, + }, + { + category: 'modules', + pattern: + /^(?:AsyncDependenciesBlock|.*Dependency|.*Module.*|Generator|Parser)$/, + }, + { + category: 'resolvers', + pattern: /^Resolve/, + }, + { + category: 'rules', + pattern: /^RuleSet/, + }, + { + category: 'serialization', + pattern: /(?:Serializer|Deserializer)/, + }, + { + category: 'templates', + pattern: /^(?:Template|RenderManifest)/, + }, + { + category: 'config', + pattern: + /^(?:Configuration|MultiConfiguration|.*Options(?:Normalized)?|validate(?:Schema)?|WebpackOptions.*)$/, + }, +]; + +export const categoryForReflection = reflection => { + for (const rule of CATEGORY_RULES) { + if (rule.match?.(reflection) || rule.pattern?.test(reflection.name)) { + return rule.category; + } + } +}; diff --git a/plugins/shared/titles.mjs b/plugins/shared/titles.mjs new file mode 100644 index 00000000..364330e5 --- /dev/null +++ b/plugins/shared/titles.mjs @@ -0,0 +1,82 @@ +import { ReflectionKind } from 'typedoc'; + +// Heading text is also anchor input. Keep all programmatic names formatted here +// so the Markdown theme and router cannot drift into different slugs. +const KIND_PREFIX = { + [ReflectionKind.Class]: 'Class', + [ReflectionKind.Interface]: 'Interface', + [ReflectionKind.Enum]: 'Enum', + [ReflectionKind.TypeAlias]: 'Type', + [ReflectionKind.Namespace]: 'Namespace', + [ReflectionKind.Accessor]: 'Accessor', +}; + +const STATIC_PREFIX = { + [ReflectionKind.Method]: 'Static method', +}; + +const escapeCode = value => String(value).replace(/`/g, '\\`'); + +export const fullName = model => { + const name = model.getFullName?.() ?? model.name; + let root = model; + + while (root.parent) root = root.parent; + + // TypeDoc omits the project name from nested full names. The generated docs + // treat the project as the public webpack namespace, so add it back whenever + // TypeDoc has not already included it. + if (!root.name || name === root.name || name.startsWith(`${root.name}.`)) { + return name; + } + + return `${root.name}.${name}`; +}; + +export const formatParams = (params = []) => + params + .map(({ name, flags }, i) => { + const paramName = flags?.isRest ? `...${name}` : name; + return flags?.isOptional || flags?.isRest + ? i + ? `[, ${paramName}]` + : `[${paramName}]` + : i + ? `, ${paramName}` + : paramName; + }) + .join(''); + +export const signatureExpression = (model, params = []) => + `${fullName(model)}(${formatParams(params)})`; + +export const callableSignatures = model => + model.signatures ?? model.type?.declaration?.signatures ?? []; + +export const getMemberPrefix = model => { + const prefix = model.flags?.isStatic + ? STATIC_PREFIX[model.kind] + : KIND_PREFIX[model.kind]; + + return prefix ? `${prefix}: ` : ''; +}; + +export const getMemberTitle = model => { + const prefix = getMemberPrefix(model); + const params = callableSignatures(model)[0]?.parameters; + const name = escapeCode(fullName(model)); + + if (params) { + return `${prefix}\`${escapeCode(signatureExpression(model, params))}\``; + } + + return `${prefix}\`${name}\``; +}; + +export const getPageTitle = model => { + const title = model.kindOf?.(ReflectionKind.Class) + ? `Class: \`${escapeCode(fullName(model))}\`` + : `\`${escapeCode(fullName(model))}\``; + + return title; +}; diff --git a/plugins/theme/helpers/index.mjs b/plugins/theme/helpers/index.mjs index 0b77228e..f5dfdcae 100644 --- a/plugins/theme/helpers/index.mjs +++ b/plugins/theme/helpers/index.mjs @@ -40,19 +40,23 @@ export default ctx => ({ return entries.map(ctx.helpers.typedListItem).join('\n'); }, - examples(comment, { headingLevel }) { + examples(comment) { const examples = comment?.blockTags?.filter(t => t.tag === '@example') ?? []; if (!examples.length) return null; - const prefix = '#'.repeat(headingLevel + 1); - const single = examples.length === 1; + // Source JSDoc examples in webpack are usually plain code snippets. The + // doc-kit spec expects code examples to be fenced, so wrap bare snippets + // while leaving already-fenced examples untouched. return examples .map((tag, i) => { const body = ctx.helpers.getCommentParts(tag.content).trim(); - return ( - body && `${prefix} Example${single ? '' : ` ${i + 1}`}\n\n${body}` - ); + if (!body) return; + if (body.includes('```')) return body; + + const displayName = + examples.length > 1 ? ` displayName="Example ${i + 1}"` : ''; + return `\`\`\`js${displayName}\n${body}\n\`\`\``; }) .filter(Boolean) .join('\n\n'); diff --git a/plugins/theme/index.mjs b/plugins/theme/index.mjs index dd2857a0..245434ea 100644 --- a/plugins/theme/index.mjs +++ b/plugins/theme/index.mjs @@ -2,8 +2,6 @@ import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown'; import helpers from './helpers/index.mjs'; import partials from './partials/index.mjs'; -import { DocKitRouter } from './router.mjs'; - export class DocKitTheme extends MarkdownTheme { getRenderContext(page) { return new DocKitThemeContext(this, page, this.application.options); @@ -25,5 +23,4 @@ export class DocKitThemeContext extends MarkdownThemeContext { */ export function load(app) { app.renderer.defineTheme('doc-kit', DocKitTheme); - app.renderer.defineRouter('doc-kit', DocKitRouter); } diff --git a/plugins/theme/partials/index.mjs b/plugins/theme/partials/index.mjs index a5dc697f..4f6315a0 100644 --- a/plugins/theme/partials/index.mjs +++ b/plugins/theme/partials/index.mjs @@ -1,115 +1,313 @@ -import { ReflectionKind } from 'typedoc'; +import { TYPE_PAGE_METADATA } from '../../processor/router.mjs'; +import { + ArrayType, + i18n, + IntersectionType, + ReflectionKind, + ReflectionType, + UnionType, +} from 'typedoc'; +import { + callableSignatures, + formatParams, + getMemberTitle, + getPageTitle, +} from '../../shared/titles.mjs'; import * as typePartials from './types.mjs'; -const KIND_PREFIX = { - [ReflectionKind.Class]: 'Class', - [ReflectionKind.Interface]: 'Interface', - [ReflectionKind.Enum]: 'Enum', - [ReflectionKind.TypeAlias]: 'Type', - [ReflectionKind.Namespace]: 'Namespace', - [ReflectionKind.Accessor]: 'Accessor', -}; - -const STATIC_PREFIX = { - [ReflectionKind.Method]: 'Static method', -}; - -const formatParams = (params = []) => - params - .map(({ name, flags }, i) => - flags?.isOptional - ? i - ? `[, ${name}]` - : `[${name}]` - : i - ? `, ${name}` - : name - ) - .join(''); - -export const getMemberPrefix = model => { - const prefix = model.flags?.isStatic - ? STATIC_PREFIX[model.kind] - : KIND_PREFIX[model.kind]; - - return prefix ? `${prefix}: ` : ''; -}; +const heading = (level, title) => `${'#'.repeat(level)} ${title}`; -export const getMemberTitle = model => { - const prefix = getMemberPrefix(model); - const params = model.signatures?.[0]?.parameters; +// These partials deliberately trade TypeDoc's default blockquote signatures for +// doc-kit headings and typed lists. The router uses the same title helpers, so +// rendered anchors and type-map URLs stay in sync. +const inheritedComment = (model, options) => + options.multipleSignatures + ? model.comment + : model.comment || model.parent?.comment; - if (!params) { - return `${prefix}\`${model.name}\``; - } - - return `${prefix}\`${model.name}(${formatParams(params)})\``; -}; +const declarationType = model => + model.type ?? + model.getSignature?.type ?? + model.setSignature?.parameters?.[0]?.type; /** * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx * @returns {import('typedoc-plugin-markdown').MarkdownThemeContext['partials']} */ -export default ctx => ({ - ...ctx.partials, - ...typePartials, - - signature(model, options) { - const comment = options.multipleSignatures - ? model.comment - : model.comment || model.parent?.comment; - - const stability = ctx.helpers.stabilityBlockquote(comment); - - return [ - stability, - stability && '', - model.typeParameters?.length && - ctx.partials.typeParametersList(model.typeParameters, { - headingLevel: options.headingLevel, - }), - model.parameters?.length && - ctx.partials.parametersList(model.parameters, { - headingLevel: options.headingLevel, - }), - ctx.helpers.typedListItem({ - label: 'Returns', - type: model.type ?? 'void', - comment: model.comment?.getTag('@returns'), - }), - '', - comment && - ctx.partials.comment(comment, { - headingLevel: options.headingLevel, - showTags: false, +export default ctx => { + return { + ...ctx.partials, + ...typePartials, + + pageTitle() { + const model = ctx.page.model; + if (model[TYPE_PAGE_METADATA]?.title) + return `\`${model[TYPE_PAGE_METADATA].title}\``; + if (model.getFullName) return getPageTitle(model); + return ctx.partials.pageTitle(); + }, + + signature(model, options) { + const comment = inheritedComment(model, options); + const stability = ctx.helpers.stabilityBlockquote(comment); + + return [ + stability, + stability && '', + model.typeParameters?.length && + ctx.partials.typeParametersList(model.typeParameters, { + headingLevel: options.headingLevel, + }), + model.parameters?.length && + ctx.partials.parametersList(model.parameters, { + headingLevel: options.headingLevel, + }), + ctx.helpers.typedListItem({ + label: 'Returns', + type: model.type ?? 'void', + comment: model.comment?.getTag('@returns'), }), - ctx.helpers.examples(comment, options), - ] - .filter(x => typeof x === 'string' || Boolean(x)) - .join('\n'); - }, + '', + comment && + ctx.partials.comment(comment, { + headingLevel: options.headingLevel, + showTags: false, + }), + ctx.helpers.examples(comment, options), + ] + .filter(x => typeof x === 'string' || Boolean(x)) + .join('\n'); + }, + + declaration(model, options) { + const opts = { headingLevel: 2, nested: false, ...options }; + const signatures = callableSignatures(model); + + if (signatures.length) { + return signatures + .map(signature => + ctx.partials.signature(signature, { + headingLevel: opts.headingLevel, + multipleSignatures: signatures.length > 1, + }) + ) + .join('\n\n'); + } - constructor(model, options) { - const md = []; - const heading = '#'.repeat(options.headingLevel); + const comment = model.comment; + const stability = ctx.helpers.stabilityBlockquote(comment); + const md = [ + stability, + stability && '', + ctx.partials.declarationTitle(model), + ]; - model.signatures?.forEach(signature => { - const paramsString = formatParams(signature.parameters ?? []); + if (comment) { + md.push( + '', + ctx.partials.comment(comment, { + headingLevel: opts.headingLevel, + showSummary: true, + showTags: false, + }) + ); + } - md.push(`${heading} \`new ${model.parent.name}(${paramsString})\``); + let typeDeclaration = model.type?.declaration; + if ( + model.type instanceof ArrayType && + model.type.elementType instanceof ReflectionType + ) { + typeDeclaration = model.type.elementType.declaration; + } + + if (model.type instanceof IntersectionType) { + for (const intersectionType of model.type.types ?? []) { + if ( + intersectionType instanceof ReflectionType && + intersectionType.declaration.children && + !intersectionType.declaration.signatures + ) { + md.push(heading(opts.headingLevel, i18n.theme_type_declaration())); + md.push( + ctx.partials.typeDeclaration(intersectionType.declaration, { + headingLevel: opts.headingLevel, + }) + ); + } + } + } + + const hasUnionDetails = + model.type instanceof UnionType && + ctx.helpers.hasUsefulTypeDetails(model.type); + if (hasUnionDetails) { + md.push(heading(opts.headingLevel, i18n.theme_union_members())); + md.push(ctx.partials.typeDeclarationUnionContainer(model, opts)); + } else if (typeDeclaration) { + const useHeading = + typeDeclaration.children?.length && + (model.kind !== ReflectionKind.Property || + ctx.helpers.useTableFormat('properties')); + if (useHeading) { + md.push(heading(opts.headingLevel, i18n.theme_type_declaration())); + } + md.push( + ctx.partials.typeDeclarationContainer(model, typeDeclaration, opts) + ); + } + + md.push(ctx.helpers.examples(comment, opts)); md.push( - ctx.partials.signature(signature, { - headingLevel: options.headingLevel + 1, - }) + ctx.partials.inheritance(model, { headingLevel: opts.headingLevel }) ); - }); - return md.join('\n\n'); - }, - typeParametersList: () => '', + return md.filter(x => typeof x === 'string' || Boolean(x)).join('\n\n'); + }, + + declarationTitle(model) { + return ctx.helpers.typedListItem({ + label: 'Type', + type: declarationType(model) ?? 'unknown', + }); + }, - memberTitle: getMemberTitle, - parametersList: ctx.helpers.typedList, - typeDeclarationList: ctx.helpers.typedList, - propertiesTable: ctx.helpers.typedList, -}); + indexSignature(model) { + const params = model.parameters ?? []; + const name = params.length + ? `[${params + .map(param => `${param.name}: ${ctx.partials.someType(param.type)}`) + .join(', ')}]` + : 'index'; + + return ctx.helpers.typedListItem({ + name, + type: model.type, + comment: model.comment, + }); + }, + + memberWithGroups(model, options) { + const md = []; + const stability = ctx.helpers.stabilityBlockquote(model.comment); + + if (stability) md.push(stability); + + if (model.kind === ReflectionKind.TypeAlias) { + md.push(ctx.partials.declarationTitle(model)); + } + + if (model.comment) { + md.push( + ctx.partials.comment(model.comment, { + headingLevel: options.headingLevel, + showTags: false, + }) + ); + } + + if (model.typeHierarchy?.next) { + md.push( + ctx.partials.hierarchy(model.typeHierarchy, { + headingLevel: options.headingLevel, + }) + ); + } + + if (model.typeParameters?.length) { + md.push( + heading( + options.headingLevel, + ReflectionKind.pluralString(ReflectionKind.TypeParameter) + ) + ); + md.push( + ctx.partials.typeParametersList(model.typeParameters, { + headingLevel: options.headingLevel, + }) + ); + } + + if (model.implementedTypes?.length) { + md.push(heading(options.headingLevel, i18n.theme_implements())); + md.push( + model.implementedTypes + .map(type => `* ${ctx.partials.someType(type)}`) + .join('\n') + ); + } + + if (model.kind === ReflectionKind.Class && model.categories?.length) { + model.groups + ?.filter(group => group.title === i18n.kind_plural_constructor()) + .forEach(group => { + md.push( + heading(options.headingLevel, i18n.kind_plural_constructor()) + ); + group.children.forEach(child => { + md.push( + ctx.partials.constructor(child, { + headingLevel: options.headingLevel + 1, + }) + ); + }); + }); + } + + if (model.signatures?.length) { + const multipleSignatures = model.signatures.length > 1; + model.signatures.forEach(signature => { + if (multipleSignatures) { + md.push(heading(options.headingLevel, i18n.kind_call_signature())); + } + md.push( + ctx.partials.signature(signature, { + headingLevel: multipleSignatures + ? options.headingLevel + 1 + : options.headingLevel, + multipleSignatures, + }) + ); + }); + } + + if (model.indexSignatures?.length) { + md.push(heading(options.headingLevel, i18n.theme_indexable())); + model.indexSignatures.forEach(indexSignature => { + md.push( + ctx.partials.indexSignature(indexSignature, { + headingLevel: options.headingLevel + 1, + }) + ); + }); + } + + md.push(ctx.partials.body(model, { headingLevel: options.headingLevel })); + return md.filter(x => typeof x === 'string' || Boolean(x)).join('\n\n'); + }, + + constructor(model, options) { + const md = []; + const heading = '#'.repeat(options.headingLevel); + + model.signatures?.forEach(signature => { + const paramsString = formatParams(signature.parameters ?? []); + + md.push(`${heading} \`new ${model.parent.name}(${paramsString})\``); + md.push( + ctx.partials.signature(signature, { + headingLevel: options.headingLevel + 1, + }) + ); + }); + return md.join('\n\n'); + }, + + typeParametersList: () => '', + + memberTitle: getMemberTitle, + parametersList: ctx.helpers.typedList, + typeDeclarationList: ctx.helpers.typedList, + propertiesTable: ctx.helpers.typedList, + }; +}; diff --git a/plugins/theme/router.mjs b/plugins/theme/router.mjs deleted file mode 100644 index 339ff1d7..00000000 --- a/plugins/theme/router.mjs +++ /dev/null @@ -1,72 +0,0 @@ -import createNodeSlugger from '@node-core/doc-kit/src/generators/metadata/utils/slugger.mjs'; -import { Reflection, ReflectionKind } from 'typedoc'; -import { ModuleRouter } from 'typedoc-plugin-markdown'; -import { getMemberTitle } from './partials/index.mjs'; - -const sluggers = new Map([]); - -export class DocKitRouter extends ModuleRouter { - /** @param {import('typedoc').RouterTarget} pageTarget */ - getSlugger(pageTarget) { - if (sluggers.has(pageTarget)) { - return sluggers.get(pageTarget); - } else { - const slugger = createNodeSlugger(); - sluggers.set(pageTarget, slugger); - return slugger; - } - } - - /** @param {import('typedoc').RouterTarget} target */ - getAnchoredURL(target) { - const anchor = this.getAnchor(target); - const [page] = this.getFullUrl(target).split('#'); - - return anchor ? `${page}#${anchor}` : page; - } - - /** - * @param {import('typedoc').RouterTarget} target - * @param {import('typedoc').RouterTarget} pageTarget - */ - buildAnchors(target, pageTarget) { - if ( - !(target instanceof Reflection) || - !(pageTarget instanceof Reflection) - ) { - return; - } - - if ( - !target.isDeclaration() && - !target.isSignature() && - !target.isTypeParameter() - ) { - return; - } - - if ( - target.kindOf(ReflectionKind.TypeLiteral) && - (!target.parent?.kindOf(ReflectionKind.SomeExport) || - target.parent.type?.type !== 'reflection') - ) { - return; - } - - if (!target.kindOf(ReflectionKind.TypeLiteral)) { - const title = getMemberTitle(target); - const anchor = this.getSlugger(pageTarget).slug(title); - - this.fullUrls.set( - target, - `${this.fullUrls.get(pageTarget).replace(/\.md$/, '.html')}#${anchor}` - ); - this.anchors.set(target, anchor); - } - - target.traverse(child => { - this.buildAnchors(child, pageTarget); - return true; - }); - } -} diff --git a/scripts/markdown.mjs b/scripts/markdown.mjs index 0bcc2064..ed7e6edd 100644 --- a/scripts/markdown.mjs +++ b/scripts/markdown.mjs @@ -9,7 +9,7 @@ const app = await Application.bootstrapWithPlugins({ // Plugins plugin: [ 'typedoc-plugin-markdown', - './plugins/processor.mjs', + './plugins/processor/index.mjs', './plugins/theme/index.mjs', ], theme: 'doc-kit', @@ -19,8 +19,10 @@ const app = await Application.bootstrapWithPlugins({ hideGroupHeadings: true, hideBreadcrumbs: true, hidePageHeader: true, + readme: 'none', disableSources: true, propertiesFormat: 'table', + membersWithOwnFile: ['Class'], modulesFileName: 'index', tsconfig: 'tsconfig.json', From 6618ea8b7522e11641994459ac34e35ffc648a31 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sat, 23 May 2026 23:11:37 -0400 Subject: [PATCH 2/6] fixup! --- components/SideBar.jsx | 22 +++++++++ doc-kit.config.mjs | 11 +++++ plugins/processor/index.mjs | 12 +++++ plugins/processor/site.mjs | 91 +++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 components/SideBar.jsx create mode 100644 plugins/processor/site.mjs diff --git a/components/SideBar.jsx b/components/SideBar.jsx new file mode 100644 index 00000000..ad858ac4 --- /dev/null +++ b/components/SideBar.jsx @@ -0,0 +1,22 @@ +import SideBar from '@node-core/ui-components/Containers/Sidebar'; +import { sidebar } from '#theme/site' with { type: 'json' }; + +/** @param {string} url */ +const redirect = url => (window.location.href = url); + +const PrefetchLink = props => ; + +const pathnameFor = path => path.replace(/\/index$/, '') || '/'; + +/** + * Sidebar component for MDX documentation with page navigation. + */ +export default ({ metadata }) => ( + +); diff --git a/doc-kit.config.mjs b/doc-kit.config.mjs index 6e3a2e35..4614d2e6 100644 --- a/doc-kit.config.mjs +++ b/doc-kit.config.mjs @@ -1,3 +1,10 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = dirname(fileURLToPath(import.meta.url)); + +// TODO(@avivkeller): v5.x should not be hardcoded + /** * Configuration for @node-core/doc-kit when generating webpack API docs. * @@ -25,5 +32,9 @@ export default { project: 'webpack', useAbsoluteURLs: true, remoteConfigUrl: null, + imports: { + '#theme/Sidebar': join(ROOT, 'components/SideBar.jsx'), + '#theme/site': join(ROOT, 'pages/v5.x/site.json'), + }, }, }; diff --git a/plugins/processor/index.mjs b/plugins/processor/index.mjs index ce59d62f..2eb1c399 100644 --- a/plugins/processor/index.mjs +++ b/plugins/processor/index.mjs @@ -3,6 +3,7 @@ import { writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { applySourceMetadata } from './metadata.mjs'; import { DocKitRouter } from './router.mjs'; +import { sidebar } from './site.mjs'; const typeMapKey = target => { const name = target.getFullName(); @@ -74,5 +75,16 @@ export function load(app) { join(app.options.getValue('out'), 'type-map.json'), JSON.stringify(typeMap, null, 2) ); + + writeFileSync( + join(app.options.getValue('out'), 'site.json'), + JSON.stringify( + { + sidebar: sidebar(app.renderer.router), + }, + null, + 2 + ) + ); }); } diff --git a/plugins/processor/site.mjs b/plugins/processor/site.mjs new file mode 100644 index 00000000..3f1dfcde --- /dev/null +++ b/plugins/processor/site.mjs @@ -0,0 +1,91 @@ +import { ReflectionKind } from 'typedoc'; +import { fullName } from '../shared/titles.mjs'; + +const ROOT_GROUP = 'webpack'; + +const toOutputPath = url => { + const withoutExtension = url.replace(/\.md$/, ''); + if (withoutExtension === 'README') return ''; + return withoutExtension.replace(/\/index$/, ''); +}; + +const toSidebarLink = url => { + const path = toOutputPath(url); + return path ? `/${path}` : '/'; +}; + +const pagePathParts = url => + url.replace(/\.md$/, '').split('/').filter(Boolean); + +const groupKeyFor = url => { + const parts = pagePathParts(url); + return parts.length > 1 ? parts[0] : ROOT_GROUP; +}; + +const rawPageName = url => { + if (url === 'README.md') return ROOT_GROUP; + return ( + pagePathParts(url) + .at(-1) + ?.replace(/\/index$/, '') ?? ROOT_GROUP + ); +}; + +const stripWebpackPrefix = value => value.replace(/^webpack\.?/, ''); + +const trimGroupPrefix = (value, groupKey) => { + if (value === groupKey) return rawPageName(`${groupKey}/index.md`); + return value.startsWith(`${groupKey}.`) + ? value.slice(groupKey.length + 1) + : value; +}; + +const itemLabelFor = (target, url, groupKey) => { + const name = stripWebpackPrefix(fullName(target)); + const label = trimGroupPrefix(name, groupKey); + return label || rawPageName(url); +}; + +const isSidebarTarget = (router, target) => { + if ( + !target.kindOf?.( + ReflectionKind.Project | ReflectionKind.Namespace | ReflectionKind.Class + ) + ) { + return false; + } + + if (!router.hasOwnDocument(target)) return false; + + const url = router.getFullUrl(target); + return url.endsWith('.md') && !url.includes('#'); +}; + +export const sidebar = router => { + const groups = new Map(); + + for (const target of router.getLinkTargets()) { + if (!isSidebarTarget(router, target)) continue; + + const url = router.getFullUrl(target); + const groupKey = groupKeyFor(url); + const group = groups.get(groupKey) ?? []; + const link = toSidebarLink(url); + + if (!group.some(item => item.link === link)) { + group.push({ + link, + label: itemLabelFor(target, url, groupKey), + }); + } + + groups.set(groupKey, group); + } + + return [...groups] + .map(([groupKey, items]) => ({ + groupName: groupKey, + items, + })) + .filter(group => group.items.length); +}; From 7825947d064ede7bc5d27e5f9cd3371f0fbe4a7b Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Sun, 24 May 2026 18:13:28 -0400 Subject: [PATCH 3/6] fixup! --- doc-kit.config.mjs | 15 +- package-lock.json | 561 ++++++++++++--------- plugins/processor/exportEquals.mjs | 46 ++ plugins/processor/index.mjs | 36 +- plugins/processor/metadata.mjs | 773 +++-------------------------- plugins/processor/router.mjs | 168 ++----- plugins/processor/site.mjs | 113 ++--- plugins/processor/source.mjs | 735 +++++++++++++++++++++++++++ plugins/processor/synthetic.mjs | 117 +++++ plugins/processor/typeMap.mjs | 31 ++ plugins/shared/categories.mjs | 27 +- plugins/shared/titles.mjs | 50 +- plugins/theme/partials/index.mjs | 24 +- scripts/markdown.mjs | 1 + 14 files changed, 1491 insertions(+), 1206 deletions(-) create mode 100644 plugins/processor/exportEquals.mjs create mode 100644 plugins/processor/source.mjs create mode 100644 plugins/processor/synthetic.mjs create mode 100644 plugins/processor/typeMap.mjs diff --git a/doc-kit.config.mjs b/doc-kit.config.mjs index 4614d2e6..de1e52d1 100644 --- a/doc-kit.config.mjs +++ b/doc-kit.config.mjs @@ -1,9 +1,10 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { major } from 'semver'; +import webpack from './webpack/package.json' with { type: 'json' }; const ROOT = dirname(fileURLToPath(import.meta.url)); - -// TODO(@avivkeller): v5.x should not be hardcoded +const DOCS_DIR = `pages/v${major(webpack.version)}.x`; /** * Configuration for @node-core/doc-kit when generating webpack API docs. @@ -16,7 +17,7 @@ export default { repository: 'webpack/webpack', // Input & Output - input: ['./pages/v5.x/**/*.md'], + input: [`./${DOCS_DIR}/**/*.md`], output: 'out', // Base URL, @@ -26,7 +27,11 @@ export default { }, threads: 1, metadata: { - typeMap: './pages/v5.x/type-map.json', + typeMap: `./${DOCS_DIR}/type-map.json`, + }, + 'jsx-ast': { + generateIndexPage: false, + generateAllPage: false, }, web: { project: 'webpack', @@ -34,7 +39,7 @@ export default { remoteConfigUrl: null, imports: { '#theme/Sidebar': join(ROOT, 'components/SideBar.jsx'), - '#theme/site': join(ROOT, 'pages/v5.x/site.json'), + '#theme/site': join(ROOT, DOCS_DIR, 'site.json'), }, }, }; diff --git a/package-lock.json b/package-lock.json index 73b17a06..3e6cc8ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "webpack-doc-kit", "dependencies": { "@node-core/doc-kit": "^1.3.6", "semver": "^7.8.1", @@ -22,9 +21,9 @@ } }, "node_modules/@actions/core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", - "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz", + "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==", "license": "MIT", "dependencies": { "@actions/exec": "^3.0.0", @@ -41,9 +40,9 @@ } }, "node_modules/@actions/http-client": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", - "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.1.tgz", + "integrity": "sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==", "license": "MIT", "dependencies": { "tunnel": "^0.0.6", @@ -69,20 +68,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, "dependencies": { @@ -90,9 +89,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "license": "MIT", "optional": true, "dependencies": { @@ -306,29 +305,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -724,19 +737,21 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@noble/hashes": { @@ -744,6 +759,7 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -901,13 +917,26 @@ "@types/hast": "^3.0.4" } }, + "node_modules/@node-core/rehype-shiki/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@node-core/ui-components": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@node-core/ui-components/-/ui-components-1.6.3.tgz", - "integrity": "sha512-Oy/bI4mZC6V/i8CdIX8q62r81zR6bAEARZE0jwTpv7w49SNomB2WAVb5fnFBSumyigOCgQKgR/OJtwh8XQJnkw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-core/ui-components/-/ui-components-1.7.0.tgz", + "integrity": "sha512-vZY+P4oEuA8w1sTh6hbMxf4OcujOnb+VZ8U33mcEYHB3fAtluh/qU9IeNEVk2ajWz0PrjI42uqrifyh3ysR7mw==", "dependencies": { "@heroicons/react": "^2.2.0", - "@orama/core": "^1.2.19", + "@orama/orama": "^3.1.18", "@orama/ui": "^1.5.4", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", @@ -917,7 +946,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@tailwindcss/postcss": "~4.2.1", + "@tailwindcss/postcss": "~4.2.2", "@types/react": "^19.2.13", "@vcarl/remark-headings": "~0.1.0", "classnames": "~2.5.1", @@ -931,11 +960,25 @@ "node": ">=20" } }, + "node_modules/@node-core/ui-components/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@orama/core": { "version": "1.2.19", "resolved": "https://registry.npmjs.org/@orama/core/-/core-1.2.19.tgz", "integrity": "sha512-AVEI0eG/a1RUQK+tBloRMppQf46Ky4kIYKEVjo0V0VfIGZHdLOE2PJR4v949kFwiTnfSJCUaxgwM74FCA1uHUA==", "license": "AGPL-3.0", + "peer": true, "dependencies": { "@orama/cuid2": "2.2.3", "@orama/oramacore-events-parser": "0.0.5" @@ -946,6 +989,7 @@ "resolved": "https://registry.npmjs.org/@orama/cuid2/-/cuid2-2.2.3.tgz", "integrity": "sha512-Lcak3chblMejdlSHgYU2lS2cdOhDpU6vkfIJH4m+YKvqQyLqs1bB8+w6NT1MG5bO12NUK2GFc34Mn2xshMIQ1g==", "license": "MIT", + "peer": true, "dependencies": { "@noble/hashes": "^1.1.5" } @@ -963,7 +1007,8 @@ "version": "0.0.5", "resolved": "https://registry.npmjs.org/@orama/oramacore-events-parser/-/oramacore-events-parser-0.0.5.tgz", "integrity": "sha512-yAuSwog+HQBAXgZ60TNKEwu04y81/09mpbYBCmz1RCxnr4ObNY2JnPZI7HmALbjAhLJ8t5p+wc2JHRK93ubO4w==", - "license": "AGPL-3.0" + "license": "AGPL-3.0", + "peer": true }, "node_modules/@orama/stopwords": { "version": "3.1.18", @@ -2560,6 +2605,18 @@ "hast-util-to-html": "^9.0.5" } }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, "node_modules/@shikijs/engine-oniguruma": { "version": "3.23.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", @@ -2571,21 +2628,21 @@ } }, "node_modules/@shikijs/langs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", - "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.1.0.tgz", + "integrity": "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.1.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/langs/node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.1.0.tgz", + "integrity": "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -2596,12 +2653,12 @@ } }, "node_modules/@shikijs/primitive": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", - "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.1.0.tgz", + "integrity": "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, @@ -2610,9 +2667,9 @@ } }, "node_modules/@shikijs/primitive/node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.1.0.tgz", + "integrity": "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -2623,21 +2680,21 @@ } }, "node_modules/@shikijs/themes": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", - "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.1.0.tgz", + "integrity": "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.1.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/themes/node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.1.0.tgz", + "integrity": "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -2678,15 +2735,15 @@ "license": "MIT" }, "node_modules/@swc/html-wasm": { - "version": "1.15.21", - "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.15.21.tgz", - "integrity": "sha512-2Wo2S5DbfAswldF/X5gSmqgTy4uMBEUix7zYcIFsXgZyVJ8PDPE19cgxSBkauJxxGXfvo5rVCcEjx0XrJHpDEA==", + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/html-wasm/-/html-wasm-1.15.40.tgz", + "integrity": "sha512-WbRp7vSPK9bAGwpP1U8+RDE/xw1yj6P8ZYVqzA/64nlnFuB4MrW92EdkHiKvTPdlFDUZUno8yJKRKcRkE/UK6Q==", "license": "Apache-2.0" }, "node_modules/@tailwindcss/node": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -2695,42 +2752,42 @@ "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.2" + "tailwindcss": "4.2.4" } }, "node_modules/@tailwindcss/node/node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", "license": "MIT" }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-x64": "4.2.2", - "@tailwindcss/oxide-freebsd-x64": "4.2.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-x64-musl": "4.2.2", - "@tailwindcss/oxide-wasm32-wasi": "4.2.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", "cpu": [ "arm64" ], @@ -2744,9 +2801,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", "cpu": [ "arm64" ], @@ -2760,9 +2817,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", "cpu": [ "x64" ], @@ -2776,9 +2833,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", "cpu": [ "x64" ], @@ -2792,9 +2849,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", "cpu": [ "arm" ], @@ -2808,9 +2865,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", "cpu": [ "arm64" ], @@ -2824,9 +2881,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", "cpu": [ "arm64" ], @@ -2840,9 +2897,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", "cpu": [ "x64" ], @@ -2856,9 +2913,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", "cpu": [ "x64" ], @@ -2872,9 +2929,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2901,9 +2958,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", "cpu": [ "arm64" ], @@ -2917,9 +2974,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", "cpu": [ "x64" ], @@ -2933,28 +2990,28 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", - "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.4.tgz", + "integrity": "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", "postcss": "^8.5.6", - "tailwindcss": "4.2.2" + "tailwindcss": "4.2.4" } }, "node_modules/@tailwindcss/postcss/node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "license": "MIT", "optional": true, "dependencies": { @@ -2978,9 +3035,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -3032,9 +3089,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3059,9 +3116,9 @@ } }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", "license": "ISC" }, "node_modules/@vcarl/remark-headings": { @@ -3314,9 +3371,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3483,9 +3540,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.10", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", - "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3537,9 +3594,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -3556,11 +3613,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3576,9 +3633,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001781", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", - "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", "funding": [ { "type": "opencollective", @@ -4003,9 +4060,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.321", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", - "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -4453,9 +4510,9 @@ "license": "ISC" }, "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -4959,9 +5016,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4989,9 +5046,9 @@ "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -5310,9 +5367,19 @@ } }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -5495,14 +5562,24 @@ } }, "node_modules/markdown-it": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -6512,10 +6589,13 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -6555,18 +6635,18 @@ } }, "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", "license": "MIT" }, "node_modules/oniguruma-to-es": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", - "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "license": "MIT", "dependencies": { - "oniguruma-parser": "^0.12.1", + "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } @@ -6966,25 +7046,25 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.6" } }, "node_modules/react-markdown": { @@ -7480,17 +7560,17 @@ } }, "node_modules/shiki": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", - "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.1.0.tgz", + "integrity": "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.0.2", - "@shikijs/engine-javascript": "4.0.2", - "@shikijs/engine-oniguruma": "4.0.2", - "@shikijs/langs": "4.0.2", - "@shikijs/themes": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/core": "4.1.0", + "@shikijs/engine-javascript": "4.1.0", + "@shikijs/engine-oniguruma": "4.1.0", + "@shikijs/langs": "4.1.0", + "@shikijs/themes": "4.1.0", + "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" }, @@ -7499,13 +7579,13 @@ } }, "node_modules/shiki/node_modules/@shikijs/core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", - "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.1.0.tgz", + "integrity": "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==", "license": "MIT", "dependencies": { - "@shikijs/primitive": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/primitive": "4.1.0", + "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" @@ -7515,26 +7595,26 @@ } }, "node_modules/shiki/node_modules/@shikijs/engine-javascript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", - "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.1.0.tgz", + "integrity": "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" + "oniguruma-to-es": "^4.3.6" }, "engines": { "node": ">=20" } }, "node_modules/shiki/node_modules/@shikijs/engine-oniguruma": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", - "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.1.0.tgz", + "integrity": "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -7542,9 +7622,9 @@ } }, "node_modules/shiki/node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.1.0.tgz", + "integrity": "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -7837,9 +7917,9 @@ "license": "MIT" }, "node_modules/thenby": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", - "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.4.1.tgz", + "integrity": "sha512-D5a/bO0KdalOE3q8MlrRmSxjbKZHT3MQmXkJP+r97Vw8MMwOZKOwUSEyTtK7eSMj2y0kyAjpYMRMZmmLw1FtNQ==", "license": "Apache-2.0" }, "node_modules/throttleit": { @@ -7865,13 +7945,13 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -7928,22 +8008,22 @@ } }, "node_modules/twoslash": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.3.6.tgz", - "integrity": "sha512-VuI5OKl+MaUO9UIW3rXKoPgHI3X40ZgB/j12VY6h98Ae1mCBihjPvhOPeJWlxCYcmSbmeZt5ZKkK0dsVtp+6pA==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.3.8.tgz", + "integrity": "sha512-OeDz0kDl8sqPUN3nr7gqcvOs70f5lZsdhKYTX3/SgB9OvdadzzoYJI/4SBXhXV1HG8E9fLc+e17itoRYTxmoig==", "license": "MIT", "dependencies": { - "@typescript/vfs": "^1.6.2", - "twoslash-protocol": "0.3.6" + "@typescript/vfs": "^1.6.4", + "twoslash-protocol": "0.3.8" }, "peerDependencies": { - "typescript": "^5.5.0" + "typescript": "^5.5.0 || ^6.0.0" } }, "node_modules/twoslash-protocol": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/twoslash-protocol/-/twoslash-protocol-0.3.6.tgz", - "integrity": "sha512-FHGsJ9Q+EsNr5bEbgG3hnbkvEBdW5STgPU824AHUjB4kw0Dn4p8tABT7Ncg1Ie6V0+mDg3Qpy41VafZXcQhWMA==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/twoslash-protocol/-/twoslash-protocol-0.3.8.tgz", + "integrity": "sha512-HmvAHoiEviK8LqvAQyc9/irkdvwTUiR1fHmNwH/0gq8EHxyBt4PWVPixjEXg6wJu1u6yBrILEWXGK9Kw58/8yQ==", "license": "MIT" }, "node_modules/type-check": { @@ -7995,10 +8075,11 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8014,9 +8095,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", "license": "MIT", "engines": { "node": ">=18.17" diff --git a/plugins/processor/exportEquals.mjs b/plugins/processor/exportEquals.mjs new file mode 100644 index 00000000..bf19537e --- /dev/null +++ b/plugins/processor/exportEquals.mjs @@ -0,0 +1,46 @@ +import { ReflectionKind } from 'typedoc'; +import { setPublicName } from './metadata.mjs'; + +const EXPORT_EQUALS_NAME = 'export='; + +/** + * @param {import('typedoc').ProjectReflection} project + * @param {import('typedoc').Reflection} reflection + * @returns {string | undefined} + */ +const exportEqualsPublicName = (project, reflection) => { + const rawParts = String(reflection.getFullName?.() ?? reflection.name).split( + '.' + ); + + if (!rawParts.includes(EXPORT_EQUALS_NAME)) { + return; + } + + const parts = rawParts.filter(part => part && part !== EXPORT_EQUALS_NAME); + return parts[0] === project.name + ? parts.join('.') + : [project.name, ...parts].filter(Boolean).join('.'); +}; + +/** + * TypeDoc exposes CommonJS `export = webpack` containers and callables as + * `export=`. Normalize every such reflection while the processor still owns + * the reflection graph: containers are merged into their parent and remaining + * declarations get public webpack names. + * + * @param {import('typedoc').ProjectReflection} project + */ +export const applyExportEqualsReflections = project => { + project + .getReflectionsByKind(ReflectionKind.SomeModule) + .filter(ref => ref.name === EXPORT_EQUALS_NAME && ref.parent) + .forEach(namespace => + project.mergeReflections(namespace, namespace.parent) + ); + + for (const reflection of project.getReflectionsByKind(ReflectionKind.All)) { + const name = exportEqualsPublicName(project, reflection); + if (name) setPublicName(reflection, name || project.name); + } +}; diff --git a/plugins/processor/index.mjs b/plugins/processor/index.mjs index 2eb1c399..eacf6b11 100644 --- a/plugins/processor/index.mjs +++ b/plugins/processor/index.mjs @@ -1,22 +1,11 @@ import { Converter, ReflectionKind, Renderer } from 'typedoc'; import { writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { applySourceMetadata } from './metadata.mjs'; +import { applyExportEqualsReflections } from './exportEquals.mjs'; +import { applySourceMetadata } from './source.mjs'; import { DocKitRouter } from './router.mjs'; import { sidebar } from './site.mjs'; - -const typeMapKey = target => { - const name = target.getFullName(); - let root = target; - - while (root.parent) root = root.parent; - - if (!root.name || name === root.name || name.startsWith(`${root.name}.`)) { - return name; - } - - return `${root.name}.${name}`; -}; +import { createTypeMap } from './typeMap.mjs'; /** * @param {import('typedoc-plugin-markdown').MarkdownApplication} app @@ -47,29 +36,14 @@ export function load(app) { .getReflectionsByKind(ReflectionKind.Reference) .forEach(ref => context.project.removeReflection(ref)); - // types.d.ts models CommonJS `export = webpack` as a nested namespace. - // Collapse it so public names are emitted as webpack.*. - context.project - .getReflectionsByKind(ReflectionKind.Namespace) - .filter(ref => ref.name === 'export=') - .forEach(namespace => - context.project.mergeReflections(namespace, namespace.parent) - ); - + applyExportEqualsReflections(context.project); applySourceMetadata(context.project); }); app.renderer.on(Renderer.EVENT_END, () => { // doc-kit resolves custom type annotations from this map while generating // HTML, so use the final router URLs instead of recomputing paths here. - const typeMap = Object.fromEntries( - app.renderer.router - .getLinkTargets() - .map(target => [ - typeMapKey(target), - app.renderer.router.getAnchoredURL(target), - ]) - ); + const typeMap = createTypeMap(app.renderer.router); writeFileSync( join(app.options.getValue('out'), 'type-map.json'), diff --git a/plugins/processor/metadata.mjs b/plugins/processor/metadata.mjs index a577fe2a..63b4b8c2 100644 --- a/plugins/processor/metadata.mjs +++ b/plugins/processor/metadata.mjs @@ -1,728 +1,73 @@ -import ts from 'typescript'; -import { Comment, CommentTag, ReflectionKind, ReflectionType } from 'typedoc'; -import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; -import { basename, dirname, extname, join, relative, resolve } from 'node:path'; -import { SOURCE_METADATA } from './router.mjs'; - /** - * Source metadata is intentionally a documentation-only overlay: - * 1. types.d.ts decides which symbols exist and what their signatures are. - * 2. webpack/lib/index.js maps those public symbols back to implementation files. - * 3. all JS files under webpack/lib contribute summaries, params, returns, - * and stability/deprecation tags when that source comment has real content. + * @typedef {object} SourceMetadata + * @property {string} sourcePath Absolute source file path. + * @property {string} sourceRelativePath Path relative to webpack/lib. + * @property {string} fileBase Source file name without extension. + * @property {string | undefined} anchorName Preferred page anchor for members + * whose public name differs from the source file name. + * @property {string | undefined} publicPath Public webpack export path. */ -const DOC_BLOCK_TAGS = new Set([ - '@deprecated', - '@example', - '@legacy', - '@remarks', - '@see', - '@since', - '@throws', -]); - -const MODIFIER_TAGS = new Set([ - '@alpha', - '@beta', - '@experimental', - '@internal', -]); - -const textPart = text => [{ kind: 'text', text }]; - -const normalizeText = value => - String(value ?? '') - .replace(/\r\n?/g, '\n') - .trim(); - -const jsDocText = value => { - if (!value) return ''; - if (Array.isArray(value)) { - return normalizeText( - value.map(part => part.text ?? part.getText?.() ?? '').join('') - ); - } - return normalizeText(value); -}; - -const hasCommentContent = comment => - Boolean( - comment?.summary || - comment?.returns || - comment?.params?.size || - comment?.blockTags?.length || - comment?.modifierTags?.size - ); - -const hasRenderableCommentContent = comment => - Boolean( - comment?.summary || - comment?.returns || - [...(comment?.params?.values() ?? [])].some(Boolean) || - comment?.blockTags?.some(tag => tag.content) || - comment?.modifierTags?.size - ); - -const mergeComments = comments => { - const merged = { - summary: '', - params: new Map(), - returns: '', - blockTags: [], - modifierTags: new Set(), - }; - - for (const comment of comments.filter(Boolean)) { - if (comment.summary) merged.summary = comment.summary; - if (comment.returns) merged.returns = comment.returns; - for (const [name, value] of comment.params) { - if (value) merged.params.set(name, value); - } - for (const tag of comment.blockTags) { - if (tag.content) merged.blockTags.push(tag); - } - for (const tag of comment.modifierTags) { - merged.modifierTags.add(tag); - } - } - - return hasCommentContent(merged) ? merged : undefined; -}; - -// TypeScript exposes JSDoc differently depending on whether a comment belongs -// to a declaration, assignment, or export wrapper. Walk up a tiny amount so -// comments attached to CommonJS assignment statements are still discovered. -const getJsDocs = node => { - let current = node; - - while (current && !ts.isSourceFile(current)) { - if (current.jsDoc?.length) return current.jsDoc; - if (ts.isBlock(current) || ts.isClassDeclaration(current)) break; - current = current.parent; - } - - return []; -}; - -const getJsDocComment = node => { - const jsDocs = getJsDocs(node); - const comments = jsDocs.map(jsDoc => { - const comment = { - summary: jsDocText(jsDoc.comment), - params: new Map(), - returns: '', - blockTags: [], - modifierTags: new Set(), - }; - - for (const tag of jsDoc.tags ?? []) { - const tagName = `@${tag.tagName?.escapedText ?? ''}`; - const content = jsDocText(tag.comment); - - if (ts.isJSDocParameterTag(tag) && tag.name) { - comment.params.set(tag.name.getText(), content); - continue; - } - - if (ts.isJSDocReturnTag(tag)) { - comment.returns = content; - continue; - } - - if (MODIFIER_TAGS.has(tagName)) { - comment.modifierTags.add(tagName); - continue; - } - - if (DOC_BLOCK_TAGS.has(tagName)) { - comment.blockTags.push({ tag: tagName, content }); - } - } - - return hasCommentContent(comment) ? comment : undefined; - }); - - return mergeComments(comments); -}; - -const toTypedocComment = sourceComment => { - if (!sourceComment || !hasRenderableCommentContent(sourceComment)) { - return undefined; - } - - const blockTags = []; - if (sourceComment.returns) { - blockTags.push(new CommentTag('@returns', textPart(sourceComment.returns))); - } - - for (const { tag, content } of sourceComment.blockTags ?? []) { - blockTags.push(new CommentTag(tag, textPart(content))); - } - - return new Comment( - sourceComment.summary ? textPart(sourceComment.summary) : [], - blockTags, - new Set(sourceComment.modifierTags ?? []) - ); -}; - -const toParameterComment = text => { - const content = normalizeText(text); - return content ? new Comment(textPart(content)) : undefined; -}; - -const assignComment = (target, sourceComment) => { - const typedocComment = toTypedocComment(sourceComment); - if (typedocComment) target.comment = typedocComment; -}; - -const applySignatureComment = (signature, sourceComment) => { - if (!signature || !hasRenderableCommentContent(sourceComment)) return; - - assignComment(signature, sourceComment); - - const params = [...sourceComment.params.values()].filter(Boolean); - - for (const parameter of signature.parameters ?? []) { - const comment = - sourceComment.params.get(parameter.name) ?? - (parameter.name.startsWith('__') && params.length === 1 - ? params[0] - : undefined); - const parameterComment = toParameterComment(comment); - if (parameterComment) parameter.comment = parameterComment; - } -}; - -const propertyNameText = name => { - if (!name) return undefined; - if ( - ts.isIdentifier(name) || - ts.isStringLiteral(name) || - ts.isNumericLiteral(name) - ) { - return name.text; - } - return undefined; -}; - -const getAssignedProperty = expression => { - if (!ts.isPropertyAccessExpression(expression)) return undefined; - - const property = expression.name.text; - const owner = expression.expression; - - if ( - ts.isPropertyAccessExpression(owner) && - owner.expression.getText() === 'module' && - owner.name.text === 'exports' - ) { - return property; - } - - if (owner.getText() === 'exports') return property; - - return undefined; -}; - -const isModuleExports = expression => - ts.isPropertyAccessExpression(expression) && - expression.expression.getText() === 'module' && - expression.name.text === 'exports'; - -const expressionLocalName = expression => - ts.isIdentifier(expression) ? expression.text : undefined; - -const resolveSourceRequest = (fromFile, request) => { - if (!request.startsWith('.')) return undefined; - - const base = resolve(dirname(fromFile), request); - const candidates = [base, `${base}.js`, join(base, 'index.js')]; - return candidates.find(candidate => existsSync(candidate)); -}; - -const firstRequire = expression => { - let found; - - const visit = node => { - if (found) return; - - if ( - ts.isCallExpression(node) && - node.expression.getText() === 'require' && - ts.isStringLiteral(node.arguments[0]) - ) { - found = { - request: node.arguments[0].text, - property: - node.parent && ts.isPropertyAccessExpression(node.parent) - ? node.parent.name.text - : undefined, - }; - return; - } - - ts.forEachChild(node, visit); - }; - - visit(expression); - return found; -}; - -const listJsFiles = directory => { - const entries = readdirSync(directory, { withFileTypes: true }); - const files = []; - - for (const entry of entries) { - const path = join(directory, entry.name); - if (entry.isDirectory()) { - files.push(...listJsFiles(path)); - } else if (entry.isFile() && extname(entry.name) === '.js') { - files.push(path); - } - } - - return files; -}; - -const sourceFileFor = filePath => - ts.createSourceFile( - filePath, - readFileSync(filePath, 'utf8'), - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.JS - ); - -const sourceFileBase = filePath => basename(filePath, extname(filePath)); - -const createFileMetadata = (filePath, libDir) => { - const sourceFile = sourceFileFor(filePath); - const symbols = new Map(); - const classes = new Map(); - const namedExports = new Map(); - let defaultExport; - - const rememberSymbol = (name, data) => { - if (!name) return; - const existing = symbols.get(name) ?? {}; - symbols.set(name, { - ...existing, - ...data, - comment: data.comment ?? existing.comment, - members: data.members ?? existing.members, - }); - }; - - // Keep per-class member comments separate from file-level exports. A public - // class comes from types.d.ts, but method/constructor prose usually lives in - // the implementation file next to the code. - const classMembers = node => { - const members = new Map(); - - for (const member of node.members ?? []) { - if (ts.isConstructorDeclaration(member)) { - const comment = getJsDocComment(member); - if (comment) members.set('constructor', comment); - continue; - } - - const name = propertyNameText(member.name); - if (!name) continue; - - const comment = getJsDocComment(member); - if (comment) members.set(name, comment); - } - - return members; - }; - - const visit = node => { - if (ts.isClassDeclaration(node) && node.name) { - const data = { - kind: 'class', - localName: node.name.text, - comment: getJsDocComment(node), - members: classMembers(node), - }; - symbols.set(node.name.text, data); - classes.set(node.name.text, data); - } - - if (ts.isFunctionDeclaration(node) && node.name) { - rememberSymbol(node.name.text, { - kind: 'function', - localName: node.name.text, - comment: getJsDocComment(node), - }); - } - - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { - const comment = getJsDocComment(node); - if (comment) { - rememberSymbol(node.name.text, { - kind: 'variable', - localName: node.name.text, - comment, - }); - } - } - if ( - ts.isExpressionStatement(node) && - ts.isBinaryExpression(node.expression) - ) { - const { left, right } = node.expression; - const comment = getJsDocComment(node) ?? getJsDocComment(right); - - if (isModuleExports(left)) { - defaultExport = { - localName: expressionLocalName(right), - comment, - }; - } else { - const property = getAssignedProperty(left); - if (property) { - namedExports.set(property, { - localName: expressionLocalName(right), - comment, - }); - } - } - - if ( - ts.isPropertyAccessExpression(left) && - ts.isIdentifier(left.expression) && - classes.has(left.expression.text) && - comment - ) { - classes.get(left.expression.text).members.set(left.name.text, comment); - } - } - - ts.forEachChild(node, visit); - }; - - visit(sourceFile); - - return { - filePath, - fileBase: sourceFileBase(filePath), - relativePath: relative(libDir, filePath).replace(/\\/g, '/'), - symbols, - classes, - namedExports, - defaultExport, - }; -}; - -const getExportObject = sourceFile => { - let exportObject; - - const visit = node => { - if (exportObject) return; - - if ( - ts.isExpressionStatement(node) && - ts.isBinaryExpression(node.expression) && - isModuleExports(node.expression.left) && - ts.isCallExpression(node.expression.right) - ) { - const objectArg = [...node.expression.right.arguments].find( - ts.isObjectLiteralExpression - ); - if (objectArg) exportObject = objectArg; - } - - ts.forEachChild(node, visit); - }; - - visit(sourceFile); - return exportObject; -}; - -const buildExportMap = libDir => { - const indexPath = join(libDir, 'index.js'); - const sourceFile = sourceFileFor(indexPath); - const exportMap = new Map(); - const exportObject = getExportObject(sourceFile); - - const addExport = (parts, data) => { - exportMap.set(parts.join('.'), { - publicPath: parts.join('.'), - publicName: parts.at(-1), - ...data, - }); - }; - - // webpack's callable export is assigned outside lib/index.js, so seed the - // root namespace before walking the object tree of re-exported helpers. - addExport(['webpack'], { - sourcePath: join(libDir, 'webpack.js'), - localName: 'webpack', - }); - - const walkObject = (object, parts = []) => { - for (const property of object.properties) { - const name = propertyNameText(property.name); - if (!name) continue; - - if ( - ts.isPropertyAssignment(property) && - ts.isObjectLiteralExpression(property.initializer) - ) { - walkObject(property.initializer, [...parts, name]); - continue; - } - - const expression = ts.isGetAccessor(property) - ? property.body?.statements.filter(ts.isReturnStatement).at(-1) - ?.expression - : ts.isPropertyAssignment(property) - ? property.initializer - : undefined; - - if (!expression) continue; - - const requireCall = firstRequire(expression); - const sourcePath = requireCall?.request - ? resolveSourceRequest(indexPath, requireCall.request) - : undefined; - - addExport([...parts, name], { - sourcePath, - localName: requireCall?.property, - comment: getJsDocComment(property), - }); - } - }; - - if (exportObject) walkObject(exportObject); - return exportMap; -}; - -const buildSourceIndex = libDir => { - const sourceFiles = new Map(); - for (const file of listJsFiles(libDir)) { - sourceFiles.set(file, createFileMetadata(file, libDir)); - } - return sourceFiles; -}; - -const allReflections = project => Object.values(project.reflections); - -const publicName = reflection => reflection.getFullName?.(); - -const exportEntryFor = (name, exportMap) => { - const exact = exportMap.get(name); - if (exact) return exact; - - const parts = name.split('.'); - for (let i = parts.length - 1; i > 0; i--) { - const parent = exportMap.get(parts.slice(0, i).join('.')); - if (!parent?.sourcePath) continue; - - const memberPath = parts.slice(i); - // If lib/index.js exports an object, types.d.ts may expose its properties as - // public members. Reuse the parent source file and match the leaf locally. - return { - ...parent, - publicPath: name, - publicName: memberPath.at(-1), - localName: memberPath.at(-1), - parentPublicPath: parent.publicPath, - memberPath, - }; - } -}; - -const sourceEntryFor = (entry, sourceFiles) => { - const file = entry.sourcePath ? sourceFiles.get(entry.sourcePath) : undefined; - if (!file) return undefined; - - const exported = entry.publicName - ? file.namedExports.get(entry.publicName) - : undefined; - const localName = - entry.localName ?? - exported?.localName ?? - file.defaultExport?.localName ?? - entry.publicName; - const symbol = - file.symbols.get(localName) ?? file.symbols.get(entry.publicName); - - return { - file, - localName, - comment: - symbol?.comment ?? - entry.comment ?? - exported?.comment ?? - file.defaultExport?.comment, - symbol, - }; -}; - -const commentForSourceEntry = sourceEntry => - sourceEntry?.comment ?? sourceEntry?.symbol?.comment; - -const memberSourceComment = (sourceEntry, memberName) => - sourceEntry?.symbol?.members?.get(memberName) ?? - sourceEntry?.file.namedExports.get(memberName)?.comment ?? - sourceEntry?.file.symbols.get(memberName)?.comment; - -const namedExportComment = (sourceEntry, memberName) => { - const exported = sourceEntry?.file.namedExports.get(memberName); - if (!exported) return undefined; - return ( - exported.comment ?? - sourceEntry.file.symbols.get(exported.localName)?.comment ?? - sourceEntry.file.symbols.get(memberName)?.comment - ); -}; - -const ownSignatures = reflection => [ - ...(reflection.signatures ?? []), - ...(reflection.type instanceof ReflectionType - ? (reflection.type.declaration.signatures ?? []) - : []), -]; - -const applyFunctionLikeComment = (reflection, sourceComment) => { - if (!hasRenderableCommentContent(sourceComment)) return; - - const signatures = ownSignatures(reflection); - if (signatures.length) { - signatures.forEach(signature => - applySignatureComment(signature, sourceComment) - ); - } else { - assignComment(reflection, sourceComment); - } -}; - -// Exported object literals (for example helper namespaces) often document their -// individual properties as named exports in the implementation file. -const applyObjectMembers = (reflection, sourceEntry) => { - const declaration = - reflection.type instanceof ReflectionType - ? reflection.type.declaration - : undefined; - - for (const child of declaration?.children ?? []) { - const sourceComment = - namedExportComment(sourceEntry, child.name) ?? - sourceEntry?.file.symbols.get(child.name)?.comment; - - if (!hasRenderableCommentContent(sourceComment)) continue; - - const signatures = ownSignatures(child); - if (signatures.length) { - signatures.forEach(signature => - applySignatureComment(signature, sourceComment) - ); - } else { - assignComment(child, sourceComment); - } - } -}; - -const applyNamespaceMembers = (reflection, sourceEntry) => { - for (const child of reflection.children ?? []) { - const sourceComment = - namedExportComment(sourceEntry, child.name) ?? - sourceEntry?.file.symbols.get(child.name)?.comment; +/** + * @typedef {object} TypePageMetadata + * @property {string} baseName Router base name for the synthetic page. + * @property {string} title Rendered page title. + */ - if (!hasRenderableCommentContent(sourceComment)) continue; +const sourceMetadata = new WeakMap(); +const typePageMetadata = new WeakMap(); +const publicNames = new WeakMap(); - applyFunctionLikeComment(child, sourceComment); - } +/** + * Attach source-file metadata discovered from webpack/lib to a reflection. + * + * @param {import('typedoc').Reflection} reflection + * @param {SourceMetadata} metadata + */ +export const setSourceMetadata = (reflection, metadata) => { + sourceMetadata.set(reflection, metadata); }; -const applyClassMembers = (reflection, sourceEntry) => { - assignComment(reflection, commentForSourceEntry(sourceEntry)); - - for (const child of reflection.children ?? []) { - if (child.kindOf(ReflectionKind.Constructor)) { - const comment = memberSourceComment(sourceEntry, 'constructor'); - child.signatures?.forEach(signature => - applySignatureComment(signature, comment) - ); - continue; - } - - const comment = memberSourceComment(sourceEntry, child.name); - if (!hasRenderableCommentContent(comment)) continue; +/** + * @param {import('typedoc').Reflection} reflection + * @returns {SourceMetadata | undefined} + */ +export const getSourceMetadata = reflection => sourceMetadata.get(reflection); - if (child.signatures?.length) { - child.signatures.forEach(signature => - applySignatureComment(signature, comment) - ); - } else { - assignComment(child, comment); - } - } +/** + * Synthetic type pages are not TypeDoc source declarations, so their routing + * and title metadata lives beside other processor-owned overlays. + * + * @param {import('typedoc').Reflection} reflection + * @param {TypePageMetadata} metadata + */ +export const setTypePageMetadata = (reflection, metadata) => { + typePageMetadata.set(reflection, metadata); }; -const attachSourceMetadata = (reflection, sourceEntry, exportEntry) => { - if (!sourceEntry?.file) return; +/** + * @param {import('typedoc').Reflection} reflection + * @returns {string | undefined} + */ +export const getTypePageTitle = reflection => + typePageMetadata.get(reflection)?.title; - const leaf = publicName(reflection)?.split('.').at(-1); - const fileBase = sourceEntry.file.fileBase; +/** @param {import('typedoc').Reflection} reflection */ +export const isTypePage = reflection => typePageMetadata.has(reflection); - reflection[SOURCE_METADATA] = { - sourcePath: sourceEntry.file.filePath, - sourceRelativePath: sourceEntry.file.relativePath, - fileBase, - anchorName: fileBase !== leaf ? leaf : undefined, - publicPath: exportEntry.publicPath, - }; +/** + * Some declaration-file shapes need processor-owned public names that differ + * from TypeDoc's internal reflection names. + * + * @param {import('typedoc').Reflection} reflection + * @param {string} name + */ +export const setPublicName = (reflection, name) => { + publicNames.set(reflection, name); }; -export const applySourceMetadata = ( - project, - { libDir = './webpack/lib' } = {} -) => { - const absoluteLibDir = resolve(libDir); - if (!existsSync(absoluteLibDir) || !statSync(absoluteLibDir).isDirectory()) { - return; - } - - const sourceFiles = buildSourceIndex(absoluteLibDir); - const exportMap = buildExportMap(absoluteLibDir); - - // The declaration file remains authoritative for shape. This pass only - // enriches public reflections that can be matched back to webpack/lib source. - for (const reflection of allReflections(project)) { - if ( - !reflection.kindOf( - ReflectionKind.Class | - ReflectionKind.Namespace | - ReflectionKind.Function | - ReflectionKind.Variable - ) - ) { - continue; - } - - const name = publicName(reflection); - const exportEntry = name ? exportEntryFor(name, exportMap) : undefined; - if (!exportEntry?.sourcePath) continue; - - const sourceEntry = sourceEntryFor(exportEntry, sourceFiles); - attachSourceMetadata(reflection, sourceEntry, exportEntry); - - if (reflection.kindOf(ReflectionKind.Namespace)) { - applyNamespaceMembers(reflection, sourceEntry); - continue; - } - - if (reflection.kindOf(ReflectionKind.Class)) { - applyClassMembers(reflection, sourceEntry); - continue; - } - - applyFunctionLikeComment(reflection, commentForSourceEntry(sourceEntry)); - applyObjectMembers(reflection, sourceEntry); - } -}; +/** + * @param {import('typedoc').Reflection} reflection + * @returns {string | undefined} + */ +export const getPublicName = reflection => publicNames.get(reflection); diff --git a/plugins/processor/router.mjs b/plugins/processor/router.mjs index 6849b430..92ef0d39 100644 --- a/plugins/processor/router.mjs +++ b/plugins/processor/router.mjs @@ -1,13 +1,10 @@ import createNodeSlugger from '@node-core/doc-kit/src/generators/metadata/utils/slugger.mjs'; -import { - DeclarationReflection, - PageKind, - Reflection, - ReflectionKind, -} from 'typedoc'; +import { PageKind, Reflection, ReflectionKind } from 'typedoc'; import { MemberRouter } from 'typedoc-plugin-markdown'; import { categoryForReflection } from '../shared/categories.mjs'; -import { getMemberTitle } from '../shared/titles.mjs'; +import { getConstructorTitle, getMemberTitle } from '../shared/titles.mjs'; +import { getSourceMetadata, isTypePage } from './metadata.mjs'; +import { createTypePages, TYPE_PAGE_HEADING_KINDS } from './synthetic.mjs'; /** * The router owns the public Markdown shape. It keeps one class per file, @@ -15,24 +12,24 @@ import { getMemberTitle } from '../shared/titles.mjs'; * root plugin classes under plugins/, and structural types on category types * pages instead of the root README. */ -export const SOURCE_METADATA = Symbol.for('webpack-doc-kit.sourceMetadata'); -export const TYPE_PAGE_METADATA = Symbol.for( - 'webpack-doc-kit.typePageMetadata' -); - const sluggers = new Map(); -// Interfaces and type aliases are still public API, but doc-kit consumes them -// more cleanly when category pages collect them instead of leaving hundreds of -// structural entries on README.md. -const TYPE_PAGE_KINDS = ReflectionKind.Interface | ReflectionKind.TypeAlias; +const NON_HEADING_KINDS = + ReflectionKind.Property | + ReflectionKind.EnumMember | + ReflectionKind.SomeSignature | + ReflectionKind.TypeLiteral | + ReflectionKind.TypeParameter; const fullNameParts = reflection => reflection.getFullName().split('.'); const pagePath = parts => parts.join('/'); +const categoryNameForReflection = reflection => + categoryForReflection(reflection)?.category; + const rootExportBaseName = reflection => { - const category = categoryForReflection(reflection); + const category = categoryNameForReflection(reflection); return category ? `${category}/${reflection.name}` : reflection.name; }; @@ -44,7 +41,7 @@ const namespaceBaseName = reflection => { const parts = fullNameParts(reflection); if (parts.length === 1) { - const category = categoryForReflection(reflection); + const category = categoryNameForReflection(reflection); if (category) { return `${category}/${reflection.name}`; @@ -108,71 +105,26 @@ export const sourceAnchorName = reflection => { return; } - return reflection[SOURCE_METADATA]?.anchorName; + return getSourceMetadata(reflection)?.anchorName; }; -const typePageBaseName = reflection => { - const category = categoryForReflection(reflection); - return category ? `${category}/types` : 'types'; -}; +const rendersHeading = (target, pageTarget) => { + if (!target.isDeclaration() || target.kindOf(NON_HEADING_KINDS)) { + return false; + } -const typePageTitle = baseName => - baseName === 'types' - ? 'webpack.types' - : `webpack.${baseName.replace(/\/types$/, '').replace(/\//g, '.')}.types`; - -const typePageName = baseName => baseName.replace(/\//g, '.'); - -const removeChildren = (items, moved) => - items?.filter(item => !moved.has(item)); - -const removeFromGroups = (groups, moved) => - groups - ?.map(group => ({ - ...group, - children: group.children.filter(child => !moved.has(child)), - categories: removeFromGroups(group.categories, moved), - })) - .filter(group => group.children.length || group.categories?.length); - -const makeTypeGroup = (title, children) => - children.length ? { title, children } : undefined; - -const createTypePage = (project, baseName, children) => { - const page = new DeclarationReflection( - typePageName(baseName), - ReflectionKind.Namespace, - project - ); - const interfaces = children.filter(child => - child.kindOf(ReflectionKind.Interface) - ); - const typeAliases = children.filter(child => - child.kindOf(ReflectionKind.TypeAlias) - ); - - page.children = children; - page.childrenIncludingDocuments = children; - page.groups = [ - makeTypeGroup('Interfaces', interfaces), - makeTypeGroup('Type Aliases', typeAliases), - ].filter(Boolean); - // Synthetic type pages are not TypeDoc declarations from the input file. This - // metadata gives the theme and router a stable title and output path for them. - page[TYPE_PAGE_METADATA] = { - baseName, - title: typePageTitle(baseName), - }; - - return page; + return isTypePage(pageTarget) ? target.kindOf(TYPE_PAGE_HEADING_KINDS) : true; }; -const compareByName = (a, b) => a.name.localeCompare(b.name); +const anchorTitle = (target, pageTarget) => + target.kindOf(ReflectionKind.Constructor) + ? getConstructorTitle(target) + : getMemberTitle(target, { local: isTypePage(pageTarget) }); export class DocKitRouter extends MemberRouter { /** @param {import('typedoc').ProjectReflection} project */ buildPages(project) { - const typePages = this.prepareTypePages(project); + const typePages = createTypePages(project); const pages = super.buildPages(project); for (const { baseName, children, model } of typePages) { @@ -181,45 +133,13 @@ export class DocKitRouter extends MemberRouter { pages.push({ kind: PageKind.Reflection, model, url }); for (const child of children) { - this.buildAnchors(child, model); + this.buildAnchors(child, model, { includeChildren: false }); } } return pages; } - /** @param {import('typedoc').ProjectReflection} project */ - prepareTypePages(project) { - const movedTypes = (project.children ?? []) - .filter(child => child.kindOf(TYPE_PAGE_KINDS)) - .sort(compareByName); - const movedSet = new Set(movedTypes); - const byPage = new Map(); - - for (const reflection of movedTypes) { - const baseName = typePageBaseName(reflection); - const group = byPage.get(baseName) ?? []; - group.push(reflection); - byPage.set(baseName, group); - } - - // Remove moved structural types from the project root before TypeDoc builds - // README.md; their URLs are rebuilt below against the synthetic type pages. - project.children = removeChildren(project.children, movedSet); - project.childrenIncludingDocuments = removeChildren( - project.childrenIncludingDocuments, - movedSet - ); - project.groups = removeFromGroups(project.groups, movedSet); - project.categories = removeFromGroups(project.categories, movedSet); - - return [...byPage].map(([baseName, children]) => ({ - baseName, - children, - model: createTypePage(project, baseName, children), - })); - } - /** * @param {import('typedoc').DeclarationReflection} reflection * @param {import('typedoc').MarkdownPageEvent[]} outPages @@ -293,8 +213,9 @@ export class DocKitRouter extends MemberRouter { /** * @param {import('typedoc').RouterTarget} target * @param {import('typedoc').RouterTarget} pageTarget + * @param {{ includeChildren?: boolean }} [options] */ - buildAnchors(target, pageTarget) { + buildAnchors(target, pageTarget, { includeChildren = true } = {}) { if ( !(target instanceof Reflection) || !(pageTarget instanceof Reflection) @@ -305,31 +226,22 @@ export class DocKitRouter extends MemberRouter { const pageUrl = this.fullUrls.get(pageTarget); if (!pageUrl) return; - if ( - !target.isDeclaration() && - !target.isSignature() && - !target.isTypeParameter() - ) { + // Only publish URLs for headings the custom theme actually renders. TypeDoc + // exposes signatures, type parameters, class properties, and type literals as + // reflections too, but in these docs they are signature bodies or typed list + // items rather than linkable headings. + if (!rendersHeading(target, pageTarget)) { return; } - if ( - target.kindOf(ReflectionKind.TypeLiteral) && - (!target.parent?.kindOf(ReflectionKind.SomeExport) || - target.parent.type?.type !== 'reflection') - ) { - return; - } + const title = anchorTitle(target, pageTarget); + const anchor = this.getSlugger(pageTarget).slug(title); - if (!target.kindOf(ReflectionKind.TypeLiteral)) { - const title = getMemberTitle(target); - const anchor = this.getSlugger(pageTarget).slug(title); + this.fullUrls.set(target, `${pageUrl.replace(/\.md$/, '.html')}#${anchor}`); + this.anchors.set(target, anchor); - this.fullUrls.set( - target, - `${pageUrl.replace(/\.md$/, '.html')}#${anchor}` - ); - this.anchors.set(target, anchor); + if (!includeChildren) { + return; } target.traverse(child => { diff --git a/plugins/processor/site.mjs b/plugins/processor/site.mjs index 3f1dfcde..b4def82b 100644 --- a/plugins/processor/site.mjs +++ b/plugins/processor/site.mjs @@ -1,60 +1,28 @@ import { ReflectionKind } from 'typedoc'; -import { fullName } from '../shared/titles.mjs'; +import { CATEGORY_RULES } from '../shared/categories.mjs'; -const ROOT_GROUP = 'webpack'; +const SIDEBAR_KINDS = + ReflectionKind.Project | ReflectionKind.Namespace | ReflectionKind.Class; -const toOutputPath = url => { - const withoutExtension = url.replace(/\.md$/, ''); - if (withoutExtension === 'README') return ''; - return withoutExtension.replace(/\/index$/, ''); -}; - -const toSidebarLink = url => { - const path = toOutputPath(url); - return path ? `/${path}` : '/'; -}; +const SIDEBAR_GROUP_NAME = 'API Documentation'; -const pagePathParts = url => - url.replace(/\.md$/, '').split('/').filter(Boolean); +const getFirstAtxHeading = text => text.match(/^#\s+(.+)$/m)?.[1]?.trim(); -const groupKeyFor = url => { - const parts = pagePathParts(url); - return parts.length > 1 ? parts[0] : ROOT_GROUP; -}; - -const rawPageName = url => { - if (url === 'README.md') return ROOT_GROUP; - return ( - pagePathParts(url) - .at(-1) - ?.replace(/\/index$/, '') ?? ROOT_GROUP - ); -}; +const getFirstPathSegment = url => url.replace(/^\//, '').split('/')[0]; -const stripWebpackPrefix = value => value.replace(/^webpack\.?/, ''); - -const trimGroupPrefix = (value, groupKey) => { - if (value === groupKey) return rawPageName(`${groupKey}/index.md`); - return value.startsWith(`${groupKey}.`) - ? value.slice(groupKey.length + 1) - : value; +const toSidebarLink = url => { + const path = url.replace(/\.md$/, '').replace(/\/index$/, ''); + return path ? `/${path}` : '/'; }; -const itemLabelFor = (target, url, groupKey) => { - const name = stripWebpackPrefix(fullName(target)); - const label = trimGroupPrefix(name, groupKey); - return label || rawPageName(url); +const defaultLabelFor = (target, url) => { + if (url.endsWith('/index.md')) return 'Overview'; + if (url.endsWith('/types.md')) return 'Types'; + return target.name; }; const isSidebarTarget = (router, target) => { - if ( - !target.kindOf?.( - ReflectionKind.Project | ReflectionKind.Namespace | ReflectionKind.Class - ) - ) { - return false; - } - + if (!target.kindOf?.(SIDEBAR_KINDS)) return false; if (!router.hasOwnDocument(target)) return false; const url = router.getFullUrl(target); @@ -62,30 +30,45 @@ const isSidebarTarget = (router, target) => { }; export const sidebar = router => { - const groups = new Map(); + const categories = new Map(); + const seen = new Set(); for (const target of router.getLinkTargets()) { if (!isSidebarTarget(router, target)) continue; const url = router.getFullUrl(target); - const groupKey = groupKeyFor(url); - const group = groups.get(groupKey) ?? []; - const link = toSidebarLink(url); - - if (!group.some(item => item.link === link)) { - group.push({ - link, - label: itemLabelFor(target, url, groupKey), - }); - } - - groups.set(groupKey, group); + if (seen.has(url)) continue; + seen.add(url); + + const firstPathSegment = getFirstPathSegment(url); + const category = CATEGORY_RULES.find( + cat => cat.category === firstPathSegment + ); + + if (!category) continue; + + const headingName = getFirstAtxHeading( + target.comment?.summary?.[0]?.text ?? '' + ); + const label = headingName ?? defaultLabelFor(target, url); + + const group = categories.get(category.category) ?? { + label: category.label, + items: [], + }; + + group.items.push({ + link: toSidebarLink(url), + label, + }); + + categories.set(category.category, group); } - return [...groups] - .map(([groupKey, items]) => ({ - groupName: groupKey, - items, - })) - .filter(group => group.items.length); + return [ + { + groupName: SIDEBAR_GROUP_NAME, + items: [...categories.values()].filter(category => category.items.length), + }, + ]; }; diff --git a/plugins/processor/source.mjs b/plugins/processor/source.mjs new file mode 100644 index 00000000..1af04165 --- /dev/null +++ b/plugins/processor/source.mjs @@ -0,0 +1,735 @@ +import ts from 'typescript'; +import { Comment, CommentTag, ReflectionKind, ReflectionType } from 'typedoc'; +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { basename, dirname, extname, join, relative, resolve } from 'node:path'; +import { getPublicName, setSourceMetadata } from './metadata.mjs'; + +/** + * Source metadata is intentionally a documentation-only overlay: + * 1. types.d.ts decides which symbols exist and what their signatures are. + * 2. webpack/lib/index.js maps those public symbols back to implementation files. + * 3. all JS files under webpack/lib contribute summaries, params, returns, + * and stability/deprecation tags when that source comment has real content. + */ +const DOC_BLOCK_TAGS = new Set([ + '@deprecated', + '@example', + '@legacy', + '@remarks', + '@see', + '@since', + '@throws', +]); + +const MODIFIER_TAGS = new Set([ + '@alpha', + '@beta', + '@experimental', + '@internal', +]); + +const textPart = text => [{ kind: 'text', text }]; + +const normalizeText = value => + String(value ?? '') + .replace(/\r\n?/g, '\n') + .trim(); + +const jsDocText = value => { + if (!value) return ''; + if (Array.isArray(value)) { + return normalizeText( + value.map(part => part.text ?? part.getText?.() ?? '').join('') + ); + } + return normalizeText(value); +}; + +const hasCommentContent = comment => + Boolean( + comment?.summary || + comment?.returns || + comment?.params?.size || + comment?.blockTags?.length || + comment?.modifierTags?.size + ); + +const hasRenderableCommentContent = comment => + Boolean( + comment?.summary || + comment?.returns || + [...(comment?.params?.values() ?? [])].some(Boolean) || + comment?.blockTags?.some(tag => tag.content) || + comment?.modifierTags?.size + ); + +const mergeComments = comments => { + const merged = { + summary: '', + params: new Map(), + returns: '', + blockTags: [], + modifierTags: new Set(), + }; + + for (const comment of comments.filter(Boolean)) { + if (comment.summary) merged.summary = comment.summary; + if (comment.returns) merged.returns = comment.returns; + for (const [name, value] of comment.params) { + if (value) merged.params.set(name, value); + } + for (const tag of comment.blockTags) { + if (tag.content) merged.blockTags.push(tag); + } + for (const tag of comment.modifierTags) { + merged.modifierTags.add(tag); + } + } + + return hasCommentContent(merged) ? merged : undefined; +}; + +// TypeScript exposes JSDoc differently depending on whether a comment belongs +// to a declaration, assignment, or export wrapper. Walk up a tiny amount so +// comments attached to CommonJS assignment statements are still discovered. +const getJsDocs = node => { + let current = node; + + while (current && !ts.isSourceFile(current)) { + if (current.jsDoc?.length) return current.jsDoc; + if (ts.isBlock(current) || ts.isClassDeclaration(current)) break; + current = current.parent; + } + + return []; +}; + +const getJsDocComment = node => { + const jsDocs = getJsDocs(node); + const comments = jsDocs.map(jsDoc => { + const comment = { + summary: jsDocText(jsDoc.comment), + params: new Map(), + returns: '', + blockTags: [], + modifierTags: new Set(), + }; + + for (const tag of jsDoc.tags ?? []) { + const tagName = `@${tag.tagName?.escapedText ?? ''}`; + const content = jsDocText(tag.comment); + + if (ts.isJSDocParameterTag(tag) && tag.name) { + comment.params.set(tag.name.getText(), content); + continue; + } + + if (ts.isJSDocReturnTag(tag)) { + comment.returns = content; + continue; + } + + if (MODIFIER_TAGS.has(tagName)) { + comment.modifierTags.add(tagName); + continue; + } + + if (DOC_BLOCK_TAGS.has(tagName)) { + comment.blockTags.push({ tag: tagName, content }); + } + } + + return hasCommentContent(comment) ? comment : undefined; + }); + + return mergeComments(comments); +}; + +const toTypedocComment = sourceComment => { + if (!sourceComment || !hasRenderableCommentContent(sourceComment)) { + return undefined; + } + + const blockTags = []; + if (sourceComment.returns) { + blockTags.push(new CommentTag('@returns', textPart(sourceComment.returns))); + } + + for (const { tag, content } of sourceComment.blockTags ?? []) { + blockTags.push(new CommentTag(tag, textPart(content))); + } + + return new Comment( + sourceComment.summary ? textPart(sourceComment.summary) : [], + blockTags, + new Set(sourceComment.modifierTags ?? []) + ); +}; + +const toParameterComment = text => { + const content = normalizeText(text); + return content ? new Comment(textPart(content)) : undefined; +}; + +const assignComment = (target, sourceComment) => { + const typedocComment = toTypedocComment(sourceComment); + if (typedocComment) target.comment = typedocComment; +}; + +const applySignatureComment = (signature, sourceComment) => { + if (!signature || !hasRenderableCommentContent(sourceComment)) return; + + assignComment(signature, sourceComment); + + const params = [...sourceComment.params.values()].filter(Boolean); + + for (const parameter of signature.parameters ?? []) { + const comment = + sourceComment.params.get(parameter.name) ?? + (parameter.name.startsWith('__') && params.length === 1 + ? params[0] + : undefined); + const parameterComment = toParameterComment(comment); + if (parameterComment) parameter.comment = parameterComment; + } +}; + +const propertyNameText = name => { + if (!name) return undefined; + if ( + ts.isIdentifier(name) || + ts.isStringLiteral(name) || + ts.isNumericLiteral(name) + ) { + return name.text; + } + return undefined; +}; + +const getAssignedProperty = expression => { + if (!ts.isPropertyAccessExpression(expression)) return undefined; + + const property = expression.name.text; + const owner = expression.expression; + + if ( + ts.isPropertyAccessExpression(owner) && + owner.expression.getText() === 'module' && + owner.name.text === 'exports' + ) { + return property; + } + + if (owner.getText() === 'exports') return property; + + return undefined; +}; + +const isModuleExports = expression => + ts.isPropertyAccessExpression(expression) && + expression.expression.getText() === 'module' && + expression.name.text === 'exports'; + +const expressionLocalName = expression => + ts.isIdentifier(expression) ? expression.text : undefined; + +const resolveSourceRequest = (fromFile, request) => { + if (!request.startsWith('.')) return undefined; + + const base = resolve(dirname(fromFile), request); + const candidates = [base, `${base}.js`, join(base, 'index.js')]; + return candidates.find(candidate => existsSync(candidate)); +}; + +const firstRequire = expression => { + let found; + + const visit = node => { + if (found) return; + + if ( + ts.isCallExpression(node) && + node.expression.getText() === 'require' && + ts.isStringLiteral(node.arguments[0]) + ) { + found = { + request: node.arguments[0].text, + property: + node.parent && ts.isPropertyAccessExpression(node.parent) + ? node.parent.name.text + : undefined, + }; + return; + } + + ts.forEachChild(node, visit); + }; + + visit(expression); + return found; +}; + +const listJsFiles = directory => { + const entries = readdirSync(directory, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const path = join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...listJsFiles(path)); + } else if (entry.isFile() && extname(entry.name) === '.js') { + files.push(path); + } + } + + return files; +}; + +const sourceFileFor = filePath => + ts.createSourceFile( + filePath, + readFileSync(filePath, 'utf8'), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS + ); + +const sourceFileBase = filePath => basename(filePath, extname(filePath)); + +const createFileMetadata = (filePath, libDir) => { + const sourceFile = sourceFileFor(filePath); + const symbols = new Map(); + const classes = new Map(); + const namedExports = new Map(); + let defaultExport; + + const rememberSymbol = (name, data) => { + if (!name) return; + const existing = symbols.get(name) ?? {}; + symbols.set(name, { + ...existing, + ...data, + comment: data.comment ?? existing.comment, + members: data.members ?? existing.members, + }); + }; + + // Keep per-class member comments separate from file-level exports. A public + // class comes from types.d.ts, but method/constructor prose usually lives in + // the implementation file next to the code. + const classMembers = node => { + const members = new Map(); + + for (const member of node.members ?? []) { + if (ts.isConstructorDeclaration(member)) { + const comment = getJsDocComment(member); + if (comment) members.set('constructor', comment); + continue; + } + + const name = propertyNameText(member.name); + if (!name) continue; + + const comment = getJsDocComment(member); + if (comment) members.set(name, comment); + } + + return members; + }; + + const visit = node => { + if (ts.isClassDeclaration(node) && node.name) { + const data = { + kind: 'class', + localName: node.name.text, + comment: getJsDocComment(node), + members: classMembers(node), + }; + symbols.set(node.name.text, data); + classes.set(node.name.text, data); + } + + if (ts.isFunctionDeclaration(node) && node.name) { + rememberSymbol(node.name.text, { + kind: 'function', + localName: node.name.text, + comment: getJsDocComment(node), + }); + } + + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { + const comment = getJsDocComment(node); + if (comment) { + rememberSymbol(node.name.text, { + kind: 'variable', + localName: node.name.text, + comment, + }); + } + } + + if ( + ts.isExpressionStatement(node) && + ts.isBinaryExpression(node.expression) + ) { + const { left, right } = node.expression; + const comment = getJsDocComment(node) ?? getJsDocComment(right); + + if (isModuleExports(left)) { + defaultExport = { + localName: expressionLocalName(right), + comment, + }; + } else { + const property = getAssignedProperty(left); + if (property) { + namedExports.set(property, { + localName: expressionLocalName(right), + comment, + }); + } + } + + if ( + ts.isPropertyAccessExpression(left) && + ts.isIdentifier(left.expression) && + classes.has(left.expression.text) && + comment + ) { + classes.get(left.expression.text).members.set(left.name.text, comment); + } + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + + return { + filePath, + fileBase: sourceFileBase(filePath), + relativePath: relative(libDir, filePath).replace(/\\/g, '/'), + symbols, + classes, + namedExports, + defaultExport, + }; +}; + +const getExportObject = sourceFile => { + let exportObject; + + const visit = node => { + if (exportObject) return; + + if ( + ts.isExpressionStatement(node) && + ts.isBinaryExpression(node.expression) && + isModuleExports(node.expression.left) && + ts.isCallExpression(node.expression.right) + ) { + const objectArg = [...node.expression.right.arguments].find( + ts.isObjectLiteralExpression + ); + if (objectArg) exportObject = objectArg; + } + + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return exportObject; +}; + +const buildExportMap = libDir => { + const indexPath = join(libDir, 'index.js'); + const sourceFile = sourceFileFor(indexPath); + const exportMap = new Map(); + const exportObject = getExportObject(sourceFile); + + const addExport = (parts, data) => { + exportMap.set(parts.join('.'), { + publicPath: parts.join('.'), + publicName: parts.at(-1), + ...data, + }); + }; + + // webpack's callable export is assigned outside lib/index.js, so seed the + // root namespace before walking the object tree of re-exported helpers. + addExport(['webpack'], { + sourcePath: join(libDir, 'webpack.js'), + localName: 'webpack', + }); + + const walkObject = (object, parts = []) => { + for (const property of object.properties) { + const name = propertyNameText(property.name); + if (!name) continue; + + if ( + ts.isPropertyAssignment(property) && + ts.isObjectLiteralExpression(property.initializer) + ) { + walkObject(property.initializer, [...parts, name]); + continue; + } + + const expression = ts.isGetAccessor(property) + ? property.body?.statements.filter(ts.isReturnStatement).at(-1) + ?.expression + : ts.isPropertyAssignment(property) + ? property.initializer + : undefined; + + if (!expression) continue; + + const requireCall = firstRequire(expression); + const sourcePath = requireCall?.request + ? resolveSourceRequest(indexPath, requireCall.request) + : undefined; + + addExport([...parts, name], { + sourcePath, + localName: requireCall?.property, + comment: getJsDocComment(property), + }); + } + }; + + if (exportObject) walkObject(exportObject); + return exportMap; +}; + +const buildSourceIndex = libDir => { + const sourceFiles = new Map(); + for (const file of listJsFiles(libDir)) { + sourceFiles.set(file, createFileMetadata(file, libDir)); + } + return sourceFiles; +}; + +const publicName = reflection => + getPublicName(reflection) ?? reflection.getFullName?.(); + +const exportEntryFor = (name, exportMap) => { + const exact = exportMap.get(name); + if (exact) return exact; + + const parts = name.split('.'); + for (let i = parts.length - 1; i > 0; i--) { + const parent = exportMap.get(parts.slice(0, i).join('.')); + if (!parent?.sourcePath) continue; + + const memberPath = parts.slice(i); + // If lib/index.js exports an object, types.d.ts may expose its properties as + // public members. Reuse the parent source file and match the leaf locally. + return { + ...parent, + publicPath: name, + publicName: memberPath.at(-1), + localName: memberPath.at(-1), + parentPublicPath: parent.publicPath, + memberPath, + }; + } +}; + +const sourceEntryFor = (entry, sourceFiles) => { + const file = entry.sourcePath ? sourceFiles.get(entry.sourcePath) : undefined; + if (!file) return undefined; + + const exported = entry.publicName + ? file.namedExports.get(entry.publicName) + : undefined; + const localName = + entry.localName ?? + exported?.localName ?? + file.defaultExport?.localName ?? + entry.publicName; + const symbol = + file.symbols.get(localName) ?? file.symbols.get(entry.publicName); + + return { + file, + localName, + comment: + symbol?.comment ?? + entry.comment ?? + exported?.comment ?? + file.defaultExport?.comment, + symbol, + }; +}; + +const commentForSourceEntry = sourceEntry => + sourceEntry?.comment ?? sourceEntry?.symbol?.comment; + +const memberSourceComment = (sourceEntry, memberName) => + sourceEntry?.symbol?.members?.get(memberName) ?? + sourceEntry?.file.namedExports.get(memberName)?.comment ?? + sourceEntry?.file.symbols.get(memberName)?.comment; + +const namedExportComment = (sourceEntry, memberName) => { + const exported = sourceEntry?.file.namedExports.get(memberName); + if (!exported) return undefined; + return ( + exported.comment ?? + sourceEntry.file.symbols.get(exported.localName)?.comment ?? + sourceEntry.file.symbols.get(memberName)?.comment + ); +}; + +const ownSignatures = reflection => [ + ...(reflection.signatures ?? []), + ...(reflection.type instanceof ReflectionType + ? (reflection.type.declaration.signatures ?? []) + : []), +]; + +const applyFunctionLikeComment = (reflection, sourceComment) => { + if (!hasRenderableCommentContent(sourceComment)) return; + + const signatures = ownSignatures(reflection); + if (signatures.length) { + signatures.forEach(signature => + applySignatureComment(signature, sourceComment) + ); + } else { + assignComment(reflection, sourceComment); + } +}; + +// Exported object literals (for example helper namespaces) often document their +// individual properties as named exports in the implementation file. +const applyObjectMembers = (reflection, sourceEntry) => { + const declaration = + reflection.type instanceof ReflectionType + ? reflection.type.declaration + : undefined; + + for (const child of declaration?.children ?? []) { + const sourceComment = + namedExportComment(sourceEntry, child.name) ?? + sourceEntry?.file.symbols.get(child.name)?.comment; + + if (!hasRenderableCommentContent(sourceComment)) continue; + + const signatures = ownSignatures(child); + if (signatures.length) { + signatures.forEach(signature => + applySignatureComment(signature, sourceComment) + ); + } else { + assignComment(child, sourceComment); + } + } +}; + +const applyNamespaceMembers = (reflection, sourceEntry) => { + for (const child of reflection.children ?? []) { + const sourceComment = + namedExportComment(sourceEntry, child.name) ?? + sourceEntry?.file.symbols.get(child.name)?.comment; + + if (!hasRenderableCommentContent(sourceComment)) continue; + + applyFunctionLikeComment(child, sourceComment); + } +}; + +const applyClassMembers = (reflection, sourceEntry) => { + assignComment(reflection, commentForSourceEntry(sourceEntry)); + + for (const child of reflection.children ?? []) { + if (child.kindOf(ReflectionKind.Constructor)) { + const comment = memberSourceComment(sourceEntry, 'constructor'); + child.signatures?.forEach(signature => + applySignatureComment(signature, comment) + ); + continue; + } + + const comment = memberSourceComment(sourceEntry, child.name); + if (!hasRenderableCommentContent(comment)) continue; + + if (child.signatures?.length) { + child.signatures.forEach(signature => + applySignatureComment(signature, comment) + ); + } else { + assignComment(child, comment); + } + } +}; + +const attachSourceMetadata = (reflection, sourceEntry, exportEntry) => { + if (!sourceEntry?.file) return; + + const leaf = publicName(reflection)?.split('.').at(-1); + const fileBase = sourceEntry.file.fileBase; + + setSourceMetadata(reflection, { + sourcePath: sourceEntry.file.filePath, + sourceRelativePath: sourceEntry.file.relativePath, + fileBase, + anchorName: fileBase !== leaf ? leaf : undefined, + publicPath: exportEntry.publicPath, + }); +}; + +/** + * Enrich declaration reflections with comments and source locations recovered + * from webpack/lib. The declaration file remains authoritative for public API + * shape; this pass only adds documentation content and routing hints. + * + * @param {import('typedoc').ProjectReflection} project + * @param {{ libDir?: string }} [options] + */ +export const applySourceMetadata = ( + project, + { libDir = './webpack/lib' } = {} +) => { + const absoluteLibDir = resolve(libDir); + if (!existsSync(absoluteLibDir) || !statSync(absoluteLibDir).isDirectory()) { + return; + } + + const sourceFiles = buildSourceIndex(absoluteLibDir); + const exportMap = buildExportMap(absoluteLibDir); + + // The declaration file remains authoritative for shape. This pass only + // enriches public reflections that can be matched back to webpack/lib source. + for (const reflection of project.getReflectionsByKind(ReflectionKind.All)) { + if ( + !reflection.kindOf( + ReflectionKind.Class | + ReflectionKind.Namespace | + ReflectionKind.Function | + ReflectionKind.Variable + ) + ) { + continue; + } + + const name = publicName(reflection); + const exportEntry = name ? exportEntryFor(name, exportMap) : undefined; + if (!exportEntry?.sourcePath) continue; + + const sourceEntry = sourceEntryFor(exportEntry, sourceFiles); + attachSourceMetadata(reflection, sourceEntry, exportEntry); + + if (reflection.kindOf(ReflectionKind.Namespace)) { + applyNamespaceMembers(reflection, sourceEntry); + continue; + } + + if (reflection.kindOf(ReflectionKind.Class)) { + applyClassMembers(reflection, sourceEntry); + continue; + } + + applyFunctionLikeComment(reflection, commentForSourceEntry(sourceEntry)); + applyObjectMembers(reflection, sourceEntry); + } +}; diff --git a/plugins/processor/synthetic.mjs b/plugins/processor/synthetic.mjs new file mode 100644 index 00000000..dcaf5bb2 --- /dev/null +++ b/plugins/processor/synthetic.mjs @@ -0,0 +1,117 @@ +import { Comment, DeclarationReflection, ReflectionKind } from 'typedoc'; +import { categoryForReflection } from '../shared/categories.mjs'; +import { setTypePageMetadata } from './metadata.mjs'; + +export const TYPE_PAGE_HEADING_KINDS = + ReflectionKind.Interface | ReflectionKind.TypeAlias; + +const TYPE_PAGE_NOTICE = + 'These types are not exported by webpack, but they are available to TypeScript consumers.'; + +const categoryNameForReflection = reflection => + categoryForReflection(reflection)?.category; + +const typePageBaseName = reflection => { + const category = categoryNameForReflection(reflection); + return category ? `${category}/types` : 'types'; +}; + +const typePageNamespace = baseName => + baseName === 'types' + ? 'webpack' + : `webpack.${baseName.replace(/\/types$/, '').replace(/\//g, '.')}`; + +const typePageTitle = baseName => `\`${typePageNamespace(baseName)}\` Types`; + +const typePageName = baseName => baseName.replace(/\//g, '.'); + +const typePageComment = () => + new Comment([ + { + kind: 'text', + text: TYPE_PAGE_NOTICE, + }, + ]); + +const removeChildren = (items, moved) => + items?.filter(item => !moved.has(item)); + +const removeFromGroups = (groups, moved) => + groups + ?.map(group => ({ + ...group, + children: group.children.filter(child => !moved.has(child)), + categories: removeFromGroups(group.categories, moved), + })) + .filter(group => group.children.length || group.categories?.length); + +const makeTypeGroup = (title, children) => + children.length ? { title, children } : undefined; + +const createTypePage = (project, baseName, children) => { + const page = new DeclarationReflection( + typePageName(baseName), + ReflectionKind.Namespace, + project + ); + const interfaces = children.filter(child => + child.kindOf(ReflectionKind.Interface) + ); + const typeAliases = children.filter(child => + child.kindOf(ReflectionKind.TypeAlias) + ); + + page.children = children; + page.childrenIncludingDocuments = children; + page.comment = typePageComment(); + page.groups = [ + makeTypeGroup('Interfaces', interfaces), + makeTypeGroup('Type Aliases', typeAliases), + ].filter(Boolean); + + setTypePageMetadata(page, { + baseName, + title: typePageTitle(baseName), + }); + + return page; +}; + +const compareByName = (a, b) => a.name.localeCompare(b.name); + +/** + * Move root interfaces and type aliases onto synthetic category pages so they + * remain linkable without appearing as root API exports. + * + * @param {import('typedoc').ProjectReflection} project + */ +export const createTypePages = project => { + const movedTypes = (project.children ?? []) + .filter(child => child.kindOf(TYPE_PAGE_HEADING_KINDS)) + .sort(compareByName); + const movedSet = new Set(movedTypes); + const byPage = new Map(); + + for (const reflection of movedTypes) { + const baseName = typePageBaseName(reflection); + const group = byPage.get(baseName) ?? []; + group.push(reflection); + byPage.set(baseName, group); + } + + // Remove moved structural types from the project root before TypeDoc builds + // README.md; their URLs are rebuilt by the router against these pages. + project.children = removeChildren(project.children, movedSet); + project.childrenIncludingDocuments = removeChildren( + project.childrenIncludingDocuments, + movedSet + ); + project.groups = removeFromGroups(project.groups, movedSet); + project.categories = removeFromGroups(project.categories, movedSet); + + return [...byPage].map(([baseName, children]) => ({ + baseName, + children, + model: createTypePage(project, baseName, children), + })); +}; diff --git a/plugins/processor/typeMap.mjs b/plugins/processor/typeMap.mjs new file mode 100644 index 00000000..de327c33 --- /dev/null +++ b/plugins/processor/typeMap.mjs @@ -0,0 +1,31 @@ +import { ReflectionKind } from 'typedoc'; +import { isTypePage } from './metadata.mjs'; + +const TYPE_MAP_KINDS = + ReflectionKind.Class | + ReflectionKind.Interface | + ReflectionKind.TypeAlias | + ReflectionKind.Enum; + +const isTypeMapTarget = target => + !isTypePage(target) && target.kindOf?.(TYPE_MAP_KINDS); + +/** + * Build the annotation resolver map from the router's final public URLs. + * + * @param {import('typedoc-plugin-markdown').Router} router + */ +export const createTypeMap = router => { + const typeMap = new Map(); + + for (const target of router.getLinkTargets()) { + if (!isTypeMapTarget(target)) continue; + + const { name } = target; + if (!typeMap.has(name)) { + typeMap.set(name, router.getAnchoredURL(target)); + } + } + + return Object.fromEntries(typeMap); +}; diff --git a/plugins/shared/categories.mjs b/plugins/shared/categories.mjs index 166dd4b7..801f0d31 100644 --- a/plugins/shared/categories.mjs +++ b/plugins/shared/categories.mjs @@ -2,9 +2,10 @@ import { ReflectionKind } from 'typedoc'; // First match wins. Keep more specific groups above broad API-family rules so // adding a new category is usually a single RegExp entry rather than router code. -const CATEGORY_RULES = [ +export const CATEGORY_RULES = [ { category: 'plugins', + label: 'Plugins', match: reflection => reflection.kindOf(ReflectionKind.Class) && reflection.name.endsWith('Plugin'), @@ -12,84 +13,104 @@ const CATEGORY_RULES = [ }, { category: 'cli', + label: 'Command-Line Interface', pattern: /^(?:Argument|Colors|ColorsOptions|Problem)$/, }, { category: 'assets', + label: 'Assets', pattern: /^Asset|AssetInfo$/, }, { category: 'cache', + label: 'Cache', pattern: /Cache|Cached|Etag|ValueCache/, }, { category: 'runtime', + label: 'Runtime', pattern: /^Runtime.*/, }, { category: 'stats', + label: 'Stats', pattern: /^(?:Multi)?Stats/, }, { category: 'errors', + label: 'Errors', pattern: /(?:Error|ValidationError)$/, }, { category: 'chunks', + label: 'Chunks', pattern: /^(?:.*Chunk.*|Entrypoint)$/, }, { category: 'compilation', + label: 'Compilation', pattern: /^(?:Compilation|Compiler|MultiCompiler|Watching|PathData|CodeGenerationResults?)$/, }, { category: 'dependencies', + label: 'Dependencies', pattern: /Dependency/, }, { category: 'entries', + label: 'Entries', pattern: /^Entry/, }, { category: 'externals', + label: 'Externals', pattern: /^External|Externals/, }, { category: 'filesystem', + label: 'File System', pattern: /FileSystem$/, }, { category: 'library', + label: 'Library', pattern: /Library/, }, { category: 'loaders', + label: 'Loaders', pattern: /Loader/, }, { category: 'modules', + label: 'Modules', pattern: /^(?:AsyncDependenciesBlock|.*Dependency|.*Module.*|Generator|Parser)$/, }, { category: 'resolvers', + label: 'Resolvers', pattern: /^Resolve/, }, { category: 'rules', + label: 'Rules', pattern: /^RuleSet/, }, { category: 'serialization', + label: 'Serialization', pattern: /(?:Serializer|Deserializer)/, }, { category: 'templates', + label: 'Templates', pattern: /^(?:Template|RenderManifest)/, }, { category: 'config', + label: 'Configuration', pattern: /^(?:Configuration|MultiConfiguration|.*Options(?:Normalized)?|validate(?:Schema)?|WebpackOptions.*)$/, }, @@ -98,7 +119,9 @@ const CATEGORY_RULES = [ export const categoryForReflection = reflection => { for (const rule of CATEGORY_RULES) { if (rule.match?.(reflection) || rule.pattern?.test(reflection.name)) { - return rule.category; + return rule; } } + + return null; }; diff --git a/plugins/shared/titles.mjs b/plugins/shared/titles.mjs index 364330e5..52da6ed5 100644 --- a/plugins/shared/titles.mjs +++ b/plugins/shared/titles.mjs @@ -1,4 +1,5 @@ import { ReflectionKind } from 'typedoc'; +import { getPublicName } from '../processor/metadata.mjs'; // Heading text is also anchor input. Keep all programmatic names formatted here // so the Markdown theme and router cannot drift into different slugs. @@ -17,16 +18,45 @@ const STATIC_PREFIX = { const escapeCode = value => String(value).replace(/`/g, '\\`'); +const hasQualifiedParent = model => { + let parent = model.parent; + + while (parent) { + if (parent.kindOf?.(ReflectionKind.Class | ReflectionKind.Namespace)) { + return true; + } + + if (parent.kindOf?.(ReflectionKind.Project)) { + return false; + } + + parent = parent.parent; + } + + return false; +}; + +const memberName = (model, { local = false } = {}) => { + return local || hasQualifiedParent(model) ? model.name : fullName(model); +}; + export const fullName = model => { + const publicName = getPublicName(model); + if (publicName) return publicName; + const name = model.getFullName?.() ?? model.name; let root = model; while (root.parent) root = root.parent; + if (model === root) { + return name; + } + // TypeDoc omits the project name from nested full names. The generated docs // treat the project as the public webpack namespace, so add it back whenever // TypeDoc has not already included it. - if (!root.name || name === root.name || name.startsWith(`${root.name}.`)) { + if (!root.name || name.startsWith(`${root.name}.`)) { return name; } @@ -47,12 +77,18 @@ export const formatParams = (params = []) => }) .join(''); -export const signatureExpression = (model, params = []) => - `${fullName(model)}(${formatParams(params)})`; +export const signatureExpression = ( + model, + params = [], + name = fullName(model) +) => `${name}(${formatParams(params)})`; export const callableSignatures = model => model.signatures ?? model.type?.declaration?.signatures ?? []; +export const getConstructorTitle = (model, signature = model.signatures?.[0]) => + `\`new ${escapeCode(model.parent.name)}(${formatParams(signature?.parameters ?? [])})\``; + export const getMemberPrefix = model => { const prefix = model.flags?.isStatic ? STATIC_PREFIX[model.kind] @@ -61,13 +97,13 @@ export const getMemberPrefix = model => { return prefix ? `${prefix}: ` : ''; }; -export const getMemberTitle = model => { +export const getMemberTitle = (model, options) => { const prefix = getMemberPrefix(model); - const params = callableSignatures(model)[0]?.parameters; - const name = escapeCode(fullName(model)); + const params = model.parameters ?? callableSignatures(model)[0]?.parameters; + const name = escapeCode(memberName(model, options)); if (params) { - return `${prefix}\`${escapeCode(signatureExpression(model, params))}\``; + return `${prefix}\`${escapeCode(signatureExpression(model, params, name))}\``; } return `${prefix}\`${name}\``; diff --git a/plugins/theme/partials/index.mjs b/plugins/theme/partials/index.mjs index 4f6315a0..c56ee3a1 100644 --- a/plugins/theme/partials/index.mjs +++ b/plugins/theme/partials/index.mjs @@ -1,4 +1,4 @@ -import { TYPE_PAGE_METADATA } from '../../processor/router.mjs'; +import { getTypePageTitle, isTypePage } from '../../processor/metadata.mjs'; import { ArrayType, i18n, @@ -9,7 +9,7 @@ import { } from 'typedoc'; import { callableSignatures, - formatParams, + getConstructorTitle, getMemberTitle, getPageTitle, } from '../../shared/titles.mjs'; @@ -41,8 +41,8 @@ export default ctx => { pageTitle() { const model = ctx.page.model; - if (model[TYPE_PAGE_METADATA]?.title) - return `\`${model[TYPE_PAGE_METADATA].title}\``; + const typePageTitle = getTypePageTitle(model); + if (typePageTitle) return typePageTitle; if (model.getFullName) return getPageTitle(model); return ctx.partials.pageTitle(); }, @@ -54,10 +54,6 @@ export default ctx => { return [ stability, stability && '', - model.typeParameters?.length && - ctx.partials.typeParametersList(model.typeParameters, { - headingLevel: options.headingLevel, - }), model.parameters?.length && ctx.partials.parametersList(model.parameters, { headingLevel: options.headingLevel, @@ -291,23 +287,23 @@ export default ctx => { const heading = '#'.repeat(options.headingLevel); model.signatures?.forEach(signature => { - const paramsString = formatParams(signature.parameters ?? []); - - md.push(`${heading} \`new ${model.parent.name}(${paramsString})\``); + md.push(`${heading} ${getConstructorTitle(model, signature)}`); md.push( ctx.partials.signature(signature, { headingLevel: options.headingLevel + 1, + hideTypeParameters: true, }) ); }); return md.join('\n\n'); }, - typeParametersList: () => '', - - memberTitle: getMemberTitle, + memberTitle: model => + getMemberTitle(model, { local: isTypePage(ctx.page.model) }), parametersList: ctx.helpers.typedList, typeDeclarationList: ctx.helpers.typedList, propertiesTable: ctx.helpers.typedList, + + typeParametersList: () => '', }; }; diff --git a/scripts/markdown.mjs b/scripts/markdown.mjs index ed7e6edd..6c632611 100644 --- a/scripts/markdown.mjs +++ b/scripts/markdown.mjs @@ -25,6 +25,7 @@ const app = await Application.bootstrapWithPlugins({ membersWithOwnFile: ['Class'], modulesFileName: 'index', + entryFileName: 'index', tsconfig: 'tsconfig.json', }); From 7a062bb525afe26137dbb8d6bbc2ba3265f71aa8 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Mon, 25 May 2026 13:20:54 -0400 Subject: [PATCH 4/6] fixup! --- plugins/processor/router.mjs | 17 +++++++++++------ plugins/shared/titles.mjs | 4 +++- scripts/markdown.mjs | 1 + 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/processor/router.mjs b/plugins/processor/router.mjs index 92ef0d39..c5f6bef0 100644 --- a/plugins/processor/router.mjs +++ b/plugins/processor/router.mjs @@ -100,12 +100,17 @@ export const sourceAnchorName = reflection => { return; } - const baseName = sourcePageBaseName(reflection); - if (!baseName || baseName.split('/').at(-1) === reflection.name) { + const anchorName = getSourceMetadata(reflection)?.anchorName; + if (!anchorName) { return; } - return getSourceMetadata(reflection)?.anchorName; + const baseName = sourcePageBaseName(reflection.parent); + if (baseName?.split('/').at(-1) === reflection.name) { + return; + } + + return anchorName; }; const rendersHeading = (target, pageTarget) => { @@ -204,10 +209,10 @@ export class DocKitRouter extends MemberRouter { const fullUrl = this.getFullUrl(target); const [page, routedAnchor] = fullUrl.split('#'); const anchor = - routedAnchor ?? sourceAnchorName(target) ?? this.getAnchor(target); - const pageUrl = anchor ? page.replace(/\.md$/, '.html') : page; + sourceAnchorName(target) ?? routedAnchor ?? this.getAnchor(target); + const pageUrl = page.replace(/\.md$/, '.html'); - return anchor ? `${pageUrl}#${anchor}` : page; + return anchor ? `${pageUrl}#${anchor}` : pageUrl; } /** diff --git a/plugins/shared/titles.mjs b/plugins/shared/titles.mjs index 52da6ed5..97c30114 100644 --- a/plugins/shared/titles.mjs +++ b/plugins/shared/titles.mjs @@ -67,7 +67,9 @@ export const formatParams = (params = []) => params .map(({ name, flags }, i) => { const paramName = flags?.isRest ? `...${name}` : name; - return flags?.isOptional || flags?.isRest + if (flags?.isRest) return i ? `, ${paramName}` : paramName; + + return flags?.isOptional ? i ? `[, ${paramName}]` : `[${paramName}]` diff --git a/scripts/markdown.mjs b/scripts/markdown.mjs index 6c632611..6a094891 100644 --- a/scripts/markdown.mjs +++ b/scripts/markdown.mjs @@ -14,6 +14,7 @@ const app = await Application.bootstrapWithPlugins({ ], theme: 'doc-kit', router: 'doc-kit', + publicPath: '/', // Formatting hideGroupHeadings: true, From 4b9b3d2d0973996afd8327bcbfa77413924b4b8e Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Mon, 25 May 2026 14:37:05 -0400 Subject: [PATCH 5/6] fixup! --- package-lock.json | 22 ++++++++++------------ package.json | 1 + scripts/markdown.mjs | 1 + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e6cc8ef..323fa946 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "semver": "^7.8.1", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", + "typedoc-plugin-missing-exports": "^4.1.3", "webpack": "^5.107.1" }, "devDependencies": { @@ -2605,18 +2606,6 @@ "hast-util-to-html": "^9.0.5" } }, - "node_modules/@shikijs/engine-javascript": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", - "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" - } - }, "node_modules/@shikijs/engine-oniguruma": { "version": "3.23.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", @@ -8074,6 +8063,15 @@ "typedoc": "0.28.x" } }, + "node_modules/typedoc-plugin-missing-exports": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-4.1.3.tgz", + "integrity": "sha512-tgrlnwzXbqMP2/3BaZk0atddPsD7UnpCoeQ0cUCtk624gODT1bLYOLBEJLXQyVmbnP8HZCMhHpRiR+rxSdZqhg==", + "license": "MIT", + "peerDependencies": { + "typedoc": "^0.28.1" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", diff --git a/package.json b/package.json index 6b698975..cec4592c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "semver": "^7.8.1", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", + "typedoc-plugin-missing-exports": "^4.1.3", "webpack": "^5.107.1" }, "devDependencies": { diff --git a/scripts/markdown.mjs b/scripts/markdown.mjs index 6a094891..90ce3d2b 100644 --- a/scripts/markdown.mjs +++ b/scripts/markdown.mjs @@ -8,6 +8,7 @@ const app = await Application.bootstrapWithPlugins({ // Plugins plugin: [ + 'typedoc-plugin-missing-exports', 'typedoc-plugin-markdown', './plugins/processor/index.mjs', './plugins/theme/index.mjs', From 709dab98ac080ea2605b92abe552da5fbc44e171 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Mon, 25 May 2026 14:40:34 -0400 Subject: [PATCH 6/6] Revert "fixup!" This reverts commit 4b9b3d2d0973996afd8327bcbfa77413924b4b8e. --- package-lock.json | 22 ++++++++++++---------- package.json | 1 - scripts/markdown.mjs | 1 - 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 323fa946..3e6cc8ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "semver": "^7.8.1", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", - "typedoc-plugin-missing-exports": "^4.1.3", "webpack": "^5.107.1" }, "devDependencies": { @@ -2606,6 +2605,18 @@ "hast-util-to-html": "^9.0.5" } }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, "node_modules/@shikijs/engine-oniguruma": { "version": "3.23.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", @@ -8063,15 +8074,6 @@ "typedoc": "0.28.x" } }, - "node_modules/typedoc-plugin-missing-exports": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-4.1.3.tgz", - "integrity": "sha512-tgrlnwzXbqMP2/3BaZk0atddPsD7UnpCoeQ0cUCtk624gODT1bLYOLBEJLXQyVmbnP8HZCMhHpRiR+rxSdZqhg==", - "license": "MIT", - "peerDependencies": { - "typedoc": "^0.28.1" - } - }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", diff --git a/package.json b/package.json index cec4592c..6b698975 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "semver": "^7.8.1", "typedoc": "^0.28.19", "typedoc-plugin-markdown": "^4.11.0", - "typedoc-plugin-missing-exports": "^4.1.3", "webpack": "^5.107.1" }, "devDependencies": { diff --git a/scripts/markdown.mjs b/scripts/markdown.mjs index 90ce3d2b..6a094891 100644 --- a/scripts/markdown.mjs +++ b/scripts/markdown.mjs @@ -8,7 +8,6 @@ const app = await Application.bootstrapWithPlugins({ // Plugins plugin: [ - 'typedoc-plugin-missing-exports', 'typedoc-plugin-markdown', './plugins/processor/index.mjs', './plugins/theme/index.mjs',