@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.
@@ -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 = [`# ${model.packageName}`, ''];
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) => `![${badgeLabel(model, badge.path)}](./${outputDir}/${badge.path})`)
@@ -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('## Usage', '');
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
- const { config } = model;
30
- if (config !== null) {
31
- lines.push('## Configuration', '');
32
- lines.push(`Create a \`${config.configFile}\` file:`, '');
33
- lines.push('```ts');
34
- if (config.factoryName !== null) {
35
- lines.push(`import { ${config.factoryName} } from '${model.packageId}';`);
36
- lines.push('');
37
- lines.push(`export default ${config.factoryName}({`);
38
- lines.push(' // ...');
39
- lines.push('});');
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
- else {
42
- lines.push(`import type { ${config.exportName} } from '${model.packageId}';`);
43
- lines.push('');
44
- lines.push('const config = {');
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
- lines.push('```', '');
51
- if (config.members.length > 0) {
52
- lines.push('### Configuration options', '');
53
- lines.push('| Field | Type | Required | Default | Description |');
54
- lines.push('| --- | --- | --- | --- | --- |');
55
- for (const configMember of flattenConfigMembers(config.members)) {
56
- lines.push(`| ${escapeTableCell(configMember.path)} | \`${escapeTableCell(configMember.type)}\` | ${configMember.required ? 'yes' : 'no'} | ${escapeTableCell(configMember.defaultValue ?? '')} | ${escapeTableCell(configMember.description ?? '')} |`);
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].content.trimEnd());
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
- if (model.exports.length > 0) {
83
- lines.push('## Public API', '');
84
- for (const item of model.exports) {
85
- lines.push(`### ${item.name}`, '');
86
- lines.push(item.description ?? `\`${item.kind}\` export.`, '');
87
- lines.push(`- Kind: \`${item.kind}\``);
88
- lines.push(`- Module: \`${item.modulePath}\``);
89
- lines.push(`- Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``);
90
- lines.push(`- Export paths: ${item.exportPaths.map((path) => `\`${path}\``).join(', ')}`);
91
- if (item.relatedSymbols.length > 0) {
92
- lines.push(`- Related symbols: ${item.relatedSymbols.map((symbol) => `\`${symbol}\``).join(', ')}`);
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
- return `${lines.join('\n').trimEnd()}\n`;
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
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/paradox",
3
- "version": "0.0.10",
3
+ "version": "0.1.1",
4
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {