@starci/eslint-canon-be 3.0.8 → 3.0.9
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/CHANGELOG.md +13 -1
- package/README.md +12 -6
- package/ambient-id.mjs +90 -0
- package/cli.mjs +181 -0
- package/comments.mjs +18 -21
- package/config-owner.mjs +20 -4
- package/connections.mjs +12 -12
- package/cookies.mjs +106 -0
- package/cqrs.mjs +22 -26
- package/data-access.mjs +9 -28
- package/default-deny.mjs +2 -1
- package/e2e-flow.mjs +4 -4
- package/event-bus.mjs +237 -0
- package/feature-thin.mjs +237 -0
- package/idempotency.mjs +3 -1
- package/index.mjs +22 -1
- package/infra-owner.mjs +1 -1
- package/injection.mjs +67 -22
- package/input-bounds.mjs +2 -1
- package/jobs.mjs +214 -0
- package/lib/ast.mjs +7 -0
- package/lib/config.mjs +1 -1
- package/lib/db-writes.mjs +1 -1
- package/lib/doors.mjs +81 -0
- package/lib/edition.mjs +45 -0
- package/lib/entity-writes.mjs +35 -0
- package/lib/path.mjs +0 -6
- package/lib/persistence.mjs +11 -18
- package/lib/ports.mjs +5 -2
- package/lib/specifier.mjs +27 -0
- package/lib/transactions.mjs +37 -0
- package/lib/transport-slots.mjs +8 -3
- package/lib/unit-spec.mjs +63 -2
- package/module-layering.mjs +3 -24
- package/module-shape.mjs +2 -2
- package/naming.mjs +2 -2
- package/package.json +3 -1
- package/project-graph.mjs +5 -2
- package/projections.mjs +117 -0
- package/query-safety.mjs +1 -1
- package/realtime.mjs +181 -0
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +160 -29
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/lib.mjs +16 -26
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +1 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/project-graph.mjs +3 -3
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/path-key.mjs +57 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/schema-authority.mjs +14 -12
- package/service-deps.mjs +7 -13
- package/service-isolation.mjs +72 -0
- package/size-budget.mjs +1 -1
- package/test-world.mjs +5 -4
- package/testing.mjs +27 -24
- package/transport.mjs +12 -9
- package/unit-spec.mjs +1 -1
- package/user-copy.mjs +5 -4
- package/webhooks.mjs +173 -0
- package/runtime/scripts/api/sops/decrypt.mjs +0 -16
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { canonical, isInside } from './config.mjs';
|
|
3
3
|
import { parseContract, installedSWR } from './next-data-contract.mjs';
|
|
4
|
-
import { relativePath,
|
|
4
|
+
import { relativePath, unwrapExpression } from './typescript.mjs';
|
|
5
|
+
import { anyDescendant, normalizedSymbol, normalizedSymbolValue, selectedNode, valueSymbol as sharedValueSymbol } from './ast-walks.mjs';
|
|
6
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
5
7
|
|
|
6
8
|
export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
|
|
7
9
|
export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
|
|
@@ -9,41 +11,9 @@ export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
|
|
|
9
11
|
|
|
10
12
|
const SWR_SPECIFIERS = new Set(['swr', 'swr/immutable', 'swr/mutation']);
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
|
|
16
|
-
seen.add(symbol);
|
|
17
|
-
const target = checker.getAliasedSymbol(symbol);
|
|
18
|
-
if (!target || target === symbol) break;
|
|
19
|
-
symbol = target;
|
|
20
|
-
}
|
|
21
|
-
return symbol;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function selectedNode(ts, expression) {
|
|
25
|
-
expression = unwrapExpression(ts, expression);
|
|
26
|
-
if (ts.isIdentifier(expression)) return expression;
|
|
27
|
-
if (ts.isPropertyAccessExpression(expression)) return expression.name;
|
|
28
|
-
if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function symbolAt(ts, checker, node) {
|
|
33
|
-
return normalizedSymbol(ts, checker, checker.getSymbolAtLocation(node));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function valueSymbol(ts, checker, expression, seen = new Set()) {
|
|
37
|
-
const selected = selectedNode(ts, expression);
|
|
38
|
-
const symbol = selected ? symbolAt(ts, checker, selected) : null;
|
|
39
|
-
if (!symbol || seen.has(symbol)) return symbol;
|
|
40
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
41
|
-
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
42
|
-
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
43
|
-
return valueSymbol(ts, checker, declarations[0].initializer, new Set(seen).add(symbol));
|
|
44
|
-
}
|
|
45
|
-
return symbol;
|
|
46
|
-
}
|
|
14
|
+
/* null when the expression selects no name at all (the shared default traces the unselected node itself). */
|
|
15
|
+
const valueSymbol = (ts, checker, expression, seen = new Set()) =>
|
|
16
|
+
sharedValueSymbol(ts, checker, expression, seen, { unselected: 'null' });
|
|
47
17
|
|
|
48
18
|
function declarationsInside(symbol, root) {
|
|
49
19
|
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
@@ -70,7 +40,7 @@ function officialTargets(config, context, installed, reasons) {
|
|
|
70
40
|
reasons.push(`${relativePath(config.root, source.fileName)} cannot resolve ${statement.moduleSpecifier.text} through the target TypeScript program`);
|
|
71
41
|
continue;
|
|
72
42
|
}
|
|
73
|
-
const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(),
|
|
43
|
+
const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
|
|
74
44
|
for (const [name, kind] of selected) {
|
|
75
45
|
const target = exports.get(name);
|
|
76
46
|
if (!target) continue;
|
|
@@ -136,7 +106,7 @@ function configMutateSymbols(config, context, targets) {
|
|
|
136
106
|
for (const element of node.name.elements) {
|
|
137
107
|
const property = element.propertyName && context.ts.isIdentifier(element.propertyName) ? element.propertyName.text
|
|
138
108
|
: context.ts.isIdentifier(element.name) ? element.name.text : null;
|
|
139
|
-
if (property === 'mutate' && context.ts.isIdentifier(element.name)) symbols.add(
|
|
109
|
+
if (property === 'mutate' && context.ts.isIdentifier(element.name)) symbols.add(normalizedSymbol(context.ts, checker, element.name));
|
|
140
110
|
}
|
|
141
111
|
}
|
|
142
112
|
context.ts.forEachChild(node, visit);
|
|
@@ -181,7 +151,7 @@ function exportedHook(config, context, entry, reasons) {
|
|
|
181
151
|
const checker = context.checkerFor(source.fileName);
|
|
182
152
|
const moduleSymbol = checker.getSymbolAtLocation(source);
|
|
183
153
|
const exported = moduleSymbol ? checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.getName() === entry.export) : null;
|
|
184
|
-
const symbol =
|
|
154
|
+
const symbol = normalizedSymbolValue(context.ts, checker, exported);
|
|
185
155
|
const declarations = (symbol?.getDeclarations?.() ?? []).filter(declaration => canonical(declaration.getSourceFile().fileName) === canonical(source.fileName));
|
|
186
156
|
const functions = declarations.map(declaration => functionNode(context.ts, declaration)).filter(Boolean);
|
|
187
157
|
if (functions.length !== 1) {
|
|
@@ -195,7 +165,7 @@ function bindingSymbols(ts, checker, fn) {
|
|
|
195
165
|
const found = new Map();
|
|
196
166
|
const addName = name => {
|
|
197
167
|
if (ts.isIdentifier(name)) {
|
|
198
|
-
const symbol =
|
|
168
|
+
const symbol = normalizedSymbol(ts, checker, name);
|
|
199
169
|
if (symbol) {
|
|
200
170
|
if (!found.has(name.text)) found.set(name.text, new Set());
|
|
201
171
|
found.get(name.text).add(symbol);
|
|
@@ -233,7 +203,7 @@ function resolvedIdentities(ts, checker, fn, entry, reasons) {
|
|
|
233
203
|
|
|
234
204
|
function accessPath(ts, checker, node) {
|
|
235
205
|
node = unwrapExpression(ts, node);
|
|
236
|
-
if (ts.isIdentifier(node)) return { symbol:
|
|
206
|
+
if (ts.isIdentifier(node)) return { symbol: normalizedSymbol(ts, checker, node), parts: [node.text] };
|
|
237
207
|
if (ts.isPropertyAccessExpression(node)) {
|
|
238
208
|
const base = accessPath(ts, checker, node.expression);
|
|
239
209
|
return base ? { symbol: base.symbol, parts: [...base.parts, node.name.text] } : null;
|
|
@@ -249,7 +219,7 @@ function expandedAccessPath(ts, checker, node, seen = new Set(), depth = 0) {
|
|
|
249
219
|
if (!node || depth > 10) return null;
|
|
250
220
|
node = unwrapExpression(ts, node);
|
|
251
221
|
if (ts.isIdentifier(node)) {
|
|
252
|
-
const symbol =
|
|
222
|
+
const symbol = normalizedSymbol(ts, checker, node);
|
|
253
223
|
if (!symbol || seen.has(symbol)) return symbol ? { symbol, parts: [node.text] } : null;
|
|
254
224
|
const initializer = constInitializer(ts, symbol);
|
|
255
225
|
return initializer ? expandedAccessPath(ts, checker, initializer, new Set(seen).add(symbol), depth + 1)
|
|
@@ -272,19 +242,8 @@ function matchesIdentityAccess(ts, checker, node, identity) {
|
|
|
272
242
|
return matches(accessPath(ts, checker, node)) || matches(expandedAccessPath(ts, checker, node));
|
|
273
243
|
}
|
|
274
244
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const visit = node => {
|
|
278
|
-
if (found) return;
|
|
279
|
-
if (matchesIdentityAccess(ts, checker, node, identity)) {
|
|
280
|
-
found = true;
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
ts.forEachChild(node, visit);
|
|
284
|
-
};
|
|
285
|
-
visit(expression);
|
|
286
|
-
return found;
|
|
287
|
-
}
|
|
245
|
+
const expressionReferences = (ts, checker, expression, identity) =>
|
|
246
|
+
anyDescendant(ts, expression, (node) => matchesIdentityAccess(ts, checker, node, identity));
|
|
288
247
|
|
|
289
248
|
function constInitializer(ts, symbol) {
|
|
290
249
|
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
@@ -316,7 +275,7 @@ function identityContribution(ts, checker, expression, identity, seen = new Set(
|
|
|
316
275
|
expression = unwrapExpression(ts, expression);
|
|
317
276
|
if (matchesIdentityAccess(ts, checker, expression, identity)) return 'yes';
|
|
318
277
|
if (ts.isIdentifier(expression)) {
|
|
319
|
-
const symbol =
|
|
278
|
+
const symbol = normalizedSymbol(ts, checker, expression);
|
|
320
279
|
if (!symbol || seen.has(symbol)) return 'no';
|
|
321
280
|
const initializer = constInitializer(ts, symbol);
|
|
322
281
|
return initializer ? identityContribution(ts, checker, initializer, identity, new Set(seen).add(symbol), depth + 1) : 'no';
|
|
@@ -378,8 +337,8 @@ function rootKeyExpression(ts, checker, expression, identities, seen = new Set()
|
|
|
378
337
|
const returned = returnedExpression(ts, expression);
|
|
379
338
|
return returned ? rootKeyExpression(ts, checker, returned, identities, seen, depth + 1) : null;
|
|
380
339
|
}
|
|
381
|
-
if (ts.isIdentifier(expression) && !identities.some(identity => identity.symbol ===
|
|
382
|
-
const symbol =
|
|
340
|
+
if (ts.isIdentifier(expression) && !identities.some(identity => identity.symbol === normalizedSymbol(ts, checker, expression))) {
|
|
341
|
+
const symbol = normalizedSymbol(ts, checker, expression);
|
|
383
342
|
if (symbol && !seen.has(symbol)) {
|
|
384
343
|
const declarations = symbol.getDeclarations?.() ?? [];
|
|
385
344
|
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
@@ -417,8 +376,8 @@ function staticKeyExpression(ts, checker, expression, identities, seen = new Set
|
|
|
417
376
|
return false;
|
|
418
377
|
}
|
|
419
378
|
if (ts.isIdentifier(expression)) {
|
|
420
|
-
if (expression.text === 'undefined' || identities.some(identity => identity.symbol ===
|
|
421
|
-
const symbol =
|
|
379
|
+
if (expression.text === 'undefined' || identities.some(identity => identity.symbol === normalizedSymbol(ts, checker, expression))) return true;
|
|
380
|
+
const symbol = normalizedSymbol(ts, checker, expression);
|
|
422
381
|
if (!symbol || seen.has(symbol)) return false;
|
|
423
382
|
const declarations = symbol.getDeclarations?.() ?? [];
|
|
424
383
|
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
|
|
@@ -494,7 +453,7 @@ function isUnshadowedCommonJsRequire(ts, checker, expression, seen = new Set(),
|
|
|
494
453
|
|
|
495
454
|
function symbolOfRequireAlias(ts, checker, expression) {
|
|
496
455
|
const symbol = checker.getSymbolAtLocation(expression);
|
|
497
|
-
return
|
|
456
|
+
return normalizedSymbolValue(ts, checker, symbol);
|
|
498
457
|
}
|
|
499
458
|
|
|
500
459
|
function swrReferences(config, context) {
|
|
@@ -535,7 +494,7 @@ function containerSymbolsFromKey(ts, checker, expression, found = new Set(), see
|
|
|
535
494
|
if (!expression || depth > 12) return found;
|
|
536
495
|
expression = unwrapExpression(ts, expression);
|
|
537
496
|
if (ts.isIdentifier(expression)) {
|
|
538
|
-
const symbol =
|
|
497
|
+
const symbol = normalizedSymbol(ts, checker, expression);
|
|
539
498
|
if (!symbol || seen.has(symbol)) return found;
|
|
540
499
|
const initializer = constInitializer(ts, symbol);
|
|
541
500
|
if (!initializer) return found;
|
|
@@ -555,7 +514,7 @@ function keyContainerRisks(ts, checker, key, owner) {
|
|
|
555
514
|
.filter(ts.isVariableDeclaration).map(declaration => declaration.name)));
|
|
556
515
|
const risks = [];
|
|
557
516
|
const visit = node => {
|
|
558
|
-
if (ts.isIdentifier(node) && symbols.has(
|
|
517
|
+
if (ts.isIdentifier(node) && symbols.has(normalizedSymbol(ts, checker, node)) && !declarationNames.has(node)
|
|
559
518
|
&& !(node.pos >= key.pos && node.end <= key.end)) risks.push(node);
|
|
560
519
|
ts.forEachChild(node, visit);
|
|
561
520
|
};
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { canonical, isInside } from './config.mjs';
|
|
3
|
-
import { relativePath,
|
|
3
|
+
import { relativePath, workspaceExportSources } from './typescript.mjs';
|
|
4
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
5
|
+
import { isServerActionModule } from './server-action.mjs';
|
|
4
6
|
|
|
5
7
|
function absolute(root, relative) {
|
|
6
8
|
return canonical(path.resolve(root, ...relative.split('/')));
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
/** The
|
|
11
|
+
/** The extra public entries the slot manifest declares for the owner (slot field `entries`, on the slot that holds its entry file), as absolute paths. */
|
|
12
|
+
function slotEntries(config, owner) {
|
|
13
|
+
const found = config.hfs?.classifyPath?.(owner.entry);
|
|
14
|
+
const names = found?.slot ? config.hfs.slot(found.slot)?.entries ?? [] : [];
|
|
15
|
+
return names.map(name => absolute(config.root, `${found.root}/${name}`));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The owner's public entries: its declared entry, the `entries` its slot declares, plus every source file its package.json `exports` maps when the owner is a workspace package. */
|
|
10
19
|
function ownerDeclarations(config, context) {
|
|
11
20
|
return config.owners.map(owner => {
|
|
12
21
|
const root = absolute(config.root, owner.root);
|
|
13
22
|
const entry = absolute(config.root, owner.entry);
|
|
14
23
|
const workspace = context.workspaces?.find(item => item.root === root);
|
|
15
|
-
const
|
|
16
|
-
|
|
24
|
+
const declared = slotEntries(config, owner);
|
|
25
|
+
const entries = new Set([entry, ...declared, ...(workspace ? workspaceExportSources(context.ts, workspace) : [])]);
|
|
26
|
+
return { ...owner, root, entry, entries, declared };
|
|
17
27
|
}).sort((a, b) => b.root.length - a.root.length);
|
|
18
28
|
}
|
|
19
29
|
|
|
@@ -21,7 +31,7 @@ function ownerOf(owners, file) {
|
|
|
21
31
|
return owners.find(owner => isInside(owner.root, file)) ?? null;
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
function privateOwnerChain(context, owners, edge) {
|
|
34
|
+
function privateOwnerChain(context, owners, actionEntries, edge) {
|
|
25
35
|
const sourceOwner = ownerOf(owners, edge.from);
|
|
26
36
|
const queue = [{ file: edge.to, chain: [edge.from, edge.to] }];
|
|
27
37
|
const visited = new Set();
|
|
@@ -30,7 +40,7 @@ function privateOwnerChain(context, owners, edge) {
|
|
|
30
40
|
if (visited.has(current.file)) continue;
|
|
31
41
|
visited.add(current.file);
|
|
32
42
|
const owner = ownerOf(owners, current.file);
|
|
33
|
-
if (owner && sourceOwner?.id !== owner.id) return owner.entries.has(current.file) ? null : { owner, chain: current.chain };
|
|
43
|
+
if (owner && sourceOwner?.id !== owner.id) return owner.entries.has(current.file) || actionEntries.has(current.file) ? null : { owner, chain: current.chain };
|
|
34
44
|
for (const candidate of context.edges.get(current.file) ?? []) if (candidate.reexport) {
|
|
35
45
|
queue.push({ file: candidate.to, chain: [...current.chain, candidate.to] });
|
|
36
46
|
}
|
|
@@ -55,6 +65,9 @@ export function checkOwners(config, context) {
|
|
|
55
65
|
const owners = ownerDeclarations(config, context);
|
|
56
66
|
const violations = [];
|
|
57
67
|
const sourceFiles = new Map(context.files.map(file => [canonical(file.fileName), file]));
|
|
68
|
+
const actionEntries = new Set(config.kinds.includes('frontend')
|
|
69
|
+
? [...sourceFiles].filter(([, file]) => isServerActionModule(context.ts, file)).map(([name]) => name)
|
|
70
|
+
: []);
|
|
58
71
|
for (const owner of owners) {
|
|
59
72
|
const sourceFile = sourceFiles.get(owner.entry);
|
|
60
73
|
if (!sourceFile) {
|
|
@@ -62,20 +75,27 @@ export function checkOwners(config, context) {
|
|
|
62
75
|
message: `Owner ${owner.id} public entry is outside the configured TypeScript programs, so its boundary cannot be checked.` });
|
|
63
76
|
continue;
|
|
64
77
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
const publicFiles = new Set([
|
|
79
|
+
sourceFile,
|
|
80
|
+
...owner.declared.map(file => sourceFiles.get(file)).filter(Boolean),
|
|
81
|
+
...[...actionEntries].filter(file => isInside(owner.root, file)).map(file => sourceFiles.get(file)).filter(Boolean),
|
|
82
|
+
]);
|
|
83
|
+
for (const entryFile of publicFiles) {
|
|
84
|
+
for (const statement of entryFile.statements) if (context.ts.isExportDeclaration(statement) && !statement.exportClause) {
|
|
85
|
+
violations.push({
|
|
86
|
+
ruleId: 'ARCH_OWNER_EXPORT_STAR',
|
|
87
|
+
path: relativePath(config.root, canonical(entryFile.fileName)),
|
|
88
|
+
...sourceLocation(entryFile, statement),
|
|
89
|
+
owner: owner.id,
|
|
90
|
+
message: `Owner ${owner.id} public entry must use explicit named exports rather than export *.`
|
|
91
|
+
});
|
|
92
|
+
}
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
95
|
for (const sourceFile of context.files) {
|
|
76
96
|
const from = canonical(sourceFile.fileName);
|
|
77
97
|
for (const edge of context.edges.get(from) ?? []) {
|
|
78
|
-
const bypass = privateOwnerChain(context, owners, edge);
|
|
98
|
+
const bypass = privateOwnerChain(context, owners, actionEntries, edge);
|
|
79
99
|
if (!bypass) continue;
|
|
80
100
|
if (arrangesSchema(config, from, bypass.chain.at(-1))) continue;
|
|
81
101
|
violations.push({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { machineKit } from './machine-ast.mjs';
|
|
3
|
+
import { isTestWorldSlot } from '../test-world-slot.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* R45 `register-once` (BE_MODULE_SHAPE), the module graph read from the app roots (owner rule of 2026-09-30, the
|
|
@@ -29,7 +30,7 @@ export function checkRegisterOnce(input) {
|
|
|
29
30
|
const isAppRoot = file => Boolean(file.slot?.startsWith('be.app.')) && path.posix.basename(file.rel) === 'app.module.ts';
|
|
30
31
|
// The test world (slot be.tests.world) is the test composition root: useTestWorld registers capability modules with
|
|
31
32
|
// `isGlobal: true` the way an app root does. A spec (integration, contract, e2e) is not a composition root.
|
|
32
|
-
const isWorld = file => file.slot
|
|
33
|
+
const isWorld = file => isTestWorldSlot(file.slot);
|
|
33
34
|
const appOf = file => config.apps.find(app => file.rel === `apps/${app.name}/src/app.module.ts`)?.name ?? null;
|
|
34
35
|
|
|
35
36
|
// Every @Module class of the program.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { canonical } from './config.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { referencedExports, relativePath, unwrapExpression } from './typescript.mjs';
|
|
4
|
+
import { commonJsRequireReasons, decoratorCallee, moduleExportsOf, mutableDecoratorKind, normalizedSymbol, normalizedSymbolValue, programSourcesOf, selectedNode, valueSymbol } from './ast-walks.mjs';
|
|
5
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
4
6
|
|
|
5
7
|
const REGISTRATION_RULE_IDS = ['BE_MODULE_HANDLER_REGISTRATION', 'BE_MODULE_PROVIDER_REREGISTRATION'];
|
|
6
8
|
const FRAMEWORK = new Map([
|
|
@@ -9,106 +11,46 @@ const FRAMEWORK = new Map([
|
|
|
9
11
|
['QueryHandler', '@nestjs/cqrs'],
|
|
10
12
|
]);
|
|
11
13
|
|
|
12
|
-
function normalizedSymbolValue(ts, checker, value) {
|
|
13
|
-
let symbol = value ?? null;
|
|
14
|
-
const seen = new Set();
|
|
15
|
-
while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
|
|
16
|
-
seen.add(symbol);
|
|
17
|
-
const target = checker.getAliasedSymbol(symbol);
|
|
18
|
-
if (!target || target === symbol) break;
|
|
19
|
-
symbol = target;
|
|
20
|
-
}
|
|
21
|
-
return symbol;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizedSymbol(ts, checker, node) {
|
|
25
|
-
return normalizedSymbolValue(ts, checker, checker?.getSymbolAtLocation(node) ?? null);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function valueSymbol(ts, checker, node, seen = new Set()) {
|
|
29
|
-
const selected = selectedNode(ts, node) ?? node;
|
|
30
|
-
const symbol = normalizedSymbol(ts, checker, selected);
|
|
31
|
-
if (!symbol || seen.has(symbol)) return symbol;
|
|
32
|
-
seen.add(symbol);
|
|
33
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
34
|
-
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
35
|
-
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
36
|
-
return valueSymbol(ts, checker, declarations[0].initializer, seen);
|
|
37
|
-
}
|
|
38
|
-
return symbol;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function calledExpression(ts, decorator) {
|
|
42
|
-
const expression = decorator.expression;
|
|
43
|
-
return ts.isCallExpression(expression) ? expression.expression : expression;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function selectedNode(ts, expression) {
|
|
47
|
-
expression = unwrapExpression(ts, expression);
|
|
48
|
-
if (ts.isIdentifier(expression)) return expression;
|
|
49
|
-
if (ts.isPropertyAccessExpression(expression)) return expression.name;
|
|
50
|
-
if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
14
|
function frameworkDecorator(ts, checker, decorator, targets) {
|
|
55
|
-
const selected = selectedNode(ts,
|
|
15
|
+
const selected = selectedNode(ts, decoratorCallee(ts, decorator));
|
|
56
16
|
if (!selected) return null;
|
|
57
17
|
const symbol = valueSymbol(ts, checker, selected);
|
|
58
18
|
for (const [name, target] of targets) if (symbol === target) return name;
|
|
59
19
|
return null;
|
|
60
20
|
}
|
|
61
21
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (const [name, expected] of targets) if (target === expected) return name;
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
22
|
+
/* This module's framework map keys decorator names, not symbols - invert it (first name wins, as the old
|
|
23
|
+
* `for...of` scan did) for the shared symbol-keyed lookup. */
|
|
24
|
+
const mutableFrameworkAlias = (ts, checker, decorator, targets) => {
|
|
25
|
+
const bySymbol = new Map();
|
|
26
|
+
for (const [name, symbol] of targets) if (!bySymbol.has(symbol)) bySymbol.set(symbol, name);
|
|
27
|
+
return mutableDecoratorKind(ts, checker, decorator, bySymbol);
|
|
28
|
+
};
|
|
72
29
|
|
|
73
30
|
function frameworkTargets(config, context, checker, localFiles) {
|
|
74
31
|
const targets = new Map();
|
|
75
32
|
const reasons = [];
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
78
|
-
for (const sourceFile of
|
|
79
|
-
if (!localFiles.has(canonical(sourceFile.fileName))) continue;
|
|
33
|
+
const files = programSourcesOf(context, checker, localFiles);
|
|
34
|
+
if (!files) return { targets, reasons: ['a TypeScript program checker could not be associated with its source'] };
|
|
35
|
+
for (const sourceFile of files) {
|
|
80
36
|
for (const statement of sourceFile.statements) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
&& statement.moduleReference.expression && context.ts.isStringLiteralLike(statement.moduleReference.expression)
|
|
85
|
-
? statement.moduleReference.expression : null;
|
|
86
|
-
const specifier = (moduleSpecifier ?? importEquals)?.text;
|
|
87
|
-
if (!specifier || ![...FRAMEWORK.values()].includes(specifier)) continue;
|
|
88
|
-
const expected = new Set([...FRAMEWORK].filter(([, packageName]) => packageName === specifier).map(([name]) => name));
|
|
37
|
+
const bound = moduleExportsOf(context.ts, checker, statement);
|
|
38
|
+
if (!bound || ![...FRAMEWORK.values()].includes(bound.specifier)) continue;
|
|
39
|
+
const expected = new Set([...FRAMEWORK].filter(([, packageName]) => packageName === bound.specifier).map(([name]) => name));
|
|
89
40
|
const selected = referencedExports(context.ts, statement, expected);
|
|
90
41
|
if (!selected.length) continue;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${specifier}`);
|
|
42
|
+
if (!bound.symbol) {
|
|
43
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${bound.specifier}`);
|
|
94
44
|
continue;
|
|
95
45
|
}
|
|
96
|
-
const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
|
|
97
46
|
for (const name of selected) {
|
|
98
|
-
const target = exports.get(name);
|
|
47
|
+
const target = bound.exports.get(name);
|
|
99
48
|
if (target) targets.set(name, target);
|
|
100
|
-
else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${specifier}`);
|
|
49
|
+
else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${bound.specifier}`);
|
|
101
50
|
}
|
|
102
51
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
&& node.arguments.length === 1 && context.ts.isStringLiteralLike(node.arguments[0])
|
|
106
|
-
&& [...FRAMEWORK.values()].includes(node.arguments[0].text)) {
|
|
107
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} uses a CommonJS Nest framework binding whose decorator identity cannot be proved`);
|
|
108
|
-
}
|
|
109
|
-
context.ts.forEachChild(node, visitRequire);
|
|
110
|
-
};
|
|
111
|
-
visitRequire(sourceFile);
|
|
52
|
+
commonJsRequireReasons(context.ts, checker, sourceFile, (specifier) => [...FRAMEWORK.values()].includes(specifier),
|
|
53
|
+
reasons, relativePath(config.root, sourceFile.fileName), 'whose decorator identity cannot be proved', 'Nest framework binding');
|
|
112
54
|
}
|
|
113
55
|
return { targets, reasons };
|
|
114
56
|
}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { lsFiles } from '../../api/git/ls-files.mjs';
|
|
4
|
+
import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
|
|
5
|
+
import { gitOutputOf } from '../../lib/git.mjs';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
|
|
7
9
|
* `requiredInstances`, `minInstances`, required app kinds) must exist in the repository tree. Everything is read through the
|
|
8
10
|
* resolver of scripts/hfs/slots.mjs; nothing here names a path.
|
|
9
11
|
*
|
|
10
|
-
* BE_REQUIRED_MODULE_MISSING a backend app, feature, domain, integrations or platform instance lacks a required file or
|
|
11
|
-
* directory, or a required platform instance (config, logging, errors, primitives) is absent
|
|
12
12
|
* FE_ERROR_BOUNDARY_MISSING a fe.app.next app lacks global-error, error, not-found or loading under src/app
|
|
13
|
-
* HFS_REQUIRED_FILE_MISSING any other required file or directory (
|
|
14
|
-
*
|
|
13
|
+
* HFS_REQUIRED_FILE_MISSING any other required file or directory of any slot (backend app, feature, domain, integrations and
|
|
14
|
+
* platform instances, a required platform instance such as config or logging, packages, fe modules,
|
|
15
|
+
* i18n, api ...), and a minimum (no api app, no feature, no next app) which is reported once at hfs.json
|
|
15
16
|
*
|
|
16
17
|
* Slots of tier none (README, root config, .starciwork, .starcistacks) belong to the slot check; fe.app.next is the one
|
|
17
18
|
* tier-none slot judged here. The tree is the tracked one (`git ls-files` from the repository root, minus files deleted
|
|
18
19
|
* on disk), else the filesystem without node_modules, .git, dist and .next.
|
|
19
20
|
*/
|
|
20
|
-
export const REQUIRED_FILE_RULE_IDS = ['
|
|
21
|
+
export const REQUIRED_FILE_RULE_IDS = ['FE_ERROR_BOUNDARY_MISSING', 'HFS_REQUIRED_FILE_MISSING'];
|
|
21
22
|
|
|
22
23
|
const SKIPPED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next']);
|
|
23
24
|
const ERROR_BOUNDARY_FILE = /^(?:global-error|error|not-found|loading)\.tsx$/u;
|
|
24
|
-
const BACKEND_MODULE_SLOT = /^be\.(?:app|feature|transport|domain|integrations|platform)(?:\.|$)/u;
|
|
25
25
|
const ALSO_JUDGED_TIER_NONE = new Set(['fe.app.next']);
|
|
26
26
|
|
|
27
27
|
function gitFiles(root) {
|
|
28
28
|
try {
|
|
29
|
-
const run = args =>
|
|
30
|
-
run(
|
|
31
|
-
const deleted = new Set(run(
|
|
32
|
-
return run(
|
|
29
|
+
const run = (call, args) => gitOutputOf(call(args, { cwd: root, maxBuffer: 256 * 1024 * 1024 }));
|
|
30
|
+
run(revParseQuery, ['--is-inside-work-tree']);
|
|
31
|
+
const deleted = new Set(run(lsFiles, ['--deleted', '-z', '--', '.']).split('\0').filter(Boolean));
|
|
32
|
+
return run(lsFiles, ['--cached', '-z', '--', '.']).split('\0').filter(file => file && !deleted.has(file));
|
|
33
33
|
} catch {
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
@@ -60,12 +60,20 @@ export function treeOf(root) {
|
|
|
60
60
|
return { files, directories };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The prelude every tree-walking architecture checker shares: the input's config/graph/context, the compiler,
|
|
65
|
+
* the slot resolver and the tracked repository tree.
|
|
66
|
+
*/
|
|
67
|
+
export function checkerScope({ config, graph, context }) {
|
|
68
|
+
return { config, graph, context, ts: context.ts, resolver: graph.resolver, tree: treeOf(config.root) };
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
const strip = entry => entry.replace(/\/+$/u, '');
|
|
64
72
|
const rootOfTarget = target => (target.endsWith('/') ? strip(target) : (path.posix.dirname(target) === '.' ? '' : path.posix.dirname(target)));
|
|
65
73
|
|
|
66
74
|
function ruleFor(slotId, target, rootExists) {
|
|
67
75
|
if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(strip(target)))) return 'FE_ERROR_BOUNDARY_MISSING';
|
|
68
|
-
return
|
|
76
|
+
return 'HFS_REQUIRED_FILE_MISSING';
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export function checkRequiredFiles({ config, graph }) {
|