@essential-apps/shopify-test-runner 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/normalize.d.ts +61 -0
- package/dist/contracts/normalize.d.ts.map +1 -0
- package/dist/contracts/normalize.js +99 -0
- package/dist/contracts/normalize.js.map +1 -0
- package/dist/contracts/normalizeHtml.d.ts +37 -0
- package/dist/contracts/normalizeHtml.d.ts.map +1 -0
- package/dist/contracts/normalizeHtml.js +89 -0
- package/dist/contracts/normalizeHtml.js.map +1 -0
- package/dist/edge/cert.d.ts +44 -0
- package/dist/edge/cert.d.ts.map +1 -0
- package/dist/edge/cert.js +117 -0
- package/dist/edge/cert.js.map +1 -0
- package/dist/edge/edgeProxy.d.ts +43 -0
- package/dist/edge/edgeProxy.d.ts.map +1 -0
- package/dist/edge/edgeProxy.js +297 -0
- package/dist/edge/edgeProxy.js.map +1 -0
- package/dist/edge/nodeShim.d.ts +2 -0
- package/dist/edge/nodeShim.d.ts.map +1 -0
- package/dist/edge/nodeShim.js +217 -0
- package/dist/edge/nodeShim.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/buildSourceBundle.d.ts +56 -0
- package/dist/lib/buildSourceBundle.d.ts.map +1 -0
- package/dist/lib/buildSourceBundle.js +153 -0
- package/dist/lib/buildSourceBundle.js.map +1 -0
- package/dist/lib/freePort.d.ts +5 -0
- package/dist/lib/freePort.d.ts.map +1 -0
- package/dist/lib/freePort.js +33 -0
- package/dist/lib/freePort.js.map +1 -0
- package/dist/lib/functionBuild.d.ts +99 -0
- package/dist/lib/functionBuild.d.ts.map +1 -0
- package/dist/lib/functionBuild.js +413 -0
- package/dist/lib/functionBuild.js.map +1 -0
- package/dist/lib/neonWsProxy.d.ts +41 -0
- package/dist/lib/neonWsProxy.d.ts.map +1 -0
- package/dist/lib/neonWsProxy.js +101 -0
- package/dist/lib/neonWsProxy.js.map +1 -0
- package/dist/lib/sourceZipUpload.d.ts +45 -0
- package/dist/lib/sourceZipUpload.d.ts.map +1 -0
- package/dist/lib/sourceZipUpload.js +129 -0
- package/dist/lib/sourceZipUpload.js.map +1 -0
- package/dist/lib/stealthLaunch.d.ts +35 -0
- package/dist/lib/stealthLaunch.d.ts.map +1 -0
- package/dist/lib/stealthLaunch.js +46 -0
- package/dist/lib/stealthLaunch.js.map +1 -0
- package/dist/lib/storeAutomation.d.ts +22 -0
- package/dist/lib/storeAutomation.d.ts.map +1 -0
- package/dist/lib/storeAutomation.js +85 -0
- package/dist/lib/storeAutomation.js.map +1 -0
- package/dist/playwright/baseConfig.d.ts +62 -0
- package/dist/playwright/baseConfig.d.ts.map +1 -0
- package/dist/playwright/baseConfig.js +68 -0
- package/dist/playwright/baseConfig.js.map +1 -0
- package/dist/playwright/globalSetup.d.ts +2 -0
- package/dist/playwright/globalSetup.d.ts.map +1 -0
- package/dist/playwright/globalSetup.js +139 -0
- package/dist/playwright/globalSetup.js.map +1 -0
- package/dist/playwright/index.d.ts +9 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +9 -0
- package/dist/playwright/index.js.map +1 -0
- package/dist/probes/fonts.d.ts +4 -0
- package/dist/probes/fonts.d.ts.map +1 -0
- package/dist/probes/fonts.js +255 -0
- package/dist/probes/fonts.js.map +1 -0
- package/dist/probes/mirror.d.ts +4 -0
- package/dist/probes/mirror.d.ts.map +1 -0
- package/dist/probes/mirror.js +260 -0
- package/dist/probes/mirror.js.map +1 -0
- package/dist/probes/runProbe.d.ts +3 -0
- package/dist/probes/runProbe.d.ts.map +1 -0
- package/dist/probes/runProbe.js +219 -0
- package/dist/probes/runProbe.js.map +1 -0
- package/dist/probes/types.d.ts +72 -0
- package/dist/probes/types.d.ts.map +1 -0
- package/dist/probes/types.js +2 -0
- package/dist/probes/types.js.map +1 -0
- package/dist/scripts/_probeSourceUrl.d.ts +3 -0
- package/dist/scripts/_probeSourceUrl.d.ts.map +1 -0
- package/dist/scripts/_probeSourceUrl.js +119 -0
- package/dist/scripts/_probeSourceUrl.js.map +1 -0
- package/dist/scripts/addStore.d.ts +3 -0
- package/dist/scripts/addStore.d.ts.map +1 -0
- package/dist/scripts/addStore.js +46 -0
- package/dist/scripts/addStore.js.map +1 -0
- package/dist/scripts/buildDockerImage.d.ts +3 -0
- package/dist/scripts/buildDockerImage.d.ts.map +1 -0
- package/dist/scripts/buildDockerImage.js +60 -0
- package/dist/scripts/buildDockerImage.js.map +1 -0
- package/dist/scripts/captureAuth.d.ts +3 -0
- package/dist/scripts/captureAuth.d.ts.map +1 -0
- package/dist/scripts/captureAuth.js +124 -0
- package/dist/scripts/captureAuth.js.map +1 -0
- package/dist/scripts/captureContracts.d.ts +3 -0
- package/dist/scripts/captureContracts.d.ts.map +1 -0
- package/dist/scripts/captureContracts.js +517 -0
- package/dist/scripts/captureContracts.js.map +1 -0
- package/dist/scripts/captureRestContracts.d.ts +3 -0
- package/dist/scripts/captureRestContracts.d.ts.map +1 -0
- package/dist/scripts/captureRestContracts.js +245 -0
- package/dist/scripts/captureRestContracts.js.map +1 -0
- package/dist/scripts/checkOperationCoverage.d.ts +3 -0
- package/dist/scripts/checkOperationCoverage.d.ts.map +1 -0
- package/dist/scripts/checkOperationCoverage.js +302 -0
- package/dist/scripts/checkOperationCoverage.js.map +1 -0
- package/dist/scripts/cleanupStores.d.ts +3 -0
- package/dist/scripts/cleanupStores.d.ts.map +1 -0
- package/dist/scripts/cleanupStores.js +77 -0
- package/dist/scripts/cleanupStores.js.map +1 -0
- package/dist/scripts/createStores.d.ts +3 -0
- package/dist/scripts/createStores.d.ts.map +1 -0
- package/dist/scripts/createStores.js +66 -0
- package/dist/scripts/createStores.js.map +1 -0
- package/dist/scripts/deployAppVersion.d.ts +3 -0
- package/dist/scripts/deployAppVersion.d.ts.map +1 -0
- package/dist/scripts/deployAppVersion.js +591 -0
- package/dist/scripts/deployAppVersion.js.map +1 -0
- package/dist/scripts/devE2eBackend.d.ts +3 -0
- package/dist/scripts/devE2eBackend.d.ts.map +1 -0
- package/dist/scripts/devE2eBackend.js +117 -0
- package/dist/scripts/devE2eBackend.js.map +1 -0
- package/dist/scripts/devOnlineBackend.d.ts +3 -0
- package/dist/scripts/devOnlineBackend.d.ts.map +1 -0
- package/dist/scripts/devOnlineBackend.js +117 -0
- package/dist/scripts/devOnlineBackend.js.map +1 -0
- package/dist/scripts/installApp.d.ts +3 -0
- package/dist/scripts/installApp.d.ts.map +1 -0
- package/dist/scripts/installApp.js +163 -0
- package/dist/scripts/installApp.js.map +1 -0
- package/dist/scripts/listStores.d.ts +3 -0
- package/dist/scripts/listStores.d.ts.map +1 -0
- package/dist/scripts/listStores.js +18 -0
- package/dist/scripts/listStores.js.map +1 -0
- package/dist/scripts/runDocker.d.ts +3 -0
- package/dist/scripts/runDocker.d.ts.map +1 -0
- package/dist/scripts/runDocker.js +88 -0
- package/dist/scripts/runDocker.js.map +1 -0
- package/dist/scripts/runDockerAuth.d.ts +3 -0
- package/dist/scripts/runDockerAuth.d.ts.map +1 -0
- package/dist/scripts/runDockerAuth.js +108 -0
- package/dist/scripts/runDockerAuth.js.map +1 -0
- package/dist/scripts/runDockerOffline.d.ts +3 -0
- package/dist/scripts/runDockerOffline.d.ts.map +1 -0
- package/dist/scripts/runDockerOffline.js +129 -0
- package/dist/scripts/runDockerOffline.js.map +1 -0
- package/dist/scripts/runDockerOfflineExplore.d.ts +3 -0
- package/dist/scripts/runDockerOfflineExplore.d.ts.map +1 -0
- package/dist/scripts/runDockerOfflineExplore.js +116 -0
- package/dist/scripts/runDockerOfflineExplore.js.map +1 -0
- package/dist/scripts/runIsolatedDockerOffline.d.ts +3 -0
- package/dist/scripts/runIsolatedDockerOffline.d.ts.map +1 -0
- package/dist/scripts/runIsolatedDockerOffline.js +351 -0
- package/dist/scripts/runIsolatedDockerOffline.js.map +1 -0
- package/dist/scripts/runOffline.d.ts +3 -0
- package/dist/scripts/runOffline.d.ts.map +1 -0
- package/dist/scripts/runOffline.js +521 -0
- package/dist/scripts/runOffline.js.map +1 -0
- package/dist/scripts/runOfflineE2e.d.ts +3 -0
- package/dist/scripts/runOfflineE2e.d.ts.map +1 -0
- package/dist/scripts/runOfflineE2e.js +408 -0
- package/dist/scripts/runOfflineE2e.js.map +1 -0
- package/dist/scripts/runOfflineFullTests.d.ts +3 -0
- package/dist/scripts/runOfflineFullTests.d.ts.map +1 -0
- package/dist/scripts/runOfflineFullTests.js +1456 -0
- package/dist/scripts/runOfflineFullTests.js.map +1 -0
- package/dist/scripts/runSupermachine.d.ts +3 -0
- package/dist/scripts/runSupermachine.d.ts.map +1 -0
- package/dist/scripts/runSupermachine.js +474 -0
- package/dist/scripts/runSupermachine.js.map +1 -0
- package/dist/scripts/runSupermachineAuth.d.ts +3 -0
- package/dist/scripts/runSupermachineAuth.d.ts.map +1 -0
- package/dist/scripts/runSupermachineAuth.js +454 -0
- package/dist/scripts/runSupermachineAuth.js.map +1 -0
- package/dist/scripts/runTests.d.ts +3 -0
- package/dist/scripts/runTests.d.ts.map +1 -0
- package/dist/scripts/runTests.js +278 -0
- package/dist/scripts/runTests.js.map +1 -0
- package/dist/scripts/runVm.d.ts +3 -0
- package/dist/scripts/runVm.d.ts.map +1 -0
- package/dist/scripts/runVm.js +524 -0
- package/dist/scripts/runVm.js.map +1 -0
- package/dist/scripts/runVmAuth.d.ts +3 -0
- package/dist/scripts/runVmAuth.d.ts.map +1 -0
- package/dist/scripts/runVmAuth.js +475 -0
- package/dist/scripts/runVmAuth.js.map +1 -0
- package/dist/scripts/runVmScript.d.ts +3 -0
- package/dist/scripts/runVmScript.d.ts.map +1 -0
- package/dist/scripts/runVmScript.js +242 -0
- package/dist/scripts/runVmScript.js.map +1 -0
- package/dist/scripts/setupTestDb.d.ts +3 -0
- package/dist/scripts/setupTestDb.d.ts.map +1 -0
- package/dist/scripts/setupTestDb.js +61 -0
- package/dist/scripts/setupTestDb.js.map +1 -0
- package/dist/scripts/verifyContracts.d.ts +3 -0
- package/dist/scripts/verifyContracts.d.ts.map +1 -0
- package/dist/scripts/verifyContracts.js +258 -0
- package/dist/scripts/verifyContracts.js.map +1 -0
- package/dist/scripts/verifyRestContracts.d.ts +3 -0
- package/dist/scripts/verifyRestContracts.d.ts.map +1 -0
- package/dist/scripts/verifyRestContracts.js +237 -0
- package/dist/scripts/verifyRestContracts.js.map +1 -0
- package/dist/vite/offlineConfig.d.ts +34 -0
- package/dist/vite/offlineConfig.d.ts.map +1 -0
- package/dist/vite/offlineConfig.js +61 -0
- package/dist/vite/offlineConfig.js.map +1 -0
- package/dist/vite/onlineConfig.d.ts +42 -0
- package/dist/vite/onlineConfig.d.ts.map +1 -0
- package/dist/vite/onlineConfig.js +56 -0
- package/dist/vite/onlineConfig.js.map +1 -0
- package/docker/Dockerfile +67 -0
- package/docker/Dockerfile.vm +137 -0
- package/docker/README.md +50 -0
- package/docker/entrypoint.sh +198 -0
- package/package.json +85 -0
- package/src/contracts/normalize.ts +96 -0
- package/src/contracts/normalizeHtml.ts +98 -0
- package/src/edge/ca.cnf +14 -0
- package/src/edge/ca.crt +22 -0
- package/src/edge/ca.key +28 -0
- package/src/edge/cert.ts +117 -0
- package/src/edge/edgeProxy.ts +390 -0
- package/src/edge/server.cnf +28 -0
- package/src/edge/server.crt +26 -0
- package/src/edge/server.key +28 -0
- package/src/index.ts +67 -0
- package/src/lib/buildSourceBundle.ts +197 -0
- package/src/lib/freePort.ts +33 -0
- package/src/lib/functionBuild.ts +490 -0
- package/src/lib/neonWsProxy.ts +124 -0
- package/src/lib/sourceZipUpload.ts +168 -0
- package/src/lib/stealthLaunch.ts +57 -0
- package/src/lib/storeAutomation.ts +110 -0
- package/src/playwright/baseConfig.ts +120 -0
- package/src/playwright/globalSetup.ts +179 -0
- package/src/playwright/index.ts +11 -0
- package/src/probes/fonts.ts +279 -0
- package/src/probes/mirror.ts +283 -0
- package/src/probes/runProbe.ts +257 -0
- package/src/probes/types.ts +73 -0
- package/src/scripts/addStore.ts +59 -0
- package/src/scripts/buildDockerImage.ts +66 -0
- package/src/scripts/captureAuth.ts +145 -0
- package/src/scripts/captureContracts.ts +675 -0
- package/src/scripts/captureRestContracts.ts +319 -0
- package/src/scripts/checkOperationCoverage.ts +365 -0
- package/src/scripts/cleanupStores.ts +91 -0
- package/src/scripts/createStores.ts +77 -0
- package/src/scripts/deployAppVersion.ts +692 -0
- package/src/scripts/devOnlineBackend.ts +141 -0
- package/src/scripts/installApp.ts +188 -0
- package/src/scripts/listStores.ts +19 -0
- package/src/scripts/runDockerAuth.ts +120 -0
- package/src/scripts/runOffline.ts +577 -0
- package/src/scripts/runOfflineFullTests.ts +1634 -0
- package/src/scripts/runTests.ts +306 -0
- package/src/scripts/runVm.ts +562 -0
- package/src/scripts/runVmAuth.ts +541 -0
- package/src/scripts/runVmScript.ts +282 -0
- package/src/scripts/setupTestDb.ts +71 -0
- package/src/scripts/verifyContracts.ts +310 -0
- package/src/scripts/verifyRestContracts.ts +275 -0
- package/src/vite/onlineConfig.ts +60 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* captureContracts — bootstrap step for the operation-contract
|
|
4
|
+
* conformance system. See docs/CONTRACTS.md for the full design.
|
|
5
|
+
*
|
|
6
|
+
* What it does:
|
|
7
|
+
* 1. Walk every `.ts/.tsx/.js/.jsx` file under `--glob` (default
|
|
8
|
+
* `./app`).
|
|
9
|
+
* 2. Extract every `#graphql` template literal.
|
|
10
|
+
* 3. Execute each operation against an IN-PROCESS offline mock
|
|
11
|
+
* (Admin GraphQL or Storefront GraphQL, per `--api`).
|
|
12
|
+
* 4. Write one JSON-per-operation under `tests/test-offline/contracts/<api>/`.
|
|
13
|
+
*
|
|
14
|
+
* The captured response is `capturedFrom: "offline"` — a tentative
|
|
15
|
+
* bootstrap. The conformance suite's `verify-contracts-from-live`
|
|
16
|
+
* probe later re-captures from real Shopify; on each successful
|
|
17
|
+
* live-capture the contract is overwritten with `capturedFrom: "live"`
|
|
18
|
+
* — the authoritative ground truth.
|
|
19
|
+
*
|
|
20
|
+
* Variables: an operation with required variables (`ID!`, `String!`,
|
|
21
|
+
* etc.) needs sample values to execute. We auto-synthesise sensible
|
|
22
|
+
* defaults for primitive types (`ID!` → first seeded product GID;
|
|
23
|
+
* `String!` → `"sample"`; `Int!` → `1`; `Boolean!` → `true`). Complex
|
|
24
|
+
* input objects are NOT synthesised — the operation is captured
|
|
25
|
+
* without execution and `response: null` + `error: "needs fixtures"`
|
|
26
|
+
* is recorded. A consuming app can hand-write `tests/test-offline/contracts/
|
|
27
|
+
* fixtures.json` mapping operation names to variables to cover those.
|
|
28
|
+
*
|
|
29
|
+
* Output is idempotent — same input + same offline mock = same
|
|
30
|
+
* contracts. Commit them.
|
|
31
|
+
*/
|
|
32
|
+
import {
|
|
33
|
+
readdirSync,
|
|
34
|
+
readFileSync,
|
|
35
|
+
statSync,
|
|
36
|
+
writeFileSync,
|
|
37
|
+
mkdirSync,
|
|
38
|
+
existsSync,
|
|
39
|
+
} from 'node:fs';
|
|
40
|
+
import { extname, resolve, dirname, basename, relative } from 'node:path';
|
|
41
|
+
import {
|
|
42
|
+
parse as parseGraphql,
|
|
43
|
+
validate as validateGraphql,
|
|
44
|
+
buildSchema,
|
|
45
|
+
typeFromAST,
|
|
46
|
+
isNonNullType,
|
|
47
|
+
isListType,
|
|
48
|
+
isScalarType,
|
|
49
|
+
isEnumType,
|
|
50
|
+
isInputObjectType,
|
|
51
|
+
TypeInfo,
|
|
52
|
+
visit,
|
|
53
|
+
visitWithTypeInfo,
|
|
54
|
+
Kind,
|
|
55
|
+
type GraphQLSchema,
|
|
56
|
+
type DocumentNode,
|
|
57
|
+
type OperationDefinitionNode,
|
|
58
|
+
type TypeNode,
|
|
59
|
+
type GraphQLType,
|
|
60
|
+
} from 'graphql';
|
|
61
|
+
import {
|
|
62
|
+
loadAdminSdl,
|
|
63
|
+
loadStorefrontSdl,
|
|
64
|
+
createAdminApi,
|
|
65
|
+
createStorefrontApi,
|
|
66
|
+
} from '@essential-apps/shopify-test-shopify-api';
|
|
67
|
+
import { ShopState } from '@essential-apps/shopify-test-storefront';
|
|
68
|
+
|
|
69
|
+
type ApiType = 'admin' | 'storefront';
|
|
70
|
+
|
|
71
|
+
interface Args {
|
|
72
|
+
api: ApiType;
|
|
73
|
+
glob: string;
|
|
74
|
+
outDir: string;
|
|
75
|
+
fixturesPath: string;
|
|
76
|
+
cwd: string;
|
|
77
|
+
quiet: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseArgs(): Args {
|
|
81
|
+
const argv = process.argv.slice(2);
|
|
82
|
+
const out: Args = {
|
|
83
|
+
api: 'admin',
|
|
84
|
+
glob: './app',
|
|
85
|
+
outDir: '',
|
|
86
|
+
fixturesPath: '',
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
quiet: false,
|
|
89
|
+
};
|
|
90
|
+
for (let i = 0; i < argv.length; i++) {
|
|
91
|
+
const a = argv[i] ?? '';
|
|
92
|
+
if (a === '--api' && i + 1 < argv.length) {
|
|
93
|
+
const next = argv[++i] ?? '';
|
|
94
|
+
if (next !== 'admin' && next !== 'storefront') {
|
|
95
|
+
console.error(`--api must be "admin" or "storefront"`);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
out.api = next;
|
|
99
|
+
} else if (a === '--glob' && i + 1 < argv.length) {
|
|
100
|
+
out.glob = argv[++i] ?? out.glob;
|
|
101
|
+
} else if (a === '--out' && i + 1 < argv.length) {
|
|
102
|
+
out.outDir = argv[++i] ?? '';
|
|
103
|
+
} else if (a === '--fixtures' && i + 1 < argv.length) {
|
|
104
|
+
out.fixturesPath = argv[++i] ?? '';
|
|
105
|
+
} else if (a === '--quiet') {
|
|
106
|
+
out.quiet = true;
|
|
107
|
+
} else {
|
|
108
|
+
console.error(`unknown arg: ${a}`);
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Default output dir scopes contracts under the consuming app's
|
|
113
|
+
// tests/test-offline/contracts/<api>/ — co-located with the spec files.
|
|
114
|
+
if (!out.outDir) {
|
|
115
|
+
out.outDir = resolve(out.cwd, 'tests/test-offline/contracts', out.api);
|
|
116
|
+
} else {
|
|
117
|
+
out.outDir = resolve(out.cwd, out.outDir);
|
|
118
|
+
}
|
|
119
|
+
if (!out.fixturesPath) {
|
|
120
|
+
out.fixturesPath = resolve(out.cwd, 'tests/test-offline/contracts/fixtures.json');
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Walk a directory tree, yielding paths whose extension is a JS/TS
|
|
127
|
+
* source file. Skips `node_modules`, `dist`, `.git`, `build` — the
|
|
128
|
+
* usual non-source noise.
|
|
129
|
+
*/
|
|
130
|
+
function* walkSources(root: string): Generator<string> {
|
|
131
|
+
let entries: string[];
|
|
132
|
+
try {
|
|
133
|
+
entries = readdirSync(root);
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const name of entries) {
|
|
138
|
+
if (
|
|
139
|
+
name === 'node_modules' ||
|
|
140
|
+
name === 'dist' ||
|
|
141
|
+
name === '.git' ||
|
|
142
|
+
name === 'build'
|
|
143
|
+
)
|
|
144
|
+
continue;
|
|
145
|
+
const full = resolve(root, name);
|
|
146
|
+
let st;
|
|
147
|
+
try {
|
|
148
|
+
st = statSync(full);
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (st.isDirectory()) {
|
|
153
|
+
yield* walkSources(full);
|
|
154
|
+
} else if (st.isFile()) {
|
|
155
|
+
const ext = extname(name);
|
|
156
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx') {
|
|
157
|
+
yield full;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface ExtractedOp {
|
|
164
|
+
file: string;
|
|
165
|
+
line: number;
|
|
166
|
+
source: string;
|
|
167
|
+
/** Operation name extracted from the document (or '__anon_<line>__'). */
|
|
168
|
+
name: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extract every `#graphql ...` template literal from a file. Parses
|
|
173
|
+
* each to discover its operation name (so contracts get filenames
|
|
174
|
+
* that match `getShop` rather than `__anon_42__`).
|
|
175
|
+
*/
|
|
176
|
+
function extractOperations(file: string, content: string): ExtractedOp[] {
|
|
177
|
+
const ops: ExtractedOp[] = [];
|
|
178
|
+
const regex = /`\s*#graphql\b([^`]*)`/g;
|
|
179
|
+
let m: RegExpExecArray | null;
|
|
180
|
+
while ((m = regex.exec(content)) !== null) {
|
|
181
|
+
const line = content.slice(0, m.index).split('\n').length;
|
|
182
|
+
const source = m[1] ?? '';
|
|
183
|
+
const name = deriveOperationName(source, file, line);
|
|
184
|
+
ops.push({ file, line, source, name });
|
|
185
|
+
}
|
|
186
|
+
return ops;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Pick a stable, filesystem-safe slug for an operation. Preference
|
|
191
|
+
* order:
|
|
192
|
+
*
|
|
193
|
+
* 1. The operation's declared name —
|
|
194
|
+
* `query getShop { ... }` → `getShop`.
|
|
195
|
+
* 2. A type+field slug derived from the first top-level selection
|
|
196
|
+
* — `query { shop { name } }` → `anon_query_shop`. Stable across
|
|
197
|
+
* file renames (only the first selected field is in the slug).
|
|
198
|
+
* 3. Filename+line fallback when the document is unparseable.
|
|
199
|
+
*
|
|
200
|
+
* Why anon slugs matter: contracts are committed to the consuming
|
|
201
|
+
* app; the slug is the filename. Anon ops whose slug depends on
|
|
202
|
+
* the file path produce noisy diffs whenever the call site moves.
|
|
203
|
+
* Type+field slugs survive refactors as long as the operation
|
|
204
|
+
* keeps its first selection.
|
|
205
|
+
*/
|
|
206
|
+
function deriveOperationName(source: string, file: string, line: number): string {
|
|
207
|
+
const fileBase =
|
|
208
|
+
file.split('/').pop()?.replace(/[^a-zA-Z0-9_-]/g, '_') ?? 'unknown';
|
|
209
|
+
const fallback = `__anon_${fileBase}_L${line}__`;
|
|
210
|
+
let doc: DocumentNode;
|
|
211
|
+
try {
|
|
212
|
+
doc = parseGraphql(source);
|
|
213
|
+
} catch {
|
|
214
|
+
return fallback;
|
|
215
|
+
}
|
|
216
|
+
const op = doc.definitions.find(
|
|
217
|
+
(d): d is OperationDefinitionNode => d.kind === Kind.OPERATION_DEFINITION,
|
|
218
|
+
);
|
|
219
|
+
if (!op) return fallback;
|
|
220
|
+
if (op.name?.value) return op.name.value;
|
|
221
|
+
// Anonymous — derive from first selection.
|
|
222
|
+
const firstField = op.selectionSet.selections.find(
|
|
223
|
+
(s) => s.kind === Kind.FIELD,
|
|
224
|
+
);
|
|
225
|
+
if (firstField && firstField.kind === Kind.FIELD) {
|
|
226
|
+
const opType = op.operation; // 'query' | 'mutation' | 'subscription'
|
|
227
|
+
const fieldName = firstField.name.value;
|
|
228
|
+
return `anon_${opType}_${fieldName}`;
|
|
229
|
+
}
|
|
230
|
+
return fallback;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Default per-test seed for the offline ShopState. Stable IDs so
|
|
235
|
+
* contracts captured from offline are deterministic across runs.
|
|
236
|
+
* Tests that need different data still seed via their own factories;
|
|
237
|
+
* contracts use this snapshot.
|
|
238
|
+
*/
|
|
239
|
+
function buildSeededState(): ShopState {
|
|
240
|
+
const state = new ShopState({
|
|
241
|
+
shop: {
|
|
242
|
+
domain: 'test-shop.myshopify.com',
|
|
243
|
+
permanent_domain: 'test-shop.myshopify.com',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
state.addProduct({
|
|
247
|
+
id: 900_000_001,
|
|
248
|
+
handle: 'sample-product',
|
|
249
|
+
title: 'Sample Product',
|
|
250
|
+
description: 'Used by contract capture as a deterministic fixture.',
|
|
251
|
+
price: 1000,
|
|
252
|
+
vendor: 'Sample Vendor',
|
|
253
|
+
type: 'Sample',
|
|
254
|
+
variants: [
|
|
255
|
+
{
|
|
256
|
+
id: 900_010_001,
|
|
257
|
+
title: 'Default Title',
|
|
258
|
+
price: 1000,
|
|
259
|
+
available: true,
|
|
260
|
+
sku: 'SAMPLE-1',
|
|
261
|
+
inventory_quantity: 100,
|
|
262
|
+
selected_options: ['Default Title'],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
tags: [],
|
|
266
|
+
});
|
|
267
|
+
state.addCollection({
|
|
268
|
+
id: 900_020_001,
|
|
269
|
+
handle: 'sample-collection',
|
|
270
|
+
title: 'Sample Collection',
|
|
271
|
+
productHandles: ['sample-product'],
|
|
272
|
+
});
|
|
273
|
+
return state;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface SynthesisedVariables {
|
|
277
|
+
values: Record<string, unknown>;
|
|
278
|
+
/** Variables we couldn't synthesise — operation will be marked partial. */
|
|
279
|
+
unhandled: string[];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Synthesise plausible variable values for an operation's variable
|
|
284
|
+
* definitions. Handles ID / String / Int / Float / Boolean scalars
|
|
285
|
+
* and their nullable + list variants. Complex input objects fall
|
|
286
|
+
* through to `unhandled` — the user can override via fixtures.json.
|
|
287
|
+
*/
|
|
288
|
+
function synthesiseVariables(
|
|
289
|
+
op: OperationDefinitionNode,
|
|
290
|
+
schema: GraphQLSchema,
|
|
291
|
+
state: ShopState,
|
|
292
|
+
override: Record<string, unknown>,
|
|
293
|
+
): SynthesisedVariables {
|
|
294
|
+
const values: Record<string, unknown> = {};
|
|
295
|
+
const unhandled: string[] = [];
|
|
296
|
+
for (const def of op.variableDefinitions ?? []) {
|
|
297
|
+
const name = def.variable.name.value;
|
|
298
|
+
if (name in override) {
|
|
299
|
+
values[name] = override[name];
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const synthValue = synthesiseTypeNode(def.type, schema, state);
|
|
303
|
+
if (synthValue === SYNTH_UNHANDLED) {
|
|
304
|
+
unhandled.push(name);
|
|
305
|
+
} else {
|
|
306
|
+
values[name] = synthValue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { values, unhandled };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const SYNTH_UNHANDLED = Symbol('unhandled');
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Convert an AST type-node (from a variable definition) to a
|
|
316
|
+
* resolved `GraphQLType`, then defer to the type-driven synthesiser.
|
|
317
|
+
*
|
|
318
|
+
* The two-step approach (AST → GraphQLType → value) is so that
|
|
319
|
+
* Input objects can be walked by their FIELD definitions, not by
|
|
320
|
+
* raw AST. Field types know exactly what's required vs optional, so
|
|
321
|
+
* we only fill required fields and leave optional ones absent.
|
|
322
|
+
*/
|
|
323
|
+
function synthesiseTypeNode(
|
|
324
|
+
typeNode: TypeNode,
|
|
325
|
+
schema: GraphQLSchema,
|
|
326
|
+
state: ShopState,
|
|
327
|
+
): unknown | typeof SYNTH_UNHANDLED {
|
|
328
|
+
const resolved = typeFromAST(schema, typeNode);
|
|
329
|
+
if (!resolved) return SYNTH_UNHANDLED;
|
|
330
|
+
return synthesiseGraphQLType(resolved, state);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Recursively synthesise a sensible default for a GraphQLType.
|
|
335
|
+
*
|
|
336
|
+
* - NonNull → required; recurse into inner type.
|
|
337
|
+
* - List → empty array (valid for any list).
|
|
338
|
+
* - Scalar → primitive defaults (ID = first seeded product GID,
|
|
339
|
+
* others sensible per name).
|
|
340
|
+
* - Enum → first value of the enum.
|
|
341
|
+
* - InputObject → object with only its required fields filled;
|
|
342
|
+
* optional fields left absent.
|
|
343
|
+
*
|
|
344
|
+
* Returns `SYNTH_UNHANDLED` only for types we genuinely can't
|
|
345
|
+
* produce a value for (custom scalars with non-obvious shapes etc.).
|
|
346
|
+
* Callers should treat that as "user must override via fixtures.json".
|
|
347
|
+
*/
|
|
348
|
+
function synthesiseGraphQLType(
|
|
349
|
+
type: GraphQLType,
|
|
350
|
+
state: ShopState,
|
|
351
|
+
): unknown | typeof SYNTH_UNHANDLED {
|
|
352
|
+
if (isNonNullType(type)) {
|
|
353
|
+
return synthesiseGraphQLType(type.ofType, state);
|
|
354
|
+
}
|
|
355
|
+
if (isListType(type)) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
if (isScalarType(type)) {
|
|
359
|
+
const name = type.name;
|
|
360
|
+
if (name === 'ID') {
|
|
361
|
+
const first = Array.from(state.products.values())[0];
|
|
362
|
+
return first
|
|
363
|
+
? `gid://shopify/Product/${first.id}`
|
|
364
|
+
: 'gid://shopify/Resource/1';
|
|
365
|
+
}
|
|
366
|
+
if (name === 'String') return 'sample';
|
|
367
|
+
if (name === 'Int') return 1;
|
|
368
|
+
if (name === 'Float') return 1.0;
|
|
369
|
+
if (name === 'Boolean') return true;
|
|
370
|
+
// Shopify-specific custom scalars we can stub with a primitive
|
|
371
|
+
// — these all serialise as strings on the wire.
|
|
372
|
+
if (
|
|
373
|
+
name === 'URL' ||
|
|
374
|
+
name === 'DateTime' ||
|
|
375
|
+
name === 'Date' ||
|
|
376
|
+
name === 'Decimal' ||
|
|
377
|
+
name === 'Money' ||
|
|
378
|
+
name === 'HTML' ||
|
|
379
|
+
name === 'JSON' ||
|
|
380
|
+
name === 'JSONString' ||
|
|
381
|
+
name === 'FormattedString' ||
|
|
382
|
+
name === 'StorefrontID' ||
|
|
383
|
+
name === 'UnsignedInt64'
|
|
384
|
+
) {
|
|
385
|
+
if (name === 'URL') return 'https://example.com';
|
|
386
|
+
if (name === 'DateTime') return '2026-01-01T00:00:00Z';
|
|
387
|
+
if (name === 'Date') return '2026-01-01';
|
|
388
|
+
if (name === 'JSON' || name === 'JSONString') return '{}';
|
|
389
|
+
if (name === 'UnsignedInt64') return '1';
|
|
390
|
+
return '1.00'; // Decimal / Money / HTML / FormattedString — primitive default
|
|
391
|
+
}
|
|
392
|
+
return SYNTH_UNHANDLED;
|
|
393
|
+
}
|
|
394
|
+
if (isEnumType(type)) {
|
|
395
|
+
const values = type.getValues();
|
|
396
|
+
return values[0]?.value ?? SYNTH_UNHANDLED;
|
|
397
|
+
}
|
|
398
|
+
if (isInputObjectType(type)) {
|
|
399
|
+
const fields = type.getFields();
|
|
400
|
+
const out: Record<string, unknown> = {};
|
|
401
|
+
let anyRequired = false;
|
|
402
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
403
|
+
// Only fill REQUIRED fields. Optional ones get omitted —
|
|
404
|
+
// keeps the variable payload minimal + valid.
|
|
405
|
+
if (!isNonNullType(field.type)) continue;
|
|
406
|
+
anyRequired = true;
|
|
407
|
+
const value = synthesiseGraphQLType(field.type, state);
|
|
408
|
+
if (value === SYNTH_UNHANDLED) return SYNTH_UNHANDLED;
|
|
409
|
+
out[fieldName] = value;
|
|
410
|
+
}
|
|
411
|
+
// Edge case: input object with NO required fields. Empty object
|
|
412
|
+
// is a valid value.
|
|
413
|
+
return anyRequired ? out : {};
|
|
414
|
+
}
|
|
415
|
+
// Interfaces / Unions / custom scalars not covered above.
|
|
416
|
+
return SYNTH_UNHANDLED;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
interface CapturedContract {
|
|
420
|
+
operationName: string;
|
|
421
|
+
source: string;
|
|
422
|
+
variables: Record<string, unknown>;
|
|
423
|
+
response: unknown;
|
|
424
|
+
capturedFrom: 'offline' | 'live';
|
|
425
|
+
capturedAt: string;
|
|
426
|
+
/** Diagnostic — set when capture didn't run cleanly. */
|
|
427
|
+
warning?: string;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Offline-only executor — POSTs the operation to an in-process
|
|
432
|
+
* Hono mock for the consuming app's API surface (admin or
|
|
433
|
+
* storefront GraphQL).
|
|
434
|
+
*
|
|
435
|
+
* No live target by design: contract capture at the consuming-app
|
|
436
|
+
* layer answers "what does the mock return for my operation?".
|
|
437
|
+
* The separate question — "does the mock match real Shopify on
|
|
438
|
+
* platform primitives?" — is owned by
|
|
439
|
+
* `@essential-apps/shopify-test-conformance` and runs against a
|
|
440
|
+
* canonical operation matrix, not per-app operation sets. An
|
|
441
|
+
* earlier version of this script had a `--target live` mode that
|
|
442
|
+
* mixed the two layers; removed for clarity.
|
|
443
|
+
*/
|
|
444
|
+
type Executor = (
|
|
445
|
+
source: string,
|
|
446
|
+
variables: Record<string, unknown>,
|
|
447
|
+
) => Promise<unknown>;
|
|
448
|
+
|
|
449
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hono types are loose
|
|
450
|
+
function buildOfflineExecutor(api: ApiType, state: ShopState): Executor {
|
|
451
|
+
const app =
|
|
452
|
+
api === 'admin' ? createAdminApi({ state }) : createStorefrontApi({ state });
|
|
453
|
+
const endpoint =
|
|
454
|
+
api === 'admin'
|
|
455
|
+
? '/admin/api/2025-07/graphql.json'
|
|
456
|
+
: '/api/2025-07/graphql.json';
|
|
457
|
+
return async (source, variables) => {
|
|
458
|
+
const resp = await app.request(endpoint, {
|
|
459
|
+
method: 'POST',
|
|
460
|
+
headers: {
|
|
461
|
+
'Content-Type': 'application/json',
|
|
462
|
+
'X-Shopify-Access-Token': 'mock-access-token',
|
|
463
|
+
},
|
|
464
|
+
body: JSON.stringify({ query: source, variables }),
|
|
465
|
+
});
|
|
466
|
+
return resp.json();
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function main(): Promise<void> {
|
|
471
|
+
const args = parseArgs();
|
|
472
|
+
|
|
473
|
+
const sdl = args.api === 'admin' ? loadAdminSdl() : loadStorefrontSdl();
|
|
474
|
+
const schema = buildSchema(sdl);
|
|
475
|
+
|
|
476
|
+
// Load per-app fixtures if present (gives users a way to override
|
|
477
|
+
// synthesised variables for specific operations).
|
|
478
|
+
let fixtures: Record<string, Record<string, unknown>> = {};
|
|
479
|
+
if (existsSync(args.fixturesPath)) {
|
|
480
|
+
fixtures = JSON.parse(readFileSync(args.fixturesPath, 'utf8')) as Record<
|
|
481
|
+
string,
|
|
482
|
+
Record<string, unknown>
|
|
483
|
+
>;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const sourceDir = resolve(args.cwd, args.glob);
|
|
487
|
+
const files = Array.from(walkSources(sourceDir));
|
|
488
|
+
if (files.length === 0) {
|
|
489
|
+
console.error(`[capture-contracts] no source files under ${sourceDir}`);
|
|
490
|
+
process.exit(2);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Collect operations + dedup by source (the same #graphql may
|
|
494
|
+
// appear in multiple files if shared via a util).
|
|
495
|
+
const opsByName = new Map<string, ExtractedOp>();
|
|
496
|
+
for (const f of files) {
|
|
497
|
+
const content = readFileSync(f, 'utf8');
|
|
498
|
+
for (const op of extractOperations(f, content)) {
|
|
499
|
+
const existing = opsByName.get(op.name);
|
|
500
|
+
if (!existing) opsByName.set(op.name, op);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!args.quiet) {
|
|
504
|
+
console.log(
|
|
505
|
+
`[capture-contracts] api=${args.api} ${files.length} file(s) scanned, ${opsByName.size} unique operation(s)`,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const state = buildSeededState();
|
|
510
|
+
const executor: Executor = buildOfflineExecutor(args.api, state);
|
|
511
|
+
|
|
512
|
+
mkdirSync(args.outDir, { recursive: true });
|
|
513
|
+
|
|
514
|
+
let captured = 0;
|
|
515
|
+
let skipped = 0;
|
|
516
|
+
let drift = 0;
|
|
517
|
+
|
|
518
|
+
for (const op of opsByName.values()) {
|
|
519
|
+
let doc: DocumentNode;
|
|
520
|
+
try {
|
|
521
|
+
doc = parseGraphql(op.source);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
writeFileSync(
|
|
524
|
+
resolve(args.outDir, `${op.name}.json`),
|
|
525
|
+
JSON.stringify(
|
|
526
|
+
{
|
|
527
|
+
operationName: op.name,
|
|
528
|
+
source: op.source,
|
|
529
|
+
variables: {},
|
|
530
|
+
response: null,
|
|
531
|
+
capturedFrom: 'offline',
|
|
532
|
+
capturedAt: new Date().toISOString(),
|
|
533
|
+
warning: `parse failed: ${(err as Error).message}`,
|
|
534
|
+
} satisfies CapturedContract,
|
|
535
|
+
null,
|
|
536
|
+
2,
|
|
537
|
+
) + '\n',
|
|
538
|
+
);
|
|
539
|
+
skipped++;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Validate against schema before executing — operations that
|
|
544
|
+
// reference fields the SDL doesn't have are recorded as drift.
|
|
545
|
+
const validationErrors = validateGraphql(schema, doc);
|
|
546
|
+
if (validationErrors.length > 0) {
|
|
547
|
+
writeFileSync(
|
|
548
|
+
resolve(args.outDir, `${op.name}.json`),
|
|
549
|
+
JSON.stringify(
|
|
550
|
+
{
|
|
551
|
+
operationName: op.name,
|
|
552
|
+
source: op.source,
|
|
553
|
+
variables: {},
|
|
554
|
+
response: null,
|
|
555
|
+
capturedFrom: 'offline',
|
|
556
|
+
capturedAt: new Date().toISOString(),
|
|
557
|
+
warning: `schema validation failed: ${validationErrors
|
|
558
|
+
.map((e) => e.message)
|
|
559
|
+
.join(' | ')}`,
|
|
560
|
+
} satisfies CapturedContract,
|
|
561
|
+
null,
|
|
562
|
+
2,
|
|
563
|
+
) + '\n',
|
|
564
|
+
);
|
|
565
|
+
drift++;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Synthesise variables (with fixture overrides).
|
|
570
|
+
const opDef = doc.definitions.find(
|
|
571
|
+
(d): d is OperationDefinitionNode =>
|
|
572
|
+
d.kind === Kind.OPERATION_DEFINITION,
|
|
573
|
+
);
|
|
574
|
+
let variables: Record<string, unknown> = {};
|
|
575
|
+
let warning: string | undefined;
|
|
576
|
+
if (opDef) {
|
|
577
|
+
const synth = synthesiseVariables(
|
|
578
|
+
opDef,
|
|
579
|
+
schema,
|
|
580
|
+
state,
|
|
581
|
+
fixtures[op.name] ?? {},
|
|
582
|
+
);
|
|
583
|
+
variables = synth.values;
|
|
584
|
+
if (synth.unhandled.length > 0) {
|
|
585
|
+
warning =
|
|
586
|
+
`variables not synthesised: ${synth.unhandled.join(', ')}. ` +
|
|
587
|
+
`Add to ${relative(args.cwd, args.fixturesPath)} under "${op.name}" to capture this operation.`;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// If we couldn't fill required vars, record + skip execution.
|
|
592
|
+
if (warning) {
|
|
593
|
+
writeFileSync(
|
|
594
|
+
resolve(args.outDir, `${op.name}.json`),
|
|
595
|
+
JSON.stringify(
|
|
596
|
+
{
|
|
597
|
+
operationName: op.name,
|
|
598
|
+
source: op.source,
|
|
599
|
+
variables,
|
|
600
|
+
response: null,
|
|
601
|
+
capturedFrom: 'offline',
|
|
602
|
+
capturedAt: new Date().toISOString(),
|
|
603
|
+
warning,
|
|
604
|
+
} satisfies CapturedContract,
|
|
605
|
+
null,
|
|
606
|
+
2,
|
|
607
|
+
) + '\n',
|
|
608
|
+
);
|
|
609
|
+
skipped++;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Execute and capture.
|
|
614
|
+
let response: unknown;
|
|
615
|
+
try {
|
|
616
|
+
response = await executor(op.source, variables);
|
|
617
|
+
} catch (err) {
|
|
618
|
+
writeFileSync(
|
|
619
|
+
resolve(args.outDir, `${op.name}.json`),
|
|
620
|
+
JSON.stringify(
|
|
621
|
+
{
|
|
622
|
+
operationName: op.name,
|
|
623
|
+
source: op.source,
|
|
624
|
+
variables,
|
|
625
|
+
response: null,
|
|
626
|
+
capturedFrom: 'offline',
|
|
627
|
+
capturedAt: new Date().toISOString(),
|
|
628
|
+
warning: `execution failed: ${(err as Error).message}`,
|
|
629
|
+
} satisfies CapturedContract,
|
|
630
|
+
null,
|
|
631
|
+
2,
|
|
632
|
+
) + '\n',
|
|
633
|
+
);
|
|
634
|
+
drift++;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
writeFileSync(
|
|
639
|
+
resolve(args.outDir, `${op.name}.json`),
|
|
640
|
+
JSON.stringify(
|
|
641
|
+
{
|
|
642
|
+
operationName: op.name,
|
|
643
|
+
source: op.source,
|
|
644
|
+
variables,
|
|
645
|
+
response,
|
|
646
|
+
capturedFrom: 'offline',
|
|
647
|
+
capturedAt: new Date().toISOString(),
|
|
648
|
+
} satisfies CapturedContract,
|
|
649
|
+
null,
|
|
650
|
+
2,
|
|
651
|
+
) + '\n',
|
|
652
|
+
);
|
|
653
|
+
captured++;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!args.quiet) {
|
|
657
|
+
console.log(
|
|
658
|
+
`[capture-contracts] ${captured} captured, ${skipped} skipped (need fixtures), ${drift} drift. Output: ${relative(args.cwd, args.outDir)}/`,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
main().catch((err) => {
|
|
664
|
+
console.error(err);
|
|
665
|
+
process.exit(2);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Silence the unused-typeinfo warning — the imports are used by the
|
|
669
|
+
// validate / TypeInfo path the script extends in follow-ups.
|
|
670
|
+
void TypeInfo;
|
|
671
|
+
void visit;
|
|
672
|
+
void visitWithTypeInfo;
|
|
673
|
+
void dirname;
|
|
674
|
+
void basename;
|
|
675
|
+
type _UseT = GraphQLType;
|