@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 +20 -0
- package/akanIndex.d.ts +2 -0
- package/index.cjs +398 -15
- package/index.d.ts +2 -1
- package/index.js +398 -15
- package/moduleFacts.d.ts +24 -4
- package/package.json +1 -1
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) =>
|
|
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({
|
|
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
|
|
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] = [
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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${
|
|
824
|
+
var stateSetterName = (key) => `set${capitalize2(key)}`;
|
|
459
825
|
var endpointNames = (key) => {
|
|
460
|
-
const 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
|
|
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 =
|
|
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) =>
|
|
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({
|
|
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
|
|
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] = [
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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${
|
|
776
|
+
var stateSetterName = (key) => `set${capitalize2(key)}`;
|
|
411
777
|
var endpointNames = (key) => {
|
|
412
|
-
const 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
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
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",
|