@endora-commerce/cli 0.100.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/bin/endora.d.ts +28 -0
- package/dist/bin/endora.d.ts.map +1 -0
- package/dist/bin/endora.js +926 -0
- package/dist/bin/endora.js.map +1 -0
- package/dist/check/estate.d.ts +189 -0
- package/dist/check/estate.d.ts.map +1 -0
- package/dist/check/estate.js +1037 -0
- package/dist/check/estate.js.map +1 -0
- package/dist/check/hosts.d.ts +25 -0
- package/dist/check/hosts.d.ts.map +1 -0
- package/dist/check/hosts.js +1347 -0
- package/dist/check/hosts.js.map +1 -0
- package/dist/check/index.d.ts +60 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +111 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/layout.d.ts +136 -0
- package/dist/check/layout.d.ts.map +1 -0
- package/dist/check/layout.js +262 -0
- package/dist/check/layout.js.map +1 -0
- package/dist/check/ledger.d.ts +98 -0
- package/dist/check/ledger.d.ts.map +1 -0
- package/dist/check/ledger.js +173 -0
- package/dist/check/ledger.js.map +1 -0
- package/dist/check/peer-owners.d.ts +116 -0
- package/dist/check/peer-owners.d.ts.map +1 -0
- package/dist/check/peer-owners.js +225 -0
- package/dist/check/peer-owners.js.map +1 -0
- package/dist/check/report.d.ts +33 -0
- package/dist/check/report.d.ts.map +1 -0
- package/dist/check/report.js +107 -0
- package/dist/check/report.js.map +1 -0
- package/dist/check/run.d.ts +147 -0
- package/dist/check/run.d.ts.map +1 -0
- package/dist/check/run.js +111 -0
- package/dist/check/run.js.map +1 -0
- package/dist/checks.d.ts +17 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +17 -0
- package/dist/checks.js.map +1 -0
- package/dist/dev/index.d.ts +83 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +298 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/generate/divergence.d.ts +38 -0
- package/dist/generate/divergence.d.ts.map +1 -0
- package/dist/generate/divergence.js +237 -0
- package/dist/generate/divergence.js.map +1 -0
- package/dist/generate/index.d.ts +90 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +369 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/declaration.d.ts +18 -0
- package/dist/inputs/declaration.d.ts.map +1 -0
- package/dist/inputs/declaration.js +64 -0
- package/dist/inputs/declaration.js.map +1 -0
- package/dist/inputs/env-file.d.ts +73 -0
- package/dist/inputs/env-file.d.ts.map +1 -0
- package/dist/inputs/env-file.js +134 -0
- package/dist/inputs/env-file.js.map +1 -0
- package/dist/inputs/prompt.d.ts +21 -0
- package/dist/inputs/prompt.d.ts.map +1 -0
- package/dist/inputs/prompt.js +59 -0
- package/dist/inputs/prompt.js.map +1 -0
- package/dist/inputs/resolve.d.ts +163 -0
- package/dist/inputs/resolve.d.ts.map +1 -0
- package/dist/inputs/resolve.js +290 -0
- package/dist/inputs/resolve.js.map +1 -0
- package/dist/install/host.d.ts +27 -0
- package/dist/install/host.d.ts.map +1 -0
- package/dist/install/host.js +90 -0
- package/dist/install/host.js.map +1 -0
- package/dist/install/index.d.ts +173 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +793 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/wizard.d.ts +144 -0
- package/dist/install/wizard.d.ts.map +1 -0
- package/dist/install/wizard.js +362 -0
- package/dist/install/wizard.js.map +1 -0
- package/dist/lib/admin-artefacts.d.ts +70 -0
- package/dist/lib/admin-artefacts.d.ts.map +1 -0
- package/dist/lib/admin-artefacts.js +354 -0
- package/dist/lib/admin-artefacts.js.map +1 -0
- package/dist/lib/admin-surfaces.d.ts +298 -0
- package/dist/lib/admin-surfaces.d.ts.map +1 -0
- package/dist/lib/admin-surfaces.js +669 -0
- package/dist/lib/admin-surfaces.js.map +1 -0
- package/dist/lib/delegated-composer.d.ts +85 -0
- package/dist/lib/delegated-composer.d.ts.map +1 -0
- package/dist/lib/delegated-composer.js +241 -0
- package/dist/lib/delegated-composer.js.map +1 -0
- package/dist/lib/divergence-artefacts.d.ts +305 -0
- package/dist/lib/divergence-artefacts.d.ts.map +1 -0
- package/dist/lib/divergence-artefacts.js +828 -0
- package/dist/lib/divergence-artefacts.js.map +1 -0
- package/dist/lib/divergence.d.ts +337 -0
- package/dist/lib/divergence.d.ts.map +1 -0
- package/dist/lib/divergence.js +1005 -0
- package/dist/lib/divergence.js.map +1 -0
- package/dist/lib/docs-artefacts.d.ts +395 -0
- package/dist/lib/docs-artefacts.d.ts.map +1 -0
- package/dist/lib/docs-artefacts.js +781 -0
- package/dist/lib/docs-artefacts.js.map +1 -0
- package/dist/lib/emitted-exports.d.ts +21 -0
- package/dist/lib/emitted-exports.d.ts.map +1 -0
- package/dist/lib/emitted-exports.js +96 -0
- package/dist/lib/emitted-exports.js.map +1 -0
- package/dist/lib/emitted-freshness.d.ts +112 -0
- package/dist/lib/emitted-freshness.d.ts.map +1 -0
- package/dist/lib/emitted-freshness.js +288 -0
- package/dist/lib/emitted-freshness.js.map +1 -0
- package/dist/lib/entity-index-artefact.d.ts +95 -0
- package/dist/lib/entity-index-artefact.d.ts.map +1 -0
- package/dist/lib/entity-index-artefact.js +210 -0
- package/dist/lib/entity-index-artefact.js.map +1 -0
- package/dist/lib/instance-build-inputs.d.ts +108 -0
- package/dist/lib/instance-build-inputs.d.ts.map +1 -0
- package/dist/lib/instance-build-inputs.js +165 -0
- package/dist/lib/instance-build-inputs.js.map +1 -0
- package/dist/lib/module-docs.d.ts +473 -0
- package/dist/lib/module-docs.d.ts.map +1 -0
- package/dist/lib/module-docs.js +711 -0
- package/dist/lib/module-docs.js.map +1 -0
- package/dist/lib/module-package-subpaths.d.ts +56 -0
- package/dist/lib/module-package-subpaths.d.ts.map +1 -0
- package/dist/lib/module-package-subpaths.js +223 -0
- package/dist/lib/module-package-subpaths.js.map +1 -0
- package/dist/lib/module-packages.d.ts +200 -0
- package/dist/lib/module-packages.d.ts.map +1 -0
- package/dist/lib/module-packages.js +580 -0
- package/dist/lib/module-packages.js.map +1 -0
- package/dist/lib/module-population.d.ts +129 -0
- package/dist/lib/module-population.d.ts.map +1 -0
- package/dist/lib/module-population.js +172 -0
- package/dist/lib/module-population.js.map +1 -0
- package/dist/lib/module-roots.d.ts +337 -0
- package/dist/lib/module-roots.d.ts.map +1 -0
- package/dist/lib/module-roots.js +586 -0
- package/dist/lib/module-roots.js.map +1 -0
- package/dist/lib/nested-checkouts.d.ts +33 -0
- package/dist/lib/nested-checkouts.d.ts.map +1 -0
- package/dist/lib/nested-checkouts.js +160 -0
- package/dist/lib/nested-checkouts.js.map +1 -0
- package/dist/lib/platform-root.d.ts +43 -0
- package/dist/lib/platform-root.d.ts.map +1 -0
- package/dist/lib/platform-root.js +134 -0
- package/dist/lib/platform-root.js.map +1 -0
- package/dist/lib/platform-surface.d.ts +235 -0
- package/dist/lib/platform-surface.d.ts.map +1 -0
- package/dist/lib/platform-surface.js +393 -0
- package/dist/lib/platform-surface.js.map +1 -0
- package/dist/lib/port-registrations.d.ts +223 -0
- package/dist/lib/port-registrations.d.ts.map +1 -0
- package/dist/lib/port-registrations.js +532 -0
- package/dist/lib/port-registrations.js.map +1 -0
- package/dist/lib/read-size.d.ts +154 -0
- package/dist/lib/read-size.d.ts.map +1 -0
- package/dist/lib/read-size.js +182 -0
- package/dist/lib/read-size.js.map +1 -0
- package/dist/lib/registration-owners.d.ts +79 -0
- package/dist/lib/registration-owners.d.ts.map +1 -0
- package/dist/lib/registration-owners.js +77 -0
- package/dist/lib/registration-owners.js.map +1 -0
- package/dist/lib/release-index.d.ts +53 -0
- package/dist/lib/release-index.d.ts.map +1 -0
- package/dist/lib/release-index.js +162 -0
- package/dist/lib/release-index.js.map +1 -0
- package/dist/lib/repeating-timers.d.ts +79 -0
- package/dist/lib/repeating-timers.d.ts.map +1 -0
- package/dist/lib/repeating-timers.js +189 -0
- package/dist/lib/repeating-timers.js.map +1 -0
- package/dist/lib/source-text.d.ts +34 -0
- package/dist/lib/source-text.d.ts.map +1 -0
- package/dist/lib/source-text.js +80 -0
- package/dist/lib/source-text.js.map +1 -0
- package/dist/lib/specifiers.d.ts +20 -0
- package/dist/lib/specifiers.d.ts.map +1 -0
- package/dist/lib/specifiers.js +130 -0
- package/dist/lib/specifiers.js.map +1 -0
- package/dist/lib/sql-tables.d.ts +166 -0
- package/dist/lib/sql-tables.d.ts.map +1 -0
- package/dist/lib/sql-tables.js +464 -0
- package/dist/lib/sql-tables.js.map +1 -0
- package/dist/lib/switchable-modules.d.ts +54 -0
- package/dist/lib/switchable-modules.d.ts.map +1 -0
- package/dist/lib/switchable-modules.js +104 -0
- package/dist/lib/switchable-modules.js.map +1 -0
- package/dist/lib/tailwind-sources.d.ts +136 -0
- package/dist/lib/tailwind-sources.d.ts.map +1 -0
- package/dist/lib/tailwind-sources.js +307 -0
- package/dist/lib/tailwind-sources.js.map +1 -0
- package/dist/lib/ui-layer.d.ts +54 -0
- package/dist/lib/ui-layer.d.ts.map +1 -0
- package/dist/lib/ui-layer.js +57 -0
- package/dist/lib/ui-layer.js.map +1 -0
- package/dist/lib/workspace-packages.d.ts +186 -0
- package/dist/lib/workspace-packages.d.ts.map +1 -0
- package/dist/lib/workspace-packages.js +351 -0
- package/dist/lib/workspace-packages.js.map +1 -0
- package/dist/new-instance/deploy.d.ts +211 -0
- package/dist/new-instance/deploy.d.ts.map +1 -0
- package/dist/new-instance/deploy.js +1381 -0
- package/dist/new-instance/deploy.js.map +1 -0
- package/dist/new-instance/docs-toolchain.d.ts +66 -0
- package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
- package/dist/new-instance/docs-toolchain.js +69 -0
- package/dist/new-instance/docs-toolchain.js.map +1 -0
- package/dist/new-instance/host.d.ts +124 -0
- package/dist/new-instance/host.d.ts.map +1 -0
- package/dist/new-instance/host.js +276 -0
- package/dist/new-instance/host.js.map +1 -0
- package/dist/new-instance/index.d.ts +118 -0
- package/dist/new-instance/index.d.ts.map +1 -0
- package/dist/new-instance/index.js +567 -0
- package/dist/new-instance/index.js.map +1 -0
- package/dist/new-instance/modules.d.ts +188 -0
- package/dist/new-instance/modules.d.ts.map +1 -0
- package/dist/new-instance/modules.js +392 -0
- package/dist/new-instance/modules.js.map +1 -0
- package/dist/new-instance/template.d.ts +505 -0
- package/dist/new-instance/template.d.ts.map +1 -0
- package/dist/new-instance/template.js +1886 -0
- package/dist/new-instance/template.js.map +1 -0
- package/dist/new-module/emit.d.ts +67 -0
- package/dist/new-module/emit.d.ts.map +1 -0
- package/dist/new-module/emit.js +1393 -0
- package/dist/new-module/emit.js.map +1 -0
- package/dist/new-module/host.d.ts +67 -0
- package/dist/new-module/host.d.ts.map +1 -0
- package/dist/new-module/host.js +224 -0
- package/dist/new-module/host.js.map +1 -0
- package/dist/new-module/index.d.ts +32 -0
- package/dist/new-module/index.d.ts.map +1 -0
- package/dist/new-module/index.js +193 -0
- package/dist/new-module/index.js.map +1 -0
- package/dist/new-module/spec.d.ts +188 -0
- package/dist/new-module/spec.d.ts.map +1 -0
- package/dist/new-module/spec.js +404 -0
- package/dist/new-module/spec.js.map +1 -0
- package/dist/new-module/text.d.ts +15 -0
- package/dist/new-module/text.d.ts.map +1 -0
- package/dist/new-module/text.js +22 -0
- package/dist/new-module/text.js.map +1 -0
- package/dist/new-storefront/dockerfile.d.ts +20 -0
- package/dist/new-storefront/dockerfile.d.ts.map +1 -0
- package/dist/new-storefront/dockerfile.js +131 -0
- package/dist/new-storefront/dockerfile.js.map +1 -0
- package/dist/new-storefront/gitignore.d.ts +23 -0
- package/dist/new-storefront/gitignore.d.ts.map +1 -0
- package/dist/new-storefront/gitignore.js +39 -0
- package/dist/new-storefront/gitignore.js.map +1 -0
- package/dist/new-storefront/index.d.ts +93 -0
- package/dist/new-storefront/index.d.ts.map +1 -0
- package/dist/new-storefront/index.js +329 -0
- package/dist/new-storefront/index.js.map +1 -0
- package/dist/new-storefront/npmrc.d.ts +98 -0
- package/dist/new-storefront/npmrc.d.ts.map +1 -0
- package/dist/new-storefront/npmrc.js +217 -0
- package/dist/new-storefront/npmrc.js.map +1 -0
- package/dist/new-storefront/reference.d.ts +189 -0
- package/dist/new-storefront/reference.d.ts.map +1 -0
- package/dist/new-storefront/reference.js +430 -0
- package/dist/new-storefront/reference.js.map +1 -0
- package/dist/new-storefront/rewrite.d.ts +171 -0
- package/dist/new-storefront/rewrite.d.ts.map +1 -0
- package/dist/new-storefront/rewrite.js +701 -0
- package/dist/new-storefront/rewrite.js.map +1 -0
- package/dist/release-index.json +284 -0
- package/dist/rules/action-route-permissions.d.ts +141 -0
- package/dist/rules/action-route-permissions.d.ts.map +1 -0
- package/dist/rules/action-route-permissions.js +556 -0
- package/dist/rules/action-route-permissions.js.map +1 -0
- package/dist/rules/bundle-pairing.d.ts +74 -0
- package/dist/rules/bundle-pairing.d.ts.map +1 -0
- package/dist/rules/bundle-pairing.js +281 -0
- package/dist/rules/bundle-pairing.js.map +1 -0
- package/dist/rules/channel-resolution.d.ts +17 -0
- package/dist/rules/channel-resolution.d.ts.map +1 -0
- package/dist/rules/channel-resolution.js +382 -0
- package/dist/rules/channel-resolution.js.map +1 -0
- package/dist/rules/command-coverage.d.ts +210 -0
- package/dist/rules/command-coverage.d.ts.map +1 -0
- package/dist/rules/command-coverage.js +714 -0
- package/dist/rules/command-coverage.js.map +1 -0
- package/dist/rules/container-imports.d.ts +60 -0
- package/dist/rules/container-imports.d.ts.map +1 -0
- package/dist/rules/container-imports.js +158 -0
- package/dist/rules/container-imports.js.map +1 -0
- package/dist/rules/default-language-prose.d.ts +212 -0
- package/dist/rules/default-language-prose.d.ts.map +1 -0
- package/dist/rules/default-language-prose.js +710 -0
- package/dist/rules/default-language-prose.js.map +1 -0
- package/dist/rules/diacritic-folds.d.ts +238 -0
- package/dist/rules/diacritic-folds.d.ts.map +1 -0
- package/dist/rules/diacritic-folds.js +681 -0
- package/dist/rules/diacritic-folds.js.map +1 -0
- package/dist/rules/entity-tenant-classification.d.ts +171 -0
- package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
- package/dist/rules/entity-tenant-classification.js +323 -0
- package/dist/rules/entity-tenant-classification.js.map +1 -0
- package/dist/rules/entry-presence.d.ts +142 -0
- package/dist/rules/entry-presence.d.ts.map +1 -0
- package/dist/rules/entry-presence.js +339 -0
- package/dist/rules/entry-presence.js.map +1 -0
- package/dist/rules/entry-scope.d.ts +91 -0
- package/dist/rules/entry-scope.d.ts.map +1 -0
- package/dist/rules/entry-scope.js +404 -0
- package/dist/rules/entry-scope.js.map +1 -0
- package/dist/rules/env-inputs.d.ts +222 -0
- package/dist/rules/env-inputs.d.ts.map +1 -0
- package/dist/rules/env-inputs.js +951 -0
- package/dist/rules/env-inputs.js.map +1 -0
- package/dist/rules/kernel-boundary.d.ts +37 -0
- package/dist/rules/kernel-boundary.d.ts.map +1 -0
- package/dist/rules/kernel-boundary.js +195 -0
- package/dist/rules/kernel-boundary.js.map +1 -0
- package/dist/rules/nul-bytes.d.ts +233 -0
- package/dist/rules/nul-bytes.d.ts.map +1 -0
- package/dist/rules/nul-bytes.js +332 -0
- package/dist/rules/nul-bytes.js.map +1 -0
- package/dist/rules/platform-surface.d.ts +479 -0
- package/dist/rules/platform-surface.d.ts.map +1 -0
- package/dist/rules/platform-surface.js +749 -0
- package/dist/rules/platform-surface.js.map +1 -0
- package/dist/rules/port-catches.d.ts +225 -0
- package/dist/rules/port-catches.d.ts.map +1 -0
- package/dist/rules/port-catches.js +1374 -0
- package/dist/rules/port-catches.js.map +1 -0
- package/dist/rules/port-shape.d.ts +213 -0
- package/dist/rules/port-shape.d.ts.map +1 -0
- package/dist/rules/port-shape.js +670 -0
- package/dist/rules/port-shape.js.map +1 -0
- package/dist/rules/queue-names.d.ts +108 -0
- package/dist/rules/queue-names.d.ts.map +1 -0
- package/dist/rules/queue-names.js +395 -0
- package/dist/rules/queue-names.js.map +1 -0
- package/dist/rules/singleton-identity.d.ts +205 -0
- package/dist/rules/singleton-identity.d.ts.map +1 -0
- package/dist/rules/singleton-identity.js +830 -0
- package/dist/rules/singleton-identity.js.map +1 -0
- package/dist/rules/subscribe-seam.d.ts +121 -0
- package/dist/rules/subscribe-seam.d.ts.map +1 -0
- package/dist/rules/subscribe-seam.js +594 -0
- package/dist/rules/subscribe-seam.js.map +1 -0
- package/dist/rules/transaction-context.d.ts +40 -0
- package/dist/rules/transaction-context.d.ts.map +1 -0
- package/dist/rules/transaction-context.js +294 -0
- package/dist/rules/transaction-context.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI check — **every environment value a running Endora reads is declared, and
|
|
3
|
+
* every declared input is read** (`specs/117-instance-bring-up/` FR-003;
|
|
4
|
+
* `contracts/environment-inputs.md` §3).
|
|
5
|
+
*
|
|
6
|
+
* ## Why a check of its own
|
|
7
|
+
*
|
|
8
|
+
* No existing check's population contains a `process.env` read.
|
|
9
|
+
* `check:module-boundary` reads import specifiers and SQL tables,
|
|
10
|
+
* `check:admin-zones` reads zone names and `useTranslation` scopes,
|
|
11
|
+
* `i18n:hardcoded` reads JSX in `.tsx`, `check:default-language-prose` reads
|
|
12
|
+
* prose literals, `check:entry-scope` reads entry sites. Widening one of them
|
|
13
|
+
* would be issue #244's shape arriving through the repair: a rule's stated
|
|
14
|
+
* subject enlarged past what its walk can see.
|
|
15
|
+
*
|
|
16
|
+
* ## Both directions, and the second one is the one that would rot
|
|
17
|
+
*
|
|
18
|
+
* `undeclared-input` is a variable a client's `.env` will be short of — loud,
|
|
19
|
+
* eventually, as a failed boot nobody can attribute. `unread-input` is an
|
|
20
|
+
* operator being asked for a value that changes nothing, and **nothing else in
|
|
21
|
+
* the tree would ever notice it**: the read went away, the declaration stayed,
|
|
22
|
+
* and the prompt goes on asking. A one-way check would leave the declaration
|
|
23
|
+
* accumulating variables the platform stopped reading three features ago,
|
|
24
|
+
* which is how a contract stops being believed.
|
|
25
|
+
*
|
|
26
|
+
* ## The population is roots, never "the trees that happen to declare"
|
|
27
|
+
*
|
|
28
|
+
* §R3.4. Inferring the population from the presence of declarations is issue
|
|
29
|
+
* #244's shape exactly: a tree that declares nothing and reads five variables
|
|
30
|
+
* would be perfectly clean. So the roots are handed in by the host, one per
|
|
31
|
+
* member of `ENVIRONMENT_CONSUMERS` — the contract package's own enum, which is
|
|
32
|
+
* authored nowhere near this check and does not move when a declaration does —
|
|
33
|
+
* and a consumer that contributed no file is a refusal rather than a consumer
|
|
34
|
+
* with nothing wrong in it.
|
|
35
|
+
*
|
|
36
|
+
* ## Two read dialects, because the tree has two
|
|
37
|
+
*
|
|
38
|
+
* `process.env.X` and `process.env['X']` for anything Node runs, and
|
|
39
|
+
* `import.meta.env.X` / `import.meta.env['X']` for the admin, which is a Vite
|
|
40
|
+
* SPA and reads **no** `process.env` at all — measured: 0 sites over 294 files.
|
|
41
|
+
* A check that knew only the Node dialect would report the admin's single input
|
|
42
|
+
* as `unread-input` and its actual read as nothing at all, which is a finding
|
|
43
|
+
* about the check dressed as one about the tree.
|
|
44
|
+
*
|
|
45
|
+
* **In the Vite dialect only a prefixed name comes from the environment.**
|
|
46
|
+
* `import.meta.env.DEV`, `PROD`, `MODE`, `SSR` and `BASE_URL` are Vite's own
|
|
47
|
+
* constants, substituted from the build mode: no `.env` can supply one and no
|
|
48
|
+
* operator can be asked for one, so they are not inputs and are not read as
|
|
49
|
+
* such. The discriminator is the {@link VITE_ENVIRONMENT_PREFIX} Vite itself
|
|
50
|
+
* uses to decide the same question, not a list of the built-ins that exist
|
|
51
|
+
* today. The Node dialect has no equivalent exclusion and needs none —
|
|
52
|
+
* `NEXT_RUNTIME` is a real process variable that Next sets, so it is declared
|
|
53
|
+
* like any other, with a sentence saying an operator has nothing to choose.
|
|
54
|
+
*
|
|
55
|
+
* ## A named constant is not a computed name
|
|
56
|
+
*
|
|
57
|
+
* `unresolvable-input-name` is a finding and never a skip (issue #113): read as
|
|
58
|
+
* "declared" it agrees with everything. But the tree's own **correct** idiom
|
|
59
|
+
* writes `process.env[PUBLIC_API_BASE_URL_VAR]`, where the constant is a string
|
|
60
|
+
* literal exported by `storefront/lib/env.mjs` — one spelling of the variable
|
|
61
|
+
* in one place, which is the shape this repository wants and which a naive
|
|
62
|
+
* "computed ⇒ unresolvable" rule would punish on the day it landed. Two of the
|
|
63
|
+
* storefront's own reads are written that way. So an identifier is resolved
|
|
64
|
+
* two hops: a `const` in the same file whose initialiser is a string literal,
|
|
65
|
+
* and an imported binding whose source file — within the walk — exports one.
|
|
66
|
+
* Anything further is `unresolvable-input-name`, which is the fail-closed
|
|
67
|
+
* direction.
|
|
68
|
+
*
|
|
69
|
+
* ## What `foreign-input` covers, and why it is two shapes under one kind
|
|
70
|
+
*
|
|
71
|
+
* A declaration is shipped by an **author** — the platform, one application,
|
|
72
|
+
* or one module — and both shapes are that author declaring something that is
|
|
73
|
+
* not theirs, with one remedy: delete it and let the owner declare it.
|
|
74
|
+
*
|
|
75
|
+
* * `owner` disagreeing with the author that ships the file. The storefront
|
|
76
|
+
* declaring `{ kind: 'platform' }` puts a fact about the platform in a tree
|
|
77
|
+
* a client owns and edits.
|
|
78
|
+
* * one name declared twice **within one author's** declaration. Two entries,
|
|
79
|
+
* two `describes`, and whichever the reader reaches first wins.
|
|
80
|
+
*
|
|
81
|
+
* **Across authors a shared name is expected and is not a finding**, and that
|
|
82
|
+
* distinction is load-bearing. `REVALIDATE_SECRET` is declared by the platform
|
|
83
|
+
* *and* by the storefront, because under D-195 those are two repositories that
|
|
84
|
+
* ship independently and each has to be able to say what it needs. That the two
|
|
85
|
+
* declarations name each other in `consumers` is precisely what §5's cross-tree
|
|
86
|
+
* agreement is derived from; collapsing them into one declaration would leave a
|
|
87
|
+
* scaffolded storefront unable to state its own requirements.
|
|
88
|
+
*
|
|
89
|
+
* ## What it does not judge, stated rather than discovered later
|
|
90
|
+
*
|
|
91
|
+
* * **Whether a value is correct.** That is `endora doctor`'s question and it
|
|
92
|
+
* needs a running environment.
|
|
93
|
+
* * **A module's inputs.** They are declared in `manifest.ts` and land with
|
|
94
|
+
* this feature's Phase 3; until then the module tree is outside this
|
|
95
|
+
* population and the host **prints** that rather than passing over it in
|
|
96
|
+
* silence.
|
|
97
|
+
* * **A read reached through a helper in another file** —
|
|
98
|
+
* `readEnv('DATABASE_URL')`. The walk sees member expressions on
|
|
99
|
+
* `process.env` and `import.meta.env`, not every function that might
|
|
100
|
+
* forward one.
|
|
101
|
+
* * **A `.env.example`.** It is prose, it is stale, and reconciling against
|
|
102
|
+
* it would make a stale file authoritative.
|
|
103
|
+
*/
|
|
104
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
105
|
+
import { join } from 'node:path';
|
|
106
|
+
import { pathToFileURL } from 'node:url';
|
|
107
|
+
import ts from 'typescript';
|
|
108
|
+
import { ENVIRONMENT_CONSUMERS, } from '@endora-commerce/contracts';
|
|
109
|
+
/** The log prefix both hosts print under — one grammar, one spelling. */
|
|
110
|
+
export const PREFIX = '[env-inputs]';
|
|
111
|
+
/** What to do about each kind, printed above the findings of that kind. */
|
|
112
|
+
export const REMEDIES = {
|
|
113
|
+
'undeclared-input': 'This code reads an environment variable nothing declares, so no `.env` written by ' +
|
|
114
|
+
'`endora new instance` or `endora new storefront` will carry it and `endora doctor` ' +
|
|
115
|
+
'cannot tell an operator it is missing. Declare it where the code that reads it lives: ' +
|
|
116
|
+
'the platform in `packages/platform/src/env/index.ts`, an application in its own ' +
|
|
117
|
+
'`environment-inputs.mjs`, a module in its `manifest.ts`.',
|
|
118
|
+
'undeclared-module-input': 'A module reads an environment variable that neither the platform, nor the application ' +
|
|
119
|
+
'tree it runs in, nor this module’s own `manifest.ts` declares — so no `.env` a ' +
|
|
120
|
+
'client writes will carry it and `endora doctor` cannot tell them it is missing. That ' +
|
|
121
|
+
'matters more for a module than for anything else here: a module package ships `dist`, ' +
|
|
122
|
+
'`i18n` and `docs`, and `.env.example` is a file in the platform’s own repository. ' +
|
|
123
|
+
'Declare it in the module’s `env` array, with `owner` naming this module. **Another ' +
|
|
124
|
+
'module’s declaration does not satisfy this read**, deliberately: a client may install ' +
|
|
125
|
+
'this module and not that one, and an input that arrives only with a sibling is an input ' +
|
|
126
|
+
'that is missing exactly when the sibling is.',
|
|
127
|
+
'unread-input': 'This input is declared and nothing reads it, so an operator is asked for a value that ' +
|
|
128
|
+
'changes nothing. Delete the declaration, or restore the read it was written for. For a ' +
|
|
129
|
+
'module the question is asked over that module’s own sources: a sibling reading the ' +
|
|
130
|
+
'name does not make this declaration earned.',
|
|
131
|
+
'module-declares-a-platform-input': 'The platform already declares this variable, in `packages/platform/src/env/index.ts`, ' +
|
|
132
|
+
'and a module’s read of it is satisfied by that declaration. Delete the entry. Seven ' +
|
|
133
|
+
'names — `NODE_ENV`, `BACKEND_ROLE`, `STOREFRONT_BASE_URL`, `PUBLIC_API_BASE_URL`, ' +
|
|
134
|
+
'`BACKEND_PUBLIC_URL`, `REVALIDATE_SECRET`, `SETTINGS_SECRET_ENCRYPTION_KEY` — are read ' +
|
|
135
|
+
'by thirty modules between them, and a module that declares what it *reads* rather than ' +
|
|
136
|
+
'what it *owns* puts one fact in thirty manifests with thirty descriptions. Whichever a ' +
|
|
137
|
+
'reader reaches first wins and the rest drift (D-100).',
|
|
138
|
+
'foreign-input': 'This declaration is not the shipping author’s to make — either its `owner` names ' +
|
|
139
|
+
'somebody else, or the same author declares the name twice. Delete it and let the owner ' +
|
|
140
|
+
'declare it. A name declared by two *different* authors is not this finding: two trees ' +
|
|
141
|
+
'that ship independently each state what they need, and `consumers` is what joins them.',
|
|
142
|
+
'module-input-without-a-settings-verdict': 'A module-owned environment input is debt against the Settings module until somebody has ' +
|
|
143
|
+
'said why it is not one (`contracts/environment-inputs.md` §4). Add an entry to the ' +
|
|
144
|
+
'module’s shard under `backend/scripts/ledgers/module-environment-inputs/`, ' +
|
|
145
|
+
'classified `bootstrap` or `configuration`, with the reason. The legitimate answer is ' +
|
|
146
|
+
'that the value is needed **before the settings store can be read** — ' +
|
|
147
|
+
'`SETTINGS_SECRET_ENCRYPTION_KEY` and `MFA_SECRET_ENCRYPTION_KEY` are the standing ' +
|
|
148
|
+
'examples, being the keys the store’s own secrets are decrypted with. "Convenience" ' +
|
|
149
|
+
'and "historical" are `configuration`, which is a finding waiting to be repaired rather ' +
|
|
150
|
+
'than an exemption — and an entry whose reason says nothing is not a verdict, so it is ' +
|
|
151
|
+
'reported here rather than counted as one.',
|
|
152
|
+
'stale-settings-verdict': 'A ledger entry describes an environment input this module no longer declares. The ' +
|
|
153
|
+
'ledger is two-way: it drains as a module moves its configuration into Settings, and an ' +
|
|
154
|
+
'entry left behind is a claim about a variable that is gone. Delete it — and delete the ' +
|
|
155
|
+
'shard when its last entry goes, because an empty file is a done signal that says ' +
|
|
156
|
+
'nothing.',
|
|
157
|
+
'unresolvable-input-name': 'The variable is named by an expression this analysis cannot resolve to a literal, so it ' +
|
|
158
|
+
'can be reconciled against no declaration. Write the name as a string literal, or as a ' +
|
|
159
|
+
'`const` initialised to one — either in this file or exported by a file in the same ' +
|
|
160
|
+
'tree, which is the idiom `storefront/lib/env.mjs` already uses.',
|
|
161
|
+
'generable-without-secret': '`generable` is permitted only on a secret whose two correct values are interchangeable ' +
|
|
162
|
+
'(`input-resolution.md` R4.5). A URL, a name, a channel code, a locale or a hostname ' +
|
|
163
|
+
'carries a human judgement and may never be generated: the tool would be inventing the ' +
|
|
164
|
+
'value the provenance line promises it does not.',
|
|
165
|
+
'requirement-without-a-consequence': 'An `optional` input has to say **what is lost** without it, in both shipped languages — ' +
|
|
166
|
+
'that sentence is what an operator decides on, and the word “optional” is not it. A ' +
|
|
167
|
+
'`requiredWhen` has to name an input some declaration carries, or the condition can ' +
|
|
168
|
+
'never be evaluated and the input is required by nobody.',
|
|
169
|
+
};
|
|
170
|
+
const authorKey = (owner) => owner.kind === 'platform'
|
|
171
|
+
? 'platform'
|
|
172
|
+
: owner.kind === 'application'
|
|
173
|
+
? `application:${owner.application}`
|
|
174
|
+
: `module:${owner.moduleId}`;
|
|
175
|
+
/**
|
|
176
|
+
* The `(name, consumer)` key both reconciliations are asked against.
|
|
177
|
+
*
|
|
178
|
+
* One function rather than two template literals, because the two have to agree
|
|
179
|
+
* exactly and the separator is invisible. `\0` is the separator for the usual
|
|
180
|
+
* reason — it is the one byte neither a variable name nor a consumer can carry —
|
|
181
|
+
* and it is spelled as an **escape**: a raw NUL in the source would make git
|
|
182
|
+
* classify this file as binary and render every diff of it as
|
|
183
|
+
* `Binary files differ`, which is `check:nul-bytes`' whole subject. The compiled
|
|
184
|
+
* string is identical, so nothing about the key changes.
|
|
185
|
+
*/
|
|
186
|
+
const consumerKey = (name, consumer) => `${name}\0${consumer}`;
|
|
187
|
+
/**
|
|
188
|
+
* The same key, narrowed to one module — what `unread-input` is asked against
|
|
189
|
+
* for a module author.
|
|
190
|
+
*
|
|
191
|
+
* A module declaring an input its **own** sources do not read is the finding;
|
|
192
|
+
* a sibling reading the name does not earn the declaration, and asking the
|
|
193
|
+
* question over the whole backend tree would say it did. That is not
|
|
194
|
+
* hypothetical here: `MEILISEARCH_URL` is legitimately declared by two modules,
|
|
195
|
+
* so the tree-wide question answers "read" for both the moment either one reads
|
|
196
|
+
* it, and the direction this check exists to protect — an operator asked for
|
|
197
|
+
* a value that changes nothing — would stop working for the module half on
|
|
198
|
+
* the day it landed.
|
|
199
|
+
*/
|
|
200
|
+
const moduleKey = (module, name, consumer) => `${module}\0${name}\0${consumer}`;
|
|
201
|
+
/**
|
|
202
|
+
* A sentence, rather than a string.
|
|
203
|
+
*
|
|
204
|
+
* `''` is refused by the schema; this is the next failure along — a `without`
|
|
205
|
+
* reading `optional`, `n/a` or `-`, which parses and says nothing. Held to a
|
|
206
|
+
* length rather than to a blocklist, in the idiom the host-internal subpath
|
|
207
|
+
* reasons are held to.
|
|
208
|
+
*/
|
|
209
|
+
const SAYS_SOMETHING = 12;
|
|
210
|
+
/** The whole rule, pure over the record, so a proof enters where a run does. */
|
|
211
|
+
export function checkEnvironmentInputs(input) {
|
|
212
|
+
const findings = [];
|
|
213
|
+
// Every declared name, whoever declared it — the vocabulary a `requiredWhen`
|
|
214
|
+
// predicate and an `undeclared-input` verdict are both answered against.
|
|
215
|
+
const declaredNames = new Set();
|
|
216
|
+
// (name, consumer) → the declarations that claim it, for the read reconciliation.
|
|
217
|
+
const byNameAndConsumer = new Map();
|
|
218
|
+
// Every name the **platform** declares — what `module-declares-a-platform-input`
|
|
219
|
+
// is asked against, and deliberately not "every name a non-module author
|
|
220
|
+
// declares". The applications' names are excluded on purpose: the backend
|
|
221
|
+
// genuinely reading a name the storefront also declares is `REVALIDATE_SECRET`'s
|
|
222
|
+
// shape, two trees that ship independently each stating what they need, and
|
|
223
|
+
// widening the predicate to cover it would turn a correct declaration into a
|
|
224
|
+
// finding.
|
|
225
|
+
const platformNames = new Set();
|
|
226
|
+
const modulesDeclaring = new Set();
|
|
227
|
+
let declared = 0;
|
|
228
|
+
let declaredByModules = 0;
|
|
229
|
+
for (const source of input.declarations) {
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
for (const entry of source.inputs) {
|
|
232
|
+
declared += 1;
|
|
233
|
+
declaredNames.add(entry.name);
|
|
234
|
+
if (source.author.kind === 'platform')
|
|
235
|
+
platformNames.add(entry.name);
|
|
236
|
+
if (source.author.kind === 'module') {
|
|
237
|
+
declaredByModules += 1;
|
|
238
|
+
modulesDeclaring.add(source.author.moduleId);
|
|
239
|
+
}
|
|
240
|
+
// — `foreign-input`, shape one: the owner is not the shipping author.
|
|
241
|
+
if (authorKey(entry.owner) !== authorKey(source.author)) {
|
|
242
|
+
findings.push({
|
|
243
|
+
kind: 'foreign-input',
|
|
244
|
+
name: entry.name,
|
|
245
|
+
where: source.file,
|
|
246
|
+
detail: `declared with owner \`${authorKey(entry.owner)}\` by ` +
|
|
247
|
+
`\`${authorKey(source.author)}\`, which is not the author of this file`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
// — `foreign-input`, shape two: declared twice by one author.
|
|
251
|
+
if (seen.has(entry.name)) {
|
|
252
|
+
findings.push({
|
|
253
|
+
kind: 'foreign-input',
|
|
254
|
+
name: entry.name,
|
|
255
|
+
where: source.file,
|
|
256
|
+
detail: 'declared twice in one declaration — two entries, two `describes`, and whichever ' +
|
|
257
|
+
'a reader reaches first wins',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
seen.add(entry.name);
|
|
261
|
+
// — `generable-without-secret`.
|
|
262
|
+
if (entry.generable && !entry.secret) {
|
|
263
|
+
findings.push({
|
|
264
|
+
kind: 'generable-without-secret',
|
|
265
|
+
name: entry.name,
|
|
266
|
+
where: source.file,
|
|
267
|
+
detail: '`generable` is set on an input that is not a secret',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// — `requirement-without-a-consequence`, the `optional` half.
|
|
271
|
+
if (entry.requirement.kind === 'optional' &&
|
|
272
|
+
(entry.requirement.without.en.trim().length < SAYS_SOMETHING ||
|
|
273
|
+
entry.requirement.without.pl.trim().length < SAYS_SOMETHING)) {
|
|
274
|
+
findings.push({
|
|
275
|
+
kind: 'requirement-without-a-consequence',
|
|
276
|
+
name: entry.name,
|
|
277
|
+
where: source.file,
|
|
278
|
+
detail: 'its `optional` sentence does not say what is lost without the input',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
for (const consumer of entry.consumers) {
|
|
282
|
+
const key = consumerKey(entry.name, consumer);
|
|
283
|
+
const claims = byNameAndConsumer.get(key);
|
|
284
|
+
if (claims === undefined)
|
|
285
|
+
byNameAndConsumer.set(key, [source]);
|
|
286
|
+
else
|
|
287
|
+
claims.push(source);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// — `module-declares-a-platform-input`. A **second pass**, for the same reason
|
|
292
|
+
// the `requiredWhen` one below is: the question is asked against another
|
|
293
|
+
// author's declaration, so asking it inside the first loop would make the
|
|
294
|
+
// verdict depend on the order the host happened to hand the declarations in
|
|
295
|
+
// — green when the platform's came first, red when it came second, over one
|
|
296
|
+
// tree.
|
|
297
|
+
//
|
|
298
|
+
// Not a shape of `foreign-input`: there the `owner` field is wrong and the
|
|
299
|
+
// remedy is "let the owner declare it"; here the `owner` is honest — the
|
|
300
|
+
// author really did mean this module — and the remedy is "delete it, the
|
|
301
|
+
// platform already said this". `defineModuleManifest` cannot see it, having
|
|
302
|
+
// no sight of the platform's declaration, so this is the only layer that can.
|
|
303
|
+
for (const source of input.declarations) {
|
|
304
|
+
if (source.author.kind !== 'module')
|
|
305
|
+
continue;
|
|
306
|
+
for (const entry of source.inputs) {
|
|
307
|
+
if (!platformNames.has(entry.name))
|
|
308
|
+
continue;
|
|
309
|
+
findings.push({
|
|
310
|
+
kind: 'module-declares-a-platform-input',
|
|
311
|
+
name: entry.name,
|
|
312
|
+
where: source.file,
|
|
313
|
+
detail: `declared by the module \`${source.author.moduleId}\`, and the platform declares it ` +
|
|
314
|
+
'too — one variable, two descriptions, and whichever a reader reaches first wins',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// — `requirement-without-a-consequence`, the `requiredWhen` half. Second
|
|
319
|
+
// pass, because the vocabulary a predicate is answered against is every
|
|
320
|
+
// author's and not the declaring author's alone: a condition may
|
|
321
|
+
// legitimately read an input the platform owns.
|
|
322
|
+
for (const source of input.declarations) {
|
|
323
|
+
for (const entry of source.inputs) {
|
|
324
|
+
if (entry.requirement.kind !== 'requiredWhen')
|
|
325
|
+
continue;
|
|
326
|
+
if (declaredNames.has(entry.requirement.input))
|
|
327
|
+
continue;
|
|
328
|
+
findings.push({
|
|
329
|
+
kind: 'requirement-without-a-consequence',
|
|
330
|
+
name: entry.name,
|
|
331
|
+
where: source.file,
|
|
332
|
+
detail: `its condition reads \`${entry.requirement.input}\`, which no declaration carries, ` +
|
|
333
|
+
'so the condition can never be true and the input is required by nobody',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// — `unresolvable-input-name`, `undeclared-input` and
|
|
338
|
+
// `undeclared-module-input`, over the reads.
|
|
339
|
+
const readNamesByConsumer = new Map();
|
|
340
|
+
const readNamesByModule = new Set();
|
|
341
|
+
for (const read of input.reads) {
|
|
342
|
+
if (read.name === null) {
|
|
343
|
+
findings.push({
|
|
344
|
+
kind: 'unresolvable-input-name',
|
|
345
|
+
name: '(computed)',
|
|
346
|
+
where: `${read.file}:${read.line}`,
|
|
347
|
+
detail: 'the variable is named by an expression that is neither a string literal nor a ' +
|
|
348
|
+
'constant initialised to one',
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const names = readNamesByConsumer.get(read.consumer) ?? new Set();
|
|
353
|
+
names.add(read.name);
|
|
354
|
+
readNamesByConsumer.set(read.consumer, names);
|
|
355
|
+
if (read.module !== undefined) {
|
|
356
|
+
readNamesByModule.add(moduleKey(read.module, read.name, read.consumer));
|
|
357
|
+
}
|
|
358
|
+
// **The resolution rule, and it is one predicate for both populations**
|
|
359
|
+
// (T3-B): a read resolves against the platform's declaration, the
|
|
360
|
+
// application tree's, and — where the file is a module's — that module's
|
|
361
|
+
// own. Never another module's.
|
|
362
|
+
//
|
|
363
|
+
// A module's declaration must not cover an *application's* read either, and
|
|
364
|
+
// the same line says so: `read.module` is `undefined` there, so the
|
|
365
|
+
// `moduleId` comparison fails for every module author. One rule, two
|
|
366
|
+
// populations, no second predicate to keep in step.
|
|
367
|
+
const claims = byNameAndConsumer.get(consumerKey(read.name, read.consumer)) ?? [];
|
|
368
|
+
const satisfied = claims.some((claim) => claim.author.kind !== 'module' || claim.author.moduleId === read.module);
|
|
369
|
+
if (satisfied)
|
|
370
|
+
continue;
|
|
371
|
+
if (read.module === undefined) {
|
|
372
|
+
findings.push({
|
|
373
|
+
kind: 'undeclared-input',
|
|
374
|
+
name: read.name,
|
|
375
|
+
where: `${read.file}:${read.line}`,
|
|
376
|
+
detail: `read in the ${read.consumer} tree, which declares no input of that name`,
|
|
377
|
+
});
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// A sibling declaring it is worth saying, because it is the shape an author
|
|
381
|
+
// is most likely to think already correct — and the answer is that a client
|
|
382
|
+
// may install this module and not that one.
|
|
383
|
+
const bySibling = claims.filter((claim) => claim.author.kind === 'module');
|
|
384
|
+
const siblings = bySibling.length === 0
|
|
385
|
+
? ''
|
|
386
|
+
: ` — ${bySibling
|
|
387
|
+
.map((claim) => claim.author.kind === 'module' ? `\`${claim.author.moduleId}\`` : '')
|
|
388
|
+
.join(', ')} declares it, and a sibling's declaration does not travel with this module`;
|
|
389
|
+
findings.push({
|
|
390
|
+
kind: 'undeclared-module-input',
|
|
391
|
+
name: read.name,
|
|
392
|
+
where: `${read.file}:${read.line}`,
|
|
393
|
+
detail: `read by the module \`${read.module}\` in the ${read.consumer} tree, and neither the ` +
|
|
394
|
+
`platform, the ${read.consumer} application nor \`${read.module}\`'s own manifest ` +
|
|
395
|
+
`declares it${siblings}`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// — `unread-input`. Asked per (name, consumer): an input declared as read by
|
|
399
|
+
// the storefront and read only by the backend is a declaration that is
|
|
400
|
+
// wrong about which tree needs it, which is exactly the fact `consumers`
|
|
401
|
+
// exists to carry and the fact member scoping runs on.
|
|
402
|
+
for (const source of input.declarations) {
|
|
403
|
+
const author = source.author;
|
|
404
|
+
// For a module the question is asked over **that module's own** sources.
|
|
405
|
+
// See {@link moduleKey}: `MEILISEARCH_URL` is legitimately declared by two
|
|
406
|
+
// modules, so a tree-wide question answers "read" for both the moment
|
|
407
|
+
// either reads it.
|
|
408
|
+
const isRead = (name, consumer) => author.kind === 'module'
|
|
409
|
+
? readNamesByModule.has(moduleKey(author.moduleId, name, consumer))
|
|
410
|
+
: (readNamesByConsumer.get(consumer)?.has(name) ?? false);
|
|
411
|
+
for (const entry of source.inputs) {
|
|
412
|
+
const unread = entry.consumers.filter((consumer) => !isRead(entry.name, consumer));
|
|
413
|
+
if (unread.length === 0)
|
|
414
|
+
continue;
|
|
415
|
+
findings.push({
|
|
416
|
+
kind: 'unread-input',
|
|
417
|
+
name: entry.name,
|
|
418
|
+
where: source.file,
|
|
419
|
+
detail: author.kind === 'module'
|
|
420
|
+
? `declared as read by ${unread.join(', ')}, and no source of the module ` +
|
|
421
|
+
`\`${author.moduleId}\` reads it there`
|
|
422
|
+
: `declared as read by ${unread.join(', ')}, and no source in ` +
|
|
423
|
+
`${unread.length === 1 ? 'that tree' : 'those trees'} reads it`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// — The Settings-debt ledger, both ways (`environment-inputs.md` §4).
|
|
428
|
+
//
|
|
429
|
+
// `undefined` is a caller that is not asking; `[]` is a ledger that was read
|
|
430
|
+
// and holds nothing, under which every module-owned declaration is
|
|
431
|
+
// unjudged. The second is the state a deleted ledger directory would
|
|
432
|
+
// produce, which is why the host refuses that rather than reaching here.
|
|
433
|
+
if (input.settingsVerdicts !== undefined) {
|
|
434
|
+
const verdictsOf = new Map(input.settingsVerdicts.map((shard) => [shard.moduleId, shard.entries]));
|
|
435
|
+
const declaredByModule = new Map();
|
|
436
|
+
for (const source of input.declarations) {
|
|
437
|
+
if (source.author.kind !== 'module')
|
|
438
|
+
continue;
|
|
439
|
+
const names = declaredByModule.get(source.author.moduleId) ?? new Set();
|
|
440
|
+
for (const entry of source.inputs)
|
|
441
|
+
names.add(entry.name);
|
|
442
|
+
declaredByModule.set(source.author.moduleId, names);
|
|
443
|
+
for (const entry of source.inputs) {
|
|
444
|
+
const verdict = verdictsOf.get(source.author.moduleId)?.[entry.name];
|
|
445
|
+
// A reason that says nothing is not a verdict. Held to a length rather
|
|
446
|
+
// than to a blocklist, in the idiom the `optional` sentence is held to
|
|
447
|
+
// twenty lines up — "historical" and "convenience" are the sentences an
|
|
448
|
+
// entry written to make this pass would carry, and §4.3 calls them debt
|
|
449
|
+
// rather than exemptions.
|
|
450
|
+
if (verdict !== undefined && verdict.reason.trim().length >= SAYS_SOMETHING)
|
|
451
|
+
continue;
|
|
452
|
+
findings.push({
|
|
453
|
+
kind: 'module-input-without-a-settings-verdict',
|
|
454
|
+
name: entry.name,
|
|
455
|
+
where: source.file,
|
|
456
|
+
detail: verdict === undefined
|
|
457
|
+
? `declared by the module \`${source.author.moduleId}\` and judged by nobody — ` +
|
|
458
|
+
'no entry in its shard of the Settings-debt ledger'
|
|
459
|
+
: `its ledger entry does not say why it is ${verdict.classification} rather ` +
|
|
460
|
+
'than a Setting',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const shard of input.settingsVerdicts) {
|
|
465
|
+
const declared = declaredByModule.get(shard.moduleId) ?? new Set();
|
|
466
|
+
for (const name of Object.keys(shard.entries)) {
|
|
467
|
+
if (declared.has(name))
|
|
468
|
+
continue;
|
|
469
|
+
findings.push({
|
|
470
|
+
kind: 'stale-settings-verdict',
|
|
471
|
+
name,
|
|
472
|
+
where: `backend/scripts/ledgers/module-environment-inputs/${shard.moduleId}.ts`,
|
|
473
|
+
detail: `the module \`${shard.moduleId}\` declares no environment input of that name`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const declaringConsumers = new Set();
|
|
479
|
+
for (const source of input.declarations) {
|
|
480
|
+
for (const entry of source.inputs) {
|
|
481
|
+
for (const consumer of entry.consumers)
|
|
482
|
+
declaringConsumers.add(consumer);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const consumersCovered = ENVIRONMENT_CONSUMERS.filter((consumer) => declaringConsumers.has(consumer) && readNamesByConsumer.has(consumer));
|
|
486
|
+
return {
|
|
487
|
+
findings,
|
|
488
|
+
declared,
|
|
489
|
+
consumersCovered,
|
|
490
|
+
declaredByModules,
|
|
491
|
+
modulesDeclaring: [...modulesDeclaring].sort(),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const isProcessEnv = (node) => ts.isPropertyAccessExpression(node) &&
|
|
495
|
+
node.name.text === 'env' &&
|
|
496
|
+
ts.isIdentifier(node.expression) &&
|
|
497
|
+
node.expression.text === 'process';
|
|
498
|
+
/**
|
|
499
|
+
* The prefix Vite requires on a variable it takes from the environment.
|
|
500
|
+
*
|
|
501
|
+
* Vite's own `envPrefix`, at its default. A name without it is one of Vite's
|
|
502
|
+
* build-mode constants and is not an operator input — see the header.
|
|
503
|
+
*/
|
|
504
|
+
export const VITE_ENVIRONMENT_PREFIX = 'VITE_';
|
|
505
|
+
const isImportMetaEnv = (node) => ts.isPropertyAccessExpression(node) &&
|
|
506
|
+
node.name.text === 'env' &&
|
|
507
|
+
ts.isMetaProperty(node.expression);
|
|
508
|
+
const scriptKindOf = (path) => path.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
509
|
+
/**
|
|
510
|
+
* Every string constant a file exports or declares at module scope.
|
|
511
|
+
*
|
|
512
|
+
* Only a bare `const X = '…'`, deliberately. A `let`, a computed initialiser
|
|
513
|
+
* and a member of an object literal are all outside it: each is a value that
|
|
514
|
+
* can differ from what the reader sees, and the whole point of resolving a
|
|
515
|
+
* constant here is that its value is decidable from the text.
|
|
516
|
+
*/
|
|
517
|
+
export function stringConstants(source) {
|
|
518
|
+
const constants = new Map();
|
|
519
|
+
for (const statement of source.statements) {
|
|
520
|
+
if (!ts.isVariableStatement(statement))
|
|
521
|
+
continue;
|
|
522
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
523
|
+
continue;
|
|
524
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
525
|
+
if (!ts.isIdentifier(declaration.name))
|
|
526
|
+
continue;
|
|
527
|
+
const initialiser = declaration.initializer;
|
|
528
|
+
if (initialiser === undefined)
|
|
529
|
+
continue;
|
|
530
|
+
// `as const` and a plain literal both land here; a template literal with
|
|
531
|
+
// no substitution is the same value written differently.
|
|
532
|
+
const unwrapped = ts.isAsExpression(initialiser) ? initialiser.expression : initialiser;
|
|
533
|
+
if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
|
|
534
|
+
constants.set(declaration.name.text, unwrapped.text);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return constants;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Where an imported identifier came from, as `(local name) → (specifier, exported name)`.
|
|
542
|
+
*
|
|
543
|
+
* Both spellings the tree writes — `import { X }` and `import { X as Y }`.
|
|
544
|
+
* A namespace import is deliberately absent: `env.PUBLIC_API_BASE_URL_VAR` is a
|
|
545
|
+
* property access, not an identifier, and resolving it would need the module
|
|
546
|
+
* graph rather than one file's text.
|
|
547
|
+
*/
|
|
548
|
+
function importedBindings(source) {
|
|
549
|
+
const bindings = new Map();
|
|
550
|
+
for (const statement of source.statements) {
|
|
551
|
+
if (!ts.isImportDeclaration(statement))
|
|
552
|
+
continue;
|
|
553
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier))
|
|
554
|
+
continue;
|
|
555
|
+
const clause = statement.importClause;
|
|
556
|
+
if (clause?.namedBindings === undefined)
|
|
557
|
+
continue;
|
|
558
|
+
if (!ts.isNamedImports(clause.namedBindings))
|
|
559
|
+
continue;
|
|
560
|
+
for (const element of clause.namedBindings.elements) {
|
|
561
|
+
bindings.set(element.name.text, {
|
|
562
|
+
specifier: statement.moduleSpecifier.text,
|
|
563
|
+
exported: (element.propertyName ?? element.name).text,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return bindings;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Resolve a relative import specifier against the importing file's directory,
|
|
571
|
+
* in the walk's own key space.
|
|
572
|
+
*
|
|
573
|
+
* POSIX-only and extension-tolerant: the tree writes `./env.mjs` from a `.ts`
|
|
574
|
+
* file (ESM's own rule) and `./env.js` where the target is `env.ts`. A bare
|
|
575
|
+
* specifier resolves to nothing here, which is right — a constant published by
|
|
576
|
+
* a package is not text this walk read.
|
|
577
|
+
*/
|
|
578
|
+
export function resolveRelative(fromFile, specifier) {
|
|
579
|
+
if (!specifier.startsWith('.'))
|
|
580
|
+
return null;
|
|
581
|
+
const fromDirectory = fromFile.slice(0, Math.max(0, fromFile.lastIndexOf('/')));
|
|
582
|
+
const parts = `${fromDirectory}/${specifier}`.split('/');
|
|
583
|
+
const stack = [];
|
|
584
|
+
for (const part of parts) {
|
|
585
|
+
if (part === '.' || part.length === 0)
|
|
586
|
+
continue;
|
|
587
|
+
if (part === '..')
|
|
588
|
+
stack.pop();
|
|
589
|
+
else
|
|
590
|
+
stack.push(part);
|
|
591
|
+
}
|
|
592
|
+
return stack.join('/');
|
|
593
|
+
}
|
|
594
|
+
/** The extensions a resolved specifier may really be, in the order tried. */
|
|
595
|
+
const CANDIDATE_EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.mjs', '.js'];
|
|
596
|
+
/**
|
|
597
|
+
* Every environment read in the walk, with its name resolved where it can be.
|
|
598
|
+
*
|
|
599
|
+
* The file text is the entry point (issue #130), so a red proof drives exactly
|
|
600
|
+
* what a real run drives.
|
|
601
|
+
*/
|
|
602
|
+
export function collectEnvironmentReads(files) {
|
|
603
|
+
const parsed = new Map();
|
|
604
|
+
for (const file of files) {
|
|
605
|
+
parsed.set(file.path, ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true, scriptKindOf(file.path)));
|
|
606
|
+
}
|
|
607
|
+
const constantsByFile = new Map();
|
|
608
|
+
const constantsOf = (path) => {
|
|
609
|
+
const cached = constantsByFile.get(path);
|
|
610
|
+
if (cached !== undefined)
|
|
611
|
+
return cached;
|
|
612
|
+
const source = parsed.get(path);
|
|
613
|
+
const computed = source === undefined ? new Map() : stringConstants(source);
|
|
614
|
+
constantsByFile.set(path, computed);
|
|
615
|
+
return computed;
|
|
616
|
+
};
|
|
617
|
+
/** The second hop: an imported constant, resolved in the walk's own files. */
|
|
618
|
+
const importedConstant = (file, identifier) => {
|
|
619
|
+
const source = parsed.get(file.path);
|
|
620
|
+
if (source === undefined)
|
|
621
|
+
return undefined;
|
|
622
|
+
const binding = importedBindings(source).get(identifier);
|
|
623
|
+
if (binding === undefined)
|
|
624
|
+
return undefined;
|
|
625
|
+
const base = resolveRelative(file.path, binding.specifier);
|
|
626
|
+
if (base === null)
|
|
627
|
+
return undefined;
|
|
628
|
+
// ESM specifiers name the emitted file; the walk holds the source. Try the
|
|
629
|
+
// specifier as written first, then the source extensions it could be.
|
|
630
|
+
const stem = base.replace(/\.(js|mjs|cjs)$/, '');
|
|
631
|
+
for (const extension of CANDIDATE_EXTENSIONS) {
|
|
632
|
+
const candidate = `${stem}${extension}`;
|
|
633
|
+
if (!parsed.has(candidate))
|
|
634
|
+
continue;
|
|
635
|
+
const value = constantsOf(candidate).get(binding.exported);
|
|
636
|
+
if (value !== undefined)
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
return undefined;
|
|
640
|
+
};
|
|
641
|
+
const reads = [];
|
|
642
|
+
for (const file of files) {
|
|
643
|
+
if (!file.text.includes('process.env') && !file.text.includes('import.meta.env'))
|
|
644
|
+
continue;
|
|
645
|
+
const source = parsed.get(file.path);
|
|
646
|
+
if (source === undefined)
|
|
647
|
+
continue;
|
|
648
|
+
const visit = (node) => {
|
|
649
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
650
|
+
const container = node.expression;
|
|
651
|
+
const viteDialect = isImportMetaEnv(container);
|
|
652
|
+
if (isProcessEnv(container) || viteDialect) {
|
|
653
|
+
const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
654
|
+
// Vite's build-mode constants are not environment inputs: no `.env`
|
|
655
|
+
// supplies one and no operator can be asked for one. The prefix is
|
|
656
|
+
// Vite's own discriminator for the same question.
|
|
657
|
+
const fromEnvironment = (name) => !viteDialect || name.startsWith(VITE_ENVIRONMENT_PREFIX);
|
|
658
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
659
|
+
if (fromEnvironment(node.name.text)) {
|
|
660
|
+
reads.push({
|
|
661
|
+
name: node.name.text,
|
|
662
|
+
file: file.path,
|
|
663
|
+
line,
|
|
664
|
+
consumer: file.consumer,
|
|
665
|
+
module: file.module,
|
|
666
|
+
shape: 'literal',
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
const argument = node.argumentExpression;
|
|
672
|
+
if (ts.isStringLiteralLike(argument)) {
|
|
673
|
+
if (fromEnvironment(argument.text)) {
|
|
674
|
+
reads.push({
|
|
675
|
+
name: argument.text,
|
|
676
|
+
file: file.path,
|
|
677
|
+
line,
|
|
678
|
+
consumer: file.consumer,
|
|
679
|
+
module: file.module,
|
|
680
|
+
shape: 'literal',
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else if (ts.isIdentifier(argument)) {
|
|
685
|
+
const local = constantsOf(file.path).get(argument.text);
|
|
686
|
+
const resolved = local ?? importedConstant(file, argument.text);
|
|
687
|
+
if (resolved === undefined || fromEnvironment(resolved)) {
|
|
688
|
+
reads.push({
|
|
689
|
+
name: resolved ?? null,
|
|
690
|
+
file: file.path,
|
|
691
|
+
line,
|
|
692
|
+
consumer: file.consumer,
|
|
693
|
+
module: file.module,
|
|
694
|
+
shape: local !== undefined
|
|
695
|
+
? 'const'
|
|
696
|
+
: resolved !== undefined
|
|
697
|
+
? 'imported-const'
|
|
698
|
+
: 'computed',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
reads.push({
|
|
704
|
+
name: null,
|
|
705
|
+
file: file.path,
|
|
706
|
+
line,
|
|
707
|
+
consumer: file.consumer,
|
|
708
|
+
module: file.module,
|
|
709
|
+
shape: 'computed',
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
ts.forEachChild(node, visit);
|
|
716
|
+
};
|
|
717
|
+
visit(source);
|
|
718
|
+
}
|
|
719
|
+
return reads;
|
|
720
|
+
}
|
|
721
|
+
/** Raised when a declaration file cannot be read as one; a caller turns it into exit 2. */
|
|
722
|
+
export class DeclarationUnreadableError extends Error {
|
|
723
|
+
name = 'DeclarationUnreadableError';
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* A declaration read out of its own **source text**, never out of a build.
|
|
727
|
+
*
|
|
728
|
+
* ## Why not `import()` the emitted module
|
|
729
|
+
*
|
|
730
|
+
* A module package resolves through its `exports` map at its build output
|
|
731
|
+
* (D-164), so a check that imported one would answer about the previous build —
|
|
732
|
+
* the `stale-artefact` class `check:action-route-permissions` had to grow a
|
|
733
|
+
* refusal for, measured three times as a **false green** on that check. Reading
|
|
734
|
+
* the text removes the question rather than guarding it: the bytes this run
|
|
735
|
+
* judged are the bytes on disk, and the check needs no build to run at all.
|
|
736
|
+
* The `exports` subpath is still the path the *runtime* readers take, and
|
|
737
|
+
* `test/unit/packages/platform-env-subpath.test.ts` is what holds it open.
|
|
738
|
+
*
|
|
739
|
+
* ## What it evaluates, and what it refuses
|
|
740
|
+
*
|
|
741
|
+
* A declaration is data: an array of object literals over strings, booleans,
|
|
742
|
+
* arrays and nested objects. Nothing else is accepted — no identifier, no
|
|
743
|
+
* spread, no call, no concatenation — and an expression outside that grammar is
|
|
744
|
+
* a refusal rather than a value guessed at, because a declaration this analysis
|
|
745
|
+
* cannot read in full is one it must not report on (issue #113).
|
|
746
|
+
*
|
|
747
|
+
* A string is taken literally, so `'\n'` and a template literal with no
|
|
748
|
+
* substitution both arrive as the text they denote. `as const` is unwrapped:
|
|
749
|
+
* it changes a type and no value.
|
|
750
|
+
*/
|
|
751
|
+
export function evaluateDeclarationArray(text, fileName, exportName) {
|
|
752
|
+
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(fileName));
|
|
753
|
+
const literal = literalReader(source, fileName);
|
|
754
|
+
for (const statement of source.statements) {
|
|
755
|
+
if (!ts.isVariableStatement(statement))
|
|
756
|
+
continue;
|
|
757
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
758
|
+
if (!ts.isIdentifier(declaration.name))
|
|
759
|
+
continue;
|
|
760
|
+
if (declaration.name.text !== exportName)
|
|
761
|
+
continue;
|
|
762
|
+
if (declaration.initializer === undefined) {
|
|
763
|
+
throw new DeclarationUnreadableError(`${fileName}: \`${exportName}\` has no value`);
|
|
764
|
+
}
|
|
765
|
+
const value = literal(declaration.initializer);
|
|
766
|
+
if (!Array.isArray(value)) {
|
|
767
|
+
throw new DeclarationUnreadableError(`${fileName}: \`${exportName}\` is not an array`);
|
|
768
|
+
}
|
|
769
|
+
return value;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
throw new DeclarationUnreadableError(`${fileName}: no \`${exportName}\` declaration — the file is not a declaration this ` +
|
|
773
|
+
'run can read, and a run that could not read its input has said nothing about the tree');
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* The literal evaluator, shared by the two declaration readers above and below.
|
|
777
|
+
*
|
|
778
|
+
* One function rather than two copies: a module's `env` entries and the
|
|
779
|
+
* platform's are the same data in two files, and two evaluators that drifted
|
|
780
|
+
* would accept a declaration in one place and refuse it in the other for
|
|
781
|
+
* reasons neither author could see.
|
|
782
|
+
*/
|
|
783
|
+
function literalReader(source, fileName) {
|
|
784
|
+
const literal = (node) => {
|
|
785
|
+
const unwrapped = ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node)
|
|
786
|
+
? node.expression
|
|
787
|
+
: node;
|
|
788
|
+
if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) {
|
|
789
|
+
return unwrapped.text;
|
|
790
|
+
}
|
|
791
|
+
if (unwrapped.kind === ts.SyntaxKind.TrueKeyword)
|
|
792
|
+
return true;
|
|
793
|
+
if (unwrapped.kind === ts.SyntaxKind.FalseKeyword)
|
|
794
|
+
return false;
|
|
795
|
+
if (unwrapped.kind === ts.SyntaxKind.NullKeyword)
|
|
796
|
+
return null;
|
|
797
|
+
if (ts.isNumericLiteral(unwrapped))
|
|
798
|
+
return Number(unwrapped.text);
|
|
799
|
+
if (ts.isArrayLiteralExpression(unwrapped))
|
|
800
|
+
return unwrapped.elements.map(literal);
|
|
801
|
+
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
802
|
+
const value = {};
|
|
803
|
+
for (const property of unwrapped.properties) {
|
|
804
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
805
|
+
throw new DeclarationUnreadableError(`${fileName}: a property that is not a plain \`key: value\` assignment — a ` +
|
|
806
|
+
'declaration is data and this analysis will not guess at it');
|
|
807
|
+
}
|
|
808
|
+
const key = ts.isIdentifier(property.name)
|
|
809
|
+
? property.name.text
|
|
810
|
+
: ts.isStringLiteral(property.name)
|
|
811
|
+
? property.name.text
|
|
812
|
+
: null;
|
|
813
|
+
if (key === null) {
|
|
814
|
+
throw new DeclarationUnreadableError(`${fileName}: a computed property name, which no declaration may carry`);
|
|
815
|
+
}
|
|
816
|
+
value[key] = literal(property.initializer);
|
|
817
|
+
}
|
|
818
|
+
return value;
|
|
819
|
+
}
|
|
820
|
+
throw new DeclarationUnreadableError(`${fileName}: \`${unwrapped.getText(source).slice(0, 60)}\` is not a literal — a ` +
|
|
821
|
+
'declaration may hold only strings, booleans, numbers, arrays and objects');
|
|
822
|
+
};
|
|
823
|
+
return literal;
|
|
824
|
+
}
|
|
825
|
+
/** The helper every module's manifest is defined through. */
|
|
826
|
+
const MANIFEST_FACTORY = 'defineModuleManifest';
|
|
827
|
+
/**
|
|
828
|
+
* A module's `env` declaration, read out of its `manifest.ts` **source text**.
|
|
829
|
+
*
|
|
830
|
+
* ## Why text and not the built manifest
|
|
831
|
+
*
|
|
832
|
+
* The same reason {@link evaluateDeclarationArray} gives, with more force: a
|
|
833
|
+
* module package resolves through its own `exports` map at its build output
|
|
834
|
+
* (D-164), so a reader that imported one would answer about the previous build.
|
|
835
|
+
* That is the `stale-artefact` class `check:action-route-permissions` had to
|
|
836
|
+
* grow a refusal for after three measured false greens — an `env` entry edited
|
|
837
|
+
* and not rebuilt would be judged as though it were not there, which for *this*
|
|
838
|
+
* check is a false green in the direction that matters: a declaration the author
|
|
839
|
+
* just wrote, reported as missing, or one they just deleted, reported as
|
|
840
|
+
* present. Reading the text removes the question instead of guarding it, and the
|
|
841
|
+
* run needs no build at all.
|
|
842
|
+
*
|
|
843
|
+
* ## What it reads, and what it refuses
|
|
844
|
+
*
|
|
845
|
+
* Only the `env` property of the object literal handed to `defineModuleManifest`,
|
|
846
|
+
* and only as data. Everything else in the file is ignored, which is not a
|
|
847
|
+
* convenience: `cms`' manifest reads `process.env['CMS_PB_BREAKPOINT_TABLET_MIN']`
|
|
848
|
+
* in its own settings defaults, so a manifest is **not** a literal file and a
|
|
849
|
+
* reader that demanded one would refuse the very module whose declaration this
|
|
850
|
+
* feature exists to collect.
|
|
851
|
+
*
|
|
852
|
+
* `null` is "this module declares no environment input", which is true of most
|
|
853
|
+
* modules and is not a finding. A manifest with **no** `defineModuleManifest`
|
|
854
|
+
* call is a {@link DeclarationUnreadableError}: a module whose manifest this run
|
|
855
|
+
* cannot find is one it cannot report on, and reading that as "declares nothing"
|
|
856
|
+
* is the silent skip the estate is against (issue #113).
|
|
857
|
+
*/
|
|
858
|
+
export function evaluateManifestEnvDeclaration(text, fileName) {
|
|
859
|
+
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(fileName));
|
|
860
|
+
const literal = literalReader(source, fileName);
|
|
861
|
+
let argument = null;
|
|
862
|
+
let sawFactory = false;
|
|
863
|
+
const visit = (node) => {
|
|
864
|
+
if (argument === null &&
|
|
865
|
+
ts.isCallExpression(node) &&
|
|
866
|
+
ts.isIdentifier(node.expression) &&
|
|
867
|
+
node.expression.text === MANIFEST_FACTORY) {
|
|
868
|
+
sawFactory = true;
|
|
869
|
+
const first = node.arguments[0];
|
|
870
|
+
if (first !== undefined && ts.isObjectLiteralExpression(first))
|
|
871
|
+
argument = first;
|
|
872
|
+
}
|
|
873
|
+
ts.forEachChild(node, visit);
|
|
874
|
+
};
|
|
875
|
+
visit(source);
|
|
876
|
+
if (argument === null) {
|
|
877
|
+
throw new DeclarationUnreadableError(sawFactory
|
|
878
|
+
? `${fileName}: \`${MANIFEST_FACTORY}\` is called with something other than an object ` +
|
|
879
|
+
'literal, so this run cannot see what the module declares'
|
|
880
|
+
: `${fileName}: no \`${MANIFEST_FACTORY}\` call — a module manifest this run cannot ` +
|
|
881
|
+
'read is one it cannot report on, and "declares nothing" is not the same answer');
|
|
882
|
+
}
|
|
883
|
+
for (const property of argument.properties) {
|
|
884
|
+
if (!ts.isPropertyAssignment(property))
|
|
885
|
+
continue;
|
|
886
|
+
const key = ts.isIdentifier(property.name)
|
|
887
|
+
? property.name.text
|
|
888
|
+
: ts.isStringLiteral(property.name)
|
|
889
|
+
? property.name.text
|
|
890
|
+
: null;
|
|
891
|
+
if (key !== 'env')
|
|
892
|
+
continue;
|
|
893
|
+
const value = literal(property.initializer);
|
|
894
|
+
if (!Array.isArray(value)) {
|
|
895
|
+
throw new DeclarationUnreadableError(`${fileName}: \`env\` is not an array`);
|
|
896
|
+
}
|
|
897
|
+
return value;
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* The Settings-debt ledger, read off disk one shard per module
|
|
903
|
+
* (`contracts/environment-inputs.md` §4.2).
|
|
904
|
+
*
|
|
905
|
+
* Sharded because the debt is the module's: a repair that moves
|
|
906
|
+
* `INVENTORY_LOW_STOCK_RECIPIENT` into Settings touches `inventory`'s manifest
|
|
907
|
+
* and `inventory`'s shard, and nothing else. It is `check:module-boundary`'s
|
|
908
|
+
* shape, one population over.
|
|
909
|
+
*
|
|
910
|
+
* Throws rather than returning a partial answer, and every throw is a caller's
|
|
911
|
+
* exit 2: a ledger this run could not read in full is one it must not report
|
|
912
|
+
* on, because the missing half reads as *"nobody has judged this"* for one
|
|
913
|
+
* direction and as *"this entry describes nothing"* for the other — two
|
|
914
|
+
* confident findings out of one silence.
|
|
915
|
+
*/
|
|
916
|
+
export async function loadModuleVerdictShards(directory) {
|
|
917
|
+
if (!existsSync(directory)) {
|
|
918
|
+
throw new DeclarationUnreadableError(`${directory} is not there — the Settings-debt ledger is what says why a module-owned ` +
|
|
919
|
+
'environment input is not a Setting, and a run that could not read it would report ' +
|
|
920
|
+
'every module declaration as unjudged');
|
|
921
|
+
}
|
|
922
|
+
const shards = [];
|
|
923
|
+
for (const name of readdirSync(directory).sort()) {
|
|
924
|
+
if (!name.endsWith('.ts'))
|
|
925
|
+
continue;
|
|
926
|
+
const moduleId = name.replace(/\.ts$/, '');
|
|
927
|
+
let loaded;
|
|
928
|
+
try {
|
|
929
|
+
loaded = (await import(pathToFileURL(join(directory, name)).href));
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
throw new DeclarationUnreadableError(`ledger shard '${moduleId}' failed to load: ${String(error)}`);
|
|
933
|
+
}
|
|
934
|
+
const entries = loaded.entries;
|
|
935
|
+
if (typeof entries !== 'object' || entries === null) {
|
|
936
|
+
throw new DeclarationUnreadableError(`ledger shard '${moduleId}' exports no \`entries\` record`);
|
|
937
|
+
}
|
|
938
|
+
if (Object.keys(entries).length === 0) {
|
|
939
|
+
// An empty shard is a done signal that says nothing: it satisfies every
|
|
940
|
+
// presence test while judging no input, and it is the file left behind
|
|
941
|
+
// when a module's last entry drains. Delete the file instead.
|
|
942
|
+
throw new DeclarationUnreadableError(`ledger shard '${moduleId}' declares no entry — delete the file instead`);
|
|
943
|
+
}
|
|
944
|
+
shards.push({
|
|
945
|
+
moduleId,
|
|
946
|
+
entries: entries,
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
return shards;
|
|
950
|
+
}
|
|
951
|
+
//# sourceMappingURL=env-inputs.js.map
|