@ankhorage/paradox 0.1.17 → 0.1.19

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.19
4
+
5
+ ### Patch Changes
6
+
7
+ - 5f6a40f: Generate the README CLI chapter from the canonical `src/cli/index.ts` `@readme` opt-in, keep executable commands under CLI instead of Installation, and declare the required Node.js types dependency.
8
+
9
+ ## 0.1.18
10
+
11
+ ### Patch Changes
12
+
13
+ - eaa56ad: Render README Configuration examples from the actual tagged Paradox config file instead of synthesized boilerplate.
14
+
3
15
  ## 0.1.17
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,18 +3,18 @@
3
3
 
4
4
  # @ankhorage/paradox
5
5
 
6
- ![license: MIT](./paradox/badges/license.svg) ![npm: v0.1.17](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
6
+ ![license: MIT](./paradox/badges/license.svg) ![npm: v0.1.19](./paradox/badges/npm.svg) ![runtime: bun](./paradox/badges/runtime.svg) ![typescript: strict](./paradox/badges/typescript.svg) ![eslint: checked](./paradox/badges/eslint.svg) ![prettier: checked](./paradox/badges/prettier.svg) ![build: checked](./paradox/badges/build.svg) ![tests: checked](./paradox/badges/tests.svg) ![docs: paradox](./paradox/badges/docs.svg)
7
7
 
8
8
  Deterministic documentation generator for TypeScript packages.
9
9
 
10
- ## Installation
10
+ ## CLI
11
+
12
+ Generates deterministic documentation for a package through the Paradox CLI.
11
13
 
12
14
  ```bash
13
15
  bunx @ankhorage/paradox
14
16
  ```
15
17
 
16
- ## CLI
17
-
18
18
  <details>
19
19
  <summary>paradox</summary>
20
20
 
@@ -24,10 +24,6 @@ The command discovers the nearest Paradox config, resolves the package and outpu
24
24
  analyzes the package, builds the documentation model, renders all documentation artifacts,
25
25
  and writes them to the configured output directory.
26
26
 
27
- ```bash
28
- bunx @ankhorage/paradox
29
- ```
30
-
31
27
  Diagram: [paradox sequence](./paradox/diagrams/sequences/paradox.mmd)
32
28
 
33
29
  ```mermaid
@@ -66,13 +62,26 @@ sequenceDiagram
66
62
 
67
63
  ## Configuration
68
64
 
69
- Create a `paradox.config.ts` file:
65
+ Canonical Paradox configuration for this package.
70
66
 
71
67
  ```ts
72
- import { defineParadoxConfig } from '@ankhorage/paradox';
68
+ import { defineParadoxConfig } from './src/config/defineParadoxConfig.js';
73
69
 
74
70
  export default defineParadoxConfig({
75
- // ...
71
+ mode: 'write',
72
+
73
+ docs: {
74
+ title: '@ankhorage/paradox',
75
+ description: 'Deterministic documentation generator for TypeScript packages.',
76
+ },
77
+
78
+ package: {
79
+ entrypoints: ['src/index.ts'],
80
+ },
81
+
82
+ output: {
83
+ dir: 'paradox',
84
+ },
76
85
  });
77
86
  ```
78
87
 
@@ -5,4 +5,5 @@ import type { AnalysisResult } from './types.js';
5
5
  */
6
6
  export declare function analyze(config: ParadoxConfig, runtime: {
7
7
  packageRoot: string;
8
+ configFilePath?: string;
8
9
  }): Promise<AnalysisResult>;
@@ -5,6 +5,8 @@ import { analyzeComponents } from './components.js';
5
5
  import { analyzeExports } from './exports.js';
6
6
  import { analyzeModules } from './modules.js';
7
7
  import { createProject } from './project.js';
8
+ import { analyzeReadmeCli } from './readmeCli.js';
9
+ import { analyzeReadmeConfig } from './readmeConfig.js';
8
10
  import { analyzeReadmeUsage } from './readmeUsage.js';
9
11
  import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
10
12
  import { collectTypeMembers, resolveTypeReference } from './semantic/exports.js';
@@ -25,6 +27,11 @@ export async function analyze(config, runtime) {
25
27
  const readmeUsageDescription = config.docs?.usage?.description ?? null;
26
28
  const usageEntryPoints = config.docs?.usage?.entrypoints ?? [];
27
29
  const readmeUsage = await analyzeReadmeUsage({ root, entrypoints: usageEntryPoints });
30
+ const readmeCli = await analyzeReadmeCli(root);
31
+ const readmeConfig = await analyzeReadmeConfig({
32
+ root,
33
+ configFilePath: runtime.configFilePath ?? null,
34
+ });
28
35
  const program = createTypeScriptProgram({ root, entrypoints, project });
29
36
  const { config: configMetadata, exports } = analyzeExports(project, { root, entrypoints });
30
37
  const components = analyzeComponents(exports, { program });
@@ -70,6 +77,8 @@ export async function analyze(config, runtime) {
70
77
  usage,
71
78
  readmeUsageDescription,
72
79
  readmeUsage,
80
+ readmeCli,
81
+ readmeConfig,
73
82
  config: configMetadata
74
83
  ? {
75
84
  exportName: configMetadata.exportName,
@@ -0,0 +1,9 @@
1
+ export interface AnalysisReadmeCli {
2
+ description: string | null;
3
+ sourcePath: string;
4
+ }
5
+ /***
6
+ * Collects README CLI metadata from the canonical Ankhorage CLI entrypoint when its leading
7
+ * Paradox comment opts into README output with @readme.
8
+ */
9
+ export declare function analyzeReadmeCli(root: string): Promise<AnalysisReadmeCli | null>;
@@ -0,0 +1,33 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { getLeadingParadoxComment } from './utils/getLeadingParadoxComment.js';
4
+ const CLI_INDEX_PATH = 'src/cli/index.ts';
5
+ /***
6
+ * Collects README CLI metadata from the canonical Ankhorage CLI entrypoint when its leading
7
+ * Paradox comment opts into README output with @readme.
8
+ */
9
+ export async function analyzeReadmeCli(root) {
10
+ const filePath = join(root, CLI_INDEX_PATH);
11
+ let source;
12
+ try {
13
+ source = await readFile(filePath, 'utf-8');
14
+ }
15
+ catch (error) {
16
+ if (isMissingPathError(error))
17
+ return null;
18
+ throw error;
19
+ }
20
+ const comment = getLeadingParadoxComment(source);
21
+ if (!comment?.parsed.isReadme)
22
+ return null;
23
+ return {
24
+ description: comment.parsed.description,
25
+ sourcePath: CLI_INDEX_PATH,
26
+ };
27
+ }
28
+ function isMissingPathError(error) {
29
+ return (error instanceof Error &&
30
+ 'code' in error &&
31
+ typeof error.code === 'string' &&
32
+ error.code === 'ENOENT');
33
+ }
@@ -0,0 +1,14 @@
1
+ export interface AnalysisReadmeConfig {
2
+ description: string | null;
3
+ language: string;
4
+ code: string;
5
+ sourcePath: string;
6
+ }
7
+ /***
8
+ * Collects a README configuration example from the actual Paradox config file when its
9
+ * leading Paradox comment is marked with both @config and @readme.
10
+ */
11
+ export declare function analyzeReadmeConfig(options: {
12
+ root: string;
13
+ configFilePath: string | null;
14
+ }): Promise<AnalysisReadmeConfig | null>;
@@ -0,0 +1,44 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname, relative } from 'node:path';
3
+ import { getLeadingParadoxComment } from './utils/getLeadingParadoxComment.js';
4
+ /***
5
+ * Collects a README configuration example from the actual Paradox config file when its
6
+ * leading Paradox comment is marked with both @config and @readme.
7
+ */
8
+ export async function analyzeReadmeConfig(options) {
9
+ if (options.configFilePath === null)
10
+ return null;
11
+ const source = await readFile(options.configFilePath, 'utf-8');
12
+ const comment = getLeadingParadoxComment(source);
13
+ if (comment === null)
14
+ return null;
15
+ if (!comment.parsed.isConfig || !comment.parsed.isReadme)
16
+ return null;
17
+ const sourcePath = toPosixPath(relative(options.root, options.configFilePath));
18
+ return {
19
+ description: comment.parsed.description,
20
+ language: getLanguage(sourcePath),
21
+ code: removeRange(source, comment.start, comment.end).trim(),
22
+ sourcePath,
23
+ };
24
+ }
25
+ function removeRange(source, start, end) {
26
+ const before = source.slice(0, start).trimEnd();
27
+ const after = source.slice(end).trimStart();
28
+ if (before.length === 0)
29
+ return after;
30
+ if (after.length === 0)
31
+ return before;
32
+ return `${before}\n\n${after}`;
33
+ }
34
+ function getLanguage(sourcePath) {
35
+ const extension = extname(sourcePath).toLowerCase();
36
+ if (extension === '.ts')
37
+ return 'ts';
38
+ if (extension === '.js' || extension === '.mjs' || extension === '.cjs')
39
+ return 'js';
40
+ return '';
41
+ }
42
+ function toPosixPath(path) {
43
+ return path.replaceAll('\\', '/');
44
+ }
@@ -86,6 +86,16 @@ interface AnalysisReadmeUsage {
86
86
  code: string;
87
87
  sourcePath: string;
88
88
  }
89
+ interface AnalysisReadmeCli {
90
+ description: string | null;
91
+ sourcePath: string;
92
+ }
93
+ interface AnalysisReadmeConfig {
94
+ description: string | null;
95
+ language: string;
96
+ code: string;
97
+ sourcePath: string;
98
+ }
89
99
  export interface AnalysisBadge {
90
100
  id: string;
91
101
  label: string;
@@ -165,6 +175,8 @@ export interface AnalysisResult {
165
175
  usage: AnalysisUsage | null;
166
176
  readmeUsageDescription: string | null;
167
177
  readmeUsage: AnalysisReadmeUsage[];
178
+ readmeCli: AnalysisReadmeCli | null;
179
+ readmeConfig: AnalysisReadmeConfig | null;
168
180
  config: {
169
181
  exportName: string;
170
182
  isReadme: boolean;
@@ -11,6 +11,6 @@ export interface PackageJsonModel {
11
11
  prettier?: unknown;
12
12
  }
13
13
  /***
14
- * Builds installation and executable usage commands from package metadata.
14
+ * Builds executable CLI commands from package metadata.
15
15
  */
16
16
  export declare function createUsageFromPackageJson(pkg: PackageJsonModel): AnalysisUsage | null;
@@ -1,5 +1,5 @@
1
1
  /***
2
- * Builds installation and executable usage commands from package metadata.
2
+ * Builds executable CLI commands from package metadata.
3
3
  */
4
4
  export function createUsageFromPackageJson(pkg) {
5
5
  if (pkg.bin == null)
@@ -0,0 +1,10 @@
1
+ import { type ParsedParadoxComment } from './parseParadoxComment.js';
2
+ export interface LeadingParadoxComment {
3
+ parsed: ParsedParadoxComment;
4
+ start: number;
5
+ end: number;
6
+ }
7
+ /***
8
+ * Parses a leading Paradox comment from a source file.
9
+ */
10
+ export declare function getLeadingParadoxComment(source: string): LeadingParadoxComment | null;
@@ -0,0 +1,16 @@
1
+ import { parseParadoxComment } from './parseParadoxComment.js';
2
+ /***
3
+ * Parses a leading Paradox comment from a source file.
4
+ */
5
+ export function getLeadingParadoxComment(source) {
6
+ const match = /^\s*(\/\*\*\*[\s\S]*?\*\/)/.exec(source);
7
+ const comment = match?.[1];
8
+ if (match === null || comment === undefined)
9
+ return null;
10
+ const start = match[0].indexOf(comment);
11
+ return {
12
+ parsed: parseParadoxComment(comment),
13
+ start,
14
+ end: start + comment.length,
15
+ };
16
+ }
@@ -1 +1,6 @@
1
+ /***
2
+ * Generates deterministic documentation for a package through the Paradox CLI.
3
+ *
4
+ * @readme
5
+ */
1
6
  export { default } from '../docsSurface.js';
package/dist/cli/index.js CHANGED
@@ -1 +1,6 @@
1
+ /***
2
+ * Generates deterministic documentation for a package through the Paradox CLI.
3
+ *
4
+ * @readme
5
+ */
1
6
  export { default } from '../docsSurface.js';
@@ -11,8 +11,6 @@ import { write } from '../write/write.js';
11
11
  * The command discovers the nearest Paradox config, resolves the package and output roots,
12
12
  * analyzes the package, builds the documentation model, renders all documentation artifacts,
13
13
  * and writes them to the configured output directory.
14
- *
15
- * @readme
16
14
  */
17
15
  async function main() {
18
16
  const cwd = process.cwd();
@@ -24,7 +22,7 @@ async function main() {
24
22
  const config = await loadParadoxConfig(configFilePath);
25
23
  const packageRoot = await resolvePackageRoot(config, configDir);
26
24
  const { outputDir, outputRoot } = resolveOutputRoot(config, packageRoot);
27
- const analysis = await analyze(config, { packageRoot });
25
+ const analysis = await analyze(config, { packageRoot, configFilePath });
28
26
  const model = buildModel(analysis);
29
27
  const result = render(model, { outputDir });
30
28
  await write(result, config, { packageRoot, outputRoot });
@@ -8,8 +8,8 @@ export declare const PARADOX_DOC_TAGS: readonly [{
8
8
  }, {
9
9
  readonly name: "config";
10
10
  readonly syntax: "@config";
11
- readonly 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.";
12
- readonly appliesTo: readonly ["interface", "type"];
11
+ readonly description: "Marks a configuration type, interface, or source block. Pair with @readme to include the schema or actual config source in README Configuration output.";
12
+ readonly appliesTo: readonly ["block", "interface", "type"];
13
13
  readonly repeatable: false;
14
14
  readonly handler: "markConfig";
15
15
  }, {
@@ -19,8 +19,8 @@ export const PARADOX_DOC_TAGS = [
19
19
  {
20
20
  name: 'config',
21
21
  syntax: '@config',
22
- 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.',
23
- appliesTo: ['interface', 'type'],
22
+ description: 'Marks a configuration type, interface, or source block. Pair with @readme to include the schema or actual config source in README Configuration output.',
23
+ appliesTo: ['block', 'interface', 'type'],
24
24
  repeatable: false,
25
25
  handler: 'markConfig',
26
26
  },
@@ -115,6 +115,16 @@ interface BuildModelInput {
115
115
  code: string;
116
116
  sourcePath: string;
117
117
  }[];
118
+ readmeCli: {
119
+ description: string | null;
120
+ sourcePath: string;
121
+ } | null;
122
+ readmeConfig: {
123
+ description: string | null;
124
+ language: string;
125
+ code: string;
126
+ sourcePath: string;
127
+ } | null;
118
128
  config: {
119
129
  exportName: string;
120
130
  isReadme: boolean;
@@ -34,14 +34,24 @@ export function buildModel(analysis) {
34
34
  sourcePath: usageEntry.sourcePath,
35
35
  }))
36
36
  .sort((left, right) => left.sourcePath.localeCompare(right.sourcePath)),
37
+ readmeCli: analysis.readmeCli !== null
38
+ ? {
39
+ description: analysis.readmeCli.description,
40
+ sourcePath: analysis.readmeCli.sourcePath,
41
+ }
42
+ : null,
43
+ readmeConfig: analysis.readmeConfig !== null
44
+ ? {
45
+ description: analysis.readmeConfig.description,
46
+ language: analysis.readmeConfig.language,
47
+ code: analysis.readmeConfig.code,
48
+ sourcePath: analysis.readmeConfig.sourcePath,
49
+ }
50
+ : null,
37
51
  config: analysis.config !== null
38
52
  ? {
39
53
  exportName: analysis.config.exportName,
40
54
  isReadme: analysis.config.isReadme,
41
- configFile: getDefaultConfigFileName(analysis.packageId),
42
- factoryName: findConfigFactoryName(analysis.config.exportName, [
43
- ...exportsByName.keys(),
44
- ]),
45
55
  members: analysis.config.members,
46
56
  }
47
57
  : null,
@@ -150,23 +160,6 @@ function mapComponent(component, exportModel) {
150
160
  }))),
151
161
  };
152
162
  }
153
- /***
154
- * Finds the conventional config factory export for a config type when present.
155
- */
156
- function findConfigFactoryName(configExportName, exportNames) {
157
- const prefix = configExportName.endsWith('Config')
158
- ? configExportName.slice(0, -'Config'.length)
159
- : configExportName;
160
- const expectedFactoryName = `define${prefix}Config`;
161
- return exportNames.includes(expectedFactoryName) ? expectedFactoryName : null;
162
- }
163
- /***
164
- * Derives the default config file name from a package id.
165
- */
166
- function getDefaultConfigFileName(packageId) {
167
- const packageBaseName = packageId.split('/').pop() ?? packageId;
168
- return `${packageBaseName}.config.ts`;
169
- }
170
163
  /***
171
164
  * Returns a copy of items sorted by their `name` property.
172
165
  */
@@ -9,6 +9,8 @@ export interface DocumentationModel {
9
9
  usage: UsageModel | null;
10
10
  readmeUsageDescription: string | null;
11
11
  readmeUsage: ReadmeUsageModel[];
12
+ readmeCli: ReadmeCliModel | null;
13
+ readmeConfig: ReadmeConfigModel | null;
12
14
  config: ConfigModel | null;
13
15
  entrypoints: string[];
14
16
  modules: ModuleModel[];
@@ -39,11 +41,19 @@ interface ReadmeUsageModel {
39
41
  code: string;
40
42
  sourcePath: string;
41
43
  }
44
+ interface ReadmeCliModel {
45
+ description: string | null;
46
+ sourcePath: string;
47
+ }
48
+ interface ReadmeConfigModel {
49
+ description: string | null;
50
+ language: string;
51
+ code: string;
52
+ sourcePath: string;
53
+ }
42
54
  interface ConfigModel {
43
55
  exportName: string;
44
56
  isReadme: boolean;
45
- configFile: string;
46
- factoryName: string | null;
47
57
  members: ConfigMemberModel[];
48
58
  }
49
59
  export interface ExportModel {
@@ -84,6 +94,12 @@ export interface SequenceScenarioModel {
84
94
  description: string | null;
85
95
  isReadme: boolean;
86
96
  }
97
+ export interface ModuleModel {
98
+ path: string;
99
+ isEntrypoint: boolean;
100
+ dependencies: string[];
101
+ exports: string[];
102
+ }
87
103
  interface ExampleModel {
88
104
  title: string | null;
89
105
  language: string | null;
@@ -116,15 +132,6 @@ interface MemberModel {
116
132
  inheritedFrom?: string;
117
133
  children?: MemberModel[];
118
134
  }
119
- interface StructuredRowModel {
120
- values: Record<string, string>;
121
- }
122
- export interface ModuleModel {
123
- path: string;
124
- isEntrypoint: boolean;
125
- dependencies: string[];
126
- exports: string[];
127
- }
128
135
  interface PropModel {
129
136
  name: string;
130
137
  type: string;
@@ -132,6 +139,9 @@ interface PropModel {
132
139
  defaultValue?: string;
133
140
  description: string | null;
134
141
  }
142
+ interface StructuredRowModel {
143
+ values: Record<string, string>;
144
+ }
135
145
  interface ConfigMemberModel {
136
146
  name: string;
137
147
  type: string;
@@ -211,7 +211,7 @@ function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
211
211
  ${model.entrypoints.map((entrypoint) => `<li><code>${escapeHtml(entrypoint)}</code></li>`).join('')}
212
212
  </ul>
213
213
  </section>
214
- ${cliScenarios.length > 0 ? renderCliPanel(model, diagrams, cliScenarios) : ''}
214
+ ${model.readmeCli !== null ? renderCliPanel(model, diagrams, cliScenarios) : ''}
215
215
  <section class="panel">
216
216
  <h2>Modules</h2>
217
217
  ${model.modules.map(renderModuleCard).join('')}
@@ -239,22 +239,33 @@ function renderHomeView(model, diagrams, cliScenarios, exportsByModule) {
239
239
  * Renders the Home CLI chapter for detected bin scenarios.
240
240
  */
241
241
  function renderCliPanel(model, diagrams, scenarios) {
242
- return `<section class="panel" data-search="cli ${scenarios.map((scenario) => scenario.name).join(' ')}">
242
+ const commands = model.usage?.commands ?? [];
243
+ const searchText = [
244
+ 'cli',
245
+ model.readmeCli?.description ?? '',
246
+ ...commands.map((command) => command.command),
247
+ ...scenarios.map((scenario) => scenario.name),
248
+ ].join(' ');
249
+ return `<section class="panel" data-search="${escapeAttribute(searchText)}">
243
250
  <h2>CLI</h2>
251
+ ${model.readmeCli?.description === null || model.readmeCli?.description === undefined ? '' : `<p>${escapeHtml(model.readmeCli.description)}</p>`}
252
+ ${commands.length === 0 ? '' : `<pre>${escapeHtml(commands.map((command) => command.command).join('\n'))}</pre>`}
244
253
  ${scenarios
245
254
  .map((scenario) => {
246
- const command = model.usage?.commands.find((item) => item.name === scenario.name);
247
255
  const diagram = findScenarioDiagram(diagrams, scenario);
248
- return `<article class="item" data-search="${escapeAttribute([scenario.name, scenario.description ?? '', command?.command ?? ''].join(' '))}">
256
+ if (scenario.description === null && diagram === undefined)
257
+ return '';
258
+ return `<article class="item" data-search="${escapeAttribute([scenario.name, scenario.description ?? ''].join(' '))}">
249
259
  <h3>${escapeHtml(scenario.name)}</h3>
250
260
  ${scenario.description === null ? '' : `<p>${escapeHtml(scenario.description)}</p>`}
251
- ${command === undefined ? '' : `<pre>${escapeHtml(command.command)}</pre>`}
252
261
  ${diagram === undefined ? '' : renderDiagramCard(diagram)}
253
262
  </article>`;
254
263
  })
255
264
  .join('')}
256
265
  </section>`;
257
266
  }
267
+ /***
268
+ * Renders one source file entry in the left navigation.
258
269
  /***
259
270
  * Renders one source file entry in the left navigation.
260
271
  */
@@ -299,8 +310,12 @@ function getSourceAreas(model) {
299
310
  * Selects bin scenarios that should be shown on the Home page.
300
311
  */
301
312
  function getReadmeCliScenarios(model) {
302
- return model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin' && scenario.isReadme);
313
+ if (model.readmeCli === null)
314
+ return [];
315
+ return model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin');
303
316
  }
317
+ /***
318
+ * Finds the generated Mermaid artifact for a sequence scenario.
304
319
  /***
305
320
  * Finds the generated Mermaid artifact for a sequence scenario.
306
321
  */
@@ -24,15 +24,8 @@ function renderReadme(model, outputDir, badges, diagrams) {
24
24
  if (model.description)
25
25
  lines.push(model.description, '');
26
26
  renderReadmeUsage(lines, model.readmeUsageDescription, model.readmeUsage);
27
- if (model.usage !== null) {
28
- lines.push('## Installation', '', '```bash');
29
- for (const command of model.usage.commands)
30
- lines.push(command.command);
31
- lines.push('```', '');
32
- }
33
- renderCliScenarios(lines, model, outputDir, diagrams);
34
- if (model.config?.isReadme)
35
- renderConfiguration(lines, model);
27
+ renderReadmeCli(lines, model, outputDir, diagrams);
28
+ renderConfiguration(lines, model);
36
29
  renderGeneratedDocumentation(lines, outputDir, diagrams);
37
30
  renderReadmeApi(lines, model);
38
31
  return `${lines.join('\n').trimEnd()}\n`;
@@ -58,23 +51,27 @@ function renderReadmeUsage(lines, description, entries) {
58
51
  lines.push('```', '');
59
52
  }
60
53
  }
61
- function renderCliScenarios(lines, model, outputDir, diagrams) {
62
- const scenarios = model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin' && scenario.isReadme);
63
- if (scenarios.length === 0)
54
+ function renderReadmeCli(lines, model, outputDir, diagrams) {
55
+ if (model.readmeCli === null)
64
56
  return;
65
57
  lines.push('## CLI', '');
58
+ if (model.readmeCli.description !== null)
59
+ lines.push(model.readmeCli.description, '');
60
+ if (model.usage !== null && model.usage.commands.length > 0) {
61
+ lines.push('```bash');
62
+ for (const command of model.usage.commands)
63
+ lines.push(command.command);
64
+ lines.push('```', '');
65
+ }
66
+ const scenarios = model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin');
66
67
  for (const scenario of scenarios) {
68
+ const diagram = findScenarioDiagram(diagrams, scenario);
69
+ if (scenario.description === null && diagram === undefined)
70
+ continue;
67
71
  lines.push('<details>');
68
72
  lines.push(`<summary>${scenario.name}</summary>`, '');
69
73
  if (scenario.description !== null)
70
74
  lines.push(scenario.description, '');
71
- const command = model.usage?.commands.find((item) => item.name === scenario.name);
72
- if (command !== undefined) {
73
- lines.push('```bash');
74
- lines.push(command.command);
75
- lines.push('```', '');
76
- }
77
- const diagram = findScenarioDiagram(diagrams, scenario);
78
75
  if (diagram !== undefined) {
79
76
  lines.push(`Diagram: [${diagram.title}](./${outputDir}/${diagram.path})`, '');
80
77
  lines.push('```mermaid');
@@ -88,30 +85,19 @@ function findScenarioDiagram(diagrams, scenario) {
88
85
  return diagrams.find((diagram) => diagram.path === `diagrams/sequences/${toFileStem(scenario.name)}.mmd`);
89
86
  }
90
87
  function renderConfiguration(lines, model) {
91
- const { config } = model;
92
- if (config === null)
88
+ const config = model.config?.isReadme ? model.config : null;
89
+ const example = model.readmeConfig;
90
+ if (config === null && example === null)
93
91
  return;
94
92
  lines.push('## Configuration', '');
95
- lines.push(`Create a \`${config.configFile}\` file:`, '');
96
- lines.push('```ts');
97
- if (config.factoryName !== null) {
98
- lines.push(`import { ${config.factoryName} } from '${model.packageId}';`);
99
- lines.push('');
100
- lines.push(`export default ${config.factoryName}({`);
101
- lines.push(' // ...');
102
- lines.push('});');
103
- }
104
- else {
105
- lines.push(`import type { ${config.exportName} } from '${model.packageId}';`);
106
- lines.push('');
107
- lines.push('const config = {');
108
- lines.push(' // ...');
109
- lines.push(`} satisfies ${config.exportName};`);
110
- lines.push('');
111
- lines.push('export default config;');
93
+ if (example !== null) {
94
+ if (example.description !== null)
95
+ lines.push(example.description, '');
96
+ lines.push(`\`\`\`${example.language}`);
97
+ lines.push(example.code);
98
+ lines.push('```', '');
112
99
  }
113
- lines.push('```', '');
114
- if (config.members.length === 0)
100
+ if (config === null || config.members.length === 0)
115
101
  return;
116
102
  lines.push('<details>');
117
103
  lines.push('<summary>Configuration options</summary>', '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/paradox",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -75,7 +75,8 @@
75
75
  "@ankhorage/devtools": "^1.0.6",
76
76
  "@changesets/cli": "^2.31.0",
77
77
  "@types/bun": "^1.3.13",
78
- "typescript": "^5.6.3"
78
+ "typescript": "^5.6.3",
79
+ "@types/node": "^25.6.0"
79
80
  },
80
81
  "packageManager": "bun@1.3.13"
81
82
  }