@akanjs/language 3.1.2-rc.3 → 3.2.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +41 -0
- package/README.md +42 -0
- package/akanGraph.d.ts +3 -1
- package/akanGraphDiff.d.ts +10 -0
- package/akanIndex.d.ts +3 -1
- package/frameworkChannelReader.d.ts +16 -0
- package/frameworkMapBuilder.d.ts +20 -0
- package/frameworkMapDoc.d.ts +10 -0
- package/index.cjs +2202 -395
- package/index.d.ts +6 -1
- package/index.js +2202 -395
- package/moduleFacts.d.ts +48 -1
- package/package.json +4 -1
- package/routeFacts.d.ts +48 -0
- package/server.js +2730 -0
package/server.js
ADDED
|
@@ -0,0 +1,2730 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// pkgs/@akanjs/language/akanLanguageServer.ts
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// pkgs/@akanjs/language/moduleFacts.ts
|
|
9
|
+
var testFilePattern = /\.(?:test|spec)\.tsx?$/;
|
|
10
|
+
var moduleCallees = new Set([
|
|
11
|
+
"via",
|
|
12
|
+
"from",
|
|
13
|
+
"into",
|
|
14
|
+
"serve",
|
|
15
|
+
"slice",
|
|
16
|
+
"endpoint",
|
|
17
|
+
"internal",
|
|
18
|
+
"store",
|
|
19
|
+
"enumOf",
|
|
20
|
+
"adapt",
|
|
21
|
+
"dangerouslyAdapt"
|
|
22
|
+
]);
|
|
23
|
+
var intervalUnits = [
|
|
24
|
+
[86400000, "d"],
|
|
25
|
+
[3600000, "h"],
|
|
26
|
+
[60000, "m"],
|
|
27
|
+
[1000, "s"]
|
|
28
|
+
];
|
|
29
|
+
var argMethods = new Set(["param", "body", "search", "room", "msg"]);
|
|
30
|
+
var ownerOf = (relativePath) => {
|
|
31
|
+
const [root, name] = relativePath.split("/");
|
|
32
|
+
return (root === "apps" || root === "libs") && name ? `${root}/${name}` : null;
|
|
33
|
+
};
|
|
34
|
+
var createFactReader = (ts, ast) => {
|
|
35
|
+
const spanOf = (source, node) => {
|
|
36
|
+
const start = node.getStart(source);
|
|
37
|
+
const end = node.getEnd();
|
|
38
|
+
const head = source.getLineAndCharacterOfPosition(start);
|
|
39
|
+
const tail = source.getLineAndCharacterOfPosition(end);
|
|
40
|
+
return {
|
|
41
|
+
fileName: source.fileName,
|
|
42
|
+
start,
|
|
43
|
+
end,
|
|
44
|
+
line: head.line,
|
|
45
|
+
character: head.character,
|
|
46
|
+
endLine: tail.line,
|
|
47
|
+
endCharacter: tail.character
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const extentOf = (source, node) => ({
|
|
51
|
+
start: node.getStart(source),
|
|
52
|
+
end: node.getEnd()
|
|
53
|
+
});
|
|
54
|
+
const unwrap = (expression) => {
|
|
55
|
+
let current = expression;
|
|
56
|
+
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
|
|
57
|
+
current = current.expression;
|
|
58
|
+
return current;
|
|
59
|
+
};
|
|
60
|
+
const moduleRefOf = (expression, holder) => {
|
|
61
|
+
let current = expression && unwrap(expression);
|
|
62
|
+
while (current && ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression))
|
|
63
|
+
current = current.expression.expression;
|
|
64
|
+
if (current && ts.isStringLiteral(current))
|
|
65
|
+
return current.text;
|
|
66
|
+
if (!current || !ts.isPropertyAccessExpression(current))
|
|
67
|
+
return null;
|
|
68
|
+
return ts.isIdentifier(current.expression) && current.expression.text === holder ? current.name.text : null;
|
|
69
|
+
};
|
|
70
|
+
const returnedObject = (builder) => builder ? ast.firstObjectReturnedByArrow(builder) : null;
|
|
71
|
+
const builderOf = (call) => [...call.arguments].reverse().find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
|
|
72
|
+
const guardNameOf = (expression) => {
|
|
73
|
+
const value = unwrap(expression);
|
|
74
|
+
return ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) ? `${value.expression.text}.${value.name.text}` : ast.expressionName(value);
|
|
75
|
+
};
|
|
76
|
+
const guardsIn = (object) => {
|
|
77
|
+
const guards = object?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "guards")?.initializer;
|
|
78
|
+
if (!guards)
|
|
79
|
+
return [];
|
|
80
|
+
if (ts.isArrayLiteralExpression(guards))
|
|
81
|
+
return guards.elements.map(guardNameOf).filter((name) => name !== null);
|
|
82
|
+
if (ts.isObjectLiteralExpression(guards))
|
|
83
|
+
return guards.properties.filter(ts.isPropertyAssignment).flatMap((property) => {
|
|
84
|
+
const value = unwrap(property.initializer);
|
|
85
|
+
const names = ts.isArrayLiteralExpression(value) ? value.elements.map(guardNameOf) : [guardNameOf(value)];
|
|
86
|
+
return names.map((name) => `${ast.nodeName(property.name)} ${name ?? "?"}`);
|
|
87
|
+
});
|
|
88
|
+
const single = guardNameOf(guards);
|
|
89
|
+
return single ? [single] : [];
|
|
90
|
+
};
|
|
91
|
+
const optionObjectOf = (call) => call.arguments.find((arg) => ts.isObjectLiteralExpression(arg) && arg.properties.some((property) => ast.propertyName(property) === "guards"));
|
|
92
|
+
const chainOf = (initializer) => {
|
|
93
|
+
let root = unwrap(initializer);
|
|
94
|
+
const methods = [];
|
|
95
|
+
while (ts.isCallExpression(root) && ts.isPropertyAccessExpression(root.expression) && ts.isCallExpression(unwrap(root.expression.expression))) {
|
|
96
|
+
methods.push(root.expression.name.text);
|
|
97
|
+
root = unwrap(root.expression.expression);
|
|
98
|
+
}
|
|
99
|
+
return { root: ts.isCallExpression(root) ? root : null, methods };
|
|
100
|
+
};
|
|
101
|
+
const keyName = (property) => ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) || ts.isShorthandPropertyAssignment(property) ? property.name : null;
|
|
102
|
+
const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
103
|
+
const name = ast.propertyName(property);
|
|
104
|
+
const node = keyName(property);
|
|
105
|
+
return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
|
|
106
|
+
});
|
|
107
|
+
const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
108
|
+
if (!ts.isPropertyAssignment(property))
|
|
109
|
+
return [];
|
|
110
|
+
const name = ast.nodeName(property.name);
|
|
111
|
+
if (!name)
|
|
112
|
+
return [];
|
|
113
|
+
const { root, methods } = chainOf(property.initializer);
|
|
114
|
+
return [
|
|
115
|
+
{
|
|
116
|
+
name,
|
|
117
|
+
span: spanOf(source, property.name),
|
|
118
|
+
extent: extentOf(source, property),
|
|
119
|
+
guards: root ? guardsIn(optionObjectOf(root)) : [],
|
|
120
|
+
live: methods.includes("live"),
|
|
121
|
+
argCount: methods.filter((method) => argMethods.has(method)).length
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
});
|
|
125
|
+
const verbReaderOf = (parameter) => {
|
|
126
|
+
const verbs = new Map;
|
|
127
|
+
if (parameter && ts.isObjectBindingPattern(parameter.name))
|
|
128
|
+
for (const element of parameter.name.elements) {
|
|
129
|
+
const verb = element.propertyName ?? element.name;
|
|
130
|
+
if (ts.isIdentifier(verb) && ts.isIdentifier(element.name))
|
|
131
|
+
verbs.set(element.name.text, verb.text);
|
|
132
|
+
}
|
|
133
|
+
const holder = parameter && ts.isIdentifier(parameter.name) ? parameter.name.text : null;
|
|
134
|
+
return (callee) => {
|
|
135
|
+
if (callee && ts.isIdentifier(callee))
|
|
136
|
+
return verbs.get(callee.text) ?? null;
|
|
137
|
+
if (callee && ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && callee.expression.text === holder)
|
|
138
|
+
return callee.name.text;
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const numberOf = (expression) => {
|
|
143
|
+
const value = unwrap(expression);
|
|
144
|
+
if (ts.isNumericLiteral(value))
|
|
145
|
+
return Number(value.text.replace(/_/g, ""));
|
|
146
|
+
if (!ts.isBinaryExpression(value))
|
|
147
|
+
return null;
|
|
148
|
+
const [left, right] = [numberOf(value.left), numberOf(value.right)];
|
|
149
|
+
if (left === null || right === null)
|
|
150
|
+
return null;
|
|
151
|
+
const operator = value.operatorToken.kind;
|
|
152
|
+
if (operator === ts.SyntaxKind.AsteriskToken)
|
|
153
|
+
return left * right;
|
|
154
|
+
if (operator === ts.SyntaxKind.PlusToken)
|
|
155
|
+
return left + right;
|
|
156
|
+
if (operator === ts.SyntaxKind.MinusToken)
|
|
157
|
+
return left - right;
|
|
158
|
+
return operator === ts.SyntaxKind.SlashToken && right ? left / right : null;
|
|
159
|
+
};
|
|
160
|
+
const durationOf = (expression, source) => {
|
|
161
|
+
if (!expression)
|
|
162
|
+
return null;
|
|
163
|
+
const ms = numberOf(expression);
|
|
164
|
+
if (ms === null)
|
|
165
|
+
return expression.getText(source);
|
|
166
|
+
const [size, unit] = intervalUnits.find(([size]) => ms >= size && ms % size === 0) ?? [1, "ms"];
|
|
167
|
+
return `${ms / size}${unit}`;
|
|
168
|
+
};
|
|
169
|
+
const modesOf = (option) => {
|
|
170
|
+
const object = option && unwrap(option);
|
|
171
|
+
if (!object || !ts.isObjectLiteralExpression(object))
|
|
172
|
+
return [];
|
|
173
|
+
return object.properties.filter(ts.isPropertyAssignment).flatMap((property) => {
|
|
174
|
+
const name = ast.nodeName(property.name);
|
|
175
|
+
const value = unwrap(property.initializer);
|
|
176
|
+
if (name === "enabled")
|
|
177
|
+
return value.kind === ts.SyntaxKind.FalseKeyword ? ["disabled"] : [];
|
|
178
|
+
if (name === "once")
|
|
179
|
+
return value.kind === ts.SyntaxKind.TrueKeyword ? ["once"] : [];
|
|
180
|
+
if (name === "lock")
|
|
181
|
+
return value.kind === ts.SyntaxKind.FalseKeyword ? ["unlocked"] : [];
|
|
182
|
+
if (name !== "serverMode" && name !== "operationMode")
|
|
183
|
+
return [];
|
|
184
|
+
const values = ts.isArrayLiteralExpression(value) ? [...value.elements] : [value];
|
|
185
|
+
return values.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : ts.isPropertyAccessExpression(element) ? [element.name.text] : []);
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
const internalKeysOf = (source, builder) => {
|
|
189
|
+
const verbOf = verbReaderOf(builder?.parameters[0]);
|
|
190
|
+
return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
191
|
+
if (!ts.isPropertyAssignment(property))
|
|
192
|
+
return [];
|
|
193
|
+
const name = ast.nodeName(property.name);
|
|
194
|
+
if (!name)
|
|
195
|
+
return [];
|
|
196
|
+
const { root } = chainOf(property.initializer);
|
|
197
|
+
const type = root ? verbOf(root.expression) : null;
|
|
198
|
+
const [first, second] = root?.arguments ?? [];
|
|
199
|
+
const schedule = type === "cron" && first ? ts.isStringLiteralLike(first) ? first.text : first.getText(source) : type === "interval" || type === "timeout" ? durationOf(first, source) : null;
|
|
200
|
+
const option = type === "initialize" || type === "destroy" ? first : second;
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
name,
|
|
204
|
+
span: spanOf(source, property.name),
|
|
205
|
+
extent: extentOf(source, property),
|
|
206
|
+
type,
|
|
207
|
+
schedule,
|
|
208
|
+
modes: modesOf(option)
|
|
209
|
+
}
|
|
210
|
+
];
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
const plugsOf = (source, builder) => {
|
|
214
|
+
const verbOf = verbReaderOf(builder?.parameters[0]);
|
|
215
|
+
return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
216
|
+
if (!ts.isPropertyAssignment(property))
|
|
217
|
+
return [];
|
|
218
|
+
const name = ast.nodeName(property.name);
|
|
219
|
+
const { root } = chainOf(property.initializer);
|
|
220
|
+
const [target] = root && verbOf(root.expression) === "plug" ? root.arguments : [];
|
|
221
|
+
const targetName = target && guardNameOf(target);
|
|
222
|
+
return name && targetName ? [{ name, span: spanOf(source, property.name), target: targetName }] : [];
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
const endpointKeysOf = (source, builder) => {
|
|
226
|
+
const verbOf = verbReaderOf(builder?.parameters[0]);
|
|
227
|
+
return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
|
|
228
|
+
if (!ts.isPropertyAssignment(property))
|
|
229
|
+
return [];
|
|
230
|
+
const name = ast.nodeName(property.name);
|
|
231
|
+
if (!name)
|
|
232
|
+
return [];
|
|
233
|
+
const { root, methods } = chainOf(property.initializer);
|
|
234
|
+
return [
|
|
235
|
+
{
|
|
236
|
+
name,
|
|
237
|
+
span: spanOf(source, property.name),
|
|
238
|
+
extent: extentOf(source, property),
|
|
239
|
+
verb: root ? verbOf(root.expression) : null,
|
|
240
|
+
guards: root ? guardsIn(optionObjectOf(root)) : [],
|
|
241
|
+
argCount: methods.filter((method) => argMethods.has(method)).length
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
const filterQueriesOf = (source, builder) => {
|
|
247
|
+
const query = returnedObject(builder)?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "query")?.initializer;
|
|
248
|
+
return query && ts.isObjectLiteralExpression(query) ? objectKeys(source, query) : [];
|
|
249
|
+
};
|
|
250
|
+
const typeOf = (expression) => {
|
|
251
|
+
const value = expression && unwrap(expression);
|
|
252
|
+
if (value && ts.isArrayLiteralExpression(value))
|
|
253
|
+
return { type: typeOf(value.elements[0]).type, array: true };
|
|
254
|
+
return { type: value ? ast.expressionName(value) : null, array: false };
|
|
255
|
+
};
|
|
256
|
+
const fieldsOf = (source, object) => (object?.properties ?? []).flatMap((property) => {
|
|
257
|
+
const name = ast.propertyName(property);
|
|
258
|
+
const node = keyName(property);
|
|
259
|
+
if (!name || !node)
|
|
260
|
+
return [];
|
|
261
|
+
const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
name,
|
|
265
|
+
span: spanOf(source, node),
|
|
266
|
+
extent: extentOf(source, property),
|
|
267
|
+
...typeOf(root?.arguments[0]),
|
|
268
|
+
optional: methods.includes("optional")
|
|
269
|
+
}
|
|
270
|
+
];
|
|
271
|
+
});
|
|
272
|
+
const constantOf = (source, name, call) => {
|
|
273
|
+
const parents = call.arguments.map(unwrap).filter((arg) => ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)).map((arg) => ast.expressionName(arg)).filter((parent) => parent !== null);
|
|
274
|
+
const fields = call.arguments.flatMap((arg) => fieldsOf(source, ast.firstObjectReturnedByArrow(arg)));
|
|
275
|
+
return { ...name, parents, fields };
|
|
276
|
+
};
|
|
277
|
+
const enumValuesOf = (call) => {
|
|
278
|
+
const list = call.arguments[1] && unwrap(call.arguments[1]);
|
|
279
|
+
if (!list || !ts.isArrayLiteralExpression(list))
|
|
280
|
+
return [];
|
|
281
|
+
return list.elements.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : []);
|
|
282
|
+
};
|
|
283
|
+
const methodsOf = (source, statement) => statement.members.flatMap((member) => {
|
|
284
|
+
if (!ts.isMethodDeclaration(member) || !member.body)
|
|
285
|
+
return [];
|
|
286
|
+
const name = ast.nodeName(member.name);
|
|
287
|
+
if (!name || ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))
|
|
288
|
+
return [];
|
|
289
|
+
return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
|
|
290
|
+
});
|
|
291
|
+
const importsOf = (statement) => {
|
|
292
|
+
const { importClause, moduleSpecifier } = statement;
|
|
293
|
+
if (!importClause || !ts.isStringLiteral(moduleSpecifier))
|
|
294
|
+
return [];
|
|
295
|
+
const from = moduleSpecifier.text;
|
|
296
|
+
const bindings = importClause.namedBindings;
|
|
297
|
+
return [
|
|
298
|
+
...importClause.name ? [{ name: importClause.name.text, imported: "default", from }] : [],
|
|
299
|
+
...bindings && ts.isNamespaceImport(bindings) ? [{ name: bindings.name.text, imported: "*", from }] : [],
|
|
300
|
+
...bindings && ts.isNamedImports(bindings) ? bindings.elements.map((element) => ({
|
|
301
|
+
name: element.name.text,
|
|
302
|
+
imported: (element.propertyName ?? element.name).text,
|
|
303
|
+
from
|
|
304
|
+
})) : []
|
|
305
|
+
];
|
|
306
|
+
};
|
|
307
|
+
const implementsGuard = (node, guardNames) => !!node.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ImplementsKeyword && clause.types.some(({ expression }) => guardNames.has(ast.expressionName(expression) ?? "")));
|
|
308
|
+
const guardClassIn = (node, guardNames) => {
|
|
309
|
+
if ((ts.isClassExpression(node) || ts.isClassDeclaration(node)) && implementsGuard(node, guardNames))
|
|
310
|
+
return node;
|
|
311
|
+
return ts.forEachChild(node, (child) => guardClassIn(child, guardNames)) ?? null;
|
|
312
|
+
};
|
|
313
|
+
const callsIn = (node, names) => {
|
|
314
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text))
|
|
315
|
+
return node.expression.text;
|
|
316
|
+
return ts.forEachChild(node, (child) => callsIn(child, names)) ?? null;
|
|
317
|
+
};
|
|
318
|
+
const scopeOf = (node) => {
|
|
319
|
+
const scope = node.members.find((member) => ts.isPropertyDeclaration(member) && ast.nodeName(member.name) === "scope" && !!ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))?.initializer;
|
|
320
|
+
const value = scope && unwrap(scope);
|
|
321
|
+
return value && ts.isStringLiteralLike(value) ? value.text : null;
|
|
322
|
+
};
|
|
323
|
+
const docOf = (source, node) => {
|
|
324
|
+
const { text } = source;
|
|
325
|
+
const ranges = ts.getLeadingCommentRanges(text, node.getFullStart()) ?? [];
|
|
326
|
+
const comments = [];
|
|
327
|
+
let end = node.getStart(source);
|
|
328
|
+
for (const range of [...ranges].reverse()) {
|
|
329
|
+
if ((text.slice(range.end, end).match(/\n/g)?.length ?? 0) > 1)
|
|
330
|
+
break;
|
|
331
|
+
comments.unshift(text.slice(range.pos, range.end));
|
|
332
|
+
end = range.pos;
|
|
333
|
+
}
|
|
334
|
+
const lines = comments.flatMap((comment) => comment.split(`
|
|
335
|
+
`)).map((line) => line.trim().replace(/^\/\*\*?|\*\/$/g, "").replace(/^(?:\/\/[?*!]?|\*)/, "").replace(/\{@link\s+([^}\s|]+)[^}]*\}/g, "$1").trim()).filter((line) => line && !/^(?:biome-ignore|eslint-|@ts-)/.test(line));
|
|
336
|
+
return lines.length ? lines.join(" ") : null;
|
|
337
|
+
};
|
|
338
|
+
const functionsOf = (source) => {
|
|
339
|
+
const functions = new Map;
|
|
340
|
+
for (const statement of source.statements) {
|
|
341
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body)
|
|
342
|
+
functions.set(statement.name.text, { statement, name: statement.name, body: statement.body });
|
|
343
|
+
if (!ts.isVariableStatement(statement))
|
|
344
|
+
continue;
|
|
345
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
346
|
+
const initializer = declaration.initializer && unwrap(declaration.initializer);
|
|
347
|
+
if (initializer && ts.isIdentifier(declaration.name) && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)))
|
|
348
|
+
functions.set(declaration.name.text, { statement, name: declaration.name, body: initializer.body });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return functions;
|
|
352
|
+
};
|
|
353
|
+
const guardFactoriesOf = (source, guardNames) => {
|
|
354
|
+
const functions = functionsOf(source);
|
|
355
|
+
const factories = new Map;
|
|
356
|
+
const factoryOf = (name, scope, via) => {
|
|
357
|
+
const found = functions.get(name);
|
|
358
|
+
if (!found)
|
|
359
|
+
return;
|
|
360
|
+
factories.set(name, {
|
|
361
|
+
name,
|
|
362
|
+
span: spanOf(source, found.name),
|
|
363
|
+
extent: extentOf(source, found.statement),
|
|
364
|
+
scope,
|
|
365
|
+
doc: docOf(source, found.statement),
|
|
366
|
+
via
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
for (const [name, { body }] of functions) {
|
|
370
|
+
const guard = guardClassIn(body, guardNames);
|
|
371
|
+
if (guard)
|
|
372
|
+
factoryOf(name, scopeOf(guard), null);
|
|
373
|
+
}
|
|
374
|
+
for (let isGrowing = true;isGrowing; ) {
|
|
375
|
+
isGrowing = false;
|
|
376
|
+
for (const [name, { body }] of functions) {
|
|
377
|
+
const called = factories.has(name) ? null : callsIn(body, new Set(factories.keys()));
|
|
378
|
+
if (!called)
|
|
379
|
+
continue;
|
|
380
|
+
factoryOf(name, factories.get(called)?.scope ?? null, called);
|
|
381
|
+
isGrowing = true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return factories;
|
|
385
|
+
};
|
|
386
|
+
const guardsOf = (source, imports) => {
|
|
387
|
+
const guardNames = new Set([
|
|
388
|
+
"Guard",
|
|
389
|
+
...imports.filter(({ imported }) => imported === "Guard").map(({ name }) => name)
|
|
390
|
+
]);
|
|
391
|
+
const imported = new Set(imports.map(({ name }) => name));
|
|
392
|
+
const factories = guardFactoriesOf(source, guardNames);
|
|
393
|
+
const guards = [];
|
|
394
|
+
const calls = [];
|
|
395
|
+
for (const statement of source.statements) {
|
|
396
|
+
if (ts.isClassDeclaration(statement) && statement.name && implementsGuard(statement, guardNames))
|
|
397
|
+
guards.push({
|
|
398
|
+
name: statement.name.text,
|
|
399
|
+
span: spanOf(source, statement.name),
|
|
400
|
+
extent: extentOf(source, statement),
|
|
401
|
+
scope: scopeOf(statement),
|
|
402
|
+
doc: docOf(source, statement),
|
|
403
|
+
factory: null
|
|
404
|
+
});
|
|
405
|
+
if (!ts.isVariableStatement(statement))
|
|
406
|
+
continue;
|
|
407
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
408
|
+
const initializer = declaration.initializer && unwrap(declaration.initializer);
|
|
409
|
+
if (!initializer || !ts.isIdentifier(declaration.name))
|
|
410
|
+
continue;
|
|
411
|
+
const made = {
|
|
412
|
+
name: declaration.name.text,
|
|
413
|
+
span: spanOf(source, declaration.name),
|
|
414
|
+
extent: extentOf(source, statement),
|
|
415
|
+
doc: docOf(source, statement)
|
|
416
|
+
};
|
|
417
|
+
if (ts.isClassExpression(initializer) && implementsGuard(initializer, guardNames))
|
|
418
|
+
guards.push({ ...made, scope: scopeOf(initializer), factory: null });
|
|
419
|
+
if (!ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression))
|
|
420
|
+
continue;
|
|
421
|
+
const callee = initializer.expression.text;
|
|
422
|
+
const factory = factories.get(callee);
|
|
423
|
+
if (factory)
|
|
424
|
+
guards.push({ ...made, scope: factory.scope, factory: callee });
|
|
425
|
+
else if (imported.has(callee))
|
|
426
|
+
calls.push({ ...made, callee });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return { guards, guardFactories: [...factories.values()], guardCalls: calls };
|
|
430
|
+
};
|
|
431
|
+
const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
432
|
+
const importsIn = (source) => source.statements.filter(ts.isImportDeclaration).flatMap(importsOf);
|
|
433
|
+
const read = (source, owner) => {
|
|
434
|
+
const imports = importsIn(source);
|
|
435
|
+
const facts = {
|
|
436
|
+
fileName: source.fileName,
|
|
437
|
+
owner,
|
|
438
|
+
imports,
|
|
439
|
+
...guardsOf(source, imports),
|
|
440
|
+
constants: [],
|
|
441
|
+
enums: [],
|
|
442
|
+
filters: [],
|
|
443
|
+
models: [],
|
|
444
|
+
services: [],
|
|
445
|
+
slices: [],
|
|
446
|
+
endpoints: [],
|
|
447
|
+
internals: [],
|
|
448
|
+
adaptors: [],
|
|
449
|
+
stores: []
|
|
450
|
+
};
|
|
451
|
+
for (const statement of source.statements) {
|
|
452
|
+
if (!ts.isClassDeclaration(statement) || !statement.name)
|
|
453
|
+
continue;
|
|
454
|
+
const heritage = ast.heritageCallNamed(statement, moduleCallees);
|
|
455
|
+
if (!heritage)
|
|
456
|
+
continue;
|
|
457
|
+
const { callee, call } = heritage;
|
|
458
|
+
const name = { name: statement.name.text, span: spanOf(source, statement.name) };
|
|
459
|
+
const builder = builderOf(call);
|
|
460
|
+
if (callee === "via")
|
|
461
|
+
facts.constants.push(constantOf(source, name, call));
|
|
462
|
+
else if (callee === "enumOf")
|
|
463
|
+
facts.enums.push({ ...name, values: enumValuesOf(call) });
|
|
464
|
+
else if (callee === "from")
|
|
465
|
+
facts.filters.push({ ...name, queries: filterQueriesOf(source, builder) });
|
|
466
|
+
else if (callee === "into")
|
|
467
|
+
facts.models.push({ ...name, model: name.name.endsWith("Model") ? lowerFirst(name.name.slice(0, -5)) : null });
|
|
468
|
+
else if (callee === "serve")
|
|
469
|
+
facts.services.push({
|
|
470
|
+
...name,
|
|
471
|
+
model: moduleRefOf(call.arguments[0], "db"),
|
|
472
|
+
methods: methodsOf(source, statement),
|
|
473
|
+
plugs: plugsOf(source, builder)
|
|
474
|
+
});
|
|
475
|
+
else if (callee === "slice")
|
|
476
|
+
facts.slices.push({
|
|
477
|
+
...name,
|
|
478
|
+
model: moduleRefOf(call.arguments[0], "srv"),
|
|
479
|
+
guards: guardsIn(call.arguments.find(ts.isObjectLiteralExpression)),
|
|
480
|
+
keys: sliceKeysOf(source, builder)
|
|
481
|
+
});
|
|
482
|
+
else if (callee === "internal")
|
|
483
|
+
facts.internals.push({
|
|
484
|
+
...name,
|
|
485
|
+
model: moduleRefOf(call.arguments[0], "srv"),
|
|
486
|
+
keys: internalKeysOf(source, builder)
|
|
487
|
+
});
|
|
488
|
+
else if (callee === "adapt" || callee === "dangerouslyAdapt") {
|
|
489
|
+
const refName = call.arguments[0] && unwrap(call.arguments[0]);
|
|
490
|
+
facts.adaptors.push({
|
|
491
|
+
...name,
|
|
492
|
+
extent: extentOf(source, statement),
|
|
493
|
+
refName: refName && ts.isStringLiteralLike(refName) ? refName.text : null,
|
|
494
|
+
plugs: plugsOf(source, builder),
|
|
495
|
+
doc: docOf(source, statement)
|
|
496
|
+
});
|
|
497
|
+
} else if (callee === "endpoint")
|
|
498
|
+
facts.endpoints.push({
|
|
499
|
+
...name,
|
|
500
|
+
model: moduleRefOf(call.arguments[0], "srv"),
|
|
501
|
+
keys: endpointKeysOf(source, builder)
|
|
502
|
+
});
|
|
503
|
+
else if (callee === "store")
|
|
504
|
+
facts.stores.push({
|
|
505
|
+
...name,
|
|
506
|
+
model: moduleRefOf(call.arguments[0], "sig"),
|
|
507
|
+
stateKeys: objectKeys(source, returnedObject(builder)),
|
|
508
|
+
actions: methodsOf(source, statement)
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
return facts;
|
|
512
|
+
};
|
|
513
|
+
return { read, spanOf, extentOf, docOf, importsIn, moduleRefOf, chainOf, unwrap };
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// pkgs/akanjs/common/trimSlashes.ts
|
|
517
|
+
var trimTrailingSlashes = (value) => {
|
|
518
|
+
let end = value.length;
|
|
519
|
+
while (end > 0 && value[end - 1] === "/")
|
|
520
|
+
end -= 1;
|
|
521
|
+
return value.slice(0, end);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// pkgs/akanjs/common/routeConvention.ts
|
|
525
|
+
var ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
|
|
526
|
+
var RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
|
|
527
|
+
var INTERNAL_ROOT_LAYOUT_LEAF = "__root_layout";
|
|
528
|
+
var IMPLICIT_LOCALE_SEGMENT = "[lang]";
|
|
529
|
+
var DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
|
|
530
|
+
var PAGE_ROUTE_EXPORTS = new Set([
|
|
531
|
+
"default",
|
|
532
|
+
"pageConfig",
|
|
533
|
+
"head",
|
|
534
|
+
"generateHead",
|
|
535
|
+
"Loading",
|
|
536
|
+
"guard"
|
|
537
|
+
]);
|
|
538
|
+
var LAYOUT_ROUTE_EXPORTS = new Set([...PAGE_ROUTE_EXPORTS, "NotFound", "Error"]);
|
|
539
|
+
var ROOT_LAYOUT_ROUTE_EXPORTS = new Set([
|
|
540
|
+
...LAYOUT_ROUTE_EXPORTS,
|
|
541
|
+
"fonts",
|
|
542
|
+
"manifest",
|
|
543
|
+
"theme",
|
|
544
|
+
"reconnect",
|
|
545
|
+
"wsConnect",
|
|
546
|
+
"layoutStyle",
|
|
547
|
+
"viewport",
|
|
548
|
+
"McpAppRoot"
|
|
549
|
+
]);
|
|
550
|
+
var RESERVED_ROUTE_CONFIG_EXPORTS = new Set([...ROOT_LAYOUT_ROUTE_EXPORTS].filter((name) => name !== "default" && !/^[A-Z]/.test(name)));
|
|
551
|
+
function parseRouteModuleKey(key) {
|
|
552
|
+
const match = ROUTE_SOURCE_RE.exec(key);
|
|
553
|
+
if (!match)
|
|
554
|
+
throw new Error(`[route-convention] invalid route module key: ${key}`);
|
|
555
|
+
const file = match[1];
|
|
556
|
+
const ext = match[2];
|
|
557
|
+
const moduleSegments = file.split("/").filter(Boolean);
|
|
558
|
+
const leaf = moduleSegments.at(-1);
|
|
559
|
+
if (!leaf)
|
|
560
|
+
throw new Error(`[route-convention] invalid route module key: ${key}`);
|
|
561
|
+
if (moduleSegments.includes(IMPLICIT_LOCALE_SEGMENT)) {
|
|
562
|
+
throw new Error(`[route-convention] Akan.js injects \`[lang]\` automatically. Move files one level up: ${key}`);
|
|
563
|
+
}
|
|
564
|
+
const isInternalRootLayout = leaf === INTERNAL_ROOT_LAYOUT_LEAF;
|
|
565
|
+
const kind = leaf === "_layout" || isInternalRootLayout ? "layout" : leaf === "_overrides" ? "overrides" : "page";
|
|
566
|
+
if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && !isInternalRootLayout) {
|
|
567
|
+
throw new Error(`[route-convention] unsupported reserved route file "${leaf}" in ${key}`);
|
|
568
|
+
}
|
|
569
|
+
const sourceRouteSegments = DIRECTORY_SCOPED_LEAVES.has(leaf) || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
|
|
570
|
+
const routeSegments = [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
|
|
571
|
+
for (const segment of routeSegments)
|
|
572
|
+
validateRouteSegment(segment, key);
|
|
573
|
+
return {
|
|
574
|
+
key,
|
|
575
|
+
kind,
|
|
576
|
+
routeSegments,
|
|
577
|
+
moduleSegments,
|
|
578
|
+
sourceRouteSegments,
|
|
579
|
+
ext,
|
|
580
|
+
leaf,
|
|
581
|
+
pattern: normalizeRoutePattern(routeSegments),
|
|
582
|
+
isInternalRootLayout
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function tryParseRouteModuleKey(key) {
|
|
586
|
+
try {
|
|
587
|
+
return parseRouteModuleKey(key);
|
|
588
|
+
} catch {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function normalizeRoutePattern(segments) {
|
|
593
|
+
const parts = segments.map(routeSegmentToPatternPart).filter((part) => part.length > 0);
|
|
594
|
+
return trimTrailingSlashes(`/${parts.join("/")}`) || "/";
|
|
595
|
+
}
|
|
596
|
+
function routeSegmentToPatternPart(segment) {
|
|
597
|
+
if (/^\(.+\)$/.test(segment))
|
|
598
|
+
return "";
|
|
599
|
+
return segment.replace(/^\[([^\]]+)\]$/, ":$1");
|
|
600
|
+
}
|
|
601
|
+
function validateRouteSegment(segment, key) {
|
|
602
|
+
if (segment.includes("[...") || segment.includes("[[...")) {
|
|
603
|
+
throw new Error(`[route-convention] catch-all routes are not supported yet: ${key}`);
|
|
604
|
+
}
|
|
605
|
+
const dynamic = /^\[(.+)\]$/.exec(segment);
|
|
606
|
+
if (dynamic && (dynamic[1]?.includes("[") || dynamic[1]?.includes("]") || dynamic[1]?.startsWith("..."))) {
|
|
607
|
+
throw new Error(`[route-convention] invalid dynamic route segment "${segment}" in ${key}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// pkgs/@akanjs/language/routeFacts.ts
|
|
612
|
+
class RouteFactReader {
|
|
613
|
+
static #pagePattern = /^((?:apps|libs)\/[^/]+)\/page\/((?!(?:.*\/)?\(libs\)\/).+\.tsx?)$/;
|
|
614
|
+
static #guardPattern = /\.guard\s*\(/;
|
|
615
|
+
static #aliasPattern = /^export default ([A-Za-z_$][\w$]*)\s*;?\s*$/m;
|
|
616
|
+
static #workspacePattern = /^(?:\.|@(?:apps|libs)\/)/;
|
|
617
|
+
#ts;
|
|
618
|
+
#reader;
|
|
619
|
+
#parse;
|
|
620
|
+
constructor(ts, reader, parse) {
|
|
621
|
+
this.#ts = ts;
|
|
622
|
+
this.#reader = reader;
|
|
623
|
+
this.#parse = parse;
|
|
624
|
+
}
|
|
625
|
+
static fileOf(relative) {
|
|
626
|
+
const match = relative.match(RouteFactReader.#pagePattern);
|
|
627
|
+
return match ? { owner: match[1], key: `./${match[2]}` } : null;
|
|
628
|
+
}
|
|
629
|
+
read(fileName, info, text, words) {
|
|
630
|
+
const ts = this.#ts;
|
|
631
|
+
const parsed = tryParseRouteModuleKey(info.key);
|
|
632
|
+
if (!parsed || parsed.kind === "overrides" || parsed.isInternalRootLayout)
|
|
633
|
+
return null;
|
|
634
|
+
const starts = [...text.matchAll(/^export default\b/gm)].map(({ index }) => index);
|
|
635
|
+
const source = RouteFactReader.#guardPattern.test(text) || starts.length > 1 ? this.#parse(fileName, text) : null;
|
|
636
|
+
const exported = source ? this.#exportOf(source) : null;
|
|
637
|
+
const start = exported ? exported.getStart(source ?? undefined) : starts[0] ?? 0;
|
|
638
|
+
const head = text.slice(0, start);
|
|
639
|
+
return {
|
|
640
|
+
fileName,
|
|
641
|
+
owner: info.owner,
|
|
642
|
+
kind: parsed.kind,
|
|
643
|
+
pattern: normalizeRoutePattern(parsed.sourceRouteSegments),
|
|
644
|
+
dir: parsed.moduleSegments.slice(0, -1),
|
|
645
|
+
isRoot: words.includes(`
|
|
646
|
+
rootLayout
|
|
647
|
+
`),
|
|
648
|
+
line: head.split(`
|
|
649
|
+
`).length - 1,
|
|
650
|
+
character: start - head.lastIndexOf(`
|
|
651
|
+
`) - 1,
|
|
652
|
+
guard: source && exported ? this.#guardOf(source, exported) : null,
|
|
653
|
+
imports: source ? this.#reader.importsIn(source) : [],
|
|
654
|
+
alias: source ? exported && ts.isExportAssignment(exported) && ts.isIdentifier(exported.expression) ? exported.expression.text : null : text.match(RouteFactReader.#aliasPattern)?.[1] ?? null
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
#exportOf(source) {
|
|
658
|
+
const ts = this.#ts;
|
|
659
|
+
return source.statements.find((statement) => ts.isExportAssignment(statement) || ts.canHaveModifiers(statement) && !!ts.getModifiers(statement)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword)) ?? null;
|
|
660
|
+
}
|
|
661
|
+
declarationIn(source, name) {
|
|
662
|
+
const ts = this.#ts;
|
|
663
|
+
for (const statement of source.statements) {
|
|
664
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name)
|
|
665
|
+
return { statement, declared: statement.name, initializer: null };
|
|
666
|
+
if (!ts.isVariableStatement(statement))
|
|
667
|
+
continue;
|
|
668
|
+
const declaration = statement.declarationList.declarations.find((item) => ts.isIdentifier(item.name) && item.name.text === name);
|
|
669
|
+
if (declaration)
|
|
670
|
+
return { statement, declared: declaration.name, initializer: declaration.initializer ?? null };
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
#guardOf(source, exported) {
|
|
675
|
+
const ts = this.#ts;
|
|
676
|
+
let current = ts.isExportAssignment(exported) ? this.#reader.unwrap(exported.expression) : null;
|
|
677
|
+
if (current && ts.isIdentifier(current)) {
|
|
678
|
+
const initializer = this.declarationIn(source, current.text)?.initializer;
|
|
679
|
+
current = initializer ? this.#reader.unwrap(initializer) : null;
|
|
680
|
+
}
|
|
681
|
+
while (current && ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
|
|
682
|
+
const { name, expression } = current.expression;
|
|
683
|
+
const [stage] = current.arguments;
|
|
684
|
+
if (name.text === "guard" && stage)
|
|
685
|
+
return { calls: this.#callsIn(source, stage), span: this.#reader.spanOf(source, name) };
|
|
686
|
+
current = this.#reader.unwrap(expression);
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
#callsIn(source, stage) {
|
|
691
|
+
const ts = this.#ts;
|
|
692
|
+
const imported = new Set(this.#reader.importsIn(source).filter(({ from }) => RouteFactReader.#workspacePattern.test(from)).map(({ name }) => name));
|
|
693
|
+
const calls = new Map;
|
|
694
|
+
const callOf = (name) => {
|
|
695
|
+
if (calls.has(name))
|
|
696
|
+
return true;
|
|
697
|
+
const declared = this.declarationIn(source, name);
|
|
698
|
+
if (!declared && !imported.has(name))
|
|
699
|
+
return false;
|
|
700
|
+
calls.set(name, {
|
|
701
|
+
name,
|
|
702
|
+
local: declared && {
|
|
703
|
+
span: this.#reader.spanOf(source, declared.declared),
|
|
704
|
+
extent: this.#reader.extentOf(source, declared.statement),
|
|
705
|
+
doc: this.#reader.docOf(source, declared.statement)
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
return true;
|
|
709
|
+
};
|
|
710
|
+
const visit = (node) => {
|
|
711
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && callOf(node.expression.text))
|
|
712
|
+
return;
|
|
713
|
+
ts.forEachChild(node, visit);
|
|
714
|
+
};
|
|
715
|
+
if (ts.isIdentifier(stage))
|
|
716
|
+
callOf(stage.text);
|
|
717
|
+
else
|
|
718
|
+
visit(stage);
|
|
719
|
+
return [...calls.values()];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// pkgs/@akanjs/language/akanGraph.ts
|
|
724
|
+
var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
725
|
+
var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
726
|
+
var memberSourcePattern = /\.(signal|service|store)\.ts$/;
|
|
727
|
+
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)$/;
|
|
728
|
+
var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
|
|
729
|
+
var moduleDirPattern = /\/lib\/([^/]+)\//;
|
|
730
|
+
var typeKinds = new Set(["model", "scalar", "enum"]);
|
|
731
|
+
var clientRoots = new Set(["fetch", "st", "sig"]);
|
|
732
|
+
var builtinGuards = {
|
|
733
|
+
Public: { scope: "account", doc: "Passes every caller, signed in or not." },
|
|
734
|
+
None: { scope: "account", doc: "Refuses every caller." }
|
|
735
|
+
};
|
|
736
|
+
var frameworkOwner = "akanjs";
|
|
737
|
+
var frameworkPattern = /^akanjs(?:\/|$)/;
|
|
738
|
+
var aliasPattern = /^@(apps|libs)\/([^/]+)/;
|
|
739
|
+
var maxMapDepth = 4;
|
|
740
|
+
|
|
741
|
+
class GraphBuilder {
|
|
742
|
+
#source;
|
|
743
|
+
#nodes = new Map;
|
|
744
|
+
#edges = new Map;
|
|
745
|
+
#contexts = new Map;
|
|
746
|
+
#members = new Map;
|
|
747
|
+
#guards = new Map;
|
|
748
|
+
#mapEntries = new Map;
|
|
749
|
+
#routeFiles = new Map;
|
|
750
|
+
#declarations = new Map;
|
|
751
|
+
#routes;
|
|
752
|
+
#adaptors = new Map;
|
|
753
|
+
#plugs = new Map;
|
|
754
|
+
constructor(source) {
|
|
755
|
+
this.#source = source;
|
|
756
|
+
this.#routes = new RouteFactReader(source.ts, source.reader, source.parse);
|
|
757
|
+
}
|
|
758
|
+
build() {
|
|
759
|
+
const { facts } = this.#source;
|
|
760
|
+
const constantsByOwner = new Map;
|
|
761
|
+
for (const file of facts)
|
|
762
|
+
constantsByOwner.set(file.owner, [...constantsByOwner.get(file.owner) ?? [], ...file.constants]);
|
|
763
|
+
const scopeOf = (owner) => [
|
|
764
|
+
...constantsByOwner.get(owner) ?? [],
|
|
765
|
+
...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
|
|
766
|
+
];
|
|
767
|
+
const typed = [];
|
|
768
|
+
for (const file of facts)
|
|
769
|
+
for (const fact of file.adaptors) {
|
|
770
|
+
const declared = { fact, owner: file.owner, fileName: file.fileName };
|
|
771
|
+
this.#adaptors.set(fact.name, [...this.#adaptors.get(fact.name) ?? [], declared]);
|
|
772
|
+
this.#adaptorNodeOf(declared);
|
|
773
|
+
}
|
|
774
|
+
for (const file of facts)
|
|
775
|
+
for (const fact of file.adaptors)
|
|
776
|
+
for (const plug of fact.plugs)
|
|
777
|
+
this.#addEdge(this.#key("adaptor", file.owner, fact.name), this.#adaptorIdOf(plug.target, file), "plugs", plug.name, this.#location(plug.span));
|
|
778
|
+
this.#declareGuards(facts);
|
|
779
|
+
for (const file of facts) {
|
|
780
|
+
const { owner } = file;
|
|
781
|
+
const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
|
|
782
|
+
const scalarClass = scalar ? file.constants.find((constant) => constant.name === capitalize(scalar)) : undefined;
|
|
783
|
+
if (scalar && scalarClass) {
|
|
784
|
+
const id = this.#key("scalar", owner, scalar);
|
|
785
|
+
const node = this.#addNode(id, "scalar", owner, scalar, scalarClass.name, scalarClass.span);
|
|
786
|
+
node.fields = this.#fieldsOf(scalarClass, scopeOf(owner));
|
|
787
|
+
typed.push(node);
|
|
788
|
+
}
|
|
789
|
+
for (const item of file.enums) {
|
|
790
|
+
const id = this.#key("enum", owner, item.name);
|
|
791
|
+
this.#addNode(id, "enum", owner, this.#moduleDirOf(file.fileName), item.name, item.span).values = item.values;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
for (const file of facts) {
|
|
795
|
+
const { owner } = file;
|
|
796
|
+
for (const service of file.services) {
|
|
797
|
+
const refName = service.model;
|
|
798
|
+
if (!refName)
|
|
799
|
+
continue;
|
|
800
|
+
const modelId = this.#key("model", owner, refName);
|
|
801
|
+
const modelClass = constantsByOwner.get(owner)?.find((constant) => constant.name === capitalize(refName));
|
|
802
|
+
if (modelClass && !this.#nodes.has(modelId)) {
|
|
803
|
+
const node = this.#addNode(modelId, "model", owner, refName, modelClass.name, modelClass.span);
|
|
804
|
+
node.fields = this.#fieldsOf(modelClass, scopeOf(owner));
|
|
805
|
+
typed.push(node);
|
|
806
|
+
}
|
|
807
|
+
for (const method of service.methods)
|
|
808
|
+
this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
|
|
809
|
+
for (const plug of service.plugs)
|
|
810
|
+
this.#plugs.set(`${owner ?? ""}
|
|
811
|
+
${refName}
|
|
812
|
+
${plug.name}`, this.#adaptorIdOf(plug.target, file));
|
|
813
|
+
}
|
|
814
|
+
for (const filter of file.filters) {
|
|
815
|
+
if (!filter.name.endsWith("Filter"))
|
|
816
|
+
continue;
|
|
817
|
+
const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
|
|
818
|
+
this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
|
|
819
|
+
for (const query of filter.queries)
|
|
820
|
+
this.#digest(this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span), this.#source.readText(file.fileName), query.extent);
|
|
821
|
+
}
|
|
822
|
+
for (const slice of file.slices) {
|
|
823
|
+
const refName = slice.model;
|
|
824
|
+
if (!refName)
|
|
825
|
+
continue;
|
|
826
|
+
const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
|
|
827
|
+
for (const guard of slice.guards)
|
|
828
|
+
this.#guard(fallback, guard, file);
|
|
829
|
+
for (const key of slice.keys) {
|
|
830
|
+
const detail = [...key.guards, ...key.live ? ["live"] : []];
|
|
831
|
+
const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
832
|
+
for (const guard of key.guards)
|
|
833
|
+
this.#guard(node, guard, file);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
for (const endpoint of file.endpoints) {
|
|
837
|
+
const refName = endpoint.model;
|
|
838
|
+
if (!refName)
|
|
839
|
+
continue;
|
|
840
|
+
for (const key of endpoint.keys) {
|
|
841
|
+
const detail = [...key.verb ? [key.verb] : [], ...key.guards];
|
|
842
|
+
const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
|
|
843
|
+
for (const guard of key.guards)
|
|
844
|
+
this.#guard(node, guard, file);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
for (const internal of file.internals)
|
|
848
|
+
for (const key of internal.keys)
|
|
849
|
+
if (internal.model && key.type !== "resolveField")
|
|
850
|
+
this.#addMember("internal", owner, internal.model, key, `${internal.name}.${key.name}`, file.fileName, [key.type, key.schedule, ...key.modes].filter((item) => item !== null));
|
|
851
|
+
for (const store of file.stores)
|
|
852
|
+
if (store.model)
|
|
853
|
+
for (const action of store.actions)
|
|
854
|
+
this.#addMember("action", owner, store.model, action, `${store.name}.${action.name}`, file.fileName);
|
|
855
|
+
}
|
|
856
|
+
const types = new Map;
|
|
857
|
+
for (const node of this.#nodes.values())
|
|
858
|
+
if (typeKinds.has(node.kind))
|
|
859
|
+
types.set(node.label, [...types.get(node.label) ?? [], node]);
|
|
860
|
+
for (const node of typed)
|
|
861
|
+
for (const field of node.fields ?? []) {
|
|
862
|
+
const candidates = (field.type ? types.get(field.type) ?? [] : []).filter((type) => this.#visible(type.owner, node.owner));
|
|
863
|
+
field.ref = (candidates.find((type) => type.owner === node.owner) ?? candidates[0])?.id ?? null;
|
|
864
|
+
if (field.ref)
|
|
865
|
+
this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
|
|
866
|
+
}
|
|
867
|
+
this.#addRoutes();
|
|
868
|
+
for (const [fileName, words] of this.#source.files)
|
|
869
|
+
this.#walk(fileName, words);
|
|
870
|
+
return {
|
|
871
|
+
version: 1,
|
|
872
|
+
nodes: [...this.#nodes.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
873
|
+
edges: [...this.#edges.values()]
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
#key(kind, owner, scope, name = "") {
|
|
877
|
+
return `${kind}:${owner ?? ""}:${scope}${name ? `.${name}` : ""}`;
|
|
878
|
+
}
|
|
879
|
+
#location(span) {
|
|
880
|
+
return { file: this.#source.relative(span.fileName), line: span.line + 1, column: span.character + 1 };
|
|
881
|
+
}
|
|
882
|
+
#addNode(id, kind, owner, module, name, span, detail = [], label = (kind === "query" || kind === "slice") && module ? `${module}${name ? `.${name}` : " (default)"}` : name) {
|
|
883
|
+
const existing = this.#nodes.get(id);
|
|
884
|
+
if (existing)
|
|
885
|
+
return existing;
|
|
886
|
+
const node = {
|
|
887
|
+
id,
|
|
888
|
+
kind,
|
|
889
|
+
name,
|
|
890
|
+
label,
|
|
891
|
+
owner,
|
|
892
|
+
module,
|
|
893
|
+
detail,
|
|
894
|
+
at: span ? this.#location(span) : null
|
|
895
|
+
};
|
|
896
|
+
this.#nodes.set(id, node);
|
|
897
|
+
return node;
|
|
898
|
+
}
|
|
899
|
+
#addMember(kind, owner, refName, member, label, fileName, detail = []) {
|
|
900
|
+
const id = this.#key(kind, owner, refName, member.name);
|
|
901
|
+
const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
|
|
902
|
+
this.#digest(node, this.#source.readText(fileName), member.extent);
|
|
903
|
+
this.#contexts.set(fileName, [
|
|
904
|
+
...this.#contexts.get(fileName) ?? [],
|
|
905
|
+
{ id, kind, owner, module: refName, extent: member.extent }
|
|
906
|
+
]);
|
|
907
|
+
const byName = `${kind}:${member.name}`;
|
|
908
|
+
this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
|
|
909
|
+
return node;
|
|
910
|
+
}
|
|
911
|
+
#hashOf(...parts) {
|
|
912
|
+
if (!this.#source.digests)
|
|
913
|
+
return null;
|
|
914
|
+
let hash = 2166136261;
|
|
915
|
+
for (const [text, { start, end }] of parts) {
|
|
916
|
+
if (text === undefined)
|
|
917
|
+
return null;
|
|
918
|
+
for (let idx = start;idx < end; idx++) {
|
|
919
|
+
const code = text.charCodeAt(idx);
|
|
920
|
+
if (code !== 13)
|
|
921
|
+
hash = Math.imul(hash ^ code, 16777619);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
925
|
+
}
|
|
926
|
+
#digest(node, text, extent) {
|
|
927
|
+
const digest = node.digest ? null : this.#hashOf([text, extent]);
|
|
928
|
+
if (digest)
|
|
929
|
+
node.digest = digest;
|
|
930
|
+
}
|
|
931
|
+
#addEdge(from, to, kind, label, at) {
|
|
932
|
+
if (from === to)
|
|
933
|
+
return;
|
|
934
|
+
const key = `${from}
|
|
935
|
+
${to}
|
|
936
|
+
${kind}
|
|
937
|
+
${label ?? ""}`;
|
|
938
|
+
const existing = this.#edges.get(key);
|
|
939
|
+
if (existing)
|
|
940
|
+
existing.count += 1;
|
|
941
|
+
else
|
|
942
|
+
this.#edges.set(key, { from, to, kind, label, count: 1, at });
|
|
943
|
+
}
|
|
944
|
+
#guard(node, guard, file) {
|
|
945
|
+
const [role, local] = guard.includes(" ") ? guard.split(" ") : [null, guard];
|
|
946
|
+
if (!local || local === "?")
|
|
947
|
+
return;
|
|
948
|
+
this.#addEdge(node.id, this.#guardIdOf(local, file), "guard", role, node.at);
|
|
949
|
+
}
|
|
950
|
+
#declareGuards(facts) {
|
|
951
|
+
const factories = new Map;
|
|
952
|
+
for (const file of facts)
|
|
953
|
+
for (const fact of file.guardFactories) {
|
|
954
|
+
const factory = { fact, owner: file.owner, fileName: file.fileName, made: [] };
|
|
955
|
+
factories.set(fact.name, [...factories.get(fact.name) ?? [], factory]);
|
|
956
|
+
}
|
|
957
|
+
const sameFile = (factory, name) => name ? factories.get(name)?.find((other) => other.fileName === factory.fileName) : undefined;
|
|
958
|
+
const declare = (guard, factory) => {
|
|
959
|
+
this.#guards.set(guard.name, [...this.#guards.get(guard.name) ?? [], guard]);
|
|
960
|
+
factory?.made.push(guard);
|
|
961
|
+
};
|
|
962
|
+
const madeBy = (factory, fileName, extent) => {
|
|
963
|
+
const parts = [{ fileName, extent }];
|
|
964
|
+
const seen = new Set;
|
|
965
|
+
for (let current = factory;current && !seen.has(current); current = sameFile(current, current.fact.via)) {
|
|
966
|
+
seen.add(current);
|
|
967
|
+
parts.push({ fileName: current.fileName, extent: current.fact.extent });
|
|
968
|
+
}
|
|
969
|
+
return parts;
|
|
970
|
+
};
|
|
971
|
+
for (const file of facts) {
|
|
972
|
+
const { owner, fileName } = file;
|
|
973
|
+
for (const { name, span, extent, scope, doc, factory: callee } of file.guards) {
|
|
974
|
+
const factory = callee ? factories.get(callee)?.find((other) => other.fileName === fileName) : undefined;
|
|
975
|
+
declare({ name, owner, span, scope, doc, parts: madeBy(factory, fileName, extent) }, factory);
|
|
976
|
+
}
|
|
977
|
+
for (const { name, span, extent, doc, callee } of file.guardCalls) {
|
|
978
|
+
const imported = file.imports.find((item) => item.name === callee);
|
|
979
|
+
if (!imported)
|
|
980
|
+
continue;
|
|
981
|
+
const from = this.#ownerOfSpecifier(fileName, imported.from);
|
|
982
|
+
const candidates = factories.get(imported.imported === "default" || imported.imported === "*" ? callee : imported.imported) ?? [];
|
|
983
|
+
const factory = candidates.find((other) => other.owner === from) ?? this.#soleVisible(candidates, from ?? owner);
|
|
984
|
+
if (factory)
|
|
985
|
+
declare({ name, owner, span, scope: factory.fact.scope, doc, parts: madeBy(factory, fileName, extent) }, factory);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
for (const list of factories.values())
|
|
989
|
+
for (const { fact, made } of list)
|
|
990
|
+
if (made.every((guard) => !guard.doc))
|
|
991
|
+
for (const guard of made)
|
|
992
|
+
guard.doc = fact.doc;
|
|
993
|
+
for (const list of this.#guards.values())
|
|
994
|
+
for (const guard of list)
|
|
995
|
+
this.#guardNodeOf(guard);
|
|
996
|
+
}
|
|
997
|
+
#guardIdOf(local, site) {
|
|
998
|
+
const found = this.#findGuard(local, site, 0);
|
|
999
|
+
if ("guard" in found)
|
|
1000
|
+
return this.#guardNodeOf(found.guard);
|
|
1001
|
+
const { name, from } = found;
|
|
1002
|
+
const isFramework = from === frameworkOwner || from === undefined && !!builtinGuards[name];
|
|
1003
|
+
const builtin = isFramework ? builtinGuards[name] : undefined;
|
|
1004
|
+
const owner = isFramework ? null : from ?? null;
|
|
1005
|
+
const detail = [...builtin ? [builtin.scope] : [], ...isFramework ? [frameworkOwner] : []];
|
|
1006
|
+
const node = this.#addNode(this.#key("guard", owner, name), "guard", owner, null, name, null, detail);
|
|
1007
|
+
if (builtin)
|
|
1008
|
+
node.doc = builtin.doc;
|
|
1009
|
+
return node.id;
|
|
1010
|
+
}
|
|
1011
|
+
#findGuard(local, site, depth) {
|
|
1012
|
+
const [head = local, member] = local.split(".");
|
|
1013
|
+
const imported = site.imports.find((item) => item.name === head);
|
|
1014
|
+
const from = imported ? this.#ownerOfSpecifier(site.fileName, imported.from) : undefined;
|
|
1015
|
+
const holder = imported ? from : site.owner;
|
|
1016
|
+
if (member && imported?.imported !== "*" && holder !== undefined && holder !== frameworkOwner && depth < maxMapDepth) {
|
|
1017
|
+
const objectName = imported && imported.imported !== "default" ? imported.imported : head;
|
|
1018
|
+
for (const entry of this.#mapEntriesOf(objectName, member, imported ? null : site.fileName, holder)) {
|
|
1019
|
+
const found = this.#findGuard(entry.name, entry.site, depth + 1);
|
|
1020
|
+
if ("guard" in found)
|
|
1021
|
+
return found;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const name = member ?? (imported && imported.imported !== "default" && imported.imported !== "*" ? imported.imported : head);
|
|
1025
|
+
const declared = this.#guards.get(name) ?? [];
|
|
1026
|
+
const guard = from === undefined ? declared.find((other) => other.owner === site.owner) ?? declared.find((other) => this.#visible(other.owner, site.owner)) : declared.find((other) => other.owner === from) ?? (from === frameworkOwner ? undefined : this.#soleVisible(declared, from));
|
|
1027
|
+
return guard ? { guard } : { name, from };
|
|
1028
|
+
}
|
|
1029
|
+
#mapEntriesOf(objectName, member, fileName, owner) {
|
|
1030
|
+
const key = `${objectName}
|
|
1031
|
+
${member}
|
|
1032
|
+
${fileName ?? ""}
|
|
1033
|
+
${owner ?? ""}`;
|
|
1034
|
+
const cached = this.#mapEntries.get(key);
|
|
1035
|
+
if (cached)
|
|
1036
|
+
return cached;
|
|
1037
|
+
const { ts, reader } = this.#source;
|
|
1038
|
+
const entries = [];
|
|
1039
|
+
const fileNames = [
|
|
1040
|
+
...fileName ? [fileName] : [],
|
|
1041
|
+
...this.#source.files.filter(([other, words]) => other !== fileName && owner !== null && !testFilePattern.test(other) && words.includes(`
|
|
1042
|
+
${objectName}
|
|
1043
|
+
`) && words.includes(`
|
|
1044
|
+
${member}
|
|
1045
|
+
`) && this.#source.ownerOf(other) === owner).map(([other]) => other)
|
|
1046
|
+
];
|
|
1047
|
+
for (const holder of fileNames) {
|
|
1048
|
+
const text = this.#source.readText(holder);
|
|
1049
|
+
if (text === undefined)
|
|
1050
|
+
continue;
|
|
1051
|
+
const source = this.#source.parse(holder, text);
|
|
1052
|
+
const site = { fileName: holder, owner: this.#source.ownerOf(holder), imports: reader.importsIn(source) };
|
|
1053
|
+
for (const statement of source.statements) {
|
|
1054
|
+
if (!ts.isVariableStatement(statement))
|
|
1055
|
+
continue;
|
|
1056
|
+
const map = statement.declarationList.declarations.find((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === objectName)?.initializer;
|
|
1057
|
+
const object = map && reader.unwrap(map);
|
|
1058
|
+
if (!object || !ts.isObjectLiteralExpression(object))
|
|
1059
|
+
continue;
|
|
1060
|
+
for (const property of object.properties) {
|
|
1061
|
+
if (ts.isShorthandPropertyAssignment(property) && property.name.text === member)
|
|
1062
|
+
entries.push({ name: member, site });
|
|
1063
|
+
if (!ts.isPropertyAssignment(property) || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || property.name.text !== member)
|
|
1064
|
+
continue;
|
|
1065
|
+
const value = reader.unwrap(property.initializer);
|
|
1066
|
+
if (ts.isIdentifier(value))
|
|
1067
|
+
entries.push({ name: value.text, site });
|
|
1068
|
+
else if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression))
|
|
1069
|
+
entries.push({ name: `${value.expression.text}.${value.name.text}`, site });
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
this.#mapEntries.set(key, entries);
|
|
1074
|
+
return entries;
|
|
1075
|
+
}
|
|
1076
|
+
#declarationOf(owner, name, near) {
|
|
1077
|
+
const key = `${owner}
|
|
1078
|
+
${name}
|
|
1079
|
+
${near ?? ""}`;
|
|
1080
|
+
const cached = this.#declarations.get(key);
|
|
1081
|
+
if (cached !== undefined)
|
|
1082
|
+
return cached;
|
|
1083
|
+
const isNear = (fileName) => {
|
|
1084
|
+
const relative = this.#source.relative(fileName);
|
|
1085
|
+
return !!near && (relative.startsWith(`${near}/`) || relative.replace(/\.tsx?$/, "") === near);
|
|
1086
|
+
};
|
|
1087
|
+
const candidates = this.#source.files.filter(([fileName, words]) => !testFilePattern.test(fileName) && words.includes(`
|
|
1088
|
+
${name}
|
|
1089
|
+
`) && this.#source.ownerOf(fileName) === owner).map(([fileName]) => fileName);
|
|
1090
|
+
let found = null;
|
|
1091
|
+
for (const fileName of [...candidates.filter(isNear), ...candidates.filter((other) => !isNear(other))]) {
|
|
1092
|
+
const text = this.#source.readText(fileName);
|
|
1093
|
+
if (text === undefined)
|
|
1094
|
+
continue;
|
|
1095
|
+
const source = this.#source.parse(fileName, text);
|
|
1096
|
+
const declared = this.#routes.declarationIn(source, name);
|
|
1097
|
+
if (!declared)
|
|
1098
|
+
continue;
|
|
1099
|
+
found = { source, statement: declared.statement, declared: declared.declared };
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
this.#declarations.set(key, found);
|
|
1103
|
+
return found;
|
|
1104
|
+
}
|
|
1105
|
+
#addRoutes() {
|
|
1106
|
+
const routes = [];
|
|
1107
|
+
const layouts = new Map;
|
|
1108
|
+
for (const [fileName, words] of this.#source.files) {
|
|
1109
|
+
const relative = this.#source.relative(fileName);
|
|
1110
|
+
const info = RouteFactReader.fileOf(relative);
|
|
1111
|
+
const text = info ? this.#source.readText(fileName) : undefined;
|
|
1112
|
+
const fact = info && text !== undefined ? this.#routes.read(fileName, info, text, words) : null;
|
|
1113
|
+
if (!fact || text === undefined)
|
|
1114
|
+
continue;
|
|
1115
|
+
const span = {
|
|
1116
|
+
fileName,
|
|
1117
|
+
start: 0,
|
|
1118
|
+
end: 0,
|
|
1119
|
+
line: fact.line,
|
|
1120
|
+
character: fact.character,
|
|
1121
|
+
endLine: 0,
|
|
1122
|
+
endCharacter: 0
|
|
1123
|
+
};
|
|
1124
|
+
const dir = `/${fact.dir.join("/")}`;
|
|
1125
|
+
const guard = fact.guard ? ["guard"] : [];
|
|
1126
|
+
const node = fact.kind === "layout" ? this.#addNode(this.#key("layout", fact.owner, dir), "layout", fact.owner, null, dir, span, [...fact.isRoot ? ["root"] : [], ...guard], `${dir === "/" ? "" : dir}/_layout`) : this.#addNode(this.#key("route", fact.owner, fact.pattern), "route", fact.owner, null, fact.pattern, span, guard);
|
|
1127
|
+
this.#digest(node, text, { start: 0, end: text.length });
|
|
1128
|
+
this.#routeFiles.set(relative, { id: node.id, alias: fact.alias });
|
|
1129
|
+
if (fact.kind === "layout")
|
|
1130
|
+
layouts.set(`${fact.owner}
|
|
1131
|
+
${dir}`, node);
|
|
1132
|
+
routes.push({ fact, node });
|
|
1133
|
+
for (const call of fact.guard?.calls ?? []) {
|
|
1134
|
+
const id = this.#routeGuardIdOf(call, fact, text);
|
|
1135
|
+
if (id && fact.guard)
|
|
1136
|
+
this.#addEdge(node.id, id, "guard", null, this.#location(fact.guard.span));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const parentOf = ({ owner, dir, kind }) => {
|
|
1140
|
+
for (let depth = kind === "layout" ? dir.length - 1 : dir.length;depth >= 0; depth--) {
|
|
1141
|
+
const layout = layouts.get(`${owner}
|
|
1142
|
+
/${dir.slice(0, depth).join("/")}`);
|
|
1143
|
+
if (layout)
|
|
1144
|
+
return layout;
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
};
|
|
1148
|
+
for (const { fact, node } of routes) {
|
|
1149
|
+
const parent = parentOf(fact);
|
|
1150
|
+
if (parent)
|
|
1151
|
+
this.#addEdge(parent.id, node.id, "wraps", null, node.at);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
#routeGuardIdOf({ name: local, local: declared }, fact, text) {
|
|
1155
|
+
if (declared) {
|
|
1156
|
+
const file = this.#source.relative(fact.fileName).slice(fact.owner.length + 1);
|
|
1157
|
+
const id = this.#key("guard", fact.owner, `${file}#${local}`);
|
|
1158
|
+
return this.#routeGuardNodeOf(id, fact.owner, local, declared.span, declared.doc, text, declared.extent);
|
|
1159
|
+
}
|
|
1160
|
+
const imported = fact.imports.find((item) => item.name === local);
|
|
1161
|
+
if (!imported)
|
|
1162
|
+
return null;
|
|
1163
|
+
const name = imported.imported !== "default" && imported.imported !== "*" ? imported.imported : local;
|
|
1164
|
+
const from = this.#ownerOfSpecifier(fact.fileName, imported.from);
|
|
1165
|
+
if (!from || from === frameworkOwner)
|
|
1166
|
+
return null;
|
|
1167
|
+
const found = this.#declarationOf(from, name, this.#pathOfSpecifier(fact.fileName, imported.from));
|
|
1168
|
+
if (!found)
|
|
1169
|
+
return null;
|
|
1170
|
+
const { reader } = this.#source;
|
|
1171
|
+
return this.#routeGuardNodeOf(this.#key("guard", from, name), from, name, reader.spanOf(found.source, found.declared), reader.docOf(found.source, found.statement), found.source.text, reader.extentOf(found.source, found.statement));
|
|
1172
|
+
}
|
|
1173
|
+
#routeGuardNodeOf(id, owner, name, span, doc, text, extent) {
|
|
1174
|
+
const isNew = !this.#nodes.has(id);
|
|
1175
|
+
const node = this.#addNode(id, "guard", owner, null, name, span, ["route"]);
|
|
1176
|
+
if (isNew && doc)
|
|
1177
|
+
node.doc = doc;
|
|
1178
|
+
this.#digest(node, text, extent);
|
|
1179
|
+
return node.id;
|
|
1180
|
+
}
|
|
1181
|
+
#adaptorIdOf(local, file) {
|
|
1182
|
+
const [head, member] = local.split(".");
|
|
1183
|
+
const imported = file.imports.find((item) => item.name === head);
|
|
1184
|
+
const from = imported ? this.#ownerOfSpecifier(file.fileName, imported.from) : undefined;
|
|
1185
|
+
const name = member ?? (imported && imported.imported !== "default" && imported.imported !== "*" ? imported.imported : head);
|
|
1186
|
+
const declared = this.#adaptors.get(name) ?? [];
|
|
1187
|
+
const found = from === undefined ? declared.find((adaptor) => adaptor.owner === file.owner) ?? declared.find((adaptor) => this.#visible(adaptor.owner, file.owner)) : declared.find((adaptor) => adaptor.owner === from) ?? (from === frameworkOwner ? undefined : declared.find((adaptor) => this.#visible(adaptor.owner, from)));
|
|
1188
|
+
if (found)
|
|
1189
|
+
return this.#adaptorNodeOf(found);
|
|
1190
|
+
const owner = from === frameworkOwner ? null : imported ? from ?? null : file.owner;
|
|
1191
|
+
const detail = from === frameworkOwner ? [frameworkOwner] : [];
|
|
1192
|
+
return this.#addNode(this.#key("adaptor", owner, name), "adaptor", owner, null, name, null, detail).id;
|
|
1193
|
+
}
|
|
1194
|
+
#adaptorNodeOf({ fact, owner, fileName }) {
|
|
1195
|
+
const id = this.#key("adaptor", owner, fact.name);
|
|
1196
|
+
const detail = fact.refName ? [fact.refName] : [];
|
|
1197
|
+
const node = this.#addNode(id, "adaptor", owner, null, fact.name, fact.span, detail);
|
|
1198
|
+
if (fact.doc && !node.doc)
|
|
1199
|
+
node.doc = fact.doc;
|
|
1200
|
+
this.#digest(node, this.#source.readText(fileName), fact.extent);
|
|
1201
|
+
return node.id;
|
|
1202
|
+
}
|
|
1203
|
+
#guardNodeOf({ name, owner, span, scope, doc, parts }) {
|
|
1204
|
+
const node = this.#addNode(this.#key("guard", owner, name), "guard", owner, null, name, span, scope ? [scope] : []);
|
|
1205
|
+
if (doc && !node.doc)
|
|
1206
|
+
node.doc = doc;
|
|
1207
|
+
const digest = node.digest ? null : this.#hashOf(...parts.map(({ fileName, extent }) => [
|
|
1208
|
+
this.#source.readText(fileName),
|
|
1209
|
+
extent
|
|
1210
|
+
]));
|
|
1211
|
+
if (digest)
|
|
1212
|
+
node.digest = digest;
|
|
1213
|
+
return node.id;
|
|
1214
|
+
}
|
|
1215
|
+
#ownerOfSpecifier(fileName, specifier) {
|
|
1216
|
+
if (frameworkPattern.test(specifier))
|
|
1217
|
+
return frameworkOwner;
|
|
1218
|
+
const path = this.#pathOfSpecifier(fileName, specifier);
|
|
1219
|
+
return path === null ? undefined : ownerOf(path) ?? undefined;
|
|
1220
|
+
}
|
|
1221
|
+
#pathOfSpecifier(fileName, specifier) {
|
|
1222
|
+
const alias = specifier.match(aliasPattern);
|
|
1223
|
+
if (alias)
|
|
1224
|
+
return `${alias[1]}/${alias[2]}${specifier.slice(alias[0].length)}`;
|
|
1225
|
+
if (!specifier.startsWith("."))
|
|
1226
|
+
return null;
|
|
1227
|
+
const segments = this.#source.relative(fileName).split("/").slice(0, -1);
|
|
1228
|
+
for (const segment of specifier.split("/"))
|
|
1229
|
+
if (segment === "..")
|
|
1230
|
+
segments.pop();
|
|
1231
|
+
else if (segment !== ".")
|
|
1232
|
+
segments.push(segment);
|
|
1233
|
+
return segments.join("/");
|
|
1234
|
+
}
|
|
1235
|
+
#moduleDirOf(fileName) {
|
|
1236
|
+
const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
|
|
1237
|
+
return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
|
|
1238
|
+
}
|
|
1239
|
+
#fieldsOf(constant, scope) {
|
|
1240
|
+
const fields = new Map;
|
|
1241
|
+
const seen = new Set;
|
|
1242
|
+
const visit = (current) => {
|
|
1243
|
+
if (seen.has(current))
|
|
1244
|
+
return;
|
|
1245
|
+
seen.add(current);
|
|
1246
|
+
for (const parent of current.parents) {
|
|
1247
|
+
const parentClass = scope.find((other) => other.name === parent && other.span.fileName === current.span.fileName) ?? scope.find((other) => other.name === parent);
|
|
1248
|
+
if (parentClass)
|
|
1249
|
+
visit(parentClass);
|
|
1250
|
+
}
|
|
1251
|
+
for (const field of current.fields) {
|
|
1252
|
+
if (fields.has(field.name))
|
|
1253
|
+
continue;
|
|
1254
|
+
const digest = this.#hashOf([this.#source.readText(field.span.fileName), field.extent]);
|
|
1255
|
+
fields.set(field.name, {
|
|
1256
|
+
name: field.name,
|
|
1257
|
+
type: field.type,
|
|
1258
|
+
array: field.array,
|
|
1259
|
+
optional: field.optional,
|
|
1260
|
+
ref: null,
|
|
1261
|
+
declaredIn: current.name,
|
|
1262
|
+
at: this.#location(field.span),
|
|
1263
|
+
...digest ? { digest } : {}
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
visit(constant);
|
|
1268
|
+
return [...fields.values()];
|
|
1269
|
+
}
|
|
1270
|
+
#visible(target, from) {
|
|
1271
|
+
return !from || !target || target === from || target.startsWith("libs/");
|
|
1272
|
+
}
|
|
1273
|
+
#soleVisible(declared, from) {
|
|
1274
|
+
const visible = declared.filter((other) => this.#visible(other.owner, from));
|
|
1275
|
+
return visible.length === 1 ? visible[0] : undefined;
|
|
1276
|
+
}
|
|
1277
|
+
#targetNodeOf({ family, owner, refName, sourceKey }) {
|
|
1278
|
+
const isDefault = /^[A-Z]/.test(sourceKey);
|
|
1279
|
+
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;
|
|
1280
|
+
return id && this.#nodes.has(id) ? id : null;
|
|
1281
|
+
}
|
|
1282
|
+
#membersNamed(kind, name, owner, refName) {
|
|
1283
|
+
const nodes = (this.#members.get(`${kind}:${name}`) ?? []).filter((node) => (!refName || node.module === refName) && this.#visible(node.owner, owner));
|
|
1284
|
+
const own = nodes.filter((node) => node.owner === owner);
|
|
1285
|
+
return (own.length ? own : nodes).map((node) => node.id);
|
|
1286
|
+
}
|
|
1287
|
+
#plainTargetsOf(chain, name, context, owner) {
|
|
1288
|
+
const [head, second] = chain ?? [];
|
|
1289
|
+
if (chain?.length === 1 && head === "fetch")
|
|
1290
|
+
return this.#membersNamed("endpoint", name, owner);
|
|
1291
|
+
if (chain?.length === 2 && head === "st" && second === "do")
|
|
1292
|
+
return this.#membersNamed("action", name, owner);
|
|
1293
|
+
if (chain?.length === 1 && head === "this" && (context.kind === "method" || context.kind === "action"))
|
|
1294
|
+
return this.#membersNamed(context.kind, name, context.owner, context.module).filter((id) => id.startsWith(`${context.kind}:${context.owner ?? ""}:`));
|
|
1295
|
+
if (chain?.length === 2 && head === "this" && second && /.Service$/.test(second))
|
|
1296
|
+
return this.#membersNamed("method", name, owner, second.slice(0, -"Service".length));
|
|
1297
|
+
return [];
|
|
1298
|
+
}
|
|
1299
|
+
#walk(fileName, words) {
|
|
1300
|
+
const relative = this.#source.relative(fileName);
|
|
1301
|
+
const contexts = this.#contexts.get(fileName);
|
|
1302
|
+
const isComponentFile = !memberSourcePattern.test(fileName) && !skippedPattern.test(relative) && (words.includes(`
|
|
1303
|
+
fetch
|
|
1304
|
+
`) || words.includes(`
|
|
1305
|
+
st
|
|
1306
|
+
`));
|
|
1307
|
+
if (!contexts && !isComponentFile)
|
|
1308
|
+
return;
|
|
1309
|
+
const text = this.#source.readText(fileName);
|
|
1310
|
+
if (text === undefined)
|
|
1311
|
+
return;
|
|
1312
|
+
const { ts } = this.#source;
|
|
1313
|
+
const owner = this.#source.ownerOf(fileName);
|
|
1314
|
+
const source = this.#source.parse(fileName, text);
|
|
1315
|
+
const visit = (node) => {
|
|
1316
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
|
|
1317
|
+
const start = node.name.getStart(source);
|
|
1318
|
+
const chain = this.#chainOf(node.expression);
|
|
1319
|
+
const context = contexts ? contexts.find(({ extent }) => extent.start <= start && start < extent.end) : chain && clientRoots.has(chain[0]) ? this.#componentOf(source, node) : undefined;
|
|
1320
|
+
if (context)
|
|
1321
|
+
this.#link(fileName, source, node.name, chain, context, owner);
|
|
1322
|
+
}
|
|
1323
|
+
ts.forEachChild(node, visit);
|
|
1324
|
+
};
|
|
1325
|
+
visit(source);
|
|
1326
|
+
}
|
|
1327
|
+
#link(fileName, source, identifier, chain, context, owner) {
|
|
1328
|
+
const plugged = chain?.length === 2 && chain[0] === "this" && context.kind === "method" && this.#isServiceThis(identifier) ? this.#plugs.get(`${context.owner ?? ""}
|
|
1329
|
+
${context.module}
|
|
1330
|
+
${chain[1]}`) : undefined;
|
|
1331
|
+
const resolved = plugged ? [plugged] : this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
|
|
1332
|
+
const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
|
|
1333
|
+
if (!targets.length)
|
|
1334
|
+
return;
|
|
1335
|
+
if (context.pending && !this.#nodes.has(context.id)) {
|
|
1336
|
+
this.#nodes.set(context.id, context.pending);
|
|
1337
|
+
this.#digest(context.pending, source.text, context.extent);
|
|
1338
|
+
}
|
|
1339
|
+
const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
|
|
1340
|
+
const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
|
|
1341
|
+
const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
|
|
1342
|
+
const at = { file: this.#source.relative(fileName), line: position.line + 1, column: position.character + 1 };
|
|
1343
|
+
for (const target of new Set(own.length ? own : targets))
|
|
1344
|
+
this.#addEdge(context.id, target, kind, identifier.text, at);
|
|
1345
|
+
}
|
|
1346
|
+
#isServiceThis(identifier) {
|
|
1347
|
+
const { ts } = this.#source;
|
|
1348
|
+
for (let current = identifier.parent;!ts.isSourceFile(current); current = current.parent) {
|
|
1349
|
+
if (ts.isMethodDeclaration(current) || ts.isAccessor(current) || ts.isConstructorDeclaration(current))
|
|
1350
|
+
return ts.isClassDeclaration(current.parent) && ts.isSourceFile(current.parent.parent);
|
|
1351
|
+
if (ts.isFunctionExpression(current) || ts.isFunctionDeclaration(current) || ts.isClassLike(current))
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
#chainOf(expression) {
|
|
1357
|
+
const { ts } = this.#source;
|
|
1358
|
+
if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
1359
|
+
return this.#chainOf(expression.expression);
|
|
1360
|
+
if (ts.isIdentifier(expression))
|
|
1361
|
+
return [expression.text];
|
|
1362
|
+
if (expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
1363
|
+
return ["this"];
|
|
1364
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
1365
|
+
return null;
|
|
1366
|
+
const head = this.#chainOf(expression.expression);
|
|
1367
|
+
return head ? [...head, expression.name.text] : null;
|
|
1368
|
+
}
|
|
1369
|
+
#componentOf(source, node) {
|
|
1370
|
+
const { ts } = this.#source;
|
|
1371
|
+
let top = node;
|
|
1372
|
+
while (top.parent && !ts.isSourceFile(top.parent))
|
|
1373
|
+
top = top.parent;
|
|
1374
|
+
const declared = ts.isVariableStatement(top) ? top.declarationList.declarations[0]?.name : ts.isFunctionDeclaration(top) || ts.isClassDeclaration(top) ? top.name : undefined;
|
|
1375
|
+
const isDefaultExport = ts.canHaveModifiers(top) && !!ts.getModifiers(top)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword);
|
|
1376
|
+
const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
|
|
1377
|
+
const relative = this.#source.relative(source.fileName);
|
|
1378
|
+
const owner = this.#source.ownerOf(source.fileName);
|
|
1379
|
+
const extent = { start: top.getStart(source), end: top.getEnd() };
|
|
1380
|
+
const routeFile = this.#routeFiles.get(relative);
|
|
1381
|
+
const routeId = routeFile && (name === "default" || name === routeFile.alias) ? routeFile.id : undefined;
|
|
1382
|
+
if (routeId)
|
|
1383
|
+
return { id: routeId, kind: "component", owner, module: null, extent };
|
|
1384
|
+
const id = `component:${relative}#${name}`;
|
|
1385
|
+
const context = { id, kind: "component", owner, module: null, extent };
|
|
1386
|
+
if (this.#nodes.has(id))
|
|
1387
|
+
return context;
|
|
1388
|
+
const pagePrefix = owner ? `${owner}/page/` : null;
|
|
1389
|
+
const route = pagePrefix && relative.startsWith(pagePrefix) ? relative.slice(pagePrefix.length).replace(/\.tsx?$/, "") : null;
|
|
1390
|
+
const base = relative.slice(relative.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
|
|
1391
|
+
const label = route ? `${route}${name === "default" ? "" : `#${name}`}` : name === "default" ? base : `${base}.${name}`;
|
|
1392
|
+
const position = source.getLineAndCharacterOfPosition((declared ?? top).getStart(source));
|
|
1393
|
+
context.pending = {
|
|
1394
|
+
id,
|
|
1395
|
+
kind: "component",
|
|
1396
|
+
name,
|
|
1397
|
+
label,
|
|
1398
|
+
owner,
|
|
1399
|
+
module: this.#moduleDirOf(source.fileName),
|
|
1400
|
+
detail: route ? ["page"] : [],
|
|
1401
|
+
at: { file: relative, line: position.line + 1, column: position.character + 1 }
|
|
1402
|
+
};
|
|
1403
|
+
return context;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// pkgs/@akanjs/language/ast.ts
|
|
1408
|
+
var createAst = (ts) => {
|
|
1409
|
+
const sourceFileFor = (fileName, content, scriptKind = ts.ScriptKind.TS) => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
1410
|
+
const nodeName = (node) => {
|
|
1411
|
+
if (!node)
|
|
1412
|
+
return null;
|
|
1413
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node))
|
|
1414
|
+
return node.text;
|
|
1415
|
+
return null;
|
|
1416
|
+
};
|
|
1417
|
+
const propertyName = (node) => ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isMethodDeclaration(node) ? nodeName(node.name) : null;
|
|
1418
|
+
const expressionName = (expression) => {
|
|
1419
|
+
if (ts.isIdentifier(expression))
|
|
1420
|
+
return expression.text;
|
|
1421
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
1422
|
+
return expression.name.text;
|
|
1423
|
+
if (ts.isCallExpression(expression))
|
|
1424
|
+
return expressionName(expression.expression);
|
|
1425
|
+
if (ts.isAsExpression(expression))
|
|
1426
|
+
return expressionName(expression.expression);
|
|
1427
|
+
return null;
|
|
1428
|
+
};
|
|
1429
|
+
const callExpressionName = (node) => ts.isPropertyAccessExpression(node.expression) ? node.expression.name.text : expressionName(node.expression);
|
|
1430
|
+
const firstObjectReturnedByArrow = (node) => {
|
|
1431
|
+
if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node))
|
|
1432
|
+
return null;
|
|
1433
|
+
if (ts.isObjectLiteralExpression(node.body))
|
|
1434
|
+
return node.body;
|
|
1435
|
+
if (ts.isParenthesizedExpression(node.body) && ts.isObjectLiteralExpression(node.body.expression)) {
|
|
1436
|
+
return node.body.expression;
|
|
1437
|
+
}
|
|
1438
|
+
if (!ts.isBlock(node.body))
|
|
1439
|
+
return null;
|
|
1440
|
+
for (const statement of node.body.statements) {
|
|
1441
|
+
if (ts.isReturnStatement(statement) && statement.expression && ts.isObjectLiteralExpression(statement.expression)) {
|
|
1442
|
+
return statement.expression;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return null;
|
|
1446
|
+
};
|
|
1447
|
+
const heritageCallNamed = (node, names) => {
|
|
1448
|
+
const heritage = node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
|
|
1449
|
+
for (const { expression } of heritage) {
|
|
1450
|
+
if (!ts.isCallExpression(expression))
|
|
1451
|
+
continue;
|
|
1452
|
+
const callee = expressionName(expression.expression);
|
|
1453
|
+
if (callee && names.has(callee))
|
|
1454
|
+
return { callee, call: expression };
|
|
1455
|
+
}
|
|
1456
|
+
return null;
|
|
1457
|
+
};
|
|
1458
|
+
const viaNames = new Set(["via"]);
|
|
1459
|
+
const heritageCall = (node) => heritageCallNamed(node, viaNames)?.call ?? null;
|
|
1460
|
+
return {
|
|
1461
|
+
sourceFileFor,
|
|
1462
|
+
nodeName,
|
|
1463
|
+
propertyName,
|
|
1464
|
+
expressionName,
|
|
1465
|
+
callExpressionName,
|
|
1466
|
+
firstObjectReturnedByArrow,
|
|
1467
|
+
heritageCallNamed,
|
|
1468
|
+
heritageCall
|
|
1469
|
+
};
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// pkgs/akanjs/common/capitalize.ts
|
|
1473
|
+
var capitalize2 = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
1474
|
+
var lowerlize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
|
|
1475
|
+
|
|
1476
|
+
// pkgs/akanjs/common/akanNames.ts
|
|
1477
|
+
var sliceNames = (refName, suffix = "") => {
|
|
1478
|
+
const [Model, Suffix] = [capitalize2(refName), capitalize2(suffix)];
|
|
1479
|
+
const live = `${refName}Live${Suffix}`;
|
|
1480
|
+
return {
|
|
1481
|
+
slice: `${refName}${Suffix}`,
|
|
1482
|
+
state: {
|
|
1483
|
+
defaultModel: `default${Model}${Suffix}`,
|
|
1484
|
+
modelInitList: `${refName}InitList${Suffix}`,
|
|
1485
|
+
modelInsight: `${refName}Insight${Suffix}`,
|
|
1486
|
+
modelList: `${refName}List${Suffix}`,
|
|
1487
|
+
modelListLoading: `${refName}ListLoading${Suffix}`,
|
|
1488
|
+
modelInitAt: `${refName}InitAt${Suffix}`,
|
|
1489
|
+
modelStaleAt: `${refName}StaleAt${Suffix}`,
|
|
1490
|
+
lastPageOfModel: `lastPageOf${Model}${Suffix}`,
|
|
1491
|
+
pageOfModel: `pageOf${Model}${Suffix}`,
|
|
1492
|
+
limitOfModel: `limitOf${Model}${Suffix}`,
|
|
1493
|
+
hasMoreOfModel: `hasMoreOf${Model}${Suffix}`,
|
|
1494
|
+
isCumulativeOfModel: `isCumulativeOf${Model}${Suffix}`,
|
|
1495
|
+
hasInsightOfModel: `hasInsightOf${Model}${Suffix}`,
|
|
1496
|
+
queryArgsOfModel: `queryArgsOf${Model}${Suffix}`,
|
|
1497
|
+
sortOfModel: `sortOf${Model}${Suffix}`,
|
|
1498
|
+
modelSelection: `${refName}Selection${Suffix}`
|
|
1499
|
+
},
|
|
1500
|
+
action: {
|
|
1501
|
+
initModel: `init${Model}${Suffix}`,
|
|
1502
|
+
refreshModel: `refresh${Model}${Suffix}`,
|
|
1503
|
+
selectModel: `select${Model}${Suffix}`,
|
|
1504
|
+
setPageOfModel: `setPageOf${Model}${Suffix}`,
|
|
1505
|
+
loadMoreOfModel: `loadMoreOf${Model}${Suffix}`,
|
|
1506
|
+
setLimitOfModel: `setLimitOf${Model}${Suffix}`,
|
|
1507
|
+
setQueryArgsOfModel: `setQueryArgsOf${Model}${Suffix}`,
|
|
1508
|
+
setSortOfModel: `setSortOf${Model}${Suffix}`,
|
|
1509
|
+
applyLiveModel: `applyLive${Model}${Suffix}`,
|
|
1510
|
+
watchLiveModel: `watchLive${Model}${Suffix}`
|
|
1511
|
+
},
|
|
1512
|
+
endpoint: {
|
|
1513
|
+
list: `${refName}List${Suffix}`,
|
|
1514
|
+
insight: `${refName}Insight${Suffix}`,
|
|
1515
|
+
live,
|
|
1516
|
+
subscribeLive: endpointNames(live).subscribe,
|
|
1517
|
+
init: `init${Model}${Suffix}`,
|
|
1518
|
+
getInit: `get${Model}Init${Suffix}`,
|
|
1519
|
+
initResult: `${refName}Init${Suffix}`
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
};
|
|
1523
|
+
var queryNames = (queryKey) => {
|
|
1524
|
+
const Key = capitalize2(queryKey);
|
|
1525
|
+
return {
|
|
1526
|
+
list: `list${Key}`,
|
|
1527
|
+
listIds: `listIds${Key}`,
|
|
1528
|
+
find: `find${Key}`,
|
|
1529
|
+
findId: `findId${Key}`,
|
|
1530
|
+
pick: `pick${Key}`,
|
|
1531
|
+
pickId: `pickId${Key}`,
|
|
1532
|
+
exists: `exists${Key}`,
|
|
1533
|
+
count: `count${Key}`,
|
|
1534
|
+
insight: `insight${Key}`,
|
|
1535
|
+
query: `query${Key}`,
|
|
1536
|
+
remove: `remove${Key}`,
|
|
1537
|
+
removeOne: `removeOne${Key}`,
|
|
1538
|
+
purge: `purge${Key}`,
|
|
1539
|
+
update: `update${Key}`,
|
|
1540
|
+
updateOne: `updateOne${Key}`
|
|
1541
|
+
};
|
|
1542
|
+
};
|
|
1543
|
+
var databaseStateNames = (refName) => {
|
|
1544
|
+
const className = capitalize2(refName);
|
|
1545
|
+
return {
|
|
1546
|
+
model: refName,
|
|
1547
|
+
modelLoading: `${refName}Loading`,
|
|
1548
|
+
modelForm: `${refName}Form`,
|
|
1549
|
+
modelFormLoading: `${refName}FormLoading`,
|
|
1550
|
+
modelFormUploading: `${refName}FormUploading`,
|
|
1551
|
+
modelSubmit: `${refName}Submit`,
|
|
1552
|
+
modelViewAt: `${refName}ViewAt`,
|
|
1553
|
+
modelModal: `${refName}Modal`,
|
|
1554
|
+
modelDraft: `${refName}FormDraft`,
|
|
1555
|
+
modelOperation: `${refName}Operation`,
|
|
1556
|
+
defaultModel: `default${className}`
|
|
1557
|
+
};
|
|
1558
|
+
};
|
|
1559
|
+
var modelNames = (refName) => {
|
|
1560
|
+
const Model = capitalize2(refName);
|
|
1561
|
+
return {
|
|
1562
|
+
...databaseStateNames(refName),
|
|
1563
|
+
getModel: `get${Model}`,
|
|
1564
|
+
loadModel: `load${Model}`,
|
|
1565
|
+
loadModelMany: `load${Model}Many`,
|
|
1566
|
+
createModel: `create${Model}`,
|
|
1567
|
+
createModelMany: `create${Model}Many`,
|
|
1568
|
+
updateModel: `update${Model}`,
|
|
1569
|
+
removeModel: `remove${Model}`,
|
|
1570
|
+
modelModel: `${refName}Model`,
|
|
1571
|
+
modelLoader: `${refName}Loader`,
|
|
1572
|
+
modelCache: `${refName}Cache`,
|
|
1573
|
+
modelId: `${refName}Id`,
|
|
1574
|
+
lightModel: `light${Model}`,
|
|
1575
|
+
viewModel: `view${Model}`,
|
|
1576
|
+
getModelView: `get${Model}View`,
|
|
1577
|
+
modelView: `${refName}View`,
|
|
1578
|
+
editModel: `edit${Model}`,
|
|
1579
|
+
getModelEdit: `get${Model}Edit`,
|
|
1580
|
+
modelEdit: `${refName}Edit`,
|
|
1581
|
+
modelObj: `${refName}Obj`,
|
|
1582
|
+
modelObjList: `${refName}ObjList`,
|
|
1583
|
+
modelObjInsight: `${refName}ObjInsight`,
|
|
1584
|
+
addModelFiles: `add${Model}Files`,
|
|
1585
|
+
mergeModel: `merge${Model}`,
|
|
1586
|
+
createModelInForm: `create${Model}InForm`,
|
|
1587
|
+
updateModelInForm: `update${Model}InForm`,
|
|
1588
|
+
checkModelSubmitable: `check${Model}Submitable`,
|
|
1589
|
+
submitModel: `submit${Model}`,
|
|
1590
|
+
newModel: `new${Model}`,
|
|
1591
|
+
setModel: `set${Model}`,
|
|
1592
|
+
resetModel: `reset${Model}`,
|
|
1593
|
+
invalidateModel: `invalidate${Model}`,
|
|
1594
|
+
loadModelFormDraft: `load${Model}FormDraft`,
|
|
1595
|
+
restoreModelFormDraft: `restore${Model}FormDraft`,
|
|
1596
|
+
discardModelFormDraft: `discard${Model}FormDraft`,
|
|
1597
|
+
writeOnModel: `writeOn${Model}`,
|
|
1598
|
+
setViewOfModel: `setViewOf${Model}`,
|
|
1599
|
+
exportCsvOfModel: `exportCsvOf${Model}`,
|
|
1600
|
+
exportJsonOfModel: `exportJsonOf${Model}`
|
|
1601
|
+
};
|
|
1602
|
+
};
|
|
1603
|
+
var moduleNames = (refName) => {
|
|
1604
|
+
const Model = capitalize2(refName);
|
|
1605
|
+
return {
|
|
1606
|
+
service: `${refName}Service`,
|
|
1607
|
+
ServiceClass: `${Model}Service`,
|
|
1608
|
+
SliceClass: `${Model}Slice`,
|
|
1609
|
+
ModelClass: `${Model}Model`,
|
|
1610
|
+
FilterClass: `${Model}Filter`,
|
|
1611
|
+
SignalClass: `${Model}Signal`,
|
|
1612
|
+
InternalClass: `${Model}Internal`,
|
|
1613
|
+
EndpointClass: `${Model}Endpoint`
|
|
1614
|
+
};
|
|
1615
|
+
};
|
|
1616
|
+
var formSetterNames = (className, key) => {
|
|
1617
|
+
const classKeyName = capitalize2(key);
|
|
1618
|
+
return {
|
|
1619
|
+
field: lowerlize(key),
|
|
1620
|
+
Field: classKeyName,
|
|
1621
|
+
setFieldOnModel: `set${classKeyName}On${className}`,
|
|
1622
|
+
addFieldOnModel: `add${classKeyName}On${className}`,
|
|
1623
|
+
subFieldOnModel: `sub${classKeyName}On${className}`,
|
|
1624
|
+
addOrSubFieldOnModel: `addOrSub${classKeyName}On${className}`,
|
|
1625
|
+
moveFieldOnModel: `move${classKeyName}On${className}`,
|
|
1626
|
+
uploadFieldOnModel: `upload${classKeyName}On${className}`,
|
|
1627
|
+
postSetField: `_postSet${classKeyName}`
|
|
1628
|
+
};
|
|
1629
|
+
};
|
|
1630
|
+
var stateSetterName = (key) => `set${capitalize2(key)}`;
|
|
1631
|
+
var endpointNames = (key) => {
|
|
1632
|
+
const Key = capitalize2(key);
|
|
1633
|
+
return { subscribe: `subscribe${Key}`, listen: `listen${Key}` };
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
// pkgs/@akanjs/language/nameTable.ts
|
|
1637
|
+
var capitalize3 = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
1638
|
+
|
|
1639
|
+
class NameTable {
|
|
1640
|
+
#byName = new Map;
|
|
1641
|
+
#byFile = new Map;
|
|
1642
|
+
constructor(files) {
|
|
1643
|
+
const all = [...files];
|
|
1644
|
+
const constantsByOwner = new Map;
|
|
1645
|
+
for (const facts of all) {
|
|
1646
|
+
const constants = constantsByOwner.get(facts.owner) ?? [];
|
|
1647
|
+
constants.push(...facts.constants);
|
|
1648
|
+
constantsByOwner.set(facts.owner, constants);
|
|
1649
|
+
}
|
|
1650
|
+
const libConstants = [...constantsByOwner].filter(([owner]) => owner?.startsWith("libs/"));
|
|
1651
|
+
const scope = (owner) => ({
|
|
1652
|
+
own: constantsByOwner.get(owner) ?? [],
|
|
1653
|
+
libs: libConstants.filter(([key]) => key !== owner).flatMap(([, constants]) => constants)
|
|
1654
|
+
});
|
|
1655
|
+
const scopes = new Map;
|
|
1656
|
+
for (const group of NameTable.#groupsOf(all)) {
|
|
1657
|
+
const constants = scopes.get(group.owner) ?? scope(group.owner);
|
|
1658
|
+
scopes.set(group.owner, constants);
|
|
1659
|
+
this.#addGroup(group, constants);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
lookup(name) {
|
|
1663
|
+
return this.#byName.get(name) ?? [];
|
|
1664
|
+
}
|
|
1665
|
+
entriesTargeting(fileName) {
|
|
1666
|
+
return this.#byFile.get(fileName) ?? [];
|
|
1667
|
+
}
|
|
1668
|
+
static #groupsOf(files) {
|
|
1669
|
+
const groups = new Map;
|
|
1670
|
+
const groupOf = (owner, refName) => {
|
|
1671
|
+
const key = `${owner ?? ""}:${refName}`;
|
|
1672
|
+
const group = groups.get(key) ?? { owner, refName, constants: [], slices: [], facts: [] };
|
|
1673
|
+
groups.set(key, group);
|
|
1674
|
+
return group;
|
|
1675
|
+
};
|
|
1676
|
+
for (const facts of files) {
|
|
1677
|
+
const refs = new Set;
|
|
1678
|
+
for (const item of [...facts.services, ...facts.slices, ...facts.endpoints, ...facts.stores, ...facts.models])
|
|
1679
|
+
if (item.model)
|
|
1680
|
+
refs.add(item.model);
|
|
1681
|
+
for (const filter of facts.filters)
|
|
1682
|
+
if (filter.name.endsWith("Filter"))
|
|
1683
|
+
refs.add(NameTable.#lowerFirst(filter.name.slice(0, -6)));
|
|
1684
|
+
for (const constant of facts.constants)
|
|
1685
|
+
refs.add(NameTable.#lowerFirst(constant.name));
|
|
1686
|
+
for (const refName of refs)
|
|
1687
|
+
groupOf(facts.owner, refName).facts.push(facts);
|
|
1688
|
+
}
|
|
1689
|
+
for (const group of groups.values()) {
|
|
1690
|
+
group.constants = group.facts.flatMap((facts) => facts.constants);
|
|
1691
|
+
group.slices = group.facts.flatMap((facts) => facts.slices.filter((slice) => slice.model === group.refName));
|
|
1692
|
+
}
|
|
1693
|
+
return [...groups.values()].filter((group) => group.facts.some((facts) => facts.services.some((item) => item.model === group.refName) || facts.slices.some((item) => item.model === group.refName) || facts.stores.some((item) => item.model === group.refName) || facts.models.some((item) => item.model === group.refName)));
|
|
1694
|
+
}
|
|
1695
|
+
static #lowerFirst(text) {
|
|
1696
|
+
return text.charAt(0).toLowerCase() + text.slice(1);
|
|
1697
|
+
}
|
|
1698
|
+
#add(entry) {
|
|
1699
|
+
if (entry.name === entry.sourceKey)
|
|
1700
|
+
return;
|
|
1701
|
+
const sameName = this.#byName.get(entry.name) ?? [];
|
|
1702
|
+
if (sameName.some((other) => other.gate === entry.gate && other.span.fileName === entry.span.fileName && other.span.start === entry.span.start))
|
|
1703
|
+
return;
|
|
1704
|
+
sameName.push(entry);
|
|
1705
|
+
this.#byName.set(entry.name, sameName);
|
|
1706
|
+
const sameFile = this.#byFile.get(entry.span.fileName) ?? [];
|
|
1707
|
+
sameFile.push(entry);
|
|
1708
|
+
this.#byFile.set(entry.span.fileName, sameFile);
|
|
1709
|
+
}
|
|
1710
|
+
#addGroup(group, constants) {
|
|
1711
|
+
const { owner, refName } = group;
|
|
1712
|
+
const Model = capitalize3(refName);
|
|
1713
|
+
const base = { owner, refName };
|
|
1714
|
+
const modelClass = group.constants.find((constant) => constant.name === Model);
|
|
1715
|
+
const facts = group.facts;
|
|
1716
|
+
const names = moduleNames(refName);
|
|
1717
|
+
const serviceClass = facts.flatMap((file) => file.services).find((item) => item.model === refName);
|
|
1718
|
+
const sliceClass = group.slices[0];
|
|
1719
|
+
const modelDoc = facts.flatMap((file) => file.models).find((item) => item.model === refName);
|
|
1720
|
+
const filterClass = facts.flatMap((file) => file.filters).find((item) => item.name === names.FilterClass);
|
|
1721
|
+
if (serviceClass) {
|
|
1722
|
+
const target = { ...base, family: "module", sourceKey: serviceClass.name, span: serviceClass.span };
|
|
1723
|
+
this.#add({ ...target, name: names.service, role: "service", gate: null, label: serviceClass.name, detail: [] });
|
|
1724
|
+
this.#add({ ...target, name: refName, role: "srv", gate: "srv", label: serviceClass.name, detail: [] });
|
|
1725
|
+
}
|
|
1726
|
+
if (sliceClass)
|
|
1727
|
+
this.#add({
|
|
1728
|
+
...base,
|
|
1729
|
+
family: "module",
|
|
1730
|
+
sourceKey: sliceClass.name,
|
|
1731
|
+
span: sliceClass.span,
|
|
1732
|
+
name: refName,
|
|
1733
|
+
role: "sig",
|
|
1734
|
+
gate: "sig",
|
|
1735
|
+
label: sliceClass.name,
|
|
1736
|
+
detail: sliceClass.guards.length ? [`guards: ${sliceClass.guards.join(", ")}`] : []
|
|
1737
|
+
});
|
|
1738
|
+
if (modelDoc)
|
|
1739
|
+
this.#add({
|
|
1740
|
+
...base,
|
|
1741
|
+
family: "module",
|
|
1742
|
+
sourceKey: modelDoc.name,
|
|
1743
|
+
span: modelDoc.span,
|
|
1744
|
+
name: refName,
|
|
1745
|
+
role: "db",
|
|
1746
|
+
gate: "db",
|
|
1747
|
+
label: modelDoc.name,
|
|
1748
|
+
detail: []
|
|
1749
|
+
});
|
|
1750
|
+
const modelRoles = modelNames(refName);
|
|
1751
|
+
const modelNameSet = new Set(Object.values(modelRoles));
|
|
1752
|
+
if (modelClass) {
|
|
1753
|
+
const target = { ...base, family: "model", sourceKey: modelClass.name, span: modelClass.span };
|
|
1754
|
+
for (const [role, name] of Object.entries(modelRoles))
|
|
1755
|
+
this.#add({
|
|
1756
|
+
...target,
|
|
1757
|
+
name,
|
|
1758
|
+
role,
|
|
1759
|
+
gate: role === "model" ? "state" : null,
|
|
1760
|
+
label: `model ${Model}`,
|
|
1761
|
+
detail: []
|
|
1762
|
+
});
|
|
1763
|
+
for (const [role, name] of Object.entries(databaseStateNames(refName)))
|
|
1764
|
+
this.#add({
|
|
1765
|
+
...target,
|
|
1766
|
+
name: stateSetterName(name),
|
|
1767
|
+
role: `set:${role}`,
|
|
1768
|
+
gate: null,
|
|
1769
|
+
label: `model ${Model}`,
|
|
1770
|
+
detail: []
|
|
1771
|
+
});
|
|
1772
|
+
for (const field of NameTable.#fieldsOf(modelClass, constants)) {
|
|
1773
|
+
const setters = formSetterNames(Model, field.name);
|
|
1774
|
+
for (const role of [
|
|
1775
|
+
"setFieldOnModel",
|
|
1776
|
+
"addFieldOnModel",
|
|
1777
|
+
"subFieldOnModel",
|
|
1778
|
+
"addOrSubFieldOnModel",
|
|
1779
|
+
"uploadFieldOnModel"
|
|
1780
|
+
])
|
|
1781
|
+
this.#add({
|
|
1782
|
+
...base,
|
|
1783
|
+
family: "formSetter",
|
|
1784
|
+
sourceKey: field.name,
|
|
1785
|
+
span: field.span,
|
|
1786
|
+
name: setters[role],
|
|
1787
|
+
role,
|
|
1788
|
+
gate: null,
|
|
1789
|
+
label: `field ${field.declaredIn}.${field.name}`,
|
|
1790
|
+
detail: []
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
for (const slice of group.slices) {
|
|
1795
|
+
const defaultTarget = {
|
|
1796
|
+
...base,
|
|
1797
|
+
family: "slice",
|
|
1798
|
+
sourceKey: slice.name,
|
|
1799
|
+
span: slice.span,
|
|
1800
|
+
label: `slice ${refName} (default)`,
|
|
1801
|
+
detail: slice.guards.length ? [`guards: ${slice.guards.join(", ")}`] : []
|
|
1802
|
+
};
|
|
1803
|
+
this.#addSliceNames(defaultTarget, refName, "", modelNameSet);
|
|
1804
|
+
for (const key of slice.keys)
|
|
1805
|
+
this.#addSliceNames({
|
|
1806
|
+
...base,
|
|
1807
|
+
family: "slice",
|
|
1808
|
+
sourceKey: key.name,
|
|
1809
|
+
span: key.span,
|
|
1810
|
+
label: `slice ${key.name}`,
|
|
1811
|
+
detail: [...key.guards.length ? [`guards: ${key.guards.join(", ")}`] : [], ...key.live ? ["live"] : []]
|
|
1812
|
+
}, refName, key.name, modelNameSet, slice.name);
|
|
1813
|
+
}
|
|
1814
|
+
if (filterClass) {
|
|
1815
|
+
const defaults = { ...base, family: "query", detail: [] };
|
|
1816
|
+
for (const [role, name] of Object.entries(queryNames("any")))
|
|
1817
|
+
this.#add({
|
|
1818
|
+
...defaults,
|
|
1819
|
+
sourceKey: filterClass.name,
|
|
1820
|
+
span: filterClass.span,
|
|
1821
|
+
name,
|
|
1822
|
+
role,
|
|
1823
|
+
gate: null,
|
|
1824
|
+
label: `query any (default) of ${filterClass.name}`
|
|
1825
|
+
});
|
|
1826
|
+
for (const query of filterClass.queries)
|
|
1827
|
+
for (const [role, name] of Object.entries(queryNames(query.name)))
|
|
1828
|
+
this.#add({
|
|
1829
|
+
...defaults,
|
|
1830
|
+
sourceKey: query.name,
|
|
1831
|
+
span: query.span,
|
|
1832
|
+
name,
|
|
1833
|
+
role,
|
|
1834
|
+
gate: null,
|
|
1835
|
+
label: `query ${query.name}`,
|
|
1836
|
+
renamed: (key) => queryNames(key)[role],
|
|
1837
|
+
dictionary: { section: "query", className: filterClass.name }
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
for (const store of facts.flatMap((file) => file.stores).filter((item) => item.model === refName))
|
|
1841
|
+
for (const key of store.stateKeys)
|
|
1842
|
+
this.#add({
|
|
1843
|
+
...base,
|
|
1844
|
+
family: "storeState",
|
|
1845
|
+
sourceKey: key.name,
|
|
1846
|
+
span: key.span,
|
|
1847
|
+
name: stateSetterName(key.name),
|
|
1848
|
+
role: "set",
|
|
1849
|
+
gate: null,
|
|
1850
|
+
label: `store state ${key.name}`,
|
|
1851
|
+
detail: []
|
|
1852
|
+
});
|
|
1853
|
+
for (const endpoint of facts.flatMap((file) => file.endpoints).filter((item) => item.model === refName))
|
|
1854
|
+
for (const key of endpoint.keys) {
|
|
1855
|
+
const derived = endpointNames(key.name);
|
|
1856
|
+
const name = key.verb === "pubsub" ? derived.subscribe : key.verb === "message" ? derived.listen : null;
|
|
1857
|
+
if (!name)
|
|
1858
|
+
continue;
|
|
1859
|
+
this.#add({
|
|
1860
|
+
...base,
|
|
1861
|
+
family: "endpoint",
|
|
1862
|
+
sourceKey: key.name,
|
|
1863
|
+
span: key.span,
|
|
1864
|
+
name,
|
|
1865
|
+
role: key.verb === "pubsub" ? "subscribe" : "listen",
|
|
1866
|
+
gate: null,
|
|
1867
|
+
label: `${key.verb} ${key.name}`,
|
|
1868
|
+
detail: key.guards.length ? [`guards: ${key.guards.join(", ")}`] : []
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
static #sliceRoles(refName, suffix) {
|
|
1873
|
+
const names = sliceNames(refName, suffix);
|
|
1874
|
+
return new Map([
|
|
1875
|
+
["slice", names.slice],
|
|
1876
|
+
...Object.entries(names.state),
|
|
1877
|
+
...Object.entries(names.action),
|
|
1878
|
+
...Object.entries(names.endpoint),
|
|
1879
|
+
...Object.entries(names.state).map(([role, name]) => [`set:${role}`, stateSetterName(name)])
|
|
1880
|
+
]);
|
|
1881
|
+
}
|
|
1882
|
+
#addSliceNames(target, refName, suffix, skip, className) {
|
|
1883
|
+
for (const [role, name] of NameTable.#sliceRoles(refName, suffix)) {
|
|
1884
|
+
if (!suffix && role !== "slice" && skip.has(name))
|
|
1885
|
+
continue;
|
|
1886
|
+
this.#add({
|
|
1887
|
+
...target,
|
|
1888
|
+
name,
|
|
1889
|
+
role,
|
|
1890
|
+
gate: role === "slice" ? "slice" : null,
|
|
1891
|
+
...suffix && className ? {
|
|
1892
|
+
renamed: (key) => NameTable.#sliceRoles(refName, key).get(role) ?? name,
|
|
1893
|
+
dictionary: { section: "slice", className }
|
|
1894
|
+
} : {}
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
static #fieldsOf(modelClass, { own, libs }) {
|
|
1899
|
+
const classNamed = (name, near) => own.find((constant) => constant.name === name && constant.span.fileName === near.span.fileName) ?? own.find((constant) => constant.name === name) ?? libs.find((constant) => constant.name === name);
|
|
1900
|
+
const fields = new Map;
|
|
1901
|
+
const seen = new Set;
|
|
1902
|
+
const visit = (constant) => {
|
|
1903
|
+
if (seen.has(constant))
|
|
1904
|
+
return;
|
|
1905
|
+
seen.add(constant);
|
|
1906
|
+
for (const field of constant.fields)
|
|
1907
|
+
if (!fields.has(field.name))
|
|
1908
|
+
fields.set(field.name, { ...field, declaredIn: constant.name });
|
|
1909
|
+
for (const parent of constant.parents) {
|
|
1910
|
+
const parentClass = classNamed(parent, constant);
|
|
1911
|
+
if (parentClass)
|
|
1912
|
+
visit(parentClass);
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
visit(modelClass);
|
|
1916
|
+
return [...fields.values()];
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// pkgs/@akanjs/language/akanIndex.ts
|
|
1921
|
+
var sourcePattern = /\.(constant|document|service|signal|store)\.ts$/;
|
|
1922
|
+
var dictionaryPattern = /\.dictionary\.ts$/;
|
|
1923
|
+
var srvkitPattern = /\/srvkit\//;
|
|
1924
|
+
var wordPattern = /[A-Za-z_$][\w$]*/g;
|
|
1925
|
+
var identifierPattern = /^[A-Za-z_$][\w$]*$/;
|
|
1926
|
+
var regExpSpecials = /[\\^$.*+?()[\]{}|]/g;
|
|
1927
|
+
var stateReceivers = new Set(["use", "sel", "get", "do"]);
|
|
1928
|
+
var lensFamilies = new Set(["slice", "query", "formSetter", "storeState", "endpoint"]);
|
|
1929
|
+
var modellessFamilies = new Set(["query", "storeState", "endpoint"]);
|
|
1930
|
+
var gateWords = { srv: "srv", sig: "sig", db: "db", slice: "slice" };
|
|
1931
|
+
var parseCacheSize = 64;
|
|
1932
|
+
var pathOf = (fileName) => fileName.replace(/\\/g, "/");
|
|
1933
|
+
var escapeRegExp = (text) => text.replace(regExpSpecials, (special) => `\\${special}`);
|
|
1934
|
+
var receiverGap = String.raw`(?:\s|\)|\/\*[\s\S]*?\*\/)*`;
|
|
1935
|
+
|
|
1936
|
+
class AkanIndex {
|
|
1937
|
+
#ts;
|
|
1938
|
+
#root;
|
|
1939
|
+
#ast;
|
|
1940
|
+
#reader;
|
|
1941
|
+
#facts = new Map;
|
|
1942
|
+
#fileWords = new Map;
|
|
1943
|
+
#parsed = new Map;
|
|
1944
|
+
#table = null;
|
|
1945
|
+
constructor({ ts, root }) {
|
|
1946
|
+
this.#ts = ts;
|
|
1947
|
+
this.#root = pathOf(root).replace(/\/$/, "");
|
|
1948
|
+
this.#ast = createAst(ts);
|
|
1949
|
+
this.#reader = createFactReader(ts, this.#ast);
|
|
1950
|
+
}
|
|
1951
|
+
static isSourcePath(fileName) {
|
|
1952
|
+
return sourcePattern.test(fileName);
|
|
1953
|
+
}
|
|
1954
|
+
get fileCount() {
|
|
1955
|
+
return this.#fileWords.size;
|
|
1956
|
+
}
|
|
1957
|
+
setFile(givenName, text) {
|
|
1958
|
+
const fileName = pathOf(givenName);
|
|
1959
|
+
const words = `
|
|
1960
|
+
${[...new Set(text.match(wordPattern))].join(`
|
|
1961
|
+
`)}
|
|
1962
|
+
`;
|
|
1963
|
+
this.#fileWords.set(fileName, words);
|
|
1964
|
+
const isSource = AkanIndex.isSourcePath(fileName);
|
|
1965
|
+
if (!isSource && !AkanIndex.#holdsFacts(fileName, words)) {
|
|
1966
|
+
this.#facts.delete(fileName);
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
const source = this.#parsed.get(fileName)?.text === text ? this.#parse(fileName, text) : this.#parseUncached(fileName, text);
|
|
1970
|
+
this.#facts.set(fileName, this.#reader.read(source, this.#ownerOf(fileName)));
|
|
1971
|
+
if (isSource)
|
|
1972
|
+
this.#table = null;
|
|
1973
|
+
}
|
|
1974
|
+
removeFile(givenName) {
|
|
1975
|
+
const fileName = pathOf(givenName);
|
|
1976
|
+
this.#fileWords.delete(fileName);
|
|
1977
|
+
this.#parsed.delete(fileName);
|
|
1978
|
+
if (this.#facts.delete(fileName) && AkanIndex.isSourcePath(fileName))
|
|
1979
|
+
this.#table = null;
|
|
1980
|
+
}
|
|
1981
|
+
definitionAt(givenName, text, offset) {
|
|
1982
|
+
const fileName = pathOf(givenName);
|
|
1983
|
+
const site = this.#useAt(this.#parse(fileName, text), offset);
|
|
1984
|
+
return site ? this.#resolve(fileName, site) : [];
|
|
1985
|
+
}
|
|
1986
|
+
hoverAt(givenName, text, offset) {
|
|
1987
|
+
const fileName = pathOf(givenName);
|
|
1988
|
+
const site = this.#useAt(this.#parse(fileName, text), offset);
|
|
1989
|
+
if (!site)
|
|
1990
|
+
return null;
|
|
1991
|
+
const targets = this.#resolve(fileName, site);
|
|
1992
|
+
return targets.length ? { name: site.identifier.text, targets } : null;
|
|
1993
|
+
}
|
|
1994
|
+
sourceKeysOf(givenName) {
|
|
1995
|
+
const keys = new Map;
|
|
1996
|
+
for (const entry of this.#nameTable().entriesTargeting(pathOf(givenName))) {
|
|
1997
|
+
if (!lensFamilies.has(entry.family) || keys.has(entry.span.start))
|
|
1998
|
+
continue;
|
|
1999
|
+
keys.set(entry.span.start, AkanIndex.#sourceKeyOf(entry));
|
|
2000
|
+
}
|
|
2001
|
+
return [...keys.values()].sort((a, b) => a.span.start - b.span.start);
|
|
2002
|
+
}
|
|
2003
|
+
sourceKeyAt(givenName, offset) {
|
|
2004
|
+
const entries = this.#sourceEntriesAt(pathOf(givenName), offset);
|
|
2005
|
+
const entry = entries.find((candidate) => lensFamilies.has(candidate.family)) ?? entries.at(0);
|
|
2006
|
+
return entry ? AkanIndex.#sourceKeyOf(entry) : null;
|
|
2007
|
+
}
|
|
2008
|
+
referencesAt(givenName, offset, readText) {
|
|
2009
|
+
return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
|
|
2010
|
+
}
|
|
2011
|
+
graph(readText, { digests = false } = {}) {
|
|
2012
|
+
return new GraphBuilder({
|
|
2013
|
+
digests,
|
|
2014
|
+
ts: this.#ts,
|
|
2015
|
+
facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
|
|
2016
|
+
files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
2017
|
+
readText,
|
|
2018
|
+
parse: (fileName, text) => this.#parse(fileName, text),
|
|
2019
|
+
resolve: (fileName, identifier) => {
|
|
2020
|
+
if (!this.#nameTable().lookup(identifier.text).length)
|
|
2021
|
+
return [];
|
|
2022
|
+
const site = this.#siteOf(identifier);
|
|
2023
|
+
return site ? this.#resolve(fileName, site) : [];
|
|
2024
|
+
},
|
|
2025
|
+
relative: (fileName) => this.#relative(fileName),
|
|
2026
|
+
ownerOf: (fileName) => this.#ownerOf(fileName),
|
|
2027
|
+
reader: this.#reader
|
|
2028
|
+
}).build();
|
|
2029
|
+
}
|
|
2030
|
+
renameTargetAt(givenName, offset) {
|
|
2031
|
+
const source = this.#sourceEntriesAt(pathOf(givenName), offset).find((entry) => entry.renamed);
|
|
2032
|
+
return source ? { family: source.family, sourceKey: source.sourceKey, span: source.span } : null;
|
|
2033
|
+
}
|
|
2034
|
+
renameAt(givenName, offset, newKey, readText) {
|
|
2035
|
+
const sources = this.#sourceEntriesAt(pathOf(givenName), offset).filter((entry) => entry.renamed);
|
|
2036
|
+
if (!sources.length || !identifierPattern.test(newKey))
|
|
2037
|
+
return null;
|
|
2038
|
+
return [
|
|
2039
|
+
{ span: sources[0].span, newText: newKey },
|
|
2040
|
+
...this.#usesOf(sources, readText).map(({ span, entry, shorthand }) => {
|
|
2041
|
+
const renamed = entry.renamed?.(newKey) ?? entry.name;
|
|
2042
|
+
return { span, newText: shorthand ? `${renamed}: ${entry.name}` : renamed };
|
|
2043
|
+
}),
|
|
2044
|
+
...this.#dictionaryKeysOf(sources[0], readText).map(({ span, quote }) => ({
|
|
2045
|
+
span,
|
|
2046
|
+
newText: `${quote}${newKey}${quote}`
|
|
2047
|
+
}))
|
|
2048
|
+
];
|
|
2049
|
+
}
|
|
2050
|
+
static #holdsFacts(fileName, words) {
|
|
2051
|
+
if (testFilePattern.test(fileName))
|
|
2052
|
+
return false;
|
|
2053
|
+
if (srvkitPattern.test(fileName))
|
|
2054
|
+
return true;
|
|
2055
|
+
const holdsGuards = words.includes(`
|
|
2056
|
+
Guard
|
|
2057
|
+
`) && words.includes(`
|
|
2058
|
+
canPass
|
|
2059
|
+
`);
|
|
2060
|
+
return holdsGuards || words.includes(`
|
|
2061
|
+
adapt
|
|
2062
|
+
`) || words.includes(`
|
|
2063
|
+
dangerouslyAdapt
|
|
2064
|
+
`);
|
|
2065
|
+
}
|
|
2066
|
+
static #sourceKeyOf({ family, sourceKey, label, detail, span }) {
|
|
2067
|
+
return { family, sourceKey, label, detail, span };
|
|
2068
|
+
}
|
|
2069
|
+
#usesOf(sources, readText) {
|
|
2070
|
+
if (!sources.length)
|
|
2071
|
+
return [];
|
|
2072
|
+
const ts = this.#ts;
|
|
2073
|
+
const isSource = (target) => sources.some((source) => source.span.fileName === target.span.fileName && source.span.start === target.span.start);
|
|
2074
|
+
const refNames = new Set(sources.map((source) => source.refName));
|
|
2075
|
+
const byName = new Map(sources.map((source) => [source.name, source]));
|
|
2076
|
+
const naming = new RegExp(`\\n(?:${[...byName.keys()].map(escapeRegExp).join("|")})\\n`);
|
|
2077
|
+
const gateWordsOf = sources.map((source) => source.gate ? gateWords[source.gate] : undefined);
|
|
2078
|
+
const receiverPatterns = sources.map((source, at) => {
|
|
2079
|
+
const gateWord = gateWordsOf[at];
|
|
2080
|
+
return gateWord ? new RegExp(String.raw`(?<![\w$])${gateWord}${receiverGap}[?!]?\.${receiverGap}${escapeRegExp(source.name)}(?![\w$])`) : null;
|
|
2081
|
+
});
|
|
2082
|
+
const named = (words, at) => words.includes(`
|
|
2083
|
+
${sources[at].name}
|
|
2084
|
+
`) && (!gateWordsOf[at] || words.includes(`
|
|
2085
|
+
${gateWordsOf[at]}
|
|
2086
|
+
`));
|
|
2087
|
+
const candidates = [...this.#fileWords].filter(([, words]) => naming.test(words) && sources.some((_, at) => named(words, at)));
|
|
2088
|
+
const found = [];
|
|
2089
|
+
for (const [file, words] of candidates.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
2090
|
+
const text = readText(file);
|
|
2091
|
+
if (text === undefined)
|
|
2092
|
+
continue;
|
|
2093
|
+
if (!sources.some((_, at) => named(words, at) && (receiverPatterns[at]?.test(text) ?? true)))
|
|
2094
|
+
continue;
|
|
2095
|
+
const source = this.#parse(file, text);
|
|
2096
|
+
const visit = (node) => {
|
|
2097
|
+
const entry = ts.isIdentifier(node) ? byName.get(node.text) : undefined;
|
|
2098
|
+
const site = entry && ts.isIdentifier(node) ? this.#siteOf(node) : null;
|
|
2099
|
+
const targets = site ? this.#resolve(file, site) : [];
|
|
2100
|
+
if (entry && site && targets.some(isSource) && targets.every((target) => refNames.has(target.refName))) {
|
|
2101
|
+
const parent = site.identifier.parent;
|
|
2102
|
+
const shorthand = ts.isBindingElement(parent) && !parent.propertyName;
|
|
2103
|
+
found.push({ span: this.#reader.spanOf(source, site.identifier), entry, shorthand });
|
|
2104
|
+
}
|
|
2105
|
+
ts.forEachChild(node, visit);
|
|
2106
|
+
};
|
|
2107
|
+
visit(source);
|
|
2108
|
+
}
|
|
2109
|
+
return found;
|
|
2110
|
+
}
|
|
2111
|
+
#dictionaryKeysOf(source, readText) {
|
|
2112
|
+
const { dictionary } = source;
|
|
2113
|
+
if (!dictionary)
|
|
2114
|
+
return [];
|
|
2115
|
+
const ts = this.#ts;
|
|
2116
|
+
const keys = [];
|
|
2117
|
+
const moduleDir = source.span.fileName.slice(0, source.span.fileName.lastIndexOf("/"));
|
|
2118
|
+
for (const [file, words] of this.#fileWords) {
|
|
2119
|
+
if (!dictionaryPattern.test(file) || this.#ownerOf(file) !== source.owner)
|
|
2120
|
+
continue;
|
|
2121
|
+
const sameModule = file.slice(0, file.lastIndexOf("/")) === moduleDir;
|
|
2122
|
+
if (!words.includes(`
|
|
2123
|
+
${dictionary.section}
|
|
2124
|
+
`))
|
|
2125
|
+
continue;
|
|
2126
|
+
if (!sameModule && !words.includes(`
|
|
2127
|
+
${dictionary.className}
|
|
2128
|
+
`))
|
|
2129
|
+
continue;
|
|
2130
|
+
const text = readText(file);
|
|
2131
|
+
if (text === undefined)
|
|
2132
|
+
continue;
|
|
2133
|
+
const parsed = this.#parse(file, text);
|
|
2134
|
+
const visit = (node) => {
|
|
2135
|
+
const typeArgument = ts.isCallExpression(node) ? node.typeArguments?.[0] : undefined;
|
|
2136
|
+
const builder = ts.isCallExpression(node) ? node.arguments.at(-1) : undefined;
|
|
2137
|
+
const typed = !!typeArgument && ts.isTypeReferenceNode(typeArgument) && ts.isIdentifier(typeArgument.typeName) && typeArgument.typeName.text === dictionary.className;
|
|
2138
|
+
const untyped = !typeArgument && sameModule;
|
|
2139
|
+
const object = ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === dictionary.section && (typed || untyped) && builder ? this.#ast.firstObjectReturnedByArrow(builder) : null;
|
|
2140
|
+
for (const property of object?.properties ?? [])
|
|
2141
|
+
if (ts.isPropertyAssignment(property) && this.#ast.nodeName(property.name) === source.sourceKey)
|
|
2142
|
+
keys.push({
|
|
2143
|
+
span: this.#reader.spanOf(parsed, property.name),
|
|
2144
|
+
quote: ts.isStringLiteral(property.name) ? parsed.text.charAt(property.name.getStart(parsed)) : ""
|
|
2145
|
+
});
|
|
2146
|
+
ts.forEachChild(node, visit);
|
|
2147
|
+
};
|
|
2148
|
+
visit(parsed);
|
|
2149
|
+
}
|
|
2150
|
+
return keys;
|
|
2151
|
+
}
|
|
2152
|
+
#sourceEntriesAt(fileName, offset) {
|
|
2153
|
+
return this.#nameTable().entriesTargeting(fileName).filter((entry) => entry.span.start <= offset && offset <= entry.span.end);
|
|
2154
|
+
}
|
|
2155
|
+
#nameTable() {
|
|
2156
|
+
this.#table ??= new NameTable([...this.#facts.values()].filter(({ fileName }) => AkanIndex.isSourcePath(fileName)));
|
|
2157
|
+
return this.#table;
|
|
2158
|
+
}
|
|
2159
|
+
#relative(fileName) {
|
|
2160
|
+
return fileName.startsWith(`${this.#root}/`) ? fileName.slice(this.#root.length + 1) : fileName;
|
|
2161
|
+
}
|
|
2162
|
+
#ownerOf(fileName) {
|
|
2163
|
+
return ownerOf(this.#relative(fileName));
|
|
2164
|
+
}
|
|
2165
|
+
#parseUncached(fileName, text) {
|
|
2166
|
+
const kind = fileName.endsWith(".tsx") ? this.#ts.ScriptKind.TSX : this.#ts.ScriptKind.TS;
|
|
2167
|
+
return this.#ast.sourceFileFor(fileName, text, kind);
|
|
2168
|
+
}
|
|
2169
|
+
#parse(fileName, text) {
|
|
2170
|
+
const cached = this.#parsed.get(fileName);
|
|
2171
|
+
if (cached?.text === text)
|
|
2172
|
+
return cached.source;
|
|
2173
|
+
const source = this.#parseUncached(fileName, text);
|
|
2174
|
+
this.#parsed.delete(fileName);
|
|
2175
|
+
this.#parsed.set(fileName, { text, source });
|
|
2176
|
+
if (this.#parsed.size > parseCacheSize) {
|
|
2177
|
+
const oldest = this.#parsed.keys().next().value;
|
|
2178
|
+
if (oldest !== undefined)
|
|
2179
|
+
this.#parsed.delete(oldest);
|
|
2180
|
+
}
|
|
2181
|
+
return source;
|
|
2182
|
+
}
|
|
2183
|
+
#useAt(source, offset) {
|
|
2184
|
+
const ts = this.#ts;
|
|
2185
|
+
let found = null;
|
|
2186
|
+
const visit = (node) => {
|
|
2187
|
+
if (offset < node.getStart(source) || offset > node.getEnd())
|
|
2188
|
+
return;
|
|
2189
|
+
if (ts.isIdentifier(node))
|
|
2190
|
+
found = node;
|
|
2191
|
+
ts.forEachChild(node, visit);
|
|
2192
|
+
};
|
|
2193
|
+
visit(source);
|
|
2194
|
+
return found ? this.#siteOf(found) : null;
|
|
2195
|
+
}
|
|
2196
|
+
#siteOf(identifier) {
|
|
2197
|
+
const ts = this.#ts;
|
|
2198
|
+
const parent = identifier.parent;
|
|
2199
|
+
let chain;
|
|
2200
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === identifier)
|
|
2201
|
+
chain = this.#chainOf(parent.expression);
|
|
2202
|
+
else if (ts.isBindingElement(parent) && !parent.dotDotDotToken && ts.isObjectBindingPattern(parent.parent) && (parent.propertyName === identifier || !parent.propertyName && parent.name === identifier))
|
|
2203
|
+
chain = null;
|
|
2204
|
+
else
|
|
2205
|
+
return null;
|
|
2206
|
+
const { model, inStore } = this.#classContextOf(identifier);
|
|
2207
|
+
return { identifier, chain, classModel: model, inStore };
|
|
2208
|
+
}
|
|
2209
|
+
#chainOf(expression) {
|
|
2210
|
+
const ts = this.#ts;
|
|
2211
|
+
if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
|
|
2212
|
+
return this.#chainOf(expression.expression);
|
|
2213
|
+
if (ts.isIdentifier(expression))
|
|
2214
|
+
return [expression.text];
|
|
2215
|
+
if (expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
2216
|
+
return ["this"];
|
|
2217
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
2218
|
+
return [...this.#chainOf(expression.expression), expression.name.text];
|
|
2219
|
+
return ["?"];
|
|
2220
|
+
}
|
|
2221
|
+
#classContextOf(node) {
|
|
2222
|
+
const ts = this.#ts;
|
|
2223
|
+
let current = node.parent;
|
|
2224
|
+
while (current && !ts.isClassDeclaration(current) && !ts.isClassExpression(current))
|
|
2225
|
+
current = current.parent;
|
|
2226
|
+
if (!current)
|
|
2227
|
+
return { model: null, inStore: false };
|
|
2228
|
+
const className = current.name?.text;
|
|
2229
|
+
const heritage = current.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
|
|
2230
|
+
for (const { expression } of heritage) {
|
|
2231
|
+
if (!ts.isCallExpression(expression))
|
|
2232
|
+
continue;
|
|
2233
|
+
const callee = this.#ast.expressionName(expression.expression);
|
|
2234
|
+
if (callee === "into" && className?.endsWith("Model"))
|
|
2235
|
+
return { model: className.charAt(0).toLowerCase() + className.slice(1, -5), inStore: false };
|
|
2236
|
+
const holder = callee === "serve" ? "db" : callee === "store" ? "sig" : callee === "slice" || callee === "endpoint" || callee === "internal" ? "srv" : null;
|
|
2237
|
+
if (!holder)
|
|
2238
|
+
continue;
|
|
2239
|
+
return { model: this.#reader.moduleRefOf(expression.arguments[0], holder), inStore: callee === "store" };
|
|
2240
|
+
}
|
|
2241
|
+
return { model: null, inStore: false };
|
|
2242
|
+
}
|
|
2243
|
+
#modelHintOf(site) {
|
|
2244
|
+
const { chain } = site;
|
|
2245
|
+
if (!chain)
|
|
2246
|
+
return null;
|
|
2247
|
+
if (chain.length === 1 && chain[0] === "this")
|
|
2248
|
+
return site.classModel;
|
|
2249
|
+
const holder = chain.at(-1)?.replace(/^#/, "");
|
|
2250
|
+
const suffix = holder?.match(/.(Service|Model)$/)?.[1];
|
|
2251
|
+
if (holder && suffix)
|
|
2252
|
+
return holder.slice(0, -suffix.length);
|
|
2253
|
+
if (chain.length === 2 && (chain[0] === "db" || chain[0] === "srv"))
|
|
2254
|
+
return chain[1];
|
|
2255
|
+
return null;
|
|
2256
|
+
}
|
|
2257
|
+
#passesGate(entry, site) {
|
|
2258
|
+
const { chain } = site;
|
|
2259
|
+
const last = chain?.at(-1);
|
|
2260
|
+
switch (entry.gate) {
|
|
2261
|
+
case "srv":
|
|
2262
|
+
case "sig":
|
|
2263
|
+
case "db":
|
|
2264
|
+
return chain?.length === 1 && last === entry.gate;
|
|
2265
|
+
case "slice":
|
|
2266
|
+
return last === "slice";
|
|
2267
|
+
case "state":
|
|
2268
|
+
return !!last && stateReceivers.has(last) || site.inStore && chain?.length === 1 && last === "this";
|
|
2269
|
+
default:
|
|
2270
|
+
return true;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
#resolve(fileName, site) {
|
|
2274
|
+
const owner = this.#ownerOf(fileName);
|
|
2275
|
+
const visible = this.#nameTable().lookup(site.identifier.text).filter((entry) => this.#passesGate(entry, site) && (!owner || !entry.owner || entry.owner === owner || entry.owner.startsWith("libs/")));
|
|
2276
|
+
const hint = this.#modelHintOf(site);
|
|
2277
|
+
const hinted = hint ? visible.filter((entry) => entry.refName === hint) : [];
|
|
2278
|
+
const chosen = hinted.length ? hinted : hint ? visible.filter((entry) => !modellessFamilies.has(entry.family)) : visible;
|
|
2279
|
+
return chosen.map(({ family, refName, owner: entryOwner, sourceKey, label, detail, span }) => ({
|
|
2280
|
+
family,
|
|
2281
|
+
refName,
|
|
2282
|
+
owner: entryOwner,
|
|
2283
|
+
sourceKey,
|
|
2284
|
+
label,
|
|
2285
|
+
detail,
|
|
2286
|
+
span
|
|
2287
|
+
})).sort((a, b) => Number(b.owner === owner) - Number(a.owner === owner));
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// pkgs/@akanjs/language/lspConnection.ts
|
|
2292
|
+
var lspErrors = {
|
|
2293
|
+
parseError: -32700,
|
|
2294
|
+
invalidRequest: -32600,
|
|
2295
|
+
methodNotFound: -32601,
|
|
2296
|
+
internalError: -32603,
|
|
2297
|
+
serverNotInitialized: -32002
|
|
2298
|
+
};
|
|
2299
|
+
var headerEnd = Buffer.from(`\r
|
|
2300
|
+
\r
|
|
2301
|
+
`);
|
|
2302
|
+
var contentLength = /^content-length:\s*(\d+)\s*$/im;
|
|
2303
|
+
|
|
2304
|
+
class LspConnection {
|
|
2305
|
+
#output;
|
|
2306
|
+
#onMessage;
|
|
2307
|
+
#buffer = Buffer.alloc(0);
|
|
2308
|
+
#nextId = 0;
|
|
2309
|
+
constructor(input, output, onMessage) {
|
|
2310
|
+
this.#output = output;
|
|
2311
|
+
this.#onMessage = onMessage;
|
|
2312
|
+
input.on("data", (chunk) => this.#read(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
|
|
2313
|
+
}
|
|
2314
|
+
#read(chunk) {
|
|
2315
|
+
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
2316
|
+
for (;; ) {
|
|
2317
|
+
const end = this.#buffer.indexOf(headerEnd);
|
|
2318
|
+
if (end < 0)
|
|
2319
|
+
return;
|
|
2320
|
+
const length = this.#buffer.subarray(0, end).toString("ascii").match(contentLength)?.[1];
|
|
2321
|
+
const start = end + headerEnd.length;
|
|
2322
|
+
if (length === undefined) {
|
|
2323
|
+
this.#buffer = this.#buffer.subarray(start);
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
if (this.#buffer.length < start + Number(length))
|
|
2327
|
+
return;
|
|
2328
|
+
const body = this.#buffer.subarray(start, start + Number(length)).toString("utf8");
|
|
2329
|
+
this.#buffer = this.#buffer.subarray(start + Number(length));
|
|
2330
|
+
this.#dispatch(body);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
#dispatch(body) {
|
|
2334
|
+
let message;
|
|
2335
|
+
try {
|
|
2336
|
+
message = JSON.parse(body);
|
|
2337
|
+
} catch {
|
|
2338
|
+
this.fail(null, lspErrors.parseError, "The message is not JSON.");
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (typeof message === "object" && message !== null && !Array.isArray(message))
|
|
2342
|
+
this.#onMessage(message);
|
|
2343
|
+
else
|
|
2344
|
+
this.fail(null, lspErrors.invalidRequest, "The message is not a JSON-RPC object.");
|
|
2345
|
+
}
|
|
2346
|
+
send(message) {
|
|
2347
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
2348
|
+
this.#output.write(Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r
|
|
2349
|
+
\r
|
|
2350
|
+
`, "ascii"), body]));
|
|
2351
|
+
}
|
|
2352
|
+
respond(id, result) {
|
|
2353
|
+
this.send({ jsonrpc: "2.0", id, result: result ?? null });
|
|
2354
|
+
}
|
|
2355
|
+
fail(id, code, message) {
|
|
2356
|
+
this.send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
2357
|
+
}
|
|
2358
|
+
notify(method, params) {
|
|
2359
|
+
this.send({ jsonrpc: "2.0", method, params });
|
|
2360
|
+
}
|
|
2361
|
+
request(method, params) {
|
|
2362
|
+
this.send({ jsonrpc: "2.0", id: `akan-${++this.#nextId}`, method, params });
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// pkgs/@akanjs/language/akanLanguageServer.ts
|
|
2367
|
+
var systemKinds = ["apps", "libs"];
|
|
2368
|
+
var isSkippedDir = (name) => name === "node_modules" || name === "dist" || name.startsWith(".");
|
|
2369
|
+
var indexedPath = /^(apps|libs)\/(?!(?:.*\/)?(?:node_modules|dist|\.[^/]*)\/).*\.tsx?$/;
|
|
2370
|
+
var sourceGlob = "{apps,libs}/**/*.{ts,tsx}";
|
|
2371
|
+
var editDelayMs = 150;
|
|
2372
|
+
var filesPerYield = 200;
|
|
2373
|
+
var infoMessage = 3;
|
|
2374
|
+
var unknownMethod = Symbol("unknownMethod");
|
|
2375
|
+
var isDirectory = (dir) => {
|
|
2376
|
+
try {
|
|
2377
|
+
return statSync(dir).isDirectory();
|
|
2378
|
+
} catch {
|
|
2379
|
+
return false;
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
var fileOf = (uri) => {
|
|
2383
|
+
try {
|
|
2384
|
+
return fileURLToPath(uri);
|
|
2385
|
+
} catch {
|
|
2386
|
+
return null;
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2389
|
+
var isAkanRoot = (dir) => systemKinds.some((kind) => {
|
|
2390
|
+
try {
|
|
2391
|
+
return readdirSync(path.join(dir, kind)).some((system) => existsSync(path.join(dir, kind, system, "akan.config.ts")));
|
|
2392
|
+
} catch {
|
|
2393
|
+
return false;
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
var workspaceRootOf = (dir) => {
|
|
2397
|
+
const parent = path.dirname(dir);
|
|
2398
|
+
if (isAkanRoot(dir))
|
|
2399
|
+
return dir;
|
|
2400
|
+
return parent === dir ? null : workspaceRootOf(parent);
|
|
2401
|
+
};
|
|
2402
|
+
var caseOf = (fileName) => process.platform === "win32" ? fileName.toLowerCase() : fileName;
|
|
2403
|
+
var sourceFilesOf = (root) => {
|
|
2404
|
+
const found = [];
|
|
2405
|
+
const visited = new Set;
|
|
2406
|
+
const walk = (dir) => {
|
|
2407
|
+
let entries;
|
|
2408
|
+
try {
|
|
2409
|
+
const real = realpathSync(dir);
|
|
2410
|
+
if (visited.has(real))
|
|
2411
|
+
return;
|
|
2412
|
+
visited.add(real);
|
|
2413
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2414
|
+
} catch {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
for (const entry of entries) {
|
|
2418
|
+
const at = path.join(dir, entry.name);
|
|
2419
|
+
const isDir = entry.isDirectory() || entry.isSymbolicLink() && isDirectory(at);
|
|
2420
|
+
if (isDir && !isSkippedDir(entry.name))
|
|
2421
|
+
walk(at);
|
|
2422
|
+
else if (!isDir && /\.tsx?$/.test(entry.name))
|
|
2423
|
+
found.push(at);
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
for (const kind of systemKinds)
|
|
2427
|
+
walk(path.join(root, kind));
|
|
2428
|
+
return found;
|
|
2429
|
+
};
|
|
2430
|
+
var offsetAt = (text, { line, character }) => {
|
|
2431
|
+
const breaks = [...text.matchAll(/\r\n|\r|\n/g)];
|
|
2432
|
+
if (line > breaks.length)
|
|
2433
|
+
return text.length;
|
|
2434
|
+
const start = line === 0 ? 0 : breaks[line - 1].index + breaks[line - 1][0].length;
|
|
2435
|
+
const end = line < breaks.length ? breaks[line].index : text.length;
|
|
2436
|
+
return Math.min(start + character, end);
|
|
2437
|
+
};
|
|
2438
|
+
var positionAt = (text, offset) => {
|
|
2439
|
+
const breaks = [...text.slice(0, offset).matchAll(/\r\n|\r|\n/g)];
|
|
2440
|
+
const last = breaks.at(-1);
|
|
2441
|
+
return { line: breaks.length, character: last ? offset - last.index - last[0].length : offset };
|
|
2442
|
+
};
|
|
2443
|
+
var escapeMarkdown = (text) => text.replace(/[\\`*_{}[\]()#+\-.!|<>]/g, "\\$&");
|
|
2444
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
2445
|
+
var yieldToLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
2446
|
+
|
|
2447
|
+
class AkanLanguageServer {
|
|
2448
|
+
#ts;
|
|
2449
|
+
#connection;
|
|
2450
|
+
#exit;
|
|
2451
|
+
#open = new Map;
|
|
2452
|
+
#pendingEdits = new Map;
|
|
2453
|
+
#roots = [];
|
|
2454
|
+
#ready = Promise.resolve();
|
|
2455
|
+
#queue = Promise.resolve();
|
|
2456
|
+
#watchesFiles = false;
|
|
2457
|
+
#watchesRelative = false;
|
|
2458
|
+
#hoversMarkdown = true;
|
|
2459
|
+
#isInitialized = false;
|
|
2460
|
+
#isShutdown = false;
|
|
2461
|
+
constructor({ ts, input, output, exit }) {
|
|
2462
|
+
this.#ts = ts;
|
|
2463
|
+
this.#exit = exit;
|
|
2464
|
+
this.#connection = new LspConnection(input, output, (message) => this.#handle(message));
|
|
2465
|
+
input.on("end", () => this.#stop());
|
|
2466
|
+
}
|
|
2467
|
+
#handle(message) {
|
|
2468
|
+
this.#queue = this.#queue.then(() => this.#process(message)).catch((error) => this.#log(messageOf(error)));
|
|
2469
|
+
}
|
|
2470
|
+
async#process({ id, method, params }) {
|
|
2471
|
+
if (method === undefined)
|
|
2472
|
+
return;
|
|
2473
|
+
if (id !== undefined && id !== null)
|
|
2474
|
+
return await this.#answer(id, method, params);
|
|
2475
|
+
if (this.#isInitialized || method === "exit")
|
|
2476
|
+
this.#notified(method, params);
|
|
2477
|
+
}
|
|
2478
|
+
async#answer(id, method, params) {
|
|
2479
|
+
if (!this.#isInitialized && method !== "initialize")
|
|
2480
|
+
return this.#connection.fail(id, lspErrors.serverNotInitialized, "The server is not initialized yet.");
|
|
2481
|
+
if (this.#isShutdown)
|
|
2482
|
+
return this.#connection.fail(id, lspErrors.invalidRequest, "The server is shutting down.");
|
|
2483
|
+
try {
|
|
2484
|
+
const result = await this.#result(method, params);
|
|
2485
|
+
if (result === unknownMethod)
|
|
2486
|
+
this.#connection.fail(id, lspErrors.methodNotFound, `${method} is not supported.`);
|
|
2487
|
+
else
|
|
2488
|
+
this.#connection.respond(id, result);
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
this.#connection.fail(id, lspErrors.internalError, messageOf(error));
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
async#result(method, params) {
|
|
2494
|
+
switch (method) {
|
|
2495
|
+
case "initialize":
|
|
2496
|
+
return this.#initialize(params);
|
|
2497
|
+
case "shutdown":
|
|
2498
|
+
this.#isShutdown = true;
|
|
2499
|
+
return null;
|
|
2500
|
+
case "textDocument/definition":
|
|
2501
|
+
return await this.#definition(params);
|
|
2502
|
+
case "textDocument/references":
|
|
2503
|
+
return await this.#references(params);
|
|
2504
|
+
case "textDocument/hover":
|
|
2505
|
+
return await this.#hover(params);
|
|
2506
|
+
default:
|
|
2507
|
+
return unknownMethod;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
#notified(method, params) {
|
|
2511
|
+
switch (method) {
|
|
2512
|
+
case "initialized":
|
|
2513
|
+
return this.#watch();
|
|
2514
|
+
case "exit":
|
|
2515
|
+
return this.#stop();
|
|
2516
|
+
case "textDocument/didOpen": {
|
|
2517
|
+
const { textDocument } = params;
|
|
2518
|
+
return this.#opened(textDocument.uri, textDocument.text);
|
|
2519
|
+
}
|
|
2520
|
+
case "textDocument/didChange": {
|
|
2521
|
+
const { textDocument, contentChanges } = params;
|
|
2522
|
+
const text = contentChanges.at(-1)?.text;
|
|
2523
|
+
return text === undefined ? undefined : this.#changed(textDocument.uri, text);
|
|
2524
|
+
}
|
|
2525
|
+
case "textDocument/didClose":
|
|
2526
|
+
return this.#closed(params.textDocument.uri);
|
|
2527
|
+
case "workspace/didChangeWatchedFiles":
|
|
2528
|
+
for (const { uri } of params.changes)
|
|
2529
|
+
this.#changedOnDisk(uri);
|
|
2530
|
+
return;
|
|
2531
|
+
default:
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
#initialize(params) {
|
|
2536
|
+
this.#isInitialized = true;
|
|
2537
|
+
const watched = params.capabilities?.workspace?.didChangeWatchedFiles;
|
|
2538
|
+
this.#watchesFiles = watched?.dynamicRegistration === true;
|
|
2539
|
+
this.#watchesRelative = watched?.relativePatternSupport === true;
|
|
2540
|
+
this.#hoversMarkdown = params.capabilities?.textDocument?.hover?.contentFormat?.includes("markdown") ?? true;
|
|
2541
|
+
const given = params.workspaceFolders?.length ? params.workspaceFolders.map((folder) => fileOf(folder.uri)) : [params.rootUri ? fileOf(params.rootUri) : params.rootPath ?? null];
|
|
2542
|
+
const roots = new Set(given.flatMap((dir) => dir ? [workspaceRootOf(path.resolve(dir))] : []));
|
|
2543
|
+
this.#roots = [...roots].flatMap((root) => root ? [{ root, index: new AkanIndex({ ts: this.#ts, root }) }] : []).sort((a, b) => b.root.length - a.root.length);
|
|
2544
|
+
this.#ready = yieldToLoop().then(() => this.#fill());
|
|
2545
|
+
return {
|
|
2546
|
+
capabilities: {
|
|
2547
|
+
positionEncoding: "utf-16",
|
|
2548
|
+
textDocumentSync: { openClose: true, change: 1 },
|
|
2549
|
+
definitionProvider: true,
|
|
2550
|
+
referencesProvider: true,
|
|
2551
|
+
hoverProvider: true
|
|
2552
|
+
},
|
|
2553
|
+
serverInfo: { name: "akan-language-server" }
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
async#fill() {
|
|
2557
|
+
for (const entry of this.#roots) {
|
|
2558
|
+
const started = Date.now();
|
|
2559
|
+
const failures = [];
|
|
2560
|
+
for (const [at, fileName] of sourceFilesOf(entry.root).entries()) {
|
|
2561
|
+
if (at % filesPerYield === filesPerYield - 1)
|
|
2562
|
+
await yieldToLoop();
|
|
2563
|
+
const text = this.#readText(fileName);
|
|
2564
|
+
try {
|
|
2565
|
+
if (text !== undefined)
|
|
2566
|
+
entry.index.setFile(fileName, text);
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
failures.push(error);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
const skipped = failures.length ? `; skipped ${failures.length}: ${messageOf(failures[0])}` : "";
|
|
2572
|
+
this.#log(`akan: indexed ${entry.index.fileCount} files in ${Date.now() - started}ms at ${entry.root}${skipped}`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
#watch() {
|
|
2576
|
+
if (!this.#watchesFiles || !this.#roots.length)
|
|
2577
|
+
return;
|
|
2578
|
+
const watchers = this.#watchesRelative ? this.#roots.map(({ root }) => ({ globPattern: { baseUri: pathToFileURL(root).href, pattern: sourceGlob } })) : [{ globPattern: `**/${sourceGlob}` }];
|
|
2579
|
+
this.#connection.request("client/registerCapability", {
|
|
2580
|
+
registrations: [
|
|
2581
|
+
{ id: "akan-source-files", method: "workspace/didChangeWatchedFiles", registerOptions: { watchers } }
|
|
2582
|
+
]
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
#stop() {
|
|
2586
|
+
for (const timer of this.#pendingEdits.values())
|
|
2587
|
+
clearTimeout(timer);
|
|
2588
|
+
this.#pendingEdits.clear();
|
|
2589
|
+
this.#exit(this.#isShutdown ? 0 : 1);
|
|
2590
|
+
}
|
|
2591
|
+
#log(message) {
|
|
2592
|
+
this.#connection.notify("window/logMessage", { type: infoMessage, message });
|
|
2593
|
+
}
|
|
2594
|
+
#readText = (fileName) => {
|
|
2595
|
+
const open = this.#open.get(path.resolve(fileName));
|
|
2596
|
+
if (open !== undefined)
|
|
2597
|
+
return open;
|
|
2598
|
+
try {
|
|
2599
|
+
return readFileSync(fileName, "utf8");
|
|
2600
|
+
} catch {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
#rootOf(fileName) {
|
|
2605
|
+
return this.#roots.find(({ root }) => caseOf(fileName).startsWith(caseOf(`${root}${path.sep}`))) ?? null;
|
|
2606
|
+
}
|
|
2607
|
+
#locationsOf(spans) {
|
|
2608
|
+
const texts = new Map;
|
|
2609
|
+
return spans.map(({ fileName, start, end, line, character, endLine, endCharacter }) => {
|
|
2610
|
+
if (!texts.has(fileName))
|
|
2611
|
+
texts.set(fileName, this.#readText(fileName));
|
|
2612
|
+
const text = texts.get(fileName);
|
|
2613
|
+
return {
|
|
2614
|
+
uri: pathToFileURL(fileName).href,
|
|
2615
|
+
range: text === undefined ? { start: { line, character }, end: { line: endLine, character: endCharacter } } : { start: positionAt(text, start), end: positionAt(text, end) }
|
|
2616
|
+
};
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
#update(fileName) {
|
|
2620
|
+
const entry = this.#rootOf(fileName);
|
|
2621
|
+
const relative = entry ? path.relative(entry.root, fileName).split(path.sep).join("/") : "";
|
|
2622
|
+
if (!entry || !indexedPath.test(relative))
|
|
2623
|
+
return;
|
|
2624
|
+
const text = this.#readText(fileName);
|
|
2625
|
+
try {
|
|
2626
|
+
if (text === undefined)
|
|
2627
|
+
entry.index.removeFile(fileName);
|
|
2628
|
+
else
|
|
2629
|
+
entry.index.setFile(fileName, text);
|
|
2630
|
+
} catch (error) {
|
|
2631
|
+
this.#log(`akan: ${fileName}: ${messageOf(error)}`);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
#flush() {
|
|
2635
|
+
for (const [fileName, timer] of this.#pendingEdits) {
|
|
2636
|
+
clearTimeout(timer);
|
|
2637
|
+
this.#update(fileName);
|
|
2638
|
+
}
|
|
2639
|
+
this.#pendingEdits.clear();
|
|
2640
|
+
}
|
|
2641
|
+
#opened(uri, text) {
|
|
2642
|
+
const fileName = fileOf(uri);
|
|
2643
|
+
if (!fileName)
|
|
2644
|
+
return;
|
|
2645
|
+
this.#open.set(path.resolve(fileName), text);
|
|
2646
|
+
this.#update(path.resolve(fileName));
|
|
2647
|
+
}
|
|
2648
|
+
#changed(uri, text) {
|
|
2649
|
+
const fileName = fileOf(uri);
|
|
2650
|
+
if (!fileName)
|
|
2651
|
+
return;
|
|
2652
|
+
const key = path.resolve(fileName);
|
|
2653
|
+
this.#open.set(key, text);
|
|
2654
|
+
clearTimeout(this.#pendingEdits.get(key));
|
|
2655
|
+
this.#pendingEdits.set(key, setTimeout(() => {
|
|
2656
|
+
this.#pendingEdits.delete(key);
|
|
2657
|
+
this.#update(key);
|
|
2658
|
+
}, editDelayMs));
|
|
2659
|
+
}
|
|
2660
|
+
#closed(uri) {
|
|
2661
|
+
const fileName = fileOf(uri);
|
|
2662
|
+
if (!fileName)
|
|
2663
|
+
return;
|
|
2664
|
+
const key = path.resolve(fileName);
|
|
2665
|
+
clearTimeout(this.#pendingEdits.get(key));
|
|
2666
|
+
this.#pendingEdits.delete(key);
|
|
2667
|
+
this.#open.delete(key);
|
|
2668
|
+
this.#update(key);
|
|
2669
|
+
}
|
|
2670
|
+
#changedOnDisk(uri) {
|
|
2671
|
+
const fileName = fileOf(uri);
|
|
2672
|
+
if (fileName && !this.#open.has(path.resolve(fileName)))
|
|
2673
|
+
this.#update(path.resolve(fileName));
|
|
2674
|
+
}
|
|
2675
|
+
async#locate({ textDocument, position }) {
|
|
2676
|
+
await this.#ready;
|
|
2677
|
+
this.#flush();
|
|
2678
|
+
const given = fileOf(textDocument.uri);
|
|
2679
|
+
const fileName = given ? path.resolve(given) : null;
|
|
2680
|
+
const entry = fileName ? this.#rootOf(fileName) : null;
|
|
2681
|
+
const text = fileName ? this.#readText(fileName) : undefined;
|
|
2682
|
+
if (!fileName || !entry || text === undefined)
|
|
2683
|
+
return null;
|
|
2684
|
+
return { entry, fileName, text, offset: offsetAt(text, position) };
|
|
2685
|
+
}
|
|
2686
|
+
async#definition(params) {
|
|
2687
|
+
const at = await this.#locate(params);
|
|
2688
|
+
return at && this.#locationsOf(at.entry.index.definitionAt(at.fileName, at.text, at.offset).map(({ span }) => span));
|
|
2689
|
+
}
|
|
2690
|
+
async#references(params) {
|
|
2691
|
+
const at = await this.#locate(params);
|
|
2692
|
+
if (!at)
|
|
2693
|
+
return null;
|
|
2694
|
+
const uses = at.entry.index.referencesAt(at.fileName, at.offset, this.#readText);
|
|
2695
|
+
const key = params.context?.includeDeclaration && uses.length ? at.entry.index.sourceKeyAt(at.fileName, at.offset) : null;
|
|
2696
|
+
return this.#locationsOf(key ? [key.span, ...uses] : uses);
|
|
2697
|
+
}
|
|
2698
|
+
async#hover(params) {
|
|
2699
|
+
const at = await this.#locate(params);
|
|
2700
|
+
const hover = at?.entry.index.hoverAt(at.fileName, at.text, at.offset);
|
|
2701
|
+
if (!at || !hover)
|
|
2702
|
+
return null;
|
|
2703
|
+
const lines = this.#locationsOf(hover.targets.map(({ span }) => span)).map(({ range }) => range.start.line + 1);
|
|
2704
|
+
const lineOf = (target, idx) => {
|
|
2705
|
+
const summary = [target.label, ...target.detail].join(" · ");
|
|
2706
|
+
const where = `${path.relative(at.entry.root, target.span.fileName).split(path.sep).join("/")}:${lines[idx]}`;
|
|
2707
|
+
return this.#hoversMarkdown ? `**akan** · ${escapeMarkdown(summary)} — \`${where}\`` : `akan · ${summary} — ${where}`;
|
|
2708
|
+
};
|
|
2709
|
+
return {
|
|
2710
|
+
contents: {
|
|
2711
|
+
kind: this.#hoversMarkdown ? "markdown" : "plaintext",
|
|
2712
|
+
value: hover.targets.map(lineOf).join(`
|
|
2713
|
+
|
|
2714
|
+
`)
|
|
2715
|
+
}
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// pkgs/@akanjs/language/server.ts
|
|
2721
|
+
if (!process.argv.includes("--stdio")) {
|
|
2722
|
+
process.stderr.write("akan-language-server speaks LSP over stdio: run it as `akan-language-server --stdio`.\n");
|
|
2723
|
+
process.exit(2);
|
|
2724
|
+
}
|
|
2725
|
+
var ts = await import("typescript").then((loaded) => loaded.default ?? loaded, () => null);
|
|
2726
|
+
if (!ts) {
|
|
2727
|
+
process.stderr.write("akan-language-server needs the `typescript` package installed next to @akanjs/language.\n");
|
|
2728
|
+
process.exit(1);
|
|
2729
|
+
}
|
|
2730
|
+
new AkanLanguageServer({ ts, input: process.stdin, output: process.stdout, exit: (code) => process.exit(code) });
|