@ankhorage/paradox 0.0.8 → 0.0.10
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 +12 -0
- package/README.md +63 -1
- package/dist/analyze/analyze.js +44 -2
- package/dist/analyze/components.d.ts +4 -1
- package/dist/analyze/components.js +13 -2
- package/dist/analyze/exports.js +8 -1
- package/dist/analyze/semantic/analyzeProject.d.ts +10 -0
- package/dist/analyze/semantic/analyzeProject.js +62 -0
- package/dist/analyze/semantic/associateDocBlocksWithSymbols.d.ts +5 -0
- package/dist/analyze/semantic/associateDocBlocksWithSymbols.js +51 -0
- package/dist/analyze/semantic/collectSourceFiles.d.ts +5 -0
- package/dist/analyze/semantic/collectSourceFiles.js +22 -0
- package/dist/analyze/semantic/createTypeScriptProgram.d.ts +13 -0
- package/dist/analyze/semantic/createTypeScriptProgram.js +23 -0
- package/dist/analyze/semantic/docBlocks.d.ts +10 -0
- package/dist/analyze/semantic/docBlocks.js +102 -0
- package/dist/analyze/semantic/exports.d.ts +29 -0
- package/dist/analyze/semantic/exports.js +399 -0
- package/dist/analyze/semantic/graphs.d.ts +13 -0
- package/dist/analyze/semantic/graphs.js +151 -0
- package/dist/analyze/semantic/isReactComponent.d.ts +5 -0
- package/dist/analyze/semantic/isReactComponent.js +30 -0
- package/dist/analyze/semantic/model.d.ts +109 -0
- package/dist/analyze/semantic/model.js +1 -0
- package/dist/analyze/semantic/paradoxComment.d.ts +16 -0
- package/dist/analyze/semantic/paradoxComment.js +64 -0
- package/dist/analyze/semantic/tagRegistry.d.ts +2 -0
- package/dist/analyze/semantic/tagRegistry.js +1 -0
- package/dist/analyze/semantic/utils.d.ts +4 -0
- package/dist/analyze/semantic/utils.js +13 -0
- package/dist/analyze/types.d.ts +44 -1
- package/dist/analyze/utils/getExportMetadata.js +30 -12
- package/dist/analyze/utils/getParadoxComment.d.ts +1 -1
- package/dist/analyze/utils/getParadoxComment.js +2 -0
- package/dist/model/buildModel.d.ts +45 -7
- package/dist/model/buildModel.js +10 -0
- package/dist/model/types.d.ts +51 -8
- package/dist/paths/policy.d.ts +0 -15
- package/dist/paths/policy.js +0 -9
- package/dist/render/renderers/diagrams.js +11 -0
- package/dist/render/renderers/markdown.js +31 -7
- package/package.json +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 46bc15e: Add a reusable semantic TypeScript analysis foundation for exports, props, type members, and graph metadata.
|
|
8
|
+
|
|
9
|
+
## 0.0.9
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 8904673: Handle exports and type members without declaration nodes defensively during analysis.
|
|
14
|
+
|
|
3
15
|
## 0.0.8
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ankhorage/paradox
|
|
2
2
|
|
|
3
|
-
         
|
|
4
4
|
|
|
5
5
|
Deterministic documentation generator for TypeScript packages.
|
|
6
6
|
|
|
@@ -22,6 +22,15 @@ export default defineParadoxConfig({
|
|
|
22
22
|
});
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
### Configuration options
|
|
26
|
+
|
|
27
|
+
| Field | Type | Required | Default | Description |
|
|
28
|
+
| ------- | --------------------------------------------------------- | -------- | ------- | ----------- |
|
|
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 | | |
|
|
33
|
+
|
|
25
34
|
## Generated documentation
|
|
26
35
|
|
|
27
36
|
- [Interactive documentation app](./paradox/index.html)
|
|
@@ -46,6 +55,9 @@ graph TD
|
|
|
46
55
|
module_src_analyze_analyze_ts --> module_src_analyze_exports_ts
|
|
47
56
|
module_src_analyze_analyze_ts --> module_src_analyze_modules_ts
|
|
48
57
|
module_src_analyze_analyze_ts --> module_src_analyze_project_ts
|
|
58
|
+
module_src_analyze_analyze_ts --> module_src_analyze_semantic_createTypeScriptProgram_ts
|
|
59
|
+
module_src_analyze_analyze_ts --> module_src_analyze_semantic_exports_ts
|
|
60
|
+
module_src_analyze_analyze_ts --> module_src_analyze_semantic_graphs_ts
|
|
49
61
|
module_src_analyze_analyze_ts --> module_src_analyze_types_ts
|
|
50
62
|
module_src_analyze_analyze_ts --> module_src_analyze_usage_ts
|
|
51
63
|
module_src_analyze_analyze_ts --> module_src_config_types_ts
|
|
@@ -55,6 +67,8 @@ graph TD
|
|
|
55
67
|
module_src_analyze_badges_ts --> module_src_analyze_usage_ts
|
|
56
68
|
module_src_analyze_components_ts["src/analyze/components.ts"]
|
|
57
69
|
package__ankhorage_paradox -.-> module_src_analyze_components_ts
|
|
70
|
+
module_src_analyze_components_ts --> module_src_analyze_semantic_exports_ts
|
|
71
|
+
module_src_analyze_components_ts --> module_src_analyze_semantic_model_ts
|
|
58
72
|
module_src_analyze_components_ts --> module_src_analyze_types_ts
|
|
59
73
|
module_src_analyze_components_ts --> module_src_analyze_utils_getComponentPropsType_ts
|
|
60
74
|
module_src_analyze_components_ts --> module_src_analyze_utils_getPropsFromType_ts
|
|
@@ -71,6 +85,54 @@ graph TD
|
|
|
71
85
|
module_src_analyze_modules_ts --> module_src_analyze_types_ts
|
|
72
86
|
module_src_analyze_project_ts["src/analyze/project.ts"]
|
|
73
87
|
package__ankhorage_paradox -.-> module_src_analyze_project_ts
|
|
88
|
+
module_src_analyze_semantic_analyzeProject_ts["src/analyze/semantic/analyzeProject.ts"]
|
|
89
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_analyzeProject_ts
|
|
90
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_associateDocBlocksWithSymbols_ts
|
|
91
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_collectSourceFiles_ts
|
|
92
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_createTypeScriptProgram_ts
|
|
93
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_docBlocks_ts
|
|
94
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_exports_ts
|
|
95
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_graphs_ts
|
|
96
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_model_ts
|
|
97
|
+
module_src_analyze_semantic_analyzeProject_ts --> module_src_analyze_semantic_tagRegistry_ts
|
|
98
|
+
module_src_analyze_semantic_associateDocBlocksWithSymbols_ts["src/analyze/semantic/associateDocBlocksWithSymbols.ts"]
|
|
99
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_associateDocBlocksWithSymbols_ts
|
|
100
|
+
module_src_analyze_semantic_associateDocBlocksWithSymbols_ts --> module_src_analyze_semantic_model_ts
|
|
101
|
+
module_src_analyze_semantic_associateDocBlocksWithSymbols_ts --> module_src_analyze_semantic_utils_ts
|
|
102
|
+
module_src_analyze_semantic_collectSourceFiles_ts["src/analyze/semantic/collectSourceFiles.ts"]
|
|
103
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_collectSourceFiles_ts
|
|
104
|
+
module_src_analyze_semantic_collectSourceFiles_ts --> module_src_analyze_semantic_model_ts
|
|
105
|
+
module_src_analyze_semantic_collectSourceFiles_ts --> module_src_analyze_semantic_utils_ts
|
|
106
|
+
module_src_analyze_semantic_createTypeScriptProgram_ts["src/analyze/semantic/createTypeScriptProgram.ts"]
|
|
107
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_createTypeScriptProgram_ts
|
|
108
|
+
module_src_analyze_semantic_createTypeScriptProgram_ts --> module_src_analyze_semantic_model_ts
|
|
109
|
+
module_src_analyze_semantic_createTypeScriptProgram_ts --> module_src_analyze_semantic_utils_ts
|
|
110
|
+
module_src_analyze_semantic_docBlocks_ts["src/analyze/semantic/docBlocks.ts"]
|
|
111
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_docBlocks_ts
|
|
112
|
+
module_src_analyze_semantic_docBlocks_ts --> module_src_analyze_semantic_model_ts
|
|
113
|
+
module_src_analyze_semantic_docBlocks_ts --> module_src_analyze_semantic_tagRegistry_ts
|
|
114
|
+
module_src_analyze_semantic_docBlocks_ts --> module_src_analyze_semantic_utils_ts
|
|
115
|
+
module_src_analyze_semantic_exports_ts["src/analyze/semantic/exports.ts"]
|
|
116
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_exports_ts
|
|
117
|
+
module_src_analyze_semantic_exports_ts --> module_src_analyze_semantic_isReactComponent_ts
|
|
118
|
+
module_src_analyze_semantic_exports_ts --> module_src_analyze_semantic_model_ts
|
|
119
|
+
module_src_analyze_semantic_exports_ts --> module_src_analyze_semantic_paradoxComment_ts
|
|
120
|
+
module_src_analyze_semantic_exports_ts --> module_src_analyze_semantic_utils_ts
|
|
121
|
+
module_src_analyze_semantic_graphs_ts["src/analyze/semantic/graphs.ts"]
|
|
122
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_graphs_ts
|
|
123
|
+
module_src_analyze_semantic_graphs_ts --> module_src_analyze_semantic_exports_ts
|
|
124
|
+
module_src_analyze_semantic_graphs_ts --> module_src_analyze_semantic_model_ts
|
|
125
|
+
module_src_analyze_semantic_graphs_ts --> module_src_analyze_semantic_utils_ts
|
|
126
|
+
module_src_analyze_semantic_isReactComponent_ts["src/analyze/semantic/isReactComponent.ts"]
|
|
127
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_isReactComponent_ts
|
|
128
|
+
module_src_analyze_semantic_model_ts["src/analyze/semantic/model.ts"]
|
|
129
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_model_ts
|
|
130
|
+
module_src_analyze_semantic_paradoxComment_ts["src/analyze/semantic/paradoxComment.ts"]
|
|
131
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_paradoxComment_ts
|
|
132
|
+
module_src_analyze_semantic_tagRegistry_ts["src/analyze/semantic/tagRegistry.ts"]
|
|
133
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_tagRegistry_ts
|
|
134
|
+
module_src_analyze_semantic_utils_ts["src/analyze/semantic/utils.ts"]
|
|
135
|
+
package__ankhorage_paradox -.-> module_src_analyze_semantic_utils_ts
|
|
74
136
|
module_src_analyze_types_ts["src/analyze/types.ts"]
|
|
75
137
|
package__ankhorage_paradox -.-> module_src_analyze_types_ts
|
|
76
138
|
module_src_analyze_usage_ts["src/analyze/usage.ts"]
|
package/dist/analyze/analyze.js
CHANGED
|
@@ -5,6 +5,9 @@ import { analyzeComponents } from './components.js';
|
|
|
5
5
|
import { analyzeExports } from './exports.js';
|
|
6
6
|
import { analyzeModules } from './modules.js';
|
|
7
7
|
import { createProject } from './project.js';
|
|
8
|
+
import { createTypeScriptProgram } from './semantic/createTypeScriptProgram.js';
|
|
9
|
+
import { collectTypeMembers, resolveTypeReference } from './semantic/exports.js';
|
|
10
|
+
import { collectCallGraph, collectComponentCompositionGraph, collectImportGraph, } from './semantic/graphs.js';
|
|
8
11
|
import { createUsageFromPackageJson } from './usage.js';
|
|
9
12
|
/***
|
|
10
13
|
* Runs the source analysis pipeline for a configured package.
|
|
@@ -16,15 +19,37 @@ export async function analyze(config, runtime) {
|
|
|
16
19
|
const badges = await analyzeBadges(root, pkg);
|
|
17
20
|
const project = createProject(root);
|
|
18
21
|
const entrypoints = config.package?.entrypoints ?? ['src/index.ts'];
|
|
22
|
+
const program = createTypeScriptProgram({ root, entrypoints, project });
|
|
19
23
|
const { config: configMetadata, exports } = analyzeExports(project, {
|
|
20
24
|
root,
|
|
21
25
|
entrypoints,
|
|
22
26
|
});
|
|
23
|
-
const components = analyzeComponents(exports);
|
|
27
|
+
const components = analyzeComponents(exports, { program });
|
|
24
28
|
const modules = analyzeModules(project, {
|
|
25
29
|
root,
|
|
26
30
|
entrypoints,
|
|
27
31
|
});
|
|
32
|
+
const configExport = configMetadata
|
|
33
|
+
? (exports.find((entry) => entry.name === configMetadata.exportName) ?? null)
|
|
34
|
+
: null;
|
|
35
|
+
const configMembers = configExport && (configExport.kind === 'type' || configExport.kind === 'unknown')
|
|
36
|
+
? collectTypeMembers(program, resolveTypeReference(program, configExport.node) ?? {
|
|
37
|
+
type: configExport.node.getType(),
|
|
38
|
+
name: configExport.name,
|
|
39
|
+
sourcePath: configExport.modulePath,
|
|
40
|
+
symbol: configExport.node.getSymbol() ?? null,
|
|
41
|
+
})
|
|
42
|
+
: [];
|
|
43
|
+
const graphs = {
|
|
44
|
+
imports: collectImportGraph(program),
|
|
45
|
+
calls: collectCallGraph(program),
|
|
46
|
+
typeReferences: exports.flatMap((entry) => entry.relatedSymbols.map((symbol) => ({
|
|
47
|
+
fromSymbol: entry.name,
|
|
48
|
+
toType: symbol,
|
|
49
|
+
sourcePath: entry.modulePath,
|
|
50
|
+
}))),
|
|
51
|
+
componentComposition: collectComponentCompositionGraph(program),
|
|
52
|
+
};
|
|
28
53
|
return {
|
|
29
54
|
packageName: config.docs?.title ?? pkg.name,
|
|
30
55
|
packageId: pkg.name,
|
|
@@ -35,9 +60,26 @@ export async function analyze(config, runtime) {
|
|
|
35
60
|
modules,
|
|
36
61
|
badges,
|
|
37
62
|
usage,
|
|
38
|
-
config: configMetadata
|
|
63
|
+
config: configMetadata
|
|
64
|
+
? {
|
|
65
|
+
exportName: configMetadata.exportName,
|
|
66
|
+
members: mapTypeMembers(configMembers),
|
|
67
|
+
}
|
|
68
|
+
: null,
|
|
69
|
+
graphs,
|
|
39
70
|
};
|
|
40
71
|
}
|
|
72
|
+
function mapTypeMembers(members) {
|
|
73
|
+
return members.map((member) => ({
|
|
74
|
+
name: member.name,
|
|
75
|
+
type: member.type,
|
|
76
|
+
required: member.required,
|
|
77
|
+
description: member.description ?? null,
|
|
78
|
+
...(member.defaultValue !== undefined ? { defaultValue: member.defaultValue } : {}),
|
|
79
|
+
...(member.inheritedFrom !== undefined ? { inheritedFrom: member.inheritedFrom } : {}),
|
|
80
|
+
...(member.children ? { children: mapTypeMembers(member.children) } : {}),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
41
83
|
async function readPackageJson(root) {
|
|
42
84
|
const raw = await readFile(join(root, 'package.json'), 'utf-8');
|
|
43
85
|
return JSON.parse(raw);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { AnalyzedProgram } from './semantic/model.js';
|
|
1
2
|
import type { AnalysisComponent, AnalysisExport } from './types.js';
|
|
2
3
|
/***
|
|
3
4
|
* Extracts React components and their props from analyzed exports.
|
|
4
5
|
*/
|
|
5
|
-
export declare function analyzeComponents(exports: readonly AnalysisExport[]
|
|
6
|
+
export declare function analyzeComponents(exports: readonly AnalysisExport[], options?: {
|
|
7
|
+
program?: AnalyzedProgram;
|
|
8
|
+
}): AnalysisComponent[];
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
import { collectPropsForExport } from './semantic/exports.js';
|
|
1
2
|
import { getComponentPropsType } from './utils/getComponentPropsType.js';
|
|
2
3
|
import { getPropsFromType } from './utils/getPropsFromType.js';
|
|
3
4
|
import { isReactComponent } from './utils/isReactComponent.js';
|
|
4
5
|
/***
|
|
5
6
|
* Extracts React components and their props from analyzed exports.
|
|
6
7
|
*/
|
|
7
|
-
export function analyzeComponents(exports) {
|
|
8
|
+
export function analyzeComponents(exports, options = {}) {
|
|
8
9
|
const components = [];
|
|
9
10
|
for (const e of exports) {
|
|
10
11
|
if (!isReactComponent(e.node))
|
|
11
12
|
continue;
|
|
13
|
+
const propsFromAnalyzer = options.program
|
|
14
|
+
? collectPropsForExport(options.program, { name: e.name, node: e.node })
|
|
15
|
+
: undefined;
|
|
16
|
+
const analyzerProps = propsFromAnalyzer?.members.map((member) => ({
|
|
17
|
+
name: member.name,
|
|
18
|
+
type: member.type,
|
|
19
|
+
required: member.required,
|
|
20
|
+
description: member.description ?? null,
|
|
21
|
+
})) ?? [];
|
|
12
22
|
const propsType = getComponentPropsType(e.node);
|
|
13
|
-
const
|
|
23
|
+
const legacyProps = propsType != null ? getPropsFromType(propsType) : [];
|
|
24
|
+
const props = analyzerProps.length > 0 ? analyzerProps : legacyProps;
|
|
14
25
|
components.push({
|
|
15
26
|
name: e.name,
|
|
16
27
|
description: e.description,
|
package/dist/analyze/exports.js
CHANGED
|
@@ -14,7 +14,10 @@ export function analyzeExports(project, options) {
|
|
|
14
14
|
const exported = sourceFile.getExportSymbols();
|
|
15
15
|
for (const symbol of exported) {
|
|
16
16
|
const resolved = resolveExportSymbol(symbol);
|
|
17
|
-
const
|
|
17
|
+
const decl = getFirstDeclaration(resolved.getDeclarations());
|
|
18
|
+
if (decl === null) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
18
21
|
const rawComment = getParadoxComment(decl);
|
|
19
22
|
const parsed = rawComment
|
|
20
23
|
? parseParadoxComment(rawComment)
|
|
@@ -67,6 +70,10 @@ function getEntryPointSourceFiles(project, options) {
|
|
|
67
70
|
})
|
|
68
71
|
.filter((sourceFile) => sourceFile != null);
|
|
69
72
|
}
|
|
73
|
+
function getFirstDeclaration(declarations) {
|
|
74
|
+
const [declaration = null] = declarations;
|
|
75
|
+
return declaration;
|
|
76
|
+
}
|
|
70
77
|
function inferKind(node) {
|
|
71
78
|
if ('getParameters' in node)
|
|
72
79
|
return 'function';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnalyzedProject } from './model.js';
|
|
2
|
+
interface AnalyzeProjectConfig {
|
|
3
|
+
root: string;
|
|
4
|
+
entrypoints?: string[];
|
|
5
|
+
}
|
|
6
|
+
/***
|
|
7
|
+
* Runs the semantic analyzer pipeline for a project.
|
|
8
|
+
*/
|
|
9
|
+
export declare function analyzeProject(config: AnalyzeProjectConfig): AnalyzedProject;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { associateDocBlocksWithSymbols } from './associateDocBlocksWithSymbols.js';
|
|
2
|
+
import { collectSourceFiles } from './collectSourceFiles.js';
|
|
3
|
+
import { createTypeScriptProgram } from './createTypeScriptProgram.js';
|
|
4
|
+
import { collectDocBlocks } from './docBlocks.js';
|
|
5
|
+
import { collectExports, collectPropsForExport, collectRelatedTypes, collectTypeMembers, getExportSignature, resolveTypeReference, } from './exports.js';
|
|
6
|
+
import { collectCallGraph, collectComponentCompositionGraph, collectImportGraph, } from './graphs.js';
|
|
7
|
+
import { defaultTagRegistry } from './tagRegistry.js';
|
|
8
|
+
/***
|
|
9
|
+
* Runs the semantic analyzer pipeline for a project.
|
|
10
|
+
*/
|
|
11
|
+
export function analyzeProject(config) {
|
|
12
|
+
const program = createTypeScriptProgram({
|
|
13
|
+
root: config.root,
|
|
14
|
+
entrypoints: config.entrypoints,
|
|
15
|
+
});
|
|
16
|
+
const files = collectSourceFiles(program);
|
|
17
|
+
const docBlocks = files.flatMap((file) => collectDocBlocks(file.sourceFile, { program, tagRegistry: defaultTagRegistry }));
|
|
18
|
+
const exports = associateDocBlocksWithSymbols(program, docBlocks, collectExports(program)).map((entry) => {
|
|
19
|
+
const signature = getExportSignature(program, entry);
|
|
20
|
+
const props = collectPropsForExport(program, entry);
|
|
21
|
+
const typeReference = resolveTypeReference(program, entry.node);
|
|
22
|
+
const typeMembers = typeReference && (entry.kind === 'interface' || entry.kind === 'type')
|
|
23
|
+
? collectTypeMembers(program, typeReference)
|
|
24
|
+
: undefined;
|
|
25
|
+
const relatedTypes = collectRelatedTypes({
|
|
26
|
+
name: entry.name,
|
|
27
|
+
signature,
|
|
28
|
+
props,
|
|
29
|
+
typeMembers,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
...entry,
|
|
33
|
+
signature,
|
|
34
|
+
props,
|
|
35
|
+
typeMembers,
|
|
36
|
+
relatedTypes,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const tags = new Map();
|
|
40
|
+
for (const docBlock of docBlocks) {
|
|
41
|
+
for (const tag of docBlock.tags) {
|
|
42
|
+
tags.set(tag.name, tag.value ?? null);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const graphs = {
|
|
46
|
+
imports: collectImportGraph(program),
|
|
47
|
+
calls: collectCallGraph(program),
|
|
48
|
+
typeReferences: exports.flatMap((entry) => entry.relatedTypes.map((typeName) => ({
|
|
49
|
+
fromSymbol: entry.name,
|
|
50
|
+
toType: typeName,
|
|
51
|
+
sourcePath: entry.sourcePath,
|
|
52
|
+
}))),
|
|
53
|
+
componentComposition: collectComponentCompositionGraph(program, exports),
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
files,
|
|
57
|
+
exports,
|
|
58
|
+
docBlocks,
|
|
59
|
+
tags: [...tags.entries()].map(([name, value]) => ({ name, value })),
|
|
60
|
+
graphs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AnalyzedDocBlock, AnalyzedExport, AnalyzedProgram } from './model.js';
|
|
2
|
+
/***
|
|
3
|
+
* Attaches doc blocks and tags to their nearest symbol declarations.
|
|
4
|
+
*/
|
|
5
|
+
export declare function associateDocBlocksWithSymbols(program: AnalyzedProgram, docBlocks: readonly AnalyzedDocBlock[], exports: readonly AnalyzedExport[]): AnalyzedExport[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { relativeToRoot } from './utils.js';
|
|
2
|
+
/***
|
|
3
|
+
* Attaches doc blocks and tags to their nearest symbol declarations.
|
|
4
|
+
*/
|
|
5
|
+
export function associateDocBlocksWithSymbols(program, docBlocks, exports) {
|
|
6
|
+
const docBlocksByFile = new Map();
|
|
7
|
+
for (const docBlock of docBlocks) {
|
|
8
|
+
const entries = docBlocksByFile.get(docBlock.sourcePath);
|
|
9
|
+
if (entries) {
|
|
10
|
+
entries.push(docBlock);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
docBlocksByFile.set(docBlock.sourcePath, [docBlock]);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const [, entries] of docBlocksByFile) {
|
|
17
|
+
entries.sort((left, right) => left.start - right.start);
|
|
18
|
+
}
|
|
19
|
+
return exports.map((entry) => {
|
|
20
|
+
const sourceFile = entry.node.getSourceFile();
|
|
21
|
+
const sourcePath = relativeToRoot(program.root, sourceFile.getFilePath());
|
|
22
|
+
const docBlocksForFile = docBlocksByFile.get(sourcePath) ?? [];
|
|
23
|
+
const nodeStart = entry.node.getStart(false);
|
|
24
|
+
let match = undefined;
|
|
25
|
+
for (const block of docBlocksForFile) {
|
|
26
|
+
if (block.end > nodeStart)
|
|
27
|
+
break;
|
|
28
|
+
if (block.end > 0) {
|
|
29
|
+
match = block;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (match) {
|
|
33
|
+
const text = sourceFile.getFullText();
|
|
34
|
+
const between = text.slice(match.end, nodeStart);
|
|
35
|
+
if (!/^[\s;]*(export\s+)?(default\s+)?$/.test(between)) {
|
|
36
|
+
match = undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!match) {
|
|
40
|
+
return {
|
|
41
|
+
...entry,
|
|
42
|
+
tags: entry.tags,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
...entry,
|
|
47
|
+
docBlock: match,
|
|
48
|
+
tags: match.tags,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { normalize } from 'node:path';
|
|
2
|
+
import { isNodeModulePath, isPathInsideRoot, relativeToRoot } from './utils.js';
|
|
3
|
+
/***
|
|
4
|
+
* Collects stable source file metadata for the analyzer.
|
|
5
|
+
*/
|
|
6
|
+
export function collectSourceFiles(program) {
|
|
7
|
+
const entrypoints = new Set(program.entrypointFilePaths.map((entrypoint) => normalize(entrypoint)));
|
|
8
|
+
return program.project
|
|
9
|
+
.getSourceFiles()
|
|
10
|
+
.filter((sourceFile) => {
|
|
11
|
+
const filePath = sourceFile.getFilePath();
|
|
12
|
+
return (!sourceFile.isDeclarationFile() &&
|
|
13
|
+
isPathInsideRoot(program.root, filePath) &&
|
|
14
|
+
!isNodeModulePath(filePath));
|
|
15
|
+
})
|
|
16
|
+
.map((sourceFile) => ({
|
|
17
|
+
path: relativeToRoot(program.root, sourceFile.getFilePath()),
|
|
18
|
+
sourceFile,
|
|
19
|
+
isEntrypoint: entrypoints.has(normalize(sourceFile.getFilePath())),
|
|
20
|
+
}))
|
|
21
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Project } from 'ts-morph';
|
|
2
|
+
import type { AnalyzedProgram } from './model.js';
|
|
3
|
+
interface CreateProgramOptions {
|
|
4
|
+
root: string;
|
|
5
|
+
entrypoints?: string[];
|
|
6
|
+
project?: Project;
|
|
7
|
+
tsconfigPath?: string;
|
|
8
|
+
}
|
|
9
|
+
/***
|
|
10
|
+
* Creates or reuses a ts-morph project for analysis.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createTypeScriptProgram(options: CreateProgramOptions): AnalyzedProgram;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isAbsolute, join, normalize } from 'node:path';
|
|
2
|
+
import { Project } from 'ts-morph';
|
|
3
|
+
import { toPosixPath } from './utils.js';
|
|
4
|
+
/***
|
|
5
|
+
* Creates or reuses a ts-morph project for analysis.
|
|
6
|
+
*/
|
|
7
|
+
export function createTypeScriptProgram(options) {
|
|
8
|
+
const root = normalize(options.root);
|
|
9
|
+
const entrypoints = options.entrypoints ?? ['src/index.ts'];
|
|
10
|
+
const entrypointFilePaths = entrypoints.map((entrypoint) => normalize(isAbsolute(entrypoint) ? entrypoint : join(root, entrypoint)));
|
|
11
|
+
const project = options.project ??
|
|
12
|
+
new Project({
|
|
13
|
+
tsConfigFilePath: options.tsconfigPath ?? join(root, 'tsconfig.json'),
|
|
14
|
+
skipAddingFilesFromTsConfig: false,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
project,
|
|
18
|
+
typeChecker: project.getTypeChecker(),
|
|
19
|
+
root,
|
|
20
|
+
entrypoints: entrypoints.map((entrypoint) => toPosixPath(entrypoint)),
|
|
21
|
+
entrypointFilePaths,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SourceFile } from 'ts-morph';
|
|
2
|
+
import type { AnalyzedDocBlock, AnalyzedProgram } from './model.js';
|
|
3
|
+
import type { TagRegistry } from './tagRegistry.js';
|
|
4
|
+
/***
|
|
5
|
+
* Collects all Paradox doc blocks from a source file.
|
|
6
|
+
*/
|
|
7
|
+
export declare function collectDocBlocks(sourceFile: SourceFile, options: {
|
|
8
|
+
program: AnalyzedProgram;
|
|
9
|
+
tagRegistry?: TagRegistry;
|
|
10
|
+
}): AnalyzedDocBlock[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { defaultTagRegistry } from './tagRegistry.js';
|
|
2
|
+
import { relativeToRoot } from './utils.js';
|
|
3
|
+
const DOC_BLOCK_REGEX = /\/\*\*\*[\s\S]*?\*\//g;
|
|
4
|
+
/***
|
|
5
|
+
* Collects all Paradox doc blocks from a source file.
|
|
6
|
+
*/
|
|
7
|
+
export function collectDocBlocks(sourceFile, options) {
|
|
8
|
+
const { program } = options;
|
|
9
|
+
const tagRegistry = options.tagRegistry ?? defaultTagRegistry;
|
|
10
|
+
const text = sourceFile.getFullText();
|
|
11
|
+
const sourcePath = relativeToRoot(program.root, sourceFile.getFilePath());
|
|
12
|
+
const blocks = [];
|
|
13
|
+
for (const match of text.matchAll(DOC_BLOCK_REGEX)) {
|
|
14
|
+
const [raw = ''] = match;
|
|
15
|
+
const start = match.index;
|
|
16
|
+
const end = start + raw.length;
|
|
17
|
+
const { line, column } = sourceFile.getLineAndColumnAtPos(start);
|
|
18
|
+
const parsed = parseDocBlock(raw, tagRegistry);
|
|
19
|
+
const id = `${sourcePath}:${line}:${column}`;
|
|
20
|
+
blocks.push({
|
|
21
|
+
id,
|
|
22
|
+
sourcePath,
|
|
23
|
+
start,
|
|
24
|
+
end,
|
|
25
|
+
line,
|
|
26
|
+
column,
|
|
27
|
+
raw,
|
|
28
|
+
description: parsed.description,
|
|
29
|
+
params: parsed.params,
|
|
30
|
+
returns: parsed.returns,
|
|
31
|
+
tags: parsed.tags,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return blocks;
|
|
35
|
+
}
|
|
36
|
+
/***
|
|
37
|
+
* Extracts registered tags from a doc block.
|
|
38
|
+
*/
|
|
39
|
+
function collectTags(raw, tagRegistry = defaultTagRegistry) {
|
|
40
|
+
const tags = [];
|
|
41
|
+
const lines = normalizeDocBlock(raw);
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed.startsWith('@'))
|
|
45
|
+
continue;
|
|
46
|
+
const [tagName, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
47
|
+
if (!tagName || !tagRegistry.has(tagName))
|
|
48
|
+
continue;
|
|
49
|
+
const value = rest.join(' ').trim();
|
|
50
|
+
tags.push({
|
|
51
|
+
name: tagName,
|
|
52
|
+
value: value.length > 0 ? value : null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return tags;
|
|
56
|
+
}
|
|
57
|
+
function parseDocBlock(raw, tagRegistry) {
|
|
58
|
+
const lines = normalizeDocBlock(raw);
|
|
59
|
+
const tags = collectTags(raw, tagRegistry);
|
|
60
|
+
const params = {};
|
|
61
|
+
let returns = null;
|
|
62
|
+
const description = lines
|
|
63
|
+
.filter((line) => {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed.startsWith('@'))
|
|
66
|
+
return true;
|
|
67
|
+
const [tagName, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
68
|
+
if (!tagName)
|
|
69
|
+
return false;
|
|
70
|
+
if (tagName === 'param') {
|
|
71
|
+
const [name, ...descParts] = rest;
|
|
72
|
+
if (name) {
|
|
73
|
+
params[name] = descParts.join(' ').trim();
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (tagName === 'returns' || tagName === 'return') {
|
|
78
|
+
const returnBody = rest.join(' ').trim();
|
|
79
|
+
returns = returnBody.length > 0 ? returnBody : null;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (tagRegistry.has(tagName)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
})
|
|
87
|
+
.join('\n')
|
|
88
|
+
.trim();
|
|
89
|
+
return {
|
|
90
|
+
description: description.length > 0 ? description : null,
|
|
91
|
+
tags,
|
|
92
|
+
params,
|
|
93
|
+
returns,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function normalizeDocBlock(raw) {
|
|
97
|
+
return raw
|
|
98
|
+
.replace(/^\/\*\*\*/, '')
|
|
99
|
+
.replace(/\*\/$/, '')
|
|
100
|
+
.split('\n')
|
|
101
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
|
|
102
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Node, Symbol as MorphSymbol, Type } from 'ts-morph';
|
|
2
|
+
import type { AnalyzedExport, AnalyzedProgram, AnalyzedProps, AnalyzedTypeMember, ResolvedTypeReference } from './model.js';
|
|
3
|
+
/***
|
|
4
|
+
* Collects exported declarations from configured entrypoints.
|
|
5
|
+
*/
|
|
6
|
+
export declare function collectExports(program: AnalyzedProgram): AnalyzedExport[];
|
|
7
|
+
/***
|
|
8
|
+
* Returns a human-readable signature for callable exports.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getExportSignature(_program: AnalyzedProgram, analyzedExport: Pick<AnalyzedExport, 'node'>): string | undefined;
|
|
11
|
+
/***
|
|
12
|
+
* Resolves a named type reference from a node or symbol.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveTypeReference(program: AnalyzedProgram, typeNodeOrSymbol: Node | Type | MorphSymbol): ResolvedTypeReference | null;
|
|
15
|
+
/***
|
|
16
|
+
* Collects nested type members for interfaces and type literals.
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectTypeMembers(program: AnalyzedProgram, reference: ResolvedTypeReference, options?: {
|
|
19
|
+
inheritedFrom?: string;
|
|
20
|
+
depth?: number;
|
|
21
|
+
}): AnalyzedTypeMember[];
|
|
22
|
+
/***
|
|
23
|
+
* Extracts props metadata for a callable export.
|
|
24
|
+
*/
|
|
25
|
+
export declare function collectPropsForExport(program: AnalyzedProgram, analyzedExport: Pick<AnalyzedExport, 'name' | 'node'>): AnalyzedProps | undefined;
|
|
26
|
+
/***
|
|
27
|
+
* Collects related types referenced by an export.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectRelatedTypes(analyzedExport: Pick<AnalyzedExport, 'name' | 'signature' | 'props' | 'typeMembers'>): string[];
|