@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.50
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 +537 -33
- package/CHANGELOG.md +201 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- 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 +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1178 -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 +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- 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 +179 -16
- package/dist/config/loader.js +435 -127
- 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 +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- 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 +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- 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 +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- 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 +5 -0
- package/dist/runtime/trace.js +43 -10
- 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/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- 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.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -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 +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -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 +9 -4
- package/dist/stealth/profiles.js +160 -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 +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1654 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- 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 +690 -163
- 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 +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- 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 +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -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 +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -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 +202 -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 +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
3
|
+
const ts = await loadTypeScript();
|
|
4
|
+
async function loadTypeScript() {
|
|
5
|
+
try {
|
|
6
|
+
return await import("typescript");
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
console.error("apifuse migrate-operation-declaration requires typescript; install it in the workspace running the CLI (bun add -d typescript)");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const OPERATION_HELPERS = new Set(["defineOperation", "defineStreamOperation"]);
|
|
14
|
+
const SAFETY_VALUES = new Set(["read", "write", "destructive", "external-send"]);
|
|
15
|
+
const DOCS_FIELDS = new Set([
|
|
16
|
+
"titleKey",
|
|
17
|
+
"descriptionKey",
|
|
18
|
+
"summaryKey",
|
|
19
|
+
"markdownKey",
|
|
20
|
+
"normalizationNotesKeys",
|
|
21
|
+
"errorCodes",
|
|
22
|
+
]);
|
|
23
|
+
const ANNOTATION_FIELDS = new Set([
|
|
24
|
+
"destructive",
|
|
25
|
+
"readOnly",
|
|
26
|
+
"openWorld",
|
|
27
|
+
"idempotent",
|
|
28
|
+
"rateLimit",
|
|
29
|
+
"timeoutMs",
|
|
30
|
+
]);
|
|
31
|
+
const TOOL_ROUTER_FIELDS = new Set([
|
|
32
|
+
"name",
|
|
33
|
+
"riskClass",
|
|
34
|
+
"approval",
|
|
35
|
+
"connectionMode",
|
|
36
|
+
"connectionExternalRefParam",
|
|
37
|
+
"requiresConnection",
|
|
38
|
+
]);
|
|
39
|
+
const TOP_LEVEL_DELETIONS = new Set(["derivations", "retryOnAuthRefresh", "title", "description"]);
|
|
40
|
+
const SOURCE_SKIP_DIRECTORIES = new Set([
|
|
41
|
+
"node_modules",
|
|
42
|
+
"__tests__",
|
|
43
|
+
"__fixtures__",
|
|
44
|
+
".git",
|
|
45
|
+
"dist",
|
|
46
|
+
]);
|
|
47
|
+
const SIDECAR_NAME = "migrate-operation-declaration.locales-todo.json";
|
|
48
|
+
/**
|
|
49
|
+
* Rewrite every operation declaration in one TypeScript module.
|
|
50
|
+
*
|
|
51
|
+
* The function is file-atomic: a single refusal discards every planned edit.
|
|
52
|
+
* It uses AST ranges as a string/comment/template-aware brace balancer and
|
|
53
|
+
* copies every untouched source slice byte-for-byte.
|
|
54
|
+
*/
|
|
55
|
+
export function migrateOperationDeclaration(sourceText, fileName, options = {}) {
|
|
56
|
+
const source = parseSource(fileName, sourceText);
|
|
57
|
+
const parseError = firstSyntaxError(source);
|
|
58
|
+
if (parseError !== undefined) {
|
|
59
|
+
return {
|
|
60
|
+
status: "refused",
|
|
61
|
+
refusals: [refusal(fileName, "<unknown>", "source_syntax", parseError)],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const constObjects = collectModuleConstObjects(source);
|
|
65
|
+
const discovery = discoverOperationSites(source, fileName, constObjects, options.operationIds);
|
|
66
|
+
if (discovery.refusals.length > 0) {
|
|
67
|
+
return { status: "refused", refusals: discovery.refusals };
|
|
68
|
+
}
|
|
69
|
+
const edits = [];
|
|
70
|
+
const todos = [];
|
|
71
|
+
const refusals = [];
|
|
72
|
+
for (const site of discovery.sites) {
|
|
73
|
+
const plan = planOperationMigration(source, fileName, site, constObjects, options.localeFiles ?? []);
|
|
74
|
+
if ("refusal" in plan) {
|
|
75
|
+
refusals.push(plan.refusal);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
edits.push(...plan.edits);
|
|
79
|
+
todos.push(...plan.localeTodos);
|
|
80
|
+
}
|
|
81
|
+
if (refusals.length > 0)
|
|
82
|
+
return { status: "refused", refusals };
|
|
83
|
+
if (edits.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
status: "unchanged",
|
|
86
|
+
code: sourceText,
|
|
87
|
+
operations: discovery.sites.length,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const code = applyEdits(sourceText, edits);
|
|
91
|
+
const outputRefusal = verifyOperationDeclarationRewrite(code, fileName);
|
|
92
|
+
if (outputRefusal !== undefined) {
|
|
93
|
+
return {
|
|
94
|
+
status: "refused",
|
|
95
|
+
refusals: [outputRefusal],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
status: "migrated",
|
|
100
|
+
code,
|
|
101
|
+
operations: discovery.sites.length,
|
|
102
|
+
localeTodos: todos,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Parse a proposed rewrite and classify a codemod-introduced syntax failure. */
|
|
106
|
+
export function verifyOperationDeclarationRewrite(code, fileName) {
|
|
107
|
+
const outputError = firstSyntaxError(parseSource(fileName, code));
|
|
108
|
+
if (outputError === undefined)
|
|
109
|
+
return undefined;
|
|
110
|
+
return refusal(fileName, "<file>", "codemod_syntax", `Transform output did not parse: ${outputError}`);
|
|
111
|
+
}
|
|
112
|
+
function planOperationMigration(source, fileName, site, constObjects, localeFiles) {
|
|
113
|
+
if (site.object.properties.some(ts.isSpreadAssignment)) {
|
|
114
|
+
return {
|
|
115
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", "Top-level operation spreads cannot be rewritten without changing their shared declaration."),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const expanded = expandMembers(site.object, constObjects, fileName, site.operationKey);
|
|
119
|
+
if ("refusal" in expanded)
|
|
120
|
+
return expanded;
|
|
121
|
+
const top = indexMembers(expanded.members, fileName, site.operationKey);
|
|
122
|
+
if ("refusal" in top)
|
|
123
|
+
return top;
|
|
124
|
+
if (top.spreads.length > 0) {
|
|
125
|
+
return {
|
|
126
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", "Top-level operation spreads cannot be rewritten without changing their shared declaration."),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const containers = new Map();
|
|
130
|
+
for (const containerName of ["annotations", "toolRouter", "docs"]) {
|
|
131
|
+
const member = top.byName.get(containerName);
|
|
132
|
+
if (member === undefined)
|
|
133
|
+
continue;
|
|
134
|
+
const object = resolveObjectInitializer(member, constObjects);
|
|
135
|
+
if (object === undefined) {
|
|
136
|
+
return {
|
|
137
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", `${containerName} must be an object literal or a module-level const object literal.`),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const members = expandMembers(object, constObjects, fileName, site.operationKey);
|
|
141
|
+
if ("refusal" in members)
|
|
142
|
+
return members;
|
|
143
|
+
const indexed = indexMembers(members.members, fileName, site.operationKey);
|
|
144
|
+
if ("refusal" in indexed)
|
|
145
|
+
return indexed;
|
|
146
|
+
if (indexed.spreads.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
refusal: refusal(fileName, site.operationKey, "non_literal", `${containerName} contains an unresolved spread.`),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
containers.set(containerName, members.members);
|
|
152
|
+
}
|
|
153
|
+
const annotations = memberMap(containers.get("annotations") ?? []);
|
|
154
|
+
const toolRouter = memberMap(containers.get("toolRouter") ?? []);
|
|
155
|
+
const docs = memberMap(containers.get("docs") ?? []);
|
|
156
|
+
const unknownAnnotation = firstUnknown(annotations, ANNOTATION_FIELDS);
|
|
157
|
+
if (unknownAnnotation !== undefined) {
|
|
158
|
+
return unsupportedContainerMember(fileName, site.operationKey, "annotations", unknownAnnotation);
|
|
159
|
+
}
|
|
160
|
+
const unknownTool = firstUnknown(toolRouter, TOOL_ROUTER_FIELDS);
|
|
161
|
+
if (unknownTool !== undefined) {
|
|
162
|
+
return unsupportedContainerMember(fileName, site.operationKey, "toolRouter", unknownTool);
|
|
163
|
+
}
|
|
164
|
+
const unknownDocs = firstUnknown(docs, new Set([...DOCS_FIELDS, "requestExample", "responseExample"]));
|
|
165
|
+
if (unknownDocs !== undefined) {
|
|
166
|
+
return unsupportedContainerMember(fileName, site.operationKey, "docs", unknownDocs);
|
|
167
|
+
}
|
|
168
|
+
const risk = resolveRiskClass(top.byName, annotations, toolRouter, fileName, site.operationKey);
|
|
169
|
+
if ("refusal" in risk)
|
|
170
|
+
return risk;
|
|
171
|
+
const insertions = new Map();
|
|
172
|
+
const removals = [];
|
|
173
|
+
const nestedContainers = ["annotations", "toolRouter", "docs"];
|
|
174
|
+
for (const name of nestedContainers) {
|
|
175
|
+
const member = top.byName.get(name);
|
|
176
|
+
if (member !== undefined)
|
|
177
|
+
removals.push(member.property);
|
|
178
|
+
}
|
|
179
|
+
for (const name of TOP_LEVEL_DELETIONS) {
|
|
180
|
+
const member = top.byName.get(name);
|
|
181
|
+
if (member !== undefined)
|
|
182
|
+
removals.push(member.property);
|
|
183
|
+
}
|
|
184
|
+
if (top.byName.get("riskClass") === undefined) {
|
|
185
|
+
const sourceContainer = risk.source === "annotations" ? "annotations" : "toolRouter";
|
|
186
|
+
addInsertion(insertions, sourceContainer, `riskClass: ${risk.text}`);
|
|
187
|
+
}
|
|
188
|
+
const timeout = mergeFlatAndNested("timeoutMs", top.byName.get("timeoutMs"), annotations.get("timeoutMs"), fileName, site.operationKey, source, "execution_conflict");
|
|
189
|
+
if ("refusal" in timeout)
|
|
190
|
+
return timeout;
|
|
191
|
+
if (timeout.insert !== undefined)
|
|
192
|
+
addInsertion(insertions, "annotations", timeout.insert);
|
|
193
|
+
const connection = resolveConnectionMode(top.byName, toolRouter, fileName, site.operationKey, source);
|
|
194
|
+
if ("refusal" in connection)
|
|
195
|
+
return connection;
|
|
196
|
+
if (connection.insert !== undefined) {
|
|
197
|
+
addInsertion(insertions, "toolRouter", connection.insert);
|
|
198
|
+
}
|
|
199
|
+
const externalRef = mergeFlatAndNested("connectionExternalRefParam", top.byName.get("connectionExternalRefParam"), toolRouter.get("connectionExternalRefParam"), fileName, site.operationKey, source, "connection_mode_conflict");
|
|
200
|
+
if ("refusal" in externalRef)
|
|
201
|
+
return externalRef;
|
|
202
|
+
if (externalRef.insert !== undefined) {
|
|
203
|
+
addInsertion(insertions, "toolRouter", externalRef.insert);
|
|
204
|
+
}
|
|
205
|
+
const approval = resolveApproval(top.byName.get("approval"), toolRouter.get("approval"), risk.value, fileName, site.operationKey, source);
|
|
206
|
+
if ("refusal" in approval)
|
|
207
|
+
return approval;
|
|
208
|
+
if (approval.removeTop !== undefined)
|
|
209
|
+
removals.push(approval.removeTop);
|
|
210
|
+
if (approval.insert !== undefined)
|
|
211
|
+
addInsertion(insertions, "toolRouter", approval.insert);
|
|
212
|
+
for (const field of DOCS_FIELDS) {
|
|
213
|
+
const merged = mergeFlatAndNested(field, top.byName.get(field), docs.get(field), fileName, site.operationKey, source, "locale_key_conflict");
|
|
214
|
+
if ("refusal" in merged)
|
|
215
|
+
return merged;
|
|
216
|
+
if (merged.insert !== undefined)
|
|
217
|
+
addInsertion(insertions, "docs", merged.insert);
|
|
218
|
+
}
|
|
219
|
+
const examples = planExamples(top.byName.get("inputExamples"), top.byName.get("examples"), fileName, site, source, localeFiles);
|
|
220
|
+
if ("refusal" in examples)
|
|
221
|
+
return examples;
|
|
222
|
+
const edits = [...examples.edits];
|
|
223
|
+
for (const containerName of nestedContainers) {
|
|
224
|
+
const member = top.byName.get(containerName);
|
|
225
|
+
if (member === undefined)
|
|
226
|
+
continue;
|
|
227
|
+
const generated = insertions.get(containerName) ?? [];
|
|
228
|
+
if (generated.length === 0)
|
|
229
|
+
continue;
|
|
230
|
+
const indentation = indentationAt(source.getFullText(), member.property.getStart(source));
|
|
231
|
+
edits.push({
|
|
232
|
+
start: member.property.getStart(source),
|
|
233
|
+
end: member.property.getEnd(),
|
|
234
|
+
text: generated.map((item) => item.text).join(`,\n${indentation}`),
|
|
235
|
+
});
|
|
236
|
+
removeFirst(removals, member.property);
|
|
237
|
+
}
|
|
238
|
+
const removalEdits = removalRanges(site.object, removals, source);
|
|
239
|
+
for (const edit of removalEdits) {
|
|
240
|
+
if (!edits.some((existing) => rangesOverlap(existing, edit)))
|
|
241
|
+
edits.push(edit);
|
|
242
|
+
}
|
|
243
|
+
return { edits, localeTodos: examples.localeTodos };
|
|
244
|
+
}
|
|
245
|
+
function resolveRiskClass(top, annotations, toolRouter, fileName, operationKey) {
|
|
246
|
+
const topRisk = top.get("riskClass");
|
|
247
|
+
const toolRisk = toolRouter.get("riskClass");
|
|
248
|
+
let declared;
|
|
249
|
+
if (topRisk !== undefined && toolRisk !== undefined) {
|
|
250
|
+
const topValue = literalString(topRisk.initializer);
|
|
251
|
+
const toolValue = literalString(toolRisk.initializer);
|
|
252
|
+
if (topValue === undefined || toolValue === undefined) {
|
|
253
|
+
return nonLiteral(fileName, operationKey, "riskClass must be a string literal.");
|
|
254
|
+
}
|
|
255
|
+
if (topValue !== toolValue) {
|
|
256
|
+
return {
|
|
257
|
+
refusal: refusal(fileName, operationKey, "safety_conflict", `Top-level riskClass ${JSON.stringify(topValue)} conflicts with toolRouter.riskClass ${JSON.stringify(toolValue)}.`),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
declared = { member: topRisk, source: "top" };
|
|
261
|
+
}
|
|
262
|
+
else if (topRisk !== undefined) {
|
|
263
|
+
declared = { member: topRisk, source: "top" };
|
|
264
|
+
}
|
|
265
|
+
else if (toolRisk !== undefined) {
|
|
266
|
+
declared = { member: toolRisk, source: "toolRouter" };
|
|
267
|
+
}
|
|
268
|
+
const destructive = literalBooleanMember(annotations.get("destructive"), fileName, operationKey, "annotations.destructive");
|
|
269
|
+
if ("refusal" in destructive)
|
|
270
|
+
return destructive;
|
|
271
|
+
const readOnly = literalBooleanMember(annotations.get("readOnly"), fileName, operationKey, "annotations.readOnly");
|
|
272
|
+
if ("refusal" in readOnly)
|
|
273
|
+
return readOnly;
|
|
274
|
+
if (declared !== undefined) {
|
|
275
|
+
const value = literalString(declared.member.initializer);
|
|
276
|
+
if (value === undefined || !SAFETY_VALUES.has(value)) {
|
|
277
|
+
return nonLiteral(fileName, operationKey, "riskClass must be a literal read, write, destructive, or external-send value.");
|
|
278
|
+
}
|
|
279
|
+
if (destructive.value === true && value !== "destructive") {
|
|
280
|
+
return {
|
|
281
|
+
refusal: refusal(fileName, operationKey, "safety_conflict", `Declared riskClass ${JSON.stringify(value)} contradicts annotations.destructive: true.`),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
value,
|
|
286
|
+
text: declared.member.initializer?.getText() ?? JSON.stringify(value),
|
|
287
|
+
source: declared.source,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (destructive.value === true) {
|
|
291
|
+
return { value: "destructive", text: '"destructive"', source: "annotations" };
|
|
292
|
+
}
|
|
293
|
+
if (readOnly.value === true) {
|
|
294
|
+
return { value: "read", text: '"read"', source: "annotations" };
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
refusal: refusal(fileName, operationKey, "no_safety", "No authored riskClass, annotations.destructive: true, or annotations.readOnly: true declaration exists; refusing to invent write."),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function resolveConnectionMode(top, toolRouter, fileName, operationKey, source) {
|
|
301
|
+
const flat = top.get("connectionMode");
|
|
302
|
+
const nested = toolRouter.get("connectionMode");
|
|
303
|
+
if (flat !== undefined && nested !== undefined) {
|
|
304
|
+
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
305
|
+
if (same === undefined) {
|
|
306
|
+
return nonLiteral(fileName, operationKey, "Conflicting connectionMode declarations must be literal values.");
|
|
307
|
+
}
|
|
308
|
+
if (!same) {
|
|
309
|
+
return {
|
|
310
|
+
refusal: refusal(fileName, operationKey, "connection_mode_conflict", "Top-level connectionMode conflicts with toolRouter.connectionMode."),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {};
|
|
314
|
+
}
|
|
315
|
+
if (flat !== undefined)
|
|
316
|
+
return {};
|
|
317
|
+
if (nested !== undefined) {
|
|
318
|
+
return { insert: memberText(nested, "connectionMode", source) };
|
|
319
|
+
}
|
|
320
|
+
const required = toolRouter.get("requiresConnection");
|
|
321
|
+
if (required === undefined)
|
|
322
|
+
return {};
|
|
323
|
+
const value = literalBooleanMember(required, fileName, operationKey, "toolRouter.requiresConnection");
|
|
324
|
+
if ("refusal" in value)
|
|
325
|
+
return value;
|
|
326
|
+
return { insert: `connectionMode: ${value.value ? '"required"' : '"none"'}` };
|
|
327
|
+
}
|
|
328
|
+
function resolveApproval(flat, nested, riskClass, fileName, operationKey, source) {
|
|
329
|
+
let selected = flat ?? nested;
|
|
330
|
+
if (flat !== undefined && nested !== undefined) {
|
|
331
|
+
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
332
|
+
if (same === undefined) {
|
|
333
|
+
return nonLiteral(fileName, operationKey, "approval declarations must be string literals.");
|
|
334
|
+
}
|
|
335
|
+
if (!same) {
|
|
336
|
+
return {
|
|
337
|
+
refusal: refusal(fileName, operationKey, "approval_conflict", "Top-level approval conflicts with toolRouter.approval."),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
selected = flat;
|
|
341
|
+
}
|
|
342
|
+
if (selected === undefined)
|
|
343
|
+
return {};
|
|
344
|
+
const value = literalString(selected.initializer);
|
|
345
|
+
if (value === undefined) {
|
|
346
|
+
return nonLiteral(fileName, operationKey, "approval must be a string literal.");
|
|
347
|
+
}
|
|
348
|
+
const defaultValue = defaultApprovalPolicy(riskClass);
|
|
349
|
+
if (value === defaultValue) {
|
|
350
|
+
return flat === undefined ? {} : { removeTop: flat.property };
|
|
351
|
+
}
|
|
352
|
+
if (flat !== undefined)
|
|
353
|
+
return {};
|
|
354
|
+
return { insert: memberText(selected, "approval", source) };
|
|
355
|
+
}
|
|
356
|
+
function defaultApprovalPolicy(riskClass) {
|
|
357
|
+
if (riskClass === "read")
|
|
358
|
+
return "never";
|
|
359
|
+
if (riskClass === "write")
|
|
360
|
+
return "risk-based";
|
|
361
|
+
return "always";
|
|
362
|
+
}
|
|
363
|
+
function mergeFlatAndNested(field, flat, nested, fileName, operationKey, source, reason) {
|
|
364
|
+
if (nested === undefined)
|
|
365
|
+
return {};
|
|
366
|
+
if (flat === undefined)
|
|
367
|
+
return { insert: memberText(nested, field, source) };
|
|
368
|
+
const same = equivalentLiteral(flat.initializer, nested.initializer);
|
|
369
|
+
if (same === undefined) {
|
|
370
|
+
return nonLiteral(fileName, operationKey, `${field} must be literal when both top-level and nested declarations exist.`);
|
|
371
|
+
}
|
|
372
|
+
if (!same) {
|
|
373
|
+
return {
|
|
374
|
+
refusal: refusal(fileName, operationKey, reason, `Top-level ${field} conflicts with nested ${field}.`),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {};
|
|
378
|
+
}
|
|
379
|
+
function planExamples(inputExamples, existingExamples, fileName, site, source, localeFiles) {
|
|
380
|
+
if (inputExamples === undefined)
|
|
381
|
+
return { edits: [], localeTodos: [] };
|
|
382
|
+
if (existingExamples !== undefined) {
|
|
383
|
+
return {
|
|
384
|
+
refusal: refusal(fileName, site.operationKey, "examples_conflict", "Both inputExamples and examples are declared."),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const array = unwrapExpression(inputExamples.initializer);
|
|
388
|
+
if (array === undefined || !ts.isArrayLiteralExpression(array)) {
|
|
389
|
+
return nonLiteral(fileName, site.operationKey, "inputExamples must be an array literal.");
|
|
390
|
+
}
|
|
391
|
+
if (array.elements.length > 0 && !site.operationIdProven) {
|
|
392
|
+
return {
|
|
393
|
+
refusal: refusal(fileName, site.operationKey, "operation_id_unresolved", "inputExamples requires an exact operation id proven from a static operations map."),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (array.elements.length > 0 && !localeFiles.includes("locales/en.json")) {
|
|
397
|
+
return {
|
|
398
|
+
refusal: refusal(fileName, site.operationKey, "missing_english_locale", "inputExamples cannot be migrated because locales/en.json does not exist."),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const edits = [];
|
|
402
|
+
const todos = [];
|
|
403
|
+
const propertyNameNode = inputExamples.property.name;
|
|
404
|
+
if (propertyNameNode === undefined) {
|
|
405
|
+
return nonLiteral(fileName, site.operationKey, "inputExamples uses an unsupported property name.");
|
|
406
|
+
}
|
|
407
|
+
edits.push({
|
|
408
|
+
start: propertyNameNode.getStart(source),
|
|
409
|
+
end: propertyNameNode.getEnd(),
|
|
410
|
+
text: "examples",
|
|
411
|
+
});
|
|
412
|
+
for (let index = 0; index < array.elements.length; index += 1) {
|
|
413
|
+
const element = array.elements[index];
|
|
414
|
+
const object = element === undefined ? undefined : unwrapExpression(element);
|
|
415
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object)) {
|
|
416
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}] must be an object literal.`);
|
|
417
|
+
}
|
|
418
|
+
const members = indexMembersFromObject(object, fileName, site.operationKey);
|
|
419
|
+
if ("refusal" in members)
|
|
420
|
+
return members;
|
|
421
|
+
const allowed = new Set(["scenario", "input", "rationale"]);
|
|
422
|
+
const unknown = firstUnknown(members.byName, allowed);
|
|
423
|
+
if (unknown !== undefined) {
|
|
424
|
+
return unsupportedContainerMember(fileName, site.operationKey, `inputExamples[${index}]`, unknown);
|
|
425
|
+
}
|
|
426
|
+
const scenario = members.byName.get("scenario");
|
|
427
|
+
const input = members.byName.get("input");
|
|
428
|
+
if (scenario === undefined || input === undefined) {
|
|
429
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}] must declare scenario and input.`);
|
|
430
|
+
}
|
|
431
|
+
const scenarioProse = literalString(scenario.initializer);
|
|
432
|
+
if (scenarioProse === undefined) {
|
|
433
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].scenario must be a string literal.`);
|
|
434
|
+
}
|
|
435
|
+
const scenarioKey = `operations.${site.operationKey}.examples.${index}.scenario`;
|
|
436
|
+
edits.push(replaceExampleLocaleMember(scenario, "scenarioKey", scenarioKey, source));
|
|
437
|
+
for (const localeFile of localeFiles) {
|
|
438
|
+
todos.push({
|
|
439
|
+
localeFile,
|
|
440
|
+
operationKey: site.operationKey,
|
|
441
|
+
key: scenarioKey,
|
|
442
|
+
originalProse: scenarioProse,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const rationale = members.byName.get("rationale");
|
|
446
|
+
if (rationale !== undefined) {
|
|
447
|
+
const rationaleProse = literalString(rationale.initializer);
|
|
448
|
+
if (rationaleProse === undefined) {
|
|
449
|
+
return nonLiteral(fileName, site.operationKey, `inputExamples[${index}].rationale must be a string literal.`);
|
|
450
|
+
}
|
|
451
|
+
const rationaleKey = `operations.${site.operationKey}.examples.${index}.rationale`;
|
|
452
|
+
edits.push(replaceExampleLocaleMember(rationale, "rationaleKey", rationaleKey, source));
|
|
453
|
+
for (const localeFile of localeFiles) {
|
|
454
|
+
todos.push({
|
|
455
|
+
localeFile,
|
|
456
|
+
operationKey: site.operationKey,
|
|
457
|
+
key: rationaleKey,
|
|
458
|
+
originalProse: rationaleProse,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { edits, localeTodos: todos };
|
|
464
|
+
}
|
|
465
|
+
function replaceExampleLocaleMember(member, newName, localeKey, source) {
|
|
466
|
+
return {
|
|
467
|
+
start: member.property.getStart(source),
|
|
468
|
+
end: member.property.getEnd(),
|
|
469
|
+
text: `${newName}: ${JSON.stringify(localeKey)}`,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function discoverOperationSites(source, fileName, constObjects, operationIds) {
|
|
473
|
+
const sitesByStart = new Map();
|
|
474
|
+
const localIds = new Map(operationIds ?? []);
|
|
475
|
+
const refusals = [];
|
|
476
|
+
for (const map of collectOperationsMaps(source, constObjects, fileName, refusals)) {
|
|
477
|
+
for (const property of map.properties) {
|
|
478
|
+
if (ts.isSpreadAssignment(property))
|
|
479
|
+
continue;
|
|
480
|
+
const key = staticPropertyName(property.name);
|
|
481
|
+
if (key === undefined) {
|
|
482
|
+
refusals.push(refusal(fileName, "<computed>", "non_literal", "An operations map uses a computed operation key."));
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const value = propertyValue(property);
|
|
486
|
+
if (value === undefined)
|
|
487
|
+
continue;
|
|
488
|
+
const unwrapped = unwrapExpression(value);
|
|
489
|
+
if (unwrapped === undefined)
|
|
490
|
+
continue;
|
|
491
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
492
|
+
localIds.set(unwrapped.text, key);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const operationObject = operationObjectFromExpression(unwrapped);
|
|
496
|
+
if (operationObject !== undefined) {
|
|
497
|
+
sitesByStart.set(operationObject.getStart(source), {
|
|
498
|
+
object: operationObject,
|
|
499
|
+
operationKey: key,
|
|
500
|
+
operationIdProven: true,
|
|
501
|
+
});
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
505
|
+
sitesByStart.set(unwrapped.getStart(source), {
|
|
506
|
+
object: unwrapped,
|
|
507
|
+
operationKey: key,
|
|
508
|
+
operationIdProven: true,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const visit = (node) => {
|
|
514
|
+
if (ts.isCallExpression(node) && isOperationHelperCall(node)) {
|
|
515
|
+
const argument = operationArgument(node);
|
|
516
|
+
const bindingName = enclosingBindingName(node);
|
|
517
|
+
const operationKey = (bindingName === undefined ? undefined : localIds.get(bindingName)) ??
|
|
518
|
+
bindingName ??
|
|
519
|
+
"<anonymous>";
|
|
520
|
+
const unwrappedArgument = unwrapExpression(argument);
|
|
521
|
+
if (unwrappedArgument === undefined || !ts.isObjectLiteralExpression(unwrappedArgument)) {
|
|
522
|
+
refusals.push(refusal(fileName, operationKey, "non_literal", "Operation helper argument must be an object literal."));
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
const object = unwrappedArgument;
|
|
526
|
+
if (ts.isObjectLiteralExpression(object)) {
|
|
527
|
+
const existing = sitesByStart.get(object.getStart(source));
|
|
528
|
+
if (existing === undefined) {
|
|
529
|
+
const operationIdProven = bindingName !== undefined && localIds.has(bindingName);
|
|
530
|
+
sitesByStart.set(object.getStart(source), {
|
|
531
|
+
object,
|
|
532
|
+
operationKey,
|
|
533
|
+
bindingName,
|
|
534
|
+
operationIdProven,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
else if (existing.operationKey.startsWith("<")) {
|
|
538
|
+
existing.operationKey = operationKey;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
ts.forEachChild(node, visit);
|
|
544
|
+
};
|
|
545
|
+
visit(source);
|
|
546
|
+
return {
|
|
547
|
+
sites: [...sitesByStart.values()].sort((left, right) => left.object.getStart(source) - right.object.getStart(source)),
|
|
548
|
+
refusals,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function collectOperationsMaps(source, constObjects, fileName, refusals) {
|
|
552
|
+
const maps = new Map();
|
|
553
|
+
const inspect = (expression, label) => {
|
|
554
|
+
const unwrapped = unwrapExpression(expression);
|
|
555
|
+
if (unwrapped === undefined)
|
|
556
|
+
return;
|
|
557
|
+
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
558
|
+
maps.set(unwrapped.getStart(source), unwrapped);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
562
|
+
const object = constObjects.get(unwrapped.text);
|
|
563
|
+
if (object !== undefined)
|
|
564
|
+
maps.set(object.getStart(source), object);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (ts.isCallExpression(unwrapped)) {
|
|
568
|
+
refusals.push(refusal(fileName, label, "factory_composed_operations", "The operations map is produced by a factory call and cannot be statically enumerated."));
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
for (const statement of source.statements) {
|
|
572
|
+
if (ts.isVariableStatement(statement)) {
|
|
573
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
574
|
+
if (ts.isIdentifier(declaration.name) &&
|
|
575
|
+
declaration.name.text === "operations" &&
|
|
576
|
+
declaration.initializer !== undefined) {
|
|
577
|
+
inspect(declaration.initializer, "<operations>");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const visit = (node) => {
|
|
583
|
+
if (ts.isPropertyAssignment(node) && staticPropertyName(node.name) === "operations") {
|
|
584
|
+
// Only provider declarations own an `operations` map: the property
|
|
585
|
+
// must sit inside an argument of defineProvider()/buildProvider()-style
|
|
586
|
+
// call, or its initializer (possibly via a same-file const) must
|
|
587
|
+
// contain defineOperation/defineStreamOperation members. A bare
|
|
588
|
+
// `operations:` key elsewhere (a zod schema field, a config object)
|
|
589
|
+
// is not a declaration site; treating it as one produced false
|
|
590
|
+
// factory_composed_operations refusals on ekitan's
|
|
591
|
+
// scripts/fixture-integrity.ts, whose schema field calls
|
|
592
|
+
// .superRefine(...).
|
|
593
|
+
if (isProviderOperationsProperty(node, constObjects)) {
|
|
594
|
+
inspect(node.initializer, "<operations>");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
ts.forEachChild(node, visit);
|
|
598
|
+
};
|
|
599
|
+
visit(source);
|
|
600
|
+
return [...maps.values()];
|
|
601
|
+
}
|
|
602
|
+
function isProviderOperationsProperty(node, constObjects) {
|
|
603
|
+
// (a) The initializer (direct or via same-file const) mentions
|
|
604
|
+
// defineOperation / defineStreamOperation — the strongest signal.
|
|
605
|
+
const target = ts.isIdentifier(unwrapExpression(node.initializer) ?? node.initializer)
|
|
606
|
+
? constObjects.get(unwrapExpression(node.initializer).text)
|
|
607
|
+
: undefined;
|
|
608
|
+
const initializerText = (target ?? node.initializer).getText();
|
|
609
|
+
if (/\bdefine(?:Stream)?Operation\b/.test(initializerText))
|
|
610
|
+
return true;
|
|
611
|
+
// (b) The property is inside a call argument whose callee looks like a
|
|
612
|
+
// provider builder (defineProvider(...)(...) / buildProvider(...)).
|
|
613
|
+
let current = node;
|
|
614
|
+
while (current.parent !== undefined) {
|
|
615
|
+
const parent = current.parent;
|
|
616
|
+
if (ts.isCallExpression(parent)) {
|
|
617
|
+
const callee = parent.expression.getText();
|
|
618
|
+
if (/(?:defineProvider|Provider)\b/.test(callee))
|
|
619
|
+
return true;
|
|
620
|
+
// A call expression that is itself the invocation of a
|
|
621
|
+
// defineProvider(...) result: defineProvider({...})({operations})
|
|
622
|
+
if (ts.isCallExpression(parent.expression)) {
|
|
623
|
+
if (/\bdefineProvider\b/.test(parent.expression.expression.getText())) {
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
if (ts.isSourceFile(parent))
|
|
630
|
+
return false;
|
|
631
|
+
current = parent;
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
function operationObjectFromExpression(expression) {
|
|
636
|
+
const unwrapped = unwrapExpression(expression);
|
|
637
|
+
if (unwrapped === undefined || !ts.isCallExpression(unwrapped))
|
|
638
|
+
return undefined;
|
|
639
|
+
if (!isOperationHelperCall(unwrapped))
|
|
640
|
+
return undefined;
|
|
641
|
+
const argument = operationArgument(unwrapped);
|
|
642
|
+
const object = argument === undefined ? undefined : unwrapExpression(argument);
|
|
643
|
+
return object !== undefined && ts.isObjectLiteralExpression(object) ? object : undefined;
|
|
644
|
+
}
|
|
645
|
+
function isOperationHelperCall(call) {
|
|
646
|
+
if (ts.isIdentifier(call.expression) && OPERATION_HELPERS.has(call.expression.text)) {
|
|
647
|
+
return call.arguments.length === 1;
|
|
648
|
+
}
|
|
649
|
+
return (ts.isCallExpression(call.expression) &&
|
|
650
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
651
|
+
OPERATION_HELPERS.has(call.expression.expression.text) &&
|
|
652
|
+
call.expression.arguments.length === 0 &&
|
|
653
|
+
call.arguments.length === 1);
|
|
654
|
+
}
|
|
655
|
+
function operationArgument(call) {
|
|
656
|
+
if (ts.isIdentifier(call.expression) && OPERATION_HELPERS.has(call.expression.text)) {
|
|
657
|
+
return call.arguments.length === 1 ? call.arguments[0] : undefined;
|
|
658
|
+
}
|
|
659
|
+
if (ts.isCallExpression(call.expression) &&
|
|
660
|
+
ts.isIdentifier(call.expression.expression) &&
|
|
661
|
+
OPERATION_HELPERS.has(call.expression.expression.text) &&
|
|
662
|
+
call.expression.arguments.length === 0) {
|
|
663
|
+
return call.arguments.length === 1 ? call.arguments[0] : undefined;
|
|
664
|
+
}
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
function enclosingBindingName(node) {
|
|
668
|
+
let current = node;
|
|
669
|
+
while (current !== undefined) {
|
|
670
|
+
if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
|
|
671
|
+
return current.name.text;
|
|
672
|
+
}
|
|
673
|
+
if (ts.isPropertyAssignment(current))
|
|
674
|
+
return staticPropertyName(current.name);
|
|
675
|
+
if (ts.isExportAssignment(current))
|
|
676
|
+
return "default";
|
|
677
|
+
current = current.parent;
|
|
678
|
+
}
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
function collectModuleConstObjects(source) {
|
|
682
|
+
const objects = new Map();
|
|
683
|
+
for (const statement of source.statements) {
|
|
684
|
+
if (!ts.isVariableStatement(statement))
|
|
685
|
+
continue;
|
|
686
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0)
|
|
687
|
+
continue;
|
|
688
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
689
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined)
|
|
690
|
+
continue;
|
|
691
|
+
const expression = unwrapExpression(declaration.initializer);
|
|
692
|
+
if (expression !== undefined && ts.isObjectLiteralExpression(expression)) {
|
|
693
|
+
objects.set(declaration.name.text, expression);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return objects;
|
|
698
|
+
}
|
|
699
|
+
function expandMembers(object, constObjects, fileName, operationKey, seen = new Set()) {
|
|
700
|
+
if (seen.has(object)) {
|
|
701
|
+
return nonLiteral(fileName, operationKey, "A module-level object spread is recursive.");
|
|
702
|
+
}
|
|
703
|
+
seen.add(object);
|
|
704
|
+
const members = [];
|
|
705
|
+
for (const property of object.properties) {
|
|
706
|
+
if (ts.isSpreadAssignment(property)) {
|
|
707
|
+
const expression = unwrapExpression(property.expression);
|
|
708
|
+
const spreadObject = expression !== undefined && ts.isObjectLiteralExpression(expression)
|
|
709
|
+
? expression
|
|
710
|
+
: expression !== undefined && ts.isIdentifier(expression)
|
|
711
|
+
? constObjects.get(expression.text)
|
|
712
|
+
: undefined;
|
|
713
|
+
if (spreadObject === undefined) {
|
|
714
|
+
members.push({ name: "<spread>", property });
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const expanded = expandMembers(spreadObject, constObjects, fileName, operationKey, new Set(seen));
|
|
718
|
+
if ("refusal" in expanded)
|
|
719
|
+
return expanded;
|
|
720
|
+
members.push(...expanded.members.map((member) => ({ ...member, fromSpread: true })));
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const name = staticPropertyName(property.name);
|
|
724
|
+
if (name === undefined) {
|
|
725
|
+
return nonLiteral(fileName, operationKey, "A declaration member uses a computed name.");
|
|
726
|
+
}
|
|
727
|
+
const initializer = propertyValue(property);
|
|
728
|
+
members.push({ name, property, initializer });
|
|
729
|
+
}
|
|
730
|
+
return { members };
|
|
731
|
+
}
|
|
732
|
+
function indexMembers(members, fileName, operationKey) {
|
|
733
|
+
const byName = new Map();
|
|
734
|
+
const spreads = [];
|
|
735
|
+
for (const member of members) {
|
|
736
|
+
if (member.name === "<spread>") {
|
|
737
|
+
spreads.push(member);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const previous = byName.get(member.name);
|
|
741
|
+
if (previous !== undefined && previous.fromSpread !== true && member.fromSpread !== true) {
|
|
742
|
+
return nonLiteral(fileName, operationKey, `Duplicate ${member.name} declarations cannot be collapsed safely.`);
|
|
743
|
+
}
|
|
744
|
+
byName.set(member.name, member);
|
|
745
|
+
}
|
|
746
|
+
return { byName, spreads };
|
|
747
|
+
}
|
|
748
|
+
function indexMembersFromObject(object, fileName, operationKey) {
|
|
749
|
+
const members = [];
|
|
750
|
+
for (const property of object.properties) {
|
|
751
|
+
if (ts.isSpreadAssignment(property)) {
|
|
752
|
+
return nonLiteral(fileName, operationKey, "Example objects cannot contain spreads.");
|
|
753
|
+
}
|
|
754
|
+
const name = staticPropertyName(property.name);
|
|
755
|
+
const initializer = propertyValue(property);
|
|
756
|
+
if (name === undefined || initializer === undefined) {
|
|
757
|
+
return nonLiteral(fileName, operationKey, "Example members must be literal properties.");
|
|
758
|
+
}
|
|
759
|
+
members.push({ name, property, initializer });
|
|
760
|
+
}
|
|
761
|
+
const indexed = indexMembers(members, fileName, operationKey);
|
|
762
|
+
if ("refusal" in indexed)
|
|
763
|
+
return indexed;
|
|
764
|
+
return { byName: indexed.byName };
|
|
765
|
+
}
|
|
766
|
+
function resolveObjectInitializer(member, constObjects) {
|
|
767
|
+
const initializer = unwrapExpression(member.initializer);
|
|
768
|
+
if (initializer === undefined)
|
|
769
|
+
return undefined;
|
|
770
|
+
if (ts.isObjectLiteralExpression(initializer))
|
|
771
|
+
return initializer;
|
|
772
|
+
if (ts.isIdentifier(initializer))
|
|
773
|
+
return constObjects.get(initializer.text);
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
function memberMap(members) {
|
|
777
|
+
return new Map(members.map((member) => [member.name, member]));
|
|
778
|
+
}
|
|
779
|
+
function firstUnknown(members, allowed) {
|
|
780
|
+
for (const name of members.keys()) {
|
|
781
|
+
if (!allowed.has(name))
|
|
782
|
+
return name;
|
|
783
|
+
}
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
function unsupportedContainerMember(fileName, operationKey, container, member) {
|
|
787
|
+
return {
|
|
788
|
+
refusal: refusal(fileName, operationKey, "unsupported_member", `${container}.${member} has no declared ADR-0009 transform.`),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function literalBooleanMember(member, fileName, operationKey, label) {
|
|
792
|
+
if (member === undefined)
|
|
793
|
+
return { value: undefined };
|
|
794
|
+
const expression = unwrapExpression(member.initializer);
|
|
795
|
+
if (expression?.kind === ts.SyntaxKind.TrueKeyword)
|
|
796
|
+
return { value: true };
|
|
797
|
+
if (expression?.kind === ts.SyntaxKind.FalseKeyword)
|
|
798
|
+
return { value: false };
|
|
799
|
+
return nonLiteral(fileName, operationKey, `${label} must be a boolean literal.`);
|
|
800
|
+
}
|
|
801
|
+
function literalString(expression) {
|
|
802
|
+
const value = unwrapExpression(expression);
|
|
803
|
+
if (value === undefined)
|
|
804
|
+
return undefined;
|
|
805
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
806
|
+
return value.text;
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
809
|
+
function equivalentLiteral(left, right) {
|
|
810
|
+
const leftValue = literalValue(left);
|
|
811
|
+
const rightValue = literalValue(right);
|
|
812
|
+
if (leftValue === NOT_LITERAL || rightValue === NOT_LITERAL)
|
|
813
|
+
return undefined;
|
|
814
|
+
return JSON.stringify(leftValue) === JSON.stringify(rightValue);
|
|
815
|
+
}
|
|
816
|
+
const NOT_LITERAL = Symbol("not-literal");
|
|
817
|
+
function literalValue(expression) {
|
|
818
|
+
const node = unwrapExpression(expression);
|
|
819
|
+
if (node === undefined)
|
|
820
|
+
return NOT_LITERAL;
|
|
821
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
822
|
+
return node.text;
|
|
823
|
+
if (ts.isNumericLiteral(node))
|
|
824
|
+
return Number(node.text);
|
|
825
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword)
|
|
826
|
+
return true;
|
|
827
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword)
|
|
828
|
+
return false;
|
|
829
|
+
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
830
|
+
return null;
|
|
831
|
+
if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
|
|
832
|
+
if (node.operator === ts.SyntaxKind.MinusToken)
|
|
833
|
+
return -Number(node.operand.text);
|
|
834
|
+
if (node.operator === ts.SyntaxKind.PlusToken)
|
|
835
|
+
return Number(node.operand.text);
|
|
836
|
+
}
|
|
837
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
838
|
+
const result = [];
|
|
839
|
+
for (const element of node.elements) {
|
|
840
|
+
if (ts.isSpreadElement(element))
|
|
841
|
+
return NOT_LITERAL;
|
|
842
|
+
const value = literalValue(element);
|
|
843
|
+
if (value === NOT_LITERAL)
|
|
844
|
+
return NOT_LITERAL;
|
|
845
|
+
result.push(value);
|
|
846
|
+
}
|
|
847
|
+
return result;
|
|
848
|
+
}
|
|
849
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
850
|
+
const result = {};
|
|
851
|
+
for (const property of node.properties) {
|
|
852
|
+
if (!ts.isPropertyAssignment(property))
|
|
853
|
+
return NOT_LITERAL;
|
|
854
|
+
const name = staticPropertyName(property.name);
|
|
855
|
+
const value = literalValue(property.initializer);
|
|
856
|
+
if (name === undefined || value === NOT_LITERAL)
|
|
857
|
+
return NOT_LITERAL;
|
|
858
|
+
result[name] = value;
|
|
859
|
+
}
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
return NOT_LITERAL;
|
|
863
|
+
}
|
|
864
|
+
function memberText(member, name, source) {
|
|
865
|
+
if (member.initializer === undefined)
|
|
866
|
+
return name;
|
|
867
|
+
return `${name}: ${member.initializer.getText(source)}`;
|
|
868
|
+
}
|
|
869
|
+
function addInsertion(insertions, sourceContainer, text) {
|
|
870
|
+
const items = insertions.get(sourceContainer) ?? [];
|
|
871
|
+
items.push({ source: sourceContainer, text });
|
|
872
|
+
insertions.set(sourceContainer, items);
|
|
873
|
+
}
|
|
874
|
+
function removalRanges(object, properties, source) {
|
|
875
|
+
const unique = [...new Set(properties)];
|
|
876
|
+
const text = source.getFullText();
|
|
877
|
+
const ranges = [];
|
|
878
|
+
for (const property of unique) {
|
|
879
|
+
let start = property.getStart(source);
|
|
880
|
+
let end = property.getEnd();
|
|
881
|
+
let cursor = end;
|
|
882
|
+
while (cursor < object.getEnd() && /\s/.test(text.charAt(cursor)))
|
|
883
|
+
cursor += 1;
|
|
884
|
+
if (text.charAt(cursor) === ",") {
|
|
885
|
+
end = cursor + 1;
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
cursor = start - 1;
|
|
889
|
+
while (cursor > object.getStart(source) && /\s/.test(text.charAt(cursor)))
|
|
890
|
+
cursor -= 1;
|
|
891
|
+
if (text.charAt(cursor) === ",")
|
|
892
|
+
start = cursor;
|
|
893
|
+
}
|
|
894
|
+
ranges.push({ start, end, text: "" });
|
|
895
|
+
}
|
|
896
|
+
return mergeRemovalEdits(ranges);
|
|
897
|
+
}
|
|
898
|
+
function mergeRemovalEdits(edits) {
|
|
899
|
+
const ordered = [...edits].sort((left, right) => left.start - right.start);
|
|
900
|
+
const merged = [];
|
|
901
|
+
for (const edit of ordered) {
|
|
902
|
+
const previous = merged.at(-1);
|
|
903
|
+
if (previous !== undefined && edit.start <= previous.end) {
|
|
904
|
+
merged[merged.length - 1] = {
|
|
905
|
+
start: previous.start,
|
|
906
|
+
end: Math.max(previous.end, edit.end),
|
|
907
|
+
text: "",
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
merged.push(edit);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return merged;
|
|
915
|
+
}
|
|
916
|
+
function removeFirst(items, value) {
|
|
917
|
+
const index = items.indexOf(value);
|
|
918
|
+
if (index >= 0)
|
|
919
|
+
items.splice(index, 1);
|
|
920
|
+
}
|
|
921
|
+
function rangesOverlap(left, right) {
|
|
922
|
+
return left.start < right.end && right.start < left.end;
|
|
923
|
+
}
|
|
924
|
+
function applyEdits(sourceText, edits) {
|
|
925
|
+
const ordered = [...edits].sort((left, right) => right.start - left.start || right.end - left.end);
|
|
926
|
+
let code = sourceText;
|
|
927
|
+
for (const edit of ordered) {
|
|
928
|
+
code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
|
|
929
|
+
}
|
|
930
|
+
return code;
|
|
931
|
+
}
|
|
932
|
+
function indentationAt(text, position) {
|
|
933
|
+
const lineStart = text.lastIndexOf("\n", position - 1) + 1;
|
|
934
|
+
return text.slice(lineStart, position).match(/^\s*/)?.[0] ?? "";
|
|
935
|
+
}
|
|
936
|
+
function propertyValue(property) {
|
|
937
|
+
if (ts.isPropertyAssignment(property))
|
|
938
|
+
return property.initializer;
|
|
939
|
+
if (ts.isShorthandPropertyAssignment(property))
|
|
940
|
+
return property.name;
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
function staticPropertyName(name) {
|
|
944
|
+
if (name === undefined)
|
|
945
|
+
return undefined;
|
|
946
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
947
|
+
return name.text;
|
|
948
|
+
}
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
function unwrapExpression(expression) {
|
|
952
|
+
let current = expression;
|
|
953
|
+
while (current !== undefined) {
|
|
954
|
+
if (ts.isParenthesizedExpression(current) ||
|
|
955
|
+
ts.isAsExpression(current) ||
|
|
956
|
+
ts.isTypeAssertionExpression(current) ||
|
|
957
|
+
ts.isSatisfiesExpression(current) ||
|
|
958
|
+
ts.isNonNullExpression(current)) {
|
|
959
|
+
current = current.expression;
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
return current;
|
|
963
|
+
}
|
|
964
|
+
return undefined;
|
|
965
|
+
}
|
|
966
|
+
function parseSource(fileName, sourceText) {
|
|
967
|
+
return ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
968
|
+
}
|
|
969
|
+
function firstSyntaxError(source) {
|
|
970
|
+
const diagnostics = source
|
|
971
|
+
.parseDiagnostics;
|
|
972
|
+
if (diagnostics === undefined || diagnostics.length === 0)
|
|
973
|
+
return undefined;
|
|
974
|
+
const first = diagnostics[0];
|
|
975
|
+
if (first === undefined)
|
|
976
|
+
return undefined;
|
|
977
|
+
const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
|
|
978
|
+
const { line } = source.getLineAndCharacterOfPosition(first.start);
|
|
979
|
+
return `${message} (line ${line + 1})`;
|
|
980
|
+
}
|
|
981
|
+
function refusal(file, operationKey, reason, detail) {
|
|
982
|
+
return { file, operationKey, reason, detail };
|
|
983
|
+
}
|
|
984
|
+
function nonLiteral(fileName, operationKey, detail) {
|
|
985
|
+
return {
|
|
986
|
+
refusal: refusal(fileName, operationKey, "non_literal", detail),
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/** Run the file transform repository-wide, committing writes only if every file is provable. */
|
|
990
|
+
export function migrateOperationDeclarationRepository(providerRootInput, options = {}) {
|
|
991
|
+
const providerRoot = resolve(providerRootInput);
|
|
992
|
+
const sourceFiles = collectSourceFiles(providerRoot);
|
|
993
|
+
const localeFiles = collectLocaleFiles(providerRoot);
|
|
994
|
+
const operationIds = buildOperationIdIndex(sourceFiles);
|
|
995
|
+
const pendingWrites = new Map();
|
|
996
|
+
const changedFiles = [];
|
|
997
|
+
const todos = [];
|
|
998
|
+
const refusals = [];
|
|
999
|
+
let operationCount = 0;
|
|
1000
|
+
for (const sourcePath of sourceFiles) {
|
|
1001
|
+
const relativePath = slash(relative(providerRoot, sourcePath));
|
|
1002
|
+
const result = migrateOperationDeclaration(readFileSync(sourcePath, "utf8"), relativePath, {
|
|
1003
|
+
operationIds: operationIds.get(sourcePath),
|
|
1004
|
+
localeFiles,
|
|
1005
|
+
});
|
|
1006
|
+
if (result.status === "refused") {
|
|
1007
|
+
refusals.push(...result.refusals);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
operationCount += result.operations;
|
|
1011
|
+
if (result.status === "migrated") {
|
|
1012
|
+
pendingWrites.set(sourcePath, result.code);
|
|
1013
|
+
changedFiles.push(relativePath);
|
|
1014
|
+
todos.push(...result.localeTodos);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const todoConflict = findLocaleTodoConflict(todos);
|
|
1018
|
+
if (todoConflict !== undefined)
|
|
1019
|
+
refusals.push(todoConflict);
|
|
1020
|
+
if (refusals.length > 0) {
|
|
1021
|
+
return { status: "refused", providerRoot, refusals };
|
|
1022
|
+
}
|
|
1023
|
+
if (pendingWrites.size === 0) {
|
|
1024
|
+
return {
|
|
1025
|
+
status: "unchanged",
|
|
1026
|
+
providerRoot,
|
|
1027
|
+
changedFiles: [],
|
|
1028
|
+
operationCount,
|
|
1029
|
+
localeTodoCount: 0,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const sidecar = todos.length > 0 ? SIDECAR_NAME : undefined;
|
|
1033
|
+
if (!options.check) {
|
|
1034
|
+
for (const [path, code] of pendingWrites)
|
|
1035
|
+
writeFileSync(path, code, "utf8");
|
|
1036
|
+
if (sidecar !== undefined) {
|
|
1037
|
+
writeFileSync(join(providerRoot, sidecar), renderLocaleTodoSidecar(todos), "utf8");
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
status: options.check ? "would-migrate" : "migrated",
|
|
1042
|
+
providerRoot,
|
|
1043
|
+
changedFiles,
|
|
1044
|
+
operationCount,
|
|
1045
|
+
sidecar,
|
|
1046
|
+
localeTodoCount: todos.length,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
export function renderLocaleTodoSidecar(todos) {
|
|
1050
|
+
const localeFiles = {};
|
|
1051
|
+
for (const todo of [...todos].sort((left, right) => left.localeFile.localeCompare(right.localeFile) || left.key.localeCompare(right.key))) {
|
|
1052
|
+
localeFiles[todo.localeFile] ??= {};
|
|
1053
|
+
localeFiles[todo.localeFile][todo.key] = todo.originalProse;
|
|
1054
|
+
}
|
|
1055
|
+
return `${JSON.stringify({ schemaVersion: 1, localeFiles }, null, 2)}\n`;
|
|
1056
|
+
}
|
|
1057
|
+
function findLocaleTodoConflict(todos) {
|
|
1058
|
+
const values = new Map();
|
|
1059
|
+
for (const todo of todos) {
|
|
1060
|
+
const id = `${todo.localeFile}\0${todo.key}`;
|
|
1061
|
+
const previous = values.get(id);
|
|
1062
|
+
if (previous !== undefined && previous.originalProse !== todo.originalProse) {
|
|
1063
|
+
return refusal(SIDECAR_NAME, todo.operationKey, "locale_todo_conflict", `${todo.localeFile} receives conflicting prose for ${todo.key}.`);
|
|
1064
|
+
}
|
|
1065
|
+
values.set(id, todo);
|
|
1066
|
+
}
|
|
1067
|
+
return undefined;
|
|
1068
|
+
}
|
|
1069
|
+
function collectSourceFiles(root) {
|
|
1070
|
+
const files = [];
|
|
1071
|
+
if (!existsSync(root))
|
|
1072
|
+
return files;
|
|
1073
|
+
const walk = (directory) => {
|
|
1074
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
1075
|
+
if (entry.isDirectory()) {
|
|
1076
|
+
if (SOURCE_SKIP_DIRECTORIES.has(entry.name))
|
|
1077
|
+
continue;
|
|
1078
|
+
walk(join(directory, entry.name));
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
if (!entry.name.endsWith(".ts"))
|
|
1082
|
+
continue;
|
|
1083
|
+
if (entry.name.endsWith(".d.ts") || entry.name.endsWith(".test.ts"))
|
|
1084
|
+
continue;
|
|
1085
|
+
files.push(join(directory, entry.name));
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
walk(root);
|
|
1089
|
+
return files.sort();
|
|
1090
|
+
}
|
|
1091
|
+
function collectLocaleFiles(root) {
|
|
1092
|
+
const localeDirectory = join(root, "locales");
|
|
1093
|
+
if (!existsSync(localeDirectory))
|
|
1094
|
+
return [];
|
|
1095
|
+
return readdirSync(localeDirectory, { withFileTypes: true })
|
|
1096
|
+
.filter((entry) => entry.isFile() && extname(entry.name) === ".json")
|
|
1097
|
+
.map((entry) => `locales/${entry.name}`)
|
|
1098
|
+
.sort();
|
|
1099
|
+
}
|
|
1100
|
+
function buildOperationIdIndex(sourceFiles) {
|
|
1101
|
+
const result = new Map();
|
|
1102
|
+
const sources = new Map();
|
|
1103
|
+
for (const path of sourceFiles) {
|
|
1104
|
+
const source = parseSource(path, readFileSync(path, "utf8"));
|
|
1105
|
+
if (firstSyntaxError(source) === undefined)
|
|
1106
|
+
sources.set(path, source);
|
|
1107
|
+
}
|
|
1108
|
+
const record = (path, binding, operationId) => {
|
|
1109
|
+
const map = result.get(path) ?? new Map();
|
|
1110
|
+
const previous = map.get(binding);
|
|
1111
|
+
if (previous === undefined || previous === operationId)
|
|
1112
|
+
map.set(binding, operationId);
|
|
1113
|
+
else
|
|
1114
|
+
map.delete(binding);
|
|
1115
|
+
result.set(path, map);
|
|
1116
|
+
};
|
|
1117
|
+
for (const [path, source] of sources) {
|
|
1118
|
+
const imports = collectImports(source, path, sources);
|
|
1119
|
+
const constObjects = collectModuleConstObjects(source);
|
|
1120
|
+
const ignored = [];
|
|
1121
|
+
for (const map of collectOperationsMaps(source, constObjects, path, ignored)) {
|
|
1122
|
+
for (const property of map.properties) {
|
|
1123
|
+
if (ts.isSpreadAssignment(property))
|
|
1124
|
+
continue;
|
|
1125
|
+
const operationId = staticPropertyName(property.name);
|
|
1126
|
+
const value = propertyValue(property);
|
|
1127
|
+
const expression = value === undefined ? undefined : unwrapExpression(value);
|
|
1128
|
+
if (operationId === undefined || expression === undefined || !ts.isIdentifier(expression)) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
const imported = imports.get(expression.text);
|
|
1132
|
+
if (imported === undefined)
|
|
1133
|
+
record(path, expression.text, operationId);
|
|
1134
|
+
else
|
|
1135
|
+
record(imported.path, imported.exportedName, operationId);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return result;
|
|
1140
|
+
}
|
|
1141
|
+
function collectImports(source, containingPath, sources) {
|
|
1142
|
+
const imports = new Map();
|
|
1143
|
+
for (const statement of source.statements) {
|
|
1144
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
const target = resolveLocalModule(containingPath, statement.moduleSpecifier.text, sources);
|
|
1148
|
+
if (target === undefined)
|
|
1149
|
+
continue;
|
|
1150
|
+
const clause = statement.importClause;
|
|
1151
|
+
if (clause?.name !== undefined) {
|
|
1152
|
+
imports.set(clause.name.text, { path: target, exportedName: "default" });
|
|
1153
|
+
}
|
|
1154
|
+
const bindings = clause?.namedBindings;
|
|
1155
|
+
if (bindings !== undefined && ts.isNamedImports(bindings)) {
|
|
1156
|
+
for (const element of bindings.elements) {
|
|
1157
|
+
imports.set(element.name.text, {
|
|
1158
|
+
path: target,
|
|
1159
|
+
exportedName: element.propertyName?.text ?? element.name.text,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return imports;
|
|
1165
|
+
}
|
|
1166
|
+
function resolveLocalModule(containingPath, specifier, sources) {
|
|
1167
|
+
if (!specifier.startsWith("."))
|
|
1168
|
+
return undefined;
|
|
1169
|
+
const base = resolve(dirname(containingPath), specifier);
|
|
1170
|
+
for (const candidate of [base, `${base}.ts`, join(base, "index.ts")]) {
|
|
1171
|
+
if (sources.has(candidate))
|
|
1172
|
+
return candidate;
|
|
1173
|
+
}
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
function slash(path) {
|
|
1177
|
+
return path.split("\\").join("/");
|
|
1178
|
+
}
|