@shopify/cli-hydrogen 5.2.2 → 5.2.3

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.
@@ -5,7 +5,7 @@ import { rmdir, fileSize, glob, removeFile, fileExists, copyFile } from '@shopif
5
5
  import { resolvePath, relativePath, joinPath } from '@shopify/cli-kit/node/path';
6
6
  import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
7
7
  import colors from '@shopify/cli-kit/node/colors';
8
- import { getProjectPaths, getRemixConfig, assertOxygenChecks } from '../../lib/remix-config.js';
8
+ import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
9
9
  import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
10
10
  import { checkLockfileStatus } from '../../lib/check-lockfile.js';
11
11
  import { findMissingRoutes } from '../../lib/missing-routes.js';
@@ -61,11 +61,13 @@ async function runBuild({
61
61
  console.time(LOG_WORKER_BUILT);
62
62
  outputInfo(`
63
63
  \u{1F3D7}\uFE0F Building in ${process.env.NODE_ENV} mode...`);
64
- const [remixConfig, { build }, { logThrown }, { createFileWatchCache }] = await Promise.all([
64
+ const [remixConfig, [{ build }, { logThrown }, { createFileWatchCache }]] = await Promise.all([
65
65
  getRemixConfig(root),
66
- import('@remix-run/dev/dist/compiler/build.js'),
67
- import('@remix-run/dev/dist/compiler/utils/log.js'),
68
- import('@remix-run/dev/dist/compiler/fileWatchCache.js'),
66
+ Promise.all([
67
+ import('@remix-run/dev/dist/compiler/build.js'),
68
+ import('@remix-run/dev/dist/compiler/utils/log.js'),
69
+ import('@remix-run/dev/dist/compiler/fileWatchCache.js')
70
+ ]).catch(handleRemixImportFail),
69
71
  rmdir(buildPath, { force: true })
70
72
  ]);
71
73
  assertOxygenChecks(remixConfig);
@@ -5,7 +5,7 @@ import { fileExists } from '@shopify/cli-kit/node/fs';
5
5
  import { renderFatalError } from '@shopify/cli-kit/node/ui';
6
6
  import colors from '@shopify/cli-kit/node/colors';
7
7
  import { copyPublicFiles } from './build.js';
8
- import { getProjectPaths, assertOxygenChecks, getRemixConfig } from '../../lib/remix-config.js';
8
+ import { getProjectPaths, assertOxygenChecks, handleRemixImportFail, getRemixConfig } from '../../lib/remix-config.js';
9
9
  import { muteDevLogs, createRemixLogger, enhanceH2Logs } from '../../lib/log.js';
10
10
  import { commonFlags, deprecated, flagsToCamelObject, DEFAULT_PORT } from '../../lib/flags.js';
11
11
  import Command from '@shopify/cli-kit/node/base-command';
@@ -98,7 +98,7 @@ async function runDev({
98
98
  const [{ watch }, { createFileWatchCache }] = await Promise.all([
99
99
  import('@remix-run/dev/dist/compiler/watch.js'),
100
100
  import('@remix-run/dev/dist/compiler/fileWatchCache.js')
101
- ]);
101
+ ]).catch(handleRemixImportFail);
102
102
  let isInitialBuild = true;
103
103
  let initialBuildDurationMs = 0;
104
104
  let initialBuildStartTimeMs = Date.now();
@@ -116,13 +116,18 @@ async function runDev({
116
116
  env: await envPromise
117
117
  });
118
118
  const graphiqlUrl = `${miniOxygen.listeningAt}/graphiql`;
119
+ const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
119
120
  enhanceH2Logs({ graphiqlUrl, ...remixConfig });
120
121
  miniOxygen.showBanner({
121
122
  appName: storefront ? colors.cyan(storefront?.title) : void 0,
122
123
  headlinePrefix: initialBuildDurationMs > 0 ? `Initial build: ${initialBuildDurationMs}ms
123
124
  ` : "",
124
- extraLines: [colors.dim(`
125
- View GraphiQL API browser: ${graphiqlUrl}`)]
125
+ extraLines: [
126
+ colors.dim(`
127
+ View GraphiQL API browser: ${graphiqlUrl}`),
128
+ colors.dim(`
129
+ View server-side network requests: ${debugNetworkUrl}`)
130
+ ]
126
131
  });
127
132
  if (useCodegen) {
128
133
  spawnCodegenProcess({ ...remixConfig, configFilePath: codegenConfigPath });
@@ -15,9 +15,9 @@
15
15
  "dependencies": {
16
16
  "@remix-run/react": "1.19.1",
17
17
  "@shopify/cli": "3.48.0",
18
- "@shopify/cli-hydrogen": "^5.2.2",
19
- "@shopify/hydrogen": "^2023.7.6",
20
- "@shopify/remix-oxygen": "^1.1.3",
18
+ "@shopify/cli-hydrogen": "^5.2.3",
19
+ "@shopify/hydrogen": "^2023.7.7",
20
+ "@shopify/remix-oxygen": "^1.1.4",
21
21
  "graphql": "^16.6.0",
22
22
  "graphql-tag": "^2.12.6",
23
23
  "isbot": "^3.6.6",
@@ -35,5 +35,6 @@ declare module '@shopify/remix-oxygen' {
35
35
  cart: HydrogenCart;
36
36
  storefront: Storefront;
37
37
  session: HydrogenSession;
38
+ waitUntil: ExecutionContext['waitUntil'];
38
39
  }
39
40
  }
@@ -70,7 +70,7 @@ export default {
70
70
  const handleRequest = createRequestHandler({
71
71
  build: remixBuild,
72
72
  mode: process.env.NODE_ENV,
73
- getLoadContext: () => ({session, storefront, env, cart}),
73
+ getLoadContext: () => ({session, storefront, cart, env, waitUntil}),
74
74
  });
75
75
 
76
76
  const response = await handleRequest(request);
@@ -1,4 +1,5 @@
1
1
  import http from 'node:http';
2
+ import { handleRemixImportFail } from './remix-config.js';
2
3
 
3
4
  async function setupLiveReload(devServerPort) {
4
5
  try {
@@ -7,7 +8,7 @@ async function setupLiveReload(devServerPort) {
7
8
  import('@remix-run/dev/dist/devServer_unstable/socket.js'),
8
9
  import('@remix-run/dev/dist/devServer_unstable/hdr.js'),
9
10
  import('@remix-run/dev/dist/result.js')
10
- ]);
11
+ ]).catch(handleRemixImportFail);
11
12
  const state = {};
12
13
  const server = http.createServer(function(req, res) {
13
14
  res.writeHead(200);
@@ -1,10 +1,13 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
3
  import { outputToken, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
2
4
  import { resolvePath } from '@shopify/cli-kit/node/path';
3
5
  import { readFile, fileExists } from '@shopify/cli-kit/node/fs';
4
6
  import colors from '@shopify/cli-kit/node/colors';
5
7
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
6
- import { startServer } from '@shopify/mini-oxygen';
8
+ import { startServer, Request } from '@shopify/mini-oxygen';
7
9
  import { DEFAULT_PORT } from './flags.js';
10
+ import { clearHistory, streamRequestEvents, DEV_ROUTES, logRequestEvent } from './request-events.js';
8
11
 
9
12
  async function startMiniOxygen({
10
13
  root,
@@ -16,6 +19,20 @@ async function startMiniOxygen({
16
19
  env
17
20
  }) {
18
21
  const dotenvPath = resolvePath(root, ".env");
22
+ const asyncLocalStorage = new AsyncLocalStorage();
23
+ const serviceBindings = {
24
+ H2O_LOG_EVENT: {
25
+ fetch: (request) => logRequestEvent(
26
+ new Request(request.url, {
27
+ headers: {
28
+ ...Object.fromEntries(request.headers.entries()),
29
+ // Merge some headers from the parent request
30
+ ...asyncLocalStorage.getStore()
31
+ }
32
+ })
33
+ )
34
+ }
35
+ };
19
36
  const miniOxygen = await startServer({
20
37
  script: await readFile(buildPathWorkerFile),
21
38
  workerFile: buildPathWorkerFile,
@@ -27,19 +44,29 @@ async function startMiniOxygen({
27
44
  modules: true,
28
45
  env: {
29
46
  ...env,
30
- ...process.env
47
+ ...process.env,
48
+ ...serviceBindings
31
49
  },
32
50
  envPath: !env && await fileExists(dotenvPath) ? dotenvPath : void 0,
33
51
  log: () => {
34
52
  },
35
- onResponse: (request, response) => (
36
- // 'Request' and 'Response' types in MiniOxygen comes from
37
- // Miniflare and are slightly different from standard types.
38
- logResponse(
39
- request,
40
- response
41
- )
42
- )
53
+ async onRequest(request, defaultDispatcher) {
54
+ const url = new URL(request.url);
55
+ if (url.pathname === "/debug-network-server") {
56
+ return request.method === "DELETE" ? clearHistory() : streamRequestEvents(request);
57
+ }
58
+ let requestId = request.headers.get("request-id");
59
+ if (!requestId) {
60
+ requestId = randomUUID();
61
+ request.headers.set("request-id", requestId);
62
+ }
63
+ const response = await asyncLocalStorage.run(
64
+ { "request-id": requestId, purpose: request.headers.get("purpose") },
65
+ () => defaultDispatcher(request)
66
+ );
67
+ logResponse(request, response);
68
+ return response;
69
+ }
43
70
  });
44
71
  const listeningAt = `http://localhost:${miniOxygen.port}`;
45
72
  return {
@@ -50,7 +77,8 @@ async function startMiniOxygen({
50
77
  if (options.env) {
51
78
  nextOptions.env = {
52
79
  ...options.env,
53
- ...process.env
80
+ ...process.env,
81
+ ...serviceBindings
54
82
  };
55
83
  }
56
84
  if (options.worker) {
@@ -74,9 +102,8 @@ async function startMiniOxygen({
74
102
  function logResponse(request, response) {
75
103
  try {
76
104
  const url = new URL(request.url);
77
- if (["/graphiql"].includes(url.pathname)) {
105
+ if (DEV_ROUTES.has(url.pathname))
78
106
  return;
79
- }
80
107
  const isProxy = !!response.url && response.url !== request.url;
81
108
  const isDataRequest = !isProxy && url.searchParams.has("_data");
82
109
  let route = request.url.replace(url.origin, "");
@@ -2,7 +2,7 @@ import { readdir } from 'node:fs/promises';
2
2
  import { packageManagerUsedForCreating, 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';
@@ -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
  }
@@ -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 };
@@ -2,12 +2,15 @@ import { createRequire } from 'node:module';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { renderWarning } from '@shopify/cli-kit/node/ui';
4
4
 
5
- function checkRemixVersions() {
6
- const require2 = createRequire(import.meta.url);
5
+ function getRequiredRemixVersion(require2 = createRequire(import.meta.url)) {
7
6
  const hydrogenPkgJson = require2(fileURLToPath(
8
7
  new URL("../../package.json", import.meta.url)
9
8
  ));
10
- const requiredVersionInHydrogen = hydrogenPkgJson.dependencies["@remix-run/dev"];
9
+ return hydrogenPkgJson.peerDependencies["@remix-run/dev"];
10
+ }
11
+ function checkRemixVersions() {
12
+ const require2 = createRequire(import.meta.url);
13
+ const requiredVersionInHydrogen = getRequiredRemixVersion(require2);
11
14
  const pkgs = [
12
15
  "dev",
13
16
  "react",
@@ -48,4 +51,4 @@ function getRemixPackageVersion(require2, name) {
48
51
  return result;
49
52
  }
50
53
 
51
- export { checkRemixVersions };
54
+ export { checkRemixVersions, getRequiredRemixVersion };
@@ -27,7 +27,7 @@ describe("remix-version-check", () => {
27
27
  const expectedVersion = "42.0.0-test";
28
28
  vi.mocked(requireMock).mockReturnValueOnce({
29
29
  // Hydrogen expected version
30
- dependencies: { "@remix-run/dev": expectedVersion }
30
+ peerDependencies: { "@remix-run/dev": expectedVersion }
31
31
  });
32
32
  const outputMock = mockAndCaptureOutput();
33
33
  checkRemixVersions();
@@ -0,0 +1,84 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { ReadableStream } from 'node:stream/web';
3
+ import { Response } from '@shopify/mini-oxygen';
4
+
5
+ const DEV_ROUTES = /* @__PURE__ */ new Set(["/graphiql", "/debug-network"]);
6
+ const EVENT_MAP = {
7
+ request: "Request",
8
+ subrequest: "Sub request"
9
+ };
10
+ function getRequestInfo(request) {
11
+ return {
12
+ id: request.headers.get("request-id"),
13
+ eventType: request.headers.get("hydrogen-event-type") || "unknown",
14
+ startTime: request.headers.get("hydrogen-start-time"),
15
+ endTime: request.headers.get("hydrogen-end-time") || String(Date.now()),
16
+ purpose: request.headers.get("purpose") === "prefetch" ? "(prefetch)" : "",
17
+ cacheStatus: request.headers.get("hydrogen-cache-status")
18
+ };
19
+ }
20
+ const eventEmitter = new EventEmitter();
21
+ const eventHistory = [];
22
+ async function clearHistory() {
23
+ eventHistory.length = 0;
24
+ return new Response("ok");
25
+ }
26
+ async function logRequestEvent(request) {
27
+ if (DEV_ROUTES.has(new URL(request.url).pathname)) {
28
+ return new Response("ok");
29
+ }
30
+ const { eventType, purpose, ...data } = getRequestInfo(request);
31
+ let description = request.url;
32
+ if (eventType === "subrequest") {
33
+ description = decodeURIComponent(request.url).match(/(query|mutation)\s+(\w+)/)?.[0]?.replace(/\s+/, " ") || request.url;
34
+ }
35
+ const event = {
36
+ event: EVENT_MAP[eventType] || eventType,
37
+ data: JSON.stringify({
38
+ ...data,
39
+ url: `${purpose} ${description}`.trim()
40
+ })
41
+ };
42
+ if (eventHistory.length > 100)
43
+ eventHistory.shift();
44
+ eventHistory.push(event);
45
+ eventEmitter.emit("request", event);
46
+ return new Response("ok");
47
+ }
48
+ function streamRequestEvents(request) {
49
+ const stream = new ReadableStream({
50
+ start(controller) {
51
+ const encoder = new TextEncoder();
52
+ const enqueueEvent = ({ event = "message", data }) => {
53
+ controller.enqueue(encoder.encode(`event: ${event}
54
+ `));
55
+ controller.enqueue(encoder.encode(`data: ${data}
56
+
57
+ `));
58
+ };
59
+ eventHistory.forEach(enqueueEvent);
60
+ eventEmitter.addListener("request", enqueueEvent);
61
+ let closed = false;
62
+ function close() {
63
+ if (closed)
64
+ return;
65
+ closed = true;
66
+ request.signal.removeEventListener("abort", close);
67
+ eventEmitter.removeListener("request", enqueueEvent);
68
+ controller.close();
69
+ }
70
+ request.signal.addEventListener("abort", close);
71
+ if (request.signal.aborted)
72
+ return close();
73
+ }
74
+ });
75
+ return new Response(stream, {
76
+ headers: {
77
+ "Content-Type": "text/event-stream",
78
+ "Cache-Control": "no-store",
79
+ Connection: "keep-alive"
80
+ }
81
+ });
82
+ }
83
+
84
+ export { DEV_ROUTES, clearHistory, logRequestEvent, streamRequestEvents };
@@ -51,9 +51,9 @@ async function getResolvedRoutes(routeKeys = Object.keys(ROUTE_MAP)) {
51
51
  return { routeGroups, resolvedRouteFiles };
52
52
  }
53
53
  const ALL_ROUTE_CHOICES = [...Object.keys(ROUTE_MAP), "all"];
54
- async function generateRoutes(options) {
54
+ async function generateRoutes(options, remixConfig) {
55
55
  const { routeGroups, resolvedRouteFiles } = options.routeName === "all" ? await getResolvedRoutes() : await getResolvedRoutes([options.routeName]);
56
- const { rootDirectory, appDirectory, future, tsconfigPath } = await getRemixConfig(options.directory);
56
+ const { rootDirectory, appDirectory, future, tsconfigPath } = remixConfig || await getRemixConfig(options.directory);
57
57
  const routesArray = resolvedRouteFiles.flatMap(
58
58
  (item) => GENERATOR_ROUTE_DIR + "/" + item
59
59
  );
@@ -1,7 +1,6 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
3
- import glob from 'fast-glob';
1
+ import { glob, removeFile, readFile, writeFile } from '@shopify/cli-kit/node/fs';
4
2
  import { outputDebug } from '@shopify/cli-kit/node/output';
3
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
4
  import { getCodeFormatOptions, formatCode } from './format-code.js';
6
5
 
7
6
  const escapeNewLines = (code) => code.replace(/\n\n/g, "\n/* :newline: */");
@@ -79,35 +78,34 @@ async function transpileProject(projectDir) {
79
78
  const formatConfig = await getCodeFormatOptions();
80
79
  for (const entry of entries) {
81
80
  if (entry.endsWith(".d.ts")) {
82
- await fs.rm(entry);
81
+ await removeFile(entry);
83
82
  continue;
84
83
  }
85
- const tsx = await fs.readFile(entry, "utf8");
84
+ const tsx = await readFile(entry);
86
85
  const mjs = await formatCode(await transpileFile(tsx), formatConfig);
87
- await fs.rm(entry);
88
- await fs.writeFile(entry.replace(/\.ts(x?)$/, ".js$1"), mjs, "utf8");
86
+ await removeFile(entry);
87
+ await writeFile(entry.replace(/\.ts(x?)$/, ".js$1"), mjs);
89
88
  }
90
89
  try {
91
- const remixConfigPath = path.join(projectDir, "remix.config.js");
92
- let remixConfig = await fs.readFile(remixConfigPath, "utf8");
90
+ const remixConfigPath = joinPath(projectDir, "remix.config.js");
91
+ let remixConfig = await readFile(remixConfigPath);
93
92
  remixConfig = remixConfig.replace(/\/server\.ts/gim, "/server.js");
94
- await fs.writeFile(remixConfigPath, remixConfig);
93
+ await writeFile(remixConfigPath, remixConfig);
95
94
  } catch (error) {
96
95
  outputDebug(
97
96
  "Could not change TS extensions in remix.config.js:\n" + error.stack
98
97
  );
99
98
  }
100
99
  try {
101
- const tsConfigPath = path.join(projectDir, "tsconfig.json");
102
- const tsConfigWithComments = await fs.readFile(tsConfigPath, "utf8");
100
+ const tsConfigPath = joinPath(projectDir, "tsconfig.json");
101
+ const tsConfigWithComments = await readFile(tsConfigPath);
103
102
  const jsConfig = convertConfigToJS(
104
103
  JSON.parse(tsConfigWithComments.replace(/^\s*\/\/.*$/gm, ""))
105
104
  );
106
- await fs.rm(tsConfigPath);
107
- await fs.writeFile(
108
- path.join(projectDir, "jsconfig.json"),
109
- JSON.stringify(jsConfig, null, 2),
110
- "utf8"
105
+ await removeFile(tsConfigPath);
106
+ await writeFile(
107
+ joinPath(projectDir, "jsconfig.json"),
108
+ JSON.stringify(jsConfig, null, 2)
111
109
  );
112
110
  } catch (error) {
113
111
  outputDebug(
@@ -116,7 +114,7 @@ async function transpileProject(projectDir) {
116
114
  }
117
115
  try {
118
116
  const pkgJson = JSON.parse(
119
- await fs.readFile(path.join(projectDir, "package.json"), "utf8")
117
+ await readFile(joinPath(projectDir, "package.json"))
120
118
  );
121
119
  delete pkgJson.scripts["typecheck"];
122
120
  delete pkgJson.devDependencies["typescript"];
@@ -133,8 +131,8 @@ async function transpileProject(projectDir) {
133
131
  if (pkgJson.scripts?.build) {
134
132
  pkgJson.scripts.build = pkgJson.scripts.build.replace(codegenFlag, "");
135
133
  }
136
- await fs.writeFile(
137
- path.join(projectDir, "package.json"),
134
+ await writeFile(
135
+ joinPath(projectDir, "package.json"),
138
136
  JSON.stringify(pkgJson, null, 2)
139
137
  );
140
138
  } catch (error) {
@@ -143,10 +141,10 @@ async function transpileProject(projectDir) {
143
141
  );
144
142
  }
145
143
  try {
146
- const eslintrcPath = path.join(projectDir, ".eslintrc.js");
147
- let eslintrc = await fs.readFile(eslintrcPath, "utf8");
144
+ const eslintrcPath = joinPath(projectDir, ".eslintrc.js");
145
+ let eslintrc = await readFile(eslintrcPath);
148
146
  eslintrc = eslintrc.replace(/\/\*\*[\s*]+@type.+\s+\*\/\s?/gim, "").replace(/\s*,?\s*['"`]plugin:hydrogen\/typescript['"`]/gim, "").replace(/\s+['"`]@typescript-eslint\/.+,/gim, "");
149
- await fs.writeFile(eslintrcPath, eslintrc);
147
+ await writeFile(eslintrcPath, eslintrc);
150
148
  } catch (error) {
151
149
  outputDebug(
152
150
  "Could not remove TS rules from .eslintrc:\n" + error.stack
@@ -1,15 +1,17 @@
1
- import path from 'path';
2
- import { fileURLToPath } from 'url';
3
- import recursiveReaddir from 'recursive-readdir';
1
+ import { fileURLToPath } from 'node:url';
2
+ import { glob } from '@shopify/cli-kit/node/fs';
3
+ import { joinPath, relativePath } from '@shopify/cli-kit/node/path';
4
4
 
5
5
  const VIRTUAL_ROUTES_DIR = "virtual-routes/routes";
6
6
  const VIRTUAL_ROOT = "virtual-routes/virtual-root";
7
7
  async function addVirtualRoutes(config) {
8
8
  const userRouteList = Object.values(config.routes);
9
9
  const distPath = fileURLToPath(new URL("..", import.meta.url));
10
- const virtualRoutesPath = path.join(distPath, VIRTUAL_ROUTES_DIR);
11
- for (const absoluteFilePath of await recursiveReaddir(virtualRoutesPath)) {
12
- const relativeFilePath = path.relative(virtualRoutesPath, absoluteFilePath);
10
+ const virtualRoutesPath = joinPath(distPath, VIRTUAL_ROUTES_DIR);
11
+ for (const absoluteFilePath of await glob(
12
+ joinPath(virtualRoutesPath, "**", "*")
13
+ )) {
14
+ const relativeFilePath = relativePath(virtualRoutesPath, absoluteFilePath);
13
15
  const routePath = relativeFilePath.replace(/\.[jt]sx?$/, "").replaceAll("\\", "/");
14
16
  const isIndex = /(^|\/)index$/.test(routePath);
15
17
  const normalizedVirtualRoutePath = isIndex ? routePath.slice(0, -"index".length).replace(/\/$/, "") || void 0 : (
@@ -27,15 +29,15 @@ async function addVirtualRoutes(config) {
27
29
  path: normalizedVirtualRoutePath,
28
30
  index: isIndex || void 0,
29
31
  caseSensitive: void 0,
30
- file: path.relative(config.appDirectory, absoluteFilePath)
32
+ file: relativePath(config.appDirectory, absoluteFilePath)
31
33
  };
32
34
  if (!config.routes[VIRTUAL_ROOT]) {
33
35
  config.routes[VIRTUAL_ROOT] = {
34
36
  id: VIRTUAL_ROOT,
35
37
  path: "",
36
- file: path.relative(
38
+ file: relativePath(
37
39
  config.appDirectory,
38
- path.join(distPath, VIRTUAL_ROOT + ".jsx")
40
+ joinPath(distPath, VIRTUAL_ROOT + ".jsx")
39
41
  )
40
42
  };
41
43
  }
@@ -0,0 +1,125 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef } from "react";
3
+ import _useResizeObserver from "use-resize-observer";
4
+ const useResizeObserver = _useResizeObserver;
5
+ const FlameChartWrapper = (props) => {
6
+ const boxRef = useRef(null);
7
+ const canvasRef = useRef(null);
8
+ const flameChart = useRef(null);
9
+ useResizeObserver({
10
+ ref: boxRef,
11
+ onResize: ({ width = 0, height = 0 }) => flameChart.current?.resize(width, height - 3)
12
+ });
13
+ const initialize = useCallback(() => {
14
+ const {
15
+ data,
16
+ marks,
17
+ waterfall,
18
+ timeseries,
19
+ settings,
20
+ colors,
21
+ plugins,
22
+ timeframeTimeseries
23
+ } = props;
24
+ if (canvasRef.current && boxRef.current) {
25
+ const { width = 0, height = 0 } = boxRef.current.getBoundingClientRect();
26
+ canvasRef.current.width = width;
27
+ canvasRef.current.height = height - 3;
28
+ flameChart.current = new flameChartJs.FlameChart({
29
+ canvas: canvasRef.current,
30
+ data,
31
+ marks,
32
+ waterfall,
33
+ timeseries,
34
+ timeframeTimeseries,
35
+ settings,
36
+ colors,
37
+ plugins
38
+ });
39
+ }
40
+ }, [props]);
41
+ const setBoxRef = useCallback(
42
+ (ref) => {
43
+ const isNewRef = ref !== boxRef.current;
44
+ boxRef.current = ref;
45
+ if (isNewRef) {
46
+ initialize();
47
+ }
48
+ },
49
+ [initialize]
50
+ );
51
+ const setCanvasRef = useCallback(
52
+ (ref) => {
53
+ const isNewRef = ref !== canvasRef.current;
54
+ canvasRef.current = ref;
55
+ if (isNewRef) {
56
+ initialize();
57
+ }
58
+ },
59
+ [initialize]
60
+ );
61
+ useEffect(() => {
62
+ if (props.data) {
63
+ flameChart.current?.setNodes(props.data);
64
+ }
65
+ }, [props.data]);
66
+ useEffect(() => {
67
+ if (props.marks) {
68
+ flameChart.current?.setMarks(props.marks);
69
+ }
70
+ }, [props.marks]);
71
+ useEffect(() => {
72
+ if (props.waterfall) {
73
+ flameChart.current?.setWaterfall(props.waterfall);
74
+ }
75
+ }, [props.waterfall]);
76
+ useEffect(() => {
77
+ if (props.timeseries) {
78
+ flameChart.current?.setTimeseries(props.timeseries);
79
+ }
80
+ }, [props.timeseries]);
81
+ useEffect(() => {
82
+ if (props.timeframeTimeseries) {
83
+ flameChart.current?.setTimeframeTimeseries(props.timeframeTimeseries);
84
+ }
85
+ }, [props.timeframeTimeseries]);
86
+ useEffect(() => {
87
+ if (props.settings && flameChart.current) {
88
+ flameChart.current.setSettings(props.settings);
89
+ flameChart.current.renderEngine.recalcChildrenSizes();
90
+ flameChart.current.render();
91
+ }
92
+ }, [props.settings]);
93
+ useEffect(() => {
94
+ if (props.position) {
95
+ flameChart.current?.setFlameChartPosition(props.position);
96
+ }
97
+ }, [props.position]);
98
+ useEffect(() => {
99
+ if (props.zoom) {
100
+ flameChart.current?.setZoom(props.zoom.start, props.zoom.end);
101
+ }
102
+ }, [props.zoom]);
103
+ useEffect(() => {
104
+ if (props.onSelect) {
105
+ flameChart.current?.on("select", props.onSelect);
106
+ }
107
+ return () => {
108
+ if (props.onSelect) {
109
+ flameChart.current?.removeListener("select", props.onSelect);
110
+ }
111
+ };
112
+ }, [props.onSelect]);
113
+ return /* @__PURE__ */ jsx(
114
+ "div",
115
+ {
116
+ style: { height: `${props.height ? props.height : 300}px` },
117
+ className: props.className,
118
+ ref: setBoxRef,
119
+ children: /* @__PURE__ */ jsx("canvas", { ref: setCanvasRef })
120
+ }
121
+ );
122
+ };
123
+ export {
124
+ FlameChartWrapper
125
+ };
@@ -0,0 +1,289 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { FlameChartWrapper } from "../components/FlameChartWrapper.jsx";
4
+ import { Link } from "@remix-run/react";
5
+ import { Script } from "@shopify/hydrogen";
6
+ function DebugNetwork() {
7
+ const serverEvents = useRef({
8
+ smallestStartTime: 0,
9
+ mainRequests: [],
10
+ subRequests: {},
11
+ showPutRequests: false
12
+ });
13
+ const [timestamp, setTimestamp] = useState();
14
+ function serverEventHandler(onEvent) {
15
+ return (event) => {
16
+ const data = JSON.parse(event.data);
17
+ if (serverEvents.current.smallestStartTime === 0) {
18
+ serverEvents.current.smallestStartTime = data.startTime;
19
+ } else {
20
+ serverEvents.current.smallestStartTime = Math.min(
21
+ data.startTime,
22
+ serverEvents.current.smallestStartTime
23
+ );
24
+ }
25
+ onEvent(data);
26
+ setTimeout(() => {
27
+ setTimestamp((/* @__PURE__ */ new Date()).getTime());
28
+ }, 0);
29
+ };
30
+ }
31
+ useEffect(() => {
32
+ const evtSource = new EventSource("/debug-network-server", {
33
+ withCredentials: true
34
+ });
35
+ const mainRequestHandler = serverEventHandler((data) => {
36
+ serverEvents.current.mainRequests = [
37
+ ...serverEvents.current.mainRequests,
38
+ {
39
+ ...data,
40
+ url: data.url.replace(location.origin, "")
41
+ }
42
+ ];
43
+ });
44
+ evtSource.addEventListener("Request", mainRequestHandler);
45
+ const subRequestHandler = serverEventHandler((data) => {
46
+ let groupEvents = serverEvents.current.subRequests[data.id] || [];
47
+ groupEvents = [...groupEvents, data];
48
+ serverEvents.current.subRequests = {
49
+ ...serverEvents.current.subRequests,
50
+ [data.id]: groupEvents
51
+ };
52
+ });
53
+ evtSource.addEventListener("Sub request", subRequestHandler);
54
+ return () => {
55
+ evtSource.removeEventListener("Request", mainRequestHandler);
56
+ evtSource.removeEventListener("Sub request", subRequestHandler);
57
+ evtSource.close();
58
+ };
59
+ }, []);
60
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
61
+ /* @__PURE__ */ jsx(
62
+ Script,
63
+ {
64
+ src: "https://unpkg.com/flame-chart-js@2.3.1/dist/index.min.js",
65
+ suppressHydrationWarning: true
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsxs(
69
+ "div",
70
+ {
71
+ style: {
72
+ width: "100vw",
73
+ backgroundColor: "#F5F5F5",
74
+ fontSize: "0.8rem"
75
+ },
76
+ children: [
77
+ /* @__PURE__ */ jsxs(
78
+ "div",
79
+ {
80
+ style: {
81
+ display: "flex",
82
+ justifyContent: "space-between"
83
+ },
84
+ children: [
85
+ /* @__PURE__ */ jsxs(
86
+ "div",
87
+ {
88
+ style: {
89
+ display: "flex",
90
+ alignItems: "center"
91
+ },
92
+ children: [
93
+ /* @__PURE__ */ jsx(
94
+ "button",
95
+ {
96
+ onClick: () => {
97
+ fetch("/debug-network-server", { method: "DELETE" }).catch(
98
+ (error) => console.error("Could not clear history:", error)
99
+ );
100
+ serverEvents.current = {
101
+ smallestStartTime: 0,
102
+ mainRequests: [],
103
+ subRequests: {},
104
+ showPutRequests: serverEvents.current.showPutRequests
105
+ };
106
+ setTimestamp((/* @__PURE__ */ new Date()).getTime());
107
+ },
108
+ children: "Clear"
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsx(
112
+ "input",
113
+ {
114
+ id: "showPutRequests",
115
+ type: "checkbox",
116
+ checked: serverEvents.current.showPutRequests,
117
+ onChange: (event) => {
118
+ serverEvents.current.showPutRequests = event.target.checked;
119
+ setTimestamp((/* @__PURE__ */ new Date()).getTime());
120
+ }
121
+ }
122
+ ),
123
+ /* @__PURE__ */ jsx("label", { htmlFor: "showPutRequests", children: "Show cache update requests (PUT)" })
124
+ ]
125
+ }
126
+ ),
127
+ /* @__PURE__ */ jsx(
128
+ "p",
129
+ {
130
+ style: {
131
+ paddingRight: "5px"
132
+ },
133
+ children: "Unstable"
134
+ }
135
+ )
136
+ ]
137
+ }
138
+ ),
139
+ /* @__PURE__ */ jsx(FlameChart, { serverEvents: serverEvents.current }, timestamp),
140
+ /* @__PURE__ */ jsxs("p", { style: { color: "#777", fontSize: "0.7rem", paddingLeft: "5px" }, children: [
141
+ "Note: You may need to turn on '",
142
+ /* @__PURE__ */ jsx("b", { children: "Disable Cache" }),
143
+ "' for your navigating window. If you are not seeing any requests, try re-running '",
144
+ /* @__PURE__ */ jsx("b", { children: "npm run dev" }),
145
+ "' in your terminal while leaving this window open."
146
+ ] })
147
+ ]
148
+ }
149
+ )
150
+ ] });
151
+ }
152
+ const PANEL_HEIGHT = 300;
153
+ function FlameChart({ serverEvents }) {
154
+ if (serverEvents.mainRequests.length === 0)
155
+ return /* @__PURE__ */ jsx(
156
+ "div",
157
+ {
158
+ style: {
159
+ height: `${PANEL_HEIGHT}px`,
160
+ display: "flex",
161
+ justifyContent: "center",
162
+ alignItems: "center",
163
+ backgroundColor: "#FAFAFA"
164
+ },
165
+ children: /* @__PURE__ */ jsxs("p", { style: { fontWeight: "bold", color: "#777" }, children: [
166
+ "Navigate your",
167
+ " ",
168
+ /* @__PURE__ */ jsx(Link, { to: "/", target: "_blank", children: "app" })
169
+ ] })
170
+ }
171
+ );
172
+ let totalRequests = 0;
173
+ let totalSubRequests = 0;
174
+ const calcDuration = (time) => time - serverEvents.smallestStartTime;
175
+ let items = [];
176
+ serverEvents.mainRequests.forEach((mainRequest) => {
177
+ const mainResponseStart = calcDuration(mainRequest.endTime);
178
+ let mainResponseEnd = mainResponseStart;
179
+ const subRequestItems = [];
180
+ const subRequests = serverEvents.subRequests[mainRequest.id] || [];
181
+ subRequests.forEach((subRequest) => {
182
+ const subRequestEnd = calcDuration(subRequest.endTime);
183
+ if (subRequest.cacheStatus !== "PUT") {
184
+ mainResponseEnd = Math.max(mainResponseEnd, subRequestEnd);
185
+ }
186
+ const subRequestItem = {
187
+ name: `${subRequest.cacheStatus} ${subRequest.url}`.trim(),
188
+ intervals: "request",
189
+ timing: {
190
+ requestStart: calcDuration(subRequest.startTime),
191
+ requestEnd: subRequestEnd
192
+ }
193
+ };
194
+ if (serverEvents.showPutRequests) {
195
+ subRequestItems.push(subRequestItem);
196
+ } else {
197
+ subRequest.cacheStatus !== "PUT" && subRequestItems.push(subRequestItem);
198
+ }
199
+ totalSubRequests++;
200
+ });
201
+ totalRequests++;
202
+ items.push({
203
+ name: mainRequest.url,
204
+ intervals: "mainRequest",
205
+ timing: {
206
+ requestStart: calcDuration(mainRequest.startTime),
207
+ responseStart: mainResponseStart,
208
+ responseEnd: mainResponseEnd
209
+ }
210
+ });
211
+ items = items.concat(subRequestItems);
212
+ });
213
+ const data = {
214
+ items,
215
+ intervals: {
216
+ mainRequest: [
217
+ {
218
+ name: "server",
219
+ color: "#99CC00",
220
+ type: "block",
221
+ start: "requestStart",
222
+ end: "responseStart"
223
+ },
224
+ {
225
+ name: "streaming",
226
+ color: "#33CCFF",
227
+ type: "block",
228
+ start: "responseStart",
229
+ end: "responseEnd"
230
+ }
231
+ ],
232
+ request: [
233
+ {
234
+ name: "request",
235
+ color: "#FFCC00",
236
+ type: "block",
237
+ start: "requestStart",
238
+ end: "requestEnd"
239
+ }
240
+ ]
241
+ }
242
+ };
243
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
244
+ /* @__PURE__ */ jsx(
245
+ FlameChartWrapper,
246
+ {
247
+ height: PANEL_HEIGHT,
248
+ waterfall: data,
249
+ settings: {
250
+ styles: {
251
+ waterfallPlugin: {
252
+ defaultHeight: PANEL_HEIGHT
253
+ }
254
+ }
255
+ }
256
+ }
257
+ ),
258
+ /* @__PURE__ */ jsxs(
259
+ "div",
260
+ {
261
+ style: {
262
+ display: "flex",
263
+ padding: "5px",
264
+ borderTop: "1px solid #CCC",
265
+ borderBottom: "1px solid #CCC"
266
+ },
267
+ children: [
268
+ totalRequests,
269
+ " requests",
270
+ /* @__PURE__ */ jsx(
271
+ "span",
272
+ {
273
+ style: {
274
+ paddingLeft: "2px",
275
+ paddingRight: "2px"
276
+ },
277
+ children: "|"
278
+ }
279
+ ),
280
+ totalSubRequests,
281
+ " sub requests"
282
+ ]
283
+ }
284
+ )
285
+ ] });
286
+ }
287
+ export {
288
+ DebugNetwork as default
289
+ };
@@ -11,6 +11,7 @@ import {
11
11
  import styles from "./assets/styles.css";
12
12
  import favicon from "./assets/favicon.svg";
13
13
  import { Layout } from "./components/Layout.jsx";
14
+ import { useNonce } from "@shopify/hydrogen";
14
15
  const links = () => {
15
16
  return [
16
17
  { rel: "stylesheet", href: styles },
@@ -18,6 +19,7 @@ const links = () => {
18
19
  ];
19
20
  };
20
21
  function App() {
22
+ const nonce = useNonce();
21
23
  return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
22
24
  /* @__PURE__ */ jsxs("head", { children: [
23
25
  /* @__PURE__ */ jsx("meta", { charSet: "utf-8" }),
@@ -35,12 +37,13 @@ function App() {
35
37
  ] }),
36
38
  /* @__PURE__ */ jsxs("body", { children: [
37
39
  /* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsx(Outlet, {}) }),
38
- /* @__PURE__ */ jsx(ScrollRestoration, {}),
39
- /* @__PURE__ */ jsx(Scripts, {})
40
+ /* @__PURE__ */ jsx(ScrollRestoration, { nonce }),
41
+ /* @__PURE__ */ jsx(Scripts, { nonce })
40
42
  ] })
41
43
  ] });
42
44
  }
43
45
  function ErrorBoundary() {
46
+ const nonce = useNonce();
44
47
  const error = useRouteError();
45
48
  let errorMessage = "Unknown error";
46
49
  let errorStatus = 500;
@@ -71,8 +74,8 @@ function ErrorBoundary() {
71
74
  /* @__PURE__ */ jsx("h2", { children: errorStatus }),
72
75
  errorMessage && /* @__PURE__ */ jsx("fieldset", { children: /* @__PURE__ */ jsx("pre", { children: errorMessage }) })
73
76
  ] }) }),
74
- /* @__PURE__ */ jsx(ScrollRestoration, {}),
75
- /* @__PURE__ */ jsx(Scripts, {})
77
+ /* @__PURE__ */ jsx(ScrollRestoration, { nonce }),
78
+ /* @__PURE__ */ jsx(Scripts, { nonce })
76
79
  ] })
77
80
  ] });
78
81
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.2.2",
2
+ "version": "5.2.3",
3
3
  "commands": {
4
4
  "hydrogen:build": {
5
5
  "id": "hydrogen:build",
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "@shopify:registry": "https://registry.npmjs.org"
6
6
  },
7
- "version": "5.2.2",
7
+ "version": "5.2.3",
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "scripts": {
@@ -23,32 +23,47 @@
23
23
  "@types/recursive-readdir": "^2.2.1",
24
24
  "@types/tar-fs": "^2.0.1",
25
25
  "@vitest/coverage-v8": "^0.33.0",
26
+ "fast-glob": "^3.2.12",
27
+ "flame-chart-js": "2.3.1",
26
28
  "type-fest": "^3.6.0",
27
29
  "vitest": "^0.33.0"
28
30
  },
29
- "peerDependencies": {
30
- "@remix-run/react": "1.19.1",
31
- "@shopify/hydrogen-react": "^2023.7.4",
32
- "@shopify/remix-oxygen": "^1.1.3"
33
- },
34
31
  "dependencies": {
35
32
  "@ast-grep/napi": "0.11.0",
36
33
  "@graphql-codegen/cli": "3.3.1",
37
34
  "@oclif/core": "2.8.11",
38
- "@remix-run/dev": "1.19.1",
39
35
  "@shopify/cli-kit": "3.48.0",
40
36
  "@shopify/hydrogen-codegen": "^0.0.2",
41
37
  "@shopify/mini-oxygen": "^2.2.1",
42
38
  "ansi-escapes": "^6.2.0",
43
39
  "diff": "^5.1.0",
44
- "fast-glob": "^3.2.12",
45
40
  "fs-extra": "^11.1.0",
46
41
  "get-port": "^7.0.0",
47
42
  "gunzip-maybe": "^1.4.2",
48
43
  "prettier": "^2.8.4",
49
- "recursive-readdir": "^2.2.3",
50
44
  "tar-fs": "^2.1.1",
51
- "typescript": "^5.2.2"
45
+ "typescript": "^5.2.2",
46
+ "use-resize-observer": "^9.1.0"
47
+ },
48
+ "peerDependencies": {
49
+ "@remix-run/dev": "1.19.1",
50
+ "@remix-run/react": "1.19.1",
51
+ "@shopify/hydrogen-react": "^2023.7.4",
52
+ "@shopify/remix-oxygen": "^1.1.4"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@remix-run/dev": {
56
+ "optional": true
57
+ },
58
+ "@remix-run/react": {
59
+ "optional": true
60
+ },
61
+ "@shopify/hydrogen-react": {
62
+ "optional": true
63
+ },
64
+ "@shopify/remix-oxygen": {
65
+ "optional": true
66
+ }
52
67
  },
53
68
  "bin": "dist/create-app.js",
54
69
  "exports": {