@starci/eslint-canon-fe 6.0.2 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +3 -5
- package/client-boundary.mjs +2 -5
- package/comments.mjs +38 -15
- package/docs/brand-values.md +2 -2
- package/docs/client-boundary.md +10 -10
- package/docs/env-owner.md +4 -4
- package/docs/formatting.md +2 -2
- package/docs/grammar-boundary.md +2 -2
- package/docs/hooks-folder.md +2 -2
- package/docs/hygiene.md +8 -8
- package/docs/lint-escape-hatch.md +2 -2
- package/docs/lists.md +6 -6
- package/docs/native-controls.md +6 -6
- package/docs/next-conventions.md +20 -20
- package/docs/size-and-state-budget.md +6 -6
- package/docs/size-growth.md +2 -2
- package/docs/status-colors.md +2 -2
- package/docs/translation.md +2 -2
- package/docs/transport.md +18 -18
- package/docs/type-safety.md +8 -8
- package/env-owner.mjs +2 -3
- package/file-layout.mjs +3 -43
- package/formatting.mjs +2 -2
- package/grammar-boundary.mjs +1 -2
- package/hooks-folder.mjs +6 -7
- package/index.mjs +5 -27
- package/lib/config.mjs +6 -22
- package/lib/scope.mjs +3 -12
- package/lib/why.mjs +3 -68
- package/lint-escape-hatch.mjs +3 -3
- package/lists.mjs +0 -4
- package/loading.mjs +2 -2
- package/native-controls.mjs +0 -4
- package/next-conventions.mjs +3 -7
- package/package.json +1 -1
- package/project-graph.mjs +23 -0
- package/props-and-slots.mjs +4 -5
- package/runtime/knowledge/hfs/slots.yaml +145 -107
- package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
- package/runtime/scripts/checks/architecture/backend.mjs +350 -0
- package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
- package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
- package/runtime/scripts/checks/architecture/clones.mjs +200 -0
- package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
- package/runtime/scripts/checks/architecture/config.mjs +310 -0
- package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
- package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
- package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
- package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
- package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
- package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
- package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
- package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
- package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
- package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
- package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
- package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
- package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
- package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
- package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
- package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
- package/runtime/scripts/checks/architecture/index.mjs +321 -0
- package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
- package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
- package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
- package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
- package/runtime/scripts/checks/architecture/owners.mjs +95 -0
- package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
- package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
- package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
- package/runtime/scripts/checks/architecture/registration.mjs +319 -0
- package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
- package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
- package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
- package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
- package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
- package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
- package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
- package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
- package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +573 -0
- package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
- package/runtime/scripts/checks/common.mjs +44 -0
- package/runtime/scripts/checks/typescript-programs.mjs +82 -0
- package/runtime/scripts/lib/hfs-allows.mjs +15 -0
- package/runtime/scripts/lib/hfs-path-findings.mjs +84 -0
- package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
- package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
- package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +29 -0
- package/runtime/scripts/lib/hfs-slots.mjs +13 -6
- package/runtime/scripts/lib/json.mjs +22 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/project-graph.mjs +0 -0
- package/runtime/scripts/lib/project-rule.mjs +39 -0
- package/runtime/scripts/lib/repo-identity.mjs +50 -0
- package/runtime/scripts/lib/secret-patterns.mjs +44 -0
- package/runtime/scripts/lib/stack-services.mjs +217 -0
- package/runtime/scripts/lib/test-secrets.mjs +120 -0
- package/served-locale.mjs +2 -2
- package/shape-slot.mjs +2 -3
- package/size-and-state-budget.mjs +1 -4
- package/size-growth.mjs +1 -2
- package/status-colors.mjs +0 -2
- package/tokens.mjs +1 -1
- package/translation.mjs +1 -29
- package/transport.mjs +13 -17
- package/docs/e2e-shape.md +0 -161
- package/docs/spec-quality.md +0 -147
- package/e2e-shape.mjs +0 -345
- package/spec-quality.mjs +0 -318
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { treeOf } from './required-files.mjs';
|
|
4
|
+
import { allowsFile } from '../../lib/hfs-allows.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* R47 `contract-fixture-guard` (BE_CONTRACT_UNGUARDED). A fake at the network edge serves payload fixtures; a fixture drifts
|
|
8
|
+
* from the real provider silently unless a sandbox contract spec compares them. So every `fakes/<provider>/` of the test
|
|
9
|
+
* world that holds payload fixtures (`payloads/*.json`) needs at least one `src/tests/contract/<provider>/*.contract-spec.ts`
|
|
10
|
+
* (slot be.tests.contract) that, in its AST,
|
|
11
|
+
* 1. references that provider's fixtures: a call whose argument is the provider's name as a string literal (the world's
|
|
12
|
+
* fixture accessor, `renderPayload("<provider>", ...)`), an import from `fakes/<provider>/`, or a string that names
|
|
13
|
+
* `fakes/<provider>/`; and
|
|
14
|
+
* 2. compares shapes: an `expect(<... shapeOf(real) ...>).toEqual(<... shapeOf(fixture) ...>)` (or `toStrictEqual`), where
|
|
15
|
+
* `shapeOf` is the helper the world vocabulary names (knowledge/hfs/slots.yaml ruleParams.be.contractShape.helper) and
|
|
16
|
+
* is imported, never declared in the spec.
|
|
17
|
+
* A fake with fixtures and no such spec is a finding at its first fixture. The tree is the tracked one over the world's own
|
|
18
|
+
* slot; nothing names a path. Fakes the test-world library provides (declared in `test-world.config.ts`, held in the library
|
|
19
|
+
* package) have no tracked fixtures here and are guarded by the library's own contract specs, so only the repository's own
|
|
20
|
+
* `fakes/<provider>/` folders are judged.
|
|
21
|
+
*/
|
|
22
|
+
export const CONTRACT_FIXTURE_GUARD_RULE_IDS = ['BE_CONTRACT_UNGUARDED'];
|
|
23
|
+
|
|
24
|
+
const RULE = 'BE_CONTRACT_UNGUARDED';
|
|
25
|
+
const WORLD_SLOT = 'be.tests.world';
|
|
26
|
+
const CONTRACT_SLOT = 'be.tests.contract';
|
|
27
|
+
const FAKES_DIRECTORY = 'fakes/';
|
|
28
|
+
const PAYLOADS = /^([^/]+)\/payloads\/[^/]+\.json$/u;
|
|
29
|
+
const SHAPE_MATCHERS = new Set(['toEqual', 'toStrictEqual']);
|
|
30
|
+
|
|
31
|
+
const unwrap = (ts, node) => {
|
|
32
|
+
let current = node;
|
|
33
|
+
while (current && (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current) || ts.isAwaitExpression(current))) current = current.expression;
|
|
34
|
+
return current;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Every node under `root`, itself included. */
|
|
38
|
+
function walk(ts, root, visit) {
|
|
39
|
+
const step = node => { visit(node); ts.forEachChild(node, step); };
|
|
40
|
+
step(root);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Facts one contract spec establishes: the providers whose fixtures it references and whether it compares shapes. */
|
|
44
|
+
function readSpec(ts, sourceFile, helper, providers) {
|
|
45
|
+
const shapeNames = new Set();
|
|
46
|
+
const referenced = new Set();
|
|
47
|
+
for (const statement of sourceFile.statements) {
|
|
48
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
49
|
+
const from = statement.moduleSpecifier.text;
|
|
50
|
+
for (const provider of providers) if (from.includes(`${FAKES_DIRECTORY}${provider}/`) || from.endsWith(`${FAKES_DIRECTORY}${provider}`)) referenced.add(provider);
|
|
51
|
+
const named = statement.importClause?.namedBindings;
|
|
52
|
+
if (!named || !ts.isNamedImports(named)) continue;
|
|
53
|
+
for (const element of named.elements) if ((element.propertyName ?? element.name).text === helper) shapeNames.add(element.name.text);
|
|
54
|
+
}
|
|
55
|
+
const callsShape = node => {
|
|
56
|
+
let found = false;
|
|
57
|
+
walk(ts, node, inner => { if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression) && shapeNames.has(inner.expression.text)) found = true; });
|
|
58
|
+
return found;
|
|
59
|
+
};
|
|
60
|
+
let compares = false;
|
|
61
|
+
walk(ts, sourceFile, node => {
|
|
62
|
+
if (ts.isStringLiteralLike(node)) for (const provider of providers) if (node.text.includes(`${FAKES_DIRECTORY}${provider}/`)) referenced.add(provider);
|
|
63
|
+
if (!ts.isCallExpression(node)) return;
|
|
64
|
+
const named = ts.isIdentifier(node.expression) ? node.expression.text : null;
|
|
65
|
+
for (const argument of node.arguments) {
|
|
66
|
+
const value = unwrap(ts, argument);
|
|
67
|
+
if (value && ts.isStringLiteralLike(value) && providers.has(value.text) && named !== 'describe' && named !== 'it' && named !== 'test') referenced.add(value.text);
|
|
68
|
+
}
|
|
69
|
+
const callee = node.expression;
|
|
70
|
+
if (!ts.isPropertyAccessExpression(callee) || !SHAPE_MATCHERS.has(callee.name.text)) return;
|
|
71
|
+
let receiver = unwrap(ts, callee.expression);
|
|
72
|
+
while (receiver && ts.isPropertyAccessExpression(receiver)) receiver = unwrap(ts, receiver.expression); // .not / .resolves
|
|
73
|
+
if (!receiver || !ts.isCallExpression(receiver) || !ts.isIdentifier(receiver.expression) || receiver.expression.text !== 'expect') return;
|
|
74
|
+
const [actual] = receiver.arguments;
|
|
75
|
+
const [expected] = node.arguments;
|
|
76
|
+
if (actual && expected && callsShape(actual) && callsShape(expected)) compares = true;
|
|
77
|
+
});
|
|
78
|
+
return { referenced, compares };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function checkContractFixtureGuard(input) {
|
|
82
|
+
const { config, graph, context } = input;
|
|
83
|
+
const ts = context.ts;
|
|
84
|
+
const resolver = graph.resolver;
|
|
85
|
+
const { contractShape } = resolver.ruleParams();
|
|
86
|
+
const tree = treeOf(config.root);
|
|
87
|
+
const violations = [];
|
|
88
|
+
const fixtures = new Map(); // provider -> first payload fixture file
|
|
89
|
+
const specs = new Map(); // provider -> [contract spec file]
|
|
90
|
+
for (const file of [...tree.files].sort()) {
|
|
91
|
+
const classified = resolver.classifyPath(file);
|
|
92
|
+
if (classified.status !== 'owned') continue;
|
|
93
|
+
if (classified.slot === CONTRACT_SLOT) {
|
|
94
|
+
const provider = classified.bindings?.provider;
|
|
95
|
+
if (provider && file.endsWith('.contract-spec.ts')) specs.set(provider, [...(specs.get(provider) ?? []), file]);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const verdict = classified.slot === WORLD_SLOT ? allowsFile(resolver, file) : null;
|
|
99
|
+
if (verdict?.slot === WORLD_SLOT) {
|
|
100
|
+
if (!verdict.relative.startsWith(FAKES_DIRECTORY)) continue;
|
|
101
|
+
const match = PAYLOADS.exec(verdict.relative.slice(FAKES_DIRECTORY.length));
|
|
102
|
+
if (match && !fixtures.has(match[1])) fixtures.set(match[1], file);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const providers = new Set(fixtures.keys());
|
|
106
|
+
const guarded = new Set();
|
|
107
|
+
for (const [provider, files] of specs) {
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const absolute = path.join(config.root, ...file.split('/'));
|
|
110
|
+
if (!fs.existsSync(absolute)) continue;
|
|
111
|
+
const sourceFile = ts.createSourceFile(absolute, fs.readFileSync(absolute, 'utf8'), ts.ScriptTarget.Latest, true); // the contract tree is outside the root program
|
|
112
|
+
const facts = readSpec(ts, sourceFile, contractShape.helper, providers);
|
|
113
|
+
if (facts.compares && facts.referenced.has(provider)) guarded.add(provider);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const [provider, file] of [...fixtures].sort(([a], [b]) => a.localeCompare(b))) {
|
|
117
|
+
if (guarded.has(provider)) continue;
|
|
118
|
+
const has = specs.get(provider)?.length ?? 0;
|
|
119
|
+
const why = has === 0
|
|
120
|
+
? `there is no src/tests/contract/${provider}/*.contract-spec.ts`
|
|
121
|
+
: `none of the ${has} contract spec(s) under src/tests/contract/${provider}/ both references the ${provider} fixtures and asserts expect(${contractShape.helper}(real)).toEqual(${contractShape.helper}(fixture))`;
|
|
122
|
+
violations.push({
|
|
123
|
+
ruleId: RULE, path: file, line: 1, column: 1, slot: WORLD_SLOT,
|
|
124
|
+
message: `fakes/${provider}/ serves payload fixtures but no contract spec guards them: ${why}. A fixture that is not compared with the real provider drifts silently; add the sandbox contract spec (the contract helper of the world skips it when the sandbox config is absent).`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { violations, coverage: { status: 'checked', fixtureFakes: fixtures.size, contractSpecs: [...specs.values()].flat().length } };
|
|
128
|
+
}
|