@akanjs/language 3.1.2-rc.2 → 3.1.2-rc.4
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 +21 -0
- package/akanGraphDiff.d.ts +10 -0
- package/akanIndex.d.ts +4 -0
- package/index.cjs +504 -16
- package/index.d.ts +3 -1
- package/index.js +504 -16
- package/moduleFacts.d.ts +26 -5
- package/package.json +4 -1
- package/server.js +1890 -0
package/index.js
CHANGED
|
@@ -1,3 +1,422 @@
|
|
|
1
|
+
// pkgs/@akanjs/language/akanGraphDiff.ts
|
|
2
|
+
var edgeKeyOf = ({ from, to, kind, label }) => `${from}
|
|
3
|
+
${to}
|
|
4
|
+
${kind}
|
|
5
|
+
${label ?? ""}`;
|
|
6
|
+
var shapeOf = ({ type, array, optional, declaredIn }) => `${type ?? ""}${array ? "[]" : ""}${optional ? "?" : ""} ${declaredIn}`;
|
|
7
|
+
var isEdited = (before, after) => before.digest !== undefined && after.digest !== undefined && before.digest !== after.digest;
|
|
8
|
+
var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
9
|
+
|
|
10
|
+
class AkanGraphDiff {
|
|
11
|
+
static between(base, head) {
|
|
12
|
+
const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
|
|
13
|
+
const headNodes = new Map(head.nodes.map((node) => [node.id, node]));
|
|
14
|
+
const marked = new Map;
|
|
15
|
+
for (const node of head.nodes) {
|
|
16
|
+
const before = baseNodes.get(node.id);
|
|
17
|
+
const next = before ? AkanGraphDiff.#compare(before, node) : AkanGraphDiff.#mark(node, "added");
|
|
18
|
+
if (next)
|
|
19
|
+
marked.set(node.id, next);
|
|
20
|
+
}
|
|
21
|
+
for (const node of base.nodes)
|
|
22
|
+
if (!headNodes.has(node.id))
|
|
23
|
+
marked.set(node.id, AkanGraphDiff.#mark(node, "removed"));
|
|
24
|
+
const baseKeys = new Set(base.edges.map(edgeKeyOf));
|
|
25
|
+
const headKeys = new Set(head.edges.map(edgeKeyOf));
|
|
26
|
+
const edges = [
|
|
27
|
+
...head.edges.flatMap((edge) => !baseKeys.has(edgeKeyOf(edge)) ? [{ ...edge, change: "added" }] : marked.has(edge.from) || marked.has(edge.to) ? [edge] : []),
|
|
28
|
+
...base.edges.filter((edge) => !headKeys.has(edgeKeyOf(edge))).map((edge) => ({ ...edge, change: "removed" }))
|
|
29
|
+
];
|
|
30
|
+
const context = edges.flatMap((edge) => [edge.from, edge.to]).filter((id) => !marked.has(id)).flatMap((id) => {
|
|
31
|
+
const node = headNodes.get(id) ?? baseNodes.get(id);
|
|
32
|
+
return node ? [node] : [];
|
|
33
|
+
});
|
|
34
|
+
const nodes = [...new Map([...marked.values(), ...context].map((node) => [node.id, node])).values()].map(AkanGraphDiff.#withoutDigest).sort(byId);
|
|
35
|
+
const summary = { added: 0, removed: 0, changed: 0 };
|
|
36
|
+
for (const node of marked.values())
|
|
37
|
+
if (node.change)
|
|
38
|
+
summary[node.change] += 1;
|
|
39
|
+
return { version: 1, nodes, edges, summary };
|
|
40
|
+
}
|
|
41
|
+
static #mark(node, change) {
|
|
42
|
+
return node.fields ? { ...node, change, fields: node.fields.map((field) => ({ ...field, change })) } : { ...node, change };
|
|
43
|
+
}
|
|
44
|
+
static #compare(before, after) {
|
|
45
|
+
const fields = AkanGraphDiff.#fieldsBetween(before.fields, after.fields);
|
|
46
|
+
const isChanged = isEdited(before, after) || before.label !== after.label || before.detail.join(`
|
|
47
|
+
`) !== after.detail.join(`
|
|
48
|
+
`) || (before.values ?? []).join(`
|
|
49
|
+
`) !== (after.values ?? []).join(`
|
|
50
|
+
`) || !!fields?.some((field) => field.change);
|
|
51
|
+
if (!isChanged)
|
|
52
|
+
return null;
|
|
53
|
+
return fields ? { ...after, change: "changed", fields } : { ...after, change: "changed" };
|
|
54
|
+
}
|
|
55
|
+
static #fieldsBetween(before, after) {
|
|
56
|
+
if (!before && !after)
|
|
57
|
+
return;
|
|
58
|
+
const previous = new Map((before ?? []).map((field) => [field.name, field]));
|
|
59
|
+
const names = new Set((after ?? []).map((field) => field.name));
|
|
60
|
+
return [
|
|
61
|
+
...(after ?? []).map((field) => {
|
|
62
|
+
const old = previous.get(field.name);
|
|
63
|
+
if (!old)
|
|
64
|
+
return { ...field, change: "added" };
|
|
65
|
+
return shapeOf(old) === shapeOf(field) && !isEdited(old, field) ? field : { ...field, change: "changed" };
|
|
66
|
+
}),
|
|
67
|
+
...(before ?? []).filter((field) => !names.has(field.name)).map((field) => ({ ...field, change: "removed" }))
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
static #withoutDigest({ digest: _digest, ...node }) {
|
|
71
|
+
return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// pkgs/@akanjs/language/akanGraph.ts
|
|
75
|
+
var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
76
|
+
var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
77
|
+
var memberSourcePattern = /\.(signal|service|store)\.ts$/;
|
|
78
|
+
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)$/;
|
|
79
|
+
var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
|
|
80
|
+
var moduleDirPattern = /\/lib\/([^/]+)\//;
|
|
81
|
+
var typeKinds = new Set(["model", "scalar", "enum"]);
|
|
82
|
+
var clientRoots = new Set(["fetch", "st", "sig"]);
|
|
83
|
+
|
|
84
|
+
class GraphBuilder {
|
|
85
|
+
#source;
|
|
86
|
+
#nodes = new Map;
|
|
87
|
+
#edges = new Map;
|
|
88
|
+
#contexts = new Map;
|
|
89
|
+
#members = new Map;
|
|
90
|
+
constructor(source) {
|
|
91
|
+
this.#source = source;
|
|
92
|
+
}
|
|
93
|
+
build() {
|
|
94
|
+
const { facts } = this.#source;
|
|
95
|
+
const constantsByOwner = new Map;
|
|
96
|
+
for (const file of facts)
|
|
97
|
+
constantsByOwner.set(file.owner, [...constantsByOwner.get(file.owner) ?? [], ...file.constants]);
|
|
98
|
+
const scopeOf = (owner) => [
|
|
99
|
+
...constantsByOwner.get(owner) ?? [],
|
|
100
|
+
...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
|
|
101
|
+
];
|
|
102
|
+
const typed = [];
|
|
103
|
+
for (const file of facts) {
|
|
104
|
+
const { owner } = file;
|
|
105
|
+
const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
|
|
106
|
+
const scalarClass = scalar ? file.constants.find((constant) => constant.name === capitalize(scalar)) : undefined;
|
|
107
|
+
if (scalar && scalarClass) {
|
|
108
|
+
const id = this.#key("scalar", owner, scalar);
|
|
109
|
+
const node = this.#addNode(id, "scalar", owner, scalar, scalarClass.name, scalarClass.span);
|
|
110
|
+
node.fields = this.#fieldsOf(scalarClass, scopeOf(owner));
|
|
111
|
+
typed.push(node);
|
|
112
|
+
}
|
|
113
|
+
for (const item of file.enums) {
|
|
114
|
+
const id = this.#key("enum", owner, item.name);
|
|
115
|
+
this.#addNode(id, "enum", owner, this.#moduleDirOf(file.fileName), item.name, item.span).values = item.values;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const file of facts) {
|
|
119
|
+
const { owner } = file;
|
|
120
|
+
for (const service of file.services) {
|
|
121
|
+
const refName = service.model;
|
|
122
|
+
if (!refName)
|
|
123
|
+
continue;
|
|
124
|
+
const modelId = this.#key("model", owner, refName);
|
|
125
|
+
const modelClass = constantsByOwner.get(owner)?.find((constant) => constant.name === capitalize(refName));
|
|
126
|
+
if (modelClass && !this.#nodes.has(modelId)) {
|
|
127
|
+
const node = this.#addNode(modelId, "model", owner, refName, modelClass.name, modelClass.span);
|
|
128
|
+
node.fields = this.#fieldsOf(modelClass, scopeOf(owner));
|
|
129
|
+
typed.push(node);
|
|
130
|
+
}
|
|
131
|
+
for (const method of service.methods)
|
|
132
|
+
this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
|
|
133
|
+
}
|
|
134
|
+
for (const filter of file.filters) {
|
|
135
|
+
if (!filter.name.endsWith("Filter"))
|
|
136
|
+
continue;
|
|
137
|
+
const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
|
|
138
|
+
this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
|
|
139
|
+
for (const query of filter.queries)
|
|
140
|
+
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);
|
|
141
|
+
}
|
|
142
|
+
for (const slice of file.slices) {
|
|
143
|
+
const refName = slice.model;
|
|
144
|
+
if (!refName)
|
|
145
|
+
continue;
|
|
146
|
+
const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
|
|
147
|
+
for (const guard of slice.guards)
|
|
148
|
+
this.#guard(fallback, guard);
|
|
149
|
+
for (const key of slice.keys) {
|
|
150
|
+
const detail = [...key.guards, ...key.live ? ["live"] : []];
|
|
151
|
+
const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
152
|
+
for (const guard of key.guards)
|
|
153
|
+
this.#guard(node, guard);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const endpoint of file.endpoints) {
|
|
157
|
+
const refName = endpoint.model;
|
|
158
|
+
if (!refName)
|
|
159
|
+
continue;
|
|
160
|
+
for (const key of endpoint.keys) {
|
|
161
|
+
const detail = [...key.verb ? [key.verb] : [], ...key.guards];
|
|
162
|
+
const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
163
|
+
for (const guard of key.guards)
|
|
164
|
+
this.#guard(node, guard);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const store of file.stores)
|
|
168
|
+
if (store.model)
|
|
169
|
+
for (const action of store.actions)
|
|
170
|
+
this.#addMember("action", owner, store.model, action, `${store.name}.${action.name}`, file.fileName);
|
|
171
|
+
}
|
|
172
|
+
const types = new Map;
|
|
173
|
+
for (const node of this.#nodes.values())
|
|
174
|
+
if (typeKinds.has(node.kind))
|
|
175
|
+
types.set(node.label, [...types.get(node.label) ?? [], node]);
|
|
176
|
+
for (const node of typed)
|
|
177
|
+
for (const field of node.fields ?? []) {
|
|
178
|
+
const candidates = (field.type ? types.get(field.type) ?? [] : []).filter((type) => this.#visible(type.owner, node.owner));
|
|
179
|
+
field.ref = (candidates.find((type) => type.owner === node.owner) ?? candidates[0])?.id ?? null;
|
|
180
|
+
if (field.ref)
|
|
181
|
+
this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
|
|
182
|
+
}
|
|
183
|
+
for (const [fileName, words] of this.#source.files)
|
|
184
|
+
this.#walk(fileName, words);
|
|
185
|
+
return {
|
|
186
|
+
version: 1,
|
|
187
|
+
nodes: [...this.#nodes.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
188
|
+
edges: [...this.#edges.values()]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
#key(kind, owner, scope, name = "") {
|
|
192
|
+
return `${kind}:${owner ?? ""}:${scope}${name ? `.${name}` : ""}`;
|
|
193
|
+
}
|
|
194
|
+
#location(span) {
|
|
195
|
+
return { file: this.#source.relative(span.fileName), line: span.line + 1, column: span.character + 1 };
|
|
196
|
+
}
|
|
197
|
+
#addNode(id, kind, owner, module, name, span, detail = [], label = (kind === "query" || kind === "slice") && module ? `${module}${name ? `.${name}` : " (default)"}` : name) {
|
|
198
|
+
const existing = this.#nodes.get(id);
|
|
199
|
+
if (existing)
|
|
200
|
+
return existing;
|
|
201
|
+
const node = {
|
|
202
|
+
id,
|
|
203
|
+
kind,
|
|
204
|
+
name,
|
|
205
|
+
label,
|
|
206
|
+
owner,
|
|
207
|
+
module,
|
|
208
|
+
detail,
|
|
209
|
+
at: span ? this.#location(span) : null
|
|
210
|
+
};
|
|
211
|
+
this.#nodes.set(id, node);
|
|
212
|
+
return node;
|
|
213
|
+
}
|
|
214
|
+
#addMember(kind, owner, refName, member, label, fileName, detail = []) {
|
|
215
|
+
const id = this.#key(kind, owner, refName, member.name);
|
|
216
|
+
const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
|
|
217
|
+
this.#digest(node, this.#source.readText(fileName), member.extent);
|
|
218
|
+
this.#contexts.set(fileName, [
|
|
219
|
+
...this.#contexts.get(fileName) ?? [],
|
|
220
|
+
{ id, kind, owner, module: refName, extent: member.extent }
|
|
221
|
+
]);
|
|
222
|
+
const byName = `${kind}:${member.name}`;
|
|
223
|
+
this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
226
|
+
#hashOf(text, { start, end }) {
|
|
227
|
+
if (!this.#source.digests || text === undefined)
|
|
228
|
+
return null;
|
|
229
|
+
let hash = 2166136261;
|
|
230
|
+
for (let idx = start;idx < end; idx++) {
|
|
231
|
+
const code = text.charCodeAt(idx);
|
|
232
|
+
if (code !== 13)
|
|
233
|
+
hash = Math.imul(hash ^ code, 16777619);
|
|
234
|
+
}
|
|
235
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
236
|
+
}
|
|
237
|
+
#digest(node, text, extent) {
|
|
238
|
+
const digest = node.digest ? null : this.#hashOf(text, extent);
|
|
239
|
+
if (digest)
|
|
240
|
+
node.digest = digest;
|
|
241
|
+
}
|
|
242
|
+
#addEdge(from, to, kind, label, at) {
|
|
243
|
+
if (from === to)
|
|
244
|
+
return;
|
|
245
|
+
const key = `${from}
|
|
246
|
+
${to}
|
|
247
|
+
${kind}
|
|
248
|
+
${label ?? ""}`;
|
|
249
|
+
const existing = this.#edges.get(key);
|
|
250
|
+
if (existing)
|
|
251
|
+
existing.count += 1;
|
|
252
|
+
else
|
|
253
|
+
this.#edges.set(key, { from, to, kind, label, count: 1, at });
|
|
254
|
+
}
|
|
255
|
+
#guard(node, guard) {
|
|
256
|
+
const [role, name] = guard.includes(" ") ? guard.split(" ") : [null, guard];
|
|
257
|
+
if (!name || name === "?")
|
|
258
|
+
return;
|
|
259
|
+
const id = `guard:${name}`;
|
|
260
|
+
if (!this.#nodes.has(id))
|
|
261
|
+
this.#nodes.set(id, { id, kind: "guard", name, label: name, owner: null, module: null, detail: [], at: null });
|
|
262
|
+
this.#addEdge(node.id, id, "guard", role, node.at);
|
|
263
|
+
}
|
|
264
|
+
#moduleDirOf(fileName) {
|
|
265
|
+
const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
|
|
266
|
+
return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
|
|
267
|
+
}
|
|
268
|
+
#fieldsOf(constant, scope) {
|
|
269
|
+
const fields = new Map;
|
|
270
|
+
const seen = new Set;
|
|
271
|
+
const visit = (current) => {
|
|
272
|
+
if (seen.has(current))
|
|
273
|
+
return;
|
|
274
|
+
seen.add(current);
|
|
275
|
+
for (const parent of current.parents) {
|
|
276
|
+
const parentClass = scope.find((other) => other.name === parent && other.span.fileName === current.span.fileName) ?? scope.find((other) => other.name === parent);
|
|
277
|
+
if (parentClass)
|
|
278
|
+
visit(parentClass);
|
|
279
|
+
}
|
|
280
|
+
for (const field of current.fields) {
|
|
281
|
+
if (fields.has(field.name))
|
|
282
|
+
continue;
|
|
283
|
+
const digest = this.#hashOf(this.#source.readText(field.span.fileName), field.extent);
|
|
284
|
+
fields.set(field.name, {
|
|
285
|
+
name: field.name,
|
|
286
|
+
type: field.type,
|
|
287
|
+
array: field.array,
|
|
288
|
+
optional: field.optional,
|
|
289
|
+
ref: null,
|
|
290
|
+
declaredIn: current.name,
|
|
291
|
+
at: this.#location(field.span),
|
|
292
|
+
...digest ? { digest } : {}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
visit(constant);
|
|
297
|
+
return [...fields.values()];
|
|
298
|
+
}
|
|
299
|
+
#visible(target, from) {
|
|
300
|
+
return !from || !target || target === from || target.startsWith("libs/");
|
|
301
|
+
}
|
|
302
|
+
#targetNodeOf({ family, owner, refName, sourceKey }) {
|
|
303
|
+
const isDefault = /^[A-Z]/.test(sourceKey);
|
|
304
|
+
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;
|
|
305
|
+
return id && this.#nodes.has(id) ? id : null;
|
|
306
|
+
}
|
|
307
|
+
#membersNamed(kind, name, owner, refName) {
|
|
308
|
+
const nodes = (this.#members.get(`${kind}:${name}`) ?? []).filter((node) => (!refName || node.module === refName) && this.#visible(node.owner, owner));
|
|
309
|
+
const own = nodes.filter((node) => node.owner === owner);
|
|
310
|
+
return (own.length ? own : nodes).map((node) => node.id);
|
|
311
|
+
}
|
|
312
|
+
#plainTargetsOf(chain, name, context, owner) {
|
|
313
|
+
const [head, second] = chain ?? [];
|
|
314
|
+
if (chain?.length === 1 && head === "fetch")
|
|
315
|
+
return this.#membersNamed("endpoint", name, owner);
|
|
316
|
+
if (chain?.length === 2 && head === "st" && second === "do")
|
|
317
|
+
return this.#membersNamed("action", name, owner);
|
|
318
|
+
if (chain?.length === 1 && head === "this" && (context.kind === "method" || context.kind === "action"))
|
|
319
|
+
return this.#membersNamed(context.kind, name, context.owner, context.module).filter((id) => id.startsWith(`${context.kind}:${context.owner ?? ""}:`));
|
|
320
|
+
if (chain?.length === 2 && head === "this" && second && /.Service$/.test(second))
|
|
321
|
+
return this.#membersNamed("method", name, owner, second.slice(0, -"Service".length));
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
#walk(fileName, words) {
|
|
325
|
+
const relative = this.#source.relative(fileName);
|
|
326
|
+
const contexts = this.#contexts.get(fileName);
|
|
327
|
+
const isComponentFile = !memberSourcePattern.test(fileName) && !skippedPattern.test(relative) && (words.includes(`
|
|
328
|
+
fetch
|
|
329
|
+
`) || words.includes(`
|
|
330
|
+
st
|
|
331
|
+
`));
|
|
332
|
+
if (!contexts && !isComponentFile)
|
|
333
|
+
return;
|
|
334
|
+
const text = this.#source.readText(fileName);
|
|
335
|
+
if (text === undefined)
|
|
336
|
+
return;
|
|
337
|
+
const { ts } = this.#source;
|
|
338
|
+
const owner = this.#source.ownerOf(fileName);
|
|
339
|
+
const source = this.#source.parse(fileName, text);
|
|
340
|
+
const visit = (node) => {
|
|
341
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
|
342
|
+
const start = node.name.getStart(source);
|
|
343
|
+
const chain = this.#chainOf(node.expression);
|
|
344
|
+
const context = contexts ? contexts.find(({ extent }) => extent.start <= start && start < extent.end) : chain && clientRoots.has(chain[0]) ? this.#componentOf(source, node) : undefined;
|
|
345
|
+
if (context)
|
|
346
|
+
this.#link(fileName, source, node.name, chain, context, owner);
|
|
347
|
+
}
|
|
348
|
+
ts.forEachChild(node, visit);
|
|
349
|
+
};
|
|
350
|
+
visit(source);
|
|
351
|
+
}
|
|
352
|
+
#link(fileName, source, identifier, chain, context, owner) {
|
|
353
|
+
const resolved = this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
|
|
354
|
+
const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
|
|
355
|
+
if (!targets.length)
|
|
356
|
+
return;
|
|
357
|
+
if (context.pending && !this.#nodes.has(context.id)) {
|
|
358
|
+
this.#nodes.set(context.id, context.pending);
|
|
359
|
+
this.#digest(context.pending, source.text, context.extent);
|
|
360
|
+
}
|
|
361
|
+
const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
|
|
362
|
+
const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
|
|
363
|
+
const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
|
|
364
|
+
const at = { file: this.#source.relative(fileName), line: position.line + 1, column: position.character + 1 };
|
|
365
|
+
for (const target of new Set(own.length ? own : targets))
|
|
366
|
+
this.#addEdge(context.id, target, kind, identifier.text, at);
|
|
367
|
+
}
|
|
368
|
+
#chainOf(expression) {
|
|
369
|
+
const { ts } = this.#source;
|
|
370
|
+
if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
371
|
+
return this.#chainOf(expression.expression);
|
|
372
|
+
if (ts.isIdentifier(expression))
|
|
373
|
+
return [expression.text];
|
|
374
|
+
if (expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
375
|
+
return ["this"];
|
|
376
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
377
|
+
return null;
|
|
378
|
+
const head = this.#chainOf(expression.expression);
|
|
379
|
+
return head ? [...head, expression.name.text] : null;
|
|
380
|
+
}
|
|
381
|
+
#componentOf(source, node) {
|
|
382
|
+
const { ts } = this.#source;
|
|
383
|
+
let top = node;
|
|
384
|
+
while (top.parent && !ts.isSourceFile(top.parent))
|
|
385
|
+
top = top.parent;
|
|
386
|
+
const declared = ts.isVariableStatement(top) ? top.declarationList.declarations[0]?.name : ts.isFunctionDeclaration(top) || ts.isClassDeclaration(top) ? top.name : undefined;
|
|
387
|
+
const isDefaultExport = ts.canHaveModifiers(top) && !!ts.getModifiers(top)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword);
|
|
388
|
+
const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
|
|
389
|
+
const relative = this.#source.relative(source.fileName);
|
|
390
|
+
const owner = this.#source.ownerOf(source.fileName);
|
|
391
|
+
const id = `component:${relative}#${name}`;
|
|
392
|
+
const context = {
|
|
393
|
+
id,
|
|
394
|
+
kind: "component",
|
|
395
|
+
owner,
|
|
396
|
+
module: null,
|
|
397
|
+
extent: { start: top.getStart(source), end: top.getEnd() }
|
|
398
|
+
};
|
|
399
|
+
if (this.#nodes.has(id))
|
|
400
|
+
return context;
|
|
401
|
+
const pagePrefix = owner ? `${owner}/page/` : null;
|
|
402
|
+
const route = pagePrefix && relative.startsWith(pagePrefix) ? relative.slice(pagePrefix.length).replace(/\.tsx?$/, "") : null;
|
|
403
|
+
const base = relative.slice(relative.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
|
|
404
|
+
const label = route ? `${route}${name === "default" ? "" : `#${name}`}` : name === "default" ? base : `${base}.${name}`;
|
|
405
|
+
const position = source.getLineAndCharacterOfPosition((declared ?? top).getStart(source));
|
|
406
|
+
context.pending = {
|
|
407
|
+
id,
|
|
408
|
+
kind: "component",
|
|
409
|
+
name,
|
|
410
|
+
label,
|
|
411
|
+
owner,
|
|
412
|
+
module: this.#moduleDirOf(source.fileName),
|
|
413
|
+
detail: route ? ["page"] : [],
|
|
414
|
+
at: { file: relative, line: position.line + 1, column: position.character + 1 }
|
|
415
|
+
};
|
|
416
|
+
return context;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
1
420
|
// pkgs/@akanjs/language/ast.ts
|
|
2
421
|
var createAst = (ts) => {
|
|
3
422
|
const sourceFileFor = (fileName, content, scriptKind = ts.ScriptKind.TS) => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
@@ -64,7 +483,7 @@ var createAst = (ts) => {
|
|
|
64
483
|
};
|
|
65
484
|
|
|
66
485
|
// pkgs/@akanjs/language/moduleFacts.ts
|
|
67
|
-
var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store"]);
|
|
486
|
+
var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store", "enumOf"]);
|
|
68
487
|
var argMethods = new Set(["param", "body", "search", "room", "msg"]);
|
|
69
488
|
var ownerOf = (relativePath) => {
|
|
70
489
|
const [root, name] = relativePath.split("/");
|
|
@@ -86,6 +505,10 @@ var createFactReader = (ts, ast) => {
|
|
|
86
505
|
endCharacter: tail.character
|
|
87
506
|
};
|
|
88
507
|
};
|
|
508
|
+
const extentOf = (source, node) => ({
|
|
509
|
+
start: node.getStart(source),
|
|
510
|
+
end: node.getEnd()
|
|
511
|
+
});
|
|
89
512
|
const unwrap = (expression) => {
|
|
90
513
|
let current = expression;
|
|
91
514
|
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
|
|
@@ -129,7 +552,7 @@ var createFactReader = (ts, ast) => {
|
|
|
129
552
|
const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
130
553
|
const name = ast.propertyName(property);
|
|
131
554
|
const node = keyName(property);
|
|
132
|
-
return name && node ? [{ name, span: spanOf(source, node) }] : [];
|
|
555
|
+
return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
|
|
133
556
|
});
|
|
134
557
|
const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
135
558
|
if (!ts.isPropertyAssignment(property))
|
|
@@ -142,6 +565,7 @@ var createFactReader = (ts, ast) => {
|
|
|
142
565
|
{
|
|
143
566
|
name,
|
|
144
567
|
span: spanOf(source, property.name),
|
|
568
|
+
extent: extentOf(source, property),
|
|
145
569
|
guards: root ? guardsIn(optionObjectOf(root)) : [],
|
|
146
570
|
live: methods.includes("live"),
|
|
147
571
|
argCount: methods.filter((method) => argMethods.has(method)).length
|
|
@@ -178,6 +602,7 @@ var createFactReader = (ts, ast) => {
|
|
|
178
602
|
{
|
|
179
603
|
name,
|
|
180
604
|
span: spanOf(source, property.name),
|
|
605
|
+
extent: extentOf(source, property),
|
|
181
606
|
verb: root ? verbOf(root.expression) : null,
|
|
182
607
|
guards: root ? guardsIn(optionObjectOf(root)) : [],
|
|
183
608
|
argCount: methods.filter((method) => argMethods.has(method)).length
|
|
@@ -189,17 +614,54 @@ var createFactReader = (ts, ast) => {
|
|
|
189
614
|
const query = returnedObject(builder)?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "query")?.initializer;
|
|
190
615
|
return query && ts.isObjectLiteralExpression(query) ? objectKeys(source, query) : [];
|
|
191
616
|
};
|
|
617
|
+
const typeOf = (expression) => {
|
|
618
|
+
const value = expression && unwrap(expression);
|
|
619
|
+
if (value && ts.isArrayLiteralExpression(value))
|
|
620
|
+
return { type: typeOf(value.elements[0]).type, array: true };
|
|
621
|
+
return { type: value ? ast.expressionName(value) : null, array: false };
|
|
622
|
+
};
|
|
623
|
+
const fieldsOf = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
624
|
+
const name = ast.propertyName(property);
|
|
625
|
+
const node = keyName(property);
|
|
626
|
+
if (!name || !node)
|
|
627
|
+
return [];
|
|
628
|
+
const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
|
|
629
|
+
return [
|
|
630
|
+
{
|
|
631
|
+
name,
|
|
632
|
+
span: spanOf(source, node),
|
|
633
|
+
extent: extentOf(source, property),
|
|
634
|
+
...typeOf(root?.arguments[0]),
|
|
635
|
+
optional: methods.includes("optional")
|
|
636
|
+
}
|
|
637
|
+
];
|
|
638
|
+
});
|
|
192
639
|
const constantOf = (source, name, call) => {
|
|
193
640
|
const parents = call.arguments.map(unwrap).filter((arg) => ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)).map((arg) => ast.expressionName(arg)).filter((parent) => parent !== null);
|
|
194
|
-
const fields = call.arguments.flatMap((arg) =>
|
|
641
|
+
const fields = call.arguments.flatMap((arg) => fieldsOf(source, ast.firstObjectReturnedByArrow(arg)));
|
|
195
642
|
return { ...name, parents, fields };
|
|
196
643
|
};
|
|
644
|
+
const enumValuesOf = (call) => {
|
|
645
|
+
const list = call.arguments[1] && unwrap(call.arguments[1]);
|
|
646
|
+
if (!list || !ts.isArrayLiteralExpression(list))
|
|
647
|
+
return [];
|
|
648
|
+
return list.elements.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : []);
|
|
649
|
+
};
|
|
650
|
+
const methodsOf = (source, statement) => statement.members.flatMap((member) => {
|
|
651
|
+
if (!ts.isMethodDeclaration(member) || !member.body)
|
|
652
|
+
return [];
|
|
653
|
+
const name = ast.nodeName(member.name);
|
|
654
|
+
if (!name || ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))
|
|
655
|
+
return [];
|
|
656
|
+
return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
|
|
657
|
+
});
|
|
197
658
|
const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
198
659
|
const read = (source, owner) => {
|
|
199
660
|
const facts = {
|
|
200
661
|
fileName: source.fileName,
|
|
201
662
|
owner,
|
|
202
663
|
constants: [],
|
|
664
|
+
enums: [],
|
|
203
665
|
filters: [],
|
|
204
666
|
models: [],
|
|
205
667
|
services: [],
|
|
@@ -218,12 +680,18 @@ var createFactReader = (ts, ast) => {
|
|
|
218
680
|
const builder = builderOf(call);
|
|
219
681
|
if (callee === "via")
|
|
220
682
|
facts.constants.push(constantOf(source, name, call));
|
|
683
|
+
else if (callee === "enumOf")
|
|
684
|
+
facts.enums.push({ ...name, values: enumValuesOf(call) });
|
|
221
685
|
else if (callee === "from")
|
|
222
686
|
facts.filters.push({ ...name, queries: filterQueriesOf(source, builder) });
|
|
223
687
|
else if (callee === "into")
|
|
224
688
|
facts.models.push({ ...name, model: name.name.endsWith("Model") ? lowerFirst(name.name.slice(0, -5)) : null });
|
|
225
689
|
else if (callee === "serve")
|
|
226
|
-
facts.services.push({
|
|
690
|
+
facts.services.push({
|
|
691
|
+
...name,
|
|
692
|
+
model: moduleRefOf(call.arguments[0], "db"),
|
|
693
|
+
methods: methodsOf(source, statement)
|
|
694
|
+
});
|
|
227
695
|
else if (callee === "slice")
|
|
228
696
|
facts.slices.push({
|
|
229
697
|
...name,
|
|
@@ -241,7 +709,8 @@ var createFactReader = (ts, ast) => {
|
|
|
241
709
|
facts.stores.push({
|
|
242
710
|
...name,
|
|
243
711
|
model: moduleRefOf(call.arguments[0], "sig"),
|
|
244
|
-
stateKeys: objectKeys(source, returnedObject(builder))
|
|
712
|
+
stateKeys: objectKeys(source, returnedObject(builder)),
|
|
713
|
+
actions: methodsOf(source, statement)
|
|
245
714
|
});
|
|
246
715
|
}
|
|
247
716
|
return facts;
|
|
@@ -250,12 +719,12 @@ var createFactReader = (ts, ast) => {
|
|
|
250
719
|
};
|
|
251
720
|
|
|
252
721
|
// pkgs/akanjs/common/capitalize.ts
|
|
253
|
-
var
|
|
722
|
+
var capitalize2 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
254
723
|
var lowerlize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
|
|
255
724
|
|
|
256
725
|
// pkgs/akanjs/common/akanNames.ts
|
|
257
726
|
var sliceNames = (refName, suffix = "") => {
|
|
258
|
-
const [Model, Suffix] = [
|
|
727
|
+
const [Model, Suffix] = [capitalize2(refName), capitalize2(suffix)];
|
|
259
728
|
const live = `${refName}Live${Suffix}`;
|
|
260
729
|
return {
|
|
261
730
|
slice: `${refName}${Suffix}`,
|
|
@@ -301,7 +770,7 @@ var sliceNames = (refName, suffix = "") => {
|
|
|
301
770
|
};
|
|
302
771
|
};
|
|
303
772
|
var queryNames = (queryKey) => {
|
|
304
|
-
const Key =
|
|
773
|
+
const Key = capitalize2(queryKey);
|
|
305
774
|
return {
|
|
306
775
|
list: `list${Key}`,
|
|
307
776
|
listIds: `listIds${Key}`,
|
|
@@ -321,7 +790,7 @@ var queryNames = (queryKey) => {
|
|
|
321
790
|
};
|
|
322
791
|
};
|
|
323
792
|
var databaseStateNames = (refName) => {
|
|
324
|
-
const className =
|
|
793
|
+
const className = capitalize2(refName);
|
|
325
794
|
return {
|
|
326
795
|
model: refName,
|
|
327
796
|
modelLoading: `${refName}Loading`,
|
|
@@ -337,7 +806,7 @@ var databaseStateNames = (refName) => {
|
|
|
337
806
|
};
|
|
338
807
|
};
|
|
339
808
|
var modelNames = (refName) => {
|
|
340
|
-
const Model =
|
|
809
|
+
const Model = capitalize2(refName);
|
|
341
810
|
return {
|
|
342
811
|
...databaseStateNames(refName),
|
|
343
812
|
getModel: `get${Model}`,
|
|
@@ -381,7 +850,7 @@ var modelNames = (refName) => {
|
|
|
381
850
|
};
|
|
382
851
|
};
|
|
383
852
|
var moduleNames = (refName) => {
|
|
384
|
-
const Model =
|
|
853
|
+
const Model = capitalize2(refName);
|
|
385
854
|
return {
|
|
386
855
|
service: `${refName}Service`,
|
|
387
856
|
ServiceClass: `${Model}Service`,
|
|
@@ -394,7 +863,7 @@ var moduleNames = (refName) => {
|
|
|
394
863
|
};
|
|
395
864
|
};
|
|
396
865
|
var formSetterNames = (className, key) => {
|
|
397
|
-
const classKeyName =
|
|
866
|
+
const classKeyName = capitalize2(key);
|
|
398
867
|
return {
|
|
399
868
|
field: lowerlize(key),
|
|
400
869
|
Field: classKeyName,
|
|
@@ -407,14 +876,14 @@ var formSetterNames = (className, key) => {
|
|
|
407
876
|
postSetField: `_postSet${classKeyName}`
|
|
408
877
|
};
|
|
409
878
|
};
|
|
410
|
-
var stateSetterName = (key) => `set${
|
|
879
|
+
var stateSetterName = (key) => `set${capitalize2(key)}`;
|
|
411
880
|
var endpointNames = (key) => {
|
|
412
|
-
const Key =
|
|
881
|
+
const Key = capitalize2(key);
|
|
413
882
|
return { subscribe: `subscribe${Key}`, listen: `listen${Key}` };
|
|
414
883
|
};
|
|
415
884
|
|
|
416
885
|
// pkgs/@akanjs/language/nameTable.ts
|
|
417
|
-
var
|
|
886
|
+
var capitalize3 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
418
887
|
|
|
419
888
|
class NameTable {
|
|
420
889
|
#byName = new Map;
|
|
@@ -489,7 +958,7 @@ class NameTable {
|
|
|
489
958
|
}
|
|
490
959
|
#addGroup(group, constants) {
|
|
491
960
|
const { owner, refName } = group;
|
|
492
|
-
const Model =
|
|
961
|
+
const Model = capitalize3(refName);
|
|
493
962
|
const base = { owner, refName };
|
|
494
963
|
const modelClass = group.constants.find((constant) => constant.name === Model);
|
|
495
964
|
const facts = group.facts;
|
|
@@ -782,6 +1251,24 @@ ${[...new Set(text.match(wordPattern))].join(`
|
|
|
782
1251
|
referencesAt(givenName, offset, readText) {
|
|
783
1252
|
return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
|
|
784
1253
|
}
|
|
1254
|
+
graph(readText, { digests = false } = {}) {
|
|
1255
|
+
return new GraphBuilder({
|
|
1256
|
+
digests,
|
|
1257
|
+
ts: this.#ts,
|
|
1258
|
+
facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
|
|
1259
|
+
files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
1260
|
+
readText,
|
|
1261
|
+
parse: (fileName, text) => this.#parse(fileName, text),
|
|
1262
|
+
resolve: (fileName, identifier) => {
|
|
1263
|
+
if (!this.#nameTable().lookup(identifier.text).length)
|
|
1264
|
+
return [];
|
|
1265
|
+
const site = this.#siteOf(identifier);
|
|
1266
|
+
return site ? this.#resolve(fileName, site) : [];
|
|
1267
|
+
},
|
|
1268
|
+
relative: (fileName) => this.#relative(fileName),
|
|
1269
|
+
ownerOf: (fileName) => this.#ownerOf(fileName)
|
|
1270
|
+
}).build();
|
|
1271
|
+
}
|
|
785
1272
|
renameTargetAt(givenName, offset) {
|
|
786
1273
|
const source = this.#sourceEntriesAt(pathOf(givenName), offset).find((entry) => entry.renamed);
|
|
787
1274
|
return source ? { family: source.family, sourceKey: source.sourceKey, span: source.span } : null;
|
|
@@ -1027,6 +1514,7 @@ ${dictionary.className}
|
|
|
1027
1514
|
}
|
|
1028
1515
|
}
|
|
1029
1516
|
export {
|
|
1517
|
+
AkanGraphDiff,
|
|
1030
1518
|
AkanIndex,
|
|
1031
1519
|
createAst,
|
|
1032
1520
|
createFactReader,
|