@ankhorage/paradox 0.0.10 → 0.1.1
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 +12 -0
- package/README.md +82 -19
- package/dist/analyze/analyze.d.ts +0 -3
- package/dist/analyze/analyze.js +6 -11
- package/dist/analyze/components.js +3 -0
- package/dist/analyze/exports.d.ts +1 -0
- package/dist/analyze/exports.js +16 -3
- package/dist/analyze/sequenceScenarios.d.ts +14 -0
- package/dist/analyze/sequenceScenarios.js +174 -0
- package/dist/analyze/types.d.ts +20 -0
- package/dist/analyze/utils/parseParadoxComment.d.ts +7 -0
- package/dist/analyze/utils/parseParadoxComment.js +61 -14
- package/dist/cli.js +9 -0
- package/dist/config/defineParadoxConfig.d.ts +2 -0
- package/dist/config/defineParadoxConfig.js +2 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/model/buildModel.d.ts +19 -0
- package/dist/model/buildModel.js +14 -0
- package/dist/model/types.d.ts +20 -0
- package/dist/render/renderers/diagrams.js +90 -28
- package/dist/render/renderers/markdown.js +258 -47
- package/package.json +1 -1
|
@@ -9,7 +9,13 @@ export function renderMarkdown({ badges, diagrams, model, outputDir, }) {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
function renderReadme(model, outputDir, badges, diagrams) {
|
|
12
|
-
const lines = [
|
|
12
|
+
const lines = [
|
|
13
|
+
'<!-- markdownlint-disable MD013 MD033 -->',
|
|
14
|
+
'<!-- This file is generated by Paradox. Do not edit manually. -->',
|
|
15
|
+
'',
|
|
16
|
+
`# ${model.packageName}`,
|
|
17
|
+
'',
|
|
18
|
+
];
|
|
13
19
|
if (badges.length > 0) {
|
|
14
20
|
lines.push(badges
|
|
15
21
|
.map((badge) => ``)
|
|
@@ -19,45 +25,100 @@ function renderReadme(model, outputDir, badges, diagrams) {
|
|
|
19
25
|
lines.push(model.description, '');
|
|
20
26
|
}
|
|
21
27
|
if (model.usage !== null) {
|
|
22
|
-
lines.push('##
|
|
28
|
+
lines.push('## Installation', '');
|
|
23
29
|
lines.push('```bash');
|
|
24
30
|
for (const command of model.usage.commands) {
|
|
25
31
|
lines.push(command.command);
|
|
26
32
|
}
|
|
27
33
|
lines.push('```', '');
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
lines
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
renderCliScenarios(lines, model, outputDir, diagrams);
|
|
36
|
+
renderDocumentationTags(lines);
|
|
37
|
+
if (model.config?.isReadme) {
|
|
38
|
+
renderConfiguration(lines, model);
|
|
39
|
+
}
|
|
40
|
+
renderGeneratedDocumentation(lines, outputDir, diagrams);
|
|
41
|
+
renderArchitecturePreview(lines, diagrams);
|
|
42
|
+
renderPathResolution(lines);
|
|
43
|
+
renderReadmeApi(lines, model);
|
|
44
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
45
|
+
}
|
|
46
|
+
function renderCliScenarios(lines, model, outputDir, diagrams) {
|
|
47
|
+
const scenarios = model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin' && scenario.isReadme);
|
|
48
|
+
if (scenarios.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
lines.push('## CLI', '');
|
|
51
|
+
for (const scenario of scenarios) {
|
|
52
|
+
lines.push(`### ${scenario.name}`, '');
|
|
53
|
+
if (scenario.description !== null) {
|
|
54
|
+
lines.push(scenario.description, '');
|
|
40
55
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
lines.push('');
|
|
44
|
-
lines.push(
|
|
45
|
-
lines.push('
|
|
46
|
-
lines.push(`} satisfies ${config.exportName};`);
|
|
47
|
-
lines.push('');
|
|
48
|
-
lines.push('export default config;');
|
|
56
|
+
const command = model.usage?.commands.find((item) => item.name === scenario.name);
|
|
57
|
+
if (command !== undefined) {
|
|
58
|
+
lines.push('```bash');
|
|
59
|
+
lines.push(command.command);
|
|
60
|
+
lines.push('```', '');
|
|
49
61
|
}
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
lines.push('
|
|
53
|
-
lines.push(
|
|
54
|
-
lines.push(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
lines.push('');
|
|
62
|
+
const diagram = findScenarioDiagram(diagrams, scenario);
|
|
63
|
+
if (diagram !== undefined) {
|
|
64
|
+
lines.push('<details>');
|
|
65
|
+
lines.push(`<summary>${scenario.name} sequence</summary>`, '');
|
|
66
|
+
lines.push(`Diagram: [${diagram.title}](./${outputDir}/${diagram.path})`, '');
|
|
67
|
+
lines.push('```mermaid');
|
|
68
|
+
lines.push(diagram.content.trimEnd());
|
|
69
|
+
lines.push('```', '');
|
|
70
|
+
lines.push('</details>', '');
|
|
59
71
|
}
|
|
60
72
|
}
|
|
73
|
+
}
|
|
74
|
+
function findScenarioDiagram(diagrams, scenario) {
|
|
75
|
+
return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
|
|
76
|
+
}
|
|
77
|
+
function renderDocumentationTags(lines) {
|
|
78
|
+
lines.push('## Documentation Tags', '');
|
|
79
|
+
for (const tag of DOCUMENTATION_TAGS) {
|
|
80
|
+
lines.push('<details>');
|
|
81
|
+
lines.push(`<summary>@${tag.name}</summary>`, '');
|
|
82
|
+
lines.push(tag.description, '');
|
|
83
|
+
lines.push('</details>', '');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function renderConfiguration(lines, model) {
|
|
87
|
+
const { config } = model;
|
|
88
|
+
if (config === null)
|
|
89
|
+
return;
|
|
90
|
+
lines.push('## Configuration', '');
|
|
91
|
+
lines.push(`Create a \`${config.configFile}\` file:`, '');
|
|
92
|
+
lines.push('```ts');
|
|
93
|
+
if (config.factoryName !== null) {
|
|
94
|
+
lines.push(`import { ${config.factoryName} } from '${model.packageId}';`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push(`export default ${config.factoryName}({`);
|
|
97
|
+
lines.push(' // ...');
|
|
98
|
+
lines.push('});');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
lines.push(`import type { ${config.exportName} } from '${model.packageId}';`);
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push('const config = {');
|
|
104
|
+
lines.push(' // ...');
|
|
105
|
+
lines.push(`} satisfies ${config.exportName};`);
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('export default config;');
|
|
108
|
+
}
|
|
109
|
+
lines.push('```', '');
|
|
110
|
+
if (config.members.length > 0) {
|
|
111
|
+
lines.push('<details>');
|
|
112
|
+
lines.push('<summary>Configuration options</summary>', '');
|
|
113
|
+
lines.push('| Field | Type | Required | Default | Description |');
|
|
114
|
+
lines.push('| --- | --- | --- | --- | --- |');
|
|
115
|
+
for (const configMember of flattenConfigMembers(config.members)) {
|
|
116
|
+
lines.push(`| ${escapeTableCell(configMember.path)} | \`${escapeTableCell(configMember.type)}\` | ${configMember.required ? 'yes' : 'no'} | ${renderDefault(configMember.defaultValue)} | ${escapeTableCell(configMember.description ?? '')} |`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('', '</details>', '');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function renderGeneratedDocumentation(lines, outputDir, diagrams) {
|
|
61
122
|
lines.push('## Generated documentation', '');
|
|
62
123
|
lines.push(`- [Interactive documentation app](./${outputDir}/index.html)`);
|
|
63
124
|
lines.push(`- [Public API reference](./${outputDir}/exports.md)`);
|
|
@@ -66,12 +127,19 @@ function renderReadme(model, outputDir, badges, diagrams) {
|
|
|
66
127
|
lines.push(`- [${diagram.title}](./${outputDir}/${diagram.path})`);
|
|
67
128
|
}
|
|
68
129
|
lines.push('');
|
|
130
|
+
}
|
|
131
|
+
function renderArchitecturePreview(lines, diagrams) {
|
|
69
132
|
lines.push('## Architecture preview', '');
|
|
70
133
|
if (diagrams.length > 0) {
|
|
134
|
+
lines.push('<details>');
|
|
135
|
+
lines.push('<summary>Architecture overview</summary>', '');
|
|
71
136
|
lines.push('```mermaid');
|
|
72
|
-
lines.push(diagrams[0]
|
|
137
|
+
lines.push(diagrams[0]?.content.trimEnd() ?? '');
|
|
73
138
|
lines.push('```', '');
|
|
139
|
+
lines.push('</details>', '');
|
|
74
140
|
}
|
|
141
|
+
}
|
|
142
|
+
function renderPathResolution(lines) {
|
|
75
143
|
lines.push('## Path resolution', '');
|
|
76
144
|
lines.push('- Config discovery: searches upward from `process.cwd()` for `paradox.config.ts/js/mjs/cjs` (required; no fallback).');
|
|
77
145
|
lines.push('- Package root: defaults to the directory containing `paradox.config.*`; `package.root` (when relative) resolves relative to that directory.');
|
|
@@ -79,22 +147,131 @@ function renderReadme(model, outputDir, badges, diagrams) {
|
|
|
79
147
|
lines.push('- Modes:');
|
|
80
148
|
lines.push(' - `safe`: writes generated artifacts only under the output directory');
|
|
81
149
|
lines.push(' - `write`: additionally updates `<packageRoot>/README.md`', '');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (item.
|
|
92
|
-
lines
|
|
150
|
+
}
|
|
151
|
+
function renderReadmeApi(lines, model) {
|
|
152
|
+
const groups = getReadmeGroups(model);
|
|
153
|
+
if (groups.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
lines.push('## Public API', '');
|
|
156
|
+
for (const group of groups) {
|
|
157
|
+
lines.push(`### ${group.title}`, '');
|
|
158
|
+
for (const item of group.items) {
|
|
159
|
+
if (item.kind === 'component') {
|
|
160
|
+
renderComponentAccordion(lines, item.component, item.exportEntry);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
renderExportAccordion(lines, item.exportEntry);
|
|
93
164
|
}
|
|
94
|
-
lines.push('');
|
|
95
165
|
}
|
|
96
166
|
}
|
|
97
|
-
|
|
167
|
+
}
|
|
168
|
+
function renderComponentAccordion(lines, component, exportEntry) {
|
|
169
|
+
lines.push('<details>');
|
|
170
|
+
lines.push(`<summary>${component.name}</summary>`, '');
|
|
171
|
+
renderSignature(lines, exportEntry);
|
|
172
|
+
if (component.description)
|
|
173
|
+
lines.push(component.description, '');
|
|
174
|
+
renderExamples(lines, component.examples);
|
|
175
|
+
if (exportEntry && exportEntry.relatedSymbols.length > 0) {
|
|
176
|
+
lines.push(`Related types: ${exportEntry.relatedSymbols.map((symbol) => `\`${symbol}\``).join(', ')}`, '');
|
|
177
|
+
}
|
|
178
|
+
if (component.props.length > 0) {
|
|
179
|
+
lines.push('<details>');
|
|
180
|
+
lines.push('<summary>Props</summary>', '');
|
|
181
|
+
lines.push('| Prop | Type | Required | Default | Description |');
|
|
182
|
+
lines.push('| --- | --- | --- | --- | --- |');
|
|
183
|
+
for (const prop of component.props) {
|
|
184
|
+
lines.push(`| ${escapeTableCell(prop.name)} | \`${escapeTableCell(prop.type)}\` | ${prop.required ? 'yes' : 'no'} | ${renderDefault(prop.defaultValue)} | ${escapeTableCell(prop.description ?? '')} |`);
|
|
185
|
+
}
|
|
186
|
+
lines.push('', '</details>', '');
|
|
187
|
+
}
|
|
188
|
+
lines.push('</details>', '');
|
|
189
|
+
}
|
|
190
|
+
function renderExportAccordion(lines, item) {
|
|
191
|
+
lines.push('<details>');
|
|
192
|
+
lines.push(`<summary>${item.name}</summary>`, '');
|
|
193
|
+
renderSignature(lines, item);
|
|
194
|
+
lines.push(item.description ?? `\`${item.kind}\` export.`, '');
|
|
195
|
+
renderExamples(lines, item.examples);
|
|
196
|
+
lines.push(`Module: \`${item.modulePath}\``);
|
|
197
|
+
lines.push(`Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``);
|
|
198
|
+
if (item.relatedSymbols.length > 0) {
|
|
199
|
+
lines.push(`Related symbols: ${item.relatedSymbols.map((symbol) => `\`${symbol}\``).join(', ')}`);
|
|
200
|
+
}
|
|
201
|
+
lines.push('', '</details>', '');
|
|
202
|
+
}
|
|
203
|
+
function renderSignature(lines, item) {
|
|
204
|
+
const signature = item?.signatures[0]?.label;
|
|
205
|
+
if (!signature)
|
|
206
|
+
return;
|
|
207
|
+
lines.push('```ts');
|
|
208
|
+
lines.push(`${item.name}${signature}`);
|
|
209
|
+
lines.push('```', '');
|
|
210
|
+
}
|
|
211
|
+
function renderExamples(lines, examples) {
|
|
212
|
+
for (const example of examples) {
|
|
213
|
+
if (example.title)
|
|
214
|
+
lines.push(`#### ${example.title}`, '');
|
|
215
|
+
lines.push(`\`\`\`${example.language ?? ''}`);
|
|
216
|
+
lines.push(example.code);
|
|
217
|
+
lines.push('```', '');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function getReadmeGroups(model) {
|
|
221
|
+
const exportsByName = new Map(model.exports.map((entry) => [entry.name, entry]));
|
|
222
|
+
const componentNames = new Set(model.components.map((component) => component.name));
|
|
223
|
+
const groups = new Map();
|
|
224
|
+
for (const component of model.components.filter((entry) => entry.isReadme)) {
|
|
225
|
+
addReadmeItem(groups, getReadmeCategory(component.modulePath, component.name), {
|
|
226
|
+
kind: 'component',
|
|
227
|
+
component,
|
|
228
|
+
exportEntry: exportsByName.get(component.name),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
for (const item of model.exports.filter((entry) => entry.isReadme)) {
|
|
232
|
+
if (componentNames.has(item.name))
|
|
233
|
+
continue;
|
|
234
|
+
addReadmeItem(groups, getReadmeCategory(item.modulePath, item.name), {
|
|
235
|
+
kind: 'export',
|
|
236
|
+
exportEntry: item,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return CATEGORY_ORDER.flatMap((title) => {
|
|
240
|
+
const items = groups.get(title);
|
|
241
|
+
if (!items || items.length === 0)
|
|
242
|
+
return [];
|
|
243
|
+
return [{ title, items: sortReadmeItems(items) }];
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function addReadmeItem(groups, title, item) {
|
|
247
|
+
const existing = groups.get(title) ?? [];
|
|
248
|
+
existing.push(item);
|
|
249
|
+
groups.set(title, existing);
|
|
250
|
+
}
|
|
251
|
+
function sortReadmeItems(items) {
|
|
252
|
+
return [...items].sort((left, right) => getReadmeItemName(left).localeCompare(getReadmeItemName(right)));
|
|
253
|
+
}
|
|
254
|
+
function getReadmeItemName(item) {
|
|
255
|
+
return item.kind === 'component' ? item.component.name : item.exportEntry.name;
|
|
256
|
+
}
|
|
257
|
+
function getReadmeCategory(modulePath, name) {
|
|
258
|
+
if (modulePath.includes('/config/'))
|
|
259
|
+
return 'Configuration';
|
|
260
|
+
if (modulePath.includes('/primitives/'))
|
|
261
|
+
return 'Primitives';
|
|
262
|
+
if (modulePath.includes('/components/'))
|
|
263
|
+
return 'Components';
|
|
264
|
+
if (modulePath.includes('/patterns/'))
|
|
265
|
+
return 'Patterns';
|
|
266
|
+
if (modulePath.includes('/layout/'))
|
|
267
|
+
return 'Layout';
|
|
268
|
+
if (modulePath.includes('/hooks/') || /^use[A-Z]/.test(name))
|
|
269
|
+
return 'Hooks';
|
|
270
|
+
if (modulePath.includes('/utils/'))
|
|
271
|
+
return 'Utilities';
|
|
272
|
+
if (modulePath.endsWith('types.ts') || modulePath.includes('/types/'))
|
|
273
|
+
return 'Types';
|
|
274
|
+
return 'Utilities';
|
|
98
275
|
}
|
|
99
276
|
function renderExports(model) {
|
|
100
277
|
const lines = ['# Public API', ''];
|
|
@@ -141,10 +318,10 @@ function renderComponents(model) {
|
|
|
141
318
|
lines.push(`Export paths: ${component.exportPaths.map((path) => `\`${path}\``).join(', ')}`, '');
|
|
142
319
|
}
|
|
143
320
|
if (component.props.length > 0) {
|
|
144
|
-
lines.push('| Prop | Type | Required | Description |');
|
|
145
|
-
lines.push('| --- | --- | --- | --- |');
|
|
321
|
+
lines.push('| Prop | Type | Required | Default | Description |');
|
|
322
|
+
lines.push('| --- | --- | --- | --- | --- |');
|
|
146
323
|
for (const prop of component.props) {
|
|
147
|
-
lines.push(`| ${escapeTableCell(prop.name)} | \`${escapeTableCell(prop.type)}\` | ${prop.required ? 'yes' : 'no'} | ${escapeTableCell(prop.description ?? '')} |`);
|
|
324
|
+
lines.push(`| ${escapeTableCell(prop.name)} | \`${escapeTableCell(prop.type)}\` | ${prop.required ? 'yes' : 'no'} | ${renderDefault(prop.defaultValue)} | ${escapeTableCell(prop.description ?? '')} |`);
|
|
148
325
|
}
|
|
149
326
|
lines.push('');
|
|
150
327
|
}
|
|
@@ -154,6 +331,9 @@ function renderComponents(model) {
|
|
|
154
331
|
function escapeTableCell(value) {
|
|
155
332
|
return value.replaceAll('|', '\\|');
|
|
156
333
|
}
|
|
334
|
+
function renderDefault(value) {
|
|
335
|
+
return value === undefined ? '—' : `\`${escapeTableCell(value)}\``;
|
|
336
|
+
}
|
|
157
337
|
function flattenConfigMembers(members, prefix = '') {
|
|
158
338
|
return members.flatMap((member) => {
|
|
159
339
|
const path = prefix ? `${prefix}.${member.name}` : member.name;
|
|
@@ -177,3 +357,34 @@ function badgeLabel(model, badgePath) {
|
|
|
177
357
|
const badge = model.badges.find((entry) => entry.id === id);
|
|
178
358
|
return badge ? `${badge.label}: ${badge.value}` : badgePath;
|
|
179
359
|
}
|
|
360
|
+
function toFileStem(value) {
|
|
361
|
+
return value
|
|
362
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
363
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
364
|
+
.replace(/^-+|-+$/g, '')
|
|
365
|
+
.toLowerCase();
|
|
366
|
+
}
|
|
367
|
+
const CATEGORY_ORDER = [
|
|
368
|
+
'Configuration',
|
|
369
|
+
'Primitives',
|
|
370
|
+
'Components',
|
|
371
|
+
'Patterns',
|
|
372
|
+
'Layout',
|
|
373
|
+
'Hooks',
|
|
374
|
+
'Utilities',
|
|
375
|
+
'Types',
|
|
376
|
+
];
|
|
377
|
+
const DOCUMENTATION_TAGS = [
|
|
378
|
+
{
|
|
379
|
+
name: 'readme',
|
|
380
|
+
description: 'Includes a documentation block or exported symbol in README output.',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: 'config',
|
|
384
|
+
description: 'Marks a type or interface as part of the Paradox configuration model. `@config` alone does not imply README inclusion; use `@config` plus `@readme` for README output.',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'example',
|
|
388
|
+
description: 'Adds a titled fenced code example to the generated documentation for a symbol.',
|
|
389
|
+
},
|
|
390
|
+
];
|