@reicek/neataptic-ts 0.1.25 → 0.1.26
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.
- package/.github/copilot-instructions.md +11 -0
- package/.github/skills/trace-analyzer-extension/SKILL.md +3 -3
- package/.github/skills/trace-analyzer-extension/assets/extension-checklist.md +1 -1
- package/.github/skills/trace-analyzer-extension/references/analyzer-extension-workflow.md +1 -1
- package/.github/skills/trace-audit-reporting/SKILL.md +3 -3
- package/.github/skills/trace-audit-reporting/references/trace-analysis-workflow.md +1 -1
- package/package.json +19 -13
- package/plans/Flappy_Bird_Folder_Documentation_Pass.md +4 -4
- package/plans/README.md +24 -0
- package/plans/Roadmap.md +62 -40
- package/plans/analyze-trace-solid-split.plans.md +66 -0
- package/plans/architecture-solid-split.plans.md +9 -15
- package/plans/asciiMaze-typescript-repair.plans.md +1 -1
- package/plans/generate-docs-solid-split.plans.md +87 -0
- package/plans/methods-docs.plans.md +25 -1
- package/plans/methods-solid-split.plans.md +14 -14
- package/plans/neat-docs.plans.md +9 -1
- package/plans/neat-test-surface-repair.plans.md +1 -1
- package/plans/render-docs-html-solid-split.plans.md +68 -0
- package/plans/src-no-explicit-any-cleanup.plans.md +1 -1
- package/plans/utils-docs.plans.md +6 -1
- package/scripts/analyze-trace/analyze-trace.analysis.ts +479 -0
- package/scripts/analyze-trace/analyze-trace.constants.ts +35 -0
- package/scripts/analyze-trace/analyze-trace.io.ts +69 -0
- package/scripts/analyze-trace/analyze-trace.report.ts +100 -0
- package/scripts/analyze-trace/analyze-trace.shared.ts +116 -0
- package/scripts/analyze-trace/analyze-trace.ts +45 -0
- package/scripts/analyze-trace/analyze-trace.types.ts +72 -0
- package/scripts/assets/theme.css +80 -23
- package/scripts/copy-examples.ts +239 -0
- package/scripts/export-onnx.ts +223 -0
- package/scripts/generate-bench-tables.ts +378 -37
- package/scripts/generate-docs/generate-docs.constants.ts +107 -0
- package/scripts/generate-docs/generate-docs.order.ts +355 -0
- package/scripts/generate-docs/generate-docs.state.ts +31 -0
- package/scripts/generate-docs/generate-docs.targets.ts +165 -0
- package/scripts/generate-docs/generate-docs.ts +63 -0
- package/scripts/generate-docs/generate-docs.types.ts +112 -0
- package/scripts/generate-docs/output/generate-docs.output.folder-index.utils.ts +167 -0
- package/scripts/generate-docs/output/generate-docs.output.ordering.utils.ts +353 -0
- package/scripts/generate-docs/output/generate-docs.output.readme.utils.ts +420 -0
- package/scripts/generate-docs/output/generate-docs.output.ts +123 -0
- package/scripts/generate-docs/output/generate-docs.output.warnings.utils.ts +219 -0
- package/scripts/generate-docs/symbols/generate-docs.symbols.collection.utils.ts +365 -0
- package/scripts/generate-docs/symbols/generate-docs.symbols.jsdoc.utils.ts +373 -0
- package/scripts/generate-docs/symbols/generate-docs.symbols.normalize.utils.ts +155 -0
- package/scripts/generate-docs/symbols/generate-docs.symbols.render.utils.ts +149 -0
- package/scripts/generate-docs/symbols/generate-docs.symbols.signature.utils.ts +289 -0
- package/scripts/generate-docs/symbols/generate-docs.symbols.ts +11 -0
- package/scripts/mermaid-cli.mjs +102 -22
- package/scripts/mermaid-cli.ts +736 -0
- package/scripts/render-docs-html/render-docs-html.assets.ts +54 -0
- package/scripts/render-docs-html/render-docs-html.mermaid.ts +245 -0
- package/scripts/{render-docs-html.sidebar.ts → render-docs-html/render-docs-html.navigation.ts} +141 -144
- package/scripts/render-docs-html/render-docs-html.pages.ts +333 -0
- package/scripts/render-docs-html/render-docs-html.shared.ts +333 -0
- package/scripts/render-docs-html/render-docs-html.types.ts +42 -0
- package/scripts/render-docs-html.ts +23 -587
- package/scripts/run-docs.ts +238 -0
- package/scripts/write-dist-docs-pkg.ts +40 -0
- package/src/README.md +75 -75
- package/src/architecture/connection/README.md +5 -5
- package/src/architecture/layer/README.md +508 -508
- package/src/architecture/network/README.md +1458 -1458
- package/src/architecture/network/activate/README.md +694 -694
- package/src/architecture/network/bootstrap/README.md +77 -77
- package/src/architecture/network/connect/README.md +74 -74
- package/src/architecture/network/deterministic/README.md +135 -135
- package/src/architecture/network/evolve/README.md +364 -364
- package/src/architecture/network/gating/README.md +130 -130
- package/src/architecture/network/genetic/README.md +399 -399
- package/src/architecture/network/mutate/README.md +897 -897
- package/src/architecture/network/onnx/README.md +720 -720
- package/src/architecture/network/onnx/export/README.md +728 -728
- package/src/architecture/network/onnx/export/layers/README.md +450 -450
- package/src/architecture/network/onnx/import/README.md +618 -618
- package/src/architecture/network/onnx/schema/README.md +32 -32
- package/src/architecture/network/prune/README.md +245 -245
- package/src/architecture/network/remove/README.md +135 -135
- package/src/architecture/network/runtime/README.md +106 -106
- package/src/architecture/network/serialize/README.md +542 -542
- package/src/architecture/network/slab/README.md +608 -608
- package/src/architecture/network/standalone/README.md +212 -212
- package/src/architecture/network/stats/README.md +84 -84
- package/src/architecture/network/topology/README.md +465 -465
- package/src/architecture/network/training/README.md +200 -200
- package/src/architecture/node/README.md +5 -5
- package/src/architecture/nodePool/README.md +14 -14
- package/src/methods/README.md +99 -99
- package/src/methods/activation/README.md +189 -189
- package/src/methods/cost/README.md +131 -131
- package/src/methods/rate/README.md +86 -86
- package/src/multithreading/README.md +77 -77
- package/src/multithreading/workers/browser/README.md +8 -8
- package/src/multithreading/workers/node/README.md +8 -8
- package/src/neat/README.md +148 -148
- package/src/neat/adaptive/README.md +120 -120
- package/src/neat/adaptive/acceptance/README.md +40 -40
- package/src/neat/adaptive/complexity/README.md +137 -137
- package/src/neat/adaptive/core/README.md +197 -197
- package/src/neat/adaptive/lineage/README.md +90 -90
- package/src/neat/adaptive/mutation/README.md +284 -284
- package/src/neat/compat/README.md +43 -43
- package/src/neat/compat/core/README.md +90 -90
- package/src/neat/diversity/README.md +35 -35
- package/src/neat/diversity/core/README.md +88 -88
- package/src/neat/evaluate/README.md +85 -85
- package/src/neat/evaluate/auto-distance/README.md +75 -75
- package/src/neat/evaluate/entropy-compat/README.md +37 -37
- package/src/neat/evaluate/entropy-sharing/README.md +43 -43
- package/src/neat/evaluate/fitness/README.md +23 -23
- package/src/neat/evaluate/novelty/README.md +120 -120
- package/src/neat/evaluate/objectives/README.md +17 -17
- package/src/neat/evaluate/shared/README.md +94 -94
- package/src/neat/evolve/README.md +96 -96
- package/src/neat/evolve/adaptive/README.md +60 -60
- package/src/neat/evolve/objectives/README.md +63 -63
- package/src/neat/evolve/offspring/README.md +56 -56
- package/src/neat/evolve/population/README.md +171 -171
- package/src/neat/evolve/runtime/README.md +79 -79
- package/src/neat/evolve/speciation/README.md +74 -74
- package/src/neat/evolve/warnings/README.md +10 -10
- package/src/neat/export/README.md +114 -114
- package/src/neat/helpers/README.md +50 -50
- package/src/neat/init/README.md +9 -9
- package/src/neat/lineage/core/README.md +101 -101
- package/src/neat/multiobjective/category/README.md +74 -74
- package/src/neat/multiobjective/crowding/README.md +272 -272
- package/src/neat/multiobjective/dominance/README.md +171 -171
- package/src/neat/multiobjective/fronts/README.md +68 -68
- package/src/neat/multiobjective/metrics/README.md +43 -43
- package/src/neat/multiobjective/objectives/README.md +31 -31
- package/src/neat/multiobjective/shared/README.md +27 -27
- package/src/neat/mutation/README.md +97 -97
- package/src/neat/mutation/add-conn/README.md +115 -115
- package/src/neat/mutation/add-node/README.md +126 -126
- package/src/neat/mutation/flow/README.md +149 -149
- package/src/neat/mutation/repair/README.md +185 -185
- package/src/neat/mutation/select/README.md +117 -117
- package/src/neat/mutation/shared/README.md +32 -32
- package/src/neat/objectives/README.md +25 -25
- package/src/neat/objectives/core/README.md +67 -67
- package/src/neat/pruning/README.md +40 -40
- package/src/neat/pruning/core/README.md +171 -171
- package/src/neat/pruning/facade/README.md +32 -32
- package/src/neat/rng/README.md +104 -104
- package/src/neat/rng/core/README.md +137 -137
- package/src/neat/rng/facade/README.md +50 -50
- package/src/neat/selection/README.md +111 -111
- package/src/neat/selection/core/README.md +227 -227
- package/src/neat/selection/facade/README.md +61 -61
- package/src/neat/shared/README.md +163 -163
- package/src/neat/speciation/README.md +31 -31
- package/src/neat/speciation/threshold/README.md +35 -35
- package/src/neat/species/README.md +25 -25
- package/src/neat/species/core/README.md +20 -20
- package/src/neat/species/core/shared/README.md +18 -18
- package/src/neat/species/history/context/README.md +22 -22
- package/src/neat/telemetry/accessors/README.md +58 -58
- package/src/neat/telemetry/exports/README.md +233 -233
- package/src/neat/telemetry/facade/README.md +252 -252
- package/src/neat/telemetry/facade/archive/README.md +57 -57
- package/src/neat/telemetry/facade/buffer/README.md +43 -43
- package/src/neat/telemetry/facade/lineage/README.md +12 -12
- package/src/neat/telemetry/facade/objectives/README.md +44 -44
- package/src/neat/telemetry/facade/runtime/README.md +26 -26
- package/src/neat/telemetry/facade/species/README.md +27 -27
- package/src/neat/telemetry/metrics/README.md +696 -696
- package/src/neat/telemetry/recorder/README.md +57 -57
- package/src/neat/telemetry/types/README.md +32 -32
- package/src/neat/topology-intent/README.md +75 -75
- package/src/utils/README.md +193 -193
- package/test/examples/asciiMaze/browser-entry/README.md +92 -92
- package/test/examples/asciiMaze/dashboardManager/README.md +109 -109
- package/test/examples/asciiMaze/dashboardManager/telemetry/README.md +28 -28
- package/test/examples/asciiMaze/evolutionEngine/README.md +1527 -1527
- package/test/examples/asciiMaze/mazeMovement/README.md +105 -105
- package/test/examples/asciiMaze/mazeMovement/finalization/README.md +16 -16
- package/test/examples/asciiMaze/mazeMovement/policy/README.md +57 -57
- package/test/examples/asciiMaze/mazeMovement/runtime/README.md +52 -52
- package/test/examples/asciiMaze/mazeMovement/shaping/README.md +46 -46
- package/test/examples/flappy_bird/browser-entry/README.md +508 -508
- package/test/examples/flappy_bird/browser-entry/host/README.md +101 -101
- package/test/examples/flappy_bird/browser-entry/host/resize/README.md +144 -144
- package/test/examples/flappy_bird/browser-entry/network-view/README.md +194 -194
- package/test/examples/flappy_bird/browser-entry/playback/README.md +278 -278
- package/test/examples/flappy_bird/browser-entry/playback/background/README.md +129 -129
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/README.md +502 -502
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/README.md +139 -139
- package/test/examples/flappy_bird/browser-entry/playback/snapshot/README.md +10 -10
- package/test/examples/flappy_bird/browser-entry/playback/trail/README.md +43 -43
- package/test/examples/flappy_bird/browser-entry/playback/worker-channel/README.md +30 -30
- package/test/examples/flappy_bird/browser-entry/runtime/README.md +59 -59
- package/test/examples/flappy_bird/browser-entry/visualization/README.md +276 -276
- package/test/examples/flappy_bird/browser-entry/worker-channel/README.md +16 -16
- package/test/examples/flappy_bird/constants/README.md +1070 -1070
- package/test/examples/flappy_bird/environment/README.md +22 -22
- package/test/examples/flappy_bird/evaluation/README.md +32 -32
- package/test/examples/flappy_bird/evaluation/rollout/README.md +141 -141
- package/test/examples/flappy_bird/flappy-evolution-worker/README.md +425 -425
- package/test/examples/flappy_bird/simulation-shared/README.md +170 -170
- package/test/examples/flappy_bird/simulation-shared/observation/README.md +109 -109
- package/test/examples/flappy_bird/trainer/README.md +325 -325
- package/test/examples/flappy_bird/trainer/evaluation/README.md +74 -74
- package/scripts/analyze-trace.ts +0 -590
- package/scripts/copy-examples.mjs +0 -114
- package/scripts/export-onnx.mjs +0 -86
- package/scripts/generate-bench-tables.mjs +0 -182
- package/scripts/generate-docs.ts +0 -2900
- package/scripts/write-dist-docs-pkg.mjs +0 -16
package/scripts/generate-docs.ts
DELETED
|
@@ -1,2900 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Generates per-folder README.md aggregating exported symbols' JSDoc.
|
|
3
|
-
* - Copies root README.md into docs/ (manual content retained)
|
|
4
|
-
* - Skips generating README for src root (leave top-level README manual)
|
|
5
|
-
* Usage: npm run docs:folders
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
Project,
|
|
9
|
-
type JSDocTag,
|
|
10
|
-
type SourceFile,
|
|
11
|
-
type Symbol as MorphSymbol,
|
|
12
|
-
} from 'ts-morph';
|
|
13
|
-
import fg from 'fast-glob';
|
|
14
|
-
import fs from 'fs-extra';
|
|
15
|
-
import * as path from 'path';
|
|
16
|
-
|
|
17
|
-
const DOCS_DIR = path.resolve('docs');
|
|
18
|
-
const DEFAULT_DOCS_TARGET = 'src';
|
|
19
|
-
const FILE_SUMMARY_SYMBOL_NAME = '__file_summary__';
|
|
20
|
-
const SOURCE_FILE_GLOBS = ['**/*.ts'];
|
|
21
|
-
const SOURCE_FILE_IGNORE_GLOBS = ['**/*.d.ts'];
|
|
22
|
-
const FOLDER_INDEX_FILE_NAME = 'FOLDERS.md';
|
|
23
|
-
const DOCS_ORDER_CONFIG_FILE_NAME = 'docs.order.json';
|
|
24
|
-
const WORKSPACE_ROOT_DIR = path.resolve('.');
|
|
25
|
-
|
|
26
|
-
interface DocsTargetConfig {
|
|
27
|
-
name: string;
|
|
28
|
-
sourceDir: string;
|
|
29
|
-
docsDir: string;
|
|
30
|
-
rootDocsDir: string;
|
|
31
|
-
rootReadmeSource?: string;
|
|
32
|
-
rootReadmeDestination?: string;
|
|
33
|
-
excludeRootSourceFiles?: boolean;
|
|
34
|
-
includeFolderIndex?: boolean;
|
|
35
|
-
publishedRootDir?: string;
|
|
36
|
-
preservePublishedEntries?: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface RenderedParameter {
|
|
40
|
-
name: string;
|
|
41
|
-
type?: string;
|
|
42
|
-
doc?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface RenderedSymbol {
|
|
46
|
-
kind: string;
|
|
47
|
-
name: string;
|
|
48
|
-
filePath: string;
|
|
49
|
-
parent?: string;
|
|
50
|
-
signature?: string;
|
|
51
|
-
jsdoc: {
|
|
52
|
-
summary?: string;
|
|
53
|
-
description?: string;
|
|
54
|
-
params?: RenderedParameter[];
|
|
55
|
-
returns?: string;
|
|
56
|
-
deprecated?: string;
|
|
57
|
-
examples?: string[];
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface FolderIndexNode {
|
|
62
|
-
name: string;
|
|
63
|
-
path: string;
|
|
64
|
-
children: Map<string, FolderIndexNode>;
|
|
65
|
-
fileCount?: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface DirectoryDocsOrderConfig {
|
|
69
|
-
introFile?: string;
|
|
70
|
-
fileOrder?: string[];
|
|
71
|
-
symbolOrder?: Record<string, string[]>;
|
|
72
|
-
folderOrder?: string[];
|
|
73
|
-
hiddenFiles?: string[];
|
|
74
|
-
hiddenSymbols?: string[];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface LoadedDirectoryDocsOrderConfig {
|
|
78
|
-
configPath: string;
|
|
79
|
-
config: DirectoryDocsOrderConfig;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface RenderedFileSummary {
|
|
83
|
-
description?: string;
|
|
84
|
-
examples?: string[];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type DirectorySymbolMap = Map<string, Map<string, RenderedSymbol[]>>;
|
|
88
|
-
|
|
89
|
-
const DOCS_TARGETS: Record<string, DocsTargetConfig> = {
|
|
90
|
-
src: {
|
|
91
|
-
name: 'src',
|
|
92
|
-
sourceDir: path.resolve('src'),
|
|
93
|
-
docsDir: DOCS_DIR,
|
|
94
|
-
rootDocsDir: path.join(DOCS_DIR, 'src'),
|
|
95
|
-
rootReadmeSource: path.resolve('README.md'),
|
|
96
|
-
rootReadmeDestination: path.join(DOCS_DIR, 'README.md'),
|
|
97
|
-
includeFolderIndex: true,
|
|
98
|
-
},
|
|
99
|
-
asciiMaze: {
|
|
100
|
-
name: 'asciiMaze',
|
|
101
|
-
sourceDir: path.resolve('test', 'examples', 'asciiMaze'),
|
|
102
|
-
docsDir: path.join(DOCS_DIR, 'examples', 'asciiMaze', 'docs'),
|
|
103
|
-
rootDocsDir: path.join(DOCS_DIR, 'examples', 'asciiMaze', 'docs'),
|
|
104
|
-
rootReadmeSource: path.resolve('test', 'examples', 'asciiMaze', 'README.md'),
|
|
105
|
-
rootReadmeDestination: path.join(
|
|
106
|
-
DOCS_DIR,
|
|
107
|
-
'examples',
|
|
108
|
-
'asciiMaze',
|
|
109
|
-
'docs',
|
|
110
|
-
'README.md',
|
|
111
|
-
),
|
|
112
|
-
excludeRootSourceFiles: true,
|
|
113
|
-
publishedRootDir: path.join(DOCS_DIR, 'examples', 'asciiMaze'),
|
|
114
|
-
preservePublishedEntries: ['index.html'],
|
|
115
|
-
},
|
|
116
|
-
'flappy-bird': {
|
|
117
|
-
name: 'flappy-bird',
|
|
118
|
-
sourceDir: path.resolve('test', 'examples', 'flappy_bird'),
|
|
119
|
-
docsDir: path.join(DOCS_DIR, 'examples', 'flappy_bird', 'docs'),
|
|
120
|
-
rootDocsDir: path.join(DOCS_DIR, 'examples', 'flappy_bird', 'docs'),
|
|
121
|
-
rootReadmeSource: path.resolve(
|
|
122
|
-
'test',
|
|
123
|
-
'examples',
|
|
124
|
-
'flappy_bird',
|
|
125
|
-
'README.md',
|
|
126
|
-
),
|
|
127
|
-
rootReadmeDestination: path.join(
|
|
128
|
-
DOCS_DIR,
|
|
129
|
-
'examples',
|
|
130
|
-
'flappy_bird',
|
|
131
|
-
'docs',
|
|
132
|
-
'README.md',
|
|
133
|
-
),
|
|
134
|
-
excludeRootSourceFiles: true,
|
|
135
|
-
publishedRootDir: path.join(DOCS_DIR, 'examples', 'flappy_bird'),
|
|
136
|
-
preservePublishedEntries: ['index.html'],
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const project = new Project({
|
|
141
|
-
tsConfigFilePath: path.resolve('tsconfig.json'),
|
|
142
|
-
skipAddingFilesFromTsConfig: true,
|
|
143
|
-
});
|
|
144
|
-
const resolvedDirectoryDocsOrderConfigCache = new Map<
|
|
145
|
-
string,
|
|
146
|
-
LoadedDirectoryDocsOrderConfig | undefined
|
|
147
|
-
>();
|
|
148
|
-
const directoryDocsOrderConfigCache = new Map<
|
|
149
|
-
string,
|
|
150
|
-
Promise<LoadedDirectoryDocsOrderConfig | undefined>
|
|
151
|
-
>();
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Generates docs for the requested target.
|
|
155
|
-
*
|
|
156
|
-
* The script runs in four passes:
|
|
157
|
-
* 1. Resolve and prepare the target output tree.
|
|
158
|
-
* 2. Load source files into the shared ts-morph project.
|
|
159
|
-
* 3. Collect and normalize rendered symbols.
|
|
160
|
-
* 4. Emit directory README files and the optional folder index.
|
|
161
|
-
*/
|
|
162
|
-
async function main(): Promise<void> {
|
|
163
|
-
const target = resolveDocsTarget();
|
|
164
|
-
await initializeDocsTarget(target);
|
|
165
|
-
|
|
166
|
-
const sourceFiles = await loadTargetSourceFiles(target);
|
|
167
|
-
const directorySymbolMap = collectDirectorySymbols(sourceFiles);
|
|
168
|
-
|
|
169
|
-
dedupeDirectorySymbols(directorySymbolMap);
|
|
170
|
-
await emitDirectoryDocs(directorySymbolMap, target);
|
|
171
|
-
|
|
172
|
-
if (target.includeFolderIndex) {
|
|
173
|
-
await emitFolderIndex(directorySymbolMap, target);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
console.log(`[docs:${target.name}] Per-folder README generation complete.`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Resolves the active docs target from CLI or environment input.
|
|
181
|
-
*
|
|
182
|
-
* Lookup order:
|
|
183
|
-
* 1. `--target=...`
|
|
184
|
-
* 2. `DOCS_TARGET`
|
|
185
|
-
* 3. default target
|
|
186
|
-
*
|
|
187
|
-
* @returns Concrete docs target configuration.
|
|
188
|
-
*/
|
|
189
|
-
function resolveDocsTarget(): DocsTargetConfig {
|
|
190
|
-
const cliTarget = process.argv
|
|
191
|
-
.find((argument) => argument.startsWith('--target='))
|
|
192
|
-
?.slice('--target='.length);
|
|
193
|
-
const requestedTarget =
|
|
194
|
-
cliTarget || process.env.DOCS_TARGET || DEFAULT_DOCS_TARGET;
|
|
195
|
-
const resolvedTarget = DOCS_TARGETS[requestedTarget];
|
|
196
|
-
|
|
197
|
-
if (!resolvedTarget) {
|
|
198
|
-
const supportedTargets = Object.keys(DOCS_TARGETS).toSorted().join(', ');
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Unknown docs target "${requestedTarget}". Supported targets: ${supportedTargets}`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return resolvedTarget;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Prepares the output tree for the selected target.
|
|
209
|
-
*
|
|
210
|
-
* @param target - Target configuration.
|
|
211
|
-
* @returns Nothing.
|
|
212
|
-
*/
|
|
213
|
-
async function initializeDocsTarget(target: DocsTargetConfig): Promise<void> {
|
|
214
|
-
await cleanupPublishedRoot(target);
|
|
215
|
-
await fs.ensureDir(target.docsDir);
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
!target.rootReadmeSource ||
|
|
219
|
-
!target.rootReadmeDestination ||
|
|
220
|
-
!(await fs.pathExists(target.rootReadmeSource))
|
|
221
|
-
) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
await fs.ensureDir(path.dirname(target.rootReadmeDestination));
|
|
226
|
-
await fs.copyFile(target.rootReadmeSource, target.rootReadmeDestination);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Loads target source files into the shared ts-morph project.
|
|
231
|
-
*
|
|
232
|
-
* @param target - Target configuration.
|
|
233
|
-
* @returns Filtered source files that belong to the target tree.
|
|
234
|
-
*/
|
|
235
|
-
async function loadTargetSourceFiles(
|
|
236
|
-
target: DocsTargetConfig,
|
|
237
|
-
): Promise<SourceFile[]> {
|
|
238
|
-
const filePaths = await fg(SOURCE_FILE_GLOBS, {
|
|
239
|
-
cwd: target.sourceDir,
|
|
240
|
-
absolute: true,
|
|
241
|
-
ignore: SOURCE_FILE_IGNORE_GLOBS,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
for (const filePath of filePaths) {
|
|
245
|
-
if (!shouldIncludeSourceFile(filePath, target)) {
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
project.addSourceFileAtPath(filePath);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const rawSourceFiles = project.getSourceFiles();
|
|
253
|
-
const sourceFiles = rawSourceFiles.filter((sourceFile) => {
|
|
254
|
-
const filePath = sourceFile.getFilePath();
|
|
255
|
-
return !filePath.endsWith('.d.ts') && !/node_modules/.test(filePath);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
console.log(
|
|
259
|
-
`[docs:${target.name}] Loaded ${sourceFiles.length} source files (raw: ${rawSourceFiles.length})`,
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
return sourceFiles;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Determines whether a discovered file should participate in docs generation.
|
|
267
|
-
*
|
|
268
|
-
* @param filePath - Absolute source file path.
|
|
269
|
-
* @param target - Target configuration.
|
|
270
|
-
* @returns True when the file belongs in the target set.
|
|
271
|
-
*/
|
|
272
|
-
function shouldIncludeSourceFile(
|
|
273
|
-
filePath: string,
|
|
274
|
-
target: DocsTargetConfig,
|
|
275
|
-
): boolean {
|
|
276
|
-
if (/\.test\.ts$/i.test(filePath)) {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (!target.excludeRootSourceFiles) {
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const relativeDirectory = path.dirname(
|
|
285
|
-
path.relative(target.sourceDir, filePath),
|
|
286
|
-
);
|
|
287
|
-
return relativeDirectory !== '' && relativeDirectory !== '.';
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Removes previously published generated files for targets that mirror output
|
|
292
|
-
* into a browsable published directory.
|
|
293
|
-
*
|
|
294
|
-
* @param target - Target configuration.
|
|
295
|
-
* @returns Nothing.
|
|
296
|
-
*/
|
|
297
|
-
async function cleanupPublishedRoot(target: DocsTargetConfig): Promise<void> {
|
|
298
|
-
if (!target.publishedRootDir) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
await fs.ensureDir(target.publishedRootDir);
|
|
303
|
-
const preservedEntries = new Set(target.preservePublishedEntries ?? []);
|
|
304
|
-
const childEntries = await fs.readdir(target.publishedRootDir);
|
|
305
|
-
|
|
306
|
-
for (const childEntry of childEntries) {
|
|
307
|
-
if (preservedEntries.has(childEntry)) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
await fs.remove(path.join(target.publishedRootDir, childEntry));
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Collects renderable symbols grouped by directory and then by file.
|
|
317
|
-
*
|
|
318
|
-
* @param sourceFiles - Loaded source files to scan.
|
|
319
|
-
* @returns Nested symbol map keyed by directory and then source file path.
|
|
320
|
-
*/
|
|
321
|
-
function collectDirectorySymbols(
|
|
322
|
-
sourceFiles: readonly SourceFile[],
|
|
323
|
-
): DirectorySymbolMap {
|
|
324
|
-
const directorySymbolMap: DirectorySymbolMap = new Map();
|
|
325
|
-
|
|
326
|
-
for (const sourceFile of sourceFiles) {
|
|
327
|
-
collectSourceFileSymbols(sourceFile, directorySymbolMap);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return directorySymbolMap;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Collects every renderable symbol from a single source file.
|
|
335
|
-
*
|
|
336
|
-
* @param sourceFile - Source file being scanned.
|
|
337
|
-
* @param directorySymbolMap - Aggregate directory symbol map.
|
|
338
|
-
* @returns Nothing.
|
|
339
|
-
*/
|
|
340
|
-
function collectSourceFileSymbols(
|
|
341
|
-
sourceFile: SourceFile,
|
|
342
|
-
directorySymbolMap: DirectorySymbolMap,
|
|
343
|
-
): void {
|
|
344
|
-
const filePath = sourceFile.getFilePath();
|
|
345
|
-
const fileSymbolMap = resolveFileSymbolMap(directorySymbolMap, filePath);
|
|
346
|
-
|
|
347
|
-
addFileSummarySymbol(sourceFile, fileSymbolMap);
|
|
348
|
-
collectExportedDeclarations(sourceFile, fileSymbolMap);
|
|
349
|
-
collectDocumentedTopLevelDeclarations(sourceFile, fileSymbolMap);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Resolves the mutable file-symbol map for the file's directory.
|
|
354
|
-
*
|
|
355
|
-
* @param directorySymbolMap - Aggregate directory symbol map.
|
|
356
|
-
* @param filePath - Absolute source file path.
|
|
357
|
-
* @returns Per-directory file symbol map.
|
|
358
|
-
*/
|
|
359
|
-
function resolveFileSymbolMap(
|
|
360
|
-
directorySymbolMap: DirectorySymbolMap,
|
|
361
|
-
filePath: string,
|
|
362
|
-
): Map<string, RenderedSymbol[]> {
|
|
363
|
-
const directoryPath = path.dirname(filePath);
|
|
364
|
-
let fileSymbolMap = directorySymbolMap.get(directoryPath);
|
|
365
|
-
|
|
366
|
-
if (!fileSymbolMap) {
|
|
367
|
-
fileSymbolMap = new Map();
|
|
368
|
-
directorySymbolMap.set(directoryPath, fileSymbolMap);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return fileSymbolMap;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Adds a synthetic file-summary symbol when the file starts with a file-level
|
|
376
|
-
* JSDoc block.
|
|
377
|
-
*
|
|
378
|
-
* @param sourceFile - Source file being scanned.
|
|
379
|
-
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
380
|
-
* @returns Nothing.
|
|
381
|
-
*/
|
|
382
|
-
function addFileSummarySymbol(
|
|
383
|
-
sourceFile: SourceFile,
|
|
384
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
385
|
-
): void {
|
|
386
|
-
const fileSummary = resolveFileSummary(sourceFile);
|
|
387
|
-
if (!fileSummary.description && !fileSummary.examples?.length) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
appendRenderedSymbol(fileSymbolMap, sourceFile.getFilePath(), {
|
|
392
|
-
kind: 'File',
|
|
393
|
-
name: FILE_SUMMARY_SYMBOL_NAME,
|
|
394
|
-
filePath: sourceFile.getFilePath(),
|
|
395
|
-
jsdoc: {
|
|
396
|
-
description: fileSummary.description,
|
|
397
|
-
examples: fileSummary.examples,
|
|
398
|
-
},
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
console.log(`[docs] Added file summary for ${sourceFile.getBaseName()}`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Resolves the renderable file-summary JSDoc for a source file.
|
|
406
|
-
*
|
|
407
|
-
* @param sourceFile - Source file to inspect.
|
|
408
|
-
* @returns File summary description/examples when present.
|
|
409
|
-
*/
|
|
410
|
-
function resolveFileSummary(sourceFile: SourceFile): RenderedFileSummary {
|
|
411
|
-
const leadingFileSummary = extractLeadingFileJsDocSummary(sourceFile);
|
|
412
|
-
if (leadingFileSummary.description || leadingFileSummary.examples?.length) {
|
|
413
|
-
return leadingFileSummary;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const attachedFileSummary = extractAttachedFileJsDocSummary(sourceFile);
|
|
417
|
-
if (attachedFileSummary.description || attachedFileSummary.examples?.length) {
|
|
418
|
-
return attachedFileSummary;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
description: getFirstJsDocDescription(
|
|
423
|
-
sourceFile as unknown as { getJsDocs?: () => unknown[] },
|
|
424
|
-
),
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Collects exported declarations and supported child members.
|
|
430
|
-
*
|
|
431
|
-
* @param sourceFile - Source file being scanned.
|
|
432
|
-
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
433
|
-
* @returns Nothing.
|
|
434
|
-
*/
|
|
435
|
-
function collectExportedDeclarations(
|
|
436
|
-
sourceFile: SourceFile,
|
|
437
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
438
|
-
): void {
|
|
439
|
-
const exportedDeclarations = sourceFile.getExportedDeclarations();
|
|
440
|
-
if (exportedDeclarations.size === 0) {
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
console.log(
|
|
445
|
-
`[docs] ${sourceFile.getBaseName()} exports: ${exportedDeclarations.size}`,
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
for (const [, declarations] of exportedDeclarations) {
|
|
449
|
-
for (const declaration of declarations) {
|
|
450
|
-
const symbol = declaration.getSymbol();
|
|
451
|
-
if (!symbol) {
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const renderedSymbol = renderSymbol(
|
|
456
|
-
symbol,
|
|
457
|
-
declaration.getKindName(),
|
|
458
|
-
sourceFile.getFilePath(),
|
|
459
|
-
);
|
|
460
|
-
if (renderedSymbol) {
|
|
461
|
-
appendRenderedSymbol(
|
|
462
|
-
fileSymbolMap,
|
|
463
|
-
sourceFile.getFilePath(),
|
|
464
|
-
renderedSymbol,
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
collectExportedObjectLiteralMembers(
|
|
469
|
-
declaration as unknown as Record<string, unknown>,
|
|
470
|
-
symbol.getName(),
|
|
471
|
-
sourceFile.getFilePath(),
|
|
472
|
-
fileSymbolMap,
|
|
473
|
-
);
|
|
474
|
-
collectExportedClassMembers(
|
|
475
|
-
declaration as unknown as Record<string, unknown>,
|
|
476
|
-
symbol.getName(),
|
|
477
|
-
sourceFile.getFilePath(),
|
|
478
|
-
fileSymbolMap,
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Collects documented properties from exported object-literal variables.
|
|
486
|
-
*
|
|
487
|
-
* @param declaration - Candidate declaration.
|
|
488
|
-
* @param parentName - Parent exported symbol name.
|
|
489
|
-
* @param filePath - Absolute source file path.
|
|
490
|
-
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
491
|
-
* @returns Nothing.
|
|
492
|
-
*/
|
|
493
|
-
function collectExportedObjectLiteralMembers(
|
|
494
|
-
declaration: Record<string, unknown>,
|
|
495
|
-
parentName: string,
|
|
496
|
-
filePath: string,
|
|
497
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
498
|
-
): void {
|
|
499
|
-
try {
|
|
500
|
-
const declarationKindName = (
|
|
501
|
-
declaration.getKindName as (() => string) | undefined
|
|
502
|
-
)?.();
|
|
503
|
-
if (declarationKindName !== 'VariableDeclaration') {
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const initializer = (
|
|
508
|
-
declaration.getInitializer as (() => any) | undefined
|
|
509
|
-
)?.();
|
|
510
|
-
if (initializer?.getKindName?.() !== 'ObjectLiteralExpression') {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const properties = (initializer.getProperties?.() as any[]) || [];
|
|
515
|
-
for (const property of properties) {
|
|
516
|
-
const propertyName =
|
|
517
|
-
property.getName?.() || property.getSymbol?.()?.getName?.();
|
|
518
|
-
const renderedProperty = renderDeclaration(
|
|
519
|
-
property,
|
|
520
|
-
String(propertyName),
|
|
521
|
-
property.getKindName?.() || 'Property',
|
|
522
|
-
filePath,
|
|
523
|
-
parentName,
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
if (renderedProperty) {
|
|
527
|
-
appendRenderedSymbol(fileSymbolMap, filePath, renderedProperty);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
} catch {
|
|
531
|
-
// Best-effort introspection only.
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Collects documented members from exported classes.
|
|
537
|
-
*
|
|
538
|
-
* @param declaration - Candidate declaration.
|
|
539
|
-
* @param parentName - Parent exported symbol name.
|
|
540
|
-
* @param filePath - Absolute source file path.
|
|
541
|
-
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
542
|
-
* @returns Nothing.
|
|
543
|
-
*/
|
|
544
|
-
function collectExportedClassMembers(
|
|
545
|
-
declaration: Record<string, unknown>,
|
|
546
|
-
parentName: string,
|
|
547
|
-
filePath: string,
|
|
548
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
549
|
-
): void {
|
|
550
|
-
try {
|
|
551
|
-
const declarationKindName = (
|
|
552
|
-
declaration.getKindName as (() => string) | undefined
|
|
553
|
-
)?.();
|
|
554
|
-
if (declarationKindName !== 'ClassDeclaration') {
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const members =
|
|
559
|
-
(declaration.getMembers as (() => any[]) | undefined)?.() || [];
|
|
560
|
-
for (const member of members) {
|
|
561
|
-
const memberName = member.getName?.();
|
|
562
|
-
if (!memberName) {
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const renderedMember = renderDeclaration(
|
|
567
|
-
member,
|
|
568
|
-
String(memberName),
|
|
569
|
-
member.getKindName?.() || 'ClassMember',
|
|
570
|
-
filePath,
|
|
571
|
-
parentName,
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
if (renderedMember) {
|
|
575
|
-
appendRenderedSymbol(fileSymbolMap, filePath, renderedMember);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
} catch {
|
|
579
|
-
// Best-effort introspection only.
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Collects documented top-level non-exported declarations.
|
|
585
|
-
*
|
|
586
|
-
* @param sourceFile - Source file being scanned.
|
|
587
|
-
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
588
|
-
* @returns Nothing.
|
|
589
|
-
*/
|
|
590
|
-
function collectDocumentedTopLevelDeclarations(
|
|
591
|
-
sourceFile: SourceFile,
|
|
592
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
593
|
-
): void {
|
|
594
|
-
try {
|
|
595
|
-
const statements =
|
|
596
|
-
(
|
|
597
|
-
sourceFile as unknown as { getStatements?: () => any[] }
|
|
598
|
-
).getStatements?.() || [];
|
|
599
|
-
|
|
600
|
-
for (const statement of statements) {
|
|
601
|
-
const declarations = statement.getDeclarations?.() || [statement];
|
|
602
|
-
|
|
603
|
-
for (const declaration of declarations) {
|
|
604
|
-
const jsDocs = declaration.getJsDocs?.() || [];
|
|
605
|
-
if (jsDocs.length === 0) {
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const name =
|
|
610
|
-
declaration.getName?.() || declaration.getSymbol?.()?.getName?.();
|
|
611
|
-
if (!name) {
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
hasRenderedSymbol(
|
|
617
|
-
fileSymbolMap,
|
|
618
|
-
sourceFile.getFilePath(),
|
|
619
|
-
String(name),
|
|
620
|
-
)
|
|
621
|
-
) {
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const renderedDeclaration = renderDeclaration(
|
|
626
|
-
declaration,
|
|
627
|
-
String(name),
|
|
628
|
-
declaration.getKindName?.() || 'Declaration',
|
|
629
|
-
sourceFile.getFilePath(),
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
if (renderedDeclaration) {
|
|
633
|
-
appendRenderedSymbol(
|
|
634
|
-
fileSymbolMap,
|
|
635
|
-
sourceFile.getFilePath(),
|
|
636
|
-
renderedDeclaration,
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
} catch {
|
|
642
|
-
// Best-effort introspection only.
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Checks whether a symbol name has already been collected for a file.
|
|
648
|
-
*
|
|
649
|
-
* @param fileSymbolMap - Mutable symbol map for the directory.
|
|
650
|
-
* @param filePath - Absolute source file path.
|
|
651
|
-
* @param name - Candidate symbol name.
|
|
652
|
-
* @returns True when the symbol already exists in the collected set.
|
|
653
|
-
*/
|
|
654
|
-
function hasRenderedSymbol(
|
|
655
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
656
|
-
filePath: string,
|
|
657
|
-
name: string,
|
|
658
|
-
): boolean {
|
|
659
|
-
return (fileSymbolMap.get(filePath) ?? []).some(
|
|
660
|
-
(symbol) => symbol.name === name,
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Appends a rendered symbol to the per-file collection.
|
|
666
|
-
*
|
|
667
|
-
* @param fileSymbolMap - Mutable symbol map for the directory.
|
|
668
|
-
* @param filePath - Absolute source file path.
|
|
669
|
-
* @param renderedSymbol - Symbol to append.
|
|
670
|
-
* @returns Nothing.
|
|
671
|
-
*/
|
|
672
|
-
function appendRenderedSymbol(
|
|
673
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
674
|
-
filePath: string,
|
|
675
|
-
renderedSymbol: RenderedSymbol,
|
|
676
|
-
): void {
|
|
677
|
-
const renderedSymbols = fileSymbolMap.get(filePath) ?? [];
|
|
678
|
-
renderedSymbols.push(renderedSymbol);
|
|
679
|
-
fileSymbolMap.set(filePath, renderedSymbols);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Tidies and deduplicates collected symbols before README rendering.
|
|
684
|
-
*
|
|
685
|
-
* @param directorySymbolMap - Directory symbol map to normalize.
|
|
686
|
-
* @returns Nothing.
|
|
687
|
-
*/
|
|
688
|
-
function dedupeDirectorySymbols(directorySymbolMap: DirectorySymbolMap): void {
|
|
689
|
-
for (const [, fileSymbolMap] of directorySymbolMap) {
|
|
690
|
-
for (const [filePath, renderedSymbols] of fileSymbolMap) {
|
|
691
|
-
fileSymbolMap.set(filePath, dedupeFileSymbols(renderedSymbols, filePath));
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* Deduplicates symbols for a single file while conservatively merging JSDoc.
|
|
698
|
-
*
|
|
699
|
-
* @param renderedSymbols - Symbols collected for one file.
|
|
700
|
-
* @param filePath - Absolute source file path.
|
|
701
|
-
* @returns Deduplicated symbol list.
|
|
702
|
-
*/
|
|
703
|
-
function dedupeFileSymbols(
|
|
704
|
-
renderedSymbols: readonly RenderedSymbol[],
|
|
705
|
-
filePath: string,
|
|
706
|
-
): RenderedSymbol[] {
|
|
707
|
-
const seen = new Map<string, RenderedSymbol>();
|
|
708
|
-
const dedupedSymbols: RenderedSymbol[] = [];
|
|
709
|
-
|
|
710
|
-
for (const renderedSymbol of renderedSymbols) {
|
|
711
|
-
const normalizedName = normalizeName(renderedSymbol, filePath);
|
|
712
|
-
const dedupeKey = `${renderedSymbol.parent || ''}::${normalizedName}::${renderedSymbol.signature || ''}`;
|
|
713
|
-
const existingSymbol = seen.get(dedupeKey);
|
|
714
|
-
|
|
715
|
-
if (existingSymbol) {
|
|
716
|
-
mergeRenderedSymbols(existingSymbol, renderedSymbol);
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const clonedSymbol: RenderedSymbol = {
|
|
721
|
-
...renderedSymbol,
|
|
722
|
-
name: normalizedName,
|
|
723
|
-
jsdoc: {
|
|
724
|
-
...renderedSymbol.jsdoc,
|
|
725
|
-
params: renderedSymbol.jsdoc.params?.slice(),
|
|
726
|
-
examples: renderedSymbol.jsdoc.examples?.slice(),
|
|
727
|
-
},
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
seen.set(dedupeKey, clonedSymbol);
|
|
731
|
-
dedupedSymbols.push(clonedSymbol);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
return dedupedSymbols;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Merges missing JSDoc fields from a duplicate symbol into the retained one.
|
|
739
|
-
*
|
|
740
|
-
* @param target - Existing retained symbol.
|
|
741
|
-
* @param incoming - Duplicate symbol carrying possible extra data.
|
|
742
|
-
* @returns Nothing.
|
|
743
|
-
*/
|
|
744
|
-
function mergeRenderedSymbols(
|
|
745
|
-
target: RenderedSymbol,
|
|
746
|
-
incoming: RenderedSymbol,
|
|
747
|
-
): void {
|
|
748
|
-
target.jsdoc.description =
|
|
749
|
-
target.jsdoc.description || incoming.jsdoc.description;
|
|
750
|
-
target.jsdoc.summary = target.jsdoc.summary || incoming.jsdoc.summary;
|
|
751
|
-
target.jsdoc.deprecated =
|
|
752
|
-
target.jsdoc.deprecated || incoming.jsdoc.deprecated;
|
|
753
|
-
|
|
754
|
-
if (incoming.jsdoc.examples?.length) {
|
|
755
|
-
target.jsdoc.examples = [
|
|
756
|
-
...(target.jsdoc.examples || []),
|
|
757
|
-
...incoming.jsdoc.examples.filter(
|
|
758
|
-
(incomingExample) => !(target.jsdoc.examples || []).includes(incomingExample),
|
|
759
|
-
),
|
|
760
|
-
];
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (incoming.jsdoc.params?.length) {
|
|
764
|
-
target.jsdoc.params = (target.jsdoc.params || []).slice();
|
|
765
|
-
|
|
766
|
-
for (const parameter of incoming.jsdoc.params) {
|
|
767
|
-
const alreadyPresent = target.jsdoc.params.some(
|
|
768
|
-
(existingParameter) => existingParameter.name === parameter.name,
|
|
769
|
-
);
|
|
770
|
-
if (!alreadyPresent) {
|
|
771
|
-
target.jsdoc.params.push(parameter);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (!target.signature && incoming.signature) {
|
|
777
|
-
target.signature = incoming.signature;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* Emits directory README files to both docs output and the source tree.
|
|
783
|
-
*
|
|
784
|
-
* @param directorySymbolMap - Normalized directory symbol map.
|
|
785
|
-
* @param target - Target configuration.
|
|
786
|
-
* @returns Nothing.
|
|
787
|
-
*/
|
|
788
|
-
async function emitDirectoryDocs(
|
|
789
|
-
directorySymbolMap: DirectorySymbolMap,
|
|
790
|
-
target: DocsTargetConfig,
|
|
791
|
-
): Promise<void> {
|
|
792
|
-
for (const [directoryPath, fileSymbolMap] of directorySymbolMap) {
|
|
793
|
-
const relativeDirectory = path.relative(target.sourceDir, directoryPath);
|
|
794
|
-
if (relativeDirectory.startsWith('..')) {
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const outputDirectory =
|
|
799
|
-
relativeDirectory === ''
|
|
800
|
-
? target.rootDocsDir
|
|
801
|
-
: path.join(target.docsDir, relativeDirectory);
|
|
802
|
-
const sourceDirectory =
|
|
803
|
-
relativeDirectory === ''
|
|
804
|
-
? target.sourceDir
|
|
805
|
-
: path.join(target.sourceDir, relativeDirectory);
|
|
806
|
-
|
|
807
|
-
await loadDirectoryDocsOrderConfig(sourceDirectory);
|
|
808
|
-
|
|
809
|
-
const markdown = buildDirectoryReadme(
|
|
810
|
-
relativeDirectory,
|
|
811
|
-
fileSymbolMap,
|
|
812
|
-
target.sourceDir,
|
|
813
|
-
);
|
|
814
|
-
|
|
815
|
-
await fs.ensureDir(outputDirectory);
|
|
816
|
-
await writeFileIfChanged(path.join(outputDirectory, 'README.md'), markdown);
|
|
817
|
-
await writeFileIfChanged(path.join(sourceDirectory, 'README.md'), markdown);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Emits the folder index for targets that want a docs landing page.
|
|
823
|
-
*
|
|
824
|
-
* @param directorySymbolMap - Normalized directory symbol map.
|
|
825
|
-
* @param target - Target configuration.
|
|
826
|
-
* @returns Nothing.
|
|
827
|
-
*/
|
|
828
|
-
async function emitFolderIndex(
|
|
829
|
-
directorySymbolMap: DirectorySymbolMap,
|
|
830
|
-
target: DocsTargetConfig,
|
|
831
|
-
): Promise<void> {
|
|
832
|
-
const rootNode: FolderIndexNode = {
|
|
833
|
-
name: 'src',
|
|
834
|
-
path: 'src',
|
|
835
|
-
children: new Map(),
|
|
836
|
-
fileCount: 0,
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
const relativeDirectories = [...directorySymbolMap.keys()]
|
|
840
|
-
.map((directoryPath) => path.relative(target.sourceDir, directoryPath))
|
|
841
|
-
.filter((relativeDirectory) => !relativeDirectory.startsWith('..'));
|
|
842
|
-
|
|
843
|
-
const directoriesWithConfigSupport = [
|
|
844
|
-
target.sourceDir,
|
|
845
|
-
...relativeDirectories.map((relativeDirectory) =>
|
|
846
|
-
path.join(target.sourceDir, relativeDirectory),
|
|
847
|
-
),
|
|
848
|
-
];
|
|
849
|
-
await Promise.all(
|
|
850
|
-
[...new Set(directoriesWithConfigSupport)].map((directoryPath) =>
|
|
851
|
-
loadDirectoryDocsOrderConfig(directoryPath),
|
|
852
|
-
),
|
|
853
|
-
);
|
|
854
|
-
|
|
855
|
-
for (const relativeDirectory of relativeDirectories) {
|
|
856
|
-
insertFolderIndexNode(
|
|
857
|
-
rootNode,
|
|
858
|
-
relativeDirectory,
|
|
859
|
-
target,
|
|
860
|
-
directorySymbolMap,
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const markdown = buildFolderIndexMarkdown(rootNode);
|
|
865
|
-
await writeFileIfChanged(
|
|
866
|
-
path.join(DOCS_DIR, FOLDER_INDEX_FILE_NAME),
|
|
867
|
-
markdown,
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* Inserts one relative directory into the folder index tree.
|
|
873
|
-
*
|
|
874
|
-
* @param rootNode - Root folder index node.
|
|
875
|
-
* @param relativeDirectory - Relative directory from the target source root.
|
|
876
|
-
* @param target - Target configuration.
|
|
877
|
-
* @param directorySymbolMap - Directory symbol map used for file counts.
|
|
878
|
-
* @returns Nothing.
|
|
879
|
-
*/
|
|
880
|
-
function insertFolderIndexNode(
|
|
881
|
-
rootNode: FolderIndexNode,
|
|
882
|
-
relativeDirectory: string,
|
|
883
|
-
target: DocsTargetConfig,
|
|
884
|
-
directorySymbolMap: DirectorySymbolMap,
|
|
885
|
-
): void {
|
|
886
|
-
const normalizedDirectory =
|
|
887
|
-
relativeDirectory === '' ? 'src' : relativeDirectory.replace(/\\/g, '/');
|
|
888
|
-
const pathParts = normalizedDirectory.split('/');
|
|
889
|
-
|
|
890
|
-
let currentNode = rootNode;
|
|
891
|
-
let accumulatedPath = '';
|
|
892
|
-
|
|
893
|
-
for (const pathPart of pathParts) {
|
|
894
|
-
accumulatedPath = accumulatedPath
|
|
895
|
-
? `${accumulatedPath}/${pathPart}`
|
|
896
|
-
: pathPart;
|
|
897
|
-
|
|
898
|
-
if (!currentNode.children.has(pathPart)) {
|
|
899
|
-
currentNode.children.set(pathPart, {
|
|
900
|
-
name: pathPart,
|
|
901
|
-
path: accumulatedPath,
|
|
902
|
-
children: new Map(),
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
currentNode = currentNode.children.get(pathPart)!;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
const absoluteDirectoryPath = path.join(
|
|
910
|
-
target.sourceDir,
|
|
911
|
-
relativeDirectory === '' ? '' : relativeDirectory,
|
|
912
|
-
);
|
|
913
|
-
const fileSymbolMap = directorySymbolMap.get(absoluteDirectoryPath);
|
|
914
|
-
if (fileSymbolMap) {
|
|
915
|
-
currentNode.fileCount = resolveVisibleDirectoryFiles(
|
|
916
|
-
absoluteDirectoryPath,
|
|
917
|
-
[...fileSymbolMap.keys()],
|
|
918
|
-
).length;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
/**
|
|
923
|
-
* Builds markdown for the generated folder index.
|
|
924
|
-
*
|
|
925
|
-
* @param rootNode - Root folder index node.
|
|
926
|
-
* @returns Folder index markdown.
|
|
927
|
-
*/
|
|
928
|
-
function buildFolderIndexMarkdown(rootNode: FolderIndexNode): string {
|
|
929
|
-
const lines = [
|
|
930
|
-
'# Docs Index',
|
|
931
|
-
'',
|
|
932
|
-
'Auto-generated index of source folders (click to open folder README).',
|
|
933
|
-
'',
|
|
934
|
-
];
|
|
935
|
-
|
|
936
|
-
renderFolderIndexNode(rootNode, 0, lines);
|
|
937
|
-
return `${lines.join('\n')}\n`;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* Renders one folder index node and its children recursively.
|
|
942
|
-
*
|
|
943
|
-
* @param node - Current folder index node.
|
|
944
|
-
* @param level - Nesting level.
|
|
945
|
-
* @param lines - Output line buffer.
|
|
946
|
-
* @returns Nothing.
|
|
947
|
-
*/
|
|
948
|
-
function renderFolderIndexNode(
|
|
949
|
-
node: FolderIndexNode,
|
|
950
|
-
level: number,
|
|
951
|
-
lines: string[],
|
|
952
|
-
): void {
|
|
953
|
-
const indent = ' '.repeat(Math.max(0, level));
|
|
954
|
-
const label = node.path === 'src' && level === 0 ? 'src (root)' : node.name;
|
|
955
|
-
const countSuffix = node.fileCount
|
|
956
|
-
? ` — ${node.fileCount} file${node.fileCount > 1 ? 's' : ''}`
|
|
957
|
-
: '';
|
|
958
|
-
|
|
959
|
-
lines.push(`${indent}- [${label}](${node.path}/README.md)${countSuffix}`);
|
|
960
|
-
|
|
961
|
-
const childNames = resolveSortedFolderIndexChildNames(node);
|
|
962
|
-
for (const childName of childNames) {
|
|
963
|
-
renderFolderIndexNode(node.children.get(childName)!, level + 1, lines);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
/**
|
|
968
|
-
* Resolves child-folder order for one folder-index node.
|
|
969
|
-
*
|
|
970
|
-
* Configured folder order only affects direct children of the current folder.
|
|
971
|
-
* Unspecified folders remain on the previous alphabetical fallback path.
|
|
972
|
-
*
|
|
973
|
-
* @param node - Folder index node whose children should be ordered.
|
|
974
|
-
* @returns Sorted child folder names.
|
|
975
|
-
*/
|
|
976
|
-
function resolveSortedFolderIndexChildNames(node: FolderIndexNode): string[] {
|
|
977
|
-
const loadedConfig = getCachedDirectoryDocsOrderConfig(path.resolve(node.path));
|
|
978
|
-
const configuredFolderOrder = loadedConfig?.config.folderOrder;
|
|
979
|
-
|
|
980
|
-
if (configuredFolderOrder?.length) {
|
|
981
|
-
warnForUnknownConfiguredFolderIndexChildren(
|
|
982
|
-
configuredFolderOrder,
|
|
983
|
-
[...node.children.keys()],
|
|
984
|
-
loadedConfig?.configPath,
|
|
985
|
-
node.path,
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const explicitFolderOrderIndex = new Map(
|
|
990
|
-
(configuredFolderOrder ?? []).map((folderName, index) => [folderName, index] as const),
|
|
991
|
-
);
|
|
992
|
-
|
|
993
|
-
return [...node.children.keys()].toSorted((leftName, rightName) => {
|
|
994
|
-
const leftExplicitOrder = explicitFolderOrderIndex.get(leftName);
|
|
995
|
-
const rightExplicitOrder = explicitFolderOrderIndex.get(rightName);
|
|
996
|
-
|
|
997
|
-
if (leftExplicitOrder !== undefined || rightExplicitOrder !== undefined) {
|
|
998
|
-
if (leftExplicitOrder === undefined) {
|
|
999
|
-
return 1;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
if (rightExplicitOrder === undefined) {
|
|
1003
|
-
return -1;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
if (leftExplicitOrder !== rightExplicitOrder) {
|
|
1007
|
-
return leftExplicitOrder - rightExplicitOrder;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
return leftName.localeCompare(rightName);
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
/**
|
|
1016
|
-
* Extracts and parses the leading file-level JSDoc block from raw source text.
|
|
1017
|
-
*
|
|
1018
|
-
* @param sourceFile - Source file to inspect.
|
|
1019
|
-
* @returns Parsed top-of-file summary description/examples when present.
|
|
1020
|
-
*/
|
|
1021
|
-
function extractLeadingFileJsDocSummary(
|
|
1022
|
-
sourceFile: SourceFile,
|
|
1023
|
-
): RenderedFileSummary {
|
|
1024
|
-
const text = sourceFile.getFullText();
|
|
1025
|
-
const match = text.match(/^\s*(?:\uFEFF)?\/\*\*([\s\S]*?)\*\//);
|
|
1026
|
-
if (!match) {
|
|
1027
|
-
return {};
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const cleanedText = match[1]
|
|
1031
|
-
.split(/\r?\n/)
|
|
1032
|
-
.map((line) => line.replace(/^\s*\*\s?/, ''))
|
|
1033
|
-
.join('\n')
|
|
1034
|
-
.trim();
|
|
1035
|
-
|
|
1036
|
-
if (!cleanedText) {
|
|
1037
|
-
return {};
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
if (
|
|
1041
|
-
cleanedText.split('\n').some((line) => line.trim().startsWith('@internal'))
|
|
1042
|
-
) {
|
|
1043
|
-
return {};
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
return parseFileSummaryBlock(cleanedText);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/**
|
|
1050
|
-
* Parses a cleaned file-summary JSDoc block into description and examples.
|
|
1051
|
-
*
|
|
1052
|
-
* @param cleanedText - Cleaned raw file-summary block text.
|
|
1053
|
-
* @returns Parsed description/examples.
|
|
1054
|
-
*/
|
|
1055
|
-
function parseFileSummaryBlock(cleanedText: string): RenderedFileSummary {
|
|
1056
|
-
const tagStartIndex = cleanedText.search(/^@\w+/m);
|
|
1057
|
-
const description = sanitizeTagBlockText(
|
|
1058
|
-
tagStartIndex >= 0 ? cleanedText.slice(0, tagStartIndex) : cleanedText,
|
|
1059
|
-
);
|
|
1060
|
-
const examples = extractFileSummaryExamples(cleanedText);
|
|
1061
|
-
|
|
1062
|
-
return {
|
|
1063
|
-
description,
|
|
1064
|
-
examples,
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/**
|
|
1069
|
-
* Extracts file-summary example blocks from a cleaned raw JSDoc block.
|
|
1070
|
-
*
|
|
1071
|
-
* @param cleanedText - Cleaned raw file-summary block text.
|
|
1072
|
-
* @returns Example blocks when present.
|
|
1073
|
-
*/
|
|
1074
|
-
function extractFileSummaryExamples(
|
|
1075
|
-
cleanedText: string,
|
|
1076
|
-
): string[] | undefined {
|
|
1077
|
-
const exampleMatches = [
|
|
1078
|
-
...cleanedText.matchAll(/(^|\n)@example\s*([\s\S]*?)(?=\n@\w+|$)/g),
|
|
1079
|
-
];
|
|
1080
|
-
|
|
1081
|
-
const examples = exampleMatches
|
|
1082
|
-
.map((match) => sanitizeTagBlockText(match[2]))
|
|
1083
|
-
.filter((example): example is string => Boolean(example));
|
|
1084
|
-
|
|
1085
|
-
return examples.length > 0 ? examples : undefined;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Extracts module-summary JSDoc blocks that are attached to the first
|
|
1090
|
-
* top-level declaration after imports/exports.
|
|
1091
|
-
*
|
|
1092
|
-
* @param sourceFile - Source file to inspect.
|
|
1093
|
-
* @returns Parsed description/examples when attached module-summary blocks exist.
|
|
1094
|
-
*/
|
|
1095
|
-
function extractAttachedFileJsDocSummary(
|
|
1096
|
-
sourceFile: SourceFile,
|
|
1097
|
-
): RenderedFileSummary {
|
|
1098
|
-
const firstStatementWithMultipleJsDocs = sourceFile
|
|
1099
|
-
.getStatements()
|
|
1100
|
-
.find((statement) => {
|
|
1101
|
-
const statementJsDocs = getJsDocs(statement);
|
|
1102
|
-
return statementJsDocs.length > 1 && !hasInternalTag(statementJsDocs);
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
if (!firstStatementWithMultipleJsDocs) {
|
|
1106
|
-
return {};
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const summaryJsDocs = getJsDocs(firstStatementWithMultipleJsDocs).slice(0, -1);
|
|
1110
|
-
return mergeRenderedFileSummaries(summaryJsDocs.map(renderJsDocSummaryBlock));
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
/**
|
|
1114
|
-
* Renders one exported symbol when it belongs to a supported declaration kind
|
|
1115
|
-
* and is not marked internal.
|
|
1116
|
-
*
|
|
1117
|
-
* @param symbol - Exported symbol.
|
|
1118
|
-
* @param fallbackKind - Declaration kind used as a fallback label.
|
|
1119
|
-
* @param filePath - Absolute source file path.
|
|
1120
|
-
* @returns Rendered symbol or null when not supported.
|
|
1121
|
-
*/
|
|
1122
|
-
function renderSymbol(
|
|
1123
|
-
symbol: MorphSymbol,
|
|
1124
|
-
fallbackKind: string,
|
|
1125
|
-
filePath: string,
|
|
1126
|
-
): RenderedSymbol | null {
|
|
1127
|
-
const declaration = symbol.getDeclarations()[0];
|
|
1128
|
-
if (!declaration) {
|
|
1129
|
-
return null;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
const declarationKind = declaration.getKindName();
|
|
1133
|
-
const supportedKinds =
|
|
1134
|
-
/ClassDeclaration|FunctionDeclaration|InterfaceDeclaration|EnumDeclaration|TypeAliasDeclaration|VariableDeclaration/;
|
|
1135
|
-
if (!supportedKinds.test(declarationKind)) {
|
|
1136
|
-
return null;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
const jsDocs = resolveRenderableJsDocs(declaration);
|
|
1140
|
-
if (hasInternalTag(jsDocs)) {
|
|
1141
|
-
return null;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
const primaryJsDoc = jsDocs.at(-1);
|
|
1145
|
-
const fullDescription = primaryJsDoc?.getDescription().trim();
|
|
1146
|
-
|
|
1147
|
-
return {
|
|
1148
|
-
kind: fallbackKind || declarationKind,
|
|
1149
|
-
name: symbol.getName(),
|
|
1150
|
-
parent: undefined,
|
|
1151
|
-
filePath,
|
|
1152
|
-
signature: resolveCallSignature(declaration),
|
|
1153
|
-
jsdoc: {
|
|
1154
|
-
summary: fullDescription?.split(/\r?\n\r?\n/)[0]?.trim(),
|
|
1155
|
-
description: fullDescription,
|
|
1156
|
-
params: extractParamDocs(primaryJsDoc?.getTags() || []),
|
|
1157
|
-
examples: extractExampleDocs(primaryJsDoc?.getTags() || []),
|
|
1158
|
-
returns: getTagCommentText(
|
|
1159
|
-
primaryJsDoc
|
|
1160
|
-
?.getTags()
|
|
1161
|
-
.find(
|
|
1162
|
-
(tag: JSDocTag) =>
|
|
1163
|
-
tag.getTagName() === 'returns' || tag.getTagName() === 'return',
|
|
1164
|
-
),
|
|
1165
|
-
),
|
|
1166
|
-
deprecated: getTagCommentText(
|
|
1167
|
-
primaryJsDoc
|
|
1168
|
-
?.getTags()
|
|
1169
|
-
.find((tag: JSDocTag) => tag.getTagName() === 'deprecated'),
|
|
1170
|
-
),
|
|
1171
|
-
},
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
/**
|
|
1176
|
-
* Renders a declaration-like node that may not have a directly usable symbol.
|
|
1177
|
-
*
|
|
1178
|
-
* @param declaration - Declaration-like node.
|
|
1179
|
-
* @param forcedName - Optional forced display name.
|
|
1180
|
-
* @param forcedKind - Optional forced kind label.
|
|
1181
|
-
* @param filePath - Absolute source file path.
|
|
1182
|
-
* @param parentName - Optional parent display name.
|
|
1183
|
-
* @returns Rendered symbol or null when not renderable.
|
|
1184
|
-
*/
|
|
1185
|
-
function renderDeclaration(
|
|
1186
|
-
declaration: any,
|
|
1187
|
-
forcedName?: string,
|
|
1188
|
-
forcedKind?: string,
|
|
1189
|
-
filePath?: string,
|
|
1190
|
-
parentName?: string,
|
|
1191
|
-
): RenderedSymbol | null {
|
|
1192
|
-
if (!declaration) {
|
|
1193
|
-
return null;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
try {
|
|
1197
|
-
const jsDocs = resolveRenderableJsDocs(declaration);
|
|
1198
|
-
if (jsDocs.length === 0 || hasInternalTag(jsDocs)) {
|
|
1199
|
-
return null;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const primaryJsDoc = jsDocs.at(-1);
|
|
1203
|
-
const fullDescription = primaryJsDoc?.getDescription?.()?.trim();
|
|
1204
|
-
|
|
1205
|
-
return {
|
|
1206
|
-
kind:
|
|
1207
|
-
forcedKind ||
|
|
1208
|
-
declaration.getKindName?.() ||
|
|
1209
|
-
declaration.getKind?.() ||
|
|
1210
|
-
'Declaration',
|
|
1211
|
-
name:
|
|
1212
|
-
forcedName ||
|
|
1213
|
-
declaration.getName?.() ||
|
|
1214
|
-
declaration.getSymbol?.()?.getName?.() ||
|
|
1215
|
-
(parentName ? `${parentName}.${forcedName}` : ''),
|
|
1216
|
-
parent: parentName || undefined,
|
|
1217
|
-
filePath: filePath || '',
|
|
1218
|
-
signature: resolveCallSignature(declaration),
|
|
1219
|
-
jsdoc: {
|
|
1220
|
-
summary: fullDescription?.split(/\r?\n\r?\n/)[0]?.trim(),
|
|
1221
|
-
description: fullDescription,
|
|
1222
|
-
params: extractParamDocs(primaryJsDoc?.getTags() || []),
|
|
1223
|
-
examples: extractExampleDocs(primaryJsDoc?.getTags() || []),
|
|
1224
|
-
returns: getTagCommentText(
|
|
1225
|
-
primaryJsDoc
|
|
1226
|
-
?.getTags()
|
|
1227
|
-
.find(
|
|
1228
|
-
(tag: JSDocTag) =>
|
|
1229
|
-
tag.getTagName() === 'returns' || tag.getTagName() === 'return',
|
|
1230
|
-
),
|
|
1231
|
-
),
|
|
1232
|
-
deprecated: getTagCommentText(
|
|
1233
|
-
primaryJsDoc
|
|
1234
|
-
?.getTags()
|
|
1235
|
-
.find((tag: JSDocTag) => tag.getTagName() === 'deprecated'),
|
|
1236
|
-
),
|
|
1237
|
-
},
|
|
1238
|
-
};
|
|
1239
|
-
} catch {
|
|
1240
|
-
return null;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* Returns JSDoc blocks from a declaration-like node.
|
|
1246
|
-
*
|
|
1247
|
-
* @param node - Declaration-like node.
|
|
1248
|
-
* @returns JSDoc array.
|
|
1249
|
-
*/
|
|
1250
|
-
function getJsDocs(node: any): any[] {
|
|
1251
|
-
return (node.getJsDocs?.() as any[]) || [];
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
/**
|
|
1255
|
-
* Resolves the JSDoc blocks that should be used for a renderable declaration.
|
|
1256
|
-
*
|
|
1257
|
-
* Exported constants are commonly documented on their enclosing
|
|
1258
|
-
* `VariableStatement` instead of on the inner `VariableDeclaration`. This
|
|
1259
|
-
* helper falls back to that enclosing statement so generated README entries for
|
|
1260
|
-
* constants keep their descriptions.
|
|
1261
|
-
*
|
|
1262
|
-
* @param node - Declaration-like node.
|
|
1263
|
-
* @returns JSDoc array, preferring direct docs and falling back when needed.
|
|
1264
|
-
*/
|
|
1265
|
-
function resolveRenderableJsDocs(node: any): any[] {
|
|
1266
|
-
const directJsDocs = getJsDocs(node);
|
|
1267
|
-
if (directJsDocs.length > 0) {
|
|
1268
|
-
return directJsDocs;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
if (node?.getKindName?.() !== 'VariableDeclaration') {
|
|
1272
|
-
return directJsDocs;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
const variableStatement =
|
|
1276
|
-
node.getFirstAncestorByKindName?.('VariableStatement') ||
|
|
1277
|
-
node.getVariableStatement?.();
|
|
1278
|
-
return getJsDocs(variableStatement);
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* Resolves the first JSDoc description from a declaration-like node.
|
|
1283
|
-
*
|
|
1284
|
-
* @param node - Declaration-like node.
|
|
1285
|
-
* @returns Description text when present.
|
|
1286
|
-
*/
|
|
1287
|
-
function getFirstJsDocDescription(node: {
|
|
1288
|
-
getJsDocs?: () => unknown[];
|
|
1289
|
-
}): string | undefined {
|
|
1290
|
-
const firstJsDoc = getJsDocs(node)[0];
|
|
1291
|
-
return firstJsDoc?.getDescription?.()?.trim() as string | undefined;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
/**
|
|
1295
|
-
* Checks whether any provided JSDoc block contains an internal tag.
|
|
1296
|
-
*
|
|
1297
|
-
* @param jsDocs - JSDoc array to inspect.
|
|
1298
|
-
* @returns True when the declaration is marked internal.
|
|
1299
|
-
*/
|
|
1300
|
-
function hasInternalTag(jsDocs: readonly any[]): boolean {
|
|
1301
|
-
return jsDocs.some((jsDoc) =>
|
|
1302
|
-
jsDoc
|
|
1303
|
-
.getTags()
|
|
1304
|
-
.some((tag: { getTagName(): string }) => tag.getTagName() === 'internal'),
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
/**
|
|
1309
|
-
* Resolves a call signature string from a declaration-like node.
|
|
1310
|
-
*
|
|
1311
|
-
* @param declaration - Declaration-like node.
|
|
1312
|
-
* @returns Signature string when the declaration is callable.
|
|
1313
|
-
*/
|
|
1314
|
-
function resolveCallSignature(declaration: any): string | undefined {
|
|
1315
|
-
try {
|
|
1316
|
-
const declarationType =
|
|
1317
|
-
declaration.getType?.() || declaration.getSymbol?.()?.getType?.();
|
|
1318
|
-
const callSignature = declarationType?.getCallSignatures?.()[0];
|
|
1319
|
-
if (!callSignature) {
|
|
1320
|
-
return undefined;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
const renderedParameters = callSignature
|
|
1324
|
-
.getParameters()
|
|
1325
|
-
.map((parameter: any) => {
|
|
1326
|
-
const parameterDeclarations = parameter.getDeclarations();
|
|
1327
|
-
const parameterType = normalizeRenderedTypeText(
|
|
1328
|
-
parameter
|
|
1329
|
-
.getTypeAtLocation(parameterDeclarations[0] || declaration)
|
|
1330
|
-
.getText(),
|
|
1331
|
-
);
|
|
1332
|
-
return `${parameter.getName()}: ${parameterType}`;
|
|
1333
|
-
})
|
|
1334
|
-
.join(', ');
|
|
1335
|
-
|
|
1336
|
-
const returnType = normalizeRenderedTypeText(
|
|
1337
|
-
callSignature.getReturnType().getText(),
|
|
1338
|
-
);
|
|
1339
|
-
return `(${renderedParameters}) => ${returnType}`;
|
|
1340
|
-
} catch {
|
|
1341
|
-
return undefined;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
/**
|
|
1346
|
-
* Formats a stored call signature into a human-friendly TypeScript block.
|
|
1347
|
-
*
|
|
1348
|
-
* @param symbolName - Symbol name shown in the heading.
|
|
1349
|
-
* @param signature - Stored canonical call signature.
|
|
1350
|
-
* @returns Fenced TypeScript block lines.
|
|
1351
|
-
*/
|
|
1352
|
-
function renderSignatureBlock(symbolName: string, signature: string): string[] {
|
|
1353
|
-
const parsedSignature = parseCallSignature(signature);
|
|
1354
|
-
if (!parsedSignature) {
|
|
1355
|
-
return ['```ts', `${symbolName}${signature}`, '```'];
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
if (parsedSignature.parameters.length === 0) {
|
|
1359
|
-
return [
|
|
1360
|
-
'```ts',
|
|
1361
|
-
`${symbolName}(): ${parsedSignature.returnType}`,
|
|
1362
|
-
'```',
|
|
1363
|
-
];
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
return [
|
|
1367
|
-
'```ts',
|
|
1368
|
-
`${symbolName}(`,
|
|
1369
|
-
...parsedSignature.parameters.map((parameter) => ` ${parameter},`),
|
|
1370
|
-
`): ${parsedSignature.returnType}`,
|
|
1371
|
-
'```',
|
|
1372
|
-
];
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
/**
|
|
1376
|
-
* Parses a canonical stored call signature.
|
|
1377
|
-
*
|
|
1378
|
-
* @param signature - Stored canonical call signature.
|
|
1379
|
-
* @returns Parsed parameters and return type when recognized.
|
|
1380
|
-
*/
|
|
1381
|
-
function parseCallSignature(
|
|
1382
|
-
signature: string,
|
|
1383
|
-
): { parameters: string[]; returnType: string } | undefined {
|
|
1384
|
-
if (!signature.startsWith('(')) {
|
|
1385
|
-
return undefined;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
const closingParenthesisIndex = findMatchingDelimiter(signature, 0, '(', ')');
|
|
1389
|
-
if (closingParenthesisIndex === -1) {
|
|
1390
|
-
return undefined;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
const parameterListText = signature.slice(1, closingParenthesisIndex);
|
|
1394
|
-
const returnTypePrefix = signature.slice(closingParenthesisIndex + 1).trimStart();
|
|
1395
|
-
if (!returnTypePrefix.startsWith('=>')) {
|
|
1396
|
-
return undefined;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
return {
|
|
1400
|
-
parameters: splitTopLevelCommaSeparated(parameterListText),
|
|
1401
|
-
returnType: returnTypePrefix.slice(2).trim(),
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
/**
|
|
1406
|
-
* Splits a comma-separated type list while respecting nested delimiters.
|
|
1407
|
-
*
|
|
1408
|
-
* @param value - Comma-separated text.
|
|
1409
|
-
* @returns Top-level list entries.
|
|
1410
|
-
*/
|
|
1411
|
-
function splitTopLevelCommaSeparated(value: string): string[] {
|
|
1412
|
-
if (!value.trim()) {
|
|
1413
|
-
return [];
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
const segments: string[] = [];
|
|
1417
|
-
let segmentStartIndex = 0;
|
|
1418
|
-
let parenthesisDepth = 0;
|
|
1419
|
-
let bracketDepth = 0;
|
|
1420
|
-
let braceDepth = 0;
|
|
1421
|
-
let angleDepth = 0;
|
|
1422
|
-
|
|
1423
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1424
|
-
const character = value[index];
|
|
1425
|
-
if (character === '(') {
|
|
1426
|
-
parenthesisDepth += 1;
|
|
1427
|
-
continue;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
if (character === ')') {
|
|
1431
|
-
parenthesisDepth -= 1;
|
|
1432
|
-
continue;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
if (character === '[') {
|
|
1436
|
-
bracketDepth += 1;
|
|
1437
|
-
continue;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (character === ']') {
|
|
1441
|
-
bracketDepth -= 1;
|
|
1442
|
-
continue;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (character === '{') {
|
|
1446
|
-
braceDepth += 1;
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
if (character === '}') {
|
|
1451
|
-
braceDepth -= 1;
|
|
1452
|
-
continue;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
if (character === '<') {
|
|
1456
|
-
angleDepth += 1;
|
|
1457
|
-
continue;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
if (character === '>') {
|
|
1461
|
-
angleDepth = Math.max(0, angleDepth - 1);
|
|
1462
|
-
continue;
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
const isTopLevelComma =
|
|
1466
|
-
character === ',' &&
|
|
1467
|
-
parenthesisDepth === 0 &&
|
|
1468
|
-
bracketDepth === 0 &&
|
|
1469
|
-
braceDepth === 0 &&
|
|
1470
|
-
angleDepth === 0;
|
|
1471
|
-
if (!isTopLevelComma) {
|
|
1472
|
-
continue;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
segments.push(value.slice(segmentStartIndex, index).trim());
|
|
1476
|
-
segmentStartIndex = index + 1;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
segments.push(value.slice(segmentStartIndex).trim());
|
|
1480
|
-
return segments.filter(Boolean);
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
/**
|
|
1484
|
-
* Finds the matching closing delimiter for a starting delimiter.
|
|
1485
|
-
*
|
|
1486
|
-
* @param value - Text to inspect.
|
|
1487
|
-
* @param startIndex - Opening delimiter index.
|
|
1488
|
-
* @param openingDelimiter - Opening delimiter character.
|
|
1489
|
-
* @param closingDelimiter - Closing delimiter character.
|
|
1490
|
-
* @returns Closing delimiter index when found.
|
|
1491
|
-
*/
|
|
1492
|
-
function findMatchingDelimiter(
|
|
1493
|
-
value: string,
|
|
1494
|
-
startIndex: number,
|
|
1495
|
-
openingDelimiter: string,
|
|
1496
|
-
closingDelimiter: string,
|
|
1497
|
-
): number {
|
|
1498
|
-
let delimiterDepth = 0;
|
|
1499
|
-
|
|
1500
|
-
for (let index = startIndex; index < value.length; index += 1) {
|
|
1501
|
-
const character = value[index];
|
|
1502
|
-
if (character === openingDelimiter) {
|
|
1503
|
-
delimiterDepth += 1;
|
|
1504
|
-
continue;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
if (character !== closingDelimiter) {
|
|
1508
|
-
continue;
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
delimiterDepth -= 1;
|
|
1512
|
-
if (delimiterDepth === 0) {
|
|
1513
|
-
return index;
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
return -1;
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
/**
|
|
1521
|
-
* Rewrites absolute import paths from ts-morph type text into repo-relative paths.
|
|
1522
|
-
*
|
|
1523
|
-
* @param typeText - Raw type text returned by ts-morph.
|
|
1524
|
-
* @returns Normalized type text safe for generated docs.
|
|
1525
|
-
*/
|
|
1526
|
-
function normalizeRenderedTypeText(typeText: string): string {
|
|
1527
|
-
const normalizedImportPaths = typeText.replace(
|
|
1528
|
-
/import\((['"])([^'"]+)\1\)/g,
|
|
1529
|
-
(_match, quote: string, importPath: string) => {
|
|
1530
|
-
const normalizedImportPath = path.normalize(importPath);
|
|
1531
|
-
if (!path.isAbsolute(normalizedImportPath)) {
|
|
1532
|
-
return `import(${quote}${importPath}${quote})`;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
const relativeImportPath = path
|
|
1536
|
-
.relative(WORKSPACE_ROOT_DIR, normalizedImportPath)
|
|
1537
|
-
.replace(/\\/g, '/');
|
|
1538
|
-
|
|
1539
|
-
const portableImportPath = relativeImportPath.startsWith('..')
|
|
1540
|
-
? importPath.replace(/\\/g, '/')
|
|
1541
|
-
: relativeImportPath;
|
|
1542
|
-
|
|
1543
|
-
return `import(${quote}${portableImportPath}${quote})`;
|
|
1544
|
-
},
|
|
1545
|
-
);
|
|
1546
|
-
|
|
1547
|
-
return normalizedImportPaths
|
|
1548
|
-
.replace(
|
|
1549
|
-
/\btypeof\s+import\((['"])([^'"]+)\1\)\.([A-Za-z_$][\w$]*)/g,
|
|
1550
|
-
'typeof $3',
|
|
1551
|
-
)
|
|
1552
|
-
.replace(
|
|
1553
|
-
/import\((['"])([^'"]+)\1\)\.([A-Za-z_$][\w$]*)/g,
|
|
1554
|
-
'$3',
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
/**
|
|
1559
|
-
* Extracts rendered parameter docs from JSDoc tags.
|
|
1560
|
-
*
|
|
1561
|
-
* @param tags - JSDoc tags to inspect.
|
|
1562
|
-
* @returns Parameter docs when present.
|
|
1563
|
-
*/
|
|
1564
|
-
function extractParamDocs(
|
|
1565
|
-
tags: readonly JSDocTag[],
|
|
1566
|
-
): RenderedParameter[] | undefined {
|
|
1567
|
-
const parameterDocs = tags
|
|
1568
|
-
.filter((tag) => tag.getTagName() === 'param')
|
|
1569
|
-
.map((tag) => {
|
|
1570
|
-
const match = tag.getText().match(/@param\s+(\w+)/);
|
|
1571
|
-
return {
|
|
1572
|
-
name: match?.[1] || '',
|
|
1573
|
-
doc: getTagCommentText(tag),
|
|
1574
|
-
} satisfies RenderedParameter;
|
|
1575
|
-
})
|
|
1576
|
-
.filter((parameter) => Boolean(parameter.name));
|
|
1577
|
-
|
|
1578
|
-
return parameterDocs.length > 0 ? parameterDocs : undefined;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
/**
|
|
1582
|
-
* Resolves a tag comment into plain text.
|
|
1583
|
-
*
|
|
1584
|
-
* @param tag - JSDoc tag to inspect.
|
|
1585
|
-
* @returns Flattened comment string when present.
|
|
1586
|
-
*/
|
|
1587
|
-
function getTagCommentText(tag: JSDocTag | undefined): string | undefined {
|
|
1588
|
-
const rawComment = tag?.getComment();
|
|
1589
|
-
if (typeof rawComment === 'string') {
|
|
1590
|
-
return sanitizeTagCommentText(rawComment);
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
if (Array.isArray(rawComment)) {
|
|
1594
|
-
return sanitizeTagCommentText(
|
|
1595
|
-
rawComment
|
|
1596
|
-
.map(
|
|
1597
|
-
(commentPart) =>
|
|
1598
|
-
(commentPart as { getText?: () => string }).getText?.() ||
|
|
1599
|
-
String(commentPart),
|
|
1600
|
-
)
|
|
1601
|
-
.join(' ')
|
|
1602
|
-
.trim(),
|
|
1603
|
-
);
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
return undefined;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
/**
|
|
1610
|
-
* Remove ts-morph JSDoc tag artifacts such as standalone trailing asterisks.
|
|
1611
|
-
*
|
|
1612
|
-
* @param commentText - Flattened tag comment text.
|
|
1613
|
-
* @returns Cleaned comment text or undefined when nothing meaningful remains.
|
|
1614
|
-
*/
|
|
1615
|
-
function sanitizeTagCommentText(
|
|
1616
|
-
commentText: string | undefined,
|
|
1617
|
-
): string | undefined {
|
|
1618
|
-
const normalizedComment = commentText
|
|
1619
|
-
?.replace(/\r\n/g, '\n')
|
|
1620
|
-
.replace(/\n\s*\*\s*$/g, '')
|
|
1621
|
-
.replace(/^\s*\*\s*$/g, '')
|
|
1622
|
-
.trim();
|
|
1623
|
-
|
|
1624
|
-
return normalizedComment ? normalizedComment : undefined;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
/**
|
|
1628
|
-
* Normalizes rendered symbol names so generated headings remain stable.
|
|
1629
|
-
*
|
|
1630
|
-
* @param renderedSymbol - Symbol being normalized.
|
|
1631
|
-
* @param filePath - Source file path used as fallback context.
|
|
1632
|
-
* @returns Stable display name.
|
|
1633
|
-
*/
|
|
1634
|
-
function normalizeName(
|
|
1635
|
-
renderedSymbol: RenderedSymbol,
|
|
1636
|
-
filePath: string,
|
|
1637
|
-
): string {
|
|
1638
|
-
let name = String(renderedSymbol.name || '');
|
|
1639
|
-
|
|
1640
|
-
if (!name || name === 'default' || name === FILE_SUMMARY_SYMBOL_NAME) {
|
|
1641
|
-
const fileBaseName = path.basename(filePath || '', '.ts');
|
|
1642
|
-
|
|
1643
|
-
if (renderedSymbol.kind === 'File' || name === FILE_SUMMARY_SYMBOL_NAME) {
|
|
1644
|
-
return fileBaseName;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
if (renderedSymbol.parent) {
|
|
1648
|
-
return `${renderedSymbol.parent}.${fileBaseName}`;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
if (renderedSymbol.signature) {
|
|
1652
|
-
return `${fileBaseName}${renderedSymbol.signature.split(')')[0]})`;
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
return fileBaseName;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
return name.replace(/^function\s+/, '').replace(/\(\)$/, '');
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
/**
|
|
1662
|
-
* Builds the README markdown for one directory.
|
|
1663
|
-
*
|
|
1664
|
-
* @param relativeDirectory - Directory path relative to the target root.
|
|
1665
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
1666
|
-
* @param sourceDir - Absolute source root.
|
|
1667
|
-
* @returns Markdown README content.
|
|
1668
|
-
*/
|
|
1669
|
-
function buildDirectoryReadme(
|
|
1670
|
-
relativeDirectory: string,
|
|
1671
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
1672
|
-
sourceDir: string,
|
|
1673
|
-
): string {
|
|
1674
|
-
const title = (relativeDirectory || path.basename(sourceDir)).replace(
|
|
1675
|
-
/\\/g,
|
|
1676
|
-
'/',
|
|
1677
|
-
);
|
|
1678
|
-
const directoryBaseName = path.basename(relativeDirectory || sourceDir);
|
|
1679
|
-
const directoryPath =
|
|
1680
|
-
relativeDirectory === ''
|
|
1681
|
-
? sourceDir
|
|
1682
|
-
: path.join(sourceDir, relativeDirectory);
|
|
1683
|
-
const lines = [`# ${title}`, ''];
|
|
1684
|
-
|
|
1685
|
-
const sortedFiles = resolveSortedDirectoryFiles(
|
|
1686
|
-
directoryPath,
|
|
1687
|
-
fileSymbolMap,
|
|
1688
|
-
);
|
|
1689
|
-
const hiddenSymbolNames = resolveHiddenDirectorySymbolNames(
|
|
1690
|
-
directoryPath,
|
|
1691
|
-
fileSymbolMap,
|
|
1692
|
-
);
|
|
1693
|
-
const visibleSortedFiles = resolveVisibleDirectoryFiles(
|
|
1694
|
-
directoryPath,
|
|
1695
|
-
sortedFiles,
|
|
1696
|
-
);
|
|
1697
|
-
|
|
1698
|
-
const primaryDirectoryIntro = resolvePrimaryDirectoryIntro(
|
|
1699
|
-
directoryPath,
|
|
1700
|
-
sortedFiles,
|
|
1701
|
-
fileSymbolMap,
|
|
1702
|
-
directoryBaseName,
|
|
1703
|
-
);
|
|
1704
|
-
|
|
1705
|
-
if (primaryDirectoryIntro?.summary.description) {
|
|
1706
|
-
lines.push(primaryDirectoryIntro.summary.description, '');
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
if (primaryDirectoryIntro?.summary.examples?.length) {
|
|
1710
|
-
lines.push(
|
|
1711
|
-
primaryDirectoryIntro.summary.examples.length === 1
|
|
1712
|
-
? 'Example:'
|
|
1713
|
-
: 'Examples:',
|
|
1714
|
-
'',
|
|
1715
|
-
);
|
|
1716
|
-
|
|
1717
|
-
for (const example of primaryDirectoryIntro.summary.examples) {
|
|
1718
|
-
lines.push(example, '');
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
for (const filePath of visibleSortedFiles) {
|
|
1723
|
-
renderFileReadmeSection(
|
|
1724
|
-
lines,
|
|
1725
|
-
filePath,
|
|
1726
|
-
fileSymbolMap,
|
|
1727
|
-
sourceDir,
|
|
1728
|
-
hiddenSymbolNames,
|
|
1729
|
-
primaryDirectoryIntro?.filePath === filePath,
|
|
1730
|
-
);
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
return `${lines.join('\n').trim()}\n`;
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
/**
|
|
1737
|
-
* Renders the README section for a single file.
|
|
1738
|
-
*
|
|
1739
|
-
* @param lines - Output line buffer.
|
|
1740
|
-
* @param filePath - Absolute source file path.
|
|
1741
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
1742
|
-
* @param sourceDir - Absolute source root.
|
|
1743
|
-
* @returns Nothing.
|
|
1744
|
-
*/
|
|
1745
|
-
function renderFileReadmeSection(
|
|
1746
|
-
lines: string[],
|
|
1747
|
-
filePath: string,
|
|
1748
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
1749
|
-
sourceDir: string,
|
|
1750
|
-
hiddenSymbolNames: ReadonlySet<string>,
|
|
1751
|
-
suppressFileSummary: boolean = false,
|
|
1752
|
-
): void {
|
|
1753
|
-
const visibleFileSymbols = (fileSymbolMap.get(filePath) ?? []).filter(
|
|
1754
|
-
(symbol) => !hiddenSymbolNames.has(symbol.name),
|
|
1755
|
-
);
|
|
1756
|
-
const fileSummarySymbol = visibleFileSymbols.find(
|
|
1757
|
-
(symbol) => symbol.kind === 'File',
|
|
1758
|
-
);
|
|
1759
|
-
const nonFileSummarySymbols = visibleFileSymbols.filter(
|
|
1760
|
-
(symbol) => symbol.kind !== 'File',
|
|
1761
|
-
);
|
|
1762
|
-
|
|
1763
|
-
if (
|
|
1764
|
-
!fileSummarySymbol?.jsdoc.description &&
|
|
1765
|
-
!fileSummarySymbol?.jsdoc.examples?.length &&
|
|
1766
|
-
nonFileSummarySymbols.length === 0
|
|
1767
|
-
) {
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
const relativeFilePath = path
|
|
1772
|
-
.relative(sourceDir, filePath)
|
|
1773
|
-
.replace(/\\/g, '/');
|
|
1774
|
-
lines.push(`## ${relativeFilePath}`, '');
|
|
1775
|
-
|
|
1776
|
-
const fileBaseName = path.basename(filePath, '.ts');
|
|
1777
|
-
const sortedSymbols = visibleFileSymbols.toSorted((left, right) =>
|
|
1778
|
-
compareFileSectionSymbols(left, right, fileBaseName),
|
|
1779
|
-
);
|
|
1780
|
-
|
|
1781
|
-
if (!suppressFileSummary && fileSummarySymbol?.jsdoc.description) {
|
|
1782
|
-
lines.push(fileSummarySymbol.jsdoc.description, '');
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
if (!suppressFileSummary && fileSummarySymbol?.jsdoc.examples?.length) {
|
|
1786
|
-
lines.push(
|
|
1787
|
-
fileSummarySymbol.jsdoc.examples.length === 1 ? 'Example:' : 'Examples:',
|
|
1788
|
-
'',
|
|
1789
|
-
);
|
|
1790
|
-
|
|
1791
|
-
for (const example of fileSummarySymbol.jsdoc.examples) {
|
|
1792
|
-
lines.push(example, '');
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
const topLevelSymbols = resolveSortedTopLevelFileSectionSymbols(
|
|
1797
|
-
filePath,
|
|
1798
|
-
nonFileSummarySymbols.filter((symbol) => !symbol.parent),
|
|
1799
|
-
);
|
|
1800
|
-
const symbolsByParent = groupSymbolsByParent(nonFileSummarySymbols);
|
|
1801
|
-
|
|
1802
|
-
for (const topLevelSymbol of topLevelSymbols) {
|
|
1803
|
-
renderSymbolBlock(lines, topLevelSymbol, 3);
|
|
1804
|
-
|
|
1805
|
-
const childSymbols = symbolsByParent.get(topLevelSymbol.name);
|
|
1806
|
-
if (!childSymbols) {
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
childSymbols.sort((left, right) => left.name.localeCompare(right.name));
|
|
1811
|
-
for (const childSymbol of childSymbols) {
|
|
1812
|
-
renderSymbolBlock(lines, childSymbol, 4);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
for (const [parentName, childSymbols] of symbolsByParent) {
|
|
1817
|
-
if (topLevelSymbols.some((symbol) => symbol.name === parentName)) {
|
|
1818
|
-
continue;
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
lines.push(`### ${parentName}`, '');
|
|
1822
|
-
childSymbols.sort((left, right) => left.name.localeCompare(right.name));
|
|
1823
|
-
for (const childSymbol of childSymbols) {
|
|
1824
|
-
renderSymbolBlock(lines, childSymbol, 4);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
/**
|
|
1830
|
-
* Compares file-section symbols using the pre-phase-3 stable fallback order.
|
|
1831
|
-
*
|
|
1832
|
-
* @param left - Left rendered symbol.
|
|
1833
|
-
* @param right - Right rendered symbol.
|
|
1834
|
-
* @param fileBaseName - Base file name without extension.
|
|
1835
|
-
* @returns Negative when left sorts before right.
|
|
1836
|
-
*/
|
|
1837
|
-
function compareFileSectionSymbols(
|
|
1838
|
-
left: RenderedSymbol,
|
|
1839
|
-
right: RenderedSymbol,
|
|
1840
|
-
fileBaseName: string,
|
|
1841
|
-
): number {
|
|
1842
|
-
const leftIsPrimary = !left.parent && left.name === fileBaseName;
|
|
1843
|
-
const rightIsPrimary = !right.parent && right.name === fileBaseName;
|
|
1844
|
-
if (leftIsPrimary !== rightIsPrimary) {
|
|
1845
|
-
return leftIsPrimary ? -1 : 1;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
return (
|
|
1849
|
-
(left.parent || left.name).localeCompare(right.parent || right.name) ||
|
|
1850
|
-
left.name.localeCompare(right.name)
|
|
1851
|
-
);
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
/**
|
|
1855
|
-
* Resolves top-level symbol order for one rendered file section.
|
|
1856
|
-
*
|
|
1857
|
-
* Configured symbol order applies only to top-level entries for the current
|
|
1858
|
-
* file. Unspecified symbols stay on the existing stable fallback path, and
|
|
1859
|
-
* nested member ordering remains conservative elsewhere.
|
|
1860
|
-
*
|
|
1861
|
-
* @param filePath - Absolute source file path.
|
|
1862
|
-
* @param topLevelSymbols - Stable fallback-sorted top-level symbols.
|
|
1863
|
-
* @returns Top-level symbols ordered for rendering.
|
|
1864
|
-
*/
|
|
1865
|
-
function resolveSortedTopLevelFileSectionSymbols(
|
|
1866
|
-
filePath: string,
|
|
1867
|
-
topLevelSymbols: readonly RenderedSymbol[],
|
|
1868
|
-
): RenderedSymbol[] {
|
|
1869
|
-
const directoryPath = path.dirname(filePath);
|
|
1870
|
-
const loadedConfig = getCachedDirectoryDocsOrderConfig(directoryPath);
|
|
1871
|
-
const configuredSymbolOrder = loadedConfig?.config.symbolOrder?.[path.basename(filePath)];
|
|
1872
|
-
|
|
1873
|
-
if (!configuredSymbolOrder?.length) {
|
|
1874
|
-
return [...topLevelSymbols];
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
warnForUnknownConfiguredFileSectionSymbols(
|
|
1878
|
-
configuredSymbolOrder,
|
|
1879
|
-
topLevelSymbols,
|
|
1880
|
-
loadedConfig?.configPath,
|
|
1881
|
-
filePath,
|
|
1882
|
-
);
|
|
1883
|
-
|
|
1884
|
-
const explicitSymbolOrderIndex = new Map(
|
|
1885
|
-
configuredSymbolOrder.map((symbolName, index) => [symbolName, index] as const),
|
|
1886
|
-
);
|
|
1887
|
-
|
|
1888
|
-
return [...topLevelSymbols].toSorted((leftSymbol, rightSymbol) => {
|
|
1889
|
-
const leftExplicitOrder = explicitSymbolOrderIndex.get(leftSymbol.name);
|
|
1890
|
-
const rightExplicitOrder = explicitSymbolOrderIndex.get(rightSymbol.name);
|
|
1891
|
-
|
|
1892
|
-
if (leftExplicitOrder !== undefined || rightExplicitOrder !== undefined) {
|
|
1893
|
-
if (leftExplicitOrder === undefined) {
|
|
1894
|
-
return 1;
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
if (rightExplicitOrder === undefined) {
|
|
1898
|
-
return -1;
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
if (leftExplicitOrder !== rightExplicitOrder) {
|
|
1902
|
-
return leftExplicitOrder - rightExplicitOrder;
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
return 0;
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
/**
|
|
1911
|
-
* Resolves the file-summary block that should be promoted into the directory
|
|
1912
|
-
* README opening.
|
|
1913
|
-
*
|
|
1914
|
-
* @param sortedFiles - Files already ordered for directory README rendering.
|
|
1915
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
1916
|
-
* @returns File path and summary when a promotable directory intro exists.
|
|
1917
|
-
*/
|
|
1918
|
-
function resolvePrimaryDirectoryIntro(
|
|
1919
|
-
directoryPath: string,
|
|
1920
|
-
sortedFiles: readonly string[],
|
|
1921
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
1922
|
-
directoryBaseName: string,
|
|
1923
|
-
): { filePath: string; summary: RenderedFileSummary } | undefined {
|
|
1924
|
-
const loadedConfig = getCachedDirectoryDocsOrderConfig(directoryPath);
|
|
1925
|
-
const configuredIntroFile = loadedConfig?.config.introFile;
|
|
1926
|
-
|
|
1927
|
-
if (configuredIntroFile) {
|
|
1928
|
-
warnForUnknownConfiguredDirectoryIntroFile(
|
|
1929
|
-
configuredIntroFile,
|
|
1930
|
-
sortedFiles,
|
|
1931
|
-
loadedConfig?.configPath,
|
|
1932
|
-
);
|
|
1933
|
-
|
|
1934
|
-
const configuredIntroFilePath = sortedFiles.find(
|
|
1935
|
-
(filePath) => path.basename(filePath) === configuredIntroFile,
|
|
1936
|
-
);
|
|
1937
|
-
const configuredSummary = configuredIntroFilePath
|
|
1938
|
-
? resolveRenderedFileSummary(configuredIntroFilePath, fileSymbolMap)
|
|
1939
|
-
: undefined;
|
|
1940
|
-
|
|
1941
|
-
if (
|
|
1942
|
-
configuredIntroFilePath &&
|
|
1943
|
-
configuredSummary &&
|
|
1944
|
-
(configuredSummary.description || configuredSummary.examples?.length)
|
|
1945
|
-
) {
|
|
1946
|
-
return {
|
|
1947
|
-
filePath: configuredIntroFilePath,
|
|
1948
|
-
summary: configuredSummary,
|
|
1949
|
-
};
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
const prioritizedFiles = sortedFiles.toSorted((leftFile, rightFile) => {
|
|
1954
|
-
const leftBaseName = path.basename(leftFile, '.ts');
|
|
1955
|
-
const rightBaseName = path.basename(rightFile, '.ts');
|
|
1956
|
-
const leftIsDirectoryEntry =
|
|
1957
|
-
leftBaseName === directoryBaseName || leftBaseName === 'index';
|
|
1958
|
-
const rightIsDirectoryEntry =
|
|
1959
|
-
rightBaseName === directoryBaseName || rightBaseName === 'index';
|
|
1960
|
-
|
|
1961
|
-
if (leftIsDirectoryEntry !== rightIsDirectoryEntry) {
|
|
1962
|
-
return leftIsDirectoryEntry ? -1 : 1;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
return sortedFiles.indexOf(leftFile) - sortedFiles.indexOf(rightFile);
|
|
1966
|
-
});
|
|
1967
|
-
|
|
1968
|
-
for (const filePath of prioritizedFiles) {
|
|
1969
|
-
const summary = resolveRenderedFileSummary(filePath, fileSymbolMap);
|
|
1970
|
-
|
|
1971
|
-
if (summary.description || summary.examples?.length) {
|
|
1972
|
-
return { filePath, summary };
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
return undefined;
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
/**
|
|
1980
|
-
* Resolves the rendered file-summary block for one collected source file.
|
|
1981
|
-
*
|
|
1982
|
-
* @param filePath - Absolute source file path.
|
|
1983
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
1984
|
-
* @returns Rendered file summary values when present.
|
|
1985
|
-
*/
|
|
1986
|
-
function resolveRenderedFileSummary(
|
|
1987
|
-
filePath: string,
|
|
1988
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
1989
|
-
): RenderedFileSummary {
|
|
1990
|
-
const fileSummarySymbol = (fileSymbolMap.get(filePath) ?? []).find(
|
|
1991
|
-
(renderedSymbol) => renderedSymbol.kind === 'File',
|
|
1992
|
-
);
|
|
1993
|
-
|
|
1994
|
-
return {
|
|
1995
|
-
description: fileSummarySymbol?.jsdoc.description,
|
|
1996
|
-
examples: fileSummarySymbol?.jsdoc.examples,
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
/**
|
|
2001
|
-
* Groups symbols that have parent names.
|
|
2002
|
-
*
|
|
2003
|
-
* @param renderedSymbols - Symbols for one file.
|
|
2004
|
-
* @returns Parent-keyed symbol map.
|
|
2005
|
-
*/
|
|
2006
|
-
function groupSymbolsByParent(
|
|
2007
|
-
renderedSymbols: readonly RenderedSymbol[],
|
|
2008
|
-
): Map<string, RenderedSymbol[]> {
|
|
2009
|
-
const symbolsByParent = new Map<string, RenderedSymbol[]>();
|
|
2010
|
-
|
|
2011
|
-
for (const renderedSymbol of renderedSymbols.filter(
|
|
2012
|
-
(symbol) => symbol.parent,
|
|
2013
|
-
)) {
|
|
2014
|
-
const childSymbols = symbolsByParent.get(renderedSymbol.parent!) ?? [];
|
|
2015
|
-
childSymbols.push(renderedSymbol);
|
|
2016
|
-
symbolsByParent.set(renderedSymbol.parent!, childSymbols);
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
return symbolsByParent;
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
/**
|
|
2023
|
-
* Renders one symbol block into the README line buffer.
|
|
2024
|
-
*
|
|
2025
|
-
* @param lines - Output line buffer.
|
|
2026
|
-
* @param renderedSymbol - Symbol to render.
|
|
2027
|
-
* @param headingLevel - Markdown heading level.
|
|
2028
|
-
* @returns Nothing.
|
|
2029
|
-
*/
|
|
2030
|
-
function renderSymbolBlock(
|
|
2031
|
-
lines: string[],
|
|
2032
|
-
renderedSymbol: RenderedSymbol,
|
|
2033
|
-
headingLevel: number,
|
|
2034
|
-
): void {
|
|
2035
|
-
const symbolDescription =
|
|
2036
|
-
renderedSymbol.jsdoc.description || renderedSymbol.jsdoc.summary;
|
|
2037
|
-
|
|
2038
|
-
lines.push(`${'#'.repeat(headingLevel)} ${renderedSymbol.name}`);
|
|
2039
|
-
|
|
2040
|
-
if (renderedSymbol.signature) {
|
|
2041
|
-
lines.push('', ...renderSignatureBlock(renderedSymbol.name, renderedSymbol.signature));
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
if (symbolDescription) {
|
|
2045
|
-
lines.push('', symbolDescription);
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
if (renderedSymbol.jsdoc.deprecated) {
|
|
2049
|
-
lines.push('', `**Deprecated:** ${renderedSymbol.jsdoc.deprecated}`);
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
if (renderedSymbol.jsdoc.params?.length) {
|
|
2053
|
-
lines.push('', 'Parameters:');
|
|
2054
|
-
for (const parameter of renderedSymbol.jsdoc.params) {
|
|
2055
|
-
lines.push(
|
|
2056
|
-
`- \`${parameter.name}\`${parameter.doc ? ` - ${parameter.doc}` : ''}`,
|
|
2057
|
-
);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
if (renderedSymbol.jsdoc.returns) {
|
|
2062
|
-
lines.push('', `Returns: ${renderedSymbol.jsdoc.returns}`);
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
if (renderedSymbol.jsdoc.examples?.length) {
|
|
2066
|
-
lines.push('', renderedSymbol.jsdoc.examples.length === 1 ? 'Example:' : 'Examples:');
|
|
2067
|
-
|
|
2068
|
-
for (const example of renderedSymbol.jsdoc.examples) {
|
|
2069
|
-
lines.push('', example);
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
lines.push('');
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
/**
|
|
2077
|
-
* Extracts rendered example blocks from JSDoc tags.
|
|
2078
|
-
*
|
|
2079
|
-
* @param tags - JSDoc tags to inspect.
|
|
2080
|
-
* @returns Example blocks when present.
|
|
2081
|
-
*/
|
|
2082
|
-
function extractExampleDocs(tags: readonly JSDocTag[]): string[] | undefined {
|
|
2083
|
-
const examples = tags
|
|
2084
|
-
.filter((tag) => tag.getTagName() === 'example')
|
|
2085
|
-
.map((tag) => getExampleTagText(tag))
|
|
2086
|
-
.filter((example): example is string => Boolean(example));
|
|
2087
|
-
|
|
2088
|
-
return examples.length > 0 ? examples : undefined;
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
/**
|
|
2092
|
-
* Resolves an example tag into markdown-friendly multiline text.
|
|
2093
|
-
*
|
|
2094
|
-
* @param tag - JSDoc example tag.
|
|
2095
|
-
* @returns Example block content when present.
|
|
2096
|
-
*/
|
|
2097
|
-
function getExampleTagText(tag: JSDocTag | undefined): string | undefined {
|
|
2098
|
-
const rawTagText = tag?.getText()?.replace(/\r\n/g, '\n');
|
|
2099
|
-
if (!rawTagText) {
|
|
2100
|
-
return undefined;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
const exampleBody = rawTagText.replace(/^@example\s*/, '');
|
|
2104
|
-
return sanitizeTagBlockText(exampleBody);
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
/**
|
|
2108
|
-
* Renders one JSDoc block into file-summary description/examples.
|
|
2109
|
-
*
|
|
2110
|
-
* @param jsDoc - JSDoc block to render.
|
|
2111
|
-
* @returns Parsed description/examples.
|
|
2112
|
-
*/
|
|
2113
|
-
function renderJsDocSummaryBlock(jsDoc: any): RenderedFileSummary {
|
|
2114
|
-
const description = sanitizeTagBlockText(jsDoc?.getDescription?.()?.trim());
|
|
2115
|
-
const examples = extractExampleDocs(jsDoc?.getTags?.() || []);
|
|
2116
|
-
|
|
2117
|
-
return {
|
|
2118
|
-
description,
|
|
2119
|
-
examples,
|
|
2120
|
-
};
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
/**
|
|
2124
|
-
* Merges multiple parsed file-summary blocks.
|
|
2125
|
-
*
|
|
2126
|
-
* @param summaries - Parsed summary blocks.
|
|
2127
|
-
* @returns One merged file summary.
|
|
2128
|
-
*/
|
|
2129
|
-
function mergeRenderedFileSummaries(
|
|
2130
|
-
summaries: readonly RenderedFileSummary[],
|
|
2131
|
-
): RenderedFileSummary {
|
|
2132
|
-
const descriptions = summaries
|
|
2133
|
-
.map((summary) => summary.description)
|
|
2134
|
-
.filter((description): description is string => Boolean(description));
|
|
2135
|
-
const examples = summaries
|
|
2136
|
-
.flatMap((summary) => summary.examples || [])
|
|
2137
|
-
.filter(
|
|
2138
|
-
(example, index, allExamples) => allExamples.indexOf(example) === index,
|
|
2139
|
-
);
|
|
2140
|
-
|
|
2141
|
-
return {
|
|
2142
|
-
description: descriptions.length > 0 ? descriptions.join('\n\n') : undefined,
|
|
2143
|
-
examples: examples.length > 0 ? examples : undefined,
|
|
2144
|
-
};
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
/**
|
|
2148
|
-
* Ranks files within a directory README so entrypoints appear before helpers.
|
|
2149
|
-
*
|
|
2150
|
-
* @param filePath - Absolute source file path.
|
|
2151
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
2152
|
-
* @returns Sort rank tuple.
|
|
2153
|
-
*/
|
|
2154
|
-
function rankFileForDirectoryReadme(
|
|
2155
|
-
filePath: string,
|
|
2156
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
2157
|
-
): number[] {
|
|
2158
|
-
const fileName = path.basename(filePath).toLowerCase();
|
|
2159
|
-
const renderedSymbols = fileSymbolMap.get(filePath) ?? [];
|
|
2160
|
-
|
|
2161
|
-
const hasFileSummary = renderedSymbols.some((renderedSymbol) => {
|
|
2162
|
-
if (
|
|
2163
|
-
renderedSymbol.kind !== 'File' ||
|
|
2164
|
-
renderedSymbol.name !== FILE_SUMMARY_SYMBOL_NAME
|
|
2165
|
-
) {
|
|
2166
|
-
return false;
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
return Boolean(renderedSymbol.jsdoc.description?.trim());
|
|
2170
|
-
});
|
|
2171
|
-
|
|
2172
|
-
const isIndexFile = fileName === 'index.ts';
|
|
2173
|
-
const isTypesFile =
|
|
2174
|
-
fileName.includes('.types.') || fileName.endsWith('.types.ts');
|
|
2175
|
-
const isUtilityLike =
|
|
2176
|
-
/(\.utils\.|\.export-|\.import-|\.internal\.|\.private\.)/i.test(fileName);
|
|
2177
|
-
const isEntrypointLike = !isUtilityLike;
|
|
2178
|
-
|
|
2179
|
-
return [
|
|
2180
|
-
isIndexFile ? 0 : 1,
|
|
2181
|
-
hasFileSummary ? 0 : 1,
|
|
2182
|
-
isEntrypointLike ? 0 : 1,
|
|
2183
|
-
isTypesFile ? 0 : 1,
|
|
2184
|
-
fileName.length,
|
|
2185
|
-
];
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
/**
|
|
2189
|
-
* Normalizes multiline tag blocks while preserving markdown structure.
|
|
2190
|
-
*
|
|
2191
|
-
* @param blockText - Multiline tag block text.
|
|
2192
|
-
* @returns Cleaned block text or undefined when empty.
|
|
2193
|
-
*/
|
|
2194
|
-
function sanitizeTagBlockText(
|
|
2195
|
-
blockText: string | undefined,
|
|
2196
|
-
): string | undefined {
|
|
2197
|
-
const normalizedBlock = blockText
|
|
2198
|
-
?.replace(/\r\n/g, '\n')
|
|
2199
|
-
.replace(/^\s*\* ?/gm, '')
|
|
2200
|
-
.replace(/^\n+/, '')
|
|
2201
|
-
.replace(/\n+$/, '')
|
|
2202
|
-
.trim();
|
|
2203
|
-
|
|
2204
|
-
return normalizedBlock ? normalizedBlock : undefined;
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
/**
|
|
2208
|
-
* Loads and caches the optional per-folder docs ordering config.
|
|
2209
|
-
*
|
|
2210
|
-
* Phase 1 only validates and stores config so later phases can consume it at
|
|
2211
|
-
* the existing generator seams without changing default behavior today.
|
|
2212
|
-
*
|
|
2213
|
-
* @param directoryPath - Absolute directory path that may contain docs config.
|
|
2214
|
-
* @returns Parsed and validated config when present.
|
|
2215
|
-
*/
|
|
2216
|
-
async function loadDirectoryDocsOrderConfig(
|
|
2217
|
-
directoryPath: string,
|
|
2218
|
-
): Promise<LoadedDirectoryDocsOrderConfig | undefined> {
|
|
2219
|
-
const normalizedDirectoryPath = path.resolve(directoryPath);
|
|
2220
|
-
if (directoryDocsOrderConfigCache.has(normalizedDirectoryPath)) {
|
|
2221
|
-
return directoryDocsOrderConfigCache.get(normalizedDirectoryPath)!;
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
const pendingConfig = readDirectoryDocsOrderConfig(normalizedDirectoryPath).then(
|
|
2225
|
-
(loadedConfig) => {
|
|
2226
|
-
resolvedDirectoryDocsOrderConfigCache.set(
|
|
2227
|
-
normalizedDirectoryPath,
|
|
2228
|
-
loadedConfig,
|
|
2229
|
-
);
|
|
2230
|
-
return loadedConfig;
|
|
2231
|
-
},
|
|
2232
|
-
);
|
|
2233
|
-
directoryDocsOrderConfigCache.set(normalizedDirectoryPath, pendingConfig);
|
|
2234
|
-
return pendingConfig;
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
/**
|
|
2238
|
-
* Reads and validates one directory's `docs.order.json` file when present.
|
|
2239
|
-
*
|
|
2240
|
-
* @param directoryPath - Absolute directory path.
|
|
2241
|
-
* @returns Parsed config or undefined when absent or invalid.
|
|
2242
|
-
*/
|
|
2243
|
-
async function readDirectoryDocsOrderConfig(
|
|
2244
|
-
directoryPath: string,
|
|
2245
|
-
): Promise<LoadedDirectoryDocsOrderConfig | undefined> {
|
|
2246
|
-
const configPath = path.join(directoryPath, DOCS_ORDER_CONFIG_FILE_NAME);
|
|
2247
|
-
if (!(await fs.pathExists(configPath))) {
|
|
2248
|
-
return undefined;
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
try {
|
|
2252
|
-
const rawConfigText = await fs.readFile(configPath, 'utf8');
|
|
2253
|
-
const parsedConfig: unknown = JSON.parse(rawConfigText);
|
|
2254
|
-
const validatedConfig = validateDirectoryDocsOrderConfig(
|
|
2255
|
-
parsedConfig,
|
|
2256
|
-
configPath,
|
|
2257
|
-
);
|
|
2258
|
-
return validatedConfig ? { configPath, config: validatedConfig } : undefined;
|
|
2259
|
-
} catch (error) {
|
|
2260
|
-
warnDirectoryDocsOrderConfig(
|
|
2261
|
-
configPath,
|
|
2262
|
-
`Failed to read ${DOCS_ORDER_CONFIG_FILE_NAME}: ${getErrorMessage(error)}`,
|
|
2263
|
-
);
|
|
2264
|
-
return undefined;
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
/**
|
|
2269
|
-
* Validates the supported phase-1 docs ordering config keys.
|
|
2270
|
-
*
|
|
2271
|
-
* @param parsedConfig - Raw parsed JSON value.
|
|
2272
|
-
* @param configPath - Absolute config path used in warnings.
|
|
2273
|
-
* @returns Sanitized config when at least one supported value is valid.
|
|
2274
|
-
*/
|
|
2275
|
-
function validateDirectoryDocsOrderConfig(
|
|
2276
|
-
parsedConfig: unknown,
|
|
2277
|
-
configPath: string,
|
|
2278
|
-
): DirectoryDocsOrderConfig | undefined {
|
|
2279
|
-
if (!isRecord(parsedConfig)) {
|
|
2280
|
-
warnDirectoryDocsOrderConfig(
|
|
2281
|
-
configPath,
|
|
2282
|
-
'Config must be a JSON object. Ignoring file.',
|
|
2283
|
-
);
|
|
2284
|
-
return undefined;
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
const supportedKeys = new Set([
|
|
2288
|
-
'introFile',
|
|
2289
|
-
'fileOrder',
|
|
2290
|
-
'symbolOrder',
|
|
2291
|
-
'folderOrder',
|
|
2292
|
-
'hiddenFiles',
|
|
2293
|
-
'hiddenSymbols',
|
|
2294
|
-
]);
|
|
2295
|
-
for (const configKey of Object.keys(parsedConfig)) {
|
|
2296
|
-
if (!supportedKeys.has(configKey)) {
|
|
2297
|
-
warnDirectoryDocsOrderConfig(
|
|
2298
|
-
configPath,
|
|
2299
|
-
`Unknown key "${configKey}". Supported keys: ${[...supportedKeys].join(', ')}`,
|
|
2300
|
-
);
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
const validatedConfig: DirectoryDocsOrderConfig = {};
|
|
2305
|
-
const introFile = normalizeOptionalStringConfigValue(
|
|
2306
|
-
parsedConfig.introFile,
|
|
2307
|
-
'introFile',
|
|
2308
|
-
configPath,
|
|
2309
|
-
);
|
|
2310
|
-
if (introFile) {
|
|
2311
|
-
validatedConfig.introFile = introFile;
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
const fileOrder = normalizeStringArrayConfigValue(
|
|
2315
|
-
parsedConfig.fileOrder,
|
|
2316
|
-
'fileOrder',
|
|
2317
|
-
configPath,
|
|
2318
|
-
);
|
|
2319
|
-
if (fileOrder) {
|
|
2320
|
-
validatedConfig.fileOrder = fileOrder;
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
const symbolOrder = normalizeSymbolOrderConfigValue(
|
|
2324
|
-
parsedConfig.symbolOrder,
|
|
2325
|
-
configPath,
|
|
2326
|
-
);
|
|
2327
|
-
if (symbolOrder) {
|
|
2328
|
-
validatedConfig.symbolOrder = symbolOrder;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
const folderOrder = normalizeStringArrayConfigValue(
|
|
2332
|
-
parsedConfig.folderOrder,
|
|
2333
|
-
'folderOrder',
|
|
2334
|
-
configPath,
|
|
2335
|
-
);
|
|
2336
|
-
if (folderOrder) {
|
|
2337
|
-
validatedConfig.folderOrder = folderOrder;
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
const hiddenFiles = normalizeStringArrayConfigValue(
|
|
2341
|
-
parsedConfig.hiddenFiles,
|
|
2342
|
-
'hiddenFiles',
|
|
2343
|
-
configPath,
|
|
2344
|
-
);
|
|
2345
|
-
if (hiddenFiles) {
|
|
2346
|
-
validatedConfig.hiddenFiles = hiddenFiles;
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
const hiddenSymbols = normalizeStringArrayConfigValue(
|
|
2350
|
-
parsedConfig.hiddenSymbols,
|
|
2351
|
-
'hiddenSymbols',
|
|
2352
|
-
configPath,
|
|
2353
|
-
);
|
|
2354
|
-
if (hiddenSymbols) {
|
|
2355
|
-
validatedConfig.hiddenSymbols = hiddenSymbols;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
return Object.keys(validatedConfig).length > 0 ? validatedConfig : {};
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
/**
|
|
2362
|
-
* Returns the cached docs ordering config for a directory.
|
|
2363
|
-
*
|
|
2364
|
-
* The loader is called before directory emission, so this helper stays
|
|
2365
|
-
* synchronous for render-time consumers in later phases.
|
|
2366
|
-
*
|
|
2367
|
-
* @param directoryPath - Absolute directory path.
|
|
2368
|
-
* @returns Cached config when already loaded.
|
|
2369
|
-
*/
|
|
2370
|
-
function getCachedDirectoryDocsOrderConfig(
|
|
2371
|
-
directoryPath: string,
|
|
2372
|
-
): LoadedDirectoryDocsOrderConfig | undefined {
|
|
2373
|
-
return resolvedDirectoryDocsOrderConfigCache.get(path.resolve(directoryPath));
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
/**
|
|
2377
|
-
* Resolves directory file order using config-first ordering with stable
|
|
2378
|
-
* fallback to the existing heuristic rank.
|
|
2379
|
-
*
|
|
2380
|
-
* @param directoryPath - Absolute directory path.
|
|
2381
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
2382
|
-
* @returns Files sorted for one directory README.
|
|
2383
|
-
*/
|
|
2384
|
-
function resolveSortedDirectoryFiles(
|
|
2385
|
-
directoryPath: string,
|
|
2386
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
2387
|
-
): string[] {
|
|
2388
|
-
const loadedConfig = getCachedDirectoryDocsOrderConfig(directoryPath);
|
|
2389
|
-
const configuredFileOrder = loadedConfig?.config.fileOrder;
|
|
2390
|
-
|
|
2391
|
-
if (configuredFileOrder?.length) {
|
|
2392
|
-
warnForUnknownConfiguredDirectoryFiles(
|
|
2393
|
-
configuredFileOrder,
|
|
2394
|
-
[...fileSymbolMap.keys()],
|
|
2395
|
-
loadedConfig?.configPath,
|
|
2396
|
-
);
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
const explicitFileOrderIndex = new Map(
|
|
2400
|
-
(configuredFileOrder ?? []).map((fileName, index) => [fileName, index] as const),
|
|
2401
|
-
);
|
|
2402
|
-
|
|
2403
|
-
return [...fileSymbolMap.keys()].toSorted((leftFile, rightFile) => {
|
|
2404
|
-
const leftExplicitOrder = explicitFileOrderIndex.get(path.basename(leftFile));
|
|
2405
|
-
const rightExplicitOrder = explicitFileOrderIndex.get(path.basename(rightFile));
|
|
2406
|
-
|
|
2407
|
-
if (leftExplicitOrder !== undefined || rightExplicitOrder !== undefined) {
|
|
2408
|
-
if (leftExplicitOrder === undefined) {
|
|
2409
|
-
return 1;
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
if (rightExplicitOrder === undefined) {
|
|
2413
|
-
return -1;
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
if (leftExplicitOrder !== rightExplicitOrder) {
|
|
2417
|
-
return leftExplicitOrder - rightExplicitOrder;
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
return compareDirectoryReadmeFiles(leftFile, rightFile, fileSymbolMap);
|
|
2422
|
-
});
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
/**
|
|
2426
|
-
* Resolves visible files for one directory README after applying optional
|
|
2427
|
-
* `hiddenFiles` filtering.
|
|
2428
|
-
*
|
|
2429
|
-
* @param directoryPath - Absolute directory path.
|
|
2430
|
-
* @param sortedFiles - Already sorted directory files.
|
|
2431
|
-
* @returns Visible files that should remain in the generated output.
|
|
2432
|
-
*/
|
|
2433
|
-
function resolveVisibleDirectoryFiles(
|
|
2434
|
-
directoryPath: string,
|
|
2435
|
-
sortedFiles: readonly string[],
|
|
2436
|
-
): string[] {
|
|
2437
|
-
const loadedConfig = getCachedDirectoryDocsOrderConfig(directoryPath);
|
|
2438
|
-
const configuredHiddenFiles = loadedConfig?.config.hiddenFiles;
|
|
2439
|
-
|
|
2440
|
-
if (configuredHiddenFiles?.length) {
|
|
2441
|
-
warnForUnknownConfiguredHiddenFiles(
|
|
2442
|
-
configuredHiddenFiles,
|
|
2443
|
-
sortedFiles,
|
|
2444
|
-
loadedConfig?.configPath,
|
|
2445
|
-
);
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
const hiddenFileNames = new Set(configuredHiddenFiles ?? []);
|
|
2449
|
-
return sortedFiles.filter(
|
|
2450
|
-
(filePath) => !hiddenFileNames.has(path.basename(filePath)),
|
|
2451
|
-
);
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
/**
|
|
2455
|
-
* Resolves hidden symbol names for one directory README.
|
|
2456
|
-
*
|
|
2457
|
-
* @param directoryPath - Absolute directory path.
|
|
2458
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
2459
|
-
* @returns Hidden symbol-name set for render-time filtering.
|
|
2460
|
-
*/
|
|
2461
|
-
function resolveHiddenDirectorySymbolNames(
|
|
2462
|
-
directoryPath: string,
|
|
2463
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
2464
|
-
): ReadonlySet<string> {
|
|
2465
|
-
const loadedConfig = getCachedDirectoryDocsOrderConfig(directoryPath);
|
|
2466
|
-
const configuredHiddenSymbols = loadedConfig?.config.hiddenSymbols;
|
|
2467
|
-
|
|
2468
|
-
if (configuredHiddenSymbols?.length) {
|
|
2469
|
-
warnForUnknownConfiguredHiddenSymbols(
|
|
2470
|
-
configuredHiddenSymbols,
|
|
2471
|
-
fileSymbolMap,
|
|
2472
|
-
loadedConfig?.configPath,
|
|
2473
|
-
);
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
return new Set(configuredHiddenSymbols ?? []);
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
/**
|
|
2480
|
-
* Warns when `fileOrder` references files missing from the current folder.
|
|
2481
|
-
*
|
|
2482
|
-
* @param configuredFileOrder - Configured file order entries.
|
|
2483
|
-
* @param filePaths - Actual files in the current directory README.
|
|
2484
|
-
* @param configPath - Absolute config path.
|
|
2485
|
-
* @returns Nothing.
|
|
2486
|
-
*/
|
|
2487
|
-
function warnForUnknownConfiguredDirectoryFiles(
|
|
2488
|
-
configuredFileOrder: readonly string[],
|
|
2489
|
-
filePaths: readonly string[],
|
|
2490
|
-
configPath: string | undefined,
|
|
2491
|
-
): void {
|
|
2492
|
-
if (!configPath) {
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
const knownFileNames = new Set(filePaths.map((filePath) => path.basename(filePath)));
|
|
2497
|
-
const unknownFileNames = configuredFileOrder.filter(
|
|
2498
|
-
(fileName) => !knownFileNames.has(fileName),
|
|
2499
|
-
);
|
|
2500
|
-
|
|
2501
|
-
if (unknownFileNames.length === 0) {
|
|
2502
|
-
return;
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
warnDirectoryDocsOrderConfig(
|
|
2506
|
-
configPath,
|
|
2507
|
-
`Field "fileOrder" references unknown files for this folder: ${unknownFileNames.join(', ')}`,
|
|
2508
|
-
);
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
/**
|
|
2512
|
-
* Warns when `hiddenFiles` references files missing from the current folder.
|
|
2513
|
-
*
|
|
2514
|
-
* @param configuredHiddenFiles - Configured hidden file entries.
|
|
2515
|
-
* @param filePaths - Actual files in the current directory README.
|
|
2516
|
-
* @param configPath - Absolute config path.
|
|
2517
|
-
* @returns Nothing.
|
|
2518
|
-
*/
|
|
2519
|
-
function warnForUnknownConfiguredHiddenFiles(
|
|
2520
|
-
configuredHiddenFiles: readonly string[],
|
|
2521
|
-
filePaths: readonly string[],
|
|
2522
|
-
configPath: string | undefined,
|
|
2523
|
-
): void {
|
|
2524
|
-
if (!configPath) {
|
|
2525
|
-
return;
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
const knownFileNames = new Set(filePaths.map((filePath) => path.basename(filePath)));
|
|
2529
|
-
const unknownFileNames = configuredHiddenFiles.filter(
|
|
2530
|
-
(fileName) => !knownFileNames.has(fileName),
|
|
2531
|
-
);
|
|
2532
|
-
|
|
2533
|
-
if (unknownFileNames.length === 0) {
|
|
2534
|
-
return;
|
|
2535
|
-
}
|
|
2536
|
-
|
|
2537
|
-
warnDirectoryDocsOrderConfig(
|
|
2538
|
-
configPath,
|
|
2539
|
-
`Field "hiddenFiles" references unknown files for this folder: ${unknownFileNames.join(', ')}`,
|
|
2540
|
-
);
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
/**
|
|
2544
|
-
* Warns when `introFile` references a file missing from the current folder.
|
|
2545
|
-
*
|
|
2546
|
-
* @param configuredIntroFile - Configured intro file name.
|
|
2547
|
-
* @param filePaths - Actual files in the current directory README.
|
|
2548
|
-
* @param configPath - Absolute config path.
|
|
2549
|
-
* @returns Nothing.
|
|
2550
|
-
*/
|
|
2551
|
-
function warnForUnknownConfiguredDirectoryIntroFile(
|
|
2552
|
-
configuredIntroFile: string,
|
|
2553
|
-
filePaths: readonly string[],
|
|
2554
|
-
configPath: string | undefined,
|
|
2555
|
-
): void {
|
|
2556
|
-
if (!configPath) {
|
|
2557
|
-
return;
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
const knownFileNames = new Set(filePaths.map((filePath) => path.basename(filePath)));
|
|
2561
|
-
if (knownFileNames.has(configuredIntroFile)) {
|
|
2562
|
-
return;
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
|
-
warnDirectoryDocsOrderConfig(
|
|
2566
|
-
configPath,
|
|
2567
|
-
`Field "introFile" references an unknown file for this folder: ${configuredIntroFile}`,
|
|
2568
|
-
);
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
/**
|
|
2572
|
-
* Warns when `symbolOrder` references top-level symbols missing from one file
|
|
2573
|
-
* section.
|
|
2574
|
-
*
|
|
2575
|
-
* @param configuredSymbolOrder - Configured symbol order entries.
|
|
2576
|
-
* @param topLevelSymbols - Top-level symbols rendered for the current file.
|
|
2577
|
-
* @param configPath - Absolute config path.
|
|
2578
|
-
* @param filePath - Absolute source file path for warning context.
|
|
2579
|
-
* @returns Nothing.
|
|
2580
|
-
*/
|
|
2581
|
-
function warnForUnknownConfiguredFileSectionSymbols(
|
|
2582
|
-
configuredSymbolOrder: readonly string[],
|
|
2583
|
-
topLevelSymbols: readonly RenderedSymbol[],
|
|
2584
|
-
configPath: string | undefined,
|
|
2585
|
-
filePath: string,
|
|
2586
|
-
): void {
|
|
2587
|
-
if (!configPath) {
|
|
2588
|
-
return;
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
const knownSymbolNames = new Set(topLevelSymbols.map((symbol) => symbol.name));
|
|
2592
|
-
const unknownSymbolNames = configuredSymbolOrder.filter(
|
|
2593
|
-
(symbolName) => !knownSymbolNames.has(symbolName),
|
|
2594
|
-
);
|
|
2595
|
-
|
|
2596
|
-
if (unknownSymbolNames.length === 0) {
|
|
2597
|
-
return;
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
warnDirectoryDocsOrderConfig(
|
|
2601
|
-
configPath,
|
|
2602
|
-
`Field "symbolOrder.${path.basename(filePath)}" references unknown top-level symbols for this file section: ${unknownSymbolNames.join(', ')}`,
|
|
2603
|
-
);
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
/**
|
|
2607
|
-
* Warns when `hiddenSymbols` references symbols missing from the current
|
|
2608
|
-
* directory README.
|
|
2609
|
-
*
|
|
2610
|
-
* @param configuredHiddenSymbols - Configured hidden symbol entries.
|
|
2611
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
2612
|
-
* @param configPath - Absolute config path.
|
|
2613
|
-
* @returns Nothing.
|
|
2614
|
-
*/
|
|
2615
|
-
function warnForUnknownConfiguredHiddenSymbols(
|
|
2616
|
-
configuredHiddenSymbols: readonly string[],
|
|
2617
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
2618
|
-
configPath: string | undefined,
|
|
2619
|
-
): void {
|
|
2620
|
-
if (!configPath) {
|
|
2621
|
-
return;
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
|
-
const knownSymbolNames = new Set(
|
|
2625
|
-
[...fileSymbolMap.values()].flatMap((renderedSymbols) =>
|
|
2626
|
-
renderedSymbols.map((symbol) => symbol.name),
|
|
2627
|
-
),
|
|
2628
|
-
);
|
|
2629
|
-
const unknownSymbolNames = configuredHiddenSymbols.filter(
|
|
2630
|
-
(symbolName) => !knownSymbolNames.has(symbolName),
|
|
2631
|
-
);
|
|
2632
|
-
|
|
2633
|
-
if (unknownSymbolNames.length === 0) {
|
|
2634
|
-
return;
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
warnDirectoryDocsOrderConfig(
|
|
2638
|
-
configPath,
|
|
2639
|
-
`Field "hiddenSymbols" references unknown symbols for this folder: ${unknownSymbolNames.join(', ')}`,
|
|
2640
|
-
);
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
/**
|
|
2644
|
-
* Warns when `folderOrder` references child folders missing from one folder
|
|
2645
|
-
* index node.
|
|
2646
|
-
*
|
|
2647
|
-
* @param configuredFolderOrder - Configured folder order entries.
|
|
2648
|
-
* @param childFolderNames - Actual child folder names under the current node.
|
|
2649
|
-
* @param configPath - Absolute config path.
|
|
2650
|
-
* @param nodePath - Relative folder-index node path for warning context.
|
|
2651
|
-
* @returns Nothing.
|
|
2652
|
-
*/
|
|
2653
|
-
function warnForUnknownConfiguredFolderIndexChildren(
|
|
2654
|
-
configuredFolderOrder: readonly string[],
|
|
2655
|
-
childFolderNames: readonly string[],
|
|
2656
|
-
configPath: string | undefined,
|
|
2657
|
-
nodePath: string,
|
|
2658
|
-
): void {
|
|
2659
|
-
if (!configPath) {
|
|
2660
|
-
return;
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
const knownFolderNames = new Set(childFolderNames);
|
|
2664
|
-
const unknownFolderNames = configuredFolderOrder.filter(
|
|
2665
|
-
(folderName) => !knownFolderNames.has(folderName),
|
|
2666
|
-
);
|
|
2667
|
-
|
|
2668
|
-
if (unknownFolderNames.length === 0) {
|
|
2669
|
-
return;
|
|
2670
|
-
}
|
|
2671
|
-
|
|
2672
|
-
warnDirectoryDocsOrderConfig(
|
|
2673
|
-
configPath,
|
|
2674
|
-
`Field "folderOrder" references unknown child folders for ${nodePath}: ${unknownFolderNames.join(', ')}`,
|
|
2675
|
-
);
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
/**
|
|
2679
|
-
* Compares two files using the pre-phase-2 directory README heuristic.
|
|
2680
|
-
*
|
|
2681
|
-
* @param leftFile - Left file path.
|
|
2682
|
-
* @param rightFile - Right file path.
|
|
2683
|
-
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
2684
|
-
* @returns Negative when left sorts before right.
|
|
2685
|
-
*/
|
|
2686
|
-
function compareDirectoryReadmeFiles(
|
|
2687
|
-
leftFile: string,
|
|
2688
|
-
rightFile: string,
|
|
2689
|
-
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
2690
|
-
): number {
|
|
2691
|
-
const leftRank = rankFileForDirectoryReadme(leftFile, fileSymbolMap);
|
|
2692
|
-
const rightRank = rankFileForDirectoryReadme(rightFile, fileSymbolMap);
|
|
2693
|
-
|
|
2694
|
-
for (
|
|
2695
|
-
let index = 0;
|
|
2696
|
-
index < Math.max(leftRank.length, rightRank.length);
|
|
2697
|
-
index += 1
|
|
2698
|
-
) {
|
|
2699
|
-
const leftValue = leftRank[index] ?? 0;
|
|
2700
|
-
const rightValue = rightRank[index] ?? 0;
|
|
2701
|
-
if (leftValue !== rightValue) {
|
|
2702
|
-
return leftValue - rightValue;
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
return leftFile.localeCompare(rightFile);
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
/**
|
|
2710
|
-
* Normalizes a single optional string config field.
|
|
2711
|
-
*
|
|
2712
|
-
* @param value - Raw config value.
|
|
2713
|
-
* @param fieldName - Config field name.
|
|
2714
|
-
* @param configPath - Absolute config path used in warnings.
|
|
2715
|
-
* @returns Trimmed string when valid.
|
|
2716
|
-
*/
|
|
2717
|
-
function normalizeOptionalStringConfigValue(
|
|
2718
|
-
value: unknown,
|
|
2719
|
-
fieldName: string,
|
|
2720
|
-
configPath: string,
|
|
2721
|
-
): string | undefined {
|
|
2722
|
-
if (value === undefined) {
|
|
2723
|
-
return undefined;
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
2727
|
-
warnDirectoryDocsOrderConfig(
|
|
2728
|
-
configPath,
|
|
2729
|
-
`Field "${fieldName}" must be a non-empty string. Ignoring value.`,
|
|
2730
|
-
);
|
|
2731
|
-
return undefined;
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
return value.trim();
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
/**
|
|
2738
|
-
* Normalizes one string-array config field.
|
|
2739
|
-
*
|
|
2740
|
-
* @param value - Raw config value.
|
|
2741
|
-
* @param fieldName - Config field name.
|
|
2742
|
-
* @param configPath - Absolute config path used in warnings.
|
|
2743
|
-
* @returns Unique non-empty strings when valid.
|
|
2744
|
-
*/
|
|
2745
|
-
function normalizeStringArrayConfigValue(
|
|
2746
|
-
value: unknown,
|
|
2747
|
-
fieldName: string,
|
|
2748
|
-
configPath: string,
|
|
2749
|
-
): string[] | undefined {
|
|
2750
|
-
if (value === undefined) {
|
|
2751
|
-
return undefined;
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
if (!Array.isArray(value)) {
|
|
2755
|
-
warnDirectoryDocsOrderConfig(
|
|
2756
|
-
configPath,
|
|
2757
|
-
`Field "${fieldName}" must be an array of non-empty strings. Ignoring value.`,
|
|
2758
|
-
);
|
|
2759
|
-
return undefined;
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
|
-
const normalizedEntries = value
|
|
2763
|
-
.filter((entry): entry is string => typeof entry === 'string')
|
|
2764
|
-
.map((entry) => entry.trim())
|
|
2765
|
-
.filter((entry) => entry.length > 0);
|
|
2766
|
-
|
|
2767
|
-
if (normalizedEntries.length !== value.length) {
|
|
2768
|
-
warnDirectoryDocsOrderConfig(
|
|
2769
|
-
configPath,
|
|
2770
|
-
`Field "${fieldName}" must contain only non-empty strings. Dropping invalid entries.`,
|
|
2771
|
-
);
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
return normalizedEntries.length > 0
|
|
2775
|
-
? [...new Set(normalizedEntries)]
|
|
2776
|
-
: undefined;
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
/**
|
|
2780
|
-
* Normalizes the per-file symbol ordering map.
|
|
2781
|
-
*
|
|
2782
|
-
* @param value - Raw config value.
|
|
2783
|
-
* @param configPath - Absolute config path used in warnings.
|
|
2784
|
-
* @returns Sanitized symbol-order map when valid.
|
|
2785
|
-
*/
|
|
2786
|
-
function normalizeSymbolOrderConfigValue(
|
|
2787
|
-
value: unknown,
|
|
2788
|
-
configPath: string,
|
|
2789
|
-
): Record<string, string[]> | undefined {
|
|
2790
|
-
if (value === undefined) {
|
|
2791
|
-
return undefined;
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
if (!isRecord(value)) {
|
|
2795
|
-
warnDirectoryDocsOrderConfig(
|
|
2796
|
-
configPath,
|
|
2797
|
-
'Field "symbolOrder" must be an object keyed by file name. Ignoring value.',
|
|
2798
|
-
);
|
|
2799
|
-
return undefined;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
const normalizedSymbolOrderEntries = Object.entries(value)
|
|
2803
|
-
.map(([fileName, symbolNames]) => {
|
|
2804
|
-
const normalizedFileName = fileName.trim();
|
|
2805
|
-
if (normalizedFileName.length === 0) {
|
|
2806
|
-
warnDirectoryDocsOrderConfig(
|
|
2807
|
-
configPath,
|
|
2808
|
-
'Field "symbolOrder" contains an empty file key. Dropping entry.',
|
|
2809
|
-
);
|
|
2810
|
-
return undefined;
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
const normalizedSymbols = normalizeStringArrayConfigValue(
|
|
2814
|
-
symbolNames,
|
|
2815
|
-
`symbolOrder.${normalizedFileName}`,
|
|
2816
|
-
configPath,
|
|
2817
|
-
);
|
|
2818
|
-
if (!normalizedSymbols) {
|
|
2819
|
-
return undefined;
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
|
-
return [normalizedFileName, normalizedSymbols] as const;
|
|
2823
|
-
})
|
|
2824
|
-
.filter(
|
|
2825
|
-
(
|
|
2826
|
-
entry,
|
|
2827
|
-
): entry is readonly [string, string[]] => entry !== undefined,
|
|
2828
|
-
);
|
|
2829
|
-
|
|
2830
|
-
return normalizedSymbolOrderEntries.length > 0
|
|
2831
|
-
? Object.fromEntries(normalizedSymbolOrderEntries)
|
|
2832
|
-
: undefined;
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
/**
|
|
2836
|
-
* Writes a scoped warning for one docs ordering config file.
|
|
2837
|
-
*
|
|
2838
|
-
* @param configPath - Absolute config path.
|
|
2839
|
-
* @param message - Warning message.
|
|
2840
|
-
* @returns Nothing.
|
|
2841
|
-
*/
|
|
2842
|
-
function warnDirectoryDocsOrderConfig(
|
|
2843
|
-
configPath: string,
|
|
2844
|
-
message: string,
|
|
2845
|
-
): void {
|
|
2846
|
-
const relativeConfigPath = path
|
|
2847
|
-
.relative(WORKSPACE_ROOT_DIR, configPath)
|
|
2848
|
-
.replace(/\\/g, '/');
|
|
2849
|
-
console.warn(`[docs] ${relativeConfigPath}: ${message}`);
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
/**
|
|
2853
|
-
* Narrows unknown values to simple object records.
|
|
2854
|
-
*
|
|
2855
|
-
* @param value - Value to inspect.
|
|
2856
|
-
* @returns True when the value is a plain record-like object.
|
|
2857
|
-
*/
|
|
2858
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2859
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
/**
|
|
2863
|
-
* Extracts a readable message from unknown thrown values.
|
|
2864
|
-
*
|
|
2865
|
-
* @param error - Thrown value.
|
|
2866
|
-
* @returns Human-readable error text.
|
|
2867
|
-
*/
|
|
2868
|
-
function getErrorMessage(error: unknown): string {
|
|
2869
|
-
if (error instanceof Error) {
|
|
2870
|
-
return error.message;
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
return String(error);
|
|
2874
|
-
}
|
|
2875
|
-
|
|
2876
|
-
/**
|
|
2877
|
-
* Writes a file only when its content changed.
|
|
2878
|
-
*
|
|
2879
|
-
* @param filePath - Output file path.
|
|
2880
|
-
* @param content - Desired file content.
|
|
2881
|
-
* @returns Nothing.
|
|
2882
|
-
*/
|
|
2883
|
-
async function writeFileIfChanged(
|
|
2884
|
-
filePath: string,
|
|
2885
|
-
content: string,
|
|
2886
|
-
): Promise<void> {
|
|
2887
|
-
if (await fs.pathExists(filePath)) {
|
|
2888
|
-
const previousContent = await fs.readFile(filePath, 'utf8');
|
|
2889
|
-
if (previousContent === content) {
|
|
2890
|
-
return;
|
|
2891
|
-
}
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
await fs.writeFile(filePath, content, 'utf8');
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
main().catch((error: unknown) => {
|
|
2898
|
-
console.error(error);
|
|
2899
|
-
process.exit(1);
|
|
2900
|
-
});
|