@textmode/tooling-scripts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +40 -0
  2. package/policy.json +29 -0
  3. package/src/cli/check-deps.mjs +61 -0
  4. package/src/cli/install-husky.mjs +23 -0
  5. package/src/config/create-config.mjs +75 -0
  6. package/src/husky/commit-msg +1 -0
  7. package/src/husky/pre-commit +1 -0
  8. package/src/husky/pre-push +6 -0
  9. package/src/scripts/add-api-doc-links.mjs +92 -0
  10. package/src/scripts/check-examples.mjs +81 -0
  11. package/src/scripts/check-public-api-docs.mjs +128 -0
  12. package/src/scripts/lib/api-doc-checks.mjs +147 -0
  13. package/src/scripts/lib/api-doc-links-config.mjs +32 -0
  14. package/src/scripts/lib/api-doc-routes.mjs +118 -0
  15. package/src/scripts/lib/example-checks.mjs +389 -0
  16. package/src/scripts/lib/jsdoc-comments.mjs +78 -0
  17. package/src/scripts/lib/reflection-policy.mjs +106 -0
  18. package/src/scripts/lib/reporting.mjs +23 -0
  19. package/src/scripts/lib/typedoc-project.mjs +25 -0
  20. package/src/typedoc-plugins/accessor-docs-normalizer/README.md +45 -0
  21. package/src/typedoc-plugins/accessor-docs-normalizer/accessor-comments.js +53 -0
  22. package/src/typedoc-plugins/accessor-docs-normalizer/index.js +32 -0
  23. package/src/typedoc-plugins/accessor-docs-normalizer/render-accessor.js +144 -0
  24. package/src/typedoc-plugins/accessor-docs-normalizer/theme-patch.js +28 -0
  25. package/src/typedoc-plugins/all-member-pages-router/README.md +57 -0
  26. package/src/typedoc-plugins/all-member-pages-router/child-pages.js +65 -0
  27. package/src/typedoc-plugins/all-member-pages-router/constants.js +58 -0
  28. package/src/typedoc-plugins/all-member-pages-router/index.js +28 -0
  29. package/src/typedoc-plugins/all-member-pages-router/member-paths.js +48 -0
  30. package/src/typedoc-plugins/all-member-pages-router/member-reflections.js +164 -0
  31. package/src/typedoc-plugins/all-member-pages-router/namespace-paths.js +44 -0
  32. package/src/typedoc-plugins/all-member-pages-router/router.js +58 -0
  33. package/src/typedoc-plugins/api-frontmatter/README.md +66 -0
  34. package/src/typedoc-plugins/api-frontmatter/constants.js +52 -0
  35. package/src/typedoc-plugins/api-frontmatter/descriptions.js +80 -0
  36. package/src/typedoc-plugins/api-frontmatter/frontmatter.js +56 -0
  37. package/src/typedoc-plugins/api-frontmatter/index.js +38 -0
  38. package/src/typedoc-plugins/api-frontmatter/library-context.js +62 -0
  39. package/src/typedoc-plugins/api-frontmatter/reflection-metadata.js +95 -0
  40. package/src/typedoc-plugins/include-code-examples/README.md +40 -0
  41. package/src/typedoc-plugins/include-code-examples/code-fences.js +55 -0
  42. package/src/typedoc-plugins/include-code-examples/example-metadata.js +41 -0
  43. package/src/typedoc-plugins/include-code-examples/index.js +23 -0
  44. package/src/typedoc-plugins/include-code-examples/transform-markdown.js +33 -0
  45. package/src/typedoc-plugins/strip-api-self-links/index.js +126 -0
@@ -0,0 +1,78 @@
1
+ export function getLineStartOffsets(text) {
2
+ const offsets = [0];
3
+ for (let index = 0; index < text.length; index += 1) {
4
+ if (text[index] === '\n') {
5
+ offsets.push(index + 1);
6
+ }
7
+ }
8
+ return offsets;
9
+ }
10
+
11
+ export function findJsdocBeforeLine(text, lineStarts, line) {
12
+ const declarationOffset = lineStarts[line - 1] ?? text.length;
13
+ const searchText = text.slice(0, declarationOffset);
14
+ const start = searchText.lastIndexOf('/**');
15
+ const end = searchText.lastIndexOf('*/');
16
+
17
+ if (start === -1 || end === -1 || start > end) {
18
+ return undefined;
19
+ }
20
+
21
+ return { start, end };
22
+ }
23
+
24
+ function replaceHostedApiSeeLines(commentText, seeLine, apiBaseUrl) {
25
+ const lines = commentText.split('\n');
26
+ let replaced = false;
27
+ const nextLines = [];
28
+
29
+ for (const line of lines) {
30
+ if (line.includes('@see') && line.includes(apiBaseUrl)) {
31
+ if (!replaced) {
32
+ nextLines.push(line.replace(/^(\s*\*\s*).*$/, `$1${seeLine}`));
33
+ replaced = true;
34
+ }
35
+ continue;
36
+ }
37
+
38
+ nextLines.push(line);
39
+ }
40
+
41
+ return replaced ? nextLines.join('\n') : undefined;
42
+ }
43
+
44
+ export function upsertSeeLine(text, comment, seeLine, apiBaseUrl) {
45
+ const commentText = text.slice(comment.start, comment.end + 2);
46
+ const replacedComment = replaceHostedApiSeeLines(commentText, seeLine, apiBaseUrl);
47
+ if (replacedComment !== undefined) {
48
+ return `${text.slice(0, comment.start)}${replacedComment}${text.slice(comment.end + 2)}`;
49
+ }
50
+
51
+ if (!commentText.includes('\n')) {
52
+ const lineStart = text.lastIndexOf('\n', comment.start) + 1;
53
+ const indent = text.slice(lineStart, comment.start);
54
+ const summary = commentText
55
+ .replace(/^\/\*\*\s?/, '')
56
+ .replace(/\s?\*\/$/, '')
57
+ .trim();
58
+ const replacement = [
59
+ `${indent}/**`,
60
+ summary ? `${indent} * ${summary}` : undefined,
61
+ `${indent} *`,
62
+ `${indent} * ${seeLine}`,
63
+ `${indent} */`,
64
+ ]
65
+ .filter(Boolean)
66
+ .join('\n');
67
+
68
+ return `${text.slice(0, lineStart)}${replacement}${text.slice(comment.end + 2)}`;
69
+ }
70
+
71
+ const lineStart = text.lastIndexOf('\n', comment.end) + 1;
72
+ const closingLine = text.slice(lineStart, comment.end + 2);
73
+ const prefixMatch = closingLine.match(/^(\s*)\*\//);
74
+ const indent = prefixMatch?.[1] ?? '';
75
+ const insertion = `${indent}*\n${indent}* ${seeLine}\n`;
76
+
77
+ return `${text.slice(0, lineStart)}${insertion}${text.slice(lineStart)}`;
78
+ }
@@ -0,0 +1,106 @@
1
+ import path from 'node:path';
2
+
3
+ import { ReflectionKind } from 'typedoc';
4
+
5
+ export function isIncludedReflection(reflection, targetKinds) {
6
+ if (!reflection.kindOf(targetKinds)) {
7
+ return false;
8
+ }
9
+
10
+ if (reflection.flags.isPrivate || reflection.flags.isProtected || reflection.flags.isInherited) {
11
+ return false;
12
+ }
13
+
14
+ for (let current = reflection; current && !current.isProject(); current = current.parent) {
15
+ if (current.name.startsWith('_')) {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ return true;
21
+ }
22
+
23
+ export function getIncludedReflections(project, targetKinds) {
24
+ return project
25
+ .getReflectionsByKind(targetKinds)
26
+ .filter((reflection) => isIncludedReflection(reflection, targetKinds));
27
+ }
28
+
29
+ export function shouldProcessEntryPointReflection(entryPoint, reflection) {
30
+ return !entryPoint.namespaceExportsOnly || reflection.kind === ReflectionKind.Namespace;
31
+ }
32
+
33
+ export function getRelevantComments(reflection) {
34
+ const comments = [];
35
+
36
+ if (reflection.comment) {
37
+ comments.push({ comment: reflection.comment, source: reflection.sources?.[0] });
38
+ }
39
+
40
+ if (!reflection.isDeclaration()) {
41
+ return comments;
42
+ }
43
+
44
+ for (const signature of reflection.signatures ?? []) {
45
+ if (signature.comment) {
46
+ comments.push({ comment: signature.comment, source: signature.sources?.[0] ?? reflection.sources?.[0] });
47
+ }
48
+ }
49
+
50
+ if (reflection.getSignature?.comment) {
51
+ comments.push({
52
+ comment: reflection.getSignature.comment,
53
+ source: reflection.getSignature.sources?.[0] ?? reflection.sources?.[0],
54
+ });
55
+ }
56
+
57
+ if (reflection.setSignature?.comment) {
58
+ comments.push({
59
+ comment: reflection.setSignature.comment,
60
+ source: reflection.setSignature.sources?.[0] ?? reflection.sources?.[0],
61
+ });
62
+ }
63
+
64
+ return comments;
65
+ }
66
+
67
+ export function getSourceCandidates(reflection) {
68
+ if (!reflection.isDeclaration()) {
69
+ return [];
70
+ }
71
+
72
+ return [
73
+ ...(reflection.sources ?? []),
74
+ ...(reflection.signatures ?? []).flatMap((signature) => signature.sources ?? []),
75
+ ...(reflection.getSignature?.sources ?? []),
76
+ ...(reflection.setSignature?.sources ?? []),
77
+ ];
78
+ }
79
+
80
+ export function getPrimaryLocation(reflection) {
81
+ const [source] = getSourceCandidates(reflection);
82
+
83
+ if (!source) {
84
+ return {
85
+ file: path.resolve('<unknown>'),
86
+ line: 1,
87
+ column: 1,
88
+ printable: '<unknown>:1:1',
89
+ };
90
+ }
91
+
92
+ const file = source.fullFileName || path.resolve(source.fileName);
93
+ const line = source.line || 1;
94
+ const column = (source.character ?? 0) + 1;
95
+
96
+ return {
97
+ file,
98
+ line,
99
+ column,
100
+ printable: `${file}:${line}:${column}`,
101
+ };
102
+ }
103
+
104
+ export function sourceIsInRoots(source, sourceRoots) {
105
+ return Boolean(source?.fullFileName) && sourceRoots.some((root) => source.fullFileName.startsWith(root));
106
+ }
@@ -0,0 +1,23 @@
1
+ export function renderIssueSection(title, issues, stream = console.log) {
2
+ if (issues.length === 0) {
3
+ return;
4
+ }
5
+
6
+ stream('');
7
+ stream(`${title}: ${issues.length}`);
8
+
9
+ for (const issue of issues) {
10
+ stream(`- ${issue.name}`);
11
+ stream(` ${issue.location}`);
12
+ if (issue.detail) {
13
+ stream(` ${issue.detail}`);
14
+ }
15
+ }
16
+ }
17
+
18
+ export function renderList(title, items, stream = console.error) {
19
+ stream(title);
20
+ for (const item of items) {
21
+ stream(` * ${item}`);
22
+ }
23
+ }
@@ -0,0 +1,25 @@
1
+ import { Application, PackageJsonReader, TSConfigReader } from 'typedoc';
2
+
3
+ export async function loadTypeDocProject({ entryPoint, tsconfig = 'tsconfig.json', errorMessage }) {
4
+ const app = await Application.bootstrap(
5
+ {
6
+ entryPoints: [entryPoint],
7
+ entryPointStrategy: 'resolve',
8
+ tsconfig,
9
+ excludeInternal: true,
10
+ excludePrivate: true,
11
+ excludeProtected: true,
12
+ readme: 'none',
13
+ emit: 'none',
14
+ logLevel: 'Error',
15
+ },
16
+ [new PackageJsonReader(), new TSConfigReader()]
17
+ );
18
+
19
+ const project = await app.convert();
20
+ if (!project || app.logger.hasErrors()) {
21
+ throw new Error(errorMessage ?? `TypeDoc failed to resolve ${entryPoint}.`);
22
+ }
23
+
24
+ return project;
25
+ }
@@ -0,0 +1,45 @@
1
+ # Accessor Docs Normalizer
2
+
3
+ ## Purpose
4
+
5
+ TypeDoc stores comments for TypeScript accessors on the getter and setter signatures. `typedoc-plugin-markdown`
6
+ overview tables read descriptions from the accessor reflection itself. When the reflection has no direct comment,
7
+ the generated table falls back to `-` even though the getter or setter is documented in source.
8
+
9
+ This plugin normalizes that shape for the textmode.js API docs:
10
+
11
+ - Copies the first getter or setter signature comment onto an accessor reflection when the accessor has no comment.
12
+ - Tracks copied comments so the accessor page does not render the same summary twice.
13
+ - Replaces the markdown accessor partial so getter documentation appears before setter documentation.
14
+ - Keeps getter return sections, setter parameter sections, inheritance sections, and `disableSources` behavior intact.
15
+
16
+ The plugin does not change runtime library behavior, the public TypeScript API, source JSDoc ownership, or TypeDoc
17
+ reflection conversion. It only adjusts markdown rendering after TypeDoc has built the reflection model.
18
+
19
+ This exists to avoid source-level documentation stubs or declaration-merging boilerplate for accessor descriptions.
20
+ Accessor docs should stay in the source declarations that own the getter and setter behavior.
21
+
22
+ ## Preserved Output Contract
23
+
24
+ - Accessor overview tables show the first available getter or setter summary.
25
+ - Accessor pages do not render copied comments twice.
26
+ - Getter content renders before setter content.
27
+ - Getter return sections, setter parameter sections, inheritance sections, and `disableSources` behavior match
28
+ `typedoc-plugin-markdown`.
29
+
30
+ ## Contributor Notes
31
+
32
+ - Safe to edit: helper names, tests, README wording, and the local comment-normalization traversal.
33
+ - Be careful with: `theme-patch.js`, because it monkey-patches `MarkdownTheme.prototype.getRenderContext`.
34
+ - Do not casually change: accessor partial output order, copied-comment tracking, or source-display conditions.
35
+
36
+ ## Maintenance
37
+
38
+ This plugin depends on `typedoc-plugin-markdown` theme internals, especially `MarkdownTheme.getRenderContext` and the
39
+ `partials.accessor` contract. Re-check this plugin after upgrading `typedoc-plugin-markdown`.
40
+
41
+ Upgrade checklist:
42
+
43
+ - Compare `render-accessor.js` against the current upstream accessor partial.
44
+ - Confirm `MarkdownTheme.getRenderContext` still exists and returns a mutable partials object.
45
+ - Run `npm run test:tooling` and inspect generated accessor pages for duplicate summaries.
@@ -0,0 +1,53 @@
1
+ // @ts-check
2
+
3
+ import { ReflectionKind } from 'typedoc';
4
+
5
+ const syntheticAccessorComments = new WeakSet();
6
+
7
+ /**
8
+ * Get the first signature-level comment available for an accessor reflection.
9
+ * TypeDoc currently keeps getter/setter comments on their signatures, while
10
+ * overview tables read from the accessor declaration itself.
11
+ *
12
+ * @param {import('typedoc').DeclarationReflection} accessor
13
+ * @returns {import('typedoc').Comment | undefined}
14
+ */
15
+ function getAccessorSignatureComment(accessor) {
16
+ return accessor.getSignature?.comment ?? accessor.setSignature?.comment;
17
+ }
18
+
19
+ /**
20
+ * Copy signature comments onto accessor declarations when needed so markdown
21
+ * overview tables can render descriptions instead of falling back to "-".
22
+ *
23
+ * @param {import('typedoc').Reflection | undefined} model
24
+ * @returns {void}
25
+ */
26
+ export function normalizeAccessorComments(model) {
27
+ if (!model) {
28
+ return;
29
+ }
30
+
31
+ if (model.kind === ReflectionKind.Accessor && 'comment' in model && !model.comment) {
32
+ const signatureComment = getAccessorSignatureComment(model);
33
+ if (signatureComment) {
34
+ model.comment = signatureComment;
35
+ syntheticAccessorComments.add(model);
36
+ }
37
+ }
38
+
39
+ model.traverse?.((child) => {
40
+ normalizeAccessorComments(child);
41
+ return true;
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Check whether a comment was copied onto an accessor by this plugin.
47
+ *
48
+ * @param {import('typedoc').DeclarationReflection} accessor
49
+ * @returns {boolean}
50
+ */
51
+ export function hasSyntheticAccessorComment(accessor) {
52
+ return syntheticAccessorComments.has(accessor);
53
+ }
@@ -0,0 +1,32 @@
1
+ // @ts-check
2
+
3
+ import { MarkdownTheme, MarkdownThemeContext } from 'typedoc-plugin-markdown';
4
+
5
+ import { normalizeAccessorComments } from './accessor-comments.js';
6
+ import { renderAccessorDocs } from './render-accessor.js';
7
+ import { patchMarkdownThemeRenderContext } from './theme-patch.js';
8
+
9
+ class AccessorDocsMarkdownThemeContext extends MarkdownThemeContext {
10
+ /**
11
+ * @param {MarkdownTheme} theme
12
+ * @param {import('typedoc-plugin-markdown').MarkdownPageEvent<import('typedoc').Reflection>} page
13
+ * @param {import('typedoc').Options} options
14
+ */
15
+ constructor(theme, page, options) {
16
+ super(theme, page, options);
17
+ normalizeAccessorComments(page.model);
18
+ this.partials.accessor = (accessor, partialOptions) => renderAccessorDocs(this, accessor, partialOptions);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * @param {import('typedoc').Application} app
24
+ * @returns {void}
25
+ */
26
+ export function load(app) {
27
+ patchMarkdownThemeRenderContext(
28
+ (theme, page) => new AccessorDocsMarkdownThemeContext(theme, page, theme.application.options)
29
+ );
30
+
31
+ app.logger.verbose('[typedoc] Registered accessor docs normalizer');
32
+ }
@@ -0,0 +1,144 @@
1
+ // @ts-check
2
+
3
+ import { i18n, ReflectionKind } from 'typedoc';
4
+
5
+ import { hasSyntheticAccessorComment } from './accessor-comments.js';
6
+
7
+ /**
8
+ * @param {number} level
9
+ * @param {string} text
10
+ * @returns {string}
11
+ */
12
+ function heading(level, text) {
13
+ return `${'#'.repeat(level)} ${text}`;
14
+ }
15
+
16
+ /**
17
+ * Render one getter or setter signature.
18
+ *
19
+ * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} context
20
+ * @param {import('typedoc').SignatureReflection} signature
21
+ * @param {{
22
+ * accessor: 'get' | 'set';
23
+ * headingLevel: number;
24
+ * includeParameters: boolean;
25
+ * showSources: boolean;
26
+ * title: string;
27
+ * }} options
28
+ * @returns {string}
29
+ */
30
+ function renderAccessorSignature(context, signature, options) {
31
+ const md = [];
32
+
33
+ md.push(heading(options.headingLevel, options.title));
34
+ md.push(
35
+ context.partials.signatureTitle(signature, {
36
+ accessor: options.accessor,
37
+ })
38
+ );
39
+
40
+ if (options.showSources && !context.options.getValue('disableSources') && signature.sources) {
41
+ md.push(context.partials.sources(signature));
42
+ }
43
+
44
+ if (signature.comment) {
45
+ md.push(
46
+ context.partials.comment(signature.comment, {
47
+ headingLevel: options.headingLevel + 1,
48
+ showTags: false,
49
+ showSummary: true,
50
+ })
51
+ );
52
+ }
53
+
54
+ if (options.includeParameters && signature.parameters?.length) {
55
+ md.push(heading(options.headingLevel + 1, ReflectionKind.pluralString(ReflectionKind.Parameter)));
56
+ if (context.helpers.useTableFormat('parameters')) {
57
+ md.push(context.partials.parametersTable(signature.parameters));
58
+ } else {
59
+ md.push(
60
+ context.partials.parametersList(signature.parameters, {
61
+ headingLevel: options.headingLevel + 1,
62
+ })
63
+ );
64
+ }
65
+ }
66
+
67
+ if (signature.type) {
68
+ md.push(
69
+ context.partials.signatureReturns(signature, {
70
+ headingLevel: options.headingLevel + 1,
71
+ })
72
+ );
73
+ }
74
+
75
+ if (signature.comment) {
76
+ md.push(
77
+ context.partials.comment(signature.comment, {
78
+ headingLevel: options.headingLevel + 1,
79
+ showTags: true,
80
+ showSummary: false,
81
+ })
82
+ );
83
+ }
84
+
85
+ return md.filter(Boolean).join('\n\n');
86
+ }
87
+
88
+ /**
89
+ * Render accessor documentation with getter content before setter content.
90
+ *
91
+ * @param {import('typedoc-plugin-markdown').MarkdownThemeContext} context
92
+ * @param {import('typedoc').DeclarationReflection} accessor
93
+ * @param {{ headingLevel: number }} options
94
+ * @returns {string}
95
+ */
96
+ export function renderAccessorDocs(context, accessor, options) {
97
+ const md = [];
98
+ const showSources = accessor.parent?.kind !== ReflectionKind.TypeLiteral;
99
+
100
+ if (accessor.getSignature) {
101
+ md.push(
102
+ renderAccessorSignature(context, accessor.getSignature, {
103
+ accessor: 'get',
104
+ headingLevel: options.headingLevel,
105
+ includeParameters: false,
106
+ showSources,
107
+ title: i18n.kind_get_signature(),
108
+ })
109
+ );
110
+ }
111
+
112
+ if (accessor.setSignature) {
113
+ md.push(
114
+ renderAccessorSignature(context, accessor.setSignature, {
115
+ accessor: 'set',
116
+ headingLevel: options.headingLevel,
117
+ includeParameters: true,
118
+ showSources,
119
+ title: i18n.kind_set_signature(),
120
+ })
121
+ );
122
+ }
123
+
124
+ if (
125
+ showSources &&
126
+ !context.options.getValue('disableSources') &&
127
+ !accessor.getSignature &&
128
+ !accessor.setSignature
129
+ ) {
130
+ md.push(context.partials.sources(accessor));
131
+ }
132
+
133
+ if (accessor.comment && !hasSyntheticAccessorComment(accessor)) {
134
+ md.push(
135
+ context.partials.comment(accessor.comment, {
136
+ headingLevel: options.headingLevel,
137
+ })
138
+ );
139
+ }
140
+
141
+ md.push(context.partials.inheritance(accessor, { headingLevel: options.headingLevel }));
142
+
143
+ return md.filter(Boolean).join('\n\n');
144
+ }
@@ -0,0 +1,28 @@
1
+ // @ts-check
2
+
3
+ import { MarkdownTheme } from 'typedoc-plugin-markdown';
4
+
5
+ let isPatched = false;
6
+
7
+ /**
8
+ * Patch typedoc-plugin-markdown's theme render-context factory once.
9
+ *
10
+ * This is the only place this plugin reaches into markdown theme internals.
11
+ * Keep the patch idempotent because TypeDoc can load plugins repeatedly in
12
+ * watch mode and tests can import the plugin more than once.
13
+ *
14
+ * @param {(theme: MarkdownTheme, page: import('typedoc-plugin-markdown').MarkdownPageEvent<import('typedoc').Reflection>) => import('typedoc-plugin-markdown').MarkdownThemeContext} createContext
15
+ * @returns {boolean} True when this call installed the patch.
16
+ */
17
+ export function patchMarkdownThemeRenderContext(createContext) {
18
+ if (isPatched) {
19
+ return false;
20
+ }
21
+
22
+ MarkdownTheme.prototype.getRenderContext = function (page) {
23
+ return createContext(this, page);
24
+ };
25
+ isPatched = true;
26
+
27
+ return true;
28
+ }
@@ -0,0 +1,57 @@
1
+ # All Member Pages Router
2
+
3
+ ## Purpose
4
+
5
+ This TypeDoc router makes direct public methods render as focused Markdown pages when an owner page would otherwise
6
+ render too many Sandpack examples.
7
+
8
+ `typedoc-plugin-markdown` can render top-level API structures as pages, but textmode.js also needs compact class and
9
+ interface overview pages that link to individual methods once their inline examples would make the page heavy. This
10
+ router keeps those owner pages small and gives each direct method a stable page.
11
+
12
+ ## Member Pages
13
+
14
+ The router creates pages for direct class/interface methods when rendering the owner page inline would exceed three
15
+ Sandpack examples:
16
+
17
+ - methods
18
+
19
+ Nested type-literal fields intentionally remain inline or anchor-based so structural types do not explode into many
20
+ small pages.
21
+
22
+ ## Paths
23
+
24
+ Examples:
25
+
26
+ - `classes/Textmodifier/methods/background.md`
27
+ - `namespaces/layering/classes/TextmodeLayer/methods/draw.md`
28
+
29
+ ## Preserved Output Contract
30
+
31
+ - The router name remains `all-member-pages`.
32
+ - Direct methods get pages only when their class/interface owner would otherwise exceed three Sandpack examples.
33
+ - Properties, accessors, constructors, variables, type aliases, functions, and nested type-literal members do not get
34
+ extra pages from this router.
35
+ - Existing `MemberRouter` behavior for modules, namespaces, documents, and configured `membersWithOwnFile` kinds is
36
+ preserved.
37
+ - Project-root namespaces use the flattened textmode.js path shape, for example `namespaces/color/classes/TextmodeColor.md`.
38
+ - Sidebar entries include generated member pages under their owners.
39
+
40
+ ## Contributor Notes
41
+
42
+ - Safe to edit: constants, helper names, tests, and README wording.
43
+ - Be careful with: `child-pages.js`, which intentionally mirrors upstream `MemberRouter` traversal.
44
+ - Do not casually change: path segment names, namespace path flattening, example-count thresholds, or direct-method page
45
+ eligibility.
46
+
47
+ ## Maintenance
48
+
49
+ This plugin extends `typedoc-plugin-markdown`'s `MemberRouter` and copies its child-page traversal policy so direct
50
+ members can participate in TypeDoc's `hasOwnDocument` behavior. Re-check this router after upgrading TypeDoc or
51
+ `typedoc-plugin-markdown`, especially around router APIs, `membersWithOwnFile`, and namespace path handling.
52
+
53
+ Upgrade checklist:
54
+
55
+ - Compare `child-pages.js` against the current upstream `MemberRouter.buildChildPages`.
56
+ - Confirm `PageKind.Reflection`, `Slugger`, `fullUrls`, and `sluggers` are still the correct router APIs.
57
+ - Run `npm run build:docs` and verify generated method links still resolve.
@@ -0,0 +1,65 @@
1
+ // @ts-check
2
+
3
+ import { Reflection, Slugger } from 'typedoc';
4
+
5
+ import { isDirectMemberPageReflection, shouldRenderOwnPage } from './member-reflections.js';
6
+
7
+ /**
8
+ * @typedef {{
9
+ * extension: string;
10
+ * fullUrls: Map<import('typedoc').Reflection, string>;
11
+ * sluggerConfiguration: unknown;
12
+ * sluggers: Map<import('typedoc').Reflection, Slugger>;
13
+ * getFileName(idealName: string): string;
14
+ * getIdealBaseName(reflection: import('typedoc').Reflection): string;
15
+ * getPageKind(reflection: import('typedoc').Reflection): import('typedoc').PageKind | undefined;
16
+ * shouldWritePage(reflection: import('typedoc').Reflection): boolean;
17
+ * buildAnchors(reflection: import('typedoc').Reflection, parent?: import('typedoc').Reflection): void;
18
+ * membersWithOwnFile: string[];
19
+ * kindsToString: Map<import('typedoc').ReflectionKind, string>;
20
+ * }} ChildPageRouter
21
+ */
22
+
23
+ /**
24
+ * Build child pages using typedoc-plugin-markdown's MemberRouter traversal
25
+ * shape, while delegating textmode.js' expanded own-page policy to local
26
+ * helpers. This mirrors upstream traversal intentionally; when upgrading
27
+ * typedoc-plugin-markdown, compare this function against MemberRouter.
28
+ *
29
+ * @param {ChildPageRouter} router
30
+ * @param {import('typedoc').Reflection} reflection
31
+ * @param {import('typedoc').PageDefinition[]} outPages
32
+ * @returns {void}
33
+ */
34
+ export function buildMemberRouterChildPages(router, reflection, outPages) {
35
+ const kind = router.getPageKind(reflection);
36
+ if (!kind) {
37
+ router.buildAnchors(reflection, reflection.parent);
38
+ return;
39
+ }
40
+
41
+ const shouldWritePage = isDirectMemberPageReflection(reflection) || router.shouldWritePage(reflection);
42
+ const idealName = router.getIdealBaseName(reflection);
43
+ const actualName = shouldWritePage ? router.getFileName(idealName) : `${idealName}${router.extension}`;
44
+ router.fullUrls.set(reflection, actualName);
45
+
46
+ if (shouldRenderOwnPage(reflection, router)) {
47
+ if (shouldWritePage) {
48
+ router.sluggers.set(reflection, new Slugger(router.sluggerConfiguration));
49
+ outPages.push({
50
+ kind,
51
+ model: reflection,
52
+ url: actualName,
53
+ });
54
+ }
55
+ } else {
56
+ router.buildAnchors(reflection, reflection.parent);
57
+ }
58
+
59
+ if (reflection instanceof Reflection) {
60
+ reflection.traverse((child) => {
61
+ buildMemberRouterChildPages(router, child, outPages);
62
+ return true;
63
+ });
64
+ }
65
+ }