@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.cjs
CHANGED
|
@@ -39,6 +39,7 @@ 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,
|
|
43
44
|
createAst: () => createAst,
|
|
44
45
|
createFactReader: () => createFactReader,
|
|
@@ -46,6 +47,425 @@ __export(exports_language, {
|
|
|
46
47
|
});
|
|
47
48
|
module.exports = __toCommonJS(exports_language);
|
|
48
49
|
|
|
50
|
+
// pkgs/@akanjs/language/akanGraphDiff.ts
|
|
51
|
+
var edgeKeyOf = ({ from, to, kind, label }) => `${from}
|
|
52
|
+
${to}
|
|
53
|
+
${kind}
|
|
54
|
+
${label ?? ""}`;
|
|
55
|
+
var shapeOf = ({ type, array, optional, declaredIn }) => `${type ?? ""}${array ? "[]" : ""}${optional ? "?" : ""} ${declaredIn}`;
|
|
56
|
+
var isEdited = (before, after) => before.digest !== undefined && after.digest !== undefined && before.digest !== after.digest;
|
|
57
|
+
var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
58
|
+
|
|
59
|
+
class AkanGraphDiff {
|
|
60
|
+
static between(base, head) {
|
|
61
|
+
const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
|
|
62
|
+
const headNodes = new Map(head.nodes.map((node) => [node.id, node]));
|
|
63
|
+
const marked = new Map;
|
|
64
|
+
for (const node of head.nodes) {
|
|
65
|
+
const before = baseNodes.get(node.id);
|
|
66
|
+
const next = before ? AkanGraphDiff.#compare(before, node) : AkanGraphDiff.#mark(node, "added");
|
|
67
|
+
if (next)
|
|
68
|
+
marked.set(node.id, next);
|
|
69
|
+
}
|
|
70
|
+
for (const node of base.nodes)
|
|
71
|
+
if (!headNodes.has(node.id))
|
|
72
|
+
marked.set(node.id, AkanGraphDiff.#mark(node, "removed"));
|
|
73
|
+
const baseKeys = new Set(base.edges.map(edgeKeyOf));
|
|
74
|
+
const headKeys = new Set(head.edges.map(edgeKeyOf));
|
|
75
|
+
const edges = [
|
|
76
|
+
...head.edges.flatMap((edge) => !baseKeys.has(edgeKeyOf(edge)) ? [{ ...edge, change: "added" }] : marked.has(edge.from) || marked.has(edge.to) ? [edge] : []),
|
|
77
|
+
...base.edges.filter((edge) => !headKeys.has(edgeKeyOf(edge))).map((edge) => ({ ...edge, change: "removed" }))
|
|
78
|
+
];
|
|
79
|
+
const context = edges.flatMap((edge) => [edge.from, edge.to]).filter((id) => !marked.has(id)).flatMap((id) => {
|
|
80
|
+
const node = headNodes.get(id) ?? baseNodes.get(id);
|
|
81
|
+
return node ? [node] : [];
|
|
82
|
+
});
|
|
83
|
+
const nodes = [...new Map([...marked.values(), ...context].map((node) => [node.id, node])).values()].map(AkanGraphDiff.#withoutDigest).sort(byId);
|
|
84
|
+
const summary = { added: 0, removed: 0, changed: 0 };
|
|
85
|
+
for (const node of marked.values())
|
|
86
|
+
if (node.change)
|
|
87
|
+
summary[node.change] += 1;
|
|
88
|
+
return { version: 1, nodes, edges, summary };
|
|
89
|
+
}
|
|
90
|
+
static #mark(node, change) {
|
|
91
|
+
return node.fields ? { ...node, change, fields: node.fields.map((field) => ({ ...field, change })) } : { ...node, change };
|
|
92
|
+
}
|
|
93
|
+
static #compare(before, after) {
|
|
94
|
+
const fields = AkanGraphDiff.#fieldsBetween(before.fields, after.fields);
|
|
95
|
+
const isChanged = isEdited(before, after) || before.label !== after.label || before.detail.join(`
|
|
96
|
+
`) !== after.detail.join(`
|
|
97
|
+
`) || (before.values ?? []).join(`
|
|
98
|
+
`) !== (after.values ?? []).join(`
|
|
99
|
+
`) || !!fields?.some((field) => field.change);
|
|
100
|
+
if (!isChanged)
|
|
101
|
+
return null;
|
|
102
|
+
return fields ? { ...after, change: "changed", fields } : { ...after, change: "changed" };
|
|
103
|
+
}
|
|
104
|
+
static #fieldsBetween(before, after) {
|
|
105
|
+
if (!before && !after)
|
|
106
|
+
return;
|
|
107
|
+
const previous = new Map((before ?? []).map((field) => [field.name, field]));
|
|
108
|
+
const names = new Set((after ?? []).map((field) => field.name));
|
|
109
|
+
return [
|
|
110
|
+
...(after ?? []).map((field) => {
|
|
111
|
+
const old = previous.get(field.name);
|
|
112
|
+
if (!old)
|
|
113
|
+
return { ...field, change: "added" };
|
|
114
|
+
return shapeOf(old) === shapeOf(field) && !isEdited(old, field) ? field : { ...field, change: "changed" };
|
|
115
|
+
}),
|
|
116
|
+
...(before ?? []).filter((field) => !names.has(field.name)).map((field) => ({ ...field, change: "removed" }))
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
static #withoutDigest({ digest: _digest, ...node }) {
|
|
120
|
+
return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// pkgs/@akanjs/language/akanGraph.ts
|
|
124
|
+
var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
125
|
+
var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
126
|
+
var memberSourcePattern = /\.(signal|service|store)\.ts$/;
|
|
127
|
+
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)$/;
|
|
128
|
+
var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
|
|
129
|
+
var moduleDirPattern = /\/lib\/([^/]+)\//;
|
|
130
|
+
var typeKinds = new Set(["model", "scalar", "enum"]);
|
|
131
|
+
var clientRoots = new Set(["fetch", "st", "sig"]);
|
|
132
|
+
|
|
133
|
+
class GraphBuilder {
|
|
134
|
+
#source;
|
|
135
|
+
#nodes = new Map;
|
|
136
|
+
#edges = new Map;
|
|
137
|
+
#contexts = new Map;
|
|
138
|
+
#members = new Map;
|
|
139
|
+
constructor(source) {
|
|
140
|
+
this.#source = source;
|
|
141
|
+
}
|
|
142
|
+
build() {
|
|
143
|
+
const { facts } = this.#source;
|
|
144
|
+
const constantsByOwner = new Map;
|
|
145
|
+
for (const file of facts)
|
|
146
|
+
constantsByOwner.set(file.owner, [...constantsByOwner.get(file.owner) ?? [], ...file.constants]);
|
|
147
|
+
const scopeOf = (owner) => [
|
|
148
|
+
...constantsByOwner.get(owner) ?? [],
|
|
149
|
+
...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
|
|
150
|
+
];
|
|
151
|
+
const typed = [];
|
|
152
|
+
for (const file of facts) {
|
|
153
|
+
const { owner } = file;
|
|
154
|
+
const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
|
|
155
|
+
const scalarClass = scalar ? file.constants.find((constant) => constant.name === capitalize(scalar)) : undefined;
|
|
156
|
+
if (scalar && scalarClass) {
|
|
157
|
+
const id = this.#key("scalar", owner, scalar);
|
|
158
|
+
const node = this.#addNode(id, "scalar", owner, scalar, scalarClass.name, scalarClass.span);
|
|
159
|
+
node.fields = this.#fieldsOf(scalarClass, scopeOf(owner));
|
|
160
|
+
typed.push(node);
|
|
161
|
+
}
|
|
162
|
+
for (const item of file.enums) {
|
|
163
|
+
const id = this.#key("enum", owner, item.name);
|
|
164
|
+
this.#addNode(id, "enum", owner, this.#moduleDirOf(file.fileName), item.name, item.span).values = item.values;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const file of facts) {
|
|
168
|
+
const { owner } = file;
|
|
169
|
+
for (const service of file.services) {
|
|
170
|
+
const refName = service.model;
|
|
171
|
+
if (!refName)
|
|
172
|
+
continue;
|
|
173
|
+
const modelId = this.#key("model", owner, refName);
|
|
174
|
+
const modelClass = constantsByOwner.get(owner)?.find((constant) => constant.name === capitalize(refName));
|
|
175
|
+
if (modelClass && !this.#nodes.has(modelId)) {
|
|
176
|
+
const node = this.#addNode(modelId, "model", owner, refName, modelClass.name, modelClass.span);
|
|
177
|
+
node.fields = this.#fieldsOf(modelClass, scopeOf(owner));
|
|
178
|
+
typed.push(node);
|
|
179
|
+
}
|
|
180
|
+
for (const method of service.methods)
|
|
181
|
+
this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
|
|
182
|
+
}
|
|
183
|
+
for (const filter of file.filters) {
|
|
184
|
+
if (!filter.name.endsWith("Filter"))
|
|
185
|
+
continue;
|
|
186
|
+
const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
|
|
187
|
+
this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
|
|
188
|
+
for (const query of filter.queries)
|
|
189
|
+
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);
|
|
190
|
+
}
|
|
191
|
+
for (const slice of file.slices) {
|
|
192
|
+
const refName = slice.model;
|
|
193
|
+
if (!refName)
|
|
194
|
+
continue;
|
|
195
|
+
const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
|
|
196
|
+
for (const guard of slice.guards)
|
|
197
|
+
this.#guard(fallback, guard);
|
|
198
|
+
for (const key of slice.keys) {
|
|
199
|
+
const detail = [...key.guards, ...key.live ? ["live"] : []];
|
|
200
|
+
const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
201
|
+
for (const guard of key.guards)
|
|
202
|
+
this.#guard(node, guard);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const endpoint of file.endpoints) {
|
|
206
|
+
const refName = endpoint.model;
|
|
207
|
+
if (!refName)
|
|
208
|
+
continue;
|
|
209
|
+
for (const key of endpoint.keys) {
|
|
210
|
+
const detail = [...key.verb ? [key.verb] : [], ...key.guards];
|
|
211
|
+
const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
212
|
+
for (const guard of key.guards)
|
|
213
|
+
this.#guard(node, guard);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const store of file.stores)
|
|
217
|
+
if (store.model)
|
|
218
|
+
for (const action of store.actions)
|
|
219
|
+
this.#addMember("action", owner, store.model, action, `${store.name}.${action.name}`, file.fileName);
|
|
220
|
+
}
|
|
221
|
+
const types = new Map;
|
|
222
|
+
for (const node of this.#nodes.values())
|
|
223
|
+
if (typeKinds.has(node.kind))
|
|
224
|
+
types.set(node.label, [...types.get(node.label) ?? [], node]);
|
|
225
|
+
for (const node of typed)
|
|
226
|
+
for (const field of node.fields ?? []) {
|
|
227
|
+
const candidates = (field.type ? types.get(field.type) ?? [] : []).filter((type) => this.#visible(type.owner, node.owner));
|
|
228
|
+
field.ref = (candidates.find((type) => type.owner === node.owner) ?? candidates[0])?.id ?? null;
|
|
229
|
+
if (field.ref)
|
|
230
|
+
this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
|
|
231
|
+
}
|
|
232
|
+
for (const [fileName, words] of this.#source.files)
|
|
233
|
+
this.#walk(fileName, words);
|
|
234
|
+
return {
|
|
235
|
+
version: 1,
|
|
236
|
+
nodes: [...this.#nodes.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
237
|
+
edges: [...this.#edges.values()]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
#key(kind, owner, scope, name = "") {
|
|
241
|
+
return `${kind}:${owner ?? ""}:${scope}${name ? `.${name}` : ""}`;
|
|
242
|
+
}
|
|
243
|
+
#location(span) {
|
|
244
|
+
return { file: this.#source.relative(span.fileName), line: span.line + 1, column: span.character + 1 };
|
|
245
|
+
}
|
|
246
|
+
#addNode(id, kind, owner, module2, name, span, detail = [], label = (kind === "query" || kind === "slice") && module2 ? `${module2}${name ? `.${name}` : " (default)"}` : name) {
|
|
247
|
+
const existing = this.#nodes.get(id);
|
|
248
|
+
if (existing)
|
|
249
|
+
return existing;
|
|
250
|
+
const node = {
|
|
251
|
+
id,
|
|
252
|
+
kind,
|
|
253
|
+
name,
|
|
254
|
+
label,
|
|
255
|
+
owner,
|
|
256
|
+
module: module2,
|
|
257
|
+
detail,
|
|
258
|
+
at: span ? this.#location(span) : null
|
|
259
|
+
};
|
|
260
|
+
this.#nodes.set(id, node);
|
|
261
|
+
return node;
|
|
262
|
+
}
|
|
263
|
+
#addMember(kind, owner, refName, member, label, fileName, detail = []) {
|
|
264
|
+
const id = this.#key(kind, owner, refName, member.name);
|
|
265
|
+
const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
|
|
266
|
+
this.#digest(node, this.#source.readText(fileName), member.extent);
|
|
267
|
+
this.#contexts.set(fileName, [
|
|
268
|
+
...this.#contexts.get(fileName) ?? [],
|
|
269
|
+
{ id, kind, owner, module: refName, extent: member.extent }
|
|
270
|
+
]);
|
|
271
|
+
const byName = `${kind}:${member.name}`;
|
|
272
|
+
this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
|
|
273
|
+
return node;
|
|
274
|
+
}
|
|
275
|
+
#hashOf(text, { start, end }) {
|
|
276
|
+
if (!this.#source.digests || text === undefined)
|
|
277
|
+
return null;
|
|
278
|
+
let hash = 2166136261;
|
|
279
|
+
for (let idx = start;idx < end; idx++) {
|
|
280
|
+
const code = text.charCodeAt(idx);
|
|
281
|
+
if (code !== 13)
|
|
282
|
+
hash = Math.imul(hash ^ code, 16777619);
|
|
283
|
+
}
|
|
284
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
285
|
+
}
|
|
286
|
+
#digest(node, text, extent) {
|
|
287
|
+
const digest = node.digest ? null : this.#hashOf(text, extent);
|
|
288
|
+
if (digest)
|
|
289
|
+
node.digest = digest;
|
|
290
|
+
}
|
|
291
|
+
#addEdge(from, to, kind, label, at) {
|
|
292
|
+
if (from === to)
|
|
293
|
+
return;
|
|
294
|
+
const key = `${from}
|
|
295
|
+
${to}
|
|
296
|
+
${kind}
|
|
297
|
+
${label ?? ""}`;
|
|
298
|
+
const existing = this.#edges.get(key);
|
|
299
|
+
if (existing)
|
|
300
|
+
existing.count += 1;
|
|
301
|
+
else
|
|
302
|
+
this.#edges.set(key, { from, to, kind, label, count: 1, at });
|
|
303
|
+
}
|
|
304
|
+
#guard(node, guard) {
|
|
305
|
+
const [role, name] = guard.includes(" ") ? guard.split(" ") : [null, guard];
|
|
306
|
+
if (!name || name === "?")
|
|
307
|
+
return;
|
|
308
|
+
const id = `guard:${name}`;
|
|
309
|
+
if (!this.#nodes.has(id))
|
|
310
|
+
this.#nodes.set(id, { id, kind: "guard", name, label: name, owner: null, module: null, detail: [], at: null });
|
|
311
|
+
this.#addEdge(node.id, id, "guard", role, node.at);
|
|
312
|
+
}
|
|
313
|
+
#moduleDirOf(fileName) {
|
|
314
|
+
const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
|
|
315
|
+
return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
|
|
316
|
+
}
|
|
317
|
+
#fieldsOf(constant, scope) {
|
|
318
|
+
const fields = new Map;
|
|
319
|
+
const seen = new Set;
|
|
320
|
+
const visit = (current) => {
|
|
321
|
+
if (seen.has(current))
|
|
322
|
+
return;
|
|
323
|
+
seen.add(current);
|
|
324
|
+
for (const parent of current.parents) {
|
|
325
|
+
const parentClass = scope.find((other) => other.name === parent && other.span.fileName === current.span.fileName) ?? scope.find((other) => other.name === parent);
|
|
326
|
+
if (parentClass)
|
|
327
|
+
visit(parentClass);
|
|
328
|
+
}
|
|
329
|
+
for (const field of current.fields) {
|
|
330
|
+
if (fields.has(field.name))
|
|
331
|
+
continue;
|
|
332
|
+
const digest = this.#hashOf(this.#source.readText(field.span.fileName), field.extent);
|
|
333
|
+
fields.set(field.name, {
|
|
334
|
+
name: field.name,
|
|
335
|
+
type: field.type,
|
|
336
|
+
array: field.array,
|
|
337
|
+
optional: field.optional,
|
|
338
|
+
ref: null,
|
|
339
|
+
declaredIn: current.name,
|
|
340
|
+
at: this.#location(field.span),
|
|
341
|
+
...digest ? { digest } : {}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
visit(constant);
|
|
346
|
+
return [...fields.values()];
|
|
347
|
+
}
|
|
348
|
+
#visible(target, from) {
|
|
349
|
+
return !from || !target || target === from || target.startsWith("libs/");
|
|
350
|
+
}
|
|
351
|
+
#targetNodeOf({ family, owner, refName, sourceKey }) {
|
|
352
|
+
const isDefault = /^[A-Z]/.test(sourceKey);
|
|
353
|
+
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;
|
|
354
|
+
return id && this.#nodes.has(id) ? id : null;
|
|
355
|
+
}
|
|
356
|
+
#membersNamed(kind, name, owner, refName) {
|
|
357
|
+
const nodes = (this.#members.get(`${kind}:${name}`) ?? []).filter((node) => (!refName || node.module === refName) && this.#visible(node.owner, owner));
|
|
358
|
+
const own = nodes.filter((node) => node.owner === owner);
|
|
359
|
+
return (own.length ? own : nodes).map((node) => node.id);
|
|
360
|
+
}
|
|
361
|
+
#plainTargetsOf(chain, name, context, owner) {
|
|
362
|
+
const [head, second] = chain ?? [];
|
|
363
|
+
if (chain?.length === 1 && head === "fetch")
|
|
364
|
+
return this.#membersNamed("endpoint", name, owner);
|
|
365
|
+
if (chain?.length === 2 && head === "st" && second === "do")
|
|
366
|
+
return this.#membersNamed("action", name, owner);
|
|
367
|
+
if (chain?.length === 1 && head === "this" && (context.kind === "method" || context.kind === "action"))
|
|
368
|
+
return this.#membersNamed(context.kind, name, context.owner, context.module).filter((id) => id.startsWith(`${context.kind}:${context.owner ?? ""}:`));
|
|
369
|
+
if (chain?.length === 2 && head === "this" && second && /.Service$/.test(second))
|
|
370
|
+
return this.#membersNamed("method", name, owner, second.slice(0, -"Service".length));
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
#walk(fileName, words) {
|
|
374
|
+
const relative = this.#source.relative(fileName);
|
|
375
|
+
const contexts = this.#contexts.get(fileName);
|
|
376
|
+
const isComponentFile = !memberSourcePattern.test(fileName) && !skippedPattern.test(relative) && (words.includes(`
|
|
377
|
+
fetch
|
|
378
|
+
`) || words.includes(`
|
|
379
|
+
st
|
|
380
|
+
`));
|
|
381
|
+
if (!contexts && !isComponentFile)
|
|
382
|
+
return;
|
|
383
|
+
const text = this.#source.readText(fileName);
|
|
384
|
+
if (text === undefined)
|
|
385
|
+
return;
|
|
386
|
+
const { ts } = this.#source;
|
|
387
|
+
const owner = this.#source.ownerOf(fileName);
|
|
388
|
+
const source = this.#source.parse(fileName, text);
|
|
389
|
+
const visit = (node) => {
|
|
390
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
|
391
|
+
const start = node.name.getStart(source);
|
|
392
|
+
const chain = this.#chainOf(node.expression);
|
|
393
|
+
const context = contexts ? contexts.find(({ extent }) => extent.start <= start && start < extent.end) : chain && clientRoots.has(chain[0]) ? this.#componentOf(source, node) : undefined;
|
|
394
|
+
if (context)
|
|
395
|
+
this.#link(fileName, source, node.name, chain, context, owner);
|
|
396
|
+
}
|
|
397
|
+
ts.forEachChild(node, visit);
|
|
398
|
+
};
|
|
399
|
+
visit(source);
|
|
400
|
+
}
|
|
401
|
+
#link(fileName, source, identifier, chain, context, owner) {
|
|
402
|
+
const resolved = this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
|
|
403
|
+
const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
|
|
404
|
+
if (!targets.length)
|
|
405
|
+
return;
|
|
406
|
+
if (context.pending && !this.#nodes.has(context.id)) {
|
|
407
|
+
this.#nodes.set(context.id, context.pending);
|
|
408
|
+
this.#digest(context.pending, source.text, context.extent);
|
|
409
|
+
}
|
|
410
|
+
const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
|
|
411
|
+
const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
|
|
412
|
+
const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
|
|
413
|
+
const at = { file: this.#source.relative(fileName), line: position.line + 1, column: position.character + 1 };
|
|
414
|
+
for (const target of new Set(own.length ? own : targets))
|
|
415
|
+
this.#addEdge(context.id, target, kind, identifier.text, at);
|
|
416
|
+
}
|
|
417
|
+
#chainOf(expression) {
|
|
418
|
+
const { ts } = this.#source;
|
|
419
|
+
if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
420
|
+
return this.#chainOf(expression.expression);
|
|
421
|
+
if (ts.isIdentifier(expression))
|
|
422
|
+
return [expression.text];
|
|
423
|
+
if (expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
424
|
+
return ["this"];
|
|
425
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
426
|
+
return null;
|
|
427
|
+
const head = this.#chainOf(expression.expression);
|
|
428
|
+
return head ? [...head, expression.name.text] : null;
|
|
429
|
+
}
|
|
430
|
+
#componentOf(source, node) {
|
|
431
|
+
const { ts } = this.#source;
|
|
432
|
+
let top = node;
|
|
433
|
+
while (top.parent && !ts.isSourceFile(top.parent))
|
|
434
|
+
top = top.parent;
|
|
435
|
+
const declared = ts.isVariableStatement(top) ? top.declarationList.declarations[0]?.name : ts.isFunctionDeclaration(top) || ts.isClassDeclaration(top) ? top.name : undefined;
|
|
436
|
+
const isDefaultExport = ts.canHaveModifiers(top) && !!ts.getModifiers(top)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword);
|
|
437
|
+
const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
|
|
438
|
+
const relative = this.#source.relative(source.fileName);
|
|
439
|
+
const owner = this.#source.ownerOf(source.fileName);
|
|
440
|
+
const id = `component:${relative}#${name}`;
|
|
441
|
+
const context = {
|
|
442
|
+
id,
|
|
443
|
+
kind: "component",
|
|
444
|
+
owner,
|
|
445
|
+
module: null,
|
|
446
|
+
extent: { start: top.getStart(source), end: top.getEnd() }
|
|
447
|
+
};
|
|
448
|
+
if (this.#nodes.has(id))
|
|
449
|
+
return context;
|
|
450
|
+
const pagePrefix = owner ? `${owner}/page/` : null;
|
|
451
|
+
const route = pagePrefix && relative.startsWith(pagePrefix) ? relative.slice(pagePrefix.length).replace(/\.tsx?$/, "") : null;
|
|
452
|
+
const base = relative.slice(relative.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
|
|
453
|
+
const label = route ? `${route}${name === "default" ? "" : `#${name}`}` : name === "default" ? base : `${base}.${name}`;
|
|
454
|
+
const position = source.getLineAndCharacterOfPosition((declared ?? top).getStart(source));
|
|
455
|
+
context.pending = {
|
|
456
|
+
id,
|
|
457
|
+
kind: "component",
|
|
458
|
+
name,
|
|
459
|
+
label,
|
|
460
|
+
owner,
|
|
461
|
+
module: this.#moduleDirOf(source.fileName),
|
|
462
|
+
detail: route ? ["page"] : [],
|
|
463
|
+
at: { file: relative, line: position.line + 1, column: position.character + 1 }
|
|
464
|
+
};
|
|
465
|
+
return context;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
49
469
|
// pkgs/@akanjs/language/ast.ts
|
|
50
470
|
var createAst = (ts) => {
|
|
51
471
|
const sourceFileFor = (fileName, content, scriptKind = ts.ScriptKind.TS) => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
@@ -112,7 +532,7 @@ var createAst = (ts) => {
|
|
|
112
532
|
};
|
|
113
533
|
|
|
114
534
|
// pkgs/@akanjs/language/moduleFacts.ts
|
|
115
|
-
var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store"]);
|
|
535
|
+
var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store", "enumOf"]);
|
|
116
536
|
var argMethods = new Set(["param", "body", "search", "room", "msg"]);
|
|
117
537
|
var ownerOf = (relativePath) => {
|
|
118
538
|
const [root, name] = relativePath.split("/");
|
|
@@ -134,6 +554,10 @@ var createFactReader = (ts, ast) => {
|
|
|
134
554
|
endCharacter: tail.character
|
|
135
555
|
};
|
|
136
556
|
};
|
|
557
|
+
const extentOf = (source, node) => ({
|
|
558
|
+
start: node.getStart(source),
|
|
559
|
+
end: node.getEnd()
|
|
560
|
+
});
|
|
137
561
|
const unwrap = (expression) => {
|
|
138
562
|
let current = expression;
|
|
139
563
|
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
|
|
@@ -177,7 +601,7 @@ var createFactReader = (ts, ast) => {
|
|
|
177
601
|
const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
178
602
|
const name = ast.propertyName(property);
|
|
179
603
|
const node = keyName(property);
|
|
180
|
-
return name && node ? [{ name, span: spanOf(source, node) }] : [];
|
|
604
|
+
return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
|
|
181
605
|
});
|
|
182
606
|
const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
183
607
|
if (!ts.isPropertyAssignment(property))
|
|
@@ -190,6 +614,7 @@ var createFactReader = (ts, ast) => {
|
|
|
190
614
|
{
|
|
191
615
|
name,
|
|
192
616
|
span: spanOf(source, property.name),
|
|
617
|
+
extent: extentOf(source, property),
|
|
193
618
|
guards: root ? guardsIn(optionObjectOf(root)) : [],
|
|
194
619
|
live: methods.includes("live"),
|
|
195
620
|
argCount: methods.filter((method) => argMethods.has(method)).length
|
|
@@ -226,6 +651,7 @@ var createFactReader = (ts, ast) => {
|
|
|
226
651
|
{
|
|
227
652
|
name,
|
|
228
653
|
span: spanOf(source, property.name),
|
|
654
|
+
extent: extentOf(source, property),
|
|
229
655
|
verb: root ? verbOf(root.expression) : null,
|
|
230
656
|
guards: root ? guardsIn(optionObjectOf(root)) : [],
|
|
231
657
|
argCount: methods.filter((method) => argMethods.has(method)).length
|
|
@@ -237,17 +663,54 @@ var createFactReader = (ts, ast) => {
|
|
|
237
663
|
const query = returnedObject(builder)?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "query")?.initializer;
|
|
238
664
|
return query && ts.isObjectLiteralExpression(query) ? objectKeys(source, query) : [];
|
|
239
665
|
};
|
|
666
|
+
const typeOf = (expression) => {
|
|
667
|
+
const value = expression && unwrap(expression);
|
|
668
|
+
if (value && ts.isArrayLiteralExpression(value))
|
|
669
|
+
return { type: typeOf(value.elements[0]).type, array: true };
|
|
670
|
+
return { type: value ? ast.expressionName(value) : null, array: false };
|
|
671
|
+
};
|
|
672
|
+
const fieldsOf = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
673
|
+
const name = ast.propertyName(property);
|
|
674
|
+
const node = keyName(property);
|
|
675
|
+
if (!name || !node)
|
|
676
|
+
return [];
|
|
677
|
+
const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
|
|
678
|
+
return [
|
|
679
|
+
{
|
|
680
|
+
name,
|
|
681
|
+
span: spanOf(source, node),
|
|
682
|
+
extent: extentOf(source, property),
|
|
683
|
+
...typeOf(root?.arguments[0]),
|
|
684
|
+
optional: methods.includes("optional")
|
|
685
|
+
}
|
|
686
|
+
];
|
|
687
|
+
});
|
|
240
688
|
const constantOf = (source, name, call) => {
|
|
241
689
|
const parents = call.arguments.map(unwrap).filter((arg) => ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)).map((arg) => ast.expressionName(arg)).filter((parent) => parent !== null);
|
|
242
|
-
const fields = call.arguments.flatMap((arg) =>
|
|
690
|
+
const fields = call.arguments.flatMap((arg) => fieldsOf(source, ast.firstObjectReturnedByArrow(arg)));
|
|
243
691
|
return { ...name, parents, fields };
|
|
244
692
|
};
|
|
693
|
+
const enumValuesOf = (call) => {
|
|
694
|
+
const list = call.arguments[1] && unwrap(call.arguments[1]);
|
|
695
|
+
if (!list || !ts.isArrayLiteralExpression(list))
|
|
696
|
+
return [];
|
|
697
|
+
return list.elements.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : []);
|
|
698
|
+
};
|
|
699
|
+
const methodsOf = (source, statement) => statement.members.flatMap((member) => {
|
|
700
|
+
if (!ts.isMethodDeclaration(member) || !member.body)
|
|
701
|
+
return [];
|
|
702
|
+
const name = ast.nodeName(member.name);
|
|
703
|
+
if (!name || ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))
|
|
704
|
+
return [];
|
|
705
|
+
return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
|
|
706
|
+
});
|
|
245
707
|
const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
246
708
|
const read = (source, owner) => {
|
|
247
709
|
const facts = {
|
|
248
710
|
fileName: source.fileName,
|
|
249
711
|
owner,
|
|
250
712
|
constants: [],
|
|
713
|
+
enums: [],
|
|
251
714
|
filters: [],
|
|
252
715
|
models: [],
|
|
253
716
|
services: [],
|
|
@@ -266,12 +729,18 @@ var createFactReader = (ts, ast) => {
|
|
|
266
729
|
const builder = builderOf(call);
|
|
267
730
|
if (callee === "via")
|
|
268
731
|
facts.constants.push(constantOf(source, name, call));
|
|
732
|
+
else if (callee === "enumOf")
|
|
733
|
+
facts.enums.push({ ...name, values: enumValuesOf(call) });
|
|
269
734
|
else if (callee === "from")
|
|
270
735
|
facts.filters.push({ ...name, queries: filterQueriesOf(source, builder) });
|
|
271
736
|
else if (callee === "into")
|
|
272
737
|
facts.models.push({ ...name, model: name.name.endsWith("Model") ? lowerFirst(name.name.slice(0, -5)) : null });
|
|
273
738
|
else if (callee === "serve")
|
|
274
|
-
facts.services.push({
|
|
739
|
+
facts.services.push({
|
|
740
|
+
...name,
|
|
741
|
+
model: moduleRefOf(call.arguments[0], "db"),
|
|
742
|
+
methods: methodsOf(source, statement)
|
|
743
|
+
});
|
|
275
744
|
else if (callee === "slice")
|
|
276
745
|
facts.slices.push({
|
|
277
746
|
...name,
|
|
@@ -289,7 +758,8 @@ var createFactReader = (ts, ast) => {
|
|
|
289
758
|
facts.stores.push({
|
|
290
759
|
...name,
|
|
291
760
|
model: moduleRefOf(call.arguments[0], "sig"),
|
|
292
|
-
stateKeys: objectKeys(source, returnedObject(builder))
|
|
761
|
+
stateKeys: objectKeys(source, returnedObject(builder)),
|
|
762
|
+
actions: methodsOf(source, statement)
|
|
293
763
|
});
|
|
294
764
|
}
|
|
295
765
|
return facts;
|
|
@@ -298,12 +768,12 @@ var createFactReader = (ts, ast) => {
|
|
|
298
768
|
};
|
|
299
769
|
|
|
300
770
|
// pkgs/akanjs/common/capitalize.ts
|
|
301
|
-
var
|
|
771
|
+
var capitalize2 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
302
772
|
var lowerlize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
|
|
303
773
|
|
|
304
774
|
// pkgs/akanjs/common/akanNames.ts
|
|
305
775
|
var sliceNames = (refName, suffix = "") => {
|
|
306
|
-
const [Model, Suffix] = [
|
|
776
|
+
const [Model, Suffix] = [capitalize2(refName), capitalize2(suffix)];
|
|
307
777
|
const live = `${refName}Live${Suffix}`;
|
|
308
778
|
return {
|
|
309
779
|
slice: `${refName}${Suffix}`,
|
|
@@ -349,7 +819,7 @@ var sliceNames = (refName, suffix = "") => {
|
|
|
349
819
|
};
|
|
350
820
|
};
|
|
351
821
|
var queryNames = (queryKey) => {
|
|
352
|
-
const Key =
|
|
822
|
+
const Key = capitalize2(queryKey);
|
|
353
823
|
return {
|
|
354
824
|
list: `list${Key}`,
|
|
355
825
|
listIds: `listIds${Key}`,
|
|
@@ -369,7 +839,7 @@ var queryNames = (queryKey) => {
|
|
|
369
839
|
};
|
|
370
840
|
};
|
|
371
841
|
var databaseStateNames = (refName) => {
|
|
372
|
-
const className =
|
|
842
|
+
const className = capitalize2(refName);
|
|
373
843
|
return {
|
|
374
844
|
model: refName,
|
|
375
845
|
modelLoading: `${refName}Loading`,
|
|
@@ -385,7 +855,7 @@ var databaseStateNames = (refName) => {
|
|
|
385
855
|
};
|
|
386
856
|
};
|
|
387
857
|
var modelNames = (refName) => {
|
|
388
|
-
const Model =
|
|
858
|
+
const Model = capitalize2(refName);
|
|
389
859
|
return {
|
|
390
860
|
...databaseStateNames(refName),
|
|
391
861
|
getModel: `get${Model}`,
|
|
@@ -429,7 +899,7 @@ var modelNames = (refName) => {
|
|
|
429
899
|
};
|
|
430
900
|
};
|
|
431
901
|
var moduleNames = (refName) => {
|
|
432
|
-
const Model =
|
|
902
|
+
const Model = capitalize2(refName);
|
|
433
903
|
return {
|
|
434
904
|
service: `${refName}Service`,
|
|
435
905
|
ServiceClass: `${Model}Service`,
|
|
@@ -442,7 +912,7 @@ var moduleNames = (refName) => {
|
|
|
442
912
|
};
|
|
443
913
|
};
|
|
444
914
|
var formSetterNames = (className, key) => {
|
|
445
|
-
const classKeyName =
|
|
915
|
+
const classKeyName = capitalize2(key);
|
|
446
916
|
return {
|
|
447
917
|
field: lowerlize(key),
|
|
448
918
|
Field: classKeyName,
|
|
@@ -455,14 +925,14 @@ var formSetterNames = (className, key) => {
|
|
|
455
925
|
postSetField: `_postSet${classKeyName}`
|
|
456
926
|
};
|
|
457
927
|
};
|
|
458
|
-
var stateSetterName = (key) => `set${
|
|
928
|
+
var stateSetterName = (key) => `set${capitalize2(key)}`;
|
|
459
929
|
var endpointNames = (key) => {
|
|
460
|
-
const Key =
|
|
930
|
+
const Key = capitalize2(key);
|
|
461
931
|
return { subscribe: `subscribe${Key}`, listen: `listen${Key}` };
|
|
462
932
|
};
|
|
463
933
|
|
|
464
934
|
// pkgs/@akanjs/language/nameTable.ts
|
|
465
|
-
var
|
|
935
|
+
var capitalize3 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
466
936
|
|
|
467
937
|
class NameTable {
|
|
468
938
|
#byName = new Map;
|
|
@@ -537,7 +1007,7 @@ class NameTable {
|
|
|
537
1007
|
}
|
|
538
1008
|
#addGroup(group, constants) {
|
|
539
1009
|
const { owner, refName } = group;
|
|
540
|
-
const Model =
|
|
1010
|
+
const Model = capitalize3(refName);
|
|
541
1011
|
const base = { owner, refName };
|
|
542
1012
|
const modelClass = group.constants.find((constant) => constant.name === Model);
|
|
543
1013
|
const facts = group.facts;
|
|
@@ -830,6 +1300,24 @@ ${[...new Set(text.match(wordPattern))].join(`
|
|
|
830
1300
|
referencesAt(givenName, offset, readText) {
|
|
831
1301
|
return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
|
|
832
1302
|
}
|
|
1303
|
+
graph(readText, { digests = false } = {}) {
|
|
1304
|
+
return new GraphBuilder({
|
|
1305
|
+
digests,
|
|
1306
|
+
ts: this.#ts,
|
|
1307
|
+
facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
|
|
1308
|
+
files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
1309
|
+
readText,
|
|
1310
|
+
parse: (fileName, text) => this.#parse(fileName, text),
|
|
1311
|
+
resolve: (fileName, identifier) => {
|
|
1312
|
+
if (!this.#nameTable().lookup(identifier.text).length)
|
|
1313
|
+
return [];
|
|
1314
|
+
const site = this.#siteOf(identifier);
|
|
1315
|
+
return site ? this.#resolve(fileName, site) : [];
|
|
1316
|
+
},
|
|
1317
|
+
relative: (fileName) => this.#relative(fileName),
|
|
1318
|
+
ownerOf: (fileName) => this.#ownerOf(fileName)
|
|
1319
|
+
}).build();
|
|
1320
|
+
}
|
|
833
1321
|
renameTargetAt(givenName, offset) {
|
|
834
1322
|
const source = this.#sourceEntriesAt(pathOf(givenName), offset).find((entry) => entry.renamed);
|
|
835
1323
|
return source ? { family: source.family, sourceKey: source.sourceKey, span: source.span } : null;
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export type { AkanChangedFile, AkanChangeMap, AkanGraph, AkanGraphChange, AkanGraphEdge, AkanGraphEdgeKind, AkanGraphField, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind, } from "akanjs/common/akanGraph.ts";
|
|
2
|
+
export { type AkanGraphChanges, AkanGraphDiff } from "./akanGraphDiff";
|
|
1
3
|
export { type AkanHover, AkanIndex, type AkanIndexOptions, type AkanRenameEdit, type AkanSourceKey } from "./akanIndex";
|
|
2
4
|
export { type AkanAst, createAst, type Ts } from "./ast";
|
|
3
|
-
export { type AkanFactReader, type AkanSpan, type ConstantClassFact, createFactReader, type EndpointClassFact, type EndpointKeyFact, type FileFacts, type FilterClassFact, type ModuleClassFact, type NamedFact, ownerOf, type SliceClassFact, type SliceKeyFact, type StoreClassFact, } from "./moduleFacts";
|
|
5
|
+
export { type AkanFactReader, type AkanSpan, type ConstantClassFact, createFactReader, type EndpointClassFact, type EndpointKeyFact, type EnumClassFact, type Extent, type FieldFact, type FileFacts, type FilterClassFact, type MemberFact, type ModuleClassFact, type NamedFact, ownerOf, type ServiceClassFact, type SliceClassFact, type SliceKeyFact, type StoreClassFact, } from "./moduleFacts";
|
|
4
6
|
export type { AkanFamily, AkanTarget } from "./nameTable";
|