@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,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Capture Shopify auth state into tests/test-online/.auth/storageState.json.
|
|
4
|
+
*
|
|
5
|
+
* The captured cookies are fingerprint-bound (UA, sec-ch-ua-platform,
|
|
6
|
+
* canvas/WebGL hashes, etc.), so this script MUST run in the same
|
|
7
|
+
* browser environment that tests use:
|
|
8
|
+
* - On host (macOS): Google Chrome via patchright.
|
|
9
|
+
* - In container: Google Chrome via patchright (same channel),
|
|
10
|
+
* under Xvfb. The runDockerAuth.ts wrapper exposes the Xvfb
|
|
11
|
+
* display over VNC so the user can interactively log in.
|
|
12
|
+
*
|
|
13
|
+
* If you re-capture on host but tests run in container (or vice-versa),
|
|
14
|
+
* Cloudflare's bot heuristics will see UA / fingerprint inconsistency
|
|
15
|
+
* and challenge or block the session.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync } from 'node:fs';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
import { createInterface } from 'node:readline';
|
|
21
|
+
import { storageStatePath, assertInVm } from '@essential-apps/shopify-test-core';
|
|
22
|
+
// Auth capture must run INSIDE the VM (via runVmAuth, which sets
|
|
23
|
+
// TEST_IN_CONTAINER + drives this over VNC) — never on the host, or the
|
|
24
|
+
// captured cf_clearance/session is fingerprint-mismatched against the VM.
|
|
25
|
+
assertInVm('capture Shopify auth');
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const chromium = require('patchright').chromium;
|
|
28
|
+
// Test Partner account (throwaway; not prod). Shown in the terminal AND
|
|
29
|
+
// pinned to a bar at the top of the browser so it's readable while
|
|
30
|
+
// logging in over VNC.
|
|
31
|
+
const LOGIN_EMAIL = 'essential-apps-test-1@supercorp.ai';
|
|
32
|
+
const LOGIN_PASSWORD = 'essential-apps-test-1-password';
|
|
33
|
+
// A fixed top bar injected into every page (survives navigation) showing
|
|
34
|
+
// the login creds. `pointer-events:none` so it never blocks the form;
|
|
35
|
+
// re-pinned on an interval because Shopify's accounts pages re-render.
|
|
36
|
+
const credsBarInitScript = `(() => {
|
|
37
|
+
const ID = '__ea_auth_creds_bar__';
|
|
38
|
+
const text = ${JSON.stringify(`TEST LOGIN · ${LOGIN_EMAIL} · password: ${LOGIN_PASSWORD}`)};
|
|
39
|
+
const render = () => {
|
|
40
|
+
if (!document.documentElement) return;
|
|
41
|
+
let b = document.getElementById(ID);
|
|
42
|
+
if (!b) {
|
|
43
|
+
b = document.createElement('div');
|
|
44
|
+
b.id = ID;
|
|
45
|
+
b.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;background:#0a7d3b;color:#fff;font:600 13px/26px ui-monospace,SFMono-Regular,Menlo,monospace;text-align:center;height:26px;padding:0 10px;box-shadow:0 1px 4px rgba(0,0,0,.35);pointer-events:none';
|
|
46
|
+
(document.body || document.documentElement).appendChild(b);
|
|
47
|
+
}
|
|
48
|
+
b.textContent = text;
|
|
49
|
+
};
|
|
50
|
+
render();
|
|
51
|
+
document.addEventListener('DOMContentLoaded', render);
|
|
52
|
+
setInterval(render, 800);
|
|
53
|
+
})();`;
|
|
54
|
+
async function main() {
|
|
55
|
+
console.log('Opening Chrome. Please:');
|
|
56
|
+
console.log(` Log in as: ${LOGIN_EMAIL}`);
|
|
57
|
+
console.log(` Password: ${LOGIN_PASSWORD} (also in the green bar atop the browser)`);
|
|
58
|
+
console.log(' 1. Log in to Shopify Partners.');
|
|
59
|
+
console.log(' 2. Then visit your test store admin (e.g. https://admin.shopify.com/store/<your-store>) —');
|
|
60
|
+
console.log(' if a Cloudflare "Verify you are human" challenge appears, click through it.');
|
|
61
|
+
console.log(' Without this step, tests will hit Turnstile and fail.');
|
|
62
|
+
console.log(' 3. When both are done, return here and press Enter.');
|
|
63
|
+
console.log('');
|
|
64
|
+
// chromium.launch + newContext (matches storePool fixture model).
|
|
65
|
+
// Critically, this is NOT launchPersistentContext — the persistent
|
|
66
|
+
// profile accumulates fingerprint state that triggers Cloudflare's
|
|
67
|
+
// bot detection. Fresh-context model means the browser fingerprint
|
|
68
|
+
// CF sees here (during capture) is the SAME fingerprint it'll see
|
|
69
|
+
// at test time (when a fresh context is built from this saved
|
|
70
|
+
// storageState). cf_clearance is fingerprint-bound, so this
|
|
71
|
+
// consistency is what makes the saved cookie actually work.
|
|
72
|
+
//
|
|
73
|
+
// Patchright (no `channel: 'chrome'`) — uses its bundled
|
|
74
|
+
// chromium with stealth patches. Same browser as storePool +
|
|
75
|
+
// conformance probes, so cf_clearance carries across all three.
|
|
76
|
+
// Real Google Chrome was tried earlier but its fingerprint
|
|
77
|
+
// doesn't match patchright's, causing CF re-challenges.
|
|
78
|
+
const browser = await chromium.launch({
|
|
79
|
+
headless: false,
|
|
80
|
+
args: [
|
|
81
|
+
'--ip-address-space-overrides=127.0.0.1:0=public,[::1]:0=public',
|
|
82
|
+
'--disable-features=LocalNetworkAccessChecks,LocalNetworkAccessChecksWebSockets,LocalNetworkAccessChecksWebTransport,BlockInsecurePrivateNetworkRequests,PrivateNetworkAccessRespectPreflightResults,PrivateNetworkAccessSendPreflights',
|
|
83
|
+
'--local-network-access-permissions-policy-default-enabled',
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
const context = (await browser.newContext({
|
|
87
|
+
viewport: { width: 1400, height: 900 },
|
|
88
|
+
ignoreHTTPSErrors: true,
|
|
89
|
+
}));
|
|
90
|
+
// Pin the creds to a bar at the top of the (VNC'd) browser.
|
|
91
|
+
await context.addInitScript(credsBarInitScript);
|
|
92
|
+
const page = await context.newPage();
|
|
93
|
+
await page.goto('https://accounts.shopify.com/lookup');
|
|
94
|
+
await waitForEnter('Press Enter once logged in AND past any admin Turnstile... ');
|
|
95
|
+
// Sanity-check cf_clearance presence and warn loudly if absent —
|
|
96
|
+
// capture without it works for Partner-API-only flows, but online
|
|
97
|
+
// tests that drive admin.shopify.com will fail on Turnstile.
|
|
98
|
+
const cookies = await context.cookies();
|
|
99
|
+
const hasCfClearance = cookies.some((c) => c.name === 'cf_clearance' && c.domain.includes('shopify.com'));
|
|
100
|
+
if (!hasCfClearance) {
|
|
101
|
+
console.warn('⚠️ No cf_clearance cookie found for *.shopify.com. The online suite will hit Turnstile.');
|
|
102
|
+
console.warn(' Visit your test store admin in this browser, pass the Cloudflare challenge, then re-run.');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log('✓ cf_clearance cookie captured (admin.shopify.com Turnstile bypass).');
|
|
106
|
+
}
|
|
107
|
+
mkdirSync(dirname(storageStatePath), { recursive: true });
|
|
108
|
+
await context.storageState({ path: storageStatePath });
|
|
109
|
+
console.log(`✓ Auth state saved: ${storageStatePath}`);
|
|
110
|
+
await context.close();
|
|
111
|
+
await browser.close();
|
|
112
|
+
}
|
|
113
|
+
function waitForEnter(prompt) {
|
|
114
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
115
|
+
return new Promise((res) => rl.question(prompt, () => {
|
|
116
|
+
rl.close();
|
|
117
|
+
res();
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
main().catch((err) => {
|
|
121
|
+
console.error(err);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=captureAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"captureAuth.js","sourceRoot":"","sources":["../../src/scripts/captureAuth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"captureContracts.d.ts","sourceRoot":"","sources":["../../src/scripts/captureContracts.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,517 @@
|
|
|
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 { readdirSync, readFileSync, statSync, writeFileSync, mkdirSync, existsSync, } from 'node:fs';
|
|
33
|
+
import { extname, resolve, dirname, basename, relative } from 'node:path';
|
|
34
|
+
import { parse as parseGraphql, validate as validateGraphql, buildSchema, typeFromAST, isNonNullType, isListType, isScalarType, isEnumType, isInputObjectType, TypeInfo, visit, visitWithTypeInfo, Kind, } from 'graphql';
|
|
35
|
+
import { loadAdminSdl, loadStorefrontSdl, createAdminApi, createStorefrontApi, } from '@essential-apps/shopify-test-shopify-api';
|
|
36
|
+
import { ShopState } from '@essential-apps/shopify-test-storefront';
|
|
37
|
+
function parseArgs() {
|
|
38
|
+
const argv = process.argv.slice(2);
|
|
39
|
+
const out = {
|
|
40
|
+
api: 'admin',
|
|
41
|
+
glob: './app',
|
|
42
|
+
outDir: '',
|
|
43
|
+
fixturesPath: '',
|
|
44
|
+
cwd: process.cwd(),
|
|
45
|
+
quiet: false,
|
|
46
|
+
};
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const a = argv[i] ?? '';
|
|
49
|
+
if (a === '--api' && i + 1 < argv.length) {
|
|
50
|
+
const next = argv[++i] ?? '';
|
|
51
|
+
if (next !== 'admin' && next !== 'storefront') {
|
|
52
|
+
console.error(`--api must be "admin" or "storefront"`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
out.api = next;
|
|
56
|
+
}
|
|
57
|
+
else if (a === '--glob' && i + 1 < argv.length) {
|
|
58
|
+
out.glob = argv[++i] ?? out.glob;
|
|
59
|
+
}
|
|
60
|
+
else if (a === '--out' && i + 1 < argv.length) {
|
|
61
|
+
out.outDir = argv[++i] ?? '';
|
|
62
|
+
}
|
|
63
|
+
else if (a === '--fixtures' && i + 1 < argv.length) {
|
|
64
|
+
out.fixturesPath = argv[++i] ?? '';
|
|
65
|
+
}
|
|
66
|
+
else if (a === '--quiet') {
|
|
67
|
+
out.quiet = true;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.error(`unknown arg: ${a}`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Default output dir scopes contracts under the consuming app's
|
|
75
|
+
// tests/test-offline/contracts/<api>/ — co-located with the spec files.
|
|
76
|
+
if (!out.outDir) {
|
|
77
|
+
out.outDir = resolve(out.cwd, 'tests/test-offline/contracts', out.api);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
out.outDir = resolve(out.cwd, out.outDir);
|
|
81
|
+
}
|
|
82
|
+
if (!out.fixturesPath) {
|
|
83
|
+
out.fixturesPath = resolve(out.cwd, 'tests/test-offline/contracts/fixtures.json');
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Walk a directory tree, yielding paths whose extension is a JS/TS
|
|
89
|
+
* source file. Skips `node_modules`, `dist`, `.git`, `build` — the
|
|
90
|
+
* usual non-source noise.
|
|
91
|
+
*/
|
|
92
|
+
function* walkSources(root) {
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = readdirSync(root);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const name of entries) {
|
|
101
|
+
if (name === 'node_modules' ||
|
|
102
|
+
name === 'dist' ||
|
|
103
|
+
name === '.git' ||
|
|
104
|
+
name === 'build')
|
|
105
|
+
continue;
|
|
106
|
+
const full = resolve(root, name);
|
|
107
|
+
let st;
|
|
108
|
+
try {
|
|
109
|
+
st = statSync(full);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (st.isDirectory()) {
|
|
115
|
+
yield* walkSources(full);
|
|
116
|
+
}
|
|
117
|
+
else if (st.isFile()) {
|
|
118
|
+
const ext = extname(name);
|
|
119
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx') {
|
|
120
|
+
yield full;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extract every `#graphql ...` template literal from a file. Parses
|
|
127
|
+
* each to discover its operation name (so contracts get filenames
|
|
128
|
+
* that match `getShop` rather than `__anon_42__`).
|
|
129
|
+
*/
|
|
130
|
+
function extractOperations(file, content) {
|
|
131
|
+
const ops = [];
|
|
132
|
+
const regex = /`\s*#graphql\b([^`]*)`/g;
|
|
133
|
+
let m;
|
|
134
|
+
while ((m = regex.exec(content)) !== null) {
|
|
135
|
+
const line = content.slice(0, m.index).split('\n').length;
|
|
136
|
+
const source = m[1] ?? '';
|
|
137
|
+
const name = deriveOperationName(source, file, line);
|
|
138
|
+
ops.push({ file, line, source, name });
|
|
139
|
+
}
|
|
140
|
+
return ops;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Pick a stable, filesystem-safe slug for an operation. Preference
|
|
144
|
+
* order:
|
|
145
|
+
*
|
|
146
|
+
* 1. The operation's declared name —
|
|
147
|
+
* `query getShop { ... }` → `getShop`.
|
|
148
|
+
* 2. A type+field slug derived from the first top-level selection
|
|
149
|
+
* — `query { shop { name } }` → `anon_query_shop`. Stable across
|
|
150
|
+
* file renames (only the first selected field is in the slug).
|
|
151
|
+
* 3. Filename+line fallback when the document is unparseable.
|
|
152
|
+
*
|
|
153
|
+
* Why anon slugs matter: contracts are committed to the consuming
|
|
154
|
+
* app; the slug is the filename. Anon ops whose slug depends on
|
|
155
|
+
* the file path produce noisy diffs whenever the call site moves.
|
|
156
|
+
* Type+field slugs survive refactors as long as the operation
|
|
157
|
+
* keeps its first selection.
|
|
158
|
+
*/
|
|
159
|
+
function deriveOperationName(source, file, line) {
|
|
160
|
+
const fileBase = file.split('/').pop()?.replace(/[^a-zA-Z0-9_-]/g, '_') ?? 'unknown';
|
|
161
|
+
const fallback = `__anon_${fileBase}_L${line}__`;
|
|
162
|
+
let doc;
|
|
163
|
+
try {
|
|
164
|
+
doc = parseGraphql(source);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return fallback;
|
|
168
|
+
}
|
|
169
|
+
const op = doc.definitions.find((d) => d.kind === Kind.OPERATION_DEFINITION);
|
|
170
|
+
if (!op)
|
|
171
|
+
return fallback;
|
|
172
|
+
if (op.name?.value)
|
|
173
|
+
return op.name.value;
|
|
174
|
+
// Anonymous — derive from first selection.
|
|
175
|
+
const firstField = op.selectionSet.selections.find((s) => s.kind === Kind.FIELD);
|
|
176
|
+
if (firstField && firstField.kind === Kind.FIELD) {
|
|
177
|
+
const opType = op.operation; // 'query' | 'mutation' | 'subscription'
|
|
178
|
+
const fieldName = firstField.name.value;
|
|
179
|
+
return `anon_${opType}_${fieldName}`;
|
|
180
|
+
}
|
|
181
|
+
return fallback;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Default per-test seed for the offline ShopState. Stable IDs so
|
|
185
|
+
* contracts captured from offline are deterministic across runs.
|
|
186
|
+
* Tests that need different data still seed via their own factories;
|
|
187
|
+
* contracts use this snapshot.
|
|
188
|
+
*/
|
|
189
|
+
function buildSeededState() {
|
|
190
|
+
const state = new ShopState({
|
|
191
|
+
shop: {
|
|
192
|
+
domain: 'test-shop.myshopify.com',
|
|
193
|
+
permanent_domain: 'test-shop.myshopify.com',
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
state.addProduct({
|
|
197
|
+
id: 900_000_001,
|
|
198
|
+
handle: 'sample-product',
|
|
199
|
+
title: 'Sample Product',
|
|
200
|
+
description: 'Used by contract capture as a deterministic fixture.',
|
|
201
|
+
price: 1000,
|
|
202
|
+
vendor: 'Sample Vendor',
|
|
203
|
+
type: 'Sample',
|
|
204
|
+
variants: [
|
|
205
|
+
{
|
|
206
|
+
id: 900_010_001,
|
|
207
|
+
title: 'Default Title',
|
|
208
|
+
price: 1000,
|
|
209
|
+
available: true,
|
|
210
|
+
sku: 'SAMPLE-1',
|
|
211
|
+
inventory_quantity: 100,
|
|
212
|
+
selected_options: ['Default Title'],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
tags: [],
|
|
216
|
+
});
|
|
217
|
+
state.addCollection({
|
|
218
|
+
id: 900_020_001,
|
|
219
|
+
handle: 'sample-collection',
|
|
220
|
+
title: 'Sample Collection',
|
|
221
|
+
productHandles: ['sample-product'],
|
|
222
|
+
});
|
|
223
|
+
return state;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Synthesise plausible variable values for an operation's variable
|
|
227
|
+
* definitions. Handles ID / String / Int / Float / Boolean scalars
|
|
228
|
+
* and their nullable + list variants. Complex input objects fall
|
|
229
|
+
* through to `unhandled` — the user can override via fixtures.json.
|
|
230
|
+
*/
|
|
231
|
+
function synthesiseVariables(op, schema, state, override) {
|
|
232
|
+
const values = {};
|
|
233
|
+
const unhandled = [];
|
|
234
|
+
for (const def of op.variableDefinitions ?? []) {
|
|
235
|
+
const name = def.variable.name.value;
|
|
236
|
+
if (name in override) {
|
|
237
|
+
values[name] = override[name];
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const synthValue = synthesiseTypeNode(def.type, schema, state);
|
|
241
|
+
if (synthValue === SYNTH_UNHANDLED) {
|
|
242
|
+
unhandled.push(name);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
values[name] = synthValue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { values, unhandled };
|
|
249
|
+
}
|
|
250
|
+
const SYNTH_UNHANDLED = Symbol('unhandled');
|
|
251
|
+
/**
|
|
252
|
+
* Convert an AST type-node (from a variable definition) to a
|
|
253
|
+
* resolved `GraphQLType`, then defer to the type-driven synthesiser.
|
|
254
|
+
*
|
|
255
|
+
* The two-step approach (AST → GraphQLType → value) is so that
|
|
256
|
+
* Input objects can be walked by their FIELD definitions, not by
|
|
257
|
+
* raw AST. Field types know exactly what's required vs optional, so
|
|
258
|
+
* we only fill required fields and leave optional ones absent.
|
|
259
|
+
*/
|
|
260
|
+
function synthesiseTypeNode(typeNode, schema, state) {
|
|
261
|
+
const resolved = typeFromAST(schema, typeNode);
|
|
262
|
+
if (!resolved)
|
|
263
|
+
return SYNTH_UNHANDLED;
|
|
264
|
+
return synthesiseGraphQLType(resolved, state);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Recursively synthesise a sensible default for a GraphQLType.
|
|
268
|
+
*
|
|
269
|
+
* - NonNull → required; recurse into inner type.
|
|
270
|
+
* - List → empty array (valid for any list).
|
|
271
|
+
* - Scalar → primitive defaults (ID = first seeded product GID,
|
|
272
|
+
* others sensible per name).
|
|
273
|
+
* - Enum → first value of the enum.
|
|
274
|
+
* - InputObject → object with only its required fields filled;
|
|
275
|
+
* optional fields left absent.
|
|
276
|
+
*
|
|
277
|
+
* Returns `SYNTH_UNHANDLED` only for types we genuinely can't
|
|
278
|
+
* produce a value for (custom scalars with non-obvious shapes etc.).
|
|
279
|
+
* Callers should treat that as "user must override via fixtures.json".
|
|
280
|
+
*/
|
|
281
|
+
function synthesiseGraphQLType(type, state) {
|
|
282
|
+
if (isNonNullType(type)) {
|
|
283
|
+
return synthesiseGraphQLType(type.ofType, state);
|
|
284
|
+
}
|
|
285
|
+
if (isListType(type)) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
if (isScalarType(type)) {
|
|
289
|
+
const name = type.name;
|
|
290
|
+
if (name === 'ID') {
|
|
291
|
+
const first = Array.from(state.products.values())[0];
|
|
292
|
+
return first
|
|
293
|
+
? `gid://shopify/Product/${first.id}`
|
|
294
|
+
: 'gid://shopify/Resource/1';
|
|
295
|
+
}
|
|
296
|
+
if (name === 'String')
|
|
297
|
+
return 'sample';
|
|
298
|
+
if (name === 'Int')
|
|
299
|
+
return 1;
|
|
300
|
+
if (name === 'Float')
|
|
301
|
+
return 1.0;
|
|
302
|
+
if (name === 'Boolean')
|
|
303
|
+
return true;
|
|
304
|
+
// Shopify-specific custom scalars we can stub with a primitive
|
|
305
|
+
// — these all serialise as strings on the wire.
|
|
306
|
+
if (name === 'URL' ||
|
|
307
|
+
name === 'DateTime' ||
|
|
308
|
+
name === 'Date' ||
|
|
309
|
+
name === 'Decimal' ||
|
|
310
|
+
name === 'Money' ||
|
|
311
|
+
name === 'HTML' ||
|
|
312
|
+
name === 'JSON' ||
|
|
313
|
+
name === 'JSONString' ||
|
|
314
|
+
name === 'FormattedString' ||
|
|
315
|
+
name === 'StorefrontID' ||
|
|
316
|
+
name === 'UnsignedInt64') {
|
|
317
|
+
if (name === 'URL')
|
|
318
|
+
return 'https://example.com';
|
|
319
|
+
if (name === 'DateTime')
|
|
320
|
+
return '2026-01-01T00:00:00Z';
|
|
321
|
+
if (name === 'Date')
|
|
322
|
+
return '2026-01-01';
|
|
323
|
+
if (name === 'JSON' || name === 'JSONString')
|
|
324
|
+
return '{}';
|
|
325
|
+
if (name === 'UnsignedInt64')
|
|
326
|
+
return '1';
|
|
327
|
+
return '1.00'; // Decimal / Money / HTML / FormattedString — primitive default
|
|
328
|
+
}
|
|
329
|
+
return SYNTH_UNHANDLED;
|
|
330
|
+
}
|
|
331
|
+
if (isEnumType(type)) {
|
|
332
|
+
const values = type.getValues();
|
|
333
|
+
return values[0]?.value ?? SYNTH_UNHANDLED;
|
|
334
|
+
}
|
|
335
|
+
if (isInputObjectType(type)) {
|
|
336
|
+
const fields = type.getFields();
|
|
337
|
+
const out = {};
|
|
338
|
+
let anyRequired = false;
|
|
339
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
340
|
+
// Only fill REQUIRED fields. Optional ones get omitted —
|
|
341
|
+
// keeps the variable payload minimal + valid.
|
|
342
|
+
if (!isNonNullType(field.type))
|
|
343
|
+
continue;
|
|
344
|
+
anyRequired = true;
|
|
345
|
+
const value = synthesiseGraphQLType(field.type, state);
|
|
346
|
+
if (value === SYNTH_UNHANDLED)
|
|
347
|
+
return SYNTH_UNHANDLED;
|
|
348
|
+
out[fieldName] = value;
|
|
349
|
+
}
|
|
350
|
+
// Edge case: input object with NO required fields. Empty object
|
|
351
|
+
// is a valid value.
|
|
352
|
+
return anyRequired ? out : {};
|
|
353
|
+
}
|
|
354
|
+
// Interfaces / Unions / custom scalars not covered above.
|
|
355
|
+
return SYNTH_UNHANDLED;
|
|
356
|
+
}
|
|
357
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hono types are loose
|
|
358
|
+
function buildOfflineExecutor(api, state) {
|
|
359
|
+
const app = api === 'admin' ? createAdminApi({ state }) : createStorefrontApi({ state });
|
|
360
|
+
const endpoint = api === 'admin'
|
|
361
|
+
? '/admin/api/2025-07/graphql.json'
|
|
362
|
+
: '/api/2025-07/graphql.json';
|
|
363
|
+
return async (source, variables) => {
|
|
364
|
+
const resp = await app.request(endpoint, {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
headers: {
|
|
367
|
+
'Content-Type': 'application/json',
|
|
368
|
+
'X-Shopify-Access-Token': 'mock-access-token',
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify({ query: source, variables }),
|
|
371
|
+
});
|
|
372
|
+
return resp.json();
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function main() {
|
|
376
|
+
const args = parseArgs();
|
|
377
|
+
const sdl = args.api === 'admin' ? loadAdminSdl() : loadStorefrontSdl();
|
|
378
|
+
const schema = buildSchema(sdl);
|
|
379
|
+
// Load per-app fixtures if present (gives users a way to override
|
|
380
|
+
// synthesised variables for specific operations).
|
|
381
|
+
let fixtures = {};
|
|
382
|
+
if (existsSync(args.fixturesPath)) {
|
|
383
|
+
fixtures = JSON.parse(readFileSync(args.fixturesPath, 'utf8'));
|
|
384
|
+
}
|
|
385
|
+
const sourceDir = resolve(args.cwd, args.glob);
|
|
386
|
+
const files = Array.from(walkSources(sourceDir));
|
|
387
|
+
if (files.length === 0) {
|
|
388
|
+
console.error(`[capture-contracts] no source files under ${sourceDir}`);
|
|
389
|
+
process.exit(2);
|
|
390
|
+
}
|
|
391
|
+
// Collect operations + dedup by source (the same #graphql may
|
|
392
|
+
// appear in multiple files if shared via a util).
|
|
393
|
+
const opsByName = new Map();
|
|
394
|
+
for (const f of files) {
|
|
395
|
+
const content = readFileSync(f, 'utf8');
|
|
396
|
+
for (const op of extractOperations(f, content)) {
|
|
397
|
+
const existing = opsByName.get(op.name);
|
|
398
|
+
if (!existing)
|
|
399
|
+
opsByName.set(op.name, op);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!args.quiet) {
|
|
403
|
+
console.log(`[capture-contracts] api=${args.api} ${files.length} file(s) scanned, ${opsByName.size} unique operation(s)`);
|
|
404
|
+
}
|
|
405
|
+
const state = buildSeededState();
|
|
406
|
+
const executor = buildOfflineExecutor(args.api, state);
|
|
407
|
+
mkdirSync(args.outDir, { recursive: true });
|
|
408
|
+
let captured = 0;
|
|
409
|
+
let skipped = 0;
|
|
410
|
+
let drift = 0;
|
|
411
|
+
for (const op of opsByName.values()) {
|
|
412
|
+
let doc;
|
|
413
|
+
try {
|
|
414
|
+
doc = parseGraphql(op.source);
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
writeFileSync(resolve(args.outDir, `${op.name}.json`), JSON.stringify({
|
|
418
|
+
operationName: op.name,
|
|
419
|
+
source: op.source,
|
|
420
|
+
variables: {},
|
|
421
|
+
response: null,
|
|
422
|
+
capturedFrom: 'offline',
|
|
423
|
+
capturedAt: new Date().toISOString(),
|
|
424
|
+
warning: `parse failed: ${err.message}`,
|
|
425
|
+
}, null, 2) + '\n');
|
|
426
|
+
skipped++;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// Validate against schema before executing — operations that
|
|
430
|
+
// reference fields the SDL doesn't have are recorded as drift.
|
|
431
|
+
const validationErrors = validateGraphql(schema, doc);
|
|
432
|
+
if (validationErrors.length > 0) {
|
|
433
|
+
writeFileSync(resolve(args.outDir, `${op.name}.json`), JSON.stringify({
|
|
434
|
+
operationName: op.name,
|
|
435
|
+
source: op.source,
|
|
436
|
+
variables: {},
|
|
437
|
+
response: null,
|
|
438
|
+
capturedFrom: 'offline',
|
|
439
|
+
capturedAt: new Date().toISOString(),
|
|
440
|
+
warning: `schema validation failed: ${validationErrors
|
|
441
|
+
.map((e) => e.message)
|
|
442
|
+
.join(' | ')}`,
|
|
443
|
+
}, null, 2) + '\n');
|
|
444
|
+
drift++;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
// Synthesise variables (with fixture overrides).
|
|
448
|
+
const opDef = doc.definitions.find((d) => d.kind === Kind.OPERATION_DEFINITION);
|
|
449
|
+
let variables = {};
|
|
450
|
+
let warning;
|
|
451
|
+
if (opDef) {
|
|
452
|
+
const synth = synthesiseVariables(opDef, schema, state, fixtures[op.name] ?? {});
|
|
453
|
+
variables = synth.values;
|
|
454
|
+
if (synth.unhandled.length > 0) {
|
|
455
|
+
warning =
|
|
456
|
+
`variables not synthesised: ${synth.unhandled.join(', ')}. ` +
|
|
457
|
+
`Add to ${relative(args.cwd, args.fixturesPath)} under "${op.name}" to capture this operation.`;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// If we couldn't fill required vars, record + skip execution.
|
|
461
|
+
if (warning) {
|
|
462
|
+
writeFileSync(resolve(args.outDir, `${op.name}.json`), JSON.stringify({
|
|
463
|
+
operationName: op.name,
|
|
464
|
+
source: op.source,
|
|
465
|
+
variables,
|
|
466
|
+
response: null,
|
|
467
|
+
capturedFrom: 'offline',
|
|
468
|
+
capturedAt: new Date().toISOString(),
|
|
469
|
+
warning,
|
|
470
|
+
}, null, 2) + '\n');
|
|
471
|
+
skipped++;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
// Execute and capture.
|
|
475
|
+
let response;
|
|
476
|
+
try {
|
|
477
|
+
response = await executor(op.source, variables);
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
writeFileSync(resolve(args.outDir, `${op.name}.json`), JSON.stringify({
|
|
481
|
+
operationName: op.name,
|
|
482
|
+
source: op.source,
|
|
483
|
+
variables,
|
|
484
|
+
response: null,
|
|
485
|
+
capturedFrom: 'offline',
|
|
486
|
+
capturedAt: new Date().toISOString(),
|
|
487
|
+
warning: `execution failed: ${err.message}`,
|
|
488
|
+
}, null, 2) + '\n');
|
|
489
|
+
drift++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
writeFileSync(resolve(args.outDir, `${op.name}.json`), JSON.stringify({
|
|
493
|
+
operationName: op.name,
|
|
494
|
+
source: op.source,
|
|
495
|
+
variables,
|
|
496
|
+
response,
|
|
497
|
+
capturedFrom: 'offline',
|
|
498
|
+
capturedAt: new Date().toISOString(),
|
|
499
|
+
}, null, 2) + '\n');
|
|
500
|
+
captured++;
|
|
501
|
+
}
|
|
502
|
+
if (!args.quiet) {
|
|
503
|
+
console.log(`[capture-contracts] ${captured} captured, ${skipped} skipped (need fixtures), ${drift} drift. Output: ${relative(args.cwd, args.outDir)}/`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
main().catch((err) => {
|
|
507
|
+
console.error(err);
|
|
508
|
+
process.exit(2);
|
|
509
|
+
});
|
|
510
|
+
// Silence the unused-typeinfo warning — the imports are used by the
|
|
511
|
+
// validate / TypeInfo path the script extends in follow-ups.
|
|
512
|
+
void TypeInfo;
|
|
513
|
+
void visit;
|
|
514
|
+
void visitWithTypeInfo;
|
|
515
|
+
void dirname;
|
|
516
|
+
void basename;
|
|
517
|
+
//# sourceMappingURL=captureContracts.js.map
|