@akanjs/language 3.1.2-rc.3 → 3.2.0-rc.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/README.ko.md +41 -0
- package/README.md +42 -0
- package/akanGraph.d.ts +3 -1
- package/akanGraphDiff.d.ts +10 -0
- package/akanIndex.d.ts +3 -1
- package/frameworkChannelReader.d.ts +16 -0
- package/frameworkMapBuilder.d.ts +20 -0
- package/frameworkMapDoc.d.ts +10 -0
- package/index.cjs +2202 -395
- package/index.d.ts +6 -1
- package/index.js +2202 -395
- package/moduleFacts.d.ts +48 -1
- package/package.json +4 -1
- package/routeFacts.d.ts +48 -0
- package/server.js +2730 -0
package/index.cjs
CHANGED
|
@@ -39,402 +39,111 @@ var __export = (target, all) => {
|
|
|
39
39
|
// pkgs/@akanjs/language/index.ts
|
|
40
40
|
var exports_language = {};
|
|
41
41
|
__export(exports_language, {
|
|
42
|
+
AkanGraphDiff: () => AkanGraphDiff,
|
|
42
43
|
AkanIndex: () => AkanIndex,
|
|
44
|
+
FrameworkChannelReader: () => FrameworkChannelReader,
|
|
45
|
+
FrameworkMapBuilder: () => FrameworkMapBuilder,
|
|
46
|
+
FrameworkMapDoc: () => FrameworkMapDoc,
|
|
43
47
|
createAst: () => createAst,
|
|
44
48
|
createFactReader: () => createFactReader,
|
|
45
49
|
ownerOf: () => ownerOf
|
|
46
50
|
});
|
|
47
51
|
module.exports = __toCommonJS(exports_language);
|
|
48
52
|
|
|
49
|
-
// pkgs/@akanjs/language/
|
|
50
|
-
var
|
|
51
|
-
var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
52
|
-
var memberSourcePattern = /\.(signal|service|store)\.ts$/;
|
|
53
|
-
var skippedPattern = /(?:\.(?:constant|document|dictionary|test|spec)\.tsx?|\/lib\/(?:cnst|db|dict|sig|srv|st|useClient|useServer)\.ts|\/index\.tsx?|^(?:apps|libs)\/[^/]+\/(?:client|server)\.ts)$/;
|
|
54
|
-
var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
|
|
55
|
-
var moduleDirPattern = /\/lib\/([^/]+)\//;
|
|
56
|
-
var typeKinds = new Set(["model", "scalar", "enum"]);
|
|
57
|
-
var clientRoots = new Set(["fetch", "st", "sig"]);
|
|
58
|
-
|
|
59
|
-
class GraphBuilder {
|
|
60
|
-
#source;
|
|
61
|
-
#nodes = new Map;
|
|
62
|
-
#edges = new Map;
|
|
63
|
-
#contexts = new Map;
|
|
64
|
-
#members = new Map;
|
|
65
|
-
constructor(source) {
|
|
66
|
-
this.#source = source;
|
|
67
|
-
}
|
|
68
|
-
build() {
|
|
69
|
-
const { facts } = this.#source;
|
|
70
|
-
const constantsByOwner = new Map;
|
|
71
|
-
for (const file of facts)
|
|
72
|
-
constantsByOwner.set(file.owner, [...constantsByOwner.get(file.owner) ?? [], ...file.constants]);
|
|
73
|
-
const scopeOf = (owner) => [
|
|
74
|
-
...constantsByOwner.get(owner) ?? [],
|
|
75
|
-
...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
|
|
76
|
-
];
|
|
77
|
-
const typed = [];
|
|
78
|
-
for (const file of facts) {
|
|
79
|
-
const { owner } = file;
|
|
80
|
-
const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
|
|
81
|
-
const scalarClass = scalar ? file.constants.find((constant) => constant.name === capitalize(scalar)) : undefined;
|
|
82
|
-
if (scalar && scalarClass) {
|
|
83
|
-
const id = this.#key("scalar", owner, scalar);
|
|
84
|
-
const node = this.#addNode(id, "scalar", owner, scalar, scalarClass.name, scalarClass.span);
|
|
85
|
-
node.fields = this.#fieldsOf(scalarClass, scopeOf(owner));
|
|
86
|
-
typed.push(node);
|
|
87
|
-
}
|
|
88
|
-
for (const item of file.enums) {
|
|
89
|
-
const id = this.#key("enum", owner, item.name);
|
|
90
|
-
this.#addNode(id, "enum", owner, this.#moduleDirOf(file.fileName), item.name, item.span).values = item.values;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
for (const file of facts) {
|
|
94
|
-
const { owner } = file;
|
|
95
|
-
for (const service of file.services) {
|
|
96
|
-
const refName = service.model;
|
|
97
|
-
if (!refName)
|
|
98
|
-
continue;
|
|
99
|
-
const modelId = this.#key("model", owner, refName);
|
|
100
|
-
const modelClass = constantsByOwner.get(owner)?.find((constant) => constant.name === capitalize(refName));
|
|
101
|
-
if (modelClass && !this.#nodes.has(modelId)) {
|
|
102
|
-
const node = this.#addNode(modelId, "model", owner, refName, modelClass.name, modelClass.span);
|
|
103
|
-
node.fields = this.#fieldsOf(modelClass, scopeOf(owner));
|
|
104
|
-
typed.push(node);
|
|
105
|
-
}
|
|
106
|
-
for (const method of service.methods)
|
|
107
|
-
this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
|
|
108
|
-
}
|
|
109
|
-
for (const filter of file.filters) {
|
|
110
|
-
if (!filter.name.endsWith("Filter"))
|
|
111
|
-
continue;
|
|
112
|
-
const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
|
|
113
|
-
this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
|
|
114
|
-
for (const query of filter.queries)
|
|
115
|
-
this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span);
|
|
116
|
-
}
|
|
117
|
-
for (const slice of file.slices) {
|
|
118
|
-
const refName = slice.model;
|
|
119
|
-
if (!refName)
|
|
120
|
-
continue;
|
|
121
|
-
const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
|
|
122
|
-
for (const guard of slice.guards)
|
|
123
|
-
this.#guard(fallback, guard);
|
|
124
|
-
for (const key of slice.keys) {
|
|
125
|
-
const detail = [...key.guards, ...key.live ? ["live"] : []];
|
|
126
|
-
const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
127
|
-
for (const guard of key.guards)
|
|
128
|
-
this.#guard(node, guard);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
for (const endpoint of file.endpoints) {
|
|
132
|
-
const refName = endpoint.model;
|
|
133
|
-
if (!refName)
|
|
134
|
-
continue;
|
|
135
|
-
for (const key of endpoint.keys) {
|
|
136
|
-
const detail = [...key.verb ? [key.verb] : [], ...key.guards];
|
|
137
|
-
const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
138
|
-
for (const guard of key.guards)
|
|
139
|
-
this.#guard(node, guard);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
for (const store of file.stores)
|
|
143
|
-
if (store.model)
|
|
144
|
-
for (const action of store.actions)
|
|
145
|
-
this.#addMember("action", owner, store.model, action, `${store.name}.${action.name}`, file.fileName);
|
|
146
|
-
}
|
|
147
|
-
const types = new Map;
|
|
148
|
-
for (const node of this.#nodes.values())
|
|
149
|
-
if (typeKinds.has(node.kind))
|
|
150
|
-
types.set(node.label, [...types.get(node.label) ?? [], node]);
|
|
151
|
-
for (const node of typed)
|
|
152
|
-
for (const field of node.fields ?? []) {
|
|
153
|
-
const candidates = (field.type ? types.get(field.type) ?? [] : []).filter((type) => this.#visible(type.owner, node.owner));
|
|
154
|
-
field.ref = (candidates.find((type) => type.owner === node.owner) ?? candidates[0])?.id ?? null;
|
|
155
|
-
if (field.ref)
|
|
156
|
-
this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
|
|
157
|
-
}
|
|
158
|
-
for (const [fileName, words] of this.#source.files)
|
|
159
|
-
this.#walk(fileName, words);
|
|
160
|
-
return {
|
|
161
|
-
version: 1,
|
|
162
|
-
nodes: [...this.#nodes.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
163
|
-
edges: [...this.#edges.values()]
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
#key(kind, owner, scope, name = "") {
|
|
167
|
-
return `${kind}:${owner ?? ""}:${scope}${name ? `.${name}` : ""}`;
|
|
168
|
-
}
|
|
169
|
-
#location(span) {
|
|
170
|
-
return { file: this.#source.relative(span.fileName), line: span.line + 1, column: span.character + 1 };
|
|
171
|
-
}
|
|
172
|
-
#addNode(id, kind, owner, module2, name, span, detail = [], label = (kind === "query" || kind === "slice") && module2 ? `${module2}${name ? `.${name}` : " (default)"}` : name) {
|
|
173
|
-
const existing = this.#nodes.get(id);
|
|
174
|
-
if (existing)
|
|
175
|
-
return existing;
|
|
176
|
-
const node = {
|
|
177
|
-
id,
|
|
178
|
-
kind,
|
|
179
|
-
name,
|
|
180
|
-
label,
|
|
181
|
-
owner,
|
|
182
|
-
module: module2,
|
|
183
|
-
detail,
|
|
184
|
-
at: span ? this.#location(span) : null
|
|
185
|
-
};
|
|
186
|
-
this.#nodes.set(id, node);
|
|
187
|
-
return node;
|
|
188
|
-
}
|
|
189
|
-
#addMember(kind, owner, refName, member, label, fileName, detail = []) {
|
|
190
|
-
const id = this.#key(kind, owner, refName, member.name);
|
|
191
|
-
const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
|
|
192
|
-
this.#contexts.set(fileName, [
|
|
193
|
-
...this.#contexts.get(fileName) ?? [],
|
|
194
|
-
{ id, kind, owner, module: refName, extent: member.extent }
|
|
195
|
-
]);
|
|
196
|
-
const byName = `${kind}:${member.name}`;
|
|
197
|
-
this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
|
|
198
|
-
return node;
|
|
199
|
-
}
|
|
200
|
-
#addEdge(from, to, kind, label, at) {
|
|
201
|
-
if (from === to)
|
|
202
|
-
return;
|
|
203
|
-
const key = `${from}
|
|
53
|
+
// pkgs/@akanjs/language/akanGraphDiff.ts
|
|
54
|
+
var edgeKeyOf = ({ from, to, kind, label }) => `${from}
|
|
204
55
|
${to}
|
|
205
56
|
${kind}
|
|
206
57
|
${label ?? ""}`;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
type: field.type,
|
|
242
|
-
array: field.array,
|
|
243
|
-
optional: field.optional,
|
|
244
|
-
ref: null,
|
|
245
|
-
declaredIn: current.name,
|
|
246
|
-
at: this.#location(field.span)
|
|
247
|
-
});
|
|
248
|
-
};
|
|
249
|
-
visit(constant);
|
|
250
|
-
return [...fields.values()];
|
|
251
|
-
}
|
|
252
|
-
#visible(target, from) {
|
|
253
|
-
return !from || !target || target === from || target.startsWith("libs/");
|
|
254
|
-
}
|
|
255
|
-
#targetNodeOf({ family, owner, refName, sourceKey }) {
|
|
256
|
-
const isDefault = /^[A-Z]/.test(sourceKey);
|
|
257
|
-
const id = family === "slice" || family === "query" ? this.#key(family, owner, refName, isDefault ? "" : sourceKey) : family === "model" || family === "formSetter" ? this.#key("model", owner, refName) : family === "endpoint" ? this.#key("endpoint", owner, refName, sourceKey) : null;
|
|
258
|
-
return id && this.#nodes.has(id) ? id : null;
|
|
58
|
+
var shapeOf = ({ type, array, optional, declaredIn }) => `${type ?? ""}${array ? "[]" : ""}${optional ? "?" : ""} ${declaredIn}`;
|
|
59
|
+
var isEdited = (before, after) => before.digest !== undefined && after.digest !== undefined && before.digest !== after.digest;
|
|
60
|
+
var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
61
|
+
|
|
62
|
+
class AkanGraphDiff {
|
|
63
|
+
static between(base, head) {
|
|
64
|
+
const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
|
|
65
|
+
const headNodes = new Map(head.nodes.map((node) => [node.id, node]));
|
|
66
|
+
const marked = new Map;
|
|
67
|
+
for (const node of head.nodes) {
|
|
68
|
+
const before = baseNodes.get(node.id);
|
|
69
|
+
const next = before ? AkanGraphDiff.#compare(before, node) : AkanGraphDiff.#mark(node, "added");
|
|
70
|
+
if (next)
|
|
71
|
+
marked.set(node.id, next);
|
|
72
|
+
}
|
|
73
|
+
for (const node of base.nodes)
|
|
74
|
+
if (!headNodes.has(node.id))
|
|
75
|
+
marked.set(node.id, AkanGraphDiff.#mark(node, "removed"));
|
|
76
|
+
const baseKeys = new Set(base.edges.map(edgeKeyOf));
|
|
77
|
+
const headKeys = new Set(head.edges.map(edgeKeyOf));
|
|
78
|
+
const edges = [
|
|
79
|
+
...head.edges.flatMap((edge) => !baseKeys.has(edgeKeyOf(edge)) ? [{ ...edge, change: "added" }] : marked.has(edge.from) || marked.has(edge.to) ? [edge] : []),
|
|
80
|
+
...base.edges.filter((edge) => !headKeys.has(edgeKeyOf(edge))).map((edge) => ({ ...edge, change: "removed" }))
|
|
81
|
+
];
|
|
82
|
+
const context = edges.flatMap((edge) => [edge.from, edge.to]).filter((id) => !marked.has(id)).flatMap((id) => {
|
|
83
|
+
const node = headNodes.get(id) ?? baseNodes.get(id);
|
|
84
|
+
return node ? [node] : [];
|
|
85
|
+
});
|
|
86
|
+
const nodes = [...new Map([...marked.values(), ...context].map((node) => [node.id, node])).values()].map(AkanGraphDiff.#withoutDigest).sort(byId);
|
|
87
|
+
const summary = { added: 0, removed: 0, changed: 0 };
|
|
88
|
+
for (const node of marked.values())
|
|
89
|
+
if (node.change)
|
|
90
|
+
summary[node.change] += 1;
|
|
91
|
+
return { version: 1, nodes, edges, summary };
|
|
259
92
|
}
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
const own = nodes.filter((node) => node.owner === owner);
|
|
263
|
-
return (own.length ? own : nodes).map((node) => node.id);
|
|
93
|
+
static #mark(node, change) {
|
|
94
|
+
return node.fields ? { ...node, change, fields: node.fields.map((field) => ({ ...field, change })) } : { ...node, change };
|
|
264
95
|
}
|
|
265
|
-
#
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return [];
|
|
96
|
+
static #compare(before, after) {
|
|
97
|
+
const fields = AkanGraphDiff.#fieldsBetween(before.fields, after.fields);
|
|
98
|
+
const isChanged = isEdited(before, after) || before.label !== after.label || before.detail.join(`
|
|
99
|
+
`) !== after.detail.join(`
|
|
100
|
+
`) || (before.values ?? []).join(`
|
|
101
|
+
`) !== (after.values ?? []).join(`
|
|
102
|
+
`) || !!fields?.some((field) => field.change);
|
|
103
|
+
if (!isChanged)
|
|
104
|
+
return null;
|
|
105
|
+
return fields ? { ...after, change: "changed", fields } : { ...after, change: "changed" };
|
|
276
106
|
}
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
const contexts = this.#contexts.get(fileName);
|
|
280
|
-
const isComponentFile = !memberSourcePattern.test(fileName) && !skippedPattern.test(relative) && (words.includes(`
|
|
281
|
-
fetch
|
|
282
|
-
`) || words.includes(`
|
|
283
|
-
st
|
|
284
|
-
`));
|
|
285
|
-
if (!contexts && !isComponentFile)
|
|
286
|
-
return;
|
|
287
|
-
const text = this.#source.readText(fileName);
|
|
288
|
-
if (text === undefined)
|
|
107
|
+
static #fieldsBetween(before, after) {
|
|
108
|
+
if (!before && !after)
|
|
289
109
|
return;
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
ts.forEachChild(node, visit);
|
|
302
|
-
};
|
|
303
|
-
visit(source);
|
|
110
|
+
const previous = new Map((before ?? []).map((field) => [field.name, field]));
|
|
111
|
+
const names = new Set((after ?? []).map((field) => field.name));
|
|
112
|
+
return [
|
|
113
|
+
...(after ?? []).map((field) => {
|
|
114
|
+
const old = previous.get(field.name);
|
|
115
|
+
if (!old)
|
|
116
|
+
return { ...field, change: "added" };
|
|
117
|
+
return shapeOf(old) === shapeOf(field) && !isEdited(old, field) ? field : { ...field, change: "changed" };
|
|
118
|
+
}),
|
|
119
|
+
...(before ?? []).filter((field) => !names.has(field.name)).map((field) => ({ ...field, change: "removed" }))
|
|
120
|
+
];
|
|
304
121
|
}
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
|
|
308
|
-
if (!targets.length)
|
|
309
|
-
return;
|
|
310
|
-
if (context.pending && !this.#nodes.has(context.id))
|
|
311
|
-
this.#nodes.set(context.id, context.pending);
|
|
312
|
-
const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
|
|
313
|
-
const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
|
|
314
|
-
const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
|
|
315
|
-
const at = { file: this.#source.relative(fileName), line: position.line + 1, column: position.character + 1 };
|
|
316
|
-
for (const target of new Set(own.length ? own : targets))
|
|
317
|
-
this.#addEdge(context.id, target, kind, identifier.text, at);
|
|
318
|
-
}
|
|
319
|
-
#chainOf(expression) {
|
|
320
|
-
const { ts } = this.#source;
|
|
321
|
-
if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
322
|
-
return this.#chainOf(expression.expression);
|
|
323
|
-
if (ts.isIdentifier(expression))
|
|
324
|
-
return [expression.text];
|
|
325
|
-
if (expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
326
|
-
return ["this"];
|
|
327
|
-
if (!ts.isPropertyAccessExpression(expression))
|
|
328
|
-
return null;
|
|
329
|
-
const head = this.#chainOf(expression.expression);
|
|
330
|
-
return head ? [...head, expression.name.text] : null;
|
|
331
|
-
}
|
|
332
|
-
#componentOf(source, node) {
|
|
333
|
-
const { ts } = this.#source;
|
|
334
|
-
let top = node;
|
|
335
|
-
while (top.parent && !ts.isSourceFile(top.parent))
|
|
336
|
-
top = top.parent;
|
|
337
|
-
const declared = ts.isVariableStatement(top) ? top.declarationList.declarations[0]?.name : ts.isFunctionDeclaration(top) || ts.isClassDeclaration(top) ? top.name : undefined;
|
|
338
|
-
const isDefaultExport = ts.canHaveModifiers(top) && !!ts.getModifiers(top)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword);
|
|
339
|
-
const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
|
|
340
|
-
const relative = this.#source.relative(source.fileName);
|
|
341
|
-
const owner = this.#source.ownerOf(source.fileName);
|
|
342
|
-
const id = `component:${relative}#${name}`;
|
|
343
|
-
const context = {
|
|
344
|
-
id,
|
|
345
|
-
kind: "component",
|
|
346
|
-
owner,
|
|
347
|
-
module: null,
|
|
348
|
-
extent: { start: top.getStart(source), end: top.getEnd() }
|
|
349
|
-
};
|
|
350
|
-
if (this.#nodes.has(id))
|
|
351
|
-
return context;
|
|
352
|
-
const pagePrefix = owner ? `${owner}/page/` : null;
|
|
353
|
-
const route = pagePrefix && relative.startsWith(pagePrefix) ? relative.slice(pagePrefix.length).replace(/\.tsx?$/, "") : null;
|
|
354
|
-
const base = relative.slice(relative.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
|
|
355
|
-
const label = route ? `${route}${name === "default" ? "" : `#${name}`}` : name === "default" ? base : `${base}.${name}`;
|
|
356
|
-
const position = source.getLineAndCharacterOfPosition((declared ?? top).getStart(source));
|
|
357
|
-
context.pending = {
|
|
358
|
-
id,
|
|
359
|
-
kind: "component",
|
|
360
|
-
name,
|
|
361
|
-
label,
|
|
362
|
-
owner,
|
|
363
|
-
module: this.#moduleDirOf(source.fileName),
|
|
364
|
-
detail: route ? ["page"] : [],
|
|
365
|
-
at: { file: relative, line: position.line + 1, column: position.character + 1 }
|
|
366
|
-
};
|
|
367
|
-
return context;
|
|
122
|
+
static #withoutDigest({ digest: _digest, ...node }) {
|
|
123
|
+
return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
|
|
368
124
|
}
|
|
369
125
|
}
|
|
370
|
-
|
|
371
|
-
// pkgs/@akanjs/language/ast.ts
|
|
372
|
-
var createAst = (ts) => {
|
|
373
|
-
const sourceFileFor = (fileName, content, scriptKind = ts.ScriptKind.TS) => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
374
|
-
const nodeName = (node) => {
|
|
375
|
-
if (!node)
|
|
376
|
-
return null;
|
|
377
|
-
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node))
|
|
378
|
-
return node.text;
|
|
379
|
-
return null;
|
|
380
|
-
};
|
|
381
|
-
const propertyName = (node) => ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isMethodDeclaration(node) ? nodeName(node.name) : null;
|
|
382
|
-
const expressionName = (expression) => {
|
|
383
|
-
if (ts.isIdentifier(expression))
|
|
384
|
-
return expression.text;
|
|
385
|
-
if (ts.isPropertyAccessExpression(expression))
|
|
386
|
-
return expression.name.text;
|
|
387
|
-
if (ts.isCallExpression(expression))
|
|
388
|
-
return expressionName(expression.expression);
|
|
389
|
-
if (ts.isAsExpression(expression))
|
|
390
|
-
return expressionName(expression.expression);
|
|
391
|
-
return null;
|
|
392
|
-
};
|
|
393
|
-
const callExpressionName = (node) => ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : expressionName(node.expression);
|
|
394
|
-
const firstObjectReturnedByArrow = (node) => {
|
|
395
|
-
if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node))
|
|
396
|
-
return null;
|
|
397
|
-
if (ts.isObjectLiteralExpression(node.body))
|
|
398
|
-
return node.body;
|
|
399
|
-
if (ts.isParenthesizedExpression(node.body) && ts.isObjectLiteralExpression(node.body.expression)) {
|
|
400
|
-
return node.body.expression;
|
|
401
|
-
}
|
|
402
|
-
if (!ts.isBlock(node.body))
|
|
403
|
-
return null;
|
|
404
|
-
for (const statement of node.body.statements) {
|
|
405
|
-
if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(statement.expression)) {
|
|
406
|
-
return statement.expression;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return null;
|
|
410
|
-
};
|
|
411
|
-
const heritageCallNamed = (node, names) => {
|
|
412
|
-
const heritage = node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
|
|
413
|
-
for (const { expression } of heritage) {
|
|
414
|
-
if (!ts.isCallExpression(expression))
|
|
415
|
-
continue;
|
|
416
|
-
const callee = expressionName(expression.expression);
|
|
417
|
-
if (callee && names.has(callee))
|
|
418
|
-
return { callee, call: expression };
|
|
419
|
-
}
|
|
420
|
-
return null;
|
|
421
|
-
};
|
|
422
|
-
const viaNames = new Set(["via"]);
|
|
423
|
-
const heritageCall = (node) => heritageCallNamed(node, viaNames)?.call ?? null;
|
|
424
|
-
return {
|
|
425
|
-
sourceFileFor,
|
|
426
|
-
nodeName,
|
|
427
|
-
propertyName,
|
|
428
|
-
expressionName,
|
|
429
|
-
callExpressionName,
|
|
430
|
-
firstObjectReturnedByArrow,
|
|
431
|
-
heritageCallNamed,
|
|
432
|
-
heritageCall
|
|
433
|
-
};
|
|
434
|
-
};
|
|
435
|
-
|
|
436
126
|
// pkgs/@akanjs/language/moduleFacts.ts
|
|
437
|
-
var
|
|
127
|
+
var testFilePattern = /\.(?:test|spec)\.tsx?$/;
|
|
128
|
+
var moduleCallees = new Set([
|
|
129
|
+
"via",
|
|
130
|
+
"from",
|
|
131
|
+
"into",
|
|
132
|
+
"serve",
|
|
133
|
+
"slice",
|
|
134
|
+
"endpoint",
|
|
135
|
+
"internal",
|
|
136
|
+
"store",
|
|
137
|
+
"enumOf",
|
|
138
|
+
"adapt",
|
|
139
|
+
"dangerouslyAdapt"
|
|
140
|
+
]);
|
|
141
|
+
var intervalUnits = [
|
|
142
|
+
[86400000, "d"],
|
|
143
|
+
[3600000, "h"],
|
|
144
|
+
[60000, "m"],
|
|
145
|
+
[1000, "s"]
|
|
146
|
+
];
|
|
438
147
|
var argMethods = new Set(["param", "body", "search", "room", "msg"]);
|
|
439
148
|
var ownerOf = (relativePath) => {
|
|
440
149
|
const [root, name] = relativePath.split("/");
|
|
@@ -478,15 +187,23 @@ var createFactReader = (ts, ast) => {
|
|
|
478
187
|
};
|
|
479
188
|
const returnedObject = (builder) => builder ? ast.firstObjectReturnedByArrow(builder) : null;
|
|
480
189
|
const builderOf = (call) => [...call.arguments].reverse().find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
|
|
190
|
+
const guardNameOf = (expression) => {
|
|
191
|
+
const value = unwrap(expression);
|
|
192
|
+
return ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) ? `${value.expression.text}.${value.name.text}` : ast.expressionName(value);
|
|
193
|
+
};
|
|
481
194
|
const guardsIn = (object) => {
|
|
482
195
|
const guards = object?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "guards")?.initializer;
|
|
483
196
|
if (!guards)
|
|
484
197
|
return [];
|
|
485
198
|
if (ts.isArrayLiteralExpression(guards))
|
|
486
|
-
return guards.elements.map(
|
|
199
|
+
return guards.elements.map(guardNameOf).filter((name) => name !== null);
|
|
487
200
|
if (ts.isObjectLiteralExpression(guards))
|
|
488
|
-
return guards.properties.filter(ts.isPropertyAssignment).
|
|
489
|
-
|
|
201
|
+
return guards.properties.filter(ts.isPropertyAssignment).flatMap((property) => {
|
|
202
|
+
const value = unwrap(property.initializer);
|
|
203
|
+
const names = ts.isArrayLiteralExpression(value) ? value.elements.map(guardNameOf) : [guardNameOf(value)];
|
|
204
|
+
return names.map((name) => `${ast.nodeName(property.name)} ${name ?? "?"}`);
|
|
205
|
+
});
|
|
206
|
+
const single = guardNameOf(guards);
|
|
490
207
|
return single ? [single] : [];
|
|
491
208
|
};
|
|
492
209
|
const optionObjectOf = (call) => call.arguments.find((arg) => ts.isObjectLiteralExpression(arg) && arg.properties.some((property) => ast.propertyName(property) === "guards"));
|
|
@@ -503,7 +220,7 @@ var createFactReader = (ts, ast) => {
|
|
|
503
220
|
const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
504
221
|
const name = ast.propertyName(property);
|
|
505
222
|
const node = keyName(property);
|
|
506
|
-
return name && node ? [{ name, span: spanOf(source, node) }] : [];
|
|
223
|
+
return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
|
|
507
224
|
});
|
|
508
225
|
const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
509
226
|
if (!ts.isPropertyAssignment(property))
|
|
@@ -540,6 +257,89 @@ var createFactReader = (ts, ast) => {
|
|
|
540
257
|
return null;
|
|
541
258
|
};
|
|
542
259
|
};
|
|
260
|
+
const numberOf = (expression) => {
|
|
261
|
+
const value = unwrap(expression);
|
|
262
|
+
if (ts.isNumericLiteral(value))
|
|
263
|
+
return Number(value.text.replace(/_/g, ""));
|
|
264
|
+
if (!ts.isBinaryExpression(value))
|
|
265
|
+
return null;
|
|
266
|
+
const [left, right] = [numberOf(value.left), numberOf(value.right)];
|
|
267
|
+
if (left === null || right === null)
|
|
268
|
+
return null;
|
|
269
|
+
const operator = value.operatorToken.kind;
|
|
270
|
+
if (operator === ts.SyntaxKind.AsteriskToken)
|
|
271
|
+
return left * right;
|
|
272
|
+
if (operator === ts.SyntaxKind.PlusToken)
|
|
273
|
+
return left + right;
|
|
274
|
+
if (operator === ts.SyntaxKind.MinusToken)
|
|
275
|
+
return left - right;
|
|
276
|
+
return operator === ts.SyntaxKind.SlashToken && right ? left / right : null;
|
|
277
|
+
};
|
|
278
|
+
const durationOf = (expression, source) => {
|
|
279
|
+
if (!expression)
|
|
280
|
+
return null;
|
|
281
|
+
const ms = numberOf(expression);
|
|
282
|
+
if (ms === null)
|
|
283
|
+
return expression.getText(source);
|
|
284
|
+
const [size, unit] = intervalUnits.find(([size]) => ms >= size && ms % size === 0) ?? [1, "ms"];
|
|
285
|
+
return `${ms / size}${unit}`;
|
|
286
|
+
};
|
|
287
|
+
const modesOf = (option) => {
|
|
288
|
+
const object = option && unwrap(option);
|
|
289
|
+
if (!object || !ts.isObjectLiteralExpression(object))
|
|
290
|
+
return [];
|
|
291
|
+
return object.properties.filter(ts.isPropertyAssignment).flatMap((property) => {
|
|
292
|
+
const name = ast.nodeName(property.name);
|
|
293
|
+
const value = unwrap(property.initializer);
|
|
294
|
+
if (name === "enabled")
|
|
295
|
+
return value.kind === ts.SyntaxKind.FalseKeyword ? ["disabled"] : [];
|
|
296
|
+
if (name === "once")
|
|
297
|
+
return value.kind === ts.SyntaxKind.TrueKeyword ? ["once"] : [];
|
|
298
|
+
if (name === "lock")
|
|
299
|
+
return value.kind === ts.SyntaxKind.FalseKeyword ? ["unlocked"] : [];
|
|
300
|
+
if (name !== "serverMode" && name !== "operationMode")
|
|
301
|
+
return [];
|
|
302
|
+
const values = ts.isArrayLiteralExpression(value) ? [...value.elements] : [value];
|
|
303
|
+
return values.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : ts.isPropertyAccessExpression(element) ? [element.name.text] : []);
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
const internalKeysOf = (source, builder) => {
|
|
307
|
+
const verbOf = verbReaderOf(builder?.parameters[0]);
|
|
308
|
+
return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
309
|
+
if (!ts.isPropertyAssignment(property))
|
|
310
|
+
return [];
|
|
311
|
+
const name = ast.nodeName(property.name);
|
|
312
|
+
if (!name)
|
|
313
|
+
return [];
|
|
314
|
+
const { root } = chainOf(property.initializer);
|
|
315
|
+
const type = root ? verbOf(root.expression) : null;
|
|
316
|
+
const [first, second] = root?.arguments ?? [];
|
|
317
|
+
const schedule = type === "cron" && first ? ts.isStringLiteralLike(first) ? first.text : first.getText(source) : type === "interval" || type === "timeout" ? durationOf(first, source) : null;
|
|
318
|
+
const option = type === "initialize" || type === "destroy" ? first : second;
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
name,
|
|
322
|
+
span: spanOf(source, property.name),
|
|
323
|
+
extent: extentOf(source, property),
|
|
324
|
+
type,
|
|
325
|
+
schedule,
|
|
326
|
+
modes: modesOf(option)
|
|
327
|
+
}
|
|
328
|
+
];
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
const plugsOf = (source, builder) => {
|
|
332
|
+
const verbOf = verbReaderOf(builder?.parameters[0]);
|
|
333
|
+
return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
334
|
+
if (!ts.isPropertyAssignment(property))
|
|
335
|
+
return [];
|
|
336
|
+
const name = ast.nodeName(property.name);
|
|
337
|
+
const { root } = chainOf(property.initializer);
|
|
338
|
+
const [target] = root && verbOf(root.expression) === "plug" ? root.arguments : [];
|
|
339
|
+
const targetName = target && guardNameOf(target);
|
|
340
|
+
return name && targetName ? [{ name, span: spanOf(source, property.name), target: targetName }] : [];
|
|
341
|
+
});
|
|
342
|
+
};
|
|
543
343
|
const endpointKeysOf = (source, builder) => {
|
|
544
344
|
const verbOf = verbReaderOf(builder?.parameters[0]);
|
|
545
345
|
return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
@@ -578,7 +378,13 @@ var createFactReader = (ts, ast) => {
|
|
|
578
378
|
return [];
|
|
579
379
|
const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
|
|
580
380
|
return [
|
|
581
|
-
{
|
|
381
|
+
{
|
|
382
|
+
name,
|
|
383
|
+
span: spanOf(source, node),
|
|
384
|
+
extent: extentOf(source, property),
|
|
385
|
+
...typeOf(root?.arguments[0]),
|
|
386
|
+
optional: methods.includes("optional")
|
|
387
|
+
}
|
|
582
388
|
];
|
|
583
389
|
});
|
|
584
390
|
const constantOf = (source, name, call) => {
|
|
@@ -600,11 +406,155 @@ var createFactReader = (ts, ast) => {
|
|
|
600
406
|
return [];
|
|
601
407
|
return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
|
|
602
408
|
});
|
|
409
|
+
const importsOf = (statement) => {
|
|
410
|
+
const { importClause, moduleSpecifier } = statement;
|
|
411
|
+
if (!importClause || !ts.isStringLiteral(moduleSpecifier))
|
|
412
|
+
return [];
|
|
413
|
+
const from = moduleSpecifier.text;
|
|
414
|
+
const bindings = importClause.namedBindings;
|
|
415
|
+
return [
|
|
416
|
+
...importClause.name ? [{ name: importClause.name.text, imported: "default", from }] : [],
|
|
417
|
+
...bindings && ts.isNamespaceImport(bindings) ? [{ name: bindings.name.text, imported: "*", from }] : [],
|
|
418
|
+
...bindings && ts.isNamedImports(bindings) ? bindings.elements.map((element) => ({
|
|
419
|
+
name: element.name.text,
|
|
420
|
+
imported: (element.propertyName ?? element.name).text,
|
|
421
|
+
from
|
|
422
|
+
})) : []
|
|
423
|
+
];
|
|
424
|
+
};
|
|
425
|
+
const implementsGuard = (node, guardNames) => !!node.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ImplementsKeyword && clause.types.some(({ expression }) => guardNames.has(ast.expressionName(expression) ?? "")));
|
|
426
|
+
const guardClassIn = (node, guardNames) => {
|
|
427
|
+
if ((ts.isClassExpression(node) || ts.isClassDeclaration(node)) && implementsGuard(node, guardNames))
|
|
428
|
+
return node;
|
|
429
|
+
return ts.forEachChild(node, (child) => guardClassIn(child, guardNames)) ?? null;
|
|
430
|
+
};
|
|
431
|
+
const callsIn = (node, names) => {
|
|
432
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text))
|
|
433
|
+
return node.expression.text;
|
|
434
|
+
return ts.forEachChild(node, (child) => callsIn(child, names)) ?? null;
|
|
435
|
+
};
|
|
436
|
+
const scopeOf = (node) => {
|
|
437
|
+
const scope = node.members.find((member) => ts.isPropertyDeclaration(member) && ast.nodeName(member.name) === "scope" && !!ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))?.initializer;
|
|
438
|
+
const value = scope && unwrap(scope);
|
|
439
|
+
return value && ts.isStringLiteralLike(value) ? value.text : null;
|
|
440
|
+
};
|
|
441
|
+
const docOf = (source, node) => {
|
|
442
|
+
const { text } = source;
|
|
443
|
+
const ranges = ts.getLeadingCommentRanges(text, node.getFullStart()) ?? [];
|
|
444
|
+
const comments = [];
|
|
445
|
+
let end = node.getStart(source);
|
|
446
|
+
for (const range of [...ranges].reverse()) {
|
|
447
|
+
if ((text.slice(range.end, end).match(/\n/g)?.length ?? 0) > 1)
|
|
448
|
+
break;
|
|
449
|
+
comments.unshift(text.slice(range.pos, range.end));
|
|
450
|
+
end = range.pos;
|
|
451
|
+
}
|
|
452
|
+
const lines = comments.flatMap((comment) => comment.split(`
|
|
453
|
+
`)).map((line) => line.trim().replace(/^\/\*\*?|\*\/$/g, "").replace(/^(?:\/\/[?*!]?|\*)/, "").replace(/\{@link\s+([^}\s|]+)[^}]*\}/g, "$1").trim()).filter((line) => line && !/^(?:biome-ignore|eslint-|@ts-)/.test(line));
|
|
454
|
+
return lines.length ? lines.join(" ") : null;
|
|
455
|
+
};
|
|
456
|
+
const functionsOf = (source) => {
|
|
457
|
+
const functions = new Map;
|
|
458
|
+
for (const statement of source.statements) {
|
|
459
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body)
|
|
460
|
+
functions.set(statement.name.text, { statement, name: statement.name, body: statement.body });
|
|
461
|
+
if (!ts.isVariableStatement(statement))
|
|
462
|
+
continue;
|
|
463
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
464
|
+
const initializer = declaration.initializer && unwrap(declaration.initializer);
|
|
465
|
+
if (initializer && ts.isIdentifier(declaration.name) && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)))
|
|
466
|
+
functions.set(declaration.name.text, { statement, name: declaration.name, body: initializer.body });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return functions;
|
|
470
|
+
};
|
|
471
|
+
const guardFactoriesOf = (source, guardNames) => {
|
|
472
|
+
const functions = functionsOf(source);
|
|
473
|
+
const factories = new Map;
|
|
474
|
+
const factoryOf = (name, scope, via) => {
|
|
475
|
+
const found = functions.get(name);
|
|
476
|
+
if (!found)
|
|
477
|
+
return;
|
|
478
|
+
factories.set(name, {
|
|
479
|
+
name,
|
|
480
|
+
span: spanOf(source, found.name),
|
|
481
|
+
extent: extentOf(source, found.statement),
|
|
482
|
+
scope,
|
|
483
|
+
doc: docOf(source, found.statement),
|
|
484
|
+
via
|
|
485
|
+
});
|
|
486
|
+
};
|
|
487
|
+
for (const [name, { body }] of functions) {
|
|
488
|
+
const guard = guardClassIn(body, guardNames);
|
|
489
|
+
if (guard)
|
|
490
|
+
factoryOf(name, scopeOf(guard), null);
|
|
491
|
+
}
|
|
492
|
+
for (let isGrowing = true;isGrowing; ) {
|
|
493
|
+
isGrowing = false;
|
|
494
|
+
for (const [name, { body }] of functions) {
|
|
495
|
+
const called = factories.has(name) ? null : callsIn(body, new Set(factories.keys()));
|
|
496
|
+
if (!called)
|
|
497
|
+
continue;
|
|
498
|
+
factoryOf(name, factories.get(called)?.scope ?? null, called);
|
|
499
|
+
isGrowing = true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return factories;
|
|
503
|
+
};
|
|
504
|
+
const guardsOf = (source, imports) => {
|
|
505
|
+
const guardNames = new Set([
|
|
506
|
+
"Guard",
|
|
507
|
+
...imports.filter(({ imported }) => imported === "Guard").map(({ name }) => name)
|
|
508
|
+
]);
|
|
509
|
+
const imported = new Set(imports.map(({ name }) => name));
|
|
510
|
+
const factories = guardFactoriesOf(source, guardNames);
|
|
511
|
+
const guards = [];
|
|
512
|
+
const calls = [];
|
|
513
|
+
for (const statement of source.statements) {
|
|
514
|
+
if (ts.isClassDeclaration(statement) && statement.name && implementsGuard(statement, guardNames))
|
|
515
|
+
guards.push({
|
|
516
|
+
name: statement.name.text,
|
|
517
|
+
span: spanOf(source, statement.name),
|
|
518
|
+
extent: extentOf(source, statement),
|
|
519
|
+
scope: scopeOf(statement),
|
|
520
|
+
doc: docOf(source, statement),
|
|
521
|
+
factory: null
|
|
522
|
+
});
|
|
523
|
+
if (!ts.isVariableStatement(statement))
|
|
524
|
+
continue;
|
|
525
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
526
|
+
const initializer = declaration.initializer && unwrap(declaration.initializer);
|
|
527
|
+
if (!initializer || !ts.isIdentifier(declaration.name))
|
|
528
|
+
continue;
|
|
529
|
+
const made = {
|
|
530
|
+
name: declaration.name.text,
|
|
531
|
+
span: spanOf(source, declaration.name),
|
|
532
|
+
extent: extentOf(source, statement),
|
|
533
|
+
doc: docOf(source, statement)
|
|
534
|
+
};
|
|
535
|
+
if (ts.isClassExpression(initializer) && implementsGuard(initializer, guardNames))
|
|
536
|
+
guards.push({ ...made, scope: scopeOf(initializer), factory: null });
|
|
537
|
+
if (!ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression))
|
|
538
|
+
continue;
|
|
539
|
+
const callee = initializer.expression.text;
|
|
540
|
+
const factory = factories.get(callee);
|
|
541
|
+
if (factory)
|
|
542
|
+
guards.push({ ...made, scope: factory.scope, factory: callee });
|
|
543
|
+
else if (imported.has(callee))
|
|
544
|
+
calls.push({ ...made, callee });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return { guards, guardFactories: [...factories.values()], guardCalls: calls };
|
|
548
|
+
};
|
|
603
549
|
const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
550
|
+
const importsIn = (source) => source.statements.filter(ts.isImportDeclaration).flatMap(importsOf);
|
|
604
551
|
const read = (source, owner) => {
|
|
552
|
+
const imports = importsIn(source);
|
|
605
553
|
const facts = {
|
|
606
554
|
fileName: source.fileName,
|
|
607
555
|
owner,
|
|
556
|
+
imports,
|
|
557
|
+
...guardsOf(source, imports),
|
|
608
558
|
constants: [],
|
|
609
559
|
enums: [],
|
|
610
560
|
filters: [],
|
|
@@ -612,6 +562,8 @@ var createFactReader = (ts, ast) => {
|
|
|
612
562
|
services: [],
|
|
613
563
|
slices: [],
|
|
614
564
|
endpoints: [],
|
|
565
|
+
internals: [],
|
|
566
|
+
adaptors: [],
|
|
615
567
|
stores: []
|
|
616
568
|
};
|
|
617
569
|
for (const statement of source.statements) {
|
|
@@ -635,7 +587,8 @@ var createFactReader = (ts, ast) => {
|
|
|
635
587
|
facts.services.push({
|
|
636
588
|
...name,
|
|
637
589
|
model: moduleRefOf(call.arguments[0], "db"),
|
|
638
|
-
methods: methodsOf(source, statement)
|
|
590
|
+
methods: methodsOf(source, statement),
|
|
591
|
+
plugs: plugsOf(source, builder)
|
|
639
592
|
});
|
|
640
593
|
else if (callee === "slice")
|
|
641
594
|
facts.slices.push({
|
|
@@ -644,7 +597,22 @@ var createFactReader = (ts, ast) => {
|
|
|
644
597
|
guards: guardsIn(call.arguments.find(ts.isObjectLiteralExpression)),
|
|
645
598
|
keys: sliceKeysOf(source, builder)
|
|
646
599
|
});
|
|
647
|
-
else if (callee === "
|
|
600
|
+
else if (callee === "internal")
|
|
601
|
+
facts.internals.push({
|
|
602
|
+
...name,
|
|
603
|
+
model: moduleRefOf(call.arguments[0], "srv"),
|
|
604
|
+
keys: internalKeysOf(source, builder)
|
|
605
|
+
});
|
|
606
|
+
else if (callee === "adapt" || callee === "dangerouslyAdapt") {
|
|
607
|
+
const refName = call.arguments[0] && unwrap(call.arguments[0]);
|
|
608
|
+
facts.adaptors.push({
|
|
609
|
+
...name,
|
|
610
|
+
extent: extentOf(source, statement),
|
|
611
|
+
refName: refName && ts.isStringLiteralLike(refName) ? refName.text : null,
|
|
612
|
+
plugs: plugsOf(source, builder),
|
|
613
|
+
doc: docOf(source, statement)
|
|
614
|
+
});
|
|
615
|
+
} else if (callee === "endpoint")
|
|
648
616
|
facts.endpoints.push({
|
|
649
617
|
...name,
|
|
650
618
|
model: moduleRefOf(call.arguments[0], "srv"),
|
|
@@ -658,9 +626,965 @@ var createFactReader = (ts, ast) => {
|
|
|
658
626
|
actions: methodsOf(source, statement)
|
|
659
627
|
});
|
|
660
628
|
}
|
|
661
|
-
return facts;
|
|
629
|
+
return facts;
|
|
630
|
+
};
|
|
631
|
+
return { read, spanOf, extentOf, docOf, importsIn, moduleRefOf, chainOf, unwrap };
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// pkgs/akanjs/common/trimSlashes.ts
|
|
635
|
+
var trimTrailingSlashes = (value) => {
|
|
636
|
+
let end = value.length;
|
|
637
|
+
while (end > 0 && value[end - 1] === "/")
|
|
638
|
+
end -= 1;
|
|
639
|
+
return value.slice(0, end);
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// pkgs/akanjs/common/routeConvention.ts
|
|
643
|
+
var ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
|
|
644
|
+
var RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
|
|
645
|
+
var INTERNAL_ROOT_LAYOUT_LEAF = "__root_layout";
|
|
646
|
+
var IMPLICIT_LOCALE_SEGMENT = "[lang]";
|
|
647
|
+
var DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
|
|
648
|
+
var PAGE_ROUTE_EXPORTS = new Set([
|
|
649
|
+
"default",
|
|
650
|
+
"pageConfig",
|
|
651
|
+
"head",
|
|
652
|
+
"generateHead",
|
|
653
|
+
"Loading",
|
|
654
|
+
"guard"
|
|
655
|
+
]);
|
|
656
|
+
var LAYOUT_ROUTE_EXPORTS = new Set([...PAGE_ROUTE_EXPORTS, "NotFound", "Error"]);
|
|
657
|
+
var ROOT_LAYOUT_ROUTE_EXPORTS = new Set([
|
|
658
|
+
...LAYOUT_ROUTE_EXPORTS,
|
|
659
|
+
"fonts",
|
|
660
|
+
"manifest",
|
|
661
|
+
"theme",
|
|
662
|
+
"reconnect",
|
|
663
|
+
"wsConnect",
|
|
664
|
+
"layoutStyle",
|
|
665
|
+
"viewport",
|
|
666
|
+
"McpAppRoot"
|
|
667
|
+
]);
|
|
668
|
+
var RESERVED_ROUTE_CONFIG_EXPORTS = new Set([...ROOT_LAYOUT_ROUTE_EXPORTS].filter((name) => name !== "default" && !/^[A-Z]/.test(name)));
|
|
669
|
+
function parseRouteModuleKey(key) {
|
|
670
|
+
const match = ROUTE_SOURCE_RE.exec(key);
|
|
671
|
+
if (!match)
|
|
672
|
+
throw new Error(`[route-convention] invalid route module key: ${key}`);
|
|
673
|
+
const file = match[1];
|
|
674
|
+
const ext = match[2];
|
|
675
|
+
const moduleSegments = file.split("/").filter(Boolean);
|
|
676
|
+
const leaf = moduleSegments.at(-1);
|
|
677
|
+
if (!leaf)
|
|
678
|
+
throw new Error(`[route-convention] invalid route module key: ${key}`);
|
|
679
|
+
if (moduleSegments.includes(IMPLICIT_LOCALE_SEGMENT)) {
|
|
680
|
+
throw new Error(`[route-convention] Akan.js injects \`[lang]\` automatically. Move files one level up: ${key}`);
|
|
681
|
+
}
|
|
682
|
+
const isInternalRootLayout = leaf === INTERNAL_ROOT_LAYOUT_LEAF;
|
|
683
|
+
const kind = leaf === "_layout" || isInternalRootLayout ? "layout" : leaf === "_overrides" ? "overrides" : "page";
|
|
684
|
+
if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && !isInternalRootLayout) {
|
|
685
|
+
throw new Error(`[route-convention] unsupported reserved route file "${leaf}" in ${key}`);
|
|
686
|
+
}
|
|
687
|
+
const sourceRouteSegments = DIRECTORY_SCOPED_LEAVES.has(leaf) || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
|
|
688
|
+
const routeSegments = [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
|
|
689
|
+
for (const segment of routeSegments)
|
|
690
|
+
validateRouteSegment(segment, key);
|
|
691
|
+
return {
|
|
692
|
+
key,
|
|
693
|
+
kind,
|
|
694
|
+
routeSegments,
|
|
695
|
+
moduleSegments,
|
|
696
|
+
sourceRouteSegments,
|
|
697
|
+
ext,
|
|
698
|
+
leaf,
|
|
699
|
+
pattern: normalizeRoutePattern(routeSegments),
|
|
700
|
+
isInternalRootLayout
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function tryParseRouteModuleKey(key) {
|
|
704
|
+
try {
|
|
705
|
+
return parseRouteModuleKey(key);
|
|
706
|
+
} catch {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function normalizeRoutePattern(segments) {
|
|
711
|
+
const parts = segments.map(routeSegmentToPatternPart).filter((part) => part.length > 0);
|
|
712
|
+
return trimTrailingSlashes(`/${parts.join("/")}`) || "/";
|
|
713
|
+
}
|
|
714
|
+
function routeSegmentToPatternPart(segment) {
|
|
715
|
+
if (/^\(.+\)$/.test(segment))
|
|
716
|
+
return "";
|
|
717
|
+
return segment.replace(/^\[([^\]]+)\]$/, ":$1");
|
|
718
|
+
}
|
|
719
|
+
function validateRouteSegment(segment, key) {
|
|
720
|
+
if (segment.includes("[...") || segment.includes("[[...")) {
|
|
721
|
+
throw new Error(`[route-convention] catch-all routes are not supported yet: ${key}`);
|
|
722
|
+
}
|
|
723
|
+
const dynamic = /^\[(.+)\]$/.exec(segment);
|
|
724
|
+
if (dynamic && (dynamic[1]?.includes("[") || dynamic[1]?.includes("]") || dynamic[1]?.startsWith("..."))) {
|
|
725
|
+
throw new Error(`[route-convention] invalid dynamic route segment "${segment}" in ${key}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// pkgs/@akanjs/language/routeFacts.ts
|
|
730
|
+
class RouteFactReader {
|
|
731
|
+
static #pagePattern = /^((?:apps|libs)\/[^/]+)\/page\/((?!(?:.*\/)?\(libs\)\/).+\.tsx?)$/;
|
|
732
|
+
static #guardPattern = /\.guard\s*\(/;
|
|
733
|
+
static #aliasPattern = /^export default ([A-Za-z_$][\w$]*)\s*;?\s*$/m;
|
|
734
|
+
static #workspacePattern = /^(?:\.|@(?:apps|libs)\/)/;
|
|
735
|
+
#ts;
|
|
736
|
+
#reader;
|
|
737
|
+
#parse;
|
|
738
|
+
constructor(ts, reader, parse) {
|
|
739
|
+
this.#ts = ts;
|
|
740
|
+
this.#reader = reader;
|
|
741
|
+
this.#parse = parse;
|
|
742
|
+
}
|
|
743
|
+
static fileOf(relative) {
|
|
744
|
+
const match = relative.match(RouteFactReader.#pagePattern);
|
|
745
|
+
return match ? { owner: match[1], key: `./${match[2]}` } : null;
|
|
746
|
+
}
|
|
747
|
+
read(fileName, info, text, words) {
|
|
748
|
+
const ts = this.#ts;
|
|
749
|
+
const parsed = tryParseRouteModuleKey(info.key);
|
|
750
|
+
if (!parsed || parsed.kind === "overrides" || parsed.isInternalRootLayout)
|
|
751
|
+
return null;
|
|
752
|
+
const starts = [...text.matchAll(/^export default\b/gm)].map(({ index }) => index);
|
|
753
|
+
const source = RouteFactReader.#guardPattern.test(text) || starts.length > 1 ? this.#parse(fileName, text) : null;
|
|
754
|
+
const exported = source ? this.#exportOf(source) : null;
|
|
755
|
+
const start = exported ? exported.getStart(source ?? undefined) : starts[0] ?? 0;
|
|
756
|
+
const head = text.slice(0, start);
|
|
757
|
+
return {
|
|
758
|
+
fileName,
|
|
759
|
+
owner: info.owner,
|
|
760
|
+
kind: parsed.kind,
|
|
761
|
+
pattern: normalizeRoutePattern(parsed.sourceRouteSegments),
|
|
762
|
+
dir: parsed.moduleSegments.slice(0, -1),
|
|
763
|
+
isRoot: words.includes(`
|
|
764
|
+
rootLayout
|
|
765
|
+
`),
|
|
766
|
+
line: head.split(`
|
|
767
|
+
`).length - 1,
|
|
768
|
+
character: start - head.lastIndexOf(`
|
|
769
|
+
`) - 1,
|
|
770
|
+
guard: source && exported ? this.#guardOf(source, exported) : null,
|
|
771
|
+
imports: source ? this.#reader.importsIn(source) : [],
|
|
772
|
+
alias: source ? exported && ts.isExportAssignment(exported) && ts.isIdentifier(exported.expression) ? exported.expression.text : null : text.match(RouteFactReader.#aliasPattern)?.[1] ?? null
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
#exportOf(source) {
|
|
776
|
+
const ts = this.#ts;
|
|
777
|
+
return source.statements.find((statement) => ts.isExportAssignment(statement) || ts.canHaveModifiers(statement) && !!ts.getModifiers(statement)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword)) ?? null;
|
|
778
|
+
}
|
|
779
|
+
declarationIn(source, name) {
|
|
780
|
+
const ts = this.#ts;
|
|
781
|
+
for (const statement of source.statements) {
|
|
782
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name)
|
|
783
|
+
return { statement, declared: statement.name, initializer: null };
|
|
784
|
+
if (!ts.isVariableStatement(statement))
|
|
785
|
+
continue;
|
|
786
|
+
const declaration = statement.declarationList.declarations.find((item) => ts.isIdentifier(item.name) && item.name.text === name);
|
|
787
|
+
if (declaration)
|
|
788
|
+
return { statement, declared: declaration.name, initializer: declaration.initializer ?? null };
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
#guardOf(source, exported) {
|
|
793
|
+
const ts = this.#ts;
|
|
794
|
+
let current = ts.isExportAssignment(exported) ? this.#reader.unwrap(exported.expression) : null;
|
|
795
|
+
if (current && ts.isIdentifier(current)) {
|
|
796
|
+
const initializer = this.declarationIn(source, current.text)?.initializer;
|
|
797
|
+
current = initializer ? this.#reader.unwrap(initializer) : null;
|
|
798
|
+
}
|
|
799
|
+
while (current && ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
800
|
+
const { name, expression } = current.expression;
|
|
801
|
+
const [stage] = current.arguments;
|
|
802
|
+
if (name.text === "guard" && stage)
|
|
803
|
+
return { calls: this.#callsIn(source, stage), span: this.#reader.spanOf(source, name) };
|
|
804
|
+
current = this.#reader.unwrap(expression);
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
#callsIn(source, stage) {
|
|
809
|
+
const ts = this.#ts;
|
|
810
|
+
const imported = new Set(this.#reader.importsIn(source).filter(({ from }) => RouteFactReader.#workspacePattern.test(from)).map(({ name }) => name));
|
|
811
|
+
const calls = new Map;
|
|
812
|
+
const callOf = (name) => {
|
|
813
|
+
if (calls.has(name))
|
|
814
|
+
return true;
|
|
815
|
+
const declared = this.declarationIn(source, name);
|
|
816
|
+
if (!declared && !imported.has(name))
|
|
817
|
+
return false;
|
|
818
|
+
calls.set(name, {
|
|
819
|
+
name,
|
|
820
|
+
local: declared && {
|
|
821
|
+
span: this.#reader.spanOf(source, declared.declared),
|
|
822
|
+
extent: this.#reader.extentOf(source, declared.statement),
|
|
823
|
+
doc: this.#reader.docOf(source, declared.statement)
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
return true;
|
|
827
|
+
};
|
|
828
|
+
const visit = (node) => {
|
|
829
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && callOf(node.expression.text))
|
|
830
|
+
return;
|
|
831
|
+
ts.forEachChild(node, visit);
|
|
832
|
+
};
|
|
833
|
+
if (ts.isIdentifier(stage))
|
|
834
|
+
callOf(stage.text);
|
|
835
|
+
else
|
|
836
|
+
visit(stage);
|
|
837
|
+
return [...calls.values()];
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// pkgs/@akanjs/language/akanGraph.ts
|
|
842
|
+
var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
843
|
+
var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
844
|
+
var memberSourcePattern = /\.(signal|service|store)\.ts$/;
|
|
845
|
+
var skippedPattern = /(?:\.(?:constant|document|dictionary|test|spec)\.tsx?|\/lib\/(?:cnst|db|dict|sig|srv|st|useClient|useServer)\.ts|\/index\.tsx?|^(?:apps|libs)\/[^/]+\/(?:client|server)\.ts)$/;
|
|
846
|
+
var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
|
|
847
|
+
var moduleDirPattern = /\/lib\/([^/]+)\//;
|
|
848
|
+
var typeKinds = new Set(["model", "scalar", "enum"]);
|
|
849
|
+
var clientRoots = new Set(["fetch", "st", "sig"]);
|
|
850
|
+
var builtinGuards = {
|
|
851
|
+
Public: { scope: "account", doc: "Passes every caller, signed in or not." },
|
|
852
|
+
None: { scope: "account", doc: "Refuses every caller." }
|
|
853
|
+
};
|
|
854
|
+
var frameworkOwner = "akanjs";
|
|
855
|
+
var frameworkPattern = /^akanjs(?:\/|$)/;
|
|
856
|
+
var aliasPattern = /^@(apps|libs)\/([^/]+)/;
|
|
857
|
+
var maxMapDepth = 4;
|
|
858
|
+
|
|
859
|
+
class GraphBuilder {
|
|
860
|
+
#source;
|
|
861
|
+
#nodes = new Map;
|
|
862
|
+
#edges = new Map;
|
|
863
|
+
#contexts = new Map;
|
|
864
|
+
#members = new Map;
|
|
865
|
+
#guards = new Map;
|
|
866
|
+
#mapEntries = new Map;
|
|
867
|
+
#routeFiles = new Map;
|
|
868
|
+
#declarations = new Map;
|
|
869
|
+
#routes;
|
|
870
|
+
#adaptors = new Map;
|
|
871
|
+
#plugs = new Map;
|
|
872
|
+
constructor(source) {
|
|
873
|
+
this.#source = source;
|
|
874
|
+
this.#routes = new RouteFactReader(source.ts, source.reader, source.parse);
|
|
875
|
+
}
|
|
876
|
+
build() {
|
|
877
|
+
const { facts } = this.#source;
|
|
878
|
+
const constantsByOwner = new Map;
|
|
879
|
+
for (const file of facts)
|
|
880
|
+
constantsByOwner.set(file.owner, [...constantsByOwner.get(file.owner) ?? [], ...file.constants]);
|
|
881
|
+
const scopeOf = (owner) => [
|
|
882
|
+
...constantsByOwner.get(owner) ?? [],
|
|
883
|
+
...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
|
|
884
|
+
];
|
|
885
|
+
const typed = [];
|
|
886
|
+
for (const file of facts)
|
|
887
|
+
for (const fact of file.adaptors) {
|
|
888
|
+
const declared = { fact, owner: file.owner, fileName: file.fileName };
|
|
889
|
+
this.#adaptors.set(fact.name, [...this.#adaptors.get(fact.name) ?? [], declared]);
|
|
890
|
+
this.#adaptorNodeOf(declared);
|
|
891
|
+
}
|
|
892
|
+
for (const file of facts)
|
|
893
|
+
for (const fact of file.adaptors)
|
|
894
|
+
for (const plug of fact.plugs)
|
|
895
|
+
this.#addEdge(this.#key("adaptor", file.owner, fact.name), this.#adaptorIdOf(plug.target, file), "plugs", plug.name, this.#location(plug.span));
|
|
896
|
+
this.#declareGuards(facts);
|
|
897
|
+
for (const file of facts) {
|
|
898
|
+
const { owner } = file;
|
|
899
|
+
const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
|
|
900
|
+
const scalarClass = scalar ? file.constants.find((constant) => constant.name === capitalize(scalar)) : undefined;
|
|
901
|
+
if (scalar && scalarClass) {
|
|
902
|
+
const id = this.#key("scalar", owner, scalar);
|
|
903
|
+
const node = this.#addNode(id, "scalar", owner, scalar, scalarClass.name, scalarClass.span);
|
|
904
|
+
node.fields = this.#fieldsOf(scalarClass, scopeOf(owner));
|
|
905
|
+
typed.push(node);
|
|
906
|
+
}
|
|
907
|
+
for (const item of file.enums) {
|
|
908
|
+
const id = this.#key("enum", owner, item.name);
|
|
909
|
+
this.#addNode(id, "enum", owner, this.#moduleDirOf(file.fileName), item.name, item.span).values = item.values;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
for (const file of facts) {
|
|
913
|
+
const { owner } = file;
|
|
914
|
+
for (const service of file.services) {
|
|
915
|
+
const refName = service.model;
|
|
916
|
+
if (!refName)
|
|
917
|
+
continue;
|
|
918
|
+
const modelId = this.#key("model", owner, refName);
|
|
919
|
+
const modelClass = constantsByOwner.get(owner)?.find((constant) => constant.name === capitalize(refName));
|
|
920
|
+
if (modelClass && !this.#nodes.has(modelId)) {
|
|
921
|
+
const node = this.#addNode(modelId, "model", owner, refName, modelClass.name, modelClass.span);
|
|
922
|
+
node.fields = this.#fieldsOf(modelClass, scopeOf(owner));
|
|
923
|
+
typed.push(node);
|
|
924
|
+
}
|
|
925
|
+
for (const method of service.methods)
|
|
926
|
+
this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
|
|
927
|
+
for (const plug of service.plugs)
|
|
928
|
+
this.#plugs.set(`${owner ?? ""}
|
|
929
|
+
${refName}
|
|
930
|
+
${plug.name}`, this.#adaptorIdOf(plug.target, file));
|
|
931
|
+
}
|
|
932
|
+
for (const filter of file.filters) {
|
|
933
|
+
if (!filter.name.endsWith("Filter"))
|
|
934
|
+
continue;
|
|
935
|
+
const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
|
|
936
|
+
this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
|
|
937
|
+
for (const query of filter.queries)
|
|
938
|
+
this.#digest(this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span), this.#source.readText(file.fileName), query.extent);
|
|
939
|
+
}
|
|
940
|
+
for (const slice of file.slices) {
|
|
941
|
+
const refName = slice.model;
|
|
942
|
+
if (!refName)
|
|
943
|
+
continue;
|
|
944
|
+
const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
|
|
945
|
+
for (const guard of slice.guards)
|
|
946
|
+
this.#guard(fallback, guard, file);
|
|
947
|
+
for (const key of slice.keys) {
|
|
948
|
+
const detail = [...key.guards, ...key.live ? ["live"] : []];
|
|
949
|
+
const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
950
|
+
for (const guard of key.guards)
|
|
951
|
+
this.#guard(node, guard, file);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
for (const endpoint of file.endpoints) {
|
|
955
|
+
const refName = endpoint.model;
|
|
956
|
+
if (!refName)
|
|
957
|
+
continue;
|
|
958
|
+
for (const key of endpoint.keys) {
|
|
959
|
+
const detail = [...key.verb ? [key.verb] : [], ...key.guards];
|
|
960
|
+
const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
961
|
+
for (const guard of key.guards)
|
|
962
|
+
this.#guard(node, guard, file);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
for (const internal of file.internals)
|
|
966
|
+
for (const key of internal.keys)
|
|
967
|
+
if (internal.model && key.type !== "resolveField")
|
|
968
|
+
this.#addMember("internal", owner, internal.model, key, `${internal.name}.${key.name}`, file.fileName, [key.type, key.schedule, ...key.modes].filter((item) => item !== null));
|
|
969
|
+
for (const store of file.stores)
|
|
970
|
+
if (store.model)
|
|
971
|
+
for (const action of store.actions)
|
|
972
|
+
this.#addMember("action", owner, store.model, action, `${store.name}.${action.name}`, file.fileName);
|
|
973
|
+
}
|
|
974
|
+
const types = new Map;
|
|
975
|
+
for (const node of this.#nodes.values())
|
|
976
|
+
if (typeKinds.has(node.kind))
|
|
977
|
+
types.set(node.label, [...types.get(node.label) ?? [], node]);
|
|
978
|
+
for (const node of typed)
|
|
979
|
+
for (const field of node.fields ?? []) {
|
|
980
|
+
const candidates = (field.type ? types.get(field.type) ?? [] : []).filter((type) => this.#visible(type.owner, node.owner));
|
|
981
|
+
field.ref = (candidates.find((type) => type.owner === node.owner) ?? candidates[0])?.id ?? null;
|
|
982
|
+
if (field.ref)
|
|
983
|
+
this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
|
|
984
|
+
}
|
|
985
|
+
this.#addRoutes();
|
|
986
|
+
for (const [fileName, words] of this.#source.files)
|
|
987
|
+
this.#walk(fileName, words);
|
|
988
|
+
return {
|
|
989
|
+
version: 1,
|
|
990
|
+
nodes: [...this.#nodes.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
991
|
+
edges: [...this.#edges.values()]
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
#key(kind, owner, scope, name = "") {
|
|
995
|
+
return `${kind}:${owner ?? ""}:${scope}${name ? `.${name}` : ""}`;
|
|
996
|
+
}
|
|
997
|
+
#location(span) {
|
|
998
|
+
return { file: this.#source.relative(span.fileName), line: span.line + 1, column: span.character + 1 };
|
|
999
|
+
}
|
|
1000
|
+
#addNode(id, kind, owner, module2, name, span, detail = [], label = (kind === "query" || kind === "slice") && module2 ? `${module2}${name ? `.${name}` : " (default)"}` : name) {
|
|
1001
|
+
const existing = this.#nodes.get(id);
|
|
1002
|
+
if (existing)
|
|
1003
|
+
return existing;
|
|
1004
|
+
const node = {
|
|
1005
|
+
id,
|
|
1006
|
+
kind,
|
|
1007
|
+
name,
|
|
1008
|
+
label,
|
|
1009
|
+
owner,
|
|
1010
|
+
module: module2,
|
|
1011
|
+
detail,
|
|
1012
|
+
at: span ? this.#location(span) : null
|
|
1013
|
+
};
|
|
1014
|
+
this.#nodes.set(id, node);
|
|
1015
|
+
return node;
|
|
1016
|
+
}
|
|
1017
|
+
#addMember(kind, owner, refName, member, label, fileName, detail = []) {
|
|
1018
|
+
const id = this.#key(kind, owner, refName, member.name);
|
|
1019
|
+
const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
|
|
1020
|
+
this.#digest(node, this.#source.readText(fileName), member.extent);
|
|
1021
|
+
this.#contexts.set(fileName, [
|
|
1022
|
+
...this.#contexts.get(fileName) ?? [],
|
|
1023
|
+
{ id, kind, owner, module: refName, extent: member.extent }
|
|
1024
|
+
]);
|
|
1025
|
+
const byName = `${kind}:${member.name}`;
|
|
1026
|
+
this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
|
|
1027
|
+
return node;
|
|
1028
|
+
}
|
|
1029
|
+
#hashOf(...parts) {
|
|
1030
|
+
if (!this.#source.digests)
|
|
1031
|
+
return null;
|
|
1032
|
+
let hash = 2166136261;
|
|
1033
|
+
for (const [text, { start, end }] of parts) {
|
|
1034
|
+
if (text === undefined)
|
|
1035
|
+
return null;
|
|
1036
|
+
for (let idx = start;idx < end; idx++) {
|
|
1037
|
+
const code = text.charCodeAt(idx);
|
|
1038
|
+
if (code !== 13)
|
|
1039
|
+
hash = Math.imul(hash ^ code, 16777619);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
1043
|
+
}
|
|
1044
|
+
#digest(node, text, extent) {
|
|
1045
|
+
const digest = node.digest ? null : this.#hashOf([text, extent]);
|
|
1046
|
+
if (digest)
|
|
1047
|
+
node.digest = digest;
|
|
1048
|
+
}
|
|
1049
|
+
#addEdge(from, to, kind, label, at) {
|
|
1050
|
+
if (from === to)
|
|
1051
|
+
return;
|
|
1052
|
+
const key = `${from}
|
|
1053
|
+
${to}
|
|
1054
|
+
${kind}
|
|
1055
|
+
${label ?? ""}`;
|
|
1056
|
+
const existing = this.#edges.get(key);
|
|
1057
|
+
if (existing)
|
|
1058
|
+
existing.count += 1;
|
|
1059
|
+
else
|
|
1060
|
+
this.#edges.set(key, { from, to, kind, label, count: 1, at });
|
|
1061
|
+
}
|
|
1062
|
+
#guard(node, guard, file) {
|
|
1063
|
+
const [role, local] = guard.includes(" ") ? guard.split(" ") : [null, guard];
|
|
1064
|
+
if (!local || local === "?")
|
|
1065
|
+
return;
|
|
1066
|
+
this.#addEdge(node.id, this.#guardIdOf(local, file), "guard", role, node.at);
|
|
1067
|
+
}
|
|
1068
|
+
#declareGuards(facts) {
|
|
1069
|
+
const factories = new Map;
|
|
1070
|
+
for (const file of facts)
|
|
1071
|
+
for (const fact of file.guardFactories) {
|
|
1072
|
+
const factory = { fact, owner: file.owner, fileName: file.fileName, made: [] };
|
|
1073
|
+
factories.set(fact.name, [...factories.get(fact.name) ?? [], factory]);
|
|
1074
|
+
}
|
|
1075
|
+
const sameFile = (factory, name) => name ? factories.get(name)?.find((other) => other.fileName === factory.fileName) : undefined;
|
|
1076
|
+
const declare = (guard, factory) => {
|
|
1077
|
+
this.#guards.set(guard.name, [...this.#guards.get(guard.name) ?? [], guard]);
|
|
1078
|
+
factory?.made.push(guard);
|
|
1079
|
+
};
|
|
1080
|
+
const madeBy = (factory, fileName, extent) => {
|
|
1081
|
+
const parts = [{ fileName, extent }];
|
|
1082
|
+
const seen = new Set;
|
|
1083
|
+
for (let current = factory;current && !seen.has(current); current = sameFile(current, current.fact.via)) {
|
|
1084
|
+
seen.add(current);
|
|
1085
|
+
parts.push({ fileName: current.fileName, extent: current.fact.extent });
|
|
1086
|
+
}
|
|
1087
|
+
return parts;
|
|
1088
|
+
};
|
|
1089
|
+
for (const file of facts) {
|
|
1090
|
+
const { owner, fileName } = file;
|
|
1091
|
+
for (const { name, span, extent, scope, doc, factory: callee } of file.guards) {
|
|
1092
|
+
const factory = callee ? factories.get(callee)?.find((other) => other.fileName === fileName) : undefined;
|
|
1093
|
+
declare({ name, owner, span, scope, doc, parts: madeBy(factory, fileName, extent) }, factory);
|
|
1094
|
+
}
|
|
1095
|
+
for (const { name, span, extent, doc, callee } of file.guardCalls) {
|
|
1096
|
+
const imported = file.imports.find((item) => item.name === callee);
|
|
1097
|
+
if (!imported)
|
|
1098
|
+
continue;
|
|
1099
|
+
const from = this.#ownerOfSpecifier(fileName, imported.from);
|
|
1100
|
+
const candidates = factories.get(imported.imported === "default" || imported.imported === "*" ? callee : imported.imported) ?? [];
|
|
1101
|
+
const factory = candidates.find((other) => other.owner === from) ?? this.#soleVisible(candidates, from ?? owner);
|
|
1102
|
+
if (factory)
|
|
1103
|
+
declare({ name, owner, span, scope: factory.fact.scope, doc, parts: madeBy(factory, fileName, extent) }, factory);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
for (const list of factories.values())
|
|
1107
|
+
for (const { fact, made } of list)
|
|
1108
|
+
if (made.every((guard) => !guard.doc))
|
|
1109
|
+
for (const guard of made)
|
|
1110
|
+
guard.doc = fact.doc;
|
|
1111
|
+
for (const list of this.#guards.values())
|
|
1112
|
+
for (const guard of list)
|
|
1113
|
+
this.#guardNodeOf(guard);
|
|
1114
|
+
}
|
|
1115
|
+
#guardIdOf(local, site) {
|
|
1116
|
+
const found = this.#findGuard(local, site, 0);
|
|
1117
|
+
if ("guard" in found)
|
|
1118
|
+
return this.#guardNodeOf(found.guard);
|
|
1119
|
+
const { name, from } = found;
|
|
1120
|
+
const isFramework = from === frameworkOwner || from === undefined && !!builtinGuards[name];
|
|
1121
|
+
const builtin = isFramework ? builtinGuards[name] : undefined;
|
|
1122
|
+
const owner = isFramework ? null : from ?? null;
|
|
1123
|
+
const detail = [...builtin ? [builtin.scope] : [], ...isFramework ? [frameworkOwner] : []];
|
|
1124
|
+
const node = this.#addNode(this.#key("guard", owner, name), "guard", owner, null, name, null, detail);
|
|
1125
|
+
if (builtin)
|
|
1126
|
+
node.doc = builtin.doc;
|
|
1127
|
+
return node.id;
|
|
1128
|
+
}
|
|
1129
|
+
#findGuard(local, site, depth) {
|
|
1130
|
+
const [head = local, member] = local.split(".");
|
|
1131
|
+
const imported = site.imports.find((item) => item.name === head);
|
|
1132
|
+
const from = imported ? this.#ownerOfSpecifier(site.fileName, imported.from) : undefined;
|
|
1133
|
+
const holder = imported ? from : site.owner;
|
|
1134
|
+
if (member && imported?.imported !== "*" && holder !== undefined && holder !== frameworkOwner && depth < maxMapDepth) {
|
|
1135
|
+
const objectName = imported && imported.imported !== "default" ? imported.imported : head;
|
|
1136
|
+
for (const entry of this.#mapEntriesOf(objectName, member, imported ? null : site.fileName, holder)) {
|
|
1137
|
+
const found = this.#findGuard(entry.name, entry.site, depth + 1);
|
|
1138
|
+
if ("guard" in found)
|
|
1139
|
+
return found;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const name = member ?? (imported && imported.imported !== "default" && imported.imported !== "*" ? imported.imported : head);
|
|
1143
|
+
const declared = this.#guards.get(name) ?? [];
|
|
1144
|
+
const guard = from === undefined ? declared.find((other) => other.owner === site.owner) ?? declared.find((other) => this.#visible(other.owner, site.owner)) : declared.find((other) => other.owner === from) ?? (from === frameworkOwner ? undefined : this.#soleVisible(declared, from));
|
|
1145
|
+
return guard ? { guard } : { name, from };
|
|
1146
|
+
}
|
|
1147
|
+
#mapEntriesOf(objectName, member, fileName, owner) {
|
|
1148
|
+
const key = `${objectName}
|
|
1149
|
+
${member}
|
|
1150
|
+
${fileName ?? ""}
|
|
1151
|
+
${owner ?? ""}`;
|
|
1152
|
+
const cached = this.#mapEntries.get(key);
|
|
1153
|
+
if (cached)
|
|
1154
|
+
return cached;
|
|
1155
|
+
const { ts, reader } = this.#source;
|
|
1156
|
+
const entries = [];
|
|
1157
|
+
const fileNames = [
|
|
1158
|
+
...fileName ? [fileName] : [],
|
|
1159
|
+
...this.#source.files.filter(([other, words]) => other !== fileName && owner !== null && !testFilePattern.test(other) && words.includes(`
|
|
1160
|
+
${objectName}
|
|
1161
|
+
`) && words.includes(`
|
|
1162
|
+
${member}
|
|
1163
|
+
`) && this.#source.ownerOf(other) === owner).map(([other]) => other)
|
|
1164
|
+
];
|
|
1165
|
+
for (const holder of fileNames) {
|
|
1166
|
+
const text = this.#source.readText(holder);
|
|
1167
|
+
if (text === undefined)
|
|
1168
|
+
continue;
|
|
1169
|
+
const source = this.#source.parse(holder, text);
|
|
1170
|
+
const site = { fileName: holder, owner: this.#source.ownerOf(holder), imports: reader.importsIn(source) };
|
|
1171
|
+
for (const statement of source.statements) {
|
|
1172
|
+
if (!ts.isVariableStatement(statement))
|
|
1173
|
+
continue;
|
|
1174
|
+
const map = statement.declarationList.declarations.find((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === objectName)?.initializer;
|
|
1175
|
+
const object = map && reader.unwrap(map);
|
|
1176
|
+
if (!object || !ts.isObjectLiteralExpression(object))
|
|
1177
|
+
continue;
|
|
1178
|
+
for (const property of object.properties) {
|
|
1179
|
+
if (ts.isShorthandPropertyAssignment(property) && property.name.text === member)
|
|
1180
|
+
entries.push({ name: member, site });
|
|
1181
|
+
if (!ts.isPropertyAssignment(property) || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || property.name.text !== member)
|
|
1182
|
+
continue;
|
|
1183
|
+
const value = reader.unwrap(property.initializer);
|
|
1184
|
+
if (ts.isIdentifier(value))
|
|
1185
|
+
entries.push({ name: value.text, site });
|
|
1186
|
+
else if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression))
|
|
1187
|
+
entries.push({ name: `${value.expression.text}.${value.name.text}`, site });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
this.#mapEntries.set(key, entries);
|
|
1192
|
+
return entries;
|
|
1193
|
+
}
|
|
1194
|
+
#declarationOf(owner, name, near) {
|
|
1195
|
+
const key = `${owner}
|
|
1196
|
+
${name}
|
|
1197
|
+
${near ?? ""}`;
|
|
1198
|
+
const cached = this.#declarations.get(key);
|
|
1199
|
+
if (cached !== undefined)
|
|
1200
|
+
return cached;
|
|
1201
|
+
const isNear = (fileName) => {
|
|
1202
|
+
const relative = this.#source.relative(fileName);
|
|
1203
|
+
return !!near && (relative.startsWith(`${near}/`) || relative.replace(/\.tsx?$/, "") === near);
|
|
1204
|
+
};
|
|
1205
|
+
const candidates = this.#source.files.filter(([fileName, words]) => !testFilePattern.test(fileName) && words.includes(`
|
|
1206
|
+
${name}
|
|
1207
|
+
`) && this.#source.ownerOf(fileName) === owner).map(([fileName]) => fileName);
|
|
1208
|
+
let found = null;
|
|
1209
|
+
for (const fileName of [...candidates.filter(isNear), ...candidates.filter((other) => !isNear(other))]) {
|
|
1210
|
+
const text = this.#source.readText(fileName);
|
|
1211
|
+
if (text === undefined)
|
|
1212
|
+
continue;
|
|
1213
|
+
const source = this.#source.parse(fileName, text);
|
|
1214
|
+
const declared = this.#routes.declarationIn(source, name);
|
|
1215
|
+
if (!declared)
|
|
1216
|
+
continue;
|
|
1217
|
+
found = { source, statement: declared.statement, declared: declared.declared };
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
this.#declarations.set(key, found);
|
|
1221
|
+
return found;
|
|
1222
|
+
}
|
|
1223
|
+
#addRoutes() {
|
|
1224
|
+
const routes = [];
|
|
1225
|
+
const layouts = new Map;
|
|
1226
|
+
for (const [fileName, words] of this.#source.files) {
|
|
1227
|
+
const relative = this.#source.relative(fileName);
|
|
1228
|
+
const info = RouteFactReader.fileOf(relative);
|
|
1229
|
+
const text = info ? this.#source.readText(fileName) : undefined;
|
|
1230
|
+
const fact = info && text !== undefined ? this.#routes.read(fileName, info, text, words) : null;
|
|
1231
|
+
if (!fact || text === undefined)
|
|
1232
|
+
continue;
|
|
1233
|
+
const span = {
|
|
1234
|
+
fileName,
|
|
1235
|
+
start: 0,
|
|
1236
|
+
end: 0,
|
|
1237
|
+
line: fact.line,
|
|
1238
|
+
character: fact.character,
|
|
1239
|
+
endLine: 0,
|
|
1240
|
+
endCharacter: 0
|
|
1241
|
+
};
|
|
1242
|
+
const dir = `/${fact.dir.join("/")}`;
|
|
1243
|
+
const guard = fact.guard ? ["guard"] : [];
|
|
1244
|
+
const node = fact.kind === "layout" ? this.#addNode(this.#key("layout", fact.owner, dir), "layout", fact.owner, null, dir, span, [...fact.isRoot ? ["root"] : [], ...guard], `${dir === "/" ? "" : dir}/_layout`) : this.#addNode(this.#key("route", fact.owner, fact.pattern), "route", fact.owner, null, fact.pattern, span, guard);
|
|
1245
|
+
this.#digest(node, text, { start: 0, end: text.length });
|
|
1246
|
+
this.#routeFiles.set(relative, { id: node.id, alias: fact.alias });
|
|
1247
|
+
if (fact.kind === "layout")
|
|
1248
|
+
layouts.set(`${fact.owner}
|
|
1249
|
+
${dir}`, node);
|
|
1250
|
+
routes.push({ fact, node });
|
|
1251
|
+
for (const call of fact.guard?.calls ?? []) {
|
|
1252
|
+
const id = this.#routeGuardIdOf(call, fact, text);
|
|
1253
|
+
if (id && fact.guard)
|
|
1254
|
+
this.#addEdge(node.id, id, "guard", null, this.#location(fact.guard.span));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const parentOf = ({ owner, dir, kind }) => {
|
|
1258
|
+
for (let depth = kind === "layout" ? dir.length - 1 : dir.length;depth >= 0; depth--) {
|
|
1259
|
+
const layout = layouts.get(`${owner}
|
|
1260
|
+
/${dir.slice(0, depth).join("/")}`);
|
|
1261
|
+
if (layout)
|
|
1262
|
+
return layout;
|
|
1263
|
+
}
|
|
1264
|
+
return null;
|
|
1265
|
+
};
|
|
1266
|
+
for (const { fact, node } of routes) {
|
|
1267
|
+
const parent = parentOf(fact);
|
|
1268
|
+
if (parent)
|
|
1269
|
+
this.#addEdge(parent.id, node.id, "wraps", null, node.at);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
#routeGuardIdOf({ name: local, local: declared }, fact, text) {
|
|
1273
|
+
if (declared) {
|
|
1274
|
+
const file = this.#source.relative(fact.fileName).slice(fact.owner.length + 1);
|
|
1275
|
+
const id = this.#key("guard", fact.owner, `${file}#${local}`);
|
|
1276
|
+
return this.#routeGuardNodeOf(id, fact.owner, local, declared.span, declared.doc, text, declared.extent);
|
|
1277
|
+
}
|
|
1278
|
+
const imported = fact.imports.find((item) => item.name === local);
|
|
1279
|
+
if (!imported)
|
|
1280
|
+
return null;
|
|
1281
|
+
const name = imported.imported !== "default" && imported.imported !== "*" ? imported.imported : local;
|
|
1282
|
+
const from = this.#ownerOfSpecifier(fact.fileName, imported.from);
|
|
1283
|
+
if (!from || from === frameworkOwner)
|
|
1284
|
+
return null;
|
|
1285
|
+
const found = this.#declarationOf(from, name, this.#pathOfSpecifier(fact.fileName, imported.from));
|
|
1286
|
+
if (!found)
|
|
1287
|
+
return null;
|
|
1288
|
+
const { reader } = this.#source;
|
|
1289
|
+
return this.#routeGuardNodeOf(this.#key("guard", from, name), from, name, reader.spanOf(found.source, found.declared), reader.docOf(found.source, found.statement), found.source.text, reader.extentOf(found.source, found.statement));
|
|
1290
|
+
}
|
|
1291
|
+
#routeGuardNodeOf(id, owner, name, span, doc, text, extent) {
|
|
1292
|
+
const isNew = !this.#nodes.has(id);
|
|
1293
|
+
const node = this.#addNode(id, "guard", owner, null, name, span, ["route"]);
|
|
1294
|
+
if (isNew && doc)
|
|
1295
|
+
node.doc = doc;
|
|
1296
|
+
this.#digest(node, text, extent);
|
|
1297
|
+
return node.id;
|
|
1298
|
+
}
|
|
1299
|
+
#adaptorIdOf(local, file) {
|
|
1300
|
+
const [head, member] = local.split(".");
|
|
1301
|
+
const imported = file.imports.find((item) => item.name === head);
|
|
1302
|
+
const from = imported ? this.#ownerOfSpecifier(file.fileName, imported.from) : undefined;
|
|
1303
|
+
const name = member ?? (imported && imported.imported !== "default" && imported.imported !== "*" ? imported.imported : head);
|
|
1304
|
+
const declared = this.#adaptors.get(name) ?? [];
|
|
1305
|
+
const found = from === undefined ? declared.find((adaptor) => adaptor.owner === file.owner) ?? declared.find((adaptor) => this.#visible(adaptor.owner, file.owner)) : declared.find((adaptor) => adaptor.owner === from) ?? (from === frameworkOwner ? undefined : declared.find((adaptor) => this.#visible(adaptor.owner, from)));
|
|
1306
|
+
if (found)
|
|
1307
|
+
return this.#adaptorNodeOf(found);
|
|
1308
|
+
const owner = from === frameworkOwner ? null : imported ? from ?? null : file.owner;
|
|
1309
|
+
const detail = from === frameworkOwner ? [frameworkOwner] : [];
|
|
1310
|
+
return this.#addNode(this.#key("adaptor", owner, name), "adaptor", owner, null, name, null, detail).id;
|
|
1311
|
+
}
|
|
1312
|
+
#adaptorNodeOf({ fact, owner, fileName }) {
|
|
1313
|
+
const id = this.#key("adaptor", owner, fact.name);
|
|
1314
|
+
const detail = fact.refName ? [fact.refName] : [];
|
|
1315
|
+
const node = this.#addNode(id, "adaptor", owner, null, fact.name, fact.span, detail);
|
|
1316
|
+
if (fact.doc && !node.doc)
|
|
1317
|
+
node.doc = fact.doc;
|
|
1318
|
+
this.#digest(node, this.#source.readText(fileName), fact.extent);
|
|
1319
|
+
return node.id;
|
|
1320
|
+
}
|
|
1321
|
+
#guardNodeOf({ name, owner, span, scope, doc, parts }) {
|
|
1322
|
+
const node = this.#addNode(this.#key("guard", owner, name), "guard", owner, null, name, span, scope ? [scope] : []);
|
|
1323
|
+
if (doc && !node.doc)
|
|
1324
|
+
node.doc = doc;
|
|
1325
|
+
const digest = node.digest ? null : this.#hashOf(...parts.map(({ fileName, extent }) => [
|
|
1326
|
+
this.#source.readText(fileName),
|
|
1327
|
+
extent
|
|
1328
|
+
]));
|
|
1329
|
+
if (digest)
|
|
1330
|
+
node.digest = digest;
|
|
1331
|
+
return node.id;
|
|
1332
|
+
}
|
|
1333
|
+
#ownerOfSpecifier(fileName, specifier) {
|
|
1334
|
+
if (frameworkPattern.test(specifier))
|
|
1335
|
+
return frameworkOwner;
|
|
1336
|
+
const path = this.#pathOfSpecifier(fileName, specifier);
|
|
1337
|
+
return path === null ? undefined : ownerOf(path) ?? undefined;
|
|
1338
|
+
}
|
|
1339
|
+
#pathOfSpecifier(fileName, specifier) {
|
|
1340
|
+
const alias = specifier.match(aliasPattern);
|
|
1341
|
+
if (alias)
|
|
1342
|
+
return `${alias[1]}/${alias[2]}${specifier.slice(alias[0].length)}`;
|
|
1343
|
+
if (!specifier.startsWith("."))
|
|
1344
|
+
return null;
|
|
1345
|
+
const segments = this.#source.relative(fileName).split("/").slice(0, -1);
|
|
1346
|
+
for (const segment of specifier.split("/"))
|
|
1347
|
+
if (segment === "..")
|
|
1348
|
+
segments.pop();
|
|
1349
|
+
else if (segment !== ".")
|
|
1350
|
+
segments.push(segment);
|
|
1351
|
+
return segments.join("/");
|
|
1352
|
+
}
|
|
1353
|
+
#moduleDirOf(fileName) {
|
|
1354
|
+
const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
|
|
1355
|
+
return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
|
|
1356
|
+
}
|
|
1357
|
+
#fieldsOf(constant, scope) {
|
|
1358
|
+
const fields = new Map;
|
|
1359
|
+
const seen = new Set;
|
|
1360
|
+
const visit = (current) => {
|
|
1361
|
+
if (seen.has(current))
|
|
1362
|
+
return;
|
|
1363
|
+
seen.add(current);
|
|
1364
|
+
for (const parent of current.parents) {
|
|
1365
|
+
const parentClass = scope.find((other) => other.name === parent && other.span.fileName === current.span.fileName) ?? scope.find((other) => other.name === parent);
|
|
1366
|
+
if (parentClass)
|
|
1367
|
+
visit(parentClass);
|
|
1368
|
+
}
|
|
1369
|
+
for (const field of current.fields) {
|
|
1370
|
+
if (fields.has(field.name))
|
|
1371
|
+
continue;
|
|
1372
|
+
const digest = this.#hashOf([this.#source.readText(field.span.fileName), field.extent]);
|
|
1373
|
+
fields.set(field.name, {
|
|
1374
|
+
name: field.name,
|
|
1375
|
+
type: field.type,
|
|
1376
|
+
array: field.array,
|
|
1377
|
+
optional: field.optional,
|
|
1378
|
+
ref: null,
|
|
1379
|
+
declaredIn: current.name,
|
|
1380
|
+
at: this.#location(field.span),
|
|
1381
|
+
...digest ? { digest } : {}
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
visit(constant);
|
|
1386
|
+
return [...fields.values()];
|
|
1387
|
+
}
|
|
1388
|
+
#visible(target, from) {
|
|
1389
|
+
return !from || !target || target === from || target.startsWith("libs/");
|
|
1390
|
+
}
|
|
1391
|
+
#soleVisible(declared, from) {
|
|
1392
|
+
const visible = declared.filter((other) => this.#visible(other.owner, from));
|
|
1393
|
+
return visible.length === 1 ? visible[0] : undefined;
|
|
1394
|
+
}
|
|
1395
|
+
#targetNodeOf({ family, owner, refName, sourceKey }) {
|
|
1396
|
+
const isDefault = /^[A-Z]/.test(sourceKey);
|
|
1397
|
+
const id = family === "slice" || family === "query" ? this.#key(family, owner, refName, isDefault ? "" : sourceKey) : family === "model" || family === "formSetter" ? this.#key("model", owner, refName) : family === "endpoint" ? this.#key("endpoint", owner, refName, sourceKey) : null;
|
|
1398
|
+
return id && this.#nodes.has(id) ? id : null;
|
|
1399
|
+
}
|
|
1400
|
+
#membersNamed(kind, name, owner, refName) {
|
|
1401
|
+
const nodes = (this.#members.get(`${kind}:${name}`) ?? []).filter((node) => (!refName || node.module === refName) && this.#visible(node.owner, owner));
|
|
1402
|
+
const own = nodes.filter((node) => node.owner === owner);
|
|
1403
|
+
return (own.length ? own : nodes).map((node) => node.id);
|
|
1404
|
+
}
|
|
1405
|
+
#plainTargetsOf(chain, name, context, owner) {
|
|
1406
|
+
const [head, second] = chain ?? [];
|
|
1407
|
+
if (chain?.length === 1 && head === "fetch")
|
|
1408
|
+
return this.#membersNamed("endpoint", name, owner);
|
|
1409
|
+
if (chain?.length === 2 && head === "st" && second === "do")
|
|
1410
|
+
return this.#membersNamed("action", name, owner);
|
|
1411
|
+
if (chain?.length === 1 && head === "this" && (context.kind === "method" || context.kind === "action"))
|
|
1412
|
+
return this.#membersNamed(context.kind, name, context.owner, context.module).filter((id) => id.startsWith(`${context.kind}:${context.owner ?? ""}:`));
|
|
1413
|
+
if (chain?.length === 2 && head === "this" && second && /.Service$/.test(second))
|
|
1414
|
+
return this.#membersNamed("method", name, owner, second.slice(0, -"Service".length));
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
#walk(fileName, words) {
|
|
1418
|
+
const relative = this.#source.relative(fileName);
|
|
1419
|
+
const contexts = this.#contexts.get(fileName);
|
|
1420
|
+
const isComponentFile = !memberSourcePattern.test(fileName) && !skippedPattern.test(relative) && (words.includes(`
|
|
1421
|
+
fetch
|
|
1422
|
+
`) || words.includes(`
|
|
1423
|
+
st
|
|
1424
|
+
`));
|
|
1425
|
+
if (!contexts && !isComponentFile)
|
|
1426
|
+
return;
|
|
1427
|
+
const text = this.#source.readText(fileName);
|
|
1428
|
+
if (text === undefined)
|
|
1429
|
+
return;
|
|
1430
|
+
const { ts } = this.#source;
|
|
1431
|
+
const owner = this.#source.ownerOf(fileName);
|
|
1432
|
+
const source = this.#source.parse(fileName, text);
|
|
1433
|
+
const visit = (node) => {
|
|
1434
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
|
1435
|
+
const start = node.name.getStart(source);
|
|
1436
|
+
const chain = this.#chainOf(node.expression);
|
|
1437
|
+
const context = contexts ? contexts.find(({ extent }) => extent.start <= start && start < extent.end) : chain && clientRoots.has(chain[0]) ? this.#componentOf(source, node) : undefined;
|
|
1438
|
+
if (context)
|
|
1439
|
+
this.#link(fileName, source, node.name, chain, context, owner);
|
|
1440
|
+
}
|
|
1441
|
+
ts.forEachChild(node, visit);
|
|
1442
|
+
};
|
|
1443
|
+
visit(source);
|
|
1444
|
+
}
|
|
1445
|
+
#link(fileName, source, identifier, chain, context, owner) {
|
|
1446
|
+
const plugged = chain?.length === 2 && chain[0] === "this" && context.kind === "method" && this.#isServiceThis(identifier) ? this.#plugs.get(`${context.owner ?? ""}
|
|
1447
|
+
${context.module}
|
|
1448
|
+
${chain[1]}`) : undefined;
|
|
1449
|
+
const resolved = plugged ? [plugged] : this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
|
|
1450
|
+
const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
|
|
1451
|
+
if (!targets.length)
|
|
1452
|
+
return;
|
|
1453
|
+
if (context.pending && !this.#nodes.has(context.id)) {
|
|
1454
|
+
this.#nodes.set(context.id, context.pending);
|
|
1455
|
+
this.#digest(context.pending, source.text, context.extent);
|
|
1456
|
+
}
|
|
1457
|
+
const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
|
|
1458
|
+
const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
|
|
1459
|
+
const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
|
|
1460
|
+
const at = { file: this.#source.relative(fileName), line: position.line + 1, column: position.character + 1 };
|
|
1461
|
+
for (const target of new Set(own.length ? own : targets))
|
|
1462
|
+
this.#addEdge(context.id, target, kind, identifier.text, at);
|
|
1463
|
+
}
|
|
1464
|
+
#isServiceThis(identifier) {
|
|
1465
|
+
const { ts } = this.#source;
|
|
1466
|
+
for (let current = identifier.parent;!ts.isSourceFile(current); current = current.parent) {
|
|
1467
|
+
if (ts.isMethodDeclaration(current) || ts.isAccessor(current) || ts.isConstructorDeclaration(current))
|
|
1468
|
+
return ts.isClassDeclaration(current.parent) && ts.isSourceFile(current.parent.parent);
|
|
1469
|
+
if (ts.isFunctionExpression(current) || ts.isFunctionDeclaration(current) || ts.isClassLike(current))
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
#chainOf(expression) {
|
|
1475
|
+
const { ts } = this.#source;
|
|
1476
|
+
if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
1477
|
+
return this.#chainOf(expression.expression);
|
|
1478
|
+
if (ts.isIdentifier(expression))
|
|
1479
|
+
return [expression.text];
|
|
1480
|
+
if (expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
1481
|
+
return ["this"];
|
|
1482
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
1483
|
+
return null;
|
|
1484
|
+
const head = this.#chainOf(expression.expression);
|
|
1485
|
+
return head ? [...head, expression.name.text] : null;
|
|
1486
|
+
}
|
|
1487
|
+
#componentOf(source, node) {
|
|
1488
|
+
const { ts } = this.#source;
|
|
1489
|
+
let top = node;
|
|
1490
|
+
while (top.parent && !ts.isSourceFile(top.parent))
|
|
1491
|
+
top = top.parent;
|
|
1492
|
+
const declared = ts.isVariableStatement(top) ? top.declarationList.declarations[0]?.name : ts.isFunctionDeclaration(top) || ts.isClassDeclaration(top) ? top.name : undefined;
|
|
1493
|
+
const isDefaultExport = ts.canHaveModifiers(top) && !!ts.getModifiers(top)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword);
|
|
1494
|
+
const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
|
|
1495
|
+
const relative = this.#source.relative(source.fileName);
|
|
1496
|
+
const owner = this.#source.ownerOf(source.fileName);
|
|
1497
|
+
const extent = { start: top.getStart(source), end: top.getEnd() };
|
|
1498
|
+
const routeFile = this.#routeFiles.get(relative);
|
|
1499
|
+
const routeId = routeFile && (name === "default" || name === routeFile.alias) ? routeFile.id : undefined;
|
|
1500
|
+
if (routeId)
|
|
1501
|
+
return { id: routeId, kind: "component", owner, module: null, extent };
|
|
1502
|
+
const id = `component:${relative}#${name}`;
|
|
1503
|
+
const context = { id, kind: "component", owner, module: null, extent };
|
|
1504
|
+
if (this.#nodes.has(id))
|
|
1505
|
+
return context;
|
|
1506
|
+
const pagePrefix = owner ? `${owner}/page/` : null;
|
|
1507
|
+
const route = pagePrefix && relative.startsWith(pagePrefix) ? relative.slice(pagePrefix.length).replace(/\.tsx?$/, "") : null;
|
|
1508
|
+
const base = relative.slice(relative.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
|
|
1509
|
+
const label = route ? `${route}${name === "default" ? "" : `#${name}`}` : name === "default" ? base : `${base}.${name}`;
|
|
1510
|
+
const position = source.getLineAndCharacterOfPosition((declared ?? top).getStart(source));
|
|
1511
|
+
context.pending = {
|
|
1512
|
+
id,
|
|
1513
|
+
kind: "component",
|
|
1514
|
+
name,
|
|
1515
|
+
label,
|
|
1516
|
+
owner,
|
|
1517
|
+
module: this.#moduleDirOf(source.fileName),
|
|
1518
|
+
detail: route ? ["page"] : [],
|
|
1519
|
+
at: { file: relative, line: position.line + 1, column: position.character + 1 }
|
|
1520
|
+
};
|
|
1521
|
+
return context;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// pkgs/@akanjs/language/ast.ts
|
|
1526
|
+
var createAst = (ts) => {
|
|
1527
|
+
const sourceFileFor = (fileName, content, scriptKind = ts.ScriptKind.TS) => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
1528
|
+
const nodeName = (node) => {
|
|
1529
|
+
if (!node)
|
|
1530
|
+
return null;
|
|
1531
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node))
|
|
1532
|
+
return node.text;
|
|
1533
|
+
return null;
|
|
1534
|
+
};
|
|
1535
|
+
const propertyName = (node) => ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isMethodDeclaration(node) ? nodeName(node.name) : null;
|
|
1536
|
+
const expressionName = (expression) => {
|
|
1537
|
+
if (ts.isIdentifier(expression))
|
|
1538
|
+
return expression.text;
|
|
1539
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
1540
|
+
return expression.name.text;
|
|
1541
|
+
if (ts.isCallExpression(expression))
|
|
1542
|
+
return expressionName(expression.expression);
|
|
1543
|
+
if (ts.isAsExpression(expression))
|
|
1544
|
+
return expressionName(expression.expression);
|
|
1545
|
+
return null;
|
|
1546
|
+
};
|
|
1547
|
+
const callExpressionName = (node) => ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : expressionName(node.expression);
|
|
1548
|
+
const firstObjectReturnedByArrow = (node) => {
|
|
1549
|
+
if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node))
|
|
1550
|
+
return null;
|
|
1551
|
+
if (ts.isObjectLiteralExpression(node.body))
|
|
1552
|
+
return node.body;
|
|
1553
|
+
if (ts.isParenthesizedExpression(node.body) && ts.isObjectLiteralExpression(node.body.expression)) {
|
|
1554
|
+
return node.body.expression;
|
|
1555
|
+
}
|
|
1556
|
+
if (!ts.isBlock(node.body))
|
|
1557
|
+
return null;
|
|
1558
|
+
for (const statement of node.body.statements) {
|
|
1559
|
+
if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(statement.expression)) {
|
|
1560
|
+
return statement.expression;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return null;
|
|
1564
|
+
};
|
|
1565
|
+
const heritageCallNamed = (node, names) => {
|
|
1566
|
+
const heritage = node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
|
|
1567
|
+
for (const { expression } of heritage) {
|
|
1568
|
+
if (!ts.isCallExpression(expression))
|
|
1569
|
+
continue;
|
|
1570
|
+
const callee = expressionName(expression.expression);
|
|
1571
|
+
if (callee && names.has(callee))
|
|
1572
|
+
return { callee, call: expression };
|
|
1573
|
+
}
|
|
1574
|
+
return null;
|
|
1575
|
+
};
|
|
1576
|
+
const viaNames = new Set(["via"]);
|
|
1577
|
+
const heritageCall = (node) => heritageCallNamed(node, viaNames)?.call ?? null;
|
|
1578
|
+
return {
|
|
1579
|
+
sourceFileFor,
|
|
1580
|
+
nodeName,
|
|
1581
|
+
propertyName,
|
|
1582
|
+
expressionName,
|
|
1583
|
+
callExpressionName,
|
|
1584
|
+
firstObjectReturnedByArrow,
|
|
1585
|
+
heritageCallNamed,
|
|
1586
|
+
heritageCall
|
|
662
1587
|
};
|
|
663
|
-
return { read, spanOf, moduleRefOf, chainOf, unwrap };
|
|
664
1588
|
};
|
|
665
1589
|
|
|
666
1590
|
// pkgs/akanjs/common/capitalize.ts
|
|
@@ -1114,6 +2038,7 @@ class NameTable {
|
|
|
1114
2038
|
// pkgs/@akanjs/language/akanIndex.ts
|
|
1115
2039
|
var sourcePattern = /\.(constant|document|service|signal|store)\.ts$/;
|
|
1116
2040
|
var dictionaryPattern = /\.dictionary\.ts$/;
|
|
2041
|
+
var srvkitPattern = /\/srvkit\//;
|
|
1117
2042
|
var wordPattern = /[A-Za-z_$][\w$]*/g;
|
|
1118
2043
|
var identifierPattern = /^[A-Za-z_$][\w$]*$/;
|
|
1119
2044
|
var regExpSpecials = /[\\^$.*+?()[\]{}|]/g;
|
|
@@ -1149,21 +2074,26 @@ class AkanIndex {
|
|
|
1149
2074
|
}
|
|
1150
2075
|
setFile(givenName, text) {
|
|
1151
2076
|
const fileName = pathOf(givenName);
|
|
1152
|
-
|
|
2077
|
+
const words = `
|
|
1153
2078
|
${[...new Set(text.match(wordPattern))].join(`
|
|
1154
2079
|
`)}
|
|
1155
|
-
|
|
1156
|
-
|
|
2080
|
+
`;
|
|
2081
|
+
this.#fileWords.set(fileName, words);
|
|
2082
|
+
const isSource = AkanIndex.isSourcePath(fileName);
|
|
2083
|
+
if (!isSource && !AkanIndex.#holdsFacts(fileName, words)) {
|
|
2084
|
+
this.#facts.delete(fileName);
|
|
1157
2085
|
return;
|
|
2086
|
+
}
|
|
1158
2087
|
const source = this.#parsed.get(fileName)?.text === text ? this.#parse(fileName, text) : this.#parseUncached(fileName, text);
|
|
1159
2088
|
this.#facts.set(fileName, this.#reader.read(source, this.#ownerOf(fileName)));
|
|
1160
|
-
|
|
2089
|
+
if (isSource)
|
|
2090
|
+
this.#table = null;
|
|
1161
2091
|
}
|
|
1162
2092
|
removeFile(givenName) {
|
|
1163
2093
|
const fileName = pathOf(givenName);
|
|
1164
2094
|
this.#fileWords.delete(fileName);
|
|
1165
2095
|
this.#parsed.delete(fileName);
|
|
1166
|
-
if (this.#facts.delete(fileName))
|
|
2096
|
+
if (this.#facts.delete(fileName) && AkanIndex.isSourcePath(fileName))
|
|
1167
2097
|
this.#table = null;
|
|
1168
2098
|
}
|
|
1169
2099
|
definitionAt(givenName, text, offset) {
|
|
@@ -1196,8 +2126,9 @@ ${[...new Set(text.match(wordPattern))].join(`
|
|
|
1196
2126
|
referencesAt(givenName, offset, readText) {
|
|
1197
2127
|
return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
|
|
1198
2128
|
}
|
|
1199
|
-
graph(readText) {
|
|
2129
|
+
graph(readText, { digests = false } = {}) {
|
|
1200
2130
|
return new GraphBuilder({
|
|
2131
|
+
digests,
|
|
1201
2132
|
ts: this.#ts,
|
|
1202
2133
|
facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
|
|
1203
2134
|
files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
@@ -1210,7 +2141,8 @@ ${[...new Set(text.match(wordPattern))].join(`
|
|
|
1210
2141
|
return site ? this.#resolve(fileName, site) : [];
|
|
1211
2142
|
},
|
|
1212
2143
|
relative: (fileName) => this.#relative(fileName),
|
|
1213
|
-
ownerOf: (fileName) => this.#ownerOf(fileName)
|
|
2144
|
+
ownerOf: (fileName) => this.#ownerOf(fileName),
|
|
2145
|
+
reader: this.#reader
|
|
1214
2146
|
}).build();
|
|
1215
2147
|
}
|
|
1216
2148
|
renameTargetAt(givenName, offset) {
|
|
@@ -1233,6 +2165,22 @@ ${[...new Set(text.match(wordPattern))].join(`
|
|
|
1233
2165
|
}))
|
|
1234
2166
|
];
|
|
1235
2167
|
}
|
|
2168
|
+
static #holdsFacts(fileName, words) {
|
|
2169
|
+
if (testFilePattern.test(fileName))
|
|
2170
|
+
return false;
|
|
2171
|
+
if (srvkitPattern.test(fileName))
|
|
2172
|
+
return true;
|
|
2173
|
+
const holdsGuards = words.includes(`
|
|
2174
|
+
Guard
|
|
2175
|
+
`) && words.includes(`
|
|
2176
|
+
canPass
|
|
2177
|
+
`);
|
|
2178
|
+
return holdsGuards || words.includes(`
|
|
2179
|
+
adapt
|
|
2180
|
+
`) || words.includes(`
|
|
2181
|
+
dangerouslyAdapt
|
|
2182
|
+
`);
|
|
2183
|
+
}
|
|
1236
2184
|
static #sourceKeyOf({ family, sourceKey, label, detail, span }) {
|
|
1237
2185
|
return { family, sourceKey, label, detail, span };
|
|
1238
2186
|
}
|
|
@@ -1323,7 +2271,7 @@ ${dictionary.className}
|
|
|
1323
2271
|
return this.#nameTable().entriesTargeting(fileName).filter((entry) => entry.span.start <= offset && offset <= entry.span.end);
|
|
1324
2272
|
}
|
|
1325
2273
|
#nameTable() {
|
|
1326
|
-
this.#table ??= new NameTable(this.#facts.values());
|
|
2274
|
+
this.#table ??= new NameTable([...this.#facts.values()].filter(({ fileName }) => AkanIndex.isSourcePath(fileName)));
|
|
1327
2275
|
return this.#table;
|
|
1328
2276
|
}
|
|
1329
2277
|
#relative(fileName) {
|
|
@@ -1457,3 +2405,862 @@ ${dictionary.className}
|
|
|
1457
2405
|
})).sort((a, b) => Number(b.owner === owner) - Number(a.owner === owner));
|
|
1458
2406
|
}
|
|
1459
2407
|
}
|
|
2408
|
+
// pkgs/@akanjs/language/frameworkChannelReader.ts
|
|
2409
|
+
class FrameworkChannelReader {
|
|
2410
|
+
#ts;
|
|
2411
|
+
#unions = [];
|
|
2412
|
+
#channels = [];
|
|
2413
|
+
#missing = [];
|
|
2414
|
+
#deferred = [];
|
|
2415
|
+
#spoken = new Map;
|
|
2416
|
+
#sendParameters = new Set;
|
|
2417
|
+
#classes = new Map;
|
|
2418
|
+
#injectedSends = [];
|
|
2419
|
+
#constructed = new Map;
|
|
2420
|
+
constructor(ts, specs, sources) {
|
|
2421
|
+
this.#ts = ts;
|
|
2422
|
+
for (const source of sources.values())
|
|
2423
|
+
for (const statement of source.statements)
|
|
2424
|
+
if (ts.isClassDeclaration(statement) && statement.name)
|
|
2425
|
+
this.#classes.set(statement.name.text, [...this.#classes.get(statement.name.text) ?? [], statement]);
|
|
2426
|
+
for (const source of sources.values())
|
|
2427
|
+
this.#unions.push(...this.#unionsIn(source));
|
|
2428
|
+
const parts = new Set(this.#unions.flatMap((union) => [...union.typeNames].filter((name) => name !== union.name).map((name) => `${union.file}
|
|
2429
|
+
${name}`)));
|
|
2430
|
+
this.#unions.splice(0, this.#unions.length, ...this.#unions.filter((union) => !parts.has(`${union.file}
|
|
2431
|
+
${union.name}`)));
|
|
2432
|
+
for (const spec of specs) {
|
|
2433
|
+
const union = this.#unions.find(({ file, name }) => file === spec.file && name === spec.union);
|
|
2434
|
+
if (!union) {
|
|
2435
|
+
this.#missing.push(`${spec.union} in ${spec.file}`);
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
union.channel = { spec, union, senders: new Map, handlers: new Map };
|
|
2439
|
+
this.#channels.push(union.channel);
|
|
2440
|
+
}
|
|
2441
|
+
for (const source of sources.values())
|
|
2442
|
+
this.#read(source);
|
|
2443
|
+
for (const { channels, ...site } of this.#deferred) {
|
|
2444
|
+
const spoken = this.#spoken.get(site.source.fileName);
|
|
2445
|
+
for (const channel of channels)
|
|
2446
|
+
if (spoken?.has(channel))
|
|
2447
|
+
this.#record(channel, site);
|
|
2448
|
+
}
|
|
2449
|
+
for (const { owner, ...site } of this.#injectedSends)
|
|
2450
|
+
for (const file of this.#constructed.get(owner) ?? [])
|
|
2451
|
+
for (const channel of [...this.#spoken.get(file) ?? []])
|
|
2452
|
+
if (channel.union.members.has(site.literal) && !this.#holds(channel, site))
|
|
2453
|
+
this.#record(channel, site);
|
|
2454
|
+
}
|
|
2455
|
+
get missing() {
|
|
2456
|
+
return [...this.#missing];
|
|
2457
|
+
}
|
|
2458
|
+
channels() {
|
|
2459
|
+
return this.#channels.map(({ spec, union, senders, handlers }) => ({
|
|
2460
|
+
name: spec.union,
|
|
2461
|
+
label: spec.label,
|
|
2462
|
+
at: union.at,
|
|
2463
|
+
messages: [...union.members].map(([type, member]) => ({
|
|
2464
|
+
type,
|
|
2465
|
+
at: member.at,
|
|
2466
|
+
senders: FrameworkChannelReader.#grouped(senders.get(type) ?? []),
|
|
2467
|
+
handlers: FrameworkChannelReader.#grouped(handlers.get(type) ?? [])
|
|
2468
|
+
}))
|
|
2469
|
+
}));
|
|
2470
|
+
}
|
|
2471
|
+
static #grouped(sites) {
|
|
2472
|
+
const byOwner = new Map;
|
|
2473
|
+
const ordered = [...sites].sort((a, b) => a.at.file === b.at.file ? a.at.line - b.at.line : a.at.file < b.at.file ? -1 : 1);
|
|
2474
|
+
for (const site of ordered) {
|
|
2475
|
+
const existing = byOwner.get(site.owner);
|
|
2476
|
+
if (existing)
|
|
2477
|
+
existing.count += 1;
|
|
2478
|
+
else
|
|
2479
|
+
byOwner.set(site.owner, { ...site, count: 1 });
|
|
2480
|
+
}
|
|
2481
|
+
return [...byOwner.values()].sort((a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : 0);
|
|
2482
|
+
}
|
|
2483
|
+
#location(source, node) {
|
|
2484
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
2485
|
+
return { file: source.fileName, line: line + 1, column: character + 1 };
|
|
2486
|
+
}
|
|
2487
|
+
#unionsIn(source) {
|
|
2488
|
+
const ts = this.#ts;
|
|
2489
|
+
const declarations = new Map;
|
|
2490
|
+
for (const statement of source.statements)
|
|
2491
|
+
if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement))
|
|
2492
|
+
declarations.set(statement.name.text, statement);
|
|
2493
|
+
return [...declarations.values()].flatMap((declaration) => {
|
|
2494
|
+
if (!ts.isTypeAliasDeclaration(declaration) || !ts.isUnionTypeNode(declaration.type))
|
|
2495
|
+
return [];
|
|
2496
|
+
const members = new Map;
|
|
2497
|
+
const typeNames = new Set([declaration.name.text]);
|
|
2498
|
+
const isShaped = this.#collect(source, declaration.type, declarations, members, typeNames);
|
|
2499
|
+
if (!isShaped || members.size < 2)
|
|
2500
|
+
return [];
|
|
2501
|
+
return [
|
|
2502
|
+
{
|
|
2503
|
+
file: source.fileName,
|
|
2504
|
+
name: declaration.name.text,
|
|
2505
|
+
at: this.#location(source, declaration.name),
|
|
2506
|
+
members,
|
|
2507
|
+
typeNames,
|
|
2508
|
+
channel: null
|
|
2509
|
+
}
|
|
2510
|
+
];
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
#collect(source, type, declarations, members, typeNames) {
|
|
2514
|
+
const ts = this.#ts;
|
|
2515
|
+
if (ts.isParenthesizedTypeNode(type))
|
|
2516
|
+
return this.#collect(source, type.type, declarations, members, typeNames);
|
|
2517
|
+
if (ts.isUnionTypeNode(type))
|
|
2518
|
+
return type.types.every((member) => this.#collect(source, member, declarations, members, typeNames));
|
|
2519
|
+
if (ts.isTypeLiteralNode(type))
|
|
2520
|
+
return this.#addMember(source, type.members, members);
|
|
2521
|
+
if (!ts.isTypeReferenceNode(type) || !ts.isIdentifier(type.typeName))
|
|
2522
|
+
return false;
|
|
2523
|
+
const name = type.typeName.text;
|
|
2524
|
+
const declaration = declarations.get(name);
|
|
2525
|
+
if (!declaration)
|
|
2526
|
+
return false;
|
|
2527
|
+
if (typeNames.has(name))
|
|
2528
|
+
return true;
|
|
2529
|
+
typeNames.add(name);
|
|
2530
|
+
return ts.isTypeAliasDeclaration(declaration) ? this.#collect(source, declaration.type, declarations, members, typeNames) : this.#addMember(source, declaration.members, members);
|
|
2531
|
+
}
|
|
2532
|
+
#addMember(source, elements, members) {
|
|
2533
|
+
const ts = this.#ts;
|
|
2534
|
+
const typeProperty = elements.find((element) => ts.isPropertySignature(element) && this.#nameOf(element.name) === "type");
|
|
2535
|
+
const literal = typeProperty?.type && ts.isLiteralTypeNode(typeProperty.type) && ts.isStringLiteral(typeProperty.type.literal) ? typeProperty.type.literal.text : null;
|
|
2536
|
+
if (!typeProperty || literal === null)
|
|
2537
|
+
return false;
|
|
2538
|
+
const keys = new Set;
|
|
2539
|
+
const required = new Set;
|
|
2540
|
+
for (const element of elements) {
|
|
2541
|
+
const name = ts.isPropertySignature(element) ? this.#nameOf(element.name) : null;
|
|
2542
|
+
if (!name)
|
|
2543
|
+
continue;
|
|
2544
|
+
keys.add(name);
|
|
2545
|
+
if (ts.isPropertySignature(element) && !element.questionToken)
|
|
2546
|
+
required.add(name);
|
|
2547
|
+
}
|
|
2548
|
+
const existing = members.get(literal);
|
|
2549
|
+
if (!existing) {
|
|
2550
|
+
members.set(literal, { at: this.#location(source, typeProperty), keys, required });
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
for (const key of keys)
|
|
2554
|
+
existing.keys.add(key);
|
|
2555
|
+
for (const key of existing.required)
|
|
2556
|
+
if (!required.has(key))
|
|
2557
|
+
existing.required.delete(key);
|
|
2558
|
+
return true;
|
|
2559
|
+
}
|
|
2560
|
+
#nameOf(name) {
|
|
2561
|
+
const ts = this.#ts;
|
|
2562
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
|
|
2563
|
+
}
|
|
2564
|
+
#literalOf(expression) {
|
|
2565
|
+
const ts = this.#ts;
|
|
2566
|
+
return expression && (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) ? expression.text : null;
|
|
2567
|
+
}
|
|
2568
|
+
#record(channel, { role, literal, source, node }) {
|
|
2569
|
+
const sites = channel[role];
|
|
2570
|
+
const site = { owner: this.#ownerOf(source, node), count: 1, at: this.#location(source, node) };
|
|
2571
|
+
const relayed = role === "handlers" && !!site.owner && !!channel.spec.relays?.includes(site.owner);
|
|
2572
|
+
if (!relayed)
|
|
2573
|
+
sites.set(literal, [...sites.get(literal) ?? [], site]);
|
|
2574
|
+
const spoken = this.#spoken.get(source.fileName) ?? new Set;
|
|
2575
|
+
spoken.add(channel);
|
|
2576
|
+
this.#spoken.set(source.fileName, spoken);
|
|
2577
|
+
}
|
|
2578
|
+
#settle(candidates, site) {
|
|
2579
|
+
const local = candidates.filter(({ file }) => file === site.source.fileName);
|
|
2580
|
+
const unions = candidates.length > 1 && local.length ? local : candidates;
|
|
2581
|
+
const channels = unions.flatMap(({ channel }) => channel ? [channel] : []);
|
|
2582
|
+
if (unions.length === 1 && channels.length === 1)
|
|
2583
|
+
this.#record(channels[0], site);
|
|
2584
|
+
else if (channels.length)
|
|
2585
|
+
this.#deferred.push({ ...site, channels });
|
|
2586
|
+
}
|
|
2587
|
+
#ownerOf(source, node) {
|
|
2588
|
+
const ts = this.#ts;
|
|
2589
|
+
let top = node;
|
|
2590
|
+
for (let current = node;current; current = current.parent) {
|
|
2591
|
+
if ((ts.isClassDeclaration(current) || ts.isClassExpression(current)) && current.name)
|
|
2592
|
+
return current.name.text;
|
|
2593
|
+
if (current.parent && !ts.isSourceFile(current.parent))
|
|
2594
|
+
top = current.parent;
|
|
2595
|
+
}
|
|
2596
|
+
if (ts.isFunctionDeclaration(top) && top.name)
|
|
2597
|
+
return top.name.text;
|
|
2598
|
+
if (ts.isVariableStatement(top)) {
|
|
2599
|
+
const declaration = top.declarationList.declarations.find(({ initializer, pos, end }) => !!initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) && pos <= node.pos && node.end <= end);
|
|
2600
|
+
if (declaration && ts.isIdentifier(declaration.name))
|
|
2601
|
+
return declaration.name.text;
|
|
2602
|
+
}
|
|
2603
|
+
const classes = source.statements.filter(ts.isClassDeclaration);
|
|
2604
|
+
const only = classes.length === 1 ? classes[0].name?.text : undefined;
|
|
2605
|
+
return only ?? source.fileName.slice(source.fileName.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
|
|
2606
|
+
}
|
|
2607
|
+
#namesIn(type) {
|
|
2608
|
+
const ts = this.#ts;
|
|
2609
|
+
const names = new Set;
|
|
2610
|
+
const visit = (node) => {
|
|
2611
|
+
if (ts.isTypeReferenceNode(node))
|
|
2612
|
+
names.add(ts.isIdentifier(node.typeName) ? node.typeName.text : node.typeName.right.text);
|
|
2613
|
+
if (!ts.isTypeLiteralNode(node))
|
|
2614
|
+
ts.forEachChild(node, visit);
|
|
2615
|
+
};
|
|
2616
|
+
if (type)
|
|
2617
|
+
visit(type);
|
|
2618
|
+
return names;
|
|
2619
|
+
}
|
|
2620
|
+
#literalTypesIn(type) {
|
|
2621
|
+
const ts = this.#ts;
|
|
2622
|
+
const literals = new Set;
|
|
2623
|
+
const visit = (node) => {
|
|
2624
|
+
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal))
|
|
2625
|
+
literals.add(node.literal.text);
|
|
2626
|
+
ts.forEachChild(node, visit);
|
|
2627
|
+
};
|
|
2628
|
+
if (type)
|
|
2629
|
+
visit(type);
|
|
2630
|
+
return literals;
|
|
2631
|
+
}
|
|
2632
|
+
#hinted(names) {
|
|
2633
|
+
return this.#unions.filter((union) => [...names].some((name) => union.typeNames.has(name)));
|
|
2634
|
+
}
|
|
2635
|
+
#read(source) {
|
|
2636
|
+
const ts = this.#ts;
|
|
2637
|
+
const comparisons = new Map;
|
|
2638
|
+
const switches = [];
|
|
2639
|
+
const templates = [];
|
|
2640
|
+
const calls = [];
|
|
2641
|
+
const visit = (node) => {
|
|
2642
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
2643
|
+
const template = this.#readSend(source, node);
|
|
2644
|
+
if (template)
|
|
2645
|
+
templates.push(template);
|
|
2646
|
+
} else if (ts.isCallExpression(node))
|
|
2647
|
+
calls.push(node);
|
|
2648
|
+
else if (ts.isNewExpression(node)) {
|
|
2649
|
+
this.#readRouteSet(source, node);
|
|
2650
|
+
if (ts.isIdentifier(node.expression)) {
|
|
2651
|
+
const files = this.#constructed.get(node.expression.text) ?? new Set;
|
|
2652
|
+
files.add(source.fileName);
|
|
2653
|
+
this.#constructed.set(node.expression.text, files);
|
|
2654
|
+
}
|
|
2655
|
+
} else if (ts.isSwitchStatement(node)) {
|
|
2656
|
+
const chain = this.#switchChain(node);
|
|
2657
|
+
if (chain)
|
|
2658
|
+
switches.push(chain);
|
|
2659
|
+
} else if (ts.isBinaryExpression(node)) {
|
|
2660
|
+
const compared = this.#comparedType(node);
|
|
2661
|
+
if (compared) {
|
|
2662
|
+
const key = `${this.#scopeOf(node)?.pos ?? -1}:${compared.receiver.getText(source)}`;
|
|
2663
|
+
const chain = comparisons.get(key) ?? {
|
|
2664
|
+
literals: [],
|
|
2665
|
+
hint: this.#declaredTypeNames(compared.receiver),
|
|
2666
|
+
parameter: this.#parameterOf(compared.receiver)
|
|
2667
|
+
};
|
|
2668
|
+
chain.literals.push([compared.literal, compared.at]);
|
|
2669
|
+
comparisons.set(key, chain);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
ts.forEachChild(node, visit);
|
|
2673
|
+
};
|
|
2674
|
+
visit(source);
|
|
2675
|
+
for (const { object, fn, index } of templates)
|
|
2676
|
+
for (const call of calls) {
|
|
2677
|
+
const argument = call.arguments[index];
|
|
2678
|
+
const literal = this.#literalOf(argument);
|
|
2679
|
+
if (literal !== null && this.#calleeOf(call) === fn)
|
|
2680
|
+
this.#sendSite(source, object, literal, argument, true);
|
|
2681
|
+
}
|
|
2682
|
+
for (const chain of [...switches, ...comparisons.values()])
|
|
2683
|
+
if (!chain.parameter || !this.#sendParameters.has(chain.parameter))
|
|
2684
|
+
this.#readChain(source, chain);
|
|
2685
|
+
}
|
|
2686
|
+
#readSend(source, object) {
|
|
2687
|
+
const ts = this.#ts;
|
|
2688
|
+
const typeProperty = object.properties.find((property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && this.#nameOf(property.name) === "type");
|
|
2689
|
+
if (!typeProperty)
|
|
2690
|
+
return null;
|
|
2691
|
+
const value = ts.isPropertyAssignment(typeProperty) ? typeProperty.initializer : typeProperty.name;
|
|
2692
|
+
const literal = this.#literalOf(value);
|
|
2693
|
+
if (literal !== null)
|
|
2694
|
+
this.#sendSite(source, object, literal, typeProperty, false);
|
|
2695
|
+
if (literal !== null || !ts.isIdentifier(value))
|
|
2696
|
+
return null;
|
|
2697
|
+
const fn = this.#scopeOf(object);
|
|
2698
|
+
const index = fn?.parameters.findIndex(({ name }) => ts.isIdentifier(name) && name.text === value.text) ?? -1;
|
|
2699
|
+
return fn && index >= 0 ? { object, fn, index } : null;
|
|
2700
|
+
}
|
|
2701
|
+
#sendSite(source, object, literal, node, forwarded) {
|
|
2702
|
+
const context = this.#sendContextOf(object, forwarded ? null : literal);
|
|
2703
|
+
if (!context)
|
|
2704
|
+
return;
|
|
2705
|
+
const site = { role: "senders", literal, source, node };
|
|
2706
|
+
const injector = this.#injectorOf(object);
|
|
2707
|
+
if (injector)
|
|
2708
|
+
this.#injectedSends.push({ ...site, owner: injector });
|
|
2709
|
+
const hinted = this.#hinted(context.names).filter((union) => union.members.has(literal));
|
|
2710
|
+
const candidates = hinted.length ? hinted : this.#fitting(object, literal);
|
|
2711
|
+
if (context.parameter && candidates.some(({ channel }) => channel))
|
|
2712
|
+
this.#sendParameters.add(context.parameter);
|
|
2713
|
+
this.#settle(candidates, site);
|
|
2714
|
+
}
|
|
2715
|
+
#injectorOf(object) {
|
|
2716
|
+
const ts = this.#ts;
|
|
2717
|
+
const call = object.parent;
|
|
2718
|
+
if (!ts.isCallExpression(call) || !call.arguments.includes(object))
|
|
2719
|
+
return null;
|
|
2720
|
+
const callee = call.expression;
|
|
2721
|
+
if (!ts.isPropertyAccessExpression(callee) || callee.expression.kind !== ts.SyntaxKind.ThisKeyword)
|
|
2722
|
+
return null;
|
|
2723
|
+
const owner = this.#classOf(call);
|
|
2724
|
+
const constructor = owner?.members.find(ts.isConstructorDeclaration);
|
|
2725
|
+
const parameters = new Set(constructor?.parameters.flatMap(({ name }) => ts.isIdentifier(name) ? [name.text] : []) ?? []);
|
|
2726
|
+
const isInjected = !!constructor?.body?.statements.some((statement) => {
|
|
2727
|
+
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression))
|
|
2728
|
+
return false;
|
|
2729
|
+
const { left, operatorToken, right } = statement.expression;
|
|
2730
|
+
return operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(left) && left.expression.kind === ts.SyntaxKind.ThisKeyword && this.#memberName(left.name) === callee.name.text && ts.isIdentifier(right) && parameters.has(right.text);
|
|
2731
|
+
});
|
|
2732
|
+
return isInjected && owner?.name ? owner.name.text : null;
|
|
2733
|
+
}
|
|
2734
|
+
#holds(channel, site) {
|
|
2735
|
+
const { file, line, column } = this.#location(site.source, site.node);
|
|
2736
|
+
return (channel[site.role].get(site.literal) ?? []).some(({ at }) => at.file === file && at.line === line && at.column === column);
|
|
2737
|
+
}
|
|
2738
|
+
#fitting(object, literal) {
|
|
2739
|
+
const ts = this.#ts;
|
|
2740
|
+
const isLoose = object.properties.some((property) => ts.isSpreadAssignment(property));
|
|
2741
|
+
const keys = new Set(object.properties.flatMap((property) => {
|
|
2742
|
+
const name = ts.isSpreadAssignment(property) ? null : this.#nameOf(property.name);
|
|
2743
|
+
return name === null ? [] : [name];
|
|
2744
|
+
}));
|
|
2745
|
+
return this.#unions.filter((union) => {
|
|
2746
|
+
const member = union.members.get(literal);
|
|
2747
|
+
return !!member && [...keys].every((key) => member.keys.has(key)) && (isLoose || [...member.required].every((key) => keys.has(key)));
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
#sendContextOf(object, literal) {
|
|
2751
|
+
const ts = this.#ts;
|
|
2752
|
+
const hint = new Set;
|
|
2753
|
+
let node = object;
|
|
2754
|
+
while (node.parent) {
|
|
2755
|
+
const parent = node.parent;
|
|
2756
|
+
if (ts.isParenthesizedExpression(parent) || ts.isConditionalExpression(parent))
|
|
2757
|
+
node = parent;
|
|
2758
|
+
else if (ts.isAsExpression(parent) || ts.isSatisfiesExpression(parent)) {
|
|
2759
|
+
for (const name of this.#namesIn(parent.type))
|
|
2760
|
+
hint.add(name);
|
|
2761
|
+
node = parent;
|
|
2762
|
+
} else if (ts.isCallExpression(parent) && this.#calleeName(parent) === "stringify")
|
|
2763
|
+
node = parent;
|
|
2764
|
+
else if (ts.isArrowFunction(parent) && parent.body === node && !parent.type)
|
|
2765
|
+
node = parent;
|
|
2766
|
+
else
|
|
2767
|
+
break;
|
|
2768
|
+
}
|
|
2769
|
+
const parent = node.parent;
|
|
2770
|
+
if (!parent)
|
|
2771
|
+
return null;
|
|
2772
|
+
const scope = this.#scopeOf(object);
|
|
2773
|
+
if (literal !== null && scope?.parameters.some(({ type }) => this.#literalTypesIn(type).has(literal)))
|
|
2774
|
+
return null;
|
|
2775
|
+
const context = (names, parameter = null) => ({
|
|
2776
|
+
names: new Set([...hint, ...names]),
|
|
2777
|
+
parameter
|
|
2778
|
+
});
|
|
2779
|
+
if (ts.isCallExpression(parent)) {
|
|
2780
|
+
const index = parent.arguments.indexOf(node);
|
|
2781
|
+
if (index < 0)
|
|
2782
|
+
return null;
|
|
2783
|
+
const parameter = this.#calleeOf(parent)?.parameters[index] ?? null;
|
|
2784
|
+
return context(this.#namesIn(parameter?.type), parameter);
|
|
2785
|
+
}
|
|
2786
|
+
if (ts.isVariableDeclaration(parent) && parent.type)
|
|
2787
|
+
return context(this.#namesIn(parent.type));
|
|
2788
|
+
if (ts.isPropertyAssignment(parent) && ts.isObjectLiteralExpression(parent.parent)) {
|
|
2789
|
+
const outer = this.#sendContextOf(parent.parent, null);
|
|
2790
|
+
const property = this.#nameOf(parent.name);
|
|
2791
|
+
const names = outer && property ? this.#propertyTypeNames(object.getSourceFile(), outer.names, property) : null;
|
|
2792
|
+
return names?.size ? context(names) : null;
|
|
2793
|
+
}
|
|
2794
|
+
const fn = ts.isReturnStatement(parent) ? this.#scopeOf(parent) : ts.isArrowFunction(parent) ? parent : null;
|
|
2795
|
+
if (fn?.type)
|
|
2796
|
+
return context(this.#namesIn(fn.type));
|
|
2797
|
+
return hint.size ? context(new Set) : null;
|
|
2798
|
+
}
|
|
2799
|
+
#propertyTypeNames(source, typeNames, property) {
|
|
2800
|
+
const ts = this.#ts;
|
|
2801
|
+
for (const statement of source.statements) {
|
|
2802
|
+
if (!(ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)))
|
|
2803
|
+
continue;
|
|
2804
|
+
if (!typeNames.has(statement.name.text))
|
|
2805
|
+
continue;
|
|
2806
|
+
const members = ts.isInterfaceDeclaration(statement) ? statement.members : ts.isTypeLiteralNode(statement.type) ? statement.type.members : [];
|
|
2807
|
+
const signature = members.find((member) => ts.isPropertySignature(member) && this.#nameOf(member.name) === property);
|
|
2808
|
+
if (signature?.type)
|
|
2809
|
+
return this.#namesIn(signature.type);
|
|
2810
|
+
}
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2813
|
+
#calleeOf(call) {
|
|
2814
|
+
const ts = this.#ts;
|
|
2815
|
+
const callee = call.expression;
|
|
2816
|
+
const isFunction = (node) => !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
|
|
2817
|
+
if (ts.isPropertyAccessExpression(callee) && callee.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
2818
|
+
const member = this.#memberOf(this.#classOf(call), callee.name.text);
|
|
2819
|
+
if (!member)
|
|
2820
|
+
return null;
|
|
2821
|
+
if (ts.isMethodDeclaration(member))
|
|
2822
|
+
return member;
|
|
2823
|
+
return ts.isPropertyDeclaration(member) && isFunction(member.initializer) ? member.initializer : null;
|
|
2824
|
+
}
|
|
2825
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
2826
|
+
for (const name of this.#receiverTypeNames(callee.expression))
|
|
2827
|
+
for (const declaration of this.#classes.get(name) ?? []) {
|
|
2828
|
+
const member = this.#memberOf(declaration, callee.name.text);
|
|
2829
|
+
if (member && ts.isMethodDeclaration(member))
|
|
2830
|
+
return member;
|
|
2831
|
+
}
|
|
2832
|
+
return null;
|
|
2833
|
+
}
|
|
2834
|
+
if (!ts.isIdentifier(callee))
|
|
2835
|
+
return null;
|
|
2836
|
+
for (const statement of call.getSourceFile().statements) {
|
|
2837
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === callee.text)
|
|
2838
|
+
return statement;
|
|
2839
|
+
if (!ts.isVariableStatement(statement))
|
|
2840
|
+
continue;
|
|
2841
|
+
for (const declaration of statement.declarationList.declarations)
|
|
2842
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === callee.text)
|
|
2843
|
+
return isFunction(declaration.initializer) ? declaration.initializer : null;
|
|
2844
|
+
}
|
|
2845
|
+
return null;
|
|
2846
|
+
}
|
|
2847
|
+
#classOf(node) {
|
|
2848
|
+
let owner = node.parent;
|
|
2849
|
+
while (owner && !this.#ts.isClassLike(owner))
|
|
2850
|
+
owner = owner.parent;
|
|
2851
|
+
return owner ?? null;
|
|
2852
|
+
}
|
|
2853
|
+
#memberOf(owner, name) {
|
|
2854
|
+
return owner?.members.find((member) => member.name && this.#memberName(member.name) === name) ?? null;
|
|
2855
|
+
}
|
|
2856
|
+
#receiverTypeNames(receiver) {
|
|
2857
|
+
const ts = this.#ts;
|
|
2858
|
+
if (ts.isPropertyAccessExpression(receiver) && receiver.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
|
2859
|
+
const field = this.#memberOf(this.#classOf(receiver), receiver.name.text);
|
|
2860
|
+
return field && ts.isPropertyDeclaration(field) ? this.#namesIn(field.type) : new Set;
|
|
2861
|
+
}
|
|
2862
|
+
return this.#declaredTypeNames(receiver) ?? new Set;
|
|
2863
|
+
}
|
|
2864
|
+
#memberName(name) {
|
|
2865
|
+
const ts = this.#ts;
|
|
2866
|
+
return ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
|
|
2867
|
+
}
|
|
2868
|
+
#calleeName(call) {
|
|
2869
|
+
const ts = this.#ts;
|
|
2870
|
+
const callee = call.expression;
|
|
2871
|
+
if (ts.isIdentifier(callee))
|
|
2872
|
+
return callee.text;
|
|
2873
|
+
if (ts.isPropertyAccessExpression(callee))
|
|
2874
|
+
return callee.name.text;
|
|
2875
|
+
return null;
|
|
2876
|
+
}
|
|
2877
|
+
#scopeOf(node) {
|
|
2878
|
+
const ts = this.#ts;
|
|
2879
|
+
for (let current = node.parent;current; current = current.parent)
|
|
2880
|
+
if (ts.isFunctionLike(current))
|
|
2881
|
+
return current;
|
|
2882
|
+
return null;
|
|
2883
|
+
}
|
|
2884
|
+
#typeReceiverOf(expression) {
|
|
2885
|
+
const ts = this.#ts;
|
|
2886
|
+
if (ts.isPropertyAccessExpression(expression) && expression.name.text === "type")
|
|
2887
|
+
return expression.expression;
|
|
2888
|
+
if (ts.isElementAccessExpression(expression) && this.#literalOf(expression.argumentExpression) === "type")
|
|
2889
|
+
return expression.expression;
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2892
|
+
#comparedType(node) {
|
|
2893
|
+
const ts = this.#ts;
|
|
2894
|
+
const { kind } = node.operatorToken;
|
|
2895
|
+
if (kind !== ts.SyntaxKind.EqualsEqualsEqualsToken && kind !== ts.SyntaxKind.ExclamationEqualsEqualsToken && kind !== ts.SyntaxKind.EqualsEqualsToken && kind !== ts.SyntaxKind.ExclamationEqualsToken)
|
|
2896
|
+
return null;
|
|
2897
|
+
for (const [side, other] of [
|
|
2898
|
+
[node.left, node.right],
|
|
2899
|
+
[node.right, node.left]
|
|
2900
|
+
]) {
|
|
2901
|
+
const receiver = this.#typeReceiverOf(side);
|
|
2902
|
+
const literal = this.#literalOf(other);
|
|
2903
|
+
if (receiver && literal !== null)
|
|
2904
|
+
return { receiver, literal, at: other };
|
|
2905
|
+
}
|
|
2906
|
+
return null;
|
|
2907
|
+
}
|
|
2908
|
+
#declaredTypeNames(receiver) {
|
|
2909
|
+
const ts = this.#ts;
|
|
2910
|
+
if (!ts.isIdentifier(receiver))
|
|
2911
|
+
return null;
|
|
2912
|
+
const name = receiver.text;
|
|
2913
|
+
for (let current = receiver.parent;current; current = current.parent) {
|
|
2914
|
+
if (ts.isFunctionLike(current)) {
|
|
2915
|
+
for (const parameter of current.parameters)
|
|
2916
|
+
if (ts.isIdentifier(parameter.name) && parameter.name.text === name)
|
|
2917
|
+
return parameter.type ? this.#namesIn(parameter.type) : null;
|
|
2918
|
+
}
|
|
2919
|
+
if (ts.isBlock(current) || ts.isSourceFile(current) || ts.isCaseClause(current))
|
|
2920
|
+
for (const statement of current.statements) {
|
|
2921
|
+
if (!ts.isVariableStatement(statement))
|
|
2922
|
+
continue;
|
|
2923
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2924
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name)
|
|
2925
|
+
continue;
|
|
2926
|
+
const { initializer } = declaration;
|
|
2927
|
+
const asType = initializer && (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer)) ? initializer.type : undefined;
|
|
2928
|
+
const type = declaration.type ?? asType;
|
|
2929
|
+
return type ? this.#namesIn(type) : null;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return null;
|
|
2934
|
+
}
|
|
2935
|
+
#parameterOf(receiver) {
|
|
2936
|
+
const ts = this.#ts;
|
|
2937
|
+
if (!ts.isIdentifier(receiver))
|
|
2938
|
+
return null;
|
|
2939
|
+
for (let current = receiver.parent;current; current = current.parent)
|
|
2940
|
+
if (ts.isFunctionLike(current)) {
|
|
2941
|
+
const parameter = current.parameters.find(({ name }) => ts.isIdentifier(name) && name.text === receiver.text);
|
|
2942
|
+
if (parameter)
|
|
2943
|
+
return parameter;
|
|
2944
|
+
}
|
|
2945
|
+
return null;
|
|
2946
|
+
}
|
|
2947
|
+
#readChain(source, { literals, hint }) {
|
|
2948
|
+
const hinted = hint?.size ? this.#hinted(hint) : [];
|
|
2949
|
+
if (hint?.size && !hinted.length)
|
|
2950
|
+
return;
|
|
2951
|
+
const scored = this.#unions.map((union) => ({ union, hits: literals.filter(([literal]) => union.members.has(literal)).length })).filter(({ hits }) => hits > 0);
|
|
2952
|
+
const best = Math.max(0, ...scored.map(({ hits }) => hits));
|
|
2953
|
+
const unions = hinted.length ? hinted : best * 2 >= literals.length ? scored.filter(({ hits }) => hits === best).map(({ union }) => union) : [];
|
|
2954
|
+
for (const [literal, node] of literals) {
|
|
2955
|
+
const holding = unions.filter((union) => union.members.has(literal));
|
|
2956
|
+
this.#settle(holding, { role: "handlers", literal, source, node });
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
#switchChain(statement) {
|
|
2960
|
+
const ts = this.#ts;
|
|
2961
|
+
const receiver = this.#typeReceiverOf(statement.expression);
|
|
2962
|
+
if (!receiver)
|
|
2963
|
+
return null;
|
|
2964
|
+
const literals = statement.caseBlock.clauses.flatMap((clause) => {
|
|
2965
|
+
if (!ts.isCaseClause(clause))
|
|
2966
|
+
return [];
|
|
2967
|
+
const literal = this.#literalOf(clause.expression);
|
|
2968
|
+
return literal === null ? [] : [[literal, clause.expression]];
|
|
2969
|
+
});
|
|
2970
|
+
return { literals, hint: this.#declaredTypeNames(receiver), parameter: this.#parameterOf(receiver) };
|
|
2971
|
+
}
|
|
2972
|
+
#readRouteSet(source, expression) {
|
|
2973
|
+
const ts = this.#ts;
|
|
2974
|
+
if (!ts.isIdentifier(expression.expression) || expression.expression.text !== "Set")
|
|
2975
|
+
return;
|
|
2976
|
+
const unions = this.#hinted(this.#namesIn(expression.typeArguments?.[0]));
|
|
2977
|
+
const list = expression.arguments?.[0];
|
|
2978
|
+
if (!unions.length || !list || !ts.isArrayLiteralExpression(list))
|
|
2979
|
+
return;
|
|
2980
|
+
for (const element of list.elements) {
|
|
2981
|
+
const literal = this.#literalOf(element);
|
|
2982
|
+
if (literal === null)
|
|
2983
|
+
continue;
|
|
2984
|
+
const holding = unions.filter((union) => union.members.has(literal));
|
|
2985
|
+
this.#settle(holding, { role: "handlers", literal, source, node: element });
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
// pkgs/@akanjs/language/frameworkMapBuilder.ts
|
|
2990
|
+
var extensions = ["", ".ts", ".tsx", "/index.ts", "/index.tsx"];
|
|
2991
|
+
var byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
2992
|
+
|
|
2993
|
+
class FrameworkMapBuilder {
|
|
2994
|
+
#source;
|
|
2995
|
+
#fileSet;
|
|
2996
|
+
constructor(source) {
|
|
2997
|
+
this.#source = source;
|
|
2998
|
+
this.#fileSet = new Set(source.files.map(([file]) => file));
|
|
2999
|
+
}
|
|
3000
|
+
static isSourcePath(file) {
|
|
3001
|
+
return /\.tsx?$/.test(file) && !/\.d\.ts$|\.(?:test|spec)\.tsx?$|fixture/i.test(file);
|
|
3002
|
+
}
|
|
3003
|
+
build() {
|
|
3004
|
+
const { ts, packages, files, channels } = this.#source;
|
|
3005
|
+
const sources = new Map;
|
|
3006
|
+
for (const [file, text] of [...files].sort(([a], [b]) => byText(a, b)))
|
|
3007
|
+
sources.set(file, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS));
|
|
3008
|
+
const facets = new Map;
|
|
3009
|
+
const imports = new Map;
|
|
3010
|
+
for (const [file, source] of sources) {
|
|
3011
|
+
const facet = this.#facetOf(file);
|
|
3012
|
+
if (!facet)
|
|
3013
|
+
continue;
|
|
3014
|
+
const pkg = this.#packageOf(file);
|
|
3015
|
+
const current = facets.get(facet) ?? {
|
|
3016
|
+
id: facet,
|
|
3017
|
+
name: pkg?.split ? facet.slice(pkg.name.length + 1) : facet,
|
|
3018
|
+
packageName: pkg?.name ?? facet,
|
|
3019
|
+
layer: 0,
|
|
3020
|
+
files: 0,
|
|
3021
|
+
classes: []
|
|
3022
|
+
};
|
|
3023
|
+
current.files += 1;
|
|
3024
|
+
current.classes.push(...this.#classesOf(source));
|
|
3025
|
+
facets.set(facet, current);
|
|
3026
|
+
for (const site of this.#importsOf(file, source)) {
|
|
3027
|
+
if (site.facet === facet)
|
|
3028
|
+
continue;
|
|
3029
|
+
const key = `${facet}
|
|
3030
|
+
${site.facet}`;
|
|
3031
|
+
const entry = imports.get(key) ?? { from: facet, to: site.facet, value: 0, type: 0, dynamic: 0, at: site.at };
|
|
3032
|
+
if (entry.value === 0 && site.kind === "value" && imports.has(key))
|
|
3033
|
+
entry.at = site.at;
|
|
3034
|
+
entry[site.kind] += 1;
|
|
3035
|
+
imports.set(key, entry);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
const { layerOf, cycles } = FrameworkMapBuilder.#layersOf([...imports.values()]);
|
|
3039
|
+
for (const facet of facets.values()) {
|
|
3040
|
+
facet.classes.sort((a, b) => b.members - a.members || byText(a.name, b.name));
|
|
3041
|
+
facet.layer = layerOf(facet.id);
|
|
3042
|
+
}
|
|
3043
|
+
const reader = new FrameworkChannelReader(ts, channels, sources);
|
|
3044
|
+
return {
|
|
3045
|
+
version: 1,
|
|
3046
|
+
packages: packages.map(({ name, root }) => ({ name, root })),
|
|
3047
|
+
facets: [...facets.values()].sort((a, b) => byText(a.id, b.id)),
|
|
3048
|
+
imports: [...imports.values()].sort((a, b) => byText(a.from, b.from) || byText(a.to, b.to)),
|
|
3049
|
+
channels: reader.channels(),
|
|
3050
|
+
missing: reader.missing,
|
|
3051
|
+
cycles
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
static #layersOf(imports) {
|
|
3055
|
+
const deps = new Map;
|
|
3056
|
+
for (const { from, to, value, dynamic } of imports)
|
|
3057
|
+
if (value + dynamic > 0)
|
|
3058
|
+
deps.set(from, [...deps.get(from) ?? [], to]);
|
|
3059
|
+
const components = FrameworkMapBuilder.#stronglyConnected(deps);
|
|
3060
|
+
const componentOf = new Map(components.flatMap((component) => component.map((id) => [id, component])));
|
|
3061
|
+
const layers = new Map;
|
|
3062
|
+
const layerOf = (id) => {
|
|
3063
|
+
const component = componentOf.get(id);
|
|
3064
|
+
if (!component)
|
|
3065
|
+
return 0;
|
|
3066
|
+
const known = layers.get(component);
|
|
3067
|
+
if (known !== undefined)
|
|
3068
|
+
return known;
|
|
3069
|
+
const below = component.flatMap((member) => (deps.get(member) ?? []).filter((to) => !component.includes(to)));
|
|
3070
|
+
const layer = Math.max(-1, ...below.map(layerOf)) + 1;
|
|
3071
|
+
layers.set(component, layer);
|
|
3072
|
+
return layer;
|
|
3073
|
+
};
|
|
3074
|
+
const cycles = components.filter((component) => component.length > 1).map((component) => [...component].sort(byText)).sort((a, b) => byText(a[0], b[0]));
|
|
3075
|
+
return { layerOf, cycles };
|
|
3076
|
+
}
|
|
3077
|
+
static #stronglyConnected(deps) {
|
|
3078
|
+
const ids = [...new Set([...deps.keys(), ...[...deps.values()].flat()])].sort(byText);
|
|
3079
|
+
const index = new Map;
|
|
3080
|
+
const low = new Map;
|
|
3081
|
+
const stack = [];
|
|
3082
|
+
const onStack = new Set;
|
|
3083
|
+
const components = [];
|
|
3084
|
+
const visit = (id) => {
|
|
3085
|
+
index.set(id, index.size);
|
|
3086
|
+
low.set(id, index.get(id) ?? 0);
|
|
3087
|
+
stack.push(id);
|
|
3088
|
+
onStack.add(id);
|
|
3089
|
+
for (const to of deps.get(id) ?? []) {
|
|
3090
|
+
if (!index.has(to)) {
|
|
3091
|
+
visit(to);
|
|
3092
|
+
low.set(id, Math.min(low.get(id) ?? 0, low.get(to) ?? 0));
|
|
3093
|
+
} else if (onStack.has(to))
|
|
3094
|
+
low.set(id, Math.min(low.get(id) ?? 0, index.get(to) ?? 0));
|
|
3095
|
+
}
|
|
3096
|
+
if (low.get(id) !== index.get(id))
|
|
3097
|
+
return;
|
|
3098
|
+
const component = [];
|
|
3099
|
+
for (let member = stack.pop();member !== undefined; member = stack.pop()) {
|
|
3100
|
+
onStack.delete(member);
|
|
3101
|
+
component.push(member);
|
|
3102
|
+
if (member === id)
|
|
3103
|
+
break;
|
|
3104
|
+
}
|
|
3105
|
+
components.push(component);
|
|
3106
|
+
};
|
|
3107
|
+
for (const id of ids)
|
|
3108
|
+
if (!index.has(id))
|
|
3109
|
+
visit(id);
|
|
3110
|
+
return components;
|
|
3111
|
+
}
|
|
3112
|
+
#packageOf(file) {
|
|
3113
|
+
return this.#source.packages.filter(({ root }) => file.startsWith(`${root}/`)).sort((a, b) => b.root.length - a.root.length)[0];
|
|
3114
|
+
}
|
|
3115
|
+
#facetOf(file) {
|
|
3116
|
+
const pkg = this.#packageOf(file);
|
|
3117
|
+
if (!pkg)
|
|
3118
|
+
return null;
|
|
3119
|
+
if (!pkg.split)
|
|
3120
|
+
return pkg.name;
|
|
3121
|
+
const rest = file.slice(pkg.root.length + 1);
|
|
3122
|
+
const slash = rest.indexOf("/");
|
|
3123
|
+
return slash < 0 ? null : `${pkg.name}/${rest.slice(0, slash)}`;
|
|
3124
|
+
}
|
|
3125
|
+
#location(source, node) {
|
|
3126
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
3127
|
+
return { file: source.fileName, line: line + 1, column: character + 1 };
|
|
3128
|
+
}
|
|
3129
|
+
#classesOf(source) {
|
|
3130
|
+
const { ts } = this.#source;
|
|
3131
|
+
return source.statements.flatMap((statement) => ts.isClassDeclaration(statement) && statement.name ? [{ name: statement.name.text, members: statement.members.length, at: this.#location(source, statement.name) }] : []);
|
|
3132
|
+
}
|
|
3133
|
+
#importsOf(file, source) {
|
|
3134
|
+
const { ts } = this.#source;
|
|
3135
|
+
const sites = [];
|
|
3136
|
+
const push = (specifier, kind, node) => {
|
|
3137
|
+
const target = this.#resolve(file, specifier);
|
|
3138
|
+
const facet = target ? this.#facetOf(target) : null;
|
|
3139
|
+
if (facet)
|
|
3140
|
+
sites.push({ facet, kind, at: this.#location(source, node) });
|
|
3141
|
+
};
|
|
3142
|
+
const visit = (node) => {
|
|
3143
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
3144
|
+
const clause = node.importClause;
|
|
3145
|
+
const named = clause?.namedBindings;
|
|
3146
|
+
const isTypeOnly = !!clause?.isTypeOnly || !!clause && !clause.name && !!named && ts.isNamedImports(named) && named.elements.length > 0 && named.elements.every((element) => element.isTypeOnly);
|
|
3147
|
+
push(node.moduleSpecifier.text, isTypeOnly ? "type" : "value", node);
|
|
3148
|
+
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
3149
|
+
const clause = node.exportClause;
|
|
3150
|
+
const isTypeOnly = node.isTypeOnly || !!clause && ts.isNamedExports(clause) && clause.elements.length > 0 && clause.elements.every((element) => element.isTypeOnly);
|
|
3151
|
+
push(node.moduleSpecifier.text, isTypeOnly ? "type" : "value", node);
|
|
3152
|
+
} else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0]))
|
|
3153
|
+
push(node.arguments[0].text, "dynamic", node);
|
|
3154
|
+
ts.forEachChild(node, visit);
|
|
3155
|
+
};
|
|
3156
|
+
visit(source);
|
|
3157
|
+
return sites;
|
|
3158
|
+
}
|
|
3159
|
+
#resolve(file, specifier) {
|
|
3160
|
+
const pkg = specifier.startsWith(".") ? null : this.#source.packages.filter(({ name }) => specifier === name || specifier.startsWith(`${name}/`)).sort((a, b) => b.name.length - a.name.length)[0];
|
|
3161
|
+
if (!specifier.startsWith(".") && !pkg)
|
|
3162
|
+
return null;
|
|
3163
|
+
const base = pkg ? `${pkg.root}${specifier.slice(pkg.name.length)}` : FrameworkMapBuilder.#normalize(`${file.slice(0, file.lastIndexOf("/"))}/${specifier}`);
|
|
3164
|
+
return extensions.map((extension) => `${base}${extension}`).find((candidate) => this.#fileSet.has(candidate)) ?? null;
|
|
3165
|
+
}
|
|
3166
|
+
static #normalize(path) {
|
|
3167
|
+
const parts = [];
|
|
3168
|
+
for (const part of path.split("/"))
|
|
3169
|
+
if (part === "..")
|
|
3170
|
+
parts.pop();
|
|
3171
|
+
else if (part !== "." && part !== "")
|
|
3172
|
+
parts.push(part);
|
|
3173
|
+
return parts.join("/");
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
// pkgs/@akanjs/language/frameworkMapDoc.ts
|
|
3177
|
+
var startMarker = "<!-- akan:framework-map:start -->";
|
|
3178
|
+
var endMarker = "<!-- akan:framework-map:end -->";
|
|
3179
|
+
var citedPattern = /`([A-Z][a-z][A-Za-z0-9]*)(?:[.#][\w$#]+)?(?:\(\))?`/g;
|
|
3180
|
+
|
|
3181
|
+
class FrameworkMapDoc {
|
|
3182
|
+
static regenerate = "bun run akan sync-architecture";
|
|
3183
|
+
static render(map, docDir) {
|
|
3184
|
+
const nameOf = new Map(map.facets.map((facet) => [facet.id, facet.name]));
|
|
3185
|
+
const facets = [...map.facets].sort((a, b) => a.layer - b.layer || (a.id < b.id ? -1 : 1));
|
|
3186
|
+
const list = (names) => names.length ? names.join(", ") : "—";
|
|
3187
|
+
const importsOf = (facet, kind) => map.imports.filter(({ from, value, type, dynamic }) => from === facet.id && (kind === "value" ? value + dynamic > 0 : type > 0 && value + dynamic === 0)).map(({ to, value }) => `\`${nameOf.get(to) ?? to}\`${kind === "value" && value === 0 ? " (lazy)" : ""}`);
|
|
3188
|
+
const lines = [
|
|
3189
|
+
startMarker,
|
|
3190
|
+
`<!-- Generated from the framework source by \`${FrameworkMapDoc.regenerate}\`. Edit outside the markers. -->`,
|
|
3191
|
+
"<!-- Outside the markers, every backticked PascalCase name must be a framework class or channel. -->",
|
|
3192
|
+
"",
|
|
3193
|
+
"## Facet layers",
|
|
3194
|
+
"",
|
|
3195
|
+
"A facet loads values only from facets in lower layers. A type-only import loads nothing, so it may point anywhere.",
|
|
3196
|
+
"",
|
|
3197
|
+
"| Layer | Facet | Loads values from | Imports only types from |",
|
|
3198
|
+
"| --- | --- | --- | --- |",
|
|
3199
|
+
...facets.map((facet) => `| ${facet.layer} | \`${facet.name}\` | ${list(importsOf(facet, "value"))} | ${list(importsOf(facet, "type"))} |`),
|
|
3200
|
+
...map.cycles.length ? [
|
|
3201
|
+
"",
|
|
3202
|
+
`Facets that load values from each other: ${map.cycles.map((cycle) => cycle.map((id) => `\`${nameOf.get(id) ?? id}\``).join(" ⇄ ")).join("; ")}.`
|
|
3203
|
+
] : [],
|
|
3204
|
+
"",
|
|
3205
|
+
"## Process messages",
|
|
3206
|
+
"",
|
|
3207
|
+
"Each channel is a union of messages told apart by `type`. A sender writes the message's `type` literal; a handler",
|
|
3208
|
+
"branches on it or relays it. Both are named by the class or top-level function they sit in. A dash means no site",
|
|
3209
|
+
"was recognised.",
|
|
3210
|
+
...map.channels.flatMap((channel) => {
|
|
3211
|
+
const declared = FrameworkMapDoc.#relative(docDir, channel.at.file);
|
|
3212
|
+
return [
|
|
3213
|
+
"",
|
|
3214
|
+
`### \`${channel.name}\`: ${channel.label}`,
|
|
3215
|
+
"",
|
|
3216
|
+
`Declared in [${declared}](${declared}).`,
|
|
3217
|
+
"",
|
|
3218
|
+
"| Message | Sent by | Handled by |",
|
|
3219
|
+
"| --- | --- | --- |",
|
|
3220
|
+
...channel.messages.map(({ type, senders, handlers }) => `| \`${type}\` | ${FrameworkMapDoc.#owners(senders)} | ${FrameworkMapDoc.#owners(handlers)} |`)
|
|
3221
|
+
];
|
|
3222
|
+
}),
|
|
3223
|
+
...map.missing.length ? ["", `Channels not found: ${map.missing.join("; ")}.`] : [],
|
|
3224
|
+
endMarker
|
|
3225
|
+
];
|
|
3226
|
+
return lines.join(`
|
|
3227
|
+
`);
|
|
3228
|
+
}
|
|
3229
|
+
static generatedOf(markdown) {
|
|
3230
|
+
const start = markdown.indexOf(startMarker);
|
|
3231
|
+
const end = markdown.indexOf(endMarker, start);
|
|
3232
|
+
return start < 0 || end < 0 ? null : markdown.slice(start, end + endMarker.length);
|
|
3233
|
+
}
|
|
3234
|
+
static splice(markdown, generated) {
|
|
3235
|
+
const current = FrameworkMapDoc.generatedOf(markdown);
|
|
3236
|
+
if (current !== null)
|
|
3237
|
+
return markdown.replace(current, () => generated);
|
|
3238
|
+
return `${markdown.trimEnd()}
|
|
3239
|
+
|
|
3240
|
+
${generated}
|
|
3241
|
+
`;
|
|
3242
|
+
}
|
|
3243
|
+
static citedClasses(markdown) {
|
|
3244
|
+
const current = FrameworkMapDoc.generatedOf(markdown);
|
|
3245
|
+
const handKept = current === null ? markdown : markdown.replace(current, "");
|
|
3246
|
+
return [...new Set([...handKept.matchAll(citedPattern)].map(([, name]) => name))].sort();
|
|
3247
|
+
}
|
|
3248
|
+
static unknownClasses(markdown, map) {
|
|
3249
|
+
const known = new Set([
|
|
3250
|
+
...map.facets.flatMap(({ classes }) => classes.map(({ name }) => name)),
|
|
3251
|
+
...map.channels.map(({ name }) => name)
|
|
3252
|
+
]);
|
|
3253
|
+
return FrameworkMapDoc.citedClasses(markdown).filter((name) => !known.has(name));
|
|
3254
|
+
}
|
|
3255
|
+
static #owners(sites) {
|
|
3256
|
+
return sites.length ? sites.map(({ owner }) => `\`${owner}\``).join(", ") : "—";
|
|
3257
|
+
}
|
|
3258
|
+
static #relative(fromDir, file) {
|
|
3259
|
+
const from = fromDir.split("/").filter(Boolean);
|
|
3260
|
+
const to = file.split("/").filter(Boolean);
|
|
3261
|
+
let shared = 0;
|
|
3262
|
+
while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared])
|
|
3263
|
+
shared += 1;
|
|
3264
|
+
return [...from.slice(shared).map(() => ".."), ...to.slice(shared)].join("/");
|
|
3265
|
+
}
|
|
3266
|
+
}
|