@akanjs/language 3.1.2-rc.4 → 3.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -71,6 +71,721 @@ class AkanGraphDiff {
71
71
  return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
72
72
  }
73
73
  }
74
+ // pkgs/@akanjs/language/moduleFacts.ts
75
+ var testFilePattern = /\.(?:test|spec)\.tsx?$/;
76
+ var moduleCallees = new Set([
77
+ "via",
78
+ "from",
79
+ "into",
80
+ "serve",
81
+ "slice",
82
+ "endpoint",
83
+ "internal",
84
+ "store",
85
+ "enumOf",
86
+ "adapt",
87
+ "dangerouslyAdapt"
88
+ ]);
89
+ var intervalUnits = [
90
+ [86400000, "d"],
91
+ [3600000, "h"],
92
+ [60000, "m"],
93
+ [1000, "s"]
94
+ ];
95
+ var argMethods = new Set(["param", "body", "search", "room", "msg"]);
96
+ var ownerOf = (relativePath) => {
97
+ const [root, name] = relativePath.split("/");
98
+ return (root === "apps" || root === "libs") && name ? `${root}/${name}` : null;
99
+ };
100
+ var createFactReader = (ts, ast) => {
101
+ const spanOf = (source, node) => {
102
+ const start = node.getStart(source);
103
+ const end = node.getEnd();
104
+ const head = source.getLineAndCharacterOfPosition(start);
105
+ const tail = source.getLineAndCharacterOfPosition(end);
106
+ return {
107
+ fileName: source.fileName,
108
+ start,
109
+ end,
110
+ line: head.line,
111
+ character: head.character,
112
+ endLine: tail.line,
113
+ endCharacter: tail.character
114
+ };
115
+ };
116
+ const extentOf = (source, node) => ({
117
+ start: node.getStart(source),
118
+ end: node.getEnd()
119
+ });
120
+ const unwrap = (expression) => {
121
+ let current = expression;
122
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
123
+ current = current.expression;
124
+ return current;
125
+ };
126
+ const moduleRefOf = (expression, holder) => {
127
+ let current = expression && unwrap(expression);
128
+ while (current && ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression))
129
+ current = current.expression.expression;
130
+ if (current && ts.isStringLiteral(current))
131
+ return current.text;
132
+ if (!current || !ts.isPropertyAccessExpression(current))
133
+ return null;
134
+ return ts.isIdentifier(current.expression) && current.expression.text === holder ? current.name.text : null;
135
+ };
136
+ const returnedObject = (builder) => builder ? ast.firstObjectReturnedByArrow(builder) : null;
137
+ const builderOf = (call) => [...call.arguments].reverse().find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
138
+ const guardNameOf = (expression) => {
139
+ const value = unwrap(expression);
140
+ return ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) ? `${value.expression.text}.${value.name.text}` : ast.expressionName(value);
141
+ };
142
+ const guardsIn = (object) => {
143
+ const guards = object?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "guards")?.initializer;
144
+ if (!guards)
145
+ return [];
146
+ if (ts.isArrayLiteralExpression(guards))
147
+ return guards.elements.map(guardNameOf).filter((name) => name !== null);
148
+ if (ts.isObjectLiteralExpression(guards))
149
+ return guards.properties.filter(ts.isPropertyAssignment).flatMap((property) => {
150
+ const value = unwrap(property.initializer);
151
+ const names = ts.isArrayLiteralExpression(value) ? value.elements.map(guardNameOf) : [guardNameOf(value)];
152
+ return names.map((name) => `${ast.nodeName(property.name)} ${name ?? "?"}`);
153
+ });
154
+ const single = guardNameOf(guards);
155
+ return single ? [single] : [];
156
+ };
157
+ const optionObjectOf = (call) => call.arguments.find((arg) => ts.isObjectLiteralExpression(arg) && arg.properties.some((property) => ast.propertyName(property) === "guards"));
158
+ const chainOf = (initializer) => {
159
+ let root = unwrap(initializer);
160
+ const methods = [];
161
+ while (ts.isCallExpression(root) && ts.isPropertyAccessExpression(root.expression) && ts.isCallExpression(unwrap(root.expression.expression))) {
162
+ methods.push(root.expression.name.text);
163
+ root = unwrap(root.expression.expression);
164
+ }
165
+ return { root: ts.isCallExpression(root) ? root : null, methods };
166
+ };
167
+ const keyName = (property) => ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) || ts.isShorthandPropertyAssignment(property) ? property.name : null;
168
+ const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
169
+ const name = ast.propertyName(property);
170
+ const node = keyName(property);
171
+ return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
172
+ });
173
+ const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
174
+ if (!ts.isPropertyAssignment(property))
175
+ return [];
176
+ const name = ast.nodeName(property.name);
177
+ if (!name)
178
+ return [];
179
+ const { root, methods } = chainOf(property.initializer);
180
+ return [
181
+ {
182
+ name,
183
+ span: spanOf(source, property.name),
184
+ extent: extentOf(source, property),
185
+ guards: root ? guardsIn(optionObjectOf(root)) : [],
186
+ live: methods.includes("live"),
187
+ argCount: methods.filter((method) => argMethods.has(method)).length
188
+ }
189
+ ];
190
+ });
191
+ const verbReaderOf = (parameter) => {
192
+ const verbs = new Map;
193
+ if (parameter && ts.isObjectBindingPattern(parameter.name))
194
+ for (const element of parameter.name.elements) {
195
+ const verb = element.propertyName ?? element.name;
196
+ if (ts.isIdentifier(verb) && ts.isIdentifier(element.name))
197
+ verbs.set(element.name.text, verb.text);
198
+ }
199
+ const holder = parameter && ts.isIdentifier(parameter.name) ? parameter.name.text : null;
200
+ return (callee) => {
201
+ if (callee && ts.isIdentifier(callee))
202
+ return verbs.get(callee.text) ?? null;
203
+ if (callee && ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && callee.expression.text === holder)
204
+ return callee.name.text;
205
+ return null;
206
+ };
207
+ };
208
+ const numberOf = (expression) => {
209
+ const value = unwrap(expression);
210
+ if (ts.isNumericLiteral(value))
211
+ return Number(value.text.replace(/_/g, ""));
212
+ if (!ts.isBinaryExpression(value))
213
+ return null;
214
+ const [left, right] = [numberOf(value.left), numberOf(value.right)];
215
+ if (left === null || right === null)
216
+ return null;
217
+ const operator = value.operatorToken.kind;
218
+ if (operator === ts.SyntaxKind.AsteriskToken)
219
+ return left * right;
220
+ if (operator === ts.SyntaxKind.PlusToken)
221
+ return left + right;
222
+ if (operator === ts.SyntaxKind.MinusToken)
223
+ return left - right;
224
+ return operator === ts.SyntaxKind.SlashToken && right ? left / right : null;
225
+ };
226
+ const durationOf = (expression, source) => {
227
+ if (!expression)
228
+ return null;
229
+ const ms = numberOf(expression);
230
+ if (ms === null)
231
+ return expression.getText(source);
232
+ const [size, unit] = intervalUnits.find(([size]) => ms >= size && ms % size === 0) ?? [1, "ms"];
233
+ return `${ms / size}${unit}`;
234
+ };
235
+ const modesOf = (option) => {
236
+ const object = option && unwrap(option);
237
+ if (!object || !ts.isObjectLiteralExpression(object))
238
+ return [];
239
+ return object.properties.filter(ts.isPropertyAssignment).flatMap((property) => {
240
+ const name = ast.nodeName(property.name);
241
+ const value = unwrap(property.initializer);
242
+ if (name === "enabled")
243
+ return value.kind === ts.SyntaxKind.FalseKeyword ? ["disabled"] : [];
244
+ if (name === "once")
245
+ return value.kind === ts.SyntaxKind.TrueKeyword ? ["once"] : [];
246
+ if (name === "lock")
247
+ return value.kind === ts.SyntaxKind.FalseKeyword ? ["unlocked"] : [];
248
+ if (name !== "serverMode" && name !== "operationMode")
249
+ return [];
250
+ const values = ts.isArrayLiteralExpression(value) ? [...value.elements] : [value];
251
+ return values.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : ts.isPropertyAccessExpression(element) ? [element.name.text] : []);
252
+ });
253
+ };
254
+ const internalKeysOf = (source, builder) => {
255
+ const verbOf = verbReaderOf(builder?.parameters[0]);
256
+ return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
257
+ if (!ts.isPropertyAssignment(property))
258
+ return [];
259
+ const name = ast.nodeName(property.name);
260
+ if (!name)
261
+ return [];
262
+ const { root } = chainOf(property.initializer);
263
+ const type = root ? verbOf(root.expression) : null;
264
+ const [first, second] = root?.arguments ?? [];
265
+ const schedule = type === "cron" && first ? ts.isStringLiteralLike(first) ? first.text : first.getText(source) : type === "interval" || type === "timeout" ? durationOf(first, source) : null;
266
+ const option = type === "initialize" || type === "destroy" ? first : second;
267
+ return [
268
+ {
269
+ name,
270
+ span: spanOf(source, property.name),
271
+ extent: extentOf(source, property),
272
+ type,
273
+ schedule,
274
+ modes: modesOf(option)
275
+ }
276
+ ];
277
+ });
278
+ };
279
+ const plugsOf = (source, builder) => {
280
+ const verbOf = verbReaderOf(builder?.parameters[0]);
281
+ return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
282
+ if (!ts.isPropertyAssignment(property))
283
+ return [];
284
+ const name = ast.nodeName(property.name);
285
+ const { root } = chainOf(property.initializer);
286
+ const [target] = root && verbOf(root.expression) === "plug" ? root.arguments : [];
287
+ const targetName = target && guardNameOf(target);
288
+ return name && targetName ? [{ name, span: spanOf(source, property.name), target: targetName }] : [];
289
+ });
290
+ };
291
+ const endpointKeysOf = (source, builder) => {
292
+ const verbOf = verbReaderOf(builder?.parameters[0]);
293
+ return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
294
+ if (!ts.isPropertyAssignment(property))
295
+ return [];
296
+ const name = ast.nodeName(property.name);
297
+ if (!name)
298
+ return [];
299
+ const { root, methods } = chainOf(property.initializer);
300
+ return [
301
+ {
302
+ name,
303
+ span: spanOf(source, property.name),
304
+ extent: extentOf(source, property),
305
+ verb: root ? verbOf(root.expression) : null,
306
+ guards: root ? guardsIn(optionObjectOf(root)) : [],
307
+ argCount: methods.filter((method) => argMethods.has(method)).length
308
+ }
309
+ ];
310
+ });
311
+ };
312
+ const filterQueriesOf = (source, builder) => {
313
+ const query = returnedObject(builder)?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "query")?.initializer;
314
+ return query && ts.isObjectLiteralExpression(query) ? objectKeys(source, query) : [];
315
+ };
316
+ const typeOf = (expression) => {
317
+ const value = expression && unwrap(expression);
318
+ if (value && ts.isArrayLiteralExpression(value))
319
+ return { type: typeOf(value.elements[0]).type, array: true };
320
+ return { type: value ? ast.expressionName(value) : null, array: false };
321
+ };
322
+ const fieldsOf = (source, object) => (object?.properties ?? []).flatMap((property) => {
323
+ const name = ast.propertyName(property);
324
+ const node = keyName(property);
325
+ if (!name || !node)
326
+ return [];
327
+ const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
328
+ return [
329
+ {
330
+ name,
331
+ span: spanOf(source, node),
332
+ extent: extentOf(source, property),
333
+ ...typeOf(root?.arguments[0]),
334
+ optional: methods.includes("optional")
335
+ }
336
+ ];
337
+ });
338
+ const constantOf = (source, name, call) => {
339
+ const parents = call.arguments.map(unwrap).filter((arg) => ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)).map((arg) => ast.expressionName(arg)).filter((parent) => parent !== null);
340
+ const fields = call.arguments.flatMap((arg) => fieldsOf(source, ast.firstObjectReturnedByArrow(arg)));
341
+ return { ...name, parents, fields };
342
+ };
343
+ const enumValuesOf = (call) => {
344
+ const list = call.arguments[1] && unwrap(call.arguments[1]);
345
+ if (!list || !ts.isArrayLiteralExpression(list))
346
+ return [];
347
+ return list.elements.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : []);
348
+ };
349
+ const methodsOf = (source, statement) => statement.members.flatMap((member) => {
350
+ if (!ts.isMethodDeclaration(member) || !member.body)
351
+ return [];
352
+ const name = ast.nodeName(member.name);
353
+ if (!name || ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))
354
+ return [];
355
+ return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
356
+ });
357
+ const importsOf = (statement) => {
358
+ const { importClause, moduleSpecifier } = statement;
359
+ if (!importClause || !ts.isStringLiteral(moduleSpecifier))
360
+ return [];
361
+ const from = moduleSpecifier.text;
362
+ const bindings = importClause.namedBindings;
363
+ return [
364
+ ...importClause.name ? [{ name: importClause.name.text, imported: "default", from }] : [],
365
+ ...bindings && ts.isNamespaceImport(bindings) ? [{ name: bindings.name.text, imported: "*", from }] : [],
366
+ ...bindings && ts.isNamedImports(bindings) ? bindings.elements.map((element) => ({
367
+ name: element.name.text,
368
+ imported: (element.propertyName ?? element.name).text,
369
+ from
370
+ })) : []
371
+ ];
372
+ };
373
+ const implementsGuard = (node, guardNames) => !!node.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ImplementsKeyword && clause.types.some(({ expression }) => guardNames.has(ast.expressionName(expression) ?? "")));
374
+ const guardClassIn = (node, guardNames) => {
375
+ if ((ts.isClassExpression(node) || ts.isClassDeclaration(node)) && implementsGuard(node, guardNames))
376
+ return node;
377
+ return ts.forEachChild(node, (child) => guardClassIn(child, guardNames)) ?? null;
378
+ };
379
+ const callsIn = (node, names) => {
380
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text))
381
+ return node.expression.text;
382
+ return ts.forEachChild(node, (child) => callsIn(child, names)) ?? null;
383
+ };
384
+ const scopeOf = (node) => {
385
+ const scope = node.members.find((member) => ts.isPropertyDeclaration(member) && ast.nodeName(member.name) === "scope" && !!ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))?.initializer;
386
+ const value = scope && unwrap(scope);
387
+ return value && ts.isStringLiteralLike(value) ? value.text : null;
388
+ };
389
+ const docOf = (source, node) => {
390
+ const { text } = source;
391
+ const ranges = ts.getLeadingCommentRanges(text, node.getFullStart()) ?? [];
392
+ const comments = [];
393
+ let end = node.getStart(source);
394
+ for (const range of [...ranges].reverse()) {
395
+ if ((text.slice(range.end, end).match(/\n/g)?.length ?? 0) > 1)
396
+ break;
397
+ comments.unshift(text.slice(range.pos, range.end));
398
+ end = range.pos;
399
+ }
400
+ const lines = comments.flatMap((comment) => comment.split(`
401
+ `)).map((line) => line.trim().replace(/^\/\*\*?|\*\/$/g, "").replace(/^(?:\/\/[?*!]?|\*)/, "").replace(/\{@link\s+([^}\s|]+)[^}]*\}/g, "$1").trim()).filter((line) => line && !/^(?:biome-ignore|eslint-|@ts-)/.test(line));
402
+ return lines.length ? lines.join(" ") : null;
403
+ };
404
+ const functionsOf = (source) => {
405
+ const functions = new Map;
406
+ for (const statement of source.statements) {
407
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body)
408
+ functions.set(statement.name.text, { statement, name: statement.name, body: statement.body });
409
+ if (!ts.isVariableStatement(statement))
410
+ continue;
411
+ for (const declaration of statement.declarationList.declarations) {
412
+ const initializer = declaration.initializer && unwrap(declaration.initializer);
413
+ if (initializer && ts.isIdentifier(declaration.name) && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)))
414
+ functions.set(declaration.name.text, { statement, name: declaration.name, body: initializer.body });
415
+ }
416
+ }
417
+ return functions;
418
+ };
419
+ const guardFactoriesOf = (source, guardNames) => {
420
+ const functions = functionsOf(source);
421
+ const factories = new Map;
422
+ const factoryOf = (name, scope, via) => {
423
+ const found = functions.get(name);
424
+ if (!found)
425
+ return;
426
+ factories.set(name, {
427
+ name,
428
+ span: spanOf(source, found.name),
429
+ extent: extentOf(source, found.statement),
430
+ scope,
431
+ doc: docOf(source, found.statement),
432
+ via
433
+ });
434
+ };
435
+ for (const [name, { body }] of functions) {
436
+ const guard = guardClassIn(body, guardNames);
437
+ if (guard)
438
+ factoryOf(name, scopeOf(guard), null);
439
+ }
440
+ for (let isGrowing = true;isGrowing; ) {
441
+ isGrowing = false;
442
+ for (const [name, { body }] of functions) {
443
+ const called = factories.has(name) ? null : callsIn(body, new Set(factories.keys()));
444
+ if (!called)
445
+ continue;
446
+ factoryOf(name, factories.get(called)?.scope ?? null, called);
447
+ isGrowing = true;
448
+ }
449
+ }
450
+ return factories;
451
+ };
452
+ const guardsOf = (source, imports) => {
453
+ const guardNames = new Set([
454
+ "Guard",
455
+ ...imports.filter(({ imported }) => imported === "Guard").map(({ name }) => name)
456
+ ]);
457
+ const imported = new Set(imports.map(({ name }) => name));
458
+ const factories = guardFactoriesOf(source, guardNames);
459
+ const guards = [];
460
+ const calls = [];
461
+ for (const statement of source.statements) {
462
+ if (ts.isClassDeclaration(statement) && statement.name && implementsGuard(statement, guardNames))
463
+ guards.push({
464
+ name: statement.name.text,
465
+ span: spanOf(source, statement.name),
466
+ extent: extentOf(source, statement),
467
+ scope: scopeOf(statement),
468
+ doc: docOf(source, statement),
469
+ factory: null
470
+ });
471
+ if (!ts.isVariableStatement(statement))
472
+ continue;
473
+ for (const declaration of statement.declarationList.declarations) {
474
+ const initializer = declaration.initializer && unwrap(declaration.initializer);
475
+ if (!initializer || !ts.isIdentifier(declaration.name))
476
+ continue;
477
+ const made = {
478
+ name: declaration.name.text,
479
+ span: spanOf(source, declaration.name),
480
+ extent: extentOf(source, statement),
481
+ doc: docOf(source, statement)
482
+ };
483
+ if (ts.isClassExpression(initializer) && implementsGuard(initializer, guardNames))
484
+ guards.push({ ...made, scope: scopeOf(initializer), factory: null });
485
+ if (!ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression))
486
+ continue;
487
+ const callee = initializer.expression.text;
488
+ const factory = factories.get(callee);
489
+ if (factory)
490
+ guards.push({ ...made, scope: factory.scope, factory: callee });
491
+ else if (imported.has(callee))
492
+ calls.push({ ...made, callee });
493
+ }
494
+ }
495
+ return { guards, guardFactories: [...factories.values()], guardCalls: calls };
496
+ };
497
+ const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
498
+ const importsIn = (source) => source.statements.filter(ts.isImportDeclaration).flatMap(importsOf);
499
+ const read = (source, owner) => {
500
+ const imports = importsIn(source);
501
+ const facts = {
502
+ fileName: source.fileName,
503
+ owner,
504
+ imports,
505
+ ...guardsOf(source, imports),
506
+ constants: [],
507
+ enums: [],
508
+ filters: [],
509
+ models: [],
510
+ services: [],
511
+ slices: [],
512
+ endpoints: [],
513
+ internals: [],
514
+ adaptors: [],
515
+ stores: []
516
+ };
517
+ for (const statement of source.statements) {
518
+ if (!ts.isClassDeclaration(statement) || !statement.name)
519
+ continue;
520
+ const heritage = ast.heritageCallNamed(statement, moduleCallees);
521
+ if (!heritage)
522
+ continue;
523
+ const { callee, call } = heritage;
524
+ const name = { name: statement.name.text, span: spanOf(source, statement.name) };
525
+ const builder = builderOf(call);
526
+ if (callee === "via")
527
+ facts.constants.push(constantOf(source, name, call));
528
+ else if (callee === "enumOf")
529
+ facts.enums.push({ ...name, values: enumValuesOf(call) });
530
+ else if (callee === "from")
531
+ facts.filters.push({ ...name, queries: filterQueriesOf(source, builder) });
532
+ else if (callee === "into")
533
+ facts.models.push({ ...name, model: name.name.endsWith("Model") ? lowerFirst(name.name.slice(0, -5)) : null });
534
+ else if (callee === "serve")
535
+ facts.services.push({
536
+ ...name,
537
+ model: moduleRefOf(call.arguments[0], "db"),
538
+ methods: methodsOf(source, statement),
539
+ plugs: plugsOf(source, builder)
540
+ });
541
+ else if (callee === "slice")
542
+ facts.slices.push({
543
+ ...name,
544
+ model: moduleRefOf(call.arguments[0], "srv"),
545
+ guards: guardsIn(call.arguments.find(ts.isObjectLiteralExpression)),
546
+ keys: sliceKeysOf(source, builder)
547
+ });
548
+ else if (callee === "internal")
549
+ facts.internals.push({
550
+ ...name,
551
+ model: moduleRefOf(call.arguments[0], "srv"),
552
+ keys: internalKeysOf(source, builder)
553
+ });
554
+ else if (callee === "adapt" || callee === "dangerouslyAdapt") {
555
+ const refName = call.arguments[0] && unwrap(call.arguments[0]);
556
+ facts.adaptors.push({
557
+ ...name,
558
+ extent: extentOf(source, statement),
559
+ refName: refName && ts.isStringLiteralLike(refName) ? refName.text : null,
560
+ plugs: plugsOf(source, builder),
561
+ doc: docOf(source, statement)
562
+ });
563
+ } else if (callee === "endpoint")
564
+ facts.endpoints.push({
565
+ ...name,
566
+ model: moduleRefOf(call.arguments[0], "srv"),
567
+ keys: endpointKeysOf(source, builder)
568
+ });
569
+ else if (callee === "store")
570
+ facts.stores.push({
571
+ ...name,
572
+ model: moduleRefOf(call.arguments[0], "sig"),
573
+ stateKeys: objectKeys(source, returnedObject(builder)),
574
+ actions: methodsOf(source, statement)
575
+ });
576
+ }
577
+ return facts;
578
+ };
579
+ return { read, spanOf, extentOf, docOf, importsIn, moduleRefOf, chainOf, unwrap };
580
+ };
581
+
582
+ // pkgs/akanjs/common/trimSlashes.ts
583
+ var trimTrailingSlashes = (value) => {
584
+ let end = value.length;
585
+ while (end > 0 && value[end - 1] === "/")
586
+ end -= 1;
587
+ return value.slice(0, end);
588
+ };
589
+
590
+ // pkgs/akanjs/common/routeConvention.ts
591
+ var ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
592
+ var RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
593
+ var INTERNAL_ROOT_LAYOUT_LEAF = "__root_layout";
594
+ var IMPLICIT_LOCALE_SEGMENT = "[lang]";
595
+ var DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
596
+ var PAGE_ROUTE_EXPORTS = new Set([
597
+ "default",
598
+ "pageConfig",
599
+ "head",
600
+ "generateHead",
601
+ "Loading",
602
+ "guard"
603
+ ]);
604
+ var LAYOUT_ROUTE_EXPORTS = new Set([...PAGE_ROUTE_EXPORTS, "NotFound", "Error"]);
605
+ var ROOT_LAYOUT_ROUTE_EXPORTS = new Set([
606
+ ...LAYOUT_ROUTE_EXPORTS,
607
+ "fonts",
608
+ "manifest",
609
+ "theme",
610
+ "reconnect",
611
+ "wsConnect",
612
+ "layoutStyle",
613
+ "viewport",
614
+ "McpAppRoot"
615
+ ]);
616
+ var RESERVED_ROUTE_CONFIG_EXPORTS = new Set([...ROOT_LAYOUT_ROUTE_EXPORTS].filter((name) => name !== "default" && !/^[A-Z]/.test(name)));
617
+ function parseRouteModuleKey(key) {
618
+ const match = ROUTE_SOURCE_RE.exec(key);
619
+ if (!match)
620
+ throw new Error(`[route-convention] invalid route module key: ${key}`);
621
+ const file = match[1];
622
+ const ext = match[2];
623
+ const moduleSegments = file.split("/").filter(Boolean);
624
+ const leaf = moduleSegments.at(-1);
625
+ if (!leaf)
626
+ throw new Error(`[route-convention] invalid route module key: ${key}`);
627
+ if (moduleSegments.includes(IMPLICIT_LOCALE_SEGMENT)) {
628
+ throw new Error(`[route-convention] Akan.js injects \`[lang]\` automatically. Move files one level up: ${key}`);
629
+ }
630
+ const isInternalRootLayout = leaf === INTERNAL_ROOT_LAYOUT_LEAF;
631
+ const kind = leaf === "_layout" || isInternalRootLayout ? "layout" : leaf === "_overrides" ? "overrides" : "page";
632
+ if (leaf.startsWith("_") && !RESERVED_ROUTE_FILES.has(leaf) && !isInternalRootLayout) {
633
+ throw new Error(`[route-convention] unsupported reserved route file "${leaf}" in ${key}`);
634
+ }
635
+ const sourceRouteSegments = DIRECTORY_SCOPED_LEAVES.has(leaf) || isInternalRootLayout ? moduleSegments.slice(0, -1) : moduleSegments;
636
+ const routeSegments = [IMPLICIT_LOCALE_SEGMENT, ...sourceRouteSegments];
637
+ for (const segment of routeSegments)
638
+ validateRouteSegment(segment, key);
639
+ return {
640
+ key,
641
+ kind,
642
+ routeSegments,
643
+ moduleSegments,
644
+ sourceRouteSegments,
645
+ ext,
646
+ leaf,
647
+ pattern: normalizeRoutePattern(routeSegments),
648
+ isInternalRootLayout
649
+ };
650
+ }
651
+ function tryParseRouteModuleKey(key) {
652
+ try {
653
+ return parseRouteModuleKey(key);
654
+ } catch {
655
+ return null;
656
+ }
657
+ }
658
+ function normalizeRoutePattern(segments) {
659
+ const parts = segments.map(routeSegmentToPatternPart).filter((part) => part.length > 0);
660
+ return trimTrailingSlashes(`/${parts.join("/")}`) || "/";
661
+ }
662
+ function routeSegmentToPatternPart(segment) {
663
+ if (/^\(.+\)$/.test(segment))
664
+ return "";
665
+ return segment.replace(/^\[([^\]]+)\]$/, ":$1");
666
+ }
667
+ function validateRouteSegment(segment, key) {
668
+ if (segment.includes("[...") || segment.includes("[[...")) {
669
+ throw new Error(`[route-convention] catch-all routes are not supported yet: ${key}`);
670
+ }
671
+ const dynamic = /^\[(.+)\]$/.exec(segment);
672
+ if (dynamic && (dynamic[1]?.includes("[") || dynamic[1]?.includes("]") || dynamic[1]?.startsWith("..."))) {
673
+ throw new Error(`[route-convention] invalid dynamic route segment "${segment}" in ${key}`);
674
+ }
675
+ }
676
+
677
+ // pkgs/@akanjs/language/routeFacts.ts
678
+ class RouteFactReader {
679
+ static #pagePattern = /^((?:apps|libs)\/[^/]+)\/page\/((?!(?:.*\/)?\(libs\)\/).+\.tsx?)$/;
680
+ static #guardPattern = /\.guard\s*\(/;
681
+ static #aliasPattern = /^export default ([A-Za-z_$][\w$]*)\s*;?\s*$/m;
682
+ static #workspacePattern = /^(?:\.|@(?:apps|libs)\/)/;
683
+ #ts;
684
+ #reader;
685
+ #parse;
686
+ constructor(ts, reader, parse) {
687
+ this.#ts = ts;
688
+ this.#reader = reader;
689
+ this.#parse = parse;
690
+ }
691
+ static fileOf(relative) {
692
+ const match = relative.match(RouteFactReader.#pagePattern);
693
+ return match ? { owner: match[1], key: `./${match[2]}` } : null;
694
+ }
695
+ read(fileName, info, text, words) {
696
+ const ts = this.#ts;
697
+ const parsed = tryParseRouteModuleKey(info.key);
698
+ if (!parsed || parsed.kind === "overrides" || parsed.isInternalRootLayout)
699
+ return null;
700
+ const starts = [...text.matchAll(/^export default\b/gm)].map(({ index }) => index);
701
+ const source = RouteFactReader.#guardPattern.test(text) || starts.length > 1 ? this.#parse(fileName, text) : null;
702
+ const exported = source ? this.#exportOf(source) : null;
703
+ const start = exported ? exported.getStart(source ?? undefined) : starts[0] ?? 0;
704
+ const head = text.slice(0, start);
705
+ return {
706
+ fileName,
707
+ owner: info.owner,
708
+ kind: parsed.kind,
709
+ pattern: normalizeRoutePattern(parsed.sourceRouteSegments),
710
+ dir: parsed.moduleSegments.slice(0, -1),
711
+ isRoot: words.includes(`
712
+ rootLayout
713
+ `),
714
+ line: head.split(`
715
+ `).length - 1,
716
+ character: start - head.lastIndexOf(`
717
+ `) - 1,
718
+ guard: source && exported ? this.#guardOf(source, exported) : null,
719
+ imports: source ? this.#reader.importsIn(source) : [],
720
+ alias: source ? exported && ts.isExportAssignment(exported) && ts.isIdentifier(exported.expression) ? exported.expression.text : null : text.match(RouteFactReader.#aliasPattern)?.[1] ?? null
721
+ };
722
+ }
723
+ #exportOf(source) {
724
+ const ts = this.#ts;
725
+ return source.statements.find((statement) => ts.isExportAssignment(statement) || ts.canHaveModifiers(statement) && !!ts.getModifiers(statement)?.some(({ kind }) => kind === ts.SyntaxKind.DefaultKeyword)) ?? null;
726
+ }
727
+ declarationIn(source, name) {
728
+ const ts = this.#ts;
729
+ for (const statement of source.statements) {
730
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === name)
731
+ return { statement, declared: statement.name, initializer: null };
732
+ if (!ts.isVariableStatement(statement))
733
+ continue;
734
+ const declaration = statement.declarationList.declarations.find((item) => ts.isIdentifier(item.name) && item.name.text === name);
735
+ if (declaration)
736
+ return { statement, declared: declaration.name, initializer: declaration.initializer ?? null };
737
+ }
738
+ return null;
739
+ }
740
+ #guardOf(source, exported) {
741
+ const ts = this.#ts;
742
+ let current = ts.isExportAssignment(exported) ? this.#reader.unwrap(exported.expression) : null;
743
+ if (current && ts.isIdentifier(current)) {
744
+ const initializer = this.declarationIn(source, current.text)?.initializer;
745
+ current = initializer ? this.#reader.unwrap(initializer) : null;
746
+ }
747
+ while (current && ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
748
+ const { name, expression } = current.expression;
749
+ const [stage] = current.arguments;
750
+ if (name.text === "guard" && stage)
751
+ return { calls: this.#callsIn(source, stage), span: this.#reader.spanOf(source, name) };
752
+ current = this.#reader.unwrap(expression);
753
+ }
754
+ return null;
755
+ }
756
+ #callsIn(source, stage) {
757
+ const ts = this.#ts;
758
+ const imported = new Set(this.#reader.importsIn(source).filter(({ from }) => RouteFactReader.#workspacePattern.test(from)).map(({ name }) => name));
759
+ const calls = new Map;
760
+ const callOf = (name) => {
761
+ if (calls.has(name))
762
+ return true;
763
+ const declared = this.declarationIn(source, name);
764
+ if (!declared && !imported.has(name))
765
+ return false;
766
+ calls.set(name, {
767
+ name,
768
+ local: declared && {
769
+ span: this.#reader.spanOf(source, declared.declared),
770
+ extent: this.#reader.extentOf(source, declared.statement),
771
+ doc: this.#reader.docOf(source, declared.statement)
772
+ }
773
+ });
774
+ return true;
775
+ };
776
+ const visit = (node) => {
777
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && callOf(node.expression.text))
778
+ return;
779
+ ts.forEachChild(node, visit);
780
+ };
781
+ if (ts.isIdentifier(stage))
782
+ callOf(stage.text);
783
+ else
784
+ visit(stage);
785
+ return [...calls.values()];
786
+ }
787
+ }
788
+
74
789
  // pkgs/@akanjs/language/akanGraph.ts
75
790
  var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
76
791
  var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
@@ -80,6 +795,14 @@ var scalarPattern = /\/lib\/__scalar\/([^/]+)\/[^/]+\.constant\.ts$/;
80
795
  var moduleDirPattern = /\/lib\/([^/]+)\//;
81
796
  var typeKinds = new Set(["model", "scalar", "enum"]);
82
797
  var clientRoots = new Set(["fetch", "st", "sig"]);
798
+ var builtinGuards = {
799
+ Public: { scope: "account", doc: "Passes every caller, signed in or not." },
800
+ None: { scope: "account", doc: "Refuses every caller." }
801
+ };
802
+ var frameworkOwner = "akanjs";
803
+ var frameworkPattern = /^akanjs(?:\/|$)/;
804
+ var aliasPattern = /^@(apps|libs)\/([^/]+)/;
805
+ var maxMapDepth = 4;
83
806
 
84
807
  class GraphBuilder {
85
808
  #source;
@@ -87,8 +810,16 @@ class GraphBuilder {
87
810
  #edges = new Map;
88
811
  #contexts = new Map;
89
812
  #members = new Map;
813
+ #guards = new Map;
814
+ #mapEntries = new Map;
815
+ #routeFiles = new Map;
816
+ #declarations = new Map;
817
+ #routes;
818
+ #adaptors = new Map;
819
+ #plugs = new Map;
90
820
  constructor(source) {
91
821
  this.#source = source;
822
+ this.#routes = new RouteFactReader(source.ts, source.reader, source.parse);
92
823
  }
93
824
  build() {
94
825
  const { facts } = this.#source;
@@ -100,6 +831,17 @@ class GraphBuilder {
100
831
  ...[...constantsByOwner].filter(([key]) => key !== owner && key?.startsWith("libs/")).flatMap(([, list]) => list)
101
832
  ];
102
833
  const typed = [];
834
+ for (const file of facts)
835
+ for (const fact of file.adaptors) {
836
+ const declared = { fact, owner: file.owner, fileName: file.fileName };
837
+ this.#adaptors.set(fact.name, [...this.#adaptors.get(fact.name) ?? [], declared]);
838
+ this.#adaptorNodeOf(declared);
839
+ }
840
+ for (const file of facts)
841
+ for (const fact of file.adaptors)
842
+ for (const plug of fact.plugs)
843
+ this.#addEdge(this.#key("adaptor", file.owner, fact.name), this.#adaptorIdOf(plug.target, file), "plugs", plug.name, this.#location(plug.span));
844
+ this.#declareGuards(facts);
103
845
  for (const file of facts) {
104
846
  const { owner } = file;
105
847
  const scalar = this.#source.relative(file.fileName).match(scalarPattern)?.[1];
@@ -130,6 +872,10 @@ class GraphBuilder {
130
872
  }
131
873
  for (const method of service.methods)
132
874
  this.#addMember("method", owner, refName, method, `${service.name}.${method.name}`, file.fileName);
875
+ for (const plug of service.plugs)
876
+ this.#plugs.set(`${owner ?? ""}
877
+ ${refName}
878
+ ${plug.name}`, this.#adaptorIdOf(plug.target, file));
133
879
  }
134
880
  for (const filter of file.filters) {
135
881
  if (!filter.name.endsWith("Filter"))
@@ -145,12 +891,12 @@ class GraphBuilder {
145
891
  continue;
146
892
  const fallback = this.#addNode(this.#key("slice", owner, refName), "slice", owner, refName, "", slice.span, slice.guards);
147
893
  for (const guard of slice.guards)
148
- this.#guard(fallback, guard);
894
+ this.#guard(fallback, guard, file);
149
895
  for (const key of slice.keys) {
150
896
  const detail = [...key.guards, ...key.live ? ["live"] : []];
151
897
  const node = this.#addMember("slice", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
152
898
  for (const guard of key.guards)
153
- this.#guard(node, guard);
899
+ this.#guard(node, guard, file);
154
900
  }
155
901
  }
156
902
  for (const endpoint of file.endpoints) {
@@ -161,9 +907,13 @@ class GraphBuilder {
161
907
  const detail = [...key.verb ? [key.verb] : [], ...key.guards];
162
908
  const node = this.#addMember("endpoint", owner, refName, key, `${refName}.${key.name}`, file.fileName, detail);
163
909
  for (const guard of key.guards)
164
- this.#guard(node, guard);
910
+ this.#guard(node, guard, file);
165
911
  }
166
912
  }
913
+ for (const internal of file.internals)
914
+ for (const key of internal.keys)
915
+ if (internal.model && key.type !== "resolveField")
916
+ this.#addMember("internal", owner, internal.model, key, `${internal.name}.${key.name}`, file.fileName, [key.type, key.schedule, ...key.modes].filter((item) => item !== null));
167
917
  for (const store of file.stores)
168
918
  if (store.model)
169
919
  for (const action of store.actions)
@@ -180,6 +930,7 @@ class GraphBuilder {
180
930
  if (field.ref)
181
931
  this.#addEdge(node.id, field.ref, "field", `${field.name}${field.array ? "[]" : ""}`, field.at);
182
932
  }
933
+ this.#addRoutes();
183
934
  for (const [fileName, words] of this.#source.files)
184
935
  this.#walk(fileName, words);
185
936
  return {
@@ -223,43 +974,329 @@ class GraphBuilder {
223
974
  this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
224
975
  return node;
225
976
  }
226
- #hashOf(text, { start, end }) {
227
- if (!this.#source.digests || text === undefined)
977
+ #hashOf(...parts) {
978
+ if (!this.#source.digests)
979
+ return null;
980
+ let hash = 2166136261;
981
+ for (const [text, { start, end }] of parts) {
982
+ if (text === undefined)
983
+ return null;
984
+ for (let idx = start;idx < end; idx++) {
985
+ const code = text.charCodeAt(idx);
986
+ if (code !== 13)
987
+ hash = Math.imul(hash ^ code, 16777619);
988
+ }
989
+ }
990
+ return (hash >>> 0).toString(16).padStart(8, "0");
991
+ }
992
+ #digest(node, text, extent) {
993
+ const digest = node.digest ? null : this.#hashOf([text, extent]);
994
+ if (digest)
995
+ node.digest = digest;
996
+ }
997
+ #addEdge(from, to, kind, label, at) {
998
+ if (from === to)
999
+ return;
1000
+ const key = `${from}
1001
+ ${to}
1002
+ ${kind}
1003
+ ${label ?? ""}`;
1004
+ const existing = this.#edges.get(key);
1005
+ if (existing)
1006
+ existing.count += 1;
1007
+ else
1008
+ this.#edges.set(key, { from, to, kind, label, count: 1, at });
1009
+ }
1010
+ #guard(node, guard, file) {
1011
+ const [role, local] = guard.includes(" ") ? guard.split(" ") : [null, guard];
1012
+ if (!local || local === "?")
1013
+ return;
1014
+ this.#addEdge(node.id, this.#guardIdOf(local, file), "guard", role, node.at);
1015
+ }
1016
+ #declareGuards(facts) {
1017
+ const factories = new Map;
1018
+ for (const file of facts)
1019
+ for (const fact of file.guardFactories) {
1020
+ const factory = { fact, owner: file.owner, fileName: file.fileName, made: [] };
1021
+ factories.set(fact.name, [...factories.get(fact.name) ?? [], factory]);
1022
+ }
1023
+ const sameFile = (factory, name) => name ? factories.get(name)?.find((other) => other.fileName === factory.fileName) : undefined;
1024
+ const declare = (guard, factory) => {
1025
+ this.#guards.set(guard.name, [...this.#guards.get(guard.name) ?? [], guard]);
1026
+ factory?.made.push(guard);
1027
+ };
1028
+ const madeBy = (factory, fileName, extent) => {
1029
+ const parts = [{ fileName, extent }];
1030
+ const seen = new Set;
1031
+ for (let current = factory;current && !seen.has(current); current = sameFile(current, current.fact.via)) {
1032
+ seen.add(current);
1033
+ parts.push({ fileName: current.fileName, extent: current.fact.extent });
1034
+ }
1035
+ return parts;
1036
+ };
1037
+ for (const file of facts) {
1038
+ const { owner, fileName } = file;
1039
+ for (const { name, span, extent, scope, doc, factory: callee } of file.guards) {
1040
+ const factory = callee ? factories.get(callee)?.find((other) => other.fileName === fileName) : undefined;
1041
+ declare({ name, owner, span, scope, doc, parts: madeBy(factory, fileName, extent) }, factory);
1042
+ }
1043
+ for (const { name, span, extent, doc, callee } of file.guardCalls) {
1044
+ const imported = file.imports.find((item) => item.name === callee);
1045
+ if (!imported)
1046
+ continue;
1047
+ const from = this.#ownerOfSpecifier(fileName, imported.from);
1048
+ const candidates = factories.get(imported.imported === "default" || imported.imported === "*" ? callee : imported.imported) ?? [];
1049
+ const factory = candidates.find((other) => other.owner === from) ?? this.#soleVisible(candidates, from ?? owner);
1050
+ if (factory)
1051
+ declare({ name, owner, span, scope: factory.fact.scope, doc, parts: madeBy(factory, fileName, extent) }, factory);
1052
+ }
1053
+ }
1054
+ for (const list of factories.values())
1055
+ for (const { fact, made } of list)
1056
+ if (made.every((guard) => !guard.doc))
1057
+ for (const guard of made)
1058
+ guard.doc = fact.doc;
1059
+ for (const list of this.#guards.values())
1060
+ for (const guard of list)
1061
+ this.#guardNodeOf(guard);
1062
+ }
1063
+ #guardIdOf(local, site) {
1064
+ const found = this.#findGuard(local, site, 0);
1065
+ if ("guard" in found)
1066
+ return this.#guardNodeOf(found.guard);
1067
+ const { name, from } = found;
1068
+ const isFramework = from === frameworkOwner || from === undefined && !!builtinGuards[name];
1069
+ const builtin = isFramework ? builtinGuards[name] : undefined;
1070
+ const owner = isFramework ? null : from ?? null;
1071
+ const detail = [...builtin ? [builtin.scope] : [], ...isFramework ? [frameworkOwner] : []];
1072
+ const node = this.#addNode(this.#key("guard", owner, name), "guard", owner, null, name, null, detail);
1073
+ if (builtin)
1074
+ node.doc = builtin.doc;
1075
+ return node.id;
1076
+ }
1077
+ #findGuard(local, site, depth) {
1078
+ const [head = local, member] = local.split(".");
1079
+ const imported = site.imports.find((item) => item.name === head);
1080
+ const from = imported ? this.#ownerOfSpecifier(site.fileName, imported.from) : undefined;
1081
+ const holder = imported ? from : site.owner;
1082
+ if (member && imported?.imported !== "*" && holder !== undefined && holder !== frameworkOwner && depth < maxMapDepth) {
1083
+ const objectName = imported && imported.imported !== "default" ? imported.imported : head;
1084
+ for (const entry of this.#mapEntriesOf(objectName, member, imported ? null : site.fileName, holder)) {
1085
+ const found = this.#findGuard(entry.name, entry.site, depth + 1);
1086
+ if ("guard" in found)
1087
+ return found;
1088
+ }
1089
+ }
1090
+ const name = member ?? (imported && imported.imported !== "default" && imported.imported !== "*" ? imported.imported : head);
1091
+ const declared = this.#guards.get(name) ?? [];
1092
+ 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));
1093
+ return guard ? { guard } : { name, from };
1094
+ }
1095
+ #mapEntriesOf(objectName, member, fileName, owner) {
1096
+ const key = `${objectName}
1097
+ ${member}
1098
+ ${fileName ?? ""}
1099
+ ${owner ?? ""}`;
1100
+ const cached = this.#mapEntries.get(key);
1101
+ if (cached)
1102
+ return cached;
1103
+ const { ts, reader } = this.#source;
1104
+ const entries = [];
1105
+ const fileNames = [
1106
+ ...fileName ? [fileName] : [],
1107
+ ...this.#source.files.filter(([other, words]) => other !== fileName && owner !== null && !testFilePattern.test(other) && words.includes(`
1108
+ ${objectName}
1109
+ `) && words.includes(`
1110
+ ${member}
1111
+ `) && this.#source.ownerOf(other) === owner).map(([other]) => other)
1112
+ ];
1113
+ for (const holder of fileNames) {
1114
+ const text = this.#source.readText(holder);
1115
+ if (text === undefined)
1116
+ continue;
1117
+ const source = this.#source.parse(holder, text);
1118
+ const site = { fileName: holder, owner: this.#source.ownerOf(holder), imports: reader.importsIn(source) };
1119
+ for (const statement of source.statements) {
1120
+ if (!ts.isVariableStatement(statement))
1121
+ continue;
1122
+ const map = statement.declarationList.declarations.find((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === objectName)?.initializer;
1123
+ const object = map && reader.unwrap(map);
1124
+ if (!object || !ts.isObjectLiteralExpression(object))
1125
+ continue;
1126
+ for (const property of object.properties) {
1127
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === member)
1128
+ entries.push({ name: member, site });
1129
+ if (!ts.isPropertyAssignment(property) || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) || property.name.text !== member)
1130
+ continue;
1131
+ const value = reader.unwrap(property.initializer);
1132
+ if (ts.isIdentifier(value))
1133
+ entries.push({ name: value.text, site });
1134
+ else if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression))
1135
+ entries.push({ name: `${value.expression.text}.${value.name.text}`, site });
1136
+ }
1137
+ }
1138
+ }
1139
+ this.#mapEntries.set(key, entries);
1140
+ return entries;
1141
+ }
1142
+ #declarationOf(owner, name, near) {
1143
+ const key = `${owner}
1144
+ ${name}
1145
+ ${near ?? ""}`;
1146
+ const cached = this.#declarations.get(key);
1147
+ if (cached !== undefined)
1148
+ return cached;
1149
+ const isNear = (fileName) => {
1150
+ const relative = this.#source.relative(fileName);
1151
+ return !!near && (relative.startsWith(`${near}/`) || relative.replace(/\.tsx?$/, "") === near);
1152
+ };
1153
+ const candidates = this.#source.files.filter(([fileName, words]) => !testFilePattern.test(fileName) && words.includes(`
1154
+ ${name}
1155
+ `) && this.#source.ownerOf(fileName) === owner).map(([fileName]) => fileName);
1156
+ let found = null;
1157
+ for (const fileName of [...candidates.filter(isNear), ...candidates.filter((other) => !isNear(other))]) {
1158
+ const text = this.#source.readText(fileName);
1159
+ if (text === undefined)
1160
+ continue;
1161
+ const source = this.#source.parse(fileName, text);
1162
+ const declared = this.#routes.declarationIn(source, name);
1163
+ if (!declared)
1164
+ continue;
1165
+ found = { source, statement: declared.statement, declared: declared.declared };
1166
+ break;
1167
+ }
1168
+ this.#declarations.set(key, found);
1169
+ return found;
1170
+ }
1171
+ #addRoutes() {
1172
+ const routes = [];
1173
+ const layouts = new Map;
1174
+ for (const [fileName, words] of this.#source.files) {
1175
+ const relative = this.#source.relative(fileName);
1176
+ const info = RouteFactReader.fileOf(relative);
1177
+ const text = info ? this.#source.readText(fileName) : undefined;
1178
+ const fact = info && text !== undefined ? this.#routes.read(fileName, info, text, words) : null;
1179
+ if (!fact || text === undefined)
1180
+ continue;
1181
+ const span = {
1182
+ fileName,
1183
+ start: 0,
1184
+ end: 0,
1185
+ line: fact.line,
1186
+ character: fact.character,
1187
+ endLine: 0,
1188
+ endCharacter: 0
1189
+ };
1190
+ const dir = `/${fact.dir.join("/")}`;
1191
+ const guard = fact.guard ? ["guard"] : [];
1192
+ 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);
1193
+ this.#digest(node, text, { start: 0, end: text.length });
1194
+ this.#routeFiles.set(relative, { id: node.id, alias: fact.alias });
1195
+ if (fact.kind === "layout")
1196
+ layouts.set(`${fact.owner}
1197
+ ${dir}`, node);
1198
+ routes.push({ fact, node });
1199
+ for (const call of fact.guard?.calls ?? []) {
1200
+ const id = this.#routeGuardIdOf(call, fact, text);
1201
+ if (id && fact.guard)
1202
+ this.#addEdge(node.id, id, "guard", null, this.#location(fact.guard.span));
1203
+ }
1204
+ }
1205
+ const parentOf = ({ owner, dir, kind }) => {
1206
+ for (let depth = kind === "layout" ? dir.length - 1 : dir.length;depth >= 0; depth--) {
1207
+ const layout = layouts.get(`${owner}
1208
+ /${dir.slice(0, depth).join("/")}`);
1209
+ if (layout)
1210
+ return layout;
1211
+ }
228
1212
  return null;
229
- let hash = 2166136261;
230
- for (let idx = start;idx < end; idx++) {
231
- const code = text.charCodeAt(idx);
232
- if (code !== 13)
233
- hash = Math.imul(hash ^ code, 16777619);
1213
+ };
1214
+ for (const { fact, node } of routes) {
1215
+ const parent = parentOf(fact);
1216
+ if (parent)
1217
+ this.#addEdge(parent.id, node.id, "wraps", null, node.at);
234
1218
  }
235
- return (hash >>> 0).toString(16).padStart(8, "0");
236
1219
  }
237
- #digest(node, text, extent) {
238
- const digest = node.digest ? null : this.#hashOf(text, extent);
1220
+ #routeGuardIdOf({ name: local, local: declared }, fact, text) {
1221
+ if (declared) {
1222
+ const file = this.#source.relative(fact.fileName).slice(fact.owner.length + 1);
1223
+ const id = this.#key("guard", fact.owner, `${file}#${local}`);
1224
+ return this.#routeGuardNodeOf(id, fact.owner, local, declared.span, declared.doc, text, declared.extent);
1225
+ }
1226
+ const imported = fact.imports.find((item) => item.name === local);
1227
+ if (!imported)
1228
+ return null;
1229
+ const name = imported.imported !== "default" && imported.imported !== "*" ? imported.imported : local;
1230
+ const from = this.#ownerOfSpecifier(fact.fileName, imported.from);
1231
+ if (!from || from === frameworkOwner)
1232
+ return null;
1233
+ const found = this.#declarationOf(from, name, this.#pathOfSpecifier(fact.fileName, imported.from));
1234
+ if (!found)
1235
+ return null;
1236
+ const { reader } = this.#source;
1237
+ 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));
1238
+ }
1239
+ #routeGuardNodeOf(id, owner, name, span, doc, text, extent) {
1240
+ const isNew = !this.#nodes.has(id);
1241
+ const node = this.#addNode(id, "guard", owner, null, name, span, ["route"]);
1242
+ if (isNew && doc)
1243
+ node.doc = doc;
1244
+ this.#digest(node, text, extent);
1245
+ return node.id;
1246
+ }
1247
+ #adaptorIdOf(local, file) {
1248
+ const [head, member] = local.split(".");
1249
+ const imported = file.imports.find((item) => item.name === head);
1250
+ const from = imported ? this.#ownerOfSpecifier(file.fileName, imported.from) : undefined;
1251
+ const name = member ?? (imported && imported.imported !== "default" && imported.imported !== "*" ? imported.imported : head);
1252
+ const declared = this.#adaptors.get(name) ?? [];
1253
+ 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)));
1254
+ if (found)
1255
+ return this.#adaptorNodeOf(found);
1256
+ const owner = from === frameworkOwner ? null : imported ? from ?? null : file.owner;
1257
+ const detail = from === frameworkOwner ? [frameworkOwner] : [];
1258
+ return this.#addNode(this.#key("adaptor", owner, name), "adaptor", owner, null, name, null, detail).id;
1259
+ }
1260
+ #adaptorNodeOf({ fact, owner, fileName }) {
1261
+ const id = this.#key("adaptor", owner, fact.name);
1262
+ const detail = fact.refName ? [fact.refName] : [];
1263
+ const node = this.#addNode(id, "adaptor", owner, null, fact.name, fact.span, detail);
1264
+ if (fact.doc && !node.doc)
1265
+ node.doc = fact.doc;
1266
+ this.#digest(node, this.#source.readText(fileName), fact.extent);
1267
+ return node.id;
1268
+ }
1269
+ #guardNodeOf({ name, owner, span, scope, doc, parts }) {
1270
+ const node = this.#addNode(this.#key("guard", owner, name), "guard", owner, null, name, span, scope ? [scope] : []);
1271
+ if (doc && !node.doc)
1272
+ node.doc = doc;
1273
+ const digest = node.digest ? null : this.#hashOf(...parts.map(({ fileName, extent }) => [
1274
+ this.#source.readText(fileName),
1275
+ extent
1276
+ ]));
239
1277
  if (digest)
240
1278
  node.digest = digest;
241
- }
242
- #addEdge(from, to, kind, label, at) {
243
- if (from === to)
244
- return;
245
- const key = `${from}
246
- ${to}
247
- ${kind}
248
- ${label ?? ""}`;
249
- const existing = this.#edges.get(key);
250
- if (existing)
251
- existing.count += 1;
252
- else
253
- this.#edges.set(key, { from, to, kind, label, count: 1, at });
254
- }
255
- #guard(node, guard) {
256
- const [role, name] = guard.includes(" ") ? guard.split(" ") : [null, guard];
257
- if (!name || name === "?")
258
- return;
259
- const id = `guard:${name}`;
260
- if (!this.#nodes.has(id))
261
- this.#nodes.set(id, { id, kind: "guard", name, label: name, owner: null, module: null, detail: [], at: null });
262
- this.#addEdge(node.id, id, "guard", role, node.at);
1279
+ return node.id;
1280
+ }
1281
+ #ownerOfSpecifier(fileName, specifier) {
1282
+ if (frameworkPattern.test(specifier))
1283
+ return frameworkOwner;
1284
+ const path = this.#pathOfSpecifier(fileName, specifier);
1285
+ return path === null ? undefined : ownerOf(path) ?? undefined;
1286
+ }
1287
+ #pathOfSpecifier(fileName, specifier) {
1288
+ const alias = specifier.match(aliasPattern);
1289
+ if (alias)
1290
+ return `${alias[1]}/${alias[2]}${specifier.slice(alias[0].length)}`;
1291
+ if (!specifier.startsWith("."))
1292
+ return null;
1293
+ const segments = this.#source.relative(fileName).split("/").slice(0, -1);
1294
+ for (const segment of specifier.split("/"))
1295
+ if (segment === "..")
1296
+ segments.pop();
1297
+ else if (segment !== ".")
1298
+ segments.push(segment);
1299
+ return segments.join("/");
263
1300
  }
264
1301
  #moduleDirOf(fileName) {
265
1302
  const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
@@ -280,7 +1317,7 @@ ${label ?? ""}`;
280
1317
  for (const field of current.fields) {
281
1318
  if (fields.has(field.name))
282
1319
  continue;
283
- const digest = this.#hashOf(this.#source.readText(field.span.fileName), field.extent);
1320
+ const digest = this.#hashOf([this.#source.readText(field.span.fileName), field.extent]);
284
1321
  fields.set(field.name, {
285
1322
  name: field.name,
286
1323
  type: field.type,
@@ -299,6 +1336,10 @@ ${label ?? ""}`;
299
1336
  #visible(target, from) {
300
1337
  return !from || !target || target === from || target.startsWith("libs/");
301
1338
  }
1339
+ #soleVisible(declared, from) {
1340
+ const visible = declared.filter((other) => this.#visible(other.owner, from));
1341
+ return visible.length === 1 ? visible[0] : undefined;
1342
+ }
302
1343
  #targetNodeOf({ family, owner, refName, sourceKey }) {
303
1344
  const isDefault = /^[A-Z]/.test(sourceKey);
304
1345
  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;
@@ -350,7 +1391,10 @@ st
350
1391
  visit(source);
351
1392
  }
352
1393
  #link(fileName, source, identifier, chain, context, owner) {
353
- const resolved = this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
1394
+ const plugged = chain?.length === 2 && chain[0] === "this" && context.kind === "method" && this.#isServiceThis(identifier) ? this.#plugs.get(`${context.owner ?? ""}
1395
+ ${context.module}
1396
+ ${chain[1]}`) : undefined;
1397
+ const resolved = plugged ? [plugged] : this.#source.resolve(fileName, identifier).map((target) => this.#targetNodeOf(target)).filter((id) => id !== null);
354
1398
  const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
355
1399
  if (!targets.length)
356
1400
  return;
@@ -365,6 +1409,16 @@ st
365
1409
  for (const target of new Set(own.length ? own : targets))
366
1410
  this.#addEdge(context.id, target, kind, identifier.text, at);
367
1411
  }
1412
+ #isServiceThis(identifier) {
1413
+ const { ts } = this.#source;
1414
+ for (let current = identifier.parent;!ts.isSourceFile(current); current = current.parent) {
1415
+ if (ts.isMethodDeclaration(current) || ts.isAccessor(current) || ts.isConstructorDeclaration(current))
1416
+ return ts.isClassDeclaration(current.parent) && ts.isSourceFile(current.parent.parent);
1417
+ if (ts.isFunctionExpression(current) || ts.isFunctionDeclaration(current) || ts.isClassLike(current))
1418
+ return false;
1419
+ }
1420
+ return false;
1421
+ }
368
1422
  #chainOf(expression) {
369
1423
  const { ts } = this.#source;
370
1424
  if (ts.isNonNullExpression(expression) || ts.isParenthesizedExpression(expression))
@@ -388,14 +1442,13 @@ st
388
1442
  const name = declared && ts.isIdentifier(declared) && !isDefaultExport ? declared.text : "default";
389
1443
  const relative = this.#source.relative(source.fileName);
390
1444
  const owner = this.#source.ownerOf(source.fileName);
1445
+ const extent = { start: top.getStart(source), end: top.getEnd() };
1446
+ const routeFile = this.#routeFiles.get(relative);
1447
+ const routeId = routeFile && (name === "default" || name === routeFile.alias) ? routeFile.id : undefined;
1448
+ if (routeId)
1449
+ return { id: routeId, kind: "component", owner, module: null, extent };
391
1450
  const id = `component:${relative}#${name}`;
392
- const context = {
393
- id,
394
- kind: "component",
395
- owner,
396
- module: null,
397
- extent: { start: top.getStart(source), end: top.getEnd() }
398
- };
1451
+ const context = { id, kind: "component", owner, module: null, extent };
399
1452
  if (this.#nodes.has(id))
400
1453
  return context;
401
1454
  const pagePrefix = owner ? `${owner}/page/` : null;
@@ -456,266 +1509,30 @@ var createAst = (ts) => {
456
1509
  }
457
1510
  }
458
1511
  return null;
459
- };
460
- const heritageCallNamed = (node, names) => {
461
- const heritage = node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
462
- for (const { expression } of heritage) {
463
- if (!ts.isCallExpression(expression))
464
- continue;
465
- const callee = expressionName(expression.expression);
466
- if (callee && names.has(callee))
467
- return { callee, call: expression };
468
- }
469
- return null;
470
- };
471
- const viaNames = new Set(["via"]);
472
- const heritageCall = (node) => heritageCallNamed(node, viaNames)?.call ?? null;
473
- return {
474
- sourceFileFor,
475
- nodeName,
476
- propertyName,
477
- expressionName,
478
- callExpressionName,
479
- firstObjectReturnedByArrow,
480
- heritageCallNamed,
481
- heritageCall
482
- };
483
- };
484
-
485
- // pkgs/@akanjs/language/moduleFacts.ts
486
- var moduleCallees = new Set(["via", "from", "into", "serve", "slice", "endpoint", "store", "enumOf"]);
487
- var argMethods = new Set(["param", "body", "search", "room", "msg"]);
488
- var ownerOf = (relativePath) => {
489
- const [root, name] = relativePath.split("/");
490
- return (root === "apps" || root === "libs") && name ? `${root}/${name}` : null;
491
- };
492
- var createFactReader = (ts, ast) => {
493
- const spanOf = (source, node) => {
494
- const start = node.getStart(source);
495
- const end = node.getEnd();
496
- const head = source.getLineAndCharacterOfPosition(start);
497
- const tail = source.getLineAndCharacterOfPosition(end);
498
- return {
499
- fileName: source.fileName,
500
- start,
501
- end,
502
- line: head.line,
503
- character: head.character,
504
- endLine: tail.line,
505
- endCharacter: tail.character
506
- };
507
- };
508
- const extentOf = (source, node) => ({
509
- start: node.getStart(source),
510
- end: node.getEnd()
511
- });
512
- const unwrap = (expression) => {
513
- let current = expression;
514
- while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
515
- current = current.expression;
516
- return current;
517
- };
518
- const moduleRefOf = (expression, holder) => {
519
- let current = expression && unwrap(expression);
520
- while (current && ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression))
521
- current = current.expression.expression;
522
- if (current && ts.isStringLiteral(current))
523
- return current.text;
524
- if (!current || !ts.isPropertyAccessExpression(current))
525
- return null;
526
- return ts.isIdentifier(current.expression) && current.expression.text === holder ? current.name.text : null;
527
- };
528
- const returnedObject = (builder) => builder ? ast.firstObjectReturnedByArrow(builder) : null;
529
- const builderOf = (call) => [...call.arguments].reverse().find((arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg));
530
- const guardsIn = (object) => {
531
- const guards = object?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "guards")?.initializer;
532
- if (!guards)
533
- return [];
534
- if (ts.isArrayLiteralExpression(guards))
535
- return guards.elements.map((element) => ast.expressionName(element)).filter((name) => name !== null);
536
- if (ts.isObjectLiteralExpression(guards))
537
- return guards.properties.filter(ts.isPropertyAssignment).map((property) => `${ast.nodeName(property.name)} ${ast.expressionName(property.initializer) ?? "?"}`);
538
- const single = ast.expressionName(guards);
539
- return single ? [single] : [];
540
- };
541
- const optionObjectOf = (call) => call.arguments.find((arg) => ts.isObjectLiteralExpression(arg) && arg.properties.some((property) => ast.propertyName(property) === "guards"));
542
- const chainOf = (initializer) => {
543
- let root = unwrap(initializer);
544
- const methods = [];
545
- while (ts.isCallExpression(root) && ts.isPropertyAccessExpression(root.expression) && ts.isCallExpression(unwrap(root.expression.expression))) {
546
- methods.push(root.expression.name.text);
547
- root = unwrap(root.expression.expression);
548
- }
549
- return { root: ts.isCallExpression(root) ? root : null, methods };
550
- };
551
- const keyName = (property) => ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) || ts.isShorthandPropertyAssignment(property) ? property.name : null;
552
- const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
553
- const name = ast.propertyName(property);
554
- const node = keyName(property);
555
- return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
556
- });
557
- const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
558
- if (!ts.isPropertyAssignment(property))
559
- return [];
560
- const name = ast.nodeName(property.name);
561
- if (!name)
562
- return [];
563
- const { root, methods } = chainOf(property.initializer);
564
- return [
565
- {
566
- name,
567
- span: spanOf(source, property.name),
568
- extent: extentOf(source, property),
569
- guards: root ? guardsIn(optionObjectOf(root)) : [],
570
- live: methods.includes("live"),
571
- argCount: methods.filter((method) => argMethods.has(method)).length
572
- }
573
- ];
574
- });
575
- const verbReaderOf = (parameter) => {
576
- const verbs = new Map;
577
- if (parameter && ts.isObjectBindingPattern(parameter.name))
578
- for (const element of parameter.name.elements) {
579
- const verb = element.propertyName ?? element.name;
580
- if (ts.isIdentifier(verb) && ts.isIdentifier(element.name))
581
- verbs.set(element.name.text, verb.text);
582
- }
583
- const holder = parameter && ts.isIdentifier(parameter.name) ? parameter.name.text : null;
584
- return (callee) => {
585
- if (callee && ts.isIdentifier(callee))
586
- return verbs.get(callee.text) ?? null;
587
- if (callee && ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression) && callee.expression.text === holder)
588
- return callee.name.text;
589
- return null;
590
- };
591
- };
592
- const endpointKeysOf = (source, builder) => {
593
- const verbOf = verbReaderOf(builder?.parameters[0]);
594
- return (returnedObject(builder)?.properties ?? []).flatMap((property) => {
595
- if (!ts.isPropertyAssignment(property))
596
- return [];
597
- const name = ast.nodeName(property.name);
598
- if (!name)
599
- return [];
600
- const { root, methods } = chainOf(property.initializer);
601
- return [
602
- {
603
- name,
604
- span: spanOf(source, property.name),
605
- extent: extentOf(source, property),
606
- verb: root ? verbOf(root.expression) : null,
607
- guards: root ? guardsIn(optionObjectOf(root)) : [],
608
- argCount: methods.filter((method) => argMethods.has(method)).length
609
- }
610
- ];
611
- });
612
- };
613
- const filterQueriesOf = (source, builder) => {
614
- const query = returnedObject(builder)?.properties.find((property) => ts.isPropertyAssignment(property) && ast.nodeName(property.name) === "query")?.initializer;
615
- return query && ts.isObjectLiteralExpression(query) ? objectKeys(source, query) : [];
616
- };
617
- const typeOf = (expression) => {
618
- const value = expression && unwrap(expression);
619
- if (value && ts.isArrayLiteralExpression(value))
620
- return { type: typeOf(value.elements[0]).type, array: true };
621
- return { type: value ? ast.expressionName(value) : null, array: false };
622
- };
623
- const fieldsOf = (source, object) => (object?.properties ?? []).flatMap((property) => {
624
- const name = ast.propertyName(property);
625
- const node = keyName(property);
626
- if (!name || !node)
627
- return [];
628
- const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
629
- return [
630
- {
631
- name,
632
- span: spanOf(source, node),
633
- extent: extentOf(source, property),
634
- ...typeOf(root?.arguments[0]),
635
- optional: methods.includes("optional")
636
- }
637
- ];
638
- });
639
- const constantOf = (source, name, call) => {
640
- const parents = call.arguments.map(unwrap).filter((arg) => ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)).map((arg) => ast.expressionName(arg)).filter((parent) => parent !== null);
641
- const fields = call.arguments.flatMap((arg) => fieldsOf(source, ast.firstObjectReturnedByArrow(arg)));
642
- return { ...name, parents, fields };
643
- };
644
- const enumValuesOf = (call) => {
645
- const list = call.arguments[1] && unwrap(call.arguments[1]);
646
- if (!list || !ts.isArrayLiteralExpression(list))
647
- return [];
648
- return list.elements.flatMap((element) => ts.isStringLiteralLike(element) ? [element.text] : []);
649
- };
650
- const methodsOf = (source, statement) => statement.members.flatMap((member) => {
651
- if (!ts.isMethodDeclaration(member) || !member.body)
652
- return [];
653
- const name = ast.nodeName(member.name);
654
- if (!name || ts.getModifiers(member)?.some(({ kind }) => kind === ts.SyntaxKind.StaticKeyword))
655
- return [];
656
- return [{ name, span: spanOf(source, member.name), extent: extentOf(source, member) }];
657
- });
658
- const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
659
- const read = (source, owner) => {
660
- const facts = {
661
- fileName: source.fileName,
662
- owner,
663
- constants: [],
664
- enums: [],
665
- filters: [],
666
- models: [],
667
- services: [],
668
- slices: [],
669
- endpoints: [],
670
- stores: []
671
- };
672
- for (const statement of source.statements) {
673
- if (!ts.isClassDeclaration(statement) || !statement.name)
674
- continue;
675
- const heritage = ast.heritageCallNamed(statement, moduleCallees);
676
- if (!heritage)
1512
+ };
1513
+ const heritageCallNamed = (node, names) => {
1514
+ const heritage = node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [];
1515
+ for (const { expression } of heritage) {
1516
+ if (!ts.isCallExpression(expression))
677
1517
  continue;
678
- const { callee, call } = heritage;
679
- const name = { name: statement.name.text, span: spanOf(source, statement.name) };
680
- const builder = builderOf(call);
681
- if (callee === "via")
682
- facts.constants.push(constantOf(source, name, call));
683
- else if (callee === "enumOf")
684
- facts.enums.push({ ...name, values: enumValuesOf(call) });
685
- else if (callee === "from")
686
- facts.filters.push({ ...name, queries: filterQueriesOf(source, builder) });
687
- else if (callee === "into")
688
- facts.models.push({ ...name, model: name.name.endsWith("Model") ? lowerFirst(name.name.slice(0, -5)) : null });
689
- else if (callee === "serve")
690
- facts.services.push({
691
- ...name,
692
- model: moduleRefOf(call.arguments[0], "db"),
693
- methods: methodsOf(source, statement)
694
- });
695
- else if (callee === "slice")
696
- facts.slices.push({
697
- ...name,
698
- model: moduleRefOf(call.arguments[0], "srv"),
699
- guards: guardsIn(call.arguments.find(ts.isObjectLiteralExpression)),
700
- keys: sliceKeysOf(source, builder)
701
- });
702
- else if (callee === "endpoint")
703
- facts.endpoints.push({
704
- ...name,
705
- model: moduleRefOf(call.arguments[0], "srv"),
706
- keys: endpointKeysOf(source, builder)
707
- });
708
- else if (callee === "store")
709
- facts.stores.push({
710
- ...name,
711
- model: moduleRefOf(call.arguments[0], "sig"),
712
- stateKeys: objectKeys(source, returnedObject(builder)),
713
- actions: methodsOf(source, statement)
714
- });
1518
+ const callee = expressionName(expression.expression);
1519
+ if (callee && names.has(callee))
1520
+ return { callee, call: expression };
715
1521
  }
716
- return facts;
1522
+ return null;
1523
+ };
1524
+ const viaNames = new Set(["via"]);
1525
+ const heritageCall = (node) => heritageCallNamed(node, viaNames)?.call ?? null;
1526
+ return {
1527
+ sourceFileFor,
1528
+ nodeName,
1529
+ propertyName,
1530
+ expressionName,
1531
+ callExpressionName,
1532
+ firstObjectReturnedByArrow,
1533
+ heritageCallNamed,
1534
+ heritageCall
717
1535
  };
718
- return { read, spanOf, moduleRefOf, chainOf, unwrap };
719
1536
  };
720
1537
 
721
1538
  // pkgs/akanjs/common/capitalize.ts
@@ -1169,6 +1986,7 @@ class NameTable {
1169
1986
  // pkgs/@akanjs/language/akanIndex.ts
1170
1987
  var sourcePattern = /\.(constant|document|service|signal|store)\.ts$/;
1171
1988
  var dictionaryPattern = /\.dictionary\.ts$/;
1989
+ var srvkitPattern = /\/srvkit\//;
1172
1990
  var wordPattern = /[A-Za-z_$][\w$]*/g;
1173
1991
  var identifierPattern = /^[A-Za-z_$][\w$]*$/;
1174
1992
  var regExpSpecials = /[\\^$.*+?()[\]{}|]/g;
@@ -1204,21 +2022,26 @@ class AkanIndex {
1204
2022
  }
1205
2023
  setFile(givenName, text) {
1206
2024
  const fileName = pathOf(givenName);
1207
- this.#fileWords.set(fileName, `
2025
+ const words = `
1208
2026
  ${[...new Set(text.match(wordPattern))].join(`
1209
2027
  `)}
1210
- `);
1211
- if (!AkanIndex.isSourcePath(fileName))
2028
+ `;
2029
+ this.#fileWords.set(fileName, words);
2030
+ const isSource = AkanIndex.isSourcePath(fileName);
2031
+ if (!isSource && !AkanIndex.#holdsFacts(fileName, words)) {
2032
+ this.#facts.delete(fileName);
1212
2033
  return;
2034
+ }
1213
2035
  const source = this.#parsed.get(fileName)?.text === text ? this.#parse(fileName, text) : this.#parseUncached(fileName, text);
1214
2036
  this.#facts.set(fileName, this.#reader.read(source, this.#ownerOf(fileName)));
1215
- this.#table = null;
2037
+ if (isSource)
2038
+ this.#table = null;
1216
2039
  }
1217
2040
  removeFile(givenName) {
1218
2041
  const fileName = pathOf(givenName);
1219
2042
  this.#fileWords.delete(fileName);
1220
2043
  this.#parsed.delete(fileName);
1221
- if (this.#facts.delete(fileName))
2044
+ if (this.#facts.delete(fileName) && AkanIndex.isSourcePath(fileName))
1222
2045
  this.#table = null;
1223
2046
  }
1224
2047
  definitionAt(givenName, text, offset) {
@@ -1266,7 +2089,8 @@ ${[...new Set(text.match(wordPattern))].join(`
1266
2089
  return site ? this.#resolve(fileName, site) : [];
1267
2090
  },
1268
2091
  relative: (fileName) => this.#relative(fileName),
1269
- ownerOf: (fileName) => this.#ownerOf(fileName)
2092
+ ownerOf: (fileName) => this.#ownerOf(fileName),
2093
+ reader: this.#reader
1270
2094
  }).build();
1271
2095
  }
1272
2096
  renameTargetAt(givenName, offset) {
@@ -1289,6 +2113,22 @@ ${[...new Set(text.match(wordPattern))].join(`
1289
2113
  }))
1290
2114
  ];
1291
2115
  }
2116
+ static #holdsFacts(fileName, words) {
2117
+ if (testFilePattern.test(fileName))
2118
+ return false;
2119
+ if (srvkitPattern.test(fileName))
2120
+ return true;
2121
+ const holdsGuards = words.includes(`
2122
+ Guard
2123
+ `) && words.includes(`
2124
+ canPass
2125
+ `);
2126
+ return holdsGuards || words.includes(`
2127
+ adapt
2128
+ `) || words.includes(`
2129
+ dangerouslyAdapt
2130
+ `);
2131
+ }
1292
2132
  static #sourceKeyOf({ family, sourceKey, label, detail, span }) {
1293
2133
  return { family, sourceKey, label, detail, span };
1294
2134
  }
@@ -1379,7 +2219,7 @@ ${dictionary.className}
1379
2219
  return this.#nameTable().entriesTargeting(fileName).filter((entry) => entry.span.start <= offset && offset <= entry.span.end);
1380
2220
  }
1381
2221
  #nameTable() {
1382
- this.#table ??= new NameTable(this.#facts.values());
2222
+ this.#table ??= new NameTable([...this.#facts.values()].filter(({ fileName }) => AkanIndex.isSourcePath(fileName)));
1383
2223
  return this.#table;
1384
2224
  }
1385
2225
  #relative(fileName) {
@@ -1513,9 +2353,871 @@ ${dictionary.className}
1513
2353
  })).sort((a, b) => Number(b.owner === owner) - Number(a.owner === owner));
1514
2354
  }
1515
2355
  }
2356
+ // pkgs/@akanjs/language/frameworkChannelReader.ts
2357
+ class FrameworkChannelReader {
2358
+ #ts;
2359
+ #unions = [];
2360
+ #channels = [];
2361
+ #missing = [];
2362
+ #deferred = [];
2363
+ #spoken = new Map;
2364
+ #sendParameters = new Set;
2365
+ #classes = new Map;
2366
+ #injectedSends = [];
2367
+ #constructed = new Map;
2368
+ constructor(ts, specs, sources) {
2369
+ this.#ts = ts;
2370
+ for (const source of sources.values())
2371
+ for (const statement of source.statements)
2372
+ if (ts.isClassDeclaration(statement) && statement.name)
2373
+ this.#classes.set(statement.name.text, [...this.#classes.get(statement.name.text) ?? [], statement]);
2374
+ for (const source of sources.values())
2375
+ this.#unions.push(...this.#unionsIn(source));
2376
+ const parts = new Set(this.#unions.flatMap((union) => [...union.typeNames].filter((name) => name !== union.name).map((name) => `${union.file}
2377
+ ${name}`)));
2378
+ this.#unions.splice(0, this.#unions.length, ...this.#unions.filter((union) => !parts.has(`${union.file}
2379
+ ${union.name}`)));
2380
+ for (const spec of specs) {
2381
+ const union = this.#unions.find(({ file, name }) => file === spec.file && name === spec.union);
2382
+ if (!union) {
2383
+ this.#missing.push(`${spec.union} in ${spec.file}`);
2384
+ continue;
2385
+ }
2386
+ union.channel = { spec, union, senders: new Map, handlers: new Map };
2387
+ this.#channels.push(union.channel);
2388
+ }
2389
+ for (const source of sources.values())
2390
+ this.#read(source);
2391
+ for (const { channels, ...site } of this.#deferred) {
2392
+ const spoken = this.#spoken.get(site.source.fileName);
2393
+ for (const channel of channels)
2394
+ if (spoken?.has(channel))
2395
+ this.#record(channel, site);
2396
+ }
2397
+ for (const { owner, ...site } of this.#injectedSends)
2398
+ for (const file of this.#constructed.get(owner) ?? [])
2399
+ for (const channel of [...this.#spoken.get(file) ?? []])
2400
+ if (channel.union.members.has(site.literal) && !this.#holds(channel, site))
2401
+ this.#record(channel, site);
2402
+ }
2403
+ get missing() {
2404
+ return [...this.#missing];
2405
+ }
2406
+ channels() {
2407
+ return this.#channels.map(({ spec, union, senders, handlers }) => ({
2408
+ name: spec.union,
2409
+ label: spec.label,
2410
+ at: union.at,
2411
+ messages: [...union.members].map(([type, member]) => ({
2412
+ type,
2413
+ at: member.at,
2414
+ senders: FrameworkChannelReader.#grouped(senders.get(type) ?? []),
2415
+ handlers: FrameworkChannelReader.#grouped(handlers.get(type) ?? [])
2416
+ }))
2417
+ }));
2418
+ }
2419
+ static #grouped(sites) {
2420
+ const byOwner = new Map;
2421
+ const ordered = [...sites].sort((a, b) => a.at.file === b.at.file ? a.at.line - b.at.line : a.at.file < b.at.file ? -1 : 1);
2422
+ for (const site of ordered) {
2423
+ const existing = byOwner.get(site.owner);
2424
+ if (existing)
2425
+ existing.count += 1;
2426
+ else
2427
+ byOwner.set(site.owner, { ...site, count: 1 });
2428
+ }
2429
+ return [...byOwner.values()].sort((a, b) => a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : 0);
2430
+ }
2431
+ #location(source, node) {
2432
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
2433
+ return { file: source.fileName, line: line + 1, column: character + 1 };
2434
+ }
2435
+ #unionsIn(source) {
2436
+ const ts = this.#ts;
2437
+ const declarations = new Map;
2438
+ for (const statement of source.statements)
2439
+ if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement))
2440
+ declarations.set(statement.name.text, statement);
2441
+ return [...declarations.values()].flatMap((declaration) => {
2442
+ if (!ts.isTypeAliasDeclaration(declaration) || !ts.isUnionTypeNode(declaration.type))
2443
+ return [];
2444
+ const members = new Map;
2445
+ const typeNames = new Set([declaration.name.text]);
2446
+ const isShaped = this.#collect(source, declaration.type, declarations, members, typeNames);
2447
+ if (!isShaped || members.size < 2)
2448
+ return [];
2449
+ return [
2450
+ {
2451
+ file: source.fileName,
2452
+ name: declaration.name.text,
2453
+ at: this.#location(source, declaration.name),
2454
+ members,
2455
+ typeNames,
2456
+ channel: null
2457
+ }
2458
+ ];
2459
+ });
2460
+ }
2461
+ #collect(source, type, declarations, members, typeNames) {
2462
+ const ts = this.#ts;
2463
+ if (ts.isParenthesizedTypeNode(type))
2464
+ return this.#collect(source, type.type, declarations, members, typeNames);
2465
+ if (ts.isUnionTypeNode(type))
2466
+ return type.types.every((member) => this.#collect(source, member, declarations, members, typeNames));
2467
+ if (ts.isTypeLiteralNode(type))
2468
+ return this.#addMember(source, type.members, members);
2469
+ if (!ts.isTypeReferenceNode(type) || !ts.isIdentifier(type.typeName))
2470
+ return false;
2471
+ const name = type.typeName.text;
2472
+ const declaration = declarations.get(name);
2473
+ if (!declaration)
2474
+ return false;
2475
+ if (typeNames.has(name))
2476
+ return true;
2477
+ typeNames.add(name);
2478
+ return ts.isTypeAliasDeclaration(declaration) ? this.#collect(source, declaration.type, declarations, members, typeNames) : this.#addMember(source, declaration.members, members);
2479
+ }
2480
+ #addMember(source, elements, members) {
2481
+ const ts = this.#ts;
2482
+ const typeProperty = elements.find((element) => ts.isPropertySignature(element) && this.#nameOf(element.name) === "type");
2483
+ const literal = typeProperty?.type && ts.isLiteralTypeNode(typeProperty.type) && ts.isStringLiteral(typeProperty.type.literal) ? typeProperty.type.literal.text : null;
2484
+ if (!typeProperty || literal === null)
2485
+ return false;
2486
+ const keys = new Set;
2487
+ const required = new Set;
2488
+ for (const element of elements) {
2489
+ const name = ts.isPropertySignature(element) ? this.#nameOf(element.name) : null;
2490
+ if (!name)
2491
+ continue;
2492
+ keys.add(name);
2493
+ if (ts.isPropertySignature(element) && !element.questionToken)
2494
+ required.add(name);
2495
+ }
2496
+ const existing = members.get(literal);
2497
+ if (!existing) {
2498
+ members.set(literal, { at: this.#location(source, typeProperty), keys, required });
2499
+ return true;
2500
+ }
2501
+ for (const key of keys)
2502
+ existing.keys.add(key);
2503
+ for (const key of existing.required)
2504
+ if (!required.has(key))
2505
+ existing.required.delete(key);
2506
+ return true;
2507
+ }
2508
+ #nameOf(name) {
2509
+ const ts = this.#ts;
2510
+ return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
2511
+ }
2512
+ #literalOf(expression) {
2513
+ const ts = this.#ts;
2514
+ return expression && (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) ? expression.text : null;
2515
+ }
2516
+ #record(channel, { role, literal, source, node }) {
2517
+ const sites = channel[role];
2518
+ const site = { owner: this.#ownerOf(source, node), count: 1, at: this.#location(source, node) };
2519
+ const relayed = role === "handlers" && !!site.owner && !!channel.spec.relays?.includes(site.owner);
2520
+ if (!relayed)
2521
+ sites.set(literal, [...sites.get(literal) ?? [], site]);
2522
+ const spoken = this.#spoken.get(source.fileName) ?? new Set;
2523
+ spoken.add(channel);
2524
+ this.#spoken.set(source.fileName, spoken);
2525
+ }
2526
+ #settle(candidates, site) {
2527
+ const local = candidates.filter(({ file }) => file === site.source.fileName);
2528
+ const unions = candidates.length > 1 && local.length ? local : candidates;
2529
+ const channels = unions.flatMap(({ channel }) => channel ? [channel] : []);
2530
+ if (unions.length === 1 && channels.length === 1)
2531
+ this.#record(channels[0], site);
2532
+ else if (channels.length)
2533
+ this.#deferred.push({ ...site, channels });
2534
+ }
2535
+ #ownerOf(source, node) {
2536
+ const ts = this.#ts;
2537
+ let top = node;
2538
+ for (let current = node;current; current = current.parent) {
2539
+ if ((ts.isClassDeclaration(current) || ts.isClassExpression(current)) && current.name)
2540
+ return current.name.text;
2541
+ if (current.parent && !ts.isSourceFile(current.parent))
2542
+ top = current.parent;
2543
+ }
2544
+ if (ts.isFunctionDeclaration(top) && top.name)
2545
+ return top.name.text;
2546
+ if (ts.isVariableStatement(top)) {
2547
+ const declaration = top.declarationList.declarations.find(({ initializer, pos, end }) => !!initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) && pos <= node.pos && node.end <= end);
2548
+ if (declaration && ts.isIdentifier(declaration.name))
2549
+ return declaration.name.text;
2550
+ }
2551
+ const classes = source.statements.filter(ts.isClassDeclaration);
2552
+ const only = classes.length === 1 ? classes[0].name?.text : undefined;
2553
+ return only ?? source.fileName.slice(source.fileName.lastIndexOf("/") + 1).replace(/\.tsx?$/, "");
2554
+ }
2555
+ #namesIn(type) {
2556
+ const ts = this.#ts;
2557
+ const names = new Set;
2558
+ const visit = (node) => {
2559
+ if (ts.isTypeReferenceNode(node))
2560
+ names.add(ts.isIdentifier(node.typeName) ? node.typeName.text : node.typeName.right.text);
2561
+ if (!ts.isTypeLiteralNode(node))
2562
+ ts.forEachChild(node, visit);
2563
+ };
2564
+ if (type)
2565
+ visit(type);
2566
+ return names;
2567
+ }
2568
+ #literalTypesIn(type) {
2569
+ const ts = this.#ts;
2570
+ const literals = new Set;
2571
+ const visit = (node) => {
2572
+ if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal))
2573
+ literals.add(node.literal.text);
2574
+ ts.forEachChild(node, visit);
2575
+ };
2576
+ if (type)
2577
+ visit(type);
2578
+ return literals;
2579
+ }
2580
+ #hinted(names) {
2581
+ return this.#unions.filter((union) => [...names].some((name) => union.typeNames.has(name)));
2582
+ }
2583
+ #read(source) {
2584
+ const ts = this.#ts;
2585
+ const comparisons = new Map;
2586
+ const switches = [];
2587
+ const templates = [];
2588
+ const calls = [];
2589
+ const visit = (node) => {
2590
+ if (ts.isObjectLiteralExpression(node)) {
2591
+ const template = this.#readSend(source, node);
2592
+ if (template)
2593
+ templates.push(template);
2594
+ } else if (ts.isCallExpression(node))
2595
+ calls.push(node);
2596
+ else if (ts.isNewExpression(node)) {
2597
+ this.#readRouteSet(source, node);
2598
+ if (ts.isIdentifier(node.expression)) {
2599
+ const files = this.#constructed.get(node.expression.text) ?? new Set;
2600
+ files.add(source.fileName);
2601
+ this.#constructed.set(node.expression.text, files);
2602
+ }
2603
+ } else if (ts.isSwitchStatement(node)) {
2604
+ const chain = this.#switchChain(node);
2605
+ if (chain)
2606
+ switches.push(chain);
2607
+ } else if (ts.isBinaryExpression(node)) {
2608
+ const compared = this.#comparedType(node);
2609
+ if (compared) {
2610
+ const key = `${this.#scopeOf(node)?.pos ?? -1}:${compared.receiver.getText(source)}`;
2611
+ const chain = comparisons.get(key) ?? {
2612
+ literals: [],
2613
+ hint: this.#declaredTypeNames(compared.receiver),
2614
+ parameter: this.#parameterOf(compared.receiver)
2615
+ };
2616
+ chain.literals.push([compared.literal, compared.at]);
2617
+ comparisons.set(key, chain);
2618
+ }
2619
+ }
2620
+ ts.forEachChild(node, visit);
2621
+ };
2622
+ visit(source);
2623
+ for (const { object, fn, index } of templates)
2624
+ for (const call of calls) {
2625
+ const argument = call.arguments[index];
2626
+ const literal = this.#literalOf(argument);
2627
+ if (literal !== null && this.#calleeOf(call) === fn)
2628
+ this.#sendSite(source, object, literal, argument, true);
2629
+ }
2630
+ for (const chain of [...switches, ...comparisons.values()])
2631
+ if (!chain.parameter || !this.#sendParameters.has(chain.parameter))
2632
+ this.#readChain(source, chain);
2633
+ }
2634
+ #readSend(source, object) {
2635
+ const ts = this.#ts;
2636
+ const typeProperty = object.properties.find((property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && this.#nameOf(property.name) === "type");
2637
+ if (!typeProperty)
2638
+ return null;
2639
+ const value = ts.isPropertyAssignment(typeProperty) ? typeProperty.initializer : typeProperty.name;
2640
+ const literal = this.#literalOf(value);
2641
+ if (literal !== null)
2642
+ this.#sendSite(source, object, literal, typeProperty, false);
2643
+ if (literal !== null || !ts.isIdentifier(value))
2644
+ return null;
2645
+ const fn = this.#scopeOf(object);
2646
+ const index = fn?.parameters.findIndex(({ name }) => ts.isIdentifier(name) && name.text === value.text) ?? -1;
2647
+ return fn && index >= 0 ? { object, fn, index } : null;
2648
+ }
2649
+ #sendSite(source, object, literal, node, forwarded) {
2650
+ const context = this.#sendContextOf(object, forwarded ? null : literal);
2651
+ if (!context)
2652
+ return;
2653
+ const site = { role: "senders", literal, source, node };
2654
+ const injector = this.#injectorOf(object);
2655
+ if (injector)
2656
+ this.#injectedSends.push({ ...site, owner: injector });
2657
+ const hinted = this.#hinted(context.names).filter((union) => union.members.has(literal));
2658
+ const candidates = hinted.length ? hinted : this.#fitting(object, literal);
2659
+ if (context.parameter && candidates.some(({ channel }) => channel))
2660
+ this.#sendParameters.add(context.parameter);
2661
+ this.#settle(candidates, site);
2662
+ }
2663
+ #injectorOf(object) {
2664
+ const ts = this.#ts;
2665
+ const call = object.parent;
2666
+ if (!ts.isCallExpression(call) || !call.arguments.includes(object))
2667
+ return null;
2668
+ const callee = call.expression;
2669
+ if (!ts.isPropertyAccessExpression(callee) || callee.expression.kind !== ts.SyntaxKind.ThisKeyword)
2670
+ return null;
2671
+ const owner = this.#classOf(call);
2672
+ const constructor = owner?.members.find(ts.isConstructorDeclaration);
2673
+ const parameters = new Set(constructor?.parameters.flatMap(({ name }) => ts.isIdentifier(name) ? [name.text] : []) ?? []);
2674
+ const isInjected = !!constructor?.body?.statements.some((statement) => {
2675
+ if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression))
2676
+ return false;
2677
+ const { left, operatorToken, right } = statement.expression;
2678
+ return operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(left) && left.expression.kind === ts.SyntaxKind.ThisKeyword && this.#memberName(left.name) === callee.name.text && ts.isIdentifier(right) && parameters.has(right.text);
2679
+ });
2680
+ return isInjected && owner?.name ? owner.name.text : null;
2681
+ }
2682
+ #holds(channel, site) {
2683
+ const { file, line, column } = this.#location(site.source, site.node);
2684
+ return (channel[site.role].get(site.literal) ?? []).some(({ at }) => at.file === file && at.line === line && at.column === column);
2685
+ }
2686
+ #fitting(object, literal) {
2687
+ const ts = this.#ts;
2688
+ const isLoose = object.properties.some((property) => ts.isSpreadAssignment(property));
2689
+ const keys = new Set(object.properties.flatMap((property) => {
2690
+ const name = ts.isSpreadAssignment(property) ? null : this.#nameOf(property.name);
2691
+ return name === null ? [] : [name];
2692
+ }));
2693
+ return this.#unions.filter((union) => {
2694
+ const member = union.members.get(literal);
2695
+ return !!member && [...keys].every((key) => member.keys.has(key)) && (isLoose || [...member.required].every((key) => keys.has(key)));
2696
+ });
2697
+ }
2698
+ #sendContextOf(object, literal) {
2699
+ const ts = this.#ts;
2700
+ const hint = new Set;
2701
+ let node = object;
2702
+ while (node.parent) {
2703
+ const parent = node.parent;
2704
+ if (ts.isParenthesizedExpression(parent) || ts.isConditionalExpression(parent))
2705
+ node = parent;
2706
+ else if (ts.isAsExpression(parent) || ts.isSatisfiesExpression(parent)) {
2707
+ for (const name of this.#namesIn(parent.type))
2708
+ hint.add(name);
2709
+ node = parent;
2710
+ } else if (ts.isCallExpression(parent) && this.#calleeName(parent) === "stringify")
2711
+ node = parent;
2712
+ else if (ts.isArrowFunction(parent) && parent.body === node && !parent.type)
2713
+ node = parent;
2714
+ else
2715
+ break;
2716
+ }
2717
+ const parent = node.parent;
2718
+ if (!parent)
2719
+ return null;
2720
+ const scope = this.#scopeOf(object);
2721
+ if (literal !== null && scope?.parameters.some(({ type }) => this.#literalTypesIn(type).has(literal)))
2722
+ return null;
2723
+ const context = (names, parameter = null) => ({
2724
+ names: new Set([...hint, ...names]),
2725
+ parameter
2726
+ });
2727
+ if (ts.isCallExpression(parent)) {
2728
+ const index = parent.arguments.indexOf(node);
2729
+ if (index < 0)
2730
+ return null;
2731
+ const parameter = this.#calleeOf(parent)?.parameters[index] ?? null;
2732
+ return context(this.#namesIn(parameter?.type), parameter);
2733
+ }
2734
+ if (ts.isVariableDeclaration(parent) && parent.type)
2735
+ return context(this.#namesIn(parent.type));
2736
+ if (ts.isPropertyAssignment(parent) && ts.isObjectLiteralExpression(parent.parent)) {
2737
+ const outer = this.#sendContextOf(parent.parent, null);
2738
+ const property = this.#nameOf(parent.name);
2739
+ const names = outer && property ? this.#propertyTypeNames(object.getSourceFile(), outer.names, property) : null;
2740
+ return names?.size ? context(names) : null;
2741
+ }
2742
+ const fn = ts.isReturnStatement(parent) ? this.#scopeOf(parent) : ts.isArrowFunction(parent) ? parent : null;
2743
+ if (fn?.type)
2744
+ return context(this.#namesIn(fn.type));
2745
+ return hint.size ? context(new Set) : null;
2746
+ }
2747
+ #propertyTypeNames(source, typeNames, property) {
2748
+ const ts = this.#ts;
2749
+ for (const statement of source.statements) {
2750
+ if (!(ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)))
2751
+ continue;
2752
+ if (!typeNames.has(statement.name.text))
2753
+ continue;
2754
+ const members = ts.isInterfaceDeclaration(statement) ? statement.members : ts.isTypeLiteralNode(statement.type) ? statement.type.members : [];
2755
+ const signature = members.find((member) => ts.isPropertySignature(member) && this.#nameOf(member.name) === property);
2756
+ if (signature?.type)
2757
+ return this.#namesIn(signature.type);
2758
+ }
2759
+ return null;
2760
+ }
2761
+ #calleeOf(call) {
2762
+ const ts = this.#ts;
2763
+ const callee = call.expression;
2764
+ const isFunction = (node) => !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
2765
+ if (ts.isPropertyAccessExpression(callee) && callee.expression.kind === ts.SyntaxKind.ThisKeyword) {
2766
+ const member = this.#memberOf(this.#classOf(call), callee.name.text);
2767
+ if (!member)
2768
+ return null;
2769
+ if (ts.isMethodDeclaration(member))
2770
+ return member;
2771
+ return ts.isPropertyDeclaration(member) && isFunction(member.initializer) ? member.initializer : null;
2772
+ }
2773
+ if (ts.isPropertyAccessExpression(callee)) {
2774
+ for (const name of this.#receiverTypeNames(callee.expression))
2775
+ for (const declaration of this.#classes.get(name) ?? []) {
2776
+ const member = this.#memberOf(declaration, callee.name.text);
2777
+ if (member && ts.isMethodDeclaration(member))
2778
+ return member;
2779
+ }
2780
+ return null;
2781
+ }
2782
+ if (!ts.isIdentifier(callee))
2783
+ return null;
2784
+ for (const statement of call.getSourceFile().statements) {
2785
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === callee.text)
2786
+ return statement;
2787
+ if (!ts.isVariableStatement(statement))
2788
+ continue;
2789
+ for (const declaration of statement.declarationList.declarations)
2790
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === callee.text)
2791
+ return isFunction(declaration.initializer) ? declaration.initializer : null;
2792
+ }
2793
+ return null;
2794
+ }
2795
+ #classOf(node) {
2796
+ let owner = node.parent;
2797
+ while (owner && !this.#ts.isClassLike(owner))
2798
+ owner = owner.parent;
2799
+ return owner ?? null;
2800
+ }
2801
+ #memberOf(owner, name) {
2802
+ return owner?.members.find((member) => member.name && this.#memberName(member.name) === name) ?? null;
2803
+ }
2804
+ #receiverTypeNames(receiver) {
2805
+ const ts = this.#ts;
2806
+ if (ts.isPropertyAccessExpression(receiver) && receiver.expression.kind === ts.SyntaxKind.ThisKeyword) {
2807
+ const field = this.#memberOf(this.#classOf(receiver), receiver.name.text);
2808
+ return field && ts.isPropertyDeclaration(field) ? this.#namesIn(field.type) : new Set;
2809
+ }
2810
+ return this.#declaredTypeNames(receiver) ?? new Set;
2811
+ }
2812
+ #memberName(name) {
2813
+ const ts = this.#ts;
2814
+ return ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name) ? name.text : null;
2815
+ }
2816
+ #calleeName(call) {
2817
+ const ts = this.#ts;
2818
+ const callee = call.expression;
2819
+ if (ts.isIdentifier(callee))
2820
+ return callee.text;
2821
+ if (ts.isPropertyAccessExpression(callee))
2822
+ return callee.name.text;
2823
+ return null;
2824
+ }
2825
+ #scopeOf(node) {
2826
+ const ts = this.#ts;
2827
+ for (let current = node.parent;current; current = current.parent)
2828
+ if (ts.isFunctionLike(current))
2829
+ return current;
2830
+ return null;
2831
+ }
2832
+ #typeReceiverOf(expression) {
2833
+ const ts = this.#ts;
2834
+ if (ts.isPropertyAccessExpression(expression) && expression.name.text === "type")
2835
+ return expression.expression;
2836
+ if (ts.isElementAccessExpression(expression) && this.#literalOf(expression.argumentExpression) === "type")
2837
+ return expression.expression;
2838
+ return null;
2839
+ }
2840
+ #comparedType(node) {
2841
+ const ts = this.#ts;
2842
+ const { kind } = node.operatorToken;
2843
+ if (kind !== ts.SyntaxKind.EqualsEqualsEqualsToken && kind !== ts.SyntaxKind.ExclamationEqualsEqualsToken && kind !== ts.SyntaxKind.EqualsEqualsToken && kind !== ts.SyntaxKind.ExclamationEqualsToken)
2844
+ return null;
2845
+ for (const [side, other] of [
2846
+ [node.left, node.right],
2847
+ [node.right, node.left]
2848
+ ]) {
2849
+ const receiver = this.#typeReceiverOf(side);
2850
+ const literal = this.#literalOf(other);
2851
+ if (receiver && literal !== null)
2852
+ return { receiver, literal, at: other };
2853
+ }
2854
+ return null;
2855
+ }
2856
+ #declaredTypeNames(receiver) {
2857
+ const ts = this.#ts;
2858
+ if (!ts.isIdentifier(receiver))
2859
+ return null;
2860
+ const name = receiver.text;
2861
+ for (let current = receiver.parent;current; current = current.parent) {
2862
+ if (ts.isFunctionLike(current)) {
2863
+ for (const parameter of current.parameters)
2864
+ if (ts.isIdentifier(parameter.name) && parameter.name.text === name)
2865
+ return parameter.type ? this.#namesIn(parameter.type) : null;
2866
+ }
2867
+ if (ts.isBlock(current) || ts.isSourceFile(current) || ts.isCaseClause(current))
2868
+ for (const statement of current.statements) {
2869
+ if (!ts.isVariableStatement(statement))
2870
+ continue;
2871
+ for (const declaration of statement.declarationList.declarations) {
2872
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name)
2873
+ continue;
2874
+ const { initializer } = declaration;
2875
+ const asType = initializer && (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer)) ? initializer.type : undefined;
2876
+ const type = declaration.type ?? asType;
2877
+ return type ? this.#namesIn(type) : null;
2878
+ }
2879
+ }
2880
+ }
2881
+ return null;
2882
+ }
2883
+ #parameterOf(receiver) {
2884
+ const ts = this.#ts;
2885
+ if (!ts.isIdentifier(receiver))
2886
+ return null;
2887
+ for (let current = receiver.parent;current; current = current.parent)
2888
+ if (ts.isFunctionLike(current)) {
2889
+ const parameter = current.parameters.find(({ name }) => ts.isIdentifier(name) && name.text === receiver.text);
2890
+ if (parameter)
2891
+ return parameter;
2892
+ }
2893
+ return null;
2894
+ }
2895
+ #readChain(source, { literals, hint }) {
2896
+ const hinted = hint?.size ? this.#hinted(hint) : [];
2897
+ if (hint?.size && !hinted.length)
2898
+ return;
2899
+ const scored = this.#unions.map((union) => ({ union, hits: literals.filter(([literal]) => union.members.has(literal)).length })).filter(({ hits }) => hits > 0);
2900
+ const best = Math.max(0, ...scored.map(({ hits }) => hits));
2901
+ const unions = hinted.length ? hinted : best * 2 >= literals.length ? scored.filter(({ hits }) => hits === best).map(({ union }) => union) : [];
2902
+ for (const [literal, node] of literals) {
2903
+ const holding = unions.filter((union) => union.members.has(literal));
2904
+ this.#settle(holding, { role: "handlers", literal, source, node });
2905
+ }
2906
+ }
2907
+ #switchChain(statement) {
2908
+ const ts = this.#ts;
2909
+ const receiver = this.#typeReceiverOf(statement.expression);
2910
+ if (!receiver)
2911
+ return null;
2912
+ const literals = statement.caseBlock.clauses.flatMap((clause) => {
2913
+ if (!ts.isCaseClause(clause))
2914
+ return [];
2915
+ const literal = this.#literalOf(clause.expression);
2916
+ return literal === null ? [] : [[literal, clause.expression]];
2917
+ });
2918
+ return { literals, hint: this.#declaredTypeNames(receiver), parameter: this.#parameterOf(receiver) };
2919
+ }
2920
+ #readRouteSet(source, expression) {
2921
+ const ts = this.#ts;
2922
+ if (!ts.isIdentifier(expression.expression) || expression.expression.text !== "Set")
2923
+ return;
2924
+ const unions = this.#hinted(this.#namesIn(expression.typeArguments?.[0]));
2925
+ const list = expression.arguments?.[0];
2926
+ if (!unions.length || !list || !ts.isArrayLiteralExpression(list))
2927
+ return;
2928
+ for (const element of list.elements) {
2929
+ const literal = this.#literalOf(element);
2930
+ if (literal === null)
2931
+ continue;
2932
+ const holding = unions.filter((union) => union.members.has(literal));
2933
+ this.#settle(holding, { role: "handlers", literal, source, node: element });
2934
+ }
2935
+ }
2936
+ }
2937
+ // pkgs/@akanjs/language/frameworkMapBuilder.ts
2938
+ var extensions = ["", ".ts", ".tsx", "/index.ts", "/index.tsx"];
2939
+ var byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
2940
+
2941
+ class FrameworkMapBuilder {
2942
+ #source;
2943
+ #fileSet;
2944
+ constructor(source) {
2945
+ this.#source = source;
2946
+ this.#fileSet = new Set(source.files.map(([file]) => file));
2947
+ }
2948
+ static isSourcePath(file) {
2949
+ return /\.tsx?$/.test(file) && !/\.d\.ts$|\.(?:test|spec)\.tsx?$|fixture/i.test(file);
2950
+ }
2951
+ build() {
2952
+ const { ts, packages, files, channels } = this.#source;
2953
+ const sources = new Map;
2954
+ for (const [file, text] of [...files].sort(([a], [b]) => byText(a, b)))
2955
+ sources.set(file, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS));
2956
+ const facets = new Map;
2957
+ const imports = new Map;
2958
+ for (const [file, source] of sources) {
2959
+ const facet = this.#facetOf(file);
2960
+ if (!facet)
2961
+ continue;
2962
+ const pkg = this.#packageOf(file);
2963
+ const current = facets.get(facet) ?? {
2964
+ id: facet,
2965
+ name: pkg?.split ? facet.slice(pkg.name.length + 1) : facet,
2966
+ packageName: pkg?.name ?? facet,
2967
+ layer: 0,
2968
+ files: 0,
2969
+ classes: []
2970
+ };
2971
+ current.files += 1;
2972
+ current.classes.push(...this.#classesOf(source));
2973
+ facets.set(facet, current);
2974
+ for (const site of this.#importsOf(file, source)) {
2975
+ if (site.facet === facet)
2976
+ continue;
2977
+ const key = `${facet}
2978
+ ${site.facet}`;
2979
+ const entry = imports.get(key) ?? { from: facet, to: site.facet, value: 0, type: 0, dynamic: 0, at: site.at };
2980
+ if (entry.value === 0 && site.kind === "value" && imports.has(key))
2981
+ entry.at = site.at;
2982
+ entry[site.kind] += 1;
2983
+ imports.set(key, entry);
2984
+ }
2985
+ }
2986
+ const { layerOf, cycles } = FrameworkMapBuilder.#layersOf([...imports.values()]);
2987
+ for (const facet of facets.values()) {
2988
+ facet.classes.sort((a, b) => b.members - a.members || byText(a.name, b.name));
2989
+ facet.layer = layerOf(facet.id);
2990
+ }
2991
+ const reader = new FrameworkChannelReader(ts, channels, sources);
2992
+ return {
2993
+ version: 1,
2994
+ packages: packages.map(({ name, root }) => ({ name, root })),
2995
+ facets: [...facets.values()].sort((a, b) => byText(a.id, b.id)),
2996
+ imports: [...imports.values()].sort((a, b) => byText(a.from, b.from) || byText(a.to, b.to)),
2997
+ channels: reader.channels(),
2998
+ missing: reader.missing,
2999
+ cycles
3000
+ };
3001
+ }
3002
+ static #layersOf(imports) {
3003
+ const deps = new Map;
3004
+ for (const { from, to, value, dynamic } of imports)
3005
+ if (value + dynamic > 0)
3006
+ deps.set(from, [...deps.get(from) ?? [], to]);
3007
+ const components = FrameworkMapBuilder.#stronglyConnected(deps);
3008
+ const componentOf = new Map(components.flatMap((component) => component.map((id) => [id, component])));
3009
+ const layers = new Map;
3010
+ const layerOf = (id) => {
3011
+ const component = componentOf.get(id);
3012
+ if (!component)
3013
+ return 0;
3014
+ const known = layers.get(component);
3015
+ if (known !== undefined)
3016
+ return known;
3017
+ const below = component.flatMap((member) => (deps.get(member) ?? []).filter((to) => !component.includes(to)));
3018
+ const layer = Math.max(-1, ...below.map(layerOf)) + 1;
3019
+ layers.set(component, layer);
3020
+ return layer;
3021
+ };
3022
+ const cycles = components.filter((component) => component.length > 1).map((component) => [...component].sort(byText)).sort((a, b) => byText(a[0], b[0]));
3023
+ return { layerOf, cycles };
3024
+ }
3025
+ static #stronglyConnected(deps) {
3026
+ const ids = [...new Set([...deps.keys(), ...[...deps.values()].flat()])].sort(byText);
3027
+ const index = new Map;
3028
+ const low = new Map;
3029
+ const stack = [];
3030
+ const onStack = new Set;
3031
+ const components = [];
3032
+ const visit = (id) => {
3033
+ index.set(id, index.size);
3034
+ low.set(id, index.get(id) ?? 0);
3035
+ stack.push(id);
3036
+ onStack.add(id);
3037
+ for (const to of deps.get(id) ?? []) {
3038
+ if (!index.has(to)) {
3039
+ visit(to);
3040
+ low.set(id, Math.min(low.get(id) ?? 0, low.get(to) ?? 0));
3041
+ } else if (onStack.has(to))
3042
+ low.set(id, Math.min(low.get(id) ?? 0, index.get(to) ?? 0));
3043
+ }
3044
+ if (low.get(id) !== index.get(id))
3045
+ return;
3046
+ const component = [];
3047
+ for (let member = stack.pop();member !== undefined; member = stack.pop()) {
3048
+ onStack.delete(member);
3049
+ component.push(member);
3050
+ if (member === id)
3051
+ break;
3052
+ }
3053
+ components.push(component);
3054
+ };
3055
+ for (const id of ids)
3056
+ if (!index.has(id))
3057
+ visit(id);
3058
+ return components;
3059
+ }
3060
+ #packageOf(file) {
3061
+ return this.#source.packages.filter(({ root }) => file.startsWith(`${root}/`)).sort((a, b) => b.root.length - a.root.length)[0];
3062
+ }
3063
+ #facetOf(file) {
3064
+ const pkg = this.#packageOf(file);
3065
+ if (!pkg)
3066
+ return null;
3067
+ if (!pkg.split)
3068
+ return pkg.name;
3069
+ const rest = file.slice(pkg.root.length + 1);
3070
+ const slash = rest.indexOf("/");
3071
+ return slash < 0 ? null : `${pkg.name}/${rest.slice(0, slash)}`;
3072
+ }
3073
+ #location(source, node) {
3074
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
3075
+ return { file: source.fileName, line: line + 1, column: character + 1 };
3076
+ }
3077
+ #classesOf(source) {
3078
+ const { ts } = this.#source;
3079
+ return source.statements.flatMap((statement) => ts.isClassDeclaration(statement) && statement.name ? [{ name: statement.name.text, members: statement.members.length, at: this.#location(source, statement.name) }] : []);
3080
+ }
3081
+ #importsOf(file, source) {
3082
+ const { ts } = this.#source;
3083
+ const sites = [];
3084
+ const push = (specifier, kind, node) => {
3085
+ const target = this.#resolve(file, specifier);
3086
+ const facet = target ? this.#facetOf(target) : null;
3087
+ if (facet)
3088
+ sites.push({ facet, kind, at: this.#location(source, node) });
3089
+ };
3090
+ const visit = (node) => {
3091
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
3092
+ const clause = node.importClause;
3093
+ const named = clause?.namedBindings;
3094
+ const isTypeOnly = !!clause?.isTypeOnly || !!clause && !clause.name && !!named && ts.isNamedImports(named) && named.elements.length > 0 && named.elements.every((element) => element.isTypeOnly);
3095
+ push(node.moduleSpecifier.text, isTypeOnly ? "type" : "value", node);
3096
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
3097
+ const clause = node.exportClause;
3098
+ const isTypeOnly = node.isTypeOnly || !!clause && ts.isNamedExports(clause) && clause.elements.length > 0 && clause.elements.every((element) => element.isTypeOnly);
3099
+ push(node.moduleSpecifier.text, isTypeOnly ? "type" : "value", node);
3100
+ } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0]))
3101
+ push(node.arguments[0].text, "dynamic", node);
3102
+ ts.forEachChild(node, visit);
3103
+ };
3104
+ visit(source);
3105
+ return sites;
3106
+ }
3107
+ #resolve(file, specifier) {
3108
+ const pkg = specifier.startsWith(".") ? null : this.#source.packages.filter(({ name }) => specifier === name || specifier.startsWith(`${name}/`)).sort((a, b) => b.name.length - a.name.length)[0];
3109
+ if (!specifier.startsWith(".") && !pkg)
3110
+ return null;
3111
+ const base = pkg ? `${pkg.root}${specifier.slice(pkg.name.length)}` : FrameworkMapBuilder.#normalize(`${file.slice(0, file.lastIndexOf("/"))}/${specifier}`);
3112
+ return extensions.map((extension) => `${base}${extension}`).find((candidate) => this.#fileSet.has(candidate)) ?? null;
3113
+ }
3114
+ static #normalize(path) {
3115
+ const parts = [];
3116
+ for (const part of path.split("/"))
3117
+ if (part === "..")
3118
+ parts.pop();
3119
+ else if (part !== "." && part !== "")
3120
+ parts.push(part);
3121
+ return parts.join("/");
3122
+ }
3123
+ }
3124
+ // pkgs/@akanjs/language/frameworkMapDoc.ts
3125
+ var startMarker = "<!-- akan:framework-map:start -->";
3126
+ var endMarker = "<!-- akan:framework-map:end -->";
3127
+ var citedPattern = /`([A-Z][a-z][A-Za-z0-9]*)(?:[.#][\w$#]+)?(?:\(\))?`/g;
3128
+
3129
+ class FrameworkMapDoc {
3130
+ static regenerate = "bun run akan sync-architecture";
3131
+ static render(map, docDir) {
3132
+ const nameOf = new Map(map.facets.map((facet) => [facet.id, facet.name]));
3133
+ const facets = [...map.facets].sort((a, b) => a.layer - b.layer || (a.id < b.id ? -1 : 1));
3134
+ const list = (names) => names.length ? names.join(", ") : "—";
3135
+ const importsOf = (facet, kind) => map.imports.filter(({ from, value, type, dynamic }) => from === facet.id && (kind === "value" ? value + dynamic > 0 : type > 0 && value + dynamic === 0)).map(({ to, value }) => `\`${nameOf.get(to) ?? to}\`${kind === "value" && value === 0 ? " (lazy)" : ""}`);
3136
+ const lines = [
3137
+ startMarker,
3138
+ `<!-- Generated from the framework source by \`${FrameworkMapDoc.regenerate}\`. Edit outside the markers. -->`,
3139
+ "<!-- Outside the markers, every backticked PascalCase name must be a framework class or channel. -->",
3140
+ "",
3141
+ "## Facet layers",
3142
+ "",
3143
+ "A facet loads values only from facets in lower layers. A type-only import loads nothing, so it may point anywhere.",
3144
+ "",
3145
+ "| Layer | Facet | Loads values from | Imports only types from |",
3146
+ "| --- | --- | --- | --- |",
3147
+ ...facets.map((facet) => `| ${facet.layer} | \`${facet.name}\` | ${list(importsOf(facet, "value"))} | ${list(importsOf(facet, "type"))} |`),
3148
+ ...map.cycles.length ? [
3149
+ "",
3150
+ `Facets that load values from each other: ${map.cycles.map((cycle) => cycle.map((id) => `\`${nameOf.get(id) ?? id}\``).join(" ⇄ ")).join("; ")}.`
3151
+ ] : [],
3152
+ "",
3153
+ "## Process messages",
3154
+ "",
3155
+ "Each channel is a union of messages told apart by `type`. A sender writes the message's `type` literal; a handler",
3156
+ "branches on it or relays it. Both are named by the class or top-level function they sit in. A dash means no site",
3157
+ "was recognised.",
3158
+ ...map.channels.flatMap((channel) => {
3159
+ const declared = FrameworkMapDoc.#relative(docDir, channel.at.file);
3160
+ return [
3161
+ "",
3162
+ `### \`${channel.name}\`: ${channel.label}`,
3163
+ "",
3164
+ `Declared in [${declared}](${declared}).`,
3165
+ "",
3166
+ "| Message | Sent by | Handled by |",
3167
+ "| --- | --- | --- |",
3168
+ ...channel.messages.map(({ type, senders, handlers }) => `| \`${type}\` | ${FrameworkMapDoc.#owners(senders)} | ${FrameworkMapDoc.#owners(handlers)} |`)
3169
+ ];
3170
+ }),
3171
+ ...map.missing.length ? ["", `Channels not found: ${map.missing.join("; ")}.`] : [],
3172
+ endMarker
3173
+ ];
3174
+ return lines.join(`
3175
+ `);
3176
+ }
3177
+ static generatedOf(markdown) {
3178
+ const start = markdown.indexOf(startMarker);
3179
+ const end = markdown.indexOf(endMarker, start);
3180
+ return start < 0 || end < 0 ? null : markdown.slice(start, end + endMarker.length);
3181
+ }
3182
+ static splice(markdown, generated) {
3183
+ const current = FrameworkMapDoc.generatedOf(markdown);
3184
+ if (current !== null)
3185
+ return markdown.replace(current, () => generated);
3186
+ return `${markdown.trimEnd()}
3187
+
3188
+ ${generated}
3189
+ `;
3190
+ }
3191
+ static citedClasses(markdown) {
3192
+ const current = FrameworkMapDoc.generatedOf(markdown);
3193
+ const handKept = current === null ? markdown : markdown.replace(current, "");
3194
+ return [...new Set([...handKept.matchAll(citedPattern)].map(([, name]) => name))].sort();
3195
+ }
3196
+ static unknownClasses(markdown, map) {
3197
+ const known = new Set([
3198
+ ...map.facets.flatMap(({ classes }) => classes.map(({ name }) => name)),
3199
+ ...map.channels.map(({ name }) => name)
3200
+ ]);
3201
+ return FrameworkMapDoc.citedClasses(markdown).filter((name) => !known.has(name));
3202
+ }
3203
+ static #owners(sites) {
3204
+ return sites.length ? sites.map(({ owner }) => `\`${owner}\``).join(", ") : "—";
3205
+ }
3206
+ static #relative(fromDir, file) {
3207
+ const from = fromDir.split("/").filter(Boolean);
3208
+ const to = file.split("/").filter(Boolean);
3209
+ let shared = 0;
3210
+ while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared])
3211
+ shared += 1;
3212
+ return [...from.slice(shared).map(() => ".."), ...to.slice(shared)].join("/");
3213
+ }
3214
+ }
1516
3215
  export {
1517
3216
  AkanGraphDiff,
1518
3217
  AkanIndex,
3218
+ FrameworkChannelReader,
3219
+ FrameworkMapBuilder,
3220
+ FrameworkMapDoc,
1519
3221
  createAst,
1520
3222
  createFactReader,
1521
3223
  ownerOf