@intentius/chant 0.124.13 → 0.124.15
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/bin/chant +11 -0
- package/dist/agents/checks.js +326 -0
- package/dist/agents/discover.js +885 -0
- package/dist/agents/importer.js +17 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents/types.js +19 -0
- package/dist/apply.js +151 -0
- package/dist/attrref.js +70 -0
- package/dist/audit/catalog.js +350 -0
- package/dist/audit/core.js +252 -0
- package/dist/audit/discover.js +633 -0
- package/dist/audit/fetch.js +666 -0
- package/dist/audit/lineage.js +91 -0
- package/dist/audit/nginx.js +366 -0
- package/dist/audit/prior-art.js +132 -0
- package/dist/audit/proof.js +321 -0
- package/dist/audit/report-html.js +194 -0
- package/dist/audit/report-model.js +198 -0
- package/dist/audit/report.js +102 -0
- package/dist/audit/rules-doc.js +144 -0
- package/dist/audit/secrets.js +405 -0
- package/dist/audit/terraform-state.js +140 -0
- package/dist/audit/wrangler.js +262 -0
- package/dist/behaviour-delta.js +401 -0
- package/dist/behaviour-engine.js +357 -0
- package/dist/behaviour-graph.js +83 -0
- package/dist/behaviour-http.js +188 -0
- package/dist/behaviour-kinds.js +172 -0
- package/dist/behaviour-overlay.js +50 -0
- package/dist/behaviour-predict.js +188 -0
- package/dist/behaviour-request.js +194 -0
- package/dist/behaviour.js +1325 -0
- package/dist/build-params.js +108 -0
- package/dist/build.d.ts +6 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +669 -0
- package/dist/builder.js +44 -0
- package/dist/cdk/advise.js +30 -0
- package/dist/cdk/assembly.js +122 -0
- package/dist/cdk/graph.js +444 -0
- package/dist/cdk/tier-map.js +64 -0
- package/dist/cdk/types.js +24 -0
- package/dist/change-set.d.ts +24 -2
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +361 -0
- package/dist/child-project.js +21 -0
- package/dist/ci-jobs.d.ts +227 -0
- package/dist/ci-jobs.d.ts.map +1 -0
- package/dist/ci-jobs.js +448 -0
- package/dist/claimed-fields.js +135 -0
- package/dist/cli/build-options.js +70 -0
- package/dist/cli/build-params-cli.js +139 -0
- package/dist/cli/command-group.js +123 -0
- package/dist/cli/commands/audit-agents.js +326 -0
- package/dist/cli/commands/audit.js +571 -0
- package/dist/cli/commands/build.js +920 -0
- package/dist/cli/commands/carve-apply.js +153 -0
- package/dist/cli/commands/carve-bridge.js +201 -0
- package/dist/cli/commands/carve-emit.js +332 -0
- package/dist/cli/commands/carve-status.js +119 -0
- package/dist/cli/commands/carve.js +204 -0
- package/dist/cli/commands/check-lexicon-docs.js +79 -0
- package/dist/cli/commands/check-lexicon-examples.js +177 -0
- package/dist/cli/commands/check-lexicon-intrinsics.js +291 -0
- package/dist/cli/commands/check-lexicon-mcp.js +82 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +104 -0
- package/dist/cli/commands/check-lexicon.js +766 -0
- package/dist/cli/commands/describe.js +85 -0
- package/dist/cli/commands/diff.js +175 -0
- package/dist/cli/commands/doctor.js +350 -0
- package/dist/cli/commands/import-agents.js +155 -0
- package/dist/cli/commands/import.js +670 -0
- package/dist/cli/commands/init-lexicon/templates/codegen.js +194 -0
- package/dist/cli/commands/init-lexicon/templates/docs.js +100 -0
- package/dist/cli/commands/init-lexicon/templates/examples.js +29 -0
- package/dist/cli/commands/init-lexicon/templates/lint.js +34 -0
- package/dist/cli/commands/init-lexicon/templates/lsp.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/plugin.js +108 -0
- package/dist/cli/commands/init-lexicon/templates/project.js +240 -0
- package/dist/cli/commands/init-lexicon/templates/spec.js +56 -0
- package/dist/cli/commands/init-lexicon/templates/tests.js +66 -0
- package/dist/cli/commands/init-lexicon.js +172 -0
- package/dist/cli/commands/init.js +381 -0
- package/dist/cli/commands/lexicon-rolling-upgrade.js +145 -0
- package/dist/cli/commands/lexicon-surface-diff.js +145 -0
- package/dist/cli/commands/licenses.js +125 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +948 -0
- package/dist/cli/commands/list.js +63 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +423 -0
- package/dist/cli/commands/onboard.js +403 -0
- package/dist/cli/commands/pinned-upgrade.js +191 -0
- package/dist/cli/commands/update.js +194 -0
- package/dist/cli/commands/vendor.js +217 -0
- package/dist/cli/conflict-check.js +104 -0
- package/dist/cli/debug.js +8 -0
- package/dist/cli/format.js +104 -0
- package/dist/cli/handlers/build.js +267 -0
- package/dist/cli/handlers/carve-apply.js +30 -0
- package/dist/cli/handlers/carve-bridge.js +28 -0
- package/dist/cli/handlers/carve-emit.js +50 -0
- package/dist/cli/handlers/carve-status.js +26 -0
- package/dist/cli/handlers/carve.js +40 -0
- package/dist/cli/handlers/change-set.js +46 -0
- package/dist/cli/handlers/components.js +730 -0
- package/dist/cli/handlers/dev.js +113 -0
- package/dist/cli/handlers/emulator.js +233 -0
- package/dist/cli/handlers/explain.js +54 -0
- package/dist/cli/handlers/fan-out.js +422 -0
- package/dist/cli/handlers/gate-resume.js +275 -0
- package/dist/cli/handlers/generate.js +84 -0
- package/dist/cli/handlers/graph.js +969 -0
- package/dist/cli/handlers/init.js +102 -0
- package/dist/cli/handlers/lifecycle.js +1727 -0
- package/dist/cli/handlers/lint.js +62 -0
- package/dist/cli/handlers/migrate.js +48 -0
- package/dist/cli/handlers/misc.js +391 -0
- package/dist/cli/handlers/operator.js +1162 -0
- package/dist/cli/handlers/override-approve.js +102 -0
- package/dist/cli/handlers/pr.js +503 -0
- package/dist/cli/handlers/promote.js +342 -0
- package/dist/cli/handlers/run-generate.js +206 -0
- package/dist/cli/handlers/run-wave.js +66 -0
- package/dist/cli/handlers/run.js +1191 -0
- package/dist/cli/handlers/scenario.js +325 -0
- package/dist/cli/handlers/search.js +924 -0
- package/dist/cli/handlers/serve.js +32 -0
- package/dist/cli/handlers/vendor.js +154 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/is-entry-point.js +23 -0
- package/dist/cli/lsp/capabilities.js +28 -0
- package/dist/cli/lsp/diagnostics.js +34 -0
- package/dist/cli/lsp/server.js +329 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +2540 -0
- package/dist/cli/mcp/lifecycle-tools.js +80 -0
- package/dist/cli/mcp/op-tools.js +306 -0
- package/dist/cli/mcp/resource-handlers.js +258 -0
- package/dist/cli/mcp/resources/context.js +85 -0
- package/dist/cli/mcp/server.js +353 -0
- package/dist/cli/mcp/tools/build.js +111 -0
- package/dist/cli/mcp/tools/composites.js +95 -0
- package/dist/cli/mcp/tools/explain.js +166 -0
- package/dist/cli/mcp/tools/import.js +49 -0
- package/dist/cli/mcp/tools/lint.js +41 -0
- package/dist/cli/mcp/tools/scaffold.js +111 -0
- package/dist/cli/mcp/tools/search.js +112 -0
- package/dist/cli/mcp/types.js +1 -0
- package/dist/cli/mcp/workspace-plugins.js +91 -0
- package/dist/cli/mcp/workspace-tools.d.ts +1 -1
- package/dist/cli/mcp/workspace-tools.js +564 -0
- package/dist/cli/mcp-config.js +70 -0
- package/dist/cli/plugins.js +313 -0
- package/dist/cli/registry.js +23 -0
- package/dist/cli/reporters/stylish.js +329 -0
- package/dist/cli/version.js +15 -0
- package/dist/cli/watch.js +79 -0
- package/dist/codegen/bound-property-types.js +82 -0
- package/dist/codegen/coverage.js +119 -0
- package/dist/codegen/docs-file-markers.js +60 -0
- package/dist/codegen/docs-pages.js +115 -0
- package/dist/codegen/docs-rule-scanning.js +164 -0
- package/dist/codegen/docs-rules.js +76 -0
- package/dist/codegen/docs-sections.js +117 -0
- package/dist/codegen/docs-sidebar.js +65 -0
- package/dist/codegen/docs-types.js +4 -0
- package/dist/codegen/docs.js +356 -0
- package/dist/codegen/fetch.js +340 -0
- package/dist/codegen/generate-post-synth-barrel.js +63 -0
- package/dist/codegen/generate-registry.js +71 -0
- package/dist/codegen/generate-runtime-index.js +56 -0
- package/dist/codegen/generate-typescript.js +119 -0
- package/dist/codegen/generate.js +115 -0
- package/dist/codegen/json-patch.js +141 -0
- package/dist/codegen/json-schema-to-ts.js +344 -0
- package/dist/codegen/json-schema.js +246 -0
- package/dist/codegen/lexicon-regen.js +412 -0
- package/dist/codegen/naming.js +217 -0
- package/dist/codegen/okf-lexicon.js +245 -0
- package/dist/codegen/package.js +150 -0
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.js +494 -0
- package/dist/codegen/registry.js +60 -0
- package/dist/codegen/rehype-base-url.mjs +68 -0
- package/dist/codegen/rolling-upgrade.js +217 -0
- package/dist/codegen/surface-snapshot.js +537 -0
- package/dist/codegen/testdata/postgres-tags.json +242 -0
- package/dist/codegen/topo-sort.js +36 -0
- package/dist/codegen/typecheck.js +115 -0
- package/dist/codegen/validate.js +200 -0
- package/dist/codegen/version-bump.js +35 -0
- package/dist/components/auto-release.js +143 -0
- package/dist/components/builders.js +47 -0
- package/dist/components/capability-plugin-loader.js +175 -0
- package/dist/components/capability-plugin-schema.js +46 -0
- package/dist/components/capability-plugin.js +99 -0
- package/dist/components/capability.js +60 -0
- package/dist/components/changed-files.js +72 -0
- package/dist/components/cli-support.js +427 -0
- package/dist/components/component.js +120 -0
- package/dist/components/component.schema.json +271 -0
- package/dist/components/config-defaults.js +130 -0
- package/dist/components/deploy-units.js +60 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/discover.js +189 -0
- package/dist/components/driver-output.js +86 -0
- package/dist/components/driver.js +728 -0
- package/dist/components/fan-out-output.js +165 -0
- package/dist/components/fan-out-record.js +94 -0
- package/dist/components/fan-out-run.js +373 -0
- package/dist/components/fan-out-support.js +60 -0
- package/dist/components/fan-out.js +273 -0
- package/dist/components/gate-plan.js +41 -0
- package/dist/components/gated-wave-pipeline.js +46 -0
- package/dist/components/index.js +24 -0
- package/dist/components/layers.js +28 -0
- package/dist/components/manifest-persistence.js +108 -0
- package/dist/components/pilots/alb-ecs.pilot.js +59 -0
- package/dist/components/pilots/dynamodb.pilot.js +51 -0
- package/dist/components/pilots/jar-emr.pilot.js +65 -0
- package/dist/components/pilots/lambda.pilot.js +52 -0
- package/dist/components/pilots/neo4j-fanout.pilot.js +68 -0
- package/dist/components/pilots/project.js +12 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +135 -0
- package/dist/components/pr-run.js +363 -0
- package/dist/components/presets/ecs-fargate.js +72 -0
- package/dist/components/presets/index.js +28 -0
- package/dist/components/presets/lambda.js +34 -0
- package/dist/components/presets/single-host-compose.js +41 -0
- package/dist/components/promote.js +549 -0
- package/dist/components/registry.js +34 -0
- package/dist/components/run-progress.js +33 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/components/sandbox/driver.js +93 -0
- package/dist/components/sandbox/run.js +115 -0
- package/dist/components/starter-plugin.js +104 -0
- package/dist/components/verbs/__tests__/mock-cloud-executor.js +68 -0
- package/dist/components/verbs/__tests__/mock-process-runner.js +45 -0
- package/dist/components/verbs/__tests__/mock-sbom-generator.js +56 -0
- package/dist/components/verbs/bom-writer.js +287 -0
- package/dist/components/verbs/build-archive.js +188 -0
- package/dist/components/verbs/build.js +142 -0
- package/dist/components/verbs/cloud-executor.js +110 -0
- package/dist/components/verbs/component-bom.js +125 -0
- package/dist/components/verbs/ensure-secret.js +50 -0
- package/dist/components/verbs/index.js +61 -0
- package/dist/components/verbs/license-policy.js +75 -0
- package/dist/components/verbs/lockfile-sbom-generator.js +191 -0
- package/dist/components/verbs/process-runner.js +78 -0
- package/dist/components/verbs/r2-sync.js +93 -0
- package/dist/components/verbs/reproducibility.js +62 -0
- package/dist/components/verbs/run-agent.js +577 -0
- package/dist/components/verbs/sbom-generator.js +119 -0
- package/dist/components/verbs/sbom.js +97 -0
- package/dist/components/verbs/shell.js +32 -0
- package/dist/components/verbs/sign.js +221 -0
- package/dist/components/verbs/stub.js +28 -0
- package/dist/components/verbs/tool-sbom-generator.js +154 -0
- package/dist/components/verbs/verify.js +112 -0
- package/dist/components/verbs/vex.js +131 -0
- package/dist/components/verbs/vuln-gate.js +152 -0
- package/dist/components/verbs/vuln-scan.js +288 -0
- package/dist/components/verbs/wait-verify.js +106 -0
- package/dist/components/verbs/wrangler.js +179 -0
- package/dist/components/wave-plan.js +102 -0
- package/dist/composite.d.ts +10 -2
- package/dist/composite.d.ts.map +1 -1
- package/dist/composite.js +313 -0
- package/dist/config-import.d.ts.map +1 -1
- package/dist/config-import.js +22 -0
- package/dist/config-sandbox.js +122 -0
- package/dist/config-static.js +206 -0
- package/dist/config.js +605 -0
- package/dist/content-digest.js +23 -0
- package/dist/declarable.js +114 -0
- package/dist/deep-observation.js +437 -0
- package/dist/detectLexicon.js +54 -0
- package/dist/discovery/cache.js +70 -0
- package/dist/discovery/collect.d.ts +2 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/collect.js +223 -0
- package/dist/discovery/cycles.js +95 -0
- package/dist/discovery/entity-wire-codec.js +391 -0
- package/dist/discovery/entity-wire.js +42 -0
- package/dist/discovery/files.js +121 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +4073 -0
- package/dist/discovery/fold-rank.js +292 -0
- package/dist/discovery/graph.js +115 -0
- package/dist/discovery/host-composite.d.ts +35 -0
- package/dist/discovery/host-composite.d.ts.map +1 -0
- package/dist/discovery/host-composite.js +93 -0
- package/dist/discovery/import.d.ts.map +1 -1
- package/dist/discovery/import.js +65 -0
- package/dist/discovery/index.d.ts +9 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +333 -0
- package/dist/discovery/param-deps.js +487 -0
- package/dist/discovery/resolve.js +39 -0
- package/dist/discovery/sandbox/bundle.js +214 -0
- package/dist/discovery/sandbox/child-errors.js +26 -0
- package/dist/discovery/sandbox/config-run.js +100 -0
- package/dist/discovery/sandbox/config-wire.js +183 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.js +333 -0
- package/dist/discovery/sandbox/fork.js +229 -0
- package/dist/discovery/sandbox/policy-run.js +142 -0
- package/dist/discovery/sandbox/policy-wire.js +152 -0
- package/dist/discovery/sandbox/run.js +77 -0
- package/dist/discovery/specifier-resolve.js +320 -0
- package/dist/discovery/walk.js +284 -0
- package/dist/effect-receipt.js +340 -0
- package/dist/env.js +55 -0
- package/dist/errors.js +62 -0
- package/dist/fold/fold.js +1434 -0
- package/dist/fold/foldable-helpers.js +229 -0
- package/dist/fold/subset.js +696 -0
- package/dist/fold-provenance.js +414 -0
- package/dist/gate-decision.d.ts +20 -9
- package/dist/gate-decision.d.ts.map +1 -1
- package/dist/gate-decision.js +313 -0
- package/dist/gate-resume-run.js +154 -0
- package/dist/gated-waves.js +165 -0
- package/dist/governance.js +43 -0
- package/dist/graph-declared.js +45 -0
- package/dist/graph-detail.js +207 -0
- package/dist/graph-dot.js +54 -0
- package/dist/graph-effective.js +107 -0
- package/dist/graph-ir.js +623 -0
- package/dist/graph-layout.js +184 -0
- package/dist/graph-lens.js +105 -0
- package/dist/graph-mermaid.js +83 -0
- package/dist/graph-ops.js +39 -0
- package/dist/graph-refs.js +156 -0
- package/dist/held-elsewhere.js +87 -0
- package/dist/identity.js +251 -0
- package/dist/import/base-parser.js +34 -0
- package/dist/import/embedded.js +255 -0
- package/dist/import/generator.js +1 -0
- package/dist/import/ir-utils.js +85 -0
- package/dist/import/parser-options.js +45 -0
- package/dist/import/parser.js +1 -0
- package/dist/index.js +118 -0
- package/dist/intrinsic-interpolation.js +73 -0
- package/dist/intrinsic.js +31 -0
- package/dist/join-key.js +35 -0
- package/dist/kubectl-context.js +150 -0
- package/dist/lexicon-config.js +58 -0
- package/dist/lexicon-integrity.js +47 -0
- package/dist/lexicon-manifest.js +70 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +241 -0
- package/dist/lexicon-output.js +218 -0
- package/dist/lexicon-plugin-helpers.js +104 -0
- package/dist/lexicon-schema.js +136 -0
- package/dist/lexicon.js +130 -0
- package/dist/lifecycle/affected.js +224 -0
- package/dist/lifecycle/assert-live.js +110 -0
- package/dist/lifecycle/build-ledger-store.js +160 -0
- package/dist/lifecycle/build-ledger.js +155 -0
- package/dist/lifecycle/change-set.js +356 -0
- package/dist/lifecycle/converge-ledger.js +161 -0
- package/dist/lifecycle/deep-diff.js +233 -0
- package/dist/lifecycle/deep-observe.js +147 -0
- package/dist/lifecycle/digest.js +143 -0
- package/dist/lifecycle/disruption.js +135 -0
- package/dist/lifecycle/gate-applied.js +113 -0
- package/dist/lifecycle/gate-audit.d.ts +69 -2
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.js +415 -0
- package/dist/lifecycle/gate-ledger-select.js +89 -0
- package/dist/lifecycle/gate-ledger.d.ts +24 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.js +201 -0
- package/dist/lifecycle/gate-origin.js +117 -0
- package/dist/lifecycle/gate-pending.js +58 -0
- package/dist/lifecycle/git.js +1182 -0
- package/dist/lifecycle/identity.js +58 -0
- package/dist/lifecycle/index.js +26 -0
- package/dist/lifecycle/lease.js +283 -0
- package/dist/lifecycle/ledger-port.js +1 -0
- package/dist/lifecycle/legacy-digest.js +91 -0
- package/dist/lifecycle/live-diff.js +274 -0
- package/dist/lifecycle/local-note.js +15 -0
- package/dist/lifecycle/member-ledger.js +122 -0
- package/dist/lifecycle/observation-baseline.js +155 -0
- package/dist/lifecycle/observe.js +375 -0
- package/dist/lifecycle/oras-referrer-lookup.js +82 -0
- package/dist/lifecycle/override-ledger.js +54 -0
- package/dist/lifecycle/plan-digest.js +110 -0
- package/dist/lifecycle/plan-ledger.js +129 -0
- package/dist/lifecycle/receipt-plan.js +192 -0
- package/dist/lifecycle/release-ledger.js +155 -0
- package/dist/lifecycle/release-read.js +64 -0
- package/dist/lifecycle/replay.js +193 -0
- package/dist/lifecycle/rollback.js +102 -0
- package/dist/lifecycle/run-ledger.js +183 -0
- package/dist/lifecycle/scenario-eval.js +204 -0
- package/dist/lifecycle/scenario.js +214 -0
- package/dist/lifecycle/snapshot.js +239 -0
- package/dist/lifecycle/status.js +447 -0
- package/dist/lifecycle/symptoms.js +117 -0
- package/dist/lifecycle/teardown.js +255 -0
- package/dist/lifecycle/types.js +1 -0
- package/dist/lifecycle/unobserved-gate.js +33 -0
- package/dist/lifecycle/wave-plans.js +68 -0
- package/dist/lifecycle/work-item-id.js +36 -0
- package/dist/lifecycle/work-lease.js +393 -0
- package/dist/lint/component-checks.js +87 -0
- package/dist/lint/config.js +483 -0
- package/dist/lint/declarative.js +122 -0
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +138 -0
- package/dist/lint/engine.js +246 -0
- package/dist/lint/knowledge-checks.js +64 -0
- package/dist/lint/named-checks.js +22 -0
- package/dist/lint/output-checks.js +87 -0
- package/dist/lint/output-docs.js +163 -0
- package/dist/lint/parser.js +33 -0
- package/dist/lint/pipeline-change-gate.js +70 -0
- package/dist/lint/policy-import.d.ts +1 -1
- package/dist/lint/policy-import.d.ts.map +1 -1
- package/dist/lint/policy-import.js +62 -0
- package/dist/lint/policy-sandbox.js +86 -0
- package/dist/lint/policy.js +122 -0
- package/dist/lint/post-synth.js +87 -0
- package/dist/lint/presets/relaxed.json +26 -0
- package/dist/lint/presets/strict.json +24 -24
- package/dist/lint/receipt-checks.js +83 -0
- package/dist/lint/rule-loader.d.ts.map +1 -1
- package/dist/lint/rule-loader.js +89 -0
- package/dist/lint/rule-registry.js +86 -0
- package/dist/lint/rule.js +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.js +7 -0
- package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.js +7 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.js +9 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.js +9 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.js +25 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.js +21 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.js +24 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.js +17 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.js +18 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.js +20 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.js +12 -0
- package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.js +16 -0
- package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.js +12 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.js +10 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.js +16 -0
- package/dist/lint/rules/comp/comp001-publishes-never-applies.js +69 -0
- package/dist/lint/rules/comp/comp002-dangling-wiring-ref.js +84 -0
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.js +103 -0
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.js +70 -0
- package/dist/lint/rules/comp/comp005-capability-kind-is-noun.js +109 -0
- package/dist/lint/rules/comp/comp006-shell-needs-reason.js +49 -0
- package/dist/lint/rules/comp/comp007-composition-sprawl.js +64 -0
- package/dist/lint/rules/comp/index.js +33 -0
- package/dist/lint/rules/comp/support.js +129 -0
- package/dist/lint/rules/composite-scope.js +51 -0
- package/dist/lint/rules/cor017-composite-name-match.js +93 -0
- package/dist/lint/rules/cor018-composite-prefer-lexicon-type.js +136 -0
- package/dist/lint/rules/cor021-env-literal-name.js +100 -0
- package/dist/lint/rules/cor022-receipt-leaf.js +118 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.js +199 -0
- package/dist/lint/rules/declarable-naming-convention.js +62 -0
- package/dist/lint/rules/evl001-non-literal-expression.js +66 -0
- package/dist/lint/rules/evl002-control-flow-resource.js +69 -0
- package/dist/lint/rules/evl003-dynamic-property-access.js +44 -0
- package/dist/lint/rules/evl004-spread-non-const.js +94 -0
- package/dist/lint/rules/evl005-resource-block-body.js +41 -0
- package/dist/lint/rules/evl007-invalid-siblings.js +105 -0
- package/dist/lint/rules/evl009-composite-no-constant.js +158 -0
- package/dist/lint/rules/evl010-composite-no-transform.js +59 -0
- package/dist/lint/rules/evl011-symbolic-in-template.js +106 -0
- package/dist/lint/rules/export-required.js +115 -0
- package/dist/lint/rules/file-declarable-limit.js +88 -0
- package/dist/lint/rules/flat-declarations.js +62 -0
- package/dist/lint/rules/index.js +90 -0
- package/dist/lint/rules/no-cyclic-declarable-ref.js +138 -0
- package/dist/lint/rules/no-redundant-type-import.js +76 -0
- package/dist/lint/rules/no-redundant-value-cast.js +41 -0
- package/dist/lint/rules/no-string-ref.js +59 -0
- package/dist/lint/rules/no-unused-declarable-import.js +75 -0
- package/dist/lint/rules/no-unused-declarable.js +142 -0
- package/dist/lint/rules/op/index.js +28 -0
- package/dist/lint/rules/op/ops012-activity-contract.js +54 -0
- package/dist/lint/rules/op/ops013-step-output-ref.js +88 -0
- package/dist/lint/rules/op/ops014-converge-rule-refusals.js +114 -0
- package/dist/lint/rules/op/ops015-gate-approval.js +45 -0
- package/dist/lint/rules/op/support.js +11 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.js +100 -0
- package/dist/lint/rules/property-kind.js +56 -0
- package/dist/lint/rules/single-concern-file.js +71 -0
- package/dist/lint/rules/sys001-no-literal-key.js +60 -0
- package/dist/lint/selectors.js +168 -0
- package/dist/lint/suppressions.js +129 -0
- package/dist/lint/typescript.js +17 -0
- package/dist/live-endpoint.js +113 -0
- package/dist/live-read-session.js +76 -0
- package/dist/lsp/lexicon-providers.js +163 -0
- package/dist/lsp/types.js +5 -0
- package/dist/managed-fields.js +293 -0
- package/dist/mcp/types.js +4 -0
- package/dist/notify-verify.js +0 -1
- package/dist/notify.js +0 -1
- package/dist/notify.schema.json +72 -0
- package/dist/observation.js +266 -0
- package/dist/okf-read.js +134 -0
- package/dist/okf.js +275 -0
- package/dist/op/__fixtures__/decide-stub-backend.js +125 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -1
- package/dist/op/activities/activity-contracts.js +313 -0
- package/dist/op/activities/apply.js +582 -0
- package/dist/op/activities/build.js +18 -0
- package/dist/op/activities/change-coverage.js +40 -0
- package/dist/op/activities/change-set.d.ts +8 -3
- package/dist/op/activities/change-set.d.ts.map +1 -1
- package/dist/op/activities/change-set.js +67 -0
- package/dist/op/activities/converge.js +492 -0
- package/dist/op/activities/decide.js +211 -0
- package/dist/op/activities/env-teardown.js +110 -0
- package/dist/op/activities/factory.js +591 -0
- package/dist/op/activities/forge-pull-request.js +82 -0
- package/dist/op/activities/gh-host.js +78 -0
- package/dist/op/activities/guard-validate.js +165 -0
- package/dist/op/activities/http-check.js +40 -0
- package/dist/op/activities/index.js +59 -0
- package/dist/op/activities/lexicon-upgrade.js +629 -0
- package/dist/op/activities/lifecycle.js +109 -0
- package/dist/op/activities/pipeline-audit.js +147 -0
- package/dist/op/activities/policy.js +26 -0
- package/dist/op/activities/predict-behaviour.js +341 -0
- package/dist/op/activities/propose-upgrade.js +165 -0
- package/dist/op/activities/reconcile.d.ts +28 -16
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/activities/reconcile.js +1011 -0
- package/dist/op/activities/shell.js +118 -0
- package/dist/op/activities/source-release.js +97 -0
- package/dist/op/activities/source-rollback.js +165 -0
- package/dist/op/activities/teardown.js +17 -0
- package/dist/op/activities/util.js +6 -0
- package/dist/op/activities/wait.js +51 -0
- package/dist/op/activities/work-evidence.js +35 -0
- package/dist/op/activities/workflow-audit.js +133 -0
- package/dist/op/activity-contract-registry.js +106 -0
- package/dist/op/activity-contract.js +232 -0
- package/dist/op/activity-failure.js +31 -0
- package/dist/op/activity-profiles.js +129 -0
- package/dist/op/activity-registry.js +105 -0
- package/dist/op/activity-runtime.js +28 -0
- package/dist/op/builders.d.ts +1 -1
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +801 -0
- package/dist/op/change-signal.js +118 -0
- package/dist/op/composites/apply-op.js +151 -0
- package/dist/op/composites/behaviour-op.js +65 -0
- package/dist/op/composites/converge-op.js +141 -0
- package/dist/op/composites/index.js +19 -0
- package/dist/op/composites/lexicon-upgrade-op.js +107 -0
- package/dist/op/composites/pipeline-audit-op.js +50 -0
- package/dist/op/composites/reconcile-op.js +84 -0
- package/dist/op/composites/watch-op.js +69 -0
- package/dist/op/composites/workflow-audit-op.js +57 -0
- package/dist/op/converge-rule.js +205 -0
- package/dist/op/cron.js +146 -0
- package/dist/op/decide-backend.js +181 -0
- package/dist/op/decide-config.js +68 -0
- package/dist/op/decide-read-inputs.js +141 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +213 -0
- package/dist/op/duration.js +21 -0
- package/dist/op/emulator-freshness.js +80 -0
- package/dist/op/emulator-lifecycle.js +269 -0
- package/dist/op/factory.js +83 -0
- package/dist/op/gate-approval.js +172 -0
- package/dist/op/gate-name.js +86 -0
- package/dist/op/gate-plan-context.js +167 -0
- package/dist/op/gate-point-run.js +67 -0
- package/dist/op/gate-point.js +111 -0
- package/dist/op/gate-resume-plan.js +155 -0
- package/dist/op/gate-resume.js +31 -0
- package/dist/op/gate-review.js +290 -0
- package/dist/op/gate-run-locator.js +85 -0
- package/dist/op/gate-summary.js +100 -0
- package/dist/op/gate-wait.js +56 -0
- package/dist/op/gate.js +449 -0
- package/dist/op/generate-pipeline.js +124 -0
- package/dist/op/index.js +51 -0
- package/dist/op/lifecycle-receipt-store.js +45 -0
- package/dist/op/local-executor.js +1164 -0
- package/dist/op/local-output.js +140 -0
- package/dist/op/op-ir.js +317 -0
- package/dist/op/op-verb-class.js +131 -0
- package/dist/op/op-waves-notify.js +136 -0
- package/dist/op/op-waves-run.d.ts +1 -1
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +1657 -0
- package/dist/op/op-waves.js +409 -0
- package/dist/op/operator.js +743 -0
- package/dist/op/receipt-store.js +94 -0
- package/dist/op/resource.js +23 -0
- package/dist/op/run-context.js +28 -0
- package/dist/op/run-live.js +256 -0
- package/dist/op/runtime.js +21 -0
- package/dist/op/runtimes/local.js +195 -0
- package/dist/op/source-archive.js +131 -0
- package/dist/op/step-output-ref.js +397 -0
- package/dist/op/steward-beside.js +169 -0
- package/dist/op/steward-points.js +138 -0
- package/dist/op/steward-turn.js +59 -0
- package/dist/op/steward.js +317 -0
- package/dist/op/types.js +25 -0
- package/dist/op/work-lease-decl.js +81 -0
- package/dist/op/work-lease-run.js +393 -0
- package/dist/override-gate.js +284 -0
- package/dist/owner-chain.js +74 -0
- package/dist/ownership.js +92 -0
- package/dist/params.js +61 -0
- package/dist/plan-summary.js +711 -0
- package/dist/point-decision.js +211 -0
- package/dist/pr-forge.js +810 -0
- package/dist/pr-loop.js +203 -0
- package/dist/project-codegen.js +191 -0
- package/dist/project-root.js +74 -0
- package/dist/project-validation.js +50 -0
- package/dist/provenance.js +157 -0
- package/dist/pseudo-parameter.js +40 -0
- package/dist/reconcile.js +450 -0
- package/dist/resource-attributes.js +33 -0
- package/dist/runtime-adapter.js +77 -0
- package/dist/runtime.js +66 -0
- package/dist/secret-materialization.js +153 -0
- package/dist/secret-provenance.js +250 -0
- package/dist/serializer-walker.js +112 -0
- package/dist/serializer.js +1 -0
- package/dist/sort.js +45 -0
- package/dist/stack-output.js +115 -0
- package/dist/telemetry-attribution.js +149 -0
- package/dist/terraform/__fixtures__/build-graph.js +63 -0
- package/dist/terraform/adopt-state.js +40 -0
- package/dist/terraform/aws-resources.js +384 -0
- package/dist/terraform/bridge.js +204 -0
- package/dist/terraform/carve-provider.js +155 -0
- package/dist/terraform/carve.js +138 -0
- package/dist/terraform/data-source-shape.js +57 -0
- package/dist/terraform/emit-source.js +36 -0
- package/dist/terraform/excise.js +123 -0
- package/dist/terraform/graduate.js +128 -0
- package/dist/terraform/graph.js +435 -0
- package/dist/terraform/hcl2json.js +60 -0
- package/dist/terraform/manifest.js +149 -0
- package/dist/terraform/parse.js +81 -0
- package/dist/terraform/providers/aws.js +146 -0
- package/dist/terraform/providers/gcp.js +294 -0
- package/dist/terraform/providers/index.js +17 -0
- package/dist/terraform/providers/kubernetes.js +253 -0
- package/dist/terraform/score.js +151 -0
- package/dist/terraform/state.js +67 -0
- package/dist/terraform/tier-map.js +74 -0
- package/dist/terraform/types.js +13 -0
- package/dist/terraform/unified-diff.js +132 -0
- package/dist/testing.js +295 -0
- package/dist/toml-emit.js +153 -0
- package/dist/toml-parse.js +355 -0
- package/dist/toml-utils.js +60 -0
- package/dist/toml.js +428 -0
- package/dist/ts-loader.d.ts +46 -0
- package/dist/ts-loader.d.ts.map +1 -0
- package/dist/ts-loader.js +103 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +61 -0
- package/dist/validation.js +20 -0
- package/dist/vendor/js-yaml-load.js +1662 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +119 -0
- package/dist/workspace/__fixtures__/decide-workspace.js +112 -0
- package/dist/workspace/__fixtures__/sessions.d.ts +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.js +91 -0
- package/dist/workspace/__fixtures__/two-member-workspace.js +34 -0
- package/dist/workspace/agent-cli.js +99 -0
- package/dist/workspace/agent.schema.json +390 -0
- package/dist/workspace/answers-ledger.js +89 -0
- package/dist/workspace/box-cli.js +142 -0
- package/dist/workspace/box-factory-write.js +196 -0
- package/dist/workspace/box-factory-write.schema.json +342 -0
- package/dist/workspace/box-factory.js +88 -0
- package/dist/workspace/box-intent.js +126 -0
- package/dist/workspace/box-isolation.js +169 -0
- package/dist/workspace/box-listing-write.schema.json +204 -0
- package/dist/workspace/box-listing.js +298 -0
- package/dist/workspace/box-publish.js +352 -0
- package/dist/workspace/box-publish.schema.json +498 -0
- package/dist/workspace/box-services.js +39 -0
- package/dist/workspace/broker-protocol.js +359 -0
- package/dist/workspace/broker-protocol.schema.json +228 -0
- package/dist/workspace/change-set.schema.json +462 -0
- package/dist/workspace/changes-cli.js +56 -0
- package/dist/workspace/changes.js +446 -0
- package/dist/workspace/changes.schema.json +642 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.js +198 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.js +155 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.js +214 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.js +180 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.js +99 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.js +847 -0
- package/dist/workspace/chant-migrations.js +113 -0
- package/dist/workspace/check.schema.json +433 -0
- package/dist/workspace/checks/box-isolation.js +62 -0
- package/dist/workspace/checks/boxes.js +491 -0
- package/dist/workspace/checks/diagrams.js +104 -0
- package/dist/workspace/checks/generated.js +419 -0
- package/dist/workspace/checks/ledgers.js +212 -0
- package/dist/workspace/checks/links.js +191 -0
- package/dist/workspace/checks/live.js +100 -0
- package/dist/workspace/checks/pipelines.js +188 -0
- package/dist/workspace/checks/records.js +169 -0
- package/dist/workspace/checks.js +426 -0
- package/dist/workspace/ci-green-cli.js +125 -0
- package/dist/workspace/ci-green-forge.js +83 -0
- package/dist/workspace/ci-green-workflow.d.ts +3 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +202 -0
- package/dist/workspace/ci-green.js +325 -0
- package/dist/workspace/ci-last-green.schema.json +158 -0
- package/dist/workspace/class-data.d.ts +84 -0
- package/dist/workspace/class-data.d.ts.map +1 -0
- package/dist/workspace/class-data.js +238 -0
- package/dist/workspace/compose-graph.js +235 -0
- package/dist/workspace/compose-reports.js +146 -0
- package/dist/workspace/composites.js +264 -0
- package/dist/workspace/composites.schema.json +617 -0
- package/dist/workspace/conformance/__fixture__/app/package.json +7 -0
- package/dist/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
- package/dist/workspace/conformance/__fixture__/decisions/decision.kind.mjs +54 -0
- package/dist/workspace/conformance/__fixture__/decisions/decision.schema.json +512 -0
- package/dist/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
- package/dist/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
- package/dist/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
- package/dist/workspace/conformance/__fixture__/delivery/package.json +7 -0
- package/dist/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
- package/dist/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
- package/dist/workspace/conformance/__fixtures__/reference-broker.js +205 -0
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +32 -0
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +135 -0
- package/dist/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
- package/dist/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
- package/dist/workspace/conformance/__writer_fixture__/decisions/points.json +228 -0
- package/dist/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
- package/dist/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
- package/dist/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +54 -0
- package/dist/workspace/conformance/__writer_fixture__/sessions/session.schema.json +289 -0
- package/dist/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +603 -0
- package/dist/workspace/conformance/broker.js +736 -0
- package/dist/workspace/conformance/index.js +673 -0
- package/dist/workspace/conformance/vitest.js +108 -0
- package/dist/workspace/conformance/writer.js +1080 -0
- package/dist/workspace/decide.js +879 -0
- package/dist/workspace/decision-points.schema.json +135 -135
- package/dist/workspace/declaration-write-cli.js +74 -0
- package/dist/workspace/declaration-write.js +238 -0
- package/dist/workspace/declaration.js +938 -0
- package/dist/workspace/declaration.schema.json +1413 -1413
- package/dist/workspace/declared-kinds.js +106 -0
- package/dist/workspace/environments.js +164 -0
- package/dist/workspace/evidence.schema.json +279 -0
- package/dist/workspace/export-cli.js +185 -0
- package/dist/workspace/export.js +499 -0
- package/dist/workspace/factory-rules.js +175 -0
- package/dist/workspace/generated-files.js +92 -0
- package/dist/workspace/glob.js +45 -0
- package/dist/workspace/graph-cache.js +352 -0
- package/dist/workspace/graph-cli.js +348 -0
- package/dist/workspace/graph.schema.json +1338 -0
- package/dist/workspace/identity-gate-rule.js +75 -0
- package/dist/workspace/identity.js +126 -0
- package/dist/workspace/import.js +283 -0
- package/dist/workspace/init.js +620 -0
- package/dist/workspace/intent-cli.js +263 -0
- package/dist/workspace/intent-joins.js +201 -0
- package/dist/workspace/intent-record.js +352 -0
- package/dist/workspace/intent-record.schema.json +864 -0
- package/dist/workspace/intent.js +1582 -0
- package/dist/workspace/intent.schema.json +2420 -0
- package/dist/workspace/json-edit.js +221 -0
- package/dist/workspace/jsonc.js +235 -0
- package/dist/workspace/kind-readers.js +103 -0
- package/dist/workspace/kinds.d.ts +3 -7
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/kinds.js +553 -0
- package/dist/workspace/lexicon-symbols.js +112 -0
- package/dist/workspace/lineage-adopt-cli.js +102 -0
- package/dist/workspace/lineage-adopt.js +360 -0
- package/dist/workspace/lineage-check.js +282 -0
- package/dist/workspace/lineage-cli.js +121 -0
- package/dist/workspace/lineage-hash-index.js +284 -0
- package/dist/workspace/lineage-init.js +507 -0
- package/dist/workspace/lineage-lock.js +387 -0
- package/dist/workspace/lineage-merge.js +45 -0
- package/dist/workspace/lineage-migrations.d.ts.map +1 -1
- package/dist/workspace/lineage-migrations.js +329 -0
- package/dist/workspace/lineage-provenance.js +36 -0
- package/dist/workspace/lineage-update.js +138 -0
- package/dist/workspace/lineage-upgrade-cli.js +131 -0
- package/dist/workspace/lineage-upgrade.js +889 -0
- package/dist/workspace/lineage-vendor.js +171 -0
- package/dist/workspace/lineage-version.js +128 -0
- package/dist/workspace/lineage-versions.js +275 -0
- package/dist/workspace/links.js +330 -0
- package/dist/workspace/ls-generated.js +95 -0
- package/dist/workspace/ls.js +277 -0
- package/dist/workspace/ls.schema.json +333 -0
- package/dist/workspace/member-commands.js +508 -0
- package/dist/workspace/member-handles.js +216 -0
- package/dist/workspace/member-pipeline.js +258 -0
- package/dist/workspace/member-run.js +117 -0
- package/dist/workspace/member-write.schema.json +193 -0
- package/dist/workspace/nested-graph.js +156 -0
- package/dist/workspace/nesting.js +38 -0
- package/dist/workspace/patch.js +301 -0
- package/dist/workspace/patch.schema.json +321 -0
- package/dist/workspace/pin-cli.js +55 -0
- package/dist/workspace/pin-integrity.js +100 -0
- package/dist/workspace/plan-summary.schema.json +155 -0
- package/dist/workspace/point-answer.schema.json +129 -129
- package/dist/workspace/points-cli.js +291 -0
- package/dist/workspace/points-write.schema.json +660 -0
- package/dist/workspace/points.js +521 -0
- package/dist/workspace/points.schema.json +916 -0
- package/dist/workspace/pr-report.schema.json +108 -0
- package/dist/workspace/principal-classes.d.ts +4 -11
- package/dist/workspace/principal-classes.d.ts.map +1 -1
- package/dist/workspace/principal-classes.js +162 -0
- package/dist/workspace/reason-codes.js +326 -0
- package/dist/workspace/record-assets.js +151 -0
- package/dist/workspace/record-decided.js +105 -0
- package/dist/workspace/record-sessions.js +96 -0
- package/dist/workspace/record-source.js +114 -0
- package/dist/workspace/records-amend.schema.json +284 -0
- package/dist/workspace/records-checkout.js +76 -0
- package/dist/workspace/records-cli.js +707 -0
- package/dist/workspace/records-close.js +116 -0
- package/dist/workspace/records-close.schema.json +278 -0
- package/dist/workspace/records-new.schema.json +202 -0
- package/dist/workspace/records-review.schema.json +367 -0
- package/dist/workspace/records-since.js +317 -0
- package/dist/workspace/records-since.schema.json +218 -0
- package/dist/workspace/records-write.js +1229 -0
- package/dist/workspace/records.js +1345 -0
- package/dist/workspace/records.schema.json +604 -0
- package/dist/workspace/reference-kinds/app/package.json +10 -0
- package/dist/workspace/reference-kinds/app/workspace-kinds.json +45 -0
- package/dist/workspace/returns.js +303 -0
- package/dist/workspace/root-refusal.js +75 -0
- package/dist/workspace/run-statement.schema.json +622 -0
- package/dist/workspace/runs-cli.js +584 -0
- package/dist/workspace/runs-write.schema.json +994 -0
- package/dist/workspace/runs.js +708 -0
- package/dist/workspace/runs.schema.json +1257 -0
- package/dist/workspace/runtimes.js +76 -0
- package/dist/workspace/session-kinds.js +83 -0
- package/dist/workspace/signers.schema.json +206 -0
- package/dist/workspace/source-block.js +125 -0
- package/dist/workspace/squash.js +177 -0
- package/dist/workspace/status-gates.js +162 -0
- package/dist/workspace/status-stewards.js +330 -0
- package/dist/workspace/status.js +514 -0
- package/dist/workspace/status.schema.json +998 -0
- package/dist/workspace/steward-index.js +191 -0
- package/dist/workspace/symbols.js +188 -0
- package/dist/workspace/template-manifest.js +227 -0
- package/dist/workspace/template-pins.js +115 -0
- package/dist/workspace/trailer-joins.js +65 -0
- package/dist/workspace/trailers.js +111 -0
- package/dist/workspace/tree.js +136 -0
- package/dist/workspace/trust/attestor.js +63 -0
- package/dist/workspace/trust/dsse.js +108 -0
- package/dist/workspace/trust/evidence-cli.js +175 -0
- package/dist/workspace/trust/evidence.js +161 -0
- package/dist/workspace/trust/gate-seal.js +226 -0
- package/dist/workspace/trust/policy.js +190 -0
- package/dist/workspace/trust/provenance.js +180 -0
- package/dist/workspace/trust/rotation.js +298 -0
- package/dist/workspace/trust/run-commits.js +101 -0
- package/dist/workspace/trust/run-statement.js +182 -0
- package/dist/workspace/trust/seal.js +272 -0
- package/dist/workspace/trust/signers-cli.js +152 -0
- package/dist/workspace/trust/ssh-commit.js +178 -0
- package/dist/workspace/trust/test-repo.js +118 -0
- package/dist/workspace/trust/trust-files.js +193 -0
- package/dist/workspace/trust/verify-cli.js +63 -0
- package/dist/workspace/trust/verify.js +192 -0
- package/dist/workspace/which-chant.js +141 -0
- package/dist/workspace/wip-cli.js +95 -0
- package/dist/workspace/wip-write.schema.json +504 -0
- package/dist/workspace/wip.js +631 -0
- package/dist/workspace/wip.schema.json +334 -0
- package/dist/workspace/work-cli.js +448 -0
- package/dist/workspace/work-evidence.js +122 -0
- package/dist/workspace/work-evidence.schema.json +222 -0
- package/dist/workspace/work-history.schema.json +421 -0
- package/dist/workspace/work-lease.schema.json +246 -0
- package/dist/workspace/work-size.js +77 -0
- package/dist/workspace/work.js +283 -0
- package/dist/workspace/workspace-kinds.schema.json +259 -259
- package/dist/workspace/workspace-principals.schema.json +53 -53
- package/dist/workspace/write-lock-cli.js +105 -0
- package/dist/workspace/write-lock.js +424 -0
- package/dist/workspace/write-lock.schema.json +172 -0
- package/dist/workspace/write-scope.js +484 -0
- package/dist/yaml.js +459 -0
- package/package.json +2 -2
- package/src/build.ts +8 -0
- package/src/change-set.test.ts +48 -0
- package/src/change-set.ts +52 -2
- package/src/ci-jobs.test.ts +49 -0
- package/src/ci-jobs.ts +515 -0
- package/src/cli/commands/check-lexicon-plugin.ts +2 -0
- package/src/cli/commands/lint.ts +3 -0
- package/src/cli/commands/migrate.ts +2 -0
- package/src/cli/compiled-cli.e2e.test.ts +165 -0
- package/src/cli/main.ts +121 -27
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -0
- package/src/components/discover.ts +2 -0
- package/src/components/pr-pipeline.ts +3 -2
- package/src/components/sandbox/driver.ts +3 -1
- package/src/composite.ts +15 -3
- package/src/config-import.ts +3 -0
- package/src/discovery/collect.ts +15 -1
- package/src/discovery/fold-build-parity.test.ts +71 -0
- package/src/discovery/fold-import.ts +25 -127
- package/src/discovery/fold-project-provenance.test.ts +21 -4
- package/src/discovery/host-composite.ts +101 -0
- package/src/discovery/import.ts +2 -0
- package/src/discovery/index.ts +11 -1
- package/src/discovery/sandbox/driver.ts +3 -1
- package/src/gate-decision-bundle.test.ts +5 -4
- package/src/gate-decision-identity.test.ts +59 -6
- package/src/gate-decision.ts +81 -21
- package/src/lexicon-module.ts +3 -0
- package/src/lifecycle/gate-audit.test.ts +55 -1
- package/src/lifecycle/gate-audit.ts +116 -3
- package/src/lifecycle/gate-ledger.test.ts +20 -1
- package/src/lifecycle/gate-ledger.ts +33 -1
- package/src/lint/config.ts +1 -1
- package/src/lint/discover.ts +28 -8
- package/src/lint/policy-import.ts +4 -1
- package/src/lint/rule-loader.ts +2 -0
- package/src/op/activities/activity-contracts.ts +1 -1
- package/src/op/activities/change-set.ts +11 -3
- package/src/op/activities/lexicon-upgrade.test.ts +2 -0
- package/src/op/activities/reconcile.test.ts +62 -2
- package/src/op/activities/reconcile.ts +46 -22
- package/src/op/builders.ts +1 -1
- package/src/op/discover.ts +3 -0
- package/src/op/op-waves-run.ts +4 -3
- package/src/op/op-waves.test.ts +2 -0
- package/src/op/work-lease-run.test.ts +12 -4
- package/src/ts-loader.test.ts +23 -0
- package/src/ts-loader.ts +122 -0
- package/src/workspace/__fixtures__/contract-repo.ts +17 -1
- package/src/workspace/__fixtures__/sessions.ts +3 -3
- package/src/workspace/change-set.schema.json +5 -3
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +1 -1
- package/src/workspace/ci-green-workflow.test.ts +4 -4
- package/src/workspace/ci-green-workflow.ts +5 -2
- package/src/workspace/class-data.test.ts +64 -0
- package/src/workspace/class-data.ts +236 -0
- package/src/workspace/declaration.ts +1 -1
- package/src/workspace/kinds.ts +7 -36
- package/src/workspace/lineage-migrations.ts +2 -0
- package/src/workspace/points.ts +1 -1
- package/src/workspace/principal-classes.ts +12 -95
- package/src/workspace/records-formats.test.ts +7 -2
- package/src/workspace/records-sessions-write.test.ts +3 -3
- package/src/workspace/status-contract.e2e.test.ts +37 -0
- package/src/workspace/status-contract.test.ts +23 -21
|
@@ -0,0 +1,4073 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { dirname, basename, join, sep, isAbsolute, resolve as resolvePath } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { isDeclarable, isResourceDeclarable } from "../declarable.js";
|
|
8
|
+
import { Composite, isCompositeInstance } from "../composite.js";
|
|
9
|
+
import { isAttrRefLike } from "../utils.js";
|
|
10
|
+
import { isIntrinsic } from "../intrinsic.js";
|
|
11
|
+
import { collectConsts, foldResource, fold, propName, FoldError, isFoldableFunction, locate, collectSameFileFunctions, constResolvesToResource, } from "../fold/fold.js";
|
|
12
|
+
import { isChantOwnedSpecifier, isFoldableHelperName } from "../fold/foldable-helpers.js";
|
|
13
|
+
import { lexiconModulePath, lexiconModuleRoot } from "../lexicon-module.js";
|
|
14
|
+
import { isFile, packageExportsTarget, probeModuleFile, splitBareSpecifier, tsconfigPathsMatch, } from "./specifier-resolve.js";
|
|
15
|
+
import { briefNodeText, callExpressionMessage, computedPropertyNameMessage, isLiteralElementKey, unsupportedBinaryMessage, unsupportedExpressionMessage, UNSUPPORTED_OBJECT_MEMBER_MESSAGE, UNSUPPORTED_UNARY_MESSAGE, SUPPORTED_BINARY_OPERATORS, SUPPORTED_UNARY_OPERATORS, } from "../fold/subset.js";
|
|
16
|
+
import { importModule } from "./import.js";
|
|
17
|
+
import { collectCompositeOrigins, collectParamDependencies, collectTagOrigins, sourceLocationOf, } from "./param-deps.js";
|
|
18
|
+
import { getInterpolationFields, getProvenance, setPathProvenance, setProvenance } from "../provenance.js";
|
|
19
|
+
import { foldProvenanceOfEntities } from "../fold-provenance.js";
|
|
20
|
+
import { collectEntities } from "./collect.js";
|
|
21
|
+
import { hostCompositeName, markHostCompositeInstance } from "./host-composite.js";
|
|
22
|
+
import { intrinsicCallFoldsEagerly, intrinsicTagFolds } from "../lexicon.js";
|
|
23
|
+
/**
|
|
24
|
+
* chant #1063 — the package specifier a lexicon NAME (`"aws"`, `"gitlab"`)
|
|
25
|
+
* is installed under. The one naming convention the whole CLI already
|
|
26
|
+
* depends on: `loadPlugin(name)` imports exactly this
|
|
27
|
+
* (../cli/plugins.ts), `detectLexicons` scans source for exactly this
|
|
28
|
+
* (../detectLexicon.ts), and `chant init` writes exactly this into
|
|
29
|
+
* package.json.
|
|
30
|
+
*/
|
|
31
|
+
export function lexiconPackageName(lexiconName) {
|
|
32
|
+
return `@intentius/chant-lexicon-${lexiconName}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a fresh, empty {@link FoldSession}.
|
|
36
|
+
*
|
|
37
|
+
* @param lexicons - chant #1063: the lexicon NAMES active for this build
|
|
38
|
+
* (`["aws", "k8s"]`). Converted to package specifiers via
|
|
39
|
+
* {@link lexiconPackageName}; see {@link FoldSession.lexiconPackages}.
|
|
40
|
+
* @param sandbox - chant #1093: this build asked for the #1045 sandbox, so
|
|
41
|
+
* fold may not import or invoke anything outside the trusted allowlist —
|
|
42
|
+
* see {@link FoldSession.sandbox}.
|
|
43
|
+
*/
|
|
44
|
+
export function createFoldSession(intrinsics = [], buildParams, lexicons = [], sandbox = false,
|
|
45
|
+
/**
|
|
46
|
+
* chant#2438 — package specifiers to allow verbatim, alongside the ones
|
|
47
|
+
* derived from `lexicons`.
|
|
48
|
+
*
|
|
49
|
+
* A chant build's active packages are always `@intentius/chant-lexicon-*`,
|
|
50
|
+
* so naming lexicons is the whole story there. A caller embedding the fold
|
|
51
|
+
* path against a host that is not a chant lexicon has packages the naming
|
|
52
|
+
* convention cannot express: the specification's conformance harness supplies
|
|
53
|
+
* `@tsad/shapes`, which is its host's own specifier. The boundary is
|
|
54
|
+
* unchanged, an explicitly named package of this build, only now it can be
|
|
55
|
+
* named as a specifier rather than only as a lexicon.
|
|
56
|
+
*/
|
|
57
|
+
lexiconPackages = [],
|
|
58
|
+
/** chant#2455 — `ι = executing`. Mutually exclusive with `sandbox`. */
|
|
59
|
+
executing = false,
|
|
60
|
+
/**
|
|
61
|
+
* chant#2590 — the build's own source files. A path lexicon's trusted
|
|
62
|
+
* directory that holds any of them is dropped (see {@link FoldSession.lexiconRoots}).
|
|
63
|
+
*/
|
|
64
|
+
sourceFiles = []) {
|
|
65
|
+
// chant#2577 — a lexicon declared by path is that module and no package, so
|
|
66
|
+
// its name contributes the module path and not `@intentius/chant-lexicon-<name>`.
|
|
67
|
+
const packages = new Set(lexiconPackages);
|
|
68
|
+
const modules = new Set();
|
|
69
|
+
const roots = [];
|
|
70
|
+
for (const name of lexicons) {
|
|
71
|
+
const modulePath = lexiconModulePath(name);
|
|
72
|
+
if (modulePath === undefined)
|
|
73
|
+
packages.add(lexiconPackageName(name));
|
|
74
|
+
else
|
|
75
|
+
modules.add(resolveModulePath(modulePath, modulePath));
|
|
76
|
+
const root = lexiconModuleRoot(name);
|
|
77
|
+
if (root !== undefined && !sourceFiles.some((file) => isUnderDirectory(resolvePath(file), root)))
|
|
78
|
+
roots.push(root);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
intrinsics,
|
|
82
|
+
cache: new Map(),
|
|
83
|
+
stack: [],
|
|
84
|
+
importCache: new Map(),
|
|
85
|
+
resolvePathCache: new Map(),
|
|
86
|
+
buildParams,
|
|
87
|
+
lexiconPackages: packages,
|
|
88
|
+
lexiconModules: modules,
|
|
89
|
+
lexiconRoots: roots,
|
|
90
|
+
sandbox,
|
|
91
|
+
executing,
|
|
92
|
+
factoryModules: new Map(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* chant #1020 hang fix — memo for {@link resolveModulePath}'s BARE
|
|
97
|
+
* (package) specifier branch, deliberately PROCESS-WIDE rather than
|
|
98
|
+
* session-scoped (contrast {@link FoldSession.resolvePathCache}, used for
|
|
99
|
+
* relative specifiers). A bare specifier like `@intentius/chant-lexicon-aws`
|
|
100
|
+
* resolves via `createRequire(fromFile).resolve(specifier)` — which package
|
|
101
|
+
* a name refers to cannot change mid-process (Node's own module cache
|
|
102
|
+
* already assumes this: nothing invalidates `require.cache`/the dynamic
|
|
103
|
+
* `import()` cache either if `node_modules` changes under a running
|
|
104
|
+
* process), so caching the answer for the process's lifetime, across every
|
|
105
|
+
* `discover()` call/`FoldSession`, is exactly as safe as Node's own
|
|
106
|
+
* assumptions — unlike a relative specifier (a project file `chant build
|
|
107
|
+
* --watch` can legitimately add/remove between rebuilds), never unsafe to
|
|
108
|
+
* reuse.
|
|
109
|
+
*
|
|
110
|
+
* This is what actually fixes the hang, not just reduces it: profiling (a
|
|
111
|
+
* real `sample` during the observed multi-minute stall) traced the cost to
|
|
112
|
+
* `createRequire(fromFile).resolve(specifier)` ITSELF taking upwards of a
|
|
113
|
+
* minute — measured on this exact call, in this exact spot, nowhere else —
|
|
114
|
+
* specifically for the FIRST bare-specifier resolution inside a
|
|
115
|
+
* `FoldSession` created right after `vitest`'s `vi.resetModules()`
|
|
116
|
+
* (`sandbox-differential.test.ts`'s own retry path, pre-existing and
|
|
117
|
+
* unrelated to #1020 — confirmed on `main`, which hits the identical retry
|
|
118
|
+
* for the identical reason and stays fast). Two narrower versions of this
|
|
119
|
+
* fix (session-scoped; then process-wide but still keyed by directory) each
|
|
120
|
+
* still paid that cost at least once more: directory-scoping alone pays it
|
|
121
|
+
* once per NEW directory a corpus entry introduces, and #1020's cross-file
|
|
122
|
+
* resolution reaches a real constructor/composite-factory/intrinsic
|
|
123
|
+
* resolution — hence a real bare-specifier resolve — from strictly more
|
|
124
|
+
* directories across a 98-entry corpus than the pre-#1020 single-file fold
|
|
125
|
+
* ever did. Keyed by specifier ALONE (see {@link resolveModulePathMemoized}'s
|
|
126
|
+
* doc for the directory-independence assumption this relies on), the answer
|
|
127
|
+
* computed the first time ANY directory needed a given package is there for
|
|
128
|
+
* every directory after it, `vi.resetModules()` retry or not — the slow call
|
|
129
|
+
* never has to happen a second time in the same process, for any package.
|
|
130
|
+
*/
|
|
131
|
+
const bareSpecifierPathCache = new Map();
|
|
132
|
+
/**
|
|
133
|
+
* Memoized {@link resolveModulePath}. A relative/absolute specifier goes
|
|
134
|
+
* through the session-scoped `resolvePathCache`, keyed by DIRECTORY (not the
|
|
135
|
+
* full file path: `resolveModulePath` only ever consults `dirname(fromFile)`
|
|
136
|
+
* — the relative branch resolves against `dirname(fromFile)`, so the
|
|
137
|
+
* specific FILE within a directory never affects the answer) — a project
|
|
138
|
+
* file can legitimately be added/removed between builds (`chant build
|
|
139
|
+
* --watch`), so this cache is thrown away with the rest of the session.
|
|
140
|
+
*
|
|
141
|
+
* A bare (package) specifier goes through {@link bareSpecifierPathCache}
|
|
142
|
+
* instead, keyed by the specifier ALONE — process-wide, no directory
|
|
143
|
+
* component. `createRequire(fromFile).resolve(specifier)` technically CAN
|
|
144
|
+
* answer differently for the same specifier from two directories (nested
|
|
145
|
+
* `node_modules` with a version override), but `discover()`'s real usage is
|
|
146
|
+
* one `srcDir` per build, whose files overwhelmingly share one effective
|
|
147
|
+
* `node_modules` resolution — the same assumption bundlers (webpack,
|
|
148
|
+
* esbuild, vite) already make for their own module resolution caches. See
|
|
149
|
+
* {@link bareSpecifierPathCache}'s own doc for why process-wide (not just
|
|
150
|
+
* directory-scoped) is what actually fixes the hang: `examples/foo`'s
|
|
151
|
+
* directory and `lexicons/aws/examples/bar`'s directory both need
|
|
152
|
+
* `@intentius/chant-lexicon-aws`, and directory-scoping alone still pays the
|
|
153
|
+
* pathological cost once per NEW directory, not just once per package.
|
|
154
|
+
*/
|
|
155
|
+
function resolveModulePathMemoized(specifier, fromFile, resolvePathCache) {
|
|
156
|
+
const isBare = !specifier.startsWith(".") && !isAbsolute(specifier);
|
|
157
|
+
// chant#3090 — a tsconfig `paths` answer depends on the importing file's
|
|
158
|
+
// directory and can name a project file that comes and goes between
|
|
159
|
+
// builds, so it is memoized per session and directory, like a relative one.
|
|
160
|
+
const key = `${dirname(fromFile)}\0${specifier}`;
|
|
161
|
+
if (isBare) {
|
|
162
|
+
const sessionCached = resolvePathCache.get(key);
|
|
163
|
+
if (sessionCached !== undefined)
|
|
164
|
+
return sessionCached;
|
|
165
|
+
const mapped = pathsMappedTarget(specifier, fromFile);
|
|
166
|
+
if (mapped !== undefined) {
|
|
167
|
+
resolvePathCache.set(key, mapped);
|
|
168
|
+
return mapped;
|
|
169
|
+
}
|
|
170
|
+
const cached = bareSpecifierPathCache.get(specifier);
|
|
171
|
+
if (cached !== undefined)
|
|
172
|
+
return cached;
|
|
173
|
+
const resolved = resolveModulePath(specifier, fromFile);
|
|
174
|
+
bareSpecifierPathCache.set(specifier, resolved);
|
|
175
|
+
return resolved;
|
|
176
|
+
}
|
|
177
|
+
const cached = resolvePathCache.get(key);
|
|
178
|
+
if (cached !== undefined)
|
|
179
|
+
return cached;
|
|
180
|
+
const resolved = resolveModulePath(specifier, fromFile);
|
|
181
|
+
resolvePathCache.set(key, resolved);
|
|
182
|
+
return resolved;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* chant #1020 hang fix — construct an `Error` for a routine, EXPECTED
|
|
186
|
+
* cross-file resolution failure (an unresolvable import, a non-function
|
|
187
|
+
* export, a shape this module doesn't support) WITHOUT V8's normal eager
|
|
188
|
+
* stack-frame capture. Every one of these is thrown, then caught a few
|
|
189
|
+
* frames up and reduced to `.message` — `tryFoldFileCore`'s own top-level
|
|
190
|
+
* catch, ultimately, same as {@link FoldError} (see its own doc in
|
|
191
|
+
* ../fold/fold.ts for the full mechanism and profiling evidence): `.stack` is
|
|
192
|
+
* never read on this path. Cheap for a shallow call stack, but this module's
|
|
193
|
+
* cross-file recursion (`foldFileMemoized` -> `buildExternals` ->
|
|
194
|
+
* `tryFoldFileCore` -> `resolveDeclaratorValue`/`resolveLiveValue` ->
|
|
195
|
+
* `resolveCallExpression`/`resolveImportedExport`, sometimes several files
|
|
196
|
+
* deep) makes the live stack deep enough, and hot enough for V8 to inline
|
|
197
|
+
* aggressively across a 98-entry corpus, that eager capture becomes the
|
|
198
|
+
* dominant cost — confirmed via `sample` during the observed multi-minute
|
|
199
|
+
* stall. Every throw site in this module that isn't a {@link FoldError}
|
|
200
|
+
* itself should use this instead of `new Error(...)`.
|
|
201
|
+
*/
|
|
202
|
+
function cheapError(message) {
|
|
203
|
+
const prevStackTraceLimit = Error.stackTraceLimit;
|
|
204
|
+
Error.stackTraceLimit = 0;
|
|
205
|
+
try {
|
|
206
|
+
return new Error(message);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
Error.stackTraceLimit = prevStackTraceLimit;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Memoized {@link importModule} — see {@link FoldSession.importCache}'s doc
|
|
214
|
+
* for why this exists. Keyed by `modulePath` exactly as callers already pass
|
|
215
|
+
* it (the output of {@link resolveModulePath}, always an absolute path), so
|
|
216
|
+
* every caller resolving the same module shares the identical in-flight/
|
|
217
|
+
* settled promise instead of issuing its own `import()`.
|
|
218
|
+
*/
|
|
219
|
+
function importModuleMemoized(modulePath, importCache) {
|
|
220
|
+
const cached = importCache.get(modulePath);
|
|
221
|
+
if (cached)
|
|
222
|
+
return cached;
|
|
223
|
+
const promise = importModule(modulePath);
|
|
224
|
+
importCache.set(modulePath, promise);
|
|
225
|
+
return promise;
|
|
226
|
+
}
|
|
227
|
+
/** True when `node` carries the `export` modifier. */
|
|
228
|
+
function hasExportModifier(node) {
|
|
229
|
+
return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
230
|
+
}
|
|
231
|
+
const executionCounts = {
|
|
232
|
+
factoryInvocations: 0,
|
|
233
|
+
projectFactoryInvocations: 0,
|
|
234
|
+
factoryInterpretations: 0,
|
|
235
|
+
};
|
|
236
|
+
/** A snapshot of {@link FoldExecutionCounts}. Process-wide and monotonic — a caller wanting a per-build figure calls {@link resetFoldExecutionCounts} first. */
|
|
237
|
+
export function foldExecutionCounts() {
|
|
238
|
+
return { ...executionCounts };
|
|
239
|
+
}
|
|
240
|
+
/** Zero {@link foldExecutionCounts}, for a caller measuring one build. */
|
|
241
|
+
export function resetFoldExecutionCounts() {
|
|
242
|
+
executionCounts.factoryInvocations = 0;
|
|
243
|
+
executionCounts.projectFactoryInvocations = 0;
|
|
244
|
+
executionCounts.factoryInterpretations = 0;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* True for any non-null, non-array object — a real `CompositeInstance`, a
|
|
248
|
+
* real `Declarable`, or (chant #1020) the synthetic plain object a
|
|
249
|
+
* namespace import's cross-file resolution builds from another file's own
|
|
250
|
+
* `exportedValues`. Property/member access on any of these is just a plain
|
|
251
|
+
* bracket index, so {@link resolveLiveValue} doesn't need to special-case
|
|
252
|
+
* which kind it is.
|
|
253
|
+
*/
|
|
254
|
+
function isIndexableObject(value) {
|
|
255
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
256
|
+
}
|
|
257
|
+
/** Extract a foldable object-binding-pattern element's source property key, or `undefined` if the shape isn't supported (rest element, nested pattern, default value, computed name). */
|
|
258
|
+
function bindingElementPropKey(el) {
|
|
259
|
+
if (el.dotDotDotToken || el.initializer || !ts.isIdentifier(el.name))
|
|
260
|
+
return undefined;
|
|
261
|
+
if (!el.propertyName)
|
|
262
|
+
return el.name.text;
|
|
263
|
+
return ts.isIdentifier(el.propertyName) ? el.propertyName.text : undefined;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Scan a source file's top-level statements for the exported shapes this
|
|
267
|
+
* module can fold: `export const X = new Type(...)` (resources, #1022),
|
|
268
|
+
* `export const X = <expr>` / `export const { a, b } = <expr>` (composite
|
|
269
|
+
* calls and member access on them, #1023), `export { a, b }` (a local
|
|
270
|
+
* named-export list), and `export { a, b } from "./other"` (a re-export
|
|
271
|
+
* chain, #1020), and `export function name(...) {...}` (a foldable-function
|
|
272
|
+
* export, #1373). Any OTHER export construct (`export default`, `export *
|
|
273
|
+
* from`, an exported class, `let`/`var`, a destructured export with
|
|
274
|
+
* a rest/nested/defaulted element) makes the whole file ineligible: the
|
|
275
|
+
* module must run so that construct is actually evaluated. This mirrors the
|
|
276
|
+
* epic's hybrid design — fallback is per-module, not per-declaration,
|
|
277
|
+
* because an unfoldable export can itself reference or be referenced by a
|
|
278
|
+
* foldable one in ways only running proves safe.
|
|
279
|
+
*/
|
|
280
|
+
function scanExports(sourceFile) {
|
|
281
|
+
const declarators = [];
|
|
282
|
+
for (const statement of sourceFile.statements) {
|
|
283
|
+
if (ts.isExportAssignment(statement)) {
|
|
284
|
+
return { declarators, unfoldableReason: "`export default` is not foldable" };
|
|
285
|
+
}
|
|
286
|
+
if (ts.isExportDeclaration(statement)) {
|
|
287
|
+
if (statement.isTypeOnly)
|
|
288
|
+
continue; // `export type { ... }` — erased, no runtime value to fold.
|
|
289
|
+
if (statement.moduleSpecifier) {
|
|
290
|
+
// chant #1020 — a genuine re-export (`export { a } from "./other"`).
|
|
291
|
+
// `export * from "./other"` has no `exportClause` at all — no
|
|
292
|
+
// enumerable element list to resolve one-by-one — and still
|
|
293
|
+
// disqualifies the file, same as before #1020.
|
|
294
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
295
|
+
return { declarators, unfoldableReason: "re-export declaration is not foldable" };
|
|
296
|
+
}
|
|
297
|
+
if (!statement.exportClause || !ts.isNamedExports(statement.exportClause)) {
|
|
298
|
+
return { declarators, unfoldableReason: "re-export declaration is not foldable" };
|
|
299
|
+
}
|
|
300
|
+
const elements = [];
|
|
301
|
+
for (const el of statement.exportClause.elements) {
|
|
302
|
+
if (el.isTypeOnly)
|
|
303
|
+
continue;
|
|
304
|
+
const importedNameNode = el.propertyName ?? el.name;
|
|
305
|
+
if (!ts.isIdentifier(importedNameNode)) {
|
|
306
|
+
return { declarators, unfoldableReason: "re-export declaration is not foldable" };
|
|
307
|
+
}
|
|
308
|
+
elements.push({ imported: importedNameNode.text, exportedName: el.name.text });
|
|
309
|
+
}
|
|
310
|
+
declarators.push({
|
|
311
|
+
kind: "re-export",
|
|
312
|
+
specifier: statement.moduleSpecifier.text,
|
|
313
|
+
specifierNode: statement.moduleSpecifier,
|
|
314
|
+
elements,
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (!statement.exportClause || !ts.isNamedExports(statement.exportClause)) {
|
|
319
|
+
return { declarators, unfoldableReason: "export declaration is not foldable" };
|
|
320
|
+
}
|
|
321
|
+
const elements = [];
|
|
322
|
+
for (const el of statement.exportClause.elements) {
|
|
323
|
+
if (el.isTypeOnly)
|
|
324
|
+
continue;
|
|
325
|
+
const localNameNode = el.propertyName ?? el.name;
|
|
326
|
+
// `export { "string name" as foo }` (TS 4.5+ module-export-name
|
|
327
|
+
// syntax) isn't a plain identifier reference — not seen in practice
|
|
328
|
+
// for local (non-re-export) lists; fall back rather than guess.
|
|
329
|
+
if (!ts.isIdentifier(localNameNode)) {
|
|
330
|
+
return { declarators, unfoldableReason: "export declaration is not foldable" };
|
|
331
|
+
}
|
|
332
|
+
elements.push({ localNameNode, exportedName: el.name.text });
|
|
333
|
+
}
|
|
334
|
+
declarators.push({ kind: "named-export", elements });
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement)) {
|
|
338
|
+
// chant #1373 — a named, bodied exported function is a foldable-function
|
|
339
|
+
// export (see {@link FunctionDeclarator}). An overload signature has no
|
|
340
|
+
// body and no value of its own — skipped, the implementation that
|
|
341
|
+
// follows it is the export. `export default function` has no name an
|
|
342
|
+
// importer could bind and stays a disqualifier, like `export default`.
|
|
343
|
+
if (statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) || !statement.name) {
|
|
344
|
+
return { declarators, unfoldableReason: "`export default` is not foldable" };
|
|
345
|
+
}
|
|
346
|
+
if (!statement.body)
|
|
347
|
+
continue;
|
|
348
|
+
declarators.push({ kind: "function", name: statement.name.text });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (ts.isClassDeclaration(statement) && hasExportModifier(statement)) {
|
|
352
|
+
return {
|
|
353
|
+
declarators,
|
|
354
|
+
unfoldableReason: `exported class declaration "${statement.name?.text ?? "<anonymous>"}" is not foldable`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (!ts.isVariableStatement(statement) || !hasExportModifier(statement))
|
|
358
|
+
continue;
|
|
359
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
360
|
+
return { declarators, unfoldableReason: "exported `let`/`var` declaration is not foldable" };
|
|
361
|
+
}
|
|
362
|
+
for (const decl of statement.declarationList.declarations) {
|
|
363
|
+
if (!decl.initializer) {
|
|
364
|
+
return { declarators, unfoldableReason: "exported destructured or uninitialized declaration is not foldable" };
|
|
365
|
+
}
|
|
366
|
+
if (ts.isIdentifier(decl.name)) {
|
|
367
|
+
if (ts.isNewExpression(decl.initializer)) {
|
|
368
|
+
declarators.push({ kind: "resource", name: decl.name.text, node: decl.initializer });
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
declarators.push({ kind: "single", name: decl.name.text, node: decl.initializer });
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
376
|
+
const elements = [];
|
|
377
|
+
let allSupported = true;
|
|
378
|
+
for (const el of decl.name.elements) {
|
|
379
|
+
const propKey = bindingElementPropKey(el);
|
|
380
|
+
if (propKey === undefined) {
|
|
381
|
+
allSupported = false;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
elements.push({ propKey, bindingName: el.name.getText() });
|
|
385
|
+
}
|
|
386
|
+
if (!allSupported) {
|
|
387
|
+
return { declarators, unfoldableReason: "exported destructured or uninitialized declaration is not foldable" };
|
|
388
|
+
}
|
|
389
|
+
declarators.push({ kind: "destructure", node: decl.initializer, elements });
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
// ArrayBindingPattern — not supported.
|
|
393
|
+
return { declarators, unfoldableReason: "exported destructured or uninitialized declaration is not foldable" };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return { declarators };
|
|
397
|
+
}
|
|
398
|
+
function collectLocalBindings(sourceFile) {
|
|
399
|
+
const bindings = new Map();
|
|
400
|
+
for (const statement of sourceFile.statements) {
|
|
401
|
+
if (!ts.isVariableStatement(statement))
|
|
402
|
+
continue;
|
|
403
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
404
|
+
continue;
|
|
405
|
+
for (const decl of statement.declarationList.declarations) {
|
|
406
|
+
if (!decl.initializer)
|
|
407
|
+
continue;
|
|
408
|
+
if (ts.isIdentifier(decl.name)) {
|
|
409
|
+
bindings.set(decl.name.text, { source: decl.initializer });
|
|
410
|
+
}
|
|
411
|
+
else if (ts.isObjectBindingPattern(decl.name)) {
|
|
412
|
+
for (const el of decl.name.elements) {
|
|
413
|
+
const propKey = bindingElementPropKey(el);
|
|
414
|
+
if (propKey === undefined)
|
|
415
|
+
continue;
|
|
416
|
+
bindings.set(el.name.getText(), { source: decl.initializer, propKey });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// ArrayBindingPattern locals: not indexed — a later reference to one
|
|
420
|
+
// of its names simply won't be found below, which correctly falls
|
|
421
|
+
// back (same as referencing any other unresolved identifier).
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return bindings;
|
|
425
|
+
}
|
|
426
|
+
/** `(x) => …` / `function (x) {…}`, possibly wrapped in parens/`as`/`satisfies` — the initializer shapes that make a `const` a function binding. */
|
|
427
|
+
/**
|
|
428
|
+
* chant #1373 — every top-level function a file declares, exported or not, as
|
|
429
|
+
* a {@link FoldableFunction}: `function f(...) {...}` and `const f = (...) =>
|
|
430
|
+
* …`. Placed in the file's own `externals` (so a same-file call folds, and a
|
|
431
|
+
* sibling function can call it) and exported under its name (so an importer's
|
|
432
|
+
* `buildExternals` can). The marker carries the file's scope BY REFERENCE:
|
|
433
|
+
* `ctx.externals` is still being filled (pre-built resources land in it after
|
|
434
|
+
* this runs) and a body reads it only when called.
|
|
435
|
+
*
|
|
436
|
+
* The marker's `consts` drops every `new`-bound const (transitively, through
|
|
437
|
+
* aliases) — see {@link FoldableFunction.consts} for why a body must reach a
|
|
438
|
+
* module-level resource through `externals` only.
|
|
439
|
+
*/
|
|
440
|
+
function collectLocalFunctions(sourceFile, ctx) {
|
|
441
|
+
// chant#2436 — the walk itself, and the `new`-bound stripping that makes it
|
|
442
|
+
// correct, live in ../fold/fold.ts so a unit-level fold with no externals of
|
|
443
|
+
// its own binds exactly what a build binds.
|
|
444
|
+
return collectSameFileFunctions(sourceFile, ctx.consts, ctx.file, ctx.externals, ctx.crossFileFailures);
|
|
445
|
+
}
|
|
446
|
+
/** Map every top-level `import`-bound local identifier to its source module + export name. */
|
|
447
|
+
function collectImports(sourceFile) {
|
|
448
|
+
const named = new Map();
|
|
449
|
+
const namespaces = new Map();
|
|
450
|
+
for (const statement of sourceFile.statements) {
|
|
451
|
+
if (!ts.isImportDeclaration(statement))
|
|
452
|
+
continue;
|
|
453
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier))
|
|
454
|
+
continue;
|
|
455
|
+
const clause = statement.importClause;
|
|
456
|
+
if (!clause)
|
|
457
|
+
continue;
|
|
458
|
+
// A whole-statement `import type { ... } from "x"` is erased at runtime
|
|
459
|
+
// and never appears in expression position — indexing it would only
|
|
460
|
+
// ever manufacture a false cross-file dependency edge (taint, or worse,
|
|
461
|
+
// a phantom import-cycle) between files that share no real value
|
|
462
|
+
// dependency. Per-specifier `import { type Foo } from "x"` is filtered
|
|
463
|
+
// below, at the named-element level.
|
|
464
|
+
if (clause.isTypeOnly)
|
|
465
|
+
continue;
|
|
466
|
+
const specifier = statement.moduleSpecifier.text;
|
|
467
|
+
const specifierNode = statement.moduleSpecifier;
|
|
468
|
+
if (clause.name) {
|
|
469
|
+
named.set(clause.name.text, { specifier, imported: "default", specifierNode });
|
|
470
|
+
}
|
|
471
|
+
if (clause.namedBindings) {
|
|
472
|
+
if (ts.isNamedImports(clause.namedBindings)) {
|
|
473
|
+
for (const element of clause.namedBindings.elements) {
|
|
474
|
+
if (element.isTypeOnly)
|
|
475
|
+
continue;
|
|
476
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
477
|
+
named.set(element.name.text, { specifier, imported, specifierNode });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
481
|
+
namespaces.set(clause.namedBindings.name.text, { specifier, specifierNode });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return { named, namespaces };
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* chant #1020 hang fix — best-effort fast path for resolving a BARE
|
|
489
|
+
* specifier by walking `node_modules` directly with the same primitives
|
|
490
|
+
* (`statSync`/`readFileSync`) the relative-specifier branch below already
|
|
491
|
+
* uses, reading `package.json`'s "exports"/"main" field by hand instead of
|
|
492
|
+
* calling `createRequire(fromFile).resolve(specifier)`. Returns `undefined`
|
|
493
|
+
* — never throws — when the package is not found, its `exports` map does not
|
|
494
|
+
* export the subpath under the `import`/`node`/`default` conditions, or a
|
|
495
|
+
* subpath of a package without `exports` has no `package.json` of its own:
|
|
496
|
+
* the caller then falls back to the slow, authoritative
|
|
497
|
+
* `createRequire().resolve()` path, so such a specifier is still resolved
|
|
498
|
+
* correctly, just not quickly.
|
|
499
|
+
*
|
|
500
|
+
* chant#3090 — subpaths (`pkg/sub/x`) and `exports` pattern keys
|
|
501
|
+
* (`"./sub/*"`) resolve here too, with condition objects read in key order
|
|
502
|
+
* as an ESM import reads them ({@link packageExportsTarget}). Before, a
|
|
503
|
+
* subpath always reached the `createRequire` fallback, whose CommonJS
|
|
504
|
+
* conditions miss an `import`-only target.
|
|
505
|
+
*
|
|
506
|
+
* Why this exists at all: profiling traced the hang to
|
|
507
|
+
* `createRequire(fromFile).resolve(specifier)` ITSELF taking upwards of a
|
|
508
|
+
* minute — and, worse, growing (measured 60s, then 69s, then 361s for
|
|
509
|
+
* successive NEW packages later in the same run) — inside a vitest worker,
|
|
510
|
+
* specifically for the FIRST resolution of a given bare specifier in the
|
|
511
|
+
* process (session-level and even process-wide-by-specifier caching, see
|
|
512
|
+
* {@link bareSpecifierPathCache}, only avoid paying that cost a SECOND
|
|
513
|
+
* time). The relative-specifier branch just below, using these exact same
|
|
514
|
+
* file-system primitives, never showed this slowdown anywhere
|
|
515
|
+
* in the same profiling — the cost is specific to Node's own
|
|
516
|
+
* `Module._resolveFilename` machinery, not to file-system access in
|
|
517
|
+
* general, so replacing just that one call with plain `statSync`/
|
|
518
|
+
* `readFileSync` sidesteps it entirely for the common case every
|
|
519
|
+
* `@intentius/chant*` package (and most well-formed npm packages) ships.
|
|
520
|
+
*/
|
|
521
|
+
function fastResolveBareSpecifier(specifier, fromFile) {
|
|
522
|
+
const parts = splitBareSpecifier(specifier);
|
|
523
|
+
if (parts === undefined)
|
|
524
|
+
return undefined;
|
|
525
|
+
let dir = dirname(fromFile);
|
|
526
|
+
for (;;) {
|
|
527
|
+
const packageDir = join(dir, "node_modules", parts.name);
|
|
528
|
+
if (existsSync(packageDir)) {
|
|
529
|
+
const resolved = fastResolvePackageEntry(packageDir, parts.subpath);
|
|
530
|
+
if (resolved === undefined || !isFile(resolved))
|
|
531
|
+
return undefined;
|
|
532
|
+
try {
|
|
533
|
+
return realpathSync(resolved);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const parent = dirname(dir);
|
|
540
|
+
if (parent === dir)
|
|
541
|
+
return undefined; // reached the filesystem root, unresolved
|
|
542
|
+
dir = parent;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The absolute file `<packageDir>/package.json` names for `subpath` (`"."` or
|
|
547
|
+
* `"./x"`), or `undefined` — see {@link fastResolveBareSpecifier}'s doc for
|
|
548
|
+
* which shapes this recognizes. With an `exports` field, that map decides.
|
|
549
|
+
* Without one, the root is `main` (or `index.js`), and a subpath is read as a
|
|
550
|
+
* nested package directory with its own `package.json`.
|
|
551
|
+
*/
|
|
552
|
+
function fastResolvePackageEntry(packageDir, subpath) {
|
|
553
|
+
const pkgJsonPath = join(packageDir, "package.json");
|
|
554
|
+
if (!existsSync(pkgJsonPath))
|
|
555
|
+
return undefined;
|
|
556
|
+
let pkg;
|
|
557
|
+
try {
|
|
558
|
+
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return undefined;
|
|
562
|
+
}
|
|
563
|
+
if (typeof pkg !== "object" || pkg === null)
|
|
564
|
+
return undefined;
|
|
565
|
+
const exportsField = pkg.exports;
|
|
566
|
+
if (exportsField !== undefined) {
|
|
567
|
+
const target = packageExportsTarget(exportsField, subpath);
|
|
568
|
+
return target === undefined ? undefined : resolvePath(packageDir, target);
|
|
569
|
+
}
|
|
570
|
+
if (subpath !== ".")
|
|
571
|
+
return fastResolvePackageEntry(join(packageDir, subpath), ".");
|
|
572
|
+
const main = pkg.main;
|
|
573
|
+
if (main !== undefined)
|
|
574
|
+
return typeof main === "string" ? resolvePath(packageDir, main) : undefined;
|
|
575
|
+
return resolvePath(packageDir, "index.js");
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* chant#3090 — the file a tsconfig `paths` entry maps a bare `specifier` to,
|
|
579
|
+
* from the nearest `tsconfig.json` above `fromFile` ({@link tsconfigPathsTarget}),
|
|
580
|
+
* or `undefined`.
|
|
581
|
+
*
|
|
582
|
+
* chant's own package specifiers ({@link isChantOwnedSpecifier}) are never
|
|
583
|
+
* mapped. They resolve through `node_modules` to the modules the CLI has
|
|
584
|
+
* already loaded, which is what {@link isTrustedExecutableBinding} relies on,
|
|
585
|
+
* and a project's `paths` cannot point them at its own files. This repository's
|
|
586
|
+
* root tsconfig maps every chant package to its source, and those entries
|
|
587
|
+
* name the same files `node_modules` does.
|
|
588
|
+
*
|
|
589
|
+
* A specifier this maps is a project import ({@link isProjectImport}): it is
|
|
590
|
+
* folded as a project file and its trust is decided on the resolved path,
|
|
591
|
+
* even when its text names an active lexicon package.
|
|
592
|
+
*/
|
|
593
|
+
function pathsMappedTarget(specifier, fromFile) {
|
|
594
|
+
return pathsMatch(specifier, fromFile)?.target;
|
|
595
|
+
}
|
|
596
|
+
/** The tsconfig `paths` match behind {@link pathsMappedTarget}, including a match whose targets name no file. */
|
|
597
|
+
function pathsMatch(specifier, fromFile) {
|
|
598
|
+
if (specifier.startsWith(".") || isAbsolute(specifier) || isChantOwnedSpecifier(specifier))
|
|
599
|
+
return undefined;
|
|
600
|
+
return tsconfigPathsMatch(specifier, fromFile);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Resolve an import specifier to an absolute module path, the way the
|
|
604
|
+
* declaring file's own `import` would — without depending on a TS-aware
|
|
605
|
+
* loader being active. Relative/absolute specifiers are probed against real
|
|
606
|
+
* TS/JS candidate files on disk ({@link probeModuleFile}, which maps `./x.js`
|
|
607
|
+
* to `./x.ts` as `tsc` does); a bare specifier a tsconfig `paths` entry maps
|
|
608
|
+
* resolves to that file ({@link pathsMappedTarget}); other bare package
|
|
609
|
+
* specifiers try {@link fastResolveBareSpecifier} first, falling back to
|
|
610
|
+
* Node's own CJS algorithm from the declaring file's location whenever that
|
|
611
|
+
* returns `undefined`.
|
|
612
|
+
*/
|
|
613
|
+
function resolveModulePath(specifier, fromFile) {
|
|
614
|
+
if (specifier.startsWith(".") || isAbsolute(specifier)) {
|
|
615
|
+
const base = specifier.startsWith(".") ? resolvePath(dirname(fromFile), specifier) : specifier;
|
|
616
|
+
// Nothing found on disk under any probed extension — hand back the bare
|
|
617
|
+
// base and let `import()` fail with its own, more specific error.
|
|
618
|
+
return probeModuleFile(base) ?? base;
|
|
619
|
+
}
|
|
620
|
+
const mapped = pathsMappedTarget(specifier, fromFile);
|
|
621
|
+
if (mapped !== undefined)
|
|
622
|
+
return mapped;
|
|
623
|
+
const fast = fastResolveBareSpecifier(specifier, fromFile);
|
|
624
|
+
if (fast !== undefined)
|
|
625
|
+
return fast;
|
|
626
|
+
return createRequire(fromFile).resolve(specifier);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* chant #1064 — the one real published subpath a project imports chant's
|
|
630
|
+
* build-time-parameters module from. {@link buildExternals} matches a bare
|
|
631
|
+
* specifier against this by TEXT ONLY, never by resolving it — see that
|
|
632
|
+
* function's own comment for why resolving an arbitrary bare specifier here
|
|
633
|
+
* would reintroduce the exact pathological cold-resolution cost chant#1020
|
|
634
|
+
* already fixed once (this module's other comments measure it at up to
|
|
635
|
+
* ~361s for the first resolution of a genuinely new bare specifier).
|
|
636
|
+
*/
|
|
637
|
+
const PARAMS_BARE_SPECIFIER = "@intentius/chant/params";
|
|
638
|
+
/**
|
|
639
|
+
* chant #1064 — the absolute path of chant-core's OWN build-time-parameters
|
|
640
|
+
* runtime module (../params.ts), resolved lazily on first use, from THIS
|
|
641
|
+
* file's own location, via the exact same relative-specifier resolution
|
|
642
|
+
* {@link resolveModulePath} already applies to project files — cheap
|
|
643
|
+
* (`existsSync`/`statSync` candidate probing only, never Node's package
|
|
644
|
+
* resolution). Used by {@link buildExternals} to recognize a RELATIVE/
|
|
645
|
+
* ABSOLUTE import of the params module (this module's own test fixtures use
|
|
646
|
+
* an absolute path, matching the rest of this file's test convention) — a
|
|
647
|
+
* real project's bare `@intentius/chant/params` import is instead matched by
|
|
648
|
+
* {@link PARAMS_BARE_SPECIFIER}'s text alone, never through this path.
|
|
649
|
+
*
|
|
650
|
+
* Lazy and failure-safe, NOT a module-scope constant: this module is also
|
|
651
|
+
* bundled into the #1045 sandbox child, where module-init code runs inside
|
|
652
|
+
* `--permission` with a read allowlist. There `import.meta.url` is the
|
|
653
|
+
* bundle's temp-dir path, so an init-time probe reaches for `<tmp>/params` —
|
|
654
|
+
* outside the allowlist — and the `existsSync` throws `ERR_ACCESS_DENIED`,
|
|
655
|
+
* killing the child before it reports (a real, observed sandbox-vs-run error
|
|
656
|
+
* drift across six corpus entries). The child runs only the run path and
|
|
657
|
+
* never calls {@link buildExternals}, so deferring the probe to first fold
|
|
658
|
+
* use keeps it out of the sandbox entirely; if probing still fails there,
|
|
659
|
+
* `null` just disables the relative-path recognition rather than erroring.
|
|
660
|
+
*/
|
|
661
|
+
let paramsModulePathMemo;
|
|
662
|
+
function paramsModulePath() {
|
|
663
|
+
if (paramsModulePathMemo === undefined) {
|
|
664
|
+
try {
|
|
665
|
+
paramsModulePathMemo = resolveModulePath("../params", fileURLToPath(import.meta.url));
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
paramsModulePathMemo = null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return paramsModulePathMemo;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* chant #1063 — is `specifier` a bare import of a package that is an ACTIVE
|
|
675
|
+
* LEXICON of this build? Returns the specifier itself when so, `undefined`
|
|
676
|
+
* otherwise.
|
|
677
|
+
*
|
|
678
|
+
* Three deliberate restrictions, each of which is the point rather than an
|
|
679
|
+
* omission:
|
|
680
|
+
*
|
|
681
|
+
* - **Text only, no resolution.** The answer is a `Set.has` on a set built
|
|
682
|
+
* from the lexicon names the build ALREADY resolved before discovery ran
|
|
683
|
+
* (`resolveProjectLexicons` -> `loadPlugins`). Nothing is probed, read, or
|
|
684
|
+
* resolved to decide whether a specifier is in scope — so an import of
|
|
685
|
+
* some unrelated package costs a string comparison and is then left alone,
|
|
686
|
+
* never resolved "just to find out what it is". That matters here more
|
|
687
|
+
* than anywhere: `resolveModulePath`'s bare branch can fall through to
|
|
688
|
+
* `createRequire(fromFile).resolve(specifier)`, measured at up to ~361s
|
|
689
|
+
* for the first resolution of a genuinely new bare specifier in a process
|
|
690
|
+
* (see {@link bareSpecifierPathCache}/{@link fastResolveBareSpecifier} —
|
|
691
|
+
* this exact class of cost regressed twice during chant#1020).
|
|
692
|
+
*
|
|
693
|
+
* - **Exact package specifier, no subpaths.** `@intentius/chant-lexicon-aws`
|
|
694
|
+
* matches; `@intentius/chant-lexicon-aws/actions` does not. Every lexicon
|
|
695
|
+
* re-exports its whole public surface from its barrel and every example
|
|
696
|
+
* imports it that way, so subpaths buy nothing — and they would cost
|
|
697
|
+
* something real: {@link fastResolveBareSpecifier} only recognizes a
|
|
698
|
+
* package ROOT (it looks for `<node_modules>/<specifier>/package.json`),
|
|
699
|
+
* so a subpath specifier falls through to exactly the slow
|
|
700
|
+
* `createRequire().resolve()` path this restriction exists to avoid.
|
|
701
|
+
*
|
|
702
|
+
* - **Lexicons of THIS build only.** Not "any `@intentius/chant-lexicon-*`
|
|
703
|
+
* package on disk", and emphatically not "any bare specifier". A lexicon
|
|
704
|
+
* the build did not load is as out of scope as `node:fs`.
|
|
705
|
+
*
|
|
706
|
+
* chant #1995 — the "no subpaths" restriction above stays as documented for
|
|
707
|
+
* THIS function's caller ({@link resolveActiveLexiconExport}, #1063's
|
|
708
|
+
* data-export resolution): every lexicon's public data surface is reachable
|
|
709
|
+
* from the barrel, so a subpath buys that caller nothing and would cost the
|
|
710
|
+
* same cold-resolution risk. {@link isTrustedExecutableBinding} (#1093) asks a
|
|
711
|
+
* different question — not "does this specifier name a reachable data
|
|
712
|
+
* export" but "has the CLI already loaded this code" — and a project file
|
|
713
|
+
* reaching a lexicon's generated module by subpath instead of through the
|
|
714
|
+
* barrel is code the CLI already imported and executed either way, so that
|
|
715
|
+
* caller resolves a subpath's package root separately, via
|
|
716
|
+
* {@link bareSpecifierPackageRoot}, rather than widening this function.
|
|
717
|
+
*/
|
|
718
|
+
function activeLexiconPackage(specifier, lexiconPackages) {
|
|
719
|
+
return lexiconPackages.has(specifier) ? specifier : undefined;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* chant#3090 — {@link activeLexiconPackage} for a specifier imported by
|
|
723
|
+
* `fromFile`: `undefined` as well when a tsconfig `paths` entry maps the
|
|
724
|
+
* specifier ({@link pathsMappedTarget}), because the import then names a
|
|
725
|
+
* project file, not the package the build loaded. Only a specifier whose text
|
|
726
|
+
* already matches pays the tsconfig lookup.
|
|
727
|
+
*/
|
|
728
|
+
function activeLexiconPackageImport(specifier, fromFile, lexiconPackages) {
|
|
729
|
+
if (activeLexiconPackage(specifier, lexiconPackages) === undefined)
|
|
730
|
+
return undefined;
|
|
731
|
+
return pathsMappedTarget(specifier, fromFile) === undefined ? specifier : undefined;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* chant#2577 — the path-declared counterpart of {@link activeLexiconPackage}:
|
|
735
|
+
* the resolved module path when a relative or absolute `specifier` reaches
|
|
736
|
+
* one of this build's {@link FoldSession.lexiconModules}, `undefined`
|
|
737
|
+
* otherwise. A bare specifier is never resolved here, and a build with no
|
|
738
|
+
* path-declared lexicon resolves nothing.
|
|
739
|
+
*/
|
|
740
|
+
function activeLexiconModule(specifier, fromFile, lexiconModules, resolvePathCache) {
|
|
741
|
+
if (lexiconModules.size === 0 || !isProjectImport(specifier, fromFile))
|
|
742
|
+
return undefined;
|
|
743
|
+
let modulePath;
|
|
744
|
+
try {
|
|
745
|
+
modulePath = resolveModulePathMemoized(specifier, fromFile, resolvePathCache);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
return lexiconModules.has(modulePath) ? modulePath : undefined;
|
|
751
|
+
}
|
|
752
|
+
/** chant#2590 — true when absolute `file` lies under absolute directory `dir`. */
|
|
753
|
+
function isUnderDirectory(file, dir) {
|
|
754
|
+
return file.startsWith(dir.endsWith(sep) ? dir : dir + sep);
|
|
755
|
+
}
|
|
756
|
+
/** chant#2590 — true when `path` lies under one of the build's trusted path-lexicon directories. */
|
|
757
|
+
function inPathLexiconRoot(path, roots) {
|
|
758
|
+
return roots.some((root) => isUnderDirectory(path, root));
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* chant #1995 — the package-ROOT portion of a bare subpath specifier
|
|
762
|
+
* (`@intentius/chant-lexicon-azure/generated/index` ->
|
|
763
|
+
* `@intentius/chant-lexicon-azure`), or `undefined` when `specifier` is a
|
|
764
|
+
* relative/absolute path, is already a bare root with no subpath, or is
|
|
765
|
+
* malformed (a lone `@scope` with nothing after it).
|
|
766
|
+
*
|
|
767
|
+
* Text only, no resolution — the same discipline {@link activeLexiconPackage}
|
|
768
|
+
* documents and for the identical reason: resolving an arbitrary bare
|
|
769
|
+
* specifier to find out what package it belongs to is the pathological cold
|
|
770
|
+
* `require.resolve` chant#1020 measured. A scoped package (`@scope/name`)
|
|
771
|
+
* reserves its first two `/`-separated segments for the root; an unscoped
|
|
772
|
+
* package (`name`) reserves one.
|
|
773
|
+
*/
|
|
774
|
+
function bareSpecifierPackageRoot(specifier) {
|
|
775
|
+
if (specifier.startsWith(".") || isAbsolute(specifier))
|
|
776
|
+
return undefined;
|
|
777
|
+
const segments = specifier.split("/");
|
|
778
|
+
if (specifier.startsWith("@")) {
|
|
779
|
+
return segments.length > 2 ? `${segments[0]}/${segments[1]}` : undefined;
|
|
780
|
+
}
|
|
781
|
+
return segments.length > 1 ? segments[0] : undefined;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* chant #1063 — resolve one named import binding against an active lexicon
|
|
785
|
+
* package's REAL exports, for {@link buildExternals}.
|
|
786
|
+
*
|
|
787
|
+
* The lexicon module is imported (through the session-wide
|
|
788
|
+
* {@link FoldSession.importCache}, so at most one real `import()` per package
|
|
789
|
+
* per build) and the requested export read straight off it. That is the same
|
|
790
|
+
* module object the run path gets — `importModule` is a plain dynamic
|
|
791
|
+
* `import()` with no cache-busting — so what fold captures here is not a
|
|
792
|
+
* reconstruction of the lexicon's data but the identical value, down to
|
|
793
|
+
* object identity for `Azure.ResourceGroupLocation`-style singletons. It is
|
|
794
|
+
* also the same two-step (resolve path, then import) that
|
|
795
|
+
* `resolveImportedExport` has always used to reach a lexicon's constructors
|
|
796
|
+
* and intrinsic functions; the only thing new is that a plain DATA export is
|
|
797
|
+
* now reachable too.
|
|
798
|
+
*
|
|
799
|
+
* Callable exports are otherwise excluded. A lexicon's functions — its
|
|
800
|
+
* resource classes, composite factories, intrinsic implementations — already
|
|
801
|
+
* have dedicated resolution paths that know how to INVOKE them
|
|
802
|
+
* (`resolveResourceEntity`, `resolveCallExpression`, `reviveFoldedValue`), and
|
|
803
|
+
* binding an arbitrary one as a plain identifier value here would widen what
|
|
804
|
+
* folds in unmeasured ways. The one exception (chant #1966) is a function its
|
|
805
|
+
* lexicon registered with {@link IntrinsicDef.foldsEagerly} — the same
|
|
806
|
+
* per-name, lexicon-declared opt-in `foldsAsCall` is for an intrinsic's
|
|
807
|
+
* envelope-and-revive form, admitting this one because `fold()`
|
|
808
|
+
* (../fold/fold.ts) needs to CALL it eagerly — synchronously, before
|
|
809
|
+
* revival — to support a lexicon's own string-building helpers used inline
|
|
810
|
+
* (`matrix("os").toString()`, `` `${inputs("x")}` ``), where an envelope
|
|
811
|
+
* would still be a plain object when the enclosing template folds.
|
|
812
|
+
*
|
|
813
|
+
* Returns `undefined` — never throws — for a package that isn't an active
|
|
814
|
+
* lexicon, an export the package doesn't have, a non-admitted callable
|
|
815
|
+
* export, or an import that fails. The binding is then simply absent from
|
|
816
|
+
* `externals`, exactly as before, and `fold()`'s ordinary "unresolved
|
|
817
|
+
* identifier" failure still fires if the name is actually referenced.
|
|
818
|
+
*/
|
|
819
|
+
async function resolveActiveLexiconExport(binding, fromFile, session) {
|
|
820
|
+
let modulePath;
|
|
821
|
+
if (activeLexiconPackageImport(binding.specifier, fromFile, session.lexiconPackages)) {
|
|
822
|
+
try {
|
|
823
|
+
modulePath = resolveModulePathMemoized(binding.specifier, fromFile, session.resolvePathCache);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
// chant#2577 — a lexicon declared by path, imported by its path.
|
|
831
|
+
modulePath = activeLexiconModule(binding.specifier, fromFile, session.lexiconModules, session.resolvePathCache);
|
|
832
|
+
if (modulePath === undefined)
|
|
833
|
+
return undefined;
|
|
834
|
+
}
|
|
835
|
+
let mod;
|
|
836
|
+
try {
|
|
837
|
+
mod = await importModuleMemoized(modulePath, session.importCache);
|
|
838
|
+
}
|
|
839
|
+
catch {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
if (!(binding.imported in mod))
|
|
843
|
+
return undefined;
|
|
844
|
+
const value = mod[binding.imported];
|
|
845
|
+
if (typeof value === "function") {
|
|
846
|
+
const eager = session.intrinsics.some((i) => i.name === binding.imported && intrinsicCallFoldsEagerly(i));
|
|
847
|
+
if (!eager)
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
return { value };
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* chant#3247 — a plain data export of a lexicon package's own module, read off
|
|
854
|
+
* the real module (see the package-root branch of {@link buildExternals}).
|
|
855
|
+
* `undefined` for a missing export, a callable one, or a module that does not
|
|
856
|
+
* import.
|
|
857
|
+
*/
|
|
858
|
+
async function packageDataExport(modulePath, name, session) {
|
|
859
|
+
let mod;
|
|
860
|
+
try {
|
|
861
|
+
mod = await importModuleMemoized(modulePath, session.importCache);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
if (!(name in mod) || typeof mod[name] === "function")
|
|
867
|
+
return undefined;
|
|
868
|
+
return { value: mod[name] };
|
|
869
|
+
}
|
|
870
|
+
function resolveMemoized(node, ctx) {
|
|
871
|
+
const cached = ctx.memo.get(node);
|
|
872
|
+
if (cached)
|
|
873
|
+
return cached;
|
|
874
|
+
const promise = resolveLiveValue(node, ctx);
|
|
875
|
+
ctx.memo.set(node, promise);
|
|
876
|
+
return promise;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Attempt to resolve `node` to the real runtime value it would have if the
|
|
880
|
+
* file were executed. Understands the "spine" of composite consumption: a
|
|
881
|
+
* bare call (a composite factory, or a wrapper like `propagate()` — resolved
|
|
882
|
+
* through this file's imports, invoked for real with statically-folded
|
|
883
|
+
* arguments), a reference to an earlier top-level `const` bound to one of
|
|
884
|
+
* those (memoized — see {@link ResolveCtx.memo}), and dotted property access
|
|
885
|
+
* or `!` non-null assertion on the result (`web.deployment`, `web.pdb!`).
|
|
886
|
+
*
|
|
887
|
+
* Throws when the shape IS recognized (a call, a member access) but
|
|
888
|
+
* resolution genuinely fails — unresolved import, non-function import,
|
|
889
|
+
* folding a nested argument failed, member access on a non-composite value,
|
|
890
|
+
* the call itself threw. That failure should fall back the whole file to
|
|
891
|
+
* run, same as any other fold gap.
|
|
892
|
+
*/
|
|
893
|
+
async function resolveLiveValue(node, ctx) {
|
|
894
|
+
if (ts.isParenthesizedExpression(node) ||
|
|
895
|
+
ts.isNonNullExpression(node) ||
|
|
896
|
+
ts.isAsExpression(node) ||
|
|
897
|
+
ts.isSatisfiesExpression(node)) {
|
|
898
|
+
return resolveLiveValue(node.expression, ctx);
|
|
899
|
+
}
|
|
900
|
+
if (ts.isIdentifier(node)) {
|
|
901
|
+
const binding = ctx.locals.get(node.text);
|
|
902
|
+
if (!binding) {
|
|
903
|
+
// chant #1020 — not a same-file local; try a cross-file resolution
|
|
904
|
+
// (a plain value, or the real live Declarable/CompositeInstance a
|
|
905
|
+
// sibling file's own fold produced, or a namespace import's synthetic
|
|
906
|
+
// exports object). Absent from `externals` falls through to
|
|
907
|
+
// `undefined`, exactly the pre-#1020 behavior for any unbound name.
|
|
908
|
+
if (ctx.externals.has(node.text)) {
|
|
909
|
+
const external = ctx.externals.get(node.text);
|
|
910
|
+
// chant #1373 — a function marker is callable, never a value. The
|
|
911
|
+
// export-by-name cases (`export const g = f`, `export { f }`) are
|
|
912
|
+
// handled by the declarator loop before it gets here.
|
|
913
|
+
if (isFoldableFunction(external)) {
|
|
914
|
+
throw cheapError(`function "${node.text}" used as a value is not foldable`);
|
|
915
|
+
}
|
|
916
|
+
return { value: external };
|
|
917
|
+
}
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
const resolvedSource = await resolveMemoized(binding.source, ctx);
|
|
921
|
+
if (resolvedSource === undefined)
|
|
922
|
+
return undefined;
|
|
923
|
+
if (binding.propKey === undefined)
|
|
924
|
+
return resolvedSource;
|
|
925
|
+
if (!isIndexableObject(resolvedSource.value)) {
|
|
926
|
+
throw cheapError(`destructured member "${binding.propKey}" is not on a composite value`);
|
|
927
|
+
}
|
|
928
|
+
return { value: resolvedSource.value[binding.propKey] };
|
|
929
|
+
}
|
|
930
|
+
if (ts.isCallExpression(node)) {
|
|
931
|
+
return { value: await resolveCallExpression(node, ctx) };
|
|
932
|
+
}
|
|
933
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
934
|
+
const base = await resolveLiveValue(node.expression, ctx);
|
|
935
|
+
if (base === undefined)
|
|
936
|
+
return undefined;
|
|
937
|
+
const key = node.name.text;
|
|
938
|
+
if (!isIndexableObject(base.value)) {
|
|
939
|
+
throw cheapError(`property access ".${key}" on a non-composite value is not foldable`);
|
|
940
|
+
}
|
|
941
|
+
return { value: base.value[key] };
|
|
942
|
+
}
|
|
943
|
+
return undefined;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Resolve a bare call expression — a composite factory call
|
|
947
|
+
* (`SomeComposite({...})`) or a wrapper that takes a composite instance and
|
|
948
|
+
* returns one (`propagate(SomeComposite({...}), {...})`). The callee must be
|
|
949
|
+
* a plain identifier bound by this file's own `import` (a namespace-import
|
|
950
|
+
* call like `ns.Foo(...)`, or a call to a function/composite DEFINED in this
|
|
951
|
+
* same file, can't be resolved without running the file — falls back, same
|
|
952
|
+
* as an unresolvable resource constructor).
|
|
953
|
+
*
|
|
954
|
+
* Two ways to get the value, tried in that order:
|
|
955
|
+
*
|
|
956
|
+
* 1. **Interpretation** (chant #1023) — the callee is an interpretable
|
|
957
|
+
* registered `Composite` defined in a PROJECT file, so its body is
|
|
958
|
+
* evaluated statically and nothing is imported or run. See
|
|
959
|
+
* {@link resolveInterpretableFactory} for the exact admissible subset.
|
|
960
|
+
* 2. **Invocation** (chant #1022) — everything else. Each argument is
|
|
961
|
+
* resolved (recursively, for a nested composite-call/member-access
|
|
962
|
+
* argument like `propagate`'s first one) or folded (for a plain props
|
|
963
|
+
* object literal via {@link fold}), the real function is imported and
|
|
964
|
+
* invoked, and the RESULT is what matters to the caller —
|
|
965
|
+
* {@link resolveLiveValue}'s callers decide what shape they need (a
|
|
966
|
+
* `CompositeInstance` for member access, an
|
|
967
|
+
* `isDeclarable`/`isCompositeInstance` value for a top-level export).
|
|
968
|
+
* No pre-check verifies the resolved callee is "really" a composite.
|
|
969
|
+
*
|
|
970
|
+
* Arm 2 is untouched by #1023, including for a factory arm 1 STARTED and then
|
|
971
|
+
* declined on: interpretation is all-or-nothing per call, and a decline is not
|
|
972
|
+
* a fold failure — it lands exactly where the code landed before this existed.
|
|
973
|
+
*/
|
|
974
|
+
async function resolveCallExpression(node, ctx) {
|
|
975
|
+
// chant #1054 — reuses ../fold/subset's `callExpressionMessage` (the SAME
|
|
976
|
+
// builder `fold()` throws with for a call used as a prop value) rather
|
|
977
|
+
// than a hand-written "call expression as a value" copy: the two used to
|
|
978
|
+
// say different things for the identical rejection, which silently broke
|
|
979
|
+
// any tooling grouping fallback reasons by text.
|
|
980
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
981
|
+
throw cheapError(callExpressionMessage(node));
|
|
982
|
+
}
|
|
983
|
+
const calleeName = node.expression.text;
|
|
984
|
+
const binding = ctx.imports.get(calleeName);
|
|
985
|
+
// chant #1373 — a project-local function with a foldable body (declared in
|
|
986
|
+
// this file, or imported from a sibling project file) is evaluated
|
|
987
|
+
// statically, through the same `fold()` branch a NESTED call to it takes.
|
|
988
|
+
// When its body turns out not to fold, an IMPORTED callee still has the
|
|
989
|
+
// pre-#1373 path below (interpret as a composite, else import and invoke) —
|
|
990
|
+
// a helper that wraps a composite call, say, keeps folding by invocation
|
|
991
|
+
// exactly as it did. A same-file callee has nothing to invoke without
|
|
992
|
+
// running the file, so the fold diagnostic is the verdict.
|
|
993
|
+
const local = ctx.externals.get(calleeName);
|
|
994
|
+
if (isFoldableFunction(local)) {
|
|
995
|
+
let foldFailure;
|
|
996
|
+
try {
|
|
997
|
+
const folded = fold(node, ctx.consts, ctx.intrinsics, ctx.externals);
|
|
998
|
+
return await reviveFoldedValue(folded, ctx, false);
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
if (!(err instanceof Error))
|
|
1002
|
+
throw err;
|
|
1003
|
+
foldFailure = err;
|
|
1004
|
+
}
|
|
1005
|
+
// chant#2453 — a DECLARED function is judged by its body, and that is the
|
|
1006
|
+
// whole answer. There is no invocation arm here any more.
|
|
1007
|
+
//
|
|
1008
|
+
// There used to be: when the body did not fold, an imported callee was
|
|
1009
|
+
// imported and invoked instead. chant#2441 narrowed that to stop it
|
|
1010
|
+
// rescuing a depth refusal. The external corpus then showed what the rest
|
|
1011
|
+
// of it does, in a project nobody here maintains
|
|
1012
|
+
// (jhgaylor/infisical-chant, INTENTIUS/typescript-as-data#129):
|
|
1013
|
+
//
|
|
1014
|
+
// export const namingParams = namingParamsFromEnv();
|
|
1015
|
+
//
|
|
1016
|
+
// whose body reads `process.env`. `F-Eval-Ident` step 4 rejects `process`,
|
|
1017
|
+
// so the body does not fold — and chant imported the module and ran it,
|
|
1018
|
+
// folding the file to whatever the FOLDING PROCESS's environment held. The
|
|
1019
|
+
// file reported `fold`, which reads as "determined statically". Two people
|
|
1020
|
+
// folding the same source got different output and nothing said so.
|
|
1021
|
+
//
|
|
1022
|
+
// That is wrong in chant's own terms before it is wrong in J1's: `--fold`
|
|
1023
|
+
// is the value you would get by running, WITHOUT running, and this ran. So
|
|
1024
|
+
// the rejection propagates and the file runs, carrying the reason
|
|
1025
|
+
// `F-Eval-CallLocal` gives.
|
|
1026
|
+
//
|
|
1027
|
+
// `F-Call` step 6's invocation is for a binding that is neither a declared
|
|
1028
|
+
// function nor an interpretable composite, and it still happens — below,
|
|
1029
|
+
// in the arm this one is not. A composite factory is not a
|
|
1030
|
+
// `FoldableFunction`, so `resolveImportedCall` still interprets it and
|
|
1031
|
+
// still invokes it when interpretation declines.
|
|
1032
|
+
//
|
|
1033
|
+
// chant#2455 — unless the caller asked for `ι = executing` (spec 1.8),
|
|
1034
|
+
// which is this behaviour as a MODE rather than as a silent default. The
|
|
1035
|
+
// value is then what a run would compute in this process, environment and
|
|
1036
|
+
// all, which is exactly why it cannot be implicit.
|
|
1037
|
+
if (ctx.executing && binding) {
|
|
1038
|
+
try {
|
|
1039
|
+
return await resolveImportedCall(node, calleeName, binding, ctx);
|
|
1040
|
+
}
|
|
1041
|
+
catch (err) {
|
|
1042
|
+
throw cheapError(`${foldFailure.message}; invoking it instead failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// Name the mode that would have folded it, so the reason is actionable
|
|
1046
|
+
// rather than only correct. Not under `sandbox`, where `executing` is not
|
|
1047
|
+
// an option the caller could take.
|
|
1048
|
+
if (binding && !ctx.sandbox) {
|
|
1049
|
+
throw cheapError(`${foldFailure.message} (the default mode does not invoke a declared project function; ` +
|
|
1050
|
+
"`executing` would run it and fold what it returns)");
|
|
1051
|
+
}
|
|
1052
|
+
throw foldFailure;
|
|
1053
|
+
}
|
|
1054
|
+
if (!binding) {
|
|
1055
|
+
throw cheapError(callExpressionMessage(node));
|
|
1056
|
+
}
|
|
1057
|
+
return resolveImportedCall(node, calleeName, binding, ctx);
|
|
1058
|
+
}
|
|
1059
|
+
/** The pre-#1373 two-arm resolution of an IMPORTED callee — see {@link resolveCallExpression}. */
|
|
1060
|
+
async function resolveImportedCall(node, calleeName, binding, ctx) {
|
|
1061
|
+
// chant #1023 — is this callee a composite whose body can be INTERPRETED
|
|
1062
|
+
// rather than run? Purely a static question (does the defining module parse,
|
|
1063
|
+
// does it declare this export as `Composite(<fn>, "<name>")`, is `<fn>`'s
|
|
1064
|
+
// body inside the admissible subset) — no argument has been evaluated yet
|
|
1065
|
+
// and nothing has been imported, so a `undefined` here has cost nothing and
|
|
1066
|
+
// changed nothing.
|
|
1067
|
+
const factory = await resolveInterpretableFactory(binding, ctx);
|
|
1068
|
+
if (factory) {
|
|
1069
|
+
const args = await resolveCallArguments(node, calleeName, ctx);
|
|
1070
|
+
const interpreted = await interpretCompositeFactory(factory, args, ctx, { file: ctx.file, node });
|
|
1071
|
+
if (interpreted)
|
|
1072
|
+
return interpreted.value;
|
|
1073
|
+
// Declined — the body's shape passed but something in it did not resolve
|
|
1074
|
+
// (an identifier the module's own imports don't reach, a constructor
|
|
1075
|
+
// --sandbox refuses). Fall through to arm 2 with the arguments ALREADY
|
|
1076
|
+
// evaluated, so a nested composite-call argument is not built twice.
|
|
1077
|
+
return invokeImportedCallee(node, calleeName, binding, ctx, args);
|
|
1078
|
+
}
|
|
1079
|
+
return invokeImportedCallee(node, calleeName, binding, ctx);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Arm 2 — import the module the callee came from and call it, in this
|
|
1083
|
+
* process, given ALREADY-RESOLVED argument values. Factored out of
|
|
1084
|
+
* {@link invokeImportedCallee} (chant #1174) so a caller with no raw
|
|
1085
|
+
* `ts.CallExpression` node — {@link resolveCompositeCall}, the nested-value
|
|
1086
|
+
* counterpart to {@link resolveCallExpression} — can reach the identical
|
|
1087
|
+
* refusal/resolve/import/callable-check/call sequence #1022 established,
|
|
1088
|
+
* without inventing a second copy of it.
|
|
1089
|
+
*/
|
|
1090
|
+
async function invokeResolvedCallee(calleeName, binding, ctx, args) {
|
|
1091
|
+
// chant #1093 — THE gap this check exists for. Invoking the callee runs
|
|
1092
|
+
// project code (the factory body, and its whole module's top level) in the
|
|
1093
|
+
// CLI's own process; under --sandbox that has to happen in the child
|
|
1094
|
+
// instead, so refuse here and let the file fall back to the sandboxed run
|
|
1095
|
+
// path. See {@link sandboxedExecutionRefusal}.
|
|
1096
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, calleeName, "composite factory");
|
|
1097
|
+
if (refusal)
|
|
1098
|
+
throw cheapError(refusal);
|
|
1099
|
+
// spec 2.0, `F-Call` step 5 — a callee that resolves to a PROJECT FILE is
|
|
1100
|
+
// not imported and invoked here. Invoking it runs the factory body, and its
|
|
1101
|
+
// whole module's top level, in the CLI's own process, and that is the
|
|
1102
|
+
// project's own code rather than a package's.
|
|
1103
|
+
//
|
|
1104
|
+
// Step 2's project-owned invocation moved behind `ι = executing` at v0.72.3
|
|
1105
|
+
// (chant#2455). Step 6 is the same path and had not moved, which is the
|
|
1106
|
+
// whole of this change. The specification took the decision first: its
|
|
1107
|
+
// rationale says of `executing` that "it is never the default", and
|
|
1108
|
+
// `verdict.md` calls `open` the strict default.
|
|
1109
|
+
//
|
|
1110
|
+
// A package factory still runs here, which is what `F-Host-Admission`
|
|
1111
|
+
// governs. A project-file callee that a `Composite` registered is
|
|
1112
|
+
// INTERPRETED at step 4 and never reaches this line; only one that
|
|
1113
|
+
// interpretation declined does, and for that the verdict is `run`. Since
|
|
1114
|
+
// chant#3247 a package's registered composite is interpreted at step 4 too,
|
|
1115
|
+
// and reaches this line only when interpretation declined; it is a
|
|
1116
|
+
// package's code, so it is still invoked.
|
|
1117
|
+
// "Project file" is the specifier's ORIGIN, not its syntax.
|
|
1118
|
+
// {@link isProjectFileSpecifier} is true of any absolute path, and chant's
|
|
1119
|
+
// own modules are imported by absolute path throughout the tests, so the
|
|
1120
|
+
// shape test alone refuses `propagate` and every other host function chant
|
|
1121
|
+
// publishes. {@link isTrustedExecutableBinding} draws the line the rule
|
|
1122
|
+
// means: an active lexicon package or chant-core's own tree is a package's
|
|
1123
|
+
// code, and everything else reached by a relative or absolute specifier is
|
|
1124
|
+
// the project's.
|
|
1125
|
+
if (!ctx.executing && isProjectImport(binding.specifier, ctx.file) && !isTrustedExecutableBinding(binding, ctx)) {
|
|
1126
|
+
throw cheapError(`"${calleeName}" is imported from "${binding.specifier}", which is a project file; ` +
|
|
1127
|
+
"the default mode does not invoke project-owned code (`executing` would run it and fold what it returns)");
|
|
1128
|
+
}
|
|
1129
|
+
let modulePath;
|
|
1130
|
+
try {
|
|
1131
|
+
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
1132
|
+
}
|
|
1133
|
+
catch (err) {
|
|
1134
|
+
throw cheapError(`could not resolve import "${binding.specifier}" for "${calleeName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1135
|
+
}
|
|
1136
|
+
// Reuses the SAME import mechanism the run path uses ({@link importModule},
|
|
1137
|
+
// ../discovery/import.ts — a plain-path `import()`) rather than a
|
|
1138
|
+
// `pathToFileURL(...)`-wrapped one. For a resource constructor (a
|
|
1139
|
+
// stateless lexicon class) the two would be interchangeable, but a
|
|
1140
|
+
// composite-call resolution (#1023) can import a SIBLING PROJECT FILE
|
|
1141
|
+
// (e.g. `import { network } from "./network"`, not just a lexicon
|
|
1142
|
+
// package) that `discover()`'s own per-file loop ALSO independently
|
|
1143
|
+
// processes — if that file falls back to run and imports the same
|
|
1144
|
+
// sibling for a live value (`network.vpc.VpcId`), the two import calls
|
|
1145
|
+
// MUST resolve to the identical cached module instance, or the run side
|
|
1146
|
+
// ends up with a second, distinct `network`/`vpc` object whose AttrRefs
|
|
1147
|
+
// can never be matched back to the entity fold already registered
|
|
1148
|
+
// (`Cannot serialize AttrRef ...: logical name not set`).
|
|
1149
|
+
let mod;
|
|
1150
|
+
try {
|
|
1151
|
+
mod = await importModuleMemoized(modulePath, ctx.importCache);
|
|
1152
|
+
}
|
|
1153
|
+
catch (err) {
|
|
1154
|
+
throw cheapError(`could not import "${binding.specifier}" to resolve "${calleeName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1155
|
+
}
|
|
1156
|
+
const Fn = mod[binding.imported];
|
|
1157
|
+
if (typeof Fn !== "function") {
|
|
1158
|
+
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a function`);
|
|
1159
|
+
}
|
|
1160
|
+
executionCounts.factoryInvocations += 1;
|
|
1161
|
+
// chant#3247 — a relative import inside a lexicon package's own composite is
|
|
1162
|
+
// the package's code, not the project's.
|
|
1163
|
+
const packageInternal = ctx.packageRoot !== undefined && isUnderDirectory(modulePath, ctx.packageRoot);
|
|
1164
|
+
if (isProjectImport(binding.specifier, ctx.file) && !packageInternal)
|
|
1165
|
+
executionCounts.projectFactoryInvocations += 1;
|
|
1166
|
+
const result = Fn(...args);
|
|
1167
|
+
// chant#3608 — a host composite invoked rather than interpreted (one the
|
|
1168
|
+
// host publishes, or one whose body interpretation declined) returns the
|
|
1169
|
+
// host's own instance. Mark it as the interpreted form is marked, so an
|
|
1170
|
+
// instance exported whole is expanded and its members, whole or
|
|
1171
|
+
// destructured, are known to come from a composite.
|
|
1172
|
+
const compositeName = Fn.compositeName;
|
|
1173
|
+
if (typeof compositeName === "string" && isIndexableObject(result) && !isCompositeInstance(result)) {
|
|
1174
|
+
markHostCompositeInstance(result, compositeName);
|
|
1175
|
+
}
|
|
1176
|
+
return result;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Arm 2's original entry point — a raw call-expression node in hand. Resolves
|
|
1180
|
+
* arguments (unless the caller already has them) and delegates to
|
|
1181
|
+
* {@link invokeResolvedCallee}. Unchanged from #1022 in both behavior and
|
|
1182
|
+
* ORDER (refusal, resolve, import, callable check, arguments, call); #1023
|
|
1183
|
+
* factored the body out once already, #1174 a second time.
|
|
1184
|
+
*
|
|
1185
|
+
* @param args - Already-evaluated arguments, when the caller has them.
|
|
1186
|
+
*/
|
|
1187
|
+
async function invokeImportedCallee(node, calleeName, binding, ctx, args) {
|
|
1188
|
+
const callArgs = args ?? (await resolveCallArguments(node, calleeName, ctx));
|
|
1189
|
+
return invokeResolvedCallee(calleeName, binding, ctx, callArgs);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Resolve a composite-factory call used as a NESTED value — chant #1174's
|
|
1193
|
+
* `<Identifier>(...).step` idiom ({@link FoldedCompositeStepCall}) — given
|
|
1194
|
+
* its callee NAME and already-REVIVED argument values. The nested-value
|
|
1195
|
+
* counterpart to {@link resolveCallExpression}, which the top-level
|
|
1196
|
+
* declarator path (`export const x = Checkout({...})`) already used: the
|
|
1197
|
+
* raw `ts.CallExpression` node isn't needed here because
|
|
1198
|
+
* {@link resolveInterpretableFactory} only needs the import `binding`, and
|
|
1199
|
+
* {@link interpretCompositeFactory}/{@link invokeResolvedCallee} only need
|
|
1200
|
+
* the resolved argument values — which `fold()`'s envelope already folded
|
|
1201
|
+
* and {@link reviveFoldedValue} has already revived by the time this runs.
|
|
1202
|
+
*
|
|
1203
|
+
* Same two arms, same order, as the top-level path: interpret a registered
|
|
1204
|
+
* `Composite` (chant #1023; a lexicon package's own since chant#3247) when its
|
|
1205
|
+
* body qualifies, otherwise import and invoke for real — the arm a
|
|
1206
|
+
* lexicon-package composite whose body leaves the subset (`Checkout`,
|
|
1207
|
+
* `SetupNode`, …) takes, exactly as it does for a top-level
|
|
1208
|
+
* `export const x = Checkout({...})`. An unbound
|
|
1209
|
+
* callee name throws the same "function call as a value" message `fold()`
|
|
1210
|
+
* itself would have, for a caller with no `FoldedCompositeStepCall` special
|
|
1211
|
+
* case to fall into.
|
|
1212
|
+
*/
|
|
1213
|
+
async function resolveCompositeCall(calleeName, args, ctx) {
|
|
1214
|
+
const binding = ctx.imports.get(calleeName);
|
|
1215
|
+
if (!binding)
|
|
1216
|
+
throw cheapError(`unresolved identifier: ${calleeName}`);
|
|
1217
|
+
const factory = await resolveInterpretableFactory(binding, ctx);
|
|
1218
|
+
if (factory) {
|
|
1219
|
+
const interpreted = await interpretCompositeFactory(factory, args, ctx);
|
|
1220
|
+
if (interpreted)
|
|
1221
|
+
return interpreted.value;
|
|
1222
|
+
// Declined — falls through to arm 2 with the SAME arguments, so a nested
|
|
1223
|
+
// composite-call argument (or a live cross-file reference) is not
|
|
1224
|
+
// resolved a second time. Identical discipline to `resolveImportedCall`.
|
|
1225
|
+
}
|
|
1226
|
+
return invokeResolvedCallee(calleeName, binding, ctx, args);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Resolve a call's arguments to the real values the callee would receive —
|
|
1230
|
+
* factored out of {@link resolveCallExpression} (chant #1023) so the
|
|
1231
|
+
* interpretation and invocation arms consume ONE evaluation of them. Anything
|
|
1232
|
+
* else would evaluate a nested composite-call argument twice and hand the
|
|
1233
|
+
* callee the second instance while the first was already wired into an
|
|
1234
|
+
* `AttrRef`.
|
|
1235
|
+
*
|
|
1236
|
+
* chant #1112 — ONE rule for a registered authoring helper's arguments,
|
|
1237
|
+
* applied at BOTH sites that can invoke one. `reviveHelperCall` (the
|
|
1238
|
+
* nested-value site, #1082) already revives a helper's arguments with
|
|
1239
|
+
* `requireLiveRefs`, because a helper reads THROUGH its ref (`output()`
|
|
1240
|
+
* derefs the `WeakRef` parent) and a look-alike `{__attrRef}` envelope
|
|
1241
|
+
* makes it produce a wrong result rather than none. This site — a
|
|
1242
|
+
* top-level `export const oArn = output(bucket.Arn, "oArn")` — took the
|
|
1243
|
+
* composite-factory rule (`false`) instead, which is right for a factory
|
|
1244
|
+
* (its props keep the envelope, and the serializer's own walker resolves
|
|
1245
|
+
* it) and wrong for a helper. It went unnoticed while the resulting
|
|
1246
|
+
* `LexiconOutput` was being discarded anyway; with the export namespace
|
|
1247
|
+
* now collected in full, a same-file `output(...)` would reach the
|
|
1248
|
+
* serializer holding an inert envelope where the run path has a real
|
|
1249
|
+
* reference. Rejected here instead, which falls the file back to run —
|
|
1250
|
+
* absent output, never a wrong one.
|
|
1251
|
+
*/
|
|
1252
|
+
async function resolveCallArguments(node, calleeName, ctx) {
|
|
1253
|
+
const helperArgs = isFoldableHelperName(calleeName);
|
|
1254
|
+
const args = [];
|
|
1255
|
+
for (const argNode of node.arguments) {
|
|
1256
|
+
const live = await resolveLiveValue(argNode, ctx);
|
|
1257
|
+
if (live !== undefined) {
|
|
1258
|
+
args.push(live.value);
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
// chant#3610 — the bindings hold for this argument only, so a read of the
|
|
1262
|
+
// same name outside any argument still refuses, whatever the declarator order.
|
|
1263
|
+
const bound = await resolveSameFileCallReferences(argNode, ctx);
|
|
1264
|
+
try {
|
|
1265
|
+
// chant #1039 — a folded (non-live) argument may itself contain a
|
|
1266
|
+
// registered intrinsic tagged template; revive it into the real value
|
|
1267
|
+
// before the composite factory actually runs on it (see the "Intrinsic
|
|
1268
|
+
// revival" section below `resolveResourceEntity` uses the same way).
|
|
1269
|
+
args.push(await reviveFoldedValue(fold(argNode, ctx.consts, ctx.intrinsics, ctx.externals), ctx, helperArgs));
|
|
1270
|
+
}
|
|
1271
|
+
finally {
|
|
1272
|
+
for (const name of bound)
|
|
1273
|
+
ctx.externals.delete(name);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return args;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* chant#3325 — a composite call's props can name a same-file `const` bound to
|
|
1280
|
+
* another call (`left: notes.table`, with `export const notes =
|
|
1281
|
+
* TenantTable({...})` earlier in the file). `fold()` cannot answer that name:
|
|
1282
|
+
* it is synchronous, and the value is a composite instance only the live spine
|
|
1283
|
+
* builds. So before the argument folds, each such name it mentions is resolved
|
|
1284
|
+
* through {@link resolveMemoized}, the same promise the declarator and every
|
|
1285
|
+
* other reference to it share, and the instance goes into `externals`, where
|
|
1286
|
+
* `fold()`'s identifier branch reads it as it reads a pre-built `new` or tag
|
|
1287
|
+
* const (#1169, #3196).
|
|
1288
|
+
*
|
|
1289
|
+
* Only a call `fold()` has no case for qualifies: an imported callee that is
|
|
1290
|
+
* neither a registered authoring helper nor a registered intrinsic. For
|
|
1291
|
+
* anything else the name is left alone and folds exactly as before. A
|
|
1292
|
+
* resolution that fails, or gives anything but a Declarable or a composite
|
|
1293
|
+
* instance, is left alone too, so the reference rejects with the message it
|
|
1294
|
+
* always had. Inside a factory body `locals` is empty, so nothing here applies.
|
|
1295
|
+
*
|
|
1296
|
+
* chant#3610 — returns the names it bound, which the caller removes from
|
|
1297
|
+
* `externals` once the argument has folded. A host composite's instance
|
|
1298
|
+
* counts as a composite instance here, as it does everywhere else.
|
|
1299
|
+
*/
|
|
1300
|
+
async function resolveSameFileCallReferences(node, ctx) {
|
|
1301
|
+
const names = new Set();
|
|
1302
|
+
const visit = (n) => {
|
|
1303
|
+
if (ts.isIdentifier(n)) {
|
|
1304
|
+
if (isUnfoldableSameFileCall(n.text, ctx))
|
|
1305
|
+
names.add(n.text);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (ts.isFunctionLike(n) || ts.isTypeNode(n))
|
|
1309
|
+
return;
|
|
1310
|
+
if (ts.isPropertyAccessExpression(n))
|
|
1311
|
+
return visit(n.expression);
|
|
1312
|
+
if (ts.isPropertyAssignment(n)) {
|
|
1313
|
+
if (ts.isComputedPropertyName(n.name))
|
|
1314
|
+
visit(n.name);
|
|
1315
|
+
return visit(n.initializer);
|
|
1316
|
+
}
|
|
1317
|
+
ts.forEachChild(n, visit);
|
|
1318
|
+
};
|
|
1319
|
+
visit(node);
|
|
1320
|
+
const bound = [];
|
|
1321
|
+
for (const name of names) {
|
|
1322
|
+
let resolved;
|
|
1323
|
+
try {
|
|
1324
|
+
resolved = await resolveMemoized(ctx.locals.get(name).source, ctx);
|
|
1325
|
+
}
|
|
1326
|
+
catch {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
if (resolved &&
|
|
1330
|
+
(isDeclarable(resolved.value) ||
|
|
1331
|
+
isCompositeInstance(resolved.value) ||
|
|
1332
|
+
hostCompositeName(resolved.value) !== undefined)) {
|
|
1333
|
+
ctx.externals.set(name, resolved.value);
|
|
1334
|
+
bound.push(name);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return bound;
|
|
1338
|
+
}
|
|
1339
|
+
/** A same-file `const name = Callee(...)` that `fold()` would refuse as a function call used as a value. */
|
|
1340
|
+
function isUnfoldableSameFileCall(name, ctx) {
|
|
1341
|
+
if (ctx.externals.has(name))
|
|
1342
|
+
return false;
|
|
1343
|
+
const binding = ctx.locals.get(name);
|
|
1344
|
+
if (!binding || binding.propKey !== undefined || ctx.consts.get(name) !== binding.source)
|
|
1345
|
+
return false;
|
|
1346
|
+
const source = binding.source;
|
|
1347
|
+
if (!ts.isCallExpression(source) || !ts.isIdentifier(source.expression))
|
|
1348
|
+
return false;
|
|
1349
|
+
const callee = source.expression.text;
|
|
1350
|
+
return (ctx.imports.has(callee) &&
|
|
1351
|
+
!isFoldableHelperName(callee) &&
|
|
1352
|
+
!ctx.intrinsics.some((i) => i.name === callee) &&
|
|
1353
|
+
!isFoldableFunction(ctx.externals.get(callee)));
|
|
1354
|
+
}
|
|
1355
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1356
|
+
// Composite factory interpretation (chant #1023, epic #1019 Phase 5).
|
|
1357
|
+
//
|
|
1358
|
+
// THE CONTRACT — the admissible factory subset, in ../fold/subset.ts's style.
|
|
1359
|
+
//
|
|
1360
|
+
// A composite is a factory function, and #1022 got its value the only way it
|
|
1361
|
+
// could: by importing the defining module and CALLING it. That is the last
|
|
1362
|
+
// place `chant build --fold` executes project-authored code in the CLI's own
|
|
1363
|
+
// process (chant #1093), and the reason `--sandbox` has to demote such a file
|
|
1364
|
+
// to the sandboxed child instead of folding it (chant #1111). This section
|
|
1365
|
+
// removes the call for the factories whose bodies can be evaluated instead.
|
|
1366
|
+
//
|
|
1367
|
+
// A call is interpreted when ALL of the following hold. Every one of them is
|
|
1368
|
+
// checked BEFORE any argument is evaluated or anything is constructed, so a
|
|
1369
|
+
// factory outside the subset costs a parse and nothing else:
|
|
1370
|
+
//
|
|
1371
|
+
// 1. **The callee is bound by an `import` from a PROJECT FILE** (a
|
|
1372
|
+
// relative/absolute specifier, or one a tsconfig `paths` entry maps), **or
|
|
1373
|
+
// from an ACTIVE LEXICON PACKAGE of this build whose module is TypeScript
|
|
1374
|
+
// source** (chant#3247). The package case is matched by text first — the
|
|
1375
|
+
// specifier, or its package root for a subpath, against
|
|
1376
|
+
// {@link FoldSession.lexiconPackages}, exactly as
|
|
1377
|
+
// {@link isTrustedExecutableBinding}'s arm 1 matches it — and only then
|
|
1378
|
+
// resolved; a module that resolves to compiled JavaScript or a declaration
|
|
1379
|
+
// file is not interpreted, so a package that ships only `dist` keeps
|
|
1380
|
+
// invoking. Every chant lexicon routes its exports at `./src/*.ts`
|
|
1381
|
+
// (check-lexicon tier 1), so in practice the source is always there.
|
|
1382
|
+
// Interpreting buys provenance: the run path cannot say which parameter
|
|
1383
|
+
// fed a field, and interpretation can (#2161, #3246).
|
|
1384
|
+
//
|
|
1385
|
+
// The trust boundary does not move. A package's composite is interpreted
|
|
1386
|
+
// with that PACKAGE's directory ({@link FactoryModuleScope.packageRoot})
|
|
1387
|
+
// as a trusted lexicon root for the length of its body and nothing else:
|
|
1388
|
+
// the package's own relative imports are trusted under `--sandbox` the way
|
|
1389
|
+
// a subpath of the same package already is, and they are never folded as
|
|
1390
|
+
// project files. The package was imported and run by `loadPlugins` before
|
|
1391
|
+
// discovery began, and every module of it is reachable as a subpath, so
|
|
1392
|
+
// reading its source adds no trust. What it may NOT do is reach outside
|
|
1393
|
+
// that directory: a relative import that leaves the package root is not
|
|
1394
|
+
// followed. A lexicon this build did not load stays as out of scope as
|
|
1395
|
+
// any other bare specifier.
|
|
1396
|
+
// 2. **The defining module declares that export as `Composite(<fn>, "<name>")`**
|
|
1397
|
+
// — a top-level `export const <name> = Composite(...)`, where `Composite`
|
|
1398
|
+
// is bound in THAT module to an import of chant's own
|
|
1399
|
+
// ({@link isChantOwnedHelperBinding}, the same provenance question #1082
|
|
1400
|
+
// asks of an authoring helper, and the same answer). This is #1023's
|
|
1401
|
+
// "recognize the callee is a registered Composite": a bare call to
|
|
1402
|
+
// anything else — a project helper function, a `withDefaults(...)`
|
|
1403
|
+
// wrapper, an `import`-then-`export` alias — is not recognized and is not
|
|
1404
|
+
// interpreted. The defining module may sit behind BARREL re-exports
|
|
1405
|
+
// (chant#3247): `export { X } from "./x"`, `export { Y as X } from "./x"`
|
|
1406
|
+
// and `export * from "./x"` are followed, relative specifiers only, at most
|
|
1407
|
+
// {@link MAX_REEXPORT_HOPS} deep, and never out of a package's root. A
|
|
1408
|
+
// module that declares the name itself is answered by that declaration.
|
|
1409
|
+
// 3. **`<fn>` is an arrow/function expression taking at most one parameter**,
|
|
1410
|
+
// bound as a plain identifier or a simple object binding pattern (no
|
|
1411
|
+
// default, rest, or nested pattern).
|
|
1412
|
+
// 4. **Its body is a single expression, or a block of `const` declarations
|
|
1413
|
+
// followed by one `return`** — and nothing else. No `if`, no `throw`, no
|
|
1414
|
+
// loop, no `let`/`var`, no nested function declaration, no bare expression
|
|
1415
|
+
// statement. This is the line a project's hand-rolled composite modules
|
|
1416
|
+
// fall outside (module-level `buildXxx()` helpers with `if`/`throw` and
|
|
1417
|
+
// `.map()`), and they are meant to: they keep invoking, exactly as before.
|
|
1418
|
+
// 5. **Every expression in it is inside the fold subset, extended with the
|
|
1419
|
+
// two things a factory body exists to do**: `new Type(...)` in ANY value
|
|
1420
|
+
// position (a member, a nested property object, an array element), and a
|
|
1421
|
+
// call through a bare identifier. Both are shape-admissible here and
|
|
1422
|
+
// resolution-checked at evaluation, the same permissive-shape /
|
|
1423
|
+
// strict-evaluation split ../fold/subset.ts documents for identifier
|
|
1424
|
+
// binding and helper provenance.
|
|
1425
|
+
//
|
|
1426
|
+
// Everything else is out: a computed key, a method call (`naming.name(...)` —
|
|
1427
|
+
// the callee is a property access, not an identifier), an array method, an
|
|
1428
|
+
// operator `fold()` doesn't implement, `await`, a class expression, a template
|
|
1429
|
+
// with a computed tag.
|
|
1430
|
+
//
|
|
1431
|
+
// WHY THIS IS NOT IN ../fold/subset.ts. That module is the shared, shape-only
|
|
1432
|
+
// predicate `fold()` and EVL both read, and its own contract is that it may
|
|
1433
|
+
// only ever be PERMISSIVE relative to `fold()`, never stricter. Admissibility
|
|
1434
|
+
// here is not a shape question at all: rules 1 and 2 need the module graph —
|
|
1435
|
+
// which file a name came from, and what that file declares — exactly like
|
|
1436
|
+
// #1082's authoring-helper provenance, which subset.ts documents (point 2b)
|
|
1437
|
+
// as deliberately out for the same reason. Putting rules 3-5 there alone would
|
|
1438
|
+
// describe a subset no caller could act on without also answering 1 and 2, and
|
|
1439
|
+
// putting all five there would mean teaching a syntax-only lint rule to
|
|
1440
|
+
// resolve imports. The shape half stays checkable by anything that wants it —
|
|
1441
|
+
// {@link findFactorySubsetViolation} takes a lone `ts` node — but the shared
|
|
1442
|
+
// predicate is not made stricter, or wider, by this issue.
|
|
1443
|
+
//
|
|
1444
|
+
// WHAT IS PRESERVED. Interpretation is not a second construction path:
|
|
1445
|
+
//
|
|
1446
|
+
// - **The instances are real, and made by the lexicon's own constructors.**
|
|
1447
|
+
// A `new Role({...})` in the body resolves `Role` through the DEFINING
|
|
1448
|
+
// module's imports and calls that class, the same class the run path would
|
|
1449
|
+
// have called from the same resolved module path.
|
|
1450
|
+
// - **Sibling references are live, not symbolic.** `role.Arn` reads the
|
|
1451
|
+
// attribute off the `Role` instance this interpretation just built, so it
|
|
1452
|
+
// is a genuine `AttrRef` wired to a genuine parent — identical to running
|
|
1453
|
+
// the factory, and strictly better than a top-level fold's `{__attrRef}`
|
|
1454
|
+
// envelope (an intrinsic that inspects its argument, `Sub`/`Ref`, gets what
|
|
1455
|
+
// it expects here).
|
|
1456
|
+
// - **The instance is assembled by `Composite()` itself.** The members go
|
|
1457
|
+
// back through chant's own {@link Composite} (../composite.ts) rather than
|
|
1458
|
+
// through a hand-built look-alike, so member validation, the non-enumerable
|
|
1459
|
+
// `members`/`_definition` layout `propagate()` and `expandComposite()`
|
|
1460
|
+
// depend on, and provenance stamping are the run path's, by construction
|
|
1461
|
+
// and not by resemblance.
|
|
1462
|
+
// - **`propagate()` still works, untouched.** It is a chant-owned import, so
|
|
1463
|
+
// `propagate(SomeComposite({...}), {...})` invokes the real `propagate` on
|
|
1464
|
+
// the interpreted instance and mutates it in place exactly as it does a run
|
|
1465
|
+
// one — which is why instance identity has to be a real `CompositeInstance`
|
|
1466
|
+
// and not a copy (chant #1097).
|
|
1467
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1468
|
+
/**
|
|
1469
|
+
* A backstop for a composite that calls itself, directly or through a member.
|
|
1470
|
+
* {@link FoldSession.stack} cannot see it — the recursion happens entirely
|
|
1471
|
+
* inside one module's source, crossing no file boundary — so this is what
|
|
1472
|
+
* terminates it. Deliberately small: real composite nesting is 2-3 deep, and a
|
|
1473
|
+
* chain past this is a bug, not a design.
|
|
1474
|
+
*
|
|
1475
|
+
* Exhausting it falls the file back to run, naming this bound (#2370). That is
|
|
1476
|
+
* the second half of the specification's `F-Depth`: an implementation states
|
|
1477
|
+
* its bounds and, on exhaustion, falls back rather than fails. The other two —
|
|
1478
|
+
* {@link MAX_RESOLUTION_DEPTH} and `fold()`'s `MAX_FUNCTION_CALL_DEPTH` —
|
|
1479
|
+
* already did.
|
|
1480
|
+
*
|
|
1481
|
+
* It previously returned `undefined` here, which
|
|
1482
|
+
* {@link resolveInterpretableFactory}'s contract defines as "not
|
|
1483
|
+
* interpretable", so the caller invoked the factory instead and the file was
|
|
1484
|
+
* still reported as folded. That conflated two different facts: a factory
|
|
1485
|
+
* whose shape is outside the subset (invoking it is correct) and a factory we
|
|
1486
|
+
* stopped looking at (invoking it is a different evaluation than the one being
|
|
1487
|
+
* reported). The first is a verdict; the second was a silent degradation, and
|
|
1488
|
+
* a reader comparing chant's output against the reference implementation's
|
|
1489
|
+
* "fell back at depth 16" had nothing to explain the difference.
|
|
1490
|
+
*/
|
|
1491
|
+
const MAX_INTERPRETATION_DEPTH = 16;
|
|
1492
|
+
/**
|
|
1493
|
+
* Exhaustion of {@link MAX_INTERPRETATION_DEPTH}, as its own type so it can
|
|
1494
|
+
* pass through {@link interpretCompositeFactory}'s decline.
|
|
1495
|
+
*
|
|
1496
|
+
* That decline — a blanket `catch { return undefined }` — is right for every
|
|
1497
|
+
* other failure inside a factory body: the body turned out not to interpret,
|
|
1498
|
+
* the caller invokes it instead, and the file still folds. It is wrong for
|
|
1499
|
+
* this one, because giving up at a depth bound is not a statement about the
|
|
1500
|
+
* body. Without a type to test, the depth error was caught by the *enclosing*
|
|
1501
|
+
* interpretation, declined, and the factory invoked — which is how the bound
|
|
1502
|
+
* came to be silent (#2370).
|
|
1503
|
+
*/
|
|
1504
|
+
class InterpretationDepthError extends FoldError {
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Parse a composite-defining module into its static scope, memoized per build.
|
|
1508
|
+
* Returns `undefined` when the file can't be read or parsed — a decline, never
|
|
1509
|
+
* a throw: the caller falls through to the invocation arm, which will produce
|
|
1510
|
+
* its own (identical, pre-#1023) error if the module is genuinely broken.
|
|
1511
|
+
*
|
|
1512
|
+
* Reads and parses only. Nothing is imported, nothing is executed, and the
|
|
1513
|
+
* module's own imports are not resolved yet — see
|
|
1514
|
+
* {@link FactoryModuleScope.resolved}.
|
|
1515
|
+
*/
|
|
1516
|
+
function readFactoryModule(modulePath, packageRoot, session) {
|
|
1517
|
+
// chant#3247 — keyed by the package root as well: the same file read as a
|
|
1518
|
+
// project file and as package code resolves its imports differently.
|
|
1519
|
+
const key = packageRoot === undefined ? modulePath : `${packageRoot}\0${modulePath}`;
|
|
1520
|
+
const cached = session.factoryModules.get(key);
|
|
1521
|
+
if (cached)
|
|
1522
|
+
return cached;
|
|
1523
|
+
const promise = readFactoryModuleCore(modulePath, packageRoot);
|
|
1524
|
+
session.factoryModules.set(key, promise);
|
|
1525
|
+
return promise;
|
|
1526
|
+
}
|
|
1527
|
+
async function readFactoryModuleCore(modulePath, packageRoot) {
|
|
1528
|
+
let sourceFile;
|
|
1529
|
+
try {
|
|
1530
|
+
const source = await readFile(modulePath, "utf-8");
|
|
1531
|
+
sourceFile = ts.createSourceFile(modulePath, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
1532
|
+
}
|
|
1533
|
+
catch {
|
|
1534
|
+
// Not a readable TypeScript source — an installed package's compiled
|
|
1535
|
+
// entry point reached through a path specifier, a missing file. Decline.
|
|
1536
|
+
return undefined;
|
|
1537
|
+
}
|
|
1538
|
+
const collected = collectImports(sourceFile);
|
|
1539
|
+
const consts = collectConsts(sourceFile);
|
|
1540
|
+
for (const [name] of [...consts]) {
|
|
1541
|
+
if (constResolvesToResource(consts, name, new Set()))
|
|
1542
|
+
consts.delete(name);
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
file: modulePath,
|
|
1546
|
+
sourceFile,
|
|
1547
|
+
packageRoot,
|
|
1548
|
+
consts,
|
|
1549
|
+
imports: collected.named,
|
|
1550
|
+
namespaceImports: collected.namespaces,
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Resolve a composite-defining module's own imports, once per module per
|
|
1555
|
+
* build. Guarded by {@link FoldSession.stack}, exactly like
|
|
1556
|
+
* {@link foldFileMemoized}: a module already being resolved further up the
|
|
1557
|
+
* same chain is a genuine cycle, and gets an empty scope (every identifier
|
|
1558
|
+
* then reads as unresolved, which declines the interpretation) rather than a
|
|
1559
|
+
* re-entry.
|
|
1560
|
+
*/
|
|
1561
|
+
function factoryModuleScopeResolved(scope, session) {
|
|
1562
|
+
if (scope.resolved)
|
|
1563
|
+
return scope.resolved;
|
|
1564
|
+
if (session.stack.includes(scope.file) || session.stack.length >= MAX_RESOLUTION_DEPTH) {
|
|
1565
|
+
return Promise.resolve({ externals: new Map(), failures: new Map() });
|
|
1566
|
+
}
|
|
1567
|
+
session.stack.push(scope.file);
|
|
1568
|
+
const roots = scope.packageRoot === undefined ? [] : [scope.packageRoot];
|
|
1569
|
+
scope.resolved = buildExternals(scope.file, scope.imports, scope.namespaceImports, session, roots).finally(() => {
|
|
1570
|
+
const idx = session.stack.lastIndexOf(scope.file);
|
|
1571
|
+
if (idx !== -1)
|
|
1572
|
+
session.stack.splice(idx, 1);
|
|
1573
|
+
});
|
|
1574
|
+
return scope.resolved;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Find `export const <exportName> = Composite(<fn>, "<name>")` at the top level
|
|
1578
|
+
* of `scope`'s module, verifying that `Composite` really is chant's own in that
|
|
1579
|
+
* module. Returns `undefined` for every other shape — see rule 2 of the
|
|
1580
|
+
* contract above.
|
|
1581
|
+
*/
|
|
1582
|
+
function findCompositeDefinition(scope, exportName, ctx) {
|
|
1583
|
+
for (const statement of scope.sourceFile.statements) {
|
|
1584
|
+
if (!ts.isVariableStatement(statement))
|
|
1585
|
+
continue;
|
|
1586
|
+
if (!hasExportModifier(statement))
|
|
1587
|
+
continue;
|
|
1588
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
1589
|
+
continue;
|
|
1590
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1591
|
+
if (!ts.isIdentifier(decl.name) || decl.name.text !== exportName)
|
|
1592
|
+
continue;
|
|
1593
|
+
const init = decl.initializer;
|
|
1594
|
+
if (!init || !ts.isCallExpression(init) || !ts.isIdentifier(init.expression))
|
|
1595
|
+
return undefined;
|
|
1596
|
+
// `Composite` must be bound, in THIS module, to an import of chant's
|
|
1597
|
+
// own — the identical provenance question #1082 asks of an authoring
|
|
1598
|
+
// helper, answered by the identical predicate. A project-local
|
|
1599
|
+
// `function Composite(...)` shadowing the name is not chant's, and a
|
|
1600
|
+
// call to it is not a registered composite.
|
|
1601
|
+
const compositeBinding = scope.imports.get(init.expression.text);
|
|
1602
|
+
if (!compositeBinding || compositeBinding.imported !== "Composite")
|
|
1603
|
+
return undefined;
|
|
1604
|
+
// chant's own is the answer for every chant project, and #1082's
|
|
1605
|
+
// provenance question is why: a project-local `function Composite(...)`
|
|
1606
|
+
// shadowing the name is not chant's. chant#2442 adds the case chant had
|
|
1607
|
+
// never had to consider, a HOST supplying the form (`F-Host-Composite`),
|
|
1608
|
+
// which only a caller that named its own host packages can reach. For a
|
|
1609
|
+
// chant build the set holds only `@intentius/chant-lexicon-*`, and no
|
|
1610
|
+
// lexicon exports a `Composite`, so nothing about a chant project moves.
|
|
1611
|
+
const chantOwned = isChantOwnedHelperBinding(compositeBinding, { ...ctx, file: scope.file });
|
|
1612
|
+
const hostOwned = activeLexiconPackageImport(compositeBinding.specifier, scope.file, ctx.lexiconPackages) !== undefined ||
|
|
1613
|
+
activeLexiconModule(compositeBinding.specifier, scope.file, ctx.lexiconModules, ctx.resolvePathCache) !==
|
|
1614
|
+
undefined;
|
|
1615
|
+
if (!chantOwned && !hostOwned)
|
|
1616
|
+
return undefined;
|
|
1617
|
+
const [fnArg, nameArg] = init.arguments;
|
|
1618
|
+
if (!fnArg || (!ts.isArrowFunction(fnArg) && !ts.isFunctionExpression(fnArg)))
|
|
1619
|
+
return undefined;
|
|
1620
|
+
// `Composite()`'s own default when the name is omitted (../composite.ts).
|
|
1621
|
+
// COR017 requires the literal in practice; anything that is not a plain
|
|
1622
|
+
// string literal is not something to guess at.
|
|
1623
|
+
if (nameArg !== undefined && !ts.isStringLiteral(nameArg))
|
|
1624
|
+
return undefined;
|
|
1625
|
+
return { fn: fnArg, compositeName: nameArg ? nameArg.text : "anonymous", hostForm: !chantOwned };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return undefined;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* chant#3247 — how many barrel re-exports {@link findFactoryDefinition}
|
|
1632
|
+
* follows from the imported module to the one that defines the composite. A
|
|
1633
|
+
* lexicon's barrel reaches its composites in two (`src/index.ts` ->
|
|
1634
|
+
* `src/composites/index.ts` -> the file), a dialect subpath in three; past
|
|
1635
|
+
* this the lookup declines, and the composite is invoked as before.
|
|
1636
|
+
*/
|
|
1637
|
+
const MAX_REEXPORT_HOPS = 8;
|
|
1638
|
+
/** A TypeScript source file, not compiled JavaScript and not a declaration file. */
|
|
1639
|
+
function isTypeScriptSource(path) {
|
|
1640
|
+
return /\.[cm]?tsx?$/.test(path) && !/\.d\.[cm]?ts$/.test(path);
|
|
1641
|
+
}
|
|
1642
|
+
/** Process-wide memo for {@link lexiconPackageDirectory}, keyed by package name and directory. */
|
|
1643
|
+
const lexiconPackageDirectoryCache = new Map();
|
|
1644
|
+
/**
|
|
1645
|
+
* chant#3247 — the directory of package `packageName` that holds the resolved
|
|
1646
|
+
* `modulePath`: the nearest ancestor whose `package.json` names that package.
|
|
1647
|
+
* `undefined` when none does before a `node_modules` directory or the
|
|
1648
|
+
* filesystem root, which declines interpretation. Reads `package.json` files
|
|
1649
|
+
* only; nothing is resolved or imported.
|
|
1650
|
+
*/
|
|
1651
|
+
function lexiconPackageDirectory(modulePath, packageName) {
|
|
1652
|
+
const start = dirname(modulePath);
|
|
1653
|
+
const key = `${packageName}\0${start}`;
|
|
1654
|
+
if (lexiconPackageDirectoryCache.has(key))
|
|
1655
|
+
return lexiconPackageDirectoryCache.get(key);
|
|
1656
|
+
let found;
|
|
1657
|
+
for (let dir = start; basename(dir) !== "node_modules";) {
|
|
1658
|
+
const pkgJson = join(dir, "package.json");
|
|
1659
|
+
if (existsSync(pkgJson)) {
|
|
1660
|
+
try {
|
|
1661
|
+
if (JSON.parse(readFileSync(pkgJson, "utf-8")).name === packageName) {
|
|
1662
|
+
found = dir;
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
catch {
|
|
1667
|
+
// An unreadable package.json is not the package's; keep walking.
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
const parent = dirname(dir);
|
|
1671
|
+
if (parent === dir)
|
|
1672
|
+
break;
|
|
1673
|
+
dir = parent;
|
|
1674
|
+
}
|
|
1675
|
+
lexiconPackageDirectoryCache.set(key, found);
|
|
1676
|
+
return found;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Rule 1 of the contract: where an import of a composite leads, or `undefined`
|
|
1680
|
+
* when the import is not one interpretation follows. No module is imported.
|
|
1681
|
+
*
|
|
1682
|
+
* - A project import (relative, absolute, or `paths`-mapped) resolves to its
|
|
1683
|
+
* file. Inside a package composite's body ({@link ResolveCtx.packageRoot})
|
|
1684
|
+
* it must stay inside that package.
|
|
1685
|
+
* - A bare specifier naming an active lexicon package, root or subpath, by
|
|
1686
|
+
* text, resolves through `node_modules` to a TypeScript source file inside
|
|
1687
|
+
* that package.
|
|
1688
|
+
* - Anything else is not followed.
|
|
1689
|
+
*/
|
|
1690
|
+
function factoryOrigin(specifier, fromFile, ctx) {
|
|
1691
|
+
if (isProjectImport(specifier, fromFile)) {
|
|
1692
|
+
let modulePath;
|
|
1693
|
+
try {
|
|
1694
|
+
modulePath = resolveModulePathMemoized(specifier, fromFile, ctx.resolvePathCache);
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
return undefined;
|
|
1698
|
+
}
|
|
1699
|
+
if (ctx.packageRoot === undefined)
|
|
1700
|
+
return { modulePath };
|
|
1701
|
+
return isUnderDirectory(modulePath, ctx.packageRoot) ? { modulePath, packageRoot: ctx.packageRoot } : undefined;
|
|
1702
|
+
}
|
|
1703
|
+
// Text first, as {@link isTrustedExecutableBinding}'s arm 1 does: no bare
|
|
1704
|
+
// specifier outside the build's own lexicons is ever resolved here.
|
|
1705
|
+
const packageName = activeLexiconPackage(specifier, ctx.lexiconPackages) ??
|
|
1706
|
+
activeLexiconPackage(bareSpecifierPackageRoot(specifier) ?? "", ctx.lexiconPackages);
|
|
1707
|
+
if (packageName === undefined)
|
|
1708
|
+
return undefined;
|
|
1709
|
+
let modulePath;
|
|
1710
|
+
try {
|
|
1711
|
+
modulePath = resolveModulePathMemoized(specifier, fromFile, ctx.resolvePathCache);
|
|
1712
|
+
}
|
|
1713
|
+
catch {
|
|
1714
|
+
return undefined;
|
|
1715
|
+
}
|
|
1716
|
+
if (!isTypeScriptSource(modulePath))
|
|
1717
|
+
return undefined;
|
|
1718
|
+
const packageRoot = lexiconPackageDirectory(modulePath, packageName);
|
|
1719
|
+
return packageRoot === undefined ? undefined : { modulePath, packageRoot };
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* chant#3247 — the re-exports of `exportName` that `sourceFile` forwards, in the order they are worth
|
|
1723
|
+
* trying. Empty when the module declares the name itself (its own
|
|
1724
|
+
* declaration is the answer, interpretable or not) or forwards it from nowhere
|
|
1725
|
+
* this follows. An explicit `export { ... } from` wins over `export *`, as it
|
|
1726
|
+
* does in the language; a type-only re-export, an `export * as ns`, and a bare
|
|
1727
|
+
* `export { X }` of an imported binding are not followed.
|
|
1728
|
+
*/
|
|
1729
|
+
function reExportsOf(sourceFile, exportName) {
|
|
1730
|
+
const stars = [];
|
|
1731
|
+
for (const statement of sourceFile.statements) {
|
|
1732
|
+
if (ts.isExportDeclaration(statement)) {
|
|
1733
|
+
if (statement.isTypeOnly)
|
|
1734
|
+
continue;
|
|
1735
|
+
const spec = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
|
|
1736
|
+
? statement.moduleSpecifier.text
|
|
1737
|
+
: undefined;
|
|
1738
|
+
const clause = statement.exportClause;
|
|
1739
|
+
if (clause === undefined) {
|
|
1740
|
+
if (spec !== undefined)
|
|
1741
|
+
stars.push({ specifier: spec, imported: exportName, star: true });
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
if (!ts.isNamedExports(clause)) {
|
|
1745
|
+
if (clause.name.text === exportName)
|
|
1746
|
+
return [];
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
for (const el of clause.elements) {
|
|
1750
|
+
if (el.name.text !== exportName)
|
|
1751
|
+
continue;
|
|
1752
|
+
if (spec === undefined || el.isTypeOnly)
|
|
1753
|
+
return [];
|
|
1754
|
+
return [{ specifier: spec, imported: (el.propertyName ?? el.name).text, star: false }];
|
|
1755
|
+
}
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
if (!hasExportModifier(statement))
|
|
1759
|
+
continue;
|
|
1760
|
+
if (ts.isVariableStatement(statement)) {
|
|
1761
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1762
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === exportName)
|
|
1763
|
+
return [];
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
else if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement)) &&
|
|
1767
|
+
statement.name?.text === exportName) {
|
|
1768
|
+
return [];
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return stars;
|
|
1772
|
+
}
|
|
1773
|
+
/** A text pre-check for an `export *` target: false only when the file cannot export `name`. */
|
|
1774
|
+
async function mayProvideExport(file, name) {
|
|
1775
|
+
try {
|
|
1776
|
+
const text = await readFile(file, "utf-8");
|
|
1777
|
+
return text.includes(name) || /export\s*\*\s*from/.test(text);
|
|
1778
|
+
}
|
|
1779
|
+
catch {
|
|
1780
|
+
return false;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Rule 2, through barrels: find `exportName`'s `Composite(...)` declaration
|
|
1785
|
+
* starting at `origin` and following {@link reExportsOf} — relative
|
|
1786
|
+
* specifiers only, confined to `origin.packageRoot` when there is one, cycle
|
|
1787
|
+
* safe, and at most {@link MAX_REEXPORT_HOPS} deep. Reads and parses; imports
|
|
1788
|
+
* nothing.
|
|
1789
|
+
*/
|
|
1790
|
+
async function findFactoryDefinition(origin, exportName, ctx, seen = new Set(), hops = 0) {
|
|
1791
|
+
seen.add(origin.modulePath);
|
|
1792
|
+
const scope = await readFactoryModule(origin.modulePath, origin.packageRoot, ctx.session);
|
|
1793
|
+
if (!scope)
|
|
1794
|
+
return undefined;
|
|
1795
|
+
const definition = findCompositeDefinition(scope, exportName, ctx);
|
|
1796
|
+
if (definition)
|
|
1797
|
+
return { scope, definition };
|
|
1798
|
+
if (hops >= MAX_REEXPORT_HOPS)
|
|
1799
|
+
return undefined;
|
|
1800
|
+
for (const { specifier, imported, star } of reExportsOf(scope.sourceFile, exportName)) {
|
|
1801
|
+
if (!isProjectFileSpecifier(specifier))
|
|
1802
|
+
continue;
|
|
1803
|
+
let target;
|
|
1804
|
+
try {
|
|
1805
|
+
target = resolveModulePathMemoized(specifier, scope.file, ctx.resolvePathCache);
|
|
1806
|
+
}
|
|
1807
|
+
catch {
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (seen.has(target) || !isTypeScriptSource(target))
|
|
1811
|
+
continue;
|
|
1812
|
+
if (origin.packageRoot !== undefined && !isUnderDirectory(target, origin.packageRoot))
|
|
1813
|
+
continue;
|
|
1814
|
+
// An `export *` target that neither names the export nor forwards a star
|
|
1815
|
+
// of its own cannot provide it, so it is not parsed. That keeps a
|
|
1816
|
+
// lexicon's generated barrel (megabytes, behind `export *` in most
|
|
1817
|
+
// lexicons) from being parsed for every call through the package root.
|
|
1818
|
+
if (star && !(await mayProvideExport(target, imported)))
|
|
1819
|
+
continue;
|
|
1820
|
+
const found = await findFactoryDefinition({ modulePath: target, packageRoot: origin.packageRoot }, imported, ctx, seen, hops + 1);
|
|
1821
|
+
if (found)
|
|
1822
|
+
return found;
|
|
1823
|
+
}
|
|
1824
|
+
return undefined;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Decide whether `binding`'s callee is an interpretable composite — rules 1-5
|
|
1828
|
+
* of the contract above, in that order, with no evaluation and no import.
|
|
1829
|
+
* `undefined` means "not interpretable", never "broken".
|
|
1830
|
+
*/
|
|
1831
|
+
async function resolveInterpretableFactory(binding, ctx) {
|
|
1832
|
+
// Rule 1 — a project file, or an active lexicon package's TypeScript source.
|
|
1833
|
+
// A bare specifier outside the build's lexicons is refused on its text.
|
|
1834
|
+
const origin = factoryOrigin(binding.specifier, ctx.file, ctx);
|
|
1835
|
+
if (!origin)
|
|
1836
|
+
return undefined;
|
|
1837
|
+
// Not `undefined`: that answer means "not interpretable" and would invoke
|
|
1838
|
+
// the factory while still reporting the file folded. Giving up is a
|
|
1839
|
+
// different fact and falls the file back to run, naming the bound (#2370).
|
|
1840
|
+
if (ctx.interpretDepth >= MAX_INTERPRETATION_DEPTH) {
|
|
1841
|
+
throw new InterpretationDepthError(`interpretation depth exceeded ${MAX_INTERPRETATION_DEPTH} at "${binding.imported}" — a composite ` +
|
|
1842
|
+
"chain this deep is almost certainly recursive. Falling back to run rather than invoking the " +
|
|
1843
|
+
"factory, which would have reported this file as folded while evaluating it a different way.", 0, 0);
|
|
1844
|
+
}
|
|
1845
|
+
// Rule 2, following barrel re-exports to the defining module.
|
|
1846
|
+
const found = await findFactoryDefinition(origin, binding.imported, ctx);
|
|
1847
|
+
if (!found)
|
|
1848
|
+
return undefined;
|
|
1849
|
+
const { scope, definition } = found;
|
|
1850
|
+
// Rules 3-5.
|
|
1851
|
+
if (findFactorySubsetViolation(definition.fn) !== undefined)
|
|
1852
|
+
return undefined;
|
|
1853
|
+
return { scope, fn: definition.fn, compositeName: definition.compositeName, hostForm: definition.hostForm };
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* The SHAPE half of the contract (rules 3-5) — a lone `ts` node in, a reason
|
|
1857
|
+
* string out, or `undefined` when the factory is admissible. Deliberately
|
|
1858
|
+
* takes nothing but the node: it is the half a caller with no module graph
|
|
1859
|
+
* could evaluate, and keeping it separable is what lets the doc above claim
|
|
1860
|
+
* the shape rules are checkable without the provenance ones.
|
|
1861
|
+
*/
|
|
1862
|
+
export function findFactorySubsetViolation(fn) {
|
|
1863
|
+
// Rule 3 — at most one parameter, bound plainly.
|
|
1864
|
+
if (fn.parameters.length > 1)
|
|
1865
|
+
return "a composite factory takes a single props parameter";
|
|
1866
|
+
const param = fn.parameters[0];
|
|
1867
|
+
if (param) {
|
|
1868
|
+
if (param.dotDotDotToken)
|
|
1869
|
+
return "a rest parameter is not interpretable";
|
|
1870
|
+
if (param.initializer)
|
|
1871
|
+
return "a defaulted parameter is not interpretable";
|
|
1872
|
+
if (ts.isObjectBindingPattern(param.name)) {
|
|
1873
|
+
for (const el of param.name.elements) {
|
|
1874
|
+
if (bindingElementPropKey(el) === undefined) {
|
|
1875
|
+
return "a destructured props parameter with a rest, default, or nested element is not interpretable";
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
else if (!ts.isIdentifier(param.name)) {
|
|
1880
|
+
return "an array-destructured props parameter is not interpretable";
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
// Rule 4 — a concise expression body, or `const`s then one `return`.
|
|
1884
|
+
if (!ts.isBlock(fn.body))
|
|
1885
|
+
return checkFactoryExpression(fn.body);
|
|
1886
|
+
const statements = fn.body.statements;
|
|
1887
|
+
if (statements.length === 0)
|
|
1888
|
+
return "an empty composite factory body has no members to interpret";
|
|
1889
|
+
for (let i = 0; i < statements.length; i += 1) {
|
|
1890
|
+
const statement = statements[i];
|
|
1891
|
+
const last = i === statements.length - 1;
|
|
1892
|
+
if (ts.isReturnStatement(statement)) {
|
|
1893
|
+
if (!last)
|
|
1894
|
+
return "an early `return` is not interpretable";
|
|
1895
|
+
if (!statement.expression)
|
|
1896
|
+
return "a composite factory must return its members";
|
|
1897
|
+
const violation = checkFactoryExpression(statement.expression);
|
|
1898
|
+
if (violation)
|
|
1899
|
+
return violation;
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
if (last)
|
|
1903
|
+
return "a composite factory body must end in `return`";
|
|
1904
|
+
if (!ts.isVariableStatement(statement)) {
|
|
1905
|
+
return `\`${ts.SyntaxKind[statement.kind]}\` in a composite factory body is not interpretable`;
|
|
1906
|
+
}
|
|
1907
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
|
|
1908
|
+
return "`let`/`var` in a composite factory body is not interpretable";
|
|
1909
|
+
}
|
|
1910
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1911
|
+
if (!decl.initializer)
|
|
1912
|
+
return "an uninitialized `const` in a composite factory body is not interpretable";
|
|
1913
|
+
if (ts.isObjectBindingPattern(decl.name)) {
|
|
1914
|
+
for (const el of decl.name.elements) {
|
|
1915
|
+
if (bindingElementPropKey(el) === undefined) {
|
|
1916
|
+
return "a destructured `const` with a rest, default, or nested element is not interpretable";
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
else if (!ts.isIdentifier(decl.name)) {
|
|
1921
|
+
return "an array-destructured `const` is not interpretable";
|
|
1922
|
+
}
|
|
1923
|
+
const violation = checkFactoryExpression(decl.initializer);
|
|
1924
|
+
if (violation)
|
|
1925
|
+
return violation;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return undefined;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Rule 5 — the fold subset, extended with `new Type(...)` in any value
|
|
1932
|
+
* position and a call through a bare identifier.
|
|
1933
|
+
*
|
|
1934
|
+
* Shape only, in ../fold/subset.ts's sense: whether a `new`'s constructor or a
|
|
1935
|
+
* call's callee actually resolves to something invocable is settled at
|
|
1936
|
+
* evaluation, where the module graph is available. Every operator/key/member
|
|
1937
|
+
* rule is read from ../fold/subset.ts's own exported sets rather than
|
|
1938
|
+
* re-listed, so widening `fold()` widens this in the same commit.
|
|
1939
|
+
*/
|
|
1940
|
+
function checkFactoryExpression(node) {
|
|
1941
|
+
if (ts.isParenthesizedExpression(node) ||
|
|
1942
|
+
ts.isAsExpression(node) ||
|
|
1943
|
+
ts.isSatisfiesExpression(node) ||
|
|
1944
|
+
ts.isNonNullExpression(node) ||
|
|
1945
|
+
ts.isTypeAssertionExpression(node)) {
|
|
1946
|
+
return checkFactoryExpression(node.expression);
|
|
1947
|
+
}
|
|
1948
|
+
if (ts.isStringLiteral(node) ||
|
|
1949
|
+
ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
1950
|
+
ts.isNumericLiteral(node) ||
|
|
1951
|
+
ts.isIdentifier(node) ||
|
|
1952
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
1953
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
1954
|
+
node.kind === ts.SyntaxKind.NullKeyword) {
|
|
1955
|
+
return undefined;
|
|
1956
|
+
}
|
|
1957
|
+
if (ts.isNewExpression(node) || ts.isCallExpression(node)) {
|
|
1958
|
+
// A bare-identifier callee only — `ns.Foo(...)`, `arr.map(...)` and
|
|
1959
|
+
// `props.factory(...)` are all property accesses and all stay out, which
|
|
1960
|
+
// is what keeps a data transform (chant EVL010) from sneaking in.
|
|
1961
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
1962
|
+
return ts.isNewExpression(node)
|
|
1963
|
+
? `\`new ${briefNodeText(node.expression)}(...)\` needs a plain imported constructor to interpret`
|
|
1964
|
+
: callExpressionMessage(node);
|
|
1965
|
+
}
|
|
1966
|
+
for (const arg of node.arguments ?? []) {
|
|
1967
|
+
const violation = checkFactoryExpression(arg);
|
|
1968
|
+
if (violation)
|
|
1969
|
+
return violation;
|
|
1970
|
+
}
|
|
1971
|
+
return undefined;
|
|
1972
|
+
}
|
|
1973
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
1974
|
+
if (!ts.isIdentifier(node.tag))
|
|
1975
|
+
return unsupportedExpressionMessage(node);
|
|
1976
|
+
if (ts.isNoSubstitutionTemplateLiteral(node.template))
|
|
1977
|
+
return undefined;
|
|
1978
|
+
for (const span of node.template.templateSpans) {
|
|
1979
|
+
const violation = checkFactoryExpression(span.expression);
|
|
1980
|
+
if (violation)
|
|
1981
|
+
return violation;
|
|
1982
|
+
}
|
|
1983
|
+
return undefined;
|
|
1984
|
+
}
|
|
1985
|
+
if (ts.isTemplateExpression(node)) {
|
|
1986
|
+
for (const span of node.templateSpans) {
|
|
1987
|
+
const violation = checkFactoryExpression(span.expression);
|
|
1988
|
+
if (violation)
|
|
1989
|
+
return violation;
|
|
1990
|
+
}
|
|
1991
|
+
return undefined;
|
|
1992
|
+
}
|
|
1993
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1994
|
+
for (const prop of node.properties) {
|
|
1995
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
1996
|
+
if (!isLiteralPropertyNameNode(prop.name))
|
|
1997
|
+
return computedPropertyNameMessage(prop.name);
|
|
1998
|
+
const violation = checkFactoryExpression(prop.initializer);
|
|
1999
|
+
if (violation)
|
|
2000
|
+
return violation;
|
|
2001
|
+
}
|
|
2002
|
+
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
2003
|
+
if (prop.objectAssignmentInitializer)
|
|
2004
|
+
return UNSUPPORTED_OBJECT_MEMBER_MESSAGE;
|
|
2005
|
+
}
|
|
2006
|
+
else if (ts.isSpreadAssignment(prop)) {
|
|
2007
|
+
const violation = checkFactoryExpression(prop.expression);
|
|
2008
|
+
if (violation)
|
|
2009
|
+
return violation;
|
|
2010
|
+
}
|
|
2011
|
+
else {
|
|
2012
|
+
return UNSUPPORTED_OBJECT_MEMBER_MESSAGE;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return undefined;
|
|
2016
|
+
}
|
|
2017
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
2018
|
+
for (const el of node.elements) {
|
|
2019
|
+
const violation = checkFactoryExpression(ts.isSpreadElement(el) ? el.expression : el);
|
|
2020
|
+
if (violation)
|
|
2021
|
+
return violation;
|
|
2022
|
+
}
|
|
2023
|
+
return undefined;
|
|
2024
|
+
}
|
|
2025
|
+
if (ts.isPropertyAccessExpression(node))
|
|
2026
|
+
return checkFactoryExpression(node.expression);
|
|
2027
|
+
if (ts.isElementAccessExpression(node)) {
|
|
2028
|
+
if (!isLiteralElementKey(node.argumentExpression)) {
|
|
2029
|
+
return `dynamic element access [${briefNodeText(node.argumentExpression)}] is not interpretable`;
|
|
2030
|
+
}
|
|
2031
|
+
return checkFactoryExpression(node.expression);
|
|
2032
|
+
}
|
|
2033
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
2034
|
+
if (!SUPPORTED_UNARY_OPERATORS.has(node.operator))
|
|
2035
|
+
return UNSUPPORTED_UNARY_MESSAGE;
|
|
2036
|
+
return checkFactoryExpression(node.operand);
|
|
2037
|
+
}
|
|
2038
|
+
if (ts.isBinaryExpression(node)) {
|
|
2039
|
+
if (!SUPPORTED_BINARY_OPERATORS.has(node.operatorToken.kind)) {
|
|
2040
|
+
return unsupportedBinaryMessage(node.operatorToken.kind);
|
|
2041
|
+
}
|
|
2042
|
+
return checkFactoryExpression(node.left) ?? checkFactoryExpression(node.right);
|
|
2043
|
+
}
|
|
2044
|
+
if (ts.isConditionalExpression(node)) {
|
|
2045
|
+
return (checkFactoryExpression(node.condition) ??
|
|
2046
|
+
checkFactoryExpression(node.whenTrue) ??
|
|
2047
|
+
checkFactoryExpression(node.whenFalse));
|
|
2048
|
+
}
|
|
2049
|
+
return unsupportedExpressionMessage(node);
|
|
2050
|
+
}
|
|
2051
|
+
/** `isLiteralPropertyName` narrowed to the node type {@link propName} accepts, without importing the type predicate's generic form twice. */
|
|
2052
|
+
function isLiteralPropertyNameNode(node) {
|
|
2053
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node);
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* chant #2161 — how one admissible factory's parameter is bound in its body,
|
|
2057
|
+
* plus the consts an attribution walk has to follow.
|
|
2058
|
+
*
|
|
2059
|
+
* Built from the SOURCE, not from the argument value: the question is which
|
|
2060
|
+
* expression a member's property was written as, not what it evaluated to. A
|
|
2061
|
+
* factory with no parameter at all still gets a recorder, so every path in its
|
|
2062
|
+
* members records as a literal the composite fixes, which is exactly what a
|
|
2063
|
+
* parameterless composite does.
|
|
2064
|
+
*/
|
|
2065
|
+
function compositeParamRecorder(factory, moduleConsts, call) {
|
|
2066
|
+
const whole = new Set();
|
|
2067
|
+
const destructured = new Map();
|
|
2068
|
+
const param = factory.fn.parameters[0];
|
|
2069
|
+
if (param) {
|
|
2070
|
+
if (ts.isIdentifier(param.name)) {
|
|
2071
|
+
whole.add(param.name.text);
|
|
2072
|
+
}
|
|
2073
|
+
else if (ts.isObjectBindingPattern(param.name)) {
|
|
2074
|
+
for (const el of param.name.elements) {
|
|
2075
|
+
const key = bindingElementPropKey(el);
|
|
2076
|
+
if (key !== undefined && ts.isIdentifier(el.name))
|
|
2077
|
+
destructured.set(el.name.text, key);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
// Body `const`s layered over the defining module's, so a hoisted value is
|
|
2082
|
+
// followed. `interpretCompositeFactory` deletes shadowed names from the
|
|
2083
|
+
// context's own const map as it binds values; this map is separate precisely
|
|
2084
|
+
// so the INITIALIZER stays available to the attribution walk.
|
|
2085
|
+
const consts = new Map(moduleConsts);
|
|
2086
|
+
const body = factory.fn.body;
|
|
2087
|
+
if (ts.isBlock(body)) {
|
|
2088
|
+
for (const statement of body.statements) {
|
|
2089
|
+
if (!ts.isVariableStatement(statement))
|
|
2090
|
+
continue;
|
|
2091
|
+
for (const decl of statement.declarationList.declarations) {
|
|
2092
|
+
if (ts.isIdentifier(decl.name) && decl.initializer)
|
|
2093
|
+
consts.set(decl.name.text, decl.initializer);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return { composite: factory.compositeName, scope: { whole, destructured }, consts, ...(call ? { call } : {}) };
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* chant #2161 — record, for one `new Type({...})` written inside an interpreted
|
|
2101
|
+
* factory body, which factory parameter produced each of its property paths and
|
|
2102
|
+
* which paths the composite fixes.
|
|
2103
|
+
*
|
|
2104
|
+
* Best-effort and additive, exactly like {@link stampParamDependencies}: a
|
|
2105
|
+
* constructor called with no object literal records nothing, and
|
|
2106
|
+
* `setPathProvenance` keeps the first (innermost) writer.
|
|
2107
|
+
*/
|
|
2108
|
+
function stampCompositeOrigins(entity, node, ctx) {
|
|
2109
|
+
const recorder = ctx.compositeParams;
|
|
2110
|
+
if (!recorder)
|
|
2111
|
+
return;
|
|
2112
|
+
if (typeof entity !== "object" || entity === null)
|
|
2113
|
+
return;
|
|
2114
|
+
let propsArg;
|
|
2115
|
+
for (const argument of node.arguments ?? []) {
|
|
2116
|
+
if (ts.isObjectLiteralExpression(argument)) {
|
|
2117
|
+
propsArg = argument;
|
|
2118
|
+
break;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (!propsArg)
|
|
2122
|
+
return;
|
|
2123
|
+
stampCompositeCall(entity, recorder);
|
|
2124
|
+
const origins = collectCompositeOrigins(propsArg, recorder.consts, recorder.scope, recorder.composite, recorder.call);
|
|
2125
|
+
for (const [path, origin] of Object.entries(origins))
|
|
2126
|
+
setPathProvenance(entity, path, origin);
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* chant #3597 — record where the composite call that expanded `entity` was
|
|
2130
|
+
* written, beside the per-path origins and in the same pass. First writer
|
|
2131
|
+
* wins, as for every provenance field, so the innermost call is kept.
|
|
2132
|
+
*/
|
|
2133
|
+
function stampCompositeCall(entity, recorder) {
|
|
2134
|
+
if (!recorder.call)
|
|
2135
|
+
return;
|
|
2136
|
+
setProvenance(entity, { compositeCall: sourceLocationOf(recorder.call.file, recorder.call.node) });
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* chant #3212 — the same record for an entity a registered tag built inside an
|
|
2140
|
+
* interpreted factory body (`` table`CREATE TABLE ${props.name} ...` ``).
|
|
2141
|
+
*
|
|
2142
|
+
* A tag's props come from parsing its text, so no object literal says which
|
|
2143
|
+
* field each `${...}` landed in. The tag says so itself through
|
|
2144
|
+
* `setInterpolationFields`; a tag that does not leaves the entity's fields
|
|
2145
|
+
* `unknown`, as before.
|
|
2146
|
+
*/
|
|
2147
|
+
function stampTagOrigins(entity, node, ctx) {
|
|
2148
|
+
const recorder = ctx.compositeParams;
|
|
2149
|
+
if (!recorder || !isDeclarable(entity) || !isResourceDeclarable(entity))
|
|
2150
|
+
return;
|
|
2151
|
+
const fields = getInterpolationFields(entity);
|
|
2152
|
+
if (!fields)
|
|
2153
|
+
return;
|
|
2154
|
+
const interpolations = ts.isTemplateExpression(node.template)
|
|
2155
|
+
? node.template.templateSpans.map((span) => span.expression)
|
|
2156
|
+
: [];
|
|
2157
|
+
stampCompositeCall(entity, recorder);
|
|
2158
|
+
const origins = collectTagOrigins(interpolations, fields, entity.props, recorder.consts, recorder.scope, recorder.composite, recorder.call);
|
|
2159
|
+
for (const [path, origin] of Object.entries(origins))
|
|
2160
|
+
setPathProvenance(entity, path, origin);
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Interpret an admissible factory's body against `args`, and assemble the
|
|
2164
|
+
* result through chant's own {@link Composite}.
|
|
2165
|
+
*
|
|
2166
|
+
* Returns `undefined` — a DECLINE, not a failure — when anything in the body
|
|
2167
|
+
* fails to resolve (an identifier the defining module's imports don't reach, a
|
|
2168
|
+
* constructor `--sandbox` refuses, a nested call that isn't a composite). The
|
|
2169
|
+
* caller then invokes for real, landing exactly where it landed before #1023.
|
|
2170
|
+
* That asymmetry is deliberate: interpretation may only ever REMOVE an
|
|
2171
|
+
* execution, never introduce a fold failure that wasn't there.
|
|
2172
|
+
*/
|
|
2173
|
+
async function interpretCompositeFactory(factory, args, ctx, call) {
|
|
2174
|
+
// A `Composite()` factory takes exactly one props argument. Anything else at
|
|
2175
|
+
// the call site means the callee is being used as something this doesn't
|
|
2176
|
+
// model.
|
|
2177
|
+
if (args.length > 1)
|
|
2178
|
+
return undefined;
|
|
2179
|
+
const { scope, fn } = factory;
|
|
2180
|
+
const resolved = await factoryModuleScopeResolved(scope, ctx.session);
|
|
2181
|
+
// The defining module's own scope, with the factory's locals layered on top.
|
|
2182
|
+
// `locals` is deliberately EMPTY: `resolveLiveValue`'s local-binding branch
|
|
2183
|
+
// exists to re-navigate a file's own top-level composite spine, which is not
|
|
2184
|
+
// what a name inside a factory body means. Body bindings live in `externals`
|
|
2185
|
+
// instead, where an identifier resolves to the value already computed for it
|
|
2186
|
+
// — which is what makes `role.Arn` a live `AttrRef` on the real instance.
|
|
2187
|
+
const consts = new Map(scope.consts);
|
|
2188
|
+
const externals = new Map(resolved.externals);
|
|
2189
|
+
const bodyCtx = {
|
|
2190
|
+
file: scope.file,
|
|
2191
|
+
consts,
|
|
2192
|
+
locals: new Map(),
|
|
2193
|
+
imports: scope.imports,
|
|
2194
|
+
namespaceImports: scope.namespaceImports,
|
|
2195
|
+
memo: new Map(),
|
|
2196
|
+
intrinsics: ctx.intrinsics,
|
|
2197
|
+
externals,
|
|
2198
|
+
crossFileFailures: resolved.failures,
|
|
2199
|
+
importCache: ctx.importCache,
|
|
2200
|
+
resolvePathCache: ctx.resolvePathCache,
|
|
2201
|
+
lexiconPackages: ctx.lexiconPackages,
|
|
2202
|
+
lexiconModules: ctx.lexiconModules,
|
|
2203
|
+
// chant#3247 — a package's composite trusts its own package directory for
|
|
2204
|
+
// the length of its body, and nothing outside it.
|
|
2205
|
+
lexiconRoots: scope.packageRoot === undefined ? ctx.lexiconRoots : [...ctx.lexiconRoots, scope.packageRoot],
|
|
2206
|
+
packageRoot: scope.packageRoot,
|
|
2207
|
+
sandbox: ctx.sandbox,
|
|
2208
|
+
executing: ctx.executing,
|
|
2209
|
+
session: ctx.session,
|
|
2210
|
+
interpretDepth: ctx.interpretDepth + 1,
|
|
2211
|
+
// chant #2161 — set unconditionally, and NOT inherited from `ctx`: a nested
|
|
2212
|
+
// composite records its own parameters, never the enclosing one's.
|
|
2213
|
+
compositeParams: compositeParamRecorder(factory, scope.consts, call),
|
|
2214
|
+
};
|
|
2215
|
+
const bind = (name, value) => {
|
|
2216
|
+
// A body binding SHADOWS a module-level const of the same name, so the
|
|
2217
|
+
// const has to go — `fold()` consults `consts` before `externals`.
|
|
2218
|
+
consts.delete(name);
|
|
2219
|
+
externals.set(name, value);
|
|
2220
|
+
};
|
|
2221
|
+
try {
|
|
2222
|
+
const param = fn.parameters[0];
|
|
2223
|
+
if (param) {
|
|
2224
|
+
const props = args[0];
|
|
2225
|
+
if (ts.isIdentifier(param.name)) {
|
|
2226
|
+
bind(param.name.text, props);
|
|
2227
|
+
}
|
|
2228
|
+
else if (ts.isObjectBindingPattern(param.name)) {
|
|
2229
|
+
if (!isIndexableObject(props))
|
|
2230
|
+
return undefined;
|
|
2231
|
+
for (const el of param.name.elements) {
|
|
2232
|
+
const key = bindingElementPropKey(el);
|
|
2233
|
+
if (key === undefined)
|
|
2234
|
+
return undefined;
|
|
2235
|
+
bind(el.name.getText(), props[key]);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const members = await interpretFactoryBody(fn, bodyCtx, bind);
|
|
2240
|
+
if (!isIndexableObject(members))
|
|
2241
|
+
return undefined;
|
|
2242
|
+
// Assembled by chant's own `Composite()` — see the contract's "what is
|
|
2243
|
+
// preserved" note. One definition object per interpreted call (rather than
|
|
2244
|
+
// one per module, as the run path has) is the single visible difference:
|
|
2245
|
+
// `_id` is a fresh symbol, which nothing outside `CompositeRegistry` — used
|
|
2246
|
+
// only by tests — reads.
|
|
2247
|
+
// chant#2442 — a HOST's registration form produces the host's own members,
|
|
2248
|
+
// so they are returned as the factory built them. Wrapping them in chant's
|
|
2249
|
+
// `Composite` would run chant's member validation, which asks for chant's
|
|
2250
|
+
// own `Declarable` and rejects an entity carrying a different marker —
|
|
2251
|
+
// rejecting it for not being chant's rather than for being malformed.
|
|
2252
|
+
// Chant's own form still goes through `Composite`, unchanged.
|
|
2253
|
+
const value = factory.hostForm
|
|
2254
|
+
? markHostCompositeInstance(members, factory.compositeName)
|
|
2255
|
+
: (Composite(() => members, factory.compositeName))();
|
|
2256
|
+
executionCounts.factoryInterpretations += 1;
|
|
2257
|
+
return { value };
|
|
2258
|
+
}
|
|
2259
|
+
catch (err) {
|
|
2260
|
+
// Every other failure here means "this body did not interpret" and the
|
|
2261
|
+
// caller invokes instead. Exhausting the depth bound means "we stopped
|
|
2262
|
+
// looking", which has to reach the file's fold verdict rather than be
|
|
2263
|
+
// answered with an invocation (#2370).
|
|
2264
|
+
if (err instanceof InterpretationDepthError)
|
|
2265
|
+
throw err;
|
|
2266
|
+
return undefined;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
/** Evaluate an admissible factory body's statements in order, binding each `const`, and return the `return` expression's value. */
|
|
2270
|
+
async function interpretFactoryBody(fn, ctx, bind) {
|
|
2271
|
+
if (!ts.isBlock(fn.body))
|
|
2272
|
+
return interpretExpression(fn.body, ctx);
|
|
2273
|
+
for (const statement of fn.body.statements) {
|
|
2274
|
+
if (ts.isReturnStatement(statement)) {
|
|
2275
|
+
// `findFactorySubsetViolation` has already established this is the last
|
|
2276
|
+
// statement and has an expression.
|
|
2277
|
+
return interpretExpression(statement.expression, ctx);
|
|
2278
|
+
}
|
|
2279
|
+
const declarations = statement.declarationList.declarations;
|
|
2280
|
+
for (const decl of declarations) {
|
|
2281
|
+
const value = await interpretExpression(decl.initializer, ctx);
|
|
2282
|
+
if (ts.isIdentifier(decl.name)) {
|
|
2283
|
+
bind(decl.name.text, value);
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
if (!isIndexableObject(value)) {
|
|
2287
|
+
throw cheapError(`destructured \`const\` source in "${briefNodeText(decl.name)}" is not an object`);
|
|
2288
|
+
}
|
|
2289
|
+
for (const el of decl.name.elements) {
|
|
2290
|
+
bind(el.name.getText(), value[bindingElementPropKey(el)]);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
// Unreachable: rule 4 requires a trailing `return`.
|
|
2295
|
+
throw cheapError("composite factory body did not return");
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Evaluate one expression of a factory body.
|
|
2299
|
+
*
|
|
2300
|
+
* Only the cases that can CONTAIN a construction or a composite call are
|
|
2301
|
+
* handled here; everything else delegates to {@link resolveDeclaratorValue},
|
|
2302
|
+
* which is the same `resolveLiveValue` -> `fold()` -> revive pipeline every
|
|
2303
|
+
* other value in a fold takes. That split is the point: the interpreter owns
|
|
2304
|
+
* as little evaluation semantics as it possibly can, so `fold()` stays the one
|
|
2305
|
+
* definition of what an expression means, and the operators/short-circuiting
|
|
2306
|
+
* duplicated below are duplicated because they must not evaluate a branch
|
|
2307
|
+
* `fold()` would not have evaluated.
|
|
2308
|
+
*/
|
|
2309
|
+
async function interpretExpression(node, ctx) {
|
|
2310
|
+
if (ts.isParenthesizedExpression(node) ||
|
|
2311
|
+
ts.isAsExpression(node) ||
|
|
2312
|
+
ts.isSatisfiesExpression(node) ||
|
|
2313
|
+
ts.isNonNullExpression(node)) {
|
|
2314
|
+
return interpretExpression(node.expression, ctx);
|
|
2315
|
+
}
|
|
2316
|
+
if (ts.isNewExpression(node))
|
|
2317
|
+
return interpretNewExpression(node, ctx);
|
|
2318
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
2319
|
+
const value = (await resolveDeclaratorValue(node, ctx)).value;
|
|
2320
|
+
stampTagOrigins(value, node, ctx);
|
|
2321
|
+
return value;
|
|
2322
|
+
}
|
|
2323
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
2324
|
+
const obj = {};
|
|
2325
|
+
for (const prop of node.properties) {
|
|
2326
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
2327
|
+
obj[propName(prop.name)] = await interpretExpression(prop.initializer, ctx);
|
|
2328
|
+
}
|
|
2329
|
+
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
2330
|
+
obj[prop.name.text] = await interpretExpression(prop.name, ctx);
|
|
2331
|
+
}
|
|
2332
|
+
else {
|
|
2333
|
+
const src = await interpretExpression(prop.expression, ctx);
|
|
2334
|
+
if (src === null || typeof src !== "object")
|
|
2335
|
+
throw cheapError("spread source not an object");
|
|
2336
|
+
Object.assign(obj, src);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return obj;
|
|
2340
|
+
}
|
|
2341
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
2342
|
+
const arr = [];
|
|
2343
|
+
for (const el of node.elements) {
|
|
2344
|
+
if (ts.isSpreadElement(el)) {
|
|
2345
|
+
const src = await interpretExpression(el.expression, ctx);
|
|
2346
|
+
if (!Array.isArray(src))
|
|
2347
|
+
throw cheapError("spread source not an array");
|
|
2348
|
+
arr.push(...src);
|
|
2349
|
+
}
|
|
2350
|
+
else {
|
|
2351
|
+
arr.push(await interpretExpression(el, ctx));
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return arr;
|
|
2355
|
+
}
|
|
2356
|
+
if (ts.isConditionalExpression(node)) {
|
|
2357
|
+
return (await interpretExpression(node.condition, ctx))
|
|
2358
|
+
? interpretExpression(node.whenTrue, ctx)
|
|
2359
|
+
: interpretExpression(node.whenFalse, ctx);
|
|
2360
|
+
}
|
|
2361
|
+
if (ts.isBinaryExpression(node)) {
|
|
2362
|
+
const S = ts.SyntaxKind;
|
|
2363
|
+
const opKind = node.operatorToken.kind;
|
|
2364
|
+
if (opKind === S.AmpersandAmpersandToken) {
|
|
2365
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2366
|
+
return left ? interpretExpression(node.right, ctx) : left;
|
|
2367
|
+
}
|
|
2368
|
+
if (opKind === S.BarBarToken) {
|
|
2369
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2370
|
+
return left ? left : interpretExpression(node.right, ctx);
|
|
2371
|
+
}
|
|
2372
|
+
if (opKind === S.QuestionQuestionToken) {
|
|
2373
|
+
const left = await interpretExpression(node.left, ctx);
|
|
2374
|
+
return left === null || left === undefined ? interpretExpression(node.right, ctx) : left;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return (await resolveDeclaratorValue(node, ctx)).value;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Construct a real resource from a `new Type(...)` anywhere inside a factory
|
|
2381
|
+
* body — a member, a nested property object, an array element.
|
|
2382
|
+
*
|
|
2383
|
+
* Deliberately NOT routed through {@link resolveResourceEntity}: a factory body
|
|
2384
|
+
* evaluates against the DEFINING module's scope with the caller's props already
|
|
2385
|
+
* bound to live values, so each argument is interpreted recursively (through
|
|
2386
|
+
* {@link interpretExpression}, which can produce a live composite instance a
|
|
2387
|
+
* plain `fold()` has no representation for) rather than folded and revived.
|
|
2388
|
+
*
|
|
2389
|
+
* chant #1169 removed the asymmetry that used to motivate this comment: a
|
|
2390
|
+
* nested `new` in a TOP-LEVEL value position now constructs too, via
|
|
2391
|
+
* {@link constructFoldedResource}. The two paths reach the same place — the
|
|
2392
|
+
* class named by an `import`, called with the arguments the source wrote — by
|
|
2393
|
+
* different routes, because a factory body and a file's own top level start
|
|
2394
|
+
* from different scopes.
|
|
2395
|
+
*/
|
|
2396
|
+
async function interpretNewExpression(node, ctx) {
|
|
2397
|
+
// Guaranteed an identifier by {@link checkFactoryExpression}; re-checked so a
|
|
2398
|
+
// future caller can't reach this with a dotted callee and get a silent miss.
|
|
2399
|
+
if (!ts.isIdentifier(node.expression)) {
|
|
2400
|
+
throw cheapError(`\`new ${briefNodeText(node.expression)}(...)\` needs a plain imported constructor`);
|
|
2401
|
+
}
|
|
2402
|
+
const typeName = node.expression.text;
|
|
2403
|
+
const binding = ctx.imports.get(typeName);
|
|
2404
|
+
if (!binding)
|
|
2405
|
+
throw cheapError(`constructor "${typeName}" is not a resolvable import`);
|
|
2406
|
+
// chant #1093 — same gate, same reason, as every other site that imports a
|
|
2407
|
+
// module in order to execute something from it.
|
|
2408
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, typeName, "constructor");
|
|
2409
|
+
if (refusal)
|
|
2410
|
+
throw cheapError(refusal);
|
|
2411
|
+
const modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
2412
|
+
const mod = await importModuleMemoized(modulePath, ctx.importCache);
|
|
2413
|
+
const Ctor = mod[binding.imported];
|
|
2414
|
+
if (typeof Ctor !== "function") {
|
|
2415
|
+
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a constructor`);
|
|
2416
|
+
}
|
|
2417
|
+
const ctorArgs = [];
|
|
2418
|
+
for (const arg of node.arguments ?? [])
|
|
2419
|
+
ctorArgs.push(await interpretExpression(arg, ctx));
|
|
2420
|
+
const instance = new Ctor(...ctorArgs);
|
|
2421
|
+
stampCompositeOrigins(instance, node, ctx);
|
|
2422
|
+
return instance;
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Record one exported name's fully-resolved value: unconditionally into
|
|
2426
|
+
* `exportedValues` (chant #1020) — the file's export namespace, which is
|
|
2427
|
+
* what discovery hands to `collectEntities` and what another file's
|
|
2428
|
+
* cross-file reference resolves against — and, additionally, into `entities`
|
|
2429
|
+
* when the value is a real `Declarable`/`CompositeInstance`.
|
|
2430
|
+
*
|
|
2431
|
+
* chant #1112 — `entities` is a REPORTING subset, not a filter. It used to
|
|
2432
|
+
* be the only thing discovery passed on, which made this function a second
|
|
2433
|
+
* owner of the "which export becomes an entity" decision; it had one fewer
|
|
2434
|
+
* case than the real owner (`enumerateEntries`, ../collect.ts), so a
|
|
2435
|
+
* `LexiconOutput` export folded fine and was then thrown away, and the
|
|
2436
|
+
* template silently lost its `Outputs` section. Discovery now passes
|
|
2437
|
+
* `exportedValues` — the whole namespace, exactly like the run path's real
|
|
2438
|
+
* `exports` object — and `collectEntities` filters it, so nothing here can
|
|
2439
|
+
* fall behind what collection understands. What `entities` still answers is
|
|
2440
|
+
* "how many resources did this file contribute", for the `[fold:fold] x.ts —
|
|
2441
|
+
* N resource(s)` decision line.
|
|
2442
|
+
*/
|
|
2443
|
+
function applyResolvedValue(name, value, entities, exportedValues) {
|
|
2444
|
+
exportedValues.set(name, value);
|
|
2445
|
+
if (isDeclarable(value) || isCompositeInstance(value)) {
|
|
2446
|
+
entities.push([name, value]);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2450
|
+
// Intrinsic revival (chant #1039).
|
|
2451
|
+
//
|
|
2452
|
+
// `fold()`/`foldResource()` reduce a registered intrinsic tagged template
|
|
2453
|
+
// (e.g. AWS `Sub`\`...\`) to a symbolic `FoldedIntrinsic` node —
|
|
2454
|
+
// `{ __intrinsic, strings, values }` — and an unresolved external symbol
|
|
2455
|
+
// chain inside it (e.g. `AWS.StackName`) to a `SymbolicValue` —
|
|
2456
|
+
// `{ __symbol }` (../fold/fold.ts's own doc: "mirrors the runtime call shape
|
|
2457
|
+
// `Tag(strings, ...values)` so a later build path can replay it into the
|
|
2458
|
+
// real intrinsic object"). This IS that later build path: without it, the
|
|
2459
|
+
// symbolic envelope would be passed straight into the constructed entity's
|
|
2460
|
+
// props as an inert plain object — the entity would be built (no crash), but
|
|
2461
|
+
// its serialized output would silently diverge from the run path's real
|
|
2462
|
+
// `Sub`/`PseudoParameter` instances (caught by the #1025 differential the
|
|
2463
|
+
// moment intrinsics started actually folding). `reviveFoldedProps` walks the
|
|
2464
|
+
// folded props/attributes tree and, for each `{__intrinsic}`/`{__symbol}`
|
|
2465
|
+
// node, resolves the real tag function / pseudo-parameter through this
|
|
2466
|
+
// file's own imports (same mechanism `resolveResourceEntity` already uses
|
|
2467
|
+
// for the resource constructor itself) and invokes/accesses it for real —
|
|
2468
|
+
// exactly what running the original tagged template would have done.
|
|
2469
|
+
//
|
|
2470
|
+
// `{__attrRef}` (a same-file sibling-resource reference, ../fold/fold.ts's
|
|
2471
|
+
// `AttrRefValue`) is left untouched when it is NOT nested inside a revived
|
|
2472
|
+
// intrinsic: the serializer's generic walker already recognizes that plain
|
|
2473
|
+
// envelope structurally (see ../serializer-walker.ts) with no revival
|
|
2474
|
+
// needed. But an intrinsic's OWN implementation (e.g. `SubIntrinsic`) needs
|
|
2475
|
+
// a genuine `AttrRef` instance internally (`instanceof` checks), which would
|
|
2476
|
+
// require wiring a live `WeakRef` to the sibling entity — out of scope here:
|
|
2477
|
+
// reject (fall back to run) rather than risk silently wrong output.
|
|
2478
|
+
//
|
|
2479
|
+
// chant #1169 adds the fourth envelope this walk revives, and the one that
|
|
2480
|
+
// closes the corpus's largest fold gate: `{__resource}`, a nested
|
|
2481
|
+
// `new Type(...)` used as a value. It is revived the same way and for the same
|
|
2482
|
+
// reason as the other three — the real class is resolved through the folding
|
|
2483
|
+
// file's own imports and called for real — so what the outer constructor
|
|
2484
|
+
// receives is the instance the run path would have handed it, not a look-alike.
|
|
2485
|
+
// See {@link constructFoldedResource}.
|
|
2486
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2487
|
+
/** Resolve a bare name bound by this file's own `import` to its real, live export — the same two-step (resolve module path, then `importModule`) `resolveResourceEntity`/`resolveCallExpression` already use for constructors and composite factories. */
|
|
2488
|
+
async function resolveImportedExport(name, ctx) {
|
|
2489
|
+
const binding = ctx.imports.get(name);
|
|
2490
|
+
if (!binding) {
|
|
2491
|
+
throw cheapError(`"${name}" is not a resolvable import`);
|
|
2492
|
+
}
|
|
2493
|
+
// chant #1093 — reached for an intrinsic tag and for a symbolic chain's root
|
|
2494
|
+
// (`AWS.StackName`), both of which are resolved BY NAME out of the file's own
|
|
2495
|
+
// imports: nothing guarantees the module behind that name is a lexicon's.
|
|
2496
|
+
// `reviveHelperCall` checks chant-ownership before it gets here; this covers
|
|
2497
|
+
// the paths that don't.
|
|
2498
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, name, "import");
|
|
2499
|
+
if (refusal)
|
|
2500
|
+
throw cheapError(refusal);
|
|
2501
|
+
let modulePath;
|
|
2502
|
+
try {
|
|
2503
|
+
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
2504
|
+
}
|
|
2505
|
+
catch (err) {
|
|
2506
|
+
throw cheapError(`could not resolve import "${binding.specifier}" for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2507
|
+
}
|
|
2508
|
+
let mod;
|
|
2509
|
+
try {
|
|
2510
|
+
mod = await importModuleMemoized(modulePath, ctx.importCache);
|
|
2511
|
+
}
|
|
2512
|
+
catch (err) {
|
|
2513
|
+
throw cheapError(`could not import "${binding.specifier}" to resolve "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2514
|
+
}
|
|
2515
|
+
return mod[binding.imported];
|
|
2516
|
+
}
|
|
2517
|
+
/** A bare identifier, or simple dotted access chain rooted at one (`AWS.StackName`, `Azure.ResourceGroupName`) — the only shape {@link fold.ts}'s `isUnresolvedSymbolChain` actually produces `SymbolicValue` text for in practice (element access/non-null on a pseudo-parameter-style namespace isn't a real authoring pattern anywhere in a lexicon today). Anything else is rejected rather than guessed at. */
|
|
2518
|
+
const SIMPLE_DOTTED_CHAIN = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/;
|
|
2519
|
+
/** Resolve a folded `SymbolicValue`'s raw source text (e.g. `"AWS.StackName"`) back to the real value it refers to, by resolving its root identifier through this file's own imports and then doing real property access down the rest of the chain — reproducing exactly what evaluating that expression at runtime would have done. */
|
|
2520
|
+
async function resolveSymbolicValue(text, ctx) {
|
|
2521
|
+
if (!SIMPLE_DOTTED_CHAIN.test(text)) {
|
|
2522
|
+
throw cheapError(`symbol "${text}" is not a simple dotted import reference`);
|
|
2523
|
+
}
|
|
2524
|
+
const [root, ...path] = text.split(".");
|
|
2525
|
+
let value = await resolveImportedExport(root, ctx);
|
|
2526
|
+
for (const key of path) {
|
|
2527
|
+
if (value === null || value === undefined) {
|
|
2528
|
+
throw cheapError(`symbol "${text}": "${root}" has no "${key}"`);
|
|
2529
|
+
}
|
|
2530
|
+
value = value[key];
|
|
2531
|
+
}
|
|
2532
|
+
return value;
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* True when {@link reviveFoldedValue} hands `value` back unchanged: a
|
|
2536
|
+
* primitive, or one of the live object kinds its passthrough keeps (an
|
|
2537
|
+
* `AttrRef`, a Declarable, a composite instance, an `Intrinsic`).
|
|
2538
|
+
*/
|
|
2539
|
+
function revivesAsItself(value) {
|
|
2540
|
+
if (value === null || typeof value !== "object")
|
|
2541
|
+
return true;
|
|
2542
|
+
return isAttrRefLike(value) || isDeclarable(value) || isCompositeInstance(value) || isIntrinsic(value);
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Revive a folded value tree: replace any
|
|
2546
|
+
* `{__intrinsic}`/`{__helper}`/`{__symbol}` envelope with the real value it
|
|
2547
|
+
* represents.
|
|
2548
|
+
*
|
|
2549
|
+
* `requireLiveRefs` tracks whether the CURRENT node is (transitively) an
|
|
2550
|
+
* argument being handed to a real function that will inspect it — a
|
|
2551
|
+
* `{__intrinsic}`'s own interpolated `values`, or a `{__helper}` call's
|
|
2552
|
+
* arguments (chant #1082). In that position a symbolic `{__attrRef}` envelope
|
|
2553
|
+
* is rejected rather than passed along: the receiving implementation needs a
|
|
2554
|
+
* genuine `AttrRef` instance (`instanceof` checks, `WeakRef` derefs — see
|
|
2555
|
+
* `SubIntrinsic`, and `LexiconOutput`'s constructor in ../lexicon-output.ts),
|
|
2556
|
+
* and handing it a look-alike plain object produces output that is wrong
|
|
2557
|
+
* rather than absent. Everywhere else the envelope is left untouched, because
|
|
2558
|
+
* the serializer's generic walker already understands it — see the module-doc
|
|
2559
|
+
* note above.
|
|
2560
|
+
*/
|
|
2561
|
+
async function reviveFoldedValue(value, ctx, requireLiveRefs) {
|
|
2562
|
+
if (value === null || typeof value !== "object")
|
|
2563
|
+
return value;
|
|
2564
|
+
// chant #1020 — a REAL, already-constructed live object reached via
|
|
2565
|
+
// cross-file resolution (`ctx.externals`, e.g. `network.vpc.VpcId` folding
|
|
2566
|
+
// to a genuine `AttrRef` wired to the actual shared `vpc` instance, or a
|
|
2567
|
+
// bare cross-file identifier folding directly to the shared
|
|
2568
|
+
// Declarable/CompositeInstance itself). None of these have an own
|
|
2569
|
+
// `__symbol`/`__intrinsic`/`__attrRef`/`__resource` marker KEY — an AttrRef
|
|
2570
|
+
// instance's own enumerable fields are `parent`/`attribute`, not a nested
|
|
2571
|
+
// `__attrRef` object — so without this early return the generic object
|
|
2572
|
+
// walk below would silently reconstruct a plain-object copy of it,
|
|
2573
|
+
// destroying the very identity #1020 exists to preserve. Passed through
|
|
2574
|
+
// completely unchanged, exactly like `resolveCallExpression`'s own
|
|
2575
|
+
// `live.value` passthrough for a composite-call argument.
|
|
2576
|
+
// chant #1063 adds the third kind of real object cross-file resolution can
|
|
2577
|
+
// now put here: a live `Intrinsic` instance read off an active lexicon
|
|
2578
|
+
// package (`Azure.ResourceGroupLocation`, `GCP.ProjectId` — see
|
|
2579
|
+
// `resolveActiveLexiconExport`). Same hazard, same fix: the generic walk
|
|
2580
|
+
// below would rebuild it as a plain `{}` copy, dropping the prototype that
|
|
2581
|
+
// carries `toJSON()` and so serializing `{}` where the run path emits
|
|
2582
|
+
// `[resourceGroup().location]`. `isIntrinsic` keys off
|
|
2583
|
+
// `Symbol.for("chant.intrinsic")` (../intrinsic.ts), a GLOBAL symbol, so it
|
|
2584
|
+
// holds across separately-loaded copies of chant-core the way a bare
|
|
2585
|
+
// `instanceof` would not.
|
|
2586
|
+
if (revivesAsItself(value))
|
|
2587
|
+
return value;
|
|
2588
|
+
if (Array.isArray(value)) {
|
|
2589
|
+
const revived = [];
|
|
2590
|
+
for (const el of value)
|
|
2591
|
+
revived.push(await reviveFoldedValue(el, ctx, requireLiveRefs));
|
|
2592
|
+
return revived;
|
|
2593
|
+
}
|
|
2594
|
+
if ("__symbol" in value) {
|
|
2595
|
+
const symbolic = value;
|
|
2596
|
+
return resolveSymbolicValue(symbolic.__symbol, ctx);
|
|
2597
|
+
}
|
|
2598
|
+
if ("__intrinsic" in value) {
|
|
2599
|
+
const intrinsic = value;
|
|
2600
|
+
const Fn = await resolveImportedExport(intrinsic.__intrinsic, ctx);
|
|
2601
|
+
if (typeof Fn !== "function") {
|
|
2602
|
+
throw cheapError(`intrinsic "${intrinsic.__intrinsic}" did not resolve to a function`);
|
|
2603
|
+
}
|
|
2604
|
+
// Two authored forms, one envelope family (../fold/fold.ts's
|
|
2605
|
+
// `FoldedIntrinsic`): the tagged template replays as
|
|
2606
|
+
// `Name(strings, ...values)`, the plain call (chant #1044) as
|
|
2607
|
+
// `Name(...args)`. Both hand their interior to the REAL function the
|
|
2608
|
+
// file itself imported, with `requireLiveRefs` — an intrinsic inspects
|
|
2609
|
+
// what it is given (`SubIntrinsic`'s `instanceof` checks, `Ref`'s
|
|
2610
|
+
// `getLogicalName`), so a look-alike `{__attrRef}` envelope must be
|
|
2611
|
+
// rejected here rather than silently serialized as something else.
|
|
2612
|
+
const revived = [];
|
|
2613
|
+
if ("args" in intrinsic) {
|
|
2614
|
+
for (const a of intrinsic.args)
|
|
2615
|
+
revived.push(await reviveFoldedValue(a, ctx, true));
|
|
2616
|
+
return Fn(...revived);
|
|
2617
|
+
}
|
|
2618
|
+
for (const v of intrinsic.values)
|
|
2619
|
+
revived.push(await reviveFoldedValue(v, ctx, true));
|
|
2620
|
+
// chant #3196 — hand the tag what a real tagged call hands it: a frozen
|
|
2621
|
+
// strings array carrying a frozen, non-enumerable `raw`. A tag that reads
|
|
2622
|
+
// `strings.raw` (a SQL tag keeps backslashes as written) would otherwise
|
|
2623
|
+
// build different text folded than run.
|
|
2624
|
+
const strings = Object.freeze(Object.defineProperty([...intrinsic.strings], "raw", { value: Object.freeze([...intrinsic.raw]) }));
|
|
2625
|
+
return Fn(strings, ...revived);
|
|
2626
|
+
}
|
|
2627
|
+
if ("__helper" in value) {
|
|
2628
|
+
return reviveHelperCall(value, ctx);
|
|
2629
|
+
}
|
|
2630
|
+
if ("__compositeStep" in value) {
|
|
2631
|
+
// chant #1174 — `<Identifier>(...).step`, see {@link FoldedCompositeStepCall}.
|
|
2632
|
+
// Arguments revive with `requireLiveRefs: false` — the same rule
|
|
2633
|
+
// `resolveCallArguments` applies to a NON-helper callee's arguments
|
|
2634
|
+
// (a composite factory stores its props, it doesn't inspect them the way
|
|
2635
|
+
// an intrinsic/helper does), so a `{__attrRef}` among them stays the
|
|
2636
|
+
// symbolic envelope the composite's own resource construction resolves
|
|
2637
|
+
// by name, exactly as a top-level resource's props would.
|
|
2638
|
+
const call = value;
|
|
2639
|
+
const revivedArgs = [];
|
|
2640
|
+
for (const a of call.args)
|
|
2641
|
+
revivedArgs.push(await reviveFoldedValue(a, ctx, false));
|
|
2642
|
+
const result = await resolveCompositeCall(call.__compositeStep, revivedArgs, ctx);
|
|
2643
|
+
if (!isIndexableObject(result)) {
|
|
2644
|
+
throw cheapError(`composite call \`${call.__compositeStep}(...)\` did not resolve to an object with a "step" member`);
|
|
2645
|
+
}
|
|
2646
|
+
return result.step;
|
|
2647
|
+
}
|
|
2648
|
+
if ("__attrRef" in value) {
|
|
2649
|
+
if (requireLiveRefs) {
|
|
2650
|
+
throw cheapError("a same-file resource reference passed to a folded intrinsic or authoring helper is not foldable yet");
|
|
2651
|
+
}
|
|
2652
|
+
return value;
|
|
2653
|
+
}
|
|
2654
|
+
if ("__resource" in value) {
|
|
2655
|
+
// chant #1169 — a nested `new Type(...)` used as a value. This is the
|
|
2656
|
+
// branch that makes the envelope safe: it is replaced, here, by a REAL
|
|
2657
|
+
// instance of the class the source named, built by the same
|
|
2658
|
+
// resolve-through-this-file's-imports machinery `resolveResourceEntity`
|
|
2659
|
+
// uses for a top-level resource and `interpretNewExpression` (#1023) uses
|
|
2660
|
+
// for a construction inside a factory body. Nothing symbolic reaches the
|
|
2661
|
+
// serializer — see {@link fold}'s `new` branch for why that is the whole
|
|
2662
|
+
// safety argument.
|
|
2663
|
+
//
|
|
2664
|
+
// `requireLiveRefs` is propagated rather than reset: a construction in an
|
|
2665
|
+
// ordinary prop position keeps the top-level rule (a `{__attrRef}` inside
|
|
2666
|
+
// it stays an envelope, which the serializer's own walker resolves by name
|
|
2667
|
+
// through `propertyDeclarable` exactly as it does for a top-level
|
|
2668
|
+
// resource's props), while a construction inside an intrinsic's interior
|
|
2669
|
+
// keeps the stricter one and rejects — the receiving implementation
|
|
2670
|
+
// inspects what it is handed, and this is the direction that falls back to
|
|
2671
|
+
// run rather than emitting something wrong.
|
|
2672
|
+
return constructFoldedResource(value, ctx, requireLiveRefs);
|
|
2673
|
+
}
|
|
2674
|
+
const revived = {};
|
|
2675
|
+
for (const [key, v] of Object.entries(value)) {
|
|
2676
|
+
revived[key] = await reviveFoldedValue(v, ctx, requireLiveRefs);
|
|
2677
|
+
}
|
|
2678
|
+
return revived;
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* chant #1082 — the provenance half of the registered-authoring-helper check
|
|
2682
|
+
* (the shape/name half is `fold()`'s, see ../fold/foldable-helpers.ts's module
|
|
2683
|
+
* doc). Being in the allowlist is not permission to invoke whatever the name
|
|
2684
|
+
* happens to be bound to: the name must be bound by THIS FILE'S OWN `import`,
|
|
2685
|
+
* and that import must actually come from chant. Only then is the real
|
|
2686
|
+
* function called, with the folded arguments — the same function the run path
|
|
2687
|
+
* would have called, from the same module the source itself named, so fold
|
|
2688
|
+
* cannot diverge from run by construction.
|
|
2689
|
+
*
|
|
2690
|
+
* Anything short of that throws, which falls the whole file back to run: a
|
|
2691
|
+
* local `function phase(...)`, a `phase` imported from the project's own
|
|
2692
|
+
* helpers, a chant-owned import that turns out not to be a function.
|
|
2693
|
+
*/
|
|
2694
|
+
async function reviveHelperCall(call, ctx) {
|
|
2695
|
+
const name = call.__helper;
|
|
2696
|
+
const binding = ctx.imports.get(name);
|
|
2697
|
+
if (!binding) {
|
|
2698
|
+
throw cheapError(`authoring helper "${name}(...)" is not a resolvable import`);
|
|
2699
|
+
}
|
|
2700
|
+
if (!isChantOwnedHelperBinding(binding, ctx)) {
|
|
2701
|
+
throw cheapError(`"${name}" is imported from "${binding.specifier}", which is not chant's own — ` +
|
|
2702
|
+
`only chant's registered authoring helpers fold as calls`);
|
|
2703
|
+
}
|
|
2704
|
+
const Fn = await resolveImportedExport(name, ctx);
|
|
2705
|
+
if (typeof Fn !== "function") {
|
|
2706
|
+
throw cheapError(`authoring helper "${name}" did not resolve to a function`);
|
|
2707
|
+
}
|
|
2708
|
+
// `requireLiveRefs` — a helper receives its arguments as real values and may
|
|
2709
|
+
// inspect them (`output()` derefs the ref's `WeakRef` parent), so a symbolic
|
|
2710
|
+
// `{__attrRef}` envelope is rejected here rather than silently wrapped into
|
|
2711
|
+
// a wrong result. A ref that resolved cross-file to a genuine live `AttrRef`
|
|
2712
|
+
// passes straight through (see `reviveFoldedValue`'s early return).
|
|
2713
|
+
const args = [];
|
|
2714
|
+
for (const arg of call.args)
|
|
2715
|
+
args.push(await reviveFoldedValue(arg, ctx, true));
|
|
2716
|
+
return Fn(...args);
|
|
2717
|
+
}
|
|
2718
|
+
/**
|
|
2719
|
+
* True when `binding` names a helper import chant itself owns: a published
|
|
2720
|
+
* chant package specifier ({@link isChantOwnedSpecifier}), or — for in-repo
|
|
2721
|
+
* and test callers, which import chant-core by relative/absolute path the way
|
|
2722
|
+
* this module's own fixtures do — a specifier that resolves to a file inside
|
|
2723
|
+
* chant-core's own tree.
|
|
2724
|
+
*
|
|
2725
|
+
* The path arm resolves only for a relative/absolute specifier, never a bare
|
|
2726
|
+
* one: resolving an arbitrary bare specifier here would reintroduce the
|
|
2727
|
+
* pathological cold-resolution cost chant#1020 measured (see
|
|
2728
|
+
* {@link fastResolveBareSpecifier}), and a bare specifier chant publishes is
|
|
2729
|
+
* already covered by the text arm.
|
|
2730
|
+
*
|
|
2731
|
+
* Not the same question as chant#1093's {@link isTrustedExecutableBinding}
|
|
2732
|
+
* below, and deliberately not shared with it: this one asks "may this NAME be
|
|
2733
|
+
* invoked as one of chant's registered authoring helpers", and a text match
|
|
2734
|
+
* is the right answer for it — a project that shadows `@intentius/chant` in
|
|
2735
|
+
* its own `node_modules` gets its own copy of `output()` invoked either way,
|
|
2736
|
+
* fold or run, so fold cannot diverge from run by trusting the text here.
|
|
2737
|
+
* #1093's question is "may this module execute in the CLI's process at all",
|
|
2738
|
+
* where a specifier the project controls the text of proves nothing.
|
|
2739
|
+
*/
|
|
2740
|
+
function isChantOwnedHelperBinding(binding, ctx) {
|
|
2741
|
+
if (isChantOwnedSpecifier(binding.specifier))
|
|
2742
|
+
return true;
|
|
2743
|
+
if (!isProjectImport(binding.specifier, ctx.file))
|
|
2744
|
+
return false;
|
|
2745
|
+
let targetPath;
|
|
2746
|
+
try {
|
|
2747
|
+
targetPath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
2748
|
+
}
|
|
2749
|
+
catch {
|
|
2750
|
+
return false;
|
|
2751
|
+
}
|
|
2752
|
+
return isChantCorePath(targetPath);
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* chant #1093 — the closed allowlist of modules fold may import AND EXECUTE
|
|
2756
|
+
* in the CLI's own process when the #1045 sandbox is active. Exactly two
|
|
2757
|
+
* arms, and neither of them trusts text the project controls:
|
|
2758
|
+
*
|
|
2759
|
+
* 1. **An ACTIVE lexicon package of this build, root OR subpath** — the
|
|
2760
|
+
* exact specifier matched against {@link FoldSession.lexiconPackages}, a
|
|
2761
|
+
* closed set built from the lexicon names the BUILD resolved and
|
|
2762
|
+
* `loadPlugins` already imported (../cli/plugins.ts), not from anything
|
|
2763
|
+
* the file under fold says; OR (chant #1995) a subpath of one of those
|
|
2764
|
+
* same packages (`@intentius/chant-lexicon-azure/generated/index`), whose
|
|
2765
|
+
* package ROOT is extracted from the specifier TEXT
|
|
2766
|
+
* ({@link bareSpecifierPackageRoot}, no resolution) and checked against
|
|
2767
|
+
* the identical set. A subpath is trusted because the code it names
|
|
2768
|
+
* belongs to a package the CLI already imported and executed to load that
|
|
2769
|
+
* lexicon's serializers/lint rules — reaching one of that package's own
|
|
2770
|
+
* modules by subpath instead of through the barrel names code the process
|
|
2771
|
+
* already trusts, not code the project controls.
|
|
2772
|
+
* 2. **chant-core's own executing tree** — the specifier is RESOLVED and the
|
|
2773
|
+
* resulting path checked against {@link chantCoreRoots}. A text match is
|
|
2774
|
+
* not enough here: `@intentius/chant` and `@intentius/chant-lexicon-evil`
|
|
2775
|
+
* are both strings an untrusted repo can write into its own source and
|
|
2776
|
+
* back with its own `node_modules` directory, and
|
|
2777
|
+
* {@link isChantOwnedSpecifier} would accept either. Resolution happens
|
|
2778
|
+
* before the decision rather than after, so an allowed binding pays
|
|
2779
|
+
* exactly the resolution it was about to pay anyway (the memoized one —
|
|
2780
|
+
* see {@link resolveModulePathMemoized}); a bare specifier that isn't
|
|
2781
|
+
* even chant-shaped is rejected on text alone, so no arbitrary bare
|
|
2782
|
+
* specifier is ever resolved here (chant#1020's cold-resolution cost).
|
|
2783
|
+
*
|
|
2784
|
+
* This is deliberately the boundary chant #1045 drew: "the boundary is around
|
|
2785
|
+
* executing PROJECT SOURCE, which is the untrusted input" — not around chant
|
|
2786
|
+
* itself or the lexicon packages, which the CLI has already imported and
|
|
2787
|
+
* executed in its own process before discovery starts, to get the serializers
|
|
2788
|
+
* and lint rules it cannot run without. Fold reaching the same already-loaded
|
|
2789
|
+
* module (the identical un-cache-busted `import()` of the identical resolved
|
|
2790
|
+
* path) adds no execution the process wasn't already performing.
|
|
2791
|
+
*
|
|
2792
|
+
* Everything else is out: a sibling project file, a project-local helper
|
|
2793
|
+
* module, an arbitrary npm dependency, a lexicon this build didn't load. A
|
|
2794
|
+
* build that supplied no lexicon list keeps only arm 2, rather than falling
|
|
2795
|
+
* back to something more permissive — the same stance
|
|
2796
|
+
* {@link FoldSession.lexiconPackages} takes for #1063.
|
|
2797
|
+
*/
|
|
2798
|
+
function isTrustedExecutableBinding(binding, ctx) {
|
|
2799
|
+
// chant#3090 — a specifier a tsconfig `paths` entry maps names a project
|
|
2800
|
+
// file whatever its text says, so neither text arm applies to it: it is
|
|
2801
|
+
// resolved and decided on the path below, like a relative specifier.
|
|
2802
|
+
const pathsMapped = pathsMappedTarget(binding.specifier, ctx.file) !== undefined;
|
|
2803
|
+
if (!pathsMapped && activeLexiconPackage(binding.specifier, ctx.lexiconPackages) !== undefined)
|
|
2804
|
+
return true;
|
|
2805
|
+
// chant #1995 — arm 1's subpath case: same allowlist, matched against the
|
|
2806
|
+
// specifier's package ROOT (text only, see bareSpecifierPackageRoot) rather
|
|
2807
|
+
// than the specifier itself, so `<activeLexiconPkg>/generated/index` is
|
|
2808
|
+
// trusted exactly like the bare package import already is.
|
|
2809
|
+
const subpathRoot = bareSpecifierPackageRoot(binding.specifier);
|
|
2810
|
+
if (!pathsMapped && subpathRoot !== undefined && activeLexiconPackage(subpathRoot, ctx.lexiconPackages) !== undefined) {
|
|
2811
|
+
return true;
|
|
2812
|
+
}
|
|
2813
|
+
// Only a chant-shaped or project-relative specifier is worth resolving; any
|
|
2814
|
+
// other bare specifier is untrusted by definition, and resolving it to find
|
|
2815
|
+
// that out would cost the pathological cold `require.resolve` (chant#1020).
|
|
2816
|
+
if (!pathsMapped && !isProjectFileSpecifier(binding.specifier) && !isChantOwnedSpecifier(binding.specifier)) {
|
|
2817
|
+
return false;
|
|
2818
|
+
}
|
|
2819
|
+
let targetPath;
|
|
2820
|
+
try {
|
|
2821
|
+
targetPath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
2822
|
+
}
|
|
2823
|
+
catch {
|
|
2824
|
+
return false;
|
|
2825
|
+
}
|
|
2826
|
+
// chant#2577 — arm 1 for a lexicon declared by path: the module
|
|
2827
|
+
// `loadPlugins` already imported, reached by the path the project imports it by.
|
|
2828
|
+
if (ctx.lexiconModules.has(targetPath))
|
|
2829
|
+
return true;
|
|
2830
|
+
// chant#2590 — the subpath case of the same arm: another file of a
|
|
2831
|
+
// multi-file path lexicon, under its trusted directory.
|
|
2832
|
+
if (inPathLexiconRoot(targetPath, ctx.lexiconRoots))
|
|
2833
|
+
return true;
|
|
2834
|
+
return isChantCorePath(targetPath);
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* chant #1093 — the one-line fold-fallback reason for a resolution that would
|
|
2838
|
+
* import and execute an untrusted module in the CLI's own process, or
|
|
2839
|
+
* `undefined` when the import may proceed (the sandbox wasn't asked for, or
|
|
2840
|
+
* the module is on {@link isTrustedExecutableBinding}'s allowlist).
|
|
2841
|
+
*
|
|
2842
|
+
* A refusal is not a failure to fold something folder-shaped — the shape is
|
|
2843
|
+
* perfectly foldable and folds fine under plain `--fold`. It is a deliberate
|
|
2844
|
+
* demotion: the file falls back to the run path, and under `--sandbox` the
|
|
2845
|
+
* run path is the sandboxed child (`./index.ts` queues every run-fallback
|
|
2846
|
+
* file for `./sandbox/run.ts`). So the factory/constructor/intrinsic still
|
|
2847
|
+
* executes, with the same arguments, in the same module graph as the rest of
|
|
2848
|
+
* that file — just behind Node's Permission Model and a scrubbed environment
|
|
2849
|
+
* instead of inside the CLI. Coverage drops; the boundary holds.
|
|
2850
|
+
*
|
|
2851
|
+
* @param what - What the binding is being resolved AS, for the message
|
|
2852
|
+
* ("composite factory", "constructor", …).
|
|
2853
|
+
*/
|
|
2854
|
+
function sandboxedExecutionRefusal(binding, ctx, name, what) {
|
|
2855
|
+
if (!ctx.sandbox)
|
|
2856
|
+
return undefined;
|
|
2857
|
+
if (isTrustedExecutableBinding(binding, ctx))
|
|
2858
|
+
return undefined;
|
|
2859
|
+
return (`${what} "${name}" is imported from "${binding.specifier}", which is neither chant's own nor an active lexicon — ` +
|
|
2860
|
+
`under --sandbox it is executed in the sandboxed child, not in this process`);
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* chant-core's own module roots, derived from THIS module's location:
|
|
2864
|
+
* `packages/core/src` in this repo, and in a published install both
|
|
2865
|
+
* `<pkg>/src` (where `@intentius/chant/*` specifiers resolve, through the
|
|
2866
|
+
* package exports) and `<pkg>/dist` (where the compiled CLI runs from, #3878).
|
|
2867
|
+
* Pure string arithmetic on `import.meta.url`, no filesystem access at all, so
|
|
2868
|
+
* it is safe in the #1045 sandbox child (which locks reads to an allowlist);
|
|
2869
|
+
* lazy and memoized purely to avoid doing it per call.
|
|
2870
|
+
*/
|
|
2871
|
+
let chantCoreRootsMemo;
|
|
2872
|
+
function chantCoreRoots() {
|
|
2873
|
+
if (!chantCoreRootsMemo) {
|
|
2874
|
+
// .../<root>/discovery/fold-import.{ts,js} -> .../<root>
|
|
2875
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
2876
|
+
const name = basename(root);
|
|
2877
|
+
const sibling = name === "dist" ? join(dirname(root), "src") : name === "src" ? join(dirname(root), "dist") : undefined;
|
|
2878
|
+
chantCoreRootsMemo = sibling ? [root, sibling] : [root];
|
|
2879
|
+
}
|
|
2880
|
+
return chantCoreRootsMemo;
|
|
2881
|
+
}
|
|
2882
|
+
/** Whether `path` is one of chant-core's own modules (see {@link chantCoreRoots}). */
|
|
2883
|
+
function isChantCorePath(path) {
|
|
2884
|
+
return chantCoreRoots().some((root) => path === root || path.startsWith(root + sep));
|
|
2885
|
+
}
|
|
2886
|
+
/** Revive every value in a folded props/attributes object (see {@link reviveFoldedValue}). */
|
|
2887
|
+
async function reviveFoldedProps(props, ctx, requireLiveRefs) {
|
|
2888
|
+
const revived = {};
|
|
2889
|
+
for (const [key, value] of Object.entries(props)) {
|
|
2890
|
+
revived[key] = await reviveFoldedValue(value, ctx, requireLiveRefs);
|
|
2891
|
+
}
|
|
2892
|
+
return revived;
|
|
2893
|
+
}
|
|
2894
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2895
|
+
// Resource construction — #1022's mechanism (fold the ctor call's arguments
|
|
2896
|
+
// via `fold()`, resolve the constructor through this file's imports,
|
|
2897
|
+
// construct the real Declarable), split by chant #1169 into two reusable
|
|
2898
|
+
// halves so a NESTED `new Type(...)` used as a value is built by exactly the
|
|
2899
|
+
// same code as a file's own top-level resource declaration, not by a second
|
|
2900
|
+
// implementation that could quietly differ.
|
|
2901
|
+
//
|
|
2902
|
+
// `resolveResourceEntity` below is the top-level entry point and keeps its
|
|
2903
|
+
// per-declarator reason strings verbatim; `reviveResourceCtorArgs` and
|
|
2904
|
+
// `instantiateFoldedResource` are the shared halves, and
|
|
2905
|
+
// {@link constructFoldedResource} is the one-call composition of the two that
|
|
2906
|
+
// `reviveFoldedValue` uses for a nested construction.
|
|
2907
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2908
|
+
/**
|
|
2909
|
+
* Revive a folded constructor call's arguments into the real values the class
|
|
2910
|
+
* will receive.
|
|
2911
|
+
*
|
|
2912
|
+
* chant #1039 — replays any folded intrinsic/symbol envelopes into their real
|
|
2913
|
+
* runtime values before the entity is constructed. A no-op walk when the file
|
|
2914
|
+
* used no registered intrinsics (the overwhelming majority of cases today).
|
|
2915
|
+
*
|
|
2916
|
+
* chant #1082 — when `spec.args` is present the constructor's argument list
|
|
2917
|
+
* isn't the classic `(props)`/`(props, attributes)` shape (AWS's `Parameter` is
|
|
2918
|
+
* `(type, props)`), so the whole list is revived and spread by the caller
|
|
2919
|
+
* instead of `spec.props`, which in that case is only a view onto one of its
|
|
2920
|
+
* entries and would be double-counted.
|
|
2921
|
+
*/
|
|
2922
|
+
async function reviveResourceCtorArgs(spec, ctx, requireLiveRefs) {
|
|
2923
|
+
if (spec.args) {
|
|
2924
|
+
const revived = [];
|
|
2925
|
+
for (const arg of spec.args)
|
|
2926
|
+
revived.push(await reviveFoldedValue(arg, ctx, requireLiveRefs));
|
|
2927
|
+
return revived;
|
|
2928
|
+
}
|
|
2929
|
+
const props = await reviveFoldedProps(spec.props, ctx, requireLiveRefs);
|
|
2930
|
+
// The runtime constructor's optional second argument (`attributes` — CFN's
|
|
2931
|
+
// DependsOn/Condition/DeletionPolicy/…, see createResource in ../runtime.ts)
|
|
2932
|
+
// is only present in `spec` when the source actually passed one (see
|
|
2933
|
+
// foldResource in ../fold/fold.ts). Passing `undefined` when it's absent
|
|
2934
|
+
// matches the run path's own default (`attributes ?? {}` inside the
|
|
2935
|
+
// constructor).
|
|
2936
|
+
return [props, spec.attributes ? await reviveFoldedProps(spec.attributes, ctx, requireLiveRefs) : undefined];
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Resolve a folded constructor's NAME through `ctx`'s own `import`
|
|
2940
|
+
* declarations and call the real class with `ctorArgs`.
|
|
2941
|
+
*
|
|
2942
|
+
* Throws a {@link cheapError} for every failure; `resolveResourceEntity` turns
|
|
2943
|
+
* those into its per-declarator `reason` strings and the nested path lets them
|
|
2944
|
+
* fall the whole file back to run. `forClause` is the ` for "<export name>"`
|
|
2945
|
+
* fragment the top-level messages carry and a nested construction has no name
|
|
2946
|
+
* for — the only difference between the two callers' diagnostics.
|
|
2947
|
+
*/
|
|
2948
|
+
async function instantiateFoldedResource(typeName, ctorArgs, ctx, forClause) {
|
|
2949
|
+
const binding = ctx.imports.get(typeName);
|
|
2950
|
+
if (!binding) {
|
|
2951
|
+
throw cheapError(`constructor "${typeName}"${forClause} is not a resolvable import`);
|
|
2952
|
+
}
|
|
2953
|
+
// chant #1093 — a resource class is a lexicon export in every corpus entry
|
|
2954
|
+
// today, but nothing forces that: `new Thing(...)` where `Thing` comes from
|
|
2955
|
+
// a project file (or an arbitrary dependency) would import and run that
|
|
2956
|
+
// module here, in the CLI's process. Same refusal as the composite-factory
|
|
2957
|
+
// path above, and the reason a nested construction cannot widen the #1093
|
|
2958
|
+
// boundary: it reaches its class by exactly this gate.
|
|
2959
|
+
const refusal = sandboxedExecutionRefusal(binding, ctx, typeName, "constructor");
|
|
2960
|
+
if (refusal)
|
|
2961
|
+
throw cheapError(refusal);
|
|
2962
|
+
let modulePath;
|
|
2963
|
+
try {
|
|
2964
|
+
modulePath = resolveModulePathMemoized(binding.specifier, ctx.file, ctx.resolvePathCache);
|
|
2965
|
+
}
|
|
2966
|
+
catch (err) {
|
|
2967
|
+
throw cheapError(`could not resolve import "${binding.specifier}" for "${typeName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2968
|
+
}
|
|
2969
|
+
// Same import mechanism as `resolveCallExpression` above — see its comment.
|
|
2970
|
+
let mod;
|
|
2971
|
+
try {
|
|
2972
|
+
mod = await importModuleMemoized(modulePath, ctx.importCache);
|
|
2973
|
+
}
|
|
2974
|
+
catch (err) {
|
|
2975
|
+
throw cheapError(`could not import "${binding.specifier}" to resolve "${typeName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
2976
|
+
}
|
|
2977
|
+
const Ctor = mod[binding.imported];
|
|
2978
|
+
if (typeof Ctor !== "function") {
|
|
2979
|
+
throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a constructor`);
|
|
2980
|
+
}
|
|
2981
|
+
// Constructed with exactly the arguments the source wrote.
|
|
2982
|
+
return new Ctor(...ctorArgs);
|
|
2983
|
+
}
|
|
2984
|
+
/**
|
|
2985
|
+
* chant #1169 — build a real instance from a nested `{__resource}` envelope:
|
|
2986
|
+
* revive its arguments, then resolve its class and call it. The composition
|
|
2987
|
+
* {@link reviveFoldedValue} reaches for; identical in every step to what
|
|
2988
|
+
* {@link resolveResourceEntity} does for a top-level declaration, which is the
|
|
2989
|
+
* point — a nested `new Image({...})` and a top-level `export const image = new
|
|
2990
|
+
* Image({...})` produce the same object, from the same class, from the same
|
|
2991
|
+
* resolved module path.
|
|
2992
|
+
*/
|
|
2993
|
+
async function constructFoldedResource(spec, ctx, requireLiveRefs) {
|
|
2994
|
+
const ctorArgs = await reviveResourceCtorArgs(spec, ctx, requireLiveRefs);
|
|
2995
|
+
return instantiateFoldedResource(spec.__resource, ctorArgs, ctx, "");
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* chant #1169 — construct every top-level `const x = new Type(...)` in the file
|
|
2999
|
+
* ONCE, in source order, before any exported declarator is resolved, and put
|
|
3000
|
+
* each instance in `ctx.externals` under its own name.
|
|
3001
|
+
*
|
|
3002
|
+
* This is what makes a same-file resource usable as a VALUE — `image:
|
|
3003
|
+
* nodeImage`, `DependsOn: [dbCluster]`, `export { app }` — and it is the half
|
|
3004
|
+
* of the #1169 gate the nested-`new` lift alone does not reach: the
|
|
3005
|
+
* `{__resource}` envelope covers a construction written INLINE at the value
|
|
3006
|
+
* position, while the far more common authoring shape names it once and refers
|
|
3007
|
+
* to it. `fold()` cannot answer that reference on its own (it is synchronous,
|
|
3008
|
+
* and constructing needs the module graph), so it defers to `externals` — see
|
|
3009
|
+
* its identifier branch for the full argument.
|
|
3010
|
+
*
|
|
3011
|
+
* ONE instance, and identity is the whole point. Every reference in the file
|
|
3012
|
+
* reads this map, and the exported-declarator loop reuses the same object
|
|
3013
|
+
* through `prebuilt` rather than constructing a second one, so a resource
|
|
3014
|
+
* referenced by name and the entity discovery registers are the same object —
|
|
3015
|
+
* which is what makes the serializer's `Ref`/`DependsOn` resolution land on a
|
|
3016
|
+
* logical name at all. Running the module top-to-bottom produces exactly this:
|
|
3017
|
+
* every top-level `const` evaluated once, in order, later ones seeing earlier
|
|
3018
|
+
* ones. Order matters and is preserved — `collectConsts` yields source order, so
|
|
3019
|
+
* `const b = new Thing({ x: a })` finds `a` already built.
|
|
3020
|
+
*
|
|
3021
|
+
* A construction that FAILS is skipped silently rather than failing the file:
|
|
3022
|
+
* the name stays absent from `externals`, so a reference to it rejects with the
|
|
3023
|
+
* identical message it produced before this existed, and an EXPORTED one falls
|
|
3024
|
+
* through to {@link resolveResourceEntity}, which reproduces the failure with
|
|
3025
|
+
* its own located reason. Strictly additive.
|
|
3026
|
+
*
|
|
3027
|
+
* Under `--sandbox` every construction here goes through the same
|
|
3028
|
+
* {@link sandboxedExecutionRefusal} as every other one, so a project-defined
|
|
3029
|
+
* class refuses, the name stays unresolved, and the file demotes to the
|
|
3030
|
+
* sandboxed child exactly as before. Under plain `--fold` this can import a
|
|
3031
|
+
* constructor's module for a const that is never exported — work the RUN path
|
|
3032
|
+
* performs unconditionally for the same file, and through the same
|
|
3033
|
+
* already-memoized `importModule`.
|
|
3034
|
+
*/
|
|
3035
|
+
/**
|
|
3036
|
+
* The local names this file bound to the build's parameter object (chant
|
|
3037
|
+
* #1443). Object identity against {@link FoldSession.buildParams}, which
|
|
3038
|
+
* `buildExternals` substituted directly, so an unrelated import that happens to
|
|
3039
|
+
* be called `params` is not mistaken for it.
|
|
3040
|
+
*/
|
|
3041
|
+
function paramLocalNames(ctx) {
|
|
3042
|
+
const out = new Set();
|
|
3043
|
+
const buildParams = ctx.session.buildParams;
|
|
3044
|
+
if (!buildParams)
|
|
3045
|
+
return out;
|
|
3046
|
+
for (const [name, value] of ctx.externals) {
|
|
3047
|
+
if (value === buildParams)
|
|
3048
|
+
out.add(name);
|
|
3049
|
+
}
|
|
3050
|
+
return out;
|
|
3051
|
+
}
|
|
3052
|
+
/**
|
|
3053
|
+
* chant #1443 — record which build parameters each of a resource's authored
|
|
3054
|
+
* property expressions reads, before fold substitutes them away. Best-effort
|
|
3055
|
+
* and additive: a constructor called with no object literal records nothing.
|
|
3056
|
+
*
|
|
3057
|
+
* INTENTIUS/typescript-as-data#248 — and which of them read host code, a
|
|
3058
|
+
* method call, an eager lexicon helper or a lexicon export, so fold provenance
|
|
3059
|
+
* reports those `unknown` rather than `direct`.
|
|
3060
|
+
*/
|
|
3061
|
+
function stampParamDependencies(entity, node, ctx) {
|
|
3062
|
+
if (typeof entity !== "object" || entity === null)
|
|
3063
|
+
return;
|
|
3064
|
+
const paramLocals = paramLocalNames(ctx);
|
|
3065
|
+
const host = {
|
|
3066
|
+
values: ctx.hostValues ?? new Set(),
|
|
3067
|
+
eagerCalls: new Set(ctx.intrinsics.filter((i) => intrinsicCallFoldsEagerly(i)).map((i) => i.name)),
|
|
3068
|
+
};
|
|
3069
|
+
// The same argument `foldResource` treats as props: the first object literal.
|
|
3070
|
+
let propsArg;
|
|
3071
|
+
for (const argument of node.arguments ?? []) {
|
|
3072
|
+
if (ts.isObjectLiteralExpression(argument)) {
|
|
3073
|
+
propsArg = argument;
|
|
3074
|
+
break;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
if (!propsArg)
|
|
3078
|
+
return;
|
|
3079
|
+
for (const [path, origin] of Object.entries(collectParamDependencies(propsArg, ctx.consts, paramLocals, host))) {
|
|
3080
|
+
setPathProvenance(entity, path, origin);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
async function preresolveResourceConsts(ctx) {
|
|
3084
|
+
const built = new Map();
|
|
3085
|
+
for (const [name, initializer] of ctx.consts) {
|
|
3086
|
+
// chant #3196 — a same-file `const x = tag\`...\`` for a registered tag
|
|
3087
|
+
// is built once here too, so the export and every reference to `x` share
|
|
3088
|
+
// the one value running the module gives them. That matters when the tag
|
|
3089
|
+
// returns an entity (a SQL `table\`...\``): a second call would build a
|
|
3090
|
+
// second entity discovery never registers.
|
|
3091
|
+
if (ts.isTaggedTemplateExpression(initializer) &&
|
|
3092
|
+
ts.isIdentifier(initializer.tag) &&
|
|
3093
|
+
ctx.intrinsics.some((i) => i.name === initializer.tag.text && intrinsicTagFolds(i))) {
|
|
3094
|
+
try {
|
|
3095
|
+
const value = await reviveFoldedValue(fold(initializer, ctx.consts, ctx.intrinsics, ctx.externals), ctx, false);
|
|
3096
|
+
built.set(initializer, value);
|
|
3097
|
+
// Only a value `reviveFoldedValue` passes through unchanged is safe to
|
|
3098
|
+
// hand a reference. Any other object would be walked into a plain
|
|
3099
|
+
// copy, so a reference to one keeps re-folding the initializer.
|
|
3100
|
+
if (revivesAsItself(value))
|
|
3101
|
+
ctx.externals.set(name, value);
|
|
3102
|
+
}
|
|
3103
|
+
catch (err) {
|
|
3104
|
+
ctx.prebuildFailures?.set(name, describeFoldFailure(err, ctx));
|
|
3105
|
+
}
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
if (!ts.isNewExpression(initializer) || !ts.isIdentifier(initializer.expression))
|
|
3109
|
+
continue;
|
|
3110
|
+
try {
|
|
3111
|
+
const spec = foldResource(initializer, ctx.consts, ctx.intrinsics, ctx.externals);
|
|
3112
|
+
const instance = await constructFoldedResource(spec, ctx, false);
|
|
3113
|
+
stampParamDependencies(instance, initializer, ctx);
|
|
3114
|
+
built.set(initializer, instance);
|
|
3115
|
+
ctx.externals.set(name, instance);
|
|
3116
|
+
}
|
|
3117
|
+
catch (err) {
|
|
3118
|
+
// Not constructible here (an unresolvable constructor import, a prop
|
|
3119
|
+
// outside the fold subset, a --sandbox refusal). Leave the name alone:
|
|
3120
|
+
// the failure is deliberately not fatal, and the first reference to the
|
|
3121
|
+
// name rejects on its own terms.
|
|
3122
|
+
//
|
|
3123
|
+
// chant#2423 — but keep the located reason. Without it the file's only
|
|
3124
|
+
// reported cause is the reference site, which is where the consequence
|
|
3125
|
+
// is, not where the problem is.
|
|
3126
|
+
ctx.prebuildFailures?.set(name, describeFoldFailure(err, ctx));
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return built;
|
|
3130
|
+
}
|
|
3131
|
+
async function resolveResourceEntity(name, node, ctx) {
|
|
3132
|
+
let spec;
|
|
3133
|
+
try {
|
|
3134
|
+
spec = foldResource(node, ctx.consts, ctx.intrinsics, ctx.externals);
|
|
3135
|
+
}
|
|
3136
|
+
catch (err) {
|
|
3137
|
+
if (err instanceof FoldError) {
|
|
3138
|
+
return { ok: false, reason: `"${name}" is not foldable: ${describeFoldFailure(err, ctx)}` };
|
|
3139
|
+
}
|
|
3140
|
+
throw err;
|
|
3141
|
+
}
|
|
3142
|
+
let ctorArgs;
|
|
3143
|
+
try {
|
|
3144
|
+
ctorArgs = await reviveResourceCtorArgs(spec, ctx, false);
|
|
3145
|
+
}
|
|
3146
|
+
catch (err) {
|
|
3147
|
+
return {
|
|
3148
|
+
ok: false,
|
|
3149
|
+
reason: `"${name}" is not foldable: ${describeFoldFailure(err, ctx)}`,
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
try {
|
|
3153
|
+
const entity = (await instantiateFoldedResource(spec.__resource, ctorArgs, ctx, ` for "${name}"`));
|
|
3154
|
+
stampParamDependencies(entity, node, ctx);
|
|
3155
|
+
return { ok: true, entity };
|
|
3156
|
+
}
|
|
3157
|
+
catch (err) {
|
|
3158
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3162
|
+
// Cross-file resolution (chant #1020) — resolves an imported PROJECT-file
|
|
3163
|
+
// binding to its real, already-constructed value by recursively folding the
|
|
3164
|
+
// DEFINING module (in that module's own scope: its own consts, its own
|
|
3165
|
+
// imports), memoized per {@link FoldSession} so each cross-file export folds
|
|
3166
|
+
// exactly once and every referrer shares the identical object — see
|
|
3167
|
+
// `planFoldTaint`'s doc below for why that identity is the entire hard part.
|
|
3168
|
+
// A bare package specifier (a lexicon/vendor module) is left alone here: it
|
|
3169
|
+
// resolves through the pre-existing `importModule` mechanism at the point a
|
|
3170
|
+
// constructor/composite-factory/intrinsic tag is actually used, same as
|
|
3171
|
+
// before #1020 — this path exists only to avoid executing PROJECT files.
|
|
3172
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3173
|
+
const UNRESOLVED_IDENTIFIER_RE = /unresolved identifier: (\S+)$/;
|
|
3174
|
+
/** The rejection a reference to a const whose pre-build failed produces (chant#2423). */
|
|
3175
|
+
const SAME_FILE_RESOURCE_RE = /same-file resource `([^`]+)` used as a value is not foldable/;
|
|
3176
|
+
/**
|
|
3177
|
+
* Enrich an otherwise-generic "unresolved identifier: X" failure when X is a
|
|
3178
|
+
* name whose OWN cross-file resolution was attempted and failed for a known
|
|
3179
|
+
* reason (most notably an import cycle) — purely cosmetic, never changes
|
|
3180
|
+
* whether something resolves. Every other failure shape passes through
|
|
3181
|
+
* unchanged.
|
|
3182
|
+
*/
|
|
3183
|
+
function describeFoldFailure(err, ctx) {
|
|
3184
|
+
if (!(err instanceof Error))
|
|
3185
|
+
return String(err);
|
|
3186
|
+
const match = UNRESOLVED_IDENTIFIER_RE.exec(err.message);
|
|
3187
|
+
if (match) {
|
|
3188
|
+
const reason = ctx.crossFileFailures.get(match[1]);
|
|
3189
|
+
if (reason)
|
|
3190
|
+
return `${err.message} (${reason})`;
|
|
3191
|
+
}
|
|
3192
|
+
// chant#2423 — the reference rejected because the pre-build never produced
|
|
3193
|
+
// the instance. Say what stopped the pre-build, at the line it stopped on.
|
|
3194
|
+
const sameFile = SAME_FILE_RESOURCE_RE.exec(err.message);
|
|
3195
|
+
if (sameFile) {
|
|
3196
|
+
const cause = ctx.prebuildFailures?.get(sameFile[1]);
|
|
3197
|
+
if (cause)
|
|
3198
|
+
return `${err.message} (${cause})`;
|
|
3199
|
+
}
|
|
3200
|
+
return err.message;
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* chant #1054 — a short, single-line label for a destructured export's
|
|
3204
|
+
* source expression, for a fold fallback reason: the callee plus `(...)`
|
|
3205
|
+
* for the common composite-call source (`GkeCluster(...)`), or a brief,
|
|
3206
|
+
* bounded rendering of whatever else it is otherwise. Never the source's own
|
|
3207
|
+
* `getText()` — for a real composite call that's the entire multi-line
|
|
3208
|
+
* argument list.
|
|
3209
|
+
*/
|
|
3210
|
+
function describeDestructureSource(node) {
|
|
3211
|
+
return ts.isCallExpression(node) ? `${briefNodeText(node.expression)}(...)` : briefNodeText(node);
|
|
3212
|
+
}
|
|
3213
|
+
/** Build a located `FoldError`'s formatted "line:col - message" string anchored at `node` — for a cross-file failure detected here in fold-import.ts (an import cycle, a name genuinely absent from the target module's exports) rather than inside `fold()` itself. */
|
|
3214
|
+
function locatedMessage(node, message) {
|
|
3215
|
+
const { line, column } = locate(node);
|
|
3216
|
+
return new FoldError(message, line, column).message;
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Fold `file` with session-wide memoization and cycle detection. The SAME
|
|
3220
|
+
* promise is handed back to every caller that asks for `file` within one
|
|
3221
|
+
* session — `discover()`'s own top-level per-file loop AND any cross-file
|
|
3222
|
+
* reference that reaches into `file` alike — so `file` is folded exactly
|
|
3223
|
+
* once and every referrer shares the identical constructed entities.
|
|
3224
|
+
*/
|
|
3225
|
+
/**
|
|
3226
|
+
* A backstop, not a realistic limit: no legitimate project should have an
|
|
3227
|
+
* import chain this deep. Exists purely so a FUTURE regression that breaks
|
|
3228
|
+
* memoization (e.g. caching by the wrong key, or checking `stack` after
|
|
3229
|
+
* awaiting instead of before) fails loudly and immediately with a
|
|
3230
|
+
* diagnosable error, instead of recursing until the process runs out of
|
|
3231
|
+
* stack/memory or simply never terminates — a correctness bug in this exact
|
|
3232
|
+
* function is a termination hazard, not just a wrong-answer one, since
|
|
3233
|
+
* `foldFileMemoized` is the one place cross-file resolution can recurse.
|
|
3234
|
+
*/
|
|
3235
|
+
const MAX_RESOLUTION_DEPTH = 200;
|
|
3236
|
+
function foldFileMemoized(file, session) {
|
|
3237
|
+
const cycleStart = session.stack.indexOf(file);
|
|
3238
|
+
if (cycleStart !== -1) {
|
|
3239
|
+
// A file currently being resolved (an ancestor of this very call, still
|
|
3240
|
+
// on the stack) is needed again — a genuine reference cycle, not merely
|
|
3241
|
+
// two files that happen to import each other with no real value
|
|
3242
|
+
// dependency (that could never re-enter here for an ancestor). The
|
|
3243
|
+
// caller ({@link buildExternals}/the re-export declarator handler, which
|
|
3244
|
+
// has the referencing import's own specifier node) attaches a source
|
|
3245
|
+
// position to this via {@link locatedMessage}; here we only have the
|
|
3246
|
+
// file-level chain to name.
|
|
3247
|
+
const cycle = [...session.stack.slice(cycleStart), file].map((f) => basename(f));
|
|
3248
|
+
return Promise.resolve({ ok: false, reason: `import cycle: ${cycle.join(" -> ")}` });
|
|
3249
|
+
}
|
|
3250
|
+
const cached = session.cache.get(file);
|
|
3251
|
+
if (cached)
|
|
3252
|
+
return cached;
|
|
3253
|
+
if (session.stack.length >= MAX_RESOLUTION_DEPTH) {
|
|
3254
|
+
const chain = [...session.stack.slice(-5), file].map((f) => basename(f));
|
|
3255
|
+
return Promise.resolve({
|
|
3256
|
+
ok: false,
|
|
3257
|
+
reason: `cross-file resolution depth exceeded ${MAX_RESOLUTION_DEPTH} ` +
|
|
3258
|
+
`(...-> ${chain.join(" -> ")}) — this is almost certainly a resolution ` +
|
|
3259
|
+
`bug (memoization not taking effect), not a genuinely thousand-file-deep import chain`,
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
session.stack.push(file);
|
|
3263
|
+
const promise = tryFoldFileCore(file, session).finally(() => {
|
|
3264
|
+
const idx = session.stack.lastIndexOf(file);
|
|
3265
|
+
if (idx !== -1)
|
|
3266
|
+
session.stack.splice(idx, 1);
|
|
3267
|
+
});
|
|
3268
|
+
session.cache.set(file, promise);
|
|
3269
|
+
return promise;
|
|
3270
|
+
}
|
|
3271
|
+
/** True for a relative or absolute specifier — a sibling PROJECT file, as opposed to a bare package specifier (an installed lexicon/vendor module). */
|
|
3272
|
+
function isProjectFileSpecifier(specifier) {
|
|
3273
|
+
return specifier.startsWith(".") || isAbsolute(specifier);
|
|
3274
|
+
}
|
|
3275
|
+
/**
|
|
3276
|
+
* chant#3090 — {@link isProjectFileSpecifier} for an import in `fromFile`,
|
|
3277
|
+
* counting a bare specifier that a tsconfig `paths` entry maps to a file
|
|
3278
|
+
* ({@link pathsMappedTarget}) as the project file it names.
|
|
3279
|
+
*/
|
|
3280
|
+
function isProjectImport(specifier, fromFile) {
|
|
3281
|
+
return isProjectFileSpecifier(specifier) || pathsMappedTarget(specifier, fromFile) !== undefined;
|
|
3282
|
+
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Eagerly resolve every project-file import binding of `file`'s to its real
|
|
3285
|
+
* cross-file value (see this section's own doc above). A binding that isn't
|
|
3286
|
+
* a project-file specifier, doesn't resolve to a file on disk, or whose
|
|
3287
|
+
* target doesn't fold or doesn't actually export the requested name is
|
|
3288
|
+
* simply left OUT of `externals` — exactly like a name that was never
|
|
3289
|
+
* imported at all, `fold()`'s ordinary "unresolved identifier" failure still
|
|
3290
|
+
* fires if (and only if) that name is actually referenced. `failures`
|
|
3291
|
+
* records WHY, purely to enrich that later message (see
|
|
3292
|
+
* {@link describeFoldFailure}).
|
|
3293
|
+
*/
|
|
3294
|
+
async function buildExternals(file, imports, namespaceImports, session,
|
|
3295
|
+
/**
|
|
3296
|
+
* chant#3247 — trusted lexicon directories beyond the session's own
|
|
3297
|
+
* {@link FoldSession.lexiconRoots}: the package root of a lexicon-package
|
|
3298
|
+
* composite whose defining module this is. Files under one are treated as
|
|
3299
|
+
* a path lexicon's are, never folded as project files.
|
|
3300
|
+
*/
|
|
3301
|
+
extraLexiconRoots = []) {
|
|
3302
|
+
const lexiconRoots = extraLexiconRoots.length === 0 ? session.lexiconRoots : [...session.lexiconRoots, ...extraLexiconRoots];
|
|
3303
|
+
const externals = new Map();
|
|
3304
|
+
const failures = new Map();
|
|
3305
|
+
// chant #1044 — see `FoldFileResult.liveSources`.
|
|
3306
|
+
const liveSources = new Set();
|
|
3307
|
+
const hostValues = new Set();
|
|
3308
|
+
for (const [localName, binding] of imports) {
|
|
3309
|
+
// chant #1064 — a named `params` import that resolves to chant-core's own
|
|
3310
|
+
// build-time-parameters module (../params.ts) is substituted directly
|
|
3311
|
+
// from this build's already-resolved values, with NO import performed —
|
|
3312
|
+
// so `params.tier` folds to a LITERAL rather than a symbolic node
|
|
3313
|
+
// (contrast the `{__symbol}` deferral a pseudo-parameter namespace import
|
|
3314
|
+
// like `AWS.StackName` gets — that genuinely can't resolve until a real
|
|
3315
|
+
// module runs; a build parameter's value is already fully known here).
|
|
3316
|
+
//
|
|
3317
|
+
// A BARE specifier is recognized by an exact TEXT match against the one
|
|
3318
|
+
// real published subpath ({@link PARAMS_BARE_SPECIFIER}), NEVER by
|
|
3319
|
+
// resolving it: `resolveModulePathMemoized`'s bare-specifier branch falls
|
|
3320
|
+
// through to Node's own package resolution
|
|
3321
|
+
// (`createRequire(fromFile).resolve(specifier)`), which chant#1020's own
|
|
3322
|
+
// fix-history (see this module's other comments) measured at up to ~361s
|
|
3323
|
+
// for the FIRST resolution of a genuinely new bare specifier in a
|
|
3324
|
+
// process — a cost `buildExternals` previously never paid at all for
|
|
3325
|
+
// bare specifiers (the pre-#1064 code skipped them outright). Since
|
|
3326
|
+
// `binding.imported === "params"` alone says nothing about which package
|
|
3327
|
+
// a project actually imported from, resolving EVERY such bare specifier
|
|
3328
|
+
// to check it would reintroduce exactly that pathological cost for any
|
|
3329
|
+
// corpus/project file that happens to import a same-named binding from
|
|
3330
|
+
// an unrelated package — a real, measured regression this text-match
|
|
3331
|
+
// avoids entirely (no filesystem/package resolution for a bare
|
|
3332
|
+
// specifier, ever, in this branch).
|
|
3333
|
+
//
|
|
3334
|
+
// A RELATIVE/ABSOLUTE specifier is still resolved and path-compared
|
|
3335
|
+
// against {@link paramsModulePath} — that resolution is always cheap
|
|
3336
|
+
// (`existsSync`/`statSync` candidate probing, never Node's package
|
|
3337
|
+
// resolution), so it's safe for this module's own absolute-path test
|
|
3338
|
+
// fixtures to exercise the identical substitution a real bare import
|
|
3339
|
+
// takes, without the bare-specifier cost concern applying.
|
|
3340
|
+
if (session.buildParams && binding.imported === "params") {
|
|
3341
|
+
if (binding.specifier === PARAMS_BARE_SPECIFIER) {
|
|
3342
|
+
externals.set(localName, session.buildParams);
|
|
3343
|
+
continue;
|
|
3344
|
+
}
|
|
3345
|
+
if (isProjectImport(binding.specifier, file)) {
|
|
3346
|
+
try {
|
|
3347
|
+
const targetPath = resolveModulePathMemoized(binding.specifier, file, session.resolvePathCache);
|
|
3348
|
+
if (targetPath === paramsModulePath()) {
|
|
3349
|
+
externals.set(localName, session.buildParams);
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
catch {
|
|
3354
|
+
// Unresolvable specifier — fall through to the ordinary handling
|
|
3355
|
+
// below, same as any other import this loop can't resolve.
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
if (!isProjectImport(binding.specifier, file)) {
|
|
3360
|
+
// chant #1063 — a bare specifier naming one of THIS BUILD's active
|
|
3361
|
+
// lexicon packages resolves to that package's real export, so a plain
|
|
3362
|
+
// data export a lexicon publishes (`Azure`/`GCP`'s pseudo-parameter
|
|
3363
|
+
// namespaces, AWS's `S3Actions`, gitlab's `CI`) is an ordinary
|
|
3364
|
+
// identifier value here rather than fold's most common remaining
|
|
3365
|
+
// "unresolved identifier" failure. See
|
|
3366
|
+
// {@link resolveActiveLexiconExport} for the allowlist, the no-cold-
|
|
3367
|
+
// resolution rule, and why callable exports stay out.
|
|
3368
|
+
//
|
|
3369
|
+
// No `liveSources` edge is recorded for what comes back, unlike the
|
|
3370
|
+
// project-file case just below. `liveSources` (chant #1044) exists so
|
|
3371
|
+
// that a folded file which captured ANOTHER FILE's objects is
|
|
3372
|
+
// invalidated when that file is forced back to run — a fold/run
|
|
3373
|
+
// disagreement about identity. A lexicon package has no such duality:
|
|
3374
|
+
// it is not a discovered source file, `planFoldTaint` never considers
|
|
3375
|
+
// it (it filters to the discovered `files` set), it never falls back to
|
|
3376
|
+
// run, and both paths reach it through the identical un-cache-busted
|
|
3377
|
+
// `import()` of the identical resolved path — so the object fold
|
|
3378
|
+
// captures IS the object the run path holds. There is nothing for the
|
|
3379
|
+
// two sides to disagree about, hence nothing to taint.
|
|
3380
|
+
const lexiconExport = await resolveActiveLexiconExport(binding, file, session);
|
|
3381
|
+
if (lexiconExport) {
|
|
3382
|
+
externals.set(localName, lexiconExport.value);
|
|
3383
|
+
hostValues.add(localName);
|
|
3384
|
+
continue;
|
|
3385
|
+
}
|
|
3386
|
+
// Every other bare specifier (a non-lexicon vendor package, a lexicon
|
|
3387
|
+
// this build didn't load) is left alone here, exactly as before #1064:
|
|
3388
|
+
// it resolves lazily, through the pre-existing `importModule`
|
|
3389
|
+
// mechanism, only once a constructor/composite-factory/intrinsic tag
|
|
3390
|
+
// actually consumes it.
|
|
3391
|
+
//
|
|
3392
|
+
// chant#3090 — except that a tsconfig `paths` entry which matches the
|
|
3393
|
+
// specifier but names no file is recorded, so the reason says which
|
|
3394
|
+
// import failed and why.
|
|
3395
|
+
const unmatched = pathsMatch(binding.specifier, file);
|
|
3396
|
+
if (unmatched) {
|
|
3397
|
+
failures.set(localName, locatedMessage(binding.specifierNode, `tsconfig paths entry "${unmatched.key}" matches "${binding.specifier}" but names no file`));
|
|
3398
|
+
}
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
let targetPath;
|
|
3402
|
+
try {
|
|
3403
|
+
targetPath = resolveModulePathMemoized(binding.specifier, file, session.resolvePathCache);
|
|
3404
|
+
}
|
|
3405
|
+
catch {
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
// chant#2577 — a path-declared lexicon is a lexicon, not a project file:
|
|
3409
|
+
// resolve its data export the way the bare-specifier branch above does,
|
|
3410
|
+
// with no `liveSources` edge for the same reason.
|
|
3411
|
+
if (session.lexiconModules.has(targetPath)) {
|
|
3412
|
+
const lexiconExport = await resolveActiveLexiconExport(binding, file, session);
|
|
3413
|
+
if (lexiconExport) {
|
|
3414
|
+
externals.set(localName, lexiconExport.value);
|
|
3415
|
+
hostValues.add(localName);
|
|
3416
|
+
}
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
// chant#2590 — another file of a multi-file path lexicon is a lexicon
|
|
3420
|
+
// subpath, not a project file: it is not folded, and like a package
|
|
3421
|
+
// subpath (see the bare-specifier branch above) its data exports are not
|
|
3422
|
+
// read as values. A constructor or composite from it resolves through the
|
|
3423
|
+
// import path, where `isTrustedExecutableBinding` admits it.
|
|
3424
|
+
//
|
|
3425
|
+
// chant#3247 — a file of the lexicon PACKAGE whose composite is being
|
|
3426
|
+
// interpreted is that package's own module, which `loadPlugins` already
|
|
3427
|
+
// imported through the package's barrel. Its plain data exports (an
|
|
3428
|
+
// action table, a policy constant) are read off the real module, so the
|
|
3429
|
+
// value is the run path's own object, identity included. Callables stay
|
|
3430
|
+
// out, as {@link resolveActiveLexiconExport} keeps them out: a
|
|
3431
|
+
// constructor or composite resolves through the import path.
|
|
3432
|
+
if (inPathLexiconRoot(targetPath, extraLexiconRoots)) {
|
|
3433
|
+
const value = await packageDataExport(targetPath, binding.imported, session);
|
|
3434
|
+
if (value) {
|
|
3435
|
+
externals.set(localName, value.value);
|
|
3436
|
+
hostValues.add(localName);
|
|
3437
|
+
}
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
if (inPathLexiconRoot(targetPath, lexiconRoots))
|
|
3441
|
+
continue;
|
|
3442
|
+
const result = await foldFileMemoized(targetPath, session);
|
|
3443
|
+
if (!result.ok) {
|
|
3444
|
+
failures.set(localName, locatedMessage(binding.specifierNode, result.reason));
|
|
3445
|
+
continue;
|
|
3446
|
+
}
|
|
3447
|
+
if (result.exportedValues.has(binding.imported)) {
|
|
3448
|
+
const value = result.exportedValues.get(binding.imported);
|
|
3449
|
+
externals.set(localName, value);
|
|
3450
|
+
if (hasObjectIdentity(value))
|
|
3451
|
+
liveSources.add(targetPath);
|
|
3452
|
+
}
|
|
3453
|
+
else {
|
|
3454
|
+
failures.set(localName, locatedMessage(binding.specifierNode, `"${binding.imported}" is not exported by "${binding.specifier}"`));
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
for (const [localName, binding] of namespaceImports) {
|
|
3458
|
+
if (!isProjectImport(binding.specifier, file))
|
|
3459
|
+
continue;
|
|
3460
|
+
let targetPath;
|
|
3461
|
+
try {
|
|
3462
|
+
targetPath = resolveModulePathMemoized(binding.specifier, file, session.resolvePathCache);
|
|
3463
|
+
}
|
|
3464
|
+
catch {
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
// chant#2577 — a namespace import of a lexicon is left alone, as the
|
|
3468
|
+
// bare-specifier check above leaves a package's alone.
|
|
3469
|
+
if (session.lexiconModules.has(targetPath) || inPathLexiconRoot(targetPath, lexiconRoots))
|
|
3470
|
+
continue;
|
|
3471
|
+
const result = await foldFileMemoized(targetPath, session);
|
|
3472
|
+
if (!result.ok) {
|
|
3473
|
+
failures.set(localName, locatedMessage(binding.specifierNode, result.reason));
|
|
3474
|
+
continue;
|
|
3475
|
+
}
|
|
3476
|
+
// A plain object wrapping the target's own exported values — property
|
|
3477
|
+
// access on it (`ns.someExport`) is then just an ordinary bracket index,
|
|
3478
|
+
// exactly like on a real composite instance (see `isIndexableObject`).
|
|
3479
|
+
externals.set(localName, Object.fromEntries(result.exportedValues));
|
|
3480
|
+
for (const value of result.exportedValues.values()) {
|
|
3481
|
+
if (hasObjectIdentity(value)) {
|
|
3482
|
+
liveSources.add(targetPath);
|
|
3483
|
+
break;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
return { externals, failures, liveSources, hostValues };
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* True when `value` is something whose IDENTITY matters across the
|
|
3491
|
+
* fold/run boundary — any object or function, as opposed to a primitive
|
|
3492
|
+
* (chant #1044). Deliberately coarse: an object that merely *contains* a
|
|
3493
|
+
* `Declarable` is as identity-bearing as the Declarable itself, and cheaply
|
|
3494
|
+
* treating every object as such avoids a deep walk whose only payoff would
|
|
3495
|
+
* be keeping a handful of extra files folded inside an entry that already
|
|
3496
|
+
* falls back. See {@link FoldFileResult.liveSources}.
|
|
3497
|
+
*/
|
|
3498
|
+
function hasObjectIdentity(value) {
|
|
3499
|
+
// chant #1373 — a foldable-function marker is a description of source, not
|
|
3500
|
+
// an object the run path could hold a different copy of: a call to it
|
|
3501
|
+
// produces the same value folded or run. No identity to disagree about.
|
|
3502
|
+
if (isFoldableFunction(value))
|
|
3503
|
+
return false;
|
|
3504
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Resolve a declarator's initializer to its final value: first via the
|
|
3508
|
+
* "live spine" ({@link resolveLiveValue} — same-file composite-call
|
|
3509
|
+
* navigation, and, chant #1020, a cross-file identifier/property-access
|
|
3510
|
+
* chain through `ctx.externals`); when that shape isn't recognized at all
|
|
3511
|
+
* (a plain literal, object/array literal, template, binary expression, …),
|
|
3512
|
+
* via the general reducer ({@link fold} + intrinsic revival) — chant #1020:
|
|
3513
|
+
* this is what lets `export const REGION = "us-east-1";` (and an IMPORTED
|
|
3514
|
+
* REGION used the same way) fold, not just a `new Type(...)`/composite-call
|
|
3515
|
+
* shape. Throws when a shape WAS recognized but resolution genuinely failed
|
|
3516
|
+
* (unresolved import, non-function import, a nested argument's own fold
|
|
3517
|
+
* failed, an unresolved identifier, …) — the caller falls the whole file
|
|
3518
|
+
* back to run, exactly as before #1020.
|
|
3519
|
+
*/
|
|
3520
|
+
async function resolveDeclaratorValue(node, ctx) {
|
|
3521
|
+
// chant#3325 — memoized, so an exported `const x = Composite(...)` and a
|
|
3522
|
+
// reference to `x` elsewhere in the file hold the one instance.
|
|
3523
|
+
const live = await resolveMemoized(node, ctx);
|
|
3524
|
+
if (live !== undefined)
|
|
3525
|
+
return live;
|
|
3526
|
+
const folded = fold(node, ctx.consts, ctx.intrinsics, ctx.externals);
|
|
3527
|
+
return { value: await reviveFoldedValue(folded, ctx, false) };
|
|
3528
|
+
}
|
|
3529
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3530
|
+
// Entry point.
|
|
3531
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3532
|
+
/**
|
|
3533
|
+
* Attempt to fold one source file with zero execution of its own top-level
|
|
3534
|
+
* code, sharing `session`'s cross-file memoization (chant #1020) — see
|
|
3535
|
+
* {@link foldFileMemoized}. Internal; {@link tryFoldFile} is the public
|
|
3536
|
+
* entry point below.
|
|
3537
|
+
*/
|
|
3538
|
+
async function tryFoldFileCore(file, session) {
|
|
3539
|
+
// chant #1373 — chant-core's own modules are library code, not project
|
|
3540
|
+
// source: their exports are reached by importing them (a helper, a
|
|
3541
|
+
// constructor, `params`), never by folding their bodies. Before #1373 every
|
|
3542
|
+
// one of them happened to disqualify itself at the scan (each exports a
|
|
3543
|
+
// function); now that an exported function is a foldable shape, the
|
|
3544
|
+
// exclusion has to be explicit, or a fixture/in-repo import by path would
|
|
3545
|
+
// walk fold into chant's own tree and fold `params` to an empty object.
|
|
3546
|
+
{
|
|
3547
|
+
if (isChantCorePath(file)) {
|
|
3548
|
+
return { ok: false, reason: "chant's own module is not project source" };
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
try {
|
|
3552
|
+
const source = await readFile(file, "utf-8");
|
|
3553
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
3554
|
+
const scan = scanExports(sourceFile);
|
|
3555
|
+
if (scan.unfoldableReason)
|
|
3556
|
+
return { ok: false, reason: scan.unfoldableReason };
|
|
3557
|
+
if (scan.declarators.length === 0)
|
|
3558
|
+
return { ok: false, reason: "no foldable resource exports" };
|
|
3559
|
+
const collected = collectImports(sourceFile);
|
|
3560
|
+
const { externals, failures, liveSources, hostValues } = await buildExternals(file, collected.named, collected.namespaces, session);
|
|
3561
|
+
const ctx = {
|
|
3562
|
+
file,
|
|
3563
|
+
consts: collectConsts(sourceFile),
|
|
3564
|
+
locals: collectLocalBindings(sourceFile),
|
|
3565
|
+
imports: collected.named,
|
|
3566
|
+
namespaceImports: collected.namespaces,
|
|
3567
|
+
memo: new Map(),
|
|
3568
|
+
intrinsics: session.intrinsics,
|
|
3569
|
+
externals,
|
|
3570
|
+
hostValues,
|
|
3571
|
+
crossFileFailures: failures,
|
|
3572
|
+
importCache: session.importCache,
|
|
3573
|
+
resolvePathCache: session.resolvePathCache,
|
|
3574
|
+
lexiconPackages: session.lexiconPackages,
|
|
3575
|
+
lexiconModules: session.lexiconModules,
|
|
3576
|
+
lexiconRoots: session.lexiconRoots,
|
|
3577
|
+
sandbox: session.sandbox,
|
|
3578
|
+
executing: session.executing,
|
|
3579
|
+
session,
|
|
3580
|
+
interpretDepth: 0,
|
|
3581
|
+
// chant#2423 — filled by the pre-build below, read by
|
|
3582
|
+
// `describeFoldFailure` when a reference rejects for a const it could
|
|
3583
|
+
// not build.
|
|
3584
|
+
prebuildFailures: new Map(),
|
|
3585
|
+
};
|
|
3586
|
+
// chant #1169 — every same-file `const x = new Type(...)`, built once, in
|
|
3587
|
+
// source order, before anything references one. See
|
|
3588
|
+
// {@link preresolveResourceConsts}.
|
|
3589
|
+
const prebuiltResources = await preresolveResourceConsts(ctx);
|
|
3590
|
+
// chant #1373 — this file's own functions, callable from its own folds
|
|
3591
|
+
// (and, once exported, from an importer's). Registered after the
|
|
3592
|
+
// resource pre-pass so a marker never shadows a pre-built instance; a
|
|
3593
|
+
// function and a const cannot share a name in valid TypeScript anyway.
|
|
3594
|
+
const localFunctions = collectLocalFunctions(sourceFile, ctx);
|
|
3595
|
+
for (const [name, marker] of localFunctions) {
|
|
3596
|
+
if (!ctx.externals.has(name))
|
|
3597
|
+
ctx.externals.set(name, marker);
|
|
3598
|
+
}
|
|
3599
|
+
const entities = [];
|
|
3600
|
+
const exportedValues = new Map();
|
|
3601
|
+
for (const decl of scan.declarators) {
|
|
3602
|
+
if (decl.kind === "function") {
|
|
3603
|
+
applyResolvedValue(decl.name, localFunctions.get(decl.name), entities, exportedValues);
|
|
3604
|
+
continue;
|
|
3605
|
+
}
|
|
3606
|
+
if (decl.kind === "resource") {
|
|
3607
|
+
// chant #1169 — the pre-pass already built this exact node. Reuse that
|
|
3608
|
+
// instance rather than constructing a second one: a sibling prop that
|
|
3609
|
+
// referenced this resource by name holds the pre-pass object, and if
|
|
3610
|
+
// the entity discovery registers were a different one, the reference
|
|
3611
|
+
// would have no logical name to resolve against.
|
|
3612
|
+
const prebuilt = prebuiltResources.get(decl.node);
|
|
3613
|
+
if (prebuilt !== undefined) {
|
|
3614
|
+
applyResolvedValue(decl.name, prebuilt, entities, exportedValues);
|
|
3615
|
+
continue;
|
|
3616
|
+
}
|
|
3617
|
+
const result = await resolveResourceEntity(decl.name, decl.node, ctx);
|
|
3618
|
+
if (!result.ok)
|
|
3619
|
+
return result;
|
|
3620
|
+
applyResolvedValue(decl.name, result.entity, entities, exportedValues);
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3623
|
+
if (decl.kind === "single") {
|
|
3624
|
+
// chant #1373 — `export const f = (…) => …` exports the function
|
|
3625
|
+
// marker, exactly like `export function f`; so does an alias of one
|
|
3626
|
+
// (`export const g = f`, `f` declared here or imported).
|
|
3627
|
+
const aliased = ts.isIdentifier(decl.node) ? ctx.externals.get(decl.node.text) : undefined;
|
|
3628
|
+
const marker = localFunctions.get(decl.name) ?? (isFoldableFunction(aliased) ? aliased : undefined);
|
|
3629
|
+
if (marker) {
|
|
3630
|
+
applyResolvedValue(decl.name, marker, entities, exportedValues);
|
|
3631
|
+
continue;
|
|
3632
|
+
}
|
|
3633
|
+
// chant #3196 — a registered tagged template the pre-pass built.
|
|
3634
|
+
if (prebuiltResources.has(decl.node)) {
|
|
3635
|
+
applyResolvedValue(decl.name, prebuiltResources.get(decl.node), entities, exportedValues);
|
|
3636
|
+
continue;
|
|
3637
|
+
}
|
|
3638
|
+
let value;
|
|
3639
|
+
try {
|
|
3640
|
+
value = (await resolveDeclaratorValue(decl.node, ctx)).value;
|
|
3641
|
+
}
|
|
3642
|
+
catch (err) {
|
|
3643
|
+
return { ok: false, reason: `"${decl.name}" is not foldable: ${describeFoldFailure(err, ctx)}` };
|
|
3644
|
+
}
|
|
3645
|
+
applyResolvedValue(decl.name, value, entities, exportedValues);
|
|
3646
|
+
continue;
|
|
3647
|
+
}
|
|
3648
|
+
if (decl.kind === "destructure") {
|
|
3649
|
+
let value;
|
|
3650
|
+
// chant #1054 — identify the destructured export by its BINDING
|
|
3651
|
+
// NAMES and the source's callee (`"cluster, nodePool" (destructured
|
|
3652
|
+
// from GkeCluster(...))`), never `decl.node.getText()`: for a real
|
|
3653
|
+
// composite call that's the entire multi-line source, which buries
|
|
3654
|
+
// the actual error after it and breaks any line-oriented consumer of
|
|
3655
|
+
// `[fold:run]` output.
|
|
3656
|
+
const boundNames = decl.elements.map((el) => el.bindingName).join(", ");
|
|
3657
|
+
const source = describeDestructureSource(decl.node);
|
|
3658
|
+
try {
|
|
3659
|
+
value = (await resolveDeclaratorValue(decl.node, ctx)).value;
|
|
3660
|
+
}
|
|
3661
|
+
catch (err) {
|
|
3662
|
+
return {
|
|
3663
|
+
ok: false,
|
|
3664
|
+
reason: `"${boundNames}" (destructured from ${source}) is not foldable: ${describeFoldFailure(err, ctx)}`,
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
if (!isIndexableObject(value)) {
|
|
3668
|
+
return {
|
|
3669
|
+
ok: false,
|
|
3670
|
+
reason: `"${boundNames}" (destructured from ${source}) is not foldable: not a composite call or object`,
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
for (const { propKey, bindingName } of decl.elements) {
|
|
3674
|
+
applyResolvedValue(bindingName, value[propKey], entities, exportedValues);
|
|
3675
|
+
}
|
|
3676
|
+
continue;
|
|
3677
|
+
}
|
|
3678
|
+
if (decl.kind === "named-export") {
|
|
3679
|
+
// Reuses resolveDeclaratorValue's own identifier handling
|
|
3680
|
+
// (ctx.locals/ctx.externals lookup + memoized destructured-member
|
|
3681
|
+
// extraction) rather than duplicating it here.
|
|
3682
|
+
for (const { localNameNode, exportedName } of decl.elements) {
|
|
3683
|
+
// chant #1373 — `function f() {…}; export { f }`.
|
|
3684
|
+
const marker = localFunctions.get(localNameNode.text);
|
|
3685
|
+
if (marker) {
|
|
3686
|
+
applyResolvedValue(exportedName, marker, entities, exportedValues);
|
|
3687
|
+
continue;
|
|
3688
|
+
}
|
|
3689
|
+
let value;
|
|
3690
|
+
try {
|
|
3691
|
+
value = (await resolveDeclaratorValue(localNameNode, ctx)).value;
|
|
3692
|
+
}
|
|
3693
|
+
catch (err) {
|
|
3694
|
+
return { ok: false, reason: `exported "${exportedName}" is not foldable: ${describeFoldFailure(err, ctx)}` };
|
|
3695
|
+
}
|
|
3696
|
+
applyResolvedValue(exportedName, value, entities, exportedValues);
|
|
3697
|
+
}
|
|
3698
|
+
continue;
|
|
3699
|
+
}
|
|
3700
|
+
// decl.kind === "re-export" (chant #1020) — resolve each element
|
|
3701
|
+
// through the target module's OWN fold, memoized via the session
|
|
3702
|
+
// exactly like any other cross-file reference (see `foldFileMemoized`
|
|
3703
|
+
// above); a cycle surfaces here too since a re-export is just another
|
|
3704
|
+
// edge in the same module graph.
|
|
3705
|
+
let targetPath;
|
|
3706
|
+
try {
|
|
3707
|
+
targetPath = resolveModulePathMemoized(decl.specifier, file, session.resolvePathCache);
|
|
3708
|
+
}
|
|
3709
|
+
catch (err) {
|
|
3710
|
+
return {
|
|
3711
|
+
ok: false,
|
|
3712
|
+
reason: `could not resolve re-export "${decl.specifier}": ${err instanceof Error ? err.message : String(err)}`,
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3715
|
+
const result = await foldFileMemoized(targetPath, session);
|
|
3716
|
+
if (!result.ok) {
|
|
3717
|
+
return {
|
|
3718
|
+
ok: false,
|
|
3719
|
+
reason: `re-export from "${decl.specifier}" is not foldable: ${locatedMessage(decl.specifierNode, result.reason)}`,
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
for (const { imported, exportedName } of decl.elements) {
|
|
3723
|
+
if (!result.exportedValues.has(imported)) {
|
|
3724
|
+
return {
|
|
3725
|
+
ok: false,
|
|
3726
|
+
reason: locatedMessage(decl.specifierNode, `"${imported}" is not exported by "${decl.specifier}"`),
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
const value = result.exportedValues.get(imported);
|
|
3730
|
+
// chant #1044 — a re-export hands another file's OBJECT straight
|
|
3731
|
+
// through under this file's name, so it is a live-identity edge
|
|
3732
|
+
// exactly like an imported binding is (see `liveSources`).
|
|
3733
|
+
if (hasObjectIdentity(value))
|
|
3734
|
+
liveSources.add(targetPath);
|
|
3735
|
+
applyResolvedValue(exportedName, value, entities, exportedValues);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
return { ok: true, entities, exportedValues, liveSources };
|
|
3739
|
+
}
|
|
3740
|
+
catch (err) {
|
|
3741
|
+
// Any unexpected failure degrades to "fall back to run" rather than
|
|
3742
|
+
// taking discovery down with it — fold is opt-in, not a new failure mode.
|
|
3743
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Attempt to fold one source file with zero execution of its own top-level
|
|
3748
|
+
* code. Returns the folded, instantiated entities on success, or a reason
|
|
3749
|
+
* to fall back to the run path (`importModule`) on the first construct
|
|
3750
|
+
* outside the fold subset.
|
|
3751
|
+
*
|
|
3752
|
+
* @param intrinsics - Lexicon-registered intrinsic tags (chant #1039), e.g.
|
|
3753
|
+
* AWS's `Sub`. Threaded down to {@link fold}/{@link foldResource} so a
|
|
3754
|
+
* registered tagged template folds instead of unconditionally throwing
|
|
3755
|
+
* "unregistered tagged template intrinsic". Defaults to none — the caller
|
|
3756
|
+
* (`discover()`, ultimately `chant build --fold`) is expected to pass the
|
|
3757
|
+
* target lexicons' combined `intrinsics()`. Ignored when `session` is
|
|
3758
|
+
* given (its own `intrinsics`, fixed at creation, apply instead).
|
|
3759
|
+
* @param session - chant #1020: share ONE {@link FoldSession} across every
|
|
3760
|
+
* file in a build (as `discover()` does) so a project file imported by
|
|
3761
|
+
* several others folds exactly once and every referrer shares the same
|
|
3762
|
+
* constructed entities — see {@link FoldSession}'s doc. Omit for a
|
|
3763
|
+
* standalone, single-file fold attempt (creates a private session scoped
|
|
3764
|
+
* to just this call — a cross-file reference reachable from `file` still
|
|
3765
|
+
* resolves, just without sharing its cache with any other top-level call).
|
|
3766
|
+
*/
|
|
3767
|
+
export async function tryFoldFile(file, intrinsics = [], session) {
|
|
3768
|
+
return foldFileMemoized(file, session ?? createFoldSession(intrinsics));
|
|
3769
|
+
}
|
|
3770
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3771
|
+
// Cross-file fold/run identity hazard (#1023).
|
|
3772
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3773
|
+
/**
|
|
3774
|
+
* A file that folds successfully in isolation must still be forced back to
|
|
3775
|
+
* run if some OTHER discovered file — one that itself falls back to run —
|
|
3776
|
+
* imports it (directly, or transitively through another sibling file it
|
|
3777
|
+
* imports). Composite folding is what makes this reachable: `network.ts`
|
|
3778
|
+
* exporting `export const network = VpcDefault({})` folds cleanly on its
|
|
3779
|
+
* own (no cross-file reference needed), landing its Declarables (`vpc`, its
|
|
3780
|
+
* subnets, …) in `entities` as ONE set of real objects, built by literally
|
|
3781
|
+
* invoking `VpcDefault` from inside `tryFoldFile` — bypassing `network.ts`
|
|
3782
|
+
* as a module entirely (that's the whole point of folding it).
|
|
3783
|
+
*
|
|
3784
|
+
* Before chant #1020, a sibling file like `alb.ts` that did
|
|
3785
|
+
* `import { network } from "./network"; ... vpcId: network.vpc.VpcId`
|
|
3786
|
+
* could never fold that cross-file reference and fell back to run — but
|
|
3787
|
+
* running `alb.ts` for real re-executes `import "./network"` for real too,
|
|
3788
|
+
* which (via Node's module cache) produces the SAME `network.ts` module
|
|
3789
|
+
* instance for every OTHER run-fallback file that imports it, but a
|
|
3790
|
+
* DIFFERENT one than the object `tryFoldFile("network.ts")` already built.
|
|
3791
|
+
* `alb.ts`'s AttrRef for `network.vpc.VpcId` then points at an object
|
|
3792
|
+
* that's never in the `entities` map (only the folded one is), so it can
|
|
3793
|
+
* never be assigned a logical name and serialization fails outright — not
|
|
3794
|
+
* drift, a crash.
|
|
3795
|
+
*
|
|
3796
|
+
* The fix: fold and run must never disagree about which object identity a
|
|
3797
|
+
* given file's exports have. Since `network.ts` itself doesn't need
|
|
3798
|
+
* anything cross-file to fold (only a file's own successful fold could ever
|
|
3799
|
+
* reach this taint — an unresolvable cross-file reference already fails
|
|
3800
|
+
* that file's OWN fold attempt), the safe rule is to force `network.ts`
|
|
3801
|
+
* back to run too, so both `alb.ts`'s real import and `network.ts`'s own
|
|
3802
|
+
* discovery entry resolve through the exact same `importModule` call and
|
|
3803
|
+
* share the exact same singleton module instance. This has to propagate
|
|
3804
|
+
* transitively (if `alb.ts` itself is only reachable by importing a file
|
|
3805
|
+
* that imports `network.ts`), so this is a forward-reachability walk over
|
|
3806
|
+
* the discovered files' relative-import graph, seeded from every file that
|
|
3807
|
+
* doesn't fold on its own.
|
|
3808
|
+
*
|
|
3809
|
+
* chant #1044 adds the OTHER half of the same hazard, in the opposite
|
|
3810
|
+
* direction along the same edges. Forward taint covers "a run file imports a
|
|
3811
|
+
* folded file"; it does not cover "a FOLDED file consumed the objects of a
|
|
3812
|
+
* file that later got forced to run". Once a plain-call intrinsic can fold,
|
|
3813
|
+
* that second case is easy to reach: in `lexicons/aws/examples/lambda-api`,
|
|
3814
|
+
* `health-api.ts` folds and captures `params.ts`'s real `Parameter` instance
|
|
3815
|
+
* through `Ref(environment)`, while `params.ts` itself is forced to run
|
|
3816
|
+
* because a DIFFERENT sibling (`data-bucket.ts`) imports it and falls back.
|
|
3817
|
+
* Discovery then collects the run instance and serializes the folded one —
|
|
3818
|
+
* the same "Logical name not set" crash described above, arriving from the
|
|
3819
|
+
* other side. So `liveSources` (see {@link FoldFileResult}) contributes
|
|
3820
|
+
* reverse edges here: a tainted file taints every folded file that captured
|
|
3821
|
+
* one of its objects. Only object identity propagates — a file that imported
|
|
3822
|
+
* a plain string from a tainted file has nothing to disagree about.
|
|
3823
|
+
*
|
|
3824
|
+
* chant #1020 changes the calculus but not this function: `alb.ts` can now
|
|
3825
|
+
* often fold `network.vpc.VpcId` too (see `buildExternals`/`foldFileMemoized`
|
|
3826
|
+
* above), by reusing THE EXACT SAME `tryFoldFile("network.ts")` call (memoized
|
|
3827
|
+
* per `FoldSession`) that `discover()`'s own per-file loop also uses — so
|
|
3828
|
+
* `alb.ts` and `network.ts` share one real `vpc` object without ever
|
|
3829
|
+
* disagreeing. This invariant is still needed for whatever STILL falls back
|
|
3830
|
+
* after #1020 (a call-as-a-value construct, #1044; a shape #1020 doesn't
|
|
3831
|
+
* cover): the edge collection below is unconditional — it doesn't care
|
|
3832
|
+
* whether an edge happens to ALSO be used for cross-file value resolution —
|
|
3833
|
+
* so the exact same forced-taint safety net still applies to that remaining
|
|
3834
|
+
* boundary, unchanged.
|
|
3835
|
+
*/
|
|
3836
|
+
/**
|
|
3837
|
+
* file -> set of OTHER files in `files` that it relatively imports OR
|
|
3838
|
+
* re-exports from, regardless of that file's own fold outcome. Extracted
|
|
3839
|
+
* from {@link planFoldTaint} (chant #1083) so a second consumer — the fold
|
|
3840
|
+
* blocker dominator ranking (`./fold-rank.ts`) — can walk the exact same
|
|
3841
|
+
* project-file edges instead of re-parsing every file with its own
|
|
3842
|
+
* specifier-resolution logic. chant #1020 — this counts a namespace import
|
|
3843
|
+
* (`import * as ns from "./x"`, previously skipped — see `collectImports`)
|
|
3844
|
+
* and a re-export (`export { x } from "./y"`, which `collectImports` never
|
|
3845
|
+
* saw at all, since it only looks at `import` declarations): both are real
|
|
3846
|
+
* cross-file value dependencies fold's own resolution follows, so both need
|
|
3847
|
+
* the same edge an ordinary named import already gets.
|
|
3848
|
+
*/
|
|
3849
|
+
export async function buildProjectImportEdges(files) {
|
|
3850
|
+
const fileSet = new Set(files);
|
|
3851
|
+
const edges = new Map();
|
|
3852
|
+
for (const file of files) {
|
|
3853
|
+
const targets = new Set();
|
|
3854
|
+
const addTarget = (specifier) => {
|
|
3855
|
+
if (!isProjectImport(specifier, file))
|
|
3856
|
+
return; // package import, not a sibling source file
|
|
3857
|
+
let resolved;
|
|
3858
|
+
try {
|
|
3859
|
+
resolved = resolveModulePath(specifier, file);
|
|
3860
|
+
}
|
|
3861
|
+
catch {
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
if (fileSet.has(resolved))
|
|
3865
|
+
targets.add(resolved);
|
|
3866
|
+
};
|
|
3867
|
+
try {
|
|
3868
|
+
const source = await readFile(file, "utf-8");
|
|
3869
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
3870
|
+
const collected = collectImports(sourceFile);
|
|
3871
|
+
for (const binding of collected.named.values())
|
|
3872
|
+
addTarget(binding.specifier);
|
|
3873
|
+
for (const binding of collected.namespaces.values())
|
|
3874
|
+
addTarget(binding.specifier);
|
|
3875
|
+
for (const statement of sourceFile.statements) {
|
|
3876
|
+
if (ts.isExportDeclaration(statement) &&
|
|
3877
|
+
!statement.isTypeOnly &&
|
|
3878
|
+
statement.moduleSpecifier &&
|
|
3879
|
+
ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
3880
|
+
addTarget(statement.moduleSpecifier.text);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
catch {
|
|
3885
|
+
// Unreadable/unparseable — no edges contributed; tryFoldFile's own
|
|
3886
|
+
// top-level catch already turns this into a "run" decision for the
|
|
3887
|
+
// file itself, which is enough to seed it as tainted below (or, for
|
|
3888
|
+
// the dominator ranking, leaves it a childless node).
|
|
3889
|
+
}
|
|
3890
|
+
edges.set(file, targets);
|
|
3891
|
+
}
|
|
3892
|
+
return edges;
|
|
3893
|
+
}
|
|
3894
|
+
export async function planFoldTaint(files, wouldFold, liveSources) {
|
|
3895
|
+
return (await planFoldTaintWithEdges(files, wouldFold, liveSources)).tainted;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* {@link planFoldTaint}'s walk, keeping the edge that fired for each file it
|
|
3899
|
+
* reaches. Split out for two consumers that need to say *why*: the fallback
|
|
3900
|
+
* reason in `discover()`, and {@link foldProject}'s verdicts.
|
|
3901
|
+
*/
|
|
3902
|
+
export async function planFoldTaintWithEdges(files, wouldFold, liveSources) {
|
|
3903
|
+
const fileSet = new Set(files);
|
|
3904
|
+
// file -> set of OTHER discovered files it relatively imports OR re-exports
|
|
3905
|
+
// from — see {@link buildProjectImportEdges}.
|
|
3906
|
+
const edges = await buildProjectImportEdges(files);
|
|
3907
|
+
// chant #1044 — reverse edges: consumed-file -> the folded files that
|
|
3908
|
+
// captured its objects. Same taint set, same fixpoint walk; see this
|
|
3909
|
+
// function's doc for the crash this closes. Kept in their own map as well,
|
|
3910
|
+
// so the walk below can tell which rule an edge belongs to.
|
|
3911
|
+
const reverse = new Map();
|
|
3912
|
+
for (const [consumer, sources] of liveSources ?? []) {
|
|
3913
|
+
if (!fileSet.has(consumer))
|
|
3914
|
+
continue;
|
|
3915
|
+
for (const source of sources) {
|
|
3916
|
+
if (!fileSet.has(source))
|
|
3917
|
+
continue;
|
|
3918
|
+
let back = edges.get(source);
|
|
3919
|
+
if (!back) {
|
|
3920
|
+
back = new Set();
|
|
3921
|
+
edges.set(source, back);
|
|
3922
|
+
}
|
|
3923
|
+
back.add(consumer);
|
|
3924
|
+
let mine = reverse.get(source);
|
|
3925
|
+
if (!mine) {
|
|
3926
|
+
mine = new Set();
|
|
3927
|
+
reverse.set(source, mine);
|
|
3928
|
+
}
|
|
3929
|
+
mine.add(consumer);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
const tainted = new Set(files.filter((f) => wouldFold.get(f) !== true));
|
|
3933
|
+
const reachedBy = new Map();
|
|
3934
|
+
const queue = [...tainted];
|
|
3935
|
+
while (queue.length > 0) {
|
|
3936
|
+
const current = queue.shift();
|
|
3937
|
+
for (const target of edges.get(current) ?? []) {
|
|
3938
|
+
if (!tainted.has(target)) {
|
|
3939
|
+
tainted.add(target);
|
|
3940
|
+
// A forward edge means `current` imports `target`; the reverse map is
|
|
3941
|
+
// consulted second so a file reachable both ways is reported as the
|
|
3942
|
+
// importer case, the one a reader can see in its own source.
|
|
3943
|
+
const forward = !reverse.get(current)?.has(target);
|
|
3944
|
+
reachedBy.set(target, { from: current, kind: forward ? "importer" : "capture" });
|
|
3945
|
+
queue.push(target);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
return { tainted, reachedBy };
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Fold a whole set of project files and report every file's verdict
|
|
3953
|
+
* (chant#2408).
|
|
3954
|
+
*
|
|
3955
|
+
* `discover()` does this as part of building, and its per-file decisions are
|
|
3956
|
+
* not addressable from outside. Anything that wants to *check* the
|
|
3957
|
+
* cross-file rules needs the whole picture: the forward rule (#1023) needs an
|
|
3958
|
+
* importer, the reverse rule (#1044) needs a capturing sibling, and the
|
|
3959
|
+
* fixpoint needs the whole set, so none of the three is observable one file at
|
|
3960
|
+
* a time. The conformance suite of the specification is the first such caller
|
|
3961
|
+
* (INTENTIUS/typescript-as-data#62).
|
|
3962
|
+
*
|
|
3963
|
+
* Same session, same memo, same taint walk as a real build — this is not a
|
|
3964
|
+
* second implementation of the rules, it is the existing one with its
|
|
3965
|
+
* intermediate results kept instead of consumed.
|
|
3966
|
+
*
|
|
3967
|
+
* `options` carries the rest of what a build's session holds (chant#2422).
|
|
3968
|
+
* Omitting it answers a harsher question than a real build asks: with no
|
|
3969
|
+
* lexicon list nothing reading a lexicon data export folds, and with no build
|
|
3970
|
+
* parameters nothing reading `params` does. See {@link FoldProjectOptions}.
|
|
3971
|
+
*/
|
|
3972
|
+
export async function foldProject(files, intrinsics = [], options = {}) {
|
|
3973
|
+
// chant#2455 — `sandbox` refuses to import project code at all and
|
|
3974
|
+
// `executing` exists to invoke it. Asking for both is not a preference
|
|
3975
|
+
// between two readings, it is a contradiction, and silently picking one would
|
|
3976
|
+
// make the fold's meaning depend on which.
|
|
3977
|
+
if (options.sandbox === true && options.executing === true) {
|
|
3978
|
+
throw new Error("foldProject: `sandbox` and `executing` are mutually exclusive — the first refuses to import " +
|
|
3979
|
+
"project code and the second exists to invoke it. Pass one.");
|
|
3980
|
+
}
|
|
3981
|
+
const session = createFoldSession(intrinsics, options.buildParams, options.lexicons ?? [], options.sandbox ?? false, options.lexiconPackages ?? [], options.executing ?? false, files);
|
|
3982
|
+
const attempts = new Map();
|
|
3983
|
+
for (const file of files)
|
|
3984
|
+
attempts.set(file, await tryFoldFile(file, intrinsics, session));
|
|
3985
|
+
const plan = await planFoldTaintWithEdges(files, new Map(files.map((file) => [file, attempts.get(file)?.ok === true])), new Map(files.flatMap((file) => {
|
|
3986
|
+
const attempt = attempts.get(file);
|
|
3987
|
+
return attempt?.ok === true ? [[file, attempt.liveSources]] : [];
|
|
3988
|
+
})));
|
|
3989
|
+
const folded = files.filter((file) => attempts.get(file)?.ok === true && !plan.tainted.has(file));
|
|
3990
|
+
const provenanceByFile = foldProvenanceByFile(folded.map((file) => {
|
|
3991
|
+
const attempt = attempts.get(file);
|
|
3992
|
+
return {
|
|
3993
|
+
file,
|
|
3994
|
+
exports: attempt?.ok === true ? Object.fromEntries(attempt.exportedValues) : {},
|
|
3995
|
+
};
|
|
3996
|
+
}));
|
|
3997
|
+
const out = new Map();
|
|
3998
|
+
for (const file of files) {
|
|
3999
|
+
const attempt = attempts.get(file);
|
|
4000
|
+
const tentative = attempt.ok ? "fold" : "run";
|
|
4001
|
+
if (attempt.ok && !plan.tainted.has(file)) {
|
|
4002
|
+
const foldProvenance = provenanceByFile?.get(file);
|
|
4003
|
+
out.set(file, {
|
|
4004
|
+
verdict: "fold",
|
|
4005
|
+
tentative,
|
|
4006
|
+
exports: attempt.exportedValues,
|
|
4007
|
+
...(foldProvenance ? { foldProvenance } : {}),
|
|
4008
|
+
});
|
|
4009
|
+
continue;
|
|
4010
|
+
}
|
|
4011
|
+
out.set(file, {
|
|
4012
|
+
verdict: "run",
|
|
4013
|
+
tentative,
|
|
4014
|
+
reason: attempt.ok ? undefined : attempt.reason,
|
|
4015
|
+
taintedBy: attempt.ok ? plan.reachedBy.get(file) : undefined,
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
return out;
|
|
4019
|
+
}
|
|
4020
|
+
/**
|
|
4021
|
+
* chant#3598 — the folded files' fold provenance, split by declaring file.
|
|
4022
|
+
*
|
|
4023
|
+
* This is `build()`'s computation, not a second one: the same
|
|
4024
|
+
* `collectEntities` over the same folded export namespaces `discover()` hands
|
|
4025
|
+
* it, then `foldProvenanceOfEntities` with `getProvenance`. One call over the
|
|
4026
|
+
* whole folded set rather than one per file, because composite expansion and
|
|
4027
|
+
* cross-directory name disambiguation (#932) are decided over the set and an
|
|
4028
|
+
* entity's name is part of what is being reported. The build root is the
|
|
4029
|
+
* files' common directory, which is what `chant build <dir>` passes when every
|
|
4030
|
+
* file sits under `<dir>`.
|
|
4031
|
+
*
|
|
4032
|
+
* Returns `undefined` when collection throws (a duplicate name), the case
|
|
4033
|
+
* where a build has no entities either.
|
|
4034
|
+
*/
|
|
4035
|
+
function foldProvenanceByFile(modules) {
|
|
4036
|
+
if (modules.length === 0)
|
|
4037
|
+
return new Map();
|
|
4038
|
+
let entities;
|
|
4039
|
+
try {
|
|
4040
|
+
entities = collectEntities(modules, commonDirectory(modules.map((m) => m.file)));
|
|
4041
|
+
}
|
|
4042
|
+
catch {
|
|
4043
|
+
return undefined;
|
|
4044
|
+
}
|
|
4045
|
+
const byResolved = new Map();
|
|
4046
|
+
const out = new Map();
|
|
4047
|
+
for (const { file } of modules) {
|
|
4048
|
+
const record = {};
|
|
4049
|
+
byResolved.set(resolvePath(file), record);
|
|
4050
|
+
out.set(file, record);
|
|
4051
|
+
}
|
|
4052
|
+
const all = foldProvenanceOfEntities(entities, getProvenance);
|
|
4053
|
+
for (const [name, record] of Object.entries(all)) {
|
|
4054
|
+
const sourceFile = getProvenance(entities.get(name))?.sourceFile;
|
|
4055
|
+
const target = sourceFile === undefined ? undefined : byResolved.get(resolvePath(sourceFile));
|
|
4056
|
+
if (target)
|
|
4057
|
+
target[name] = record;
|
|
4058
|
+
}
|
|
4059
|
+
return out;
|
|
4060
|
+
}
|
|
4061
|
+
/** The deepest directory every one of `files` sits under. */
|
|
4062
|
+
function commonDirectory(files) {
|
|
4063
|
+
const split = files.map((file) => dirname(resolvePath(file)).split(sep));
|
|
4064
|
+
const first = split[0];
|
|
4065
|
+
let depth = first.length;
|
|
4066
|
+
for (const parts of split.slice(1)) {
|
|
4067
|
+
let i = 0;
|
|
4068
|
+
while (i < depth && i < parts.length && parts[i] === first[i])
|
|
4069
|
+
i++;
|
|
4070
|
+
depth = i;
|
|
4071
|
+
}
|
|
4072
|
+
return first.slice(0, depth).join(sep) || sep;
|
|
4073
|
+
}
|