@ankhorage/paradox 0.0.9 → 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 +12 -0
- package/README.md +117 -13
- package/dist/analyze/analyze.d.ts +0 -3
- package/dist/analyze/analyze.js +47 -13
- package/dist/analyze/components.d.ts +4 -1
- package/dist/analyze/components.js +16 -2
- package/dist/analyze/exports.d.ts +1 -0
- package/dist/analyze/exports.js +16 -3
- 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 +55 -1
- package/dist/analyze/utils/parseParadoxComment.d.ts +7 -0
- package/dist/analyze/utils/parseParadoxComment.js +61 -14
- package/dist/config/defineParadoxConfig.d.ts +2 -0
- package/dist/config/defineParadoxConfig.js +2 -0
- package/dist/config/types.d.ts +1 -0
- package/dist/model/buildModel.d.ts +56 -7
- package/dist/model/buildModel.js +16 -0
- package/dist/model/types.d.ts +62 -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 +235 -39
- package/package.json +15 -6
|
@@ -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[];
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { Node as MorphNode, TypeFormatFlags } from 'ts-morph';
|
|
2
|
+
import { isReactComponent } from './isReactComponent.js';
|
|
3
|
+
import { getParadoxComment, parseParadoxComment } from './paradoxComment.js';
|
|
4
|
+
import { isNodeModulePath, isPathInsideRoot, relativeToRoot } from './utils.js';
|
|
5
|
+
/***
|
|
6
|
+
* Collects exported declarations from configured entrypoints.
|
|
7
|
+
*/
|
|
8
|
+
export function collectExports(program) {
|
|
9
|
+
const exportsByName = new Map();
|
|
10
|
+
for (const entrypoint of program.entrypointFilePaths) {
|
|
11
|
+
const sourceFile = program.project.getSourceFile((file) => file.getFilePath() === entrypoint);
|
|
12
|
+
if (!sourceFile)
|
|
13
|
+
continue;
|
|
14
|
+
const entrypointPath = relativeToRoot(program.root, sourceFile.getFilePath());
|
|
15
|
+
for (const symbol of sourceFile.getExportSymbols()) {
|
|
16
|
+
const resolved = resolveExportSymbol(symbol);
|
|
17
|
+
const declaration = getFirstDeclaration(resolved.getDeclarations());
|
|
18
|
+
if (!declaration)
|
|
19
|
+
continue;
|
|
20
|
+
const name = resolved.getName();
|
|
21
|
+
const sourcePath = relativeToRoot(program.root, declaration.getSourceFile().getFilePath());
|
|
22
|
+
const existing = exportsByName.get(name);
|
|
23
|
+
const exportPaths = existing
|
|
24
|
+
? uniqueSorted([...existing.exportPaths, entrypointPath])
|
|
25
|
+
: [entrypointPath];
|
|
26
|
+
const kind = existing?.kind ?? detectExportKind(declaration);
|
|
27
|
+
exportsByName.set(name, {
|
|
28
|
+
name,
|
|
29
|
+
sourcePath,
|
|
30
|
+
exportPaths,
|
|
31
|
+
kind,
|
|
32
|
+
signature: existing?.signature,
|
|
33
|
+
docBlock: existing?.docBlock,
|
|
34
|
+
tags: existing?.tags ?? [],
|
|
35
|
+
props: existing?.props,
|
|
36
|
+
typeMembers: existing?.typeMembers,
|
|
37
|
+
relatedTypes: existing?.relatedTypes ?? [],
|
|
38
|
+
node: declaration,
|
|
39
|
+
symbol: resolved,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...exportsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
44
|
+
}
|
|
45
|
+
/***
|
|
46
|
+
* Determines the kind of export for documentation.
|
|
47
|
+
*/
|
|
48
|
+
function detectExportKind(node) {
|
|
49
|
+
if (isReactComponent(node))
|
|
50
|
+
return 'component';
|
|
51
|
+
const name = getDeclarationName(node);
|
|
52
|
+
if (name && /^use[A-Z]/.test(name))
|
|
53
|
+
return 'hook';
|
|
54
|
+
if (MorphNode.isFunctionDeclaration(node))
|
|
55
|
+
return 'function';
|
|
56
|
+
if (MorphNode.isClassDeclaration(node))
|
|
57
|
+
return 'class';
|
|
58
|
+
if (MorphNode.isInterfaceDeclaration(node))
|
|
59
|
+
return 'interface';
|
|
60
|
+
if (MorphNode.isTypeAliasDeclaration(node))
|
|
61
|
+
return 'type';
|
|
62
|
+
if (MorphNode.isVariableDeclaration(node))
|
|
63
|
+
return 'const';
|
|
64
|
+
return 'unknown';
|
|
65
|
+
}
|
|
66
|
+
/***
|
|
67
|
+
* Returns a human-readable signature for callable exports.
|
|
68
|
+
*/
|
|
69
|
+
export function getExportSignature(_program, analyzedExport) {
|
|
70
|
+
const callableNode = getCallableNode(analyzedExport.node);
|
|
71
|
+
if (!callableNode)
|
|
72
|
+
return undefined;
|
|
73
|
+
const callSignatures = callableNode.getType().getCallSignatures();
|
|
74
|
+
if (callSignatures.length === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
const [callSignature] = callSignatures;
|
|
77
|
+
const parameters = callSignature.getParameters().map((parameter) => {
|
|
78
|
+
const declaration = getFirstDeclaration(parameter.getDeclarations());
|
|
79
|
+
const name = parameter.getName();
|
|
80
|
+
const isOptional = declaration && MorphNode.isParameterDeclaration(declaration)
|
|
81
|
+
? declaration.isOptional()
|
|
82
|
+
: false;
|
|
83
|
+
const type = getParameterTypeText(parameter, declaration, callableNode);
|
|
84
|
+
return `${name}${isOptional ? '?' : ''}: ${type}`;
|
|
85
|
+
});
|
|
86
|
+
const returnType = callSignature.getReturnType().getText(callableNode);
|
|
87
|
+
const returnSuffix = returnType === 'void' ? '' : ` => ${returnType}`;
|
|
88
|
+
return `(${parameters.join(', ')})${returnSuffix}`;
|
|
89
|
+
}
|
|
90
|
+
/***
|
|
91
|
+
* Resolves a named type reference from a node or symbol.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveTypeReference(program, typeNodeOrSymbol) {
|
|
94
|
+
const type = MorphNode.isNode(typeNodeOrSymbol)
|
|
95
|
+
? typeNodeOrSymbol.getType()
|
|
96
|
+
: 'getText' in typeNodeOrSymbol
|
|
97
|
+
? typeNodeOrSymbol
|
|
98
|
+
: typeNodeOrSymbol.getDeclaredType();
|
|
99
|
+
const symbol = type.getSymbol() ?? ('getDeclarations' in typeNodeOrSymbol ? typeNodeOrSymbol : null);
|
|
100
|
+
const name = symbol?.getName() ?? null;
|
|
101
|
+
const declaration = getFirstDeclaration(symbol?.getDeclarations() ?? []);
|
|
102
|
+
const sourcePath = declaration
|
|
103
|
+
? relativeToRoot(program.root, declaration.getSourceFile().getFilePath())
|
|
104
|
+
: null;
|
|
105
|
+
return {
|
|
106
|
+
type,
|
|
107
|
+
name,
|
|
108
|
+
sourcePath,
|
|
109
|
+
symbol: symbol ?? null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/***
|
|
113
|
+
* Collects nested type members for interfaces and type literals.
|
|
114
|
+
*/
|
|
115
|
+
export function collectTypeMembers(program, reference, options = {}) {
|
|
116
|
+
const depth = options.depth ?? 0;
|
|
117
|
+
if (depth > 3)
|
|
118
|
+
return [];
|
|
119
|
+
const membersByName = new Map();
|
|
120
|
+
const ownMembers = collectMembersFromType(program, reference, options.inheritedFrom, depth);
|
|
121
|
+
for (const member of ownMembers) {
|
|
122
|
+
membersByName.set(member.name, member);
|
|
123
|
+
}
|
|
124
|
+
const baseTypes = reference.type.getBaseTypes();
|
|
125
|
+
for (const baseType of baseTypes) {
|
|
126
|
+
const baseReference = resolveTypeReference(program, baseType);
|
|
127
|
+
if (!baseReference)
|
|
128
|
+
continue;
|
|
129
|
+
const baseName = baseReference.name ?? 'BaseType';
|
|
130
|
+
const baseMembers = collectTypeMembers(program, baseReference, {
|
|
131
|
+
inheritedFrom: baseName,
|
|
132
|
+
depth: depth + 1,
|
|
133
|
+
});
|
|
134
|
+
for (const member of baseMembers) {
|
|
135
|
+
if (!membersByName.has(member.name)) {
|
|
136
|
+
membersByName.set(member.name, member);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [...membersByName.values()];
|
|
141
|
+
}
|
|
142
|
+
/***
|
|
143
|
+
* Extracts props metadata for a callable export.
|
|
144
|
+
*/
|
|
145
|
+
export function collectPropsForExport(program, analyzedExport) {
|
|
146
|
+
const callableNode = getCallableNode(analyzedExport.node);
|
|
147
|
+
if (!callableNode)
|
|
148
|
+
return undefined;
|
|
149
|
+
const parameters = callableNode.getParameters();
|
|
150
|
+
if (parameters.length === 0)
|
|
151
|
+
return undefined;
|
|
152
|
+
const [parameter] = parameters;
|
|
153
|
+
const parameterType = parameter.getType();
|
|
154
|
+
if (parameterType.isAny())
|
|
155
|
+
return undefined;
|
|
156
|
+
const resolved = resolveTypeReference(program, parameterType);
|
|
157
|
+
if (!resolved)
|
|
158
|
+
return undefined;
|
|
159
|
+
const typeName = resolved.name ??
|
|
160
|
+
parameterType.getText(parameter, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
161
|
+
const defaultValues = collectDefaultValuesFromBinding(parameter);
|
|
162
|
+
const members = collectTypeMembers(program, resolved)
|
|
163
|
+
.filter((member) => member.name.length > 0)
|
|
164
|
+
.map((member) => ({
|
|
165
|
+
name: member.name,
|
|
166
|
+
type: member.type,
|
|
167
|
+
required: member.required,
|
|
168
|
+
defaultValue: defaultValues[member.name],
|
|
169
|
+
description: member.description,
|
|
170
|
+
inheritedFrom: member.inheritedFrom,
|
|
171
|
+
}));
|
|
172
|
+
return {
|
|
173
|
+
typeName,
|
|
174
|
+
sourcePath: resolved.sourcePath,
|
|
175
|
+
members,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/***
|
|
179
|
+
* Collects default values from destructured bindings.
|
|
180
|
+
*/
|
|
181
|
+
function collectDefaultValuesFromBinding(parameter) {
|
|
182
|
+
const defaults = {};
|
|
183
|
+
const nameNode = parameter.getNameNode();
|
|
184
|
+
if (!MorphNode.isObjectBindingPattern(nameNode))
|
|
185
|
+
return defaults;
|
|
186
|
+
for (const element of nameNode.getElements()) {
|
|
187
|
+
const initializer = element.getInitializer();
|
|
188
|
+
if (!initializer)
|
|
189
|
+
continue;
|
|
190
|
+
const name = element.getName();
|
|
191
|
+
defaults[name] = initializer.getText();
|
|
192
|
+
}
|
|
193
|
+
return defaults;
|
|
194
|
+
}
|
|
195
|
+
/***
|
|
196
|
+
* Collects related types referenced by an export.
|
|
197
|
+
*/
|
|
198
|
+
export function collectRelatedTypes(analyzedExport) {
|
|
199
|
+
const candidates = [];
|
|
200
|
+
if (analyzedExport.signature)
|
|
201
|
+
candidates.push(analyzedExport.signature);
|
|
202
|
+
if (analyzedExport.props) {
|
|
203
|
+
candidates.push(analyzedExport.props.typeName);
|
|
204
|
+
candidates.push(...analyzedExport.props.members.map((member) => member.type));
|
|
205
|
+
}
|
|
206
|
+
if (analyzedExport.typeMembers) {
|
|
207
|
+
candidates.push(...flattenMembers(analyzedExport.typeMembers).map((member) => member.type));
|
|
208
|
+
}
|
|
209
|
+
const related = new Set();
|
|
210
|
+
for (const candidate of candidates) {
|
|
211
|
+
for (const match of candidate.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)) {
|
|
212
|
+
const [name] = match;
|
|
213
|
+
if (!IGNORED_RELATED_SYMBOLS.has(name) && name !== analyzedExport.name) {
|
|
214
|
+
related.add(name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return [...related].sort((left, right) => left.localeCompare(right));
|
|
219
|
+
}
|
|
220
|
+
function collectMembersFromType(program, reference, inheritedFrom, depth) {
|
|
221
|
+
const declarationMembers = getDeclaredMembers(reference);
|
|
222
|
+
const declarations = declarationMembers.length > 0 ? declarationMembers : reference.type.getProperties();
|
|
223
|
+
return declarations.flatMap((property) => {
|
|
224
|
+
const declaration = getFirstDeclaration(property.getDeclarations());
|
|
225
|
+
if (!declaration)
|
|
226
|
+
return [];
|
|
227
|
+
const propertyType = property.getTypeAtLocation(declaration);
|
|
228
|
+
const rawComment = getParadoxComment(declaration);
|
|
229
|
+
const parsed = rawComment
|
|
230
|
+
? parseParadoxComment(rawComment)
|
|
231
|
+
: { description: null, isConfig: false, params: {}, returns: null };
|
|
232
|
+
const required = !property.isOptional() && !isUndefinedUnion(propertyType);
|
|
233
|
+
const childReference = shouldExpandType(program, propertyType)
|
|
234
|
+
? resolveTypeReference(program, propertyType)
|
|
235
|
+
: null;
|
|
236
|
+
const children = childReference
|
|
237
|
+
? collectTypeMembers(program, childReference, {
|
|
238
|
+
inheritedFrom: undefined,
|
|
239
|
+
depth: depth + 1,
|
|
240
|
+
})
|
|
241
|
+
: undefined;
|
|
242
|
+
const declaredTypeText = getDeclaredTypeText(declaration);
|
|
243
|
+
const baseTypeText = declaredTypeText ??
|
|
244
|
+
propertyType.getText(declaration, TypeFormatFlags.UseAliasDefinedOutsideCurrentScope);
|
|
245
|
+
const typeText = appendUndefinedIfOptional(baseTypeText, required);
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
name: property.getName(),
|
|
249
|
+
type: typeText,
|
|
250
|
+
required,
|
|
251
|
+
description: parsed.description ?? undefined,
|
|
252
|
+
inheritedFrom,
|
|
253
|
+
children: children && children.length > 0 ? children : undefined,
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function getDeclaredTypeText(declaration) {
|
|
259
|
+
if (MorphNode.isPropertySignature(declaration) || MorphNode.isPropertyDeclaration(declaration)) {
|
|
260
|
+
const typeNode = declaration.getTypeNode();
|
|
261
|
+
if (!typeNode)
|
|
262
|
+
return null;
|
|
263
|
+
if (MorphNode.isTypeLiteral(typeNode))
|
|
264
|
+
return null;
|
|
265
|
+
return typeNode.getText();
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
function getDeclaredMembers(reference) {
|
|
270
|
+
const declarations = reference.symbol?.getDeclarations() ?? [];
|
|
271
|
+
const members = [];
|
|
272
|
+
for (const declaration of declarations) {
|
|
273
|
+
if (MorphNode.isInterfaceDeclaration(declaration)) {
|
|
274
|
+
for (const property of declaration.getProperties()) {
|
|
275
|
+
const symbol = property.getSymbol();
|
|
276
|
+
if (symbol)
|
|
277
|
+
members.push(symbol);
|
|
278
|
+
}
|
|
279
|
+
for (const method of declaration.getMethods()) {
|
|
280
|
+
const symbol = method.getSymbol();
|
|
281
|
+
if (symbol)
|
|
282
|
+
members.push(symbol);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (MorphNode.isTypeAliasDeclaration(declaration)) {
|
|
286
|
+
const typeNode = declaration.getTypeNode();
|
|
287
|
+
if (typeNode && MorphNode.isTypeLiteral(typeNode)) {
|
|
288
|
+
for (const member of typeNode.getMembers()) {
|
|
289
|
+
if (MorphNode.isPropertySignature(member) || MorphNode.isMethodSignature(member)) {
|
|
290
|
+
const symbol = member.getSymbol();
|
|
291
|
+
if (symbol)
|
|
292
|
+
members.push(symbol);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return members;
|
|
299
|
+
}
|
|
300
|
+
function shouldExpandType(program, type) {
|
|
301
|
+
if (type.isUnion() || type.isIntersection())
|
|
302
|
+
return false;
|
|
303
|
+
if (type.isString() || type.isNumber() || type.isBoolean() || type.isEnum())
|
|
304
|
+
return false;
|
|
305
|
+
const symbol = type.getSymbol();
|
|
306
|
+
const declaration = getFirstDeclaration(symbol?.getDeclarations() ?? []);
|
|
307
|
+
if (declaration) {
|
|
308
|
+
const sourceFile = declaration.getSourceFile();
|
|
309
|
+
const sourcePath = relativeToRoot(program.root, sourceFile.getFilePath());
|
|
310
|
+
return !sourcePath.includes('node_modules');
|
|
311
|
+
}
|
|
312
|
+
if (!type.isObject())
|
|
313
|
+
return false;
|
|
314
|
+
const propertyDeclarations = type
|
|
315
|
+
.getProperties()
|
|
316
|
+
.flatMap((property) => property.getDeclarations());
|
|
317
|
+
return propertyDeclarations.some((decl) => {
|
|
318
|
+
const filePath = decl.getSourceFile().getFilePath();
|
|
319
|
+
return isPathInsideRoot(program.root, filePath) && !isNodeModulePath(filePath);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function flattenMembers(members) {
|
|
323
|
+
return members.flatMap((member) => [
|
|
324
|
+
member,
|
|
325
|
+
...(member.children ? flattenMembers(member.children) : []),
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
function isUndefinedUnion(type) {
|
|
329
|
+
if (!type.isUnion())
|
|
330
|
+
return false;
|
|
331
|
+
return type.getUnionTypes().some((unionType) => unionType.isUndefined());
|
|
332
|
+
}
|
|
333
|
+
function getFirstDeclaration(declarations) {
|
|
334
|
+
const [declaration = null] = declarations;
|
|
335
|
+
return declaration;
|
|
336
|
+
}
|
|
337
|
+
function resolveExportSymbol(symbol) {
|
|
338
|
+
return symbol.getAliasedSymbol() ?? symbol;
|
|
339
|
+
}
|
|
340
|
+
function getDeclarationName(node) {
|
|
341
|
+
if (MorphNode.isFunctionDeclaration(node))
|
|
342
|
+
return node.getName() ?? null;
|
|
343
|
+
if (MorphNode.isVariableDeclaration(node))
|
|
344
|
+
return node.getName();
|
|
345
|
+
if (MorphNode.isClassDeclaration(node))
|
|
346
|
+
return node.getName() ?? null;
|
|
347
|
+
if (MorphNode.isInterfaceDeclaration(node))
|
|
348
|
+
return node.getName();
|
|
349
|
+
if (MorphNode.isTypeAliasDeclaration(node))
|
|
350
|
+
return node.getName();
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function getParameterTypeText(parameter, declaration, fallbackNode) {
|
|
354
|
+
return declaration
|
|
355
|
+
? parameter.getTypeAtLocation(declaration).getText(declaration)
|
|
356
|
+
: parameter.getTypeAtLocation(fallbackNode).getText(fallbackNode);
|
|
357
|
+
}
|
|
358
|
+
function appendUndefinedIfOptional(typeText, required) {
|
|
359
|
+
if (required || typeText.includes('undefined'))
|
|
360
|
+
return typeText;
|
|
361
|
+
return `${typeText} | undefined`;
|
|
362
|
+
}
|
|
363
|
+
function getCallableNode(node) {
|
|
364
|
+
if (MorphNode.isFunctionDeclaration(node))
|
|
365
|
+
return node;
|
|
366
|
+
if (MorphNode.isMethodDeclaration(node))
|
|
367
|
+
return node;
|
|
368
|
+
if (MorphNode.isMethodSignature(node))
|
|
369
|
+
return node;
|
|
370
|
+
if (MorphNode.isArrowFunction(node))
|
|
371
|
+
return node;
|
|
372
|
+
if (MorphNode.isFunctionExpression(node))
|
|
373
|
+
return node;
|
|
374
|
+
if (MorphNode.isVariableDeclaration(node)) {
|
|
375
|
+
const initializer = node.getInitializer();
|
|
376
|
+
if (initializer &&
|
|
377
|
+
(MorphNode.isArrowFunction(initializer) || MorphNode.isFunctionExpression(initializer))) {
|
|
378
|
+
return initializer;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
function uniqueSorted(values) {
|
|
384
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
385
|
+
}
|
|
386
|
+
const IGNORED_RELATED_SYMBOLS = new Set([
|
|
387
|
+
'Array',
|
|
388
|
+
'Boolean',
|
|
389
|
+
'Date',
|
|
390
|
+
'Element',
|
|
391
|
+
'JSX',
|
|
392
|
+
'Map',
|
|
393
|
+
'Number',
|
|
394
|
+
'Promise',
|
|
395
|
+
'ReadonlyArray',
|
|
396
|
+
'Record',
|
|
397
|
+
'Set',
|
|
398
|
+
'String',
|
|
399
|
+
]);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnalyzedExport, AnalyzedProgram, CallEdge, ComponentCompositionEdge, ImportEdge } from './model.js';
|
|
2
|
+
/***
|
|
3
|
+
* Collects local import relationships between source files.
|
|
4
|
+
*/
|
|
5
|
+
export declare function collectImportGraph(program: AnalyzedProgram): ImportEdge[];
|
|
6
|
+
/***
|
|
7
|
+
* Collects call relationships between local symbols.
|
|
8
|
+
*/
|
|
9
|
+
export declare function collectCallGraph(program: AnalyzedProgram): CallEdge[];
|
|
10
|
+
/***
|
|
11
|
+
* Collects component composition edges for React components.
|
|
12
|
+
*/
|
|
13
|
+
export declare function collectComponentCompositionGraph(program: AnalyzedProgram, exports?: AnalyzedExport[]): ComponentCompositionEdge[];
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Node as MorphNode } from 'ts-morph';
|
|
2
|
+
import { collectExports } from './exports.js';
|
|
3
|
+
import { isNodeModulePath, isPathInsideRoot, relativeToRoot } from './utils.js';
|
|
4
|
+
/***
|
|
5
|
+
* Collects local import relationships between source files.
|
|
6
|
+
*/
|
|
7
|
+
export function collectImportGraph(program) {
|
|
8
|
+
const edges = [];
|
|
9
|
+
for (const sourceFile of program.project.getSourceFiles()) {
|
|
10
|
+
if (sourceFile.isDeclarationFile())
|
|
11
|
+
continue;
|
|
12
|
+
if (!isPathInsideRoot(program.root, sourceFile.getFilePath()))
|
|
13
|
+
continue;
|
|
14
|
+
if (isNodeModulePath(sourceFile.getFilePath()))
|
|
15
|
+
continue;
|
|
16
|
+
const sourcePath = relativeToRoot(program.root, sourceFile.getFilePath());
|
|
17
|
+
const moduleDeclarations = [
|
|
18
|
+
...sourceFile.getImportDeclarations(),
|
|
19
|
+
...sourceFile.getExportDeclarations(),
|
|
20
|
+
];
|
|
21
|
+
for (const declaration of moduleDeclarations) {
|
|
22
|
+
const dependency = declaration.getModuleSpecifierSourceFile();
|
|
23
|
+
if (!dependency)
|
|
24
|
+
continue;
|
|
25
|
+
const dependencyPath = dependency.getFilePath();
|
|
26
|
+
if (!isPathInsideRoot(program.root, dependencyPath) || isNodeModulePath(dependencyPath))
|
|
27
|
+
continue;
|
|
28
|
+
edges.push({
|
|
29
|
+
fromPath: sourcePath,
|
|
30
|
+
toPath: relativeToRoot(program.root, dependencyPath),
|
|
31
|
+
sourcePath,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return uniqueEdges(edges, (edge) => `${edge.fromPath}->${edge.toPath}`);
|
|
36
|
+
}
|
|
37
|
+
/***
|
|
38
|
+
* Collects call relationships between local symbols.
|
|
39
|
+
*/
|
|
40
|
+
export function collectCallGraph(program) {
|
|
41
|
+
const edges = [];
|
|
42
|
+
for (const sourceFile of program.project.getSourceFiles()) {
|
|
43
|
+
if (sourceFile.isDeclarationFile())
|
|
44
|
+
continue;
|
|
45
|
+
if (!isPathInsideRoot(program.root, sourceFile.getFilePath()))
|
|
46
|
+
continue;
|
|
47
|
+
if (isNodeModulePath(sourceFile.getFilePath()))
|
|
48
|
+
continue;
|
|
49
|
+
const sourcePath = relativeToRoot(program.root, sourceFile.getFilePath());
|
|
50
|
+
sourceFile.forEachDescendant((node) => {
|
|
51
|
+
if (!MorphNode.isCallExpression(node))
|
|
52
|
+
return;
|
|
53
|
+
const edge = toCallEdge(program, node, sourcePath);
|
|
54
|
+
if (edge)
|
|
55
|
+
edges.push(edge);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return uniqueEdges(edges, (edge) => `${edge.fromSymbol}->${edge.toSymbol}@${edge.sourcePath}:${edge.callExpression}`);
|
|
59
|
+
}
|
|
60
|
+
/***
|
|
61
|
+
* Collects component composition edges for React components.
|
|
62
|
+
*/
|
|
63
|
+
export function collectComponentCompositionGraph(program, exports = collectExports(program)) {
|
|
64
|
+
const edges = [];
|
|
65
|
+
const components = exports.filter((entry) => entry.kind === 'component');
|
|
66
|
+
for (const component of components) {
|
|
67
|
+
const componentName = component.name;
|
|
68
|
+
const { sourcePath } = component;
|
|
69
|
+
const jsxElements = findJsxElements(component.node);
|
|
70
|
+
for (const element of jsxElements) {
|
|
71
|
+
const jsxTagName = element.getTagNameNode().getText();
|
|
72
|
+
if (!/^[A-Z]/.test(jsxTagName))
|
|
73
|
+
continue;
|
|
74
|
+
edges.push({
|
|
75
|
+
fromComponent: componentName,
|
|
76
|
+
toComponent: jsxTagName,
|
|
77
|
+
jsxElement: element.getText(),
|
|
78
|
+
sourcePath,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return uniqueEdges(edges, (edge) => `${edge.fromComponent}->${edge.toComponent}@${edge.sourcePath}:${edge.jsxElement}`);
|
|
83
|
+
}
|
|
84
|
+
function toCallEdge(program, node, sourcePath) {
|
|
85
|
+
const expression = node.getExpression();
|
|
86
|
+
const symbol = expression.getSymbol() ?? expression.getType().getSymbol();
|
|
87
|
+
if (!symbol)
|
|
88
|
+
return null;
|
|
89
|
+
const declarations = symbol.getDeclarations();
|
|
90
|
+
if (declarations.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const [declaration] = declarations;
|
|
93
|
+
const declarationPath = declaration.getSourceFile().getFilePath();
|
|
94
|
+
if (!isPathInsideRoot(program.root, declarationPath) || isNodeModulePath(declarationPath))
|
|
95
|
+
return null;
|
|
96
|
+
const fromSymbol = getEnclosingSymbolName(node) ?? sourcePath;
|
|
97
|
+
const toSymbol = symbol.getName();
|
|
98
|
+
if (!toSymbol)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
fromSymbol,
|
|
102
|
+
toSymbol,
|
|
103
|
+
callExpression: expression.getText(),
|
|
104
|
+
sourcePath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function findJsxElements(node) {
|
|
108
|
+
const elements = [];
|
|
109
|
+
node.forEachDescendant((descendant) => {
|
|
110
|
+
if (MorphNode.isJsxSelfClosingElement(descendant) ||
|
|
111
|
+
MorphNode.isJsxOpeningElement(descendant)) {
|
|
112
|
+
elements.push(descendant);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return elements;
|
|
116
|
+
}
|
|
117
|
+
function getEnclosingSymbolName(node) {
|
|
118
|
+
const ancestor = node.getFirstAncestor((candidate) => {
|
|
119
|
+
return (MorphNode.isFunctionDeclaration(candidate) ||
|
|
120
|
+
MorphNode.isMethodDeclaration(candidate) ||
|
|
121
|
+
MorphNode.isFunctionExpression(candidate) ||
|
|
122
|
+
MorphNode.isArrowFunction(candidate) ||
|
|
123
|
+
MorphNode.isClassDeclaration(candidate));
|
|
124
|
+
});
|
|
125
|
+
if (!ancestor)
|
|
126
|
+
return null;
|
|
127
|
+
if (MorphNode.isFunctionDeclaration(ancestor))
|
|
128
|
+
return ancestor.getName() ?? null;
|
|
129
|
+
if (MorphNode.isMethodDeclaration(ancestor))
|
|
130
|
+
return ancestor.getName();
|
|
131
|
+
if (MorphNode.isClassDeclaration(ancestor))
|
|
132
|
+
return ancestor.getName() ?? null;
|
|
133
|
+
if (MorphNode.isArrowFunction(ancestor) || MorphNode.isFunctionExpression(ancestor)) {
|
|
134
|
+
const parent = ancestor.getParent();
|
|
135
|
+
if (MorphNode.isVariableDeclaration(parent))
|
|
136
|
+
return parent.getName();
|
|
137
|
+
if (MorphNode.isPropertyAssignment(parent))
|
|
138
|
+
return parent.getName();
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function uniqueEdges(edges, key) {
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
return edges.filter((edge) => {
|
|
145
|
+
const edgeKey = key(edge);
|
|
146
|
+
if (seen.has(edgeKey))
|
|
147
|
+
return false;
|
|
148
|
+
seen.add(edgeKey);
|
|
149
|
+
return true;
|
|
150
|
+
});
|
|
151
|
+
}
|