@ankhorage/paradox 0.1.18 → 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,11 @@
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
+
3
9
  ## 0.1.18
4
10
 
5
11
  ### 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.18](./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
@@ -5,6 +5,7 @@ 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';
8
9
  import { analyzeReadmeConfig } from './readmeConfig.js';
9
10
  import { analyzeReadmeUsage } from './readmeUsage.js';
10
11
  import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
@@ -26,6 +27,7 @@ export async function analyze(config, runtime) {
26
27
  const readmeUsageDescription = config.docs?.usage?.description ?? null;
27
28
  const usageEntryPoints = config.docs?.usage?.entrypoints ?? [];
28
29
  const readmeUsage = await analyzeReadmeUsage({ root, entrypoints: usageEntryPoints });
30
+ const readmeCli = await analyzeReadmeCli(root);
29
31
  const readmeConfig = await analyzeReadmeConfig({
30
32
  root,
31
33
  configFilePath: runtime.configFilePath ?? null,
@@ -75,6 +77,7 @@ export async function analyze(config, runtime) {
75
77
  usage,
76
78
  readmeUsageDescription,
77
79
  readmeUsage,
80
+ readmeCli,
78
81
  readmeConfig,
79
82
  config: configMetadata
80
83
  ? {
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { extname, relative } from 'node:path';
3
- import { parseParadoxComment } from './utils/parseParadoxComment.js';
3
+ import { getLeadingParadoxComment } from './utils/getLeadingParadoxComment.js';
4
4
  /***
5
5
  * Collects a README configuration example from the actual Paradox config file when its
6
6
  * leading Paradox comment is marked with both @config and @readme.
@@ -9,32 +9,19 @@ export async function analyzeReadmeConfig(options) {
9
9
  if (options.configFilePath === null)
10
10
  return null;
11
11
  const source = await readFile(options.configFilePath, 'utf-8');
12
- const match = findLeadingParadoxComment(source);
13
- if (match === null)
12
+ const comment = getLeadingParadoxComment(source);
13
+ if (comment === null)
14
14
  return null;
15
- const parsed = parseParadoxComment(match.comment);
16
- if (!parsed.isConfig || !parsed.isReadme)
15
+ if (!comment.parsed.isConfig || !comment.parsed.isReadme)
17
16
  return null;
18
17
  const sourcePath = toPosixPath(relative(options.root, options.configFilePath));
19
18
  return {
20
- description: parsed.description,
19
+ description: comment.parsed.description,
21
20
  language: getLanguage(sourcePath),
22
- code: removeRange(source, match.start, match.end).trim(),
21
+ code: removeRange(source, comment.start, comment.end).trim(),
23
22
  sourcePath,
24
23
  };
25
24
  }
26
- function findLeadingParadoxComment(source) {
27
- const match = /^\s*(\/\*\*\*[\s\S]*?\*\/)/.exec(source);
28
- const comment = match?.[1];
29
- if (match === null || comment === undefined)
30
- return null;
31
- const start = match[0].indexOf(comment);
32
- return {
33
- comment,
34
- start,
35
- end: start + comment.length,
36
- };
37
- }
38
25
  function removeRange(source, start, end) {
39
26
  const before = source.slice(0, start).trimEnd();
40
27
  const after = source.slice(end).trimStart();
@@ -86,6 +86,10 @@ interface AnalysisReadmeUsage {
86
86
  code: string;
87
87
  sourcePath: string;
88
88
  }
89
+ interface AnalysisReadmeCli {
90
+ description: string | null;
91
+ sourcePath: string;
92
+ }
89
93
  interface AnalysisReadmeConfig {
90
94
  description: string | null;
91
95
  language: string;
@@ -171,6 +175,7 @@ export interface AnalysisResult {
171
175
  usage: AnalysisUsage | null;
172
176
  readmeUsageDescription: string | null;
173
177
  readmeUsage: AnalysisReadmeUsage[];
178
+ readmeCli: AnalysisReadmeCli | null;
174
179
  readmeConfig: AnalysisReadmeConfig | null;
175
180
  config: {
176
181
  exportName: string;
@@ -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();
@@ -115,6 +115,10 @@ interface BuildModelInput {
115
115
  code: string;
116
116
  sourcePath: string;
117
117
  }[];
118
+ readmeCli: {
119
+ description: string | null;
120
+ sourcePath: string;
121
+ } | null;
118
122
  readmeConfig: {
119
123
  description: string | null;
120
124
  language: string;
@@ -34,6 +34,12 @@ 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,
37
43
  readmeConfig: analysis.readmeConfig !== null
38
44
  ? {
39
45
  description: analysis.readmeConfig.description,
@@ -9,6 +9,7 @@ export interface DocumentationModel {
9
9
  usage: UsageModel | null;
10
10
  readmeUsageDescription: string | null;
11
11
  readmeUsage: ReadmeUsageModel[];
12
+ readmeCli: ReadmeCliModel | null;
12
13
  readmeConfig: ReadmeConfigModel | null;
13
14
  config: ConfigModel | null;
14
15
  entrypoints: string[];
@@ -40,6 +41,10 @@ interface ReadmeUsageModel {
40
41
  code: string;
41
42
  sourcePath: string;
42
43
  }
44
+ interface ReadmeCliModel {
45
+ description: string | null;
46
+ sourcePath: string;
47
+ }
43
48
  interface ReadmeConfigModel {
44
49
  description: string | null;
45
50
  language: string;
@@ -89,6 +94,12 @@ export interface SequenceScenarioModel {
89
94
  description: string | null;
90
95
  isReadme: boolean;
91
96
  }
97
+ export interface ModuleModel {
98
+ path: string;
99
+ isEntrypoint: boolean;
100
+ dependencies: string[];
101
+ exports: string[];
102
+ }
92
103
  interface ExampleModel {
93
104
  title: string | null;
94
105
  language: string | null;
@@ -121,15 +132,6 @@ interface MemberModel {
121
132
  inheritedFrom?: string;
122
133
  children?: MemberModel[];
123
134
  }
124
- interface StructuredRowModel {
125
- values: Record<string, string>;
126
- }
127
- export interface ModuleModel {
128
- path: string;
129
- isEntrypoint: boolean;
130
- dependencies: string[];
131
- exports: string[];
132
- }
133
135
  interface PropModel {
134
136
  name: string;
135
137
  type: string;
@@ -137,6 +139,9 @@ interface PropModel {
137
139
  defaultValue?: string;
138
140
  description: string | null;
139
141
  }
142
+ interface StructuredRowModel {
143
+ values: Record<string, string>;
144
+ }
140
145
  interface ConfigMemberModel {
141
146
  name: string;
142
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,13 +24,7 @@ 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);
27
+ renderReadmeCli(lines, model, outputDir, diagrams);
34
28
  renderConfiguration(lines, model);
35
29
  renderGeneratedDocumentation(lines, outputDir, diagrams);
36
30
  renderReadmeApi(lines, model);
@@ -57,23 +51,27 @@ function renderReadmeUsage(lines, description, entries) {
57
51
  lines.push('```', '');
58
52
  }
59
53
  }
60
- function renderCliScenarios(lines, model, outputDir, diagrams) {
61
- const scenarios = model.sequenceScenarios.filter((scenario) => scenario.kind === 'bin' && scenario.isReadme);
62
- if (scenarios.length === 0)
54
+ function renderReadmeCli(lines, model, outputDir, diagrams) {
55
+ if (model.readmeCli === null)
63
56
  return;
64
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');
65
67
  for (const scenario of scenarios) {
68
+ const diagram = findScenarioDiagram(diagrams, scenario);
69
+ if (scenario.description === null && diagram === undefined)
70
+ continue;
66
71
  lines.push('<details>');
67
72
  lines.push(`<summary>${scenario.name}</summary>`, '');
68
73
  if (scenario.description !== null)
69
74
  lines.push(scenario.description, '');
70
- const command = model.usage?.commands.find((item) => item.name === scenario.name);
71
- if (command !== undefined) {
72
- lines.push('```bash');
73
- lines.push(command.command);
74
- lines.push('```', '');
75
- }
76
- const diagram = findScenarioDiagram(diagrams, scenario);
77
75
  if (diagram !== undefined) {
78
76
  lines.push(`Diagram: [${diagram.title}](./${outputDir}/${diagram.path})`, '');
79
77
  lines.push('```mermaid');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/paradox",
3
- "version": "0.1.18",
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
  }