@ankhorage/paradox 0.1.3 → 0.1.4
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/CHANGELOG.md +6 -0
- package/README.md +8 -2
- package/dist/analyze/analyze.d.ts +3 -0
- package/dist/analyze/analyze.js +12 -0
- package/dist/analyze/badges.js +51 -0
- package/dist/analyze/modules.js +6 -0
- package/dist/analyze/sourceFunctions.d.ts +6 -0
- package/dist/analyze/sourceFunctions.js +64 -0
- package/dist/analyze/types.d.ts +6 -0
- package/dist/analyze/usage.d.ts +3 -0
- package/dist/analyze/usage.js +6 -0
- package/dist/model/buildModel.d.ts +9 -0
- package/dist/model/buildModel.js +24 -0
- package/dist/model/types.d.ts +6 -0
- package/dist/paths/policy.d.ts +12 -0
- package/dist/paths/policy.js +27 -2
- package/dist/render/renderers/html.js +233 -60
- package/dist/render/renderers/markdown.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @ankhorage/paradox
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Deterministic documentation generator for TypeScript packages.
|
|
9
9
|
|
|
@@ -142,6 +142,7 @@ graph TD
|
|
|
142
142
|
module_src_analyze_analyze_ts --> module_src_analyze_semantic_exports_ts
|
|
143
143
|
module_src_analyze_analyze_ts --> module_src_analyze_semantic_graphs_ts
|
|
144
144
|
module_src_analyze_analyze_ts --> module_src_analyze_sequenceScenarios_ts
|
|
145
|
+
module_src_analyze_analyze_ts --> module_src_analyze_sourceFunctions_ts
|
|
145
146
|
module_src_analyze_analyze_ts --> module_src_analyze_types_ts
|
|
146
147
|
module_src_analyze_analyze_ts --> module_src_analyze_usage_ts
|
|
147
148
|
module_src_analyze_analyze_ts --> module_src_config_types_ts
|
|
@@ -224,6 +225,11 @@ graph TD
|
|
|
224
225
|
module_src_analyze_sequenceScenarios_ts --> module_src_analyze_usage_ts
|
|
225
226
|
module_src_analyze_sequenceScenarios_ts --> module_src_analyze_utils_getParadoxComment_ts
|
|
226
227
|
module_src_analyze_sequenceScenarios_ts --> module_src_analyze_utils_parseParadoxComment_ts
|
|
228
|
+
module_src_analyze_sourceFunctions_ts["src/analyze/sourceFunctions.ts"]
|
|
229
|
+
package__ankhorage_paradox -.-> module_src_analyze_sourceFunctions_ts
|
|
230
|
+
module_src_analyze_sourceFunctions_ts --> module_src_analyze_types_ts
|
|
231
|
+
module_src_analyze_sourceFunctions_ts --> module_src_analyze_utils_getParadoxComment_ts
|
|
232
|
+
module_src_analyze_sourceFunctions_ts --> module_src_analyze_utils_parseParadoxComment_ts
|
|
227
233
|
module_src_analyze_types_ts["src/analyze/types.ts"]
|
|
228
234
|
package__ankhorage_paradox -.-> module_src_analyze_types_ts
|
|
229
235
|
module_src_analyze_usage_ts["src/analyze/usage.ts"]
|
|
@@ -316,7 +322,7 @@ graph TD
|
|
|
316
322
|
|
|
317
323
|
## Public API
|
|
318
324
|
|
|
319
|
-
###
|
|
325
|
+
### Config
|
|
320
326
|
|
|
321
327
|
<details>
|
|
322
328
|
<summary>defineParadoxConfig</summary>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ParadoxConfig } from '../config/types.js';
|
|
2
2
|
import type { AnalysisResult } from './types.js';
|
|
3
|
+
/***
|
|
4
|
+
* Analyzes a package and returns the complete documentation input model.
|
|
5
|
+
*/
|
|
3
6
|
export declare function analyze(config: ParadoxConfig, runtime: {
|
|
4
7
|
packageRoot: string;
|
|
5
8
|
}): Promise<AnalysisResult>;
|
package/dist/analyze/analyze.js
CHANGED
|
@@ -9,7 +9,11 @@ import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
|
|
|
9
9
|
import { collectTypeMembers, resolveTypeReference } from './semantic/exports.js';
|
|
10
10
|
import { collectCallGraph, collectComponentCompositionGraph, collectImportGraph, } from './semantic/graphs.js';
|
|
11
11
|
import { analyzeSequenceScenarios } from './sequenceScenarios.js';
|
|
12
|
+
import { analyzeSourceFunctions } from './sourceFunctions.js';
|
|
12
13
|
import { createUsageFromPackageJson } from './usage.js';
|
|
14
|
+
/***
|
|
15
|
+
* Analyzes a package and returns the complete documentation input model.
|
|
16
|
+
*/
|
|
13
17
|
export async function analyze(config, runtime) {
|
|
14
18
|
const root = runtime.packageRoot;
|
|
15
19
|
const pkg = await readPackageJson(root);
|
|
@@ -21,6 +25,7 @@ export async function analyze(config, runtime) {
|
|
|
21
25
|
const { config: configMetadata, exports } = analyzeExports(project, { root, entrypoints });
|
|
22
26
|
const components = analyzeComponents(exports, { program });
|
|
23
27
|
const modules = analyzeModules(project, { root, entrypoints });
|
|
28
|
+
const sourceFunctions = analyzeSourceFunctions(project, root);
|
|
24
29
|
const sequenceScenarios = analyzeSequenceScenarios({ project, root, pkg, exports });
|
|
25
30
|
const configExport = configMetadata
|
|
26
31
|
? (exports.find((entry) => entry.name === configMetadata.exportName) ?? null)
|
|
@@ -49,6 +54,7 @@ export async function analyze(config, runtime) {
|
|
|
49
54
|
description: config.docs?.description ?? pkg.description ?? null,
|
|
50
55
|
exports,
|
|
51
56
|
components,
|
|
57
|
+
sourceFunctions,
|
|
52
58
|
entrypoints: entrypoints.map((entrypoint) => entrypoint.replaceAll('\\', '/')).sort(),
|
|
53
59
|
modules,
|
|
54
60
|
badges,
|
|
@@ -64,6 +70,9 @@ export async function analyze(config, runtime) {
|
|
|
64
70
|
graphs,
|
|
65
71
|
};
|
|
66
72
|
}
|
|
73
|
+
/***
|
|
74
|
+
* Converts semantic type members into serializable analysis output.
|
|
75
|
+
*/
|
|
67
76
|
function mapTypeMembers(members) {
|
|
68
77
|
return members.map((member) => ({
|
|
69
78
|
name: member.name,
|
|
@@ -75,6 +84,9 @@ function mapTypeMembers(members) {
|
|
|
75
84
|
...(member.children ? { children: mapTypeMembers(member.children) } : {}),
|
|
76
85
|
}));
|
|
77
86
|
}
|
|
87
|
+
/***
|
|
88
|
+
* Reads package metadata from the analyzed package root.
|
|
89
|
+
*/
|
|
78
90
|
async function readPackageJson(root) {
|
|
79
91
|
const raw = await readFile(join(root, 'package.json'), 'utf-8');
|
|
80
92
|
return JSON.parse(raw);
|
package/dist/analyze/badges.js
CHANGED
|
@@ -132,6 +132,9 @@ export async function analyzeBadges(root, pkg) {
|
|
|
132
132
|
});
|
|
133
133
|
return sortBadges(badges);
|
|
134
134
|
}
|
|
135
|
+
/***
|
|
136
|
+
* Checks whether any candidate file exists under the repository root.
|
|
137
|
+
*/
|
|
135
138
|
async function hasAnyFile(root, paths) {
|
|
136
139
|
for (const relativePath of paths) {
|
|
137
140
|
if (await fileExists(join(root, relativePath))) {
|
|
@@ -140,18 +143,30 @@ async function hasAnyFile(root, paths) {
|
|
|
140
143
|
}
|
|
141
144
|
return false;
|
|
142
145
|
}
|
|
146
|
+
/***
|
|
147
|
+
* Reads tsconfig metadata and detects strict TypeScript mode.
|
|
148
|
+
*/
|
|
143
149
|
async function hasTypeScriptStrictMode(root) {
|
|
144
150
|
const tsconfig = await readJsonFile(join(root, 'tsconfig.json'));
|
|
145
151
|
return tsconfig?.compilerOptions?.strict === true;
|
|
146
152
|
}
|
|
153
|
+
/***
|
|
154
|
+
* Reads the line coverage percentage from a coverage summary file when present.
|
|
155
|
+
*/
|
|
147
156
|
async function readCoveragePercent(root) {
|
|
148
157
|
const coverage = await readJsonFile(join(root, 'coverage', 'coverage-summary.json'));
|
|
149
158
|
const value = coverage?.total?.lines?.pct;
|
|
150
159
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
151
160
|
}
|
|
161
|
+
/***
|
|
162
|
+
* Formats integer and fractional percentages for badge labels.
|
|
163
|
+
*/
|
|
152
164
|
function formatPercent(value) {
|
|
153
165
|
return Number.isInteger(value) ? `${value}` : value.toFixed(1);
|
|
154
166
|
}
|
|
167
|
+
/***
|
|
168
|
+
* Selects the coverage badge color from the coverage percentage.
|
|
169
|
+
*/
|
|
155
170
|
function getCoverageColor(value) {
|
|
156
171
|
if (value >= 90)
|
|
157
172
|
return '0a7f3f';
|
|
@@ -161,12 +176,21 @@ function getCoverageColor(value) {
|
|
|
161
176
|
return 'ca8a04';
|
|
162
177
|
return 'dc2626';
|
|
163
178
|
}
|
|
179
|
+
/***
|
|
180
|
+
* Checks whether a named package script is configured.
|
|
181
|
+
*/
|
|
164
182
|
function hasScript(pkg, name) {
|
|
165
183
|
return typeof pkg.scripts?.[name] === 'string' && pkg.scripts[name].length > 0;
|
|
166
184
|
}
|
|
185
|
+
/***
|
|
186
|
+
* Checks whether a package script contains a specific command fragment.
|
|
187
|
+
*/
|
|
167
188
|
function scriptContains(pkg, name, command) {
|
|
168
189
|
return pkg.scripts?.[name]?.includes(command) ?? false;
|
|
169
190
|
}
|
|
191
|
+
/***
|
|
192
|
+
* Detects whether package metadata or workflows indicate Bun support.
|
|
193
|
+
*/
|
|
170
194
|
function supportsBun(pkg, workflowFiles) {
|
|
171
195
|
if (pkg.packageManager?.startsWith('bun@') ?? false) {
|
|
172
196
|
return true;
|
|
@@ -178,6 +202,9 @@ function supportsBun(pkg, workflowFiles) {
|
|
|
178
202
|
workflow.includes('bun install') ||
|
|
179
203
|
/\bbun(?:x)?\b/.test(workflow));
|
|
180
204
|
}
|
|
205
|
+
/***
|
|
206
|
+
* Checks whether any workflow file invokes a named package script.
|
|
207
|
+
*/
|
|
181
208
|
function workflowRunsScript(workflowFiles, scriptName) {
|
|
182
209
|
const escaped = escapeRegExp(scriptName);
|
|
183
210
|
const patterns = [
|
|
@@ -188,9 +215,15 @@ function workflowRunsScript(workflowFiles, scriptName) {
|
|
|
188
215
|
];
|
|
189
216
|
return workflowFiles.some((workflow) => patterns.some((pattern) => pattern.test(workflow)));
|
|
190
217
|
}
|
|
218
|
+
/***
|
|
219
|
+
* Escapes regular expression syntax in a script name.
|
|
220
|
+
*/
|
|
191
221
|
function escapeRegExp(value) {
|
|
192
222
|
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
193
223
|
}
|
|
224
|
+
/***
|
|
225
|
+
* Reads all GitHub Actions workflow files from the repository.
|
|
226
|
+
*/
|
|
194
227
|
async function readWorkflowFiles(root) {
|
|
195
228
|
const workflowsPath = join(root, '.github', 'workflows');
|
|
196
229
|
const entries = await readDirectory(workflowsPath);
|
|
@@ -200,6 +233,9 @@ async function readWorkflowFiles(root) {
|
|
|
200
233
|
.map((entry) => readFile(join(workflowsPath, entry), 'utf-8')));
|
|
201
234
|
return contents;
|
|
202
235
|
}
|
|
236
|
+
/***
|
|
237
|
+
* Reads a directory and treats a missing path as an empty directory.
|
|
238
|
+
*/
|
|
203
239
|
async function readDirectory(path) {
|
|
204
240
|
try {
|
|
205
241
|
return await readdir(path);
|
|
@@ -211,9 +247,15 @@ async function readDirectory(path) {
|
|
|
211
247
|
throw new Error(`Unable to read workflow directory: ${path}`, { cause: error });
|
|
212
248
|
}
|
|
213
249
|
}
|
|
250
|
+
/***
|
|
251
|
+
* Checks whether an unknown error is a file-not-found filesystem error.
|
|
252
|
+
*/
|
|
214
253
|
function isNotFoundError(error) {
|
|
215
254
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
|
|
216
255
|
}
|
|
256
|
+
/***
|
|
257
|
+
* Reads and parses a JSON file, returning null when the file does not exist.
|
|
258
|
+
*/
|
|
217
259
|
async function readJsonFile(path) {
|
|
218
260
|
let content;
|
|
219
261
|
try {
|
|
@@ -232,6 +274,9 @@ async function readJsonFile(path) {
|
|
|
232
274
|
throw new Error(`Unable to parse JSON metadata file: ${path}`, { cause: error });
|
|
233
275
|
}
|
|
234
276
|
}
|
|
277
|
+
/***
|
|
278
|
+
* Checks whether a filesystem path exists.
|
|
279
|
+
*/
|
|
235
280
|
async function fileExists(path) {
|
|
236
281
|
try {
|
|
237
282
|
await access(path);
|
|
@@ -244,12 +289,18 @@ async function fileExists(path) {
|
|
|
244
289
|
throw error;
|
|
245
290
|
}
|
|
246
291
|
}
|
|
292
|
+
/***
|
|
293
|
+
* Sorts badges into the stable preferred display order.
|
|
294
|
+
*/
|
|
247
295
|
function sortBadges(badges) {
|
|
248
296
|
return [...badges].sort((left, right) => {
|
|
249
297
|
const order = getBadgeOrder(left.id) - getBadgeOrder(right.id);
|
|
250
298
|
return order !== 0 ? order : left.id.localeCompare(right.id);
|
|
251
299
|
});
|
|
252
300
|
}
|
|
301
|
+
/***
|
|
302
|
+
* Returns the configured display order index for a badge id.
|
|
303
|
+
*/
|
|
253
304
|
function getBadgeOrder(id) {
|
|
254
305
|
const index = BADGE_ORDER.indexOf(id);
|
|
255
306
|
return index === -1 ? BADGE_ORDER.length : index;
|
package/dist/analyze/modules.js
CHANGED
|
@@ -36,9 +36,15 @@ export function analyzeModules(project, options) {
|
|
|
36
36
|
})
|
|
37
37
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
38
38
|
}
|
|
39
|
+
/***
|
|
40
|
+
* Returns unique string values sorted for deterministic generated output.
|
|
41
|
+
*/
|
|
39
42
|
function uniqueSorted(values) {
|
|
40
43
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
41
44
|
}
|
|
45
|
+
/***
|
|
46
|
+
* Normalizes platform-specific path separators for generated documentation output.
|
|
47
|
+
*/
|
|
42
48
|
function toPosixPath(path) {
|
|
43
49
|
return path.replaceAll('\\', '/');
|
|
44
50
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Project } from 'ts-morph';
|
|
2
|
+
import type { AnalysisSourceFunction } from './types.js';
|
|
3
|
+
/***
|
|
4
|
+
* Collects callable declarations from analyzed source files for source-area documentation.
|
|
5
|
+
*/
|
|
6
|
+
export declare function analyzeSourceFunctions(project: Project, root: string): AnalysisSourceFunction[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { Node } from 'ts-morph';
|
|
3
|
+
import { getParadoxComment } from './utils/getParadoxComment.js';
|
|
4
|
+
import { parseParadoxComment } from './utils/parseParadoxComment.js';
|
|
5
|
+
/***
|
|
6
|
+
* Collects callable declarations from analyzed source files for source-area documentation.
|
|
7
|
+
*/
|
|
8
|
+
export function analyzeSourceFunctions(project, root) {
|
|
9
|
+
return project
|
|
10
|
+
.getSourceFiles()
|
|
11
|
+
.flatMap((sourceFile) => {
|
|
12
|
+
if (sourceFile.isDeclarationFile())
|
|
13
|
+
return [];
|
|
14
|
+
const filePath = toPosixPath(relative(root, sourceFile.getFilePath()));
|
|
15
|
+
if (!filePath.startsWith('src/'))
|
|
16
|
+
return [];
|
|
17
|
+
return sourceFile.getDescendants().flatMap((node) => {
|
|
18
|
+
if (Node.isFunctionDeclaration(node)) {
|
|
19
|
+
const name = node.getName();
|
|
20
|
+
if (name === undefined)
|
|
21
|
+
return [];
|
|
22
|
+
return [createSourceFunction(name, node, root)];
|
|
23
|
+
}
|
|
24
|
+
if (Node.isVariableDeclaration(node)) {
|
|
25
|
+
const initializer = node.getInitializer();
|
|
26
|
+
if (initializer === undefined ||
|
|
27
|
+
(!Node.isArrowFunction(initializer) && !Node.isFunctionExpression(initializer))) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return [createSourceFunction(node.getName(), node, root)];
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
});
|
|
34
|
+
})
|
|
35
|
+
.sort((left, right) => left.sourceLocation.filePath === right.sourceLocation.filePath
|
|
36
|
+
? left.sourceLocation.line - right.sourceLocation.line
|
|
37
|
+
: left.sourceLocation.filePath.localeCompare(right.sourceLocation.filePath));
|
|
38
|
+
}
|
|
39
|
+
/***
|
|
40
|
+
* Converts a callable declaration into source documentation metadata.
|
|
41
|
+
*/
|
|
42
|
+
function createSourceFunction(name, node, root) {
|
|
43
|
+
const sourceFile = node.getSourceFile();
|
|
44
|
+
const { column, line } = sourceFile.getLineAndColumnAtPos(node.getStart(false));
|
|
45
|
+
const rawComment = getParadoxComment(node);
|
|
46
|
+
const parsedComment = rawComment
|
|
47
|
+
? parseParadoxComment(rawComment)
|
|
48
|
+
: { description: null, isReadme: false };
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
description: parsedComment.description,
|
|
52
|
+
sourceLocation: {
|
|
53
|
+
filePath: toPosixPath(relative(root, sourceFile.getFilePath())),
|
|
54
|
+
line,
|
|
55
|
+
column,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/***
|
|
60
|
+
* Normalizes platform-specific path separators for generated documentation output.
|
|
61
|
+
*/
|
|
62
|
+
function toPosixPath(path) {
|
|
63
|
+
return path.replaceAll('\\', '/');
|
|
64
|
+
}
|
package/dist/analyze/types.d.ts
CHANGED
|
@@ -95,6 +95,11 @@ export interface AnalysisSequenceScenario {
|
|
|
95
95
|
description: string | null;
|
|
96
96
|
isReadme: boolean;
|
|
97
97
|
}
|
|
98
|
+
export interface AnalysisSourceFunction {
|
|
99
|
+
name: string;
|
|
100
|
+
description: string | null;
|
|
101
|
+
sourceLocation: AnalysisSourceLocation;
|
|
102
|
+
}
|
|
98
103
|
interface AnalysisTypeMember {
|
|
99
104
|
name: string;
|
|
100
105
|
type: string;
|
|
@@ -141,6 +146,7 @@ export interface AnalysisResult {
|
|
|
141
146
|
description: string | null;
|
|
142
147
|
exports: AnalysisExport[];
|
|
143
148
|
components: AnalysisComponent[];
|
|
149
|
+
sourceFunctions: AnalysisSourceFunction[];
|
|
144
150
|
entrypoints: string[];
|
|
145
151
|
modules: AnalysisModule[];
|
|
146
152
|
badges: AnalysisBadge[];
|
package/dist/analyze/usage.d.ts
CHANGED
|
@@ -10,4 +10,7 @@ export interface PackageJsonModel {
|
|
|
10
10
|
eslintConfig?: unknown;
|
|
11
11
|
prettier?: unknown;
|
|
12
12
|
}
|
|
13
|
+
/***
|
|
14
|
+
* Builds installation and executable usage commands from package metadata.
|
|
15
|
+
*/
|
|
13
16
|
export declare function createUsageFromPackageJson(pkg: PackageJsonModel): AnalysisUsage | null;
|
package/dist/analyze/usage.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/***
|
|
2
|
+
* Builds installation and executable usage commands from package metadata.
|
|
3
|
+
*/
|
|
1
4
|
export function createUsageFromPackageJson(pkg) {
|
|
2
5
|
if (pkg.bin == null)
|
|
3
6
|
return null;
|
|
@@ -35,6 +38,9 @@ export function createUsageFromPackageJson(pkg) {
|
|
|
35
38
|
})),
|
|
36
39
|
};
|
|
37
40
|
}
|
|
41
|
+
/***
|
|
42
|
+
* Extracts the unscoped package name from a package id.
|
|
43
|
+
*/
|
|
38
44
|
function getPackageBaseName(packageName) {
|
|
39
45
|
return packageName.split('/').pop() ?? packageName;
|
|
40
46
|
}
|
|
@@ -80,6 +80,15 @@ interface BuildModelInput {
|
|
|
80
80
|
description: string | null;
|
|
81
81
|
}[];
|
|
82
82
|
}[];
|
|
83
|
+
sourceFunctions: {
|
|
84
|
+
name: string;
|
|
85
|
+
description: string | null;
|
|
86
|
+
sourceLocation: {
|
|
87
|
+
filePath: string;
|
|
88
|
+
line: number;
|
|
89
|
+
column: number;
|
|
90
|
+
};
|
|
91
|
+
}[];
|
|
83
92
|
sequenceScenarios: {
|
|
84
93
|
kind: 'bin' | 'export';
|
|
85
94
|
name: string;
|
package/dist/model/buildModel.js
CHANGED
|
@@ -46,6 +46,15 @@ export function buildModel(analysis) {
|
|
|
46
46
|
.sort((left, right) => left.path.localeCompare(right.path)),
|
|
47
47
|
exports,
|
|
48
48
|
components: sortByName(analysis.components.map((component) => mapComponent(component, exportsByName.get(component.name)))),
|
|
49
|
+
sourceFunctions: analysis.sourceFunctions.map((sourceFunction) => ({
|
|
50
|
+
name: sourceFunction.name,
|
|
51
|
+
description: sourceFunction.description,
|
|
52
|
+
sourceLocation: {
|
|
53
|
+
filePath: sourceFunction.sourceLocation.filePath,
|
|
54
|
+
line: sourceFunction.sourceLocation.line,
|
|
55
|
+
column: sourceFunction.sourceLocation.column,
|
|
56
|
+
},
|
|
57
|
+
})),
|
|
49
58
|
sequenceScenarios: sortByName(analysis.sequenceScenarios.map((scenario) => ({
|
|
50
59
|
kind: scenario.kind,
|
|
51
60
|
name: scenario.name,
|
|
@@ -62,6 +71,9 @@ export function buildModel(analysis) {
|
|
|
62
71
|
},
|
|
63
72
|
};
|
|
64
73
|
}
|
|
74
|
+
/***
|
|
75
|
+
* Maps one analyzed export into the serializable documentation shape.
|
|
76
|
+
*/
|
|
65
77
|
function mapExport(item, exportNames) {
|
|
66
78
|
return {
|
|
67
79
|
name: item.name,
|
|
@@ -102,6 +114,9 @@ function mapExport(item, exportNames) {
|
|
|
102
114
|
}))),
|
|
103
115
|
};
|
|
104
116
|
}
|
|
117
|
+
/***
|
|
118
|
+
* Maps an analyzed component while preserving export metadata when available.
|
|
119
|
+
*/
|
|
105
120
|
function mapComponent(component, exportModel) {
|
|
106
121
|
return {
|
|
107
122
|
name: component.name,
|
|
@@ -124,6 +139,9 @@ function mapComponent(component, exportModel) {
|
|
|
124
139
|
}))),
|
|
125
140
|
};
|
|
126
141
|
}
|
|
142
|
+
/***
|
|
143
|
+
* Finds the conventional config factory export for a config type when present.
|
|
144
|
+
*/
|
|
127
145
|
function findConfigFactoryName(configExportName, exportNames) {
|
|
128
146
|
const prefix = configExportName.endsWith('Config')
|
|
129
147
|
? configExportName.slice(0, -'Config'.length)
|
|
@@ -131,10 +149,16 @@ function findConfigFactoryName(configExportName, exportNames) {
|
|
|
131
149
|
const expectedFactoryName = `define${prefix}Config`;
|
|
132
150
|
return exportNames.includes(expectedFactoryName) ? expectedFactoryName : null;
|
|
133
151
|
}
|
|
152
|
+
/***
|
|
153
|
+
* Derives the default config file name from a package id.
|
|
154
|
+
*/
|
|
134
155
|
function getDefaultConfigFileName(packageId) {
|
|
135
156
|
const packageBaseName = packageId.split('/').pop() ?? packageId;
|
|
136
157
|
return `${packageBaseName}.config.ts`;
|
|
137
158
|
}
|
|
159
|
+
/***
|
|
160
|
+
* Returns a copy of items sorted by their `name` property.
|
|
161
|
+
*/
|
|
138
162
|
function sortByName(items) {
|
|
139
163
|
return [...items].sort((a, b) => a.name.localeCompare(b.name));
|
|
140
164
|
}
|
package/dist/model/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface DocumentationModel {
|
|
|
12
12
|
modules: ModuleModel[];
|
|
13
13
|
exports: ExportModel[];
|
|
14
14
|
components: ComponentModel[];
|
|
15
|
+
sourceFunctions: SourceFunctionModel[];
|
|
15
16
|
sequenceScenarios: SequenceScenarioModel[];
|
|
16
17
|
graphs: GraphModel;
|
|
17
18
|
}
|
|
@@ -60,6 +61,11 @@ export interface ComponentModel {
|
|
|
60
61
|
exportPaths: string[];
|
|
61
62
|
props: PropModel[];
|
|
62
63
|
}
|
|
64
|
+
interface SourceFunctionModel {
|
|
65
|
+
name: string;
|
|
66
|
+
description: string | null;
|
|
67
|
+
sourceLocation: SourceLocationModel;
|
|
68
|
+
}
|
|
63
69
|
export interface SequenceScenarioModel {
|
|
64
70
|
kind: 'bin' | 'export';
|
|
65
71
|
name: string;
|
package/dist/paths/policy.d.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { ParadoxConfig } from '../config/types.js';
|
|
2
|
+
/***
|
|
3
|
+
* Searches upward from a start directory until it finds a supported Paradox config file.
|
|
4
|
+
*/
|
|
2
5
|
export declare function findParadoxConfigFile(startDir: string): Promise<string | null>;
|
|
6
|
+
/***
|
|
7
|
+
* Loads a Paradox config module and returns its default export or an empty config.
|
|
8
|
+
*/
|
|
3
9
|
export declare function loadParadoxConfig(configFilePath: string): Promise<ParadoxConfig>;
|
|
10
|
+
/***
|
|
11
|
+
* Resolves and validates the package root from config and config directory.
|
|
12
|
+
*/
|
|
4
13
|
export declare function resolvePackageRoot(config: ParadoxConfig, configDir: string): Promise<string>;
|
|
14
|
+
/***
|
|
15
|
+
* Resolves the configured output directory and enforces that it stays inside the package root.
|
|
16
|
+
*/
|
|
5
17
|
export declare function resolveOutputRoot(config: ParadoxConfig, packageRoot: string): {
|
|
6
18
|
outputDir: string;
|
|
7
19
|
outputRoot: string;
|
package/dist/paths/policy.js
CHANGED
|
@@ -7,6 +7,9 @@ const CONFIG_FILENAMES = [
|
|
|
7
7
|
'paradox.config.mjs',
|
|
8
8
|
'paradox.config.cjs',
|
|
9
9
|
];
|
|
10
|
+
/***
|
|
11
|
+
* Searches upward from a start directory until it finds a supported Paradox config file.
|
|
12
|
+
*/
|
|
10
13
|
export async function findParadoxConfigFile(startDir) {
|
|
11
14
|
let current = resolve(startDir);
|
|
12
15
|
let parent = dirname(current);
|
|
@@ -26,11 +29,17 @@ export async function findParadoxConfigFile(startDir) {
|
|
|
26
29
|
}
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
32
|
+
/***
|
|
33
|
+
* Loads a Paradox config module and returns its default export or an empty config.
|
|
34
|
+
*/
|
|
29
35
|
export async function loadParadoxConfig(configFilePath) {
|
|
30
36
|
const url = pathToFileURL(configFilePath).href;
|
|
31
37
|
const mod = (await import(url));
|
|
32
38
|
return mod.default ?? {};
|
|
33
39
|
}
|
|
40
|
+
/***
|
|
41
|
+
* Resolves and validates the package root from config and config directory.
|
|
42
|
+
*/
|
|
34
43
|
export async function resolvePackageRoot(config, configDir) {
|
|
35
44
|
const configuredRoot = config.package?.root;
|
|
36
45
|
const resolvedRoot = configuredRoot
|
|
@@ -41,6 +50,9 @@ export async function resolvePackageRoot(config, configDir) {
|
|
|
41
50
|
await assertPathExists(join(resolvedRoot, 'package.json'), `Unable to find package.json at resolved package root: ${resolvedRoot}`);
|
|
42
51
|
return resolvedRoot;
|
|
43
52
|
}
|
|
53
|
+
/***
|
|
54
|
+
* Resolves the configured output directory and enforces that it stays inside the package root.
|
|
55
|
+
*/
|
|
44
56
|
export function resolveOutputRoot(config, packageRoot) {
|
|
45
57
|
const outputDir = config.output?.dir ?? 'paradox';
|
|
46
58
|
validateOutputDir(outputDir);
|
|
@@ -48,6 +60,9 @@ export function resolveOutputRoot(config, packageRoot) {
|
|
|
48
60
|
assertWithinRoot(outputRoot, packageRoot, `Resolved output directory escapes package root: ${outputRoot}`);
|
|
49
61
|
return { outputDir, outputRoot };
|
|
50
62
|
}
|
|
63
|
+
/***
|
|
64
|
+
* Checks whether a filesystem path can be accessed.
|
|
65
|
+
*/
|
|
51
66
|
async function pathExists(path) {
|
|
52
67
|
try {
|
|
53
68
|
await access(path);
|
|
@@ -57,11 +72,17 @@ async function pathExists(path) {
|
|
|
57
72
|
return false;
|
|
58
73
|
}
|
|
59
74
|
}
|
|
75
|
+
/***
|
|
76
|
+
* Throws a supplied error message when a required path does not exist.
|
|
77
|
+
*/
|
|
60
78
|
async function assertPathExists(path, message) {
|
|
61
79
|
if (!(await pathExists(path))) {
|
|
62
80
|
throw new Error(message);
|
|
63
81
|
}
|
|
64
82
|
}
|
|
83
|
+
/***
|
|
84
|
+
* Validates an output directory string before resolving it against the package root.
|
|
85
|
+
*/
|
|
65
86
|
function validateOutputDir(outputDir) {
|
|
66
87
|
const trimmed = outputDir.trim();
|
|
67
88
|
if (trimmed.length === 0) {
|
|
@@ -74,18 +95,22 @@ function validateOutputDir(outputDir) {
|
|
|
74
95
|
if (isAbsolute(normalized)) {
|
|
75
96
|
throw new Error('Invalid output.dir: must be a relative path inside the package root (absolute paths are not allowed).');
|
|
76
97
|
}
|
|
77
|
-
// Reject explicit parent traversal to keep writes deterministic and scoped.
|
|
78
98
|
for (const segment of splitPathSegments(trimmed)) {
|
|
79
99
|
if (segment === '..') {
|
|
80
100
|
throw new Error('Invalid output.dir: must not contain ".." path segments.');
|
|
81
101
|
}
|
|
82
102
|
}
|
|
83
103
|
}
|
|
104
|
+
/***
|
|
105
|
+
* Splits a configured path into normalized non-empty path segments.
|
|
106
|
+
*/
|
|
84
107
|
function splitPathSegments(path) {
|
|
85
|
-
// Treat both separators as potential delimiters to stay robust across platforms/config styles.
|
|
86
108
|
const normalized = path.replaceAll('\\', '/');
|
|
87
109
|
return normalized.split('/').filter((segment) => segment.length > 0 && segment !== '.');
|
|
88
110
|
}
|
|
111
|
+
/***
|
|
112
|
+
* Throws when a resolved path is outside the expected package root.
|
|
113
|
+
*/
|
|
89
114
|
function assertWithinRoot(resolvedPath, root, message) {
|
|
90
115
|
const rel = relative(root, resolvedPath);
|
|
91
116
|
if (rel === '')
|
|
@@ -2,18 +2,8 @@
|
|
|
2
2
|
* Renders a deterministic static HTML documentation app.
|
|
3
3
|
*/
|
|
4
4
|
export function renderHtml({ diagrams, model, }) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
href: `#symbol-${toAnchorId(item.name)}`,
|
|
8
|
-
label: item.name,
|
|
9
|
-
meta: `${item.kind} • ${item.modulePath}`,
|
|
10
|
-
})),
|
|
11
|
-
...model.components.map((component) => ({
|
|
12
|
-
href: `#component-${toAnchorId(component.name)}`,
|
|
13
|
-
label: component.name,
|
|
14
|
-
meta: `component • ${component.modulePath}`,
|
|
15
|
-
})),
|
|
16
|
-
].sort((left, right) => left.label.localeCompare(right.label));
|
|
5
|
+
const sourceAreas = getSourceAreas(model);
|
|
6
|
+
const cliScenarios = getReadmeCliScenarios(model);
|
|
17
7
|
const exportsByModule = groupBy(model.exports, (item) => item.modulePath);
|
|
18
8
|
return {
|
|
19
9
|
indexHtml: `<!doctype html>
|
|
@@ -33,6 +23,7 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
33
23
|
body { margin: 0; }
|
|
34
24
|
a { color: #2459d3; text-decoration: none; }
|
|
35
25
|
a:hover { text-decoration: underline; }
|
|
26
|
+
button { font: inherit; }
|
|
36
27
|
code, pre { font-family: "SFMono-Regular", ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
37
28
|
.layout {
|
|
38
29
|
display: grid;
|
|
@@ -48,9 +39,7 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
48
39
|
max-height: 100vh;
|
|
49
40
|
overflow: auto;
|
|
50
41
|
}
|
|
51
|
-
.content {
|
|
52
|
-
padding: 2rem;
|
|
53
|
-
}
|
|
42
|
+
.content { padding: 2rem; }
|
|
54
43
|
.panel, .item {
|
|
55
44
|
background: #ffffff;
|
|
56
45
|
border: 1px solid #d8dfec;
|
|
@@ -80,7 +69,28 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
80
69
|
padding: 0;
|
|
81
70
|
margin: 0;
|
|
82
71
|
}
|
|
83
|
-
.nav-list li + li { margin-top: 0.
|
|
72
|
+
.nav-list li + li { margin-top: 0.5rem; }
|
|
73
|
+
.nav-button {
|
|
74
|
+
width: 100%;
|
|
75
|
+
display: block;
|
|
76
|
+
border: 0;
|
|
77
|
+
border-radius: 0.7rem;
|
|
78
|
+
background: transparent;
|
|
79
|
+
color: #24427a;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
padding: 0.55rem 0.7rem;
|
|
82
|
+
text-align: left;
|
|
83
|
+
}
|
|
84
|
+
.nav-button:hover, .nav-button[aria-current="page"] {
|
|
85
|
+
background: #e8eefb;
|
|
86
|
+
text-decoration: none;
|
|
87
|
+
}
|
|
88
|
+
.nav-button small {
|
|
89
|
+
display: block;
|
|
90
|
+
color: #5e6d8c;
|
|
91
|
+
margin-top: 0.15rem;
|
|
92
|
+
}
|
|
93
|
+
.view[hidden] { display: none; }
|
|
84
94
|
.muted { color: #5e6d8c; }
|
|
85
95
|
.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0; list-style: none; }
|
|
86
96
|
.chip {
|
|
@@ -115,61 +125,64 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
115
125
|
<p class="muted">Generated with Paradox</p>
|
|
116
126
|
<h1>${escapeHtml(model.packageName)}</h1>
|
|
117
127
|
<p>${escapeHtml(model.description ?? 'Deterministic package documentation.')}</p>
|
|
118
|
-
<input id="search" class="search" type="search" placeholder="Search
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
<input id="search" class="search" type="search" placeholder="Search current view" />
|
|
129
|
+
<nav aria-label="Documentation sections">
|
|
130
|
+
<ul class="nav-list">
|
|
131
|
+
<li>
|
|
132
|
+
<button class="nav-button" type="button" data-target="home" aria-current="page">
|
|
133
|
+
Home
|
|
134
|
+
<small>Overview, CLI, public API, diagrams</small>
|
|
135
|
+
</button>
|
|
136
|
+
</li>
|
|
137
|
+
${sourceAreas.map(renderSourceNavItem).join('')}
|
|
138
|
+
</ul>
|
|
139
|
+
</nav>
|
|
124
140
|
</aside>
|
|
125
141
|
<main class="content">
|
|
126
|
-
<section class="
|
|
127
|
-
|
|
128
|
-
<div class="summary">
|
|
129
|
-
<div><strong>${model.exports.length}</strong><span class="muted">public exports</span></div>
|
|
130
|
-
<div><strong>${model.components.length}</strong><span class="muted">components</span></div>
|
|
131
|
-
<div><strong>${model.modules.length}</strong><span class="muted">modules</span></div>
|
|
132
|
-
<div><strong>${model.entrypoints.length}</strong><span class="muted">entrypoints</span></div>
|
|
133
|
-
</div>
|
|
134
|
-
<h3>Entrypoints</h3>
|
|
135
|
-
<ul class="meta-list">
|
|
136
|
-
${model.entrypoints.map((entrypoint) => `<li><code>${escapeHtml(entrypoint)}</code></li>`).join('')}
|
|
137
|
-
</ul>
|
|
138
|
-
</section>
|
|
139
|
-
<section class="panel">
|
|
140
|
-
<h2>Modules</h2>
|
|
141
|
-
${model.modules.map(renderModuleCard).join('')}
|
|
142
|
-
</section>
|
|
143
|
-
<section class="panel">
|
|
144
|
-
<h2>Exports by module</h2>
|
|
145
|
-
${[...exportsByModule.entries()]
|
|
146
|
-
.map(([modulePath, exports]) => `
|
|
147
|
-
<section>
|
|
148
|
-
<h3>${escapeHtml(modulePath)}</h3>
|
|
149
|
-
${exports.map((item) => renderExportCard(item)).join('')}
|
|
150
|
-
</section>`)
|
|
151
|
-
.join('')}
|
|
152
|
-
</section>
|
|
153
|
-
<section class="panel">
|
|
154
|
-
<h2>Component registry</h2>
|
|
155
|
-
${model.components.length === 0 ? '<p class="empty">No components were detected.</p>' : model.components.map(renderComponentCard).join('')}
|
|
156
|
-
</section>
|
|
157
|
-
<section class="panel">
|
|
158
|
-
<h2>Diagrams</h2>
|
|
159
|
-
${diagrams.map(renderDiagramCard).join('')}
|
|
142
|
+
<section id="view-home" class="view" data-view="home">
|
|
143
|
+
${renderHomeView(model, diagrams, cliScenarios, exportsByModule)}
|
|
160
144
|
</section>
|
|
145
|
+
${sourceAreas.map(renderSourceAreaView).join('')}
|
|
161
146
|
</main>
|
|
162
147
|
</div>
|
|
163
148
|
<script>
|
|
164
149
|
const search = document.getElementById('search');
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
150
|
+
const navButtons = Array.from(document.querySelectorAll('[data-target]'));
|
|
151
|
+
const views = Array.from(document.querySelectorAll('[data-view]'));
|
|
152
|
+
|
|
153
|
+
function getActiveView() {
|
|
154
|
+
return document.querySelector('[data-view]:not([hidden])');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function applySearch() {
|
|
158
|
+
const value = search?.value.trim().toLowerCase() || '';
|
|
159
|
+
const activeView = getActiveView();
|
|
160
|
+
const items = Array.from(activeView?.querySelectorAll('[data-search]') || []);
|
|
161
|
+
|
|
168
162
|
for (const item of items) {
|
|
169
163
|
const haystack = (item.getAttribute('data-search') || '').toLowerCase();
|
|
170
164
|
item.style.display = value === '' || haystack.includes(value) ? '' : 'none';
|
|
171
165
|
}
|
|
172
|
-
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const button of navButtons) {
|
|
169
|
+
button.addEventListener('click', () => {
|
|
170
|
+
const target = button.getAttribute('data-target');
|
|
171
|
+
|
|
172
|
+
for (const view of views) {
|
|
173
|
+
view.hidden = view.getAttribute('data-view') !== target;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const item of navButtons) {
|
|
177
|
+
item.setAttribute('aria-current', item === button ? 'page' : 'false');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (search) search.value = '';
|
|
181
|
+
applySearch();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
search?.addEventListener('input', applySearch);
|
|
173
186
|
</script>
|
|
174
187
|
<script type="module">
|
|
175
188
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
@@ -180,6 +193,123 @@ export function renderHtml({ diagrams, model, }) {
|
|
|
180
193
|
`,
|
|
181
194
|
};
|
|
182
195
|
}
|
|
196
|
+
/***
|
|
197
|
+
* Renders the Home view that keeps public API and package-level information together.
|
|
198
|
+
*/
|
|
199
|
+
function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
|
|
200
|
+
return `
|
|
201
|
+
<section class="panel">
|
|
202
|
+
<h2>Package overview</h2>
|
|
203
|
+
<div class="summary">
|
|
204
|
+
<div><strong>${model.exports.length}</strong><span class="muted">public exports</span></div>
|
|
205
|
+
<div><strong>${model.components.length}</strong><span class="muted">components</span></div>
|
|
206
|
+
<div><strong>${model.modules.length}</strong><span class="muted">modules</span></div>
|
|
207
|
+
<div><strong>${model.entrypoints.length}</strong><span class="muted">entrypoints</span></div>
|
|
208
|
+
</div>
|
|
209
|
+
<h3>Entrypoints</h3>
|
|
210
|
+
<ul class="meta-list">
|
|
211
|
+
${model.entrypoints.map((entrypoint) => `<li><code>${escapeHtml(entrypoint)}</code></li>`).join('')}
|
|
212
|
+
</ul>
|
|
213
|
+
</section>
|
|
214
|
+
${cliScenarios.length > 0 ? renderCliPanel(model, diagrams, cliScenarios) : ''}
|
|
215
|
+
<section class="panel">
|
|
216
|
+
<h2>Modules</h2>
|
|
217
|
+
${model.modules.map(renderModuleCard).join('')}
|
|
218
|
+
</section>
|
|
219
|
+
<section class="panel">
|
|
220
|
+
<h2>Public API</h2>
|
|
221
|
+
${[...exportsByModule.entries()]
|
|
222
|
+
.map(([modulePath, exports]) => `
|
|
223
|
+
<section>
|
|
224
|
+
<h3>${escapeHtml(modulePath)}</h3>
|
|
225
|
+
${exports.map((item) => renderExportCard(item)).join('')}
|
|
226
|
+
</section>`)
|
|
227
|
+
.join('')}
|
|
228
|
+
</section>
|
|
229
|
+
<section class="panel">
|
|
230
|
+
<h2>Component registry</h2>
|
|
231
|
+
${model.components.length === 0 ? '<p class="empty">No components were detected.</p>' : model.components.map(renderComponentCard).join('')}
|
|
232
|
+
</section>
|
|
233
|
+
<section class="panel">
|
|
234
|
+
<h2>Diagrams</h2>
|
|
235
|
+
${diagrams.map(renderDiagramCard).join('')}
|
|
236
|
+
</section>`;
|
|
237
|
+
}
|
|
238
|
+
/***
|
|
239
|
+
* Renders the Home CLI chapter for detected bin scenarios.
|
|
240
|
+
*/
|
|
241
|
+
function renderCliPanel(model, diagrams, scenarios) {
|
|
242
|
+
return `<section class="panel" data-search="cli ${scenarios.map((scenario) => scenario.name).join(' ')}">
|
|
243
|
+
<h2>CLI</h2>
|
|
244
|
+
${scenarios
|
|
245
|
+
.map((scenario) => {
|
|
246
|
+
const command = model.usage?.commands.find((item) => item.name === scenario.name);
|
|
247
|
+
const diagram = findScenarioDiagram(diagrams, scenario);
|
|
248
|
+
return `<article class="item" data-search="${escapeAttribute([scenario.name, scenario.description ?? '', command?.command ?? ''].join(' '))}">
|
|
249
|
+
<h3>${escapeHtml(scenario.name)}</h3>
|
|
250
|
+
${scenario.description === null ? '' : `<p>${escapeHtml(scenario.description)}</p>`}
|
|
251
|
+
${command === undefined ? '' : `<pre>${escapeHtml(command.command)}</pre>`}
|
|
252
|
+
${diagram === undefined ? '' : renderDiagramCard(diagram)}
|
|
253
|
+
</article>`;
|
|
254
|
+
})
|
|
255
|
+
.join('')}
|
|
256
|
+
</section>`;
|
|
257
|
+
}
|
|
258
|
+
/***
|
|
259
|
+
* Renders one source file entry in the left navigation.
|
|
260
|
+
*/
|
|
261
|
+
function renderSourceNavItem(area) {
|
|
262
|
+
return `<li>
|
|
263
|
+
<button class="nav-button" type="button" data-target="${escapeAttribute(area.path)}">
|
|
264
|
+
${escapeHtml(area.path)}
|
|
265
|
+
<small>${area.functions.length} function${area.functions.length === 1 ? '' : 's'}</small>
|
|
266
|
+
</button>
|
|
267
|
+
</li>`;
|
|
268
|
+
}
|
|
269
|
+
/***
|
|
270
|
+
* Renders the right-hand source area view for a selected file.
|
|
271
|
+
*/
|
|
272
|
+
function renderSourceAreaView(area) {
|
|
273
|
+
return `<section id="view-${toAnchorId(area.path)}" class="view" data-view="${escapeAttribute(area.path)}" hidden>
|
|
274
|
+
<section class="panel">
|
|
275
|
+
<h2>${escapeHtml(area.path)}</h2>
|
|
276
|
+
${area.functions.map(renderSourceFunctionCard).join('')}
|
|
277
|
+
</section>
|
|
278
|
+
</section>`;
|
|
279
|
+
}
|
|
280
|
+
/***
|
|
281
|
+
* Renders one source function card in a source-area view.
|
|
282
|
+
*/
|
|
283
|
+
function renderSourceFunctionCard(item) {
|
|
284
|
+
return `<article class="item" data-search="${escapeAttribute([item.name, item.sourceLocation.filePath, item.description ?? ''].join(' '))}">
|
|
285
|
+
<h3>${escapeHtml(item.name)}</h3>
|
|
286
|
+
<p class="muted"><code>${escapeHtml(item.sourceLocation.filePath)}:${item.sourceLocation.line}:${item.sourceLocation.column}</code></p>
|
|
287
|
+
${item.description === null ? '<p class="empty">No description available.</p>' : `<p>${escapeHtml(item.description)}</p>`}
|
|
288
|
+
</article>`;
|
|
289
|
+
}
|
|
290
|
+
/***
|
|
291
|
+
* Groups analyzed source functions by their source file path.
|
|
292
|
+
*/
|
|
293
|
+
function getSourceAreas(model) {
|
|
294
|
+
return [...groupBy(model.sourceFunctions, (item) => item.sourceLocation.filePath).entries()]
|
|
295
|
+
.map(([path, functions]) => ({ path, functions }))
|
|
296
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
297
|
+
}
|
|
298
|
+
/***
|
|
299
|
+
* Selects bin scenarios that should be shown on the Home page.
|
|
300
|
+
*/
|
|
301
|
+
function getReadmeCliScenarios(model) {
|
|
302
|
+
return model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin' && scenario.isReadme);
|
|
303
|
+
}
|
|
304
|
+
/***
|
|
305
|
+
* Finds the generated Mermaid artifact for a sequence scenario.
|
|
306
|
+
*/
|
|
307
|
+
function findScenarioDiagram(diagrams, scenario) {
|
|
308
|
+
return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
|
|
309
|
+
}
|
|
310
|
+
/***
|
|
311
|
+
* Renders module metadata on the Home page.
|
|
312
|
+
*/
|
|
183
313
|
function renderModuleCard(module) {
|
|
184
314
|
return `<article class="item" data-search="${escapeAttribute([module.path, ...module.dependencies, ...module.exports].join(' '))}">
|
|
185
315
|
<h3>${escapeHtml(module.path)}</h3>
|
|
@@ -188,6 +318,9 @@ function renderModuleCard(module) {
|
|
|
188
318
|
<p><strong>Exports:</strong> ${renderInlineCodeList(module.exports)}</p>
|
|
189
319
|
</article>`;
|
|
190
320
|
}
|
|
321
|
+
/***
|
|
322
|
+
* Renders one public API export card on the Home page.
|
|
323
|
+
*/
|
|
191
324
|
function renderExportCard(item) {
|
|
192
325
|
return `<article class="item" id="symbol-${toAnchorId(item.name)}" data-search="${escapeAttribute([
|
|
193
326
|
item.name,
|
|
@@ -206,6 +339,9 @@ function renderExportCard(item) {
|
|
|
206
339
|
${item.members.length > 0 ? renderMemberTable(item) : ''}
|
|
207
340
|
</article>`;
|
|
208
341
|
}
|
|
342
|
+
/***
|
|
343
|
+
* Renders all call signatures for a public API export.
|
|
344
|
+
*/
|
|
209
345
|
function renderSignatureBlock(item) {
|
|
210
346
|
return item.signatures
|
|
211
347
|
.map((signature) => `<div>
|
|
@@ -230,6 +366,9 @@ function renderSignatureBlock(item) {
|
|
|
230
366
|
</div>`)
|
|
231
367
|
.join('');
|
|
232
368
|
}
|
|
369
|
+
/***
|
|
370
|
+
* Renders the members table for a type-like public API export.
|
|
371
|
+
*/
|
|
233
372
|
function renderMemberTable(item) {
|
|
234
373
|
return `<table>
|
|
235
374
|
<thead><tr><th>Member</th><th>Kind</th><th>Type</th><th>Required</th><th>Description</th></tr></thead>
|
|
@@ -246,6 +385,9 @@ function renderMemberTable(item) {
|
|
|
246
385
|
</tbody>
|
|
247
386
|
</table>`;
|
|
248
387
|
}
|
|
388
|
+
/***
|
|
389
|
+
* Renders one detected component card on the Home page.
|
|
390
|
+
*/
|
|
249
391
|
function renderComponentCard(component) {
|
|
250
392
|
return `<article class="item" id="component-${toAnchorId(component.name)}" data-search="${escapeAttribute([
|
|
251
393
|
component.name,
|
|
@@ -272,6 +414,9 @@ function renderComponentCard(component) {
|
|
|
272
414
|
</table>
|
|
273
415
|
</article>`;
|
|
274
416
|
}
|
|
417
|
+
/***
|
|
418
|
+
* Renders one Mermaid diagram card.
|
|
419
|
+
*/
|
|
275
420
|
function renderDiagramCard(diagram) {
|
|
276
421
|
return `<article class="item" data-search="${escapeAttribute(`${diagram.title} ${diagram.path}`)}">
|
|
277
422
|
<h3>${escapeHtml(diagram.title)}</h3>
|
|
@@ -283,17 +428,26 @@ function renderDiagramCard(diagram) {
|
|
|
283
428
|
</details>
|
|
284
429
|
</article>`;
|
|
285
430
|
}
|
|
431
|
+
/***
|
|
432
|
+
* Renders an inline comma-separated list of code values.
|
|
433
|
+
*/
|
|
286
434
|
function renderInlineCodeList(values) {
|
|
287
435
|
if (values.length === 0) {
|
|
288
436
|
return '<span class="empty">None</span>';
|
|
289
437
|
}
|
|
290
438
|
return values.map((value) => `<code>${escapeHtml(value)}</code>`).join(', ');
|
|
291
439
|
}
|
|
440
|
+
/***
|
|
441
|
+
* Renders a compact chip list for related symbols.
|
|
442
|
+
*/
|
|
292
443
|
function renderChipList(values) {
|
|
293
444
|
return `<ul class="chips">${values
|
|
294
445
|
.map((value) => `<li class="chip">${escapeHtml(value)}</li>`)
|
|
295
446
|
.join('')}</ul>`;
|
|
296
447
|
}
|
|
448
|
+
/***
|
|
449
|
+
* Groups items by a string key and returns deterministic key order.
|
|
450
|
+
*/
|
|
297
451
|
function groupBy(items, key) {
|
|
298
452
|
const groups = new Map();
|
|
299
453
|
for (const item of items) {
|
|
@@ -308,12 +462,28 @@ function groupBy(items, key) {
|
|
|
308
462
|
}
|
|
309
463
|
return new Map([...groups.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
310
464
|
}
|
|
465
|
+
/***
|
|
466
|
+
* Converts a label to a stable HTML anchor id fragment.
|
|
467
|
+
*/
|
|
311
468
|
function toAnchorId(value) {
|
|
312
469
|
return value
|
|
313
470
|
.toLowerCase()
|
|
314
471
|
.replace(/[^a-z0-9]+/g, '-')
|
|
315
472
|
.replace(/^-|-$/g, '');
|
|
316
473
|
}
|
|
474
|
+
/***
|
|
475
|
+
* Converts a scenario name to the generated Mermaid file stem.
|
|
476
|
+
*/
|
|
477
|
+
function toFileStem(value) {
|
|
478
|
+
return value
|
|
479
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
480
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
481
|
+
.replace(/^-+|-+$/g, '')
|
|
482
|
+
.toLowerCase();
|
|
483
|
+
}
|
|
484
|
+
/***
|
|
485
|
+
* Escapes user-controlled text for safe HTML rendering.
|
|
486
|
+
*/
|
|
317
487
|
function escapeHtml(value) {
|
|
318
488
|
return value
|
|
319
489
|
.replaceAll('&', '&')
|
|
@@ -322,6 +492,9 @@ function escapeHtml(value) {
|
|
|
322
492
|
.replaceAll('"', '"')
|
|
323
493
|
.replaceAll("'", ''');
|
|
324
494
|
}
|
|
495
|
+
/***
|
|
496
|
+
* Escapes text for use inside HTML attribute values.
|
|
497
|
+
*/
|
|
325
498
|
function escapeAttribute(value) {
|
|
326
499
|
return escapeHtml(value).replaceAll('\n', ' ');
|
|
327
500
|
}
|
|
@@ -255,7 +255,7 @@ function getReadmeItemName(item) {
|
|
|
255
255
|
}
|
|
256
256
|
function getReadmeCategory(modulePath, name) {
|
|
257
257
|
if (modulePath.includes('/config/'))
|
|
258
|
-
return '
|
|
258
|
+
return 'Config';
|
|
259
259
|
if (modulePath.includes('/primitives/'))
|
|
260
260
|
return 'Primitives';
|
|
261
261
|
if (modulePath.includes('/components/'))
|
|
@@ -364,7 +364,7 @@ function toFileStem(value) {
|
|
|
364
364
|
.toLowerCase();
|
|
365
365
|
}
|
|
366
366
|
const CATEGORY_ORDER = [
|
|
367
|
-
'
|
|
367
|
+
'Config',
|
|
368
368
|
'Primitives',
|
|
369
369
|
'Components',
|
|
370
370
|
'Patterns',
|