@starci/hfs 4.0.9 → 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/LICENSE +21 -0
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +82 -8
- package/package.json +3 -2
- 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 +247 -273
- 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 +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- 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 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- 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/lib.mjs +6 -0
- 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 +36 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
- package/runtime/scripts/api/process/run-program.mjs +8 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -16
- package/runtime/scripts/api/sops/lib.mjs +309 -11
- package/runtime/scripts/api/sops/seal.mjs +8 -4
- 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 +3 -2
- 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 +90 -45
- 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/fs-kind.mjs +14 -4
- 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/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- 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 +103 -2
- 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
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { lsFiles } from '../../api/git/ls-files.mjs';
|
|
4
4
|
import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
|
|
5
5
|
import { gitOutputOf } from '../../lib/git.mjs';
|
|
6
|
+
import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
|
|
@@ -68,28 +69,27 @@ export function checkerScope({ config, graph, context }) {
|
|
|
68
69
|
return { config, graph, context, ts: context.ts, resolver: graph.resolver, tree: treeOf(config.root) };
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
const
|
|
72
|
-
|
|
72
|
+
const rootOfTarget = target => {
|
|
73
|
+
if (target.endsWith('/')) return trimTrailingSlashes(target);
|
|
74
|
+
const parent = path.posix.dirname(target);
|
|
75
|
+
return parent === '.' ? '' : parent;
|
|
76
|
+
};
|
|
73
77
|
|
|
74
78
|
function ruleFor(slotId, target, rootExists) {
|
|
75
|
-
if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(
|
|
79
|
+
if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(trimTrailingSlashes(target)))) return 'FE_ERROR_BOUNDARY_MISSING';
|
|
76
80
|
return 'HFS_REQUIRED_FILE_MISSING';
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const slot = resolver.slot(slotId);
|
|
84
|
-
return Boolean(slot) && (slot.tier !== 'none' || ALSO_JUDGED_TIER_NONE.has(slotId));
|
|
85
|
-
};
|
|
83
|
+
function slotIsJudged(resolver, slotId) {
|
|
84
|
+
const slot = resolver.slot(slotId);
|
|
85
|
+
return Boolean(slot) && (slot.tier !== 'none' || ALSO_JUDGED_TIER_NONE.has(slotId));
|
|
86
|
+
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
function addRequirement(requirements, slot, target, root) {
|
|
89
|
+
if (!requirements.has(target)) requirements.set(target, { slot, target, root });
|
|
90
|
+
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
const declared = resolver.requiredPaths();
|
|
92
|
+
function collectDeclaredRequirements(declared, requirements, instances, judged) {
|
|
93
93
|
const currentRoot = new Map();
|
|
94
94
|
for (const item of declared.paths) {
|
|
95
95
|
if (!judged(item.slot)) continue;
|
|
@@ -97,84 +97,116 @@ export function checkRequiredFiles({ config, graph }) {
|
|
|
97
97
|
const root = rootOfTarget(item.path);
|
|
98
98
|
currentRoot.set(item.slot, root);
|
|
99
99
|
instances.add(`${item.slot}|${root}`);
|
|
100
|
-
addRequirement(item.slot, item.path, root);
|
|
100
|
+
addRequirement(requirements, item.slot, item.path, root);
|
|
101
101
|
} else {
|
|
102
|
-
addRequirement(item.slot, item.path, currentRoot.get(item.slot) ?? '');
|
|
102
|
+
addRequirement(requirements, item.slot, item.path, currentRoot.get(item.slot) ?? '');
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addInstanceRequirements(resolver, classified, representative, requirements, instances, seenInstances, judged) {
|
|
108
|
+
const { slot: slotId, root } = classified;
|
|
109
|
+
const key = `${slotId}|${root}`;
|
|
110
|
+
if (seenInstances.has(key) || !judged(slotId)) return;
|
|
111
|
+
seenInstances.add(key);
|
|
112
|
+
instances.add(key);
|
|
113
|
+
for (const target of resolver.requiredFiles(representative)) addRequirement(requirements, slotId, target, root);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function addOwnerRequirements(resolver, owner, requirements, instances, seenInstances, judged) {
|
|
117
|
+
const key = `${owner.slot}|${owner.root}`;
|
|
118
|
+
if (seenInstances.has(key) || !judged(owner.slot) || !resolver.slotEnabled(resolver.slot(owner.slot))) return;
|
|
119
|
+
seenInstances.add(key);
|
|
120
|
+
instances.add(key);
|
|
121
|
+
for (const entry of resolver.slot(owner.slot).requires ?? []) {
|
|
122
|
+
const rooted = entry.startsWith('/');
|
|
123
|
+
const filled = (rooted ? entry.slice(1) : entry).replace(/<([A-Za-z][A-Za-z0-9-]*)>/g, (whole, name) => owner.bindings[name] ?? whole);
|
|
124
|
+
addRequirement(requirements, owner.slot, rooted || !owner.root ? filled : `${owner.root}/${filled}`, owner.root);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function noteInstance(found, slotId, root) {
|
|
129
|
+
if (!found.has(slotId)) found.set(slotId, new Set());
|
|
130
|
+
found.get(slotId).add(root);
|
|
131
|
+
}
|
|
105
132
|
|
|
106
|
-
|
|
133
|
+
function collectTreeRequirements(tree, resolver, requirements, instances, judged) {
|
|
107
134
|
const seenInstances = new Set();
|
|
108
|
-
const instanceRequires = (slotId, root, representative) => {
|
|
109
|
-
const key = `${slotId}|${root}`;
|
|
110
|
-
if (seenInstances.has(key) || !judged(slotId)) return;
|
|
111
|
-
seenInstances.add(key);
|
|
112
|
-
instances.add(key);
|
|
113
|
-
for (const target of resolver.requiredFiles(representative)) addRequirement(slotId, target, root);
|
|
114
|
-
};
|
|
115
|
-
const ownerRequires = owner => {
|
|
116
|
-
const key = `${owner.slot}|${owner.root}`;
|
|
117
|
-
if (seenInstances.has(key) || !judged(owner.slot) || !resolver.slotEnabled(resolver.slot(owner.slot))) return;
|
|
118
|
-
seenInstances.add(key);
|
|
119
|
-
instances.add(key);
|
|
120
|
-
for (const entry of resolver.slot(owner.slot).requires ?? []) {
|
|
121
|
-
const rooted = entry.startsWith('/');
|
|
122
|
-
const filled = (rooted ? entry.slice(1) : entry).replace(/<([A-Za-z][A-Za-z0-9-]*)>/g, (whole, name) => owner.bindings[name] ?? whole);
|
|
123
|
-
addRequirement(owner.slot, rooted || !owner.root ? filled : `${owner.root}/${filled}`, owner.root);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
135
|
const found = new Map(); // slot -> Set(root) of instances seen while walking
|
|
127
|
-
const note = (slotId, root) => {
|
|
128
|
-
if (!found.has(slotId)) found.set(slotId, new Set());
|
|
129
|
-
found.get(slotId).add(root);
|
|
130
|
-
};
|
|
131
136
|
for (const file of tree.files) {
|
|
132
137
|
const classified = resolver.classifyPath(file);
|
|
133
138
|
if (classified.status === 'owned' && classified.slot) {
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
noteInstance(found, classified.slot, classified.root);
|
|
140
|
+
addInstanceRequirements(resolver, classified, file, requirements, instances, seenInstances, judged);
|
|
136
141
|
}
|
|
137
142
|
const owner = resolver.ownerOf(file);
|
|
138
143
|
if (owner) {
|
|
139
|
-
|
|
144
|
+
noteInstance(found, owner.slot, owner.root);
|
|
140
145
|
// an owner root may classify to a more specific slot (modules/i18n) or a nested one, so expand the owner slot's own requires
|
|
141
|
-
|
|
146
|
+
addOwnerRequirements(resolver, owner, requirements, instances, seenInstances, judged);
|
|
142
147
|
}
|
|
143
148
|
}
|
|
149
|
+
return found;
|
|
150
|
+
}
|
|
144
151
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (exists(requirement.target)) continue;
|
|
157
|
-
const rootExists = !requirement.root || tree.directories.has(requirement.root);
|
|
158
|
-
if (!rootExists) {
|
|
159
|
-
report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
|
|
160
|
-
`Required directory ${requirement.root} (slot ${requirement.slot}) does not exist; the slot manifest requires it, along with what it holds.`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const kind = requirement.target.endsWith('/') ? 'directory' : 'file';
|
|
164
|
-
report(ruleFor(requirement.slot, requirement.target, true), strip(requirement.target), requirement.slot, requirement.root, { kind },
|
|
165
|
-
`Required ${kind} ${strip(requirement.target)} is missing; slot ${requirement.slot} requires it in every instance${requirement.root ? ` (${requirement.root})` : ''}.`);
|
|
152
|
+
function requirementExists(tree, target) {
|
|
153
|
+
return target.endsWith('/') ? tree.directories.has(trimTrailingSlashes(target)) : tree.files.has(target);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function reportMissingRequirement(requirement, tree, report) {
|
|
157
|
+
if (requirementExists(tree, requirement.target)) return;
|
|
158
|
+
const rootExists = !requirement.root || tree.directories.has(requirement.root);
|
|
159
|
+
if (!rootExists) {
|
|
160
|
+
report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
|
|
161
|
+
`Required directory ${requirement.root} (slot ${requirement.slot}) does not exist; the slot manifest requires it, along with what it holds.`);
|
|
162
|
+
return;
|
|
166
163
|
}
|
|
164
|
+
const kind = requirement.target.endsWith('/') ? 'directory' : 'file';
|
|
165
|
+
const rootNote = requirement.root ? ` (${requirement.root})` : '';
|
|
166
|
+
report(ruleFor(requirement.slot, requirement.target, true), trimTrailingSlashes(requirement.target), requirement.slot, requirement.root, { kind },
|
|
167
|
+
`Required ${kind} ${trimTrailingSlashes(requirement.target)} is missing; slot ${requirement.slot} requires it in every instance${rootNote}.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function reportMissingRequirements(requirements, tree, report) {
|
|
171
|
+
for (const requirement of requirements.values()) reportMissingRequirement(requirement, tree, report);
|
|
172
|
+
}
|
|
167
173
|
|
|
168
|
-
|
|
169
|
-
for (const minimum of
|
|
174
|
+
function reportMinimumRequirements(minimums, resolver, judged, found, report) {
|
|
175
|
+
for (const minimum of minimums) {
|
|
170
176
|
if (!judged(minimum.slot)) continue;
|
|
171
177
|
const count = minimum.appKind !== undefined
|
|
172
178
|
? resolver.repo.apps.filter(app => app.kind === minimum.appKind).length
|
|
173
179
|
: (found.get(minimum.slot)?.size ?? 0);
|
|
174
180
|
if (count >= minimum.min) continue;
|
|
181
|
+
const appKindNote = minimum.appKind ? ' (an app of kind ' + minimum.appKind + ' declared in hfs.json)' : '';
|
|
175
182
|
report('HFS_REQUIRED_FILE_MISSING', 'hfs.json', minimum.slot, '', { minimum: minimum.min, found: count },
|
|
176
|
-
`Slot ${minimum.slot} needs at least ${minimum.min} instance${minimum.min === 1 ? '' : 's'}${
|
|
183
|
+
`Slot ${minimum.slot} needs at least ${minimum.min} instance${minimum.min === 1 ? '' : 's'}${appKindNote}; the repository has ${count}.`);
|
|
177
184
|
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function checkRequiredFiles({ config, graph }) {
|
|
188
|
+
const resolver = graph?.resolver ?? config.hfs;
|
|
189
|
+
const tree = treeOf(config.root);
|
|
190
|
+
const judged = slotId => slotIsJudged(resolver, slotId);
|
|
191
|
+
|
|
192
|
+
const requirements = new Map(); // repository-relative target (as written, dirs end with /) -> {slot, target, root}
|
|
193
|
+
const instances = new Set();
|
|
194
|
+
const declared = resolver.requiredPaths();
|
|
195
|
+
collectDeclaredRequirements(declared, requirements, instances, judged);
|
|
196
|
+
|
|
197
|
+
const found = collectTreeRequirements(tree, resolver, requirements, instances, judged);
|
|
198
|
+
|
|
199
|
+
const violations = [];
|
|
200
|
+
const reported = new Set();
|
|
201
|
+
const report = (ruleId, target, slotId, root, extra, message) => {
|
|
202
|
+
const key = `${ruleId}|${target}`;
|
|
203
|
+
if (reported.has(key)) return;
|
|
204
|
+
reported.add(key);
|
|
205
|
+
violations.push({ ruleId, path: target, slot: slotId, ...(root ? { root } : {}), ...extra, message });
|
|
206
|
+
};
|
|
207
|
+
reportMissingRequirements(requirements, tree, report);
|
|
208
|
+
|
|
209
|
+
reportMinimumRequirements(declared.minimums, resolver, judged, found, report);
|
|
178
210
|
|
|
179
211
|
return { violations, coverage: { status: 'checked', instances: instances.size, requirements: requirements.size, missing: violations.length } };
|
|
180
212
|
}
|
|
@@ -24,74 +24,99 @@ const HOOK_ROUTES = new Set([...MOUNTING_ROUTES, 'page']);
|
|
|
24
24
|
const HOOK_NAME = /^use[A-Z0-9]/u;
|
|
25
25
|
const HOOKS_SLOT = 'fe.hooks';
|
|
26
26
|
|
|
27
|
+
const isFunction = (ts, node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
|
|
28
|
+
const jsxOf = (ts, node) => ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
|
|
29
|
+
|
|
30
|
+
function tagOf(ts, node) {
|
|
31
|
+
if (ts.isJsxElement(node)) return node.openingElement.tagName;
|
|
32
|
+
if (ts.isJsxSelfClosingElement(node)) return node.tagName;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addExportAssignmentDefaults(ts, statement, kit, checker, defaults) {
|
|
37
|
+
const target = statement.expression;
|
|
38
|
+
if (!ts.isIdentifier(target)) { defaults.add(target); return; }
|
|
39
|
+
for (const declaration of kit.declarationsOf(checker, target)) defaults.add(ts.isVariableDeclaration(declaration) ? declaration.initializer ?? declaration : declaration);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function routeDefaults(ts, file, kit, checker) {
|
|
43
|
+
const defaults = new Set();
|
|
44
|
+
for (const statement of file.sourceFile.statements) {
|
|
45
|
+
if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) defaults.add(statement);
|
|
46
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) addExportAssignmentDefaults(ts, statement, kit, checker, defaults);
|
|
47
|
+
}
|
|
48
|
+
return defaults;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True when the function contains JSX that is not inside a nested function. */
|
|
52
|
+
function drawsDirectly(ts, fn) {
|
|
53
|
+
let found = false;
|
|
54
|
+
const visit = node => {
|
|
55
|
+
if (found) return;
|
|
56
|
+
if (node !== fn && isFunction(ts, node)) return;
|
|
57
|
+
if (jsxOf(ts, node)) { found = true; return; }
|
|
58
|
+
ts.forEachChild(node, visit);
|
|
59
|
+
};
|
|
60
|
+
visit(fn);
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reportRoute(violations, kit, file, stem, node, message, extra = {}) {
|
|
65
|
+
violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, route: stem, ...extra });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inspectMountingTag(tag, { graph, kit, ts, file, stem, checker, violations, state }) {
|
|
69
|
+
if (ts.isIdentifier(tag) && /^[a-z]/u.test(tag.text)) {
|
|
70
|
+
reportRoute(violations, kit, file, stem, tag, `<${tag.text}> is drawing in a ${stem} route file; a route file mounts one feature and draws nothing. Move the markup into the feature.`);
|
|
71
|
+
} else if (kit.declarationsOf(checker, ts.isPropertyAccessExpression(tag) ? tag.name : tag).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.tier === 'feature')) {
|
|
72
|
+
state.owners += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inspectHookCall(node, { graph, kit, ts, file, stem, checker, violations }) {
|
|
77
|
+
const callee = ts.isIdentifier(node.expression) ? node.expression : null;
|
|
78
|
+
if (!callee || !HOOK_NAME.test(callee.text)) return;
|
|
79
|
+
const binding = kit.importBinding(checker, callee);
|
|
80
|
+
const inHooks = kit.declarationsOf(checker, callee).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.slot === HOOKS_SLOT);
|
|
81
|
+
if (binding || inHooks) reportRoute(violations, kit, file, stem, callee, `${callee.text} is a hook called in a ${stem} route file; a route file is a server adapter and holds no hook. Read the data in the feature it mounts.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function inspectRouteNode(node, context) {
|
|
85
|
+
const { kit, ts, file, stem, mounting, defaults, violations, state } = context;
|
|
86
|
+
if (jsxOf(ts, node)) state.hasJsx = true;
|
|
87
|
+
const tag = tagOf(ts, node);
|
|
88
|
+
if (mounting && tag) inspectMountingTag(tag, context);
|
|
89
|
+
if (mounting && isFunction(ts, node) && !defaults.has(node) && drawsDirectly(ts, node)) {
|
|
90
|
+
reportRoute(violations, kit, file, stem, node, `A function other than the default export draws JSX in a ${stem} route file; that is an inline component. Move it into the feature the route mounts.`);
|
|
91
|
+
}
|
|
92
|
+
if (ts.isCallExpression(node)) inspectHookCall(node, context);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inspectRouteFile(graph, kit, file, stem, violations) {
|
|
97
|
+
const { ts } = kit;
|
|
98
|
+
const mounting = MOUNTING_ROUTES.has(stem);
|
|
99
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
100
|
+
const defaults = routeDefaults(ts, file, kit, checker);
|
|
101
|
+
const state = { hasJsx: false, owners: 0 };
|
|
102
|
+
kit.walk(file.sourceFile, node => inspectRouteNode(node, { graph, kit, ts, file, stem, mounting, checker, defaults, violations, state }));
|
|
103
|
+
if (mounting && state.hasJsx && state.owners !== 1) {
|
|
104
|
+
violations.push({ ruleId: RULE, path: file.rel, line: 1, column: 1, route: stem, owners: state.owners,
|
|
105
|
+
message: `${file.rel} mounts ${state.owners} feature owners; a ${stem} route file mounts exactly one feature (a layouts- or pages-tier component of the app), optionally inside a package shell.` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
27
109
|
export function checkRouteFilesThin(input) {
|
|
28
110
|
const { graph } = input;
|
|
29
111
|
const kit = machineKit(input);
|
|
30
|
-
const { ts } = kit;
|
|
31
112
|
const violations = [];
|
|
32
113
|
let routes = 0;
|
|
33
|
-
|
|
34
114
|
for (const file of graph.files.values()) {
|
|
35
115
|
if (file.slot !== ROUTE_SLOT) continue;
|
|
36
116
|
const stem = path.posix.basename(file.rel).replace(/\.[cm]?[jt]sx?$/u, '');
|
|
37
117
|
if (!HOOK_ROUTES.has(stem)) continue;
|
|
38
118
|
routes += 1;
|
|
39
|
-
|
|
40
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
41
|
-
const report = (node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, route: stem, ...extra });
|
|
42
|
-
|
|
43
|
-
// The default export function: `export default function X`, `export default X` (X a function), `export default () => ...`.
|
|
44
|
-
const defaults = new Set();
|
|
45
|
-
for (const statement of file.sourceFile.statements) {
|
|
46
|
-
if (ts.isFunctionDeclaration(statement) && statement.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) defaults.add(statement);
|
|
47
|
-
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
48
|
-
const target = statement.expression;
|
|
49
|
-
if (ts.isIdentifier(target)) for (const declaration of kit.declarationsOf(checker, target)) defaults.add(ts.isVariableDeclaration(declaration) ? declaration.initializer ?? declaration : declaration);
|
|
50
|
-
else defaults.add(target);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const isFunction = node => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
|
|
54
|
-
const jsxOf = node => ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
|
|
55
|
-
const tagOf = node => (ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : null);
|
|
56
|
-
/** True when the function contains JSX that is not inside a nested function. */
|
|
57
|
-
const drawsDirectly = fn => {
|
|
58
|
-
let found = false;
|
|
59
|
-
const visit = node => {
|
|
60
|
-
if (found) return;
|
|
61
|
-
if (node !== fn && isFunction(node)) return;
|
|
62
|
-
if (jsxOf(node)) { found = true; return; }
|
|
63
|
-
ts.forEachChild(node, visit);
|
|
64
|
-
};
|
|
65
|
-
visit(fn);
|
|
66
|
-
return found;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
let hasJsx = false;
|
|
70
|
-
let owners = 0;
|
|
71
|
-
kit.walk(file.sourceFile, node => {
|
|
72
|
-
if (jsxOf(node)) hasJsx = true;
|
|
73
|
-
const tag = tagOf(node);
|
|
74
|
-
if (mounting && tag) {
|
|
75
|
-
if (ts.isIdentifier(tag) && /^[a-z]/u.test(tag.text)) report(tag, `<${tag.text}> is drawing in a ${stem} route file; a route file mounts one feature and draws nothing. Move the markup into the feature.`);
|
|
76
|
-
else if (kit.declarationsOf(checker, ts.isPropertyAccessExpression(tag) ? tag.name : tag).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.tier === 'feature')) owners += 1;
|
|
77
|
-
}
|
|
78
|
-
if (mounting && isFunction(node) && !defaults.has(node) && drawsDirectly(node)) {
|
|
79
|
-
report(node, `A function other than the default export draws JSX in a ${stem} route file; that is an inline component. Move it into the feature the route mounts.`);
|
|
80
|
-
}
|
|
81
|
-
if (ts.isCallExpression(node)) {
|
|
82
|
-
const callee = ts.isIdentifier(node.expression) ? node.expression : null;
|
|
83
|
-
if (callee && HOOK_NAME.test(callee.text)) {
|
|
84
|
-
const binding = kit.importBinding(checker, callee);
|
|
85
|
-
const inHooks = kit.declarationsOf(checker, callee).some(declaration => graph.files.get(kit.graphPath(declaration) ?? '')?.slot === HOOKS_SLOT);
|
|
86
|
-
if (binding || inHooks) report(callee, `${callee.text} is a hook called in a ${stem} route file; a route file is a server adapter and holds no hook. Read the data in the feature it mounts.`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return true;
|
|
90
|
-
});
|
|
91
|
-
if (mounting && hasJsx && owners !== 1) {
|
|
92
|
-
violations.push({ ruleId: RULE, path: file.rel, line: 1, column: 1, route: stem, owners,
|
|
93
|
-
message: `${file.rel} mounts ${owners} feature owners; a ${stem} route file mounts exactly one feature (a layouts- or pages-tier component of the app), optionally inside a package shell.` });
|
|
94
|
-
}
|
|
119
|
+
inspectRouteFile(graph, kit, file, stem, violations);
|
|
95
120
|
}
|
|
96
121
|
return { violations, coverage: { status: 'checked', routes } };
|
|
97
122
|
}
|