@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
@@ -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();
@@ -30,7 +30,7 @@ function renderMissingLink({ session, cliCommand }) {
30
30
  type: 0,
31
31
  message: `No linked Hydrogen storefront on ${session.storeFqdn}`,
32
32
  tryMessage: [
33
- "To pull environment variables, link this project to a Hydrogen storefront. To select a storefront to link, run",
33
+ "To pull environment variables or to deploy to Oxygen, link this project to a Hydrogen storefront. To select a storefront to link, run",
34
34
  { command: `${cliCommand} link` }
35
35
  ]
36
36
  });
@@ -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 };
@@ -14,7 +14,7 @@ const NO_LOCALE_PATTERNS = [/robots\.txt/];
14
14
  const ROUTE_MAP = {
15
15
  home: ["_index", "$"],
16
16
  page: "pages*",
17
- cart: "cart",
17
+ cart: ["cart", "cart.$lines", "discount.$code"],
18
18
  products: "products*",
19
19
  collections: "collections*",
20
20
  policies: "policies*",
@@ -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
+ };