@starci/hfs 4.0.10 → 4.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 +1 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +3 -2
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +221 -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/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/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +98 -65
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +92 -256
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- 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 +158 -102
- 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 +100 -68
- 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/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- 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/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +59 -48
- 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 +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- 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/event-contract.mjs +17 -13
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- 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/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- 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/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { machineKit } from './machine-ast.mjs';
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* R56 `hooks-are-hooks` (FE_HOOKS_ARE_HOOKS), the repository half of the eslint rule `hooks-folder-holds-hooks-only`, which
|
|
@@ -19,16 +20,9 @@ const HOOKS_SLOT = 'fe.hooks';
|
|
|
19
20
|
const HOOK_NAME = /^use[A-Z0-9]/u;
|
|
20
21
|
const SHARED = /\.shared\.[cm]?tsx?$/u;
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const { config, graph } = input;
|
|
24
|
-
const kit = machineKit(input);
|
|
25
|
-
const { ts, resolver } = kit;
|
|
26
|
-
const violations = [];
|
|
27
|
-
const domains = new Set();
|
|
28
|
-
|
|
29
|
-
const tree = treeOf(config.root);
|
|
23
|
+
const sharedFilesOf = (tree, resolver, domains) => {
|
|
30
24
|
const shared = new Map(); // domain root -> [file]
|
|
31
|
-
for (const rel of [...tree.files].sort()) {
|
|
25
|
+
for (const rel of [...tree.files].sort(byCodeUnit)) {
|
|
32
26
|
const classified = resolver.classifyPath(rel);
|
|
33
27
|
if (classified.slot !== HOOKS_SLOT || !classified.root) continue;
|
|
34
28
|
domains.add(classified.root);
|
|
@@ -37,6 +31,10 @@ export function checkHooksAreHooks(input) {
|
|
|
37
31
|
if (!shared.has(classified.root)) shared.set(classified.root, []);
|
|
38
32
|
shared.get(classified.root).push({ rel, base });
|
|
39
33
|
}
|
|
34
|
+
return shared;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const reportSharedFileViolations = (shared, violations) => {
|
|
40
38
|
for (const [root, list] of shared) {
|
|
41
39
|
const domain = path.posix.basename(root);
|
|
42
40
|
const at = (item, message) => violations.push({ ruleId: RULE, path: item.rel, line: 1, column: 1, domain, message });
|
|
@@ -49,40 +47,62 @@ export function checkHooksAreHooks(input) {
|
|
|
49
47
|
at(item, `${item.rel} is a second shared file of hooks domain ${domain}; a domain has exactly one, ${domain}.shared.ts. Merge the helpers into it.`);
|
|
50
48
|
}
|
|
51
49
|
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const helperDeclarationsOf = (statement, ts) => {
|
|
53
|
+
const found = [];
|
|
54
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) found.push([statement.name.text, statement.name]);
|
|
55
|
+
if (ts.isVariableStatement(statement)) {
|
|
56
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
57
|
+
const init = declaration.initializer;
|
|
58
|
+
if (ts.isIdentifier(declaration.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) found.push([declaration.name.text, declaration.name]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
62
|
+
};
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
const recordDeclaredHelper = (declared, root, name, file, node) => {
|
|
65
|
+
if (HOOK_NAME.test(name)) return;
|
|
66
|
+
if (!declared.has(root)) declared.set(root, new Map());
|
|
67
|
+
const byName = declared.get(root);
|
|
68
|
+
if (!byName.has(name)) byName.set(name, []);
|
|
69
|
+
byName.get(name).push({ file, node });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const declaredHelpersOf = (graph, resolver, ts) => {
|
|
54
73
|
const declared = new Map(); // domain root -> name -> [{file, node}]
|
|
55
74
|
for (const file of graph.files.values()) {
|
|
56
75
|
if (file.slot !== HOOKS_SLOT) continue;
|
|
57
76
|
const root = resolver.classifyPath(file.rel).root;
|
|
58
77
|
for (const statement of file.sourceFile.statements) {
|
|
59
|
-
const
|
|
60
|
-
if (ts.isFunctionDeclaration(statement) && statement.name) found.push([statement.name.text, statement.name]);
|
|
61
|
-
if (ts.isVariableStatement(statement)) {
|
|
62
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
63
|
-
const init = declaration.initializer;
|
|
64
|
-
if (ts.isIdentifier(declaration.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) found.push([declaration.name.text, declaration.name]);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (const [name, node] of found) {
|
|
68
|
-
if (HOOK_NAME.test(name)) continue;
|
|
69
|
-
if (!declared.has(root)) declared.set(root, new Map());
|
|
70
|
-
const byName = declared.get(root);
|
|
71
|
-
if (!byName.has(name)) byName.set(name, []);
|
|
72
|
-
byName.get(name).push({ file, node });
|
|
73
|
-
}
|
|
78
|
+
for (const [name, node] of helperDeclarationsOf(statement, ts)) recordDeclaredHelper(declared, root, name, file, node);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
81
|
+
return declared;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const reportDuplicateHelpers = (declared, violations, kit) => {
|
|
76
85
|
for (const [root, byName] of declared) {
|
|
77
86
|
const domain = path.posix.basename(root);
|
|
78
87
|
for (const [name, list] of byName) {
|
|
79
88
|
if (list.length < 2) continue;
|
|
80
89
|
for (const item of list.slice(1)) {
|
|
81
|
-
const others = list.map(entry => entry.file.rel).filter(rel => rel !== item.file.rel).sort();
|
|
90
|
+
const others = list.map(entry => entry.file.rel).filter(rel => rel !== item.file.rel).sort(byCodeUnit);
|
|
82
91
|
violations.push({ ruleId: RULE, path: item.file.rel, ...kit.at(item.file.rel, item.file.sourceFile, item.node), domain, helper: name,
|
|
83
92
|
message: `${name} is declared again in hooks domain ${domain} (also in ${others.join(', ')}); a helper the hooks share is written once, in ${domain}.shared.ts.` });
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function checkHooksAreHooks(input) {
|
|
99
|
+
const { config, graph } = input;
|
|
100
|
+
const kit = machineKit(input);
|
|
101
|
+
const { ts, resolver } = kit;
|
|
102
|
+
const violations = [];
|
|
103
|
+
const domains = new Set();
|
|
104
|
+
const shared = sharedFilesOf(treeOf(config.root), resolver, domains);
|
|
105
|
+
reportSharedFileViolations(shared, violations);
|
|
106
|
+
reportDuplicateHelpers(declaredHelpersOf(graph, resolver, ts), violations, kit);
|
|
87
107
|
return { violations, coverage: { status: 'checked', domains: domains.size } };
|
|
88
108
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* R106 `i18n-keys` (FE_I18N_KEYS). The catalogs of an app (`apps/<app>/src/modules/i18n/messages/<locale>.json`, slot fe.modules.i18n) and
|
|
@@ -40,15 +41,8 @@ const held = (leaves, full) => leaves.has(full) || [...leaves].some(leaf => leaf
|
|
|
40
41
|
const tailMatches = (pattern, leafSegments) => pattern.some(segment => segment !== '*' && segment !== '') && pattern.length <= leafSegments.length
|
|
41
42
|
&& pattern.every((segment, index) => segment === '*' || segment === leafSegments[leafSegments.length - pattern.length + index]);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const violations = [];
|
|
46
|
-
const report = (file, node, message, extra = {}) => {
|
|
47
|
-
const at = file.sourceFile.getLineAndCharacterOfPosition(node.getStart(file.sourceFile));
|
|
48
|
-
violations.push({ ruleId: RULE, path: file.rel, line: at.line + 1, column: at.character + 1, message, ...extra });
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const catalogsByApp = new Map();
|
|
44
|
+
function catalogsPerApp(config, graph) {
|
|
45
|
+
const catalogs = new Map();
|
|
52
46
|
const seen = new Set();
|
|
53
47
|
for (const file of graph.files.values()) {
|
|
54
48
|
const app = APP_SOURCE.exec(file.rel)?.[1];
|
|
@@ -56,9 +50,113 @@ export function checkI18nKeys({ config, graph, context }) {
|
|
|
56
50
|
seen.add(app);
|
|
57
51
|
const dir = `apps/${app}/src/modules/i18n/messages/`;
|
|
58
52
|
let names = [];
|
|
59
|
-
try { names = fs.readdirSync(path.join(config.root, dir)).filter(name => name.endsWith('.json')).sort(); } catch { /* the app has no catalog directory: FE_I18N_PLACEMENT's finding */ }
|
|
60
|
-
if (names.length)
|
|
53
|
+
try { names = fs.readdirSync(path.join(config.root, dir)).filter(name => name.endsWith('.json')).sort(byCodeUnit); } catch { /* the app has no catalog directory: FE_I18N_PLACEMENT's finding */ }
|
|
54
|
+
if (names.length) catalogs.set(dir, names.map(name => ({ rel: `${dir}${name}`, locale: name.slice(0, -5) })));
|
|
55
|
+
}
|
|
56
|
+
return catalogs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function patternsOf(ts, files) {
|
|
60
|
+
const patterns = [];
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const visit = node => {
|
|
63
|
+
if (ts.isStringLiteralLike(node)) patterns.push(node.text.split('.'));
|
|
64
|
+
else if (ts.isTemplateExpression(node)) patterns.push([node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').replaceAll('\u0000', '*').split('.'));
|
|
65
|
+
ts.forEachChild(node, visit);
|
|
66
|
+
};
|
|
67
|
+
visit(file.sourceFile);
|
|
68
|
+
}
|
|
69
|
+
return patterns;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function translationFactories(ts, file) {
|
|
73
|
+
const factories = new Set();
|
|
74
|
+
for (const statement of file.sourceFile.statements) {
|
|
75
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !MODULES.has(statement.moduleSpecifier.text)) continue;
|
|
76
|
+
const named = statement.importClause?.namedBindings;
|
|
77
|
+
if (!named || !ts.isNamedImports(named)) continue;
|
|
78
|
+
for (const element of named.elements) {
|
|
79
|
+
if (!element.isTypeOnly && FACTORIES.has((element.propertyName ?? element.name).text)) factories.add(element.name.text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return factories;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function namespaceForCall(ts, file, init, factories) {
|
|
86
|
+
let expression = init;
|
|
87
|
+
if (ts.isAwaitExpression(expression)) expression = expression.expression;
|
|
88
|
+
if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || !factories.has(expression.expression.text)) return null;
|
|
89
|
+
const [first] = expression.arguments;
|
|
90
|
+
if (first === undefined) return '';
|
|
91
|
+
if (ts.isStringLiteralLike(first)) return first.text;
|
|
92
|
+
if (!ts.isObjectLiteralExpression(first)) return null;
|
|
93
|
+
const property = first.properties.find(candidate => ts.isPropertyAssignment(candidate) && candidate.name.getText(file.sourceFile) === 'namespace');
|
|
94
|
+
if (!property) return '';
|
|
95
|
+
return ts.isStringLiteralLike(property.initializer) ? property.initializer.text : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function translatorNamespaces(ts, file, factories) {
|
|
99
|
+
const namespaces = new Map();
|
|
100
|
+
const collect = node => {
|
|
101
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
102
|
+
const namespace = namespaceForCall(ts, file, node.initializer, factories);
|
|
103
|
+
if (namespace !== null) namespaces.set(node, namespace);
|
|
104
|
+
}
|
|
105
|
+
ts.forEachChild(node, collect);
|
|
106
|
+
};
|
|
107
|
+
collect(file.sourceFile);
|
|
108
|
+
return namespaces;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function keysOf(ts, argument) {
|
|
112
|
+
if (ts.isConditionalExpression(argument)) return [...keysOf(ts, argument.whenTrue), ...keysOf(ts, argument.whenFalse)];
|
|
113
|
+
if (ts.isParenthesizedExpression(argument) || ts.isAsExpression(argument) || ts.isSatisfiesExpression(argument)) return keysOf(ts, argument.expression);
|
|
114
|
+
return ts.isStringLiteralLike(argument) ? [argument] : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reportCallKeys(reads, node) {
|
|
118
|
+
const { ts, file, checker, namespaces, loaded, app, report } = reads;
|
|
119
|
+
const callee = node.expression;
|
|
120
|
+
let owner = ts.isIdentifier(callee) ? callee : null;
|
|
121
|
+
if (ts.isPropertyAccessExpression(callee) && READERS.has(callee.name.text) && ts.isIdentifier(callee.expression)) owner = callee.expression;
|
|
122
|
+
const [first] = node.arguments;
|
|
123
|
+
const declaration = owner ? checker.getSymbolAtLocation(owner)?.valueDeclaration : null;
|
|
124
|
+
if (!owner || first === undefined || !namespaces.has(declaration)) return;
|
|
125
|
+
for (const literal of keysOf(ts, first)) {
|
|
126
|
+
const namespace = namespaces.get(declaration);
|
|
127
|
+
const full = namespace ? `${namespace}.${literal.text}` : literal.text;
|
|
128
|
+
const lacking = loaded.filter(catalog => !held(catalog.leaves, full)).map(catalog => catalog.locale);
|
|
129
|
+
if (lacking.length) report(file, literal, `${file.rel} reads "${full}" and ${lacking.join(', ')} ${lacking.length === 1 ? 'holds' : 'hold'} no such key; add it to every catalog of ${app} or read a key that exists`, { key: full, locales: lacking });
|
|
61
130
|
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reportTranslationReads(ts, file, checker, namespaces, loaded, app, report) {
|
|
134
|
+
const reads = { ts, file, checker, namespaces, loaded, app, report };
|
|
135
|
+
const read = node => {
|
|
136
|
+
if (ts.isCallExpression(node)) reportCallKeys(reads, node);
|
|
137
|
+
ts.forEachChild(node, read);
|
|
138
|
+
};
|
|
139
|
+
read(file.sourceFile);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function reportUnusedCatalogKeys(primary, loaded, patterns, app, violations) {
|
|
143
|
+
const every = new Set(loaded.flatMap(catalog => [...catalog.leaves]));
|
|
144
|
+
for (const leaf of [...every].sort(byCodeUnit)) {
|
|
145
|
+
const segments = leaf.split('.');
|
|
146
|
+
if (patterns.some(pattern => tailMatches(pattern, segments))) continue;
|
|
147
|
+
violations.push({ ruleId: RULE, path: primary.rel, line: 1, column: 1, message: `${primary.rel} holds the key "${leaf}" and no source of ${app} or the shared packages reads it; delete the key from every catalog`, key: leaf });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function checkI18nKeys({ config, graph, context }) {
|
|
152
|
+
const ts = context.ts;
|
|
153
|
+
const violations = [];
|
|
154
|
+
const report = (file, node, message, extra = {}) => {
|
|
155
|
+
const at = file.sourceFile.getLineAndCharacterOfPosition(node.getStart(file.sourceFile));
|
|
156
|
+
violations.push({ ruleId: RULE, path: file.rel, line: at.line + 1, column: at.character + 1, message, ...extra });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const catalogsByApp = catalogsPerApp(config, graph);
|
|
62
160
|
|
|
63
161
|
let apps = 0;
|
|
64
162
|
for (const [dir, catalogs] of catalogsByApp) {
|
|
@@ -68,79 +166,19 @@ export function checkI18nKeys({ config, graph, context }) {
|
|
|
68
166
|
apps += 1;
|
|
69
167
|
const own = [...graph.files.values()].filter(file => file.rel.startsWith(`apps/${app}/src/`));
|
|
70
168
|
const shared = [...graph.files.values()].filter(file => /^packages\/[^/]+\/src\//u.test(file.rel));
|
|
71
|
-
const patterns = [];
|
|
72
|
-
|
|
73
|
-
for (const file of [...own, ...shared]) {
|
|
74
|
-
const visit = node => {
|
|
75
|
-
if (ts.isStringLiteralLike(node)) patterns.push(node.text.split('.'));
|
|
76
|
-
else if (ts.isTemplateExpression(node)) patterns.push([node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').split('\u0000').join('*').split('.'));
|
|
77
|
-
ts.forEachChild(node, visit);
|
|
78
|
-
};
|
|
79
|
-
visit(file.sourceFile);
|
|
80
|
-
}
|
|
169
|
+
const patterns = patternsOf(ts, [...own, ...shared]);
|
|
81
170
|
|
|
82
171
|
for (const file of own) {
|
|
83
|
-
const factories =
|
|
84
|
-
for (const statement of file.sourceFile.statements) {
|
|
85
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !MODULES.has(statement.moduleSpecifier.text)) continue;
|
|
86
|
-
const named = statement.importClause?.namedBindings;
|
|
87
|
-
if (!named || !ts.isNamedImports(named)) continue;
|
|
88
|
-
for (const element of named.elements) if (!element.isTypeOnly && FACTORIES.has((element.propertyName ?? element.name).text)) factories.add(element.name.text);
|
|
89
|
-
}
|
|
172
|
+
const factories = translationFactories(ts, file);
|
|
90
173
|
if (!factories.size) continue;
|
|
91
174
|
const checker = context.checkerFor(file.sourceFile.fileName);
|
|
92
|
-
const namespaces =
|
|
93
|
-
const collect = node => {
|
|
94
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
95
|
-
let init = node.initializer;
|
|
96
|
-
if (ts.isAwaitExpression(init)) init = init.expression;
|
|
97
|
-
if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && factories.has(init.expression.text)) {
|
|
98
|
-
const [first] = init.arguments;
|
|
99
|
-
if (first === undefined) namespaces.set(node, '');
|
|
100
|
-
else if (ts.isStringLiteralLike(first)) namespaces.set(node, first.text);
|
|
101
|
-
else if (ts.isObjectLiteralExpression(first)) {
|
|
102
|
-
const property = first.properties.find(candidate => ts.isPropertyAssignment(candidate) && candidate.name.getText(file.sourceFile) === 'namespace');
|
|
103
|
-
if (!property) namespaces.set(node, '');
|
|
104
|
-
else if (ts.isStringLiteralLike(property.initializer)) namespaces.set(node, property.initializer.text);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
ts.forEachChild(node, collect);
|
|
109
|
-
};
|
|
110
|
-
collect(file.sourceFile);
|
|
175
|
+
const namespaces = translatorNamespaces(ts, file, factories);
|
|
111
176
|
if (!namespaces.size) continue;
|
|
112
|
-
|
|
113
|
-
if (ts.isConditionalExpression(argument)) return [...keysOf(argument.whenTrue), ...keysOf(argument.whenFalse)];
|
|
114
|
-
if (ts.isParenthesizedExpression(argument) || ts.isAsExpression(argument) || ts.isSatisfiesExpression(argument)) return keysOf(argument.expression);
|
|
115
|
-
return ts.isStringLiteralLike(argument) ? [argument] : [];
|
|
116
|
-
};
|
|
117
|
-
const read = node => {
|
|
118
|
-
if (ts.isCallExpression(node)) {
|
|
119
|
-
const callee = node.expression;
|
|
120
|
-
const owner = ts.isIdentifier(callee) ? callee : (ts.isPropertyAccessExpression(callee) && READERS.has(callee.name.text) && ts.isIdentifier(callee.expression) ? callee.expression : null);
|
|
121
|
-
const [first] = node.arguments;
|
|
122
|
-
const declaration = owner ? checker.getSymbolAtLocation(owner)?.valueDeclaration : null;
|
|
123
|
-
if (owner && first !== undefined && namespaces.has(declaration)) {
|
|
124
|
-
for (const literal of keysOf(first)) {
|
|
125
|
-
const namespace = namespaces.get(declaration);
|
|
126
|
-
const full = namespace ? `${namespace}.${literal.text}` : literal.text;
|
|
127
|
-
const lacking = loaded.filter(catalog => !held(catalog.leaves, full)).map(catalog => catalog.locale);
|
|
128
|
-
if (lacking.length) report(file, literal, `${file.rel} reads "${full}" and ${lacking.join(', ')} ${lacking.length === 1 ? 'holds' : 'hold'} no such key; add it to every catalog of ${app} or read a key that exists`, { key: full, locales: lacking });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
ts.forEachChild(node, read);
|
|
133
|
-
};
|
|
134
|
-
read(file.sourceFile);
|
|
177
|
+
reportTranslationReads(ts, file, checker, namespaces, loaded, app, report);
|
|
135
178
|
}
|
|
136
179
|
|
|
137
180
|
const [primary] = [...loaded].sort((a, b) => a.locale.localeCompare(b.locale));
|
|
138
|
-
|
|
139
|
-
for (const leaf of [...every].sort()) {
|
|
140
|
-
const segments = leaf.split('.');
|
|
141
|
-
if (patterns.some(pattern => tailMatches(pattern, segments))) continue;
|
|
142
|
-
violations.push({ ruleId: RULE, path: primary.rel, line: 1, column: 1, message: `${primary.rel} holds the key "${leaf}" and no source of ${app} or the shared packages reads it; delete the key from every catalog`, key: leaf });
|
|
143
|
-
}
|
|
181
|
+
reportUnusedCatalogKeys(primary, loaded, patterns, app, violations);
|
|
144
182
|
}
|
|
145
183
|
return { violations, coverage: { status: 'checked', apps } };
|
|
146
184
|
}
|