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