@reicek/neataptic-ts 0.1.21 → 0.1.22
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/agents/boundary-mapper.agent.md +29 -0
- package/.github/agents/docs-scout.agent.md +29 -0
- package/.github/agents/plan-scout.agent.md +29 -0
- package/.github/agents/solid-split.agent.md +138 -0
- package/.github/copilot-instructions.md +103 -0
- package/package.json +6 -3
- package/plans/ES2023 migration +13 -8
- package/plans/Evolution_Training_Interoperability_Contracts.md +1 -1
- package/plans/Interactive_Examples_and_Learning_Path.md +10 -2
- package/plans/Memory_Optimization.md +3 -3
- package/plans/README.md +63 -0
- package/plans/Roadmap.md +15 -3
- package/plans/asciiMaze_SOLID_split.done.md +130 -0
- package/plans/flappy_bird_SOLID_split.done.md +67 -0
- package/scripts/assets/theme.css +221 -34
- package/scripts/copy-examples.mjs +9 -5
- package/scripts/export-onnx.mjs +3 -3
- package/scripts/generate-bench-tables.mjs +10 -10
- package/scripts/generate-bench-tables.ts +10 -10
- package/scripts/generate-docs.ts +1415 -449
- package/scripts/render-docs-html.ts +15 -8
- package/src/README.md +101 -223
- package/src/architecture/README.md +57 -185
- package/src/architecture/layer/README.md +38 -38
- package/src/architecture/network/README.md +33 -31
- package/src/architecture/network/activate/README.md +77 -77
- package/src/architecture/network/connect/README.md +15 -13
- package/src/architecture/network/deterministic/README.md +7 -7
- package/src/architecture/network/evolve/README.md +44 -44
- package/src/architecture/network/gating/README.md +20 -20
- package/src/architecture/network/genetic/README.md +51 -51
- package/src/architecture/network/mutate/README.md +97 -97
- package/src/architecture/network/onnx/README.md +264 -264
- package/src/architecture/network/prune/README.md +39 -39
- package/src/architecture/network/remove/README.md +26 -26
- package/src/architecture/network/serialize/README.md +56 -56
- package/src/architecture/network/slab/README.md +61 -61
- package/src/architecture/network/standalone/README.md +24 -24
- package/src/architecture/network/stats/README.md +9 -9
- package/src/architecture/network/topology/README.md +46 -46
- package/src/architecture/network/training/README.md +21 -21
- package/src/methods/README.md +9 -87
- package/src/multithreading/README.md +8 -77
- package/src/multithreading/workers/README.md +2 -2
- package/src/multithreading/workers/browser/README.md +0 -6
- package/src/multithreading/workers/node/README.md +0 -3
- package/src/neat/README.md +562 -568
- package/src/utils/README.md +18 -18
- package/test/examples/asciiMaze/README.md +59 -59
- package/test/examples/asciiMaze/asciiMaze.e2e.test.ts +14 -9
- package/test/examples/asciiMaze/browser-entry/README.md +196 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.abort.services.ts +95 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.constants.ts +23 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.curriculum.services.ts +115 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.globals.services.ts +106 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.host.services.ts +157 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.services.ts +14 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.ts +129 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.types.ts +120 -0
- package/test/examples/asciiMaze/browser-entry/browser-entry.utils.ts +98 -0
- package/test/examples/asciiMaze/browser-entry.ts +10 -576
- package/test/examples/asciiMaze/dashboardManager/README.md +276 -0
- package/test/examples/asciiMaze/dashboardManager/archive/README.md +16 -0
- package/test/examples/asciiMaze/dashboardManager/archive/dashboardManager.archive.services.ts +267 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.constants.ts +35 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.services.ts +103 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.ts +181 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.types.ts +267 -0
- package/test/examples/asciiMaze/dashboardManager/dashboardManager.utils.ts +254 -0
- package/test/examples/asciiMaze/dashboardManager/live/README.md +14 -0
- package/test/examples/asciiMaze/dashboardManager/live/dashboardManager.live.services.ts +264 -0
- package/test/examples/asciiMaze/dashboardManager/telemetry/README.md +47 -0
- package/test/examples/asciiMaze/dashboardManager/telemetry/dashboardManager.telemetry.services.ts +513 -0
- package/test/examples/asciiMaze/dashboardManager.ts +13 -2335
- package/test/examples/asciiMaze/evolutionEngine/README.md +1058 -0
- package/test/examples/asciiMaze/evolutionEngine/curriculumPhase.ts +90 -0
- package/test/examples/asciiMaze/evolutionEngine/engineState.constants.ts +36 -0
- package/test/examples/asciiMaze/evolutionEngine/engineState.ts +58 -513
- package/test/examples/asciiMaze/evolutionEngine/engineState.types.ts +212 -0
- package/test/examples/asciiMaze/evolutionEngine/engineState.utils.ts +301 -0
- package/test/examples/asciiMaze/evolutionEngine/evolutionEngine.types.ts +445 -0
- package/test/examples/asciiMaze/evolutionEngine/evolutionLoop.ts +81 -50
- package/test/examples/asciiMaze/evolutionEngine/optionsAndSetup.ts +2 -4
- package/test/examples/asciiMaze/evolutionEngine/populationDynamics.ts +17 -33
- package/test/examples/asciiMaze/evolutionEngine/populationPruning.ts +1 -1
- package/test/examples/asciiMaze/evolutionEngine/rngAndTiming.ts +1 -2
- package/test/examples/asciiMaze/evolutionEngine/sampling.ts +1 -1
- package/test/examples/asciiMaze/evolutionEngine/scratchPools.ts +2 -5
- package/test/examples/asciiMaze/evolutionEngine/setupHelpers.ts +30 -37
- package/test/examples/asciiMaze/evolutionEngine/telemetryMetrics.ts +16 -58
- package/test/examples/asciiMaze/evolutionEngine/trainingWarmStart.ts +2 -2
- package/test/examples/asciiMaze/evolutionEngine.ts +55 -55
- package/test/examples/asciiMaze/fitness.ts +2 -2
- package/test/examples/asciiMaze/fitness.types.ts +65 -0
- package/test/examples/asciiMaze/interfaces.ts +64 -1352
- package/test/examples/asciiMaze/mazeMovement/README.md +356 -0
- package/test/examples/asciiMaze/mazeMovement/finalization/README.md +49 -0
- package/test/examples/asciiMaze/mazeMovement/finalization/mazeMovement.finalization.ts +138 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.constants.ts +101 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.services.ts +230 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.ts +299 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.types.ts +185 -0
- package/test/examples/asciiMaze/mazeMovement/mazeMovement.utils.ts +153 -0
- package/test/examples/asciiMaze/mazeMovement/policy/README.md +91 -0
- package/test/examples/asciiMaze/mazeMovement/policy/mazeMovement.policy.ts +467 -0
- package/test/examples/asciiMaze/mazeMovement/runtime/README.md +95 -0
- package/test/examples/asciiMaze/mazeMovement/runtime/mazeMovement.runtime.ts +354 -0
- package/test/examples/asciiMaze/mazeMovement/shaping/README.md +124 -0
- package/test/examples/asciiMaze/mazeMovement/shaping/mazeMovement.shaping.ts +459 -0
- package/test/examples/asciiMaze/mazeMovement.ts +12 -2978
- package/test/examples/flappy_bird/Trace-20260309T191949.json +24124 -0
- package/test/examples/flappy_bird/browser-entry/README.md +1129 -0
- package/test/examples/flappy_bird/browser-entry/browser-entry.host.utils.ts +4 -324
- package/test/examples/flappy_bird/browser-entry/browser-entry.network-view.utils.ts +6 -399
- package/test/examples/flappy_bird/browser-entry/browser-entry.playback.utils.ts +1 -717
- package/test/examples/flappy_bird/browser-entry/browser-entry.spawn.utils.ts +11 -31
- package/test/examples/flappy_bird/browser-entry/browser-entry.visualization.utils.ts +15 -893
- package/test/examples/flappy_bird/browser-entry/host/README.md +307 -0
- package/test/examples/flappy_bird/browser-entry/host/host.resize.service.ts +1 -295
- package/test/examples/flappy_bird/browser-entry/host/host.ts +562 -6
- package/test/examples/flappy_bird/browser-entry/host/resize/README.md +274 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.constants.ts +31 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.services.ts +360 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.ts +117 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.types.ts +63 -0
- package/test/examples/flappy_bird/browser-entry/host/resize/host.resize.service.utils.ts +250 -0
- package/test/examples/flappy_bird/browser-entry/network-view/README.md +399 -0
- package/test/examples/flappy_bird/browser-entry/network-view/network-view.topology.utils.ts +255 -0
- package/test/examples/flappy_bird/browser-entry/network-view/network-view.ts +802 -7
- package/test/examples/flappy_bird/browser-entry/playback/README.md +684 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/README.md +277 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/README.md +770 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.cache.services.ts +178 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.constants.ts +107 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.geometry.utils.ts +518 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.math.utils.ts +117 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.pulse.utils.ts +233 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.services.ts +211 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.ts +48 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.types.ts +212 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/ground-grid/playback.background.ground-grid.utils.ts +81 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.cache.services.ts +96 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.constants.ts +62 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.services.ts +244 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.ts +53 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.types.ts +68 -0
- package/test/examples/flappy_bird/browser-entry/playback/background/playback.background.utils.ts +100 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/README.md +310 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.service.ts +92 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.services.ts +272 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.types.ts +39 -0
- package/test/examples/flappy_bird/browser-entry/playback/frame-render/playback.frame-render.utils.ts +493 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.constants.ts +1 -1
- package/test/examples/flappy_bird/browser-entry/playback/playback.frame-render.service.ts +4 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.snapshot.utils.ts +44 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.service.ts +39 -122
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.services.ts +272 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.types.ts +62 -0
- package/test/examples/flappy_bird/browser-entry/playback/playback.starfield.utils.ts +11 -4
- package/test/examples/flappy_bird/browser-entry/playback/playback.ts +409 -8
- package/test/examples/flappy_bird/browser-entry/playback/playback.types.ts +4 -12
- package/test/examples/flappy_bird/browser-entry/runtime/README.md +235 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.evolution-launch.service.ts +45 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.lifecycle.service.ts +81 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.startup.service.ts +74 -0
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.ts +31 -121
- package/test/examples/flappy_bird/browser-entry/runtime/runtime.types.ts +36 -0
- package/test/examples/flappy_bird/browser-entry/visualization/README.md +557 -0
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.constants.ts +110 -0
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.draw.service.ts +957 -19
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.legend.utils.ts +138 -3
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.topology.utils.ts +3 -27
- package/test/examples/flappy_bird/browser-entry/visualization/visualization.ts +1 -23
- package/test/examples/flappy_bird/browser-entry/worker-channel/README.md +156 -0
- package/test/examples/flappy_bird/constants/README.md +1179 -0
- package/test/examples/flappy_bird/constants/constants.network-view.ts +24 -0
- package/test/examples/flappy_bird/constants/constants.palette.ts +7 -0
- package/test/examples/flappy_bird/constants/constants.starfield.ts +78 -3
- package/test/examples/flappy_bird/environment/README.md +143 -0
- package/test/examples/flappy_bird/environment/environment.observation.utils.ts +1 -19
- package/test/examples/flappy_bird/environment/environment.step.service.ts +3 -66
- package/test/examples/flappy_bird/evaluation/README.md +130 -0
- package/test/examples/flappy_bird/evaluation/evaluation.fitness.utils.ts +1 -1
- package/test/examples/flappy_bird/evaluation/evaluation.rollout.service.ts +5 -375
- package/test/examples/flappy_bird/evaluation/rollout/README.md +291 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.constants.ts +30 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.service.ts +58 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.services.ts +310 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.types.ts +56 -0
- package/test/examples/flappy_bird/evaluation/rollout/evaluation.rollout.utils.ts +368 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/README.md +618 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.playback.service.ts +7 -7
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.simulation.frame.service.ts +364 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.simulation.types.ts +14 -0
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.simulation.utils.ts +4 -201
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.ts +184 -345
- package/test/examples/flappy_bird/flappy-evolution-worker/flappy-evolution-worker.warm-start.service.ts +291 -0
- package/test/examples/flappy_bird/flappy.simulation.shared.utils.ts +5 -0
- package/test/examples/flappy_bird/simulation-shared/README.md +417 -0
- package/test/examples/flappy_bird/simulation-shared/observation/README.md +183 -0
- package/test/examples/flappy_bird/simulation-shared/observation/observation.features.utils.ts +301 -0
- package/test/examples/flappy_bird/simulation-shared/observation/observation.ts +9 -0
- package/test/examples/flappy_bird/simulation-shared/observation/observation.vector.utils.ts +59 -0
- package/test/examples/flappy_bird/simulation-shared/simulation-shared.observation.utils.ts +5 -403
- package/test/examples/flappy_bird/simulation-shared/simulation-shared.spawn.utils.ts +20 -6
- package/test/examples/flappy_bird/{evaluation/evaluation.statistics.utils.ts → simulation-shared/simulation-shared.statistics.utils.ts} +23 -8
- package/test/examples/flappy_bird/trainer/README.md +563 -0
- package/test/examples/flappy_bird/trainer/evaluation/README.md +199 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.constants.ts +9 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.services.ts +73 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.ts +165 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.types.ts +25 -0
- package/test/examples/flappy_bird/trainer/evaluation/trainer.evaluation.service.utils.ts +161 -0
- package/test/examples/flappy_bird/trainer/trainer.evaluation.service.ts +13 -0
- package/test/examples/flappy_bird/trainer/trainer.report.service.services.ts +181 -0
- package/test/examples/flappy_bird/trainer/trainer.report.service.ts +126 -0
- package/test/examples/flappy_bird/trainer/trainer.selection.utils.ts +89 -0
- package/test/examples/flappy_bird/trainer/trainer.ts +11 -553
- package/test/examples/flappy_bird/browser-entry/browser-entry.utils.ts +0 -12
- package/test/examples/flappy_bird/environment/environment.ts +0 -7
- package/test/examples/flappy_bird/evaluation/evaluation.ts +0 -7
- package/test/examples/flappy_bird/simulation-shared/simulation-shared.ts +0 -15
- package/test/examples/flappy_bird/trainer/trainer.statistics.utils.ts +0 -78
package/scripts/generate-docs.ts
CHANGED
|
@@ -4,15 +4,42 @@
|
|
|
4
4
|
* - Skips generating README for src root (leave top-level README manual)
|
|
5
5
|
* Usage: npm run docs:folders
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Project,
|
|
9
|
+
type JSDocTag,
|
|
10
|
+
type SourceFile,
|
|
11
|
+
type Symbol as MorphSymbol,
|
|
12
|
+
} from 'ts-morph';
|
|
8
13
|
import fg from 'fast-glob';
|
|
9
|
-
import * as path from 'path';
|
|
10
14
|
import fs from 'fs-extra';
|
|
15
|
+
import * as path from 'path';
|
|
11
16
|
|
|
12
|
-
const SRC_DIR = path.resolve('src');
|
|
13
17
|
const DOCS_DIR = path.resolve('docs');
|
|
14
|
-
const
|
|
15
|
-
const
|
|
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 WORKSPACE_ROOT_DIR = path.resolve('.');
|
|
24
|
+
|
|
25
|
+
interface DocsTargetConfig {
|
|
26
|
+
name: string;
|
|
27
|
+
sourceDir: string;
|
|
28
|
+
docsDir: string;
|
|
29
|
+
rootDocsDir: string;
|
|
30
|
+
rootReadmeSource?: string;
|
|
31
|
+
rootReadmeDestination?: string;
|
|
32
|
+
excludeRootSourceFiles?: boolean;
|
|
33
|
+
includeFolderIndex?: boolean;
|
|
34
|
+
publishedRootDir?: string;
|
|
35
|
+
preservePublishedEntries?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RenderedParameter {
|
|
39
|
+
name: string;
|
|
40
|
+
type?: string;
|
|
41
|
+
doc?: string;
|
|
42
|
+
}
|
|
16
43
|
|
|
17
44
|
interface RenderedSymbol {
|
|
18
45
|
kind: string;
|
|
@@ -23,507 +50,1443 @@ interface RenderedSymbol {
|
|
|
23
50
|
jsdoc: {
|
|
24
51
|
summary?: string;
|
|
25
52
|
description?: string;
|
|
26
|
-
params?:
|
|
53
|
+
params?: RenderedParameter[];
|
|
27
54
|
returns?: string;
|
|
28
55
|
deprecated?: string;
|
|
29
56
|
};
|
|
30
57
|
}
|
|
31
58
|
|
|
59
|
+
interface FolderIndexNode {
|
|
60
|
+
name: string;
|
|
61
|
+
path: string;
|
|
62
|
+
children: Map<string, FolderIndexNode>;
|
|
63
|
+
fileCount?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type DirectorySymbolMap = Map<string, Map<string, RenderedSymbol[]>>;
|
|
67
|
+
|
|
68
|
+
const DOCS_TARGETS: Record<string, DocsTargetConfig> = {
|
|
69
|
+
src: {
|
|
70
|
+
name: 'src',
|
|
71
|
+
sourceDir: path.resolve('src'),
|
|
72
|
+
docsDir: DOCS_DIR,
|
|
73
|
+
rootDocsDir: path.join(DOCS_DIR, 'src'),
|
|
74
|
+
rootReadmeSource: path.resolve('README.md'),
|
|
75
|
+
rootReadmeDestination: path.join(DOCS_DIR, 'README.md'),
|
|
76
|
+
includeFolderIndex: true,
|
|
77
|
+
},
|
|
78
|
+
asciiMaze: {
|
|
79
|
+
name: 'asciiMaze',
|
|
80
|
+
sourceDir: path.resolve('test', 'examples', 'asciiMaze'),
|
|
81
|
+
docsDir: path.join(DOCS_DIR, 'examples', 'asciiMaze', 'docs'),
|
|
82
|
+
rootDocsDir: path.join(DOCS_DIR, 'examples', 'asciiMaze', 'docs'),
|
|
83
|
+
rootReadmeSource: path.resolve('test', 'examples', 'asciiMaze', 'README.md'),
|
|
84
|
+
rootReadmeDestination: path.join(
|
|
85
|
+
DOCS_DIR,
|
|
86
|
+
'examples',
|
|
87
|
+
'asciiMaze',
|
|
88
|
+
'docs',
|
|
89
|
+
'README.md',
|
|
90
|
+
),
|
|
91
|
+
excludeRootSourceFiles: true,
|
|
92
|
+
publishedRootDir: path.join(DOCS_DIR, 'examples', 'asciiMaze'),
|
|
93
|
+
preservePublishedEntries: ['index.html'],
|
|
94
|
+
},
|
|
95
|
+
'flappy-bird': {
|
|
96
|
+
name: 'flappy-bird',
|
|
97
|
+
sourceDir: path.resolve('test', 'examples', 'flappy_bird'),
|
|
98
|
+
docsDir: path.join(DOCS_DIR, 'examples', 'flappy_bird', 'docs'),
|
|
99
|
+
rootDocsDir: path.join(DOCS_DIR, 'examples', 'flappy_bird', 'docs'),
|
|
100
|
+
rootReadmeSource: path.resolve(
|
|
101
|
+
'test',
|
|
102
|
+
'examples',
|
|
103
|
+
'flappy_bird',
|
|
104
|
+
'README.md',
|
|
105
|
+
),
|
|
106
|
+
rootReadmeDestination: path.join(
|
|
107
|
+
DOCS_DIR,
|
|
108
|
+
'examples',
|
|
109
|
+
'flappy_bird',
|
|
110
|
+
'docs',
|
|
111
|
+
'README.md',
|
|
112
|
+
),
|
|
113
|
+
excludeRootSourceFiles: true,
|
|
114
|
+
publishedRootDir: path.join(DOCS_DIR, 'examples', 'flappy_bird'),
|
|
115
|
+
preservePublishedEntries: ['index.html'],
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
32
119
|
const project = new Project({
|
|
33
120
|
tsConfigFilePath: path.resolve('tsconfig.json'),
|
|
34
|
-
skipAddingFilesFromTsConfig: true
|
|
121
|
+
skipAddingFilesFromTsConfig: true,
|
|
35
122
|
});
|
|
36
123
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Generates docs for the requested target.
|
|
126
|
+
*
|
|
127
|
+
* The script runs in four passes:
|
|
128
|
+
* 1. Resolve and prepare the target output tree.
|
|
129
|
+
* 2. Load source files into the shared ts-morph project.
|
|
130
|
+
* 3. Collect and normalize rendered symbols.
|
|
131
|
+
* 4. Emit directory README files and the optional folder index.
|
|
132
|
+
*/
|
|
133
|
+
async function main(): Promise<void> {
|
|
134
|
+
const target = resolveDocsTarget();
|
|
135
|
+
await initializeDocsTarget(target);
|
|
47
136
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
137
|
+
const sourceFiles = await loadTargetSourceFiles(target);
|
|
138
|
+
const directorySymbolMap = collectDirectorySymbols(sourceFiles);
|
|
139
|
+
|
|
140
|
+
dedupeDirectorySymbols(directorySymbolMap);
|
|
141
|
+
await emitDirectoryDocs(directorySymbolMap, target);
|
|
142
|
+
|
|
143
|
+
if (target.includeFolderIndex) {
|
|
144
|
+
await emitFolderIndex(directorySymbolMap, target);
|
|
145
|
+
}
|
|
54
146
|
|
|
55
|
-
|
|
56
|
-
if (cleaned.split('\n').some(line => line.trim().startsWith('@internal'))) return undefined;
|
|
57
|
-
return cleaned;
|
|
147
|
+
console.log(`[docs:${target.name}] Per-folder README generation complete.`);
|
|
58
148
|
}
|
|
59
149
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Resolves the active docs target from CLI or environment input.
|
|
152
|
+
*
|
|
153
|
+
* Lookup order:
|
|
154
|
+
* 1. `--target=...`
|
|
155
|
+
* 2. `DOCS_TARGET`
|
|
156
|
+
* 3. default target
|
|
157
|
+
*
|
|
158
|
+
* @returns Concrete docs target configuration.
|
|
159
|
+
*/
|
|
160
|
+
function resolveDocsTarget(): DocsTargetConfig {
|
|
161
|
+
const cliTarget = process.argv
|
|
162
|
+
.find((argument) => argument.startsWith('--target='))
|
|
163
|
+
?.slice('--target='.length);
|
|
164
|
+
const requestedTarget =
|
|
165
|
+
cliTarget || process.env.DOCS_TARGET || DEFAULT_DOCS_TARGET;
|
|
166
|
+
const resolvedTarget = DOCS_TARGETS[requestedTarget];
|
|
167
|
+
|
|
168
|
+
if (!resolvedTarget) {
|
|
169
|
+
const supportedTargets = Object.keys(DOCS_TARGETS).toSorted().join(', ');
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Unknown docs target "${requestedTarget}". Supported targets: ${supportedTargets}`,
|
|
172
|
+
);
|
|
65
173
|
}
|
|
66
174
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
175
|
+
return resolvedTarget;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Prepares the output tree for the selected target.
|
|
180
|
+
*
|
|
181
|
+
* @param target - Target configuration.
|
|
182
|
+
* @returns Nothing.
|
|
183
|
+
*/
|
|
184
|
+
async function initializeDocsTarget(target: DocsTargetConfig): Promise<void> {
|
|
185
|
+
await cleanupPublishedRoot(target);
|
|
186
|
+
await fs.ensureDir(target.docsDir);
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
!target.rootReadmeSource ||
|
|
190
|
+
!target.rootReadmeDestination ||
|
|
191
|
+
!(await fs.pathExists(target.rootReadmeSource))
|
|
192
|
+
) {
|
|
193
|
+
return;
|
|
71
194
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
195
|
+
|
|
196
|
+
await fs.ensureDir(path.dirname(target.rootReadmeDestination));
|
|
197
|
+
await fs.copyFile(target.rootReadmeSource, target.rootReadmeDestination);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Loads target source files into the shared ts-morph project.
|
|
202
|
+
*
|
|
203
|
+
* @param target - Target configuration.
|
|
204
|
+
* @returns Filtered source files that belong to the target tree.
|
|
205
|
+
*/
|
|
206
|
+
async function loadTargetSourceFiles(
|
|
207
|
+
target: DocsTargetConfig,
|
|
208
|
+
): Promise<SourceFile[]> {
|
|
209
|
+
const filePaths = await fg(SOURCE_FILE_GLOBS, {
|
|
210
|
+
cwd: target.sourceDir,
|
|
211
|
+
absolute: true,
|
|
212
|
+
ignore: SOURCE_FILE_IGNORE_GLOBS,
|
|
76
213
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
214
|
+
|
|
215
|
+
for (const filePath of filePaths) {
|
|
216
|
+
if (!shouldIncludeSourceFile(filePath, target)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
project.addSourceFileAtPath(filePath);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const rawSourceFiles = project.getSourceFiles();
|
|
224
|
+
const sourceFiles = rawSourceFiles.filter((sourceFile) => {
|
|
225
|
+
const filePath = sourceFile.getFilePath();
|
|
226
|
+
return !filePath.endsWith('.d.ts') && !/node_modules/.test(filePath);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
console.log(
|
|
230
|
+
`[docs:${target.name}] Loaded ${sourceFiles.length} source files (raw: ${rawSourceFiles.length})`,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return sourceFiles;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Determines whether a discovered file should participate in docs generation.
|
|
238
|
+
*
|
|
239
|
+
* @param filePath - Absolute source file path.
|
|
240
|
+
* @param target - Target configuration.
|
|
241
|
+
* @returns True when the file belongs in the target set.
|
|
242
|
+
*/
|
|
243
|
+
function shouldIncludeSourceFile(
|
|
244
|
+
filePath: string,
|
|
245
|
+
target: DocsTargetConfig,
|
|
246
|
+
): boolean {
|
|
247
|
+
if (/\.test\.ts$/i.test(filePath)) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!target.excludeRootSourceFiles) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const relativeDirectory = path.dirname(
|
|
256
|
+
path.relative(target.sourceDir, filePath),
|
|
257
|
+
);
|
|
258
|
+
return relativeDirectory !== '' && relativeDirectory !== '.';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Removes previously published generated files for targets that mirror output
|
|
263
|
+
* into a browsable published directory.
|
|
264
|
+
*
|
|
265
|
+
* @param target - Target configuration.
|
|
266
|
+
* @returns Nothing.
|
|
267
|
+
*/
|
|
268
|
+
async function cleanupPublishedRoot(target: DocsTargetConfig): Promise<void> {
|
|
269
|
+
if (!target.publishedRootDir) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await fs.ensureDir(target.publishedRootDir);
|
|
274
|
+
const preservedEntries = new Set(target.preservePublishedEntries ?? []);
|
|
275
|
+
const childEntries = await fs.readdir(target.publishedRootDir);
|
|
276
|
+
|
|
277
|
+
for (const childEntry of childEntries) {
|
|
278
|
+
if (preservedEntries.has(childEntry)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await fs.remove(path.join(target.publishedRootDir, childEntry));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Collects renderable symbols grouped by directory and then by file.
|
|
288
|
+
*
|
|
289
|
+
* @param sourceFiles - Loaded source files to scan.
|
|
290
|
+
* @returns Nested symbol map keyed by directory and then source file path.
|
|
291
|
+
*/
|
|
292
|
+
function collectDirectorySymbols(
|
|
293
|
+
sourceFiles: readonly SourceFile[],
|
|
294
|
+
): DirectorySymbolMap {
|
|
295
|
+
const directorySymbolMap: DirectorySymbolMap = new Map();
|
|
296
|
+
|
|
297
|
+
for (const sourceFile of sourceFiles) {
|
|
298
|
+
collectSourceFileSymbols(sourceFile, directorySymbolMap);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return directorySymbolMap;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Collects every renderable symbol from a single source file.
|
|
306
|
+
*
|
|
307
|
+
* @param sourceFile - Source file being scanned.
|
|
308
|
+
* @param directorySymbolMap - Aggregate directory symbol map.
|
|
309
|
+
* @returns Nothing.
|
|
310
|
+
*/
|
|
311
|
+
function collectSourceFileSymbols(
|
|
312
|
+
sourceFile: SourceFile,
|
|
313
|
+
directorySymbolMap: DirectorySymbolMap,
|
|
314
|
+
): void {
|
|
315
|
+
const filePath = sourceFile.getFilePath();
|
|
316
|
+
const fileSymbolMap = resolveFileSymbolMap(directorySymbolMap, filePath);
|
|
317
|
+
|
|
318
|
+
addFileSummarySymbol(sourceFile, fileSymbolMap);
|
|
319
|
+
collectExportedDeclarations(sourceFile, fileSymbolMap);
|
|
320
|
+
collectDocumentedTopLevelDeclarations(sourceFile, fileSymbolMap);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resolves the mutable file-symbol map for the file's directory.
|
|
325
|
+
*
|
|
326
|
+
* @param directorySymbolMap - Aggregate directory symbol map.
|
|
327
|
+
* @param filePath - Absolute source file path.
|
|
328
|
+
* @returns Per-directory file symbol map.
|
|
329
|
+
*/
|
|
330
|
+
function resolveFileSymbolMap(
|
|
331
|
+
directorySymbolMap: DirectorySymbolMap,
|
|
332
|
+
filePath: string,
|
|
333
|
+
): Map<string, RenderedSymbol[]> {
|
|
334
|
+
const directoryPath = path.dirname(filePath);
|
|
335
|
+
let fileSymbolMap = directorySymbolMap.get(directoryPath);
|
|
336
|
+
|
|
337
|
+
if (!fileSymbolMap) {
|
|
338
|
+
fileSymbolMap = new Map();
|
|
339
|
+
directorySymbolMap.set(directoryPath, fileSymbolMap);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return fileSymbolMap;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Adds a synthetic file-summary symbol when the file starts with a file-level
|
|
347
|
+
* JSDoc block.
|
|
348
|
+
*
|
|
349
|
+
* @param sourceFile - Source file being scanned.
|
|
350
|
+
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
351
|
+
* @returns Nothing.
|
|
352
|
+
*/
|
|
353
|
+
function addFileSummarySymbol(
|
|
354
|
+
sourceFile: SourceFile,
|
|
355
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
356
|
+
): void {
|
|
357
|
+
const fileSummaryDescription = resolveFileSummaryDescription(sourceFile);
|
|
358
|
+
if (!fileSummaryDescription) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
appendRenderedSymbol(fileSymbolMap, sourceFile.getFilePath(), {
|
|
363
|
+
kind: 'File',
|
|
364
|
+
name: FILE_SUMMARY_SYMBOL_NAME,
|
|
365
|
+
filePath: sourceFile.getFilePath(),
|
|
366
|
+
jsdoc: { description: fileSummaryDescription },
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
console.log(`[docs] Added file summary for ${sourceFile.getBaseName()}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolves a file-level description for a source file.
|
|
374
|
+
*
|
|
375
|
+
* @param sourceFile - Source file to inspect.
|
|
376
|
+
* @returns File summary text when present.
|
|
377
|
+
*/
|
|
378
|
+
function resolveFileSummaryDescription(
|
|
379
|
+
sourceFile: SourceFile,
|
|
380
|
+
): string | undefined {
|
|
381
|
+
return (
|
|
382
|
+
extractLeadingFileJsDocDescription(sourceFile) ||
|
|
383
|
+
getFirstJsDocDescription(
|
|
384
|
+
sourceFile as unknown as { getJsDocs?: () => unknown[] },
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Collects exported declarations and supported child members.
|
|
391
|
+
*
|
|
392
|
+
* @param sourceFile - Source file being scanned.
|
|
393
|
+
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
394
|
+
* @returns Nothing.
|
|
395
|
+
*/
|
|
396
|
+
function collectExportedDeclarations(
|
|
397
|
+
sourceFile: SourceFile,
|
|
398
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
399
|
+
): void {
|
|
400
|
+
const exportedDeclarations = sourceFile.getExportedDeclarations();
|
|
401
|
+
if (exportedDeclarations.size === 0) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(
|
|
406
|
+
`[docs] ${sourceFile.getBaseName()} exports: ${exportedDeclarations.size}`,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
for (const [, declarations] of exportedDeclarations) {
|
|
410
|
+
for (const declaration of declarations) {
|
|
411
|
+
const symbol = declaration.getSymbol();
|
|
412
|
+
if (!symbol) {
|
|
413
|
+
continue;
|
|
104
414
|
}
|
|
105
415
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
416
|
+
const renderedSymbol = renderSymbol(
|
|
417
|
+
symbol,
|
|
418
|
+
declaration.getKindName(),
|
|
419
|
+
sourceFile.getFilePath(),
|
|
420
|
+
);
|
|
421
|
+
if (renderedSymbol) {
|
|
422
|
+
appendRenderedSymbol(
|
|
423
|
+
fileSymbolMap,
|
|
424
|
+
sourceFile.getFilePath(),
|
|
425
|
+
renderedSymbol,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
collectExportedObjectLiteralMembers(
|
|
430
|
+
declaration as unknown as Record<string, unknown>,
|
|
431
|
+
symbol.getName(),
|
|
432
|
+
sourceFile.getFilePath(),
|
|
433
|
+
fileSymbolMap,
|
|
434
|
+
);
|
|
435
|
+
collectExportedClassMembers(
|
|
436
|
+
declaration as unknown as Record<string, unknown>,
|
|
437
|
+
symbol.getName(),
|
|
438
|
+
sourceFile.getFilePath(),
|
|
439
|
+
fileSymbolMap,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Collects documented properties from exported object-literal variables.
|
|
447
|
+
*
|
|
448
|
+
* @param declaration - Candidate declaration.
|
|
449
|
+
* @param parentName - Parent exported symbol name.
|
|
450
|
+
* @param filePath - Absolute source file path.
|
|
451
|
+
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
452
|
+
* @returns Nothing.
|
|
453
|
+
*/
|
|
454
|
+
function collectExportedObjectLiteralMembers(
|
|
455
|
+
declaration: Record<string, unknown>,
|
|
456
|
+
parentName: string,
|
|
457
|
+
filePath: string,
|
|
458
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
459
|
+
): void {
|
|
460
|
+
try {
|
|
461
|
+
const declarationKindName = (
|
|
462
|
+
declaration.getKindName as (() => string) | undefined
|
|
463
|
+
)?.();
|
|
464
|
+
if (declarationKindName !== 'VariableDeclaration') {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const initializer = (
|
|
469
|
+
declaration.getInitializer as (() => any) | undefined
|
|
470
|
+
)?.();
|
|
471
|
+
if (initializer?.getKindName?.() !== 'ObjectLiteralExpression') {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const properties = (initializer.getProperties?.() as any[]) || [];
|
|
476
|
+
for (const property of properties) {
|
|
477
|
+
const propertyName =
|
|
478
|
+
property.getName?.() || property.getSymbol?.()?.getName?.();
|
|
479
|
+
const renderedProperty = renderDeclaration(
|
|
480
|
+
property,
|
|
481
|
+
String(propertyName),
|
|
482
|
+
property.getKindName?.() || 'Property',
|
|
483
|
+
filePath,
|
|
484
|
+
parentName,
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
if (renderedProperty) {
|
|
488
|
+
appendRenderedSymbol(fileSymbolMap, filePath, renderedProperty);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
// Best-effort introspection only.
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Collects documented members from exported classes.
|
|
498
|
+
*
|
|
499
|
+
* @param declaration - Candidate declaration.
|
|
500
|
+
* @param parentName - Parent exported symbol name.
|
|
501
|
+
* @param filePath - Absolute source file path.
|
|
502
|
+
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
503
|
+
* @returns Nothing.
|
|
504
|
+
*/
|
|
505
|
+
function collectExportedClassMembers(
|
|
506
|
+
declaration: Record<string, unknown>,
|
|
507
|
+
parentName: string,
|
|
508
|
+
filePath: string,
|
|
509
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
510
|
+
): void {
|
|
511
|
+
try {
|
|
512
|
+
const declarationKindName = (
|
|
513
|
+
declaration.getKindName as (() => string) | undefined
|
|
514
|
+
)?.();
|
|
515
|
+
if (declarationKindName !== 'ClassDeclaration') {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const members =
|
|
520
|
+
(declaration.getMembers as (() => any[]) | undefined)?.() || [];
|
|
521
|
+
for (const member of members) {
|
|
522
|
+
const memberName = member.getName?.();
|
|
523
|
+
if (!memberName) {
|
|
524
|
+
continue;
|
|
156
525
|
}
|
|
157
526
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
527
|
+
const renderedMember = renderDeclaration(
|
|
528
|
+
member,
|
|
529
|
+
String(memberName),
|
|
530
|
+
member.getKindName?.() || 'ClassMember',
|
|
531
|
+
filePath,
|
|
532
|
+
parentName,
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (renderedMember) {
|
|
536
|
+
appendRenderedSymbol(fileSymbolMap, filePath, renderedMember);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
// Best-effort introspection only.
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Collects documented top-level non-exported declarations.
|
|
546
|
+
*
|
|
547
|
+
* @param sourceFile - Source file being scanned.
|
|
548
|
+
* @param fileSymbolMap - Mutable symbol map for the file's directory.
|
|
549
|
+
* @returns Nothing.
|
|
550
|
+
*/
|
|
551
|
+
function collectDocumentedTopLevelDeclarations(
|
|
552
|
+
sourceFile: SourceFile,
|
|
553
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
554
|
+
): void {
|
|
555
|
+
try {
|
|
556
|
+
const statements =
|
|
557
|
+
(
|
|
558
|
+
sourceFile as unknown as { getStatements?: () => any[] }
|
|
559
|
+
).getStatements?.() || [];
|
|
560
|
+
|
|
561
|
+
for (const statement of statements) {
|
|
562
|
+
const declarations = statement.getDeclarations?.() || [statement];
|
|
563
|
+
|
|
564
|
+
for (const declaration of declarations) {
|
|
565
|
+
const jsDocs = declaration.getJsDocs?.() || [];
|
|
566
|
+
if (jsDocs.length === 0) {
|
|
567
|
+
continue;
|
|
179
568
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
569
|
+
|
|
570
|
+
const name =
|
|
571
|
+
declaration.getName?.() || declaration.getSymbol?.()?.getName?.();
|
|
572
|
+
if (!name) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
hasRenderedSymbol(
|
|
578
|
+
fileSymbolMap,
|
|
579
|
+
sourceFile.getFilePath(),
|
|
580
|
+
String(name),
|
|
581
|
+
)
|
|
582
|
+
) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const renderedDeclaration = renderDeclaration(
|
|
587
|
+
declaration,
|
|
588
|
+
String(name),
|
|
589
|
+
declaration.getKindName?.() || 'Declaration',
|
|
590
|
+
sourceFile.getFilePath(),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (renderedDeclaration) {
|
|
594
|
+
appendRenderedSymbol(
|
|
595
|
+
fileSymbolMap,
|
|
596
|
+
sourceFile.getFilePath(),
|
|
597
|
+
renderedDeclaration,
|
|
598
|
+
);
|
|
209
599
|
}
|
|
210
600
|
}
|
|
211
|
-
fileMap.set(filePath, deduped);
|
|
212
601
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
602
|
+
} catch {
|
|
603
|
+
// Best-effort introspection only.
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Checks whether a symbol name has already been collected for a file.
|
|
609
|
+
*
|
|
610
|
+
* @param fileSymbolMap - Mutable symbol map for the directory.
|
|
611
|
+
* @param filePath - Absolute source file path.
|
|
612
|
+
* @param name - Candidate symbol name.
|
|
613
|
+
* @returns True when the symbol already exists in the collected set.
|
|
614
|
+
*/
|
|
615
|
+
function hasRenderedSymbol(
|
|
616
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
617
|
+
filePath: string,
|
|
618
|
+
name: string,
|
|
619
|
+
): boolean {
|
|
620
|
+
return (fileSymbolMap.get(filePath) ?? []).some(
|
|
621
|
+
(symbol) => symbol.name === name,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Appends a rendered symbol to the per-file collection.
|
|
627
|
+
*
|
|
628
|
+
* @param fileSymbolMap - Mutable symbol map for the directory.
|
|
629
|
+
* @param filePath - Absolute source file path.
|
|
630
|
+
* @param renderedSymbol - Symbol to append.
|
|
631
|
+
* @returns Nothing.
|
|
632
|
+
*/
|
|
633
|
+
function appendRenderedSymbol(
|
|
634
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
635
|
+
filePath: string,
|
|
636
|
+
renderedSymbol: RenderedSymbol,
|
|
637
|
+
): void {
|
|
638
|
+
const renderedSymbols = fileSymbolMap.get(filePath) ?? [];
|
|
639
|
+
renderedSymbols.push(renderedSymbol);
|
|
640
|
+
fileSymbolMap.set(filePath, renderedSymbols);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Tidies and deduplicates collected symbols before README rendering.
|
|
645
|
+
*
|
|
646
|
+
* @param directorySymbolMap - Directory symbol map to normalize.
|
|
647
|
+
* @returns Nothing.
|
|
648
|
+
*/
|
|
649
|
+
function dedupeDirectorySymbols(directorySymbolMap: DirectorySymbolMap): void {
|
|
650
|
+
for (const [, fileSymbolMap] of directorySymbolMap) {
|
|
651
|
+
for (const [filePath, renderedSymbols] of fileSymbolMap) {
|
|
652
|
+
fileSymbolMap.set(filePath, dedupeFileSymbols(renderedSymbols, filePath));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Deduplicates symbols for a single file while conservatively merging JSDoc.
|
|
659
|
+
*
|
|
660
|
+
* @param renderedSymbols - Symbols collected for one file.
|
|
661
|
+
* @param filePath - Absolute source file path.
|
|
662
|
+
* @returns Deduplicated symbol list.
|
|
663
|
+
*/
|
|
664
|
+
function dedupeFileSymbols(
|
|
665
|
+
renderedSymbols: readonly RenderedSymbol[],
|
|
666
|
+
filePath: string,
|
|
667
|
+
): RenderedSymbol[] {
|
|
668
|
+
const seen = new Map<string, RenderedSymbol>();
|
|
669
|
+
const dedupedSymbols: RenderedSymbol[] = [];
|
|
670
|
+
|
|
671
|
+
for (const renderedSymbol of renderedSymbols) {
|
|
672
|
+
const normalizedName = normalizeName(renderedSymbol, filePath);
|
|
673
|
+
const dedupeKey = `${renderedSymbol.parent || ''}::${normalizedName}::${renderedSymbol.signature || ''}`;
|
|
674
|
+
const existingSymbol = seen.get(dedupeKey);
|
|
675
|
+
|
|
676
|
+
if (existingSymbol) {
|
|
677
|
+
mergeRenderedSymbols(existingSymbol, renderedSymbol);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const clonedSymbol: RenderedSymbol = {
|
|
682
|
+
...renderedSymbol,
|
|
683
|
+
name: normalizedName,
|
|
684
|
+
jsdoc: {
|
|
685
|
+
...renderedSymbol.jsdoc,
|
|
686
|
+
params: renderedSymbol.jsdoc.params?.slice(),
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
seen.set(dedupeKey, clonedSymbol);
|
|
691
|
+
dedupedSymbols.push(clonedSymbol);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return dedupedSymbols;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Merges missing JSDoc fields from a duplicate symbol into the retained one.
|
|
699
|
+
*
|
|
700
|
+
* @param target - Existing retained symbol.
|
|
701
|
+
* @param incoming - Duplicate symbol carrying possible extra data.
|
|
702
|
+
* @returns Nothing.
|
|
703
|
+
*/
|
|
704
|
+
function mergeRenderedSymbols(
|
|
705
|
+
target: RenderedSymbol,
|
|
706
|
+
incoming: RenderedSymbol,
|
|
707
|
+
): void {
|
|
708
|
+
target.jsdoc.description =
|
|
709
|
+
target.jsdoc.description || incoming.jsdoc.description;
|
|
710
|
+
target.jsdoc.summary = target.jsdoc.summary || incoming.jsdoc.summary;
|
|
711
|
+
target.jsdoc.deprecated =
|
|
712
|
+
target.jsdoc.deprecated || incoming.jsdoc.deprecated;
|
|
713
|
+
|
|
714
|
+
if (incoming.jsdoc.params?.length) {
|
|
715
|
+
target.jsdoc.params = (target.jsdoc.params || []).slice();
|
|
716
|
+
|
|
717
|
+
for (const parameter of incoming.jsdoc.params) {
|
|
718
|
+
const alreadyPresent = target.jsdoc.params.some(
|
|
719
|
+
(existingParameter) => existingParameter.name === parameter.name,
|
|
720
|
+
);
|
|
721
|
+
if (!alreadyPresent) {
|
|
722
|
+
target.jsdoc.params.push(parameter);
|
|
242
723
|
}
|
|
243
|
-
node = node.children.get(part)!;
|
|
244
724
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
console.log('Per-folder README generation complete.');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function renderSymbol(sym: MorphSymbol, fallbackKind: string, filePath: string): RenderedSymbol | null {
|
|
271
|
-
const decl = sym.getDeclarations()[0];
|
|
272
|
-
if (!decl) return null;
|
|
273
|
-
const kind = decl.getKindName();
|
|
274
|
-
const allow = /ClassDeclaration|FunctionDeclaration|InterfaceDeclaration|EnumDeclaration|TypeAliasDeclaration|VariableDeclaration/;
|
|
275
|
-
if (!allow.test(kind)) return null;
|
|
276
|
-
// ts-morph Declaration with JSDoc support; cast to any to access getJsDocs generically
|
|
277
|
-
const jsDocs = (decl as any).getJsDocs?.() || [];
|
|
278
|
-
if (jsDocs.some((j: any) => j.getTags().some((t: any) => t.getTagName() === 'internal'))) return null;
|
|
279
|
-
|
|
280
|
-
const primary = jsDocs[0];
|
|
281
|
-
const fullDesc = primary?.getDescription().trim();
|
|
282
|
-
const summary = fullDesc?.split(/\r?\n\r?\n/)[0]?.trim();
|
|
283
|
-
const tags: JSDocTag[] = primary?.getTags() || [];
|
|
284
|
-
const paramsTags = tags.filter(t => t.getTagName() === 'param');
|
|
285
|
-
const returnsTag = tags.find(t => t.getTagName() === 'returns' || t.getTagName() === 'return');
|
|
286
|
-
const deprecatedTag = tags.find(t => t.getTagName() === 'deprecated');
|
|
287
|
-
|
|
288
|
-
let signature: string | undefined;
|
|
289
|
-
try {
|
|
290
|
-
const sig = decl.getType().getCallSignatures()[0];
|
|
291
|
-
if (sig) {
|
|
292
|
-
const params = sig.getParameters().map((p: any) => {
|
|
293
|
-
const decls = p.getDeclarations();
|
|
294
|
-
const t = p.getTypeAtLocation(decls[0] || decl).getText();
|
|
295
|
-
return `${p.getName()}: ${t}`;
|
|
296
|
-
}).join(', ');
|
|
297
|
-
const ret = sig.getReturnType().getText();
|
|
298
|
-
signature = `(${params}) => ${ret}`;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (!target.signature && incoming.signature) {
|
|
728
|
+
target.signature = incoming.signature;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Emits directory README files to both docs output and the source tree.
|
|
734
|
+
*
|
|
735
|
+
* @param directorySymbolMap - Normalized directory symbol map.
|
|
736
|
+
* @param target - Target configuration.
|
|
737
|
+
* @returns Nothing.
|
|
738
|
+
*/
|
|
739
|
+
async function emitDirectoryDocs(
|
|
740
|
+
directorySymbolMap: DirectorySymbolMap,
|
|
741
|
+
target: DocsTargetConfig,
|
|
742
|
+
): Promise<void> {
|
|
743
|
+
for (const [directoryPath, fileSymbolMap] of directorySymbolMap) {
|
|
744
|
+
const relativeDirectory = path.relative(target.sourceDir, directoryPath);
|
|
745
|
+
if (relativeDirectory.startsWith('..')) {
|
|
746
|
+
continue;
|
|
299
747
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
748
|
+
|
|
749
|
+
const outputDirectory =
|
|
750
|
+
relativeDirectory === ''
|
|
751
|
+
? target.rootDocsDir
|
|
752
|
+
: path.join(target.docsDir, relativeDirectory);
|
|
753
|
+
const sourceDirectory =
|
|
754
|
+
relativeDirectory === ''
|
|
755
|
+
? target.sourceDir
|
|
756
|
+
: path.join(target.sourceDir, relativeDirectory);
|
|
757
|
+
|
|
758
|
+
const markdown = buildDirectoryReadme(
|
|
759
|
+
relativeDirectory,
|
|
760
|
+
fileSymbolMap,
|
|
761
|
+
target.sourceDir,
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
await fs.ensureDir(outputDirectory);
|
|
765
|
+
await writeFileIfChanged(path.join(outputDirectory, 'README.md'), markdown);
|
|
766
|
+
await writeFileIfChanged(path.join(sourceDirectory, 'README.md'), markdown);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Emits the folder index for targets that want a docs landing page.
|
|
772
|
+
*
|
|
773
|
+
* @param directorySymbolMap - Normalized directory symbol map.
|
|
774
|
+
* @param target - Target configuration.
|
|
775
|
+
* @returns Nothing.
|
|
776
|
+
*/
|
|
777
|
+
async function emitFolderIndex(
|
|
778
|
+
directorySymbolMap: DirectorySymbolMap,
|
|
779
|
+
target: DocsTargetConfig,
|
|
780
|
+
): Promise<void> {
|
|
781
|
+
const rootNode: FolderIndexNode = {
|
|
782
|
+
name: 'src',
|
|
783
|
+
path: 'src',
|
|
784
|
+
children: new Map(),
|
|
785
|
+
fileCount: 0,
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const relativeDirectories = [...directorySymbolMap.keys()]
|
|
789
|
+
.map((directoryPath) => path.relative(target.sourceDir, directoryPath))
|
|
790
|
+
.filter((relativeDirectory) => !relativeDirectory.startsWith('..'));
|
|
791
|
+
|
|
792
|
+
for (const relativeDirectory of relativeDirectories) {
|
|
793
|
+
insertFolderIndexNode(
|
|
794
|
+
rootNode,
|
|
795
|
+
relativeDirectory,
|
|
796
|
+
target,
|
|
797
|
+
directorySymbolMap,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const markdown = buildFolderIndexMarkdown(rootNode);
|
|
802
|
+
await writeFileIfChanged(
|
|
803
|
+
path.join(DOCS_DIR, FOLDER_INDEX_FILE_NAME),
|
|
804
|
+
markdown,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Inserts one relative directory into the folder index tree.
|
|
810
|
+
*
|
|
811
|
+
* @param rootNode - Root folder index node.
|
|
812
|
+
* @param relativeDirectory - Relative directory from the target source root.
|
|
813
|
+
* @param target - Target configuration.
|
|
814
|
+
* @param directorySymbolMap - Directory symbol map used for file counts.
|
|
815
|
+
* @returns Nothing.
|
|
816
|
+
*/
|
|
817
|
+
function insertFolderIndexNode(
|
|
818
|
+
rootNode: FolderIndexNode,
|
|
819
|
+
relativeDirectory: string,
|
|
820
|
+
target: DocsTargetConfig,
|
|
821
|
+
directorySymbolMap: DirectorySymbolMap,
|
|
822
|
+
): void {
|
|
823
|
+
const normalizedDirectory =
|
|
824
|
+
relativeDirectory === '' ? 'src' : relativeDirectory.replace(/\\/g, '/');
|
|
825
|
+
const pathParts = normalizedDirectory.split('/');
|
|
826
|
+
|
|
827
|
+
let currentNode = rootNode;
|
|
828
|
+
let accumulatedPath = '';
|
|
829
|
+
|
|
830
|
+
for (const pathPart of pathParts) {
|
|
831
|
+
accumulatedPath = accumulatedPath
|
|
832
|
+
? `${accumulatedPath}/${pathPart}`
|
|
833
|
+
: pathPart;
|
|
834
|
+
|
|
835
|
+
if (!currentNode.children.has(pathPart)) {
|
|
836
|
+
currentNode.children.set(pathPart, {
|
|
837
|
+
name: pathPart,
|
|
838
|
+
path: accumulatedPath,
|
|
839
|
+
children: new Map(),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
currentNode = currentNode.children.get(pathPart)!;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const absoluteDirectoryPath = path.join(
|
|
847
|
+
target.sourceDir,
|
|
848
|
+
relativeDirectory === '' ? '' : relativeDirectory,
|
|
849
|
+
);
|
|
850
|
+
const fileSymbolMap = directorySymbolMap.get(absoluteDirectoryPath);
|
|
851
|
+
if (fileSymbolMap) {
|
|
852
|
+
currentNode.fileCount = fileSymbolMap.size;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Builds markdown for the generated folder index.
|
|
858
|
+
*
|
|
859
|
+
* @param rootNode - Root folder index node.
|
|
860
|
+
* @returns Folder index markdown.
|
|
861
|
+
*/
|
|
862
|
+
function buildFolderIndexMarkdown(rootNode: FolderIndexNode): string {
|
|
863
|
+
const lines = [
|
|
864
|
+
'# Docs Index',
|
|
865
|
+
'',
|
|
866
|
+
'Auto-generated index of source folders (click to open folder README).',
|
|
867
|
+
'',
|
|
868
|
+
];
|
|
869
|
+
|
|
870
|
+
renderFolderIndexNode(rootNode, 0, lines);
|
|
871
|
+
return `${lines.join('\n')}\n`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Renders one folder index node and its children recursively.
|
|
876
|
+
*
|
|
877
|
+
* @param node - Current folder index node.
|
|
878
|
+
* @param level - Nesting level.
|
|
879
|
+
* @param lines - Output line buffer.
|
|
880
|
+
* @returns Nothing.
|
|
881
|
+
*/
|
|
882
|
+
function renderFolderIndexNode(
|
|
883
|
+
node: FolderIndexNode,
|
|
884
|
+
level: number,
|
|
885
|
+
lines: string[],
|
|
886
|
+
): void {
|
|
887
|
+
const indent = ' '.repeat(Math.max(0, level));
|
|
888
|
+
const label = node.path === 'src' && level === 0 ? 'src (root)' : node.name;
|
|
889
|
+
const countSuffix = node.fileCount
|
|
890
|
+
? ` — ${node.fileCount} file${node.fileCount > 1 ? 's' : ''}`
|
|
891
|
+
: '';
|
|
892
|
+
|
|
893
|
+
lines.push(`${indent}- [${label}](${node.path}/README.md)${countSuffix}`);
|
|
894
|
+
|
|
895
|
+
const childNames = [...node.children.keys()].sort();
|
|
896
|
+
for (const childName of childNames) {
|
|
897
|
+
renderFolderIndexNode(node.children.get(childName)!, level + 1, lines);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Extracts the leading file-level JSDoc block from raw source text.
|
|
903
|
+
*
|
|
904
|
+
* @param sourceFile - Source file to inspect.
|
|
905
|
+
* @returns Cleaned top-of-file description when present.
|
|
906
|
+
*/
|
|
907
|
+
function extractLeadingFileJsDocDescription(
|
|
908
|
+
sourceFile: SourceFile,
|
|
909
|
+
): string | undefined {
|
|
910
|
+
const text = sourceFile.getFullText();
|
|
911
|
+
const match = text.match(/^\s*(?:\uFEFF)?\/\*\*([\s\S]*?)\*\//);
|
|
912
|
+
if (!match) {
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const cleanedText = match[1]
|
|
917
|
+
.split(/\r?\n/)
|
|
918
|
+
.map((line) => line.replace(/^\s*\*\s?/, ''))
|
|
919
|
+
.join('\n')
|
|
920
|
+
.trim();
|
|
921
|
+
|
|
922
|
+
if (!cleanedText) {
|
|
923
|
+
return undefined;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (
|
|
927
|
+
cleanedText.split('\n').some((line) => line.trim().startsWith('@internal'))
|
|
928
|
+
) {
|
|
929
|
+
return undefined;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return cleanedText;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Renders one exported symbol when it belongs to a supported declaration kind
|
|
937
|
+
* and is not marked internal.
|
|
938
|
+
*
|
|
939
|
+
* @param symbol - Exported symbol.
|
|
940
|
+
* @param fallbackKind - Declaration kind used as a fallback label.
|
|
941
|
+
* @param filePath - Absolute source file path.
|
|
942
|
+
* @returns Rendered symbol or null when not supported.
|
|
943
|
+
*/
|
|
944
|
+
function renderSymbol(
|
|
945
|
+
symbol: MorphSymbol,
|
|
946
|
+
fallbackKind: string,
|
|
947
|
+
filePath: string,
|
|
948
|
+
): RenderedSymbol | null {
|
|
949
|
+
const declaration = symbol.getDeclarations()[0];
|
|
950
|
+
if (!declaration) {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const declarationKind = declaration.getKindName();
|
|
955
|
+
const supportedKinds =
|
|
956
|
+
/ClassDeclaration|FunctionDeclaration|InterfaceDeclaration|EnumDeclaration|TypeAliasDeclaration|VariableDeclaration/;
|
|
957
|
+
if (!supportedKinds.test(declarationKind)) {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const jsDocs = getJsDocs(declaration);
|
|
962
|
+
if (hasInternalTag(jsDocs)) {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const primaryJsDoc = jsDocs[0];
|
|
967
|
+
const fullDescription = primaryJsDoc?.getDescription().trim();
|
|
310
968
|
|
|
311
969
|
return {
|
|
312
|
-
kind: fallbackKind ||
|
|
313
|
-
name:
|
|
314
|
-
|
|
970
|
+
kind: fallbackKind || declarationKind,
|
|
971
|
+
name: symbol.getName(),
|
|
972
|
+
parent: undefined,
|
|
315
973
|
filePath,
|
|
316
|
-
signature,
|
|
974
|
+
signature: resolveCallSignature(declaration),
|
|
317
975
|
jsdoc: {
|
|
318
|
-
summary,
|
|
319
|
-
description:
|
|
320
|
-
params:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
976
|
+
summary: fullDescription?.split(/\r?\n\r?\n/)[0]?.trim(),
|
|
977
|
+
description: fullDescription,
|
|
978
|
+
params: extractParamDocs(primaryJsDoc?.getTags() || []),
|
|
979
|
+
returns: getTagCommentText(
|
|
980
|
+
primaryJsDoc
|
|
981
|
+
?.getTags()
|
|
982
|
+
.find(
|
|
983
|
+
(tag: JSDocTag) =>
|
|
984
|
+
tag.getTagName() === 'returns' || tag.getTagName() === 'return',
|
|
985
|
+
),
|
|
986
|
+
),
|
|
987
|
+
deprecated: getTagCommentText(
|
|
988
|
+
primaryJsDoc
|
|
989
|
+
?.getTags()
|
|
990
|
+
.find((tag: JSDocTag) => tag.getTagName() === 'deprecated'),
|
|
991
|
+
),
|
|
992
|
+
},
|
|
324
993
|
};
|
|
325
994
|
}
|
|
326
995
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
996
|
+
/**
|
|
997
|
+
* Renders a declaration-like node that may not have a directly usable symbol.
|
|
998
|
+
*
|
|
999
|
+
* @param declaration - Declaration-like node.
|
|
1000
|
+
* @param forcedName - Optional forced display name.
|
|
1001
|
+
* @param forcedKind - Optional forced kind label.
|
|
1002
|
+
* @param filePath - Absolute source file path.
|
|
1003
|
+
* @param parentName - Optional parent display name.
|
|
1004
|
+
* @returns Rendered symbol or null when not renderable.
|
|
1005
|
+
*/
|
|
1006
|
+
function renderDeclaration(
|
|
1007
|
+
declaration: any,
|
|
1008
|
+
forcedName?: string,
|
|
1009
|
+
forcedKind?: string,
|
|
1010
|
+
filePath?: string,
|
|
1011
|
+
parentName?: string,
|
|
1012
|
+
): RenderedSymbol | null {
|
|
1013
|
+
if (!declaration) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
330
1017
|
try {
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (jsDocs.some((j: any) => j.getTags().some((t: any) => t.getTagName() === 'internal'))) return null;
|
|
336
|
-
|
|
337
|
-
const primary = jsDocs[0];
|
|
338
|
-
const fullDesc = primary?.getDescription?.()?.trim();
|
|
339
|
-
const summary = fullDesc?.split(/\r?\n\r?\n/)[0]?.trim();
|
|
340
|
-
const tags: JSDocTag[] = primary?.getTags() || [];
|
|
341
|
-
const paramsTags = tags.filter(t => t.getTagName() === 'param');
|
|
342
|
-
const returnsTag = tags.find(t => t.getTagName() === 'returns' || t.getTagName() === 'return');
|
|
343
|
-
const deprecatedTag = tags.find(t => t.getTagName() === 'deprecated');
|
|
344
|
-
|
|
345
|
-
let signature: string | undefined;
|
|
346
|
-
try {
|
|
347
|
-
const type = decl.getType?.() || (decl.getSymbol && decl.getSymbol()?.getType?.());
|
|
348
|
-
const sig = type?.getCallSignatures?.()[0];
|
|
349
|
-
if (sig) {
|
|
350
|
-
const params = sig.getParameters().map((p: any) => {
|
|
351
|
-
const decls = p.getDeclarations();
|
|
352
|
-
const t = p.getTypeAtLocation(decls[0] || decl).getText();
|
|
353
|
-
return `${p.getName()}: ${t}`;
|
|
354
|
-
}).join(', ');
|
|
355
|
-
const ret = sig.getReturnType().getText();
|
|
356
|
-
signature = `(${params}) => ${ret}`;
|
|
357
|
-
}
|
|
358
|
-
} catch { /* ignore */ }
|
|
1018
|
+
const jsDocs = getJsDocs(declaration);
|
|
1019
|
+
if (jsDocs.length === 0 || hasInternalTag(jsDocs)) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
359
1022
|
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
const match = text.match(/@param\s+(\w+)/);
|
|
363
|
-
const name = match?.[1] || '';
|
|
364
|
-
const raw = t.getComment?.();
|
|
365
|
-
const doc = Array.isArray(raw) ? raw.map(r => (r as any).getText ? (r as any).getText() : String(r)).join(' ').trim() : (raw || undefined);
|
|
366
|
-
return { name, doc };
|
|
367
|
-
});
|
|
1023
|
+
const primaryJsDoc = jsDocs[0];
|
|
1024
|
+
const fullDescription = primaryJsDoc?.getDescription?.()?.trim();
|
|
368
1025
|
|
|
369
1026
|
return {
|
|
370
|
-
kind
|
|
371
|
-
|
|
1027
|
+
kind:
|
|
1028
|
+
forcedKind ||
|
|
1029
|
+
declaration.getKindName?.() ||
|
|
1030
|
+
declaration.getKind?.() ||
|
|
1031
|
+
'Declaration',
|
|
1032
|
+
name:
|
|
1033
|
+
forcedName ||
|
|
1034
|
+
declaration.getName?.() ||
|
|
1035
|
+
declaration.getSymbol?.()?.getName?.() ||
|
|
1036
|
+
(parentName ? `${parentName}.${forcedName}` : ''),
|
|
372
1037
|
parent: parentName || undefined,
|
|
373
1038
|
filePath: filePath || '',
|
|
374
|
-
signature,
|
|
1039
|
+
signature: resolveCallSignature(declaration),
|
|
375
1040
|
jsdoc: {
|
|
376
|
-
summary,
|
|
377
|
-
description:
|
|
378
|
-
params:
|
|
379
|
-
returns: (
|
|
380
|
-
|
|
381
|
-
|
|
1041
|
+
summary: fullDescription?.split(/\r?\n\r?\n/)[0]?.trim(),
|
|
1042
|
+
description: fullDescription,
|
|
1043
|
+
params: extractParamDocs(primaryJsDoc?.getTags() || []),
|
|
1044
|
+
returns: getTagCommentText(
|
|
1045
|
+
primaryJsDoc
|
|
1046
|
+
?.getTags()
|
|
1047
|
+
.find(
|
|
1048
|
+
(tag: JSDocTag) =>
|
|
1049
|
+
tag.getTagName() === 'returns' || tag.getTagName() === 'return',
|
|
1050
|
+
),
|
|
1051
|
+
),
|
|
1052
|
+
deprecated: getTagCommentText(
|
|
1053
|
+
primaryJsDoc
|
|
1054
|
+
?.getTags()
|
|
1055
|
+
.find((tag: JSDocTag) => tag.getTagName() === 'deprecated'),
|
|
1056
|
+
),
|
|
1057
|
+
},
|
|
382
1058
|
};
|
|
383
|
-
} catch
|
|
1059
|
+
} catch {
|
|
384
1060
|
return null;
|
|
385
1061
|
}
|
|
386
1062
|
}
|
|
387
1063
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (s.parent) return `${s.parent}.${base}`;
|
|
397
|
-
// try to extract from signature
|
|
398
|
-
if (s.signature) return `${base}${s.signature.split(')')[0]})`;
|
|
399
|
-
return base;
|
|
400
|
-
}
|
|
401
|
-
// strip trailing redundant '()' or 'function ' prefixes
|
|
402
|
-
name = name.replace(/^function\s+/, '').replace(/\(\)$/, '');
|
|
403
|
-
return name;
|
|
1064
|
+
/**
|
|
1065
|
+
* Returns JSDoc blocks from a declaration-like node.
|
|
1066
|
+
*
|
|
1067
|
+
* @param node - Declaration-like node.
|
|
1068
|
+
* @returns JSDoc array.
|
|
1069
|
+
*/
|
|
1070
|
+
function getJsDocs(node: any): any[] {
|
|
1071
|
+
return (node.getJsDocs?.() as any[]) || [];
|
|
404
1072
|
}
|
|
405
1073
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
1074
|
+
/**
|
|
1075
|
+
* Resolves the first JSDoc description from a declaration-like node.
|
|
1076
|
+
*
|
|
1077
|
+
* @param node - Declaration-like node.
|
|
1078
|
+
* @returns Description text when present.
|
|
1079
|
+
*/
|
|
1080
|
+
function getFirstJsDocDescription(node: {
|
|
1081
|
+
getJsDocs?: () => unknown[];
|
|
1082
|
+
}): string | undefined {
|
|
1083
|
+
const firstJsDoc = getJsDocs(node)[0];
|
|
1084
|
+
return firstJsDoc?.getDescription?.()?.trim() as string | undefined;
|
|
1085
|
+
}
|
|
412
1086
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
1087
|
+
/**
|
|
1088
|
+
* Checks whether any provided JSDoc block contains an internal tag.
|
|
1089
|
+
*
|
|
1090
|
+
* @param jsDocs - JSDoc array to inspect.
|
|
1091
|
+
* @returns True when the declaration is marked internal.
|
|
1092
|
+
*/
|
|
1093
|
+
function hasInternalTag(jsDocs: readonly any[]): boolean {
|
|
1094
|
+
return jsDocs.some((jsDoc) =>
|
|
1095
|
+
jsDoc
|
|
1096
|
+
.getTags()
|
|
1097
|
+
.some((tag: { getTagName(): string }) => tag.getTagName() === 'internal'),
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
416
1100
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
1101
|
+
/**
|
|
1102
|
+
* Resolves a call signature string from a declaration-like node.
|
|
1103
|
+
*
|
|
1104
|
+
* @param declaration - Declaration-like node.
|
|
1105
|
+
* @returns Signature string when the declaration is callable.
|
|
1106
|
+
*/
|
|
1107
|
+
function resolveCallSignature(declaration: any): string | undefined {
|
|
1108
|
+
try {
|
|
1109
|
+
const declarationType =
|
|
1110
|
+
declaration.getType?.() || declaration.getSymbol?.()?.getType?.();
|
|
1111
|
+
const callSignature = declarationType?.getCallSignatures?.()[0];
|
|
1112
|
+
if (!callSignature) {
|
|
1113
|
+
return undefined;
|
|
421
1114
|
}
|
|
422
1115
|
|
|
423
|
-
|
|
424
|
-
|
|
1116
|
+
const renderedParameters = callSignature
|
|
1117
|
+
.getParameters()
|
|
1118
|
+
.map((parameter: any) => {
|
|
1119
|
+
const parameterDeclarations = parameter.getDeclarations();
|
|
1120
|
+
const parameterType = normalizeRenderedTypeText(
|
|
1121
|
+
parameter
|
|
1122
|
+
.getTypeAtLocation(parameterDeclarations[0] || declaration)
|
|
1123
|
+
.getText(),
|
|
1124
|
+
);
|
|
1125
|
+
return `${parameter.getName()}: ${parameterType}`;
|
|
1126
|
+
})
|
|
1127
|
+
.join(', ');
|
|
1128
|
+
|
|
1129
|
+
const returnType = normalizeRenderedTypeText(
|
|
1130
|
+
callSignature.getReturnType().getText(),
|
|
1131
|
+
);
|
|
1132
|
+
return `(${renderedParameters}) => ${returnType}`;
|
|
1133
|
+
} catch {
|
|
1134
|
+
return undefined;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Rewrites absolute import paths from ts-morph type text into repo-relative paths.
|
|
1140
|
+
*
|
|
1141
|
+
* @param typeText - Raw type text returned by ts-morph.
|
|
1142
|
+
* @returns Normalized type text safe for generated docs.
|
|
1143
|
+
*/
|
|
1144
|
+
function normalizeRenderedTypeText(typeText: string): string {
|
|
1145
|
+
return typeText.replace(
|
|
1146
|
+
/import\((['"])([^'"]+)\1\)/g,
|
|
1147
|
+
(_match, quote: string, importPath: string) => {
|
|
1148
|
+
const normalizedImportPath = path.normalize(importPath);
|
|
1149
|
+
if (!path.isAbsolute(normalizedImportPath)) {
|
|
1150
|
+
return `import(${quote}${importPath}${quote})`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const relativeImportPath = path
|
|
1154
|
+
.relative(WORKSPACE_ROOT_DIR, normalizedImportPath)
|
|
1155
|
+
.replace(/\\/g, '/');
|
|
1156
|
+
|
|
1157
|
+
const portableImportPath = relativeImportPath.startsWith('..')
|
|
1158
|
+
? importPath.replace(/\\/g, '/')
|
|
1159
|
+
: relativeImportPath;
|
|
425
1160
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
1161
|
+
return `import(${quote}${portableImportPath}${quote})`;
|
|
1162
|
+
},
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Extracts rendered parameter docs from JSDoc tags.
|
|
1168
|
+
*
|
|
1169
|
+
* @param tags - JSDoc tags to inspect.
|
|
1170
|
+
* @returns Parameter docs when present.
|
|
1171
|
+
*/
|
|
1172
|
+
function extractParamDocs(
|
|
1173
|
+
tags: readonly JSDocTag[],
|
|
1174
|
+
): RenderedParameter[] | undefined {
|
|
1175
|
+
const parameterDocs = tags
|
|
1176
|
+
.filter((tag) => tag.getTagName() === 'param')
|
|
1177
|
+
.map((tag) => {
|
|
1178
|
+
const match = tag.getText().match(/@param\s+(\w+)/);
|
|
1179
|
+
return {
|
|
1180
|
+
name: match?.[1] || '',
|
|
1181
|
+
doc: getTagCommentText(tag),
|
|
1182
|
+
} satisfies RenderedParameter;
|
|
1183
|
+
})
|
|
1184
|
+
.filter((parameter) => Boolean(parameter.name));
|
|
1185
|
+
|
|
1186
|
+
return parameterDocs.length > 0 ? parameterDocs : undefined;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Resolves a tag comment into plain text.
|
|
1191
|
+
*
|
|
1192
|
+
* @param tag - JSDoc tag to inspect.
|
|
1193
|
+
* @returns Flattened comment string when present.
|
|
1194
|
+
*/
|
|
1195
|
+
function getTagCommentText(tag: JSDocTag | undefined): string | undefined {
|
|
1196
|
+
const rawComment = tag?.getComment();
|
|
1197
|
+
if (typeof rawComment === 'string') {
|
|
1198
|
+
return sanitizeTagCommentText(rawComment);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (Array.isArray(rawComment)) {
|
|
1202
|
+
return sanitizeTagCommentText(
|
|
1203
|
+
rawComment
|
|
1204
|
+
.map(
|
|
1205
|
+
(commentPart) =>
|
|
1206
|
+
(commentPart as { getText?: () => string }).getText?.() ||
|
|
1207
|
+
String(commentPart),
|
|
1208
|
+
)
|
|
1209
|
+
.join(' ')
|
|
1210
|
+
.trim(),
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return undefined;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Remove ts-morph JSDoc tag artifacts such as standalone trailing asterisks.
|
|
1219
|
+
*
|
|
1220
|
+
* @param commentText - Flattened tag comment text.
|
|
1221
|
+
* @returns Cleaned comment text or undefined when nothing meaningful remains.
|
|
1222
|
+
*/
|
|
1223
|
+
function sanitizeTagCommentText(
|
|
1224
|
+
commentText: string | undefined,
|
|
1225
|
+
): string | undefined {
|
|
1226
|
+
const normalizedComment = commentText
|
|
1227
|
+
?.replace(/\r\n/g, '\n')
|
|
1228
|
+
.replace(/\n\s*\*\s*$/g, '')
|
|
1229
|
+
.replace(/^\s*\*\s*$/g, '')
|
|
1230
|
+
.trim();
|
|
1231
|
+
|
|
1232
|
+
return normalizedComment ? normalizedComment : undefined;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Normalizes rendered symbol names so generated headings remain stable.
|
|
1237
|
+
*
|
|
1238
|
+
* @param renderedSymbol - Symbol being normalized.
|
|
1239
|
+
* @param filePath - Source file path used as fallback context.
|
|
1240
|
+
* @returns Stable display name.
|
|
1241
|
+
*/
|
|
1242
|
+
function normalizeName(
|
|
1243
|
+
renderedSymbol: RenderedSymbol,
|
|
1244
|
+
filePath: string,
|
|
1245
|
+
): string {
|
|
1246
|
+
let name = String(renderedSymbol.name || '');
|
|
1247
|
+
|
|
1248
|
+
if (!name || name === 'default' || name === FILE_SUMMARY_SYMBOL_NAME) {
|
|
1249
|
+
const fileBaseName = path.basename(filePath || '', '.ts');
|
|
1250
|
+
|
|
1251
|
+
if (renderedSymbol.kind === 'File' || name === FILE_SUMMARY_SYMBOL_NAME) {
|
|
1252
|
+
return fileBaseName;
|
|
442
1253
|
}
|
|
443
1254
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const byParent = new Map<string, RenderedSymbol[]>();
|
|
447
|
-
for (const s of symbols.filter(s => s.parent)) {
|
|
448
|
-
const arr = byParent.get(s.parent!) || [];
|
|
449
|
-
arr.push(s);
|
|
450
|
-
byParent.set(s.parent!, arr);
|
|
1255
|
+
if (renderedSymbol.parent) {
|
|
1256
|
+
return `${renderedSymbol.parent}.${fileBaseName}`;
|
|
451
1257
|
}
|
|
452
1258
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
lines.push(`### ${s.name}`);
|
|
456
|
-
if (s.signature) lines.push('', '`' + s.signature + '`');
|
|
457
|
-
if (s.jsdoc.description) lines.push('', s.jsdoc.description);
|
|
458
|
-
if (s.jsdoc.deprecated) lines.push('', `**Deprecated:** ${s.jsdoc.deprecated}`);
|
|
459
|
-
if (s.jsdoc.params?.length) {
|
|
460
|
-
lines.push('', 'Parameters:');
|
|
461
|
-
for (const p of s.jsdoc.params) lines.push(`- \`${p.name}\`${p.doc ? ' - ' + p.doc : ''}`);
|
|
462
|
-
}
|
|
463
|
-
if (s.jsdoc.returns) lines.push('', `Returns: ${s.jsdoc.returns}`);
|
|
464
|
-
lines.push('');
|
|
465
|
-
|
|
466
|
-
// if this top-level has grouped children (byParent keyed by this name), render them nested
|
|
467
|
-
const children = byParent.get(s.name);
|
|
468
|
-
if (children) {
|
|
469
|
-
children.sort((a, b) => a.name.localeCompare(b.name));
|
|
470
|
-
for (const c of children) {
|
|
471
|
-
lines.push(`#### ${c.name}`);
|
|
472
|
-
if (c.signature) lines.push('', '`' + c.signature + '`');
|
|
473
|
-
if (c.jsdoc.description) lines.push('', c.jsdoc.description);
|
|
474
|
-
if (c.jsdoc.deprecated) lines.push('', `**Deprecated:** ${c.jsdoc.deprecated}`);
|
|
475
|
-
if (c.jsdoc.params?.length) {
|
|
476
|
-
lines.push('', 'Parameters:');
|
|
477
|
-
for (const p of c.jsdoc.params) lines.push(`- \`${p.name}\`${p.doc ? ' - ' + p.doc : ''}`);
|
|
478
|
-
}
|
|
479
|
-
if (c.jsdoc.returns) lines.push('', `Returns: ${c.jsdoc.returns}`);
|
|
480
|
-
lines.push('');
|
|
481
|
-
}
|
|
482
|
-
}
|
|
1259
|
+
if (renderedSymbol.signature) {
|
|
1260
|
+
return `${fileBaseName}${renderedSymbol.signature.split(')')[0]})`;
|
|
483
1261
|
}
|
|
484
1262
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1263
|
+
return fileBaseName;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return name.replace(/^function\s+/, '').replace(/\(\)$/, '');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Builds the README markdown for one directory.
|
|
1271
|
+
*
|
|
1272
|
+
* @param relativeDirectory - Directory path relative to the target root.
|
|
1273
|
+
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
1274
|
+
* @param sourceDir - Absolute source root.
|
|
1275
|
+
* @returns Markdown README content.
|
|
1276
|
+
*/
|
|
1277
|
+
function buildDirectoryReadme(
|
|
1278
|
+
relativeDirectory: string,
|
|
1279
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
1280
|
+
sourceDir: string,
|
|
1281
|
+
): string {
|
|
1282
|
+
const title = (relativeDirectory || path.basename(sourceDir)).replace(
|
|
1283
|
+
/\\/g,
|
|
1284
|
+
'/',
|
|
1285
|
+
);
|
|
1286
|
+
const lines = [`# ${title}`, ''];
|
|
1287
|
+
|
|
1288
|
+
const sortedFiles = [...fileSymbolMap.keys()].toSorted(
|
|
1289
|
+
(leftFile, rightFile) => {
|
|
1290
|
+
const leftRank = rankFileForDirectoryReadme(leftFile, fileSymbolMap);
|
|
1291
|
+
const rightRank = rankFileForDirectoryReadme(rightFile, fileSymbolMap);
|
|
1292
|
+
|
|
1293
|
+
for (
|
|
1294
|
+
let index = 0;
|
|
1295
|
+
index < Math.max(leftRank.length, rightRank.length);
|
|
1296
|
+
index += 1
|
|
1297
|
+
) {
|
|
1298
|
+
const leftValue = leftRank[index] ?? 0;
|
|
1299
|
+
const rightValue = rightRank[index] ?? 0;
|
|
1300
|
+
if (leftValue !== rightValue) {
|
|
1301
|
+
return leftValue - rightValue;
|
|
498
1302
|
}
|
|
499
|
-
if (c.jsdoc.returns) lines.push('', `Returns: ${c.jsdoc.returns}`);
|
|
500
|
-
lines.push('');
|
|
501
1303
|
}
|
|
1304
|
+
|
|
1305
|
+
return leftFile.localeCompare(rightFile);
|
|
1306
|
+
},
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
for (const filePath of sortedFiles) {
|
|
1310
|
+
renderFileReadmeSection(lines, filePath, fileSymbolMap, sourceDir);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
return `${lines.join('\n').trim()}\n`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Renders the README section for a single file.
|
|
1318
|
+
*
|
|
1319
|
+
* @param lines - Output line buffer.
|
|
1320
|
+
* @param filePath - Absolute source file path.
|
|
1321
|
+
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
1322
|
+
* @param sourceDir - Absolute source root.
|
|
1323
|
+
* @returns Nothing.
|
|
1324
|
+
*/
|
|
1325
|
+
function renderFileReadmeSection(
|
|
1326
|
+
lines: string[],
|
|
1327
|
+
filePath: string,
|
|
1328
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
1329
|
+
sourceDir: string,
|
|
1330
|
+
): void {
|
|
1331
|
+
const relativeFilePath = path
|
|
1332
|
+
.relative(sourceDir, filePath)
|
|
1333
|
+
.replace(/\\/g, '/');
|
|
1334
|
+
lines.push(`## ${relativeFilePath}`, '');
|
|
1335
|
+
|
|
1336
|
+
const fileBaseName = path.basename(filePath, '.ts');
|
|
1337
|
+
const sortedSymbols = (fileSymbolMap.get(filePath) ?? []).toSorted(
|
|
1338
|
+
(left, right) => {
|
|
1339
|
+
const leftIsPrimary = !left.parent && left.name === fileBaseName;
|
|
1340
|
+
const rightIsPrimary = !right.parent && right.name === fileBaseName;
|
|
1341
|
+
if (leftIsPrimary !== rightIsPrimary) {
|
|
1342
|
+
return leftIsPrimary ? -1 : 1;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return (
|
|
1346
|
+
(left.parent || left.name).localeCompare(right.parent || right.name) ||
|
|
1347
|
+
left.name.localeCompare(right.name)
|
|
1348
|
+
);
|
|
1349
|
+
},
|
|
1350
|
+
);
|
|
1351
|
+
|
|
1352
|
+
const fileSummaryIndex = sortedSymbols.findIndex(
|
|
1353
|
+
(symbol) =>
|
|
1354
|
+
symbol.kind === 'File' && symbol.name === FILE_SUMMARY_SYMBOL_NAME,
|
|
1355
|
+
);
|
|
1356
|
+
if (fileSummaryIndex >= 0) {
|
|
1357
|
+
const [fileSummarySymbol] = sortedSymbols.splice(fileSummaryIndex, 1);
|
|
1358
|
+
if (fileSummarySymbol.jsdoc.description) {
|
|
1359
|
+
lines.push(fileSummarySymbol.jsdoc.description, '');
|
|
502
1360
|
}
|
|
503
1361
|
}
|
|
504
|
-
|
|
1362
|
+
|
|
1363
|
+
const topLevelSymbols = sortedSymbols.filter((symbol) => !symbol.parent);
|
|
1364
|
+
const symbolsByParent = groupSymbolsByParent(sortedSymbols);
|
|
1365
|
+
|
|
1366
|
+
for (const topLevelSymbol of topLevelSymbols) {
|
|
1367
|
+
renderSymbolBlock(lines, topLevelSymbol, 3);
|
|
1368
|
+
|
|
1369
|
+
const childSymbols = symbolsByParent.get(topLevelSymbol.name);
|
|
1370
|
+
if (!childSymbols) {
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
childSymbols.sort((left, right) => left.name.localeCompare(right.name));
|
|
1375
|
+
for (const childSymbol of childSymbols) {
|
|
1376
|
+
renderSymbolBlock(lines, childSymbol, 4);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
for (const [parentName, childSymbols] of symbolsByParent) {
|
|
1381
|
+
if (topLevelSymbols.some((symbol) => symbol.name === parentName)) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
lines.push(`### ${parentName}`, '');
|
|
1386
|
+
childSymbols.sort((left, right) => left.name.localeCompare(right.name));
|
|
1387
|
+
for (const childSymbol of childSymbols) {
|
|
1388
|
+
renderSymbolBlock(lines, childSymbol, 4);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Groups symbols that have parent names.
|
|
1395
|
+
*
|
|
1396
|
+
* @param renderedSymbols - Symbols for one file.
|
|
1397
|
+
* @returns Parent-keyed symbol map.
|
|
1398
|
+
*/
|
|
1399
|
+
function groupSymbolsByParent(
|
|
1400
|
+
renderedSymbols: readonly RenderedSymbol[],
|
|
1401
|
+
): Map<string, RenderedSymbol[]> {
|
|
1402
|
+
const symbolsByParent = new Map<string, RenderedSymbol[]>();
|
|
1403
|
+
|
|
1404
|
+
for (const renderedSymbol of renderedSymbols.filter(
|
|
1405
|
+
(symbol) => symbol.parent,
|
|
1406
|
+
)) {
|
|
1407
|
+
const childSymbols = symbolsByParent.get(renderedSymbol.parent!) ?? [];
|
|
1408
|
+
childSymbols.push(renderedSymbol);
|
|
1409
|
+
symbolsByParent.set(renderedSymbol.parent!, childSymbols);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return symbolsByParent;
|
|
505
1413
|
}
|
|
506
1414
|
|
|
507
|
-
|
|
1415
|
+
/**
|
|
1416
|
+
* Renders one symbol block into the README line buffer.
|
|
1417
|
+
*
|
|
1418
|
+
* @param lines - Output line buffer.
|
|
1419
|
+
* @param renderedSymbol - Symbol to render.
|
|
1420
|
+
* @param headingLevel - Markdown heading level.
|
|
1421
|
+
* @returns Nothing.
|
|
1422
|
+
*/
|
|
1423
|
+
function renderSymbolBlock(
|
|
1424
|
+
lines: string[],
|
|
1425
|
+
renderedSymbol: RenderedSymbol,
|
|
1426
|
+
headingLevel: number,
|
|
1427
|
+
): void {
|
|
1428
|
+
lines.push(`${'#'.repeat(headingLevel)} ${renderedSymbol.name}`);
|
|
1429
|
+
|
|
1430
|
+
if (renderedSymbol.signature) {
|
|
1431
|
+
lines.push('', `\`${renderedSymbol.signature}\``);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (renderedSymbol.jsdoc.description) {
|
|
1435
|
+
lines.push('', renderedSymbol.jsdoc.description);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (renderedSymbol.jsdoc.deprecated) {
|
|
1439
|
+
lines.push('', `**Deprecated:** ${renderedSymbol.jsdoc.deprecated}`);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (renderedSymbol.jsdoc.params?.length) {
|
|
1443
|
+
lines.push('', 'Parameters:');
|
|
1444
|
+
for (const parameter of renderedSymbol.jsdoc.params) {
|
|
1445
|
+
lines.push(
|
|
1446
|
+
`- \`${parameter.name}\`${parameter.doc ? ` - ${parameter.doc}` : ''}`,
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (renderedSymbol.jsdoc.returns) {
|
|
1452
|
+
lines.push('', `Returns: ${renderedSymbol.jsdoc.returns}`);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
lines.push('');
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Ranks files within a directory README so entrypoints appear before helpers.
|
|
1460
|
+
*
|
|
1461
|
+
* @param filePath - Absolute source file path.
|
|
1462
|
+
* @param fileSymbolMap - Symbols grouped by file within the directory.
|
|
1463
|
+
* @returns Sort rank tuple.
|
|
1464
|
+
*/
|
|
1465
|
+
function rankFileForDirectoryReadme(
|
|
1466
|
+
filePath: string,
|
|
1467
|
+
fileSymbolMap: Map<string, RenderedSymbol[]>,
|
|
1468
|
+
): number[] {
|
|
508
1469
|
const fileName = path.basename(filePath).toLowerCase();
|
|
509
|
-
const
|
|
1470
|
+
const renderedSymbols = fileSymbolMap.get(filePath) ?? [];
|
|
1471
|
+
|
|
1472
|
+
const hasFileSummary = renderedSymbols.some((renderedSymbol) => {
|
|
1473
|
+
if (
|
|
1474
|
+
renderedSymbol.kind !== 'File' ||
|
|
1475
|
+
renderedSymbol.name !== FILE_SUMMARY_SYMBOL_NAME
|
|
1476
|
+
) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
510
1479
|
|
|
511
|
-
|
|
512
|
-
if (s.kind !== 'File' || s.name !== '__file_summary__') return false;
|
|
513
|
-
return Boolean(s.jsdoc.description?.trim());
|
|
1480
|
+
return Boolean(renderedSymbol.jsdoc.description?.trim());
|
|
514
1481
|
});
|
|
515
1482
|
|
|
516
1483
|
const isIndexFile = fileName === 'index.ts';
|
|
517
|
-
const isTypesFile =
|
|
518
|
-
|
|
1484
|
+
const isTypesFile =
|
|
1485
|
+
fileName.includes('.types.') || fileName.endsWith('.types.ts');
|
|
1486
|
+
const isUtilityLike =
|
|
1487
|
+
/(\.utils\.|\.export-|\.import-|\.internal\.|\.private\.)/i.test(fileName);
|
|
519
1488
|
const isEntrypointLike = !isUtilityLike;
|
|
520
1489
|
|
|
521
|
-
// Rank tuple (lower is earlier):
|
|
522
|
-
// 1) index.ts first
|
|
523
|
-
// 2) files with file-level docs next (directory overview / entrypoints)
|
|
524
|
-
// 3) entrypoint-like (non-utils) before helpers
|
|
525
|
-
// 4) types early (after entrypoints)
|
|
526
|
-
// 5) shorter file names tend to be higher-level modules
|
|
527
1490
|
return [
|
|
528
1491
|
isIndexFile ? 0 : 1,
|
|
529
1492
|
hasFileSummary ? 0 : 1,
|
|
@@ -533,25 +1496,28 @@ function rankFileForDirectoryReadme(filePath: string, fileMap: Map<string, Rende
|
|
|
533
1496
|
];
|
|
534
1497
|
}
|
|
535
1498
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1499
|
+
/**
|
|
1500
|
+
* Writes a file only when its content changed.
|
|
1501
|
+
*
|
|
1502
|
+
* @param filePath - Output file path.
|
|
1503
|
+
* @param content - Desired file content.
|
|
1504
|
+
* @returns Nothing.
|
|
1505
|
+
*/
|
|
1506
|
+
async function writeFileIfChanged(
|
|
1507
|
+
filePath: string,
|
|
1508
|
+
content: string,
|
|
1509
|
+
): Promise<void> {
|
|
1510
|
+
if (await fs.pathExists(filePath)) {
|
|
1511
|
+
const previousContent = await fs.readFile(filePath, 'utf8');
|
|
1512
|
+
if (previousContent === content) {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
540
1515
|
}
|
|
541
|
-
await fs.writeFile(file, content, 'utf8');
|
|
542
|
-
}
|
|
543
1516
|
|
|
544
|
-
|
|
545
|
-
async function emitSourceReadme(file: string, content: string) {
|
|
546
|
-
// Always overwrite to keep docs in sync (educational repo preference: no banner / frictionless reading)
|
|
547
|
-
if (await fs.pathExists(file)) {
|
|
548
|
-
const prev = await fs.readFile(file, 'utf8');
|
|
549
|
-
if (prev === content) return;
|
|
550
|
-
}
|
|
551
|
-
await fs.writeFile(file, content, 'utf8');
|
|
1517
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
552
1518
|
}
|
|
553
1519
|
|
|
554
|
-
main().catch(
|
|
555
|
-
console.error(
|
|
1520
|
+
main().catch((error: unknown) => {
|
|
1521
|
+
console.error(error);
|
|
556
1522
|
process.exit(1);
|
|
557
1523
|
});
|