@shopify/cli-hydrogen 5.2.3 → 5.3.1

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 (37) hide show
  1. package/dist/commands/hydrogen/build.js +85 -27
  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 +21 -13
  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 +3 -3
  15. package/dist/generator-templates/starter/remix.env.d.ts +11 -3
  16. package/dist/generator-templates/starter/server.ts +21 -18
  17. package/dist/generator-templates/starter/tsconfig.json +1 -1
  18. package/dist/lib/bundle/analyzer.js +62 -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/log.js +56 -13
  25. package/dist/lib/mini-oxygen/common.js +58 -0
  26. package/dist/lib/mini-oxygen/index.js +12 -0
  27. package/dist/lib/{mini-oxygen.js → mini-oxygen/node.js} +27 -52
  28. package/dist/lib/mini-oxygen/types.js +1 -0
  29. package/dist/lib/mini-oxygen/workerd-inspector.js +392 -0
  30. package/dist/lib/mini-oxygen/workerd.js +182 -0
  31. package/dist/lib/onboarding/common.js +4 -4
  32. package/dist/lib/onboarding/local.js +1 -1
  33. package/dist/lib/render-errors.js +1 -1
  34. package/dist/lib/setups/routes/generate.js +1 -1
  35. package/dist/virtual-routes/routes/index.jsx +4 -4
  36. package/oclif.manifest.json +81 -3
  37. package/package.json +12 -4
@@ -1,8 +1,8 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
- import { outputInfo, outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
4
- import { rmdir, fileSize, glob, removeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
- import { resolvePath, relativePath, joinPath } from '@shopify/cli-kit/node/path';
3
+ import { outputInfo, outputWarn, outputContent, outputToken } from '@shopify/cli-kit/node/output';
4
+ import { rmdir, fileSize, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
+ import { resolvePath, relativePath } 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
8
  import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
@@ -11,8 +11,11 @@ import { checkLockfileStatus } from '../../lib/check-lockfile.js';
11
11
  import { findMissingRoutes } from '../../lib/missing-routes.js';
12
12
  import { muteRemixLogs, createRemixLogger } from '../../lib/log.js';
13
13
  import { codegen } from '../../lib/codegen.js';
14
+ import { hasMetafile, buildBundleAnalysis, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
15
+ import { AbortError } from '@shopify/cli-kit/node/error';
14
16
 
15
17
  const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
18
+ const MAX_WORKER_BUNDLE_SIZE = 10;
16
19
  class Build extends Command {
17
20
  static description = "Builds a Hydrogen storefront for production.";
18
21
  static flags = {
@@ -20,7 +23,13 @@ class Build extends Command {
20
23
  sourcemap: Flags.boolean({
21
24
  description: "Generate sourcemaps for the build.",
22
25
  env: "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP",
23
- default: false
26
+ allowNo: true,
27
+ default: true
28
+ }),
29
+ ["bundle-stats"]: Flags.boolean({
30
+ description: "Show a bundle size summary after building.",
31
+ default: true,
32
+ allowNo: true
24
33
  }),
25
34
  "disable-route-warning": Flags.boolean({
26
35
  description: "Disable warning about missing standard routes.",
@@ -51,11 +60,16 @@ async function runBuild({
51
60
  useCodegen = false,
52
61
  codegenConfigPath,
53
62
  sourcemap = false,
54
- disableRouteWarning = false
63
+ disableRouteWarning = false,
64
+ bundleStats = true,
65
+ assetPath
55
66
  }) {
56
67
  if (!process.env.NODE_ENV) {
57
68
  process.env.NODE_ENV = "production";
58
69
  }
70
+ if (assetPath) {
71
+ process.env.HYDROGEN_ASSET_BASE_URL = assetPath;
72
+ }
59
73
  const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
60
74
  await Promise.all([checkLockfileStatus(root), muteRemixLogs()]);
61
75
  console.time(LOG_WORKER_BUILT);
@@ -90,28 +104,22 @@ async function runBuild({
90
104
  if (process.env.NODE_ENV !== "development") {
91
105
  console.timeEnd(LOG_WORKER_BUILT);
92
106
  const sizeMB = await fileSize(buildPathWorkerFile) / (1024 * 1024);
93
- outputInfo(
94
- outputContent` ${colors.dim(
95
- relativePath(root, buildPathWorkerFile)
96
- )} ${outputToken.yellow(sizeMB.toFixed(2))} MB\n`
97
- );
98
- if (sizeMB >= 1) {
99
- outputWarn(
100
- `\u{1F6A8} Worker bundle exceeds 1 MB! This can delay your worker response.${remixConfig.serverMinify ? "" : " Minify your bundle by adding `serverMinify: true` to remix.config.js."}
101
- `
107
+ if (await hasMetafile(buildPath)) {
108
+ await writeBundleAnalysis(
109
+ buildPath,
110
+ root,
111
+ buildPathWorkerFile,
112
+ sizeMB,
113
+ bundleStats,
114
+ remixConfig
115
+ );
116
+ } else {
117
+ await writeSimpleBuildStatus(
118
+ root,
119
+ buildPathWorkerFile,
120
+ sizeMB,
121
+ remixConfig
102
122
  );
103
- }
104
- if (sourcemap) {
105
- if (process.env.HYDROGEN_ASSET_BASE_URL) {
106
- const filepaths = await glob(joinPath(buildPathClient, "**/*.js.map"));
107
- for (const filepath of filepaths) {
108
- await removeFile(filepath);
109
- }
110
- } else {
111
- outputWarn(
112
- "\u{1F6A8} Sourcemaps are enabled in production! Use this only for testing.\n"
113
- );
114
- }
115
123
  }
116
124
  }
117
125
  if (!disableRouteWarning) {
@@ -127,10 +135,60 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
127
135
  );
128
136
  }
129
137
  }
130
- if (!process.env.SHOPIFY_UNIT_TEST) {
138
+ if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
131
139
  process.exit(0);
132
140
  }
133
141
  }
142
+ async function writeBundleAnalysis(buildPath, root, buildPathWorkerFile, sizeMB, bundleStats, remixConfig) {
143
+ const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
144
+ outputInfo(
145
+ outputContent` ${colors.dim(
146
+ relativePath(root, buildPathWorkerFile)
147
+ )} ${outputToken.link(
148
+ colors.yellow(sizeMB.toFixed(2) + " MB"),
149
+ bundleAnalysisPath
150
+ )}\n`
151
+ );
152
+ if (bundleStats && sizeMB < MAX_WORKER_BUNDLE_SIZE) {
153
+ outputInfo(
154
+ outputContent`${await getBundleAnalysisSummary(buildPathWorkerFile) || "\n"}\n │\n └─── ${outputToken.link(
155
+ "Complete analysis: " + bundleAnalysisPath,
156
+ bundleAnalysisPath
157
+ )}\n\n`
158
+ );
159
+ }
160
+ if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
161
+ throw new AbortError(
162
+ "\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB.",
163
+ outputContent`See the bundle analysis for a breakdown of what is contributing to the bundle size:\n${outputToken.link(
164
+ bundleAnalysisPath,
165
+ bundleAnalysisPath
166
+ )}`
167
+ );
168
+ } else if (sizeMB >= 5) {
169
+ outputWarn(
170
+ `\u{1F6A8} Worker bundle exceeds 5 MB! This can delay your worker response.${remixConfig.serverMinify ? "" : " Minify your bundle by adding `serverMinify: true` to remix.config.js."}
171
+ `
172
+ );
173
+ }
174
+ }
175
+ async function writeSimpleBuildStatus(root, buildPathWorkerFile, sizeMB, remixConfig) {
176
+ outputInfo(
177
+ outputContent` ${colors.dim(
178
+ relativePath(root, buildPathWorkerFile)
179
+ )} ${colors.yellow(sizeMB.toFixed(2) + " MB")}\n`
180
+ );
181
+ if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
182
+ throw new AbortError(
183
+ "\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB."
184
+ );
185
+ } else if (sizeMB >= 5) {
186
+ outputWarn(
187
+ `\u{1F6A8} Worker bundle exceeds 5 MB! This can delay your worker response.${remixConfig.serverMinify ? "" : " Minify your bundle by adding `serverMinify: true` to remix.config.js."}
188
+ `
189
+ );
190
+ }
191
+ }
134
192
  async function copyPublicFiles(publicPath, buildPathClient) {
135
193
  if (!await fileExists(publicPath)) {
136
194
  return;
@@ -0,0 +1,171 @@
1
+ import { Flags } from '@oclif/core';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+ import colors from '@shopify/cli-kit/node/colors';
4
+ import { outputWarn, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
5
+ import { AbortError } from '@shopify/cli-kit/node/error';
6
+ import { resolvePath } from '@shopify/cli-kit/node/path';
7
+ import { renderFatalError, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
8
+ import { parseToken, createDeploy } from '@shopify/oxygen-cli/deploy';
9
+ import { commonFlags } from '../../lib/flags.js';
10
+ import { getOxygenDeploymentToken } from '../../lib/get-oxygen-token.js';
11
+ import { runBuild } from './build.js';
12
+
13
+ const deploymentLogger = (message, level = "info") => {
14
+ if (level === "error" || level === "warn") {
15
+ outputWarn(message);
16
+ }
17
+ };
18
+ class Deploy extends Command {
19
+ static flags = {
20
+ path: commonFlags.path,
21
+ shop: commonFlags.shop,
22
+ publicDeployment: Flags.boolean({
23
+ env: "SHOPIFY_HYDROGEN_FLAG_PUBLIC_DEPLOYMENT",
24
+ description: "Marks a preview deployment as publicly accessible.",
25
+ required: false,
26
+ default: false
27
+ }),
28
+ metadataUrl: Flags.string({
29
+ description: "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
30
+ required: false,
31
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL"
32
+ }),
33
+ metadataUser: Flags.string({
34
+ description: "User that initiated the deployment. Will be saved and displayed in the Shopify admin",
35
+ required: false,
36
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_USER"
37
+ }),
38
+ metadataVersion: Flags.string({
39
+ description: "A version identifier for the deployment. Will be saved and displayed in the Shopify admin",
40
+ required: false,
41
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION"
42
+ })
43
+ };
44
+ static hidden = true;
45
+ async run() {
46
+ const { flags } = await this.parse(Deploy);
47
+ const actualPath = flags.path ? resolvePath(flags.path) : process.cwd();
48
+ await oxygenDeploy({
49
+ path: actualPath,
50
+ shop: flags.shop,
51
+ publicDeployment: flags.publicDeployment,
52
+ metadataUrl: flags.metadataUrl,
53
+ metadataUser: flags.metadataUser,
54
+ metadataVersion: flags.metadataVersion
55
+ }).catch((error) => {
56
+ renderFatalError(error);
57
+ process.exit(1);
58
+ }).finally(() => {
59
+ process.exit(0);
60
+ });
61
+ }
62
+ }
63
+ async function oxygenDeploy(options) {
64
+ const {
65
+ path,
66
+ shop,
67
+ publicDeployment,
68
+ metadataUrl,
69
+ metadataUser,
70
+ metadataVersion
71
+ } = options;
72
+ const token = await getOxygenDeploymentToken({
73
+ root: path,
74
+ flagShop: shop
75
+ });
76
+ if (!token) {
77
+ throw new AbortError("Could not obtain Oxygen deployment token");
78
+ }
79
+ const config = {
80
+ assetsDir: "dist/client",
81
+ deploymentUrl: "https://oxygen.shopifyapps.com",
82
+ deploymentToken: parseToken(token),
83
+ healthCheckMaxDuration: 180,
84
+ metadata: {
85
+ ...metadataUrl ? { url: metadataUrl } : {},
86
+ ...metadataUser ? { user: metadataUser } : {},
87
+ ...metadataVersion ? { version: metadataVersion } : {}
88
+ },
89
+ publicDeployment,
90
+ skipHealthCheck: false,
91
+ rootPath: path,
92
+ skipBuild: false,
93
+ workerOnly: false,
94
+ workerDir: "dist/worker"
95
+ };
96
+ let resolveUpload;
97
+ const uploadPromise = new Promise((resolve) => {
98
+ resolveUpload = resolve;
99
+ });
100
+ let resolveHealthCheck;
101
+ const healthCheckPromise = new Promise((resolve) => {
102
+ resolveHealthCheck = resolve;
103
+ });
104
+ let deployError = null;
105
+ let resolveDeploy;
106
+ let rejectDeploy;
107
+ const deployPromise = new Promise((resolve, reject) => {
108
+ resolveDeploy = resolve;
109
+ rejectDeploy = reject;
110
+ });
111
+ const hooks = {
112
+ buildFunction: async (assetPath) => {
113
+ outputInfo(
114
+ outputContent`${colors.whiteBright("Building project...")}`.value
115
+ );
116
+ await runBuild({
117
+ directory: path,
118
+ assetPath,
119
+ sourcemap: false,
120
+ useCodegen: false
121
+ });
122
+ },
123
+ onHealthCheckComplete: () => resolveHealthCheck(),
124
+ onUploadFilesStart: () => uploadStart(),
125
+ onUploadFilesComplete: () => resolveUpload(),
126
+ onHealthCheckError: (error) => {
127
+ deployError = new AbortError(
128
+ error.message,
129
+ "Please verify the deployment status in the Shopify Admin and retry deploying if necessary."
130
+ );
131
+ },
132
+ onUploadFilesError: (error) => {
133
+ deployError = new AbortError(
134
+ error.message,
135
+ "Check your connection and try again. If the problem persists, try again later or contact support."
136
+ );
137
+ }
138
+ };
139
+ const uploadStart = async () => {
140
+ outputInfo(
141
+ outputContent`${colors.whiteBright("Deploying to Oxygen..\n")}`.value
142
+ );
143
+ await renderTasks([
144
+ {
145
+ title: "Uploading files",
146
+ task: async () => await uploadPromise
147
+ },
148
+ {
149
+ title: "Performing health check",
150
+ task: async () => await healthCheckPromise
151
+ }
152
+ ]);
153
+ };
154
+ await createDeploy({ config, hooks, logger: deploymentLogger }).then((url) => {
155
+ const deploymentType = config.publicDeployment ? "public" : "private";
156
+ renderSuccess({
157
+ body: ["Successfully deployed to Oxygen"],
158
+ nextSteps: [
159
+ [
160
+ `Open ${url} in your browser to view your ${deploymentType} deployment`
161
+ ]
162
+ ]
163
+ });
164
+ resolveDeploy();
165
+ }).catch((error) => {
166
+ rejectDeploy(deployError || error);
167
+ });
168
+ return deployPromise;
169
+ }
170
+
171
+ export { Deploy as default, deploymentLogger, oxygenDeploy };
@@ -0,0 +1,185 @@
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { login } from '../../lib/auth.js';
3
+ import { getStorefronts } from '../../lib/graphql/admin/link-storefront.js';
4
+ import { AbortError } from '@shopify/cli-kit/node/error';
5
+ import { renderSelectPrompt, renderSuccess, renderFatalError } from '@shopify/cli-kit/node/ui';
6
+ import { oxygenDeploy, deploymentLogger } from './deploy.js';
7
+ import { getOxygenDeploymentToken } from '../../lib/get-oxygen-token.js';
8
+ import { createDeploy, parseToken } from '@shopify/oxygen-cli/deploy';
9
+
10
+ vi.mock("../../lib/get-oxygen-token.js");
11
+ vi.mock("@shopify/oxygen-cli/deploy");
12
+ vi.mock("../../lib/auth.js");
13
+ vi.mock("../../lib/shopify-config.js");
14
+ vi.mock("../../lib/graphql/admin/link-storefront.js");
15
+ vi.mock("../../lib/graphql/admin/create-storefront.js");
16
+ vi.mock("../../lib/graphql/admin/fetch-job.js");
17
+ vi.mock("../../lib/shell.js", () => ({ getCliCommand: () => "h2" }));
18
+ vi.mock("@shopify/cli-kit/node/output", async () => {
19
+ return {
20
+ outputContent: () => ({ value: "" }),
21
+ outputInfo: () => {
22
+ }
23
+ };
24
+ });
25
+ vi.mock("@shopify/cli-kit/node/ui", async () => {
26
+ return {
27
+ renderFatalError: vi.fn(),
28
+ renderSelectPrompt: vi.fn(),
29
+ renderSuccess: vi.fn(),
30
+ renderTasks: vi.fn()
31
+ };
32
+ });
33
+ describe("deploy", () => {
34
+ const ADMIN_SESSION = {
35
+ token: "abc123",
36
+ storeFqdn: "my-shop.myshopify.com"
37
+ };
38
+ const FULL_SHOPIFY_CONFIG = {
39
+ shop: "my-shop.myshopify.com",
40
+ shopName: "My Shop",
41
+ email: "email",
42
+ storefront: {
43
+ id: "gid://shopify/HydrogenStorefront/1",
44
+ title: "Hydrogen"
45
+ }
46
+ };
47
+ const UNLINKED_SHOPIFY_CONFIG = {
48
+ ...FULL_SHOPIFY_CONFIG,
49
+ storefront: void 0
50
+ };
51
+ const originalExit = process.exit;
52
+ const deployParams = {
53
+ path: "./",
54
+ shop: "snowdevil.myshopify.com",
55
+ publicDeployment: false,
56
+ metadataUrl: "https://example.com",
57
+ metadataUser: "user",
58
+ metadataVersion: "1.0.0"
59
+ };
60
+ const mockToken = {
61
+ accessToken: "some-token",
62
+ allowedResource: "some-resource",
63
+ appId: "1",
64
+ client: "1",
65
+ expiresAt: "some-time",
66
+ namespace: "some-namespace",
67
+ namespaceId: "1"
68
+ };
69
+ beforeEach(async () => {
70
+ process.exit = vi.fn();
71
+ vi.mocked(login).mockResolvedValue({
72
+ session: ADMIN_SESSION,
73
+ config: UNLINKED_SHOPIFY_CONFIG
74
+ });
75
+ vi.mocked(getStorefronts).mockResolvedValue([
76
+ {
77
+ ...FULL_SHOPIFY_CONFIG.storefront,
78
+ parsedId: "1",
79
+ productionUrl: "https://example.com"
80
+ }
81
+ ]);
82
+ vi.mocked(renderSelectPrompt).mockResolvedValue(FULL_SHOPIFY_CONFIG.shop);
83
+ vi.mocked(createDeploy).mockResolvedValue(
84
+ "https://a-lovely-deployment.com"
85
+ );
86
+ vi.mocked(getOxygenDeploymentToken).mockResolvedValue("some-encoded-token");
87
+ vi.mocked(parseToken).mockReturnValue(mockToken);
88
+ });
89
+ afterEach(() => {
90
+ vi.resetAllMocks();
91
+ process.exit = originalExit;
92
+ });
93
+ it("calls getOxygenDeploymentToken with the correct parameters", async () => {
94
+ await oxygenDeploy(deployParams);
95
+ expect(getOxygenDeploymentToken).toHaveBeenCalledWith({
96
+ root: "./",
97
+ flagShop: "snowdevil.myshopify.com"
98
+ });
99
+ expect(getOxygenDeploymentToken).toHaveBeenCalledTimes(1);
100
+ });
101
+ it("calls createDeploy with the correct parameters", async () => {
102
+ await oxygenDeploy(deployParams);
103
+ const expectedConfig = {
104
+ assetsDir: "dist/client",
105
+ deploymentUrl: "https://oxygen.shopifyapps.com",
106
+ deploymentToken: mockToken,
107
+ healthCheckMaxDuration: 180,
108
+ metadata: {
109
+ url: deployParams.metadataUrl,
110
+ user: deployParams.metadataUser,
111
+ version: deployParams.metadataVersion
112
+ },
113
+ publicDeployment: deployParams.publicDeployment,
114
+ skipHealthCheck: false,
115
+ rootPath: deployParams.path,
116
+ skipBuild: false,
117
+ workerOnly: false,
118
+ workerDir: "dist/worker"
119
+ };
120
+ expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
121
+ config: expectedConfig,
122
+ hooks: {
123
+ buildFunction: expect.any(Function),
124
+ onHealthCheckComplete: expect.any(Function),
125
+ onUploadFilesStart: expect.any(Function),
126
+ onUploadFilesComplete: expect.any(Function),
127
+ onHealthCheckError: expect.any(Function),
128
+ onUploadFilesError: expect.any(Function)
129
+ },
130
+ logger: deploymentLogger
131
+ });
132
+ expect(vi.mocked(renderSuccess)).toHaveBeenCalled;
133
+ });
134
+ it("handles error during uploadFiles", async () => {
135
+ const mockRenderFatalError = vi.fn();
136
+ vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
137
+ const error = new Error("Wonky internet!");
138
+ vi.mocked(createDeploy).mockImplementation((options) => {
139
+ options.hooks?.onUploadFilesStart?.();
140
+ options.hooks?.onUploadFilesError?.(error);
141
+ return new Promise((_resolve, reject) => {
142
+ reject(error);
143
+ });
144
+ });
145
+ try {
146
+ await oxygenDeploy(deployParams);
147
+ expect(true).toBe(false);
148
+ } catch (err) {
149
+ if (err instanceof AbortError) {
150
+ expect(err.message).toBe(error.message);
151
+ expect(err.tryMessage).toBe(
152
+ "Check your connection and try again. If the problem persists, try again later or contact support."
153
+ );
154
+ } else {
155
+ expect(true).toBe(false);
156
+ }
157
+ }
158
+ });
159
+ it("handles error during health check", async () => {
160
+ const mockRenderFatalError = vi.fn();
161
+ vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
162
+ const error = new Error("Cloudflare is down!");
163
+ vi.mocked(createDeploy).mockImplementation((options) => {
164
+ options.hooks?.onUploadFilesStart?.();
165
+ options.hooks?.onUploadFilesComplete?.();
166
+ options.hooks?.onHealthCheckError?.(error);
167
+ return new Promise((_resolve, reject) => {
168
+ reject(error);
169
+ });
170
+ });
171
+ try {
172
+ await oxygenDeploy(deployParams);
173
+ expect(true).toBe(false);
174
+ } catch (err) {
175
+ if (err instanceof AbortError) {
176
+ expect(err.message).toBe(error.message);
177
+ expect(err.tryMessage).toBe(
178
+ "Please verify the deployment status in the Shopify Admin and retry deploying if necessary."
179
+ );
180
+ } else {
181
+ expect(true).toBe(false);
182
+ }
183
+ }
184
+ });
185
+ });
@@ -10,7 +10,7 @@ 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';
12
12
  import { Flags } from '@oclif/core';
13
- import { startMiniOxygen } from '../../lib/mini-oxygen.js';
13
+ import { startMiniOxygen } from '../../lib/mini-oxygen/index.js';
14
14
  import { checkHydrogenVersion } from '../../lib/check-version.js';
15
15
  import { addVirtualRoutes } from '../../lib/virtual-routes.js';
16
16
  import { spawnCodegenProcess } from '../../lib/codegen.js';
@@ -26,6 +26,7 @@ class Dev extends Command {
26
26
  static flags = {
27
27
  path: commonFlags.path,
28
28
  port: commonFlags.port,
29
+ ["worker-unstable"]: commonFlags.workerRuntime,
29
30
  ["codegen-unstable"]: Flags.boolean({
30
31
  description: "Generate types for the Storefront API queries found in your project. It updates the types on file save.",
31
32
  required: false,
@@ -52,6 +53,7 @@ class Dev extends Command {
52
53
  await runDev({
53
54
  ...flagsToCamelObject(flags),
54
55
  useCodegen: flags["codegen-unstable"],
56
+ workerRuntime: flags["worker-unstable"],
55
57
  path: directory
56
58
  });
57
59
  }
@@ -60,6 +62,7 @@ async function runDev({
60
62
  port: portFlag = DEFAULT_PORT,
61
63
  path: appPath,
62
64
  useCodegen = false,
65
+ workerRuntime = false,
63
66
  codegenConfigPath,
64
67
  disableVirtualRoutes,
65
68
  envBranch,
@@ -107,14 +110,17 @@ async function runDev({
107
110
  async function safeStartMiniOxygen() {
108
111
  if (miniOxygen)
109
112
  return;
110
- miniOxygen = await startMiniOxygen({
111
- root,
112
- port: portFlag,
113
- watch: !liveReload,
114
- buildPathWorkerFile,
115
- buildPathClient,
116
- env: await envPromise
117
- });
113
+ miniOxygen = await startMiniOxygen(
114
+ {
115
+ root,
116
+ port: portFlag,
117
+ watch: !liveReload,
118
+ buildPathWorkerFile,
119
+ buildPathClient,
120
+ env: await envPromise
121
+ },
122
+ workerRuntime
123
+ );
118
124
  const graphiqlUrl = `${miniOxygen.listeningAt}/graphiql`;
119
125
  const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
120
126
  enhanceH2Logs({ graphiqlUrl, ...remixConfig });
@@ -125,9 +131,11 @@ async function runDev({
125
131
  extraLines: [
126
132
  colors.dim(`
127
133
  View GraphiQL API browser: ${graphiqlUrl}`),
128
- colors.dim(`
129
- View server-side network requests: ${debugNetworkUrl}`)
130
- ]
134
+ workerRuntime ? "" : colors.dim(
135
+ `
136
+ View server-side network requests: ${debugNetworkUrl}`
137
+ )
138
+ ].filter(Boolean)
131
139
  });
132
140
  if (useCodegen) {
133
141
  spawnCodegenProcess({ ...remixConfig, configFilePath: codegenConfigPath });
@@ -182,7 +190,7 @@ View server-side network requests: ${debugNetworkUrl}`)
182
190
  if (!miniOxygen) {
183
191
  await safeStartMiniOxygen();
184
192
  } else if (liveReload) {
185
- await miniOxygen.reload({ worker: true });
193
+ await miniOxygen.reload();
186
194
  }
187
195
  liveReload?.onAppReady(context);
188
196
  }
@@ -1,6 +1,6 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { packageManagerUsedForCreating } from '@shopify/cli-kit/node/node-package-manager';
3
+ import { packageManagerFromUserAgent } from '@shopify/cli-kit/node/node-package-manager';
4
4
  import { Flags } from '@oclif/core';
5
5
  import { AbortError } from '@shopify/cli-kit/node/error';
6
6
  import { AbortController } from '@shopify/cli-kit/node/abort';
@@ -42,7 +42,8 @@ class Init extends Command {
42
42
  routes: Flags.boolean({
43
43
  description: "Generate routes for all pages.",
44
44
  env: "SHOPIFY_HYDROGEN_FLAG_ROUTES",
45
- hidden: true
45
+ hidden: true,
46
+ allowNo: true
46
47
  }),
47
48
  git: Flags.boolean({
48
49
  description: "Init Git and create initial commits.",
@@ -52,10 +53,13 @@ class Init extends Command {
52
53
  })
53
54
  };
54
55
  async run() {
55
- const { flags } = await this.parse(Init);
56
- if (flags.markets && !I18N_CHOICES.includes(flags.markets)) {
56
+ const {
57
+ flags: { markets, ..._flags }
58
+ } = await this.parse(Init);
59
+ const flags = { ..._flags, i18n: markets };
60
+ if (flags.i18n && !I18N_CHOICES.includes(flags.i18n)) {
57
61
  throw new AbortError(
58
- `Invalid URL structure strategy: ${flags.markets}. Must be one of ${I18N_CHOICES.join(", ")}`
62
+ `Invalid URL structure strategy: ${flags.i18n}. Must be one of ${I18N_CHOICES.join(", ")}`
59
63
  );
60
64
  }
61
65
  if (flags.styling && !STYLING_CHOICES.includes(flags.styling)) {
@@ -77,7 +81,7 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
77
81
  "cli"
78
82
  );
79
83
  if (showUpgrade) {
80
- const packageManager = await packageManagerUsedForCreating();
84
+ const packageManager = packageManagerFromUserAgent();
81
85
  showUpgrade(
82
86
  packageManager === "unknown" ? "" : `Please use the latest version with \`${packageManager} create @shopify/hydrogen@latest\``
83
87
  );
@@ -46,7 +46,7 @@ vi.mock(
46
46
  return {
47
47
  ...original,
48
48
  getPackageManager: () => Promise.resolve("npm"),
49
- packageManagerUsedForCreating: () => Promise.resolve("npm"),
49
+ packageManagerFromUserAgent: () => "npm",
50
50
  installNodeModules: vi.fn(async ({ directory }) => {
51
51
  renderTasksHook.mockImplementationOnce(async () => {
52
52
  await writeFile(`${directory}/package-lock.json`, "{}");
@@ -519,6 +519,21 @@ describe("init", () => {
519
519
  );
520
520
  expect(mb).toBeGreaterThan(0);
521
521
  expect(mb).toBeLessThan(1);
522
+ expect(output).toMatch("Complete analysis: file://");
523
+ const clientAnalysisPath = "dist/worker/client-bundle-analyzer.html";
524
+ const workerAnalysisPath = "dist/worker/worker-bundle-analyzer.html";
525
+ expect(
526
+ fileExists(joinPath(tmpDir, clientAnalysisPath))
527
+ ).resolves.toBeTruthy();
528
+ expect(
529
+ fileExists(joinPath(tmpDir, workerAnalysisPath))
530
+ ).resolves.toBeTruthy();
531
+ expect(await readFile(joinPath(tmpDir, clientAnalysisPath))).toMatch(
532
+ /globalThis\.METAFILE = '.+';/g
533
+ );
534
+ expect(await readFile(joinPath(tmpDir, workerAnalysisPath))).toMatch(
535
+ /globalThis\.METAFILE = '.+';/g
536
+ );
522
537
  });
523
538
  });
524
539
  });