@shopify/cli-hydrogen 5.2.2 → 5.3.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 (48) hide show
  1. package/dist/commands/hydrogen/build.js +49 -25
  2. package/dist/commands/hydrogen/deploy.js +171 -0
  3. package/dist/commands/hydrogen/deploy.test.js +185 -0
  4. package/dist/commands/hydrogen/dev.js +27 -14
  5. package/dist/commands/hydrogen/init.js +10 -6
  6. package/dist/commands/hydrogen/init.test.js +16 -1
  7. package/dist/commands/hydrogen/preview.js +27 -11
  8. package/dist/generator-templates/starter/app/root.tsx +6 -4
  9. package/dist/generator-templates/starter/app/routes/account.tsx +1 -1
  10. package/dist/generator-templates/starter/app/routes/cart.$lines.tsx +70 -0
  11. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -1
  12. package/dist/generator-templates/starter/app/routes/discount.$code.tsx +43 -0
  13. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +3 -1
  14. package/dist/generator-templates/starter/package.json +4 -4
  15. package/dist/generator-templates/starter/remix.env.d.ts +12 -3
  16. package/dist/generator-templates/starter/server.ts +22 -19
  17. package/dist/generator-templates/starter/tsconfig.json +1 -1
  18. package/dist/lib/bundle/analyzer.js +56 -0
  19. package/dist/lib/bundle/bundle-analyzer.html +2045 -0
  20. package/dist/lib/flags.js +4 -0
  21. package/dist/lib/get-oxygen-token.js +47 -0
  22. package/dist/lib/get-oxygen-token.test.js +104 -0
  23. package/dist/lib/graphql/admin/oxygen-token.js +21 -0
  24. package/dist/lib/live-reload.js +2 -1
  25. package/dist/lib/log.js +56 -13
  26. package/dist/lib/mini-oxygen/common.js +58 -0
  27. package/dist/lib/mini-oxygen/index.js +12 -0
  28. package/dist/lib/mini-oxygen/node.js +110 -0
  29. package/dist/lib/mini-oxygen/types.js +1 -0
  30. package/dist/lib/mini-oxygen/workerd-inspector.js +392 -0
  31. package/dist/lib/mini-oxygen/workerd.js +182 -0
  32. package/dist/lib/onboarding/common.js +24 -13
  33. package/dist/lib/onboarding/local.js +1 -1
  34. package/dist/lib/remix-config.js +12 -2
  35. package/dist/lib/remix-version-check.js +7 -4
  36. package/dist/lib/remix-version-check.test.js +1 -1
  37. package/dist/lib/render-errors.js +1 -1
  38. package/dist/lib/request-events.js +84 -0
  39. package/dist/lib/setups/routes/generate.js +3 -3
  40. package/dist/lib/transpile-ts.js +21 -23
  41. package/dist/lib/virtual-routes.js +11 -9
  42. package/dist/virtual-routes/components/FlameChartWrapper.jsx +125 -0
  43. package/dist/virtual-routes/routes/debug-network.jsx +289 -0
  44. package/dist/virtual-routes/routes/index.jsx +4 -4
  45. package/dist/virtual-routes/virtual-root.jsx +7 -4
  46. package/oclif.manifest.json +81 -3
  47. package/package.json +35 -12
  48. package/dist/lib/mini-oxygen.js +0 -108
@@ -0,0 +1,392 @@
1
+ import { dirname } from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { SourceMapConsumer } from 'source-map';
4
+ import { parse } from 'stack-trace';
5
+ import WebSocket from 'ws';
6
+
7
+ async function findInspectorUrl(inspectorPort) {
8
+ try {
9
+ const jsonUrl = `http://127.0.0.1:${inspectorPort}/json`;
10
+ const body = await (await fetch(jsonUrl)).json();
11
+ return body?.find(({ id }) => id === "core:user:hydrogen")?.webSocketDebuggerUrl;
12
+ } catch (error) {
13
+ console.error("Error attempting to retrieve debugger URL:", error);
14
+ }
15
+ }
16
+ function connectToInspector({
17
+ inspectorUrl,
18
+ sourceMapPath
19
+ }) {
20
+ const messageCounterRef = { value: -1 };
21
+ const getMessageId = () => messageCounterRef.value--;
22
+ const pendingMessages = /* @__PURE__ */ new Map();
23
+ const ws = new WebSocket(inspectorUrl);
24
+ let keepAliveInterval;
25
+ const isClosed = () => ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING;
26
+ const send = (method, params) => {
27
+ if (!isClosed()) {
28
+ const id = getMessageId();
29
+ let promiseResolve = void 0;
30
+ const promise = new Promise(
31
+ (resolve) => promiseResolve = resolve
32
+ );
33
+ pendingMessages.set(id, promiseResolve);
34
+ ws.send(JSON.stringify({ id, method, params }));
35
+ return promise;
36
+ }
37
+ return Promise.resolve(void 0);
38
+ };
39
+ const cleanupMessageQueue = (data) => {
40
+ try {
41
+ if (data?.id < 0) {
42
+ const resolve = pendingMessages.get(data.id);
43
+ if (resolve !== void 0) {
44
+ pendingMessages.delete(data.id);
45
+ resolve(data.result);
46
+ }
47
+ return true;
48
+ }
49
+ } catch (error) {
50
+ console.error(error);
51
+ }
52
+ return false;
53
+ };
54
+ function getPropertyValue(name, response) {
55
+ return response?.result.find((prop) => prop.name === name)?.value;
56
+ }
57
+ async function reconstructError(initialProperties, ro) {
58
+ let errorProperties = { ...initialProperties };
59
+ const objectId = ro?.objectId;
60
+ if (objectId) {
61
+ const [sourceMapConsumer, getPropertiesResponse] = await Promise.all([
62
+ getSourceMapConsumer(),
63
+ send("Runtime.getProperties", {
64
+ objectId,
65
+ ownProperties: false,
66
+ accessorPropertiesOnly: false,
67
+ generatePreview: false,
68
+ nonIndexedPropertiesOnly: false
69
+ })
70
+ ]);
71
+ const message = getPropertyValue("message", getPropertiesResponse);
72
+ if (message?.value) {
73
+ errorProperties.message = message.value;
74
+ }
75
+ const stack = getPropertyValue("stack", getPropertiesResponse);
76
+ if (stack?.value) {
77
+ errorProperties.stack = sourceMapConsumer ? formatStack(sourceMapConsumer, stack.value) : stack.value;
78
+ }
79
+ const cause = getPropertyValue("cause", getPropertiesResponse);
80
+ if (cause) {
81
+ errorProperties.cause = cause.description ?? cause.value;
82
+ if (cause.subtype === "error" && sourceMapConsumer && cause.description !== void 0) {
83
+ errorProperties.stack = formatStack(
84
+ sourceMapConsumer,
85
+ cause.description
86
+ );
87
+ }
88
+ }
89
+ const isDomException = ro?.className === "DOMException";
90
+ if (isDomException) {
91
+ const stackDescriptor = getPropertiesResponse?.result.find(
92
+ (prop) => prop.name === "stack"
93
+ );
94
+ const getObjectId = stackDescriptor?.get?.objectId;
95
+ if (getObjectId !== void 0) {
96
+ const callFunctionResponse = await send("Runtime.callFunctionOn", {
97
+ objectId,
98
+ functionDeclaration: "function invokeGetter(getter) { return Reflect.apply(getter, this, []); }",
99
+ arguments: [{ objectId: getObjectId }],
100
+ silent: true
101
+ });
102
+ if (callFunctionResponse !== void 0) {
103
+ const stack2 = callFunctionResponse.result.value;
104
+ if (typeof stack2 === "string" && sourceMapConsumer !== void 0) {
105
+ errorProperties.stack = formatStack(sourceMapConsumer, stack2);
106
+ } else {
107
+ try {
108
+ errorProperties.stack = JSON.stringify(stack2);
109
+ } catch {
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ const error = new Error(errorProperties.message);
117
+ error.stack = errorProperties.stack;
118
+ if (errorProperties.cause) {
119
+ error.cause = errorProperties.cause;
120
+ }
121
+ return error;
122
+ }
123
+ const sourceMapAbortController = new AbortController();
124
+ let sourceMapConsumerPromise;
125
+ const getSourceMapConsumer = () => {
126
+ return sourceMapConsumerPromise ??= (async () => {
127
+ if (!sourceMapPath || sourceMapAbortController.signal.aborted) {
128
+ return;
129
+ }
130
+ try {
131
+ const mapContent = await readFile(sourceMapPath, "utf-8");
132
+ if (sourceMapAbortController.signal.aborted)
133
+ return;
134
+ const map = JSON.parse(mapContent);
135
+ map.sourceRoot = dirname(sourceMapPath);
136
+ const sourceMapConsumer = await new SourceMapConsumer(map);
137
+ if (sourceMapAbortController.signal.aborted) {
138
+ sourceMapConsumer.destroy();
139
+ return;
140
+ }
141
+ sourceMapAbortController.signal.addEventListener("abort", () => {
142
+ sourceMapConsumerPromise = Promise.resolve(void 0);
143
+ sourceMapConsumer.destroy();
144
+ });
145
+ return sourceMapConsumer;
146
+ } catch {
147
+ }
148
+ })();
149
+ };
150
+ ws.addEventListener("message", async (event) => {
151
+ if (typeof event.data === "string") {
152
+ const evt = JSON.parse(event.data);
153
+ cleanupMessageQueue(evt);
154
+ if (evt.method === "Runtime.exceptionThrown") {
155
+ const params = evt.params;
156
+ const errorProperties = {};
157
+ const sourceMapConsumer = await getSourceMapConsumer();
158
+ if (sourceMapConsumer !== void 0) {
159
+ const message = params.exceptionDetails.exception?.description?.split("\n")[0];
160
+ const stack = params.exceptionDetails.stackTrace?.callFrames;
161
+ const formatted = formatStructuredError(
162
+ sourceMapConsumer,
163
+ message,
164
+ stack
165
+ );
166
+ errorProperties.message = params.exceptionDetails.text;
167
+ errorProperties.stack = formatted;
168
+ } else {
169
+ errorProperties.message = params.exceptionDetails.text + " " + (params.exceptionDetails.exception?.description ?? "");
170
+ }
171
+ console.error(
172
+ await reconstructError(
173
+ errorProperties,
174
+ params.exceptionDetails.exception
175
+ )
176
+ );
177
+ }
178
+ if (evt.method === "Runtime.consoleAPICalled") {
179
+ const params = evt.params;
180
+ await logConsoleMessage(params, reconstructError);
181
+ }
182
+ } else {
183
+ console.error("Unrecognised devtools event:", event);
184
+ }
185
+ });
186
+ ws.once("open", () => {
187
+ send("Runtime.enable");
188
+ keepAliveInterval = setInterval(() => send("Runtime.getIsolateId"), 1e4);
189
+ });
190
+ ws.on("unexpected-response", () => {
191
+ console.log("Waiting for connection...");
192
+ });
193
+ ws.once("close", () => {
194
+ clearInterval(keepAliveInterval);
195
+ sourceMapAbortController.abort();
196
+ });
197
+ return () => {
198
+ clearInterval(keepAliveInterval);
199
+ if (!isClosed()) {
200
+ try {
201
+ ws.close();
202
+ } catch (err) {
203
+ }
204
+ }
205
+ sourceMapAbortController.abort();
206
+ };
207
+ }
208
+ const mapConsoleAPIMessageTypeToConsoleMethod = {
209
+ log: "log",
210
+ debug: "debug",
211
+ info: "info",
212
+ warning: "warn",
213
+ error: "error",
214
+ dir: "dir",
215
+ dirxml: "dirxml",
216
+ table: "table",
217
+ trace: "trace",
218
+ clear: "clear",
219
+ count: "count",
220
+ assert: "assert",
221
+ profile: "profile",
222
+ profileEnd: "profileEnd",
223
+ timeEnd: "timeEnd",
224
+ startGroup: "group",
225
+ startGroupCollapsed: "groupCollapsed",
226
+ endGroup: "groupEnd"
227
+ };
228
+ async function logConsoleMessage(evt, reconstructError) {
229
+ const args = [];
230
+ for (const ro of evt.args) {
231
+ switch (ro.type) {
232
+ case "string":
233
+ case "number":
234
+ case "boolean":
235
+ case "undefined":
236
+ case "symbol":
237
+ case "bigint":
238
+ args.push(ro.value);
239
+ break;
240
+ case "function":
241
+ args.push(`[Function: ${ro.description ?? "<no-description>"}]`);
242
+ break;
243
+ case "object":
244
+ if (!ro.preview) {
245
+ args.push(
246
+ ro.subtype === "null" ? "null" : ro.description ?? "<no-description>"
247
+ );
248
+ } else {
249
+ if (ro.preview.description)
250
+ args.push(ro.preview.description);
251
+ switch (ro.preview.subtype) {
252
+ case "array":
253
+ args.push(
254
+ "[ " + ro.preview.properties.map(({ value }) => {
255
+ return value;
256
+ }).join(", ") + (ro.preview.overflow ? "..." : "") + " ]"
257
+ );
258
+ break;
259
+ case "weakmap":
260
+ case "map":
261
+ ro.preview.entries === void 0 ? args.push("{}") : args.push(
262
+ "{\n" + ro.preview.entries.map(({ key, value }) => {
263
+ return ` ${key?.description ?? "<unknown>"} => ${value.description}`;
264
+ }).join(",\n") + (ro.preview.overflow ? "\n ..." : "") + "\n}"
265
+ );
266
+ break;
267
+ case "weakset":
268
+ case "set":
269
+ ro.preview.entries === void 0 ? args.push("{}") : args.push(
270
+ "{ " + ro.preview.entries.map(({ value }) => {
271
+ return `${value.description}`;
272
+ }).join(", ") + (ro.preview.overflow ? ", ..." : "") + " }"
273
+ );
274
+ break;
275
+ case "regexp":
276
+ break;
277
+ case "date":
278
+ break;
279
+ case "generator":
280
+ args.push(ro.preview?.properties[0]?.value || "");
281
+ break;
282
+ case "promise":
283
+ if (ro.preview?.properties[0]?.value === "pending") {
284
+ args.push(`{<${ro.preview.properties[0].value}>}`);
285
+ } else {
286
+ args.push(
287
+ `{<${ro.preview?.properties[0]?.value}>: ${ro.preview?.properties[1]?.value}}`
288
+ );
289
+ }
290
+ break;
291
+ case "node":
292
+ case "iterator":
293
+ case "proxy":
294
+ case "typedarray":
295
+ case "arraybuffer":
296
+ case "dataview":
297
+ case "webassemblymemory":
298
+ case "wasmvalue":
299
+ break;
300
+ case "error":
301
+ const errorProperties = {
302
+ message: ro.preview.description?.split("\n").filter((line) => !/^\s+at\s/.test(line)).join("\n") ?? ro.preview.properties.find(({ name }) => name === "message")?.value ?? "",
303
+ stack: ro.preview.description ?? ro.description ?? ro.preview.properties.find(({ name }) => name === "stack")?.value,
304
+ cause: ro.preview.properties.find(({ name }) => name === "cause")?.value
305
+ };
306
+ const error = await reconstructError(errorProperties, ro);
307
+ args.splice(-1, 1, error);
308
+ break;
309
+ default:
310
+ args.push(
311
+ "{\n" + ro.preview.properties.map(({ name, value }) => {
312
+ return ` ${name}: ${value}`;
313
+ }).join(",\n") + (ro.preview.overflow ? "\n ..." : "") + "\n}"
314
+ );
315
+ }
316
+ }
317
+ break;
318
+ default:
319
+ args.push(ro.description || ro.unserializableValue || "\u{1F98B}");
320
+ break;
321
+ }
322
+ }
323
+ const method = mapConsoleAPIMessageTypeToConsoleMethod[evt.type];
324
+ if (method in console) {
325
+ switch (method) {
326
+ case "dir":
327
+ console.dir(args);
328
+ break;
329
+ case "table":
330
+ console.table(args);
331
+ break;
332
+ default:
333
+ console[method].apply(console, args);
334
+ break;
335
+ }
336
+ } else {
337
+ console.warn(`Unsupported console method: ${method}`);
338
+ console.warn("console event:", evt);
339
+ }
340
+ }
341
+ function formatStructuredError(sourceMapConsumer, message, frames) {
342
+ const lines = [];
343
+ if (message !== void 0)
344
+ lines.push(message);
345
+ frames?.forEach(({ functionName, lineNumber, columnNumber }, i) => {
346
+ try {
347
+ if (lineNumber) {
348
+ const pos = sourceMapConsumer.originalPositionFor({
349
+ line: lineNumber + 1,
350
+ column: columnNumber
351
+ });
352
+ if (i === 0 && pos.source && pos.line) {
353
+ const fileSource = sourceMapConsumer.sourceContentFor(pos.source);
354
+ const fileSourceLine = fileSource?.split("\n")[pos.line - 1] || "";
355
+ lines.push(fileSourceLine.trim());
356
+ if (pos.column) {
357
+ lines.push(
358
+ `${" ".repeat(pos.column - fileSourceLine.search(/\S/))}^`
359
+ );
360
+ }
361
+ }
362
+ if (pos && pos.line !== null && pos.column !== null) {
363
+ const convertedFnName = pos.name || functionName || "";
364
+ let convertedLocation = `${pos.source}:${pos.line}:${pos.column + 1}`;
365
+ if (convertedFnName === "") {
366
+ lines.push(` at ${convertedLocation}`);
367
+ } else {
368
+ lines.push(` at ${convertedFnName} (${convertedLocation})`);
369
+ }
370
+ }
371
+ }
372
+ } catch {
373
+ }
374
+ });
375
+ return lines.join("\n");
376
+ }
377
+ function formatStack(sourceMapConsumer, stack) {
378
+ const message = stack.split("\n")[0];
379
+ const callSites = parse({ stack });
380
+ const frames = callSites.map((site) => ({
381
+ functionName: site.getFunctionName() ?? "",
382
+ // `Protocol.Runtime.CallFrame`s line numbers are 0-indexed, hence `- 1`
383
+ lineNumber: (site.getLineNumber() ?? 1) - 1,
384
+ columnNumber: site.getColumnNumber() ?? 1,
385
+ // Unused by `formattedError`
386
+ scriptId: "",
387
+ url: ""
388
+ }));
389
+ return formatStructuredError(sourceMapConsumer, message, frames);
390
+ }
391
+
392
+ export { connectToInspector, findInspectorUrl, mapConsoleAPIMessageTypeToConsoleMethod };
@@ -0,0 +1,182 @@
1
+ import { Miniflare, NoOpLog, Response, Request } from 'miniflare';
2
+ import { resolvePath } from '@shopify/cli-kit/node/path';
3
+ import { glob, readFile, createFileReadStream, fileSize } from '@shopify/cli-kit/node/fs';
4
+ import { renderSuccess } from '@shopify/cli-kit/node/ui';
5
+ import { lookupMimeType } from '@shopify/cli-kit/node/mimes';
6
+ import { findInspectorUrl, connectToInspector } from './workerd-inspector.js';
7
+ import { DEFAULT_PORT } from '../flags.js';
8
+ import { findPort } from '../find-port.js';
9
+ import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
10
+
11
+ async function startWorkerdServer({
12
+ root,
13
+ port = DEFAULT_PORT,
14
+ watch = false,
15
+ buildPathWorkerFile,
16
+ buildPathClient,
17
+ env
18
+ }) {
19
+ const inspectorPort = await findPort(8787);
20
+ const oxygenHeadersMap = Object.values(OXYGEN_HEADERS_MAP).reduce(
21
+ (acc, item) => {
22
+ acc[item.name] = item.defaultValue;
23
+ return acc;
24
+ },
25
+ {}
26
+ );
27
+ const buildMiniOxygenOptions = async () => ({
28
+ cf: false,
29
+ verbose: false,
30
+ port,
31
+ log: new NoOpLog(),
32
+ liveReload: watch,
33
+ inspectorPort,
34
+ host: "localhost",
35
+ workers: [
36
+ {
37
+ name: "mini-oxygen",
38
+ modules: true,
39
+ script: `export default { fetch: ${miniOxygenHandler.toString()} }`,
40
+ bindings: {
41
+ initialAssets: await glob("**/*", { cwd: buildPathClient }),
42
+ oxygenHeadersMap
43
+ },
44
+ serviceBindings: {
45
+ hydrogen: "hydrogen",
46
+ assets: createAssetHandler(buildPathClient),
47
+ logRequest
48
+ }
49
+ },
50
+ {
51
+ name: "hydrogen",
52
+ modules: [
53
+ {
54
+ type: "ESModule",
55
+ path: resolvePath(root, buildPathWorkerFile),
56
+ contents: await readFile(resolvePath(root, buildPathWorkerFile))
57
+ }
58
+ ],
59
+ bindings: { ...env },
60
+ compatibilityFlags: ["streams_enable_constructors"],
61
+ compatibilityDate: "2022-10-31"
62
+ }
63
+ ]
64
+ });
65
+ let miniOxygenOptions = await buildMiniOxygenOptions();
66
+ const miniOxygen = new Miniflare(miniOxygenOptions);
67
+ const listeningAt = (await miniOxygen.ready).origin;
68
+ const sourceMapPath = buildPathWorkerFile + ".map";
69
+ let inspectorUrl = await findInspectorUrl(inspectorPort);
70
+ let cleanupInspector = inspectorUrl ? connectToInspector({ inspectorUrl, sourceMapPath }) : void 0;
71
+ return {
72
+ port,
73
+ listeningAt,
74
+ async reload(nextOptions) {
75
+ miniOxygenOptions = await buildMiniOxygenOptions();
76
+ if (nextOptions) {
77
+ const hydrogen = miniOxygenOptions.workers.find(
78
+ (worker) => worker.name === "hydrogen"
79
+ );
80
+ if (hydrogen) {
81
+ hydrogen.bindings = { ...nextOptions?.env ?? env };
82
+ }
83
+ }
84
+ cleanupInspector?.();
85
+ await miniOxygen.setOptions(miniOxygenOptions);
86
+ inspectorUrl ??= await findInspectorUrl(inspectorPort);
87
+ if (inspectorUrl) {
88
+ cleanupInspector = connectToInspector({ inspectorUrl, sourceMapPath });
89
+ }
90
+ },
91
+ showBanner(options) {
92
+ console.log("");
93
+ renderSuccess({
94
+ headline: `${options?.headlinePrefix ?? ""}MiniOxygen (Unstable Worker Runtime) ${options?.mode ?? "development"} server running.`,
95
+ body: [
96
+ `View ${options?.appName ?? "Hydrogen"} app: ${listeningAt}`,
97
+ ...options?.extraLines ?? []
98
+ ]
99
+ });
100
+ console.log("");
101
+ },
102
+ async close() {
103
+ await miniOxygen.dispose();
104
+ }
105
+ };
106
+ }
107
+ async function miniOxygenHandler(request, env, context) {
108
+ if (request.method === "GET") {
109
+ const pathname = new URL(request.url).pathname;
110
+ if (pathname.startsWith("/debug-network")) {
111
+ return new Response(
112
+ "The Network Debugger is currently not supported in the Worker Runtime."
113
+ );
114
+ }
115
+ if (new Set(env.initialAssets).has(pathname.slice(1))) {
116
+ const response2 = await env.assets.fetch(
117
+ new Request(request.url, {
118
+ signal: request.signal,
119
+ headers: request.headers
120
+ })
121
+ );
122
+ if (response2.status !== 404)
123
+ return response2;
124
+ }
125
+ }
126
+ const requestInit = {
127
+ headers: {
128
+ ...env.oxygenHeadersMap,
129
+ ...Object.fromEntries(request.headers.entries())
130
+ }
131
+ };
132
+ const startTimeMs = Date.now();
133
+ const response = await env.hydrogen.fetch(request, requestInit);
134
+ const durationMs = Date.now() - startTimeMs;
135
+ context.waitUntil(
136
+ env.logRequest.fetch(
137
+ new Request(request.url, {
138
+ method: request.method,
139
+ signal: request.signal,
140
+ headers: {
141
+ ...Object.fromEntries(request.headers.entries()),
142
+ "h2-duration-ms": String(durationMs),
143
+ "h2-response-status": String(response.status)
144
+ }
145
+ })
146
+ )
147
+ );
148
+ return response;
149
+ }
150
+ function createAssetHandler(buildPathClient) {
151
+ return async (request) => {
152
+ const relativeAssetPath = new URL(request.url).pathname.replace("/", "");
153
+ if (relativeAssetPath) {
154
+ try {
155
+ const absoluteAssetPath = resolvePath(
156
+ buildPathClient,
157
+ relativeAssetPath
158
+ );
159
+ return new Response(createFileReadStream(absoluteAssetPath), {
160
+ headers: {
161
+ "Content-Type": lookupMimeType(relativeAssetPath) || "text/plain",
162
+ "Content-Length": String(await fileSize(absoluteAssetPath))
163
+ }
164
+ });
165
+ } catch (error) {
166
+ if (error.code !== "ENOENT") {
167
+ throw error;
168
+ }
169
+ }
170
+ }
171
+ return new Response("Not Found", { status: 404 });
172
+ };
173
+ }
174
+ async function logRequest(request) {
175
+ logRequestLine(request, {
176
+ responseStatus: Number(request.headers.get("h2-response-status") || 200),
177
+ durationMs: Number(request.headers.get("h2-duration-ms") || 0)
178
+ });
179
+ return new Response("ok");
180
+ }
181
+
182
+ export { startWorkerdServer };
@@ -1,8 +1,8 @@
1
1
  import { readdir } from 'node:fs/promises';
2
- import { packageManagerUsedForCreating, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
2
+ import { packageManagerFromUserAgent, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
3
3
  import { renderConfirmationPrompt, renderInfo, renderTextPrompt, renderSelectPrompt, renderFatalError, renderWarning, renderSuccess } from '@shopify/cli-kit/node/ui';
4
4
  import { hyphenate, capitalize } from '@shopify/cli-kit/common/string';
5
- import { resolvePath, basename, joinPath } from '@shopify/cli-kit/node/path';
5
+ import { joinPath, resolvePath, basename } from '@shopify/cli-kit/node/path';
6
6
  import { initializeGitRepository, addAllToGitFromDirectory, createGitCommit } from '@shopify/cli-kit/node/git';
7
7
  import { AbortError } from '@shopify/cli-kit/node/error';
8
8
  import { rmdir, writeFile, fileExists, isDirectory } from '@shopify/cli-kit/node/fs';
@@ -39,7 +39,7 @@ async function handleI18n(controller, cliCommand, flagI18n) {
39
39
  };
40
40
  }
41
41
  async function handleRouteGeneration(controller, flagRoutes) {
42
- const routesToScaffold = flagRoutes ? "all" : await renderRoutePrompt({
42
+ const routesToScaffold = flagRoutes === true ? "all" : flagRoutes === false ? [] : await renderRoutePrompt({
43
43
  abortSignal: controller.signal
44
44
  });
45
45
  const needsRouteGeneration = routesToScaffold === "all" || routesToScaffold.length > 0;
@@ -47,14 +47,25 @@ async function handleRouteGeneration(controller, flagRoutes) {
47
47
  needsRouteGeneration,
48
48
  setupRoutes: async (directory, language, i18nStrategy) => {
49
49
  if (needsRouteGeneration) {
50
- const result = await generateRoutes({
51
- routeName: routesToScaffold,
52
- directory,
53
- force: true,
54
- typescript: language === "ts",
55
- localePrefix: i18nStrategy === "subfolders" ? "locale" : false,
56
- signal: controller.signal
57
- });
50
+ const result = await generateRoutes(
51
+ {
52
+ routeName: routesToScaffold,
53
+ directory,
54
+ force: true,
55
+ typescript: language === "ts",
56
+ localePrefix: i18nStrategy === "subfolders" ? "locale" : false,
57
+ signal: controller.signal
58
+ },
59
+ {
60
+ rootDirectory: directory,
61
+ appDirectory: joinPath(directory, "app"),
62
+ future: {
63
+ v2_errorBoundary: true,
64
+ v2_meta: true,
65
+ v2_routeConvention: true
66
+ }
67
+ }
68
+ );
58
69
  return result.routeGroups;
59
70
  }
60
71
  }
@@ -189,7 +200,7 @@ async function handleLanguage(projectDir, controller, flagLanguage) {
189
200
  };
190
201
  }
191
202
  async function handleCssStrategy(projectDir, controller, flagStyling) {
192
- const selection = flagStyling ? flagStyling : await renderCssPrompt({
203
+ const selection = flagStyling ?? await renderCssPrompt({
193
204
  abortSignal: controller.signal,
194
205
  extraChoices: { none: "Skip and set up later" }
195
206
  });
@@ -215,7 +226,7 @@ async function handleCssStrategy(projectDir, controller, flagStyling) {
215
226
  };
216
227
  }
217
228
  async function handleDependencies(projectDir, controller, shouldInstallDeps) {
218
- const detectedPackageManager = await packageManagerUsedForCreating();
229
+ const detectedPackageManager = packageManagerFromUserAgent();
219
230
  let actualPackageManager = "npm";
220
231
  if (shouldInstallDeps !== false) {
221
232
  if (detectedPackageManager === "unknown") {
@@ -199,7 +199,7 @@ async function setupLocalStarterTemplate(options, controller) {
199
199
  );
200
200
  const { setupRoutes } = await handleRouteGeneration(
201
201
  controller,
202
- options.routes || true
202
+ options.routes ?? true
203
203
  // TODO: Remove default value when multi-select UI component is available
204
204
  );
205
205
  setupSummary.i18n = i18nStrategy;
@@ -6,6 +6,7 @@ import { AbortError } from '@shopify/cli-kit/node/error';
6
6
  import { outputWarn } from '@shopify/cli-kit/node/output';
7
7
  import { fileExists } from '@shopify/cli-kit/node/fs';
8
8
  import { muteRemixLogs } from './log.js';
9
+ import { getRequiredRemixVersion } from './remix-version-check.js';
9
10
 
10
11
  const BUILD_DIR = "dist";
11
12
  const CLIENT_SUBDIR = "client";
@@ -25,9 +26,18 @@ function getProjectPaths(appPath, entry) {
25
26
  publicPath
26
27
  };
27
28
  }
29
+ function handleRemixImportFail() {
30
+ const remixVersion = getRequiredRemixVersion();
31
+ throw new AbortError(
32
+ "Could not load Remix packages.",
33
+ `Please make sure you have \`@remix-run/dev@${remixVersion}\` installed and all the other Remix packages have the same version.`
34
+ );
35
+ }
28
36
  async function getRemixConfig(root, mode = process.env.NODE_ENV) {
29
37
  await muteRemixLogs();
30
- const { readConfig } = await import('@remix-run/dev/dist/config.js');
38
+ const { readConfig } = await import('@remix-run/dev/dist/config.js').catch(
39
+ handleRemixImportFail
40
+ );
31
41
  const config = await readConfig(root, mode);
32
42
  if (process.env.LOCAL_DEV) {
33
43
  const packagesPath = fileURLToPath(new URL("../../..", import.meta.url));
@@ -134,4 +144,4 @@ async function assertEntryFileExists(root, fileRelative) {
134
144
  }
135
145
  }
136
146
 
137
- export { assertOxygenChecks, getProjectPaths, getRemixConfig };
147
+ export { assertOxygenChecks, getProjectPaths, getRemixConfig, handleRemixImportFail };