@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
|
@@ -25,7 +25,12 @@ export function testSecretPaths(name, { repo, stack = 'dev' } = {}) {
|
|
|
25
25
|
export const setCommand = (name = '<name>', stack = 'dev') => `node scripts/stack-secret.mjs set ${stack}/secrets/test/${name}`;
|
|
26
26
|
|
|
27
27
|
/** sops' format for a plaintext path, as the repository's stack-secret.mjs formatFor decides it. */
|
|
28
|
-
export const sopsFormatFor = (file) =>
|
|
28
|
+
export const sopsFormatFor = (file) => {
|
|
29
|
+
if (file.endsWith('.env')) return 'dotenv';
|
|
30
|
+
if (file.endsWith('.json')) return 'json';
|
|
31
|
+
if (/\.(ya?ml|kubeconfig)$/.test(file)) return 'yaml';
|
|
32
|
+
return 'binary';
|
|
33
|
+
};
|
|
29
34
|
|
|
30
35
|
/** Every leaf outside the `sops` block is an ENC[...] value (or null); the block itself carries a mac. */
|
|
31
36
|
const allEncrypted = (node) => {
|
|
@@ -48,7 +53,7 @@ export function isSopsEnvelope(text) {
|
|
|
48
53
|
if (!src.trim()) return false;
|
|
49
54
|
try { return sopsTree(JSON.parse(src)); } catch { /* not JSON */ }
|
|
50
55
|
const lines = src.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#/.test(l));
|
|
51
|
-
if (lines.length && lines.every((l) => /^[A-Za-z_]
|
|
56
|
+
if (lines.length && lines.every((l) => /^[A-Za-z_]\w*=/.test(l))) {
|
|
52
57
|
const pairs = lines.map((l) => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]);
|
|
53
58
|
const meta = pairs.filter(([k]) => k.startsWith('sops_'));
|
|
54
59
|
const vals = pairs.filter(([k]) => !k.startsWith('sops_'));
|
|
@@ -58,55 +63,97 @@ export function isSopsEnvelope(text) {
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/** Admit one flat age envelope for the native selected-identity invocation; this is shape admission, never cryptographic proof. */
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
const refusal = (reason) => ({ ok: false, reason });
|
|
67
|
+
|
|
68
|
+
function dotenvMetadataValue(meta, age, key, value) {
|
|
69
|
+
if (key === 'age__list_0__map_recipient') age.recipient = value;
|
|
70
|
+
else if (key === 'age__list_0__map_enc') age.enc = value;
|
|
71
|
+
else if (key === 'mac_only_encrypted') {
|
|
72
|
+
if (value !== 'false') return 'partial-mac';
|
|
73
|
+
meta[key] = false;
|
|
74
|
+
} else if (key.includes('__')) return 'unsupported-key-group';
|
|
75
|
+
else meta[key] = value;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readDotenvMetadata(text) {
|
|
80
|
+
const pairs = new Map();
|
|
81
|
+
for (const line of String(text).split('\n')) {
|
|
82
|
+
if (!line || line.startsWith('#')) continue;
|
|
83
|
+
if (line.includes('\r')) return { refusal: 'unsupported-dotenv' };
|
|
84
|
+
const at = line.indexOf('=');
|
|
85
|
+
if (at < 1) return { refusal: 'unsupported-dotenv' };
|
|
86
|
+
const name = line.slice(0, at);
|
|
87
|
+
if (pairs.has(name)) return { refusal: 'duplicate-key' };
|
|
88
|
+
pairs.set(name, line.slice(at + 1).replaceAll(String.raw`\n`, '\n'));
|
|
89
|
+
}
|
|
90
|
+
const meta = {};
|
|
91
|
+
const age = {};
|
|
92
|
+
for (const [name, value] of pairs) {
|
|
93
|
+
if (!name.startsWith('sops_')) continue;
|
|
94
|
+
const reason = dotenvMetadataValue(meta, age, name.slice('sops_'.length), value);
|
|
95
|
+
if (reason) return { refusal: reason };
|
|
96
|
+
}
|
|
97
|
+
meta.age = [age];
|
|
98
|
+
return { meta };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readStructuredMetadata(text, format) {
|
|
102
|
+
const doc = parseYaml(String(text));
|
|
103
|
+
if (format !== 'yaml') JSON.parse(String(text));
|
|
104
|
+
if (format === 'binary' && (!doc || Object.keys(doc).length !== 2 || !Object.hasOwn(doc, 'data') || !Object.hasOwn(doc, 'sops') || typeof doc.data !== 'string'))
|
|
105
|
+
return { refusal: 'unsupported-binary-shape' };
|
|
106
|
+
return { meta: doc?.sops };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readAgeMetadata(text, format) {
|
|
65
110
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
meta = doc?.sops;
|
|
96
|
-
}
|
|
97
|
-
} catch { return refuse('invalid-envelope'); }
|
|
98
|
-
if (!meta || typeof meta !== 'object' || Array.isArray(meta) || !isSopsEnvelope(text)) return refuse('invalid-envelope');
|
|
99
|
-
if (Object.hasOwn(meta, 'key_groups') || Object.hasOwn(meta, 'shamir_threshold')) return refuse('unsupported-key-group');
|
|
111
|
+
return format === 'dotenv' ? readDotenvMetadata(text) : readStructuredMetadata(text, format);
|
|
112
|
+
} catch { return { refusal: 'invalid-envelope' }; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasNonAgeProvider(meta, providers) {
|
|
116
|
+
for (const name of providers) {
|
|
117
|
+
if (Object.hasOwn(meta, name) && (!Array.isArray(meta[name]) || meta[name].length)) return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isAgeEntry(entry) {
|
|
123
|
+
return !!entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
124
|
+
&& !Object.keys(entry).some(name => !['recipient', 'enc'].includes(name));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasValidAgePayload(entry) {
|
|
128
|
+
return typeof entry.enc === 'string' && entry.enc.startsWith('-----BEGIN AGE ENCRYPTED FILE-----\n')
|
|
129
|
+
&& entry.enc.includes('\n-----END AGE ENCRYPTED FILE-----');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasValidEnvelopeVersion(meta) {
|
|
133
|
+
return typeof meta.version === 'string' && /^\d+\.\d+\.\d+$/.test(meta.version)
|
|
134
|
+
&& typeof meta.lastmodified === 'string' && Number.isFinite(Date.parse(meta.lastmodified));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateAgeMetadata(meta, text, recipient) {
|
|
138
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta) || !isSopsEnvelope(text)) return refusal('invalid-envelope');
|
|
139
|
+
if (Object.hasOwn(meta, 'key_groups') || Object.hasOwn(meta, 'shamir_threshold')) return refusal('unsupported-key-group');
|
|
100
140
|
const providers = ['kms', 'gcp_kms', 'hckms', 'azure_kv', 'hc_vault', 'pgp'];
|
|
101
|
-
|
|
141
|
+
if (hasNonAgeProvider(meta, providers)) return refusal('non-age-provider');
|
|
102
142
|
const fields = new Set([...providers, 'age', 'lastmodified', 'mac', 'version', 'unencrypted_suffix', 'encrypted_suffix', 'unencrypted_regex', 'encrypted_regex', 'unencrypted_comment_regex', 'encrypted_comment_regex', 'mac_only_encrypted']);
|
|
103
|
-
if (Object.keys(meta).some(name => !fields.has(name))) return
|
|
104
|
-
if (Object.hasOwn(meta, 'mac_only_encrypted') && meta.mac_only_encrypted !== false) return
|
|
105
|
-
if (!Array.isArray(meta.age) || meta.age.length !== 1) return
|
|
143
|
+
if (Object.keys(meta).some(name => !fields.has(name))) return refusal('unknown-metadata');
|
|
144
|
+
if (Object.hasOwn(meta, 'mac_only_encrypted') && meta.mac_only_encrypted !== false) return refusal('partial-mac');
|
|
145
|
+
if (!Array.isArray(meta.age) || meta.age.length !== 1) return refusal('unsupported-age-set');
|
|
106
146
|
const entry = meta.age[0];
|
|
107
|
-
if (!
|
|
108
|
-
if (typeof entry.recipient !== 'string' || entry.recipient !== recipient) return
|
|
109
|
-
if (
|
|
110
|
-
if (
|
|
147
|
+
if (!isAgeEntry(entry)) return refusal('invalid-age-entry');
|
|
148
|
+
if (typeof entry.recipient !== 'string' || entry.recipient !== recipient) return refusal('recipient-mismatch');
|
|
149
|
+
if (!hasValidAgePayload(entry)) return refusal('invalid-age-entry');
|
|
150
|
+
if (!hasValidEnvelopeVersion(meta)) return refusal('invalid-envelope');
|
|
111
151
|
return { ok: true };
|
|
112
152
|
}
|
|
153
|
+
|
|
154
|
+
export function selectedAgeEnvelope(text, format, recipient) {
|
|
155
|
+
if (!['binary', 'json', 'yaml', 'dotenv'].includes(format)) return refusal('unsupported-format');
|
|
156
|
+
const parsed = readAgeMetadata(text, format);
|
|
157
|
+
if (parsed.refusal) return refusal(parsed.refusal);
|
|
158
|
+
return validateAgeMetadata(parsed.meta, text, recipient);
|
|
159
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseYaml } from '../../engine/yaml.mjs';
|
|
4
4
|
import { words } from './dockerfile.mjs';
|
|
5
|
+
import { asList } from './list.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The services of a repository's own stack definition, read from `.starcistacks/application-stacks.yaml` and the compose
|
|
@@ -74,8 +75,6 @@ const imagePathOf = image => {
|
|
|
74
75
|
/** True when the image reference carries a tag or digest (an external image, not a placeholder of the repository's own build). */
|
|
75
76
|
const isTagged = image => image.includes('@') || image.slice(image.lastIndexOf('/') + 1).includes(':');
|
|
76
77
|
|
|
77
|
-
const asList = value => (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]);
|
|
78
|
-
|
|
79
78
|
/** A compose `environment` (map or `KEY=VALUE` list) as a plain object; a `*_FILE` variable becomes the plain variable with the test value. */
|
|
80
79
|
const environmentOf = value => {
|
|
81
80
|
const entries = Array.isArray(value)
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
// literal text, a node's 1-based position, a named property's initializer text. `ts` is the caller's
|
|
3
3
|
// already-resolved compiler (see package-at.mjs loadTypescript); nothing here resolves a package.
|
|
4
4
|
|
|
5
|
+
/** Whether the single-bit `flag` (a ts.NodeFlags member) is set in `flags`: the bit read as arithmetic, no bitwise operator in the condition. */
|
|
6
|
+
export const hasFlag = (flags, flag) => Math.floor(flags / flag) % 2 === 1;
|
|
7
|
+
|
|
8
|
+
/** Whether any bit of `mask` (a ts flag or an OR of flags) is set in `flags` (absent flags read as none): an explicit comparison of the masked value. */
|
|
9
|
+
export const hasAnyFlag = (flags, mask) => ((flags ?? 0) & mask) !== 0;
|
|
10
|
+
|
|
11
|
+
/** Whether the variable `declaration` belongs to a `const` list: the TypeScript node-flag test, an explicit comparison of the masked flags. */
|
|
12
|
+
export const isConstVariable = (ts, declaration) => (ts.getCombinedNodeFlags(declaration.parent) & ts.NodeFlags.Const) !== 0;
|
|
13
|
+
|
|
5
14
|
/** The text of an identifier or a string-literal-like `node`, or null (property names, literal specifiers). */
|
|
6
15
|
export const nameText = (ts, node) => (node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) ? node.text : null);
|
|
7
16
|
|
|
@@ -20,7 +20,12 @@ export function walkFiles(dir, {filter = () => true, sorted = false, exclude = (
|
|
|
20
20
|
if (depth > maxDepth) return;
|
|
21
21
|
let entries;
|
|
22
22
|
try { entries = fs.readdirSync(current, {withFileTypes: true}); }
|
|
23
|
-
catch (error) {
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (ignoreReadErrors) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
24
29
|
if (sorted) entries = entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
25
30
|
for (const entry of entries) {
|
|
26
31
|
const full = path.join(current, entry.name);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// yaml-cached.mjs - parse a YAML text once per distinct text in a process.
|
|
2
|
+
//
|
|
3
|
+
// The HFS slot manifest and rule catalog are large documents that one process asks for again at every check, every render and every fixture. Their
|
|
4
|
+
// bytes do not change between those asks, so the parse (the dominant cost of a load) is keyed by the text itself; each caller gets its own copy of the
|
|
5
|
+
// parsed document, so nothing a caller does to it reaches the next one. A changed file is a different text and parses again.
|
|
6
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
7
|
+
|
|
8
|
+
const parsed = new Map();
|
|
9
|
+
|
|
10
|
+
/** The parsed document of `text`: parsed on the first ask for this text, a structured copy afterwards. Throws what parseYaml throws, every time. */
|
|
11
|
+
export function parseYamlCached(text) {
|
|
12
|
+
if (!parsed.has(text)) parsed.set(text, parseYaml(text));
|
|
13
|
+
return structuredClone(parsed.get(text));
|
|
14
|
+
}
|
package/schema-authority.mjs
CHANGED
|
@@ -224,7 +224,7 @@ export const noRuntimeSchema = {
|
|
|
224
224
|
const isEntityDecorator = (checker, decorator) => {
|
|
225
225
|
const callee = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression
|
|
226
226
|
let symbol = checker.getSymbolAtLocation(callee)
|
|
227
|
-
if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol)
|
|
227
|
+
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol)
|
|
228
228
|
return symbol?.name === "Entity" && (symbol.declarations ?? []).some((declaration) => packageOfFile(declaration.getSourceFile().fileName) === "typeorm")
|
|
229
229
|
}
|
|
230
230
|
|
package/service-deps.mjs
CHANGED
|
@@ -59,7 +59,7 @@ const isClassToken = (context, annotation) => {
|
|
|
59
59
|
if (!tsNode) return false
|
|
60
60
|
const type = checker.getTypeFromTypeNode(tsNode)
|
|
61
61
|
const symbol = type.getSymbol()
|
|
62
|
-
if (!symbol ||
|
|
62
|
+
if (!symbol || (symbol.flags & ts.SymbolFlags.Class) === 0) return false
|
|
63
63
|
const declaration = symbol.declarations?.find((entry) => ts.isClassDeclaration(entry) || ts.isClassExpression(entry))
|
|
64
64
|
return Boolean(declaration)
|
|
65
65
|
}
|
package/sonar-parity.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The law that keeps the code clear of the smells SonarCloud measures (R235 `SCAN_SMELL`).
|
|
3
|
+
*
|
|
4
|
+
* Each rule is the syntax-tree form of one Sonar rule, shared with the front-end canon (scripts/lib/sonar-syntax-rules.mjs,
|
|
5
|
+
* bundled under `runtime/`): `no-await-in-loop` (S9382), `prefer-code-point` (S7758), `prefer-string-raw` (S7780),
|
|
6
|
+
* `no-nested-conditional` (S3358), `no-void-operator` (S3735), `no-unused-import` (S1128) and `prefer-export-from` (S7763).
|
|
7
|
+
* The three Sonar rules that need the type checker, S1874 (a deprecated API), S6551 (an object turned into text) and S7503 (an `async`
|
|
8
|
+
* function that never awaits and returns no promise), are the typescript-eslint rules `no-deprecated`, `no-base-to-string` and
|
|
9
|
+
* `require-await` the factory turns on.
|
|
10
|
+
*/
|
|
11
|
+
import { SONAR_SYNTAX_RULES } from "./runtime/scripts/lib/sonar-syntax-rules.mjs"
|
|
12
|
+
|
|
13
|
+
const NAMES = ["no-await-in-loop", "prefer-code-point", "prefer-string-raw", "no-nested-conditional", "no-void-operator", "no-unused-import", "prefer-export-from"]
|
|
14
|
+
|
|
15
|
+
/** The rules this law contributes to the plugin. */
|
|
16
|
+
export const rules = Object.fromEntries(NAMES.map((name) => [name, SONAR_SYNTAX_RULES[name]]))
|
|
17
|
+
|
|
18
|
+
/** Starts at error: a repository's fix lane clears the debt before the rule ships. */
|
|
19
|
+
export const recommended = Object.fromEntries(NAMES.map((name) => [`starci-be/${name}`, "error"]))
|
package/suppression.mjs
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* - TypeScript: `@ts-ignore`, `@ts-expect-error` and `@ts-nocheck`, wherever they sit in a comment;
|
|
10
10
|
* - coverage: `istanbul ignore`, `c8 ignore`, `v8 ignore`;
|
|
11
11
|
* - Sonar: `NOSONAR` and `sonar-disable`;
|
|
12
|
-
* - Prettier: `prettier-ignore
|
|
13
|
-
* - the `vn-ok` language marker.
|
|
12
|
+
* - Prettier: `prettier-ignore`.
|
|
14
13
|
*
|
|
15
14
|
* The factory also sets `noInlineConfig: true`, so ESLint itself ignores such a comment; this rule is what turns the
|
|
16
15
|
* ignored comment into a failure a person reads, instead of a silent no-op that looks like an exemption. There is no
|
|
@@ -24,7 +23,6 @@ const TS_DIRECTIVE = /@ts-(?:ignore|expect-error|nocheck)\b/
|
|
|
24
23
|
const COVERAGE_IGNORE = /(?:^|[\s*/])(?:istanbul|c8|v8)\s+ignore\b/
|
|
25
24
|
const SONAR = /\bNOSONAR\b|\bsonar-(?:disable|enable)\b/i
|
|
26
25
|
const PRETTIER_IGNORE = /(?:^|[\s*/])prettier-ignore\b/
|
|
27
|
-
const LANGUAGE_MARKER = /\bvn-ok\b/
|
|
28
26
|
|
|
29
27
|
/** The finding a comment is, or null when it is ordinary prose. `/* global x *\/` is a block-comment form only. */
|
|
30
28
|
const kindOf = (comment) => {
|
|
@@ -35,7 +33,6 @@ const kindOf = (comment) => {
|
|
|
35
33
|
if (COVERAGE_IGNORE.test(text)) return "coverage"
|
|
36
34
|
if (SONAR.test(text)) return "sonar"
|
|
37
35
|
if (PRETTIER_IGNORE.test(text)) return "prettier"
|
|
38
|
-
if (LANGUAGE_MARKER.test(text)) return "vn-ok"
|
|
39
36
|
return null
|
|
40
37
|
}
|
|
41
38
|
|
|
@@ -43,7 +40,7 @@ const kindOf = (comment) => {
|
|
|
43
40
|
export const noInlineSuppression = {
|
|
44
41
|
meta: {
|
|
45
42
|
type: "problem",
|
|
46
|
-
docs: { description: "No eslint, TypeScript, coverage, Sonar or Prettier suppression comment
|
|
43
|
+
docs: { description: "No eslint, TypeScript, coverage, Sonar or Prettier suppression comment." },
|
|
47
44
|
schema: [],
|
|
48
45
|
messages: {
|
|
49
46
|
eslint: "An `eslint-disable`/`eslint-enable`, `eslint-env`, `/* global */` or rule-configuration comment. HFS does not switch a rule off or declare a global in place: fix the code, or propose a change to the rule in the canon.",
|
|
@@ -51,7 +48,6 @@ export const noInlineSuppression = {
|
|
|
51
48
|
coverage: "An `istanbul`/`c8`/`v8 ignore` comment hides code from coverage. Write the test that covers it, or delete the code that nothing reaches.",
|
|
52
49
|
sonar: "A `NOSONAR` or `sonar-disable` comment hides a finding from Sonar. Fix the finding.",
|
|
53
50
|
prettier: "A `prettier-ignore` comment exempts code from the formatter. Let the formatter write it.",
|
|
54
|
-
"vn-ok": "A `vn-ok` marker exempts a line from the language rule. HFS keeps no exemption marker: write the text in English, or move the data to a locale or data file.",
|
|
55
51
|
},
|
|
56
52
|
},
|
|
57
53
|
create(context) {
|
package/test-world.mjs
CHANGED
|
@@ -145,8 +145,7 @@ const plainKey = (property) => {
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* The world an integration spec (`{ modules }`, with real peer apps beside them as `apps` when its client calls another app of
|
|
148
|
-
* ours) or an e2e spec (`{ apps }`, never `modules`) asks for; every such spec asks for one
|
|
149
|
-
* exists nowhere.
|
|
148
|
+
* ours) or an e2e spec (`{ apps }`, never `modules`) asks for; every such spec asks for one.
|
|
150
149
|
*/
|
|
151
150
|
export const testWorldShape = {
|
|
152
151
|
meta: {
|
|
@@ -158,21 +157,14 @@ export const testWorldShape = {
|
|
|
158
157
|
apps: "An e2e spec boots applications: call `useTestWorld({ apps: {...} })`, not `modules`.",
|
|
159
158
|
literal: "The options of `useTestWorld(...)` must be one object literal with plain keys: a variable, a spread or a computed key hides whether the spec asks for `modules` or `apps`.",
|
|
160
159
|
missing: "This spec never calls `useTestWorld(...)`: integration and e2e specs run inside the one test world.",
|
|
161
|
-
retired: "`useE2eWorld` is the retired name of `useTestWorld`; there is one name, no alias.",
|
|
162
160
|
},
|
|
163
161
|
},
|
|
164
162
|
create(context) {
|
|
165
163
|
const slot = hfsOf(context).slotOf(context.filename || context.getFilename())
|
|
166
164
|
const wants = slot === "be.tests.integration" ? "modules" : slot === "be.tests.e2e" ? "apps" : null
|
|
167
|
-
|
|
168
|
-
Identifier(node) {
|
|
169
|
-
if (node.name === "useE2eWorld") context.report({ node, messageId: "retired" })
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
if (!wants) return retired
|
|
165
|
+
if (!wants) return {}
|
|
173
166
|
let seen = false
|
|
174
167
|
return {
|
|
175
|
-
...retired,
|
|
176
168
|
CallExpression(node) {
|
|
177
169
|
if (node.callee.type !== "Identifier" || node.callee.name !== "useTestWorld") return
|
|
178
170
|
seen = true
|