@akanjs/language 3.1.2-rc.1 → 3.1.2-rc.3

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/akanGraph.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { AkanGraph } from "akanjs/common/akanGraph.ts";
2
+ import type * as TS from "typescript";
3
+ import type { Ts } from "./ast";
4
+ import type { FileFacts } from "./moduleFacts";
5
+ import type { AkanTarget } from "./nameTable";
6
+ export interface AkanGraphSource {
7
+ ts: Ts;
8
+ facts: FileFacts[];
9
+ files: [fileName: string, words: string][];
10
+ readText: (fileName: string) => string | undefined;
11
+ parse: (fileName: string, text: string) => TS.SourceFile;
12
+ resolve: (fileName: string, identifier: TS.Identifier) => AkanTarget[];
13
+ relative: (fileName: string) => string;
14
+ ownerOf: (fileName: string) => string | null;
15
+ }
16
+ export declare class GraphBuilder {
17
+ #private;
18
+ constructor(source: AkanGraphSource);
19
+ build(): AkanGraph;
20
+ }
package/akanIndex.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { AkanGraph } from "akanjs/common/akanGraph.ts";
1
2
  import { type Ts } from "./ast";
2
3
  import { type AkanSpan } from "./moduleFacts";
3
4
  import { type AkanFamily, type AkanTarget } from "./nameTable";
@@ -32,6 +33,7 @@ export declare class AkanIndex {
32
33
  sourceKeysOf(givenName: string): AkanSourceKey[];
33
34
  sourceKeyAt(givenName: string, offset: number): AkanSourceKey | null;
34
35
  referencesAt(givenName: string, offset: number, readText: (fileName: string) => string | undefined): AkanSpan[];
36
+ graph(readText: (fileName: string) => string | undefined): AkanGraph;
35
37
  renameTargetAt(givenName: string, offset: number): {
36
38
  family: AkanFamily;
37
39
  sourceKey: string;
package/index.cjs CHANGED
@@ -46,6 +46,328 @@ __export(exports_language, {
46
46
  });
47
47
  module.exports = __toCommonJS(exports_language);
48
48
 
49
+ // pkgs/@akanjs/language/akanGraph.ts
50
+ var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
51
+ var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
52
+ var memberSourcePattern = /\.(signal|service|store)\.ts$/;
53
+ var skippedPattern = /(?:\.(?:constant|document|dictionary|test|spec)\.tsx?|\/lib\/(?:cnst|db|dict|sig|srv|st|useClient|useServer)\.ts|\/index\.tsx?|^(?:apps|libs)\/[^/]+\/(?:client|server)\.ts)$/;
54
+ var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
55
+ var moduleDirPattern = /\/lib\/([^/]+)\//;
56
+ var typeKinds = new Set(["model", "scalar", "enum"]);
57
+ var clientRoots = new Set(["fetch", "st", "sig"]);
58
+
59
+ class GraphBuilder {
60
+ #source;
61
+ #nodes = new Map;
62
+ #edges = new Map;
63
+ #contexts = new Map;
64
+ #members = new Map;
65
+ constructor(source) {
66
+ this.#source = source;
67
+ }
68
+ build() {
69
+ const { facts } = this.#source;
70
+ const constantsByOwner = new Map;
71
+ for (const file of facts)
72
+ constantsByOwner.set(file.owner, [...constantsByOwner.get(file.owner) ?? [], ...file.constants]);
73
+ const scopeOf = (owner) => [
74
+ ...constantsByOwner.get(owner) ?? [],
75
+ ...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
76
+ ];
77
+ const typed = [];
78
+ for (const file of facts) {
79
+ const { owner } = file;
80
+ const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
81
+ const scalarClass = scalar ? file.constants.find((constant) => constant.name === capitalize(scalar)) : undefined;
82
+ if (scalar && scalarClass) {
83
+ const id = this.#key("scalar", owner, scalar);
84
+ const node = this.#addNode(id, "scalar", owner, scalar, scalarClass.name, scalarClass.span);
85
+ node.fields = this.#fieldsOf(scalarClass, scopeOf(owner));
86
+ typed.push(node);
87
+ }
88
+ for (const item of file.enums) {
89
+ const id = this.#key("enum", owner, item.name);
90
+ this.#addNode(id, "enum", owner, this.#moduleDirOf(file.fileName), item.name, item.span).values = item.values;
91
+ }
92
+ }
93
+ for (const file of facts) {
94
+ const { owner } = file;
95
+ for (const service of file.services) {
96
+ const refName = service.model;
97
+ if (!refName)
98
+ continue;
99
+ const modelId = this.#key("model", owner, refName);
100
+ const modelClass = constantsByOwner.get(owner)?.find((constant) => constant.name === capitalize(refName));
101
+ if (modelClass && !this.#nodes.has(modelId)) {
102
+ const node = this.#addNode(modelId, "model", owner, refName, modelClass.name, modelClass.span);
103
+ node.fields = this.#fieldsOf(modelClass, scopeOf(owner));
104
+ typed.push(node);
105
+ }
106
+ for (const method of service.methods)
107
+ this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
108
+ }
109
+ for (const filter of file.filters) {
110
+ if (!filter.name.endsWith("Filter"))
111
+ continue;
112
+ const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
113
+ this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
114
+ for (const query of filter.queries)
115
+ this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span);
116
+ }
117
+ for (const slice of file.slices) {
118
+ const refName = slice.model;
119
+ if (!refName)
120
+ continue;
121
+ const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
122
+ for (const guard of slice.guards)
123
+ this.#guard(fallback, guard);
124
+ for (const key of slice.keys) {
125
+ const detail = [...key.guards, ...key.live ? ["live"] : []];
126
+ const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
127
+ for (const guard of key.guards)
128
+ this.#guard(node, guard);
129
+ }
130
+ }
131
+ for (const endpoint of file.endpoints) {
132
+ const refName = endpoint.model;
133
+ if (!refName)
134
+ continue;
135
+ for (const key of endpoint.keys) {
136
+ const detail = [...key.verb ? [key.verb] : [], ...key.guards];
137
+ const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
138
+ for (const guard of key.guards)
139
+ this.#guard(node, guard);
140
+ }
141
+ }
142
+ for (const store of file.stores)
143
+ if (store.model)
144
+ for (const action of store.actions)
145
+ this.#addMember("action", owner, store.model, action, `${store.name}.${action.name}`, file.fileName);
146
+ }
147
+ const types = new Map;
148
+ for (const node of this.#nodes.values())
149
+ if (typeKinds.has(node.kind))
150
+ types.set(node.label, [...types.get(node.label) ?? [], node]);
151
+ for (const node of typed)
152
+ for (const field of node.fields ?? []) {
153
+ const candidates = (field.type ? types.get(field.type) ?? [] : []).filter((type) => this.#visible(type.owner, node.owner));
154
+ field.ref = (candidates.find((type) => type.owner === node.owner) ?? candidates[0])?.id ?? null;
155
+ if (field.ref)
156
+ this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
157
+ }
158
+ for (const [fileName, words] of this.#source.files)
159
+ this.#walk(fileName, words);
160
+ return {
161
+ version: 1,
162
+ nodes: [...this.#nodes.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
163
+ edges: [...this.#edges.values()]
164
+ };
165
+ }
166
+ #key(kind, owner, scope, name = "") {
167
+ return `${kind}:${owner ?? ""}:${scope}${name ? `.${name}` : ""}`;
168
+ }
169
+ #location(span) {
170
+ return { file: this.#source.relative(span.fileName), line: span.line + 1, column: span.character + 1 };
171
+ }
172
+ #addNode(id, kind, owner, module2, name, span, detail = [], label = (kind === "query" || kind === "slice") && module2 ? `${module2}${name ? `.${name}` : " (default)"}` : name) {
173
+ const existing = this.#nodes.get(id);
174
+ if (existing)
175
+ return existing;
176
+ const node = {
177
+ id,
178
+ kind,
179
+ name,
180
+ label,
181
+ owner,
182
+ module: module2,
183
+ detail,
184
+ at: span ? this.#location(span) : null
185
+ };
186
+ this.#nodes.set(id, node);
187
+ return node;
188
+ }
189
+ #addMember(kind, owner, refName, member, label, fileName, detail = []) {
190
+ const id = this.#key(kind, owner, refName, member.name);
191
+ const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
192
+ this.#contexts.set(fileName, [
193
+ ...this.#contexts.get(fileName) ?? [],
194
+ { id, kind, owner, module: refName, extent: member.extent }
195
+ ]);
196
+ const byName = `${kind}:${member.name}`;
197
+ this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
198
+ return node;
199
+ }
200
+ #addEdge(from, to, kind, label, at) {
201
+ if (from === to)
202
+ return;
203
+ const key = `${from}
204
+ ${to}
205
+ ${kind}
206
+ ${label ?? ""}`;
207
+ const existing = this.#edges.get(key);
208
+ if (existing)
209
+ existing.count += 1;
210
+ else
211
+ this.#edges.set(key, { from, to, kind, label, count: 1, at });
212
+ }
213
+ #guard(node, guard) {
214
+ const [role, name] = guard.includes(" ") ? guard.split(" ") : [null, guard];
215
+ if (!name || name === "?")
216
+ return;
217
+ const id = `guard:${name}`;
218
+ if (!this.#nodes.has(id))
219
+ this.#nodes.set(id, { id, kind: "guard", name, label: name, owner: null, module: null, detail: [], at: null });
220
+ this.#addEdge(node.id, id, "guard", role, node.at);
221
+ }
222
+ #moduleDirOf(fileName) {
223
+ return this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
224
+ }
225
+ #fieldsOf(constant, scope) {
226
+ const fields = new Map;
227
+ const seen = new Set;
228
+ const visit = (current) => {
229
+ if (seen.has(current))
230
+ return;
231
+ seen.add(current);
232
+ for (const parent of current.parents) {
233
+ const parentClass = scope.find((other) => other.name === parent && other.span.fileName === current.span.fileName) ?? scope.find((other) => other.name === parent);
234
+ if (parentClass)
235
+ visit(parentClass);
236
+ }
237
+ for (const field of current.fields)
238
+ if (!fields.has(field.name))
239
+ fields.set(field.name, {
240
+ name: field.name,
241
+ type: field.type,
242
+ array: field.array,
243
+ optional: field.optional,
244
+ ref: null,
245
+ declaredIn: current.name,
246
+ at: this.#location(field.span)
247
+ });
248
+ };
249
+ visit(constant);
250
+ return [...fields.values()];
251
+ }
252
+ #visible(target, from) {
253
+ return !from || !target || target === from || target.startsWith("libs/");
254
+ }
255
+ #targetNodeOf({ family, owner, refName, sourceKey }) {
256
+ const isDefault = /^[A-Z]/.test(sourceKey);
257
+ const id = family === "slice" || family === "query" ? this.#key(family, owner, refName, isDefault ? "" : sourceKey) : family === "model" || family === "formSetter" ? this.#key("model", owner, refName) : family === "endpoint" ? this.#key("endpoint", owner, refName, sourceKey) : null;
258
+ return id && this.#nodes.has(id) ? id : null;
259
+ }
260
+ #membersNamed(kind, name, owner, refName) {
261
+ const nodes = (this.#members.get(`${kind}:${name}`) ?? []).filter((node) => (!refName || node.module === refName) && this.#visible(node.owner, owner));
262
+ const own = nodes.filter((node) => node.owner === owner);
263
+ return (own.length ? own : nodes).map((node) => node.id);
264
+ }
265
+ #plainTargetsOf(chain, name, context, owner) {
266
+ const [head, second] = chain ?? [];
267
+ if (chain?.length === 1 && head === "fetch")
268
+ return this.#membersNamed("endpoint", name, owner);
269
+ if (chain?.length === 2 && head === "st" && second === "do")
270
+ return this.#membersNamed("action", name, owner);
271
+ if (chain?.length === 1 && head === "this" && (context.kind === "method" || context.kind === "action"))
272
+ return this.#membersNamed(context.kind, name, context.owner, context.module).filter((id) => id.startsWith(`${context.kind}:${context.owner ?? ""}:`));
273
+ if (chain?.length === 2 && head === "this" && second && /.Service$/.test(second))
274
+ return this.#membersNamed("method", name, owner, second.slice(0, -"Service".length));
275
+ return [];
276
+ }
277
+ #walk(fileName, words) {
278
+ const relative = this.#source.relative(fileName);
279
+ const contexts = this.#contexts.get(fileName);
280
+ const isComponentFile = !memberSourcePattern.test(fileName) && !skippedPattern.test(relative) && (words.includes(`
281
+ fetch
282
+ `) || words.includes(`
283
+ st
284
+ `));
285
+ if (!contexts && !isComponentFile)
286
+ return;
287
+ const text = this.#source.readText(fileName);
288
+ if (text === undefined)
289
+ return;
290
+ const { ts } = this.#source;
291
+ const owner = this.#source.ownerOf(fileName);
292
+ const source = this.#source.parse(fileName, text);
293
+ const visit = (node) => {
294
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
295
+ const start = node.name.getStart(source);
296
+ const chain = this.#chainOf(node.expression);
297
+ const context = contexts ? contexts.find(({ extent }) => extent.start <= start && start < extent.end) : chain && clientRoots.has(chain[0]) ? this.#componentOf(source, node) : undefined;
298
+ if (context)
299
+ this.#link(fileName, source, node.name, chain, context, owner);
300
+ }
301
+ ts.forEachChild(node, visit);
302
+ };
303
+ visit(source);
304
+ }
305
+ #link(fileName, source, identifier, chain, context, owner) {
306
+ const resolved = this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
307
+ const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
308
+ if (!targets.length)
309
+ return;
310
+ if (context.pending && !this.#nodes.has(context.id))
311
+ this.#nodes.set(context.id, context.pending);
312
+ const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
313
+ const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
314
+ const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
315
+ const at = { file: this.#source.relative(fileName), line: position.line + 1, column: position.character + 1 };
316
+ for (const target of new Set(own.length ? own : targets))
317
+ this.#addEdge(context.id, target, kind, identifier.text, at);
318
+ }
319
+ #chainOf(expression) {
320
+ const { ts } = this.#source;
321
+ if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
322
+ return this.#chainOf(expression.expression);
323
+ if (ts.isIdentifier(expression))
324
+ return [expression.text];
325
+ if (expression.kind === ts.SyntaxKind.ThisKeyword)
326
+ return ["this"];
327
+ if (!ts.isPropertyAccessExpression(expression))
328
+ return null;
329
+ const head = this.#chainOf(expression.expression);
330
+ return head ? [...head, expression.name.text] : null;
331
+ }
332
+ #componentOf(source, node) {
333
+ const { ts } = this.#source;
334
+ let top = node;
335
+ while (top.parent && !ts.isSourceFile(top.parent))
336
+ top = top.parent;
337
+ const declared = ts.isVariableStatement(top) ? top.declarationList.declarations[0]?.name : ts.isFunctionDeclaration(top) || ts.isClassDeclaration(top) ? top.name : undefined;
338
+ const isDefaultExport = ts.canHaveModifiers(top) && !!ts.getModifiers(top)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword);
339
+ const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
340
+ const relative = this.#source.relative(source.fileName);
341
+ const owner = this.#source.ownerOf(source.fileName);
342
+ const id = `component:${relative}#${name}`;
343
+ const context = {
344
+ id,
345
+ kind: "component",
346
+ owner,
347
+ module: null,
348
+ extent: { start: top.getStart(source), end: top.getEnd() }
349
+ };
350
+ if (this.#nodes.has(id))
351
+ return context;
352
+ const pagePrefix = owner ? `${owner}/page/` : null;
353
+ const route = pagePrefix && relative.startsWith(pagePrefix) ? relative.slice(pagePrefix.length).replace(/\.tsx?$/, "") : null;
354
+ const base = relative.slice(relative.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
355
+ const label = route ? `${route}${name === "default" ? "" : `#${name}`}` : name === "default" ? base : `${base}.${name}`;
356
+ const position = source.getLineAndCharacterOfPosition((declared ?? top).getStart(source));
357
+ context.pending = {
358
+ id,
359
+ kind: "component",
360
+ name,
361
+ label,
362
+ owner,
363
+ module: this.#moduleDirOf(source.fileName),
364
+ detail: route ? ["page"] : [],
365
+ at: { file: relative, line: position.line + 1, column: position.character + 1 }
366
+ };
367
+ return context;
368
+ }
369
+ }
370
+
49
371
  // pkgs/@akanjs/language/ast.ts
50
372
  var createAst = (ts) => {
51
373
  const sourceFileFor = (fileName, content, scriptKind = ts.ScriptKind.TS) => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
@@ -112,7 +434,7 @@ var createAst = (ts) => {
112
434
  };
113
435
 
114
436
  // pkgs/@akanjs/language/moduleFacts.ts
115
- var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store"]);
437
+ var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store", "enumOf"]);
116
438
  var argMethods = new Set(["param", "body", "search", "room", "msg"]);
117
439
  var ownerOf = (relativePath) => {
118
440
  const [root, name] = relativePath.split("/");
@@ -134,6 +456,10 @@ var createFactReader = (ts, ast) => {
134
456
  endCharacter: tail.character
135
457
  };
136
458
  };
459
+ const extentOf = (source, node) => ({
460
+ start: node.getStart(source),
461
+ end: node.getEnd()
462
+ });
137
463
  const unwrap = (expression) => {
138
464
  let current = expression;
139
465
  while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
@@ -190,6 +516,7 @@ var createFactReader = (ts, ast) => {
190
516
  {
191
517
  name,
192
518
  span: spanOf(source, property.name),
519
+ extent: extentOf(source, property),
193
520
  guards: root ? guardsIn(optionObjectOf(root)) : [],
194
521
  live: methods.includes("live"),
195
522
  argCount: methods.filter((method) => argMethods.has(method)).length
@@ -226,6 +553,7 @@ var createFactReader = (ts, ast) => {
226
553
  {
227
554
  name,
228
555
  span: spanOf(source, property.name),
556
+ extent: extentOf(source, property),
229
557
  verb: root ? verbOf(root.expression) : null,
230
558
  guards: root ? guardsIn(optionObjectOf(root)) : [],
231
559
  argCount: methods.filter((method) => argMethods.has(method)).length
@@ -237,17 +565,48 @@ var createFactReader = (ts, ast) => {
237
565
  const query = returnedObject(builder)?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "query")?.initializer;
238
566
  return query && ts.isObjectLiteralExpression(query) ? objectKeys(source, query) : [];
239
567
  };
568
+ const typeOf = (expression) => {
569
+ const value = expression && unwrap(expression);
570
+ if (value && ts.isArrayLiteralExpression(value))
571
+ return { type: typeOf(value.elements[0]).type, array: true };
572
+ return { type: value ? ast.expressionName(value) : null, array: false };
573
+ };
574
+ const fieldsOf = (source, object) => (object?.properties ?? []).flatMap((property) => {
575
+ const name = ast.propertyName(property);
576
+ const node = keyName(property);
577
+ if (!name || !node)
578
+ return [];
579
+ const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
580
+ return [
581
+ { name, span: spanOf(source, node), ...typeOf(root?.arguments[0]), optional: methods.includes("optional") }
582
+ ];
583
+ });
240
584
  const constantOf = (source, name, call) => {
241
585
  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)));
586
+ const fields = call.arguments.flatMap((arg) => fieldsOf(source, ast.firstObjectReturnedByArrow(arg)));
243
587
  return { ...name, parents, fields };
244
588
  };
589
+ const enumValuesOf = (call) => {
590
+ const list = call.arguments[1] && unwrap(call.arguments[1]);
591
+ if (!list || !ts.isArrayLiteralExpression(list))
592
+ return [];
593
+ return list.elements.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : []);
594
+ };
595
+ const methodsOf = (source, statement) => statement.members.flatMap((member) => {
596
+ if (!ts.isMethodDeclaration(member) || !member.body)
597
+ return [];
598
+ const name = ast.nodeName(member.name);
599
+ if (!name || ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))
600
+ return [];
601
+ return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
602
+ });
245
603
  const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
246
604
  const read = (source, owner) => {
247
605
  const facts = {
248
606
  fileName: source.fileName,
249
607
  owner,
250
608
  constants: [],
609
+ enums: [],
251
610
  filters: [],
252
611
  models: [],
253
612
  services: [],
@@ -266,12 +625,18 @@ var createFactReader = (ts, ast) => {
266
625
  const builder = builderOf(call);
267
626
  if (callee === "via")
268
627
  facts.constants.push(constantOf(source, name, call));
628
+ else if (callee === "enumOf")
629
+ facts.enums.push({ ...name, values: enumValuesOf(call) });
269
630
  else if (callee === "from")
270
631
  facts.filters.push({ ...name, queries: filterQueriesOf(source, builder) });
271
632
  else if (callee === "into")
272
633
  facts.models.push({ ...name, model: name.name.endsWith("Model") ? lowerFirst(name.name.slice(0, -5)) : null });
273
634
  else if (callee === "serve")
274
- facts.services.push({ ...name, model: moduleRefOf(call.arguments[0], "db") });
635
+ facts.services.push({
636
+ ...name,
637
+ model: moduleRefOf(call.arguments[0], "db"),
638
+ methods: methodsOf(source, statement)
639
+ });
275
640
  else if (callee === "slice")
276
641
  facts.slices.push({
277
642
  ...name,
@@ -289,7 +654,8 @@ var createFactReader = (ts, ast) => {
289
654
  facts.stores.push({
290
655
  ...name,
291
656
  model: moduleRefOf(call.arguments[0], "sig"),
292
- stateKeys: objectKeys(source, returnedObject(builder))
657
+ stateKeys: objectKeys(source, returnedObject(builder)),
658
+ actions: methodsOf(source, statement)
293
659
  });
294
660
  }
295
661
  return facts;
@@ -298,12 +664,12 @@ var createFactReader = (ts, ast) => {
298
664
  };
299
665
 
300
666
  // pkgs/akanjs/common/capitalize.ts
301
- var capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
667
+ var capitalize2 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
302
668
  var lowerlize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
303
669
 
304
670
  // pkgs/akanjs/common/akanNames.ts
305
671
  var sliceNames = (refName, suffix = "") => {
306
- const [Model, Suffix] = [capitalize(refName), capitalize(suffix)];
672
+ const [Model, Suffix] = [capitalize2(refName), capitalize2(suffix)];
307
673
  const live = `${refName}Live${Suffix}`;
308
674
  return {
309
675
  slice: `${refName}${Suffix}`,
@@ -349,7 +715,7 @@ var sliceNames = (refName, suffix = "") => {
349
715
  };
350
716
  };
351
717
  var queryNames = (queryKey) => {
352
- const Key = capitalize(queryKey);
718
+ const Key = capitalize2(queryKey);
353
719
  return {
354
720
  list: `list${Key}`,
355
721
  listIds: `listIds${Key}`,
@@ -369,7 +735,7 @@ var queryNames = (queryKey) => {
369
735
  };
370
736
  };
371
737
  var databaseStateNames = (refName) => {
372
- const className = capitalize(refName);
738
+ const className = capitalize2(refName);
373
739
  return {
374
740
  model: refName,
375
741
  modelLoading: `${refName}Loading`,
@@ -385,7 +751,7 @@ var databaseStateNames = (refName) => {
385
751
  };
386
752
  };
387
753
  var modelNames = (refName) => {
388
- const Model = capitalize(refName);
754
+ const Model = capitalize2(refName);
389
755
  return {
390
756
  ...databaseStateNames(refName),
391
757
  getModel: `get${Model}`,
@@ -429,7 +795,7 @@ var modelNames = (refName) => {
429
795
  };
430
796
  };
431
797
  var moduleNames = (refName) => {
432
- const Model = capitalize(refName);
798
+ const Model = capitalize2(refName);
433
799
  return {
434
800
  service: `${refName}Service`,
435
801
  ServiceClass: `${Model}Service`,
@@ -442,7 +808,7 @@ var moduleNames = (refName) => {
442
808
  };
443
809
  };
444
810
  var formSetterNames = (className, key) => {
445
- const classKeyName = capitalize(key);
811
+ const classKeyName = capitalize2(key);
446
812
  return {
447
813
  field: lowerlize(key),
448
814
  Field: classKeyName,
@@ -455,14 +821,14 @@ var formSetterNames = (className, key) => {
455
821
  postSetField: `_postSet${classKeyName}`
456
822
  };
457
823
  };
458
- var stateSetterName = (key) => `set${capitalize(key)}`;
824
+ var stateSetterName = (key) => `set${capitalize2(key)}`;
459
825
  var endpointNames = (key) => {
460
- const Key = capitalize(key);
826
+ const Key = capitalize2(key);
461
827
  return { subscribe: `subscribe${Key}`, listen: `listen${Key}` };
462
828
  };
463
829
 
464
830
  // pkgs/@akanjs/language/nameTable.ts
465
- var capitalize2 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
831
+ var capitalize3 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
466
832
 
467
833
  class NameTable {
468
834
  #byName = new Map;
@@ -537,7 +903,7 @@ class NameTable {
537
903
  }
538
904
  #addGroup(group, constants) {
539
905
  const { owner, refName } = group;
540
- const Model = capitalize2(refName);
906
+ const Model = capitalize3(refName);
541
907
  const base = { owner, refName };
542
908
  const modelClass = group.constants.find((constant) => constant.name === Model);
543
909
  const facts = group.facts;
@@ -830,6 +1196,23 @@ ${[...new Set(text.match(wordPattern))].join(`
830
1196
  referencesAt(givenName, offset, readText) {
831
1197
  return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
832
1198
  }
1199
+ graph(readText) {
1200
+ return new GraphBuilder({
1201
+ ts: this.#ts,
1202
+ facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
1203
+ files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
1204
+ readText,
1205
+ parse: (fileName, text) => this.#parse(fileName, text),
1206
+ resolve: (fileName, identifier) => {
1207
+ if (!this.#nameTable().lookup(identifier.text).length)
1208
+ return [];
1209
+ const site = this.#siteOf(identifier);
1210
+ return site ? this.#resolve(fileName, site) : [];
1211
+ },
1212
+ relative: (fileName) => this.#relative(fileName),
1213
+ ownerOf: (fileName) => this.#ownerOf(fileName)
1214
+ }).build();
1215
+ }
833
1216
  renameTargetAt(givenName, offset) {
834
1217
  const source = this.#sourceEntriesAt(pathOf(givenName), offset).find((entry) => entry.renamed);
835
1218
  return source ? { family: source.family, sourceKey: source.sourceKey, span: source.span } : null;
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
+ export type { AkanGraph, AkanGraphEdge, AkanGraphEdgeKind, AkanGraphField, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind, } from "akanjs/common/akanGraph.ts";
1
2
  export { type AkanHover, AkanIndex, type AkanIndexOptions, type AkanRenameEdit, type AkanSourceKey } from "./akanIndex";
2
3
  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";
4
+ 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
5
  export type { AkanFamily, AkanTarget } from "./nameTable";
package/index.js CHANGED
@@ -1,3 +1,325 @@
1
+ // pkgs/@akanjs/language/akanGraph.ts
2
+ var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
3
+ var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
4
+ var memberSourcePattern = /\.(signal|service|store)\.ts$/;
5
+ 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)$/;
6
+ var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
7
+ var moduleDirPattern = /\/lib\/([^/]+)\//;
8
+ var typeKinds = new Set(["model", "scalar", "enum"]);
9
+ var clientRoots = new Set(["fetch", "st", "sig"]);
10
+
11
+ class GraphBuilder {
12
+ #source;
13
+ #nodes = new Map;
14
+ #edges = new Map;
15
+ #contexts = new Map;
16
+ #members = new Map;
17
+ constructor(source) {
18
+ this.#source = source;
19
+ }
20
+ build() {
21
+ const { facts } = this.#source;
22
+ const constantsByOwner = new Map;
23
+ for (const file of facts)
24
+ constantsByOwner.set(file.owner, [...constantsByOwner.get(file.owner) ?? [], ...file.constants]);
25
+ const scopeOf = (owner) => [
26
+ ...constantsByOwner.get(owner) ?? [],
27
+ ...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
28
+ ];
29
+ const typed = [];
30
+ for (const file of facts) {
31
+ const { owner } = file;
32
+ const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
33
+ const scalarClass = scalar ? file.constants.find((constant) => constant.name === capitalize(scalar)) : undefined;
34
+ if (scalar && scalarClass) {
35
+ const id = this.#key("scalar", owner, scalar);
36
+ const node = this.#addNode(id, "scalar", owner, scalar, scalarClass.name, scalarClass.span);
37
+ node.fields = this.#fieldsOf(scalarClass, scopeOf(owner));
38
+ typed.push(node);
39
+ }
40
+ for (const item of file.enums) {
41
+ const id = this.#key("enum", owner, item.name);
42
+ this.#addNode(id, "enum", owner, this.#moduleDirOf(file.fileName), item.name, item.span).values = item.values;
43
+ }
44
+ }
45
+ for (const file of facts) {
46
+ const { owner } = file;
47
+ for (const service of file.services) {
48
+ const refName = service.model;
49
+ if (!refName)
50
+ continue;
51
+ const modelId = this.#key("model", owner, refName);
52
+ const modelClass = constantsByOwner.get(owner)?.find((constant) => constant.name === capitalize(refName));
53
+ if (modelClass && !this.#nodes.has(modelId)) {
54
+ const node = this.#addNode(modelId, "model", owner, refName, modelClass.name, modelClass.span);
55
+ node.fields = this.#fieldsOf(modelClass, scopeOf(owner));
56
+ typed.push(node);
57
+ }
58
+ for (const method of service.methods)
59
+ this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
60
+ }
61
+ for (const filter of file.filters) {
62
+ if (!filter.name.endsWith("Filter"))
63
+ continue;
64
+ const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
65
+ this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
66
+ for (const query of filter.queries)
67
+ this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span);
68
+ }
69
+ for (const slice of file.slices) {
70
+ const refName = slice.model;
71
+ if (!refName)
72
+ continue;
73
+ const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
74
+ for (const guard of slice.guards)
75
+ this.#guard(fallback, guard);
76
+ for (const key of slice.keys) {
77
+ const detail = [...key.guards, ...key.live ? ["live"] : []];
78
+ const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
79
+ for (const guard of key.guards)
80
+ this.#guard(node, guard);
81
+ }
82
+ }
83
+ for (const endpoint of file.endpoints) {
84
+ const refName = endpoint.model;
85
+ if (!refName)
86
+ continue;
87
+ for (const key of endpoint.keys) {
88
+ const detail = [...key.verb ? [key.verb] : [], ...key.guards];
89
+ const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
90
+ for (const guard of key.guards)
91
+ this.#guard(node, guard);
92
+ }
93
+ }
94
+ for (const store of file.stores)
95
+ if (store.model)
96
+ for (const action of store.actions)
97
+ this.#addMember("action", owner, store.model, action, `${store.name}.${action.name}`, file.fileName);
98
+ }
99
+ const types = new Map;
100
+ for (const node of this.#nodes.values())
101
+ if (typeKinds.has(node.kind))
102
+ types.set(node.label, [...types.get(node.label) ?? [], node]);
103
+ for (const node of typed)
104
+ for (const field of node.fields ?? []) {
105
+ const candidates = (field.type ? types.get(field.type) ?? [] : []).filter((type) => this.#visible(type.owner, node.owner));
106
+ field.ref = (candidates.find((type) => type.owner === node.owner) ?? candidates[0])?.id ?? null;
107
+ if (field.ref)
108
+ this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
109
+ }
110
+ for (const [fileName, words] of this.#source.files)
111
+ this.#walk(fileName, words);
112
+ return {
113
+ version: 1,
114
+ nodes: [...this.#nodes.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
115
+ edges: [...this.#edges.values()]
116
+ };
117
+ }
118
+ #key(kind, owner, scope, name = "") {
119
+ return `${kind}:${owner ?? ""}:${scope}${name ? `.${name}` : ""}`;
120
+ }
121
+ #location(span) {
122
+ return { file: this.#source.relative(span.fileName), line: span.line + 1, column: span.character + 1 };
123
+ }
124
+ #addNode(id, kind, owner, module, name, span, detail = [], label = (kind === "query" || kind === "slice") && module ? `${module}${name ? `.${name}` : " (default)"}` : name) {
125
+ const existing = this.#nodes.get(id);
126
+ if (existing)
127
+ return existing;
128
+ const node = {
129
+ id,
130
+ kind,
131
+ name,
132
+ label,
133
+ owner,
134
+ module,
135
+ detail,
136
+ at: span ? this.#location(span) : null
137
+ };
138
+ this.#nodes.set(id, node);
139
+ return node;
140
+ }
141
+ #addMember(kind, owner, refName, member, label, fileName, detail = []) {
142
+ const id = this.#key(kind, owner, refName, member.name);
143
+ const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
144
+ this.#contexts.set(fileName, [
145
+ ...this.#contexts.get(fileName) ?? [],
146
+ { id, kind, owner, module: refName, extent: member.extent }
147
+ ]);
148
+ const byName = `${kind}:${member.name}`;
149
+ this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
150
+ return node;
151
+ }
152
+ #addEdge(from, to, kind, label, at) {
153
+ if (from === to)
154
+ return;
155
+ const key = `${from}
156
+ ${to}
157
+ ${kind}
158
+ ${label ?? ""}`;
159
+ const existing = this.#edges.get(key);
160
+ if (existing)
161
+ existing.count += 1;
162
+ else
163
+ this.#edges.set(key, { from, to, kind, label, count: 1, at });
164
+ }
165
+ #guard(node, guard) {
166
+ const [role, name] = guard.includes(" ") ? guard.split(" ") : [null, guard];
167
+ if (!name || name === "?")
168
+ return;
169
+ const id = `guard:${name}`;
170
+ if (!this.#nodes.has(id))
171
+ this.#nodes.set(id, { id, kind: "guard", name, label: name, owner: null, module: null, detail: [], at: null });
172
+ this.#addEdge(node.id, id, "guard", role, node.at);
173
+ }
174
+ #moduleDirOf(fileName) {
175
+ return this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
176
+ }
177
+ #fieldsOf(constant, scope) {
178
+ const fields = new Map;
179
+ const seen = new Set;
180
+ const visit = (current) => {
181
+ if (seen.has(current))
182
+ return;
183
+ seen.add(current);
184
+ for (const parent of current.parents) {
185
+ const parentClass = scope.find((other) => other.name === parent && other.span.fileName === current.span.fileName) ?? scope.find((other) => other.name === parent);
186
+ if (parentClass)
187
+ visit(parentClass);
188
+ }
189
+ for (const field of current.fields)
190
+ if (!fields.has(field.name))
191
+ fields.set(field.name, {
192
+ name: field.name,
193
+ type: field.type,
194
+ array: field.array,
195
+ optional: field.optional,
196
+ ref: null,
197
+ declaredIn: current.name,
198
+ at: this.#location(field.span)
199
+ });
200
+ };
201
+ visit(constant);
202
+ return [...fields.values()];
203
+ }
204
+ #visible(target, from) {
205
+ return !from || !target || target === from || target.startsWith("libs/");
206
+ }
207
+ #targetNodeOf({ family, owner, refName, sourceKey }) {
208
+ const isDefault = /^[A-Z]/.test(sourceKey);
209
+ 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;
210
+ return id && this.#nodes.has(id) ? id : null;
211
+ }
212
+ #membersNamed(kind, name, owner, refName) {
213
+ const nodes = (this.#members.get(`${kind}:${name}`) ?? []).filter((node) => (!refName || node.module === refName) && this.#visible(node.owner, owner));
214
+ const own = nodes.filter((node) => node.owner === owner);
215
+ return (own.length ? own : nodes).map((node) => node.id);
216
+ }
217
+ #plainTargetsOf(chain, name, context, owner) {
218
+ const [head, second] = chain ?? [];
219
+ if (chain?.length === 1 && head === "fetch")
220
+ return this.#membersNamed("endpoint", name, owner);
221
+ if (chain?.length === 2 && head === "st" && second === "do")
222
+ return this.#membersNamed("action", name, owner);
223
+ if (chain?.length === 1 && head === "this" && (context.kind === "method" || context.kind === "action"))
224
+ return this.#membersNamed(context.kind, name, context.owner, context.module).filter((id) => id.startsWith(`${context.kind}:${context.owner ?? ""}:`));
225
+ if (chain?.length === 2 && head === "this" && second && /.Service$/.test(second))
226
+ return this.#membersNamed("method", name, owner, second.slice(0, -"Service".length));
227
+ return [];
228
+ }
229
+ #walk(fileName, words) {
230
+ const relative = this.#source.relative(fileName);
231
+ const contexts = this.#contexts.get(fileName);
232
+ const isComponentFile = !memberSourcePattern.test(fileName) && !skippedPattern.test(relative) && (words.includes(`
233
+ fetch
234
+ `) || words.includes(`
235
+ st
236
+ `));
237
+ if (!contexts && !isComponentFile)
238
+ return;
239
+ const text = this.#source.readText(fileName);
240
+ if (text === undefined)
241
+ return;
242
+ const { ts } = this.#source;
243
+ const owner = this.#source.ownerOf(fileName);
244
+ const source = this.#source.parse(fileName, text);
245
+ const visit = (node) => {
246
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
247
+ const start = node.name.getStart(source);
248
+ const chain = this.#chainOf(node.expression);
249
+ const context = contexts ? contexts.find(({ extent }) => extent.start <= start && start < extent.end) : chain && clientRoots.has(chain[0]) ? this.#componentOf(source, node) : undefined;
250
+ if (context)
251
+ this.#link(fileName, source, node.name, chain, context, owner);
252
+ }
253
+ ts.forEachChild(node, visit);
254
+ };
255
+ visit(source);
256
+ }
257
+ #link(fileName, source, identifier, chain, context, owner) {
258
+ const resolved = this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
259
+ const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
260
+ if (!targets.length)
261
+ return;
262
+ if (context.pending && !this.#nodes.has(context.id))
263
+ this.#nodes.set(context.id, context.pending);
264
+ const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
265
+ const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
266
+ const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
267
+ const at = { file: this.#source.relative(fileName), line: position.line + 1, column: position.character + 1 };
268
+ for (const target of new Set(own.length ? own : targets))
269
+ this.#addEdge(context.id, target, kind, identifier.text, at);
270
+ }
271
+ #chainOf(expression) {
272
+ const { ts } = this.#source;
273
+ if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
274
+ return this.#chainOf(expression.expression);
275
+ if (ts.isIdentifier(expression))
276
+ return [expression.text];
277
+ if (expression.kind === ts.SyntaxKind.ThisKeyword)
278
+ return ["this"];
279
+ if (!ts.isPropertyAccessExpression(expression))
280
+ return null;
281
+ const head = this.#chainOf(expression.expression);
282
+ return head ? [...head, expression.name.text] : null;
283
+ }
284
+ #componentOf(source, node) {
285
+ const { ts } = this.#source;
286
+ let top = node;
287
+ while (top.parent && !ts.isSourceFile(top.parent))
288
+ top = top.parent;
289
+ const declared = ts.isVariableStatement(top) ? top.declarationList.declarations[0]?.name : ts.isFunctionDeclaration(top) || ts.isClassDeclaration(top) ? top.name : undefined;
290
+ const isDefaultExport = ts.canHaveModifiers(top) && !!ts.getModifiers(top)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword);
291
+ const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
292
+ const relative = this.#source.relative(source.fileName);
293
+ const owner = this.#source.ownerOf(source.fileName);
294
+ const id = `component:${relative}#${name}`;
295
+ const context = {
296
+ id,
297
+ kind: "component",
298
+ owner,
299
+ module: null,
300
+ extent: { start: top.getStart(source), end: top.getEnd() }
301
+ };
302
+ if (this.#nodes.has(id))
303
+ return context;
304
+ const pagePrefix = owner ? `${owner}/page/` : null;
305
+ const route = pagePrefix && relative.startsWith(pagePrefix) ? relative.slice(pagePrefix.length).replace(/\.tsx?$/, "") : null;
306
+ const base = relative.slice(relative.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
307
+ const label = route ? `${route}${name === "default" ? "" : `#${name}`}` : name === "default" ? base : `${base}.${name}`;
308
+ const position = source.getLineAndCharacterOfPosition((declared ?? top).getStart(source));
309
+ context.pending = {
310
+ id,
311
+ kind: "component",
312
+ name,
313
+ label,
314
+ owner,
315
+ module: this.#moduleDirOf(source.fileName),
316
+ detail: route ? ["page"] : [],
317
+ at: { file: relative, line: position.line + 1, column: position.character + 1 }
318
+ };
319
+ return context;
320
+ }
321
+ }
322
+
1
323
  // pkgs/@akanjs/language/ast.ts
2
324
  var createAst = (ts) => {
3
325
  const sourceFileFor = (fileName, content, scriptKind = ts.ScriptKind.TS) => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
@@ -64,7 +386,7 @@ var createAst = (ts) => {
64
386
  };
65
387
 
66
388
  // pkgs/@akanjs/language/moduleFacts.ts
67
- var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store"]);
389
+ var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store", "enumOf"]);
68
390
  var argMethods = new Set(["param", "body", "search", "room", "msg"]);
69
391
  var ownerOf = (relativePath) => {
70
392
  const [root, name] = relativePath.split("/");
@@ -86,6 +408,10 @@ var createFactReader = (ts, ast) => {
86
408
  endCharacter: tail.character
87
409
  };
88
410
  };
411
+ const extentOf = (source, node) => ({
412
+ start: node.getStart(source),
413
+ end: node.getEnd()
414
+ });
89
415
  const unwrap = (expression) => {
90
416
  let current = expression;
91
417
  while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
@@ -142,6 +468,7 @@ var createFactReader = (ts, ast) => {
142
468
  {
143
469
  name,
144
470
  span: spanOf(source, property.name),
471
+ extent: extentOf(source, property),
145
472
  guards: root ? guardsIn(optionObjectOf(root)) : [],
146
473
  live: methods.includes("live"),
147
474
  argCount: methods.filter((method) => argMethods.has(method)).length
@@ -178,6 +505,7 @@ var createFactReader = (ts, ast) => {
178
505
  {
179
506
  name,
180
507
  span: spanOf(source, property.name),
508
+ extent: extentOf(source, property),
181
509
  verb: root ? verbOf(root.expression) : null,
182
510
  guards: root ? guardsIn(optionObjectOf(root)) : [],
183
511
  argCount: methods.filter((method) => argMethods.has(method)).length
@@ -189,17 +517,48 @@ var createFactReader = (ts, ast) => {
189
517
  const query = returnedObject(builder)?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "query")?.initializer;
190
518
  return query && ts.isObjectLiteralExpression(query) ? objectKeys(source, query) : [];
191
519
  };
520
+ const typeOf = (expression) => {
521
+ const value = expression && unwrap(expression);
522
+ if (value && ts.isArrayLiteralExpression(value))
523
+ return { type: typeOf(value.elements[0]).type, array: true };
524
+ return { type: value ? ast.expressionName(value) : null, array: false };
525
+ };
526
+ const fieldsOf = (source, object) => (object?.properties ?? []).flatMap((property) => {
527
+ const name = ast.propertyName(property);
528
+ const node = keyName(property);
529
+ if (!name || !node)
530
+ return [];
531
+ const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
532
+ return [
533
+ { name, span: spanOf(source, node), ...typeOf(root?.arguments[0]), optional: methods.includes("optional") }
534
+ ];
535
+ });
192
536
  const constantOf = (source, name, call) => {
193
537
  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)));
538
+ const fields = call.arguments.flatMap((arg) => fieldsOf(source, ast.firstObjectReturnedByArrow(arg)));
195
539
  return { ...name, parents, fields };
196
540
  };
541
+ const enumValuesOf = (call) => {
542
+ const list = call.arguments[1] && unwrap(call.arguments[1]);
543
+ if (!list || !ts.isArrayLiteralExpression(list))
544
+ return [];
545
+ return list.elements.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : []);
546
+ };
547
+ const methodsOf = (source, statement) => statement.members.flatMap((member) => {
548
+ if (!ts.isMethodDeclaration(member) || !member.body)
549
+ return [];
550
+ const name = ast.nodeName(member.name);
551
+ if (!name || ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))
552
+ return [];
553
+ return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
554
+ });
197
555
  const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
198
556
  const read = (source, owner) => {
199
557
  const facts = {
200
558
  fileName: source.fileName,
201
559
  owner,
202
560
  constants: [],
561
+ enums: [],
203
562
  filters: [],
204
563
  models: [],
205
564
  services: [],
@@ -218,12 +577,18 @@ var createFactReader = (ts, ast) => {
218
577
  const builder = builderOf(call);
219
578
  if (callee === "via")
220
579
  facts.constants.push(constantOf(source, name, call));
580
+ else if (callee === "enumOf")
581
+ facts.enums.push({ ...name, values: enumValuesOf(call) });
221
582
  else if (callee === "from")
222
583
  facts.filters.push({ ...name, queries: filterQueriesOf(source, builder) });
223
584
  else if (callee === "into")
224
585
  facts.models.push({ ...name, model: name.name.endsWith("Model") ? lowerFirst(name.name.slice(0, -5)) : null });
225
586
  else if (callee === "serve")
226
- facts.services.push({ ...name, model: moduleRefOf(call.arguments[0], "db") });
587
+ facts.services.push({
588
+ ...name,
589
+ model: moduleRefOf(call.arguments[0], "db"),
590
+ methods: methodsOf(source, statement)
591
+ });
227
592
  else if (callee === "slice")
228
593
  facts.slices.push({
229
594
  ...name,
@@ -241,7 +606,8 @@ var createFactReader = (ts, ast) => {
241
606
  facts.stores.push({
242
607
  ...name,
243
608
  model: moduleRefOf(call.arguments[0], "sig"),
244
- stateKeys: objectKeys(source, returnedObject(builder))
609
+ stateKeys: objectKeys(source, returnedObject(builder)),
610
+ actions: methodsOf(source, statement)
245
611
  });
246
612
  }
247
613
  return facts;
@@ -250,12 +616,12 @@ var createFactReader = (ts, ast) => {
250
616
  };
251
617
 
252
618
  // pkgs/akanjs/common/capitalize.ts
253
- var capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
619
+ var capitalize2 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
254
620
  var lowerlize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
255
621
 
256
622
  // pkgs/akanjs/common/akanNames.ts
257
623
  var sliceNames = (refName, suffix = "") => {
258
- const [Model, Suffix] = [capitalize(refName), capitalize(suffix)];
624
+ const [Model, Suffix] = [capitalize2(refName), capitalize2(suffix)];
259
625
  const live = `${refName}Live${Suffix}`;
260
626
  return {
261
627
  slice: `${refName}${Suffix}`,
@@ -301,7 +667,7 @@ var sliceNames = (refName, suffix = "") => {
301
667
  };
302
668
  };
303
669
  var queryNames = (queryKey) => {
304
- const Key = capitalize(queryKey);
670
+ const Key = capitalize2(queryKey);
305
671
  return {
306
672
  list: `list${Key}`,
307
673
  listIds: `listIds${Key}`,
@@ -321,7 +687,7 @@ var queryNames = (queryKey) => {
321
687
  };
322
688
  };
323
689
  var databaseStateNames = (refName) => {
324
- const className = capitalize(refName);
690
+ const className = capitalize2(refName);
325
691
  return {
326
692
  model: refName,
327
693
  modelLoading: `${refName}Loading`,
@@ -337,7 +703,7 @@ var databaseStateNames = (refName) => {
337
703
  };
338
704
  };
339
705
  var modelNames = (refName) => {
340
- const Model = capitalize(refName);
706
+ const Model = capitalize2(refName);
341
707
  return {
342
708
  ...databaseStateNames(refName),
343
709
  getModel: `get${Model}`,
@@ -381,7 +747,7 @@ var modelNames = (refName) => {
381
747
  };
382
748
  };
383
749
  var moduleNames = (refName) => {
384
- const Model = capitalize(refName);
750
+ const Model = capitalize2(refName);
385
751
  return {
386
752
  service: `${refName}Service`,
387
753
  ServiceClass: `${Model}Service`,
@@ -394,7 +760,7 @@ var moduleNames = (refName) => {
394
760
  };
395
761
  };
396
762
  var formSetterNames = (className, key) => {
397
- const classKeyName = capitalize(key);
763
+ const classKeyName = capitalize2(key);
398
764
  return {
399
765
  field: lowerlize(key),
400
766
  Field: classKeyName,
@@ -407,14 +773,14 @@ var formSetterNames = (className, key) => {
407
773
  postSetField: `_postSet${classKeyName}`
408
774
  };
409
775
  };
410
- var stateSetterName = (key) => `set${capitalize(key)}`;
776
+ var stateSetterName = (key) => `set${capitalize2(key)}`;
411
777
  var endpointNames = (key) => {
412
- const Key = capitalize(key);
778
+ const Key = capitalize2(key);
413
779
  return { subscribe: `subscribe${Key}`, listen: `listen${Key}` };
414
780
  };
415
781
 
416
782
  // pkgs/@akanjs/language/nameTable.ts
417
- var capitalize2 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
783
+ var capitalize3 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
418
784
 
419
785
  class NameTable {
420
786
  #byName = new Map;
@@ -489,7 +855,7 @@ class NameTable {
489
855
  }
490
856
  #addGroup(group, constants) {
491
857
  const { owner, refName } = group;
492
- const Model = capitalize2(refName);
858
+ const Model = capitalize3(refName);
493
859
  const base = { owner, refName };
494
860
  const modelClass = group.constants.find((constant) => constant.name === Model);
495
861
  const facts = group.facts;
@@ -782,6 +1148,23 @@ ${[...new Set(text.match(wordPattern))].join(`
782
1148
  referencesAt(givenName, offset, readText) {
783
1149
  return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
784
1150
  }
1151
+ graph(readText) {
1152
+ return new GraphBuilder({
1153
+ ts: this.#ts,
1154
+ facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
1155
+ files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
1156
+ readText,
1157
+ parse: (fileName, text) => this.#parse(fileName, text),
1158
+ resolve: (fileName, identifier) => {
1159
+ if (!this.#nameTable().lookup(identifier.text).length)
1160
+ return [];
1161
+ const site = this.#siteOf(identifier);
1162
+ return site ? this.#resolve(fileName, site) : [];
1163
+ },
1164
+ relative: (fileName) => this.#relative(fileName),
1165
+ ownerOf: (fileName) => this.#ownerOf(fileName)
1166
+ }).build();
1167
+ }
785
1168
  renameTargetAt(givenName, offset) {
786
1169
  const source = this.#sourceEntriesAt(pathOf(givenName), offset).find((entry) => entry.renamed);
787
1170
  return source ? { family: source.family, sourceKey: source.sourceKey, span: source.span } : null;
package/moduleFacts.d.ts CHANGED
@@ -13,20 +13,35 @@ export interface NamedFact {
13
13
  name: string;
14
14
  span: AkanSpan;
15
15
  }
16
- export interface SliceKeyFact extends NamedFact {
16
+ export interface Extent {
17
+ start: number;
18
+ end: number;
19
+ }
20
+ export interface MemberFact extends NamedFact {
21
+ extent: Extent;
22
+ }
23
+ export interface SliceKeyFact extends MemberFact {
17
24
  guards: string[];
18
25
  live: boolean;
19
26
  argCount: number;
20
27
  }
21
- export interface EndpointKeyFact extends NamedFact {
28
+ export interface EndpointKeyFact extends MemberFact {
22
29
  verb: string | null;
23
30
  guards: string[];
24
31
  argCount: number;
25
32
  }
33
+ export interface FieldFact extends NamedFact {
34
+ type: string | null;
35
+ array: boolean;
36
+ optional: boolean;
37
+ }
26
38
  export interface ConstantClassFact extends NamedFact {
27
- fields: NamedFact[];
39
+ fields: FieldFact[];
28
40
  parents: string[];
29
41
  }
42
+ export interface EnumClassFact extends NamedFact {
43
+ values: string[];
44
+ }
30
45
  export interface FilterClassFact extends NamedFact {
31
46
  queries: NamedFact[];
32
47
  }
@@ -40,16 +55,21 @@ export interface SliceClassFact extends ModuleClassFact {
40
55
  export interface EndpointClassFact extends ModuleClassFact {
41
56
  keys: EndpointKeyFact[];
42
57
  }
58
+ export interface ServiceClassFact extends ModuleClassFact {
59
+ methods: MemberFact[];
60
+ }
43
61
  export interface StoreClassFact extends ModuleClassFact {
44
62
  stateKeys: NamedFact[];
63
+ actions: MemberFact[];
45
64
  }
46
65
  export interface FileFacts {
47
66
  fileName: string;
48
67
  owner: string | null;
49
68
  constants: ConstantClassFact[];
69
+ enums: EnumClassFact[];
50
70
  filters: FilterClassFact[];
51
71
  models: ModuleClassFact[];
52
- services: ModuleClassFact[];
72
+ services: ServiceClassFact[];
53
73
  slices: SliceClassFact[];
54
74
  endpoints: EndpointClassFact[];
55
75
  stores: StoreClassFact[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/language",
3
- "version": "3.1.2-rc.1",
3
+ "version": "3.1.2-rc.3",
4
4
  "description": "Editor-independent Akan.js language core: resolves derived names (slices, queries, CRUD, form setters) back to the source key that produced them.",
5
5
  "sourceType": "module",
6
6
  "type": "module",