@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/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) => objectKeys(source, ast.firstObjectReturnedByArrow(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({ ...name, model: moduleRefOf(call.arguments[0], "db") });
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 capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
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] = [capitalize(refName), capitalize(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 = capitalize(queryKey);
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 = capitalize(refName);
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 = capitalize(refName);
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 = capitalize(refName);
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 = capitalize(key);
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${capitalize(key)}`;
879
+ var stateSetterName = (key) => `set${capitalize2(key)}`;
411
880
  var endpointNames = (key) => {
412
- const Key = capitalize(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 capitalize2 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
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 = capitalize2(refName);
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,