@ankhorage/paradox 0.0.4 → 0.0.6

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.
@@ -7,10 +7,43 @@ interface BuildModelInput {
7
7
  name: string;
8
8
  description: string | null;
9
9
  kind: ExportKind;
10
+ modulePath: string;
11
+ sourceLocation: {
12
+ filePath: string;
13
+ line: number;
14
+ column: number;
15
+ };
16
+ exportPaths: string[];
17
+ relatedSymbols: string[];
18
+ signatures: {
19
+ label: string;
20
+ parameters: {
21
+ name: string;
22
+ type: string;
23
+ required: boolean;
24
+ description: string | null;
25
+ }[];
26
+ returnType: string | null;
27
+ returnDescription: string | null;
28
+ }[];
29
+ members: {
30
+ name: string;
31
+ kind: 'property' | 'method';
32
+ type: string;
33
+ required: boolean;
34
+ description: string | null;
35
+ }[];
10
36
  }[];
11
37
  components: {
12
38
  name: string;
13
39
  description: string | null;
40
+ modulePath: string;
41
+ sourceLocation: {
42
+ filePath: string;
43
+ line: number;
44
+ column: number;
45
+ };
46
+ exportPaths: string[];
14
47
  props: {
15
48
  name: string;
16
49
  type: string;
@@ -28,6 +61,13 @@ interface BuildModelInput {
28
61
  config: {
29
62
  exportName: string;
30
63
  } | null;
64
+ entrypoints: string[];
65
+ modules: {
66
+ path: string;
67
+ isEntrypoint: boolean;
68
+ dependencies: string[];
69
+ exports: string[];
70
+ }[];
31
71
  }
32
72
  /***
33
73
  * Converts analysis output into a serializable documentation model.
@@ -2,7 +2,8 @@
2
2
  * Converts analysis output into a serializable documentation model.
3
3
  */
4
4
  export function buildModel(analysis) {
5
- const exportsByName = new Map(analysis.exports.map((item) => [item.name, mapExport(item)]));
5
+ const exportNames = new Set(analysis.exports.map((item) => item.name));
6
+ const exportsByName = new Map(analysis.exports.map((item) => [item.name, mapExport(item, exportNames)]));
6
7
  const exports = sortByName([...exportsByName.values()]);
7
8
  return {
8
9
  packageName: analysis.packageName,
@@ -26,21 +27,65 @@ export function buildModel(analysis) {
26
27
  ]),
27
28
  }
28
29
  : null,
30
+ entrypoints: [...analysis.entrypoints].sort((a, b) => a.localeCompare(b)),
31
+ modules: [...analysis.modules]
32
+ .map((module) => ({
33
+ path: module.path,
34
+ isEntrypoint: module.isEntrypoint,
35
+ dependencies: [...module.dependencies].sort((a, b) => a.localeCompare(b)),
36
+ exports: [...module.exports].sort((a, b) => a.localeCompare(b)),
37
+ }))
38
+ .sort((left, right) => left.path.localeCompare(right.path)),
29
39
  exports,
30
- components: sortByName(analysis.components.map(mapComponent)),
40
+ components: sortByName(analysis.components.map((component) => mapComponent(component, exportsByName.get(component.name)))),
31
41
  };
32
42
  }
33
- function mapExport(item) {
43
+ function mapExport(item, exportNames) {
34
44
  return {
35
45
  name: item.name,
36
46
  description: item.description,
37
47
  kind: item.kind,
48
+ modulePath: item.modulePath,
49
+ sourceLocation: {
50
+ filePath: item.sourceLocation.filePath,
51
+ line: item.sourceLocation.line,
52
+ column: item.sourceLocation.column,
53
+ },
54
+ exportPaths: [...item.exportPaths].sort((a, b) => a.localeCompare(b)),
55
+ relatedSymbols: item.relatedSymbols
56
+ .filter((symbol) => exportNames.has(symbol))
57
+ .sort((a, b) => a.localeCompare(b)),
58
+ signatures: item.signatures.map((signature) => ({
59
+ label: signature.label,
60
+ parameters: sortByName(signature.parameters.map((parameter) => ({
61
+ name: parameter.name,
62
+ type: parameter.type,
63
+ required: parameter.required,
64
+ description: parameter.description,
65
+ }))),
66
+ returnType: signature.returnType,
67
+ returnDescription: signature.returnDescription,
68
+ })),
69
+ members: sortByName(item.members.map((member) => ({
70
+ name: member.name,
71
+ kind: member.kind,
72
+ type: member.type,
73
+ required: member.required,
74
+ description: member.description,
75
+ }))),
38
76
  };
39
77
  }
40
- function mapComponent(component) {
78
+ function mapComponent(component, exportModel) {
41
79
  return {
42
80
  name: component.name,
43
81
  description: component.description,
82
+ modulePath: component.modulePath,
83
+ sourceLocation: {
84
+ filePath: component.sourceLocation.filePath,
85
+ line: component.sourceLocation.line,
86
+ column: component.sourceLocation.column,
87
+ },
88
+ exportPaths: exportModel?.exportPaths ?? [...component.exportPaths].sort((a, b) => a.localeCompare(b)),
44
89
  props: sortByName(component.props.map((prop) => ({
45
90
  name: prop.name,
46
91
  type: prop.type,
@@ -7,6 +7,8 @@ export interface DocumentationModel {
7
7
  description: string | null;
8
8
  usage: UsageModel | null;
9
9
  config: ConfigModel | null;
10
+ entrypoints: string[];
11
+ modules: ModuleModel[];
10
12
  exports: ExportModel[];
11
13
  components: ComponentModel[];
12
14
  }
@@ -27,13 +29,52 @@ export interface ExportModel {
27
29
  name: string;
28
30
  description: string | null;
29
31
  kind: ExportKind;
32
+ modulePath: string;
33
+ sourceLocation: SourceLocationModel;
34
+ exportPaths: string[];
35
+ relatedSymbols: string[];
36
+ signatures: SignatureModel[];
37
+ members: MemberModel[];
30
38
  }
31
39
  export type ExportKind = 'function' | 'type' | 'unknown';
32
40
  export interface ComponentModel {
33
41
  name: string;
34
42
  description: string | null;
43
+ modulePath: string;
44
+ sourceLocation: SourceLocationModel;
45
+ exportPaths: string[];
35
46
  props: PropModel[];
36
47
  }
48
+ export interface SourceLocationModel {
49
+ filePath: string;
50
+ line: number;
51
+ column: number;
52
+ }
53
+ export interface SignatureModel {
54
+ label: string;
55
+ parameters: ParameterModel[];
56
+ returnType: string | null;
57
+ returnDescription: string | null;
58
+ }
59
+ export interface ParameterModel {
60
+ name: string;
61
+ type: string;
62
+ required: boolean;
63
+ description: string | null;
64
+ }
65
+ export interface MemberModel {
66
+ name: string;
67
+ kind: 'property' | 'method';
68
+ type: string;
69
+ required: boolean;
70
+ description: string | null;
71
+ }
72
+ export interface ModuleModel {
73
+ path: string;
74
+ isEntrypoint: boolean;
75
+ dependencies: string[];
76
+ exports: string[];
77
+ }
37
78
  export interface PropModel {
38
79
  name: string;
39
80
  type: string;
@@ -3,4 +3,6 @@ import type { RenderResult } from './types.js';
3
3
  /***
4
4
  * Renders the documentation model into README and artifact files.
5
5
  */
6
- export declare function render(model: DocumentationModel): RenderResult;
6
+ export declare function render(model: DocumentationModel, options?: {
7
+ outputDir?: string;
8
+ }): RenderResult;
@@ -1,95 +1,29 @@
1
+ import { renderDiagramArtifacts } from './renderers/diagrams.js';
2
+ import { renderHtml } from './renderers/html.js';
3
+ import { renderMarkdown } from './renderers/markdown.js';
1
4
  /***
2
5
  * Renders the documentation model into README and artifact files.
3
6
  */
4
- export function render(model) {
5
- return {
6
- readme: renderReadme(model),
7
- exportsMarkdown: renderExports(model),
8
- components: renderComponents(model),
7
+ export function render(model, options = {}) {
8
+ const diagrams = renderDiagramArtifacts(model);
9
+ const outputDir = options.outputDir ?? 'paradox';
10
+ const result = {
11
+ readme: '',
12
+ exportsMarkdown: '',
13
+ components: '',
9
14
  exportsJson: `${JSON.stringify(model.exports, null, 2)}\n`,
10
15
  paradoxJson: `${JSON.stringify(model, null, 2)}\n`,
16
+ indexHtml: '',
17
+ diagrams,
11
18
  };
12
- }
13
- function renderReadme(model) {
14
- const lines = [`# ${model.packageName}`, ''];
15
- if (model.description) {
16
- lines.push(model.description, '');
17
- }
18
- if (model.usage !== null) {
19
- lines.push('## Usage', '');
20
- lines.push('```bash');
21
- for (const command of model.usage.commands) {
22
- lines.push(command.command);
23
- }
24
- lines.push('```', '');
25
- }
26
- if (model.config !== null) {
27
- lines.push('## Configuration', '');
28
- lines.push(`Create a \`${model.config.configFile}\` file:`, '');
29
- lines.push('```ts');
30
- if (model.config.factoryName !== null) {
31
- lines.push(`import { ${model.config.factoryName} } from '${model.packageId}';`);
32
- lines.push('');
33
- lines.push(`export default ${model.config.factoryName}({`);
34
- lines.push(' // ...');
35
- lines.push('});');
36
- }
37
- else {
38
- lines.push(`import type { ${model.config.exportName} } from '${model.packageId}';`);
39
- lines.push('');
40
- lines.push('const config = {');
41
- lines.push(' // ...');
42
- lines.push(`} satisfies ${model.config.exportName};`);
43
- lines.push('');
44
- lines.push('export default config;');
45
- }
46
- lines.push('```', '');
47
- }
48
- lines.push('## Path resolution', '');
49
- lines.push('- Config discovery: searches upward from `process.cwd()` for `paradox.config.ts/js/mjs/cjs` (required; no fallback).');
50
- lines.push('- Package root: defaults to the directory containing `paradox.config.*`; `package.root` (when relative) resolves relative to that directory.');
51
- lines.push('- Output directory: defaults to `paradox/`; `output.dir` (when relative) resolves relative to the resolved package root and must stay inside it.');
52
- lines.push('- Modes:');
53
- lines.push(' - `safe`: writes generated artifacts only under the output directory');
54
- lines.push(' - `write`: additionally updates `<packageRoot>/README.md`', '');
55
- if (model.exports.length > 0) {
56
- lines.push('## Public API', '');
57
- for (const item of model.exports) {
58
- lines.push(`### ${item.name}`, '');
59
- lines.push(item.description ?? `\`${item.kind}\` export.`, '');
60
- }
61
- }
62
- return `${lines.join('\n').trimEnd()}\n`;
63
- }
64
- function renderExports(model) {
65
- const lines = ['# Public API', ''];
66
- for (const item of model.exports) {
67
- lines.push(`## ${item.name}`, '');
68
- lines.push(`Kind: \`${item.kind}\``, '');
69
- if (item.description) {
70
- lines.push(item.description, '');
71
- }
72
- }
73
- return `${lines.join('\n').trimEnd()}\n`;
74
- }
75
- function renderComponents(model) {
76
- const lines = ['# Components', ''];
77
- for (const component of model.components) {
78
- lines.push(`## ${component.name}`, '');
79
- if (component.description) {
80
- lines.push(component.description, '');
81
- }
82
- if (component.props.length > 0) {
83
- lines.push('| Prop | Type | Required | Description |');
84
- lines.push('| --- | --- | --- | --- |');
85
- for (const prop of component.props) {
86
- lines.push(`| ${escapeTableCell(prop.name)} | \`${escapeTableCell(prop.type)}\` | ${prop.required ? 'yes' : 'no'} | ${escapeTableCell(prop.description ?? '')} |`);
87
- }
88
- lines.push('');
89
- }
19
+ const context = {
20
+ model,
21
+ outputDir,
22
+ diagrams,
23
+ result,
24
+ };
25
+ for (const renderer of [renderMarkdown, renderHtml]) {
26
+ Object.assign(result, renderer(context));
90
27
  }
91
- return `${lines.join('\n').trimEnd()}\n`;
92
- }
93
- function escapeTableCell(value) {
94
- return value.replaceAll('|', '\\|');
28
+ return result;
95
29
  }
@@ -0,0 +1,6 @@
1
+ import type { DocumentationModel } from '../../model/types.js';
2
+ import type { DiagramArtifact } from '../types.js';
3
+ /***
4
+ * Generates deterministic Mermaid diagrams for the documentation app.
5
+ */
6
+ export declare function renderDiagramArtifacts(model: DocumentationModel): DiagramArtifact[];
@@ -0,0 +1,106 @@
1
+ /***
2
+ * Generates deterministic Mermaid diagrams for the documentation app.
3
+ */
4
+ export function renderDiagramArtifacts(model) {
5
+ return [
6
+ {
7
+ path: 'diagrams/architecture-overview.mmd',
8
+ title: 'Architecture overview',
9
+ content: renderArchitectureOverview(model),
10
+ },
11
+ {
12
+ path: 'diagrams/module-relationships.mmd',
13
+ title: 'Module relationships',
14
+ content: renderModuleRelationships(model),
15
+ },
16
+ {
17
+ path: 'diagrams/export-graph.mmd',
18
+ title: 'Export graph',
19
+ content: renderExportGraph(model),
20
+ },
21
+ {
22
+ path: 'diagrams/entrypoint-sequence.mmd',
23
+ title: 'Entrypoint sequence',
24
+ content: renderEntrypointSequence(model),
25
+ },
26
+ ];
27
+ }
28
+ function renderArchitectureOverview(model) {
29
+ const lines = ['graph TD'];
30
+ const packageId = toMermaidId(`package-${model.packageId}`);
31
+ lines.push(` ${packageId}["${escapeLabel(model.packageName)}"]`);
32
+ for (const entrypoint of model.entrypoints) {
33
+ const entrypointId = toMermaidId(`entrypoint-${entrypoint}`);
34
+ lines.push(` ${entrypointId}["${escapeLabel(entrypoint)}"]`);
35
+ lines.push(` ${packageId} --> ${entrypointId}`);
36
+ }
37
+ for (const module of model.modules) {
38
+ const moduleId = toMermaidId(`module-${module.path}`);
39
+ lines.push(` ${moduleId}["${escapeLabel(module.path)}"]`);
40
+ if (!module.isEntrypoint) {
41
+ lines.push(` ${packageId} -.-> ${moduleId}`);
42
+ }
43
+ for (const dependency of module.dependencies) {
44
+ lines.push(` ${moduleId} --> ${toMermaidId(`module-${dependency}`)}`);
45
+ }
46
+ }
47
+ return `${lines.join('\n')}\n`;
48
+ }
49
+ function renderModuleRelationships(model) {
50
+ const lines = ['graph LR'];
51
+ for (const module of model.modules) {
52
+ lines.push(` ${toMermaidId(`module-${module.path}`)}["${escapeLabel(module.path)}"]`);
53
+ }
54
+ const edges = model.modules.flatMap((module) => module.dependencies.map((dependency) => ` ${toMermaidId(`module-${module.path}`)} --> ${toMermaidId(`module-${dependency}`)}`));
55
+ lines.push(...(edges.length > 0 ? edges : renderFallbackEdge(model.modules, 'module')));
56
+ return `${lines.join('\n')}\n`;
57
+ }
58
+ function renderExportGraph(model) {
59
+ const lines = ['graph LR'];
60
+ const exportedNames = new Set(model.exports.map((item) => item.name));
61
+ for (const module of model.modules) {
62
+ lines.push(` ${toMermaidId(`module-${module.path}`)}["${escapeLabel(module.path)}"]`);
63
+ }
64
+ for (const item of model.exports) {
65
+ const exportId = toMermaidId(`export-${item.name}`);
66
+ lines.push(` ${exportId}["${escapeLabel(item.name)}"]`);
67
+ lines.push(` ${toMermaidId(`module-${item.modulePath}`)} --> ${exportId}`);
68
+ for (const relatedSymbol of item.relatedSymbols.filter((symbol) => exportedNames.has(symbol))) {
69
+ lines.push(` ${exportId} -.-> ${toMermaidId(`export-${relatedSymbol}`)}`);
70
+ }
71
+ }
72
+ return `${lines.join('\n')}\n`;
73
+ }
74
+ function renderEntrypointSequence(model) {
75
+ const lines = ['sequenceDiagram'];
76
+ const participants = new Map();
77
+ for (const module of model.modules) {
78
+ participants.set(module.path, `participant ${toMermaidId(`participant-${module.path}`)} as ${module.path}`);
79
+ }
80
+ lines.push(...participants.values());
81
+ const interactions = model.modules.flatMap((module) => module.dependencies.map((dependency) => ` ${toMermaidId(`participant-${module.path}`)}->>${toMermaidId(`participant-${dependency}`)}: imports`));
82
+ if (interactions.length === 0) {
83
+ const packageId = toMermaidId(`participant-${model.packageId}`);
84
+ lines.push(` participant ${packageId} as ${model.packageName}`);
85
+ lines.push(` Note over ${packageId}: No internal module relationships detected.`);
86
+ }
87
+ else {
88
+ lines.push(...interactions);
89
+ }
90
+ return `${lines.join('\n')}\n`;
91
+ }
92
+ function renderFallbackEdge(modules, prefix) {
93
+ if (modules.length === 0) {
94
+ return [' empty["No modules analyzed"]'];
95
+ }
96
+ return modules.slice(1).map((module, index) => {
97
+ const previous = modules[index];
98
+ return ` ${toMermaidId(`${prefix}-${previous.path}`)} -.-> ${toMermaidId(`${prefix}-${module.path}`)}`;
99
+ });
100
+ }
101
+ function toMermaidId(value) {
102
+ return value.replace(/[^A-Za-z0-9_]/g, '_');
103
+ }
104
+ function escapeLabel(value) {
105
+ return value.replaceAll('"', '&quot;');
106
+ }
@@ -0,0 +1,5 @@
1
+ import type { RenderContext } from '../types.js';
2
+ /***
3
+ * Renders a deterministic static HTML documentation app.
4
+ */
5
+ export declare function renderHtml({ diagrams, model, }: RenderContext): Pick<RenderContext['result'], 'indexHtml'>;