@shopify/cli-hydrogen 6.1.0 → 7.0.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 (97) hide show
  1. package/dist/commands/hydrogen/build.js +40 -78
  2. package/dist/commands/hydrogen/codegen.js +8 -3
  3. package/dist/commands/hydrogen/deploy.js +107 -35
  4. package/dist/commands/hydrogen/deploy.test.js +83 -13
  5. package/dist/commands/hydrogen/dev.js +30 -15
  6. package/dist/commands/hydrogen/init.js +1 -1
  7. package/dist/commands/hydrogen/init.test.js +155 -53
  8. package/dist/commands/hydrogen/link.js +5 -21
  9. package/dist/commands/hydrogen/link.test.js +10 -10
  10. package/dist/commands/hydrogen/preview.js +7 -6
  11. package/dist/commands/hydrogen/setup.js +0 -4
  12. package/dist/commands/hydrogen/setup.test.js +0 -1
  13. package/dist/commands/hydrogen/upgrade.js +15 -0
  14. package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
  15. package/dist/generator-templates/starter/CHANGELOG.md +56 -0
  16. package/dist/generator-templates/starter/README.md +23 -0
  17. package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
  18. package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
  19. package/dist/generator-templates/starter/app/components/Layout.tsx +1 -1
  20. package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
  21. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
  22. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
  23. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
  24. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
  25. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
  26. package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
  27. package/dist/generator-templates/starter/app/lib/session.ts +67 -0
  28. package/dist/generator-templates/starter/app/root.tsx +11 -45
  29. package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
  30. package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
  31. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
  32. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
  33. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
  34. package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
  35. package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
  36. package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
  37. package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
  38. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
  39. package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
  40. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
  41. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
  42. package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
  43. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
  44. package/dist/generator-templates/starter/package.json +11 -11
  45. package/dist/generator-templates/starter/remix.config.js +4 -0
  46. package/dist/generator-templates/starter/remix.env.d.ts +4 -11
  47. package/dist/generator-templates/starter/server.ts +24 -167
  48. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
  49. package/dist/hooks/init.js +4 -4
  50. package/dist/lib/auth.js +5 -10
  51. package/dist/lib/build.js +6 -1
  52. package/dist/lib/bundle/analyzer.js +36 -26
  53. package/dist/lib/codegen.js +58 -18
  54. package/dist/lib/defer.js +12 -0
  55. package/dist/lib/file.js +52 -3
  56. package/dist/lib/flags.js +15 -8
  57. package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
  58. package/dist/lib/graphql/admin/client.test.js +2 -2
  59. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  60. package/dist/lib/log.js +31 -14
  61. package/dist/lib/mini-oxygen/index.js +4 -5
  62. package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
  63. package/dist/lib/mini-oxygen/node.js +4 -2
  64. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +2 -2
  65. package/dist/lib/mini-oxygen/workerd.js +27 -10
  66. package/dist/lib/missing-routes.js +6 -3
  67. package/dist/lib/onboarding/common.js +40 -9
  68. package/dist/lib/onboarding/local.js +19 -11
  69. package/dist/lib/onboarding/remote.js +48 -28
  70. package/dist/lib/request-events.js +65 -31
  71. package/dist/lib/setups/css/assets.js +1 -46
  72. package/dist/lib/setups/css/css-modules.js +3 -2
  73. package/dist/lib/setups/css/postcss.js +4 -2
  74. package/dist/lib/setups/css/tailwind.js +4 -2
  75. package/dist/lib/setups/css/vanilla-extract.js +3 -2
  76. package/dist/lib/setups/i18n/replacers.test.js +54 -38
  77. package/dist/lib/template-diff.js +89 -0
  78. package/dist/lib/template-downloader.js +3 -2
  79. package/dist/lib/transpile/project.js +1 -1
  80. package/dist/virtual-routes/assets/debug-network.css +592 -0
  81. package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
  82. package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
  83. package/dist/virtual-routes/components/IconClose.jsx +38 -0
  84. package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
  85. package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
  86. package/dist/virtual-routes/components/RequestTable.jsx +92 -0
  87. package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
  88. package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
  89. package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
  90. package/oclif.manifest.json +54 -61
  91. package/package.json +14 -11
  92. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
  93. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
  94. package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
  95. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
  96. package/dist/virtual-routes/routes/debug-network.jsx +0 -289
  97. /package/dist/generator-templates/starter/app/{utils.ts → lib/variants.ts} +0 -0
@@ -1,22 +1,22 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
- import { outputInfo, outputWarn, outputContent, outputToken } from '@shopify/cli-kit/node/output';
3
+ import { outputInfo, outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
4
4
  import { rmdir, fileSize, glob, readFile, writeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
- import { resolvePath, joinPath, relativePath } from '@shopify/cli-kit/node/path';
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
8
  import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
9
- import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
9
+ import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
10
10
  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
+ import { buildBundleAnalysis, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
16
15
  import { isCI } from '../../lib/is-ci.js';
16
+ import { prepareDiffDirectory, copyDiffBuild } from '../../lib/template-diff.js';
17
17
 
18
18
  const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
19
- const MAX_WORKER_BUNDLE_SIZE = 10;
19
+ const WORKER_BUILD_SIZE_LIMIT = 5;
20
20
  class Build extends Command {
21
21
  static description = "Builds a Hydrogen storefront for production.";
22
22
  static flags = {
@@ -44,18 +44,24 @@ class Build extends Command {
44
44
  }),
45
45
  codegen: commonFlags.codegen,
46
46
  "codegen-config-path": commonFlags.codegenConfigPath,
47
- base: deprecated("--base")(),
48
- entry: deprecated("--entry")(),
49
- target: deprecated("--target")()
47
+ diff: commonFlags.diff
50
48
  };
51
49
  async run() {
52
50
  const { flags } = await this.parse(Build);
53
- const directory = flags.path ? resolvePath(flags.path) : process.cwd();
51
+ const originalDirectory = flags.path ? resolvePath(flags.path) : process.cwd();
52
+ let directory = originalDirectory;
53
+ if (flags.diff) {
54
+ directory = await prepareDiffDirectory(originalDirectory, false);
55
+ }
54
56
  await runBuild({
55
57
  ...flagsToCamelObject(flags),
56
58
  useCodegen: flags.codegen,
57
59
  directory
58
60
  });
61
+ if (flags.diff) {
62
+ await copyDiffBuild(directory, originalDirectory);
63
+ }
64
+ process.exit(0);
59
65
  }
60
66
  }
61
67
  async function runBuild({
@@ -104,28 +110,37 @@ async function runBuild({
104
110
  fileWatchCache: createFileWatchCache()
105
111
  }).catch((thrown) => {
106
112
  logThrown(thrown);
107
- process.exit(1);
113
+ if (process.env.SHOPIFY_UNIT_TEST) {
114
+ throw thrown;
115
+ } else {
116
+ process.exit(1);
117
+ }
108
118
  }),
109
119
  useCodegen && codegen({ ...remixConfig, configFilePath: codegenConfigPath })
110
120
  ]);
111
121
  if (process.env.NODE_ENV !== "development") {
112
122
  console.timeEnd(LOG_WORKER_BUILT);
123
+ const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
113
124
  const sizeMB = await fileSize(buildPathWorkerFile) / (1024 * 1024);
114
- if (await hasMetafile(buildPath)) {
115
- await writeBundleAnalysis(
116
- buildPath,
117
- root,
118
- buildPathWorkerFile,
119
- sizeMB,
120
- bundleStats,
121
- remixConfig
125
+ const formattedSize = colors.yellow(sizeMB.toFixed(2) + " MB");
126
+ outputInfo(
127
+ outputContent` ${colors.dim(
128
+ relativePath(root, buildPathWorkerFile)
129
+ )} ${bundleAnalysisPath ? outputToken.link(formattedSize, bundleAnalysisPath) : formattedSize}\n`
130
+ );
131
+ if (bundleStats && bundleAnalysisPath) {
132
+ outputInfo(
133
+ outputContent`${await getBundleAnalysisSummary(buildPathWorkerFile) || "\n"}\n │\n └─── ${outputToken.link(
134
+ "Complete analysis: " + bundleAnalysisPath,
135
+ bundleAnalysisPath
136
+ )}\n\n`
122
137
  );
123
- } else {
124
- await writeSimpleBuildStatus(
125
- root,
126
- buildPathWorkerFile,
127
- sizeMB,
128
- remixConfig
138
+ }
139
+ if (sizeMB >= WORKER_BUILD_SIZE_LIMIT) {
140
+ outputWarn(
141
+ `\u{1F6A8} Smaller worker bundles are faster to deploy and run.${remixConfig.serverMinify ? "" : "\n Minify your bundle by adding `serverMinify: true` to remix.config.js."}
142
+ Learn more about optimizing your worker bundle file: https://h2o.fyi/debugging/bundle-size
143
+ `
129
144
  );
130
145
  }
131
146
  }
@@ -145,9 +160,6 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
145
160
  if (process.env.NODE_ENV !== "development") {
146
161
  await cleanClientSourcemaps(buildPathClient);
147
162
  }
148
- if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
149
- process.exit(0);
150
- }
151
163
  }
152
164
  async function cleanClientSourcemaps(buildPathClient) {
153
165
  const bundleFiles = await glob(joinPath(buildPathClient, "**/*.js"));
@@ -161,56 +173,6 @@ async function cleanClientSourcemaps(buildPathClient) {
161
173
  })
162
174
  );
163
175
  }
164
- async function writeBundleAnalysis(buildPath, root, buildPathWorkerFile, sizeMB, bundleStats, remixConfig) {
165
- const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
166
- outputInfo(
167
- outputContent` ${colors.dim(
168
- relativePath(root, buildPathWorkerFile)
169
- )} ${outputToken.link(
170
- colors.yellow(sizeMB.toFixed(2) + " MB"),
171
- bundleAnalysisPath
172
- )}\n`
173
- );
174
- if (bundleStats && sizeMB < MAX_WORKER_BUNDLE_SIZE) {
175
- outputInfo(
176
- outputContent`${await getBundleAnalysisSummary(buildPathWorkerFile) || "\n"}\n │\n └─── ${outputToken.link(
177
- "Complete analysis: " + bundleAnalysisPath,
178
- bundleAnalysisPath
179
- )}\n\n`
180
- );
181
- }
182
- if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
183
- throw new AbortError(
184
- "\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB.",
185
- outputContent`See the bundle analysis for a breakdown of what is contributing to the bundle size:\n${outputToken.link(
186
- bundleAnalysisPath,
187
- bundleAnalysisPath
188
- )}`
189
- );
190
- } else if (sizeMB >= 5) {
191
- outputWarn(
192
- `\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."}
193
- `
194
- );
195
- }
196
- }
197
- async function writeSimpleBuildStatus(root, buildPathWorkerFile, sizeMB, remixConfig) {
198
- outputInfo(
199
- outputContent` ${colors.dim(
200
- relativePath(root, buildPathWorkerFile)
201
- )} ${colors.yellow(sizeMB.toFixed(2) + " MB")}\n`
202
- );
203
- if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
204
- throw new AbortError(
205
- "\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB."
206
- );
207
- } else if (sizeMB >= 5) {
208
- outputWarn(
209
- `\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."}
210
- `
211
- );
212
- }
213
- }
214
176
  async function copyPublicFiles(publicPath, buildPathClient) {
215
177
  if (!await fileExists(publicPath)) {
216
178
  return;
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
3
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
4
+ import colors from '@shopify/cli-kit/node/colors';
4
5
  import { Flags } from '@oclif/core';
5
6
  import { getProjectPaths, getRemixConfig } from '../../lib/remix-config.js';
6
7
  import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
@@ -24,8 +25,6 @@ class Codegen extends Command {
24
25
  default: false
25
26
  })
26
27
  };
27
- static aliases = ["codegen-unstable"];
28
- static deprecateAliases = true;
29
28
  async run() {
30
29
  const { flags } = await this.parse(Codegen);
31
30
  const directory = flags.path ? path.resolve(flags.path) : process.cwd();
@@ -53,7 +52,13 @@ async function runCodegen({
53
52
  if (!watch) {
54
53
  renderSuccess({
55
54
  headline: "Generated types for GraphQL:",
56
- body: generatedFiles.map((file) => `- ${file}`).join("\n")
55
+ body: {
56
+ list: {
57
+ items: Object.entries(generatedFiles).map(
58
+ ([key, value]) => key + "\n" + value.map((item) => colors.dim(`- ${item}`)).join("\n")
59
+ )
60
+ }
61
+ }
57
62
  });
58
63
  }
59
64
  }
@@ -19,12 +19,17 @@ const deploymentLogger = (message, level = "info") => {
19
19
  }
20
20
  };
21
21
  class Deploy extends Command {
22
+ static description = "Builds and deploys a Hydrogen storefront to Oxygen.";
22
23
  static flags = {
23
24
  "env-branch": Flags.string({
24
- char: "e",
25
- description: "Environment branch (tag) for environment to deploy to",
25
+ description: "Environment branch (tag) for environment to deploy to.",
26
26
  required: false
27
27
  }),
28
+ preview: Flags.boolean({
29
+ description: "Deploys to the Preview environment. Overrides --env-branch and Git metadata.",
30
+ required: false,
31
+ default: false
32
+ }),
28
33
  force: Flags.boolean({
29
34
  char: "f",
30
35
  description: "Forces a deployment to proceed if there are uncommited changes in its Git repository.",
@@ -32,22 +37,21 @@ class Deploy extends Command {
32
37
  env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
33
38
  required: false
34
39
  }),
35
- path: commonFlags.path,
36
- shop: commonFlags.shop,
37
- "public-deployment": Flags.boolean({
38
- env: "SHOPIFY_HYDROGEN_FLAG_PUBLIC_DEPLOYMENT",
39
- description: "Marks a preview deployment as publicly accessible.",
40
+ "auth-bypass-token": Flags.boolean({
41
+ description: "Generate an authentication bypass token, which can be used to perform end-to-end tests against the deployment.",
40
42
  required: false,
41
43
  default: false
42
44
  }),
45
+ path: commonFlags.path,
46
+ shop: commonFlags.shop,
43
47
  "no-json-output": Flags.boolean({
44
- description: "Prevents the command from creating a JSON file containing the deployment URL (in CI environments).",
48
+ description: "Prevents the command from creating a JSON file containing the deployment URL in CI environments.",
45
49
  required: false,
46
50
  default: false
47
51
  }),
48
52
  token: Flags.string({
49
53
  char: "t",
50
- description: "Oxygen deployment token",
54
+ description: "Oxygen deployment token. Defaults to the linked storefront's token if available.",
51
55
  env: "SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN",
52
56
  required: false
53
57
  }),
@@ -57,9 +61,9 @@ class Deploy extends Command {
57
61
  env: "SHOPIFY_HYDROGEN_FLAG_METADATA_DESCRIPTION"
58
62
  }),
59
63
  "metadata-url": Flags.string({
60
- description: "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
61
64
  required: false,
62
- env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL"
65
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL",
66
+ hidden: true
63
67
  }),
64
68
  "metadata-user": Flags.string({
65
69
  description: "User that initiated the deployment. Will be saved and displayed in the Shopify admin",
@@ -67,12 +71,11 @@ class Deploy extends Command {
67
71
  env: "SHOPIFY_HYDROGEN_FLAG_METADATA_USER"
68
72
  }),
69
73
  "metadata-version": Flags.string({
70
- description: "A version identifier for the deployment. Will be saved and displayed in the Shopify admin",
71
74
  required: false,
72
- env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION"
75
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION",
76
+ hidden: true
73
77
  })
74
78
  };
75
- static hidden = true;
76
79
  async run() {
77
80
  const { flags } = await this.parse(Deploy);
78
81
  const deploymentOptions = this.flagsToOxygenDeploymentOptions(flags);
@@ -87,19 +90,39 @@ class Deploy extends Command {
87
90
  const camelFlags = flagsToCamelObject(flags);
88
91
  return {
89
92
  ...camelFlags,
93
+ defaultEnvironment: flags.preview,
90
94
  environmentTag: flags["env-branch"],
91
95
  path: flags.path ? resolvePath(flags.path) : process.cwd()
92
96
  };
93
97
  }
94
98
  }
99
+ function createUnexpectedAbortError(message) {
100
+ return new AbortError(
101
+ message || "The deployment failed due to an unexpected error.",
102
+ "Retrying the deployement may succeed.",
103
+ [
104
+ [
105
+ "If the issue persits, please check the",
106
+ {
107
+ link: {
108
+ label: "Shopify status page",
109
+ url: "https://status.shopify.com/"
110
+ }
111
+ },
112
+ "for any known issues."
113
+ ]
114
+ ]
115
+ );
116
+ }
95
117
  async function oxygenDeploy(options) {
96
118
  const {
119
+ authBypassToken: generateAuthBypassToken,
120
+ defaultEnvironment,
97
121
  environmentTag,
98
122
  force: forceOnUncommitedChanges,
99
123
  noJsonOutput,
100
124
  path,
101
125
  shop,
102
- publicDeployment,
103
126
  metadataUrl,
104
127
  metadataUser,
105
128
  metadataVersion
@@ -168,12 +191,17 @@ async function oxygenDeploy(options) {
168
191
  ] : `Could not obtain an Oxygen deployment token, please try again or contact Shopify support.`;
169
192
  throw new AbortError(errMessage);
170
193
  }
171
- if (!isCI && !environmentTag && deploymentData?.environments) {
194
+ if (!isCI && !defaultEnvironment && !environmentTag && deploymentData?.environments) {
172
195
  if (deploymentData.environments.length > 1) {
173
196
  const choices = [
174
- ...deploymentData.environments.map(({ name, branch: branch2 }) => ({
197
+ ...deploymentData.environments.map(({ name, branch: branch2, type }) => ({
175
198
  label: name,
176
- value: branch2
199
+ // The preview environment will never have an associated branch so
200
+ // we're using a custom string here to identify it later in our code.
201
+ // Using a period at the end of the value is an invalid branch name
202
+ // in Git so we can be sure that this won't conflict with a merchant's
203
+ // repository.
204
+ value: type === "PREVIEW" ? "shopify-preview-environment." : branch2
177
205
  }))
178
206
  ];
179
207
  deploymentEnvironmentTag = await renderSelectPrompt({
@@ -194,12 +222,21 @@ async function oxygenDeploy(options) {
194
222
  "Using a custom deployment service. Don't do this in production!"
195
223
  );
196
224
  }
225
+ let fallbackEnvironmentTag = branch;
226
+ let isPreview = false;
227
+ if (deploymentEnvironmentTag === "shopify-preview-environment.") {
228
+ fallbackEnvironmentTag = void 0;
229
+ deploymentEnvironmentTag = void 0;
230
+ isPreview = true;
231
+ }
197
232
  const config = {
198
233
  assetsDir: "dist/client",
199
234
  bugsnag: true,
200
235
  deploymentUrl,
236
+ defaultEnvironment: defaultEnvironment || isPreview,
201
237
  deploymentToken: parseToken(token),
202
- environmentTag: environmentTag || deploymentEnvironmentTag || branch,
238
+ environmentTag: environmentTag || deploymentEnvironmentTag || fallbackEnvironmentTag,
239
+ generateAuthBypassToken,
203
240
  verificationMaxDuration: 180,
204
241
  metadata: {
205
242
  ...metadataDescription ? { description: metadataDescription } : {},
@@ -207,7 +244,6 @@ async function oxygenDeploy(options) {
207
244
  ...metadataUser ? { user: metadataUser } : {},
208
245
  ...metadataVersion ? { version: metadataVersion } : {}
209
246
  },
210
- publicDeployment,
211
247
  skipVerification: false,
212
248
  rootPath: path,
213
249
  skipBuild: false,
@@ -218,10 +254,16 @@ async function oxygenDeploy(options) {
218
254
  const uploadPromise = new Promise((resolve) => {
219
255
  resolveUpload = resolve;
220
256
  });
221
- let resolveHealthCheck;
222
- const healthCheckPromise = new Promise((resolve) => {
223
- resolveHealthCheck = resolve;
257
+ let resolveRoutableCheck;
258
+ const routableCheckPromise = new Promise((resolve) => {
259
+ resolveRoutableCheck = resolve;
224
260
  });
261
+ let resolveDeploymentCompletedVerification;
262
+ const deploymentCompletedVerificationPromise = new Promise(
263
+ (resolve) => {
264
+ resolveDeploymentCompletedVerification = resolve;
265
+ }
266
+ );
225
267
  let deployError = null;
226
268
  let resolveDeploy;
227
269
  let rejectDeploy;
@@ -237,11 +279,21 @@ async function oxygenDeploy(options) {
237
279
  await runBuild({
238
280
  directory: path,
239
281
  assetPath,
240
- sourcemap: false,
282
+ sourcemap: true,
241
283
  useCodegen: false
242
284
  });
243
285
  },
244
- onVerificationComplete: () => resolveHealthCheck(),
286
+ onDeploymentCompleted: () => resolveDeploymentCompletedVerification(),
287
+ onVerificationComplete: () => resolveRoutableCheck(),
288
+ onDeploymentCompletedVerificationError() {
289
+ deployError = new AbortError(
290
+ "Unable to verify the deployment was completed successfully",
291
+ "Please verify the deployment status in the Shopify Admin and retry deploying if necessary."
292
+ );
293
+ },
294
+ onDeploymentFailed: (details) => {
295
+ deployError = createUnexpectedAbortError(details.error || details.status);
296
+ },
245
297
  onUploadFilesStart: () => uploadStart(),
246
298
  onUploadFilesComplete: () => resolveUpload(),
247
299
  onVerificationError: (error) => {
@@ -267,23 +319,43 @@ async function oxygenDeploy(options) {
267
319
  task: async () => await uploadPromise
268
320
  },
269
321
  {
270
- title: "Verifying deployment",
271
- task: async () => await healthCheckPromise
322
+ title: "Verifying deployment has been completed",
323
+ task: async () => await deploymentCompletedVerificationPromise
324
+ },
325
+ {
326
+ title: "Verifying deployment is routable",
327
+ task: async () => await routableCheckPromise
272
328
  }
273
329
  ]);
274
330
  };
275
- await createDeploy({ config, hooks, logger: deploymentLogger }).then(async (url) => {
276
- const deploymentType = config.publicDeployment ? "public" : "private";
331
+ await createDeploy({ config, hooks, logger: deploymentLogger }).then(async (completedDeployment) => {
332
+ if (!completedDeployment) {
333
+ rejectDeploy(createUnexpectedAbortError());
334
+ return;
335
+ }
336
+ const nextSteps = [
337
+ [
338
+ "Open",
339
+ { link: { url: completedDeployment.url } },
340
+ `in your browser to view your deployment.`
341
+ ]
342
+ ];
343
+ if (completedDeployment?.authBypassToken) {
344
+ nextSteps.push([
345
+ "Use the",
346
+ { subdued: completedDeployment.authBypassToken },
347
+ "token to perform end-to-end tests against the deployment."
348
+ ]);
349
+ }
277
350
  renderSuccess({
278
351
  body: ["Successfully deployed to Oxygen"],
279
- nextSteps: [
280
- [
281
- `Open ${url} in your browser to view your ${deploymentType} deployment`
282
- ]
283
- ]
352
+ nextSteps
284
353
  });
285
354
  if (isCI && !noJsonOutput) {
286
- await writeFile("h2_deploy_log.json", JSON.stringify({ url }));
355
+ await writeFile(
356
+ "h2_deploy_log.json",
357
+ JSON.stringify(completedDeployment)
358
+ );
287
359
  }
288
360
  resolveDeploy();
289
361
  }).catch((error) => {
@@ -66,11 +66,12 @@ describe("deploy", () => {
66
66
  };
67
67
  const originalExit = process.exit;
68
68
  const deployParams = {
69
+ authBypassToken: true,
70
+ defaultEnvironment: false,
69
71
  force: false,
70
72
  noJsonOutput: false,
71
73
  path: "./",
72
74
  shop: "snowdevil.myshopify.com",
73
- publicDeployment: false,
74
75
  metadataUrl: "https://example.com",
75
76
  metadataUser: "user",
76
77
  metadataVersion: "1.0.0"
@@ -87,15 +88,16 @@ describe("deploy", () => {
87
88
  const expectedConfig = {
88
89
  assetsDir: "dist/client",
89
90
  bugsnag: true,
91
+ defaultEnvironment: false,
90
92
  deploymentUrl: "https://oxygen.shopifyapps.com",
91
93
  deploymentToken: mockToken,
94
+ generateAuthBypassToken: true,
92
95
  verificationMaxDuration: 180,
93
96
  metadata: {
94
97
  url: deployParams.metadataUrl,
95
98
  user: deployParams.metadataUser,
96
99
  version: deployParams.metadataVersion
97
100
  },
98
- publicDeployment: deployParams.publicDeployment,
99
101
  skipVerification: false,
100
102
  rootPath: deployParams.path,
101
103
  skipBuild: false,
@@ -104,10 +106,13 @@ describe("deploy", () => {
104
106
  };
105
107
  const expectedHooks = {
106
108
  buildFunction: expect.any(Function),
109
+ onDeploymentCompleted: expect.any(Function),
110
+ onDeploymentFailed: expect.any(Function),
111
+ onDeploymentCompletedVerificationError: expect.any(Function),
107
112
  onVerificationComplete: expect.any(Function),
113
+ onVerificationError: expect.any(Function),
108
114
  onUploadFilesStart: expect.any(Function),
109
115
  onUploadFilesComplete: expect.any(Function),
110
- onVerificationError: expect.any(Function),
111
116
  onUploadFilesError: expect.any(Function)
112
117
  };
113
118
  beforeEach(async () => {
@@ -125,9 +130,10 @@ describe("deploy", () => {
125
130
  }
126
131
  ]);
127
132
  vi.mocked(renderSelectPrompt).mockResolvedValue(FULL_SHOPIFY_CONFIG.shop);
128
- vi.mocked(createDeploy).mockResolvedValue(
129
- "https://a-lovely-deployment.com"
130
- );
133
+ vi.mocked(createDeploy).mockResolvedValue({
134
+ authBypassToken: "some-token",
135
+ url: "https://a-lovely-deployment.com"
136
+ });
131
137
  vi.mocked(getOxygenDeploymentData).mockResolvedValue({
132
138
  oxygenDeploymentToken: "some-encoded-token",
133
139
  environments: []
@@ -252,19 +258,52 @@ describe("deploy", () => {
252
258
  vi.mocked(getOxygenDeploymentData).mockResolvedValue({
253
259
  oxygenDeploymentToken: "some-encoded-token",
254
260
  environments: [
255
- { name: "production", branch: "main" },
256
- { name: "preview", branch: "staging" }
261
+ { name: "Production", branch: "main", type: "PRODUCTION" },
262
+ { name: "Preview", branch: null, type: "PREVIEW" }
257
263
  ]
258
264
  });
259
265
  await oxygenDeploy(deployParams);
260
266
  expect(vi.mocked(renderSelectPrompt)).toHaveBeenCalledWith({
261
267
  message: "Select an environment to deploy to",
262
268
  choices: [
263
- { label: "production", value: "main" },
264
- { label: "preview", value: "staging" }
269
+ { label: "Production", value: "main" },
270
+ { label: "Preview", value: "shopify-preview-environment." }
265
271
  ]
266
272
  });
267
273
  });
274
+ describe("when Preview is selected", () => {
275
+ it("calls createDeploy with defaultEnvironment and an undefined environmentTag", async () => {
276
+ vi.mocked(getLatestGitCommit).mockResolvedValue({
277
+ hash: "123",
278
+ message: "test commit",
279
+ date: "2021-01-01",
280
+ author_name: "test author",
281
+ author_email: "test@author.com",
282
+ body: "test body",
283
+ refs: "HEAD -> main"
284
+ });
285
+ vi.mocked(getOxygenDeploymentData).mockResolvedValue({
286
+ oxygenDeploymentToken: "some-encoded-token",
287
+ environments: [
288
+ { name: "Production", branch: "main", type: "PRODUCTION" },
289
+ { name: "Preview", branch: null, type: "PREVIEW" }
290
+ ]
291
+ });
292
+ vi.mocked(renderSelectPrompt).mockResolvedValue(
293
+ "shopify-preview-environment."
294
+ );
295
+ await oxygenDeploy(deployParams);
296
+ expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
297
+ config: {
298
+ ...expectedConfig,
299
+ defaultEnvironment: true,
300
+ environmentTag: void 0
301
+ },
302
+ hooks: expectedHooks,
303
+ logger: deploymentLogger
304
+ });
305
+ });
306
+ });
268
307
  it("writes a file with JSON content in CI environments", async () => {
269
308
  vi.mocked(ciPlatform).mockReturnValue({
270
309
  isCI: true,
@@ -274,12 +313,16 @@ describe("deploy", () => {
274
313
  const ciDeployParams = {
275
314
  ...deployParams,
276
315
  token: "some-token",
277
- metadataDescription: "cool new stuff"
316
+ metadataDescription: "cool new stuff",
317
+ generateAuthBypassToken: true
278
318
  };
279
319
  await oxygenDeploy(ciDeployParams);
280
320
  expect(vi.mocked(writeFile)).toHaveBeenCalledWith(
281
321
  "h2_deploy_log.json",
282
- JSON.stringify({ url: "https://a-lovely-deployment.com" })
322
+ JSON.stringify({
323
+ authBypassToken: "some-token",
324
+ url: "https://a-lovely-deployment.com"
325
+ })
283
326
  );
284
327
  vi.mocked(writeFile).mockClear();
285
328
  ciDeployParams.noJsonOutput = true;
@@ -311,7 +354,7 @@ describe("deploy", () => {
311
354
  }
312
355
  }
313
356
  });
314
- it("handles error during deployment verification", async () => {
357
+ it("handles error during deployment routability verification", async () => {
315
358
  const mockRenderFatalError = vi.fn();
316
359
  vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
317
360
  const error = new Error("Cloudflare is down!");
@@ -337,4 +380,31 @@ describe("deploy", () => {
337
380
  }
338
381
  }
339
382
  });
383
+ it("handles error during deployment completion verification", async () => {
384
+ const mockRenderFatalError = vi.fn();
385
+ vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
386
+ vi.mocked(createDeploy).mockImplementation((options) => {
387
+ options.hooks?.onUploadFilesStart?.();
388
+ options.hooks?.onUploadFilesComplete?.();
389
+ options.hooks?.onDeploymentCompletedVerificationStart?.();
390
+ options.hooks?.onDeploymentFailed?.({
391
+ status: "oh shit",
392
+ url: "https://a-lovely-deployment.com"
393
+ });
394
+ return new Promise((_resolve, reject) => {
395
+ reject();
396
+ });
397
+ });
398
+ try {
399
+ await oxygenDeploy(deployParams);
400
+ expect(true).toBe(false);
401
+ } catch (err) {
402
+ if (err instanceof AbortError) {
403
+ expect(err.message).toBe("oh shit");
404
+ expect(err.tryMessage).toBe("Retrying the deployement may succeed.");
405
+ } else {
406
+ expect(true).toBe(false);
407
+ }
408
+ }
409
+ });
340
410
  });