@ankhorage/paradox 0.0.10 → 0.1.0

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.0
4
+
5
+ ### Minor Changes
6
+
7
+ - dd41897: Generate compact README docs from `@readme` symbols.
8
+
3
9
  ## 0.0.10
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -1,15 +1,41 @@
1
+ <!-- markdownlint-disable MD013 MD033 -->
2
+ <!-- This file is generated by Paradox. Do not edit manually. -->
3
+
1
4
  # @ankhorage/paradox
2
5
 
3
- ![license: MIT](./paradox/badges/license.svg) ![npm: v0.0.9](./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.0.10](./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)
4
7
 
5
8
  Deterministic documentation generator for TypeScript packages.
6
9
 
7
- ## Usage
10
+ ## Installation
8
11
 
9
12
  ```bash
10
13
  bunx @ankhorage/paradox
11
14
  ```
12
15
 
16
+ ## Documentation Tags
17
+
18
+ <details>
19
+ <summary>@readme</summary>
20
+
21
+ Includes a documentation block or exported symbol in README output.
22
+
23
+ </details>
24
+
25
+ <details>
26
+ <summary>@config</summary>
27
+
28
+ 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.
29
+
30
+ </details>
31
+
32
+ <details>
33
+ <summary>@example</summary>
34
+
35
+ Adds a titled fenced code example to the generated documentation for a symbol.
36
+
37
+ </details>
38
+
13
39
  ## Configuration
14
40
 
15
41
  Create a `paradox.config.ts` file:
@@ -22,14 +48,17 @@ export default defineParadoxConfig({
22
48
  });
23
49
  ```
24
50
 
25
- ### Configuration options
51
+ <details>
52
+ <summary>Configuration options</summary>
26
53
 
27
54
  | Field | Type | Required | Default | Description |
28
55
  | ------- | --------------------------------------------------------- | -------- | ------- | ----------- |
29
- | mode | `'safe' \| 'write' \| undefined` | no | | |
30
- | docs | `{ title?: string; description?: string; } \| undefined` | no | | |
31
- | package | `{ root?: string; entrypoints?: string[]; } \| undefined` | no | | |
32
- | output | `{ dir?: string; } \| undefined` | no | | |
56
+ | mode | `'safe' \| 'write' \| undefined` | no | — | |
57
+ | docs | `{ title?: string; description?: string; } \| undefined` | no | — | |
58
+ | package | `{ root?: string; entrypoints?: string[]; } \| undefined` | no | — | |
59
+ | output | `{ dir?: string; } \| undefined` | no | — | |
60
+
61
+ </details>
33
62
 
34
63
  ## Generated documentation
35
64
 
@@ -43,6 +72,9 @@ export default defineParadoxConfig({
43
72
 
44
73
  ## Architecture preview
45
74
 
75
+ <details>
76
+ <summary>Architecture overview</summary>
77
+
46
78
  ```mermaid
47
79
  graph TD
48
80
  package__ankhorage_paradox["@ankhorage/paradox"]
@@ -212,6 +244,8 @@ graph TD
212
244
  module_src_write_write_ts --> module_src_render_types_ts
213
245
  ```
214
246
 
247
+ </details>
248
+
215
249
  ## Path resolution
216
250
 
217
251
  - Config discovery: searches upward from `process.cwd()` for `paradox.config.ts/js/mjs/cjs` (required; no fallback).
@@ -223,21 +257,29 @@ graph TD
223
257
 
224
258
  ## Public API
225
259
 
226
- ### defineParadoxConfig
260
+ ### Configuration
261
+
262
+ <details>
263
+ <summary>defineParadoxConfig</summary>
264
+
265
+ ```ts
266
+ defineParadoxConfig(config: ParadoxConfig) => ParadoxConfig
267
+ ```
227
268
 
228
269
  Defines a Paradox configuration object without changing its shape.
229
270
 
230
- - Kind: `function`
231
- - Module: `src/config/defineParadoxConfig.ts`
232
- - Source: `src/config/defineParadoxConfig.ts:6:1`
233
- - Export paths: `src/index.ts`
234
- - Related symbols: `ParadoxConfig`
271
+ Module: `src/config/defineParadoxConfig.ts`
272
+ Source: `src/config/defineParadoxConfig.ts:8:1`
273
+ Related symbols: `ParadoxConfig`
235
274
 
236
- ### ParadoxConfig
275
+ </details>
276
+
277
+ <details>
278
+ <summary>ParadoxConfig</summary>
237
279
 
238
280
  Configuration for running Paradox.
239
281
 
240
- - Kind: `type`
241
- - Module: `src/config/types.ts`
242
- - Source: `src/config/types.ts:6:1`
243
- - Export paths: `src/index.ts`
282
+ Module: `src/config/types.ts`
283
+ Source: `src/config/types.ts:7:1`
284
+
285
+ </details>
@@ -1,8 +1,5 @@
1
1
  import type { ParadoxConfig } from '../config/types.js';
2
2
  import type { AnalysisResult } from './types.js';
3
- /***
4
- * Runs the source analysis pipeline for a configured package.
5
- */
6
3
  export declare function analyze(config: ParadoxConfig, runtime: {
7
4
  packageRoot: string;
8
5
  }): Promise<AnalysisResult>;
@@ -9,9 +9,6 @@ import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
9
9
  import { collectTypeMembers, resolveTypeReference } from './semantic/exports.js';
10
10
  import { collectCallGraph, collectComponentCompositionGraph, collectImportGraph, } from './semantic/graphs.js';
11
11
  import { createUsageFromPackageJson } from './usage.js';
12
- /***
13
- * Runs the source analysis pipeline for a configured package.
14
- */
15
12
  export async function analyze(config, runtime) {
16
13
  const root = runtime.packageRoot;
17
14
  const pkg = await readPackageJson(root);
@@ -20,15 +17,9 @@ export async function analyze(config, runtime) {
20
17
  const project = createProject(root);
21
18
  const entrypoints = config.package?.entrypoints ?? ['src/index.ts'];
22
19
  const program = createTypeScriptProgram({ root, entrypoints, project });
23
- const { config: configMetadata, exports } = analyzeExports(project, {
24
- root,
25
- entrypoints,
26
- });
20
+ const { config: configMetadata, exports } = analyzeExports(project, { root, entrypoints });
27
21
  const components = analyzeComponents(exports, { program });
28
- const modules = analyzeModules(project, {
29
- root,
30
- entrypoints,
31
- });
22
+ const modules = analyzeModules(project, { root, entrypoints });
32
23
  const configExport = configMetadata
33
24
  ? (exports.find((entry) => entry.name === configMetadata.exportName) ?? null)
34
25
  : null;
@@ -63,6 +54,7 @@ export async function analyze(config, runtime) {
63
54
  config: configMetadata
64
55
  ? {
65
56
  exportName: configMetadata.exportName,
57
+ isReadme: configMetadata.isReadme,
66
58
  members: mapTypeMembers(configMembers),
67
59
  }
68
60
  : null,
@@ -17,6 +17,7 @@ export function analyzeComponents(exports, options = {}) {
17
17
  name: member.name,
18
18
  type: member.type,
19
19
  required: member.required,
20
+ ...(member.defaultValue !== undefined ? { defaultValue: member.defaultValue } : {}),
20
21
  description: member.description ?? null,
21
22
  })) ?? [];
22
23
  const propsType = getComponentPropsType(e.node);
@@ -25,6 +26,8 @@ export function analyzeComponents(exports, options = {}) {
25
26
  components.push({
26
27
  name: e.name,
27
28
  description: e.description,
29
+ isReadme: e.isReadme,
30
+ examples: e.examples,
28
31
  modulePath: e.modulePath,
29
32
  sourceLocation: e.sourceLocation,
30
33
  exportPaths: e.exportPaths,
@@ -4,6 +4,7 @@ interface AnalyzeExportsResult {
4
4
  exports: AnalysisExport[];
5
5
  config: {
6
6
  exportName: string;
7
+ isReadme: boolean;
7
8
  } | null;
8
9
  }
9
10
  /***
@@ -19,13 +19,12 @@ export function analyzeExports(project, options) {
19
19
  continue;
20
20
  }
21
21
  const rawComment = getParadoxComment(decl);
22
- const parsed = rawComment
23
- ? parseParadoxComment(rawComment)
24
- : { description: null, isConfig: false, params: {}, returns: null };
22
+ const parsed = rawComment ? parseParadoxComment(rawComment) : createEmptyMetadata();
25
23
  const name = resolved.getName();
26
24
  if (parsed.isConfig) {
27
25
  config = {
28
26
  exportName: name,
27
+ isReadme: parsed.isReadme,
29
28
  };
30
29
  }
31
30
  const metadata = getExportMetadata({
@@ -40,6 +39,8 @@ export function analyzeExports(project, options) {
40
39
  ? {
41
40
  ...existing,
42
41
  description: existing.description ?? parsed.description,
42
+ isReadme: existing.isReadme || parsed.isReadme,
43
+ examples: existing.examples.length > 0 ? existing.examples : parsed.examples,
43
44
  exportPaths: uniqueSorted([...existing.exportPaths, ...metadata.exportPaths]),
44
45
  relatedSymbols: uniqueSorted([
45
46
  ...existing.relatedSymbols,
@@ -52,6 +53,8 @@ export function analyzeExports(project, options) {
52
53
  name,
53
54
  node: decl,
54
55
  description: parsed.description,
56
+ isReadme: parsed.isReadme,
57
+ examples: parsed.examples,
55
58
  kind: inferKind(decl),
56
59
  ...metadata,
57
60
  });
@@ -87,3 +90,13 @@ function uniqueSorted(values) {
87
90
  function toPosixPath(path) {
88
91
  return path.replaceAll('\\', '/');
89
92
  }
93
+ function createEmptyMetadata() {
94
+ return {
95
+ description: null,
96
+ isConfig: false,
97
+ isReadme: false,
98
+ examples: [],
99
+ params: {},
100
+ returns: null,
101
+ };
102
+ }
@@ -1,4 +1,9 @@
1
1
  import type { Node } from 'ts-morph';
2
+ interface AnalysisExample {
3
+ title: string | null;
4
+ language: string | null;
5
+ code: string;
6
+ }
2
7
  /***
3
8
  * Describes one exported declaration discovered in a package.
4
9
  */
@@ -33,6 +38,8 @@ export interface AnalysisExport {
33
38
  name: string;
34
39
  node: Node;
35
40
  description: string | null;
41
+ isReadme: boolean;
42
+ examples: AnalysisExample[];
36
43
  kind: 'function' | 'type' | 'unknown';
37
44
  modulePath: string;
38
45
  sourceLocation: AnalysisSourceLocation;
@@ -47,6 +54,8 @@ export interface AnalysisExport {
47
54
  export interface AnalysisComponent {
48
55
  name: string;
49
56
  description: string | null;
57
+ isReadme: boolean;
58
+ examples: AnalysisExample[];
50
59
  modulePath: string;
51
60
  sourceLocation: AnalysisSourceLocation;
52
61
  exportPaths: string[];
@@ -54,6 +63,7 @@ export interface AnalysisComponent {
54
63
  name: string;
55
64
  type: string;
56
65
  required: boolean;
66
+ defaultValue?: string;
57
67
  description: string | null;
58
68
  }[];
59
69
  }
@@ -129,6 +139,7 @@ export interface AnalysisResult {
129
139
  usage: AnalysisUsage | null;
130
140
  config: {
131
141
  exportName: string;
142
+ isReadme: boolean;
132
143
  members: AnalysisTypeMember[];
133
144
  } | null;
134
145
  graphs: AnalysisGraphs;
@@ -4,9 +4,16 @@
4
4
  interface ParsedParadoxComment {
5
5
  description: string | null;
6
6
  isConfig: boolean;
7
+ isReadme: boolean;
8
+ examples: ParsedExample[];
7
9
  params: Record<string, string>;
8
10
  returns: string | null;
9
11
  }
12
+ interface ParsedExample {
13
+ title: string | null;
14
+ language: string | null;
15
+ code: string;
16
+ }
10
17
  /***
11
18
  * Parses a Paradox doc comment into structured metadata.
12
19
  */
@@ -2,20 +2,29 @@
2
2
  * Parses a Paradox doc comment into structured metadata.
3
3
  */
4
4
  export function parseParadoxComment(rawComment) {
5
- const lines = rawComment
6
- .replace(/^\/\*\*\*/, '')
7
- .replace(/\*\/$/, '')
8
- .split('\n')
9
- .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
5
+ const lines = normalizeCommentLines(rawComment);
6
+ const descriptionLines = [];
7
+ const examples = [];
10
8
  let isConfig = false;
9
+ let isReadme = false;
11
10
  const params = {};
12
11
  let returns = null;
13
- const description = lines
14
- .filter((line) => {
12
+ for (let index = 0; index < lines.length; index += 1) {
13
+ const line = lines[index] ?? '';
15
14
  const trimmed = line.trimStart();
16
15
  if (trimmed.startsWith('@config')) {
17
16
  isConfig = true;
18
- return false;
17
+ continue;
18
+ }
19
+ if (trimmed.startsWith('@readme')) {
20
+ isReadme = true;
21
+ continue;
22
+ }
23
+ if (trimmed.startsWith('@example')) {
24
+ const parsed = parseExample(lines, index);
25
+ examples.push(parsed.example);
26
+ index = parsed.nextIndex;
27
+ continue;
19
28
  }
20
29
  if (trimmed.startsWith('@param ')) {
21
30
  const paramBody = trimmed.slice('@param '.length).trim();
@@ -23,21 +32,59 @@ export function parseParadoxComment(rawComment) {
23
32
  if (name) {
24
33
  params[name] = descriptionParts.join(' ').trim();
25
34
  }
26
- return false;
35
+ continue;
27
36
  }
28
37
  if (trimmed.startsWith('@returns') || trimmed.startsWith('@return')) {
29
38
  const returnBody = trimmed.replace(/^@returns?/, '').trim();
30
39
  returns = returnBody.length > 0 ? returnBody : null;
31
- return false;
40
+ continue;
32
41
  }
33
- return true;
34
- })
35
- .join('\n')
36
- .trim();
42
+ descriptionLines.push(line);
43
+ }
44
+ const description = descriptionLines.join('\n').trim();
37
45
  return {
38
46
  description: description.length > 0 ? description : null,
39
47
  isConfig,
48
+ isReadme,
49
+ examples,
40
50
  params,
41
51
  returns,
42
52
  };
43
53
  }
54
+ function parseExample(lines, startIndex) {
55
+ const header = lines[startIndex]?.trimStart() ?? '';
56
+ const title = header.slice('@example'.length).trim();
57
+ let language = null;
58
+ const codeLines = [];
59
+ let index = startIndex + 1;
60
+ while (index < lines.length && (lines[index] ?? '').trim() === '') {
61
+ index += 1;
62
+ }
63
+ const firstCodeLine = lines[index]?.trim() ?? '';
64
+ if (firstCodeLine.startsWith('```')) {
65
+ language = firstCodeLine.slice('```'.length).trim() || null;
66
+ index += 1;
67
+ while (index < lines.length) {
68
+ const current = lines[index] ?? '';
69
+ if (current.trim() === '```')
70
+ break;
71
+ codeLines.push(current);
72
+ index += 1;
73
+ }
74
+ }
75
+ return {
76
+ example: {
77
+ title: title.length > 0 ? title : null,
78
+ language,
79
+ code: codeLines.join('\n').trimEnd(),
80
+ },
81
+ nextIndex: index,
82
+ };
83
+ }
84
+ function normalizeCommentLines(rawComment) {
85
+ return rawComment
86
+ .replace(/^\/\*\*\*/, '')
87
+ .replace(/\*\/$/, '')
88
+ .split('\n')
89
+ .map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
90
+ }
@@ -1,5 +1,7 @@
1
1
  import type { ParadoxConfig } from './types.js';
2
2
  /***
3
3
  * Defines a Paradox configuration object without changing its shape.
4
+ *
5
+ * @readme
4
6
  */
5
7
  export declare function defineParadoxConfig(config: ParadoxConfig): ParadoxConfig;
@@ -1,5 +1,7 @@
1
1
  /***
2
2
  * Defines a Paradox configuration object without changing its shape.
3
+ *
4
+ * @readme
3
5
  */
4
6
  export function defineParadoxConfig(config) {
5
7
  return config;
@@ -2,6 +2,7 @@
2
2
  * Configuration for running Paradox.
3
3
  *
4
4
  * @config
5
+ * @readme
5
6
  */
6
7
  export interface ParadoxConfig {
7
8
  mode?: 'safe' | 'write';
@@ -1,4 +1,9 @@
1
1
  import type { DocumentationModel, ExportKind } from './types.js';
2
+ interface ExampleInput {
3
+ title: string | null;
4
+ language: string | null;
5
+ code: string;
6
+ }
2
7
  interface ExportMemberInput {
3
8
  name: string;
4
9
  kind: 'property' | 'method';
@@ -31,6 +36,8 @@ interface BuildModelInput {
31
36
  exports: {
32
37
  name: string;
33
38
  description: string | null;
39
+ isReadme: boolean;
40
+ examples: ExampleInput[];
34
41
  kind: ExportKind;
35
42
  modulePath: string;
36
43
  sourceLocation: {
@@ -56,6 +63,8 @@ interface BuildModelInput {
56
63
  components: {
57
64
  name: string;
58
65
  description: string | null;
66
+ isReadme: boolean;
67
+ examples: ExampleInput[];
59
68
  modulePath: string;
60
69
  sourceLocation: {
61
70
  filePath: string;
@@ -67,6 +76,7 @@ interface BuildModelInput {
67
76
  name: string;
68
77
  type: string;
69
78
  required: boolean;
79
+ defaultValue?: string;
70
80
  description: string | null;
71
81
  }[];
72
82
  }[];
@@ -79,6 +89,7 @@ interface BuildModelInput {
79
89
  } | null;
80
90
  config: {
81
91
  exportName: string;
92
+ isReadme: boolean;
82
93
  members: ConfigMemberInput[];
83
94
  } | null;
84
95
  entrypoints: string[];
@@ -27,6 +27,7 @@ export function buildModel(analysis) {
27
27
  config: analysis.config !== null
28
28
  ? {
29
29
  exportName: analysis.config.exportName,
30
+ isReadme: analysis.config.isReadme,
30
31
  configFile: getDefaultConfigFileName(analysis.packageId),
31
32
  factoryName: findConfigFactoryName(analysis.config.exportName, [
32
33
  ...exportsByName.keys(),
@@ -57,6 +58,8 @@ function mapExport(item, exportNames) {
57
58
  return {
58
59
  name: item.name,
59
60
  description: item.description,
61
+ isReadme: item.isReadme,
62
+ examples: item.examples.map((example) => ({ ...example })),
60
63
  kind: item.kind,
61
64
  modulePath: item.modulePath,
62
65
  sourceLocation: {
@@ -95,6 +98,8 @@ function mapComponent(component, exportModel) {
95
98
  return {
96
99
  name: component.name,
97
100
  description: component.description,
101
+ isReadme: component.isReadme,
102
+ examples: component.examples.map((example) => ({ ...example })),
98
103
  modulePath: component.modulePath,
99
104
  sourceLocation: {
100
105
  filePath: component.sourceLocation.filePath,
@@ -106,6 +111,7 @@ function mapComponent(component, exportModel) {
106
111
  name: prop.name,
107
112
  type: prop.type,
108
113
  required: prop.required,
114
+ defaultValue: prop.defaultValue,
109
115
  description: prop.description,
110
116
  }))),
111
117
  };
@@ -30,6 +30,7 @@ interface UsageCommandModel {
30
30
  }
31
31
  interface ConfigModel {
32
32
  exportName: string;
33
+ isReadme: boolean;
33
34
  configFile: string;
34
35
  factoryName: string | null;
35
36
  members: ConfigMemberModel[];
@@ -37,6 +38,8 @@ interface ConfigModel {
37
38
  export interface ExportModel {
38
39
  name: string;
39
40
  description: string | null;
41
+ isReadme: boolean;
42
+ examples: ExampleModel[];
40
43
  kind: ExportKind;
41
44
  modulePath: string;
42
45
  sourceLocation: SourceLocationModel;
@@ -49,11 +52,18 @@ export type ExportKind = 'function' | 'type' | 'unknown';
49
52
  export interface ComponentModel {
50
53
  name: string;
51
54
  description: string | null;
55
+ isReadme: boolean;
56
+ examples: ExampleModel[];
52
57
  modulePath: string;
53
58
  sourceLocation: SourceLocationModel;
54
59
  exportPaths: string[];
55
60
  props: PropModel[];
56
61
  }
62
+ interface ExampleModel {
63
+ title: string | null;
64
+ language: string | null;
65
+ code: string;
66
+ }
57
67
  interface SourceLocationModel {
58
68
  filePath: string;
59
69
  line: number;
@@ -91,6 +101,7 @@ interface PropModel {
91
101
  name: string;
92
102
  type: string;
93
103
  required: boolean;
104
+ defaultValue?: string;
94
105
  description: string | null;
95
106
  }
96
107
  interface ConfigMemberModel {
@@ -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,68 @@ 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
  }
35
+ renderDocumentationTags(lines);
36
+ if (model.config?.isReadme) {
37
+ renderConfiguration(lines, model);
38
+ }
39
+ renderGeneratedDocumentation(lines, outputDir, diagrams);
40
+ renderArchitecturePreview(lines, diagrams);
41
+ renderPathResolution(lines);
42
+ renderReadmeApi(lines, model);
43
+ return `${lines.join('\n').trimEnd()}\n`;
44
+ }
45
+ function renderDocumentationTags(lines) {
46
+ lines.push('## Documentation Tags', '');
47
+ for (const tag of DOCUMENTATION_TAGS) {
48
+ lines.push('<details>');
49
+ lines.push(`<summary>@${tag.name}</summary>`, '');
50
+ lines.push(tag.description, '');
51
+ lines.push('</details>', '');
52
+ }
53
+ }
54
+ function renderConfiguration(lines, model) {
29
55
  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('});');
40
- }
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;');
49
- }
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('');
56
+ if (config === null)
57
+ return;
58
+ lines.push('## Configuration', '');
59
+ lines.push(`Create a \`${config.configFile}\` file:`, '');
60
+ lines.push('```ts');
61
+ if (config.factoryName !== null) {
62
+ lines.push(`import { ${config.factoryName} } from '${model.packageId}';`);
63
+ lines.push('');
64
+ lines.push(`export default ${config.factoryName}({`);
65
+ lines.push(' // ...');
66
+ lines.push('});');
67
+ }
68
+ else {
69
+ lines.push(`import type { ${config.exportName} } from '${model.packageId}';`);
70
+ lines.push('');
71
+ lines.push('const config = {');
72
+ lines.push(' // ...');
73
+ lines.push(`} satisfies ${config.exportName};`);
74
+ lines.push('');
75
+ lines.push('export default config;');
76
+ }
77
+ lines.push('```', '');
78
+ if (config.members.length > 0) {
79
+ lines.push('<details>');
80
+ lines.push('<summary>Configuration options</summary>', '');
81
+ lines.push('| Field | Type | Required | Default | Description |');
82
+ lines.push('| --- | --- | --- | --- | --- |');
83
+ for (const configMember of flattenConfigMembers(config.members)) {
84
+ lines.push(`| ${escapeTableCell(configMember.path)} | \`${escapeTableCell(configMember.type)}\` | ${configMember.required ? 'yes' : 'no'} | ${renderDefault(configMember.defaultValue)} | ${escapeTableCell(configMember.description ?? '')} |`);
59
85
  }
86
+ lines.push('', '</details>', '');
60
87
  }
88
+ }
89
+ function renderGeneratedDocumentation(lines, outputDir, diagrams) {
61
90
  lines.push('## Generated documentation', '');
62
91
  lines.push(`- [Interactive documentation app](./${outputDir}/index.html)`);
63
92
  lines.push(`- [Public API reference](./${outputDir}/exports.md)`);
@@ -66,12 +95,19 @@ function renderReadme(model, outputDir, badges, diagrams) {
66
95
  lines.push(`- [${diagram.title}](./${outputDir}/${diagram.path})`);
67
96
  }
68
97
  lines.push('');
98
+ }
99
+ function renderArchitecturePreview(lines, diagrams) {
69
100
  lines.push('## Architecture preview', '');
70
101
  if (diagrams.length > 0) {
102
+ lines.push('<details>');
103
+ lines.push('<summary>Architecture overview</summary>', '');
71
104
  lines.push('```mermaid');
72
- lines.push(diagrams[0].content.trimEnd());
105
+ lines.push(diagrams[0]?.content.trimEnd() ?? '');
73
106
  lines.push('```', '');
107
+ lines.push('</details>', '');
74
108
  }
109
+ }
110
+ function renderPathResolution(lines) {
75
111
  lines.push('## Path resolution', '');
76
112
  lines.push('- Config discovery: searches upward from `process.cwd()` for `paradox.config.ts/js/mjs/cjs` (required; no fallback).');
77
113
  lines.push('- Package root: defaults to the directory containing `paradox.config.*`; `package.root` (when relative) resolves relative to that directory.');
@@ -79,22 +115,131 @@ function renderReadme(model, outputDir, badges, diagrams) {
79
115
  lines.push('- Modes:');
80
116
  lines.push(' - `safe`: writes generated artifacts only under the output directory');
81
117
  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(', ')}`);
118
+ }
119
+ function renderReadmeApi(lines, model) {
120
+ const groups = getReadmeGroups(model);
121
+ if (groups.length === 0)
122
+ return;
123
+ lines.push('## Public API', '');
124
+ for (const group of groups) {
125
+ lines.push(`### ${group.title}`, '');
126
+ for (const item of group.items) {
127
+ if (item.kind === 'component') {
128
+ renderComponentAccordion(lines, item.component, item.exportEntry);
129
+ }
130
+ else {
131
+ renderExportAccordion(lines, item.exportEntry);
93
132
  }
94
- lines.push('');
95
133
  }
96
134
  }
97
- return `${lines.join('\n').trimEnd()}\n`;
135
+ }
136
+ function renderComponentAccordion(lines, component, exportEntry) {
137
+ lines.push('<details>');
138
+ lines.push(`<summary>${component.name}</summary>`, '');
139
+ renderSignature(lines, exportEntry);
140
+ if (component.description)
141
+ lines.push(component.description, '');
142
+ renderExamples(lines, component.examples);
143
+ if (exportEntry && exportEntry.relatedSymbols.length > 0) {
144
+ lines.push(`Related types: ${exportEntry.relatedSymbols.map((symbol) => `\`${symbol}\``).join(', ')}`, '');
145
+ }
146
+ if (component.props.length > 0) {
147
+ lines.push('<details>');
148
+ lines.push('<summary>Props</summary>', '');
149
+ lines.push('| Prop | Type | Required | Default | Description |');
150
+ lines.push('| --- | --- | --- | --- | --- |');
151
+ for (const prop of component.props) {
152
+ lines.push(`| ${escapeTableCell(prop.name)} | \`${escapeTableCell(prop.type)}\` | ${prop.required ? 'yes' : 'no'} | ${renderDefault(prop.defaultValue)} | ${escapeTableCell(prop.description ?? '')} |`);
153
+ }
154
+ lines.push('', '</details>', '');
155
+ }
156
+ lines.push('</details>', '');
157
+ }
158
+ function renderExportAccordion(lines, item) {
159
+ lines.push('<details>');
160
+ lines.push(`<summary>${item.name}</summary>`, '');
161
+ renderSignature(lines, item);
162
+ lines.push(item.description ?? `\`${item.kind}\` export.`, '');
163
+ renderExamples(lines, item.examples);
164
+ lines.push(`Module: \`${item.modulePath}\``);
165
+ lines.push(`Source: \`${item.sourceLocation.filePath}:${item.sourceLocation.line}:${item.sourceLocation.column}\``);
166
+ if (item.relatedSymbols.length > 0) {
167
+ lines.push(`Related symbols: ${item.relatedSymbols.map((symbol) => `\`${symbol}\``).join(', ')}`);
168
+ }
169
+ lines.push('', '</details>', '');
170
+ }
171
+ function renderSignature(lines, item) {
172
+ const signature = item?.signatures[0]?.label;
173
+ if (!signature)
174
+ return;
175
+ lines.push('```ts');
176
+ lines.push(`${item.name}${signature}`);
177
+ lines.push('```', '');
178
+ }
179
+ function renderExamples(lines, examples) {
180
+ for (const example of examples) {
181
+ if (example.title)
182
+ lines.push(`#### ${example.title}`, '');
183
+ lines.push(`\`\`\`${example.language ?? ''}`);
184
+ lines.push(example.code);
185
+ lines.push('```', '');
186
+ }
187
+ }
188
+ function getReadmeGroups(model) {
189
+ const exportsByName = new Map(model.exports.map((entry) => [entry.name, entry]));
190
+ const componentNames = new Set(model.components.map((component) => component.name));
191
+ const groups = new Map();
192
+ for (const component of model.components.filter((entry) => entry.isReadme)) {
193
+ addReadmeItem(groups, getReadmeCategory(component.modulePath, component.name), {
194
+ kind: 'component',
195
+ component,
196
+ exportEntry: exportsByName.get(component.name),
197
+ });
198
+ }
199
+ for (const item of model.exports.filter((entry) => entry.isReadme)) {
200
+ if (componentNames.has(item.name))
201
+ continue;
202
+ addReadmeItem(groups, getReadmeCategory(item.modulePath, item.name), {
203
+ kind: 'export',
204
+ exportEntry: item,
205
+ });
206
+ }
207
+ return CATEGORY_ORDER.flatMap((title) => {
208
+ const items = groups.get(title);
209
+ if (!items || items.length === 0)
210
+ return [];
211
+ return [{ title, items: sortReadmeItems(items) }];
212
+ });
213
+ }
214
+ function addReadmeItem(groups, title, item) {
215
+ const existing = groups.get(title) ?? [];
216
+ existing.push(item);
217
+ groups.set(title, existing);
218
+ }
219
+ function sortReadmeItems(items) {
220
+ return [...items].sort((left, right) => getReadmeItemName(left).localeCompare(getReadmeItemName(right)));
221
+ }
222
+ function getReadmeItemName(item) {
223
+ return item.kind === 'component' ? item.component.name : item.exportEntry.name;
224
+ }
225
+ function getReadmeCategory(modulePath, name) {
226
+ if (modulePath.includes('/config/'))
227
+ return 'Configuration';
228
+ if (modulePath.includes('/primitives/'))
229
+ return 'Primitives';
230
+ if (modulePath.includes('/components/'))
231
+ return 'Components';
232
+ if (modulePath.includes('/patterns/'))
233
+ return 'Patterns';
234
+ if (modulePath.includes('/layout/'))
235
+ return 'Layout';
236
+ if (modulePath.includes('/hooks/') || /^use[A-Z]/.test(name))
237
+ return 'Hooks';
238
+ if (modulePath.includes('/utils/'))
239
+ return 'Utilities';
240
+ if (modulePath.endsWith('types.ts') || modulePath.includes('/types/'))
241
+ return 'Types';
242
+ return 'Utilities';
98
243
  }
99
244
  function renderExports(model) {
100
245
  const lines = ['# Public API', ''];
@@ -141,10 +286,10 @@ function renderComponents(model) {
141
286
  lines.push(`Export paths: ${component.exportPaths.map((path) => `\`${path}\``).join(', ')}`, '');
142
287
  }
143
288
  if (component.props.length > 0) {
144
- lines.push('| Prop | Type | Required | Description |');
145
- lines.push('| --- | --- | --- | --- |');
289
+ lines.push('| Prop | Type | Required | Default | Description |');
290
+ lines.push('| --- | --- | --- | --- | --- |');
146
291
  for (const prop of component.props) {
147
- lines.push(`| ${escapeTableCell(prop.name)} | \`${escapeTableCell(prop.type)}\` | ${prop.required ? 'yes' : 'no'} | ${escapeTableCell(prop.description ?? '')} |`);
292
+ lines.push(`| ${escapeTableCell(prop.name)} | \`${escapeTableCell(prop.type)}\` | ${prop.required ? 'yes' : 'no'} | ${renderDefault(prop.defaultValue)} | ${escapeTableCell(prop.description ?? '')} |`);
148
293
  }
149
294
  lines.push('');
150
295
  }
@@ -154,6 +299,9 @@ function renderComponents(model) {
154
299
  function escapeTableCell(value) {
155
300
  return value.replaceAll('|', '\\|');
156
301
  }
302
+ function renderDefault(value) {
303
+ return value === undefined ? '—' : `\`${escapeTableCell(value)}\``;
304
+ }
157
305
  function flattenConfigMembers(members, prefix = '') {
158
306
  return members.flatMap((member) => {
159
307
  const path = prefix ? `${prefix}.${member.name}` : member.name;
@@ -177,3 +325,27 @@ function badgeLabel(model, badgePath) {
177
325
  const badge = model.badges.find((entry) => entry.id === id);
178
326
  return badge ? `${badge.label}: ${badge.value}` : badgePath;
179
327
  }
328
+ const CATEGORY_ORDER = [
329
+ 'Configuration',
330
+ 'Primitives',
331
+ 'Components',
332
+ 'Patterns',
333
+ 'Layout',
334
+ 'Hooks',
335
+ 'Utilities',
336
+ 'Types',
337
+ ];
338
+ const DOCUMENTATION_TAGS = [
339
+ {
340
+ name: 'readme',
341
+ description: 'Includes a documentation block or exported symbol in README output.',
342
+ },
343
+ {
344
+ name: 'config',
345
+ 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.',
346
+ },
347
+ {
348
+ name: 'example',
349
+ description: 'Adds a titled fenced code example to the generated documentation for a symbol.',
350
+ },
351
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/paradox",
3
- "version": "0.0.10",
3
+ "version": "0.1.0",
4
4
  "description": "Deterministic documentation generator for TypeScript packages.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {