@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,8 @@
|
|
|
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';
|
|
5
|
+
import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* R45 `module-per-transport` (BE_MODULE_SHAPE, BE-CONVENTION 1.2 and 1.6). A feature has one Nest module for its
|
|
@@ -22,48 +24,38 @@ export const MODULE_PER_TRANSPORT_RULE_IDS = ['BE_MODULE_SHAPE'];
|
|
|
22
24
|
const RULE = 'BE_MODULE_SHAPE';
|
|
23
25
|
const APP_ROOT_FILE = 'app.module.ts';
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const violations = [];
|
|
30
|
-
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
31
|
-
const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
|
|
27
|
+
function featureOwnerOf(resolver, rel) {
|
|
28
|
+
const owner = resolver.ownerOf(rel);
|
|
29
|
+
return owner && resolver.slot(owner.slot)?.tier === 'feature' ? owner : null;
|
|
30
|
+
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const placeOf = rel => {
|
|
43
|
-
const owner = featureOf(rel);
|
|
44
|
-
if (!owner) return null;
|
|
45
|
-
const slot = resolver.slot(owner.slot);
|
|
46
|
-
const below = rel.slice(owner.root.length + 1).split('/');
|
|
47
|
-
const feature = path.posix.basename(owner.root);
|
|
48
|
-
if (slot.composedBy) return { root: owner.root, feature, protocol: null, below, composed: slot };
|
|
49
|
-
return { root: owner.root, feature, protocol: below[0] === 'transport' && below.length > 2 ? below[1] : null, below, composed: null };
|
|
50
|
-
};
|
|
51
|
-
/** The one module file a folder of a composed root may hold: <folder>/<folder>.module.ts. */
|
|
52
|
-
const composedModuleOf = rel => {
|
|
53
|
-
const dir = path.posix.dirname(rel);
|
|
54
|
-
return `${dir}/${path.posix.basename(dir)}.module.ts`;
|
|
55
|
-
};
|
|
32
|
+
function modulePlaceOf(resolver, rel) {
|
|
33
|
+
const owner = featureOwnerOf(resolver, rel);
|
|
34
|
+
if (!owner) return null;
|
|
35
|
+
const slot = resolver.slot(owner.slot);
|
|
36
|
+
const below = rel.slice(owner.root.length + 1).split('/');
|
|
37
|
+
const feature = path.posix.basename(owner.root);
|
|
38
|
+
if (slot.composedBy) return { root: owner.root, feature, protocol: null, below, composed: slot };
|
|
39
|
+
return { root: owner.root, feature, protocol: below[0] === 'transport' && below.length > 2 ? below[1] : null, below, composed: null };
|
|
40
|
+
}
|
|
56
41
|
|
|
57
|
-
|
|
42
|
+
function composedModulePath(rel) {
|
|
43
|
+
const dir = path.posix.dirname(rel);
|
|
44
|
+
return `${dir}/${path.posix.basename(dir)}.module.ts`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function composedByProtocol(resolver) {
|
|
58
48
|
const composedBy = new Map();
|
|
59
49
|
for (const slot of resolver.slots()) {
|
|
60
50
|
if (!slot.composedBy || slot.owner) continue;
|
|
61
|
-
composedBy.set(path.posix.basename(slot.path
|
|
51
|
+
composedBy.set(path.posix.basename(trimTrailingSlashes(slot.path)), slot.composedBy);
|
|
62
52
|
}
|
|
53
|
+
return composedBy;
|
|
54
|
+
}
|
|
63
55
|
|
|
56
|
+
function reportTransportFileShapes(tree, placeOf, plain) {
|
|
64
57
|
let transportModules = 0;
|
|
65
|
-
const
|
|
66
|
-
for (const rel of [...tree.files].sort()) {
|
|
58
|
+
for (const rel of [...tree.files].sort(byCodeUnit)) {
|
|
67
59
|
const place = placeOf(rel);
|
|
68
60
|
if (!place) continue;
|
|
69
61
|
const base = path.posix.basename(rel);
|
|
@@ -74,81 +66,138 @@ export function checkModulePerTransport(input) {
|
|
|
74
66
|
else transportModules += 1;
|
|
75
67
|
}
|
|
76
68
|
}
|
|
69
|
+
return transportModules;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isNestModuleClass(ts, kit, checker, statement) {
|
|
73
|
+
return kit.decorators(statement).some(decorator => {
|
|
74
|
+
const call = ts.isCallExpression(decorator.expression) ? decorator.expression.expression : decorator.expression;
|
|
75
|
+
return kit.isImportOf(checker, call, 'Module', '@nestjs/common');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function reportModuleClassLocation(file, statement, place, composedModuleOf, report) {
|
|
80
|
+
if (place.composed) {
|
|
81
|
+
const own = composedModuleOf(file.rel);
|
|
82
|
+
if (file.rel !== own) report(file, statement.name, `${statement.name.text} is a Nest module declared in ${file.rel}; every folder of ${place.root} holds exactly one static module named after it (${own}), never a module per command.`, { feature: place.feature });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const own = place.protocol === null
|
|
86
|
+
? `${place.root}/${place.feature}.module.ts`
|
|
87
|
+
: `${place.root}/transport/${place.protocol}/${place.feature}-${place.protocol}.module.ts`;
|
|
88
|
+
if (file.rel !== own) report(file, statement.name, `${statement.name.text} is a Nest module declared in ${file.rel}; a feature declares exactly its application module (${place.feature}.module.ts) and one module per transport folder (${place.feature}-<protocol>.module.ts), never a module per operation.`, { feature: place.feature });
|
|
89
|
+
}
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
function reportConfigurableModuleUses(file, checker, kit, ts, place, report) {
|
|
92
|
+
kit.walk(file.sourceFile, node => {
|
|
93
|
+
if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common')) {
|
|
94
|
+
report(file, node, `ConfigurableModuleBuilder builds a capability's module definition; feature ${place.feature} has static modules only, so declare the providers in its @Module.`, { feature: place.feature });
|
|
95
|
+
} else if (ts.isHeritageClause(node)) {
|
|
96
|
+
for (const type of node.types) if (kit.isImportOf(checker, type.expression, 'ConfigurableModuleClass', '@nestjs/common')) {
|
|
97
|
+
report(file, type, `A feature module never extends ConfigurableModuleClass; feature ${place.feature} has static modules only.`, { feature: place.feature });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function moduleClassesInFile(file, checker, place, modules, env) {
|
|
105
|
+
const { kit, ts, composedModuleOf, report } = env;
|
|
106
|
+
for (const statement of file.sourceFile.statements) {
|
|
107
|
+
if (!ts.isClassDeclaration(statement) || !statement.name) continue;
|
|
108
|
+
if (!isNestModuleClass(ts, kit, checker, statement)) continue;
|
|
109
|
+
modules.set(statement, { name: statement.name.text, file });
|
|
110
|
+
if (place) reportModuleClassLocation(file, statement, place, composedModuleOf, report);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function collectFeatureModules(graph, kit, ts, placeOf, composedModuleOf, report) {
|
|
79
115
|
const modules = new Map(); // class declaration -> {name, file}
|
|
116
|
+
const env = { kit, ts, composedModuleOf, report };
|
|
80
117
|
for (const file of graph.files.values()) {
|
|
81
118
|
const checker = kit.checkerOf(file.sourceFile);
|
|
82
119
|
const place = placeOf(file.rel);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (file.rel !== own) report(file, statement.name, `${statement.name.text} is a Nest module declared in ${file.rel}; a feature declares exactly its application module (${place.feature}.module.ts) and one module per transport folder (${place.feature}-<protocol>.module.ts), never a module per operation.`, { feature: place.feature });
|
|
101
|
-
}
|
|
102
|
-
if (place) {
|
|
103
|
-
kit.walk(file.sourceFile, node => {
|
|
104
|
-
if (ts.isNewExpression(node) && kit.isImportOf(checker, node.expression, 'ConfigurableModuleBuilder', '@nestjs/common')) {
|
|
105
|
-
report(file, node, `ConfigurableModuleBuilder builds a capability's module definition; feature ${place.feature} has static modules only, so declare the providers in its @Module.`, { feature: place.feature });
|
|
106
|
-
} else if (ts.isHeritageClause(node)) {
|
|
107
|
-
for (const type of node.types) if (kit.isImportOf(checker, type.expression, 'ConfigurableModuleClass', '@nestjs/common')) {
|
|
108
|
-
report(file, type, `A feature module never extends ConfigurableModuleClass; feature ${place.feature} has static modules only.`, { feature: place.feature });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
});
|
|
113
|
-
}
|
|
120
|
+
moduleClassesInFile(file, checker, place, modules, env);
|
|
121
|
+
if (place) reportConfigurableModuleUses(file, checker, kit, ts, place, report);
|
|
122
|
+
}
|
|
123
|
+
return modules;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function reportModuleReference(root, node, app, module, place, composedBy, report) {
|
|
127
|
+
if (place.composed) {
|
|
128
|
+
const rootModule = `${place.root}/${place.feature}.module.ts`;
|
|
129
|
+
if (!place.composed.composedBy.includes(app.kind)) report(root, node, `App ${app.name} is of kind ${app.kind} and lists ${module.name} from ${place.root}; only an app of kind ${place.composed.composedBy.join(', ')} composes that root.`, { app: app.name, module: module.name });
|
|
130
|
+
else if (module.file.rel !== rootModule) report(root, node, `App ${app.name} lists ${module.name}, a group module inside ${place.root}; the app lists the root module (${rootModule}), which imports every group module.`, { app: app.name, module: module.name });
|
|
131
|
+
} else if (place.protocol === null) {
|
|
132
|
+
report(root, node, `App ${app.name} lists ${module.name}, the application module of feature ${place.feature}; apps import only transport modules (${place.feature}-<protocol>.module.ts), which import the application module themselves.`, { app: app.name, module: module.name });
|
|
133
|
+
} else if (!(composedBy.get(place.protocol) ?? []).includes(app.kind)) {
|
|
134
|
+
const allowed = [...composedBy].filter(([, kinds]) => kinds.includes(app.kind)).map(([protocol]) => protocol).sort(byCodeUnit);
|
|
135
|
+
const transports = allowed.length ? `${allowed.join(', ')} transports only` : 'no transport module';
|
|
136
|
+
report(root, node, `App ${app.name} is of kind ${app.kind} and lists ${module.name}, a ${place.protocol} transport module; an app of kind ${app.kind} composes ${transports}.`, { app: app.name, module: module.name, protocol: place.protocol });
|
|
114
137
|
}
|
|
138
|
+
}
|
|
115
139
|
|
|
116
|
-
|
|
140
|
+
function inspectModuleDeclarations(checker, target, node, root, app, scan, seen) {
|
|
141
|
+
const { moduleMap, kit, placeOf, composedBy, report } = scan;
|
|
117
142
|
let references = 0;
|
|
118
|
-
for (const
|
|
119
|
-
const
|
|
120
|
-
if (!
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const target = call ? node.expression.expression : node;
|
|
127
|
-
for (const declaration of kit.declarationsOf(checker, target)) {
|
|
128
|
-
const module = modules.get(declaration);
|
|
129
|
-
if (!module) continue;
|
|
130
|
-
const place = placeOf(module.file.rel);
|
|
131
|
-
if (!place || seen.has(node)) continue;
|
|
132
|
-
seen.add(node);
|
|
133
|
-
references += 1;
|
|
134
|
-
if (place.composed) {
|
|
135
|
-
const rootModule = `${place.root}/${place.feature}.module.ts`;
|
|
136
|
-
if (!place.composed.composedBy.includes(app.kind)) report(root, node, `App ${app.name} is of kind ${app.kind} and lists ${module.name} from ${place.root}; only an app of kind ${place.composed.composedBy.join(', ')} composes that root.`, { app: app.name, module: module.name });
|
|
137
|
-
else if (module.file.rel !== rootModule) report(root, node, `App ${app.name} lists ${module.name}, a group module inside ${place.root}; the app lists the root module (${rootModule}), which imports every group module.`, { app: app.name, module: module.name });
|
|
138
|
-
} else if (place.protocol === null) {
|
|
139
|
-
report(root, node, `App ${app.name} lists ${module.name}, the application module of feature ${place.feature}; apps import only transport modules (${place.feature}-<protocol>.module.ts), which import the application module themselves.`, { app: app.name, module: module.name });
|
|
140
|
-
} else if (!(composedBy.get(place.protocol) ?? []).includes(app.kind)) {
|
|
141
|
-
const allowed = [...composedBy].filter(([, kinds]) => kinds.includes(app.kind)).map(([protocol]) => protocol).sort();
|
|
142
|
-
report(root, node, `App ${app.name} is of kind ${app.kind} and lists ${module.name}, a ${place.protocol} transport module; an app of kind ${app.kind} composes ${allowed.length ? `${allowed.join(', ')} transports only` : 'no transport module'}.`, { app: app.name, module: module.name, protocol: place.protocol });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
kit.walk(root.sourceFile, node => {
|
|
147
|
-
if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === 'imports' && ts.isArrayLiteralExpression(node.initializer)) {
|
|
148
|
-
for (const element of node.initializer.elements) judge(element);
|
|
149
|
-
} else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register') judge(node);
|
|
150
|
-
return true;
|
|
151
|
-
});
|
|
143
|
+
for (const declaration of kit.declarationsOf(checker, target)) {
|
|
144
|
+
const module = moduleMap.get(declaration);
|
|
145
|
+
if (!module) continue;
|
|
146
|
+
const place = placeOf(module.file.rel);
|
|
147
|
+
if (!place || seen.has(node)) continue;
|
|
148
|
+
seen.add(node);
|
|
149
|
+
references += 1;
|
|
150
|
+
reportModuleReference(root, node, app, module, place, composedBy, report);
|
|
152
151
|
}
|
|
152
|
+
return references;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function inspectAppRoot(app, scan) {
|
|
156
|
+
const { graph, kit, ts } = scan;
|
|
157
|
+
const root = graph.files.get(`apps/${app.name}/src/${APP_ROOT_FILE}`);
|
|
158
|
+
if (!root) return 0;
|
|
159
|
+
const checker = kit.checkerOf(root.sourceFile);
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
let references = 0;
|
|
162
|
+
const judge = node => {
|
|
163
|
+
const call = ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register';
|
|
164
|
+
if (ts.isCallExpression(node) && !call) return;
|
|
165
|
+
const target = call ? node.expression.expression : node;
|
|
166
|
+
references += inspectModuleDeclarations(checker, target, node, root, app, scan, seen);
|
|
167
|
+
};
|
|
168
|
+
kit.walk(root.sourceFile, node => {
|
|
169
|
+
if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === 'imports' && ts.isArrayLiteralExpression(node.initializer)) {
|
|
170
|
+
for (const element of node.initializer.elements) judge(element);
|
|
171
|
+
} else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'register') judge(node);
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
174
|
+
return references;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function appModuleReferences(apps, scan) {
|
|
178
|
+
let references = 0;
|
|
179
|
+
for (const app of apps) references += inspectAppRoot(app, scan);
|
|
180
|
+
return references;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function checkModulePerTransport(input) {
|
|
184
|
+
const { config, graph } = input;
|
|
185
|
+
const kit = machineKit(input);
|
|
186
|
+
const { ts, resolver } = kit;
|
|
187
|
+
const violations = [];
|
|
188
|
+
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
189
|
+
const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
|
|
190
|
+
|
|
191
|
+
const placeOf = rel => modulePlaceOf(resolver, rel);
|
|
192
|
+
const composedModuleOf = composedModulePath;
|
|
193
|
+
|
|
194
|
+
const composedBy = composedByProtocol(resolver);
|
|
195
|
+
|
|
196
|
+
const tree = treeOf(config.root);
|
|
197
|
+
const transportModules = reportTransportFileShapes(tree, placeOf, plain);
|
|
198
|
+
|
|
199
|
+
const modules = collectFeatureModules(graph, kit, ts, placeOf, composedModuleOf, report);
|
|
200
|
+
|
|
201
|
+
const references = appModuleReferences(resolver.repo.apps, { graph, kit, ts, moduleMap: modules, placeOf, composedBy, report });
|
|
153
202
|
return { violations, coverage: { status: 'checked', modules: modules.size, transportModules, appReferences: references } };
|
|
154
203
|
}
|
|
@@ -13,64 +13,72 @@ const HOOK_KEYS = new Set(['id', 'path', 'export', 'kind', 'resultBinding', 'ide
|
|
|
13
13
|
const IDENTITY_KEYS = new Set(['id', 'binding', 'gatesRequest', 'resource']);
|
|
14
14
|
function relativeSource(repository, value, label) {
|
|
15
15
|
repository = path.resolve(repository);
|
|
16
|
-
if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw Error(`${label} must be a non-empty repository-relative source path.`);
|
|
16
|
+
if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) throw new Error(`${label} must be a non-empty repository-relative source path.`);
|
|
17
17
|
const relative = slash(value.trim()).replace(/^\.\//, '');
|
|
18
18
|
if (relative.split('/').includes('..') || !SOURCE.test(relative) || /\.d\.[cm]?[jt]s$/i.test(relative)) {
|
|
19
|
-
throw Error(`${label} must be a production TypeScript or JavaScript source path.`);
|
|
19
|
+
throw new Error(`${label} must be a production TypeScript or JavaScript source path.`);
|
|
20
20
|
}
|
|
21
21
|
const absolute = path.resolve(repository, ...relative.split('/'));
|
|
22
|
-
if (!isInside(repository, absolute)) throw Error(`${label} must stay inside the repository.`);
|
|
22
|
+
if (!isInside(repository, absolute)) throw new Error(`${label} must stay inside the repository.`);
|
|
23
23
|
for (let current = absolute; current !== repository; current = path.dirname(current)) {
|
|
24
24
|
const stat = fs.lstatSync(current);
|
|
25
|
-
if (stat.isSymbolicLink()) throw Error(`${label} must not cross a symbolic-link or junction ancestor.`);
|
|
25
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} must not cross a symbolic-link or junction ancestor.`);
|
|
26
26
|
}
|
|
27
27
|
const stat = fs.lstatSync(absolute);
|
|
28
|
-
if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw Error(`${label} must be a regular source file inside the repository.`);
|
|
28
|
+
if (!stat.isFile() || !isInside(canonical(repository), canonical(absolute))) throw new Error(`${label} must be a regular source file inside the repository.`);
|
|
29
29
|
return relative;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const CONTRACT_LABEL = 'package.json#starci.codePatterns.next.dataLifecycle';
|
|
33
|
+
|
|
34
|
+
function parseSwr(swr) {
|
|
35
|
+
exactKeys(swr, SWR_KEYS, 'dataLifecycle.swr');
|
|
36
|
+
if (swr.package !== 'swr' || !Number.isInteger(swr.major) || swr.major < 1) {
|
|
37
|
+
throw new Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseIdentity(identity, identityLabel, seen) {
|
|
42
|
+
exactKeys(identity, IDENTITY_KEYS, identityLabel);
|
|
43
|
+
if (typeof identity.id !== 'string' || !identity.id.trim() || seen.ids.has(identity.id.trim())) throw new Error(`${identityLabel}.id must be unique and non-empty.`);
|
|
44
|
+
if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || seen.bindings.has(identity.binding)) throw new Error(`${identityLabel}.binding must be a unique identifier or property path.`);
|
|
45
|
+
if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw new Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
|
|
46
|
+
seen.ids.add(identity.id.trim());
|
|
47
|
+
seen.bindings.add(identity.binding);
|
|
48
|
+
return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseHook(config, hook, label, ids) {
|
|
52
|
+
exactKeys(hook, HOOK_KEYS, label);
|
|
53
|
+
if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw new Error(`${label}.id must be unique and non-empty.`);
|
|
54
|
+
ids.add(hook.id.trim());
|
|
55
|
+
const source = relativeSource(config.root, hook.path, `${label}.path`);
|
|
56
|
+
if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw new Error(`${label}.export must name one exported use* hook.`);
|
|
57
|
+
if (!['query', 'mutation'].includes(hook.kind)) throw new Error(`${label}.kind must be query or mutation.`);
|
|
58
|
+
if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
|
|
59
|
+
throw new Error(`${label}.resultBinding must be one local identifier.`);
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(hook.identities)) throw new Error(`${label}.identities must be an array.`);
|
|
62
|
+
const seen = { ids: new Set(), bindings: new Set() };
|
|
63
|
+
const identities = hook.identities.map((identity, identityIndex) => parseIdentity(identity, `${label}.identities[${identityIndex}]`, seen));
|
|
64
|
+
if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw new Error(`${label} mutation must declare at least one resource identity.`);
|
|
65
|
+
return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
|
|
66
|
+
...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
|
|
67
|
+
}
|
|
68
|
+
|
|
32
69
|
export function parseContract(config) {
|
|
33
70
|
// The contract is declared in the app's one package.json (the app root; config.root is the fe side folder).
|
|
34
71
|
const manifest = JSON.parse(fs.readFileSync(path.join(config.packageRoot ?? config.root, 'package.json'), 'utf8'));
|
|
35
72
|
const next = manifest?.starci?.codePatterns?.next;
|
|
36
73
|
const value = next?.dataLifecycle;
|
|
37
74
|
if (value === undefined) return null;
|
|
38
|
-
if (next.schema !== 'starci/next-code-pattern-contract@1') throw Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
|
|
39
|
-
exactKeys(value, TOP_KEYS,
|
|
40
|
-
if (value.schema !== CONTRACT_SCHEMA) throw Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
throw Error('dataLifecycle.swr must bind package swr and one positive integer major version.');
|
|
44
|
-
}
|
|
45
|
-
if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
|
|
75
|
+
if (next.schema !== 'starci/next-code-pattern-contract@1') throw new Error('package.json#starci.codePatterns.next must use starci/next-code-pattern-contract@1.');
|
|
76
|
+
exactKeys(value, TOP_KEYS, CONTRACT_LABEL);
|
|
77
|
+
if (value.schema !== CONTRACT_SCHEMA) throw new Error(`dataLifecycle.schema must be ${CONTRACT_SCHEMA}.`);
|
|
78
|
+
parseSwr(value.swr);
|
|
79
|
+
if (!Array.isArray(value.hooks) || value.hooks.length === 0) throw new Error('dataLifecycle.hooks must contain at least one declared lifecycle call.');
|
|
46
80
|
const ids = new Set();
|
|
47
|
-
const hooks = value.hooks.map((hook, index) => {
|
|
48
|
-
const label = `dataLifecycle.hooks[${index}]`;
|
|
49
|
-
exactKeys(hook, HOOK_KEYS, label);
|
|
50
|
-
if (typeof hook.id !== 'string' || !hook.id.trim() || ids.has(hook.id.trim())) throw Error(`${label}.id must be unique and non-empty.`);
|
|
51
|
-
ids.add(hook.id.trim());
|
|
52
|
-
const source = relativeSource(config.root, hook.path, `${label}.path`);
|
|
53
|
-
if (typeof hook.export !== 'string' || !/^use[A-Z0-9_$][\w$]*$/.test(hook.export)) throw Error(`${label}.export must name one exported use* hook.`);
|
|
54
|
-
if (!['query', 'mutation'].includes(hook.kind)) throw Error(`${label}.kind must be query or mutation.`);
|
|
55
|
-
if (hook.resultBinding !== undefined && (typeof hook.resultBinding !== 'string' || !IDENTIFIER.test(hook.resultBinding))) {
|
|
56
|
-
throw Error(`${label}.resultBinding must be one local identifier.`);
|
|
57
|
-
}
|
|
58
|
-
if (!Array.isArray(hook.identities)) throw Error(`${label}.identities must be an array.`);
|
|
59
|
-
const identityIds = new Set(), bindings = new Set();
|
|
60
|
-
const identities = hook.identities.map((identity, identityIndex) => {
|
|
61
|
-
const identityLabel = `${label}.identities[${identityIndex}]`;
|
|
62
|
-
exactKeys(identity, IDENTITY_KEYS, identityLabel);
|
|
63
|
-
if (typeof identity.id !== 'string' || !identity.id.trim() || identityIds.has(identity.id.trim())) throw Error(`${identityLabel}.id must be unique and non-empty.`);
|
|
64
|
-
if (typeof identity.binding !== 'string' || !BINDING.test(identity.binding) || bindings.has(identity.binding)) throw Error(`${identityLabel}.binding must be a unique identifier or property path.`);
|
|
65
|
-
if (typeof identity.gatesRequest !== 'boolean' || typeof identity.resource !== 'boolean') throw Error(`${identityLabel} must explicitly declare gatesRequest and resource booleans.`);
|
|
66
|
-
identityIds.add(identity.id.trim());
|
|
67
|
-
bindings.add(identity.binding);
|
|
68
|
-
return { id: identity.id.trim(), binding: identity.binding, gatesRequest: identity.gatesRequest, resource: identity.resource };
|
|
69
|
-
});
|
|
70
|
-
if (hook.kind === 'mutation' && !identities.some(identity => identity.resource)) throw Error(`${label} mutation must declare at least one resource identity.`);
|
|
71
|
-
return { id: hook.id.trim(), path: source, export: hook.export, kind: hook.kind,
|
|
72
|
-
...(hook.resultBinding === undefined ? {} : { resultBinding: hook.resultBinding }), identities };
|
|
73
|
-
});
|
|
81
|
+
const hooks = value.hooks.map((hook, index) => parseHook(config, hook, `dataLifecycle.hooks[${index}]`, ids));
|
|
74
82
|
return { schema: value.schema, swr: { package: 'swr', major: value.swr.major }, hooks };
|
|
75
83
|
}
|
|
76
84
|
|
|
@@ -79,7 +87,7 @@ export function installedSWR(repository) {
|
|
|
79
87
|
let manifest;
|
|
80
88
|
try { manifest = require.resolve('swr/package.json'); } catch {
|
|
81
89
|
let entry;
|
|
82
|
-
try { entry = require.resolve('swr'); } catch { throw Error('Installed swr package is unavailable.'); }
|
|
90
|
+
try { entry = require.resolve('swr'); } catch { throw new Error('Installed swr package is unavailable.'); }
|
|
83
91
|
let current = path.dirname(entry);
|
|
84
92
|
while (isInside(repository, current) && current !== repository) {
|
|
85
93
|
const candidate = path.join(current, 'package.json');
|
|
@@ -87,10 +95,10 @@ export function installedSWR(repository) {
|
|
|
87
95
|
current = path.dirname(current);
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
|
-
if (!manifest) throw Error('Installed swr package.json is unavailable.');
|
|
98
|
+
if (!manifest) throw new Error('Installed swr package.json is unavailable.');
|
|
91
99
|
const actualManifest = canonical(manifest);
|
|
92
100
|
const parsed = JSON.parse(fs.readFileSync(actualManifest, 'utf8'));
|
|
93
101
|
const major = Number.parseInt(String(parsed.version ?? '').split('.')[0], 10);
|
|
94
|
-
if (parsed.name !== 'swr' || !Number.isInteger(major)) throw Error('Installed swr package identity or version is invalid.');
|
|
102
|
+
if (parsed.name !== 'swr' || !Number.isInteger(major)) throw new Error('Installed swr package identity or version is invalid.');
|
|
95
103
|
return { name: parsed.name, version: parsed.version, major, root: canonical(path.dirname(actualManifest)) };
|
|
96
104
|
}
|