@ankhorage/paradox 0.0.0 → 0.0.2

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.
@@ -0,0 +1,5 @@
1
+ ---
2
+ '@ankhorage/paradox': patch
3
+ ---
4
+
5
+ Stabilize usage metadata, documentation model serialization, and deterministic output ordering.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 17729fb: Add generated README usage and configuration sections from package metadata and `@config` doc tags.
8
+
3
9
  ## 0.0.0
4
10
 
5
11
  ### Initial Changes
package/README.md CHANGED
@@ -2,23 +2,25 @@
2
2
 
3
3
  Deterministic documentation generator for TypeScript packages.
4
4
 
5
- ## Package Exports
5
+ ## Usage
6
6
 
7
- ### analyze
7
+ ```bash
8
+ bunx @ankhorage/paradox
9
+ ```
8
10
 
9
- Runs the source analysis pipeline for a configured package.
11
+ ## Configuration
10
12
 
11
- ### AnalysisComponent
13
+ Create a `paradox.config.ts` file:
12
14
 
13
- Describes one React component and its extracted props.
15
+ ```ts
16
+ import { defineParadoxConfig } from '@ankhorage/paradox';
14
17
 
15
- ### AnalysisExport
18
+ export default defineParadoxConfig({
19
+ // ...
20
+ });
21
+ ```
16
22
 
17
- Describes one exported declaration discovered in a package.
18
-
19
- ### AnalysisResult
20
-
21
- Complete analysis output used to build the documentation model.
23
+ ## Public API
22
24
 
23
25
  ### defineParadoxConfig
24
26
 
@@ -26,24 +28,4 @@ Defines a Paradox configuration object without changing its shape.
26
28
 
27
29
  ### ParadoxConfig
28
30
 
29
- Configuration for running Paradox against a TypeScript package.
30
-
31
- ### buildModel
32
-
33
- Converts analysis output into a serializable documentation model.
34
-
35
- ### DocumentationModel
36
-
37
- Serializable model consumed by renderers and writers.
38
-
39
- ### render
40
-
41
- Renders the documentation model into README and artifact files.
42
-
43
- ### RenderResult
44
-
45
- Rendered documentation files ready to be written to disk.
46
-
47
- ### write
48
-
49
- Writes generated documentation artifacts to the configured output paths.
31
+ Configuration for running Paradox.
package/knip.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@latest/schema.json",
3
+ "entry": ["src/index.ts", "eslint.config.mjs", ".prettierrc.js", "paradox.config.ts"],
4
+ "project": ["src/**/*.ts", "tests/**/*.ts"],
5
+ "ignore": ["tests/fixtures/**"],
6
+ "ignoreBinaries": ["eslint", "prettier"],
7
+ "ignoreUnresolved": ["bun-types"]
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/paradox",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -1,24 +1,4 @@
1
1
  [
2
- {
3
- "name": "analyze",
4
- "description": "Runs the source analysis pipeline for a configured package.",
5
- "kind": "function"
6
- },
7
- {
8
- "name": "AnalysisComponent",
9
- "description": "Describes one React component and its extracted props.",
10
- "kind": "type"
11
- },
12
- {
13
- "name": "AnalysisExport",
14
- "description": "Describes one exported declaration discovered in a package.",
15
- "kind": "type"
16
- },
17
- {
18
- "name": "AnalysisResult",
19
- "description": "Complete analysis output used to build the documentation model.",
20
- "kind": "type"
21
- },
22
2
  {
23
3
  "name": "defineParadoxConfig",
24
4
  "description": "Defines a Paradox configuration object without changing its shape.",
@@ -26,32 +6,7 @@
26
6
  },
27
7
  {
28
8
  "name": "ParadoxConfig",
29
- "description": "Configuration for running Paradox against a TypeScript package.",
9
+ "description": "Configuration for running Paradox.",
30
10
  "kind": "type"
31
- },
32
- {
33
- "name": "buildModel",
34
- "description": "Converts analysis output into a serializable documentation model.",
35
- "kind": "function"
36
- },
37
- {
38
- "name": "DocumentationModel",
39
- "description": "Serializable model consumed by renderers and writers.",
40
- "kind": "type"
41
- },
42
- {
43
- "name": "render",
44
- "description": "Renders the documentation model into README and artifact files.",
45
- "kind": "function"
46
- },
47
- {
48
- "name": "RenderResult",
49
- "description": "Rendered documentation files ready to be written to disk.",
50
- "kind": "type"
51
- },
52
- {
53
- "name": "write",
54
- "description": "Writes generated documentation artifacts to the configured output paths.",
55
- "kind": "function"
56
11
  }
57
12
  ]
@@ -1,28 +1,4 @@
1
- # Package Exports
2
-
3
- ## analyze
4
-
5
- Kind: `function`
6
-
7
- Runs the source analysis pipeline for a configured package.
8
-
9
- ## AnalysisComponent
10
-
11
- Kind: `type`
12
-
13
- Describes one React component and its extracted props.
14
-
15
- ## AnalysisExport
16
-
17
- Kind: `type`
18
-
19
- Describes one exported declaration discovered in a package.
20
-
21
- ## AnalysisResult
22
-
23
- Kind: `type`
24
-
25
- Complete analysis output used to build the documentation model.
1
+ # Public API
26
2
 
27
3
  ## defineParadoxConfig
28
4
 
@@ -34,34 +10,4 @@ Defines a Paradox configuration object without changing its shape.
34
10
 
35
11
  Kind: `type`
36
12
 
37
- Configuration for running Paradox against a TypeScript package.
38
-
39
- ## buildModel
40
-
41
- Kind: `function`
42
-
43
- Converts analysis output into a serializable documentation model.
44
-
45
- ## DocumentationModel
46
-
47
- Kind: `type`
48
-
49
- Serializable model consumed by renderers and writers.
50
-
51
- ## render
52
-
53
- Kind: `function`
54
-
55
- Renders the documentation model into README and artifact files.
56
-
57
- ## RenderResult
58
-
59
- Kind: `type`
60
-
61
- Rendered documentation files ready to be written to disk.
62
-
63
- ## write
64
-
65
- Kind: `function`
66
-
67
- Writes generated documentation artifacts to the configured output paths.
13
+ Configuration for running Paradox.
@@ -1,27 +1,22 @@
1
1
  {
2
2
  "packageName": "@ankhorage/paradox",
3
+ "packageId": "@ankhorage/paradox",
3
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
+ "usage": {
6
+ "packageName": "@ankhorage/paradox",
7
+ "commands": [
8
+ {
9
+ "name": "paradox",
10
+ "command": "bunx @ankhorage/paradox"
11
+ }
12
+ ]
13
+ },
14
+ "config": {
15
+ "exportName": "ParadoxConfig",
16
+ "configFile": "paradox.config.ts",
17
+ "factoryName": "defineParadoxConfig"
18
+ },
4
19
  "exports": [
5
- {
6
- "name": "analyze",
7
- "description": "Runs the source analysis pipeline for a configured package.",
8
- "kind": "function"
9
- },
10
- {
11
- "name": "AnalysisComponent",
12
- "description": "Describes one React component and its extracted props.",
13
- "kind": "type"
14
- },
15
- {
16
- "name": "AnalysisExport",
17
- "description": "Describes one exported declaration discovered in a package.",
18
- "kind": "type"
19
- },
20
- {
21
- "name": "AnalysisResult",
22
- "description": "Complete analysis output used to build the documentation model.",
23
- "kind": "type"
24
- },
25
20
  {
26
21
  "name": "defineParadoxConfig",
27
22
  "description": "Defines a Paradox configuration object without changing its shape.",
@@ -29,33 +24,8 @@
29
24
  },
30
25
  {
31
26
  "name": "ParadoxConfig",
32
- "description": "Configuration for running Paradox against a TypeScript package.",
27
+ "description": "Configuration for running Paradox.",
33
28
  "kind": "type"
34
- },
35
- {
36
- "name": "buildModel",
37
- "description": "Converts analysis output into a serializable documentation model.",
38
- "kind": "function"
39
- },
40
- {
41
- "name": "DocumentationModel",
42
- "description": "Serializable model consumed by renderers and writers.",
43
- "kind": "type"
44
- },
45
- {
46
- "name": "render",
47
- "description": "Renders the documentation model into README and artifact files.",
48
- "kind": "function"
49
- },
50
- {
51
- "name": "RenderResult",
52
- "description": "Rendered documentation files ready to be written to disk.",
53
- "kind": "type"
54
- },
55
- {
56
- "name": "write",
57
- "description": "Writes generated documentation artifacts to the configured output paths.",
58
- "kind": "function"
59
29
  }
60
30
  ],
61
31
  "components": []
@@ -6,6 +6,7 @@ import { analyzeComponents } from './components.js';
6
6
  import { analyzeExports } from './exports.js';
7
7
  import { createProject } from './project.js';
8
8
  import type { AnalysisResult } from './types.js';
9
+ import { createUsageFromPackageJson, type PackageJsonModel } from './usage.js';
9
10
 
10
11
  /***
11
12
  * Runs the source analysis pipeline for a configured package.
@@ -14,11 +15,12 @@ export async function analyze(config: ParadoxConfig): Promise<AnalysisResult> {
14
15
  const root = config.package?.root ?? process.cwd();
15
16
 
16
17
  const pkg = await readPackageJson(root);
18
+ const usage = createUsageFromPackageJson(pkg);
17
19
 
18
20
  const project = createProject(root);
19
21
  const entrypoints = config.package?.entrypoints ?? ['src/index.ts'];
20
22
 
21
- const exports = analyzeExports(project, {
23
+ const { config: configMetadata, exports } = analyzeExports(project, {
22
24
  root,
23
25
  entrypoints,
24
26
  });
@@ -26,21 +28,18 @@ export async function analyze(config: ParadoxConfig): Promise<AnalysisResult> {
26
28
 
27
29
  return {
28
30
  packageName: config.docs?.title ?? pkg.name,
31
+ packageId: pkg.name,
29
32
  description: config.docs?.description ?? pkg.description ?? null,
30
33
 
31
34
  exports,
32
35
  components,
36
+ usage,
37
+ config: configMetadata,
33
38
  };
34
39
  }
35
40
 
36
- async function readPackageJson(root: string): Promise<{
37
- name: string;
38
- description?: string;
39
- }> {
41
+ async function readPackageJson(root: string): Promise<PackageJsonModel> {
40
42
  const raw = await readFile(join(root, 'package.json'), 'utf-8');
41
43
 
42
- return JSON.parse(raw) as {
43
- name: string;
44
- description?: string;
45
- };
44
+ return JSON.parse(raw) as PackageJsonModel;
46
45
  }
@@ -7,6 +7,13 @@ import { getParadoxComment } from './utils/getParadoxComment.js';
7
7
  import { parseParadoxComment } from './utils/parseParadoxComment.js';
8
8
  import { resolveExportSymbol } from './utils/resolveExportSymbol.js';
9
9
 
10
+ interface AnalyzeExportsResult {
11
+ exports: AnalysisExport[];
12
+ config: {
13
+ exportName: string;
14
+ } | null;
15
+ }
16
+
10
17
  /***
11
18
  * Collects exported declarations from configured package entrypoints.
12
19
  */
@@ -16,8 +23,9 @@ export function analyzeExports(
16
23
  root: string;
17
24
  entrypoints: readonly string[];
18
25
  },
19
- ): AnalysisExport[] {
26
+ ): AnalyzeExportsResult {
20
27
  const exports: AnalysisExport[] = [];
28
+ let config: AnalyzeExportsResult['config'] = null;
21
29
 
22
30
  for (const sourceFile of getEntryPointSourceFiles(project, options)) {
23
31
  const exported = sourceFile.getExportSymbols();
@@ -27,7 +35,15 @@ export function analyzeExports(
27
35
  const [decl] = resolved.getDeclarations();
28
36
 
29
37
  const rawComment = getParadoxComment(decl);
30
- const parsed = rawComment ? parseParadoxComment(rawComment) : { description: null };
38
+ const parsed = rawComment
39
+ ? parseParadoxComment(rawComment)
40
+ : { description: null, isConfig: false };
41
+
42
+ if (parsed.isConfig) {
43
+ config = {
44
+ exportName: resolved.getName(),
45
+ };
46
+ }
31
47
 
32
48
  exports.push({
33
49
  name: resolved.getName(),
@@ -38,7 +54,10 @@ export function analyzeExports(
38
54
  }
39
55
  }
40
56
 
41
- return exports;
57
+ return {
58
+ exports,
59
+ config,
60
+ };
42
61
  }
43
62
 
44
63
  function getEntryPointSourceFiles(
@@ -24,13 +24,30 @@ export interface AnalysisComponent {
24
24
  }[];
25
25
  }
26
26
 
27
+ export interface AnalysisUsage {
28
+ packageName: string;
29
+ commands: AnalysisUsageCommand[];
30
+ }
31
+
32
+ export interface AnalysisUsageCommand {
33
+ name: string;
34
+ command: string;
35
+ }
36
+
27
37
  /***
28
38
  * Complete analysis output used to build the documentation model.
29
39
  */
30
40
  export interface AnalysisResult {
31
41
  packageName: string;
42
+ packageId: string;
32
43
  description: string | null;
33
44
 
34
45
  exports: AnalysisExport[];
35
46
  components: AnalysisComponent[];
47
+
48
+ usage: AnalysisUsage | null;
49
+
50
+ config: {
51
+ exportName: string;
52
+ } | null;
36
53
  }
@@ -0,0 +1,53 @@
1
+ import type { AnalysisUsage } from './types.js';
2
+
3
+ export interface PackageJsonModel {
4
+ name: string;
5
+ description?: string;
6
+ bin?: string | Record<string, string>;
7
+ }
8
+
9
+ export function createUsageFromPackageJson(pkg: PackageJsonModel): AnalysisUsage | null {
10
+ if (pkg.bin == null) return null;
11
+
12
+ if (typeof pkg.bin === 'string') {
13
+ return {
14
+ packageName: pkg.name,
15
+ commands: [
16
+ {
17
+ name: getPackageBaseName(pkg.name),
18
+ command: `bunx ${pkg.name}`,
19
+ },
20
+ ],
21
+ };
22
+ }
23
+
24
+ const entries = Object.keys(pkg.bin).sort((a, b) => a.localeCompare(b));
25
+
26
+ if (entries.length === 0) return null;
27
+
28
+ if (entries.length === 1) {
29
+ const [name] = entries;
30
+
31
+ return {
32
+ packageName: pkg.name,
33
+ commands: [
34
+ {
35
+ name,
36
+ command: `bunx ${pkg.name}`,
37
+ },
38
+ ],
39
+ };
40
+ }
41
+
42
+ return {
43
+ packageName: pkg.name,
44
+ commands: entries.map((name) => ({
45
+ name,
46
+ command: `bunx ${pkg.name} ${name}`,
47
+ })),
48
+ };
49
+ }
50
+
51
+ function getPackageBaseName(packageName: string): string {
52
+ return packageName.split('/').pop() ?? packageName;
53
+ }
@@ -1,23 +1,37 @@
1
1
  /***
2
2
  * Parsed representation of a Paradox doc comment.
3
3
  */
4
- export interface ParsedParadoxComment {
4
+ interface ParsedParadoxComment {
5
5
  description: string | null;
6
+ isConfig: boolean;
6
7
  }
7
8
 
8
9
  /***
9
10
  * Parses a Paradox doc comment into structured metadata.
10
11
  */
11
12
  export function parseParadoxComment(rawComment: string): ParsedParadoxComment {
12
- const body = rawComment
13
+ const lines = rawComment
13
14
  .replace(/^\/\*\*\*/, '')
14
15
  .replace(/\*\/$/, '')
15
16
  .split('\n')
16
- .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd())
17
+ .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
18
+
19
+ let isConfig = false;
20
+
21
+ const description = lines
22
+ .filter((line) => {
23
+ if (line.trimStart().startsWith('@config')) {
24
+ isConfig = true;
25
+ return false;
26
+ }
27
+
28
+ return true;
29
+ })
17
30
  .join('\n')
18
31
  .trim();
19
32
 
20
33
  return {
21
- description: body.length > 0 ? body : null,
34
+ description: description.length > 0 ? description : null,
35
+ isConfig,
22
36
  };
23
37
  }
@@ -1,5 +1,7 @@
1
1
  /***
2
- * Configuration for running Paradox against a TypeScript package.
2
+ * Configuration for running Paradox.
3
+ *
4
+ * @config
3
5
  */
4
6
  export interface ParadoxConfig {
5
7
  mode?: 'safe' | 'write';
package/src/index.ts CHANGED
@@ -1,9 +1,2 @@
1
- export { analyze } from './analyze/analyze.js';
2
- export type { AnalysisComponent, AnalysisExport, AnalysisResult } from './analyze/types.js';
3
1
  export { defineParadoxConfig } from './config/defineParadoxConfig.js';
4
2
  export type { ParadoxConfig } from './config/types.js';
5
- export { buildModel } from './model/buildModel.js';
6
- export type { DocumentationModel } from './model/types.js';
7
- export { render } from './render/render.js';
8
- export type { RenderResult } from './render/types.js';
9
- export { write } from './write/write.js';
@@ -1,25 +1,112 @@
1
- import type { AnalysisResult } from '../analyze/types.js';
2
- import type { DocumentationModel } from './types.js';
1
+ import type { ComponentModel, DocumentationModel, ExportKind, ExportModel } from './types.js';
2
+
3
+ interface BuildModelInput {
4
+ packageName: string;
5
+ packageId: string;
6
+ description: string | null;
7
+ exports: {
8
+ name: string;
9
+ description: string | null;
10
+ kind: ExportKind;
11
+ }[];
12
+ components: {
13
+ name: string;
14
+ description: string | null;
15
+ props: {
16
+ name: string;
17
+ type: string;
18
+ required: boolean;
19
+ description: string | null;
20
+ }[];
21
+ }[];
22
+ usage: {
23
+ packageName: string;
24
+ commands: {
25
+ name: string;
26
+ command: string;
27
+ }[];
28
+ } | null;
29
+ config: {
30
+ exportName: string;
31
+ } | null;
32
+ }
3
33
 
4
34
  /***
5
35
  * Converts analysis output into a serializable documentation model.
6
36
  */
7
- export function buildModel(analysis: AnalysisResult): DocumentationModel {
8
- const exportsByName = new Map(
9
- analysis.exports.map((item) => [
10
- item.name,
11
- {
12
- name: item.name,
13
- description: item.description,
14
- kind: item.kind,
15
- },
16
- ]),
17
- );
37
+ export function buildModel(analysis: BuildModelInput): DocumentationModel {
38
+ const exportsByName = new Map(analysis.exports.map((item) => [item.name, mapExport(item)]));
39
+ const exports = sortByName([...exportsByName.values()]);
18
40
 
19
41
  return {
20
42
  packageName: analysis.packageName,
43
+ packageId: analysis.packageId,
21
44
  description: analysis.description,
22
- exports: [...exportsByName.values()],
23
- components: analysis.components,
45
+ usage:
46
+ analysis.usage !== null
47
+ ? {
48
+ packageName: analysis.usage.packageName,
49
+ commands: sortByName(
50
+ analysis.usage.commands.map((command) => ({
51
+ name: command.name,
52
+ command: command.command,
53
+ })),
54
+ ),
55
+ }
56
+ : null,
57
+ config:
58
+ analysis.config !== null
59
+ ? {
60
+ exportName: analysis.config.exportName,
61
+ configFile: getDefaultConfigFileName(analysis.packageId),
62
+ factoryName: findConfigFactoryName(analysis.config.exportName, [
63
+ ...exportsByName.keys(),
64
+ ]),
65
+ }
66
+ : null,
67
+ exports,
68
+ components: sortByName(analysis.components.map(mapComponent)),
69
+ };
70
+ }
71
+
72
+ function mapExport(item: BuildModelInput['exports'][number]): ExportModel {
73
+ return {
74
+ name: item.name,
75
+ description: item.description,
76
+ kind: item.kind,
77
+ };
78
+ }
79
+
80
+ function mapComponent(component: BuildModelInput['components'][number]): ComponentModel {
81
+ return {
82
+ name: component.name,
83
+ description: component.description,
84
+ props: sortByName(
85
+ component.props.map((prop) => ({
86
+ name: prop.name,
87
+ type: prop.type,
88
+ required: prop.required,
89
+ description: prop.description,
90
+ })),
91
+ ),
24
92
  };
25
93
  }
94
+
95
+ function findConfigFactoryName(configExportName: string, exportNames: string[]): string | null {
96
+ const prefix = configExportName.endsWith('Config')
97
+ ? configExportName.slice(0, -'Config'.length)
98
+ : configExportName;
99
+ const expectedFactoryName = `define${prefix}Config`;
100
+
101
+ return exportNames.includes(expectedFactoryName) ? expectedFactoryName : null;
102
+ }
103
+
104
+ function getDefaultConfigFileName(packageId: string): string {
105
+ const packageBaseName = packageId.split('/').pop() ?? packageId;
106
+
107
+ return `${packageBaseName}.config.ts`;
108
+ }
109
+
110
+ function sortByName<T extends { name: string }>(items: readonly T[]): T[] {
111
+ return [...items].sort((a, b) => a.name.localeCompare(b.name));
112
+ }
@@ -1,19 +1,49 @@
1
- import type { AnalysisComponent, AnalysisResult } from '../analyze/types.js';
2
-
3
1
  /***
4
2
  * Serializable model consumed by renderers and writers.
5
3
  */
6
4
  export interface DocumentationModel {
7
5
  packageName: string;
6
+ packageId: string;
8
7
  description: string | null;
9
- exports: {
10
- name: string;
11
- description: string | null;
12
- kind: string;
13
- }[];
14
- components: AnalysisComponent[];
8
+ usage: UsageModel | null;
9
+ config: ConfigModel | null;
10
+ exports: ExportModel[];
11
+ components: ComponentModel[];
15
12
  }
16
13
 
17
- export type SerializableAnalysisResult = Omit<AnalysisResult, 'exports'> & {
18
- exports: DocumentationModel['exports'];
19
- };
14
+ export interface UsageModel {
15
+ packageName: string;
16
+ commands: UsageCommandModel[];
17
+ }
18
+
19
+ export interface UsageCommandModel {
20
+ name: string;
21
+ command: string;
22
+ }
23
+
24
+ export interface ConfigModel {
25
+ exportName: string;
26
+ configFile: string;
27
+ factoryName: string | null;
28
+ }
29
+
30
+ export interface ExportModel {
31
+ name: string;
32
+ description: string | null;
33
+ kind: ExportKind;
34
+ }
35
+
36
+ export type ExportKind = 'function' | 'type' | 'unknown';
37
+
38
+ export interface ComponentModel {
39
+ name: string;
40
+ description: string | null;
41
+ props: PropModel[];
42
+ }
43
+
44
+ export interface PropModel {
45
+ name: string;
46
+ type: string;
47
+ required: boolean;
48
+ description: string | null;
49
+ }
@@ -21,8 +21,41 @@ function renderReadme(model: DocumentationModel): string {
21
21
  lines.push(model.description, '');
22
22
  }
23
23
 
24
+ if (model.usage !== null) {
25
+ lines.push('## Usage', '');
26
+ lines.push('```bash');
27
+ for (const command of model.usage.commands) {
28
+ lines.push(command.command);
29
+ }
30
+ lines.push('```', '');
31
+ }
32
+
33
+ if (model.config !== null) {
34
+ lines.push('## Configuration', '');
35
+ lines.push(`Create a \`${model.config.configFile}\` file:`, '');
36
+ lines.push('```ts');
37
+
38
+ if (model.config.factoryName !== null) {
39
+ lines.push(`import { ${model.config.factoryName} } from '${model.packageId}';`);
40
+ lines.push('');
41
+ lines.push(`export default ${model.config.factoryName}({`);
42
+ lines.push(' // ...');
43
+ lines.push('});');
44
+ } else {
45
+ lines.push(`import type { ${model.config.exportName} } from '${model.packageId}';`);
46
+ lines.push('');
47
+ lines.push('const config = {');
48
+ lines.push(' // ...');
49
+ lines.push(`} satisfies ${model.config.exportName};`);
50
+ lines.push('');
51
+ lines.push('export default config;');
52
+ }
53
+
54
+ lines.push('```', '');
55
+ }
56
+
24
57
  if (model.exports.length > 0) {
25
- lines.push('## Package Exports', '');
58
+ lines.push('## Public API', '');
26
59
 
27
60
  for (const item of model.exports) {
28
61
  lines.push(`### ${item.name}`, '');
@@ -34,7 +67,7 @@ function renderReadme(model: DocumentationModel): string {
34
67
  }
35
68
 
36
69
  function renderExports(model: DocumentationModel): string {
37
- const lines = ['# Package Exports', ''];
70
+ const lines = ['# Public API', ''];
38
71
 
39
72
  for (const item of model.exports) {
40
73
  lines.push(`## ${item.name}`, '');
@@ -6,5 +6,5 @@ Renders the fixture button component.
6
6
 
7
7
  | Prop | Type | Required | Description |
8
8
  | --- | --- | --- | --- |
9
- | label | `string` | yes | Visible button label. |
10
9
  | disabled | `boolean \| undefined` | no | Optional disabled state. |
10
+ | label | `string` | yes | Visible button label. |
@@ -1,4 +1,4 @@
1
- # Package Exports
1
+ # Public API
2
2
 
3
3
  ## Button
4
4
 
@@ -11,3 +11,9 @@ Renders the fixture button component.
11
11
  Kind: `type`
12
12
 
13
13
  Props accepted by the fixture button.
14
+
15
+ ## ToolConfig
16
+
17
+ Kind: `type`
18
+
19
+ Configuration for the fixture package.
@@ -2,7 +2,27 @@
2
2
 
3
3
  Generated fixture docs.
4
4
 
5
- ## Package Exports
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx @fixture/basic
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ Create a `basic.config.ts` file:
14
+
15
+ ```ts
16
+ import type { ToolConfig } from '@fixture/basic';
17
+
18
+ const config = {
19
+ // ...
20
+ } satisfies ToolConfig;
21
+
22
+ export default config;
23
+ ```
24
+
25
+ ## Public API
6
26
 
7
27
  ### Button
8
28
 
@@ -11,3 +31,7 @@ Renders the fixture button component.
11
31
  ### ButtonProps
12
32
 
13
33
  Props accepted by the fixture button.
34
+
35
+ ### ToolConfig
36
+
37
+ Configuration for the fixture package.
@@ -0,0 +1,16 @@
1
+ # Multi Bin Fixture
2
+
3
+ Fixture docs for multiple binaries.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx fixture-multi-bin alpha
9
+ bunx fixture-multi-bin beta
10
+ ```
11
+
12
+ ## Public API
13
+
14
+ ### example
15
+
16
+ Example public function.
@@ -3,9 +3,13 @@ import { join } from 'node:path';
3
3
 
4
4
  import { describe, expect, test } from 'bun:test';
5
5
 
6
- import { analyze, buildModel, render } from '../src/index.js';
6
+ import { analyze } from '../src/analyze/analyze.js';
7
+ import { createUsageFromPackageJson } from '../src/analyze/usage.js';
8
+ import { buildModel } from '../src/model/buildModel.js';
9
+ import { render } from '../src/render/render.js';
7
10
 
8
11
  const fixtureRoot = join(import.meta.dir, 'fixtures/basic');
12
+ const multiBinFixtureRoot = join(import.meta.dir, 'fixtures/multi-bin');
9
13
  const snapshotRoot = join(import.meta.dir, '__snapshots__');
10
14
 
11
15
  describe('analyze', () => {
@@ -21,8 +25,24 @@ describe('analyze', () => {
21
25
  },
22
26
  });
23
27
 
24
- expect(analysis.exports.map((item) => item.name)).toEqual(['Button', 'ButtonProps']);
28
+ expect(analysis.exports.map((item) => item.name)).toEqual([
29
+ 'Button',
30
+ 'ToolConfig',
31
+ 'ButtonProps',
32
+ ]);
25
33
  expect(analysis.exports.map((item) => item.name)).not.toContain('internalHelper');
34
+ expect(analysis.usage).toEqual({
35
+ packageName: '@fixture/basic',
36
+ commands: [
37
+ {
38
+ name: 'fixture-basic',
39
+ command: 'bunx @fixture/basic',
40
+ },
41
+ ],
42
+ });
43
+ expect(analysis.config).toEqual({
44
+ exportName: 'ToolConfig',
45
+ });
26
46
 
27
47
  expect(analysis.components).toEqual([
28
48
  {
@@ -51,6 +71,79 @@ describe('analyze', () => {
51
71
  await expectSnapshot('basic.exports.md', output.exportsMarkdown);
52
72
  await expectSnapshot('basic.components.md', output.components);
53
73
  });
74
+
75
+ test('renders multiple bin commands deterministically', async () => {
76
+ const analysis = await analyze({
77
+ docs: {
78
+ title: 'Multi Bin Fixture',
79
+ description: 'Fixture docs for multiple binaries.',
80
+ },
81
+ package: {
82
+ root: multiBinFixtureRoot,
83
+ entrypoints: ['src/index.ts'],
84
+ },
85
+ });
86
+
87
+ expect(analysis.usage).toEqual({
88
+ packageName: 'fixture-multi-bin',
89
+ commands: [
90
+ {
91
+ name: 'alpha',
92
+ command: 'bunx fixture-multi-bin alpha',
93
+ },
94
+ {
95
+ name: 'beta',
96
+ command: 'bunx fixture-multi-bin beta',
97
+ },
98
+ ],
99
+ });
100
+
101
+ const output = render(buildModel(analysis));
102
+
103
+ await expectSnapshot('multi-bin.readme.md', output.readme);
104
+ });
105
+
106
+ test('normalizes string bin usage', () => {
107
+ expect(
108
+ createUsageFromPackageJson({
109
+ name: 'fixture-string-bin',
110
+ bin: './src/cli.ts',
111
+ }),
112
+ ).toEqual({
113
+ packageName: 'fixture-string-bin',
114
+ commands: [
115
+ {
116
+ name: 'fixture-string-bin',
117
+ command: 'bunx fixture-string-bin',
118
+ },
119
+ ],
120
+ });
121
+ });
122
+
123
+ test('normalizes missing bin usage', () => {
124
+ expect(
125
+ createUsageFromPackageJson({
126
+ name: 'fixture-no-bin',
127
+ }),
128
+ ).toBeNull();
129
+ });
130
+
131
+ test('normalizes scoped string bin usage', () => {
132
+ expect(
133
+ createUsageFromPackageJson({
134
+ name: '@fixture/string-bin',
135
+ bin: './src/cli.ts',
136
+ }),
137
+ ).toEqual({
138
+ packageName: '@fixture/string-bin',
139
+ commands: [
140
+ {
141
+ name: 'string-bin',
142
+ command: 'bunx @fixture/string-bin',
143
+ },
144
+ ],
145
+ });
146
+ });
54
147
  });
55
148
 
56
149
  async function expectSnapshot(name: string, actual: string): Promise<void> {
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "name": "@fixture/basic",
3
3
  "description": "Fixture package for Paradox tests.",
4
- "type": "module"
4
+ "type": "module",
5
+ "bin": {
6
+ "fixture-basic": "./src/index.ts"
7
+ }
5
8
  }
@@ -0,0 +1,8 @@
1
+ /***
2
+ * Configuration for the fixture package.
3
+ *
4
+ * @config
5
+ */
6
+ export interface ToolConfig {
7
+ enabled: boolean;
8
+ }
@@ -1,2 +1,3 @@
1
1
  export { Button } from './ui.js';
2
+ export type { ToolConfig } from './config.js';
2
3
  export type { ButtonProps } from './ui.js';
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "fixture-multi-bin",
3
+ "description": "Fixture package with multiple binaries.",
4
+ "type": "module",
5
+ "bin": {
6
+ "beta": "./src/beta.ts",
7
+ "alpha": "./src/alpha.ts"
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ export function alpha(): void {}
@@ -0,0 +1 @@
1
+ export function beta(): void {}
@@ -0,0 +1,4 @@
1
+ /***
2
+ * Example public function.
3
+ */
4
+ export function example(): void {}
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true
9
+ },
10
+ "include": ["src/**/*.ts"]
11
+ }