@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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function appendProjectReferences(relative, parsed, config, ts, helpers, queue, errors) {
|
|
5
|
+
for (const reference of parsed.projectReferences ?? []) {
|
|
6
|
+
const referencedFile = helpers.typeScriptProjectReferencePath(ts, reference);
|
|
7
|
+
const absoluteReference = path.resolve(referencedFile);
|
|
8
|
+
if (!helpers.isInside(config.root, absoluteReference)) {
|
|
9
|
+
errors.push({ ruleId: 'ARCH_TSCONFIG_REFERENCE_OUTSIDE', project: relative, message: `Project reference leaves the repository: ${helpers.slash(path.relative(config.root, absoluteReference))}.` });
|
|
10
|
+
} else {
|
|
11
|
+
queue.push(helpers.slash(path.relative(config.root, absoluteReference)));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readProject(relative, config, ts, helpers, state) {
|
|
17
|
+
const configFile = path.join(config.root, ...relative.split('/'));
|
|
18
|
+
if (!fs.existsSync(configFile)) {
|
|
19
|
+
state.errors.push({ ruleId: 'ARCH_TSCONFIG_MISSING', project: relative, message: `${relative} does not exist.` });
|
|
20
|
+
state.invalidProjects.add(relative);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const { read, parsed } = helpers.readTypeScriptProject(ts, configFile);
|
|
24
|
+
if (read.error) {
|
|
25
|
+
state.errors.push(helpers.compilerError(ts, config.root, read.error, relative));
|
|
26
|
+
state.invalidProjects.add(relative);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (parsed.errors.length) {
|
|
30
|
+
state.errors.push(...parsed.errors.map(item => helpers.compilerError(ts, config.root, item, relative)));
|
|
31
|
+
state.invalidProjects.add(relative);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
appendProjectReferences(relative, parsed, config, ts, helpers, state.queue, state.errors);
|
|
35
|
+
state.parsedProjects.set(relative, parsed);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadProjectConfigurations(config, ts, helpers, errors) {
|
|
39
|
+
const state = { errors, parsedProjects: new Map(), invalidProjects: new Set(), queue: [...config.projects] };
|
|
40
|
+
const seenProjects = new Set();
|
|
41
|
+
while (state.queue.length) {
|
|
42
|
+
const relative = state.queue.shift();
|
|
43
|
+
if (seenProjects.has(relative)) continue;
|
|
44
|
+
seenProjects.add(relative);
|
|
45
|
+
readProject(relative, config, ts, helpers, state);
|
|
46
|
+
}
|
|
47
|
+
return state;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pathMatches(config, paths, file, helpers) {
|
|
51
|
+
const relative = helpers.slash(path.relative(config.root, file));
|
|
52
|
+
const normalized = helpers.slash(paths).replace(/\/$/, '');
|
|
53
|
+
return helpers.sameOrUnder(relative, normalized);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function visitProjectReferences(relative, selected, parsedProjects, config, ts, helpers) {
|
|
57
|
+
if (selected.has(relative)) return;
|
|
58
|
+
selected.add(relative);
|
|
59
|
+
for (const reference of parsedProjects.get(relative)?.projectReferences ?? []) {
|
|
60
|
+
const referencedFile = helpers.typeScriptProjectReferencePath(ts, reference);
|
|
61
|
+
const referenced = helpers.slash(path.relative(config.root, path.resolve(referencedFile)));
|
|
62
|
+
if (parsedProjects.has(referenced)) visitProjectReferences(referenced, selected, parsedProjects, config, ts, helpers);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function invalidProjectMatches(config, paths, relative, helpers) {
|
|
67
|
+
const directory = path.dirname(path.join(config.root, ...relative.split('/')));
|
|
68
|
+
return paths.some(prefix => {
|
|
69
|
+
const absolute = path.join(config.root, ...helpers.slash(prefix).split('/'));
|
|
70
|
+
return helpers.isInside(directory, absolute);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function selectProjects(config, paths, ts, state, helpers) {
|
|
75
|
+
const selected = new Set();
|
|
76
|
+
const direct = new Set();
|
|
77
|
+
if (!paths.length) return { selected, direct };
|
|
78
|
+
for (const [relative, parsed] of state.parsedProjects) {
|
|
79
|
+
if (parsed.fileNames.some(file => paths.some(prefix => pathMatches(config, prefix, file, helpers)))) {
|
|
80
|
+
direct.add(relative);
|
|
81
|
+
visitProjectReferences(relative, selected, state.parsedProjects, config, ts, helpers);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const relative of state.invalidProjects) {
|
|
85
|
+
if (invalidProjectMatches(config, paths, relative, helpers)) selected.add(relative);
|
|
86
|
+
}
|
|
87
|
+
state.errors.splice(0, state.errors.length, ...state.errors.filter(error => !error.project || selected.has(error.project)));
|
|
88
|
+
return { selected, direct };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createProjects(config, paths, state, selection, ts, helpers) {
|
|
92
|
+
const projects = [];
|
|
93
|
+
for (const [relative, parsed] of state.parsedProjects) {
|
|
94
|
+
if (paths.length && !selection.selected.has(relative)) continue;
|
|
95
|
+
const rootNames = paths.length && selection.direct.has(relative)
|
|
96
|
+
? parsed.fileNames.filter(file => paths.some(prefix => pathMatches(config, prefix, file, helpers)))
|
|
97
|
+
: parsed.fileNames;
|
|
98
|
+
const program = helpers.createTypeScriptProgram(ts, { rootNames, options: parsed.options, projectReferences: parsed.projectReferences });
|
|
99
|
+
state.errors.push(...program.getSyntacticDiagnostics().filter(item => !item.file || helpers.isInside(config.root, item.file.fileName))
|
|
100
|
+
.map(item => helpers.compilerError(ts, config.root, item, relative, 'ARCH_SYNTAX_INVALID')));
|
|
101
|
+
projects.push({ relative, program, options: parsed.options });
|
|
102
|
+
}
|
|
103
|
+
return projects;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collectProjectFiles(config, projects, helpers) {
|
|
107
|
+
const occurrences = new Map();
|
|
108
|
+
const sourceIn = new Map();
|
|
109
|
+
for (const project of projects) {
|
|
110
|
+
for (const sourceFile of project.program.getSourceFiles().filter(file => helpers.isProductionSource(config.root, file))) {
|
|
111
|
+
const name = helpers.canonical(sourceFile.fileName);
|
|
112
|
+
if (!occurrences.has(name)) occurrences.set(name, []);
|
|
113
|
+
occurrences.get(name).push(project);
|
|
114
|
+
sourceIn.set(`${project.relative}|${name}`, sourceFile);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { occurrences, sourceIn };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function owningProject(config, candidates, file, helpers) {
|
|
121
|
+
return candidates.filter(item => helpers.isInside(path.dirname(path.join(config.root, ...item.relative.split('/'))), file))
|
|
122
|
+
.sort((x, y) => y.relative.split('/').length - x.relative.split('/').length)[0] ?? candidates[0];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function makeFileGraph(config, projects, helpers) {
|
|
126
|
+
const { occurrences, sourceIn } = collectProjectFiles(config, projects, helpers);
|
|
127
|
+
const fileMap = new Map();
|
|
128
|
+
const checkerByFile = new Map();
|
|
129
|
+
const ownerByFile = new Map();
|
|
130
|
+
for (const [name, candidates] of occurrences) {
|
|
131
|
+
const owner = owningProject(config, candidates, name, helpers);
|
|
132
|
+
ownerByFile.set(name, owner);
|
|
133
|
+
fileMap.set(name, sourceIn.get(`${owner.relative}|${name}`));
|
|
134
|
+
checkerByFile.set(name, owner.program.getTypeChecker());
|
|
135
|
+
}
|
|
136
|
+
const files = [...fileMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, file]) => file);
|
|
137
|
+
const edges = new Map([...fileMap.keys()].map(file => [file, []]));
|
|
138
|
+
for (const [file, sourceFile] of fileMap) {
|
|
139
|
+
const sourceName = path.resolve(sourceFile.fileName);
|
|
140
|
+
if (sourceName !== file) edges.set(sourceName, edges.get(file));
|
|
141
|
+
}
|
|
142
|
+
return { fileMap, checkerByFile, ownerByFile, files, edges };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function addUnprovenFindings(sourceFile, project, references, errors, config, helpers) {
|
|
146
|
+
for (const item of references.unproven) errors.push({
|
|
147
|
+
ruleId: 'ARCH_DYNAMIC_DEPENDENCY_UNPROVEN',
|
|
148
|
+
project: project.relative,
|
|
149
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
150
|
+
...helpers.sourceLocation(sourceFile, item.node),
|
|
151
|
+
message: `${item.kind} must use a string-literal module name so architecture coverage can resolve its dependency.`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function unresolvedReferenceFinding(reference, sourceFile, project, config, workspaceNames, errors, helpers) {
|
|
156
|
+
const codeLike = !helpers.assetExtension.test(reference.specifier);
|
|
157
|
+
const workspaceImport = [...workspaceNames].some(name => helpers.sameOrUnder(reference.specifier, name));
|
|
158
|
+
const internal = reference.specifier.startsWith('.') || helpers.pathAliasMatches(reference.specifier, project.options.paths) || workspaceImport;
|
|
159
|
+
if (codeLike && internal) errors.push({
|
|
160
|
+
ruleId: 'ARCH_INTERNAL_IMPORT_UNRESOLVED',
|
|
161
|
+
project: project.relative,
|
|
162
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
163
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
164
|
+
specifier: reference.specifier,
|
|
165
|
+
message: `Internal import ${reference.specifier} is not resolvable with ${project.relative}.`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function internalReference(reference, project, workspaceNames, helpers) {
|
|
170
|
+
const workspaceImport = [...workspaceNames].some(name => reference.specifier === name || reference.specifier.startsWith(`${name}/`));
|
|
171
|
+
return reference.specifier.startsWith('.') || helpers.pathAliasMatches(reference.specifier, project.options.paths) || workspaceImport;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function appendBoundaryFinding(state, reference, sourceFile, project, actualTarget, { internal, reviewable }) {
|
|
175
|
+
const { config, errors, helpers } = state;
|
|
176
|
+
if (internal && helpers.crossesSide(config, actualTarget)) {
|
|
177
|
+
errors.push({
|
|
178
|
+
ruleId: 'ARCH_INTERNAL_IMPORT_OUTSIDE',
|
|
179
|
+
project: project.relative,
|
|
180
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
181
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
182
|
+
specifier: reference.specifier,
|
|
183
|
+
message: `Internal import ${reference.specifier} leaves the ${path.basename(config.root)} side for ${helpers.slash(path.relative(config.packageRoot, actualTarget))}; nothing crosses the sides of an app.`,
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (internal && !reviewable) {
|
|
188
|
+
errors.push({
|
|
189
|
+
ruleId: 'ARCH_INTERNAL_IMPORT_OUTSIDE',
|
|
190
|
+
project: project.relative,
|
|
191
|
+
path: helpers.relativePath(config.root, sourceFile.fileName),
|
|
192
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
193
|
+
specifier: reference.specifier,
|
|
194
|
+
message: `Internal import ${reference.specifier} resolves outside the checked repository.`,
|
|
195
|
+
});
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function appendEdge(from, sourceFile, project, owningWorkspace, reference, actualTarget, state) {
|
|
202
|
+
if (!state.fileMap.has(actualTarget)) return;
|
|
203
|
+
const { config, ts, edges, edgeKeys, errors, helpers, workspaces } = state;
|
|
204
|
+
const edge = {
|
|
205
|
+
from,
|
|
206
|
+
to: actualTarget,
|
|
207
|
+
runtime: reference.runtime,
|
|
208
|
+
specifier: reference.specifier,
|
|
209
|
+
node: reference.node,
|
|
210
|
+
declaration: reference.declaration,
|
|
211
|
+
reexport: ts.isExportDeclaration(reference.declaration),
|
|
212
|
+
sourceFile,
|
|
213
|
+
project: project.relative,
|
|
214
|
+
...helpers.sourceLocation(sourceFile, reference.node),
|
|
215
|
+
};
|
|
216
|
+
const key = `${from}\0${actualTarget}\0${reference.node.getStart(sourceFile)}\0${reference.specifier}`;
|
|
217
|
+
if (!edgeKeys.has(key)) { edges.get(from).push(edge); edgeKeys.add(key); }
|
|
218
|
+
const targetWorkspace = helpers.workspaceOf(workspaces, actualTarget);
|
|
219
|
+
if (owningWorkspace && targetWorkspace && owningWorkspace !== targetWorkspace) {
|
|
220
|
+
if (!owningWorkspace.app && targetWorkspace.app) errors.push(helpers.boundaryViolation(config.root, edge, owningWorkspace, targetWorkspace, 'ARCH_PACKAGE_IMPORTS_APP', 'A reusable workspace package cannot depend on an application workspace.'));
|
|
221
|
+
if (!helpers.packageExported(ts, targetWorkspace, reference.specifier, actualTarget)) errors.push(helpers.boundaryViolation(config.root, edge, owningWorkspace, targetWorkspace, 'ARCH_PACKAGE_EXPORT_BYPASS', 'Cross-package imports must use the target package name and a declared package export.'));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function processReference(from, sourceFile, project, owningWorkspace, reference, state) {
|
|
226
|
+
const { config, ts, workspaceNames, workspaces, host, errors, helpers } = state;
|
|
227
|
+
// A workspace package is read at its source, never at its build: the same import resolves whether or not dist exists.
|
|
228
|
+
const workspaceTarget = workspaces.filter(item => item.root !== helpers.canonical(config.root)).map(item => helpers.workspaceSourceEntry(ts, item, reference.specifier)).find(Boolean);
|
|
229
|
+
const resolvedName = workspaceTarget ?? helpers.resolveTypeScriptModule(ts, reference.specifier, sourceFile.fileName, project.options, host);
|
|
230
|
+
if (!resolvedName) {
|
|
231
|
+
unresolvedReferenceFinding(reference, sourceFile, project, config, workspaceNames, errors, helpers);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const actualTarget = helpers.canonical(resolvedName);
|
|
235
|
+
const internal = internalReference(reference, project, workspaceNames, helpers);
|
|
236
|
+
// The boundary is the checkout, not the checked project: a path alias that lands in a sibling
|
|
237
|
+
// package of the same repository (`@fe-kit/*` -> packages/fe-kit) is still source a reviewer can
|
|
238
|
+
// open, while anything past the repository, or anything inside an installed dependency tree, is
|
|
239
|
+
// not. `config.repository` is null when there is no git checkout around the project, and the
|
|
240
|
+
// project is then the boundary it always was.
|
|
241
|
+
const reviewable = helpers.isInside(config.root, actualTarget)
|
|
242
|
+
|| (config.repository && helpers.isInside(config.repository, actualTarget) && !helpers.slash(actualTarget).includes('/node_modules/'));
|
|
243
|
+
// A side of an app (config.root is be/ or fe/) imports nothing of the app outside itself: the root holds no source, the other
|
|
244
|
+
// side is another program, and a declared read (sides.fe.reads, be/contracts/) is codegen input, never an import.
|
|
245
|
+
if (appendBoundaryFinding(state, reference, sourceFile, project, actualTarget, { internal, reviewable })) return;
|
|
246
|
+
appendEdge(from, sourceFile, project, owningWorkspace, reference, actualTarget, state);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function processSourceFile(from, sourceFile, state) {
|
|
250
|
+
const { config, ts, workspaces, ownerByFile, helpers } = state;
|
|
251
|
+
const owningWorkspace = helpers.workspaceOf(workspaces, from);
|
|
252
|
+
const project = ownerByFile.get(from);
|
|
253
|
+
if (!project) return;
|
|
254
|
+
const references = helpers.moduleReferences(ts, sourceFile, project.program.getTypeChecker());
|
|
255
|
+
addUnprovenFindings(sourceFile, project, references, state.errors, config, helpers);
|
|
256
|
+
for (const reference of references.found) processReference(from, sourceFile, project, owningWorkspace, reference, state);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function dependencyGraph(config, loaded, fileGraph, ownerByFile, errors, helpers) {
|
|
260
|
+
const { ts } = loaded;
|
|
261
|
+
const edgeKeys = new Set();
|
|
262
|
+
const host = { ...ts.sys, fileExists: ts.sys.fileExists, readFile: ts.sys.readFile, realpath: ts.sys.realpath };
|
|
263
|
+
const workspaces = helpers.workspaceMetadata(config);
|
|
264
|
+
const workspaceNames = new Set(workspaces.map(item => item.name).filter(Boolean));
|
|
265
|
+
const state = { config, ts, fileMap: fileGraph.fileMap, edges: fileGraph.edges, edgeKeys, errors, workspaces, workspaceNames, host, ownerByFile, helpers };
|
|
266
|
+
for (const [from, sourceFile] of fileGraph.fileMap) processSourceFile(from, sourceFile, state);
|
|
267
|
+
return { edges: fileGraph.edges, workspaces };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function contextFromProjects(config, loaded, paths, helpers) {
|
|
271
|
+
const { ts } = loaded;
|
|
272
|
+
const errors = [];
|
|
273
|
+
const projectState = loadProjectConfigurations(config, ts, helpers, errors);
|
|
274
|
+
const selection = selectProjects(config, paths, ts, projectState, helpers);
|
|
275
|
+
const projects = createProjects(config, paths, projectState, selection, ts, helpers);
|
|
276
|
+
const fileGraph = makeFileGraph(config, projects, helpers);
|
|
277
|
+
const { edges, workspaces } = dependencyGraph(config, loaded, fileGraph, fileGraph.ownerByFile, errors, helpers);
|
|
278
|
+
if (projects.length && fileGraph.files.length === 0) errors.push({ ruleId: 'ARCH_NO_SOURCE', message: 'The configured TypeScript projects contain no production TypeScript or JavaScript source.' });
|
|
279
|
+
return { loaded, errors, files: fileGraph.files, edges, programs: projects.map(item => item.program), program: projects[0]?.program ?? null, projects, ts, workspaces,
|
|
280
|
+
checkerFor: file => fileGraph.checkerByFile.get(helpers.canonical(file)) ?? null,
|
|
281
|
+
workspaceOf: file => helpers.workspaceOf(workspaces, helpers.canonical(file)) };
|
|
282
|
+
}
|