@shopify/hydrogen 2026.10.0-preview.1 → 2026.10.0-preview.2

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 (149) hide show
  1. package/dist/cli/index.mjs +326 -9
  2. package/dist/client/client.mjs +20 -7
  3. package/dist/client/client.mjs.map +1 -1
  4. package/dist/core/cart/cart.d.mts +5 -0
  5. package/dist/core/cart/cart.mjs +41 -21
  6. package/dist/core/cart/cart.mjs.map +1 -1
  7. package/dist/core/collection/url.d.mts +1 -0
  8. package/dist/core/collection/url.mjs +10 -1
  9. package/dist/core/collection/url.mjs.map +1 -1
  10. package/dist/core/headers.mjs +2 -3
  11. package/dist/core/headers.mjs.map +1 -1
  12. package/dist/core/index.d.mts +3 -2
  13. package/dist/core/index.mjs +7 -6
  14. package/dist/core/logging/logging.mjs +1 -1
  15. package/dist/core/product/accept-variant-id.mjs +98 -0
  16. package/dist/core/product/accept-variant-id.mjs.map +1 -0
  17. package/dist/core/product/index.d.mts +1 -0
  18. package/dist/core/product/options.d.mts +3 -0
  19. package/dist/core/product/options.mjs +5 -0
  20. package/dist/core/product/options.mjs.map +1 -1
  21. package/dist/core/product/url.d.mts +47 -0
  22. package/dist/core/product/url.mjs +69 -0
  23. package/dist/core/product/url.mjs.map +1 -0
  24. package/dist/core/request-context.d.mts +6 -1
  25. package/dist/core/request-context.mjs +51 -30
  26. package/dist/core/request-context.mjs.map +1 -1
  27. package/dist/core/request-routing/handle-shopify-routes.mjs +2 -2
  28. package/dist/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  29. package/dist/core/request-routing/interceptors/ajax-api.mjs +1 -1
  30. package/dist/core/request-routing/interceptors/ajax-api.mjs.map +1 -1
  31. package/dist/core/request-routing/interceptors/api-proxy.mjs +1 -1
  32. package/dist/core/request-routing/interceptors/api-proxy.mjs.map +1 -1
  33. package/dist/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
  34. package/dist/core/request-routing/interceptors/mcp-proxy.mjs.map +1 -1
  35. package/dist/core/request-routing/interceptors/proxy.mjs +12 -7
  36. package/dist/core/request-routing/interceptors/proxy.mjs.map +1 -1
  37. package/dist/core/request-routing/interceptors/sfapi-proxy.mjs +1 -1
  38. package/dist/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
  39. package/dist/core/request-routing/interceptors/well-known.mjs +6 -3
  40. package/dist/core/request-routing/interceptors/well-known.mjs.map +1 -1
  41. package/dist/core/request-routing/registered-routes.d.mts +1 -1
  42. package/dist/core/request-routing/registered-routes.mjs +20 -7
  43. package/dist/core/request-routing/registered-routes.mjs.map +1 -1
  44. package/dist/core/request-routing/route-types.d.mts +7 -2
  45. package/dist/core/url.mjs +10 -3
  46. package/dist/core/url.mjs.map +1 -1
  47. package/dist/customer-account/client.mjs +1 -1
  48. package/dist/customer-account/session.mjs +2 -2
  49. package/dist/development/client/client.mjs +20 -7
  50. package/dist/development/client/client.mjs.map +1 -1
  51. package/dist/development/core/cart/cart.d.mts +5 -0
  52. package/dist/development/core/cart/cart.mjs +41 -21
  53. package/dist/development/core/cart/cart.mjs.map +1 -1
  54. package/dist/development/core/cart/queries.d.mts +57 -57
  55. package/dist/development/core/collection/url.d.mts +1 -0
  56. package/dist/development/core/collection/url.mjs +10 -1
  57. package/dist/development/core/collection/url.mjs.map +1 -1
  58. package/dist/development/core/development.d.mts +3 -2
  59. package/dist/development/core/development.mjs +6 -5
  60. package/dist/development/core/headers.mjs +2 -3
  61. package/dist/development/core/headers.mjs.map +1 -1
  62. package/dist/development/core/index.d.mts +2 -1
  63. package/dist/development/core/predictive-search/queries.d.mts +2 -2
  64. package/dist/development/core/product/accept-variant-id.mjs +98 -0
  65. package/dist/development/core/product/accept-variant-id.mjs.map +1 -0
  66. package/dist/development/core/product/index.d.mts +1 -0
  67. package/dist/development/core/product/options.d.mts +3 -0
  68. package/dist/development/core/product/options.mjs +5 -0
  69. package/dist/development/core/product/options.mjs.map +1 -1
  70. package/dist/development/core/product/url.d.mts +47 -0
  71. package/dist/development/core/product/url.mjs +69 -0
  72. package/dist/development/core/product/url.mjs.map +1 -0
  73. package/dist/development/core/request-context.d.mts +6 -1
  74. package/dist/development/core/request-context.mjs +51 -30
  75. package/dist/development/core/request-context.mjs.map +1 -1
  76. package/dist/development/core/request-routing/handle-shopify-routes.mjs +2 -2
  77. package/dist/development/core/request-routing/handle-shopify-routes.mjs.map +1 -1
  78. package/dist/development/core/request-routing/interceptors/ajax-api.mjs +1 -1
  79. package/dist/development/core/request-routing/interceptors/ajax-api.mjs.map +1 -1
  80. package/dist/development/core/request-routing/interceptors/api-proxy.mjs +1 -1
  81. package/dist/development/core/request-routing/interceptors/api-proxy.mjs.map +1 -1
  82. package/dist/development/core/request-routing/interceptors/mcp-proxy.mjs +1 -1
  83. package/dist/development/core/request-routing/interceptors/mcp-proxy.mjs.map +1 -1
  84. package/dist/development/core/request-routing/interceptors/proxy.mjs +12 -7
  85. package/dist/development/core/request-routing/interceptors/proxy.mjs.map +1 -1
  86. package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs +1 -1
  87. package/dist/development/core/request-routing/interceptors/sfapi-proxy.mjs.map +1 -1
  88. package/dist/development/core/request-routing/interceptors/well-known.mjs +6 -3
  89. package/dist/development/core/request-routing/interceptors/well-known.mjs.map +1 -1
  90. package/dist/development/core/request-routing/registered-routes.d.mts +1 -1
  91. package/dist/development/core/request-routing/registered-routes.mjs +20 -7
  92. package/dist/development/core/request-routing/registered-routes.mjs.map +1 -1
  93. package/dist/development/core/request-routing/route-types.d.mts +7 -2
  94. package/dist/development/core/url.mjs +10 -3
  95. package/dist/development/core/url.mjs.map +1 -1
  96. package/dist/development/react/cart.d.mts +6 -2
  97. package/dist/development/react/cart.mjs +8 -3
  98. package/dist/development/react/cart.mjs.map +1 -1
  99. package/dist/development/react/index.d.mts +2 -2
  100. package/dist/development/react/index.mjs +2 -2
  101. package/dist/development/vue/cart.d.mts +6 -2
  102. package/dist/development/vue/cart.mjs +5 -1
  103. package/dist/development/vue/cart.mjs.map +1 -1
  104. package/dist/development/vue/index.d.mts +2 -2
  105. package/dist/development/vue/index.mjs +2 -2
  106. package/dist/react/cart.d.mts +6 -2
  107. package/dist/react/cart.mjs +8 -3
  108. package/dist/react/cart.mjs.map +1 -1
  109. package/dist/react/index.d.mts +2 -2
  110. package/dist/react/index.mjs +2 -2
  111. package/dist/vite/certificate-prompt.mjs +24 -0
  112. package/dist/vite/certificate-prompt.mjs.map +1 -0
  113. package/dist/vite/index.d.mts +2 -2
  114. package/dist/vite/index.mjs +2 -2
  115. package/dist/vite/local-https.d.mts +13 -1
  116. package/dist/vite/local-https.mjs +79 -5
  117. package/dist/vite/local-https.mjs.map +1 -1
  118. package/dist/vite/mkcert.mjs +141 -0
  119. package/dist/vite/mkcert.mjs.map +1 -0
  120. package/dist/vue/cart.d.mts +6 -2
  121. package/dist/vue/cart.mjs +5 -1
  122. package/dist/vue/cart.mjs.map +1 -1
  123. package/dist/vue/index.d.mts +2 -2
  124. package/dist/vue/index.mjs +2 -2
  125. package/package.json +1 -1
  126. package/skills/hydrogen-cart-ui/SKILL.md +6 -0
  127. package/skills/hydrogen-cart-ui/references/react.md +24 -2
  128. package/skills/hydrogen-cart-ui/references/vue.md +26 -2
  129. package/skills/hydrogen-collection-browser/SKILL.md +4 -1
  130. package/skills/hydrogen-collection-browser/references/nextjs.md +1 -1
  131. package/skills/hydrogen-collection-browser/references/nuxt.md +1 -1
  132. package/skills/hydrogen-collection-browser/references/react.md +68 -4
  133. package/skills/hydrogen-local-https/SKILL.md +17 -19
  134. package/skills/hydrogen-request-handlers/SKILL.md +21 -4
  135. package/skills/hydrogen-request-handlers/references/frameworks.md +5 -0
  136. package/skills/hydrogen-request-handlers/references/nextjs.md +2 -1
  137. package/skills/hydrogen-request-handlers/references/nuxt.md +2 -1
  138. package/skills/hydrogen-routing/SKILL.md +20 -3
  139. package/skills/hydrogen-setup/steps/2-scaffold.md +1 -1
  140. package/skills/hydrogen-setup/steps/4-collection-and-search.md +1 -0
  141. package/skills/hydrogen-setup/steps/6-product-detail-page.md +31 -5
  142. package/skills/hydrogen-smoke-test/SKILL.md +2 -0
  143. package/skills/hydrogen-variant-form/SKILL.md +18 -1
  144. package/skills/hydrogen-variant-form/references/nextjs.md +7 -3
  145. package/skills/hydrogen-variant-form/references/nuxt.md +1 -1
  146. package/dist/core/request-routing/interceptors/agent-proxy.mjs +0 -19
  147. package/dist/core/request-routing/interceptors/agent-proxy.mjs.map +0 -1
  148. package/dist/development/core/request-routing/interceptors/agent-proxy.mjs +0 -19
  149. package/dist/development/core/request-routing/interceptors/agent-proxy.mjs.map +0 -1
@@ -2,11 +2,317 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
5
8
  import { spawn } from "node:child_process";
6
- import { randomUUID } from "node:crypto";
7
- import { dirname, join, parse, resolve } from "node:path";
9
+ import { createHash, randomUUID } from "node:crypto";
8
10
  import { parseArgs } from "node:util";
9
11
 
12
+ //#region src/core/logging/types.ts
13
+ const DEFAULT_LOG_LEVEL = "info";
14
+ const LOG_LEVEL_VALUES = {
15
+ trace: 10,
16
+ debug: 20,
17
+ info: 30,
18
+ warn: 40,
19
+ error: 50,
20
+ fatal: 60,
21
+ silent: Number.POSITIVE_INFINITY
22
+ };
23
+ function isLevelEnabled(level, threshold) {
24
+ return LOG_LEVEL_VALUES[level] >= LOG_LEVEL_VALUES[threshold];
25
+ }
26
+
27
+ //#endregion
28
+ //#region src/core/logging/logging.ts
29
+ const CONSOLE_METHODS = {
30
+ trace: "debug",
31
+ debug: "debug",
32
+ info: "info",
33
+ warn: "warn",
34
+ error: "error",
35
+ fatal: "error"
36
+ };
37
+ function formatLogPrefix(level, scope) {
38
+ return `[hydrogen:${level}:${scope}]`;
39
+ }
40
+ function writeToConsole(level, message, context) {
41
+ const { scope, error, ...rest } = context ?? {};
42
+ const args = [scope ? `${formatLogPrefix(level, scope)} ${message}` : message];
43
+ if (error !== void 0) args.push(error);
44
+ if (Object.keys(rest).length > 0) args.push(rest);
45
+ console[CONSOLE_METHODS[level]](...args);
46
+ }
47
+ /**
48
+ * Built-in sink: writes `[hydrogen:<level>:<scope>] <message>` to a
49
+ * level-specific `console` method, followed by `context.error` and any extra
50
+ * context fields. `trace` uses `console.debug`; `fatal` uses `console.error`.
51
+ */
52
+ const consoleLogger = {
53
+ trace: (message, context) => writeToConsole("trace", message, context),
54
+ debug: (message, context) => writeToConsole("debug", message, context),
55
+ info: (message, context) => writeToConsole("info", message, context),
56
+ warn: (message, context) => writeToConsole("warn", message, context),
57
+ error: (message, context) => writeToConsole("error", message, context),
58
+ fatal: (message, context) => writeToConsole("fatal", message, context)
59
+ };
60
+ const state = {
61
+ logger: consoleLogger,
62
+ level: DEFAULT_LOG_LEVEL
63
+ };
64
+ function emit(scope, level, message, context) {
65
+ if (!isLevelEnabled(level, state.level)) return;
66
+ try {
67
+ state.logger[level](message, {
68
+ ...context,
69
+ scope
70
+ });
71
+ } catch (error) {
72
+ reportLoggerFailure(error, {
73
+ level,
74
+ scope,
75
+ message,
76
+ context
77
+ });
78
+ }
79
+ }
80
+ function reportLoggerFailure(error, failedEntry) {
81
+ if (state.logger === consoleLogger) return;
82
+ const fallbackContext = {
83
+ scope: "logging",
84
+ error,
85
+ originalLevel: failedEntry.level,
86
+ originalScope: failedEntry.scope,
87
+ originalMessage: failedEntry.message
88
+ };
89
+ if (failedEntry.context !== void 0) fallbackContext.originalContext = failedEntry.context;
90
+ try {
91
+ consoleLogger.error("configured logger failed", fallbackContext);
92
+ } catch {}
93
+ }
94
+ /** @internal Returns the lazily-resolved scoped logger for a Hydrogen subsystem. */
95
+ function getLogger(scope) {
96
+ return {
97
+ trace: (message, context) => emit(scope, "trace", message, context),
98
+ debug: (message, context) => emit(scope, "debug", message, context),
99
+ info: (message, context) => emit(scope, "info", message, context),
100
+ warn: (message, context) => emit(scope, "warn", message, context),
101
+ error: (message, context) => emit(scope, "error", message, context),
102
+ fatal: (message, context) => emit(scope, "fatal", message, context)
103
+ };
104
+ }
105
+
106
+ //#endregion
107
+ //#region src/vite/mkcert.ts
108
+ const log = getLogger("local-https");
109
+ function confirm(message) {
110
+ consoleLogger.info(`☑️ ${message}`);
111
+ }
112
+ const MKCERT_VERSION = "v1.4.4";
113
+ const MKCERT_RELEASE_BASE_URL = `https://github.com/FiloSottile/mkcert/releases/download/${MKCERT_VERSION}`;
114
+ const EXECUTABLE_MODE = 493;
115
+ const SUCCESS_EXIT_CODE$2 = 0;
116
+ const DOWNLOAD_TIMEOUT_MS = 6e4;
117
+ const MKCERT_BINARIES = {
118
+ "darwin-arm64": {
119
+ assetName: `mkcert-${MKCERT_VERSION}-darwin-arm64`,
120
+ sha256: "c8af0df44bce04359794dad8ea28d750437411d632748049d08644ffb66a60c6"
121
+ },
122
+ "darwin-x64": {
123
+ assetName: `mkcert-${MKCERT_VERSION}-darwin-amd64`,
124
+ sha256: "a32dfab51f1845d51e810db8e47dcf0e6b51ae3422426514bf5a2b8302e97d4e"
125
+ },
126
+ "linux-arm64": {
127
+ assetName: `mkcert-${MKCERT_VERSION}-linux-arm64`,
128
+ sha256: "b98f2cc69fd9147fe4d405d859c57504571adec0d3611c3eefd04107c7ac00d0"
129
+ },
130
+ "linux-x64": {
131
+ assetName: `mkcert-${MKCERT_VERSION}-linux-amd64`,
132
+ sha256: "6d31c65b03972c6dc4a14ab429f2928300518b26503f58723e532d1b0a3bbb52"
133
+ },
134
+ "win32-arm64": {
135
+ assetName: `mkcert-${MKCERT_VERSION}-windows-arm64.exe`,
136
+ sha256: "793747256c562622d40127c8080df26add2fb44c50906ce9db63b42a5280582e"
137
+ },
138
+ "win32-x64": {
139
+ assetName: `mkcert-${MKCERT_VERSION}-windows-amd64.exe`,
140
+ sha256: "d2660b50a9ed59eada480750561c96abc2ed4c9a38c6a24d93e30e0977631398"
141
+ }
142
+ };
143
+ /** @internal Exported for tests. */
144
+ function resolveMkcertBinary(platform, arch) {
145
+ const binary = MKCERT_BINARIES[`${platform}-${arch}`];
146
+ if (!binary) return void 0;
147
+ return {
148
+ ...binary,
149
+ url: `${MKCERT_RELEASE_BASE_URL}/${binary.assetName}`
150
+ };
151
+ }
152
+ function sha256Hex(bytes) {
153
+ return createHash("sha256").update(bytes).digest("hex");
154
+ }
155
+ /** @internal Exported for tests. */
156
+ async function downloadVerified(options) {
157
+ let bytes;
158
+ try {
159
+ const response = await fetch(options.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
160
+ if (!response.ok) throw new Error(`mkcert download failed with status ${response.status}: ${options.url}`);
161
+ bytes = new Uint8Array(await response.arrayBuffer());
162
+ } catch (error) {
163
+ if (error instanceof Error && error.name === "TimeoutError") throw new Error(`mkcert download timed out after ${DOWNLOAD_TIMEOUT_MS / 1e3}s: ${options.url}`, { cause: error });
164
+ throw error;
165
+ }
166
+ const digest = sha256Hex(bytes);
167
+ if (digest !== options.sha256) throw new Error(`mkcert download did not match the pinned SHA-256 checksum; refusing to run it.\n URL: ${options.url}\n Expected: ${options.sha256}\n Received: ${digest}`);
168
+ await mkdir(dirname(options.destination), { recursive: true });
169
+ const temporaryPath = `${options.destination}.tmp-${process.pid}`;
170
+ try {
171
+ await writeFile(temporaryPath, bytes, { mode: EXECUTABLE_MODE });
172
+ await chmod(temporaryPath, EXECUTABLE_MODE);
173
+ await rename(temporaryPath, options.destination);
174
+ } catch (error) {
175
+ await rm(temporaryPath, { force: true });
176
+ throw error;
177
+ }
178
+ }
179
+ async function ensureMkcertBinary() {
180
+ const binary = resolveMkcertBinary(process.platform, process.arch);
181
+ if (!binary) throw new Error(`Automatic mkcert download is not available for ${process.platform}-${process.arch}. Install mkcert manually and generate the certificate.`);
182
+ const binaryPath = join(homedir(), ".shopify", "hydrogen", "mkcert", binary.assetName);
183
+ if (existsSync(binaryPath) && sha256Hex(await readFile(binaryPath)) === binary.sha256) return binaryPath;
184
+ log.info(`downloading mkcert ${MKCERT_VERSION}…`);
185
+ await downloadVerified({
186
+ url: binary.url,
187
+ sha256: binary.sha256,
188
+ destination: binaryPath
189
+ });
190
+ confirm(`mkcert ${MKCERT_VERSION} downloaded`);
191
+ return binaryPath;
192
+ }
193
+ function runMkcert(binaryPath, args) {
194
+ return new Promise((resolve, reject) => {
195
+ const child = spawn(binaryPath, args, { stdio: [
196
+ "inherit",
197
+ "pipe",
198
+ "pipe"
199
+ ] });
200
+ let output = "";
201
+ const collect = (chunk) => {
202
+ output += chunk.toString();
203
+ };
204
+ child.stdout?.on("data", collect);
205
+ child.stderr?.on("data", collect);
206
+ child.on("error", reject);
207
+ child.on("close", (code, signal) => {
208
+ if (code === SUCCESS_EXIT_CODE$2) {
209
+ resolve(output);
210
+ return;
211
+ }
212
+ const reason = signal ? `mkcert was killed by ${signal}` : `mkcert exited with code ${code}`;
213
+ reject(new Error(output.trim() === "" ? reason : `${reason}\n${output.trim()}`));
214
+ });
215
+ });
216
+ }
217
+ async function provisionCertificates(settings) {
218
+ const binaryPath = await ensureMkcertBinary();
219
+ await mkdir(dirname(settings.certPath), { recursive: true });
220
+ await mkdir(dirname(settings.keyPath), { recursive: true });
221
+ log.info("generating a trusted local certificate; mkcert may prompt for your password.");
222
+ confirm((await runMkcert(binaryPath, [
223
+ "-install",
224
+ "-cert-file",
225
+ settings.certPath,
226
+ "-key-file",
227
+ settings.keyPath,
228
+ settings.host
229
+ ])).includes("The local CA is already installed") ? "The local CA is already installed in the system trust store" : "The local CA is now installed in the system trust store");
230
+ confirm(`Created a new certificate for "${settings.host}"`);
231
+ log.info("verifying certificates…");
232
+ const missingPaths = [settings.certPath, settings.keyPath].filter((path) => !existsSync(path));
233
+ if (missingPaths.length > 0) throw new Error(`mkcert did not create the expected files: ${missingPaths.join(", ")}`);
234
+ confirm("local https certificates ready");
235
+ }
236
+ async function uninstallCertificateAuthority() {
237
+ const binaryPath = await ensureMkcertBinary();
238
+ log.info("removing the local certificate authority; mkcert may prompt for your password.");
239
+ await runMkcert(binaryPath, ["-uninstall"]);
240
+ confirm("The local CA was removed from the system trust stores");
241
+ }
242
+
243
+ //#endregion
244
+ //#region src/vite/local-https.ts
245
+ const LOCAL_HTTPS_DEFAULTS = {
246
+ host: "local.tryhydrogen.dev",
247
+ port: 5173
248
+ };
249
+ /**
250
+ * Downloads a pinned, checksum-verified mkcert release and generates the
251
+ * trusted local certificate files when they do not exist yet. The Vite plugin
252
+ * runs this automatically on `vite dev`; call it directly for frameworks that
253
+ * read certificate paths before Vite starts or from setup scripts.
254
+ */
255
+ async function provisionLocalHttps(options = {}) {
256
+ const settings = resolveLocalHttpsSettings({
257
+ enabled: true,
258
+ ...options
259
+ });
260
+ if (!certificateFilesExist(settings)) await provisionCertificates(settings);
261
+ return {
262
+ host: settings.host,
263
+ certPath: settings.certPath,
264
+ keyPath: settings.keyPath
265
+ };
266
+ }
267
+ function resolveLocalHttpsSettings(options) {
268
+ const host = options.host ?? LOCAL_HTTPS_DEFAULTS.host;
269
+ const certificateDirectory = join(homedir(), ".shopify", "hydrogen", "certs");
270
+ return {
271
+ host,
272
+ port: options.port ?? LOCAL_HTTPS_DEFAULTS.port,
273
+ certPath: resolveCertificatePath(options.certPath ?? join(certificateDirectory, `${host}.pem`)),
274
+ keyPath: resolveCertificatePath(options.keyPath ?? join(certificateDirectory, `${host}-key.pem`))
275
+ };
276
+ }
277
+ function resolveCertificatePath(path) {
278
+ if (path instanceof URL) return fileURLToPath(path);
279
+ if (path === "~") return homedir();
280
+ if (path.startsWith("~/")) return resolve(homedir(), path.slice(2));
281
+ return isAbsolute(path) ? path : resolve(path);
282
+ }
283
+ function certificateFilesExist(settings) {
284
+ return existsSync(settings.certPath) && existsSync(settings.keyPath);
285
+ }
286
+
287
+ //#endregion
288
+ //#region src/cli/certs.ts
289
+ const REMOVE_CA_FLAG = "--remove-ca";
290
+ async function installLocalHttpsCertificates() {
291
+ const { host, certPath, keyPath } = await provisionLocalHttps();
292
+ console.log(`Local HTTPS is ready for https://${host}`);
293
+ console.log(` Certificate: ${certPath}`);
294
+ console.log(` Private key: ${keyPath}`);
295
+ }
296
+ async function uninstallLocalHttpsCertificates(args = []) {
297
+ const unknownArgument = args.find((argument) => argument !== REMOVE_CA_FLAG);
298
+ if (unknownArgument) throw new Error(`Unknown argument: ${unknownArgument}`);
299
+ const removeCertificateAuthority = args.includes(REMOVE_CA_FLAG);
300
+ if (removeCertificateAuthority) {
301
+ console.warn("Warning: removing mkcert's shared local CA will make certificates created by other projects untrusted.");
302
+ await uninstallCertificateAuthority();
303
+ }
304
+ const hydrogenDirectory = join(homedir(), ".shopify", "hydrogen");
305
+ const certificateDirectory = join(hydrogenDirectory, "certs");
306
+ const host = LOCAL_HTTPS_DEFAULTS.host;
307
+ const binary = resolveMkcertBinary(process.platform, process.arch);
308
+ const removals = [rm(join(certificateDirectory, `${host}.pem`), { force: true }), rm(join(certificateDirectory, `${host}-key.pem`), { force: true })];
309
+ if (binary) removals.push(rm(join(hydrogenDirectory, "mkcert", binary.assetName), { force: true }));
310
+ await Promise.all(removals);
311
+ console.log("Hydrogen's local HTTPS certificate files were removed.");
312
+ if (!removeCertificateAuthority) console.log(`The shared mkcert CA remains trusted. Pass ${REMOVE_CA_FLAG} to remove it.`);
313
+ }
314
+
315
+ //#endregion
10
316
  //#region src/graphql/plugin-config.ts
11
317
  const STOREFRONT_SCHEMA_FILE_NAME = "storefront.schema.json";
12
318
  const CUSTOMER_ACCOUNT_SCHEMA_FILE_NAME = "customer-account.schema.json";
@@ -299,13 +605,24 @@ async function setupHydrogen(options = {}) {
299
605
  //#region src/cli/index.ts
300
606
  const CLI_ARGUMENTS_INDEX = 2;
301
607
  const FAILURE_EXIT_CODE = 1;
302
- const COMMANDS = [{
303
- path: ["setup"],
304
- run: async (_args) => setupHydrogen()
305
- }, {
306
- path: ["gql", "check"],
307
- run: async (args) => checkGraphQL({ args })
308
- }];
608
+ const COMMANDS = [
609
+ {
610
+ path: ["certs", "install"],
611
+ run: async (_args) => installLocalHttpsCertificates()
612
+ },
613
+ {
614
+ path: ["certs", "uninstall"],
615
+ run: async (args) => uninstallLocalHttpsCertificates(args)
616
+ },
617
+ {
618
+ path: ["setup"],
619
+ run: async (_args) => setupHydrogen()
620
+ },
621
+ {
622
+ path: ["gql", "check"],
623
+ run: async (args) => checkGraphQL({ args })
624
+ }
625
+ ];
309
626
  function commandMatches(path, args) {
310
627
  return path.every((segment, index) => args[index] === segment);
311
628
  }
@@ -1,8 +1,8 @@
1
- import { HYDROGEN_VERSION_HEADER, REQUEST_GROUP_ID_HEADER, SDK_VARIANT_HEADER, SDK_VARIANT_SOURCE_HEADER, SDK_VERSION_HEADER, SHOPIFY_STOREFRONT_S_HEADER, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_UNIQUE_TOKEN_HEADER, SHOPIFY_VISIT_TOKEN_HEADER, STOREFRONT_ACCESS_TOKEN_HEADER, STOREFRONT_BUYER_IP_HEADER, STOREFRONT_ID_HEADER, STOREFRONT_PRIVATE_TOKEN_HEADER } from "../core/headers.mjs";
2
- import { normalizeStoreDomain } from "../core/url.mjs";
3
1
  import { getHydrogenCacheStatus } from "../core/cache/cache-status.mjs";
4
2
  import { createFetchWithCache } from "../core/cache/fetch-with-cache.mjs";
5
3
  import { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from "../core/constants.mjs";
4
+ import { HYDROGEN_VERSION_HEADER, REQUEST_GROUP_ID_HEADER, SDK_VARIANT_HEADER, SDK_VARIANT_SOURCE_HEADER, SDK_VERSION_HEADER, SHOPIFY_STOREFRONT_S_HEADER, SHOPIFY_STOREFRONT_Y_HEADER, SHOPIFY_UNIQUE_TOKEN_HEADER, SHOPIFY_VISIT_TOKEN_HEADER, STOREFRONT_ACCESS_TOKEN_HEADER, STOREFRONT_BUYER_IP_HEADER, STOREFRONT_ID_HEADER, STOREFRONT_PRIVATE_TOKEN_HEADER } from "../core/headers.mjs";
5
+ import { normalizeStoreDomain } from "../core/url.mjs";
6
6
  import { StorefrontApiError, StorefrontTimeoutError } from "./errors.mjs";
7
7
 
8
8
  //#region src/client/client.ts
@@ -27,7 +27,17 @@ const REQUEST_IDENTITY_HEADERS = new Set([
27
27
  SHOPIFY_STOREFRONT_Y_HEADER.toLowerCase(),
28
28
  SHOPIFY_STOREFRONT_S_HEADER.toLowerCase()
29
29
  ]);
30
+ const CACHE_ENABLED_GRAPHQL_CLIENTS = /* @__PURE__ */ new WeakSet();
31
+ const SHOULD_CACHE_RESULT = Symbol();
30
32
  var StorefrontCacheConfigError = class extends Error {};
33
+ function withStorefrontClientCache(client, options, strategy, shouldCacheResult) {
34
+ if (!CACHE_ENABLED_GRAPHQL_CLIENTS.has(client.graphql)) return options;
35
+ return {
36
+ ...options,
37
+ cache: strategy,
38
+ [SHOULD_CACHE_RESULT]: shouldCacheResult
39
+ };
40
+ }
31
41
  function createStorefrontClient(args) {
32
42
  const { config, requestContext, type: clientType } = args;
33
43
  if (!config.storeDomain) throw new Error("storeDomain is required and must be non-empty");
@@ -94,7 +104,7 @@ function createStorefrontClient(args) {
94
104
  }),
95
105
  signal: externalSignals.length > 0 ? AbortSignal.any(externalSignals) : void 0
96
106
  };
97
- const response = await resolvedFetch(apiUrl, init, createStorefrontCacheOptions(apiUrl, init, queryText, opts.cache));
107
+ const response = await resolvedFetch(apiUrl, init, createStorefrontCacheOptions(apiUrl, init, queryText, opts.cache, opts[SHOULD_CACHE_RESULT]));
98
108
  responseHeaders = response.headers;
99
109
  if (getHydrogenCacheStatus(responseHeaders) !== "hit") requestContext.captureSubrequestHeaders(responseHeaders);
100
110
  requestId = responseHeaders.get("x-request-id") ?? void 0;
@@ -146,7 +156,7 @@ function createStorefrontClient(args) {
146
156
  headers: responseHeaders
147
157
  };
148
158
  }
149
- return {
159
+ const client = {
150
160
  type: clientType,
151
161
  i18n,
152
162
  graphql,
@@ -155,6 +165,8 @@ function createStorefrontClient(args) {
155
165
  storefrontId: config.storefrontId,
156
166
  requestContext
157
167
  };
168
+ if (config.cache) CACHE_ENABLED_GRAPHQL_CLIENTS.add(client.graphql);
169
+ return client;
158
170
  }
159
171
  function buildVariables(queryText, userVariables, resolvedI18n) {
160
172
  const variables = { ...userVariables };
@@ -162,7 +174,7 @@ function buildVariables(queryText, userVariables, resolvedI18n) {
162
174
  if (LANGUAGE_VAR_RE.test(queryText)) variables.language = resolvedI18n.language;
163
175
  return variables;
164
176
  }
165
- function createStorefrontCacheOptions(apiUrl, init, queryText, strategy) {
177
+ function createStorefrontCacheOptions(apiUrl, init, queryText, strategy, shouldCacheResult) {
166
178
  if (!strategy) return void 0;
167
179
  if (/\bmutation\b/.test(queryText)) return void 0;
168
180
  return {
@@ -171,7 +183,8 @@ function createStorefrontCacheOptions(apiUrl, init, queryText, strategy) {
171
183
  shouldCacheResponse: async ({ json }) => {
172
184
  if (!json) return false;
173
185
  const body = await json().catch(() => void 0);
174
- return typeof body === "object" && body != null && !Array.isArray(body) && !("errors" in body);
186
+ if (typeof body !== "object" || body == null || Array.isArray(body) || "errors" in body) return false;
187
+ return shouldCacheResult?.(body) ?? true;
175
188
  }
176
189
  };
177
190
  }
@@ -189,5 +202,5 @@ function createStorefrontCacheKey(apiUrl, init) {
189
202
  }
190
203
 
191
204
  //#endregion
192
- export { createStorefrontClient };
205
+ export { createStorefrontClient, withStorefrontClientCache };
193
206
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import type { TadaDocumentNode } from \"gql.tada\";\n\nimport { getHydrogenCacheStatus } from \"../core/cache/cache-status\";\nimport { createFetchWithCache } from \"../core/cache/fetch-with-cache\";\nimport type { FetchCacheOptions, FetchCacheResponseContext } from \"../core/cache/fetch-with-cache\";\nimport type { CacheInstance } from \"../core/cache/run-with-cache\";\nimport type { CachingStrategy } from \"../core/cache/strategies\";\nimport { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from \"../core/constants\";\nimport {\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n STOREFRONT_ACCESS_TOKEN_HEADER,\n STOREFRONT_BUYER_IP_HEADER,\n STOREFRONT_ID_HEADER,\n STOREFRONT_PRIVATE_TOKEN_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n} from \"../core/headers\";\nimport type { I18nConfig, ShopifyRequestContext } from \"../core/request-context\";\nimport { normalizeStoreDomain } from \"../core/url\";\nimport type { AnyStorefrontQueryString } from \"../graphql\";\nimport { StorefrontApiError, StorefrontTimeoutError } from \"./errors\";\nimport type {\n ClientType,\n CreateStorefrontClientArgs,\n GraphQLFormattedError,\n StorefrontClient,\n StorefrontGraphqlResult,\n} from \"./types\";\n\ntype DocLike = TadaDocumentNode<unknown, unknown> | AnyStorefrontQueryString;\ntype FetchInput = Parameters<typeof globalThis.fetch>[0];\ntype FetchInit = Parameters<typeof globalThis.fetch>[1];\ntype ResolvedStorefrontFetch = (\n input: FetchInput,\n init: FetchInit,\n cacheOptions: FetchCacheOptions | undefined,\n) => Promise<Response>;\n\nconst COUNTRY_VAR_RE = /\\$country\\s*:/;\nconst LANGUAGE_VAR_RE = /\\$language\\s*:/;\nconst REQUEST_CACHE_KEY_HEADERS = new Set([\n \"content-type\",\n \"user-agent\",\n SDK_VARIANT_HEADER.toLowerCase(),\n SDK_VARIANT_SOURCE_HEADER.toLowerCase(),\n SDK_VERSION_HEADER.toLowerCase(),\n HYDROGEN_VERSION_HEADER.toLowerCase(),\n STOREFRONT_ACCESS_TOKEN_HEADER.toLowerCase(),\n STOREFRONT_PRIVATE_TOKEN_HEADER.toLowerCase(),\n]);\nconst REQUEST_IDENTITY_HEADERS = new Set([\n \"cookie\",\n REQUEST_GROUP_ID_HEADER.toLowerCase(),\n STOREFRONT_BUYER_IP_HEADER.toLowerCase(),\n SHOPIFY_UNIQUE_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_VISIT_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_Y_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_S_HEADER.toLowerCase(),\n]);\n\nclass StorefrontCacheConfigError extends Error {}\n\n/**\n * Creates a type-safe Storefront API client.\n *\n * Pick a `type` based on where your code runs and whether buyer context is available:\n *\n * | | public | private | private_no_buyer_context |\n * |-----------------|--------------------------------|-------------------------------|--------------------------|\n * | Runs in | Browser or mobile | Server (SSR) | Server (background) |\n * | Token | Public access token, or none | Private access token | Private access token |\n * | Buyer context | Browser request context | You forward trusted `buyerIp` | None |\n * | Best for | Client-side fetches | SSR with buyer context | Webhooks, background jobs |\n *\n * Pass `requestContext` so the client's SFAPI requests carry request-scoped\n * headers, follow the incoming request's abort signal, and read the resolved\n * `i18n` used for Storefront API variable injection and localized routes.\n *\n * Token-based access is required for some Storefront API fields, including\n * product tags, metaobjects, metafields, menus, and customers. Use a private\n * client only in trusted server code where the token cannot leak to browsers.\n *\n * @see {@link https://shopify.dev/docs/api/storefront#authentication | Storefront API authentication}\n */\nexport function createStorefrontClient<\n const Type extends ClientType,\n const RequestContext extends ShopifyRequestContext,\n const CacheConfig extends CacheInstance | undefined = undefined,\n>(\n args: CreateStorefrontClientArgs<RequestContext, Type, CacheConfig>,\n): StorefrontClient<\n // Tuple-wrapped to stay non-distributive so an explicit\n // `CacheInstance | undefined` type argument yields one client type, not a\n // union. Note a *value* of that type still unlocks cache options: optional\n // property inference strips `undefined` before it reaches CacheConfig, so\n // definiteness is undecidable here — the runtime guard rejects cache\n // options when no cache instance was actually configured.\n [CacheConfig] extends [CacheInstance] ? { cache?: CachingStrategy } : {},\n Type,\n RequestContext\n>;\nexport function createStorefrontClient(args: CreateStorefrontClientArgs): StorefrontClient {\n const { config, requestContext, type: clientType } = args;\n\n if (!config.storeDomain) {\n throw new Error(\"storeDomain is required and must be non-empty\");\n }\n\n const storeUrl = normalizeStoreDomain(config.storeDomain);\n const apiVersion = config.apiVersion ?? STOREFRONT_API_VERSION;\n const apiUrl = `${storeUrl}/api/${apiVersion}/graphql.json`;\n\n const originFetch = config.fetch ?? globalThis.fetch;\n if (typeof originFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n const resolvedFetch = config.cache\n ? createFetchWithCache({\n cache: config.cache,\n waitUntil: config.waitUntil,\n fetch: originFetch as typeof globalThis.fetch,\n })\n : originFetch;\n\n const timeoutInMs = config.defaultTimeoutInMs ?? DEFAULT_TIMEOUT_IN_MS;\n if (timeoutInMs < 0 || Number.isNaN(timeoutInMs)) {\n throw new Error(\"defaultTimeoutInMs must be a non-negative number\");\n }\n\n const staticHeaders: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (config.storefrontId) {\n staticHeaders[STOREFRONT_ID_HEADER] = config.storefrontId;\n }\n\n switch (clientType) {\n case \"private\":\n case \"private_no_buyer_context\":\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"private storefront tokens cannot be used in a browser context. Use a publicStorefrontToken instead.\",\n );\n }\n if (!config.privateStorefrontToken) {\n throw new Error(\"privateStorefrontToken is required\");\n }\n staticHeaders[STOREFRONT_PRIVATE_TOKEN_HEADER] = config.privateStorefrontToken;\n break;\n case \"public\":\n if (config.publicStorefrontToken !== undefined && !config.publicStorefrontToken) {\n throw new Error(\"publicStorefrontToken must be non-empty when provided\");\n }\n if (config.publicStorefrontToken) {\n staticHeaders[STOREFRONT_ACCESS_TOKEN_HEADER] = config.publicStorefrontToken;\n }\n break;\n default:\n clientType satisfies never;\n throw new Error(`Unsupported client type: ${clientType}`);\n }\n\n const i18n = requestContext.i18n;\n const requestHeaders = new Headers(staticHeaders);\n requestContext.applyStorefrontRequestHeaders(requestHeaders);\n\n if (clientType === \"private\") {\n const { buyerIp } = requestContext;\n if (!buyerIp) {\n throw new TypeError(\"requestContext.buyerIp is required for private Storefront API clients\");\n }\n requestHeaders.set(STOREFRONT_BUYER_IP_HEADER, buyerIp);\n }\n\n async function graphql(\n doc: DocLike | string,\n ...rest: [options?: Record<string, unknown>]\n ): Promise<StorefrontGraphqlResult<DocLike>> {\n const opts = (rest[0] ?? {}) as {\n variables?: Record<string, unknown>;\n signal?: AbortSignal;\n cache?: CachingStrategy;\n };\n\n const queryText = typeof doc === \"string\" ? doc : (doc as unknown as string);\n if (typeof queryText !== \"string\") {\n throw new StorefrontApiError(\n \"Expected gql() to return a string at runtime. Check gql.tada version compatibility.\",\n );\n }\n\n if (opts.cache && !config.cache) {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache options require a cache configured in createStorefrontClient().\",\n );\n }\n\n if (opts.cache?.mode === \"private\") {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache mode 'private' is not supported yet. Use Cache() without private mode, or cache buyer-specific work with createRunWithCache() and an explicit private key.\",\n );\n }\n\n const variables = buildVariables(queryText, opts.variables, i18n);\n const externalSignals = [requestContext.signal, opts.signal].filter(\n (signal): signal is AbortSignal => Boolean(signal),\n );\n\n const earlyAbortSignal = externalSignals.find((signal) => signal.aborted);\n if (earlyAbortSignal) {\n throw (\n earlyAbortSignal.reason ??\n new DOMException(\"signal is aborted without reason\", \"AbortError\")\n );\n }\n\n let requestId: string | undefined;\n let responseHeaders: Headers;\n let responseText: string;\n let status: number;\n let timeoutSignal: AbortSignal | undefined;\n\n if (timeoutInMs > 0) {\n timeoutSignal = AbortSignal.timeout(timeoutInMs);\n externalSignals.push(timeoutSignal);\n }\n\n try {\n const init: PlainRequestInit = {\n method: \"POST\",\n headers: new Headers(requestHeaders),\n body: JSON.stringify({ query: queryText, variables }),\n signal: externalSignals.length > 0 ? AbortSignal.any(externalSignals) : undefined,\n };\n\n const cacheOptions = createStorefrontCacheOptions(apiUrl, init, queryText, opts.cache);\n const response = await (resolvedFetch as ResolvedStorefrontFetch)(apiUrl, init, cacheOptions);\n\n responseHeaders = response.headers;\n const hydrogenCacheStatus = getHydrogenCacheStatus(responseHeaders);\n if (hydrogenCacheStatus !== \"hit\") {\n requestContext.captureSubrequestHeaders(responseHeaders);\n }\n\n requestId = responseHeaders.get(\"x-request-id\") ?? undefined;\n status = response.status;\n\n if (!response.ok) {\n const bodyHint = await response.text().catch(() => \"\");\n throw new StorefrontApiError(\n `SFAPI responded with ${status}${bodyHint ? `: ${bodyHint.slice(0, 200)}` : \"\"}`,\n { status, requestId, queryText, variables },\n );\n }\n\n responseText = await response.text();\n } catch (error) {\n if (error instanceof StorefrontApiError) throw error;\n if (error instanceof StorefrontTimeoutError) throw error;\n if (error instanceof StorefrontCacheConfigError) throw error;\n if (\n error instanceof DOMException &&\n error.name === \"TimeoutError\" &&\n timeoutSignal?.aborted\n ) {\n throw new StorefrontTimeoutError(timeoutInMs);\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new StorefrontApiError(\"SFAPI request failed\", { cause: error });\n }\n\n let json: unknown;\n try {\n json = JSON.parse(responseText);\n } catch (cause) {\n throw new StorefrontApiError(\n `Failed to parse SFAPI response as JSON (${apiUrl}, status ${status})` +\n (responseText ? `: ${responseText.slice(0, 200)}` : \"\"),\n { status, requestId, cause, queryText, variables },\n );\n }\n\n if (typeof json !== \"object\" || json === null || Array.isArray(json)) {\n throw new StorefrontApiError(\n `SFAPI returned unexpected JSON type: ${Array.isArray(json) ? \"array\" : typeof json}`,\n { status, requestId, queryText, variables },\n );\n }\n\n const body = json as { data?: unknown; errors?: GraphQLFormattedError[] };\n // Branch to populate the two arms of the StorefrontGraphqlResult union: callers\n // narrowing on `errors` get a non-null `data` only when no errors are present.\n if (body.errors) {\n return {\n data: body.data ?? null,\n errors: body.errors,\n headers: responseHeaders,\n };\n }\n return {\n data: body.data,\n headers: responseHeaders,\n };\n }\n\n return {\n type: clientType,\n i18n,\n graphql: graphql as StorefrontClient[\"graphql\"],\n apiUrl,\n storeUrl,\n storefrontId: config.storefrontId,\n requestContext,\n };\n}\n\nfunction buildVariables(\n queryText: string,\n userVariables: Record<string, unknown> | undefined,\n resolvedI18n: I18nConfig,\n): Record<string, unknown> {\n const variables: Record<string, unknown> = { ...userVariables };\n\n if (COUNTRY_VAR_RE.test(queryText)) {\n variables.country = resolvedI18n.country;\n }\n if (LANGUAGE_VAR_RE.test(queryText)) {\n variables.language = resolvedI18n.language;\n }\n\n return variables;\n}\n\ntype PlainRequestInit = RequestInit & { headers: Headers; body: string; method: string };\n\nfunction createStorefrontCacheOptions(\n apiUrl: string,\n init: PlainRequestInit,\n queryText: string,\n strategy: CachingStrategy | undefined,\n): FetchCacheOptions | undefined {\n if (!strategy) return undefined;\n\n // Runtime only needs a safety belt for untyped callers. False positives are\n // acceptable because they bypass cache rather than risking mutation caching.\n if (/\\bmutation\\b/.test(queryText)) return undefined;\n\n return {\n key: createStorefrontCacheKey(apiUrl, init),\n strategy,\n shouldCacheResponse: async ({ json }: FetchCacheResponseContext) => {\n if (!json) return false;\n\n const body = await json().catch(() => undefined);\n return (\n typeof body === \"object\" && body != null && !Array.isArray(body) && !(\"errors\" in body)\n );\n },\n };\n}\n\nfunction createStorefrontCacheKey(apiUrl: string, init: PlainRequestInit) {\n return [\n \"storefront-api\",\n apiUrl,\n init.method,\n init.body,\n [...init.headers]\n .filter(([name]) => {\n const normalized = name.toLowerCase();\n return (\n REQUEST_CACHE_KEY_HEADERS.has(normalized) && !REQUEST_IDENTITY_HEADERS.has(normalized)\n );\n })\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([name, value]) => `${name}:${value}`)\n .join(\"\\n\"),\n ] as const;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import type { TadaDocumentNode } from \"gql.tada\";\n\nimport { getHydrogenCacheStatus } from \"../core/cache/cache-status\";\nimport { createFetchWithCache } from \"../core/cache/fetch-with-cache\";\nimport type { FetchCacheOptions, FetchCacheResponseContext } from \"../core/cache/fetch-with-cache\";\nimport type { CacheInstance } from \"../core/cache/run-with-cache\";\nimport type { CachingStrategy } from \"../core/cache/strategies\";\nimport { DEFAULT_TIMEOUT_IN_MS, STOREFRONT_API_VERSION } from \"../core/constants\";\nimport {\n HYDROGEN_VERSION_HEADER,\n REQUEST_GROUP_ID_HEADER,\n SDK_VARIANT_HEADER,\n SDK_VARIANT_SOURCE_HEADER,\n SDK_VERSION_HEADER,\n SHOPIFY_STOREFRONT_S_HEADER,\n SHOPIFY_STOREFRONT_Y_HEADER,\n STOREFRONT_ACCESS_TOKEN_HEADER,\n STOREFRONT_BUYER_IP_HEADER,\n STOREFRONT_ID_HEADER,\n STOREFRONT_PRIVATE_TOKEN_HEADER,\n SHOPIFY_UNIQUE_TOKEN_HEADER,\n SHOPIFY_VISIT_TOKEN_HEADER,\n} from \"../core/headers\";\nimport type { I18nConfig, ShopifyRequestContext } from \"../core/request-context\";\nimport { normalizeStoreDomain } from \"../core/url\";\nimport type { AnyStorefrontQueryString } from \"../graphql\";\nimport { StorefrontApiError, StorefrontTimeoutError } from \"./errors\";\nimport type {\n ClientType,\n CreateStorefrontClientArgs,\n GraphQLFormattedError,\n StorefrontClient,\n StorefrontGraphqlResult,\n} from \"./types\";\n\ntype DocLike = TadaDocumentNode<unknown, unknown> | AnyStorefrontQueryString;\ntype FetchInput = Parameters<typeof globalThis.fetch>[0];\ntype FetchInit = Parameters<typeof globalThis.fetch>[1];\ntype ResolvedStorefrontFetch = (\n input: FetchInput,\n init: FetchInit,\n cacheOptions: FetchCacheOptions | undefined,\n) => Promise<Response>;\n\nconst COUNTRY_VAR_RE = /\\$country\\s*:/;\nconst LANGUAGE_VAR_RE = /\\$language\\s*:/;\nconst REQUEST_CACHE_KEY_HEADERS = new Set([\n \"content-type\",\n \"user-agent\",\n SDK_VARIANT_HEADER.toLowerCase(),\n SDK_VARIANT_SOURCE_HEADER.toLowerCase(),\n SDK_VERSION_HEADER.toLowerCase(),\n HYDROGEN_VERSION_HEADER.toLowerCase(),\n STOREFRONT_ACCESS_TOKEN_HEADER.toLowerCase(),\n STOREFRONT_PRIVATE_TOKEN_HEADER.toLowerCase(),\n]);\nconst REQUEST_IDENTITY_HEADERS = new Set([\n \"cookie\",\n REQUEST_GROUP_ID_HEADER.toLowerCase(),\n STOREFRONT_BUYER_IP_HEADER.toLowerCase(),\n SHOPIFY_UNIQUE_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_VISIT_TOKEN_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_Y_HEADER.toLowerCase(),\n SHOPIFY_STOREFRONT_S_HEADER.toLowerCase(),\n]);\nconst CACHE_ENABLED_GRAPHQL_CLIENTS = new WeakSet<StorefrontClient[\"graphql\"]>();\nconst SHOULD_CACHE_RESULT = Symbol();\n\ntype ShouldCacheResult = (body: object) => boolean;\n\nclass StorefrontCacheConfigError extends Error {}\n\nexport function withStorefrontClientCache<TOptions extends object>(\n client: Pick<StorefrontClient, \"graphql\">,\n options: TOptions,\n strategy: CachingStrategy,\n shouldCacheResult?: ShouldCacheResult,\n): TOptions {\n if (!CACHE_ENABLED_GRAPHQL_CLIENTS.has(client.graphql)) return options;\n\n return { ...options, cache: strategy, [SHOULD_CACHE_RESULT]: shouldCacheResult };\n}\n\n/**\n * Creates a type-safe Storefront API client.\n *\n * Pick a `type` based on where your code runs and whether buyer context is available:\n *\n * | | public | private | private_no_buyer_context |\n * |-----------------|--------------------------------|-------------------------------|--------------------------|\n * | Runs in | Browser or mobile | Server (SSR) | Server (background) |\n * | Token | Public access token, or none | Private access token | Private access token |\n * | Buyer context | Browser request context | You forward trusted `buyerIp` | None |\n * | Best for | Client-side fetches | SSR with buyer context | Webhooks, background jobs |\n *\n * Pass `requestContext` so the client's SFAPI requests carry request-scoped\n * headers, follow the incoming request's abort signal, and read the resolved\n * `i18n` used for Storefront API variable injection and localized routes.\n *\n * Token-based access is required for some Storefront API fields, including\n * product tags, metaobjects, metafields, menus, and customers. Use a private\n * client only in trusted server code where the token cannot leak to browsers.\n *\n * @see {@link https://shopify.dev/docs/api/storefront#authentication | Storefront API authentication}\n */\nexport function createStorefrontClient<\n const Type extends ClientType,\n const RequestContext extends ShopifyRequestContext,\n const CacheConfig extends CacheInstance | undefined = undefined,\n>(\n args: CreateStorefrontClientArgs<RequestContext, Type, CacheConfig>,\n): StorefrontClient<\n // Tuple-wrapped to stay non-distributive so an explicit\n // `CacheInstance | undefined` type argument yields one client type, not a\n // union. Note a *value* of that type still unlocks cache options: optional\n // property inference strips `undefined` before it reaches CacheConfig, so\n // definiteness is undecidable here — the runtime guard rejects cache\n // options when no cache instance was actually configured.\n [CacheConfig] extends [CacheInstance] ? { cache?: CachingStrategy } : {},\n Type,\n RequestContext\n>;\nexport function createStorefrontClient(args: CreateStorefrontClientArgs): StorefrontClient {\n const { config, requestContext, type: clientType } = args;\n\n if (!config.storeDomain) {\n throw new Error(\"storeDomain is required and must be non-empty\");\n }\n\n const storeUrl = normalizeStoreDomain(config.storeDomain);\n const apiVersion = config.apiVersion ?? STOREFRONT_API_VERSION;\n const apiUrl = `${storeUrl}/api/${apiVersion}/graphql.json`;\n\n const originFetch = config.fetch ?? globalThis.fetch;\n if (typeof originFetch !== \"function\") {\n throw new Error(\n \"No fetch function available. Pass a fetch option or ensure globalThis.fetch exists.\",\n );\n }\n const resolvedFetch = config.cache\n ? createFetchWithCache({\n cache: config.cache,\n waitUntil: config.waitUntil,\n fetch: originFetch as typeof globalThis.fetch,\n })\n : originFetch;\n\n const timeoutInMs = config.defaultTimeoutInMs ?? DEFAULT_TIMEOUT_IN_MS;\n if (timeoutInMs < 0 || Number.isNaN(timeoutInMs)) {\n throw new Error(\"defaultTimeoutInMs must be a non-negative number\");\n }\n\n const staticHeaders: Record<string, string> = {\n \"content-type\": \"application/json\",\n };\n if (config.storefrontId) {\n staticHeaders[STOREFRONT_ID_HEADER] = config.storefrontId;\n }\n\n switch (clientType) {\n case \"private\":\n case \"private_no_buyer_context\":\n if (typeof document !== \"undefined\") {\n throw new Error(\n \"private storefront tokens cannot be used in a browser context. Use a publicStorefrontToken instead.\",\n );\n }\n if (!config.privateStorefrontToken) {\n throw new Error(\"privateStorefrontToken is required\");\n }\n staticHeaders[STOREFRONT_PRIVATE_TOKEN_HEADER] = config.privateStorefrontToken;\n break;\n case \"public\":\n if (config.publicStorefrontToken !== undefined && !config.publicStorefrontToken) {\n throw new Error(\"publicStorefrontToken must be non-empty when provided\");\n }\n if (config.publicStorefrontToken) {\n staticHeaders[STOREFRONT_ACCESS_TOKEN_HEADER] = config.publicStorefrontToken;\n }\n break;\n default:\n clientType satisfies never;\n throw new Error(`Unsupported client type: ${clientType}`);\n }\n\n const i18n = requestContext.i18n;\n const requestHeaders = new Headers(staticHeaders);\n requestContext.applyStorefrontRequestHeaders(requestHeaders);\n\n if (clientType === \"private\") {\n const { buyerIp } = requestContext;\n if (!buyerIp) {\n throw new TypeError(\"requestContext.buyerIp is required for private Storefront API clients\");\n }\n requestHeaders.set(STOREFRONT_BUYER_IP_HEADER, buyerIp);\n }\n\n async function graphql(\n doc: DocLike | string,\n ...rest: [options?: Record<string, unknown>]\n ): Promise<StorefrontGraphqlResult<DocLike>> {\n const opts = (rest[0] ?? {}) as {\n variables?: Record<string, unknown>;\n signal?: AbortSignal;\n cache?: CachingStrategy;\n [SHOULD_CACHE_RESULT]?: ShouldCacheResult;\n };\n\n const queryText = typeof doc === \"string\" ? doc : (doc as unknown as string);\n if (typeof queryText !== \"string\") {\n throw new StorefrontApiError(\n \"Expected gql() to return a string at runtime. Check gql.tada version compatibility.\",\n );\n }\n\n if (opts.cache && !config.cache) {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache options require a cache configured in createStorefrontClient().\",\n );\n }\n\n if (opts.cache?.mode === \"private\") {\n throw new StorefrontCacheConfigError(\n \"Storefront API cache mode 'private' is not supported yet. Use Cache() without private mode, or cache buyer-specific work with createRunWithCache() and an explicit private key.\",\n );\n }\n\n const variables = buildVariables(queryText, opts.variables, i18n);\n const externalSignals = [requestContext.signal, opts.signal].filter(\n (signal): signal is AbortSignal => Boolean(signal),\n );\n\n const earlyAbortSignal = externalSignals.find((signal) => signal.aborted);\n if (earlyAbortSignal) {\n throw (\n earlyAbortSignal.reason ??\n new DOMException(\"signal is aborted without reason\", \"AbortError\")\n );\n }\n\n let requestId: string | undefined;\n let responseHeaders: Headers;\n let responseText: string;\n let status: number;\n let timeoutSignal: AbortSignal | undefined;\n\n if (timeoutInMs > 0) {\n timeoutSignal = AbortSignal.timeout(timeoutInMs);\n externalSignals.push(timeoutSignal);\n }\n\n try {\n const init: PlainRequestInit = {\n method: \"POST\",\n headers: new Headers(requestHeaders),\n body: JSON.stringify({ query: queryText, variables }),\n signal: externalSignals.length > 0 ? AbortSignal.any(externalSignals) : undefined,\n };\n\n const cacheOptions = createStorefrontCacheOptions(\n apiUrl,\n init,\n queryText,\n opts.cache,\n opts[SHOULD_CACHE_RESULT],\n );\n const response = await (resolvedFetch as ResolvedStorefrontFetch)(apiUrl, init, cacheOptions);\n\n responseHeaders = response.headers;\n const hydrogenCacheStatus = getHydrogenCacheStatus(responseHeaders);\n if (hydrogenCacheStatus !== \"hit\") {\n requestContext.captureSubrequestHeaders(responseHeaders);\n }\n\n requestId = responseHeaders.get(\"x-request-id\") ?? undefined;\n status = response.status;\n\n if (!response.ok) {\n const bodyHint = await response.text().catch(() => \"\");\n throw new StorefrontApiError(\n `SFAPI responded with ${status}${bodyHint ? `: ${bodyHint.slice(0, 200)}` : \"\"}`,\n { status, requestId, queryText, variables },\n );\n }\n\n responseText = await response.text();\n } catch (error) {\n if (error instanceof StorefrontApiError) throw error;\n if (error instanceof StorefrontTimeoutError) throw error;\n if (error instanceof StorefrontCacheConfigError) throw error;\n if (\n error instanceof DOMException &&\n error.name === \"TimeoutError\" &&\n timeoutSignal?.aborted\n ) {\n throw new StorefrontTimeoutError(timeoutInMs);\n }\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n throw new StorefrontApiError(\"SFAPI request failed\", { cause: error });\n }\n\n let json: unknown;\n try {\n json = JSON.parse(responseText);\n } catch (cause) {\n throw new StorefrontApiError(\n `Failed to parse SFAPI response as JSON (${apiUrl}, status ${status})` +\n (responseText ? `: ${responseText.slice(0, 200)}` : \"\"),\n { status, requestId, cause, queryText, variables },\n );\n }\n\n if (typeof json !== \"object\" || json === null || Array.isArray(json)) {\n throw new StorefrontApiError(\n `SFAPI returned unexpected JSON type: ${Array.isArray(json) ? \"array\" : typeof json}`,\n { status, requestId, queryText, variables },\n );\n }\n\n const body = json as { data?: unknown; errors?: GraphQLFormattedError[] };\n // Branch to populate the two arms of the StorefrontGraphqlResult union: callers\n // narrowing on `errors` get a non-null `data` only when no errors are present.\n if (body.errors) {\n return {\n data: body.data ?? null,\n errors: body.errors,\n headers: responseHeaders,\n };\n }\n return {\n data: body.data,\n headers: responseHeaders,\n };\n }\n\n const client: StorefrontClient = {\n type: clientType,\n i18n,\n graphql: graphql as StorefrontClient[\"graphql\"],\n apiUrl,\n storeUrl,\n storefrontId: config.storefrontId,\n requestContext,\n };\n\n if (config.cache) CACHE_ENABLED_GRAPHQL_CLIENTS.add(client.graphql);\n\n return client;\n}\n\nfunction buildVariables(\n queryText: string,\n userVariables: Record<string, unknown> | undefined,\n resolvedI18n: I18nConfig,\n): Record<string, unknown> {\n const variables: Record<string, unknown> = { ...userVariables };\n\n if (COUNTRY_VAR_RE.test(queryText)) {\n variables.country = resolvedI18n.country;\n }\n if (LANGUAGE_VAR_RE.test(queryText)) {\n variables.language = resolvedI18n.language;\n }\n\n return variables;\n}\n\ntype PlainRequestInit = RequestInit & { headers: Headers; body: string; method: string };\n\nfunction createStorefrontCacheOptions(\n apiUrl: string,\n init: PlainRequestInit,\n queryText: string,\n strategy: CachingStrategy | undefined,\n shouldCacheResult: ShouldCacheResult | undefined,\n): FetchCacheOptions | undefined {\n if (!strategy) return undefined;\n\n // Runtime only needs a safety belt for untyped callers. False positives are\n // acceptable because they bypass cache rather than risking mutation caching.\n if (/\\bmutation\\b/.test(queryText)) return undefined;\n\n return {\n key: createStorefrontCacheKey(apiUrl, init),\n strategy,\n shouldCacheResponse: async ({ json }: FetchCacheResponseContext) => {\n if (!json) return false;\n\n const body = await json().catch(() => undefined);\n if (typeof body !== \"object\" || body == null || Array.isArray(body) || \"errors\" in body) {\n return false;\n }\n\n return shouldCacheResult?.(body) ?? true;\n },\n };\n}\n\nfunction createStorefrontCacheKey(apiUrl: string, init: PlainRequestInit) {\n return [\n \"storefront-api\",\n apiUrl,\n init.method,\n init.body,\n [...init.headers]\n .filter(([name]) => {\n const normalized = name.toLowerCase();\n return (\n REQUEST_CACHE_KEY_HEADERS.has(normalized) && !REQUEST_IDENTITY_HEADERS.has(normalized)\n );\n })\n .toSorted(([left], [right]) => left.localeCompare(right))\n .map(([name, value]) => `${name}:${value}`)\n .join(\"\\n\"),\n ] as const;\n}\n"],"mappings":"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"}
@@ -13,6 +13,11 @@ type CartStore = {
13
13
  getState(): CartState;
14
14
  subscribe(listener: (state: CartState) => void): () => void;
15
15
  fetch(): Promise<void>;
16
+ /**
17
+ * Reconciles the cart after an out-of-band mutation: revalidates the current
18
+ * cart, or loads one when none is present yet (e.g. just created server-side).
19
+ */
20
+ refresh(): void;
16
21
  reset(): void;
17
22
  handleFormSubmit(event: SubmitEvent, eventDetail?: Record<string, unknown>): Promise<void>;
18
23
  };
@@ -973,6 +973,9 @@ function markOverlappingTransactionForRevalidation(store, transaction) {
973
973
  if (store.transactions.length === 0 && !store.revalidation.active && store.activeMutationTransports.size === 0) return;
974
974
  transaction.requiresRevalidation = true;
975
975
  for (const pending of store.transactions) pending.requiresRevalidation = true;
976
+ requestCartRevalidation(store);
977
+ }
978
+ function requestCartRevalidation(store) {
976
979
  store.revalidation.requested = true;
977
980
  store.revalidation.visible = true;
978
981
  store.revalidation.active?.controller.abort();
@@ -1418,6 +1421,7 @@ function createCartStore(options = {}) {
1418
1421
  getState: () => store.observable.state,
1419
1422
  subscribe: (listener) => store.observable.subscribe(listener),
1420
1423
  fetch: () => loadCartInStore(store),
1424
+ refresh: () => refreshCartInStore(store),
1421
1425
  reset: () => resetCartStore(store),
1422
1426
  handleFormSubmit: (event, eventDetail) => handleFormSubmitInStore(store, handlers, event, eventDetail)
1423
1427
  };
@@ -1579,8 +1583,8 @@ async function fetchCart(cartId, callerSignal) {
1579
1583
  if (!getCart) throw new Error("Standard Actions not available. Ensure the Shopify script tag is loaded before calling fetchCart.");
1580
1584
  return { cart: (await getCart(cartId ? { cartId } : void 0, { signal })).cart };
1581
1585
  }
1582
- function fetchCartData(cartId) {
1583
- return fetchCart(cartId).then(({ cart }) => cart ? {
1586
+ function fetchCartData(cartId, signal) {
1587
+ return fetchCart(cartId, signal).then(({ cart }) => cart ? {
1584
1588
  ...cart,
1585
1589
  checkoutUrl: cart.checkoutUrl ?? null,
1586
1590
  note: extractNoteFromCart(cart),
@@ -1611,35 +1615,51 @@ function fetchCartRevalidationData(cartId, signal) {
1611
1615
  return fetchCart(cartId, signal).then(({ cart }) => cart ? cartResponseFromStandardEvent(cart) : null);
1612
1616
  }
1613
1617
  function isCurrentCartRevalidationRequest(store, request) {
1614
- return store.revalidation.active === request && store.generation === request.generation && store.mutationRevision === request.mutationRevision && store.transactions.length === 0 && store.settled.data.id === request.cartId;
1615
- }
1616
- function isCurrentCartRevalidation(store, request, cart) {
1617
- return isCurrentCartRevalidationRequest(store, request) && cart.id === request.cartId;
1618
+ return !request.controller.signal.aborted && store.revalidation.active === request && store.generation === request.generation && store.mutationRevision === request.mutationRevision && store.transactions.length === 0 && store.settled.data.id === request.cartId;
1618
1619
  }
1619
- function revalidateCartWhenIdle(store) {
1620
- if (!store.revalidation.requested || store.revalidation.active || store.transactions.length > 0 || store.activeMutationTransports.size > 0) return;
1621
- const cartId = store.settled.data.id;
1622
- if (!cartId) {
1623
- store.revalidation.requested = false;
1620
+ function applyCartRevalidation(store, request, cart) {
1621
+ if (!isCurrentCartRevalidationRequest(store, request)) return;
1622
+ if (request.cartId === null) {
1623
+ if (cart) {
1624
+ hydrateCartInStore(store, cart);
1625
+ return;
1626
+ }
1624
1627
  store.revalidation.visible = false;
1625
1628
  publishVisibleState(store);
1626
1629
  return;
1627
1630
  }
1631
+ if (!cart) throw new Error(CART_REVALIDATION_ERROR_MESSAGE);
1632
+ if (cart.id !== request.cartId) {
1633
+ hydrateCartInStore(store, cart);
1634
+ return;
1635
+ }
1636
+ store.settled = {
1637
+ ...store.settled,
1638
+ data: mergeAuthoritativeCartData(store.settled.data, cart)
1639
+ };
1640
+ store.revalidation.visible = false;
1641
+ publishVisibleState(store);
1642
+ }
1643
+ function refreshCartInStore(store) {
1644
+ const activeCartLoad = store.activeCartLoad;
1645
+ if (activeCartLoad) {
1646
+ const runAfterLoad = () => refreshCartInStore(store);
1647
+ activeCartLoad.promise.then(runAfterLoad, runAfterLoad);
1648
+ return;
1649
+ }
1650
+ requestCartRevalidation(store);
1651
+ publishVisibleState(store);
1652
+ revalidateCartWhenIdle(store);
1653
+ }
1654
+ function revalidateCartWhenIdle(store) {
1655
+ if (!store.revalidation.requested || store.revalidation.active || store.transactions.length > 0 || store.activeMutationTransports.size > 0) return;
1628
1656
  store.revalidation.requested = false;
1629
1657
  clearProjectedErrors(store, [REVALIDATION_ERROR_KEY]);
1630
1658
  publishVisibleState(store);
1659
+ const cartId = store.settled.data.id;
1631
1660
  const controller = new AbortController();
1632
1661
  let request;
1633
- const promise = fetchCartRevalidationData(cartId, controller.signal).then((cart) => {
1634
- if (!cart) throw new Error(CART_REVALIDATION_ERROR_MESSAGE);
1635
- if (!isCurrentCartRevalidation(store, request, cart)) return;
1636
- store.settled = {
1637
- ...store.settled,
1638
- data: mergeAuthoritativeCartData(store.settled.data, cart)
1639
- };
1640
- store.revalidation.visible = false;
1641
- publishVisibleState(store);
1642
- }).catch((error) => {
1662
+ const promise = (cartId ? fetchCartRevalidationData(cartId, controller.signal) : fetchCartData(null, controller.signal)).then((cart) => applyCartRevalidation(store, request, cart)).catch((error) => {
1643
1663
  if (isAbortError(error) || !isCurrentCartRevalidationRequest(store, request)) return;
1644
1664
  store.revalidation.visible = false;
1645
1665
  addProjectedError(store, [REVALIDATION_ERROR_KEY], (state, timestampMs) => writeNetworkError(state, new Error(CART_REVALIDATION_ERROR_MESSAGE), timestampMs));