@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,275 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verifyRestContracts — replay each vendored REST contract,
4
+ * diff vs the contract.
5
+ *
6
+ * Sister to `verifyContracts.ts`, but for REST. See
7
+ * `captureRestContracts.ts` for the manifest format and capture
8
+ * flow; this script consumes the captured contracts and asserts the
9
+ * target still returns the same shape.
10
+ *
11
+ * - `--target offline` (default): hit the in-process mock-admin
12
+ * Hono app. Detects regression in the mock's REST stubs.
13
+ * - `--target live`: hit a real Shopify admin endpoint with
14
+ * CONTRACTS_LIVE_SHOP + CONTRACTS_LIVE_ACCESS_TOKEN. Detects
15
+ * drift between vendored contracts and what Shopify actually
16
+ * returns today.
17
+ *
18
+ * Responses are normalised (volatile IDs / timestamps / CDN URLs /
19
+ * cursors → stable tokens) before diffing — same normaliser the
20
+ * GraphQL verifier uses. Offline-offline matches byte-for-byte
21
+ * anyway; the normaliser only matters for live-vs-contract.
22
+ */
23
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
24
+ import { resolve, relative } from 'node:path';
25
+ import {
26
+ createAdminApi,
27
+ } from '@essential-apps/shopify-test-shopify-api';
28
+ import { ShopState } from '@essential-apps/shopify-test-storefront';
29
+ import { normaliseResponse } from '../contracts/normalize.js';
30
+
31
+ interface Args {
32
+ contractsDir: string;
33
+ cwd: string;
34
+ }
35
+
36
+ interface Contract {
37
+ operationName: string;
38
+ protocol: 'rest';
39
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
40
+ path: string;
41
+ pathParams?: Record<string, string>;
42
+ query?: Record<string, string>;
43
+ body?: unknown;
44
+ response: {
45
+ status: number;
46
+ body: unknown;
47
+ };
48
+ capturedFrom: 'offline';
49
+ warning?: string;
50
+ }
51
+
52
+ const ADMIN_API_VERSION = '2025-07';
53
+
54
+ function parseArgs(): Args {
55
+ const argv = process.argv.slice(2);
56
+ const out: Args = {
57
+ contractsDir: '',
58
+ cwd: process.cwd(),
59
+ };
60
+ for (let i = 0; i < argv.length; i++) {
61
+ const a = argv[i] ?? '';
62
+ if (a === '--dir' && i + 1 < argv.length) {
63
+ out.contractsDir = argv[++i] ?? '';
64
+ }
65
+ }
66
+ if (!out.contractsDir) {
67
+ out.contractsDir = resolve(out.cwd, 'tests/test-offline/contracts/admin-rest');
68
+ } else {
69
+ out.contractsDir = resolve(out.cwd, out.contractsDir);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function fillPath(template: string, params: Record<string, string>): string {
75
+ return template.replace(/\{([^}]+)\}/g, (_, key) => {
76
+ if (!(key in params)) {
77
+ throw new Error(`path template references {${key}} but no value provided`);
78
+ }
79
+ return encodeURIComponent(params[key] ?? '');
80
+ });
81
+ }
82
+
83
+ function appendQuery(path: string, query?: Record<string, string>): string {
84
+ if (!query || Object.keys(query).length === 0) return path;
85
+ const params = new URLSearchParams();
86
+ for (const [k, v] of Object.entries(query)) params.set(k, v);
87
+ const joiner = path.includes('?') ? '&' : '?';
88
+ return `${path}${joiner}${params.toString()}`;
89
+ }
90
+
91
+ interface RestExecutor {
92
+ (contract: Contract): Promise<{ status: number; body: unknown }>;
93
+ }
94
+
95
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Hono is loosely typed
96
+ function buildOfflineExecutor(state: ShopState): RestExecutor {
97
+ const app = createAdminApi({ state });
98
+ return async (contract) => {
99
+ const filled = fillPath(contract.path, {
100
+ version: ADMIN_API_VERSION,
101
+ ...(contract.pathParams ?? {}),
102
+ });
103
+ const finalPath = appendQuery(filled, contract.query);
104
+ const init: RequestInit = {
105
+ method: contract.method,
106
+ headers: {
107
+ 'Content-Type': 'application/json',
108
+ 'X-Shopify-Access-Token': 'mock-access-token',
109
+ },
110
+ };
111
+ if (contract.body !== undefined) {
112
+ init.body = JSON.stringify(contract.body);
113
+ }
114
+ const resp: Response = await (app.request as any)(finalPath, init);
115
+ let body: unknown = null;
116
+ try {
117
+ body = await resp.json();
118
+ } catch {
119
+ // empty / non-JSON
120
+ }
121
+ return { status: resp.status, body };
122
+ };
123
+ }
124
+
125
+ // Live executor intentionally removed — platform parity (does the
126
+ // mock match real Shopify) is owned by
127
+ // `@essential-apps/shopify-test-conformance`. Consuming-app
128
+ // contract verification only runs against the offline mock.
129
+
130
+ /**
131
+ * Deep-diff with normalised first-mismatch reporting. Same shape
132
+ * as the GraphQL verifier — kept independent rather than imported
133
+ * so the REST script can be invoked standalone.
134
+ */
135
+ function diff(
136
+ expected: unknown,
137
+ actual: unknown,
138
+ path = '$',
139
+ ): string | null {
140
+ if (expected === actual) return null;
141
+ if (
142
+ typeof expected !== typeof actual ||
143
+ expected === null ||
144
+ actual === null
145
+ ) {
146
+ return `${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`;
147
+ }
148
+ if (Array.isArray(expected)) {
149
+ if (!Array.isArray(actual)) return `${path}: expected array, got object`;
150
+ if (expected.length !== actual.length) {
151
+ return `${path}: expected length ${expected.length}, got ${actual.length}`;
152
+ }
153
+ for (let i = 0; i < expected.length; i++) {
154
+ const d = diff(expected[i], actual[i], `${path}[${i}]`);
155
+ if (d) return d;
156
+ }
157
+ return null;
158
+ }
159
+ if (typeof expected === 'object') {
160
+ const e = expected as Record<string, unknown>;
161
+ const a = actual as Record<string, unknown>;
162
+ const allKeys = new Set([...Object.keys(e), ...Object.keys(a)]);
163
+ for (const k of allKeys) {
164
+ if (!(k in e)) return `${path}.${k}: unexpected key`;
165
+ if (!(k in a)) return `${path}.${k}: missing key`;
166
+ const d = diff(e[k], a[k], `${path}.${k}`);
167
+ if (d) return d;
168
+ }
169
+ return null;
170
+ }
171
+ return `${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`;
172
+ }
173
+
174
+ async function main(): Promise<void> {
175
+ const args = parseArgs();
176
+ let entries: string[];
177
+ try {
178
+ entries = readdirSync(args.contractsDir);
179
+ } catch {
180
+ console.error(
181
+ `[verify-rest] no contracts directory at ${args.contractsDir}. ` +
182
+ `Run \`npm run test:online:capture-rest-contracts\` first.`,
183
+ );
184
+ process.exit(2);
185
+ }
186
+ const contractFiles = entries.filter((f) => f.endsWith('.json'));
187
+ if (contractFiles.length === 0) {
188
+ console.error(`[verify-rest] no contracts found in ${args.contractsDir}`);
189
+ process.exit(2);
190
+ }
191
+
192
+ const state = buildSeededState();
193
+ const executor: RestExecutor =
194
+ buildOfflineExecutor(state);
195
+
196
+ let pass = 0;
197
+ let drift = 0;
198
+ let skipped = 0;
199
+ const failures: { contract: string; diff: string }[] = [];
200
+
201
+ for (const f of contractFiles) {
202
+ const path = resolve(args.contractsDir, f);
203
+ let st;
204
+ try {
205
+ st = statSync(path);
206
+ } catch {
207
+ continue;
208
+ }
209
+ if (!st.isFile()) continue;
210
+ const contract = JSON.parse(readFileSync(path, 'utf8')) as Contract;
211
+ if (contract.warning) {
212
+ skipped++;
213
+ continue;
214
+ }
215
+ let actual: { status: number; body: unknown };
216
+ try {
217
+ actual = await executor(contract);
218
+ } catch (err) {
219
+ drift++;
220
+ failures.push({ contract: f, diff: `executor threw: ${(err as Error).message}` });
221
+ continue;
222
+ }
223
+ // Status-code mismatch is a hard diff — surface it directly.
224
+ if (actual.status !== contract.response.status) {
225
+ drift++;
226
+ failures.push({
227
+ contract: f,
228
+ diff: `$.status: expected ${contract.response.status}, got ${actual.status}`,
229
+ });
230
+ continue;
231
+ }
232
+ // Body diff — normalise both sides first (same rationale as
233
+ // GraphQL verify; see contracts/normalize.ts).
234
+ const expectedNorm = normaliseResponse(contract.response.body);
235
+ const actualNorm = normaliseResponse(actual.body);
236
+ const d = diff(expectedNorm, actualNorm, '$.body');
237
+ if (d === null) {
238
+ pass++;
239
+ } else {
240
+ drift++;
241
+ failures.push({ contract: f, diff: d });
242
+ }
243
+ }
244
+
245
+ console.log(
246
+ `[verify-rest]: ${pass} pass, ${drift} drift, ${skipped} skipped`,
247
+ );
248
+ if (failures.length > 0) {
249
+ console.log('');
250
+ console.log('[verify-rest] drift:');
251
+ for (const f of failures) {
252
+ console.log(` ${relative(args.cwd, resolve(args.contractsDir, f.contract))}`);
253
+ console.log(` ${f.diff}`);
254
+ }
255
+ console.log('');
256
+ console.log(
257
+ 'If the mock is correct and the contract is stale, regenerate with `npm run test:online:capture-rest-contracts`.',
258
+ );
259
+ }
260
+ process.exit(drift === 0 ? 0 : 1);
261
+ }
262
+
263
+ function buildSeededState(): ShopState {
264
+ return new ShopState({
265
+ shop: {
266
+ domain: 'test-shop.myshopify.com',
267
+ permanent_domain: 'test-shop.myshopify.com',
268
+ },
269
+ });
270
+ }
271
+
272
+ main().catch((err) => {
273
+ console.error(err);
274
+ process.exit(2);
275
+ });
@@ -0,0 +1,60 @@
1
+ import { loadConfigFromFile, mergeConfig, type ConfigEnv, type UserConfig } from 'vite';
2
+
3
+ /**
4
+ * Vite config used ONLY to serve the app backend for the ONLINE test
5
+ * suite — `runTests.ts` boots the dev server with
6
+ * `vite --config <this file>`. It is never referenced by an app's own
7
+ * `vite dev` / `vite build`, so it cannot change any app's production
8
+ * or local-dev behaviour. (Offline-full serves a production build via
9
+ * remix-serve; conformance runs VM probes — neither uses Vite dev, so
10
+ * this is online-only.)
11
+ *
12
+ * What it does: load the consuming app's OWN Vite config (auto-
13
+ * discovered from cwd) and merge `optimizeDeps.entries` on top so Vite
14
+ * crawls every route at server startup and pre-bundles all their
15
+ * client deps in the FIRST optimize pass.
16
+ *
17
+ * Why: in dev, Vite discovers a route's deps lazily — on the first
18
+ * navigation to that route — then re-optimizes and FORCE-RELOADS the
19
+ * page to pick up the freshly pre-bundled deps. Inside the embedded
20
+ * app iframe that reload tears the page down mid-test. Observed as the
21
+ * funnels "placement type list shows all 4 placements" flake: the
22
+ * first hit to /app/funnels/new pulls in react-hook-form / @dnd-kit /
23
+ * zod, Vite reloads, and the content assertion times out; the retry is
24
+ * fast because the deps are now warm. Crawling every route up front
25
+ * leaves nothing to discover on navigation, so there is no reload.
26
+ *
27
+ * Generic + safe for every consuming app:
28
+ * - additive: only ADDS optimizeDeps.entries (mergeConfig concatenates
29
+ * arrays); never removes or rewrites the app's plugins/options.
30
+ * - auto-discovers the app config (any extension) — no path assumption.
31
+ * - the glob is a Remix-convention default; if it matches nothing it's
32
+ * a harmless no-op (no breakage, just no speed-up).
33
+ * - `.server.*` files are excluded so server-only modules aren't
34
+ * dragged into the client dep scan.
35
+ * - override the glob with TEST_ONLINE_VITE_OPTIMIZE_ENTRIES
36
+ * (comma-separated) for apps with an unusual route layout.
37
+ *
38
+ * `runTests.ts` only passes `--config` to this file when the optimize
39
+ * is enabled; TEST_ONLINE_VITE_OPTIMIZE_ALL=0 boots bare `vite` with
40
+ * the app config untouched (kill switch).
41
+ */
42
+ export default async function viteOnlineConfig(configEnv: ConfigEnv): Promise<UserConfig> {
43
+ const root = process.cwd();
44
+ const loaded = await loadConfigFromFile(configEnv, undefined, root);
45
+ if (!loaded?.config) {
46
+ throw new Error(
47
+ `viteOnlineConfig: could not load the app's Vite config from ${root}. ` +
48
+ `Expected a vite.config.{ts,js,mjs,mts,…} at the repo root.`,
49
+ );
50
+ }
51
+
52
+ const entries = process.env['TEST_ONLINE_VITE_OPTIMIZE_ENTRIES']
53
+ ? process.env['TEST_ONLINE_VITE_OPTIMIZE_ENTRIES']
54
+ .split(',')
55
+ .map((s) => s.trim())
56
+ .filter(Boolean)
57
+ : ['app/**/*.{ts,tsx,jsx,js}', '!app/**/*.server.*'];
58
+
59
+ return mergeConfig(loaded.config, { optimizeDeps: { entries } } satisfies UserConfig);
60
+ }