diff --git a/components/MetaBar/index.jsx b/components/MetaBar/index.jsx
index e0ea5c31..dca634bc 100644
--- a/components/MetaBar/index.jsx
+++ b/components/MetaBar/index.jsx
@@ -17,8 +17,9 @@ function MetaBarComponent({ metadata, headings = [], readingTime }) {
.filter(sponsor => sponsor.monthly.tier === 'platinum')
.sort((a, b) => b.monthly.value - a.monthly.value);
+ // Generated API pages have no editable source, unless they name one.
const editThisPage =
- metadata.source ?? editURL.replace('{path}', metadata.path);
+ metadata.source ?? editURL?.replace('{path}', metadata.path);
const authors = metadata.authors?.split(',').map(id => ({
image: `https://avatars.githubusercontent.com/${id.trim()}`,
url: `https://github.com/${id.trim()}`,
@@ -36,12 +37,16 @@ function MetaBarComponent({ metadata, headings = [], readingTime }) {
Authors: ,
}
: {}),
- Contribute: (
- <>
-
- Edit this page
- >
- ),
+ ...(editThisPage
+ ? {
+ Contribute: (
+ <>
+
+ Edit this page
+ >
+ ),
+ }
+ : {}),
...(platinumSponsors.length
? {
[platinumSponsors.length > 1
diff --git a/package-lock.json b/package-lock.json
index ce853e49..da8f4ae8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"dependencies": {
"@doc-kit/cli": "^1.0.5",
"@doc-kit/generator-react": "^0.5.0",
+ "@doc-kit/typedoc": "^0.0.0",
"@tailwindcss/vite": "^4.3.3",
"@vercel/analytics": "^2.0.1",
"@vercel/functions": "^3.10.1",
@@ -18,7 +19,6 @@
"npm-run-all": "^4.1.5",
"semver": "^7.8.5",
"typedoc": "^0.28.20",
- "typedoc-plugin-markdown": "^4.14.0",
"webpack": "^5.112.0"
},
"devDependencies": {
@@ -47,6 +47,41 @@
"url": "https://opencollective.com/11ty"
}
},
+ "node_modules/@actions/core": {
+ "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",
+ "@actions/http-client": "^4.0.0"
+ }
+ },
+ "node_modules/@actions/exec": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
+ "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
+ "license": "MIT",
+ "dependencies": {
+ "@actions/io": "^3.0.2"
+ }
+ },
+ "node_modules/@actions/http-client": {
+ "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",
+ "undici": "^6.23.0"
+ }
+ },
+ "node_modules/@actions/io": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
+ "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==",
+ "license": "MIT"
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -360,6 +395,70 @@
"vite": "~8.3.1"
}
},
+ "node_modules/@doc-kit/typedoc": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/@doc-kit/typedoc/-/typedoc-0.0.0.tgz",
+ "integrity": "sha512-zbVhmmHMT7I2vCE/IM0vba0Wj2KbBBJDdzbw+anuW8HXSc5suvmPMOeSSMdghU2fIMbb9Ay/3JOyjVZd0QSE/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@doc-kit/core": "1.2.1",
+ "typedoc": "^0.28.20"
+ }
+ },
+ "node_modules/@doc-kit/typedoc/node_modules/@doc-kit/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@doc-kit/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-nzKO7hRMj4kYq1hPMtDtJ+Fs4A2I0OFcMlZy2/TbhAc/jeeYsEHgZU4FZmKIEhE59ebxorzoRyN52lJ16nuRLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@actions/core": "^3.0.0",
+ "@node-core/rehype-shiki": "^1.5.0",
+ "@swc/html-wasm": "^1.16.1",
+ "@swc/wasm": "^1.15.46",
+ "acorn": "^8.17.0",
+ "cosmiconfig": "^9.0.2",
+ "dedent": "^1.7.2",
+ "github-slugger": "^2.0.0",
+ "glob-parent": "^6.0.2",
+ "hastscript": "^9.0.1",
+ "mdast-util-slice-markdown": "^2.0.1",
+ "mdast-util-to-string": "^4.0.0",
+ "piscina": "^5.3.1",
+ "rehype-stringify": "^10.0.1",
+ "remark-gfm": "^4.0.1",
+ "remark-mdx": "^3.1.1",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.1.2",
+ "remark-stringify": "^11.0.0",
+ "semver": "^7.8.5",
+ "shiki": "^4.4.3",
+ "tinyglobby": "^0.2.17",
+ "unified": "^11.0.5",
+ "unist-builder": "^4.0.0",
+ "unist-util-find-after": "^5.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-remove": "^4.0.0",
+ "unist-util-select": "^5.1.0",
+ "unist-util-visit": "^5.1.0",
+ "yaml": "^2.9.0"
+ },
+ "peerDependencies": {
+ "@doc-kit/generator-react": ">=0.1.0",
+ "@node-core/doc-kit": ">=2.0.0",
+ "@node-core/doc-kit-legacy": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@doc-kit/generator-react": {
+ "optional": true
+ },
+ "@node-core/doc-kit": {
+ "optional": true
+ },
+ "@node-core/doc-kit-legacy": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
@@ -10425,6 +10524,15 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
+ },
"node_modules/twoslash": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.3.9.tgz",
@@ -10554,18 +10662,6 @@
"typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x"
}
},
- "node_modules/typedoc-plugin-markdown": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.14.0.tgz",
- "integrity": "sha512-Y2a3oDV9ekrbkk6RBSGH8GyNkh0xIdemIOOTr3tiRN/33nvFGStOs9nw+6Q84gF1XFkcVZrgM4Ckcsf6e/YpKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 18"
- },
- "peerDependencies": {
- "typedoc": "0.28.x"
- }
- },
"node_modules/typedoc-plugin-missing-exports": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-4.1.4.tgz",
@@ -10628,6 +10724,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici": {
+ "version": "6.29.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz",
+ "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
diff --git a/package.json b/package.json
index 65c4ec8c..d9f4b66d 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"dependencies": {
"@doc-kit/cli": "^1.0.5",
"@doc-kit/generator-react": "^0.5.0",
+ "@doc-kit/typedoc": "^0.0.0",
"@tailwindcss/vite": "^4.3.3",
"@vercel/analytics": "^2.0.1",
"@vercel/functions": "^3.10.1",
@@ -35,7 +36,6 @@
"npm-run-all": "^4.1.5",
"semver": "^7.8.5",
"typedoc": "^0.28.20",
- "typedoc-plugin-markdown": "^4.14.0",
"webpack": "^5.112.0"
},
"devDependencies": {
diff --git a/plugins/processor/exportEquals.mjs b/plugins/processor/exportEquals.mjs
deleted file mode 100644
index bf19537e..00000000
--- a/plugins/processor/exportEquals.mjs
+++ /dev/null
@@ -1,46 +0,0 @@
-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/extractHooksGroup.mjs b/plugins/processor/extractHooksGroup.mjs
deleted file mode 100644
index 4838b6db..00000000
--- a/plugins/processor/extractHooksGroup.mjs
+++ /dev/null
@@ -1,117 +0,0 @@
-import {
- ReflectionKind,
- ReflectionGroup,
- ParameterReflection,
- SignatureReflection,
- IntrinsicType,
-} from 'typedoc';
-
-function extractHooks(type) {
- let hooks = [];
- if (!type) return hooks;
-
- type.visit({
- reflection: t => {
- if (t.declaration?.children) {
- hooks.push(...t.declaration.children);
- }
- },
- reference: t => {
- if (t.name === 'Readonly' && t.typeArguments?.length) {
- hooks.push(...extractHooks(t.typeArguments[0]));
- } else if (t.reflection?.children) {
- hooks.push(...t.reflection.children);
- }
- },
- intersection: t => {
- if (t.types) {
- t.types.forEach(innerType => {
- hooks.push(...extractHooks(innerType));
- });
- }
- },
- });
-
- return hooks;
-}
-
-export function extractHooksGroup(reflection) {
- if (!reflection.groups) return;
-
- const propsGroup = reflection.groups.find(g => g.title === 'Properties');
- if (!propsGroup) return;
-
- const hooksIndex = propsGroup.children.findIndex(p => p.name === 'hooks');
- if (hooksIndex === -1) return;
-
- const hooksProp = propsGroup.children[hooksIndex];
-
- const individualHooks = extractHooks(hooksProp.type);
-
- if (individualHooks.length > 0) {
- propsGroup.children.splice(hooksIndex, 1);
-
- individualHooks.forEach(hook => {
- hook.kind = ReflectionKind.Method;
- const sig = new SignatureReflection(
- hook.name,
- ReflectionKind.CallSignature,
- hook
- );
-
- if (hook.type?.type === 'reference' && hook.type.typeArguments?.length) {
- const argsTuple = hook.type.typeArguments[0];
- if (argsTuple.type === 'tuple' && argsTuple.elements) {
- sig.parameters = argsTuple.elements.map((el, i) => {
- const type = el.type === 'named-tuple-member' ? el.element : el;
- let name = el.type === 'named-tuple-member' ? el.name : undefined;
- if (!name) {
- if (type.type === 'reference' && type.name) {
- name = type.name.charAt(0).toLowerCase() + type.name.slice(1);
- } else if (type.type === 'intrinsic') {
- name = type.name;
- } else {
- name = `arg${i}`;
- }
- }
- const param = new ParameterReflection(
- name,
- ReflectionKind.Parameter,
- sig
- );
- param.type = type;
- return param;
- });
- }
-
- const hookName = hook.type.name;
- if (hookName.includes('Bail') && hook.type.typeArguments.length > 1) {
- sig.type = hook.type.typeArguments[1];
- } else if (
- hookName.includes('Waterfall') &&
- argsTuple.type === 'tuple' &&
- argsTuple.elements?.length
- ) {
- const el = argsTuple.elements[0];
- sig.type = el.type === 'named-tuple-member' ? el.element : el;
- } else {
- sig.type = new IntrinsicType('void');
- }
- }
-
- hook.signatures = [sig];
- });
-
- const hooksGroup = new ReflectionGroup('Hooks', ReflectionKind.Method);
- hooksGroup.children = individualHooks;
-
- const methodsIndex = reflection.groups.findIndex(
- g => g.title === 'Methods'
- );
- if (methodsIndex !== -1) {
- reflection.groups.splice(methodsIndex + 1, 0, hooksGroup);
- } else {
- reflection.groups.push(hooksGroup);
- }
- }
-}
diff --git a/plugins/processor/index.mjs b/plugins/processor/index.mjs
deleted file mode 100644
index 9a6f6b11..00000000
--- a/plugins/processor/index.mjs
+++ /dev/null
@@ -1,117 +0,0 @@
-import { Converter, ReflectionKind, Renderer } from 'typedoc';
-import { MarkdownPageEvent } from 'typedoc-plugin-markdown';
-import { extractHooksGroup } from './extractHooksGroup.mjs';
-import { getSourceMetadata } from './metadata.mjs';
-import { writeFileSync } from 'node:fs';
-import { join } from 'node:path';
-import { applyExportEqualsReflections } from './exportEquals.mjs';
-import { applySourceMetadata } from './source.mjs';
-import { DocKitRouter } from './router.mjs';
-import { sidebar } from './site.mjs';
-import { createTypeMap } from './typeMap.mjs';
-
-/**
- * @param {import('typedoc-plugin-markdown').MarkdownApplication} app
- */
-export function load(app) {
- app.renderer.defineRouter('doc-kit', DocKitRouter);
-
- app.converter.on(Converter.EVENT_RESOLVE_BEGIN, context => {
- // doc-kit has property metadata, not TypeDoc accessor metadata.
- context.project
- .getReflectionsByKind(ReflectionKind.Accessor)
- .forEach(accessor => {
- accessor.kind = ReflectionKind.Property;
- if (accessor.getSignature) {
- accessor.type = accessor.getSignature.type;
- accessor.comment = accessor.getSignature.comment;
- } else if (accessor.setSignature) {
- accessor.type = accessor.setSignature.parameters?.[0]?.type;
- accessor.comment = accessor.setSignature.comment;
- }
- });
-
- // 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));
-
- // Destructured parameters are not supported by TypeDoc, so we rename them to
- // a more generic name.
- context.project
- .getReflectionsByKind(ReflectionKind.Parameter)
- .forEach(param => {
- if (param.name.startsWith('__namedParameters')) {
- if (
- param.type?.type === 'reflection' &&
- param.type.declaration?.children
- ) {
- const destructuredKeys = param.type.declaration.children.map(
- child => child.name
- );
- param.name = `{ ${destructuredKeys.join(', ')} }`;
- } else if (param.type?.type === 'reference' && param.type.name) {
- const interfaceName = param.type.name;
- param.name =
- interfaceName[0].toLowerCase() + interfaceName.slice(1);
- } else {
- param.name = 'options';
- }
- }
- });
-
- applyExportEqualsReflections(context.project);
- applySourceMetadata(context.project);
- });
-
- app.converter.on(Converter.EVENT_RESOLVE_END, context => {
- const project = context.project;
-
- const internalModule = project.children?.find(c => c.name === '');
-
- if (internalModule) {
- project.mergeReflections(internalModule, project);
- }
- });
-
- app.renderer.on(MarkdownPageEvent.BEGIN, page => {
- extractHooksGroup(page.model);
- });
-
- app.renderer.on(MarkdownPageEvent.END, page => {
- const sourceMeta = getSourceMetadata(page.model);
-
- if (sourceMeta && sourceMeta.sourceRelativePath) {
- const source = `https://github.com/webpack/webpack/edit/main/lib/${sourceMeta.sourceRelativePath}`;
- const frontmatter = `---\n` + `source: ${source}\n` + `---\n\n`;
-
- page.contents = frontmatter + page.contents;
- }
- });
-
- 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 = createTypeMap(app.renderer.router);
-
- writeFileSync(
- 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,
- app.options.getValue('publicPath')
- ),
- },
- null,
- 2
- )
- );
- });
-}
diff --git a/plugins/processor/metadata.mjs b/plugins/processor/metadata.mjs
deleted file mode 100644
index 63b4b8c2..00000000
--- a/plugins/processor/metadata.mjs
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * @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.
- */
-
-/**
- * @typedef {object} TypePageMetadata
- * @property {string} baseName Router base name for the synthetic page.
- * @property {string} title Rendered page title.
- */
-
-const sourceMetadata = new WeakMap();
-const typePageMetadata = new WeakMap();
-const publicNames = new WeakMap();
-
-/**
- * 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);
-};
-
-/**
- * @param {import('typedoc').Reflection} reflection
- * @returns {SourceMetadata | undefined}
- */
-export const getSourceMetadata = reflection => sourceMetadata.get(reflection);
-
-/**
- * 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);
-};
-
-/**
- * @param {import('typedoc').Reflection} reflection
- * @returns {string | undefined}
- */
-export const getTypePageTitle = reflection =>
- typePageMetadata.get(reflection)?.title;
-
-/** @param {import('typedoc').Reflection} reflection */
-export const isTypePage = reflection => typePageMetadata.has(reflection);
-
-/**
- * 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);
-};
-
-/**
- * @param {import('typedoc').Reflection} reflection
- * @returns {string | undefined}
- */
-export const getPublicName = reflection => publicNames.get(reflection);
diff --git a/plugins/processor/ownership.mjs b/plugins/processor/ownership.mjs
deleted file mode 100644
index ed3bb0dd..00000000
--- a/plugins/processor/ownership.mjs
+++ /dev/null
@@ -1,150 +0,0 @@
-import { ReflectionKind } from 'typedoc';
-
-const HOST_KINDS = ReflectionKind.Class | ReflectionKind.Namespace;
-
-const collectReferences = (type, out, depth = 0) => {
- if (!type || depth > 30) return;
-
- const next = [];
-
- switch (type.type) {
- case 'reference':
- if (type.reflection) out.add(type.reflection.id);
- next.push(...(type.typeArguments ?? []));
- break;
- case 'union':
- case 'intersection':
- next.push(...(type.types ?? []));
- break;
- case 'array':
- case 'optional':
- case 'rest':
- next.push(type.elementType);
- break;
- case 'tuple':
- next.push(...(type.elements ?? []));
- break;
- case 'namedTupleMember':
- next.push(type.element);
- break;
- case 'indexedAccess':
- next.push(type.objectType, type.indexType);
- break;
- case 'conditional':
- next.push(
- type.checkType,
- type.extendsType,
- type.trueType,
- type.falseType
- );
- break;
- case 'predicate':
- next.push(type.targetType);
- break;
- case 'query':
- next.push(type.queryType);
- break;
- case 'mapped':
- next.push(type.parameterType, type.templateType, type.nameType);
- break;
- case 'templateLiteral':
- next.push(...(type.tail ?? []).map(([tailType]) => tailType));
- break;
- case 'typeOperator':
- next.push(type.target);
- break;
- default:
- break;
- }
-
- for (const child of next) collectReferences(child, out, depth + 1);
-};
-
-// The page that hosts a referring reflection: the nearest class or namespace,
-// a root structural type (resolved transitively), or the project itself.
-// Project-level referrers still count as owners so types used by root exports
-// never look single-owned, but the project is never a co-location target.
-const hostFor = (reflection, structuralIds) => {
- let current = reflection;
-
- while (current) {
- if (current.kindOf?.(HOST_KINDS)) return current;
- if (structuralIds.has(current.id)) return current;
- if (current.isProject?.()) return current;
- current = current.parent;
- }
-
- return undefined;
-};
-
-/**
- * Map each root structural type to the single class or namespace page that
- * references it, when exactly one exists.
- */
-export const soleOwnerPages = (project, structuralTypes) => {
- const structuralIds = new Set(structuralTypes.map(type => type.id));
- const owners = new Map();
- const structuralReferrers = new Map();
-
- for (const type of structuralTypes) {
- owners.set(type.id, new Set());
- structuralReferrers.set(type.id, new Set());
- }
-
- for (const reflection of Object.values(project.reflections)) {
- const references = new Set();
-
- collectReferences(reflection.type, references);
- collectReferences(reflection.default, references);
- for (const extended of reflection.extendedTypes ?? []) {
- collectReferences(extended, references);
- }
- for (const implemented of reflection.implementedTypes ?? []) {
- collectReferences(implemented, references);
- }
-
- if (!references.size) continue;
-
- const host = hostFor(reflection, structuralIds);
- if (!host) continue;
-
- for (const id of references) {
- if (id === host.id || !owners.has(id)) continue;
-
- if (structuralIds.has(host.id)) {
- structuralReferrers.get(id).add(host.id);
- } else {
- owners.get(id).add(host);
- }
- }
- }
-
- let changed = true;
- while (changed) {
- changed = false;
-
- for (const [id, referrerIds] of structuralReferrers) {
- const ownHosts = owners.get(id);
-
- for (const referrerId of referrerIds) {
- for (const host of owners.get(referrerId)) {
- if (!ownHosts.has(host)) {
- ownHosts.add(host);
- changed = true;
- }
- }
- }
- }
- }
-
- const soleOwners = new Map();
-
- for (const type of structuralTypes) {
- const [owner, ...rest] = owners.get(type.id);
- if (owner && !rest.length && owner.kindOf?.(HOST_KINDS)) {
- soleOwners.set(type, owner);
- }
- }
-
- return soleOwners;
-};
diff --git a/plugins/processor/router.mjs b/plugins/processor/router.mjs
deleted file mode 100644
index 1adea3fc..00000000
--- a/plugins/processor/router.mjs
+++ /dev/null
@@ -1,258 +0,0 @@
-import createNodeSlugger from '@doc-kit/core/src/generators/metadata/utils/slugger.mjs';
-import { PageKind, Reflection, ReflectionKind } from 'typedoc';
-import { MemberRouter } from 'typedoc-plugin-markdown';
-import { categoryForReflection } from '../shared/categories.mjs';
-import { getConstructorTitle, getMemberTitle } from '../shared/titles.mjs';
-import { getSourceMetadata, isTypePage } from './metadata.mjs';
-import { createTypePages, TYPE_PAGE_HEADING_KINDS } from './synthetic.mjs';
-import { normalizeLink } from '../../utils/helpers/urls.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.
- */
-const sluggers = new Map();
-
-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 = categoryNameForReflection(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 = categoryNameForReflection(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 anchorName = getSourceMetadata(reflection)?.anchorName;
- if (!anchorName) {
- return;
- }
-
- const baseName = sourcePageBaseName(reflection.parent);
- if (baseName?.split('/').at(-1) === reflection.name) {
- return;
- }
-
- return anchorName;
-};
-
-const rendersHeading = (target, pageTarget) => {
- if (!target.isDeclaration() || target.kindOf(NON_HEADING_KINDS)) {
- return false;
- }
-
- return isTypePage(pageTarget) ? target.kindOf(TYPE_PAGE_HEADING_KINDS) : true;
-};
-
-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 = createTypePages(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, { includeChildren: false });
- }
- }
-
- return pages;
- }
-
- /**
- * @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 =
- sourceAnchorName(target) ?? routedAnchor ?? this.getAnchor(target);
- const pageUrl = normalizeLink(page);
-
- return anchor ? `${pageUrl}#${anchor}` : pageUrl;
- }
-
- /**
- * @param {import('typedoc').RouterTarget} target
- * @param {import('typedoc').RouterTarget} pageTarget
- * @param {{ includeChildren?: boolean }} [options]
- */
- buildAnchors(target, pageTarget, { includeChildren = true } = {}) {
- if (
- !(target instanceof Reflection) ||
- !(pageTarget instanceof Reflection)
- ) {
- return;
- }
-
- const pageUrl = this.fullUrls.get(pageTarget);
- if (!pageUrl) return;
-
- // 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;
- }
-
- const title = anchorTitle(target, pageTarget);
- const anchor = this.getSlugger(pageTarget).slug(title);
-
- this.fullUrls.set(target, `${normalizeLink(pageUrl)}#${anchor}`);
- this.anchors.set(target, anchor);
-
- if (!includeChildren) {
- return;
- }
-
- target.traverse(child => {
- this.buildAnchors(child, pageTarget);
- return true;
- });
- }
-}
diff --git a/plugins/processor/site.mjs b/plugins/processor/site.mjs
deleted file mode 100644
index 48a695b0..00000000
--- a/plugins/processor/site.mjs
+++ /dev/null
@@ -1,82 +0,0 @@
-import { ReflectionKind } from 'typedoc';
-import { CATEGORY_RULES } from '../shared/categories.mjs';
-import { toPublicLink } from '../../utils/helpers/urls.mjs';
-
-const SIDEBAR_KINDS =
- ReflectionKind.Project | ReflectionKind.Namespace | ReflectionKind.Class;
-
-const SIDEBAR_GROUP_NAME = 'API';
-
-const getFirstAtxHeading = text => text.match(/^#\s+(.+)$/m)?.[1]?.trim();
-
-const getPathSegments = url => url.replace(/^\//, '').split('/');
-
-const getFirstPathSegment = url => getPathSegments(url)[0];
-
-const getPathDepth = url => getPathSegments(url).length;
-
-const defaultLabelFor = (target, url) => {
- if (url.endsWith('/index.md')) {
- return getPathDepth(url) === 2 ? 'Overview' : target.name;
- }
- if (url.endsWith('/types.md')) return 'Types';
- return target.name;
-};
-
-const isSidebarTarget = (router, target) => {
- if (!target.kindOf?.(SIDEBAR_KINDS)) return false;
- if (!router.hasOwnDocument(target)) return false;
-
- const url = router.getFullUrl(target);
- return url.endsWith('.md') && !url.includes('#');
-};
-
-export const sidebar = (router, basePath) => {
- const categories = new Map();
- const seen = new Set();
-
- for (const target of router.getLinkTargets()) {
- if (!isSidebarTarget(router, target)) continue;
-
- const url = router.getFullUrl(target);
- 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: toPublicLink(url, basePath),
- label,
- });
-
- categories.set(category.category, group);
- }
-
- return [
- {
- groupName: SIDEBAR_GROUP_NAME,
- items: [
- ...[...categories.values()].filter(category => category.items.length),
- {
- link: toPublicLink('options', basePath),
- label: 'Options',
- },
- ],
- },
- ];
-};
diff --git a/plugins/processor/source.mjs b/plugins/processor/source.mjs
deleted file mode 100644
index f4d809b4..00000000
--- a/plugins/processor/source.mjs
+++ /dev/null
@@ -1,737 +0,0 @@
-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,
- });
-};
-
-const getWebpackPath = () => {
- const versions = JSON.parse(readFileSync('./versions.json'));
- return versions ? `./.cache/webpack/${versions[0]}/lib` : `./webpack/lib`;
-};
-
-/**
- * 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 => {
- const absoluteLibDir = resolve(getWebpackPath());
- 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
deleted file mode 100644
index d6e68ebf..00000000
--- a/plugins/processor/synthetic.mjs
+++ /dev/null
@@ -1,142 +0,0 @@
-import { Comment, DeclarationReflection, ReflectionKind } from 'typedoc';
-import { categoryForReflection } from '../shared/categories.mjs';
-import { setTypePageMetadata } from './metadata.mjs';
-import { soleOwnerPages } from './ownership.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 attachToOwner = (owner, children) => {
- for (const child of children) {
- child.parent = owner;
- owner.addChild(child);
- }
-
- owner.groups = [...(owner.groups ?? []), { title: 'Types', children }];
-};
-
-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 the page that owns them 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 ownerPages = soleOwnerPages(project, movedTypes);
- const byPage = new Map();
- const byOwner = new Map();
-
- for (const reflection of movedTypes) {
- const owner = ownerPages.get(reflection);
-
- if (owner) {
- const siblings = byOwner.get(owner) ?? [];
- siblings.push(reflection);
- byOwner.set(owner, siblings);
- continue;
- }
-
- 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);
-
- for (const [owner, children] of byOwner) {
- attachToOwner(owner, children);
- }
-
- return [...byPage].map(([baseName, children]) => ({
- baseName,
- children,
- model: createTypePage(project, baseName, children),
- }));
-};
diff --git a/plugins/processor/typeMap.mjs b/plugins/processor/typeMap.mjs
deleted file mode 100644
index 12116c65..00000000
--- a/plugins/processor/typeMap.mjs
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ReflectionKind } from 'typedoc';
-import { isTypePage } from './metadata.mjs';
-import { normalizeLink } from '../../utils/helpers/urls.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, escapedName } = target;
- const url = normalizeLink(router.getAnchoredURL(target));
-
- if (!typeMap.has(name)) {
- typeMap.set(name, url);
- }
-
- if (escapedName && !typeMap.has(escapedName)) {
- typeMap.set(escapedName, url);
- }
- }
-
- return Object.fromEntries(typeMap);
-};
diff --git a/plugins/shared/categories.mjs b/plugins/shared/categories.mjs
deleted file mode 100644
index 7af9d9b5..00000000
--- a/plugins/shared/categories.mjs
+++ /dev/null
@@ -1,152 +0,0 @@
-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.
-export const CATEGORY_RULES = [
- {
- category: 'optimize',
- label: 'Optimization',
- match: reflection => /^optimize\./i.test(reflection.getFullName()),
- },
- {
- category: 'container',
- label: 'Container',
- },
- {
- category: 'debug',
- label: 'Debug',
- },
- {
- category: 'experiments',
- label: 'Experiments',
- },
- {
- category: 'ids',
- label: 'IDs',
- },
- {
- category: 'javascript',
- label: 'JavaScript',
- },
- {
- category: 'plugins',
- label: 'Plugins',
- match: reflection =>
- reflection.kindOf(ReflectionKind.Class) &&
- reflection.name.endsWith('Plugin'),
- pattern: /^WebpackPlugin/,
- },
- {
- 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.*)$/,
- },
-];
-
-export const categoryForReflection = reflection => {
- for (const rule of CATEGORY_RULES) {
- if (rule.match?.(reflection) || rule.pattern?.test(reflection.name)) {
- return rule;
- }
- }
-
- return null;
-};
diff --git a/plugins/shared/titles.mjs b/plugins/shared/titles.mjs
deleted file mode 100644
index 1ba9d6e2..00000000
--- a/plugins/shared/titles.mjs
+++ /dev/null
@@ -1,131 +0,0 @@
-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.
-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, '\\`');
-
-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.startsWith(`${root.name}.`)) {
- return name;
- }
-
- return `${root.name}.${name}`;
-};
-
-export const formatParams = (params = []) => {
- let result = '';
- let openBrackets = 0;
-
- for (let i = 0; i < params.length; i++) {
- const { name, flags } = params[i];
- const isRest = flags?.isRest ?? false;
- const paramName = isRest ? `...${name}` : name;
-
- if (isRest || flags?.isOptional) {
- result += (i ? '[, ' : '[') + paramName;
- openBrackets++;
- } else {
- if (openBrackets) {
- result += ']'.repeat(openBrackets);
- openBrackets = 0;
- }
- result += (i ? ', ' : '') + paramName;
- }
- }
-
- return result + ']'.repeat(openBrackets);
-};
-
-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]
- : KIND_PREFIX[model.kind];
-
- return prefix ? `${prefix}: ` : '';
-};
-
-export const getMemberTitle = (model, options, specificSignature = null) => {
- const prefix = getMemberPrefix(model);
- const name = escapeCode(memberName(model, options));
-
- const params =
- specificSignature?.parameters ??
- model.parameters ??
- callableSignatures(model)[0]?.parameters;
- if (params) {
- return `${prefix}\`${escapeCode(signatureExpression(model, params, name))}\``;
- }
-
- 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/sources.mjs b/plugins/sources.mjs
new file mode 100644
index 00000000..afba0162
--- /dev/null
+++ b/plugins/sources.mjs
@@ -0,0 +1,221 @@
+import { readdirSync, readFileSync } from 'node:fs';
+import { basename, join, relative } from 'node:path';
+import ts from 'typescript';
+import { Converter, ReflectionKind, SourceReference } from 'typedoc';
+
+/**
+ * `@typedef {Type} Name` and `@callback Name`, but not the typedefs importing
+ * a type from another file (`@typedef {import("./Chunk")} Chunk`), which are
+ * not where it is declared.
+ */
+const TYPEDEF = /@(?:typedef\s+\{(?!import\().*\}|callback)\s+([\w$]+)/;
+const PROPERTY = /@property\s+\{.*\}\s+\[?([\w$]+)/;
+
+/** @param {Map} map */
+const add = (map, name, entry) =>
+ map.set(name, [...(map.get(name) ?? []), entry]);
+
+/**
+ * Where the members of a class are declared: methods, accessors and fields,
+ * then the `this.name = …` assignments of its constructor.
+ *
+ * @param {ts.ClassDeclaration} node
+ * @param {(node: ts.Node) => number} lineOf
+ */
+const classMembers = (node, lineOf) => {
+ const members = new Map();
+
+ for (const member of node.members) {
+ if (ts.isConstructorDeclaration(member)) {
+ members.set('constructor', lineOf(member));
+
+ for (const statement of member.body?.statements ?? []) {
+ const expression = ts.isExpressionStatement(statement)
+ ? statement.expression
+ : undefined;
+
+ if (
+ expression &&
+ ts.isBinaryExpression(expression) &&
+ expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
+ ts.isPropertyAccessExpression(expression.left) &&
+ expression.left.expression.kind === ts.SyntaxKind.ThisKeyword &&
+ !members.has(expression.left.name.text)
+ ) {
+ members.set(expression.left.name.text, lineOf(statement));
+ }
+ }
+ } else if (member.name && ts.isIdentifier(member.name)) {
+ if (!members.has(member.name.text)) {
+ members.set(member.name.text, lineOf(member));
+ }
+ }
+ }
+
+ return members;
+};
+
+/**
+ * Where webpack's `lib` declares its classes, top-level functions and
+ * constants, and JSDoc typedefs, by name.
+ *
+ * @param {string} root The webpack package
+ */
+const indexSources = root => {
+ const classes = new Map();
+ const values = new Map();
+ const typedefs = new Map();
+
+ const files = readdirSync(join(root, 'lib'), { recursive: true })
+ .filter(file => file.endsWith('.js'))
+ .map(file => join(root, 'lib', file));
+
+ for (const path of files) {
+ const file = relative(root, path);
+ const text = readFileSync(path, 'utf8');
+ const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest);
+ const lineOf = node =>
+ source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
+
+ for (const statement of source.statements) {
+ if (ts.isClassDeclaration(statement) && statement.name) {
+ add(classes, statement.name.text, {
+ file,
+ line: lineOf(statement),
+ members: classMembers(statement, lineOf),
+ });
+ } else if (ts.isFunctionDeclaration(statement) && statement.name) {
+ add(values, statement.name.text, { file, line: lineOf(statement) });
+ } else if (ts.isVariableStatement(statement)) {
+ for (const declaration of statement.declarationList.declarations) {
+ if (ts.isIdentifier(declaration.name)) {
+ add(values, declaration.name.text, {
+ file,
+ line: lineOf(statement),
+ });
+ }
+ }
+ }
+ }
+
+ // Typedefs are comments, so they are read line by line, with the
+ // `@property` lines of the comment each one is in.
+ const lines = text.split('\n');
+ lines.forEach((content, index) => {
+ const name = content.match(TYPEDEF)?.[1];
+ if (!name) return;
+
+ const members = new Map();
+ for (let next = index + 1; next < lines.length; next++) {
+ if (lines[next].includes('*/') || TYPEDEF.test(lines[next])) break;
+
+ const property = lines[next].match(PROPERTY)?.[1];
+ if (property && !members.has(property)) members.set(property, next + 1);
+ }
+
+ add(typedefs, name, { file, line: index + 1, members });
+ });
+ }
+
+ return { classes, values, typedefs };
+};
+
+/**
+ * The one declaration of a name, or the one in a file named after it when
+ * several files declare it.
+ *
+ * @param {object[] | undefined} entries
+ * @param {string} name
+ */
+const pick = (entries = [], name) => {
+ if (entries.length === 1) return entries[0];
+
+ const named = entries.filter(({ file }) => basename(file, '.js') === name);
+ return named.length === 1 ? named[0] : undefined;
+};
+
+/**
+ * Points the source links at the JavaScript in webpack's `lib` that
+ * implements each declaration, rather than at `types.d.ts`, which only
+ * declares it. Declarations not found there get no source link.
+ *
+ * @param {import('typedoc').Application} app
+ */
+export const load = app => {
+ app.converter.on(
+ Converter.EVENT_RESOLVE_END,
+ ({ project }) => {
+ const { classes, values, typedefs } = indexSources(
+ app.options.getValue('basePath')
+ );
+
+ /** @type {Map} */
+ const located = new Map();
+
+ /** @param {import('typedoc').Reflection} reflection */
+ const ownerOf = reflection => {
+ const { parent } = reflection;
+
+ if (parent?.kindOf(ReflectionKind.Class)) {
+ return pick(classes.get(parent.name), parent.name);
+ }
+ if (
+ parent?.kindOf(ReflectionKind.Interface | ReflectionKind.TypeAlias)
+ ) {
+ return pick(typedefs.get(parent.name), parent.name);
+ }
+ };
+
+ /** @param {import('typedoc').Reflection} reflection */
+ const find = reflection => {
+ if (reflection.isSignature?.()) return locate(reflection.parent);
+
+ if (reflection.kindOf(ReflectionKind.Class)) {
+ return pick(classes.get(reflection.name), reflection.name);
+ }
+ if (
+ reflection.kindOf(ReflectionKind.Function | ReflectionKind.Variable)
+ ) {
+ return pick(values.get(reflection.name), reflection.name);
+ }
+ if (
+ reflection.kindOf(ReflectionKind.Interface | ReflectionKind.TypeAlias)
+ ) {
+ return pick(typedefs.get(reflection.name), reflection.name);
+ }
+
+ // Inherited members are declared where they are inherited from
+ const inherited = reflection.inheritedFrom?.reflection;
+ if (inherited && inherited !== reflection) return locate(inherited);
+
+ const owner = ownerOf(reflection);
+ const name = reflection.kindOf(ReflectionKind.Constructor)
+ ? 'constructor'
+ : reflection.name;
+ const line = owner?.members.get(name);
+
+ return line ? { file: owner.file, line } : undefined;
+ };
+
+ /** @param {import('typedoc').Reflection} reflection */
+ const locate = reflection => {
+ if (!located.has(reflection)) {
+ located.set(reflection, undefined);
+ located.set(reflection, find(reflection));
+ }
+ return located.get(reflection);
+ };
+
+ for (const reflection of Object.values(project.reflections)) {
+ if (!reflection.sources) continue;
+
+ const source = locate(reflection);
+ reflection.sources = source
+ ? [new SourceReference(source.file, source.line, 0)]
+ : undefined;
+ }
+ },
+ // After `plugins/webpack.mjs` merges the declarations into the project
+ -100
+ );
+};
diff --git a/plugins/theme/helpers/index.mjs b/plugins/theme/helpers/index.mjs
deleted file mode 100644
index 8ff9fa50..00000000
--- a/plugins/theme/helpers/index.mjs
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * @typedef {object} TypedListEntry
- * @property {string} [label]
- * @property {string} [name]
- * @property {import("typedoc").SomeType | string} [type]
- * @property {import("typedoc").Comment | import("typedoc").CommentTag} [comment]
- */
-
-const formatStability = (ctx, level, tag) => {
- const message = tag.content?.length
- ? ctx.helpers.getCommentParts(tag.content).trim()
- : '';
- return message
- ? `> Stability: ${level}: ${message.replace(/\n/g, '\n> ')}`
- : `> Stability: ${level}`;
-};
-
-/**
- * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} ctx
- * @returns {import('typedoc-plugin-markdown').MarkdownThemeContext['helpers']}
- */
-export default ctx => ({
- ...ctx.helpers,
-
- typedListItem({ label, name, type, comment }) {
- const namePart = label ? ` ${label}:` : name ? ` \`${name}\`` : '';
-
- const typePart = type
- ? ` ${typeof type === 'string' ? type : ctx.partials.someType(type)}`
- : '';
-
- const descPart = comment
- ? ` ${ctx.helpers.getCommentParts(comment.summary ?? comment.content)}`
- : '';
-
- return `*${namePart}${typePart}${descPart}`;
- },
-
- typedList(entries) {
- return entries.map(ctx.helpers.typedListItem).join('\n');
- },
-
- examples(comment) {
- const examples =
- comment?.blockTags?.filter(t => t.tag === '@example') ?? [];
- if (!examples.length) return null;
-
- // 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();
- 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');
- },
-
- stabilityBlockquote(comment) {
- if (!comment) return null;
-
- const deprecated = comment.blockTags?.find(t => t.tag === '@deprecated');
- if (deprecated) return formatStability(ctx, '0 - Deprecated', deprecated);
-
- if (
- comment.modifierTags?.has('@experimental') ||
- comment.modifierTags?.has('@beta')
- ) {
- return '> Stability: 1 - Experimental';
- }
-
- const legacy = comment.blockTags?.find(t => t.tag === '@legacy');
- if (legacy) return formatStability(ctx, '3 - Legacy', legacy);
-
- return null;
- },
-});
diff --git a/plugins/theme/index.mjs b/plugins/theme/index.mjs
deleted file mode 100644
index 245434ea..00000000
--- a/plugins/theme/index.mjs
+++ /dev/null
@@ -1,26 +0,0 @@
-import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown';
-import helpers from './helpers/index.mjs';
-import partials from './partials/index.mjs';
-
-export class DocKitTheme extends MarkdownTheme {
- getRenderContext(page) {
- return new DocKitThemeContext(this, page, this.application.options);
- }
-}
-
-export class DocKitThemeContext extends MarkdownThemeContext {
- helpers = helpers(this);
-
- partials = partials(this);
-
- templates = {
- ...this.templates,
- };
-}
-
-/**
- * @param {import('typedoc-plugin-markdown').MarkdownApplication} app
- */
-export function load(app) {
- app.renderer.defineTheme('doc-kit', DocKitTheme);
-}
diff --git a/plugins/theme/partials/index.mjs b/plugins/theme/partials/index.mjs
deleted file mode 100644
index e3cad8b6..00000000
--- a/plugins/theme/partials/index.mjs
+++ /dev/null
@@ -1,395 +0,0 @@
-import { getTypePageTitle, isTypePage } from '../../processor/metadata.mjs';
-import {
- ArrayType,
- i18n,
- IntersectionType,
- ReflectionKind,
- ReflectionType,
- UnionType,
-} from 'typedoc';
-import {
- callableSignatures,
- getConstructorTitle,
- getMemberTitle,
- getPageTitle,
-} from '../../shared/titles.mjs';
-import * as typePartials from './types.mjs';
-
-const heading = (level, title) => `${'#'.repeat(level)} ${title}`;
-
-// 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;
-
-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 => {
- return {
- ...ctx.partials,
- ...typePartials,
-
- pageTitle() {
- const model = ctx.page.model;
- const typePageTitle = getTypePageTitle(model);
- if (typePageTitle) return typePageTitle;
- 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.parameters?.length &&
- ctx.partials.parametersList(model.parameters),
- ctx.helpers.typedListItem({
- label: 'Returns',
- type: model.type ?? 'void',
- comment: model.comment?.getTag('@returns'),
- }),
- '',
- comment &&
- ctx.partials.comment(comment, {
- headingLevel: options.headingLevel,
- showTags: false,
- }),
- ctx.helpers.examples(comment, options),
- ]
- .filter(x => typeof x === 'string' || Boolean(x))
- .join('\n');
- },
-
- signatures(model, options) {
- const md = [];
- const multipleSignatures =
- model.signatures && model.signatures?.length > 1;
-
- model.signatures?.forEach(signature => {
- if (multipleSignatures) {
- md.push(
- heading(
- options.headingLevel,
- getMemberTitle(
- model,
- { local: isTypePage(ctx.page.model) },
- signature
- )
- )
- );
- }
- md.push(
- ctx.partials.signature(signature, {
- headingLevel: multipleSignatures
- ? options.headingLevel + 1
- : options.headingLevel,
- nested: options.nested,
- multipleSignatures: false,
- })
- );
- });
- return md.join('\n\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');
- }
-
- const comment = model.comment;
- const stability = ctx.helpers.stabilityBlockquote(comment);
- const md = [
- stability,
- stability && '',
- ctx.partials.declarationTitle(model),
- ];
-
- if (comment) {
- md.push(
- '',
- ctx.partials.comment(comment, {
- headingLevel: opts.headingLevel,
- showSummary: true,
- showTags: false,
- })
- );
- }
-
- 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.inheritance(model, { headingLevel: opts.headingLevel })
- );
-
- return md.filter(x => typeof x === 'string' || Boolean(x)).join('\n\n');
- },
-
- declarationTitle(model) {
- return ctx.helpers.typedListItem({
- label: 'Type',
- type: declarationType(model) ?? 'unknown',
- });
- },
-
- 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,
- });
- },
-
- memberContainer(model, options) {
- const md = [];
-
- const isOverloadedMethod =
- model.signatures &&
- model.signatures.length > 1 &&
- [ReflectionKind.Function, ReflectionKind.Method].includes(model.kind);
-
- if (
- !isOverloadedMethod &&
- !ctx.router.hasOwnDocument(model) &&
- model.kind !== ReflectionKind.Constructor
- ) {
- md.push(heading(options.headingLevel, ctx.partials.memberTitle(model)));
- }
-
- md.push(
- ctx.partials.member(model, {
- headingLevel: options.headingLevel,
- nested: options.nested,
- })
- );
- return md.join('\n\n');
- },
-
- 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 && !model.typeHierarchy.isTarget) {
- md.push(
- ctx.helpers.typedListItem({
- label: 'Extends',
- type: model.typeHierarchy.types[0],
- })
- );
- }
-
- const signatures = callableSignatures(model);
- const isCallable = signatures.length > 0;
-
- if (!isCallable && model.typeParameters?.length) {
- const typeParametersContent = ctx.partials.typeParametersList(
- model.typeParameters,
- {
- headingLevel: options.headingLevel,
- }
- );
- if (typeParametersContent) {
- md.push(
- heading(
- options.headingLevel,
- ReflectionKind.pluralString(ReflectionKind.TypeParameter)
- )
- );
- md.push(typeParametersContent);
- }
- }
-
- 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,
- getMemberTitle(
- model,
- { local: isTypePage(ctx.page.model) },
- 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 => {
- md.push(`${heading} ${getConstructorTitle(model, signature)}`);
- md.push(
- ctx.partials.signature(signature, {
- headingLevel: options.headingLevel + 1,
- hideTypeParameters: true,
- })
- );
- });
- return md.join('\n\n');
- },
-
- memberTitle: model =>
- getMemberTitle(model, { local: isTypePage(ctx.page.model) }),
- parametersList(parameters) {
- const flatList = [];
-
- parameters.forEach(param => {
- if (
- param.type?.type === 'reflection' &&
- param.type.declaration?.children
- ) {
- flatList.push(...param.type.declaration.children);
- } else {
- flatList.push(param);
- }
- });
-
- return ctx.helpers.typedList(flatList);
- },
- typeDeclarationList: ctx.helpers.typedList,
- propertiesTable: ctx.helpers.typedList,
-
- typeParametersList: () => '',
- };
-};
diff --git a/plugins/theme/partials/types.mjs b/plugins/theme/partials/types.mjs
deleted file mode 100644
index a638ddec..00000000
--- a/plugins/theme/partials/types.mjs
+++ /dev/null
@@ -1,20 +0,0 @@
-export const someType = model => `{${model ?? 'unknown'}}`;
-
-export const arrayType = someType,
- conditionalType = someType,
- indexAccessType = someType,
- inferredType = someType,
- intersectionType = someType,
- intrinsicType = someType,
- literalType = someType,
- declarationType = someType,
- functionType = someType,
- namedTupleType = someType,
- optionalType = someType,
- queryType = someType,
- referenceType = someType,
- reflectionType = someType,
- tupleType = someType,
- typeOperatorType = someType,
- unionType = someType,
- unknownType = someType;
diff --git a/plugins/webpack.mjs b/plugins/webpack.mjs
new file mode 100644
index 00000000..07c3e3b1
--- /dev/null
+++ b/plugins/webpack.mjs
@@ -0,0 +1,37 @@
+import { Converter, ReflectionKind } from 'typedoc';
+
+/**
+ * Containers TypeDoc puts webpack's declarations in, which `@doc-kit/typedoc`
+ * does not look into: it documents the children of the project (or of its
+ * entry-point modules).
+ *
+ * - `export=`: webpack's `types.d.ts` is a CommonJS declaration file, and
+ * `export = exports` parks the real exports in a namespace of that name.
+ * - ``: `typedoc-plugin-missing-exports` collects the unexported
+ * types the exports refer to in a module of that name.
+ */
+const CONTAINERS = new Set(['export=', '']);
+
+/**
+ * Merges both containers into the project, so their declarations get pages.
+ * The callable side of `export =` (a function named `export=`) is dropped:
+ * it is `webpack()`, which the `webpack` variable already documents.
+ *
+ * @param {import('typedoc').Application} app
+ */
+export const load = app => {
+ app.converter.on(Converter.EVENT_RESOLVE_END, ({ project }) => {
+ const containers = (project.children ?? []).filter(
+ child =>
+ child.kindOf(ReflectionKind.SomeModule) && CONTAINERS.has(child.name)
+ );
+
+ for (const container of containers) {
+ project.mergeReflections(container, project);
+ }
+
+ for (const child of [...(project.children ?? [])]) {
+ if (child.name === 'export=') project.removeReflection(child);
+ }
+ });
+};
diff --git a/scripts/html/doc-kit.config.mjs b/scripts/html/doc-kit.config.mjs
index 2c4c48e7..51cee2b4 100644
--- a/scripts/html/doc-kit.config.mjs
+++ b/scripts/html/doc-kit.config.mjs
@@ -48,10 +48,9 @@ export default {
: undefined,
},
- 'jsx-ast': {
- generateIndexPage: false,
- generateAllPage: false,
- },
+ // Source links point at the published version's tag, whose line numbers
+ // the API reference was generated from.
+ 'jsx-ast': VERSION ? { ref: VERSION } : {},
'llms-txt': {
templatePath: join(ROOT, 'scripts/html/llms-template.txt'),
@@ -59,11 +58,14 @@ export default {
},
html: {
+ generateAllPage: false,
useAbsoluteURLs: true,
remoteConfigUrl: '/assets/banners.json',
title: VERSION ? `Webpack ${MAJOR_VERSION} Documentation` : 'Webpack',
- editURL:
- 'https://github.com/webpack/webpack.js.org/blob/main/pages/{path}.md',
+ // The versioned API reference is generated, so it has nothing to edit.
+ editURL: VERSION
+ ? undefined
+ : 'https://github.com/webpack/webpack.js.org/blob/main/pages/{path}.md',
head: {
meta: [
{
diff --git a/scripts/markdown/api/index.mjs b/scripts/markdown/api/index.mjs
index 848a7af3..3c748a9e 100644
--- a/scripts/markdown/api/index.mjs
+++ b/scripts/markdown/api/index.mjs
@@ -1,6 +1,8 @@
+import { readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path/posix';
import { Application } from 'typedoc';
import { major } from 'semver';
+import { buildIndex, buildSidebar } from './sidebar.mjs';
import {
getSources,
outputDir,
@@ -8,18 +10,57 @@ import {
typeDocConfig,
} from './utils.mjs';
+const readJson = async file => JSON.parse(await readFile(file, 'utf8'));
+
+const writeJson = (file, value) =>
+ writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
+
+/**
+ * doc-kit resolves type-map entries relative to the root of its input, which
+ * for a versioned build is the version's directory rather than the site root
+ * the plugin's paths start from. Strip the base path from the entries.
+ */
+const localizeTypeMap = async (out, basePath) => {
+ const file = join(out, 'type-map.json');
+ const prefix = `${basePath.slice(1)}/`;
+ const typeMap = await readJson(file);
+
+ for (const [name, url] of Object.entries(typeMap)) {
+ if (url.startsWith(prefix)) typeMap[name] = url.slice(prefix.length);
+ }
+
+ await writeJson(file, typeMap);
+};
+
const generate = async packageDir => {
const { version } = await getPackageFile(packageDir);
+ const out = join(outputDir, `v${major(version)}.x`);
+ const basePath = `/docs/api/v${major(version)}.x`;
+
+ // The plugin doesn't empty its output, so pages of removed exports linger.
+ await rm(out, { recursive: true, force: true });
const app = await Application.bootstrapWithPlugins({
...typeDocConfig,
entryPoints: [join(packageDir, 'types.d.ts')],
- out: join(outputDir, `v${major(version)}.x`),
- publicPath: `/docs/api/v${major(version)}.x/`,
+ // Source links are relative to this: the root of webpack's repository
+ basePath: packageDir,
+ docKit: out,
+ docKitBasePath: basePath,
});
const project = await app.convert();
+ if (!project) throw new Error(`TypeDoc failed to convert ${packageDir}`);
+
await app.generateOutputs(project);
+
+ const pages = await readJson(join(out, 'pages.json'));
+ await writeJson(join(out, 'site.json'), {
+ sidebar: buildSidebar(pages, basePath),
+ });
+ // The plugin leaves the project's own page to the site.
+ await writeFile(join(out, 'index.md'), buildIndex(pages, basePath, version));
+ await localizeTypeMap(out, basePath);
};
const sources = await getSources();
diff --git a/scripts/markdown/api/sidebar.mjs b/scripts/markdown/api/sidebar.mjs
new file mode 100644
index 00000000..d81decab
--- /dev/null
+++ b/scripts/markdown/api/sidebar.mjs
@@ -0,0 +1,171 @@
+/** The groups of the API reference, with the page kinds they list. */
+const GROUPS = [
+ { label: 'Namespaces', kinds: ['Namespace', 'Module'] },
+ { label: 'Classes', kinds: ['Class'] },
+ { label: 'Functions', kinds: ['Function'] },
+ { label: 'Variables', kinds: ['Variable'] },
+ { label: 'Interfaces', kinds: ['Interface'] },
+ { label: 'Enums', kinds: ['Enum'] },
+ { label: 'Type Aliases', kinds: ['TypeAlias'] },
+ { label: 'Members', kinds: ['Property', 'Method', 'Accessor'] },
+];
+
+/**
+ * The pages of each group, leaving out groups without pages.
+ *
+ * @param {Array} pages
+ * @param {typeof GROUPS} groups
+ */
+const groupPages = (pages, groups) =>
+ groups
+ .map(({ label, kinds }) => ({
+ label,
+ pages: pages.filter(page => kinds.includes(page.kind)),
+ }))
+ .filter(group => group.pages.length);
+
+/**
+ * Every page of the reference embeds the sidebar, so it only lists these
+ * kinds; the others are linked from the index page.
+ */
+const SIDEBAR_KINDS = ['Namespace', 'Module', 'Class', 'Function'];
+
+const NAMESPACE_KINDS = ['Namespace', 'Module'];
+
+const byLabel = (a, b) => a.label.localeCompare(b.label);
+
+/**
+ * The pages directly in a namespace, split into its namespaces and the rest.
+ *
+ * @param {Array} pages
+ * @param {string} prefix The namespace's full name and a dot, or `''` for the
+ * top level
+ */
+const childrenOf = (pages, prefix) => {
+ const own = pages.filter(
+ page =>
+ page.name.startsWith(prefix) &&
+ !page.name.slice(prefix.length).includes('.')
+ );
+
+ return {
+ namespaces: own.filter(page => NAMESPACE_KINDS.includes(page.kind)),
+ others: own.filter(page => !NAMESPACE_KINDS.includes(page.kind)),
+ };
+};
+
+/**
+ * Sidebar links to pages, labelled by their names within the namespace.
+ *
+ * @param {Array} entries
+ * @param {string} prefix
+ */
+const links = (entries, prefix) =>
+ entries
+ .map(page => ({ link: page.url, label: page.name.slice(prefix.length) }))
+ .sort(byLabel);
+
+/**
+ * A namespace collapsed into a sub-group: its own page, its namespaces, then
+ * its classes and functions. A namespace exporting none of these is a link.
+ *
+ * @param {Array} pages
+ * @param {import('@doc-kit/typedoc/src/types').PageEntry} namespace
+ * @param {string} prefix The prefix of the namespace's own name
+ */
+const namespaceItem = (pages, namespace, prefix) => {
+ const label = namespace.name.slice(prefix.length);
+ const items = namespaceItems(pages, `${namespace.name}.`);
+
+ return items.length
+ ? { label, items: [{ link: namespace.url, label: 'Overview' }, ...items] }
+ : { link: namespace.url, label };
+};
+
+/**
+ * @param {Array} pages
+ * @param {string} prefix
+ */
+const namespaceItems = (pages, prefix) => {
+ const { namespaces, others } = childrenOf(pages, prefix);
+
+ return [
+ ...namespaces
+ .map(namespace => namespaceItem(pages, namespace, prefix))
+ .sort(byLabel),
+ ...links(others, prefix),
+ ];
+};
+
+/**
+ * The sidebar of a versioned API reference, from the page list written by
+ * `@doc-kit/typedoc`: the overview, the namespaces (collapsed into sub-groups
+ * of what they export), the top-level classes and functions, then the
+ * configuration options page.
+ *
+ * @param {Array} pages
+ * @param {string} basePath The URL path the reference is served under
+ */
+export const buildSidebar = (pages, basePath) => {
+ const listed = pages.filter(page => SIDEBAR_KINDS.includes(page.kind));
+ const { namespaces, others } = childrenOf(listed, '');
+
+ const groups = [
+ {
+ label: 'Namespaces',
+ items: namespaces
+ .map(namespace => namespaceItem(listed, namespace, ''))
+ .sort(byLabel),
+ },
+ {
+ label: 'Classes',
+ items: links(
+ others.filter(page => page.kind === 'Class'),
+ ''
+ ),
+ },
+ {
+ label: 'Functions',
+ items: links(
+ others.filter(page => page.kind === 'Function'),
+ ''
+ ),
+ },
+ ].filter(group => group.items.length);
+
+ return [
+ {
+ groupName: 'API',
+ items: [
+ { link: basePath, label: 'Overview' },
+ ...groups,
+ { link: `${basePath}/options`, label: 'Options' },
+ ],
+ },
+ ];
+};
+
+/**
+ * The index page of a versioned API reference, linking to every page.
+ *
+ * @param {Array} pages
+ * @param {string} basePath The URL path the reference is served under
+ * @param {string} version The documented webpack version
+ */
+export const buildIndex = (pages, basePath, version) => {
+ const lines = [
+ '# API',
+ '',
+ `The API of webpack ${version}, generated from its type declarations. ` +
+ 'Its configuration is documented in [Configuration Options](options.md).',
+ ];
+
+ for (const { label, pages: entries } of groupPages(pages, GROUPS)) {
+ lines.push('', `## ${label}`, '');
+ for (const { name, url } of entries) {
+ lines.push(`* [\`${name}\`](${url.slice(basePath.length + 1)}.md)`);
+ }
+ }
+
+ return `${lines.join('\n')}\n`;
+};
diff --git a/scripts/markdown/api/utils.mjs b/scripts/markdown/api/utils.mjs
index e854cc62..44295eee 100644
--- a/scripts/markdown/api/utils.mjs
+++ b/scripts/markdown/api/utils.mjs
@@ -1,5 +1,6 @@
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path/posix';
+import { load } from '@doc-kit/typedoc';
const [packageDir] = process.argv.slice(2);
const cacheDir = join('.', '.cache', 'webpack');
@@ -19,24 +20,13 @@ export const getPackageFile = async (packageDir, file = 'package.json') =>
export const typeDocConfig = {
plugin: [
- 'typedoc-plugin-markdown',
+ load,
'typedoc-plugin-missing-exports',
- './plugins/processor/index.mjs',
- './plugins/theme/index.mjs',
+ './plugins/webpack.mjs',
+ './plugins/sources.mjs',
],
- theme: 'doc-kit',
- router: 'doc-kit',
- hideGroupHeadings: true,
- hideBreadcrumbs: true,
- hidePageHeader: true,
readme: 'none',
- disableSources: true,
- propertiesFormat: 'table',
- membersWithOwnFile: ['Class'],
-
- modulesFileName: 'index',
- entryFileName: 'index',
tsconfig: 'tsconfig.json',
excludeExternals: true,
};
diff --git a/tests/sidebar.test.mjs b/tests/sidebar.test.mjs
new file mode 100644
index 00000000..d8a898bf
--- /dev/null
+++ b/tests/sidebar.test.mjs
@@ -0,0 +1,133 @@
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import { buildIndex, buildSidebar } from '../scripts/markdown/api/sidebar.mjs';
+
+const BASE = '/docs/api/v5.x';
+
+const pages = [
+ {
+ name: 'optimize.SplitChunksPlugin',
+ kind: 'Class',
+ url: `${BASE}/classes/optimize.SplitChunksPlugin`,
+ },
+ { name: 'Compiler', kind: 'Class', url: `${BASE}/classes/Compiler` },
+ { name: 'optimize', kind: 'Namespace', url: `${BASE}/modules/optimize` },
+ { name: 'webpack', kind: 'Function', url: `${BASE}/functions/webpack` },
+ { name: 'version', kind: 'Variable', url: `${BASE}/variables/version` },
+ {
+ name: 'Configuration',
+ kind: 'Interface',
+ url: `${BASE}/interfaces/Configuration`,
+ },
+ {
+ name: 'Configuration.output',
+ kind: 'Property',
+ url: `${BASE}/interfaces/Configuration.output`,
+ },
+];
+
+test('lists namespaces, classes and functions between the overview and the options page', () => {
+ assert.deepEqual(buildSidebar(pages, BASE), [
+ {
+ groupName: 'API',
+ items: [
+ { link: BASE, label: 'Overview' },
+ {
+ label: 'Namespaces',
+ items: [
+ {
+ label: 'optimize',
+ items: [
+ { link: `${BASE}/modules/optimize`, label: 'Overview' },
+ {
+ link: `${BASE}/classes/optimize.SplitChunksPlugin`,
+ label: 'SplitChunksPlugin',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ label: 'Classes',
+ items: [{ link: `${BASE}/classes/Compiler`, label: 'Compiler' }],
+ },
+ {
+ label: 'Functions',
+ items: [{ link: `${BASE}/functions/webpack`, label: 'webpack' }],
+ },
+ { link: `${BASE}/options`, label: 'Options' },
+ ],
+ },
+ ]);
+});
+
+test('nests namespaces in namespaces, and links those exporting nothing listed', () => {
+ const nested = [
+ { name: 'util', kind: 'Namespace', url: `${BASE}/modules/util` },
+ {
+ name: 'util.runtime',
+ kind: 'Namespace',
+ url: `${BASE}/modules/util.runtime`,
+ },
+ {
+ name: 'util.runtime.forEachRuntime',
+ kind: 'Function',
+ url: `${BASE}/functions/util.runtime.forEachRuntime`,
+ },
+ {
+ name: 'RuntimeGlobals',
+ kind: 'Namespace',
+ url: `${BASE}/modules/RuntimeGlobals`,
+ },
+ ];
+
+ assert.deepEqual(buildSidebar(nested, BASE)[0].items[1], {
+ label: 'Namespaces',
+ items: [
+ { link: `${BASE}/modules/RuntimeGlobals`, label: 'RuntimeGlobals' },
+ {
+ label: 'util',
+ items: [
+ { link: `${BASE}/modules/util`, label: 'Overview' },
+ {
+ label: 'runtime',
+ items: [
+ { link: `${BASE}/modules/util.runtime`, label: 'Overview' },
+ {
+ link: `${BASE}/functions/util.runtime.forEachRuntime`,
+ label: 'forEachRuntime',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+});
+
+test('leaves out the groups of kinds without pages', () => {
+ assert.deepEqual(buildSidebar([], BASE), [
+ {
+ groupName: 'API',
+ items: [
+ { link: BASE, label: 'Overview' },
+ { link: `${BASE}/options`, label: 'Options' },
+ ],
+ },
+ ]);
+});
+
+test('links every page from the index, by kind', () => {
+ const index = buildIndex(pages, BASE, '5.0.0');
+
+ assert.match(index, /^# API\n/);
+ assert.match(
+ index,
+ /## Variables\n\n\* \[`version`\]\(variables\/version\.md\)/
+ );
+ assert.match(
+ index,
+ /## Members\n\n\* \[`Configuration.output`\]\(interfaces\/Configuration\.output\.md\)/
+ );
+ assert.doesNotMatch(index, /## Enums/);
+});
diff --git a/tests/theme/fixtures/input.js b/tests/theme/fixtures/input.js
deleted file mode 100644
index a8b922f9..00000000
--- a/tests/theme/fixtures/input.js
+++ /dev/null
@@ -1,218 +0,0 @@
-/* eslint-disable */
-
-/**
- * Class demonstrating Webpack as a parent class.
- */
-export class WebpackTest {}
-
-/**
- * Interface demonstrating Webpack as an interface.
- * @interface
- */
-export class WebpackInterface {}
-
-/**
- * Class demonstrating overload signatures.
- */
-export class OverloadTest {
- /**
- * First overload signature.
- * @overload
- * @param {string} name A simple string parameter
- * @param {number} count A simple number parameter
- * @returns {void}
- */
- /**
- * Second overload signature.
- * @overload
- * @param {boolean} isEnabled A simple boolean parameter
- * @returns {string}
- */
- /**
- * Third overload signature.
- * @overload
- * @param {number} id A simple string parameter
- * @param {boolean} isEnabled A simple boolean parameter
- * @returns {void}
- */
- overloadMethod(id, isEnabled) {}
-}
-
-/**
- * Class demonstrating stability for deprecated, experimental, and legacy APIs.
- */
-export class StabilityTest {
- /**
- * @deprecated Use the new API instead.
- */
- deprecatedMethod() {}
-
- /**
- * @experimental
- */
- experimentalMethod() {}
-
- /**
- * @legacy This is a legacy Webpack v4 API.
- */
- legacyMethod() {}
-}
-
-/**
- * Class demonstrating the formatting of JSDoc example blocks.
- */
-export class ExamplesTest {
- /**
- * A sample method for the ExamplesTest class.
- * @param {string} name A simple string parameter
- * @returns {void}
- * @example
- * exampleMethod('example-1');
- *
- * @example
- * ```ts
- * exampleMethod('example-2');
- * ```
- */
- exampleMethod(name) {}
-}
-
-/**
- * Class demonstrating properties and typed lists.
- */
-export class TypedListTest extends WebpackTest {
- /**
- * A sample property for the TypedListTest class.
- * @type {string}
- */
- typedListProperty;
-
- /**
- * A sample method for the TypedListTest class.
- * @param {string} name The name of the instance
- * @param {number} count The count of items
- * @returns {boolean} A boolean indicating success
- */
- typedListMethod(name, count) {}
-}
-
-/**
- * Complex array type.
- * @typedef {Array<{ name: string, age: number }>} ComplexArrayType
- */
-
-/**
- * Simple array type.
- * @typedef {Array} SimpleArrayType
- */
-
-/**
- * Complex union type.
- * @typedef {string | { advanced: boolean, timeout: number }} ComplexUnionType
- */
-
-/**
- * Simple union type.
- * @typedef {string | number} SimpleUnionType
- */
-
-/**
- * Complex intersection type.
- * @typedef {{ id: string } & { metadata: object }} ComplexIntersectionType
- */
-
-/**
- * Simple intersection type.
- * @typedef {number & string} SimpleIntersectionType
- */
-
-/**
- * A standalone constant demonstrating declarationTitle formatting and variable rendering.
- * @type {string}
- */
-export const CONSTANT_AND_DECLARATION_TITLE_TEST = 'pass me';
-
-/**
- * Class demonstrating indexSignature rendering.
- */
-export class IndexSignatureTest {
- /**
- * An object property to trigger indexSignature.
- * @type {{ [key: string]: number }}
- */
- dictionaryProperty;
-}
-
-/**
- * Class demonstrating Implements and Constructor grouping.
- * @implements {WebpackInterface}
- */
-export class MemberGroupsTest {
- /**
- * The constructor for MemberGroupsTest.
- * @param {any} data
- */
- constructor(data) {}
-
- /**
- * A categorized method.
- * @category Lifecycle
- */
- init() {}
-}
-
-/**
- * A simple enum demonstrating enum rendering.
- * @enum {number}
- */
-export const EnumTest = {
- /** The first option */
- ONE: 1,
- /** The second option */
- TWO: 2,
-};
-
-/**
- * A standalone function demonstrating generics outside a class.
- * @template T
- * @param {T} value The input value
- * @returns {T} The same value
- */
-export function standAloneWithGenericsTest(value) {
- return value;
-}
-
-/**
- * Class demonstrating Getters and Setters (Accessors).
- */
-export class AccessorTest {
- /**
- * Gets the current configuration name.
- * @returns {string}
- */
- get configName() {
- return '';
- }
-
- /**
- * Sets the configuration name.
- * @param {string} val The new name
- */
- set configName(val) {}
-}
-
-/**
- * A namespace demonstrating nested members.
- * @namespace
- */
-export const WebpackUtils = {
- /**
- * Merges two configurations.
- * @param {object} a First config
- * @param {object} b Second config
- * @returns {object} Merged config
- */
- merge(a, b) {
- return Object.assign({}, a, b);
- },
-};
diff --git a/tests/theme/fixtures/tsconfig.json b/tests/theme/fixtures/tsconfig.json
deleted file mode 100644
index ff8f0ace..00000000
--- a/tests/theme/fixtures/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "include": ["**/*.js"],
- "compilerOptions": {
- "allowJs": true,
- "esModuleInterop": true,
- "declaration": true,
- "emitDeclarationOnly": true,
- "outDir": "../.temp/types",
- "skipLibCheck": true
- }
-}
diff --git a/tests/theme/theme.test.mjs b/tests/theme/theme.test.mjs
deleted file mode 100644
index 93e67bef..00000000
--- a/tests/theme/theme.test.mjs
+++ /dev/null
@@ -1,45 +0,0 @@
-import { test } from 'node:test';
-import { Application } from 'typedoc';
-import { readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
-import { join } from 'node:path/posix';
-import { execSync } from 'node:child_process';
-import { typeDocConfig } from '../../scripts/markdown/api/utils.mjs';
-
-const fixturesDir = './tests/theme/fixtures';
-const outputDir = './tests/theme/.temp';
-const typesDir = './tests/theme/.temp/types';
-const tsconfig = { include: ['**/*.d.ts'] };
-
-test('TypeDoc Theme - Edge Cases Fixture', async t => {
- execSync(`npx tsc -p ${join(fixturesDir, 'tsconfig.json')}`, {
- stdio: 'inherit',
- });
- writeFileSync(join(typesDir, 'tsconfig.json'), JSON.stringify(tsconfig));
-
- const app = await Application.bootstrapWithPlugins({
- ...typeDocConfig,
- entryPoints: [join(typesDir, 'input.d.ts')],
- out: outputDir,
- publicPath: '/',
- tsconfig: join(typesDir, 'tsconfig.json'),
- });
-
- const project = await app.convert();
- await app.generateOutputs(project);
-
- const mdFiles = readdirSync(outputDir, { recursive: true }).filter(f =>
- f.endsWith('.md')
- );
-
- if (mdFiles.length === 0) throw new Error('No markdown file generated');
-
- mdFiles.sort();
-
- const actualMarkdown = mdFiles
- .map(f => readFileSync(join(outputDir, f), 'utf-8'))
- .join('\n\n---\n\n');
-
- rmSync(outputDir, { recursive: true, force: true });
-
- t.assert.snapshot(actualMarkdown, { serializers: [val => val] });
-});
diff --git a/tests/theme/theme.test.mjs.snapshot b/tests/theme/theme.test.mjs.snapshot
deleted file mode 100644
index 4280cdec..00000000
--- a/tests/theme/theme.test.mjs.snapshot
+++ /dev/null
@@ -1,363 +0,0 @@
-exports[`TypeDoc Theme - Edge Cases Fixture 1`] = `
-# Class: \`webpack.js.org.AccessorTest\`
-
-Class demonstrating Getters and Setters (Accessors).
-
-## Constructors
-
-### \`new AccessorTest()\`
-
-* Returns: {AccessorTest}
-
-## Properties
-
-* \`configName\` {string} Gets the current configuration name.
-
-
----
-
-# \`webpack.js.org.EnumTest\`
-
-## \`ONE\`
-
-* Type: {number}
-
-***
-
-## \`TWO\`
-
-* Type: {number}
-
-
----
-
-# Class: \`webpack.js.org.ExamplesTest\`
-
-Class demonstrating the formatting of JSDoc example blocks.
-
-## Constructors
-
-### \`new ExamplesTest()\`
-
-* Returns: {ExamplesTest}
-
-## Methods
-
-### \`exampleMethod(name)\`
-
-* \`name\` {string} A simple string parameter
-* Returns: {void}
-
-A sample method for the ExamplesTest class.
-\`\`\`ts
-exampleMethod('example-1');
-\`\`\`
-
-\`\`\`ts
-exampleMethod('example-2');
-\`\`\`
-
-
----
-
-# Class: \`webpack.js.org.IndexSignatureTest\`
-
-Class demonstrating indexSignature rendering.
-
-## Constructors
-
-### \`new IndexSignatureTest()\`
-
-* Returns: {IndexSignatureTest}
-
-## Properties
-
-* \`dictionaryProperty\` {{ [key: string]: number }} An object property to trigger indexSignature.
-
-
----
-
-# Class: \`webpack.js.org.MemberGroupsTest\`
-
-Class demonstrating Implements and Constructor grouping.
-
-## Implements
-
-* {WebpackInterface}
-
-## Constructors
-
-### \`new MemberGroupsTest(data)\`
-
-* \`data\` {any}
-* Returns: {MemberGroupsTest}
-
-The constructor for MemberGroupsTest.
-
-## Lifecycle
-
-### \`init()\`
-
-* Returns: {void}
-
-A categorized method.
-
-
----
-
-# Class: \`webpack.js.org.OverloadTest\`
-
-Class demonstrating overload signatures.
-
-## Constructors
-
-### \`new OverloadTest()\`
-
-* Returns: {OverloadTest}
-
-## Methods
-
-### \`overloadMethod(name, count)\`
-
-* \`name\` {string} A simple string parameter
-* \`count\` {number} A simple number parameter
-* Returns: {void}
-
-First overload signature.
-
-### \`overloadMethod(isEnabled)\`
-
-* \`isEnabled\` {boolean} A simple boolean parameter
-* Returns: {string}
-
-Second overload signature.
-
-### \`overloadMethod(id, isEnabled)\`
-
-* \`id\` {number} A simple string parameter
-* \`isEnabled\` {boolean} A simple boolean parameter
-* Returns: {void}
-
-Third overload signature.
-
-
----
-
-# Class: \`webpack.js.org.StabilityTest\`
-
-Class demonstrating stability for deprecated, experimental, and legacy APIs.
-
-## Constructors
-
-### \`new StabilityTest()\`
-
-* Returns: {StabilityTest}
-
-## Methods
-
-### \`deprecatedMethod()\`
-
-> Stability: 0 - Deprecated: Use the new API instead.
-
-* Returns: {void}
-
-***
-
-### \`experimentalMethod()\`
-
-> Stability: 1 - Experimental
-
-* Returns: {void}
-
-**\`Experimental\`**
-
-***
-
-### \`legacyMethod()\`
-
-> Stability: 3 - Legacy: This is a legacy Webpack v4 API.
-
-* Returns: {void}
-
-
----
-
-# Class: \`webpack.js.org.TypedListTest\`
-
-Class demonstrating properties and typed lists.
-
-* Extends: {WebpackTest}
-
-## Constructors
-
-### \`new TypedListTest()\`
-
-* Returns: {TypedListTest}
-
-## Properties
-
-* \`typedListProperty\` {string} A sample property for the TypedListTest class.
-
-## Methods
-
-### \`typedListMethod(name, count)\`
-
-* \`name\` {string} The name of the instance
-* \`count\` {number} The count of items
-* Returns: {boolean} A boolean indicating success
-
-A sample method for the TypedListTest class.
-
-
----
-
-# Class: \`webpack.js.org.WebpackInterface\`
-
-**\`Interface\`**
-
-Interface demonstrating Webpack as an interface.
-
-## Constructors
-
-### \`new WebpackInterface()\`
-
-* Returns: {WebpackInterface}
-
-
----
-
-# Class: \`webpack.js.org.WebpackTest\`
-
-Class demonstrating Webpack as a parent class.
-
-## Constructors
-
-### \`new WebpackTest()\`
-
-* Returns: {WebpackTest}
-
-
----
-
-# \`webpack.js.org.WebpackUtils\`
-
-## \`merge(a, b)\`
-
-* \`a\` {object} First config
-* \`b\` {object} Second config
-* Returns: {object} Merged config
-
-Merges two configurations.
-
-
----
-
-# \`webpack.js.org\`
-
-## Namespaces
-
-- [EnumTest](/EnumTest.md)
-- [WebpackUtils](/WebpackUtils.md)
-
-## Classes
-
-- [AccessorTest](/AccessorTest.md)
-- [ExamplesTest](/ExamplesTest.md)
-- [IndexSignatureTest](/IndexSignatureTest.md)
-- [MemberGroupsTest](/MemberGroupsTest.md)
-- [OverloadTest](/OverloadTest.md)
-- [StabilityTest](/StabilityTest.md)
-- [TypedListTest](/TypedListTest.md)
-- [WebpackInterface](/WebpackInterface.md)
-- [WebpackTest](/WebpackTest.md)
-
-## \`webpack.js.org.CONSTANT_AND_DECLARATION_TITLE_TEST\`
-
-* Type: {string}
-
-A standalone constant demonstrating declarationTitle formatting and variable rendering.
-
-***
-
-## \`webpack.js.org.standAloneWithGenericsTest(value)\`
-
-* \`value\` {T} The input value
-* Returns: {T} The same value
-
-A standalone function demonstrating generics outside a class.
-
-
----
-
-# \`webpack\` Types
-
-These types are not exported by webpack, but they are available to TypeScript consumers.
-
-## Type: \`ComplexArrayType\`
-
-* Type: {{ age: number; name: string }[]}
-
-Complex array type.
-
-### Type Declaration
-
-* \`age\` {number}
-* \`name\` {string}
-
-***
-
-## Type: \`ComplexIntersectionType\`
-
-* Type: {{ id: string } & { metadata: object }}
-
-Complex intersection type.
-
-### Type Declaration
-
-* \`id\` {string}
-
-### Type Declaration
-
-* \`metadata\` {object}
-
-***
-
-## Type: \`ComplexUnionType\`
-
-* Type: {string | { advanced: boolean; timeout: number }}
-
-Complex union type.
-
-***
-
-## Type: \`EnumTest\`
-
-* Type: {number}
-
-A simple enum demonstrating enum rendering.
-
-***
-
-## Type: \`SimpleArrayType\`
-
-* Type: {string[]}
-
-Simple array type.
-
-***
-
-## Type: \`SimpleIntersectionType\`
-
-* Type: {number & string}
-
-Simple intersection type.
-
-***
-
-## Type: \`SimpleUnionType\`
-
-* Type: {string | number}
-
-Simple union type.
-
-`;