@starci/eslint-canon-be 3.0.10 → 3.2.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 +7 -1
- package/README.md +2 -1
- package/builders.mjs +1 -1
- package/comments.mjs +1 -1
- package/config-owner.mjs +1 -1
- package/index.mjs +2 -0
- package/lib/config.mjs +4 -1
- package/lib/doors.mjs +3 -1
- package/module-shape.mjs +1 -2
- package/package.json +1 -1
- package/resilience.mjs +1 -1
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/release-config.mjs +28 -0
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -0
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +164 -104
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/project-graph.mjs +4 -1
- package/runtime/scripts/hfs/repo-identity.mjs +21 -4
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +74 -695
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/path-key.mjs +38 -5
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
- package/runtime/scripts/lib/sops-envelope.mjs +95 -48
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/runtime/scripts/lib/yaml-cached.mjs +14 -0
- package/schema-authority.mjs +1 -1
- package/service-deps.mjs +1 -1
- package/sonar-parity.mjs +19 -0
- package/suppression.mjs +2 -6
- package/test-world.mjs +2 -10
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { machineKit } from './machine-ast.mjs';
|
|
4
4
|
import { constructorDependencies } from './constructor-deps.mjs';
|
|
5
5
|
import { treeOf } from './required-files.mjs';
|
|
6
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* R48 `unit-spec-providers` (BE_SPEC_QUALITY), the unit test standard's rule 3: the unit spec of a service builds its
|
|
@@ -22,7 +23,6 @@ import { treeOf } from './required-files.mjs';
|
|
|
22
23
|
export const UNIT_SPEC_PROVIDERS_RULE_IDS = ['BE_SPEC_QUALITY'];
|
|
23
24
|
|
|
24
25
|
const RULE = 'BE_SPEC_QUALITY';
|
|
25
|
-
const SERVICE_SPEC = /\.service\.spec\.ts$/u;
|
|
26
26
|
const SPEC_ROOT = /^(?:src|apps)\//u;
|
|
27
27
|
|
|
28
28
|
/** local name -> {name, module} for every named import of a source file. */
|
|
@@ -49,6 +49,87 @@ function testingModules(ts, sourceFile) {
|
|
|
49
49
|
|
|
50
50
|
const literalKey = (ts, property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
|
|
51
51
|
|
|
52
|
+
function providerList(ts, call, rel, report) {
|
|
53
|
+
const options = call.arguments[0];
|
|
54
|
+
const property = options && ts.isObjectLiteralExpression(options) ? options.properties.find(item => literalKey(ts, item) === 'providers') : null;
|
|
55
|
+
const list = property && ts.isPropertyAssignment(property) && ts.isArrayLiteralExpression(property.initializer) ? property.initializer : null;
|
|
56
|
+
if (!list) report(call, `${rel}: Test.createTestingModule has no literal \`providers\` array; list the service and each constructor dependency there.`);
|
|
57
|
+
return list;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readProviders(ts, list, rel, nameOf, report) {
|
|
61
|
+
const provided = [];
|
|
62
|
+
let readable = true;
|
|
63
|
+
for (const element of list.elements) {
|
|
64
|
+
if (ts.isIdentifier(element)) { provided.push({ name: nameOf(element), node: element, isClass: true }); continue; }
|
|
65
|
+
if (ts.isObjectLiteralExpression(element)) {
|
|
66
|
+
const keys = element.properties.map(item => literalKey(ts, item));
|
|
67
|
+
const provide = element.properties.find(item => literalKey(ts, item) === 'provide');
|
|
68
|
+
const value = provide && ts.isPropertyAssignment(provide) ? provide.initializer : null;
|
|
69
|
+
if (keys.length === 2 && keys.includes('useValue') && value && ts.isIdentifier(value)) { provided.push({ name: nameOf(value), node: element, isClass: false }); continue; }
|
|
70
|
+
}
|
|
71
|
+
readable = false;
|
|
72
|
+
report(element, `${rel}: a provider is a class or \`{ provide: TOKEN, useValue: double }\` and nothing else (no useClass, useFactory, useExisting, spread or computed token).`);
|
|
73
|
+
}
|
|
74
|
+
return { provided, readable };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serviceClassFor(ts, rel, serviceRel, serviceFile, imports, list, report) {
|
|
78
|
+
const first = list.elements[0];
|
|
79
|
+
const serviceName = first && ts.isIdentifier(first) ? imports.get(first.text) : null;
|
|
80
|
+
const target = serviceName ? path.posix.normalize(path.posix.join(path.posix.dirname(rel), serviceName.module)).replace(/\.[cm]?[jt]s$/u, '') : null;
|
|
81
|
+
const serviceClass = serviceName && target === serviceRel.replace(/\.ts$/u, '')
|
|
82
|
+
? serviceFile.sourceFile.statements.find(statement => ts.isClassDeclaration(statement) && statement.name?.text === serviceName.name) : null;
|
|
83
|
+
if (!serviceClass) report(first ?? list, `${rel}: the first provider is the service under test, imported from ./${path.posix.basename(serviceRel, '.ts')}.`);
|
|
84
|
+
return serviceClass ? { serviceClass, serviceName } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function checkDependencies(kit, rel, service, serviceFile, list, provided, report) {
|
|
88
|
+
const { serviceName, serviceClass } = service;
|
|
89
|
+
const dependencies = constructorDependencies(kit, serviceFile, serviceClass);
|
|
90
|
+
const expected = [];
|
|
91
|
+
for (const dependency of dependencies) {
|
|
92
|
+
if (dependency.kind === 'unresolved') {
|
|
93
|
+
report(list, `${rel}: a constructor dependency of ${serviceName.name} cannot be resolved to a token (${dependency.reason}), so its provider cannot be checked; inject it through an exported Inject<Thing>() decorator or a class type.`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
expected.push(dependency.name);
|
|
97
|
+
}
|
|
98
|
+
const remaining = [...expected];
|
|
99
|
+
for (const item of provided.slice(1)) {
|
|
100
|
+
const index = remaining.indexOf(item.name);
|
|
101
|
+
if (index === -1) report(item.node, `${rel}: ${item.name} is provided but is not a constructor dependency of ${serviceName.name}; a unit spec provides exactly what the constructor needs.`);
|
|
102
|
+
else remaining.splice(index, 1);
|
|
103
|
+
}
|
|
104
|
+
for (const name of remaining) report(list, `${rel}: ${name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${name}, useValue: double }\` (or the class itself) to the providers.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkTestingModule({ ts, kit, rel, serviceRel, serviceFile, imports, call, report }) {
|
|
108
|
+
const list = providerList(ts, call, rel, report);
|
|
109
|
+
if (!list) return;
|
|
110
|
+
const nameOf = identifier => imports.get(identifier.text)?.name ?? identifier.text;
|
|
111
|
+
const { provided, readable } = readProviders(ts, list, rel, nameOf, report);
|
|
112
|
+
if (!readable) return;
|
|
113
|
+
const service = serviceClassFor(ts, rel, serviceRel, serviceFile, imports, list, report);
|
|
114
|
+
if (!service) return;
|
|
115
|
+
checkDependencies(kit, rel, service, serviceFile, list, provided, report);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function checkServiceSpec({ config, graph, kit, ts, rel, violations }) {
|
|
119
|
+
const abs = path.join(config.root, ...rel.split('/'));
|
|
120
|
+
if (!fs.existsSync(abs)) return false;
|
|
121
|
+
const sourceFile = ts.createSourceFile(abs, fs.readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
122
|
+
const report = (node, message) => violations.push({ ruleId: RULE, path: rel, ...kit.at(rel, sourceFile, node), message });
|
|
123
|
+
const imports = importsOf(ts, sourceFile);
|
|
124
|
+
const serviceRel = rel.replace(/\.spec\.ts$/u, '.ts');
|
|
125
|
+
const serviceFile = graph.files.get(serviceRel);
|
|
126
|
+
if (!serviceFile) { report(sourceFile, `${rel} has no ${path.posix.basename(serviceRel)} beside it; a service spec sits next to the service it tests.`); return true; }
|
|
127
|
+
const modules = testingModules(ts, sourceFile);
|
|
128
|
+
if (!modules.length) { report(sourceFile, `${rel} never calls Test.createTestingModule({ providers: [...] }); build the service from a testing module holding exactly its constructor dependencies.`); return true; }
|
|
129
|
+
for (const call of modules) checkTestingModule({ ts, kit, rel, serviceRel, serviceFile, imports, call, report });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
52
133
|
export function checkUnitSpecProviders(input) {
|
|
53
134
|
const { config, graph } = input;
|
|
54
135
|
const kit = machineKit(input);
|
|
@@ -56,67 +137,8 @@ export function checkUnitSpecProviders(input) {
|
|
|
56
137
|
const violations = [];
|
|
57
138
|
let specs = 0;
|
|
58
139
|
const tree = treeOf(config.root);
|
|
59
|
-
for (const rel of [...tree.files].filter(file => SPEC_ROOT.test(file) &&
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(abs)) continue;
|
|
62
|
-
specs += 1;
|
|
63
|
-
const sourceFile = ts.createSourceFile(abs, fs.readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
64
|
-
const report = (node, message) => violations.push({ ruleId: RULE, path: rel, ...kit.at(rel, sourceFile, node), message });
|
|
65
|
-
const imports = importsOf(ts, sourceFile);
|
|
66
|
-
const nameOf = identifier => imports.get(identifier.text)?.name ?? identifier.text;
|
|
67
|
-
|
|
68
|
-
const serviceRel = rel.replace(/\.spec\.ts$/u, '.ts');
|
|
69
|
-
const serviceFile = graph.files.get(serviceRel);
|
|
70
|
-
if (!serviceFile) { report(sourceFile, `${rel} has no ${path.posix.basename(serviceRel)} beside it; a service spec sits next to the service it tests.`); continue; }
|
|
71
|
-
|
|
72
|
-
const modules = testingModules(ts, sourceFile);
|
|
73
|
-
if (!modules.length) { report(sourceFile, `${rel} never calls Test.createTestingModule({ providers: [...] }); build the service from a testing module holding exactly its constructor dependencies.`); continue; }
|
|
74
|
-
|
|
75
|
-
for (const call of modules) {
|
|
76
|
-
const options = call.arguments[0];
|
|
77
|
-
const property = options && ts.isObjectLiteralExpression(options) ? options.properties.find(item => literalKey(ts, item) === 'providers') : null;
|
|
78
|
-
const list = property && ts.isPropertyAssignment(property) && ts.isArrayLiteralExpression(property.initializer) ? property.initializer : null;
|
|
79
|
-
if (!list) { report(call, `${rel}: Test.createTestingModule has no literal \`providers\` array; list the service and each constructor dependency there.`); continue; }
|
|
80
|
-
|
|
81
|
-
const provided = []; // [{name, node}]
|
|
82
|
-
let readable = true;
|
|
83
|
-
for (const element of list.elements) {
|
|
84
|
-
if (ts.isIdentifier(element)) { provided.push({ name: nameOf(element), node: element, isClass: true }); continue; }
|
|
85
|
-
if (ts.isObjectLiteralExpression(element)) {
|
|
86
|
-
const keys = element.properties.map(item => literalKey(ts, item));
|
|
87
|
-
const provide = element.properties.find(item => literalKey(ts, item) === 'provide');
|
|
88
|
-
const value = provide && ts.isPropertyAssignment(provide) ? provide.initializer : null;
|
|
89
|
-
if (keys.length === 2 && keys.includes('useValue') && value && ts.isIdentifier(value)) { provided.push({ name: nameOf(value), node: element, isClass: false }); continue; }
|
|
90
|
-
}
|
|
91
|
-
readable = false;
|
|
92
|
-
report(element, `${rel}: a provider is a class or \`{ provide: TOKEN, useValue: double }\` and nothing else (no useClass, useFactory, useExisting, spread or computed token).`);
|
|
93
|
-
}
|
|
94
|
-
if (!readable) continue;
|
|
95
|
-
|
|
96
|
-
const first = list.elements[0];
|
|
97
|
-
const serviceName = first && ts.isIdentifier(first) ? imports.get(first.text) : null;
|
|
98
|
-
const target = serviceName ? path.posix.normalize(path.posix.join(path.posix.dirname(rel), serviceName.module)).replace(/\.[cm]?[jt]s$/u, '') : null;
|
|
99
|
-
const serviceClass = serviceName && target === serviceRel.replace(/\.ts$/u, '')
|
|
100
|
-
? serviceFile.sourceFile.statements.find(statement => ts.isClassDeclaration(statement) && statement.name?.text === serviceName.name) : null;
|
|
101
|
-
if (!serviceClass) { report(first ?? list, `${rel}: the first provider is the service under test, imported from ./${path.posix.basename(serviceRel, '.ts')}.`); continue; }
|
|
102
|
-
|
|
103
|
-
const dependencies = constructorDependencies(kit, serviceFile, serviceClass);
|
|
104
|
-
const expected = [];
|
|
105
|
-
for (const dependency of dependencies) {
|
|
106
|
-
if (dependency.kind === 'unresolved') {
|
|
107
|
-
report(list, `${rel}: a constructor dependency of ${serviceName.name} cannot be resolved to a token (${dependency.reason}), so its provider cannot be checked; inject it through an exported Inject<Thing>() decorator or a class type.`);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
expected.push(dependency.name);
|
|
111
|
-
}
|
|
112
|
-
const remaining = [...expected];
|
|
113
|
-
for (const item of provided.slice(1)) {
|
|
114
|
-
const index = remaining.indexOf(item.name);
|
|
115
|
-
if (index === -1) report(item.node, `${rel}: ${item.name} is provided but is not a constructor dependency of ${serviceName.name}; a unit spec provides exactly what the constructor needs.`);
|
|
116
|
-
else remaining.splice(index, 1);
|
|
117
|
-
}
|
|
118
|
-
for (const name of remaining) report(list, `${rel}: ${name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${name}, useValue: double }\` (or the class itself) to the providers.`);
|
|
119
|
-
}
|
|
140
|
+
for (const rel of [...tree.files].filter(file => SPEC_ROOT.test(file) && file.endsWith('.service.spec.ts')).sort(byCodeUnit)) {
|
|
141
|
+
if (checkServiceSpec({ config, graph, kit, ts, rel, violations })) specs += 1;
|
|
120
142
|
}
|
|
121
143
|
return { violations, coverage: { status: 'checked', specs, files: graph.files.size } };
|
|
122
144
|
}
|
|
@@ -3,17 +3,88 @@
|
|
|
3
3
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
4
4
|
import { APP_KIND, connectionShapeProblems, NAME, RUNTIME_KIND, SLOT_ID } from './manifest-shape.mjs';
|
|
5
5
|
import { kindShapeProblems, patternShapeProblems } from './declaration-slots.mjs';
|
|
6
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
/** Shape problems of the optional hfs.json `supabase` block whose auth posture DB_CONFIG_POLICY checks. */
|
|
9
|
+
const isSupabaseStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string' && entry.trim());
|
|
10
|
+
|
|
11
|
+
function supabaseValueProblems(block) {
|
|
12
|
+
const bad = [];
|
|
13
|
+
if (block.enableSignup !== undefined && typeof block.enableSignup !== 'boolean') bad.push('supabase.enableSignup must be true or false');
|
|
14
|
+
if (block.jwtExpiry !== undefined && !(Number.isInteger(block.jwtExpiry) && block.jwtExpiry > 0 && block.jwtExpiry <= 3600)) bad.push('supabase.jwtExpiry must be an integer of seconds, 3600 at most');
|
|
15
|
+
if (block.siteUrl !== undefined && (typeof block.siteUrl !== 'string' || !block.siteUrl.trim())) bad.push('supabase.siteUrl must be a URL string');
|
|
16
|
+
for (const key of ['redirectUrls', 'forceRls']) if (block[key] !== undefined && !isSupabaseStringList(block[key])) bad.push(`supabase.${key} must be a list of strings`);
|
|
17
|
+
return bad;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
function supabaseBlockProblems(block) {
|
|
9
21
|
if (block === undefined) return [];
|
|
10
22
|
if (!isPlainObject(block)) return ['supabase must be an object'];
|
|
11
23
|
const bad = [];
|
|
12
24
|
for (const key of Object.keys(block)) if (!['enableSignup', 'jwtExpiry', 'siteUrl', 'redirectUrls', 'forceRls'].includes(key)) bad.push(`supabase has unknown key ${key}`);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
bad.push(...supabaseValueProblems(block));
|
|
26
|
+
return bad;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function appShapeProblems(apps, at, side, names) {
|
|
30
|
+
const bad = [];
|
|
31
|
+
if (!Array.isArray(apps) || !apps.length) {
|
|
32
|
+
bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
|
|
33
|
+
return bad;
|
|
34
|
+
}
|
|
35
|
+
apps.forEach((app, index) => {
|
|
36
|
+
if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
37
|
+
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
38
|
+
else names.set(app.name, side);
|
|
39
|
+
});
|
|
40
|
+
return bad;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function connectionProblems(side, value) {
|
|
44
|
+
const bad = [];
|
|
45
|
+
if (side !== 'be') {
|
|
46
|
+
bad.push('connections belong to the be side');
|
|
47
|
+
return bad;
|
|
48
|
+
}
|
|
49
|
+
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
50
|
+
const shape = connectionShapeProblems(list, value.apps);
|
|
51
|
+
if (shape.length) {
|
|
52
|
+
bad.push(...shape);
|
|
53
|
+
return bad;
|
|
54
|
+
}
|
|
55
|
+
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
56
|
+
for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
57
|
+
return bad;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
|
|
61
|
+
const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
|
|
62
|
+
|
|
63
|
+
function declarationSideProblems(side, value, names) {
|
|
64
|
+
const bad = [];
|
|
65
|
+
const at = `sides.${side}`;
|
|
66
|
+
if (!isPlainObject(value)) return [`${at} must be an object`];
|
|
67
|
+
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
68
|
+
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
69
|
+
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
70
|
+
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
71
|
+
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
72
|
+
if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
|
|
73
|
+
return bad;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser']);
|
|
77
|
+
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
78
|
+
|
|
79
|
+
function productDeclarationProblems(declaration, profiles) {
|
|
80
|
+
const bad = [];
|
|
81
|
+
for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
|
|
82
|
+
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
83
|
+
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
84
|
+
if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
85
|
+
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
86
|
+
const names = new Map();
|
|
87
|
+
for (const side of profiles) bad.push(...declarationSideProblems(side, declaration.sides[side], names));
|
|
17
88
|
return bad;
|
|
18
89
|
}
|
|
19
90
|
|
|
@@ -24,39 +95,9 @@ export function declarationShapeProblems(declaration, profiles) {
|
|
|
24
95
|
if (!(Number.isInteger(declaration.hfs) && declaration.hfs >= 1)) bad.push('hfs must be the pinned manifest major (an integer, 1 or more)');
|
|
25
96
|
if (!NAME.test(String(declaration.project))) bad.push('project must be a project name');
|
|
26
97
|
if (declaration.kind === RUNTIME_KIND) {
|
|
27
|
-
for (const key of Object.keys(declaration)) if (!
|
|
98
|
+
for (const key of Object.keys(declaration)) if (!RUNTIME_DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key} (a runtime declaration is {hfs, kind, project})`);
|
|
28
99
|
return bad;
|
|
29
100
|
}
|
|
30
|
-
|
|
31
|
-
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
32
|
-
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
33
|
-
if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
34
|
-
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort().join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
35
|
-
const names = new Map();
|
|
36
|
-
for (const side of profiles) {
|
|
37
|
-
const value = declaration.sides[side];
|
|
38
|
-
const at = `sides.${side}`;
|
|
39
|
-
if (!isPlainObject(value)) { bad.push(`${at} must be an object`); continue; }
|
|
40
|
-
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
41
|
-
if (!Array.isArray(value.apps) || !value.apps.length) bad.push(`${at}.apps must list every ${side}/apps/<name> with its kind`);
|
|
42
|
-
else value.apps.forEach((app, index) => {
|
|
43
|
-
if (!isPlainObject(app) || !NAME.test(String(app.name)) || !NAME.test(String(app.kind)) || Object.keys(app).some((key) => key !== 'name' && key !== 'kind')) bad.push(`${at}.apps[${index}] must be {name, kind}`);
|
|
44
|
-
else if (names.has(app.name)) bad.push(`app ${app.name} is declared twice (${names.get(app.name)} and ${side})`);
|
|
45
|
-
else names.set(app.name, side);
|
|
46
|
-
});
|
|
47
|
-
if (value.optionalSlots !== undefined && (!Array.isArray(value.optionalSlots) || !value.optionalSlots.every((slot) => SLOT_ID.test(String(slot))) || new Set(value.optionalSlots).size !== value.optionalSlots.length)) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
48
|
-
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
49
|
-
if (value.reads !== undefined && (!Array.isArray(value.reads) || !value.reads.every((read) => typeof read === 'string' && read.length > 0) || new Set(value.reads).size !== value.reads.length)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
50
|
-
if (value.connections !== undefined) {
|
|
51
|
-
if (side !== 'be') { bad.push('connections belong to the be side'); continue; }
|
|
52
|
-
const list = Array.isArray(value.connections) ? value.connections : null;
|
|
53
|
-
const shape = connectionShapeProblems(list, value.apps);
|
|
54
|
-
if (shape.length) bad.push(...shape);
|
|
55
|
-
else {
|
|
56
|
-
if (new Set(list.map((connection) => connection.name)).size !== list.length) bad.push('connections names must be unique');
|
|
57
|
-
for (const a of list) for (const b of list) if (a !== b && `${b.envPrefix}_`.startsWith(`${a.envPrefix}_`)) bad.push(`connections ${a.name} and ${b.name} share env keys (${a.envPrefix}_ covers ${b.envPrefix}_)`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
101
|
+
bad.push(...productDeclarationProblems(declaration, profiles));
|
|
61
102
|
return bad;
|
|
62
103
|
}
|
|
@@ -8,7 +8,7 @@ export function optionalSlotProblems(manifest, side, s) {
|
|
|
8
8
|
const bad = [];
|
|
9
9
|
for (const id of s.optionalSlots ?? []) {
|
|
10
10
|
const slot = manifest.slots.find((candidate) => candidate.id === id);
|
|
11
|
-
if (!slot
|
|
11
|
+
if (!slot?.profiles.includes(side)) bad.push(`sides.${side}.optionalSlots names ${id}, which is not a ${side} slot`);
|
|
12
12
|
else if (slot.presence !== 'opt-in') bad.push(`sides.${side}.optionalSlots names ${id}, which is ${slot.presence}, not opt-in`);
|
|
13
13
|
else if (slot.appKind !== undefined) bad.push(`sides.${side}.optionalSlots names ${id}; an app of kind ${slot.appKind} enables it`);
|
|
14
14
|
}
|
|
@@ -20,8 +20,10 @@ export function effectiveSlot(slot, profile, edition) {
|
|
|
20
20
|
if (edition !== 'lite') return slot;
|
|
21
21
|
const { lite, litePresence, ...base } = slot;
|
|
22
22
|
const presence = litePresenceOf(slot, profile);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
let tracked = slot.tracked;
|
|
24
|
+
if (presence === 'forbidden') tracked = 'external';
|
|
25
|
+
else if (tracked === 'external') tracked = 'tracked';
|
|
26
|
+
return { ...base, ...lite, presence, tracked, tests: 'none' };
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** The template group starci app sync renders `slot` from under `edition`: `liteManagedBy` (else `managedBy`) in lite, `managedBy` otherwise; undefined when unmanaged. */
|
|
@@ -61,6 +63,6 @@ export function enforcerJudgedInEdition(rules, kind, id, edition = 'full') {
|
|
|
61
63
|
export function editionRuleParams(params, edition) {
|
|
62
64
|
const { lite, ...base } = params;
|
|
63
65
|
const merged = edition === 'lite' && lite ? { ...base, ...lite } : base;
|
|
64
|
-
const deepFreeze = (value) => { if (value && typeof value === 'object') Object.values(value).forEach(deepFreeze); return Object.freeze(value); };
|
|
66
|
+
const deepFreeze = (value) => { if (value && typeof value === 'object') { Object.values(value).forEach(deepFreeze); } return Object.freeze(value); };
|
|
65
67
|
return deepFreeze(structuredClone(merged));
|
|
66
68
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Linear-time text trimming and `<placeholder>` replacement for the HFS path and prose checks.
|
|
2
|
+
|
|
3
|
+
/** `text` without its trailing characters that are in `chars`. */
|
|
4
|
+
export function trimTrailingChars(text, chars) {
|
|
5
|
+
let last = text.length - 1;
|
|
6
|
+
while (last >= 0 && chars.includes(text[last])) last -= 1;
|
|
7
|
+
return text.slice(0, last + 1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `text` with every `<name>` placeholder (a `<`, one or more characters other than `>`, a `>`) replaced by `replacement`. */
|
|
11
|
+
export function replacePlaceholders(text, replacement) {
|
|
12
|
+
let out = '';
|
|
13
|
+
let from = 0;
|
|
14
|
+
let open = text.indexOf('<');
|
|
15
|
+
while (open >= 0) {
|
|
16
|
+
const close = text.indexOf('>', open + 1);
|
|
17
|
+
if (close < 0) break;
|
|
18
|
+
if (close === open + 1) {
|
|
19
|
+
open = text.indexOf('<', close);
|
|
20
|
+
} else {
|
|
21
|
+
out += text.slice(from, open) + replacement;
|
|
22
|
+
from = close + 1;
|
|
23
|
+
open = text.indexOf('<', from);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out + text.slice(from);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether `text` holds a `<name>` placeholder. */
|
|
30
|
+
export const hasPlaceholder = (text) => replacePlaceholders(text, '') !== text;
|