@ankhorage/paradox 0.0.1 → 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/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.1",
3
+ "version": "0.0.2",
4
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -2,6 +2,20 @@
2
2
  "packageName": "@ankhorage/paradox",
3
3
  "packageId": "@ankhorage/paradox",
4
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
+ },
5
19
  "exports": [
6
20
  {
7
21
  "name": "defineParadoxConfig",
@@ -14,13 +28,5 @@
14
28
  "kind": "type"
15
29
  }
16
30
  ],
17
- "components": [],
18
- "usage": {
19
- "command": "bunx @ankhorage/paradox"
20
- },
21
- "config": {
22
- "exportName": "ParadoxConfig",
23
- "configFile": "paradox.config.ts",
24
- "factoryName": "defineParadoxConfig"
25
- }
31
+ "components": []
26
32
  }
@@ -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,7 @@ 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);
17
- const usage = pkg.bin
18
- ? {
19
- command: `bunx ${pkg.name}`,
20
- }
21
- : null;
18
+ const usage = createUsageFromPackageJson(pkg);
22
19
 
23
20
  const project = createProject(root);
24
21
  const entrypoints = config.package?.entrypoints ?? ['src/index.ts'];
@@ -41,16 +38,8 @@ export async function analyze(config: ParadoxConfig): Promise<AnalysisResult> {
41
38
  };
42
39
  }
43
40
 
44
- async function readPackageJson(root: string): Promise<{
45
- name: string;
46
- description?: string;
47
- bin?: string | Record<string, string>;
48
- }> {
41
+ async function readPackageJson(root: string): Promise<PackageJsonModel> {
49
42
  const raw = await readFile(join(root, 'package.json'), 'utf-8');
50
43
 
51
- return JSON.parse(raw) as {
52
- name: string;
53
- description?: string;
54
- bin?: string | Record<string, string>;
55
- };
44
+ return JSON.parse(raw) as PackageJsonModel;
56
45
  }
@@ -7,7 +7,7 @@ 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
- export interface AnalyzeExportsResult {
10
+ interface AnalyzeExportsResult {
11
11
  exports: AnalysisExport[];
12
12
  config: {
13
13
  exportName: string;
@@ -24,6 +24,16 @@ 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
  */
@@ -35,9 +45,7 @@ export interface AnalysisResult {
35
45
  exports: AnalysisExport[];
36
46
  components: AnalysisComponent[];
37
47
 
38
- usage: {
39
- command: string;
40
- } | null;
48
+ usage: AnalysisUsage | null;
41
49
 
42
50
  config: {
43
51
  exportName: string;
@@ -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,7 +1,7 @@
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
6
  isConfig: boolean;
7
7
  }
@@ -1,38 +1,94 @@
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,
21
43
  packageId: analysis.packageId,
22
44
  description: analysis.description,
23
- exports: [...exportsByName.values()],
24
- components: analysis.components,
25
- usage: analysis.usage,
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,
26
57
  config:
27
58
  analysis.config !== null
28
59
  ? {
29
60
  exportName: analysis.config.exportName,
30
- configFile: `${analysis.packageId.split('/').pop()}.config.ts`,
61
+ configFile: getDefaultConfigFileName(analysis.packageId),
31
62
  factoryName: findConfigFactoryName(analysis.config.exportName, [
32
63
  ...exportsByName.keys(),
33
64
  ]),
34
65
  }
35
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
+ ),
36
92
  };
37
93
  }
38
94
 
@@ -44,3 +100,13 @@ function findConfigFactoryName(configExportName: string, exportNames: string[]):
44
100
 
45
101
  return exportNames.includes(expectedFactoryName) ? expectedFactoryName : null;
46
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,5 +1,3 @@
1
- import type { AnalysisComponent, AnalysisResult } from '../analyze/types.js';
2
-
3
1
  /***
4
2
  * Serializable model consumed by renderers and writers.
5
3
  */
@@ -7,22 +5,45 @@ export interface DocumentationModel {
7
5
  packageName: string;
8
6
  packageId: string;
9
7
  description: string | null;
10
- exports: {
11
- name: string;
12
- description: string | null;
13
- kind: string;
14
- }[];
15
- components: AnalysisComponent[];
16
- usage: {
17
- command: string;
18
- } | null;
19
- config: {
20
- exportName: string;
21
- configFile: string;
22
- factoryName: string | null;
23
- } | null;
8
+ usage: UsageModel | null;
9
+ config: ConfigModel | null;
10
+ exports: ExportModel[];
11
+ components: ComponentModel[];
24
12
  }
25
13
 
26
- export type SerializableAnalysisResult = Omit<AnalysisResult, 'exports'> & {
27
- exports: DocumentationModel['exports'];
28
- };
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
+ }
@@ -24,7 +24,9 @@ function renderReadme(model: DocumentationModel): string {
24
24
  if (model.usage !== null) {
25
25
  lines.push('## Usage', '');
26
26
  lines.push('```bash');
27
- lines.push(model.usage.command);
27
+ for (const command of model.usage.commands) {
28
+ lines.push(command.command);
29
+ }
28
30
  lines.push('```', '');
29
31
  }
30
32
 
@@ -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. |
@@ -6,14 +6,14 @@ Kind: `function`
6
6
 
7
7
  Renders the fixture button component.
8
8
 
9
- ## ToolConfig
9
+ ## ButtonProps
10
10
 
11
11
  Kind: `type`
12
12
 
13
- Configuration for the fixture package.
13
+ Props accepted by the fixture button.
14
14
 
15
- ## ButtonProps
15
+ ## ToolConfig
16
16
 
17
17
  Kind: `type`
18
18
 
19
- Props accepted by the fixture button.
19
+ Configuration for the fixture package.
@@ -28,10 +28,10 @@ export default config;
28
28
 
29
29
  Renders the fixture button component.
30
30
 
31
- ### ToolConfig
32
-
33
- Configuration for the fixture package.
34
-
35
31
  ### ButtonProps
36
32
 
37
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.
@@ -4,10 +4,12 @@ import { join } from 'node:path';
4
4
  import { describe, expect, test } from 'bun:test';
5
5
 
6
6
  import { analyze } from '../src/analyze/analyze.js';
7
+ import { createUsageFromPackageJson } from '../src/analyze/usage.js';
7
8
  import { buildModel } from '../src/model/buildModel.js';
8
9
  import { render } from '../src/render/render.js';
9
10
 
10
11
  const fixtureRoot = join(import.meta.dir, 'fixtures/basic');
12
+ const multiBinFixtureRoot = join(import.meta.dir, 'fixtures/multi-bin');
11
13
  const snapshotRoot = join(import.meta.dir, '__snapshots__');
12
14
 
13
15
  describe('analyze', () => {
@@ -30,7 +32,13 @@ describe('analyze', () => {
30
32
  ]);
31
33
  expect(analysis.exports.map((item) => item.name)).not.toContain('internalHelper');
32
34
  expect(analysis.usage).toEqual({
33
- command: 'bunx @fixture/basic',
35
+ packageName: '@fixture/basic',
36
+ commands: [
37
+ {
38
+ name: 'fixture-basic',
39
+ command: 'bunx @fixture/basic',
40
+ },
41
+ ],
34
42
  });
35
43
  expect(analysis.config).toEqual({
36
44
  exportName: 'ToolConfig',
@@ -63,6 +71,79 @@ describe('analyze', () => {
63
71
  await expectSnapshot('basic.exports.md', output.exportsMarkdown);
64
72
  await expectSnapshot('basic.components.md', output.components);
65
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
+ });
66
147
  });
67
148
 
68
149
  async function expectSnapshot(name: string, actual: string): Promise<void> {
@@ -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
+ }