@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.
Files changed (265) hide show
  1. package/dist/contracts/normalize.d.ts +61 -0
  2. package/dist/contracts/normalize.d.ts.map +1 -0
  3. package/dist/contracts/normalize.js +99 -0
  4. package/dist/contracts/normalize.js.map +1 -0
  5. package/dist/contracts/normalizeHtml.d.ts +37 -0
  6. package/dist/contracts/normalizeHtml.d.ts.map +1 -0
  7. package/dist/contracts/normalizeHtml.js +89 -0
  8. package/dist/contracts/normalizeHtml.js.map +1 -0
  9. package/dist/edge/cert.d.ts +44 -0
  10. package/dist/edge/cert.d.ts.map +1 -0
  11. package/dist/edge/cert.js +117 -0
  12. package/dist/edge/cert.js.map +1 -0
  13. package/dist/edge/edgeProxy.d.ts +43 -0
  14. package/dist/edge/edgeProxy.d.ts.map +1 -0
  15. package/dist/edge/edgeProxy.js +297 -0
  16. package/dist/edge/edgeProxy.js.map +1 -0
  17. package/dist/edge/nodeShim.d.ts +2 -0
  18. package/dist/edge/nodeShim.d.ts.map +1 -0
  19. package/dist/edge/nodeShim.js +217 -0
  20. package/dist/edge/nodeShim.js.map +1 -0
  21. package/dist/index.d.ts +39 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +36 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lib/buildSourceBundle.d.ts +56 -0
  26. package/dist/lib/buildSourceBundle.d.ts.map +1 -0
  27. package/dist/lib/buildSourceBundle.js +153 -0
  28. package/dist/lib/buildSourceBundle.js.map +1 -0
  29. package/dist/lib/freePort.d.ts +5 -0
  30. package/dist/lib/freePort.d.ts.map +1 -0
  31. package/dist/lib/freePort.js +33 -0
  32. package/dist/lib/freePort.js.map +1 -0
  33. package/dist/lib/functionBuild.d.ts +99 -0
  34. package/dist/lib/functionBuild.d.ts.map +1 -0
  35. package/dist/lib/functionBuild.js +413 -0
  36. package/dist/lib/functionBuild.js.map +1 -0
  37. package/dist/lib/neonWsProxy.d.ts +41 -0
  38. package/dist/lib/neonWsProxy.d.ts.map +1 -0
  39. package/dist/lib/neonWsProxy.js +101 -0
  40. package/dist/lib/neonWsProxy.js.map +1 -0
  41. package/dist/lib/sourceZipUpload.d.ts +45 -0
  42. package/dist/lib/sourceZipUpload.d.ts.map +1 -0
  43. package/dist/lib/sourceZipUpload.js +129 -0
  44. package/dist/lib/sourceZipUpload.js.map +1 -0
  45. package/dist/lib/stealthLaunch.d.ts +35 -0
  46. package/dist/lib/stealthLaunch.d.ts.map +1 -0
  47. package/dist/lib/stealthLaunch.js +46 -0
  48. package/dist/lib/stealthLaunch.js.map +1 -0
  49. package/dist/lib/storeAutomation.d.ts +22 -0
  50. package/dist/lib/storeAutomation.d.ts.map +1 -0
  51. package/dist/lib/storeAutomation.js +85 -0
  52. package/dist/lib/storeAutomation.js.map +1 -0
  53. package/dist/playwright/baseConfig.d.ts +62 -0
  54. package/dist/playwright/baseConfig.d.ts.map +1 -0
  55. package/dist/playwright/baseConfig.js +68 -0
  56. package/dist/playwright/baseConfig.js.map +1 -0
  57. package/dist/playwright/globalSetup.d.ts +2 -0
  58. package/dist/playwright/globalSetup.d.ts.map +1 -0
  59. package/dist/playwright/globalSetup.js +139 -0
  60. package/dist/playwright/globalSetup.js.map +1 -0
  61. package/dist/playwright/index.d.ts +9 -0
  62. package/dist/playwright/index.d.ts.map +1 -0
  63. package/dist/playwright/index.js +9 -0
  64. package/dist/playwright/index.js.map +1 -0
  65. package/dist/probes/fonts.d.ts +4 -0
  66. package/dist/probes/fonts.d.ts.map +1 -0
  67. package/dist/probes/fonts.js +255 -0
  68. package/dist/probes/fonts.js.map +1 -0
  69. package/dist/probes/mirror.d.ts +4 -0
  70. package/dist/probes/mirror.d.ts.map +1 -0
  71. package/dist/probes/mirror.js +260 -0
  72. package/dist/probes/mirror.js.map +1 -0
  73. package/dist/probes/runProbe.d.ts +3 -0
  74. package/dist/probes/runProbe.d.ts.map +1 -0
  75. package/dist/probes/runProbe.js +219 -0
  76. package/dist/probes/runProbe.js.map +1 -0
  77. package/dist/probes/types.d.ts +72 -0
  78. package/dist/probes/types.d.ts.map +1 -0
  79. package/dist/probes/types.js +2 -0
  80. package/dist/probes/types.js.map +1 -0
  81. package/dist/scripts/_probeSourceUrl.d.ts +3 -0
  82. package/dist/scripts/_probeSourceUrl.d.ts.map +1 -0
  83. package/dist/scripts/_probeSourceUrl.js +119 -0
  84. package/dist/scripts/_probeSourceUrl.js.map +1 -0
  85. package/dist/scripts/addStore.d.ts +3 -0
  86. package/dist/scripts/addStore.d.ts.map +1 -0
  87. package/dist/scripts/addStore.js +46 -0
  88. package/dist/scripts/addStore.js.map +1 -0
  89. package/dist/scripts/buildDockerImage.d.ts +3 -0
  90. package/dist/scripts/buildDockerImage.d.ts.map +1 -0
  91. package/dist/scripts/buildDockerImage.js +60 -0
  92. package/dist/scripts/buildDockerImage.js.map +1 -0
  93. package/dist/scripts/captureAuth.d.ts +3 -0
  94. package/dist/scripts/captureAuth.d.ts.map +1 -0
  95. package/dist/scripts/captureAuth.js +124 -0
  96. package/dist/scripts/captureAuth.js.map +1 -0
  97. package/dist/scripts/captureContracts.d.ts +3 -0
  98. package/dist/scripts/captureContracts.d.ts.map +1 -0
  99. package/dist/scripts/captureContracts.js +517 -0
  100. package/dist/scripts/captureContracts.js.map +1 -0
  101. package/dist/scripts/captureRestContracts.d.ts +3 -0
  102. package/dist/scripts/captureRestContracts.d.ts.map +1 -0
  103. package/dist/scripts/captureRestContracts.js +245 -0
  104. package/dist/scripts/captureRestContracts.js.map +1 -0
  105. package/dist/scripts/checkOperationCoverage.d.ts +3 -0
  106. package/dist/scripts/checkOperationCoverage.d.ts.map +1 -0
  107. package/dist/scripts/checkOperationCoverage.js +302 -0
  108. package/dist/scripts/checkOperationCoverage.js.map +1 -0
  109. package/dist/scripts/cleanupStores.d.ts +3 -0
  110. package/dist/scripts/cleanupStores.d.ts.map +1 -0
  111. package/dist/scripts/cleanupStores.js +77 -0
  112. package/dist/scripts/cleanupStores.js.map +1 -0
  113. package/dist/scripts/createStores.d.ts +3 -0
  114. package/dist/scripts/createStores.d.ts.map +1 -0
  115. package/dist/scripts/createStores.js +66 -0
  116. package/dist/scripts/createStores.js.map +1 -0
  117. package/dist/scripts/deployAppVersion.d.ts +3 -0
  118. package/dist/scripts/deployAppVersion.d.ts.map +1 -0
  119. package/dist/scripts/deployAppVersion.js +591 -0
  120. package/dist/scripts/deployAppVersion.js.map +1 -0
  121. package/dist/scripts/devE2eBackend.d.ts +3 -0
  122. package/dist/scripts/devE2eBackend.d.ts.map +1 -0
  123. package/dist/scripts/devE2eBackend.js +117 -0
  124. package/dist/scripts/devE2eBackend.js.map +1 -0
  125. package/dist/scripts/devOnlineBackend.d.ts +3 -0
  126. package/dist/scripts/devOnlineBackend.d.ts.map +1 -0
  127. package/dist/scripts/devOnlineBackend.js +117 -0
  128. package/dist/scripts/devOnlineBackend.js.map +1 -0
  129. package/dist/scripts/installApp.d.ts +3 -0
  130. package/dist/scripts/installApp.d.ts.map +1 -0
  131. package/dist/scripts/installApp.js +163 -0
  132. package/dist/scripts/installApp.js.map +1 -0
  133. package/dist/scripts/listStores.d.ts +3 -0
  134. package/dist/scripts/listStores.d.ts.map +1 -0
  135. package/dist/scripts/listStores.js +18 -0
  136. package/dist/scripts/listStores.js.map +1 -0
  137. package/dist/scripts/runDocker.d.ts +3 -0
  138. package/dist/scripts/runDocker.d.ts.map +1 -0
  139. package/dist/scripts/runDocker.js +88 -0
  140. package/dist/scripts/runDocker.js.map +1 -0
  141. package/dist/scripts/runDockerAuth.d.ts +3 -0
  142. package/dist/scripts/runDockerAuth.d.ts.map +1 -0
  143. package/dist/scripts/runDockerAuth.js +108 -0
  144. package/dist/scripts/runDockerAuth.js.map +1 -0
  145. package/dist/scripts/runDockerOffline.d.ts +3 -0
  146. package/dist/scripts/runDockerOffline.d.ts.map +1 -0
  147. package/dist/scripts/runDockerOffline.js +129 -0
  148. package/dist/scripts/runDockerOffline.js.map +1 -0
  149. package/dist/scripts/runDockerOfflineExplore.d.ts +3 -0
  150. package/dist/scripts/runDockerOfflineExplore.d.ts.map +1 -0
  151. package/dist/scripts/runDockerOfflineExplore.js +116 -0
  152. package/dist/scripts/runDockerOfflineExplore.js.map +1 -0
  153. package/dist/scripts/runIsolatedDockerOffline.d.ts +3 -0
  154. package/dist/scripts/runIsolatedDockerOffline.d.ts.map +1 -0
  155. package/dist/scripts/runIsolatedDockerOffline.js +351 -0
  156. package/dist/scripts/runIsolatedDockerOffline.js.map +1 -0
  157. package/dist/scripts/runOffline.d.ts +3 -0
  158. package/dist/scripts/runOffline.d.ts.map +1 -0
  159. package/dist/scripts/runOffline.js +521 -0
  160. package/dist/scripts/runOffline.js.map +1 -0
  161. package/dist/scripts/runOfflineE2e.d.ts +3 -0
  162. package/dist/scripts/runOfflineE2e.d.ts.map +1 -0
  163. package/dist/scripts/runOfflineE2e.js +408 -0
  164. package/dist/scripts/runOfflineE2e.js.map +1 -0
  165. package/dist/scripts/runOfflineFullTests.d.ts +3 -0
  166. package/dist/scripts/runOfflineFullTests.d.ts.map +1 -0
  167. package/dist/scripts/runOfflineFullTests.js +1456 -0
  168. package/dist/scripts/runOfflineFullTests.js.map +1 -0
  169. package/dist/scripts/runSupermachine.d.ts +3 -0
  170. package/dist/scripts/runSupermachine.d.ts.map +1 -0
  171. package/dist/scripts/runSupermachine.js +474 -0
  172. package/dist/scripts/runSupermachine.js.map +1 -0
  173. package/dist/scripts/runSupermachineAuth.d.ts +3 -0
  174. package/dist/scripts/runSupermachineAuth.d.ts.map +1 -0
  175. package/dist/scripts/runSupermachineAuth.js +454 -0
  176. package/dist/scripts/runSupermachineAuth.js.map +1 -0
  177. package/dist/scripts/runTests.d.ts +3 -0
  178. package/dist/scripts/runTests.d.ts.map +1 -0
  179. package/dist/scripts/runTests.js +278 -0
  180. package/dist/scripts/runTests.js.map +1 -0
  181. package/dist/scripts/runVm.d.ts +3 -0
  182. package/dist/scripts/runVm.d.ts.map +1 -0
  183. package/dist/scripts/runVm.js +524 -0
  184. package/dist/scripts/runVm.js.map +1 -0
  185. package/dist/scripts/runVmAuth.d.ts +3 -0
  186. package/dist/scripts/runVmAuth.d.ts.map +1 -0
  187. package/dist/scripts/runVmAuth.js +475 -0
  188. package/dist/scripts/runVmAuth.js.map +1 -0
  189. package/dist/scripts/runVmScript.d.ts +3 -0
  190. package/dist/scripts/runVmScript.d.ts.map +1 -0
  191. package/dist/scripts/runVmScript.js +242 -0
  192. package/dist/scripts/runVmScript.js.map +1 -0
  193. package/dist/scripts/setupTestDb.d.ts +3 -0
  194. package/dist/scripts/setupTestDb.d.ts.map +1 -0
  195. package/dist/scripts/setupTestDb.js +61 -0
  196. package/dist/scripts/setupTestDb.js.map +1 -0
  197. package/dist/scripts/verifyContracts.d.ts +3 -0
  198. package/dist/scripts/verifyContracts.d.ts.map +1 -0
  199. package/dist/scripts/verifyContracts.js +258 -0
  200. package/dist/scripts/verifyContracts.js.map +1 -0
  201. package/dist/scripts/verifyRestContracts.d.ts +3 -0
  202. package/dist/scripts/verifyRestContracts.d.ts.map +1 -0
  203. package/dist/scripts/verifyRestContracts.js +237 -0
  204. package/dist/scripts/verifyRestContracts.js.map +1 -0
  205. package/dist/vite/offlineConfig.d.ts +34 -0
  206. package/dist/vite/offlineConfig.d.ts.map +1 -0
  207. package/dist/vite/offlineConfig.js +61 -0
  208. package/dist/vite/offlineConfig.js.map +1 -0
  209. package/dist/vite/onlineConfig.d.ts +42 -0
  210. package/dist/vite/onlineConfig.d.ts.map +1 -0
  211. package/dist/vite/onlineConfig.js +56 -0
  212. package/dist/vite/onlineConfig.js.map +1 -0
  213. package/docker/Dockerfile +67 -0
  214. package/docker/Dockerfile.vm +137 -0
  215. package/docker/README.md +50 -0
  216. package/docker/entrypoint.sh +198 -0
  217. package/package.json +85 -0
  218. package/src/contracts/normalize.ts +96 -0
  219. package/src/contracts/normalizeHtml.ts +98 -0
  220. package/src/edge/ca.cnf +14 -0
  221. package/src/edge/ca.crt +22 -0
  222. package/src/edge/ca.key +28 -0
  223. package/src/edge/cert.ts +117 -0
  224. package/src/edge/edgeProxy.ts +390 -0
  225. package/src/edge/server.cnf +28 -0
  226. package/src/edge/server.crt +26 -0
  227. package/src/edge/server.key +28 -0
  228. package/src/index.ts +67 -0
  229. package/src/lib/buildSourceBundle.ts +197 -0
  230. package/src/lib/freePort.ts +33 -0
  231. package/src/lib/functionBuild.ts +490 -0
  232. package/src/lib/neonWsProxy.ts +124 -0
  233. package/src/lib/sourceZipUpload.ts +168 -0
  234. package/src/lib/stealthLaunch.ts +57 -0
  235. package/src/lib/storeAutomation.ts +110 -0
  236. package/src/playwright/baseConfig.ts +120 -0
  237. package/src/playwright/globalSetup.ts +179 -0
  238. package/src/playwright/index.ts +11 -0
  239. package/src/probes/fonts.ts +279 -0
  240. package/src/probes/mirror.ts +283 -0
  241. package/src/probes/runProbe.ts +257 -0
  242. package/src/probes/types.ts +73 -0
  243. package/src/scripts/addStore.ts +59 -0
  244. package/src/scripts/buildDockerImage.ts +66 -0
  245. package/src/scripts/captureAuth.ts +145 -0
  246. package/src/scripts/captureContracts.ts +675 -0
  247. package/src/scripts/captureRestContracts.ts +319 -0
  248. package/src/scripts/checkOperationCoverage.ts +365 -0
  249. package/src/scripts/cleanupStores.ts +91 -0
  250. package/src/scripts/createStores.ts +77 -0
  251. package/src/scripts/deployAppVersion.ts +692 -0
  252. package/src/scripts/devOnlineBackend.ts +141 -0
  253. package/src/scripts/installApp.ts +188 -0
  254. package/src/scripts/listStores.ts +19 -0
  255. package/src/scripts/runDockerAuth.ts +120 -0
  256. package/src/scripts/runOffline.ts +577 -0
  257. package/src/scripts/runOfflineFullTests.ts +1634 -0
  258. package/src/scripts/runTests.ts +306 -0
  259. package/src/scripts/runVm.ts +562 -0
  260. package/src/scripts/runVmAuth.ts +541 -0
  261. package/src/scripts/runVmScript.ts +282 -0
  262. package/src/scripts/setupTestDb.ts +71 -0
  263. package/src/scripts/verifyContracts.ts +310 -0
  264. package/src/scripts/verifyRestContracts.ts +275 -0
  265. 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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=captureContracts.d.ts.map
@@ -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