@shopify/cli-hydrogen 6.0.2 → 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 (108) 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 +173 -37
  4. package/dist/commands/hydrogen/deploy.test.js +192 -20
  5. package/dist/commands/hydrogen/dev.js +56 -31
  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 +22 -11
  11. package/dist/commands/hydrogen/setup.js +0 -4
  12. package/dist/commands/hydrogen/setup.test.js +0 -1
  13. package/dist/commands/hydrogen/shortcut.js +1 -0
  14. package/dist/commands/hydrogen/upgrade.js +720 -0
  15. package/dist/commands/hydrogen/upgrade.test.js +786 -0
  16. package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
  17. package/dist/generator-templates/starter/CHANGELOG.md +126 -0
  18. package/dist/generator-templates/starter/README.md +23 -0
  19. package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
  20. package/dist/generator-templates/starter/app/components/Footer.tsx +3 -1
  21. package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
  22. package/dist/generator-templates/starter/app/components/Layout.tsx +14 -11
  23. package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
  24. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
  25. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
  26. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
  27. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
  28. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
  29. package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
  30. package/dist/generator-templates/starter/app/lib/session.ts +67 -0
  31. package/dist/generator-templates/starter/app/root.tsx +11 -45
  32. package/dist/generator-templates/starter/app/routes/[robots.txt].tsx +0 -27
  33. package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
  34. package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
  35. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
  36. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
  37. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
  38. package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
  39. package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
  40. package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
  41. package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
  42. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
  43. package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
  44. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
  45. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
  46. package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
  47. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
  48. package/dist/generator-templates/starter/package.json +11 -10
  49. package/dist/generator-templates/starter/remix.config.js +4 -0
  50. package/dist/generator-templates/starter/remix.env.d.ts +6 -11
  51. package/dist/generator-templates/starter/server.ts +24 -167
  52. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
  53. package/dist/hooks/init.js +4 -4
  54. package/dist/lib/auth.js +5 -10
  55. package/dist/lib/build.js +6 -1
  56. package/dist/lib/bundle/analyzer.js +36 -26
  57. package/dist/lib/check-lockfile.js +1 -0
  58. package/dist/lib/codegen.js +59 -18
  59. package/dist/lib/defer.js +12 -0
  60. package/dist/lib/file.js +52 -3
  61. package/dist/lib/flags.js +27 -9
  62. package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
  63. package/dist/lib/graphql/admin/client.test.js +2 -2
  64. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  65. package/dist/lib/log.js +32 -14
  66. package/dist/lib/mini-oxygen/assets.js +118 -0
  67. package/dist/lib/mini-oxygen/common.js +2 -1
  68. package/dist/lib/mini-oxygen/index.js +7 -5
  69. package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
  70. package/dist/lib/mini-oxygen/node.js +19 -5
  71. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +227 -0
  72. package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +200 -0
  73. package/dist/lib/mini-oxygen/workerd-inspector.js +62 -235
  74. package/dist/lib/mini-oxygen/workerd.js +74 -50
  75. package/dist/lib/missing-routes.js +6 -3
  76. package/dist/lib/onboarding/common.js +40 -9
  77. package/dist/lib/onboarding/local.js +19 -11
  78. package/dist/lib/onboarding/remote.js +48 -28
  79. package/dist/lib/render-errors.js +2 -0
  80. package/dist/lib/request-events.js +65 -31
  81. package/dist/lib/setups/css/assets.js +1 -46
  82. package/dist/lib/setups/css/css-modules.js +3 -2
  83. package/dist/lib/setups/css/postcss.js +4 -2
  84. package/dist/lib/setups/css/tailwind.js +4 -2
  85. package/dist/lib/setups/css/vanilla-extract.js +3 -2
  86. package/dist/lib/setups/i18n/replacers.test.js +56 -38
  87. package/dist/lib/shell.js +1 -1
  88. package/dist/lib/template-diff.js +89 -0
  89. package/dist/lib/template-downloader.js +3 -2
  90. package/dist/lib/transpile/project.js +1 -1
  91. package/dist/virtual-routes/assets/debug-network.css +592 -0
  92. package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
  93. package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
  94. package/dist/virtual-routes/components/IconClose.jsx +38 -0
  95. package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
  96. package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
  97. package/dist/virtual-routes/components/RequestTable.jsx +92 -0
  98. package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
  99. package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
  100. package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
  101. package/oclif.manifest.json +134 -59
  102. package/package.json +18 -26
  103. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
  104. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
  105. package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
  106. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
  107. package/dist/virtual-routes/routes/debug-network.jsx +0 -289
  108. /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
  }
@@ -3,9 +3,10 @@ import Command from '@shopify/cli-kit/node/base-command';
3
3
  import colors from '@shopify/cli-kit/node/colors';
4
4
  import { outputWarn, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
5
5
  import { AbortError } from '@shopify/cli-kit/node/error';
6
- import { getLatestGitCommit } from '@shopify/cli-kit/node/git';
6
+ import { writeFile } from '@shopify/cli-kit/node/fs';
7
+ import { ensureIsClean, getLatestGitCommit, GitDirectoryNotCleanError } from '@shopify/cli-kit/node/git';
7
8
  import { resolvePath } from '@shopify/cli-kit/node/path';
8
- import { renderFatalError, renderSelectPrompt, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
9
+ import { renderFatalError, renderWarning, renderSelectPrompt, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
9
10
  import { ciPlatform } from '@shopify/cli-kit/node/context/local';
10
11
  import { parseToken, createDeploy } from '@shopify/oxygen-cli/deploy';
11
12
  import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
@@ -18,30 +19,51 @@ const deploymentLogger = (message, level = "info") => {
18
19
  }
19
20
  };
20
21
  class Deploy extends Command {
22
+ static description = "Builds and deploys a Hydrogen storefront to Oxygen.";
21
23
  static flags = {
22
24
  "env-branch": Flags.string({
23
- char: "e",
24
- description: "Environment branch (tag) for environment to deploy to",
25
+ description: "Environment branch (tag) for environment to deploy to.",
25
26
  required: false
26
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
+ }),
33
+ force: Flags.boolean({
34
+ char: "f",
35
+ description: "Forces a deployment to proceed if there are uncommited changes in its Git repository.",
36
+ default: false,
37
+ env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
38
+ required: false
39
+ }),
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.",
42
+ required: false,
43
+ default: false
44
+ }),
27
45
  path: commonFlags.path,
28
46
  shop: commonFlags.shop,
29
- "public-deployment": Flags.boolean({
30
- env: "SHOPIFY_HYDROGEN_FLAG_PUBLIC_DEPLOYMENT",
31
- description: "Marks a preview deployment as publicly accessible.",
47
+ "no-json-output": Flags.boolean({
48
+ description: "Prevents the command from creating a JSON file containing the deployment URL in CI environments.",
32
49
  required: false,
33
50
  default: false
34
51
  }),
35
52
  token: Flags.string({
36
53
  char: "t",
37
- description: "Oxygen deployment token",
54
+ description: "Oxygen deployment token. Defaults to the linked storefront's token if available.",
38
55
  env: "SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN",
39
56
  required: false
40
57
  }),
58
+ "metadata-description": Flags.string({
59
+ description: "Description of the changes in the deployment. Defaults to the commit message of the latest commit if there are no uncommited changes.",
60
+ required: false,
61
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_DESCRIPTION"
62
+ }),
41
63
  "metadata-url": Flags.string({
42
- description: "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
43
64
  required: false,
44
- env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL"
65
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL",
66
+ hidden: true
45
67
  }),
46
68
  "metadata-user": Flags.string({
47
69
  description: "User that initiated the deployment. Will be saved and displayed in the Shopify admin",
@@ -49,12 +71,11 @@ class Deploy extends Command {
49
71
  env: "SHOPIFY_HYDROGEN_FLAG_METADATA_USER"
50
72
  }),
51
73
  "metadata-version": Flags.string({
52
- description: "A version identifier for the deployment. Will be saved and displayed in the Shopify admin",
53
74
  required: false,
54
- env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION"
75
+ env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION",
76
+ hidden: true
55
77
  })
56
78
  };
57
- static hidden = true;
58
79
  async run() {
59
80
  const { flags } = await this.parse(Deploy);
60
81
  const deploymentOptions = this.flagsToOxygenDeploymentOptions(flags);
@@ -69,35 +90,90 @@ class Deploy extends Command {
69
90
  const camelFlags = flagsToCamelObject(flags);
70
91
  return {
71
92
  ...camelFlags,
93
+ defaultEnvironment: flags.preview,
72
94
  environmentTag: flags["env-branch"],
73
95
  path: flags.path ? resolvePath(flags.path) : process.cwd()
74
96
  };
75
97
  }
76
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
+ }
77
117
  async function oxygenDeploy(options) {
78
118
  const {
119
+ authBypassToken: generateAuthBypassToken,
120
+ defaultEnvironment,
79
121
  environmentTag,
122
+ force: forceOnUncommitedChanges,
123
+ noJsonOutput,
80
124
  path,
81
125
  shop,
82
- publicDeployment,
83
126
  metadataUrl,
84
127
  metadataUser,
85
128
  metadataVersion
86
129
  } = options;
87
- const ci = ciPlatform();
130
+ let { metadataDescription } = options;
131
+ let isCleanGit = true;
132
+ try {
133
+ await ensureIsClean(path);
134
+ } catch (error) {
135
+ if (error instanceof GitDirectoryNotCleanError) {
136
+ isCleanGit = false;
137
+ }
138
+ if (!forceOnUncommitedChanges && !isCleanGit) {
139
+ throw new AbortError("Uncommitted changes detected.", null, [
140
+ [
141
+ "Commit your changes before deploying or use the ",
142
+ { command: "--force" },
143
+ " flag to deploy with uncommitted changes."
144
+ ]
145
+ ]);
146
+ }
147
+ }
148
+ const isCI = ciPlatform().isCI;
88
149
  let token = options.token;
89
150
  let branch;
151
+ let commitHash;
90
152
  let deploymentData;
91
153
  let deploymentEnvironmentTag = void 0;
92
154
  let gitCommit;
93
155
  try {
94
156
  gitCommit = await getLatestGitCommit(path);
95
157
  branch = (/HEAD -> ([^,]*)/.exec(gitCommit.refs) || [])[1];
158
+ commitHash = gitCommit.hash;
96
159
  } catch (error) {
97
160
  outputWarn("Could not retrieve Git history.");
98
161
  branch = void 0;
99
162
  }
100
- if (!ci.isCI) {
163
+ if (!metadataDescription && !isCleanGit) {
164
+ renderWarning({
165
+ headline: "No deployment description provided",
166
+ body: [
167
+ "Deploying uncommited changes, but no description has been provided. Use the ",
168
+ { command: "--metadata-description" },
169
+ "flag to provide a description. If no description is provided, the description defaults to ",
170
+ { userInput: "<sha> with additional changes" },
171
+ " using the SHA of the last commit."
172
+ ]
173
+ });
174
+ metadataDescription = `${commitHash} with additional changes`;
175
+ }
176
+ if (!isCI) {
101
177
  deploymentData = await getOxygenDeploymentData({
102
178
  root: path,
103
179
  flagShop: shop
@@ -108,19 +184,24 @@ async function oxygenDeploy(options) {
108
184
  token = token || deploymentData.oxygenDeploymentToken;
109
185
  }
110
186
  if (!token) {
111
- const errMessage = ci.isCI ? [
187
+ const errMessage = isCI ? [
112
188
  "No deployment token provided. Use the ",
113
189
  { command: "--token" },
114
190
  " flag to provide a token."
115
191
  ] : `Could not obtain an Oxygen deployment token, please try again or contact Shopify support.`;
116
192
  throw new AbortError(errMessage);
117
193
  }
118
- if (!ci.isCI && !environmentTag && deploymentData?.environments) {
194
+ if (!isCI && !defaultEnvironment && !environmentTag && deploymentData?.environments) {
119
195
  if (deploymentData.environments.length > 1) {
120
196
  const choices = [
121
- ...deploymentData.environments.map(({ name, branch: branch2 }) => ({
197
+ ...deploymentData.environments.map(({ name, branch: branch2, type }) => ({
122
198
  label: name,
123
- 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
124
205
  }))
125
206
  ];
126
207
  deploymentEnvironmentTag = await renderSelectPrompt({
@@ -134,19 +215,35 @@ async function oxygenDeploy(options) {
134
215
  );
135
216
  }
136
217
  }
218
+ let deploymentUrl = "https://oxygen.shopifyapps.com";
219
+ if (process.env.UNSAFE_SHOPIFY_HYDROGEN_DEPLOYMENT_URL) {
220
+ deploymentUrl = process.env.UNSAFE_SHOPIFY_HYDROGEN_DEPLOYMENT_URL;
221
+ outputWarn(
222
+ "Using a custom deployment service. Don't do this in production!"
223
+ );
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
+ }
137
232
  const config = {
138
233
  assetsDir: "dist/client",
139
234
  bugsnag: true,
140
- deploymentUrl: "https://oxygen.shopifyapps.com",
235
+ deploymentUrl,
236
+ defaultEnvironment: defaultEnvironment || isPreview,
141
237
  deploymentToken: parseToken(token),
142
- environmentTag: environmentTag || deploymentEnvironmentTag || branch,
238
+ environmentTag: environmentTag || deploymentEnvironmentTag || fallbackEnvironmentTag,
239
+ generateAuthBypassToken,
143
240
  verificationMaxDuration: 180,
144
241
  metadata: {
242
+ ...metadataDescription ? { description: metadataDescription } : {},
145
243
  ...metadataUrl ? { url: metadataUrl } : {},
146
244
  ...metadataUser ? { user: metadataUser } : {},
147
245
  ...metadataVersion ? { version: metadataVersion } : {}
148
246
  },
149
- publicDeployment,
150
247
  skipVerification: false,
151
248
  rootPath: path,
152
249
  skipBuild: false,
@@ -157,10 +254,16 @@ async function oxygenDeploy(options) {
157
254
  const uploadPromise = new Promise((resolve) => {
158
255
  resolveUpload = resolve;
159
256
  });
160
- let resolveHealthCheck;
161
- const healthCheckPromise = new Promise((resolve) => {
162
- resolveHealthCheck = resolve;
257
+ let resolveRoutableCheck;
258
+ const routableCheckPromise = new Promise((resolve) => {
259
+ resolveRoutableCheck = resolve;
163
260
  });
261
+ let resolveDeploymentCompletedVerification;
262
+ const deploymentCompletedVerificationPromise = new Promise(
263
+ (resolve) => {
264
+ resolveDeploymentCompletedVerification = resolve;
265
+ }
266
+ );
164
267
  let deployError = null;
165
268
  let resolveDeploy;
166
269
  let rejectDeploy;
@@ -176,11 +279,21 @@ async function oxygenDeploy(options) {
176
279
  await runBuild({
177
280
  directory: path,
178
281
  assetPath,
179
- sourcemap: false,
282
+ sourcemap: true,
180
283
  useCodegen: false
181
284
  });
182
285
  },
183
- 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
+ },
184
297
  onUploadFilesStart: () => uploadStart(),
185
298
  onUploadFilesComplete: () => resolveUpload(),
186
299
  onVerificationError: (error) => {
@@ -206,21 +319,44 @@ async function oxygenDeploy(options) {
206
319
  task: async () => await uploadPromise
207
320
  },
208
321
  {
209
- title: "Verifying deployment",
210
- 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
211
328
  }
212
329
  ]);
213
330
  };
214
- await createDeploy({ config, hooks, logger: deploymentLogger }).then((url) => {
215
- 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
+ }
216
350
  renderSuccess({
217
351
  body: ["Successfully deployed to Oxygen"],
218
- nextSteps: [
219
- [
220
- `Open ${url} in your browser to view your ${deploymentType} deployment`
221
- ]
222
- ]
352
+ nextSteps
223
353
  });
354
+ if (isCI && !noJsonOutput) {
355
+ await writeFile(
356
+ "h2_deploy_log.json",
357
+ JSON.stringify(completedDeployment)
358
+ );
359
+ }
224
360
  resolveDeploy();
225
361
  }).catch((error) => {
226
362
  rejectDeploy(deployError || error);