@apifuse/provider-sdk 2.2.0-beta.7 → 2.2.0-beta.70
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/AUTHORING.md +943 -51
- package/CHANGELOG.md +273 -1
- package/LICENSE +21 -0
- package/README.md +117 -29
- package/SUBMISSION.md +3 -2
- package/bin/apifuse-check.ts +293 -13
- package/bin/apifuse-dev.ts +42 -19
- package/bin/apifuse-migrate-deployment.ts +200 -0
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +19 -6
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +736 -38
- package/bin/apifuse-perf.ts +59 -23
- package/bin/apifuse-record.ts +923 -94
- package/bin/apifuse-submit-check.ts +2313 -281
- package/bin/apifuse-test.ts +3 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +30 -0
- package/dist/cli/create.d.ts +1 -1
- package/dist/cli/create.js +8 -2
- package/dist/cli/deployment-intent.d.ts +178 -0
- package/dist/cli/deployment-intent.js +277 -0
- package/dist/cli/local-connection.d.ts +111 -0
- package/dist/cli/local-connection.js +151 -0
- package/dist/cli/migrate-deployment-intent.d.ts +132 -0
- package/dist/cli/migrate-deployment-intent.js +1552 -0
- package/dist/cli/migrate-operation-declaration.d.ts +70 -0
- package/dist/cli/migrate-operation-declaration.js +2206 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +56 -0
- package/dist/cli/migrate-provider-shape.js +580 -0
- package/dist/cli/resolver-runtime.d.ts +21 -0
- package/dist/cli/resolver-runtime.js +31 -0
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +41 -19
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +202 -16
- package/dist/config/loader.js +491 -141
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +34 -0
- package/dist/declaration-validation.js +305 -0
- package/dist/define.d.ts +71 -29
- package/dist/define.js +807 -69
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +8 -1
- package/dist/engine.d.ts +129 -0
- package/dist/engine.js +209 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +18 -0
- package/dist/error-resolution.js +243 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +99 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +371 -0
- package/dist/handle-meta.d.ts +90 -0
- package/dist/handle-meta.js +73 -0
- package/dist/handle.d.ts +226 -0
- package/dist/handle.js +345 -0
- package/dist/health-scenario.d.ts +1901 -0
- package/dist/health-scenario.js +653 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/i18n/error-messages.d.ts +86 -0
- package/dist/i18n/error-messages.js +192 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/keys.d.ts +12 -0
- package/dist/i18n/keys.js +76 -5
- package/dist/i18n/operation-locale-namespace.d.ts +2 -0
- package/dist/i18n/operation-locale-namespace.js +7 -0
- package/dist/index.d.ts +40 -15
- package/dist/index.js +29 -11
- package/dist/lint.d.ts +43 -11
- package/dist/lint.js +2512 -177
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +18 -8
- package/dist/provider.js +11 -6
- package/dist/runtime/auth-flow.d.ts +6 -1
- package/dist/runtime/auth-flow.js +8 -0
- package/dist/runtime/bounded-expiring-map.d.ts +21 -0
- package/dist/runtime/bounded-expiring-map.js +64 -0
- package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
- package/dist/runtime/browser-telemetry-binding.js +83 -0
- package/dist/runtime/browser-telemetry.d.ts +76 -0
- package/dist/runtime/browser-telemetry.js +165 -0
- package/dist/runtime/browser.d.ts +4 -0
- package/dist/runtime/browser.js +688 -83
- package/dist/runtime/cache-telemetry.d.ts +71 -0
- package/dist/runtime/cache-telemetry.js +177 -0
- package/dist/runtime/cache.d.ts +3 -0
- package/dist/runtime/cache.js +245 -23
- package/dist/runtime/capability-response.d.ts +3 -0
- package/dist/runtime/capability-response.js +56 -0
- package/dist/runtime/capability-telemetry.d.ts +46 -0
- package/dist/runtime/capability-telemetry.js +68 -0
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +14 -0
- package/dist/runtime/diagnostic-env.js +44 -0
- package/dist/runtime/diagnostic-redactor.d.ts +54 -0
- package/dist/runtime/diagnostic-redactor.js +571 -0
- package/dist/runtime/egress-lease.d.ts +65 -0
- package/dist/runtime/egress-lease.js +236 -0
- package/dist/runtime/engine-mode.d.ts +74 -0
- package/dist/runtime/engine-mode.js +152 -0
- package/dist/runtime/env.js +14 -1
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +23 -26
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/handle-wordlist.js +256 -0
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +908 -0
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +16 -0
- package/dist/runtime/http-telemetry-guard.js +202 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +34 -0
- package/dist/runtime/http.js +680 -121
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +462 -19
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +105 -0
- package/dist/runtime/native-network.js +1433 -0
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/ocr-telemetry.d.ts +82 -0
- package/dist/runtime/ocr-telemetry.js +192 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +468 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +398 -16
- package/dist/runtime/provider.d.ts +7 -0
- package/dist/runtime/provider.js +14 -0
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +147 -0
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/proxy-telemetry.d.ts +133 -3
- package/dist/runtime/proxy-telemetry.js +218 -70
- package/dist/runtime/redirect-hop-headers.d.ts +19 -0
- package/dist/runtime/redirect-hop-headers.js +58 -0
- package/dist/runtime/redirects.d.ts +61 -0
- package/dist/runtime/redirects.js +149 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +75 -1
- package/dist/runtime/request-options.js +588 -0
- package/dist/runtime/request-telemetry.d.ts +70 -0
- package/dist/runtime/request-telemetry.js +229 -0
- package/dist/runtime/resolver-config.d.ts +13 -0
- package/dist/runtime/resolver-config.js +14 -0
- package/dist/runtime/resolver-public.d.ts +3 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-runtime-options.d.ts +14 -0
- package/dist/runtime/resolver-runtime-options.js +33 -0
- package/dist/runtime/resolver-shared.d.ts +24 -0
- package/dist/runtime/resolver-shared.js +69 -0
- package/dist/runtime/resolver-span-attributes.d.ts +34 -0
- package/dist/runtime/resolver-span-attributes.js +93 -0
- package/dist/runtime/resolver-telemetry.d.ts +135 -0
- package/dist/runtime/resolver-telemetry.js +204 -0
- package/dist/runtime/resolver-usage.d.ts +32 -0
- package/dist/runtime/resolver-usage.js +54 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +53 -0
- package/dist/runtime/resolver-vendors/bindings.js +45 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
- package/dist/runtime/resolver-vendors/browser.js +423 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +544 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +23 -0
- package/dist/runtime/resolver-vendors/hosts.js +68 -0
- package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +425 -0
- package/dist/runtime/resolver-vendors/types.d.ts +125 -0
- package/dist/runtime/resolver-vendors/types.js +106 -0
- package/dist/runtime/resolver.d.ts +79 -0
- package/dist/runtime/resolver.js +1094 -0
- package/dist/runtime/state-telemetry.d.ts +79 -0
- package/dist/runtime/state-telemetry.js +194 -0
- package/dist/runtime/state.d.ts +10 -1
- package/dist/runtime/state.js +322 -82
- package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
- package/dist/runtime/stealth-akamai-sbsd.js +142 -0
- package/dist/runtime/stealth-cookies.d.ts +24 -0
- package/dist/runtime/stealth-cookies.js +131 -0
- package/dist/runtime/stealth-owned-headers.d.ts +39 -0
- package/dist/runtime/stealth-owned-headers.js +57 -0
- package/dist/runtime/stealth-telemetry.d.ts +92 -0
- package/dist/runtime/stealth-telemetry.js +179 -0
- package/dist/runtime/stealth.d.ts +45 -6
- package/dist/runtime/stealth.js +1775 -320
- package/dist/runtime/stt-telemetry.d.ts +77 -0
- package/dist/runtime/stt-telemetry.js +176 -0
- package/dist/runtime/stt.js +18 -19
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +17 -0
- package/dist/runtime/trace.js +176 -25
- package/dist/runtime-boundary-lint.d.ts +137 -0
- package/dist/runtime-boundary-lint.js +590 -0
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +72 -2
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/error-stack-frames.d.ts +35 -0
- package/dist/server/error-stack-frames.js +219 -0
- package/dist/server/index.d.ts +21 -2
- package/dist/server/index.js +11 -2
- package/dist/server/runtime-diagnostics.d.ts +165 -0
- package/dist/server/runtime-diagnostics.js +360 -0
- package/dist/server/sdk-version.d.ts +3 -0
- package/dist/server/sdk-version.js +19 -0
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/self-test.d.ts +18 -3
- package/dist/server/self-test.js +139 -76
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +137 -0
- package/dist/server/serve-implementation.d.ts +285 -0
- package/dist/server/serve-implementation.js +3172 -0
- package/dist/server/serve.d.ts +1 -75
- package/dist/server/serve.js +1 -1170
- package/dist/server/state-backend-report.d.ts +70 -0
- package/dist/server/state-backend-report.js +113 -0
- package/dist/server/trace-output.d.ts +7 -0
- package/dist/server/trace-output.js +111 -0
- package/dist/server/types.d.ts +37 -5
- package/dist/server/types.js +34 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +237 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +35 -0
- package/dist/stateful/stateful-provider-runtime-context.js +61 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +15 -4
- package/dist/stealth/profiles.js +169 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +509 -25
- package/dist/trace-sanitization.d.ts +6 -0
- package/dist/trace-sanitization.js +68 -0
- package/dist/types.d.ts +899 -210
- package/dist/types.js +1 -0
- package/dist/typescript-module.d.ts +39 -0
- package/dist/typescript-module.js +72 -0
- package/package.json +52 -6
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/commands.ts +37 -0
- package/src/cli/create.ts +8 -2
- package/src/cli/deployment-intent.ts +364 -0
- package/src/cli/local-connection.ts +221 -0
- package/src/cli/migrate-deployment-intent.ts +1908 -0
- package/src/cli/migrate-operation-declaration.ts +3026 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +778 -0
- package/src/cli/resolver-runtime.ts +72 -0
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +41 -19
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +774 -181
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +376 -0
- package/src/define.ts +1105 -131
- package/src/dev.ts +13 -1
- package/src/engine.ts +390 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +269 -0
- package/src/errors.ts +157 -0
- package/src/fixture-sanitization.ts +422 -0
- package/src/handle-meta.ts +143 -0
- package/src/handle.ts +694 -0
- package/src/health-scenario.ts +960 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/i18n/error-messages.ts +255 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/keys.ts +80 -6
- package/src/i18n/operation-locale-namespace.ts +10 -0
- package/src/index.ts +390 -23
- package/src/lint.ts +2975 -234
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +207 -27
- package/src/runtime/auth-flow.ts +15 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/browser-telemetry-binding.ts +92 -0
- package/src/runtime/browser-telemetry.ts +289 -0
- package/src/runtime/browser.ts +882 -99
- package/src/runtime/cache-telemetry.ts +247 -0
- package/src/runtime/cache.ts +258 -22
- package/src/runtime/capability-response.ts +53 -0
- package/src/runtime/capability-telemetry.ts +101 -0
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +65 -0
- package/src/runtime/diagnostic-redactor.ts +668 -0
- package/src/runtime/egress-lease.ts +341 -0
- package/src/runtime/engine-mode.ts +233 -0
- package/src/runtime/env.ts +15 -1
- package/src/runtime/executor.ts +39 -32
- package/src/runtime/handle-wordlist.ts +266 -0
- package/src/runtime/handle.ts +1180 -0
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +208 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +896 -150
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +628 -28
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1853 -0
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +564 -0
- package/src/runtime/otlp.ts +468 -21
- package/src/runtime/provider.ts +15 -0
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +222 -0
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/proxy-telemetry.ts +358 -103
- package/src/runtime/redirect-hop-headers.ts +71 -0
- package/src/runtime/redirects.ts +215 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +728 -1
- package/src/runtime/request-telemetry.ts +375 -0
- package/src/runtime/resolver-config.ts +18 -0
- package/src/runtime/resolver-public.ts +44 -0
- package/src/runtime/resolver-runtime-options.ts +50 -0
- package/src/runtime/resolver-shared.ts +113 -0
- package/src/runtime/resolver-span-attributes.ts +139 -0
- package/src/runtime/resolver-telemetry.ts +362 -0
- package/src/runtime/resolver-usage.ts +99 -0
- package/src/runtime/resolver-vendors/bindings.ts +61 -0
- package/src/runtime/resolver-vendors/browser.ts +591 -0
- package/src/runtime/resolver-vendors/capsolver.ts +727 -0
- package/src/runtime/resolver-vendors/hosts.ts +81 -0
- package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +555 -0
- package/src/runtime/resolver-vendors/types.ts +261 -0
- package/src/runtime/resolver.ts +1543 -0
- package/src/runtime/state-telemetry.ts +298 -0
- package/src/runtime/state.ts +435 -89
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth-cookies.ts +155 -0
- package/src/runtime/stealth-owned-headers.ts +67 -0
- package/src/runtime/stealth-telemetry.ts +309 -0
- package/src/runtime/stealth.ts +2275 -372
- package/src/runtime/stt-telemetry.ts +282 -0
- package/src/runtime/stt.ts +96 -91
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +257 -35
- package/src/runtime-boundary-lint.ts +707 -0
- package/src/schema.ts +80 -2
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +184 -2
- package/src/server/runtime-diagnostics.ts +522 -0
- package/src/server/sdk-version.ts +22 -0
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/self-test.ts +205 -89
- package/src/server/sensitive-values.ts +211 -0
- package/src/server/serve-implementation.ts +4596 -0
- package/src/server/serve.ts +1 -1685
- package/src/server/state-backend-report.ts +185 -0
- package/src/server/trace-output.ts +189 -0
- package/src/server/types.ts +34 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +310 -0
- package/src/stateful/stateful-provider-runtime-context.ts +96 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +212 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +685 -19
- package/src/trace-sanitization.ts +110 -0
- package/src/types.ts +1014 -273
- package/src/typescript-module.ts +100 -0
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice.d.ts +0 -15
- package/dist/runtime/choice.js +0 -413
- package/src/choice-token.ts +0 -165
- package/src/runtime/choice.ts +0 -686
|
@@ -0,0 +1,2206 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { assertProviderLocaleKey } from "../i18n/keys.js";
|
|
4
|
+
import { operationIdToLocaleNamespace } from "../i18n/operation-locale-namespace.js";
|
|
5
|
+
const ts = await loadTypeScript();
|
|
6
|
+
async function loadTypeScript() {
|
|
7
|
+
try {
|
|
8
|
+
return await import("typescript");
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
console.error("apifuse migrate-operation-declaration requires typescript; install it in the workspace running the CLI (bun add -d typescript)");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const OPERATION_HELPERS = new Set(["defineOperation", "defineStreamOperation"]);
|
|
16
|
+
const SAFETY_VALUES = new Set(["read", "write", "destructive", "external-send"]);
|
|
17
|
+
const DOCS_FIELDS = new Set([
|
|
18
|
+
"titleKey",
|
|
19
|
+
"descriptionKey",
|
|
20
|
+
"summaryKey",
|
|
21
|
+
"markdownKey",
|
|
22
|
+
"normalizationNotesKeys",
|
|
23
|
+
"errorCodes",
|
|
24
|
+
]);
|
|
25
|
+
const ANNOTATION_FIELDS = new Set([
|
|
26
|
+
"destructive",
|
|
27
|
+
"readOnly",
|
|
28
|
+
"openWorld",
|
|
29
|
+
"idempotent",
|
|
30
|
+
"rateLimit",
|
|
31
|
+
"timeoutMs",
|
|
32
|
+
]);
|
|
33
|
+
const TOOL_ROUTER_FIELDS = new Set([
|
|
34
|
+
"name",
|
|
35
|
+
"riskClass",
|
|
36
|
+
"approval",
|
|
37
|
+
"connectionMode",
|
|
38
|
+
"connectionExternalRefParam",
|
|
39
|
+
"requiresConnection",
|
|
40
|
+
]);
|
|
41
|
+
const TOP_LEVEL_DELETIONS = new Set(["derivations", "retryOnAuthRefresh", "title", "description"]);
|
|
42
|
+
const SOURCE_SKIP_DIRECTORIES = new Set([
|
|
43
|
+
"node_modules",
|
|
44
|
+
"__tests__",
|
|
45
|
+
"__fixtures__",
|
|
46
|
+
".git",
|
|
47
|
+
"dist",
|
|
48
|
+
]);
|
|
49
|
+
const SIDECAR_NAME = "migrate-operation-declaration.locales-todo.json";
|
|
50
|
+
/**
|
|
51
|
+
* Rewrite every operation declaration in one TypeScript module.
|
|
52
|
+
*
|
|
53
|
+
* The function is file-atomic: a single refusal discards every planned edit.
|
|
54
|
+
* It uses AST ranges as a string/comment/template-aware brace balancer and
|
|
55
|
+
* copies every untouched source slice byte-for-byte.
|
|
56
|
+
*/
|
|
57
|
+
export function migrateOperationDeclaration(sourceText, fileName, options = {}) {
|
|
58
|
+
return migrateOperationDeclarationInternal(sourceText, fileName, options);
|
|
59
|
+
}
|
|
60
|
+
function migrateOperationDeclarationInternal(sourceText, fileName, options, context = {}) {
|
|
61
|
+
const source = parseSource(fileName, sourceText);
|
|
62
|
+
const parseError = firstSyntaxError(source);
|
|
63
|
+
if (parseError !== undefined) {
|
|
64
|
+
return {
|
|
65
|
+
status: "refused",
|
|
66
|
+
refusals: [refusal(fileName, "<unknown>", "source_syntax", parseError)],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const constObjects = collectModuleConstObjects(source);
|
|
70
|
+
const constArrays = collectModuleConstArrays(source);
|
|
71
|
+
const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds, context.operationSites, context.excludedBindings, context.runtimeComposedInitializers);
|
|
72
|
+
context.recordDiscoveredSites?.(discovery.discoveredCount);
|
|
73
|
+
if (discovery.refusals.length > 0) {
|
|
74
|
+
return { status: "refused", refusals: discovery.refusals };
|
|
75
|
+
}
|
|
76
|
+
const edits = [];
|
|
77
|
+
const todos = [];
|
|
78
|
+
const refusals = [];
|
|
79
|
+
for (const site of discovery.sites) {
|
|
80
|
+
const plan = planOperationMigration(source, fileName, site, constObjects, constArrays, options.localeFiles ?? [], context.staticObjectResolver);
|
|
81
|
+
if ("refusal" in plan) {
|
|
82
|
+
refusals.push(plan.refusal);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
edits.push(...plan.edits);
|
|
86
|
+
todos.push(...plan.localeTodos);
|
|
87
|
+
}
|
|
88
|
+
if (refusals.length > 0)
|
|
89
|
+
return { status: "refused", refusals };
|
|
90
|
+
const invalidLocaleKey = findInvalidLocaleTodo(todos, fileName);
|
|
91
|
+
if (invalidLocaleKey !== undefined) {
|
|
92
|
+
return { status: "refused", refusals: [invalidLocaleKey] };
|
|
93
|
+
}
|
|
94
|
+
if (edits.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
status: "unchanged",
|
|
97
|
+
code: sourceText,
|
|
98
|
+
operations: discovery.sites.length,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const normalizedEdits = normalizeEdits(edits, fileName);
|
|
102
|
+
if ("refusal" in normalizedEdits) {
|
|
103
|
+
return { status: "refused", refusals: [normalizedEdits.refusal] };
|
|
104
|
+
}
|
|
105
|
+
const code = applyEdits(sourceText, normalizedEdits.edits);
|
|
106
|
+
const outputRefusal = verifyOperationDeclarationRewrite(code, fileName);
|
|
107
|
+
if (outputRefusal !== undefined) {
|
|
108
|
+
return {
|
|
109
|
+
status: "refused",
|
|
110
|
+
refusals: [outputRefusal],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
status: "migrated",
|
|
115
|
+
code,
|
|
116
|
+
operations: discovery.sites.length,
|
|
117
|
+
localeTodos: todos,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Parse a proposed rewrite and classify a codemod-introduced syntax failure. */
|
|
121
|
+
export function verifyOperationDeclarationRewrite(code, fileName) {
|
|
122
|
+
const outputError = firstSyntaxError(parseSource(fileName, code));
|
|
123
|
+
if (outputError === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
return refusal(fileName, "<file>", "codemod_syntax", `Transform output did not parse: ${outputError}`);
|
|
126
|
+
}
|
|
127
|
+
function planOperationMigration(source, fileName, site, constObjects, constArrays, localeFiles, staticObjectResolver) {
|
|
128
|
+
if (site.object.properties.some(ts.isSpreadAssignment)) {
|
|
129
|
+
return {
|
|
130
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", "Top-level operation spreads cannot be rewritten without changing their shared declaration."),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const expanded = expandMembers(site.object, source, constObjects, fileName, site.operationKey, staticObjectResolver);
|
|
134
|
+
if ("refusal" in expanded)
|
|
135
|
+
return expanded;
|
|
136
|
+
const top = indexMembers(expanded.members, fileName, site.operationKey);
|
|
137
|
+
if ("refusal" in top)
|
|
138
|
+
return top;
|
|
139
|
+
if (top.spreads.length > 0) {
|
|
140
|
+
return {
|
|
141
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", "Top-level operation spreads cannot be rewritten without changing their shared declaration."),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const containers = new Map();
|
|
145
|
+
for (const containerName of ["annotations", "toolRouter", "docs"]) {
|
|
146
|
+
const member = top.byName.get(containerName);
|
|
147
|
+
if (member === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
const resolvedObject = resolveObjectInitializer(member, source, constObjects, staticObjectResolver);
|
|
150
|
+
if (resolvedObject === undefined) {
|
|
151
|
+
return {
|
|
152
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", `${containerName} must be an object literal or a module-level const object literal.`),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const members = expandMembers(resolvedObject.object, resolvedObject.source, constObjects, fileName, site.operationKey, staticObjectResolver);
|
|
156
|
+
if ("refusal" in members)
|
|
157
|
+
return members;
|
|
158
|
+
const indexed = indexMembers(members.members, fileName, site.operationKey);
|
|
159
|
+
if ("refusal" in indexed)
|
|
160
|
+
return indexed;
|
|
161
|
+
if (indexed.spreads.length > 0) {
|
|
162
|
+
return {
|
|
163
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", `${containerName} contains an unresolved spread.`),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
containers.set(containerName, members.members);
|
|
167
|
+
}
|
|
168
|
+
const annotations = memberMap(containers.get("annotations") ?? []);
|
|
169
|
+
const toolRouter = memberMap(containers.get("toolRouter") ?? []);
|
|
170
|
+
const docs = memberMap(containers.get("docs") ?? []);
|
|
171
|
+
const unknownAnnotation = firstUnknown(annotations, ANNOTATION_FIELDS);
|
|
172
|
+
if (unknownAnnotation !== undefined) {
|
|
173
|
+
return unsupportedContainerMember(fileName, site.operationKey, "annotations", unknownAnnotation);
|
|
174
|
+
}
|
|
175
|
+
const unknownTool = firstUnknown(toolRouter, TOOL_ROUTER_FIELDS);
|
|
176
|
+
if (unknownTool !== undefined) {
|
|
177
|
+
return unsupportedContainerMember(fileName, site.operationKey, "toolRouter", unknownTool);
|
|
178
|
+
}
|
|
179
|
+
const unknownDocs = firstUnknown(docs, new Set([...DOCS_FIELDS, "requestExample", "responseExample"]));
|
|
180
|
+
if (unknownDocs !== undefined) {
|
|
181
|
+
return unsupportedContainerMember(fileName, site.operationKey, "docs", unknownDocs);
|
|
182
|
+
}
|
|
183
|
+
const risk = resolveRiskClass(top.byName, annotations, toolRouter, fileName, site.operationKey);
|
|
184
|
+
if ("refusal" in risk)
|
|
185
|
+
return risk;
|
|
186
|
+
const insertions = new Map();
|
|
187
|
+
const removals = [];
|
|
188
|
+
const nestedContainers = ["annotations", "toolRouter", "docs"];
|
|
189
|
+
for (const name of nestedContainers) {
|
|
190
|
+
const member = top.byName.get(name);
|
|
191
|
+
if (member !== undefined)
|
|
192
|
+
removals.push(member.property);
|
|
193
|
+
}
|
|
194
|
+
for (const name of TOP_LEVEL_DELETIONS) {
|
|
195
|
+
const member = top.byName.get(name);
|
|
196
|
+
if (member !== undefined)
|
|
197
|
+
removals.push(member.property);
|
|
198
|
+
}
|
|
199
|
+
if (top.byName.get("riskClass") === undefined) {
|
|
200
|
+
const sourceContainer = risk.source === "annotations" ? "annotations" : "toolRouter";
|
|
201
|
+
addInsertion(insertions, sourceContainer, `riskClass: ${risk.text}`);
|
|
202
|
+
}
|
|
203
|
+
const timeout = mergeFlatAndNested("timeoutMs", top.byName.get("timeoutMs"), annotations.get("timeoutMs"), fileName, site.operationKey, "execution_conflict");
|
|
204
|
+
if ("refusal" in timeout)
|
|
205
|
+
return timeout;
|
|
206
|
+
if (timeout.insert !== undefined)
|
|
207
|
+
addInsertion(insertions, "annotations", timeout.insert);
|
|
208
|
+
const connection = resolveConnectionMode(top.byName, toolRouter, fileName, site.operationKey);
|
|
209
|
+
if ("refusal" in connection)
|
|
210
|
+
return connection;
|
|
211
|
+
if (connection.insert !== undefined) {
|
|
212
|
+
addInsertion(insertions, "toolRouter", connection.insert);
|
|
213
|
+
}
|
|
214
|
+
const externalRef = mergeFlatAndNested("connectionExternalRefParam", top.byName.get("connectionExternalRefParam"), toolRouter.get("connectionExternalRefParam"), fileName, site.operationKey, "connection_mode_conflict");
|
|
215
|
+
if ("refusal" in externalRef)
|
|
216
|
+
return externalRef;
|
|
217
|
+
if (externalRef.insert !== undefined) {
|
|
218
|
+
addInsertion(insertions, "toolRouter", externalRef.insert);
|
|
219
|
+
}
|
|
220
|
+
const approval = resolveApproval(top.byName.get("approval"), toolRouter.get("approval"), risk.value, fileName, site.operationKey);
|
|
221
|
+
if ("refusal" in approval)
|
|
222
|
+
return approval;
|
|
223
|
+
if (approval.removeTop !== undefined)
|
|
224
|
+
removals.push(approval.removeTop);
|
|
225
|
+
if (approval.insert !== undefined)
|
|
226
|
+
addInsertion(insertions, "toolRouter", approval.insert);
|
|
227
|
+
for (const field of DOCS_FIELDS) {
|
|
228
|
+
const merged = mergeFlatAndNested(field, top.byName.get(field), docs.get(field), fileName, site.operationKey, "locale_key_conflict");
|
|
229
|
+
if ("refusal" in merged)
|
|
230
|
+
return merged;
|
|
231
|
+
if (merged.insert !== undefined)
|
|
232
|
+
addInsertion(insertions, "docs", merged.insert);
|
|
233
|
+
}
|
|
234
|
+
const title = planTitleLocale(top.byName.get("title"), top.byName.get("titleKey"), docs.get("titleKey"), fileName, site, localeFiles);
|
|
235
|
+
if ("refusal" in title)
|
|
236
|
+
return title;
|
|
237
|
+
const localeNamespace = top.byName.get("inputExamples") === undefined
|
|
238
|
+
? { namespace: site.operationKey }
|
|
239
|
+
: resolveOperationLocaleNamespace([
|
|
240
|
+
top.byName.get("titleKey"),
|
|
241
|
+
docs.get("titleKey"),
|
|
242
|
+
top.byName.get("descriptionKey"),
|
|
243
|
+
docs.get("descriptionKey"),
|
|
244
|
+
], fileName, site);
|
|
245
|
+
if ("refusal" in localeNamespace)
|
|
246
|
+
return localeNamespace;
|
|
247
|
+
const examples = planExamples(top.byName.get("inputExamples"), top.byName.get("examples"), fileName, site, source, constArrays, localeFiles, localeNamespace.namespace);
|
|
248
|
+
if ("refusal" in examples)
|
|
249
|
+
return examples;
|
|
250
|
+
const edits = [...examples.edits];
|
|
251
|
+
for (const containerName of nestedContainers) {
|
|
252
|
+
const member = top.byName.get(containerName);
|
|
253
|
+
if (member === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
const generated = insertions.get(containerName) ?? [];
|
|
256
|
+
if (generated.length === 0)
|
|
257
|
+
continue;
|
|
258
|
+
const indentation = indentationAt(source.getFullText(), member.property.getStart(source));
|
|
259
|
+
edits.push({
|
|
260
|
+
start: member.property.getStart(source),
|
|
261
|
+
end: member.property.getEnd(),
|
|
262
|
+
text: generated.map((item) => item.text).join(`,\n${indentation}`),
|
|
263
|
+
});
|
|
264
|
+
removeFirst(removals, member.property);
|
|
265
|
+
}
|
|
266
|
+
const removalEdits = removalRanges(site.object, removals, source);
|
|
267
|
+
for (const edit of removalEdits) {
|
|
268
|
+
if (!edits.some((existing) => rangesOverlap(existing, edit)))
|
|
269
|
+
edits.push(edit);
|
|
270
|
+
}
|
|
271
|
+
return { edits, localeTodos: [...title.localeTodos, ...examples.localeTodos] };
|
|
272
|
+
}
|
|
273
|
+
function planTitleLocale(title, flatTitleKey, nestedTitleKey, fileName, site, localeFiles) {
|
|
274
|
+
if (title === undefined)
|
|
275
|
+
return { localeTodos: [] };
|
|
276
|
+
const originalProse = literalString(title.initializer);
|
|
277
|
+
if (originalProse === undefined) {
|
|
278
|
+
return nonLiteral(fileName, site.operationKey, "title must be a string literal so its authored prose can be preserved in the English locale catalog.");
|
|
279
|
+
}
|
|
280
|
+
if (!site.operationIdProven) {
|
|
281
|
+
return {
|
|
282
|
+
refusal: refusal(fileName, site.operationKey, "operation_id_unresolved", "title requires an exact operation id proven from a static operations map."),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (!localeFiles.includes("locales/en.json")) {
|
|
286
|
+
return {
|
|
287
|
+
refusal: refusal(fileName, site.operationKey, "missing_english_locale", "title cannot be migrated because locales/en.json does not exist."),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
let selectedTitleKey = flatTitleKey ?? nestedTitleKey;
|
|
291
|
+
if (flatTitleKey !== undefined && nestedTitleKey !== undefined) {
|
|
292
|
+
const same = equivalentLiteral(flatTitleKey.initializer, nestedTitleKey.initializer);
|
|
293
|
+
if (same === undefined) {
|
|
294
|
+
return nonLiteral(fileName, site.operationKey, "titleKey must be literal when both top-level and nested declarations exist.");
|
|
295
|
+
}
|
|
296
|
+
if (!same) {
|
|
297
|
+
return {
|
|
298
|
+
refusal: refusal(fileName, site.operationKey, "locale_key_conflict", "Top-level titleKey conflicts with nested titleKey."),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
selectedTitleKey = flatTitleKey;
|
|
302
|
+
}
|
|
303
|
+
const explicitTitleKey = selectedTitleKey === undefined ? undefined : literalString(selectedTitleKey.initializer);
|
|
304
|
+
if (selectedTitleKey !== undefined && explicitTitleKey === undefined) {
|
|
305
|
+
return nonLiteral(fileName, site.operationKey, "titleKey must be a string literal so the title locale destination is provable.");
|
|
306
|
+
}
|
|
307
|
+
let selectedTitleLocaleKey;
|
|
308
|
+
if (explicitTitleKey !== undefined) {
|
|
309
|
+
selectedTitleLocaleKey = explicitTitleKey;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
try {
|
|
313
|
+
selectedTitleLocaleKey = `operations.${operationIdToLocaleNamespace(site.operationKey)}.title`;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
return {
|
|
317
|
+
refusal: invalidLocaleKeyRefusal(fileName, site.operationKey, `operations.${site.operationKey}.title`, error),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
localeTodos: [
|
|
323
|
+
{
|
|
324
|
+
localeFile: "locales/en.json",
|
|
325
|
+
operationKey: site.operationKey,
|
|
326
|
+
key: selectedTitleLocaleKey,
|
|
327
|
+
originalProse,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function resolveRiskClass(top, annotations, toolRouter, fileName, operationKey) {
|
|
333
|
+
const topRisk = top.get("riskClass");
|
|
334
|
+
const toolRisk = toolRouter.get("riskClass");
|
|
335
|
+
let declared;
|
|
336
|
+
if (topRisk !== undefined && toolRisk !== undefined) {
|
|
337
|
+
const topValue = literalString(topRisk.initializer);
|
|
338
|
+
const toolValue = literalString(toolRisk.initializer);
|
|
339
|
+
if (topValue === undefined || toolValue === undefined) {
|
|
340
|
+
return nonLiteral(fileName, operationKey, "riskClass must be a string literal.");
|
|
341
|
+
}
|
|
342
|
+
if (topValue !== toolValue) {
|
|
343
|
+
return {
|
|
344
|
+
refusal: refusal(fileName, operationKey, "safety_conflict", `Top-level riskClass ${JSON.stringify(topValue)} conflicts with toolRouter.riskClass ${JSON.stringify(toolValue)}.`),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
declared = { member: topRisk, source: "top" };
|
|
348
|
+
}
|
|
349
|
+
else if (topRisk !== undefined) {
|
|
350
|
+
declared = { member: topRisk, source: "top" };
|
|
351
|
+
}
|
|
352
|
+
else if (toolRisk !== undefined) {
|
|
353
|
+
declared = { member: toolRisk, source: "toolRouter" };
|
|
354
|
+
}
|
|
355
|
+
const destructive = literalBooleanMember(annotations.get("destructive"), fileName, operationKey, "annotations.destructive");
|
|
356
|
+
if ("refusal" in destructive)
|
|
357
|
+
return destructive;
|
|
358
|
+
const readOnly = literalBooleanMember(annotations.get("readOnly"), fileName, operationKey, "annotations.readOnly");
|
|
359
|
+
if ("refusal" in readOnly)
|
|
360
|
+
return readOnly;
|
|
361
|
+
if (declared !== undefined) {
|
|
362
|
+
const value = literalString(declared.member.initializer);
|
|
363
|
+
if (value === undefined || !SAFETY_VALUES.has(value)) {
|
|
364
|
+
return nonLiteral(fileName, operationKey, "riskClass must be a literal read, write, destructive, or external-send value.");
|
|
365
|
+
}
|
|
366
|
+
if (destructive.value === true && value !== "destructive") {
|
|
367
|
+
return {
|
|
368
|
+
refusal: refusal(fileName, operationKey, "safety_conflict", `Declared riskClass ${JSON.stringify(value)} contradicts annotations.destructive: true.`),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
value,
|
|
373
|
+
text: declared.member.initializer?.getText() ?? JSON.stringify(value),
|
|
374
|
+
source: declared.source,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (destructive.value === true) {
|
|
378
|
+
return { value: "destructive", text: '"destructive"', source: "annotations" };
|
|
379
|
+
}
|
|
380
|
+
if (readOnly.value === true) {
|
|
381
|
+
return { value: "read", text: '"read"', source: "annotations" };
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
refusal: refusal(fileName, operationKey, "no_safety", "No authored riskClass, annotations.destructive: true, or annotations.readOnly: true declaration exists; refusing to invent write."),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function resolveConnectionMode(top, toolRouter, fileName, operationKey) {
|
|
388
|
+
const flat = top.get("connectionMode");
|
|
389
|
+
const nested = toolRouter.get("connectionMode");
|
|
390
|
+
if (flat !== undefined && nested !== undefined) {
|
|
391
|
+
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
392
|
+
if (same === undefined) {
|
|
393
|
+
return nonLiteral(fileName, operationKey, "Conflicting connectionMode declarations must be literal values.");
|
|
394
|
+
}
|
|
395
|
+
if (!same) {
|
|
396
|
+
return {
|
|
397
|
+
refusal: refusal(fileName, operationKey, "connection_mode_conflict", "Top-level connectionMode conflicts with toolRouter.connectionMode."),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return {};
|
|
401
|
+
}
|
|
402
|
+
if (flat !== undefined)
|
|
403
|
+
return {};
|
|
404
|
+
if (nested !== undefined) {
|
|
405
|
+
return { insert: memberText(nested, "connectionMode") };
|
|
406
|
+
}
|
|
407
|
+
const required = toolRouter.get("requiresConnection");
|
|
408
|
+
if (required === undefined)
|
|
409
|
+
return {};
|
|
410
|
+
const value = literalBooleanMember(required, fileName, operationKey, "toolRouter.requiresConnection");
|
|
411
|
+
if ("refusal" in value)
|
|
412
|
+
return value;
|
|
413
|
+
return { insert: `connectionMode: ${value.value ? '"required"' : '"none"'}` };
|
|
414
|
+
}
|
|
415
|
+
function resolveApproval(flat, nested, riskClass, fileName, operationKey) {
|
|
416
|
+
let selected = flat ?? nested;
|
|
417
|
+
if (flat !== undefined && nested !== undefined) {
|
|
418
|
+
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
419
|
+
if (same === undefined) {
|
|
420
|
+
return nonLiteral(fileName, operationKey, "approval declarations must be string literals.");
|
|
421
|
+
}
|
|
422
|
+
if (!same) {
|
|
423
|
+
return {
|
|
424
|
+
refusal: refusal(fileName, operationKey, "approval_conflict", "Top-level approval conflicts with toolRouter.approval."),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
selected = flat;
|
|
428
|
+
}
|
|
429
|
+
if (selected === undefined)
|
|
430
|
+
return {};
|
|
431
|
+
const value = literalString(selected.initializer);
|
|
432
|
+
if (value === undefined) {
|
|
433
|
+
return nonLiteral(fileName, operationKey, "approval must be a string literal.");
|
|
434
|
+
}
|
|
435
|
+
const defaultValue = defaultApprovalPolicy(riskClass);
|
|
436
|
+
if (value === defaultValue) {
|
|
437
|
+
return flat === undefined ? {} : { removeTop: flat.property };
|
|
438
|
+
}
|
|
439
|
+
if (flat !== undefined)
|
|
440
|
+
return {};
|
|
441
|
+
return { insert: memberText(selected, "approval") };
|
|
442
|
+
}
|
|
443
|
+
function defaultApprovalPolicy(riskClass) {
|
|
444
|
+
if (riskClass === "read")
|
|
445
|
+
return "never";
|
|
446
|
+
if (riskClass === "write")
|
|
447
|
+
return "risk-based";
|
|
448
|
+
return "always";
|
|
449
|
+
}
|
|
450
|
+
function mergeFlatAndNested(field, flat, nested, fileName, operationKey, reason) {
|
|
451
|
+
if (nested === undefined)
|
|
452
|
+
return {};
|
|
453
|
+
if (flat === undefined)
|
|
454
|
+
return { insert: memberText(nested, field) };
|
|
455
|
+
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
456
|
+
if (same === undefined) {
|
|
457
|
+
return nonLiteral(fileName, operationKey, `${field} must be literal when both top-level and nested declarations exist.`);
|
|
458
|
+
}
|
|
459
|
+
if (!same) {
|
|
460
|
+
// A shared imported docs template is intentionally lower-precedence than
|
|
461
|
+
// an operation's explicit flat locale key. Keep direct-vs-direct
|
|
462
|
+
// conflicts fail-closed, but do not let a spread default replace the
|
|
463
|
+
// operation-specific key during flattening.
|
|
464
|
+
if (reason === "locale_key_conflict" && nested.fromSpread === true)
|
|
465
|
+
return {};
|
|
466
|
+
return {
|
|
467
|
+
refusal: refusal(fileName, operationKey, reason, `Top-level ${field} conflicts with nested ${field}.`),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {};
|
|
471
|
+
}
|
|
472
|
+
function planExamples(inputExamples, existingExamples, fileName, site, source, constArrays, localeFiles, localeNamespace) {
|
|
473
|
+
if (inputExamples === undefined)
|
|
474
|
+
return { edits: [], localeTodos: [] };
|
|
475
|
+
if (existingExamples !== undefined) {
|
|
476
|
+
return {
|
|
477
|
+
refusal: refusal(fileName, site.operationKey, "examples_conflict", "Both inputExamples and examples are declared."),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const array = unwrapExpression(inputExamples.initializer);
|
|
481
|
+
if (array === undefined || !ts.isArrayLiteralExpression(array)) {
|
|
482
|
+
return nonLiteral(fileName, site.operationKey, "inputExamples must be an array literal.");
|
|
483
|
+
}
|
|
484
|
+
const expanded = expandArrayElements(array, constArrays, fileName, site.operationKey);
|
|
485
|
+
if ("refusal" in expanded)
|
|
486
|
+
return expanded;
|
|
487
|
+
if (expanded.elements.length > 0 && !site.operationIdProven) {
|
|
488
|
+
return {
|
|
489
|
+
refusal: refusal(fileName, site.operationKey, "operation_id_unresolved", "inputExamples requires an exact operation id proven from a static operations map."),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (expanded.elements.length > 0 && !localeFiles.includes("locales/en.json")) {
|
|
493
|
+
return {
|
|
494
|
+
refusal: refusal(fileName, site.operationKey, "missing_english_locale", "inputExamples cannot be migrated because locales/en.json does not exist."),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const edits = [];
|
|
498
|
+
const todos = [];
|
|
499
|
+
const propertyNameNode = inputExamples.property.name;
|
|
500
|
+
if (propertyNameNode === undefined) {
|
|
501
|
+
return nonLiteral(fileName, site.operationKey, "inputExamples uses an unsupported property name.");
|
|
502
|
+
}
|
|
503
|
+
edits.push({
|
|
504
|
+
start: propertyNameNode.getStart(source),
|
|
505
|
+
end: propertyNameNode.getEnd(),
|
|
506
|
+
text: "examples",
|
|
507
|
+
});
|
|
508
|
+
for (let index = 0; index < expanded.elements.length; index += 1) {
|
|
509
|
+
const element = expanded.elements[index];
|
|
510
|
+
const object = element === undefined ? undefined : unwrapExpression(element);
|
|
511
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object)) {
|
|
512
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}] must be an object literal.`);
|
|
513
|
+
}
|
|
514
|
+
const members = indexMembersFromObject(object, fileName, site.operationKey);
|
|
515
|
+
if ("refusal" in members)
|
|
516
|
+
return members;
|
|
517
|
+
const allowed = new Set(["scenario", "input", "rationale"]);
|
|
518
|
+
const unknown = firstUnknown(members.byName, allowed);
|
|
519
|
+
if (unknown !== undefined) {
|
|
520
|
+
return unsupportedContainerMember(fileName, site.operationKey, `inputExamples[${index}]`, unknown);
|
|
521
|
+
}
|
|
522
|
+
const scenario = members.byName.get("scenario");
|
|
523
|
+
const input = members.byName.get("input");
|
|
524
|
+
if (scenario === undefined || input === undefined) {
|
|
525
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}] must declare scenario and input.`);
|
|
526
|
+
}
|
|
527
|
+
const scenarioProse = literalString(scenario.initializer);
|
|
528
|
+
if (scenarioProse === undefined) {
|
|
529
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].scenario must be a string literal.`);
|
|
530
|
+
}
|
|
531
|
+
const scenarioKey = `operations.${localeNamespace}.examples.${index}.scenario`;
|
|
532
|
+
edits.push(replaceExampleLocaleMember(scenario, "scenarioKey", scenarioKey, source));
|
|
533
|
+
for (const localeFile of localeFiles) {
|
|
534
|
+
todos.push({
|
|
535
|
+
localeFile,
|
|
536
|
+
operationKey: site.operationKey,
|
|
537
|
+
key: scenarioKey,
|
|
538
|
+
originalProse: scenarioProse,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
const rationale = members.byName.get("rationale");
|
|
542
|
+
if (rationale !== undefined) {
|
|
543
|
+
const rationaleProse = literalString(rationale.initializer);
|
|
544
|
+
if (rationaleProse === undefined) {
|
|
545
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].rationale must be a string literal.`);
|
|
546
|
+
}
|
|
547
|
+
const rationaleKey = `operations.${localeNamespace}.examples.${index}.rationale`;
|
|
548
|
+
edits.push(replaceExampleLocaleMember(rationale, "rationaleKey", rationaleKey, source));
|
|
549
|
+
for (const localeFile of localeFiles) {
|
|
550
|
+
todos.push({
|
|
551
|
+
localeFile,
|
|
552
|
+
operationKey: site.operationKey,
|
|
553
|
+
key: rationaleKey,
|
|
554
|
+
originalProse: rationaleProse,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { edits, localeTodos: todos };
|
|
560
|
+
}
|
|
561
|
+
function resolveOperationLocaleNamespace(members, fileName, site) {
|
|
562
|
+
const authoredNamespaces = new Set();
|
|
563
|
+
const directMembers = members.filter((member) => member !== undefined && member.fromSpread !== true);
|
|
564
|
+
const namespaceMembers = directMembers.length > 0 ? directMembers : members;
|
|
565
|
+
for (const member of namespaceMembers) {
|
|
566
|
+
const localeKey = literalString(member?.initializer);
|
|
567
|
+
if (localeKey === undefined)
|
|
568
|
+
continue;
|
|
569
|
+
const segments = localeKey.split(".");
|
|
570
|
+
if (segments[0] === "operations" && segments[1] !== undefined) {
|
|
571
|
+
authoredNamespaces.add(segments[1]);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (authoredNamespaces.size > 1) {
|
|
575
|
+
return {
|
|
576
|
+
refusal: refusal(fileName, site.operationKey, "locale_key_conflict", `Operation titleKey and descriptionKey declarations use different locale namespaces: ${[...authoredNamespaces].join(", ")}.`),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const authored = authoredNamespaces.values().next().value;
|
|
580
|
+
if (authored !== undefined)
|
|
581
|
+
return { namespace: authored };
|
|
582
|
+
// planExamples proves the id before using this placeholder to construct a
|
|
583
|
+
// locale key. Avoid validating an unproven binding such as <anonymous> as
|
|
584
|
+
// though it were an authored operation id.
|
|
585
|
+
if (!site.operationIdProven)
|
|
586
|
+
return { namespace: site.operationKey };
|
|
587
|
+
try {
|
|
588
|
+
return { namespace: operationIdToLocaleNamespace(site.operationKey) };
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
return {
|
|
592
|
+
refusal: invalidLocaleKeyRefusal(fileName, site.operationKey, `operations.${site.operationKey}.examples`, error),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function replaceExampleLocaleMember(member, newName, localeKey, source) {
|
|
597
|
+
return {
|
|
598
|
+
start: member.property.getStart(source),
|
|
599
|
+
end: member.property.getEnd(),
|
|
600
|
+
text: `${newName}: ${JSON.stringify(localeKey)}`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function discoverOperationSites(source, fileName, constObjects, operationIds, operationSites, excludedBindings, runtimeComposedInitializers) {
|
|
604
|
+
const sitesByStart = new Map();
|
|
605
|
+
const discoveredStarts = new Set();
|
|
606
|
+
const localIds = new Map(operationIds ?? []);
|
|
607
|
+
const localExcludedBindings = new Set(excludedBindings ?? []);
|
|
608
|
+
const refusals = [];
|
|
609
|
+
const operationFactories = collectSimpleOperationFactories(source);
|
|
610
|
+
const factoryCallIds = new Map();
|
|
611
|
+
for (const map of collectOperationsMaps(source, constObjects, fileName, refusals, runtimeComposedInitializers)) {
|
|
612
|
+
for (const property of map.properties) {
|
|
613
|
+
if (ts.isSpreadAssignment(property))
|
|
614
|
+
continue;
|
|
615
|
+
const key = staticPropertyName(property.name);
|
|
616
|
+
if (key === undefined) {
|
|
617
|
+
refusals.push(refusal(fileName, "<computed>", "non_literal", "An operations map uses a computed operation key."));
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const value = propertyValue(property);
|
|
621
|
+
if (value === undefined)
|
|
622
|
+
continue;
|
|
623
|
+
const unwrapped = unwrapExpression(value);
|
|
624
|
+
if (unwrapped === undefined)
|
|
625
|
+
continue;
|
|
626
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
627
|
+
localIds.set(unwrapped.text, key);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const operationObject = operationObjectFromExpression(unwrapped);
|
|
631
|
+
if (operationObject !== undefined) {
|
|
632
|
+
sitesByStart.set(operationObject.getStart(source), {
|
|
633
|
+
object: operationObject,
|
|
634
|
+
operationKey: key,
|
|
635
|
+
operationIdProven: true,
|
|
636
|
+
});
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (ts.isCallExpression(unwrapped) &&
|
|
640
|
+
ts.isIdentifier(unwrapped.expression) &&
|
|
641
|
+
operationFactories.has(unwrapped.expression.text) &&
|
|
642
|
+
!localExcludedBindings.has(unwrapped.expression.text)) {
|
|
643
|
+
const factoryName = unwrapped.expression.text;
|
|
644
|
+
const operationIdsForFactory = factoryCallIds.get(factoryName) ?? new Set();
|
|
645
|
+
operationIdsForFactory.add(key);
|
|
646
|
+
factoryCallIds.set(factoryName, operationIdsForFactory);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
650
|
+
sitesByStart.set(unwrapped.getStart(source), {
|
|
651
|
+
object: unwrapped,
|
|
652
|
+
operationKey: key,
|
|
653
|
+
operationIdProven: true,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for (const [factoryName, operationIdsForFactory] of factoryCallIds) {
|
|
659
|
+
const ids = [...operationIdsForFactory];
|
|
660
|
+
if (ids.length === 1) {
|
|
661
|
+
const operationId = ids[0];
|
|
662
|
+
if (operationId !== undefined)
|
|
663
|
+
localIds.set(factoryName, operationId);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
localExcludedBindings.add(factoryName);
|
|
667
|
+
refusals.push(refusal(fileName, factoryName, "factory_operation_id_ambiguous", `Factory ${factoryName} is registered under multiple operation ids: ${ids
|
|
668
|
+
.map((id) => JSON.stringify(id))
|
|
669
|
+
.join(", ")}. A shared operation body cannot own one examples locale namespace.`));
|
|
670
|
+
}
|
|
671
|
+
const visit = (node) => {
|
|
672
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
673
|
+
const indexedOperationKey = operationSites?.get(node.getStart(source));
|
|
674
|
+
if (indexedOperationKey !== undefined) {
|
|
675
|
+
sitesByStart.set(node.getStart(source), {
|
|
676
|
+
object: node,
|
|
677
|
+
operationKey: indexedOperationKey,
|
|
678
|
+
operationIdProven: true,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (ts.isCallExpression(node) && isOperationHelperCall(node)) {
|
|
683
|
+
const argument = operationArgument(node);
|
|
684
|
+
const bindingName = enclosingBindingName(node);
|
|
685
|
+
const unwrappedArgument = unwrapExpression(argument);
|
|
686
|
+
if (unwrappedArgument !== undefined && ts.isObjectLiteralExpression(unwrappedArgument)) {
|
|
687
|
+
discoveredStarts.add(unwrappedArgument.getStart(source));
|
|
688
|
+
}
|
|
689
|
+
if (bindingName !== undefined && localExcludedBindings.has(bindingName))
|
|
690
|
+
return;
|
|
691
|
+
const operationKey = (bindingName === undefined ? undefined : localIds.get(bindingName)) ??
|
|
692
|
+
bindingName ??
|
|
693
|
+
"<anonymous>";
|
|
694
|
+
if (unwrappedArgument === undefined || !ts.isObjectLiteralExpression(unwrappedArgument)) {
|
|
695
|
+
refusals.push(refusal(fileName, operationKey, "non_literal", "Operation helper argument must be an object literal."));
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
const object = unwrappedArgument;
|
|
699
|
+
if (ts.isObjectLiteralExpression(object)) {
|
|
700
|
+
const existing = sitesByStart.get(object.getStart(source));
|
|
701
|
+
if (existing === undefined) {
|
|
702
|
+
const operationIdProven = bindingName !== undefined && localIds.has(bindingName);
|
|
703
|
+
sitesByStart.set(object.getStart(source), {
|
|
704
|
+
object,
|
|
705
|
+
operationKey,
|
|
706
|
+
bindingName,
|
|
707
|
+
operationIdProven,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
else if (existing.operationKey.startsWith("<")) {
|
|
711
|
+
existing.operationKey = operationKey;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
ts.forEachChild(node, visit);
|
|
717
|
+
};
|
|
718
|
+
visit(source);
|
|
719
|
+
for (const start of sitesByStart.keys())
|
|
720
|
+
discoveredStarts.add(start);
|
|
721
|
+
return {
|
|
722
|
+
sites: [...sitesByStart.values()].sort((left, right) => left.object.getStart(source) - right.object.getStart(source)),
|
|
723
|
+
discoveredCount: discoveredStarts.size,
|
|
724
|
+
refusals,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function collectSimpleOperationFactories(source) {
|
|
728
|
+
const factories = new Set();
|
|
729
|
+
for (const statement of source.statements) {
|
|
730
|
+
if (!ts.isFunctionDeclaration(statement) ||
|
|
731
|
+
statement.name === undefined ||
|
|
732
|
+
statement.body === undefined ||
|
|
733
|
+
statement.body.statements.length !== 1) {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const returned = statement.body.statements[0];
|
|
737
|
+
if (!ts.isReturnStatement(returned) || returned.expression === undefined)
|
|
738
|
+
continue;
|
|
739
|
+
const expression = unwrapExpression(returned.expression);
|
|
740
|
+
if (expression !== undefined &&
|
|
741
|
+
ts.isCallExpression(expression) &&
|
|
742
|
+
isOperationHelperCall(expression)) {
|
|
743
|
+
const argument = operationArgument(expression);
|
|
744
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
745
|
+
if (object !== undefined && ts.isObjectLiteralExpression(object)) {
|
|
746
|
+
factories.add(statement.name.text);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return factories;
|
|
751
|
+
}
|
|
752
|
+
function collectOperationsMaps(source, constObjects, fileName, refusals, runtimeComposedInitializers) {
|
|
753
|
+
const maps = new Map();
|
|
754
|
+
const inspect = (expression, label) => {
|
|
755
|
+
const unwrapped = unwrapExpression(expression);
|
|
756
|
+
if (unwrapped === undefined)
|
|
757
|
+
return;
|
|
758
|
+
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
759
|
+
maps.set(unwrapped.getStart(source), unwrapped);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
763
|
+
const object = constObjects.get(unwrapped.text);
|
|
764
|
+
if (object !== undefined)
|
|
765
|
+
maps.set(object.getStart(source), object);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (ts.isCallExpression(unwrapped)) {
|
|
769
|
+
if (runtimeComposedInitializers?.has(expression.getStart(source)) === true)
|
|
770
|
+
return;
|
|
771
|
+
refusals.push(refusal(fileName, label, "factory_composed_operations", "The operations map is produced by a factory call and cannot be statically enumerated."));
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
for (const statement of source.statements) {
|
|
775
|
+
if (ts.isVariableStatement(statement)) {
|
|
776
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
777
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
778
|
+
declaration.name.text === "operations" &&
|
|
779
|
+
declaration.initializer !== undefined) {
|
|
780
|
+
inspect(declaration.initializer, "<operations>");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const visit = (node) => {
|
|
786
|
+
if (ts.isPropertyAssignment(node) && staticPropertyName(node.name) === "operations") {
|
|
787
|
+
// Only provider declarations own an `operations` map: the property
|
|
788
|
+
// must sit inside an argument of defineProvider()/buildProvider()-style
|
|
789
|
+
// call, or its initializer (possibly via a same-file const) must
|
|
790
|
+
// contain defineOperation/defineStreamOperation members. A bare
|
|
791
|
+
// `operations:` key elsewhere (a zod schema field, a config object)
|
|
792
|
+
// is not a declaration site; treating it as one produced false
|
|
793
|
+
// factory_composed_operations refusals on ekitan's
|
|
794
|
+
// scripts/fixture-integrity.ts, whose schema field calls
|
|
795
|
+
// .superRefine(...).
|
|
796
|
+
if (isProviderOperationsProperty(node, constObjects)) {
|
|
797
|
+
inspect(node.initializer, "<operations>");
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
ts.forEachChild(node, visit);
|
|
801
|
+
};
|
|
802
|
+
visit(source);
|
|
803
|
+
return [...maps.values()];
|
|
804
|
+
}
|
|
805
|
+
function isProviderOperationsProperty(node, constObjects) {
|
|
806
|
+
// (a) The initializer (direct or via same-file const) mentions
|
|
807
|
+
// defineOperation / defineStreamOperation — the strongest signal.
|
|
808
|
+
const target = ts.isIdentifier(unwrapExpression(node.initializer) ?? node.initializer)
|
|
809
|
+
? constObjects.get(unwrapExpression(node.initializer).text)
|
|
810
|
+
: undefined;
|
|
811
|
+
const initializerText = (target ?? node.initializer).getText();
|
|
812
|
+
if (/\bdefine(?:Stream)?Operation\b/.test(initializerText))
|
|
813
|
+
return true;
|
|
814
|
+
// (b) The property is inside a call argument whose callee looks like a
|
|
815
|
+
// provider builder (defineProvider(...)(...) / buildProvider(...)).
|
|
816
|
+
let current = node;
|
|
817
|
+
while (current.parent !== undefined) {
|
|
818
|
+
const parent = current.parent;
|
|
819
|
+
if (ts.isCallExpression(parent)) {
|
|
820
|
+
const callee = parent.expression.getText();
|
|
821
|
+
if (/(?:defineProvider|Provider)\b/.test(callee))
|
|
822
|
+
return true;
|
|
823
|
+
// A call expression that is itself the invocation of a
|
|
824
|
+
// defineProvider(...) result: defineProvider({...})({operations})
|
|
825
|
+
if (ts.isCallExpression(parent.expression)) {
|
|
826
|
+
if (/\bdefineProvider\b/.test(parent.expression.expression.getText())) {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
if (ts.isSourceFile(parent))
|
|
833
|
+
return false;
|
|
834
|
+
current = parent;
|
|
835
|
+
}
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
function operationObjectFromExpression(expression) {
|
|
839
|
+
const unwrapped = unwrapExpression(expression);
|
|
840
|
+
if (unwrapped === undefined || !ts.isCallExpression(unwrapped))
|
|
841
|
+
return undefined;
|
|
842
|
+
if (!isOperationHelperCall(unwrapped))
|
|
843
|
+
return undefined;
|
|
844
|
+
const argument = operationArgument(unwrapped);
|
|
845
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
846
|
+
return object !== undefined && ts.isObjectLiteralExpression(object) ? object : undefined;
|
|
847
|
+
}
|
|
848
|
+
function isOperationHelperCall(call) {
|
|
849
|
+
if (ts.isIdentifier(call.expression) && OPERATION_HELPERS.has(call.expression.text)) {
|
|
850
|
+
return call.arguments.length === 1;
|
|
851
|
+
}
|
|
852
|
+
return (ts.isCallExpression(call.expression) &&
|
|
853
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
854
|
+
OPERATION_HELPERS.has(call.expression.expression.text) &&
|
|
855
|
+
call.expression.arguments.length === 0 &&
|
|
856
|
+
call.arguments.length === 1);
|
|
857
|
+
}
|
|
858
|
+
function operationArgument(call) {
|
|
859
|
+
if (ts.isIdentifier(call.expression) && OPERATION_HELPERS.has(call.expression.text)) {
|
|
860
|
+
return call.arguments.length === 1 ? call.arguments[0] : undefined;
|
|
861
|
+
}
|
|
862
|
+
if (ts.isCallExpression(call.expression) &&
|
|
863
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
864
|
+
OPERATION_HELPERS.has(call.expression.expression.text) &&
|
|
865
|
+
call.expression.arguments.length === 0) {
|
|
866
|
+
return call.arguments.length === 1 ? call.arguments[0] : undefined;
|
|
867
|
+
}
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
function enclosingBindingName(node) {
|
|
871
|
+
let current = node;
|
|
872
|
+
while (current !== undefined) {
|
|
873
|
+
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
|
|
874
|
+
return current.name.text;
|
|
875
|
+
}
|
|
876
|
+
if (ts.isFunctionDeclaration(current) && current.name !== undefined) {
|
|
877
|
+
return current.name.text;
|
|
878
|
+
}
|
|
879
|
+
if (ts.isPropertyAssignment(current))
|
|
880
|
+
return staticPropertyName(current.name);
|
|
881
|
+
if (ts.isExportAssignment(current))
|
|
882
|
+
return "default";
|
|
883
|
+
current = current.parent;
|
|
884
|
+
}
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
function collectModuleConstObjects(source) {
|
|
888
|
+
const objects = new Map();
|
|
889
|
+
for (const statement of source.statements) {
|
|
890
|
+
if (!ts.isVariableStatement(statement))
|
|
891
|
+
continue;
|
|
892
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
893
|
+
continue;
|
|
894
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
895
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined)
|
|
896
|
+
continue;
|
|
897
|
+
const expression = unwrapExpression(declaration.initializer);
|
|
898
|
+
if (expression !== undefined && ts.isObjectLiteralExpression(expression)) {
|
|
899
|
+
objects.set(declaration.name.text, expression);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return objects;
|
|
904
|
+
}
|
|
905
|
+
function collectModuleConstArrays(source) {
|
|
906
|
+
const arrays = new Map();
|
|
907
|
+
for (const statement of source.statements) {
|
|
908
|
+
if (!ts.isVariableStatement(statement))
|
|
909
|
+
continue;
|
|
910
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
911
|
+
continue;
|
|
912
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
913
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined)
|
|
914
|
+
continue;
|
|
915
|
+
const expression = unwrapExpression(declaration.initializer);
|
|
916
|
+
if (expression !== undefined && ts.isArrayLiteralExpression(expression)) {
|
|
917
|
+
arrays.set(declaration.name.text, expression);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return arrays;
|
|
922
|
+
}
|
|
923
|
+
function expandArrayElements(array, constArrays, fileName, operationKey, seen = new Set()) {
|
|
924
|
+
if (seen.has(array)) {
|
|
925
|
+
return nonLiteral(fileName, operationKey, "A module-level array spread is recursive.");
|
|
926
|
+
}
|
|
927
|
+
seen.add(array);
|
|
928
|
+
const elements = [];
|
|
929
|
+
for (const element of array.elements) {
|
|
930
|
+
if (!ts.isSpreadElement(element)) {
|
|
931
|
+
elements.push(element);
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
const expression = unwrapExpression(element.expression);
|
|
935
|
+
const spreadArray = expression !== undefined && ts.isArrayLiteralExpression(expression)
|
|
936
|
+
? expression
|
|
937
|
+
: expression !== undefined && ts.isIdentifier(expression)
|
|
938
|
+
? constArrays.get(expression.text)
|
|
939
|
+
: undefined;
|
|
940
|
+
if (spreadArray === undefined) {
|
|
941
|
+
return nonLiteral(fileName, operationKey, "inputExamples contains an unresolved array spread.");
|
|
942
|
+
}
|
|
943
|
+
const expanded = expandArrayElements(spreadArray, constArrays, fileName, operationKey, new Set(seen));
|
|
944
|
+
if ("refusal" in expanded)
|
|
945
|
+
return expanded;
|
|
946
|
+
elements.push(...expanded.elements);
|
|
947
|
+
}
|
|
948
|
+
return { elements };
|
|
949
|
+
}
|
|
950
|
+
function expandMembers(object, source, constObjects, fileName, operationKey, staticObjectResolver, seen = new Set()) {
|
|
951
|
+
if (seen.has(object)) {
|
|
952
|
+
return nonLiteral(fileName, operationKey, "A module-level object spread is recursive.");
|
|
953
|
+
}
|
|
954
|
+
seen.add(object);
|
|
955
|
+
const members = [];
|
|
956
|
+
for (const property of object.properties) {
|
|
957
|
+
if (ts.isSpreadAssignment(property)) {
|
|
958
|
+
const spreadObject = resolveStaticObjectExpression(property.expression, source, constObjects, staticObjectResolver);
|
|
959
|
+
if (spreadObject === undefined) {
|
|
960
|
+
members.push({ name: "<spread>", property, source });
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const expanded = expandMembers(spreadObject.object, spreadObject.source, constObjects, fileName, operationKey, staticObjectResolver, new Set(seen));
|
|
964
|
+
if ("refusal" in expanded)
|
|
965
|
+
return expanded;
|
|
966
|
+
members.push(...expanded.members.map((member) => ({ ...member, fromSpread: true })));
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const name = staticPropertyName(property.name);
|
|
970
|
+
if (name === undefined) {
|
|
971
|
+
return nonLiteral(fileName, operationKey, "A declaration member uses a computed name.");
|
|
972
|
+
}
|
|
973
|
+
const initializer = propertyValue(property);
|
|
974
|
+
members.push({ name, property, initializer, source });
|
|
975
|
+
}
|
|
976
|
+
return { members };
|
|
977
|
+
}
|
|
978
|
+
function indexMembers(members, fileName, operationKey) {
|
|
979
|
+
const byName = new Map();
|
|
980
|
+
const spreads = [];
|
|
981
|
+
for (const member of members) {
|
|
982
|
+
if (member.name === "<spread>") {
|
|
983
|
+
spreads.push(member);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const previous = byName.get(member.name);
|
|
987
|
+
if (previous !== undefined && previous.fromSpread !== true && member.fromSpread !== true) {
|
|
988
|
+
return nonLiteral(fileName, operationKey, `Duplicate ${member.name} declarations cannot be collapsed safely.`);
|
|
989
|
+
}
|
|
990
|
+
byName.set(member.name, member);
|
|
991
|
+
}
|
|
992
|
+
return { byName, spreads };
|
|
993
|
+
}
|
|
994
|
+
function indexMembersFromObject(object, fileName, operationKey) {
|
|
995
|
+
const members = [];
|
|
996
|
+
for (const property of object.properties) {
|
|
997
|
+
if (ts.isSpreadAssignment(property)) {
|
|
998
|
+
return nonLiteral(fileName, operationKey, "Example objects cannot contain spreads.");
|
|
999
|
+
}
|
|
1000
|
+
const name = staticPropertyName(property.name);
|
|
1001
|
+
const initializer = propertyValue(property);
|
|
1002
|
+
if (name === undefined || initializer === undefined) {
|
|
1003
|
+
return nonLiteral(fileName, operationKey, "Example members must be literal properties.");
|
|
1004
|
+
}
|
|
1005
|
+
members.push({ name, property, initializer, source: object.getSourceFile() });
|
|
1006
|
+
}
|
|
1007
|
+
const indexed = indexMembers(members, fileName, operationKey);
|
|
1008
|
+
if ("refusal" in indexed)
|
|
1009
|
+
return indexed;
|
|
1010
|
+
return { byName: indexed.byName };
|
|
1011
|
+
}
|
|
1012
|
+
function resolveObjectInitializer(member, rootSource, constObjects, staticObjectResolver) {
|
|
1013
|
+
if (member.initializer === undefined)
|
|
1014
|
+
return undefined;
|
|
1015
|
+
return resolveStaticObjectExpression(member.initializer, member.source, constObjects, staticObjectResolver, rootSource);
|
|
1016
|
+
}
|
|
1017
|
+
function resolveStaticObjectExpression(expression, source, constObjects, staticObjectResolver, rootSource = source) {
|
|
1018
|
+
const unwrapped = unwrapExpression(expression);
|
|
1019
|
+
if (unwrapped === undefined)
|
|
1020
|
+
return undefined;
|
|
1021
|
+
if (ts.isObjectLiteralExpression(unwrapped))
|
|
1022
|
+
return { object: unwrapped, source };
|
|
1023
|
+
const externallyResolved = staticObjectResolver?.(unwrapped, source);
|
|
1024
|
+
if (externallyResolved !== undefined)
|
|
1025
|
+
return externallyResolved;
|
|
1026
|
+
if (source === rootSource && ts.isIdentifier(unwrapped)) {
|
|
1027
|
+
const object = constObjects.get(unwrapped.text);
|
|
1028
|
+
if (object !== undefined)
|
|
1029
|
+
return { object, source };
|
|
1030
|
+
}
|
|
1031
|
+
return undefined;
|
|
1032
|
+
}
|
|
1033
|
+
function memberMap(members) {
|
|
1034
|
+
return new Map(members.map((member) => [member.name, member]));
|
|
1035
|
+
}
|
|
1036
|
+
function firstUnknown(members, allowed) {
|
|
1037
|
+
for (const name of members.keys()) {
|
|
1038
|
+
if (!allowed.has(name))
|
|
1039
|
+
return name;
|
|
1040
|
+
}
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
function unsupportedContainerMember(fileName, operationKey, container, member) {
|
|
1044
|
+
return {
|
|
1045
|
+
refusal: refusal(fileName, operationKey, "unsupported_member", `${container}.${member} has no declared ADR-0009 transform.`),
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function literalBooleanMember(member, fileName, operationKey, label) {
|
|
1049
|
+
if (member === undefined)
|
|
1050
|
+
return { value: undefined };
|
|
1051
|
+
const expression = unwrapExpression(member.initializer);
|
|
1052
|
+
if (expression?.kind === ts.SyntaxKind.TrueKeyword)
|
|
1053
|
+
return { value: true };
|
|
1054
|
+
if (expression?.kind === ts.SyntaxKind.FalseKeyword)
|
|
1055
|
+
return { value: false };
|
|
1056
|
+
return nonLiteral(fileName, operationKey, `${label} must be a boolean literal.`);
|
|
1057
|
+
}
|
|
1058
|
+
function literalString(expression) {
|
|
1059
|
+
const value = unwrapExpression(expression);
|
|
1060
|
+
if (value === undefined)
|
|
1061
|
+
return undefined;
|
|
1062
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
1063
|
+
return value.text;
|
|
1064
|
+
return undefined;
|
|
1065
|
+
}
|
|
1066
|
+
function equivalentLiteral(left, right) {
|
|
1067
|
+
const leftValue = literalValue(left);
|
|
1068
|
+
const rightValue = literalValue(right);
|
|
1069
|
+
if (leftValue === NOT_LITERAL || rightValue === NOT_LITERAL)
|
|
1070
|
+
return undefined;
|
|
1071
|
+
return JSON.stringify(leftValue) === JSON.stringify(rightValue);
|
|
1072
|
+
}
|
|
1073
|
+
const NOT_LITERAL = Symbol("not-literal");
|
|
1074
|
+
function literalValue(expression) {
|
|
1075
|
+
const node = unwrapExpression(expression);
|
|
1076
|
+
if (node === undefined)
|
|
1077
|
+
return NOT_LITERAL;
|
|
1078
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
1079
|
+
return node.text;
|
|
1080
|
+
if (ts.isNumericLiteral(node))
|
|
1081
|
+
return Number(node.text);
|
|
1082
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
1083
|
+
return true;
|
|
1084
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
1085
|
+
return false;
|
|
1086
|
+
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
1087
|
+
return null;
|
|
1088
|
+
if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
|
|
1089
|
+
if (node.operator === ts.SyntaxKind.MinusToken)
|
|
1090
|
+
return -Number(node.operand.text);
|
|
1091
|
+
if (node.operator === ts.SyntaxKind.PlusToken)
|
|
1092
|
+
return Number(node.operand.text);
|
|
1093
|
+
}
|
|
1094
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
1095
|
+
const result = [];
|
|
1096
|
+
for (const element of node.elements) {
|
|
1097
|
+
if (ts.isSpreadElement(element))
|
|
1098
|
+
return NOT_LITERAL;
|
|
1099
|
+
const value = literalValue(element);
|
|
1100
|
+
if (value === NOT_LITERAL)
|
|
1101
|
+
return NOT_LITERAL;
|
|
1102
|
+
result.push(value);
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1107
|
+
const result = {};
|
|
1108
|
+
for (const property of node.properties) {
|
|
1109
|
+
if (!ts.isPropertyAssignment(property))
|
|
1110
|
+
return NOT_LITERAL;
|
|
1111
|
+
const name = staticPropertyName(property.name);
|
|
1112
|
+
const value = literalValue(property.initializer);
|
|
1113
|
+
if (name === undefined || value === NOT_LITERAL)
|
|
1114
|
+
return NOT_LITERAL;
|
|
1115
|
+
result[name] = value;
|
|
1116
|
+
}
|
|
1117
|
+
return result;
|
|
1118
|
+
}
|
|
1119
|
+
return NOT_LITERAL;
|
|
1120
|
+
}
|
|
1121
|
+
function memberText(member, name) {
|
|
1122
|
+
if (member.initializer === undefined)
|
|
1123
|
+
return name;
|
|
1124
|
+
return `${name}: ${member.initializer.getText(member.source)}`;
|
|
1125
|
+
}
|
|
1126
|
+
function addInsertion(insertions, sourceContainer, text) {
|
|
1127
|
+
const items = insertions.get(sourceContainer) ?? [];
|
|
1128
|
+
items.push({ source: sourceContainer, text });
|
|
1129
|
+
insertions.set(sourceContainer, items);
|
|
1130
|
+
}
|
|
1131
|
+
function removalRanges(object, properties, source) {
|
|
1132
|
+
const unique = [...new Set(properties)];
|
|
1133
|
+
const text = source.getFullText();
|
|
1134
|
+
const ranges = [];
|
|
1135
|
+
for (const property of unique) {
|
|
1136
|
+
let start = property.getStart(source);
|
|
1137
|
+
let end = property.getEnd();
|
|
1138
|
+
let cursor = end;
|
|
1139
|
+
while (cursor < object.getEnd() && /\s/.test(text.charAt(cursor)))
|
|
1140
|
+
cursor += 1;
|
|
1141
|
+
if (text.charAt(cursor) === ",") {
|
|
1142
|
+
end = cursor + 1;
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
cursor = start - 1;
|
|
1146
|
+
while (cursor > object.getStart(source) && /\s/.test(text.charAt(cursor)))
|
|
1147
|
+
cursor -= 1;
|
|
1148
|
+
if (text.charAt(cursor) === ",")
|
|
1149
|
+
start = cursor;
|
|
1150
|
+
}
|
|
1151
|
+
ranges.push({ start, end, text: "" });
|
|
1152
|
+
}
|
|
1153
|
+
return mergeRemovalEdits(ranges);
|
|
1154
|
+
}
|
|
1155
|
+
function mergeRemovalEdits(edits) {
|
|
1156
|
+
const ordered = [...edits].sort((left, right) => left.start - right.start);
|
|
1157
|
+
const merged = [];
|
|
1158
|
+
for (const edit of ordered) {
|
|
1159
|
+
const previous = merged.at(-1);
|
|
1160
|
+
if (previous !== undefined && edit.start <= previous.end) {
|
|
1161
|
+
merged[merged.length - 1] = {
|
|
1162
|
+
start: previous.start,
|
|
1163
|
+
end: Math.max(previous.end, edit.end),
|
|
1164
|
+
text: "",
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
merged.push(edit);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return merged;
|
|
1172
|
+
}
|
|
1173
|
+
function removeFirst(items, value) {
|
|
1174
|
+
const index = items.indexOf(value);
|
|
1175
|
+
if (index >= 0)
|
|
1176
|
+
items.splice(index, 1);
|
|
1177
|
+
}
|
|
1178
|
+
function rangesOverlap(left, right) {
|
|
1179
|
+
return left.start < right.end && right.start < left.end;
|
|
1180
|
+
}
|
|
1181
|
+
function applyEdits(sourceText, edits) {
|
|
1182
|
+
const ordered = [...edits].sort((left, right) => right.start - left.start || right.end - left.end);
|
|
1183
|
+
let code = sourceText;
|
|
1184
|
+
for (const edit of ordered) {
|
|
1185
|
+
code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
|
|
1186
|
+
}
|
|
1187
|
+
return code;
|
|
1188
|
+
}
|
|
1189
|
+
function normalizeEdits(edits, fileName) {
|
|
1190
|
+
const unique = new Map();
|
|
1191
|
+
for (const edit of edits) {
|
|
1192
|
+
const key = `${edit.start}:${edit.end}`;
|
|
1193
|
+
const previous = unique.get(key);
|
|
1194
|
+
if (previous === undefined) {
|
|
1195
|
+
unique.set(key, edit);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (previous.text !== edit.text) {
|
|
1199
|
+
return nonLiteral(fileName, "<shared>", "A module-level input examples array is shared by operations that need different locale keys.");
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return { edits: [...unique.values()] };
|
|
1203
|
+
}
|
|
1204
|
+
function indentationAt(text, position) {
|
|
1205
|
+
const lineStart = text.lastIndexOf("\n", position - 1) + 1;
|
|
1206
|
+
return text.slice(lineStart, position).match(/^\s*/)?.[0] ?? "";
|
|
1207
|
+
}
|
|
1208
|
+
function propertyValue(property) {
|
|
1209
|
+
if (ts.isPropertyAssignment(property))
|
|
1210
|
+
return property.initializer;
|
|
1211
|
+
if (ts.isShorthandPropertyAssignment(property))
|
|
1212
|
+
return property.name;
|
|
1213
|
+
return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
function staticPropertyName(name) {
|
|
1216
|
+
if (name === undefined)
|
|
1217
|
+
return undefined;
|
|
1218
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
1219
|
+
return name.text;
|
|
1220
|
+
}
|
|
1221
|
+
return undefined;
|
|
1222
|
+
}
|
|
1223
|
+
function unwrapExpression(expression) {
|
|
1224
|
+
let current = expression;
|
|
1225
|
+
while (current !== undefined) {
|
|
1226
|
+
if (ts.isParenthesizedExpression(current) ||
|
|
1227
|
+
ts.isAsExpression(current) ||
|
|
1228
|
+
ts.isTypeAssertionExpression(current) ||
|
|
1229
|
+
ts.isSatisfiesExpression(current) ||
|
|
1230
|
+
ts.isNonNullExpression(current)) {
|
|
1231
|
+
current = current.expression;
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
return current;
|
|
1235
|
+
}
|
|
1236
|
+
return undefined;
|
|
1237
|
+
}
|
|
1238
|
+
function parseSource(fileName, sourceText) {
|
|
1239
|
+
return ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
1240
|
+
}
|
|
1241
|
+
function firstSyntaxError(source) {
|
|
1242
|
+
const diagnostics = source
|
|
1243
|
+
.parseDiagnostics;
|
|
1244
|
+
if (diagnostics === undefined || diagnostics.length === 0)
|
|
1245
|
+
return undefined;
|
|
1246
|
+
const first = diagnostics[0];
|
|
1247
|
+
if (first === undefined)
|
|
1248
|
+
return undefined;
|
|
1249
|
+
const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
|
|
1250
|
+
const { line } = source.getLineAndCharacterOfPosition(first.start);
|
|
1251
|
+
return `${message} (line ${line + 1})`;
|
|
1252
|
+
}
|
|
1253
|
+
function refusal(file, operationKey, reason, detail) {
|
|
1254
|
+
return { file, operationKey, reason, detail };
|
|
1255
|
+
}
|
|
1256
|
+
function findInvalidLocaleTodo(todos, fileName) {
|
|
1257
|
+
for (const todo of todos) {
|
|
1258
|
+
try {
|
|
1259
|
+
assertProviderLocaleKey(todo.key);
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
return invalidLocaleKeyRefusal(fileName, todo.operationKey, todo.key, error);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return undefined;
|
|
1266
|
+
}
|
|
1267
|
+
function invalidLocaleKeyRefusal(fileName, operationKey, localeKey, error) {
|
|
1268
|
+
const validatorDetail = error instanceof Error ? error.message : String(error);
|
|
1269
|
+
return refusal(fileName, operationKey, "invalid_locale_key", `Refusing to write invalid provider locale key ${JSON.stringify(localeKey)}: ${validatorDetail}`);
|
|
1270
|
+
}
|
|
1271
|
+
function nonLiteral(fileName, operationKey, detail) {
|
|
1272
|
+
return {
|
|
1273
|
+
refusal: refusal(fileName, operationKey, "non_literal", detail),
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/** Run the file transform repository-wide, committing writes only if every file is provable. */
|
|
1277
|
+
export function migrateOperationDeclarationRepository(providerRootInput, options = {}) {
|
|
1278
|
+
const providerRoot = resolve(providerRootInput);
|
|
1279
|
+
const sourceFiles = collectSourceFiles(providerRoot);
|
|
1280
|
+
const localeFiles = collectLocaleFiles(providerRoot);
|
|
1281
|
+
const repositoryIndex = buildRepositoryOperationIndex(sourceFiles, providerRoot);
|
|
1282
|
+
const pendingWrites = new Map();
|
|
1283
|
+
const changedFiles = [];
|
|
1284
|
+
const todos = [];
|
|
1285
|
+
const refusals = [...repositoryIndex.refusals];
|
|
1286
|
+
let operationCount = 0;
|
|
1287
|
+
let repositoryDiscoveredCount = 0;
|
|
1288
|
+
for (const sourcePath of sourceFiles) {
|
|
1289
|
+
const relativePath = slash(relative(providerRoot, sourcePath));
|
|
1290
|
+
const result = migrateOperationDeclarationInternal(readFileSync(sourcePath, "utf8"), relativePath, {
|
|
1291
|
+
operationIds: repositoryIndex.operationIds.get(sourcePath),
|
|
1292
|
+
localeFiles,
|
|
1293
|
+
}, {
|
|
1294
|
+
operationSites: repositoryIndex.operationSites.get(sourcePath),
|
|
1295
|
+
excludedBindings: repositoryIndex.excludedBindings.get(sourcePath),
|
|
1296
|
+
staticObjectResolver: repositoryIndex.staticObjectResolverFor(sourcePath),
|
|
1297
|
+
runtimeComposedInitializers: repositoryIndex.runtimeComposedInitializers.get(sourcePath),
|
|
1298
|
+
recordDiscoveredSites: (count) => {
|
|
1299
|
+
repositoryDiscoveredCount += count;
|
|
1300
|
+
},
|
|
1301
|
+
});
|
|
1302
|
+
if (result.status === "refused") {
|
|
1303
|
+
refusals.push(...result.refusals);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
operationCount += result.operations;
|
|
1307
|
+
if (result.status === "migrated") {
|
|
1308
|
+
pendingWrites.set(sourcePath, result.code);
|
|
1309
|
+
changedFiles.push(relativePath);
|
|
1310
|
+
todos.push(...result.localeTodos);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const notes = repositoryDiscoveredCount === 0
|
|
1314
|
+
? []
|
|
1315
|
+
: repositoryIndex.declarations.flatMap((declaration) => declaration.runtimeInitializer === undefined
|
|
1316
|
+
? []
|
|
1317
|
+
: [
|
|
1318
|
+
{
|
|
1319
|
+
code: "runtime_composed_registry",
|
|
1320
|
+
path: slash(relative(providerRoot, declaration.path)),
|
|
1321
|
+
initializer: declaration.runtimeInitializer,
|
|
1322
|
+
},
|
|
1323
|
+
]);
|
|
1324
|
+
if (repositoryIndex.declarations.length > 0 && repositoryDiscoveredCount === 0) {
|
|
1325
|
+
for (const declaration of repositoryIndex.declarations) {
|
|
1326
|
+
refusals.push(refusal(slash(relative(providerRoot, declaration.path)), "<operations>", "no_operations_discovered", `Provider construct ${declaration.construct} declares operations via unresolved initializer ${JSON.stringify(declaration.initializer.getText(declaration.initializer.getSourceFile()))}, but repository-wide discovery found zero operation sites.`));
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const todoConflict = findLocaleTodoConflict(todos);
|
|
1330
|
+
if (todoConflict !== undefined)
|
|
1331
|
+
refusals.push(todoConflict);
|
|
1332
|
+
if (refusals.length > 0) {
|
|
1333
|
+
return {
|
|
1334
|
+
status: "refused",
|
|
1335
|
+
providerRoot,
|
|
1336
|
+
refusals,
|
|
1337
|
+
operationCount,
|
|
1338
|
+
notes,
|
|
1339
|
+
changedFiles,
|
|
1340
|
+
localeTodoCount: todos.length,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
for (const [path, code] of renderLocaleCatalogWrites(providerRoot, todos)) {
|
|
1344
|
+
pendingWrites.set(path, code);
|
|
1345
|
+
}
|
|
1346
|
+
if (pendingWrites.size === 0) {
|
|
1347
|
+
return {
|
|
1348
|
+
status: "unchanged",
|
|
1349
|
+
providerRoot,
|
|
1350
|
+
changedFiles: [],
|
|
1351
|
+
operationCount,
|
|
1352
|
+
notes,
|
|
1353
|
+
localeTodoCount: 0,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
const sidecar = todos.length > 0 ? SIDECAR_NAME : undefined;
|
|
1357
|
+
if (!options.check) {
|
|
1358
|
+
for (const [path, code] of pendingWrites)
|
|
1359
|
+
writeFileSync(path, code, "utf8");
|
|
1360
|
+
if (sidecar !== undefined) {
|
|
1361
|
+
writeFileSync(join(providerRoot, sidecar), renderLocaleTodoSidecar(todos), "utf8");
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
status: options.check ? "would-migrate" : "migrated",
|
|
1366
|
+
providerRoot,
|
|
1367
|
+
changedFiles,
|
|
1368
|
+
operationCount,
|
|
1369
|
+
notes,
|
|
1370
|
+
sidecar,
|
|
1371
|
+
localeTodoCount: todos.length,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
export function renderLocaleTodoSidecar(todos) {
|
|
1375
|
+
const localeFiles = {};
|
|
1376
|
+
for (const todo of [...todos].sort((left, right) => left.localeFile.localeCompare(right.localeFile) || left.key.localeCompare(right.key))) {
|
|
1377
|
+
localeFiles[todo.localeFile] ??= {};
|
|
1378
|
+
localeFiles[todo.localeFile][todo.key] = todo.originalProse;
|
|
1379
|
+
}
|
|
1380
|
+
return `${JSON.stringify({ schemaVersion: 1, localeFiles }, null, 2)}\n`;
|
|
1381
|
+
}
|
|
1382
|
+
function renderLocaleCatalogWrites(providerRoot, todos) {
|
|
1383
|
+
const todosByFile = new Map();
|
|
1384
|
+
for (const todo of todos) {
|
|
1385
|
+
const fileTodos = todosByFile.get(todo.localeFile) ?? [];
|
|
1386
|
+
fileTodos.push(todo);
|
|
1387
|
+
todosByFile.set(todo.localeFile, fileTodos);
|
|
1388
|
+
}
|
|
1389
|
+
const englishPath = "locales/en.json";
|
|
1390
|
+
const englishTodos = todosByFile.get(englishPath);
|
|
1391
|
+
if (englishTodos === undefined)
|
|
1392
|
+
return new Map();
|
|
1393
|
+
const english = readLocaleCatalog(join(providerRoot, englishPath));
|
|
1394
|
+
applyLocaleTodos(english, englishTodos);
|
|
1395
|
+
const writes = new Map([
|
|
1396
|
+
[join(providerRoot, englishPath), renderCanonicalLocaleCatalog(english)],
|
|
1397
|
+
]);
|
|
1398
|
+
for (const [localeFile, fileTodos] of todosByFile) {
|
|
1399
|
+
if (localeFile === englishPath)
|
|
1400
|
+
continue;
|
|
1401
|
+
const catalog = readLocaleCatalog(join(providerRoot, localeFile));
|
|
1402
|
+
applyLocaleTodos(catalog, fileTodos);
|
|
1403
|
+
writes.set(join(providerRoot, localeFile), renderCanonicalLocaleCatalog(reorderLikeReference(english, catalog)));
|
|
1404
|
+
}
|
|
1405
|
+
return writes;
|
|
1406
|
+
}
|
|
1407
|
+
function readLocaleCatalog(path) {
|
|
1408
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
1409
|
+
if (!isRecord(value))
|
|
1410
|
+
throw new Error(`${path} must contain a JSON object.`);
|
|
1411
|
+
return value;
|
|
1412
|
+
}
|
|
1413
|
+
function applyLocaleTodos(catalog, todos) {
|
|
1414
|
+
for (const todo of todos) {
|
|
1415
|
+
const segments = todo.key.split(".");
|
|
1416
|
+
const leaf = segments.pop();
|
|
1417
|
+
if (leaf === undefined)
|
|
1418
|
+
continue;
|
|
1419
|
+
let cursor = catalog;
|
|
1420
|
+
for (const segment of segments) {
|
|
1421
|
+
const child = cursor[segment];
|
|
1422
|
+
if (isRecord(child)) {
|
|
1423
|
+
cursor = child;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
const created = {};
|
|
1427
|
+
cursor[segment] = created;
|
|
1428
|
+
cursor = created;
|
|
1429
|
+
}
|
|
1430
|
+
cursor[leaf] = todo.originalProse;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/** Match provider-contract's canonical locale serialization exactly. */
|
|
1434
|
+
function renderCanonicalLocaleCatalog(value) {
|
|
1435
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Put shared keys first in English order, then retain locale-only authored order.
|
|
1439
|
+
* Arrays keep their shape and use the English item at the same index as a reference.
|
|
1440
|
+
*/
|
|
1441
|
+
function reorderLikeReference(reference, value) {
|
|
1442
|
+
if (Array.isArray(value)) {
|
|
1443
|
+
const referenceArray = Array.isArray(reference) ? reference : [];
|
|
1444
|
+
return value.map((item, index) => reorderLikeReference(referenceArray[index], item));
|
|
1445
|
+
}
|
|
1446
|
+
if (!isRecord(value))
|
|
1447
|
+
return value;
|
|
1448
|
+
const referenceRecord = isRecord(reference) ? reference : {};
|
|
1449
|
+
const ordered = {};
|
|
1450
|
+
for (const key of Object.keys(referenceRecord)) {
|
|
1451
|
+
if (Object.hasOwn(value, key)) {
|
|
1452
|
+
ordered[key] = reorderLikeReference(referenceRecord[key], value[key]);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1456
|
+
if (!Object.hasOwn(ordered, key)) {
|
|
1457
|
+
ordered[key] = reorderLikeReference(Object.hasOwn(referenceRecord, key) ? referenceRecord[key] : undefined, child);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return ordered;
|
|
1461
|
+
}
|
|
1462
|
+
function isRecord(value) {
|
|
1463
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1464
|
+
}
|
|
1465
|
+
function findLocaleTodoConflict(todos) {
|
|
1466
|
+
const values = new Map();
|
|
1467
|
+
for (const todo of todos) {
|
|
1468
|
+
const id = `${todo.localeFile}\0${todo.key}`;
|
|
1469
|
+
const previous = values.get(id);
|
|
1470
|
+
if (previous !== undefined && previous.originalProse !== todo.originalProse) {
|
|
1471
|
+
return refusal(SIDECAR_NAME, todo.operationKey, "locale_todo_conflict", `${todo.localeFile} receives conflicting prose for ${todo.key}.`);
|
|
1472
|
+
}
|
|
1473
|
+
values.set(id, todo);
|
|
1474
|
+
}
|
|
1475
|
+
return undefined;
|
|
1476
|
+
}
|
|
1477
|
+
function collectSourceFiles(root) {
|
|
1478
|
+
const files = [];
|
|
1479
|
+
if (!existsSync(root))
|
|
1480
|
+
return files;
|
|
1481
|
+
const walk = (directory) => {
|
|
1482
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1483
|
+
if (entry.isDirectory()) {
|
|
1484
|
+
if (SOURCE_SKIP_DIRECTORIES.has(entry.name))
|
|
1485
|
+
continue;
|
|
1486
|
+
const child = join(directory, entry.name);
|
|
1487
|
+
if (existsSync(join(child, ".git")))
|
|
1488
|
+
continue;
|
|
1489
|
+
walk(child);
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if (!entry.name.endsWith(".ts"))
|
|
1493
|
+
continue;
|
|
1494
|
+
if (entry.name.endsWith(".d.ts") || entry.name.endsWith(".test.ts"))
|
|
1495
|
+
continue;
|
|
1496
|
+
files.push(join(directory, entry.name));
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
walk(root);
|
|
1500
|
+
return files.sort();
|
|
1501
|
+
}
|
|
1502
|
+
function collectLocaleFiles(root) {
|
|
1503
|
+
const localeDirectory = join(root, "locales");
|
|
1504
|
+
if (!existsSync(localeDirectory))
|
|
1505
|
+
return [];
|
|
1506
|
+
return readdirSync(localeDirectory, { withFileTypes: true })
|
|
1507
|
+
.filter((entry) => entry.isFile() && extname(entry.name) === ".json")
|
|
1508
|
+
.map((entry) => `locales/${entry.name}`)
|
|
1509
|
+
.sort();
|
|
1510
|
+
}
|
|
1511
|
+
function buildRepositoryOperationIndex(sourceFiles, providerRoot) {
|
|
1512
|
+
const sources = new Map();
|
|
1513
|
+
for (const path of sourceFiles) {
|
|
1514
|
+
const source = parseSource(path, readFileSync(path, "utf8"));
|
|
1515
|
+
if (firstSyntaxError(source) === undefined)
|
|
1516
|
+
sources.set(path, source);
|
|
1517
|
+
}
|
|
1518
|
+
const operationIds = buildOperationIdIndex(sourceFiles);
|
|
1519
|
+
const operationSites = new Map();
|
|
1520
|
+
const excludedBindings = new Map();
|
|
1521
|
+
const runtimeComposedInitializers = new Map();
|
|
1522
|
+
const refusals = [];
|
|
1523
|
+
const declarations = [];
|
|
1524
|
+
const indexedProviderProperties = new Set();
|
|
1525
|
+
const factoryIds = new Map();
|
|
1526
|
+
const bindingCandidates = new Map();
|
|
1527
|
+
const relativePath = (path) => slash(relative(providerRoot, path));
|
|
1528
|
+
const resolutionRefusal = (path, operationKey, detail) => {
|
|
1529
|
+
refusals.push(refusal(relativePath(path), operationKey, "non_literal", detail));
|
|
1530
|
+
};
|
|
1531
|
+
const resolveExport = (path, exportedName, active) => {
|
|
1532
|
+
const key = `${path}\0export\0${exportedName}`;
|
|
1533
|
+
if (active.has(key)) {
|
|
1534
|
+
return {
|
|
1535
|
+
status: "refused",
|
|
1536
|
+
detail: `Static relative export cycle while resolving ${JSON.stringify(exportedName)} from ${relativePath(path)}.`,
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
const source = sources.get(path);
|
|
1540
|
+
if (source === undefined)
|
|
1541
|
+
return { status: "missing" };
|
|
1542
|
+
const nextActive = new Set(active);
|
|
1543
|
+
nextActive.add(key);
|
|
1544
|
+
const explicit = [];
|
|
1545
|
+
const exportStars = [];
|
|
1546
|
+
for (const statement of source.statements) {
|
|
1547
|
+
if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
|
|
1548
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1549
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
1550
|
+
declaration.name.text === exportedName &&
|
|
1551
|
+
declaration.initializer !== undefined) {
|
|
1552
|
+
const initializer = declaration.initializer;
|
|
1553
|
+
const bindingName = declaration.name.text;
|
|
1554
|
+
explicit.push(() => ({
|
|
1555
|
+
status: "resolved",
|
|
1556
|
+
value: {
|
|
1557
|
+
path,
|
|
1558
|
+
source,
|
|
1559
|
+
expression: initializer,
|
|
1560
|
+
bindingName,
|
|
1561
|
+
},
|
|
1562
|
+
}));
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (ts.isExportAssignment(statement) &&
|
|
1567
|
+
!statement.isExportEquals &&
|
|
1568
|
+
exportedName === "default") {
|
|
1569
|
+
explicit.push(() => ({
|
|
1570
|
+
status: "resolved",
|
|
1571
|
+
value: { path, source, expression: statement.expression },
|
|
1572
|
+
}));
|
|
1573
|
+
}
|
|
1574
|
+
if (!ts.isExportDeclaration(statement))
|
|
1575
|
+
continue;
|
|
1576
|
+
const specifier = statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier)
|
|
1577
|
+
? statement.moduleSpecifier.text
|
|
1578
|
+
: undefined;
|
|
1579
|
+
if (statement.exportClause === undefined) {
|
|
1580
|
+
if (specifier !== undefined)
|
|
1581
|
+
exportStars.push(specifier);
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (!ts.isNamedExports(statement.exportClause))
|
|
1585
|
+
continue;
|
|
1586
|
+
for (const element of statement.exportClause.elements) {
|
|
1587
|
+
if (element.name.text !== exportedName)
|
|
1588
|
+
continue;
|
|
1589
|
+
const localName = element.propertyName?.text ?? element.name.text;
|
|
1590
|
+
explicit.push(() => {
|
|
1591
|
+
if (specifier === undefined) {
|
|
1592
|
+
return resolveIdentifier(path, localName, nextActive);
|
|
1593
|
+
}
|
|
1594
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
1595
|
+
if (target.status !== "resolved")
|
|
1596
|
+
return target;
|
|
1597
|
+
return resolveExport(target.path, localName, nextActive);
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
if (explicit.length > 1) {
|
|
1602
|
+
return {
|
|
1603
|
+
status: "refused",
|
|
1604
|
+
detail: `Export ${JSON.stringify(exportedName)} is ambiguous in ${relativePath(path)}.`,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (explicit.length === 1)
|
|
1608
|
+
return explicit[0]?.() ?? { status: "missing" };
|
|
1609
|
+
const resolvedStars = [];
|
|
1610
|
+
let firstStarRefusal;
|
|
1611
|
+
for (const specifier of exportStars) {
|
|
1612
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
1613
|
+
if (target.status !== "resolved") {
|
|
1614
|
+
if (target.status === "refused")
|
|
1615
|
+
firstStarRefusal ??= target;
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
const candidate = resolveExport(target.path, exportedName, nextActive);
|
|
1619
|
+
if (candidate.status === "resolved")
|
|
1620
|
+
resolvedStars.push(candidate.value);
|
|
1621
|
+
else if (candidate.status === "refused")
|
|
1622
|
+
firstStarRefusal ??= candidate;
|
|
1623
|
+
}
|
|
1624
|
+
const origins = new Set(resolvedStars.map((item) => `${item.path}\0${item.expression.getStart(item.source)}`));
|
|
1625
|
+
if (origins.size > 1) {
|
|
1626
|
+
return {
|
|
1627
|
+
status: "refused",
|
|
1628
|
+
detail: `Export ${JSON.stringify(exportedName)} is ambiguous across relative re-exports from ${relativePath(path)}.`,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
if (firstStarRefusal !== undefined)
|
|
1632
|
+
return firstStarRefusal;
|
|
1633
|
+
const resolved = resolvedStars[0];
|
|
1634
|
+
return resolved === undefined ? { status: "missing" } : { status: "resolved", value: resolved };
|
|
1635
|
+
};
|
|
1636
|
+
const resolveIdentifier = (path, name, active) => {
|
|
1637
|
+
const source = sources.get(path);
|
|
1638
|
+
if (source === undefined)
|
|
1639
|
+
return { status: "missing" };
|
|
1640
|
+
for (const statement of source.statements) {
|
|
1641
|
+
if (!ts.isVariableStatement(statement))
|
|
1642
|
+
continue;
|
|
1643
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
1644
|
+
continue;
|
|
1645
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1646
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
1647
|
+
declaration.name.text === name &&
|
|
1648
|
+
declaration.initializer !== undefined) {
|
|
1649
|
+
return {
|
|
1650
|
+
status: "resolved",
|
|
1651
|
+
value: {
|
|
1652
|
+
path,
|
|
1653
|
+
source,
|
|
1654
|
+
expression: declaration.initializer,
|
|
1655
|
+
bindingName: name,
|
|
1656
|
+
},
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
for (const statement of source.statements) {
|
|
1662
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
let exportedName;
|
|
1666
|
+
if (statement.importClause?.name?.text === name)
|
|
1667
|
+
exportedName = "default";
|
|
1668
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1669
|
+
if (bindings !== undefined && ts.isNamedImports(bindings)) {
|
|
1670
|
+
for (const element of bindings.elements) {
|
|
1671
|
+
if (element.name.text === name) {
|
|
1672
|
+
exportedName = element.propertyName?.text ?? element.name.text;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (exportedName === undefined)
|
|
1677
|
+
continue;
|
|
1678
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1679
|
+
const target = resolveStaticModule(path, specifier, sources);
|
|
1680
|
+
if (target.status !== "resolved")
|
|
1681
|
+
return target;
|
|
1682
|
+
return resolveExport(target.path, exportedName, active);
|
|
1683
|
+
}
|
|
1684
|
+
return { status: "missing" };
|
|
1685
|
+
};
|
|
1686
|
+
const resolveLocatedExpression = (located, active) => {
|
|
1687
|
+
const expression = unwrapExpression(located.expression);
|
|
1688
|
+
if (expression === undefined)
|
|
1689
|
+
return { status: "missing" };
|
|
1690
|
+
if (!ts.isIdentifier(expression)) {
|
|
1691
|
+
return { status: "resolved", value: { ...located, expression } };
|
|
1692
|
+
}
|
|
1693
|
+
const key = `${located.path}\0identifier\0${expression.text}`;
|
|
1694
|
+
if (active.has(key)) {
|
|
1695
|
+
return {
|
|
1696
|
+
status: "refused",
|
|
1697
|
+
detail: `Static identifier cycle while resolving ${JSON.stringify(expression.text)} in ${relativePath(located.path)}.`,
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
const nextActive = new Set(active);
|
|
1701
|
+
nextActive.add(key);
|
|
1702
|
+
const resolved = resolveIdentifier(located.path, expression.text, nextActive);
|
|
1703
|
+
return resolved.status === "resolved"
|
|
1704
|
+
? resolveLocatedExpression(resolved.value, nextActive)
|
|
1705
|
+
: resolved;
|
|
1706
|
+
};
|
|
1707
|
+
const flattenRegistry = (locatedInput, active) => {
|
|
1708
|
+
const resolved = resolveLocatedExpression(locatedInput, active);
|
|
1709
|
+
if (resolved.status === "refused")
|
|
1710
|
+
return { detail: resolved.detail };
|
|
1711
|
+
if (resolved.status === "missing") {
|
|
1712
|
+
return {
|
|
1713
|
+
detail: `Operations initializer ${JSON.stringify(locatedInput.expression.getText(locatedInput.source))} is not a static local or relative-imported object.`,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
const expression = unwrapExpression(resolved.value.expression);
|
|
1717
|
+
if (expression === undefined || !ts.isObjectLiteralExpression(expression)) {
|
|
1718
|
+
return {
|
|
1719
|
+
detail: `Operations initializer ${JSON.stringify(resolved.value.expression.getText(resolved.value.source))} is not a static object literal.`,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
const cycleKey = `${resolved.value.path}\0object\0${expression.getStart(resolved.value.source)}`;
|
|
1723
|
+
if (active.has(cycleKey)) {
|
|
1724
|
+
return {
|
|
1725
|
+
detail: `Static operations registry spread cycle reaches ${relativePath(resolved.value.path)}.`,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
const nextActive = new Set(active);
|
|
1729
|
+
nextActive.add(cycleKey);
|
|
1730
|
+
const entries = new Map();
|
|
1731
|
+
for (const property of expression.properties) {
|
|
1732
|
+
if (ts.isSpreadAssignment(property)) {
|
|
1733
|
+
const spread = flattenRegistry({
|
|
1734
|
+
path: resolved.value.path,
|
|
1735
|
+
source: resolved.value.source,
|
|
1736
|
+
expression: property.expression,
|
|
1737
|
+
}, nextActive);
|
|
1738
|
+
if ("detail" in spread)
|
|
1739
|
+
return spread;
|
|
1740
|
+
for (const [operationId, entry] of spread.entries)
|
|
1741
|
+
entries.set(operationId, entry);
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const operationId = staticPropertyName(property.name);
|
|
1745
|
+
const value = propertyValue(property);
|
|
1746
|
+
if (operationId === undefined) {
|
|
1747
|
+
return {
|
|
1748
|
+
detail: `Operations registry in ${relativePath(resolved.value.path)} uses a computed key.`,
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
if (value === undefined) {
|
|
1752
|
+
return { detail: `Operation ${JSON.stringify(operationId)} has a non-static initializer.` };
|
|
1753
|
+
}
|
|
1754
|
+
entries.set(operationId, {
|
|
1755
|
+
operationId,
|
|
1756
|
+
value: {
|
|
1757
|
+
path: resolved.value.path,
|
|
1758
|
+
source: resolved.value.source,
|
|
1759
|
+
expression: value,
|
|
1760
|
+
},
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
return { entries };
|
|
1764
|
+
};
|
|
1765
|
+
const addBindingCandidate = (path, name, operationId) => {
|
|
1766
|
+
const key = `${path}\0${name}`;
|
|
1767
|
+
const candidate = bindingCandidates.get(key) ?? { path, name, ids: new Set() };
|
|
1768
|
+
candidate.ids.add(operationId);
|
|
1769
|
+
bindingCandidates.set(key, candidate);
|
|
1770
|
+
};
|
|
1771
|
+
const addObjectSite = (path, source, object, operationId) => {
|
|
1772
|
+
const sites = operationSites.get(path) ?? new Map();
|
|
1773
|
+
const start = object.getStart(source);
|
|
1774
|
+
const previous = sites.get(start);
|
|
1775
|
+
if (previous !== undefined && previous !== operationId) {
|
|
1776
|
+
resolutionRefusal(path, operationId, `One static operation object is registered under both ${JSON.stringify(previous)} and ${JSON.stringify(operationId)}.`);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
sites.set(start, operationId);
|
|
1780
|
+
operationSites.set(path, sites);
|
|
1781
|
+
};
|
|
1782
|
+
const classifyEntry = (entry) => {
|
|
1783
|
+
const resolved = resolveLocatedExpression(entry.value, new Set());
|
|
1784
|
+
if (resolved.status === "refused") {
|
|
1785
|
+
resolutionRefusal(entry.value.path, entry.operationId, resolved.detail);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (resolved.status === "missing") {
|
|
1789
|
+
resolutionRefusal(entry.value.path, entry.operationId, `Operation initializer ${JSON.stringify(entry.value.expression.getText(entry.value.source))} is not statically resolvable.`);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const expression = unwrapExpression(resolved.value.expression);
|
|
1793
|
+
if (expression === undefined)
|
|
1794
|
+
return;
|
|
1795
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1796
|
+
addObjectSite(resolved.value.path, resolved.value.source, expression, entry.operationId);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (ts.isCallExpression(expression) && isOperationHelperCall(expression)) {
|
|
1800
|
+
const argument = operationArgument(expression);
|
|
1801
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
1802
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object)) {
|
|
1803
|
+
resolutionRefusal(resolved.value.path, entry.operationId, "Operation helper argument must be a static object literal.");
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (resolved.value.bindingName !== undefined) {
|
|
1807
|
+
addBindingCandidate(resolved.value.path, resolved.value.bindingName, entry.operationId);
|
|
1808
|
+
}
|
|
1809
|
+
addObjectSite(resolved.value.path, resolved.value.source, object, entry.operationId);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (ts.isCallExpression(expression) &&
|
|
1813
|
+
ts.isIdentifier(expression.expression) &&
|
|
1814
|
+
collectSimpleOperationFactories(resolved.value.source).has(expression.expression.text)) {
|
|
1815
|
+
const name = expression.expression.text;
|
|
1816
|
+
const key = `${resolved.value.path}\0${name}`;
|
|
1817
|
+
const factory = factoryIds.get(key) ?? {
|
|
1818
|
+
path: resolved.value.path,
|
|
1819
|
+
name,
|
|
1820
|
+
ids: new Set(),
|
|
1821
|
+
};
|
|
1822
|
+
factory.ids.add(entry.operationId);
|
|
1823
|
+
factoryIds.set(key, factory);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
resolutionRefusal(resolved.value.path, entry.operationId, `Operation initializer ${JSON.stringify(expression.getText(resolved.value.source))} is not a raw object, operation helper, or simple same-file factory call.`);
|
|
1827
|
+
};
|
|
1828
|
+
const inspectProviderProperty = (path, source, node, construct) => {
|
|
1829
|
+
const key = `${path}\0${node.getStart(source)}`;
|
|
1830
|
+
if (indexedProviderProperties.has(key))
|
|
1831
|
+
return;
|
|
1832
|
+
indexedProviderProperties.add(key);
|
|
1833
|
+
const resolvedInitializer = resolveLocatedExpression({ path, source, expression: node.initializer }, new Set());
|
|
1834
|
+
let runtimeInitializer;
|
|
1835
|
+
if (resolvedInitializer.status === "resolved") {
|
|
1836
|
+
const resolvedExpression = unwrapExpression(resolvedInitializer.value.expression);
|
|
1837
|
+
if (resolvedExpression !== undefined && ts.isCallExpression(resolvedExpression)) {
|
|
1838
|
+
runtimeInitializer = resolvedExpression.getText(resolvedInitializer.value.source);
|
|
1839
|
+
const initializers = runtimeComposedInitializers.get(path) ?? new Set();
|
|
1840
|
+
initializers.add(node.initializer.getStart(source));
|
|
1841
|
+
runtimeComposedInitializers.set(path, initializers);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
declarations.push({ path, construct, initializer: node.initializer, runtimeInitializer });
|
|
1845
|
+
const flattened = flattenRegistry({ path, source, expression: node.initializer }, new Set());
|
|
1846
|
+
if ("detail" in flattened) {
|
|
1847
|
+
if (runtimeInitializer === undefined) {
|
|
1848
|
+
resolutionRefusal(path, "<operations>", flattened.detail);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
for (const entry of flattened.entries.values())
|
|
1853
|
+
classifyEntry(entry);
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
for (const [path, source] of sources) {
|
|
1857
|
+
const constObjects = collectModuleConstObjects(source);
|
|
1858
|
+
const visit = (node) => {
|
|
1859
|
+
if (ts.isPropertyAssignment(node) &&
|
|
1860
|
+
staticPropertyName(node.name) === "operations" &&
|
|
1861
|
+
isProviderOperationsProperty(node, constObjects)) {
|
|
1862
|
+
inspectProviderProperty(path, source, node, providerConstructName(node));
|
|
1863
|
+
}
|
|
1864
|
+
if (ts.isCallExpression(node)) {
|
|
1865
|
+
const construct = providerCallConstruct(node);
|
|
1866
|
+
if (construct !== undefined) {
|
|
1867
|
+
for (const argument of node.arguments) {
|
|
1868
|
+
const resolved = resolveLocatedExpression({ path, source, expression: argument }, new Set());
|
|
1869
|
+
if (resolved.status !== "resolved")
|
|
1870
|
+
continue;
|
|
1871
|
+
const object = unwrapExpression(resolved.value.expression);
|
|
1872
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object))
|
|
1873
|
+
continue;
|
|
1874
|
+
for (const property of object.properties) {
|
|
1875
|
+
if (ts.isPropertyAssignment(property) &&
|
|
1876
|
+
staticPropertyName(property.name) === "operations") {
|
|
1877
|
+
inspectProviderProperty(resolved.value.path, resolved.value.source, property, construct);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
ts.forEachChild(node, visit);
|
|
1884
|
+
};
|
|
1885
|
+
visit(source);
|
|
1886
|
+
}
|
|
1887
|
+
for (const candidate of bindingCandidates.values()) {
|
|
1888
|
+
const ids = [...candidate.ids];
|
|
1889
|
+
const indexed = operationIds.get(candidate.path) ?? new Map();
|
|
1890
|
+
if (ids.length === 1) {
|
|
1891
|
+
const operationId = ids[0];
|
|
1892
|
+
if (operationId !== undefined)
|
|
1893
|
+
indexed.set(candidate.name, operationId);
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
indexed.delete(candidate.name);
|
|
1897
|
+
const excluded = excludedBindings.get(candidate.path) ?? new Set();
|
|
1898
|
+
excluded.add(candidate.name);
|
|
1899
|
+
excludedBindings.set(candidate.path, excluded);
|
|
1900
|
+
}
|
|
1901
|
+
operationIds.set(candidate.path, indexed);
|
|
1902
|
+
}
|
|
1903
|
+
for (const factory of factoryIds.values()) {
|
|
1904
|
+
const indexed = operationIds.get(factory.path) ?? new Map();
|
|
1905
|
+
const ids = [...factory.ids];
|
|
1906
|
+
if (ids.length === 1) {
|
|
1907
|
+
const operationId = ids[0];
|
|
1908
|
+
if (operationId !== undefined)
|
|
1909
|
+
indexed.set(factory.name, operationId);
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
indexed.delete(factory.name);
|
|
1913
|
+
const excluded = excludedBindings.get(factory.path) ?? new Set();
|
|
1914
|
+
excluded.add(factory.name);
|
|
1915
|
+
excludedBindings.set(factory.path, excluded);
|
|
1916
|
+
refusals.push(refusal(relativePath(factory.path), factory.name, "factory_operation_id_ambiguous", `Factory ${factory.name} is registered under multiple operation ids: ${ids
|
|
1917
|
+
.map((id) => JSON.stringify(id))
|
|
1918
|
+
.join(", ")}. A shared operation body cannot own one examples locale namespace.`));
|
|
1919
|
+
}
|
|
1920
|
+
operationIds.set(factory.path, indexed);
|
|
1921
|
+
}
|
|
1922
|
+
const resolveStaticObject = (expressionInput, path, active = new Set()) => {
|
|
1923
|
+
const expression = unwrapExpression(expressionInput);
|
|
1924
|
+
if (expression === undefined)
|
|
1925
|
+
return undefined;
|
|
1926
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
1927
|
+
const source = sources.get(path) ?? expression.getSourceFile();
|
|
1928
|
+
return { object: expression, source };
|
|
1929
|
+
}
|
|
1930
|
+
if (ts.isIdentifier(expression)) {
|
|
1931
|
+
const key = `${path}\0member\0${expression.text}`;
|
|
1932
|
+
if (active.has(key))
|
|
1933
|
+
return undefined;
|
|
1934
|
+
const nextActive = new Set(active);
|
|
1935
|
+
nextActive.add(key);
|
|
1936
|
+
const resolved = resolveIdentifier(path, expression.text, nextActive);
|
|
1937
|
+
return resolved.status === "resolved"
|
|
1938
|
+
? resolveStaticObject(resolved.value.expression, resolved.value.path, nextActive)
|
|
1939
|
+
: undefined;
|
|
1940
|
+
}
|
|
1941
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1942
|
+
const owner = resolveStaticObject(expression.expression, path, active);
|
|
1943
|
+
if (owner === undefined)
|
|
1944
|
+
return undefined;
|
|
1945
|
+
for (const property of owner.object.properties) {
|
|
1946
|
+
if (ts.isSpreadAssignment(property))
|
|
1947
|
+
continue;
|
|
1948
|
+
if (staticPropertyName(property.name) !== expression.name.text)
|
|
1949
|
+
continue;
|
|
1950
|
+
const value = propertyValue(property);
|
|
1951
|
+
if (value === undefined)
|
|
1952
|
+
return undefined;
|
|
1953
|
+
return resolveStaticObject(value, owner.source.fileName, active);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return undefined;
|
|
1957
|
+
};
|
|
1958
|
+
return {
|
|
1959
|
+
operationIds,
|
|
1960
|
+
operationSites,
|
|
1961
|
+
excludedBindings,
|
|
1962
|
+
runtimeComposedInitializers,
|
|
1963
|
+
refusals,
|
|
1964
|
+
declarations,
|
|
1965
|
+
staticObjectResolverFor: (currentPath) => (expression, source) => resolveStaticObject(expression, sources.has(source.fileName) ? source.fileName : currentPath),
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
function hasExportModifier(node) {
|
|
1969
|
+
return (ts.canHaveModifiers(node) &&
|
|
1970
|
+
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ===
|
|
1971
|
+
true);
|
|
1972
|
+
}
|
|
1973
|
+
function resolveStaticModule(containingPath, specifier, sources) {
|
|
1974
|
+
if (!specifier.startsWith(".")) {
|
|
1975
|
+
return {
|
|
1976
|
+
status: "refused",
|
|
1977
|
+
detail: `Static operation discovery refuses non-relative import ${JSON.stringify(specifier)}.`,
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
const path = resolveLocalModule(containingPath, specifier, sources);
|
|
1981
|
+
if (path === undefined) {
|
|
1982
|
+
return {
|
|
1983
|
+
status: "refused",
|
|
1984
|
+
detail: `Static relative import ${JSON.stringify(specifier)} from ${containingPath} could not be resolved.`,
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
return { status: "resolved", path };
|
|
1988
|
+
}
|
|
1989
|
+
function providerConstructName(node) {
|
|
1990
|
+
let current = node;
|
|
1991
|
+
while (current.parent !== undefined) {
|
|
1992
|
+
const parent = current.parent;
|
|
1993
|
+
if (ts.isCallExpression(parent)) {
|
|
1994
|
+
return providerCallConstruct(parent) ?? parent.expression.getText();
|
|
1995
|
+
}
|
|
1996
|
+
current = parent;
|
|
1997
|
+
}
|
|
1998
|
+
return "provider declaration";
|
|
1999
|
+
}
|
|
2000
|
+
function providerCallConstruct(call) {
|
|
2001
|
+
if (ts.isIdentifier(call.expression) &&
|
|
2002
|
+
/(?:defineProvider|Provider)\b/.test(call.expression.text)) {
|
|
2003
|
+
return call.expression.text;
|
|
2004
|
+
}
|
|
2005
|
+
if (ts.isCallExpression(call.expression) &&
|
|
2006
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
2007
|
+
/\bdefineProvider\b/.test(call.expression.expression.text)) {
|
|
2008
|
+
return call.expression.expression.text;
|
|
2009
|
+
}
|
|
2010
|
+
return undefined;
|
|
2011
|
+
}
|
|
2012
|
+
function buildOperationIdIndex(sourceFiles) {
|
|
2013
|
+
const result = new Map();
|
|
2014
|
+
const idsByPath = new Map();
|
|
2015
|
+
const ambiguousBindingsByPath = new Map();
|
|
2016
|
+
const sources = new Map();
|
|
2017
|
+
for (const path of sourceFiles) {
|
|
2018
|
+
const source = parseSource(path, readFileSync(path, "utf8"));
|
|
2019
|
+
if (firstSyntaxError(source) === undefined)
|
|
2020
|
+
sources.set(path, source);
|
|
2021
|
+
}
|
|
2022
|
+
const record = (path, binding, operationId) => {
|
|
2023
|
+
const map = result.get(path) ?? new Map();
|
|
2024
|
+
const ids = idsByPath.get(path) ?? new Map();
|
|
2025
|
+
const ambiguousBindings = ambiguousBindingsByPath.get(path) ?? new Set();
|
|
2026
|
+
// An operation id is usable only when it identifies exactly one binding.
|
|
2027
|
+
// Keep an explicit null marker for an ambiguous id so a later occurrence
|
|
2028
|
+
// cannot accidentally make it usable again.
|
|
2029
|
+
const previousBinding = ids.get(operationId);
|
|
2030
|
+
if (ids.has(operationId)) {
|
|
2031
|
+
if (previousBinding !== undefined &&
|
|
2032
|
+
previousBinding !== null &&
|
|
2033
|
+
previousBinding !== binding) {
|
|
2034
|
+
map.delete(previousBinding);
|
|
2035
|
+
map.delete(binding);
|
|
2036
|
+
ambiguousBindings.add(previousBinding);
|
|
2037
|
+
ambiguousBindings.add(binding);
|
|
2038
|
+
ids.set(operationId, null);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
ids.set(operationId, binding);
|
|
2043
|
+
}
|
|
2044
|
+
// A binding registered under two different ids is likewise ambiguous.
|
|
2045
|
+
const previousId = map.get(binding);
|
|
2046
|
+
if (previousId !== undefined && previousId !== operationId) {
|
|
2047
|
+
map.delete(binding);
|
|
2048
|
+
ambiguousBindings.add(binding);
|
|
2049
|
+
}
|
|
2050
|
+
else if (!ambiguousBindings.has(binding) && ids.get(operationId) === binding) {
|
|
2051
|
+
map.set(binding, operationId);
|
|
2052
|
+
}
|
|
2053
|
+
result.set(path, map);
|
|
2054
|
+
idsByPath.set(path, ids);
|
|
2055
|
+
ambiguousBindingsByPath.set(path, ambiguousBindings);
|
|
2056
|
+
};
|
|
2057
|
+
for (const [path, source] of sources) {
|
|
2058
|
+
const imports = collectImports(source, path, sources);
|
|
2059
|
+
const constObjects = collectModuleConstObjects(source);
|
|
2060
|
+
const ignored = [];
|
|
2061
|
+
for (const map of collectOperationsMaps(source, constObjects, path, ignored)) {
|
|
2062
|
+
for (const property of map.properties) {
|
|
2063
|
+
if (ts.isSpreadAssignment(property))
|
|
2064
|
+
continue;
|
|
2065
|
+
const operationId = staticPropertyName(property.name);
|
|
2066
|
+
const value = propertyValue(property);
|
|
2067
|
+
const expression = value === undefined ? undefined : unwrapExpression(value);
|
|
2068
|
+
if (operationId === undefined || expression === undefined || !ts.isIdentifier(expression)) {
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
const imported = imports.get(expression.text);
|
|
2072
|
+
if (imported === undefined)
|
|
2073
|
+
record(path, expression.text, operationId);
|
|
2074
|
+
else
|
|
2075
|
+
record(imported.path, imported.exportedName, operationId);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
// Some providers keep their operation registry in a same-file const with
|
|
2079
|
+
// an arbitrary name (for example, `companionsOperations`). This scan is
|
|
2080
|
+
// deliberately ID-only: it accepts only static object members whose value
|
|
2081
|
+
// is a same-file operation binding, and never evaluates spreads, factories,
|
|
2082
|
+
// or imported values.
|
|
2083
|
+
const operationBindings = collectModuleOperationBindings(source);
|
|
2084
|
+
const importedBindings = collectImportedBindingNames(source);
|
|
2085
|
+
for (const entry of collectStaticOperationRegistryEntries(source, operationBindings)) {
|
|
2086
|
+
if (importedBindings.has(entry.binding))
|
|
2087
|
+
continue;
|
|
2088
|
+
record(path, entry.binding, entry.operationId);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return result;
|
|
2092
|
+
}
|
|
2093
|
+
function collectModuleOperationBindings(source) {
|
|
2094
|
+
const bindings = new Set();
|
|
2095
|
+
for (const statement of source.statements) {
|
|
2096
|
+
if (!ts.isVariableStatement(statement))
|
|
2097
|
+
continue;
|
|
2098
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
2099
|
+
continue;
|
|
2100
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2101
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined)
|
|
2102
|
+
continue;
|
|
2103
|
+
const initializer = unwrapExpression(declaration.initializer);
|
|
2104
|
+
if (initializer !== undefined &&
|
|
2105
|
+
ts.isCallExpression(initializer) &&
|
|
2106
|
+
isOperationHelperCall(initializer)) {
|
|
2107
|
+
bindings.add(declaration.name.text);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
return bindings;
|
|
2112
|
+
}
|
|
2113
|
+
function collectImportedBindingNames(source) {
|
|
2114
|
+
const bindings = new Set();
|
|
2115
|
+
for (const statement of source.statements) {
|
|
2116
|
+
if (!ts.isImportDeclaration(statement))
|
|
2117
|
+
continue;
|
|
2118
|
+
const clause = statement.importClause;
|
|
2119
|
+
if (clause?.name !== undefined)
|
|
2120
|
+
bindings.add(clause.name.text);
|
|
2121
|
+
const named = clause?.namedBindings;
|
|
2122
|
+
if (named === undefined)
|
|
2123
|
+
continue;
|
|
2124
|
+
if (ts.isNamespaceImport(named)) {
|
|
2125
|
+
bindings.add(named.name.text);
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
for (const element of named.elements)
|
|
2129
|
+
bindings.add(element.name.text);
|
|
2130
|
+
}
|
|
2131
|
+
return bindings;
|
|
2132
|
+
}
|
|
2133
|
+
function collectStaticOperationRegistryEntries(source, operationBindings) {
|
|
2134
|
+
const entries = [];
|
|
2135
|
+
for (const statement of source.statements) {
|
|
2136
|
+
if (!ts.isVariableStatement(statement))
|
|
2137
|
+
continue;
|
|
2138
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
2139
|
+
continue;
|
|
2140
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2141
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined)
|
|
2142
|
+
continue;
|
|
2143
|
+
const object = unwrapExpression(declaration.initializer);
|
|
2144
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object))
|
|
2145
|
+
continue;
|
|
2146
|
+
for (const property of object.properties) {
|
|
2147
|
+
if (ts.isSpreadAssignment(property))
|
|
2148
|
+
continue;
|
|
2149
|
+
const name = property.name;
|
|
2150
|
+
if (name === undefined || (!ts.isIdentifier(name) && !ts.isStringLiteral(name)))
|
|
2151
|
+
continue;
|
|
2152
|
+
const value = propertyValue(property);
|
|
2153
|
+
const binding = unwrapExpression(value);
|
|
2154
|
+
if (binding === undefined || !ts.isIdentifier(binding))
|
|
2155
|
+
continue;
|
|
2156
|
+
if (!operationBindings.has(binding.text))
|
|
2157
|
+
continue;
|
|
2158
|
+
entries.push({ operationId: name.text, binding: binding.text });
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return entries;
|
|
2163
|
+
}
|
|
2164
|
+
function collectImports(source, containingPath, sources) {
|
|
2165
|
+
const imports = new Map();
|
|
2166
|
+
for (const statement of source.statements) {
|
|
2167
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
const target = resolveLocalModule(containingPath, statement.moduleSpecifier.text, sources);
|
|
2171
|
+
if (target === undefined)
|
|
2172
|
+
continue;
|
|
2173
|
+
const clause = statement.importClause;
|
|
2174
|
+
if (clause?.name !== undefined) {
|
|
2175
|
+
imports.set(clause.name.text, { path: target, exportedName: "default" });
|
|
2176
|
+
}
|
|
2177
|
+
const bindings = clause?.namedBindings;
|
|
2178
|
+
if (bindings !== undefined && ts.isNamedImports(bindings)) {
|
|
2179
|
+
for (const element of bindings.elements) {
|
|
2180
|
+
imports.set(element.name.text, {
|
|
2181
|
+
path: target,
|
|
2182
|
+
exportedName: element.propertyName?.text ?? element.name.text,
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return imports;
|
|
2188
|
+
}
|
|
2189
|
+
function resolveLocalModule(containingPath, specifier, sources) {
|
|
2190
|
+
if (!specifier.startsWith("."))
|
|
2191
|
+
return undefined;
|
|
2192
|
+
const base = resolve(dirname(containingPath), specifier);
|
|
2193
|
+
const emittedExtensionSource = /\.(?:c|m)?js$/.test(base)
|
|
2194
|
+
? base.replace(/\.(?:c|m)?js$/, ".ts")
|
|
2195
|
+
: undefined;
|
|
2196
|
+
for (const candidate of [base, emittedExtensionSource, `${base}.ts`, join(base, "index.ts")]) {
|
|
2197
|
+
if (candidate === undefined)
|
|
2198
|
+
continue;
|
|
2199
|
+
if (sources.has(candidate))
|
|
2200
|
+
return candidate;
|
|
2201
|
+
}
|
|
2202
|
+
return undefined;
|
|
2203
|
+
}
|
|
2204
|
+
function slash(path) {
|
|
2205
|
+
return path.split("\\").join("/");
|
|
2206
|
+
}
|