@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
@@ -0,0 +1,720 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { createRequire } from 'node:module';
4
+ import semver from 'semver';
5
+ import cliTruncate from 'cli-truncate';
6
+ import { Flags } from '@oclif/core';
7
+ import { ensureInsideGitDirectory, isClean } from '@shopify/cli-kit/node/git';
8
+ import Command from '@shopify/cli-kit/node/base-command';
9
+ import { renderSuccess, renderInfo, renderConfirmationPrompt, renderTasks, renderSelectPrompt } from '@shopify/cli-kit/node/ui';
10
+ import { readFile, isDirectory, mkdir, fileExists, touchFile, removeFile, writeFile } from '@shopify/cli-kit/node/fs';
11
+ import { installNodeModules, getPackageManager, getDependencies } from '@shopify/cli-kit/node/node-package-manager';
12
+ import { AbortError } from '@shopify/cli-kit/node/error';
13
+ import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
14
+ import { getProjectPaths } from '../../lib/remix-config.js';
15
+
16
+ const INSTRUCTIONS_FOLDER = ".hydrogen";
17
+ class Upgrade extends Command {
18
+ static description = "Upgrade Remix and Hydrogen npm dependencies.";
19
+ static flags = {
20
+ path: commonFlags.path,
21
+ version: Flags.string({
22
+ description: "A target hydrogen version to update to",
23
+ required: false,
24
+ char: "v"
25
+ }),
26
+ force: Flags.boolean({
27
+ description: "Ignore warnings and force the upgrade to the target version",
28
+ env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
29
+ char: "f"
30
+ })
31
+ };
32
+ async run() {
33
+ const { flags } = await this.parse(Upgrade);
34
+ await runUpgrade({
35
+ ...flagsToCamelObject(flags),
36
+ appPath: flags.path ? path.resolve(flags.path) : process.cwd()
37
+ });
38
+ }
39
+ }
40
+ let CACHED_CHANGELOG = null;
41
+ async function runUpgrade({
42
+ appPath,
43
+ version: targetVersion,
44
+ force
45
+ }) {
46
+ if (!force) {
47
+ await checkIsGitRepo(appPath);
48
+ await checkDirtyGitBranch(appPath);
49
+ }
50
+ const { currentVersion, currentDependencies } = await getHydrogenVersion({
51
+ appPath
52
+ });
53
+ const isPrerelease = semver.prerelease(currentVersion);
54
+ if (isPrerelease) {
55
+ throw new AbortError(
56
+ "The upgrade command cannot be run over a prerelease Hydrogen version"
57
+ );
58
+ }
59
+ const changelog = await getChangelog();
60
+ const { availableUpgrades } = getAvailableUpgrades({
61
+ releases: changelog.releases,
62
+ currentVersion,
63
+ currentDependencies
64
+ });
65
+ if (!availableUpgrades?.length) {
66
+ renderSuccess({
67
+ headline: `You are on the latest Hydrogen version: ${getAbsoluteVersion(
68
+ currentVersion
69
+ )}`
70
+ });
71
+ return;
72
+ }
73
+ let confirmed = false;
74
+ let selectedRelease = void 0;
75
+ let cumulativeRelease = void 0;
76
+ do {
77
+ selectedRelease = await getSelectedRelease({
78
+ currentVersion,
79
+ targetVersion,
80
+ availableUpgrades
81
+ });
82
+ cumulativeRelease = getCummulativeRelease({
83
+ availableUpgrades,
84
+ currentVersion,
85
+ currentDependencies,
86
+ selectedRelease
87
+ });
88
+ confirmed = await displayConfirmation({
89
+ cumulativeRelease,
90
+ selectedRelease
91
+ });
92
+ } while (!confirmed);
93
+ const instrunctionsFilePathPromise = generateUpgradeInstructionsFile({
94
+ appPath,
95
+ cumulativeRelease,
96
+ currentVersion,
97
+ selectedRelease
98
+ });
99
+ await upgradeNodeModules({ appPath, selectedRelease, currentDependencies });
100
+ await validateUpgrade({
101
+ appPath,
102
+ selectedRelease
103
+ });
104
+ const instrunctionsFilePath = await instrunctionsFilePathPromise;
105
+ await displayUpgradeSummary({
106
+ appPath,
107
+ currentVersion,
108
+ instrunctionsFilePath,
109
+ selectedRelease
110
+ });
111
+ }
112
+ function checkIsGitRepo(appPath) {
113
+ return ensureInsideGitDirectory(appPath).catch(() => {
114
+ throw new AbortError(
115
+ "The upgrade command can only be run on a git repository",
116
+ `Please run the command inside a git repository or run 'git init' to create one`
117
+ );
118
+ });
119
+ }
120
+ async function checkDirtyGitBranch(appPath) {
121
+ if (!await isClean(appPath)) {
122
+ throw new AbortError(
123
+ "The upgrade command can only be run on a clean git branch",
124
+ "Please commit your changes or re-run the command on a clean branch"
125
+ );
126
+ }
127
+ }
128
+ async function getHydrogenVersion({ appPath }) {
129
+ const { root } = getProjectPaths(appPath);
130
+ const packageJsonPath = path.join(root, "package.json");
131
+ let packageJson;
132
+ try {
133
+ packageJson = JSON.parse(await readFile(packageJsonPath));
134
+ } catch {
135
+ throw new AbortError(
136
+ "Could not find a valid package.json",
137
+ "Please make sure you are running the command in a npm project"
138
+ );
139
+ }
140
+ const currentDependencies = {
141
+ ...packageJson.dependencies ?? {},
142
+ ...packageJson.devDependencies ?? {}
143
+ };
144
+ const currentVersion = currentDependencies["@shopify/hydrogen"];
145
+ if (!currentVersion) {
146
+ throw new AbortError(
147
+ "Could not find a valid Hydrogen version in package.json",
148
+ "Please make sure you are running the command in a Hydrogen project"
149
+ );
150
+ }
151
+ return { currentVersion, currentDependencies };
152
+ }
153
+ async function getChangelog() {
154
+ if (CACHED_CHANGELOG)
155
+ return CACHED_CHANGELOG;
156
+ if (process.env.FORCE_CHANGELOG_SOURCE === "local" || process.env.FORCE_CHANGELOG_SOURCE !== "remote" && !!process.env.LOCAL_ENV) {
157
+ const require2 = createRequire(import.meta.url);
158
+ return require2(fileURLToPath(
159
+ new URL("../../../../../docs/changelog.json", import.meta.url)
160
+ ));
161
+ }
162
+ try {
163
+ const response = await fetch("https://hydrogen.shopify.dev/changelog.json");
164
+ if (!response.ok) {
165
+ throw new Error("Failed to fetch changelog.json");
166
+ }
167
+ const json = await response.json();
168
+ if ("releases" in json && "url" in json) {
169
+ CACHED_CHANGELOG = json;
170
+ return CACHED_CHANGELOG;
171
+ }
172
+ } catch {
173
+ }
174
+ throw new AbortError(
175
+ "Failed to fetch changelog",
176
+ "Ensure you have internet connection and try again"
177
+ );
178
+ }
179
+ function hasOutdatedDependencies({
180
+ release,
181
+ currentDependencies
182
+ }) {
183
+ return Object.entries(release.dependencies).some(([name, version]) => {
184
+ const currentDependencyVersion = currentDependencies?.[name];
185
+ if (!currentDependencyVersion)
186
+ return false;
187
+ const isDependencyOutdated = semver.gt(
188
+ getAbsoluteVersion(version),
189
+ getAbsoluteVersion(currentDependencyVersion)
190
+ );
191
+ return isDependencyOutdated;
192
+ });
193
+ }
194
+ function isUpgradeableRelease({
195
+ currentDependencies,
196
+ currentPinnedVersion,
197
+ release
198
+ }) {
199
+ if (!currentDependencies)
200
+ return false;
201
+ const isHydrogenOutdated = semver.gt(release.version, currentPinnedVersion);
202
+ if (isHydrogenOutdated)
203
+ return true;
204
+ const isCurrentHydrogen = getAbsoluteVersion(release.version) === currentPinnedVersion;
205
+ if (!isCurrentHydrogen)
206
+ return false;
207
+ return hasOutdatedDependencies({ release, currentDependencies });
208
+ }
209
+ function getAvailableUpgrades({
210
+ releases,
211
+ currentVersion,
212
+ currentDependencies
213
+ }) {
214
+ const currentPinnedVersion = getAbsoluteVersion(currentVersion);
215
+ let currentMajorVersion = "";
216
+ const availableUpgrades = releases.filter((release) => {
217
+ const isUpgradeable = isUpgradeableRelease({
218
+ release,
219
+ currentPinnedVersion,
220
+ currentDependencies
221
+ });
222
+ if (!isUpgradeable)
223
+ return false;
224
+ if (currentMajorVersion !== release.version) {
225
+ currentMajorVersion = release.version;
226
+ return true;
227
+ }
228
+ return false;
229
+ });
230
+ const uniqueAvailableUpgrades = availableUpgrades.reduce((acc, release) => {
231
+ if (acc[release.version])
232
+ return acc;
233
+ acc[release.version] = release;
234
+ return acc;
235
+ }, {});
236
+ return { availableUpgrades, uniqueAvailableUpgrades };
237
+ }
238
+ async function getSelectedRelease({
239
+ targetVersion,
240
+ availableUpgrades,
241
+ currentVersion
242
+ }) {
243
+ const targetRelease = targetVersion ? availableUpgrades.find(
244
+ (release) => getAbsoluteVersion(release.version) === getAbsoluteVersion(targetVersion)
245
+ ) : void 0;
246
+ return targetRelease ?? promptUpgradeOptions(currentVersion, availableUpgrades);
247
+ }
248
+ function getCummulativeRelease({
249
+ availableUpgrades,
250
+ selectedRelease,
251
+ currentVersion,
252
+ currentDependencies
253
+ }) {
254
+ const currentPinnedVersion = getAbsoluteVersion(currentVersion);
255
+ if (!availableUpgrades?.length) {
256
+ return { features: [], fixes: [] };
257
+ }
258
+ const upgradingReleases = availableUpgrades.filter((release) => {
259
+ const isHydrogenUpgrade = semver.gt(release.version, currentPinnedVersion) && semver.lte(release.version, selectedRelease.version);
260
+ if (isHydrogenUpgrade)
261
+ return true;
262
+ const isSameHydrogenVersion = getAbsoluteVersion(release.version) === currentPinnedVersion;
263
+ if (!isSameHydrogenVersion || !currentDependencies)
264
+ return false;
265
+ return hasOutdatedDependencies({ release, currentDependencies });
266
+ });
267
+ return upgradingReleases.reduce(
268
+ (acc, release) => {
269
+ acc.features = [...acc.features, ...release.features];
270
+ acc.fixes = [...acc.fixes, ...release.fixes];
271
+ return acc;
272
+ },
273
+ { features: [], fixes: [] }
274
+ );
275
+ }
276
+ function displayConfirmation({
277
+ cumulativeRelease,
278
+ selectedRelease
279
+ }) {
280
+ const { features, fixes } = cumulativeRelease;
281
+ if (features.length || fixes.length) {
282
+ renderInfo({
283
+ headline: `Included in this upgrade:`,
284
+ //@ts-ignore we know that filter(Boolean) will always return an array
285
+ customSections: [
286
+ features.length && {
287
+ title: "Features",
288
+ body: [
289
+ {
290
+ list: {
291
+ items: features.map((item) => item.title)
292
+ }
293
+ }
294
+ ]
295
+ },
296
+ fixes.length && {
297
+ title: "Fixes",
298
+ body: [
299
+ {
300
+ list: {
301
+ items: fixes.map((item) => item.title)
302
+ }
303
+ }
304
+ ]
305
+ }
306
+ ].filter(Boolean)
307
+ });
308
+ }
309
+ return renderConfirmationPrompt({
310
+ message: `Are you sure you want to upgrade to ${selectedRelease.version}?`,
311
+ cancellationMessage: `No, choose another version`,
312
+ defaultValue: true
313
+ });
314
+ }
315
+ function isRemixDependency([name]) {
316
+ if (name.includes("@remix-run")) {
317
+ return true;
318
+ }
319
+ return false;
320
+ }
321
+ function maybeIncludeDependency({
322
+ currentDependencies,
323
+ dependency: [name, version],
324
+ selectedRelease
325
+ }) {
326
+ const existingDependencyVersion = currentDependencies[name];
327
+ const isRemixPackage = isRemixDependency([name, version]);
328
+ if (isRemixPackage)
329
+ return false;
330
+ const isNextVersion = existingDependencyVersion === "next";
331
+ if (isNextVersion)
332
+ return false;
333
+ const depMeta = selectedRelease.dependenciesMeta?.[name];
334
+ if (!depMeta)
335
+ return true;
336
+ const isRequired = Boolean(
337
+ selectedRelease.dependenciesMeta?.[name]?.required
338
+ );
339
+ if (!isRequired)
340
+ return false;
341
+ if (!existingDependencyVersion)
342
+ return true;
343
+ const isOlderVersion = semver.lt(
344
+ getAbsoluteVersion(existingDependencyVersion),
345
+ getAbsoluteVersion(version)
346
+ );
347
+ if (isOlderVersion)
348
+ return true;
349
+ return false;
350
+ }
351
+ function buildUpgradeCommandArgs({
352
+ selectedRelease,
353
+ currentDependencies
354
+ }) {
355
+ const args = [];
356
+ for (const dependency of Object.entries(selectedRelease.dependencies)) {
357
+ const shouldUpgradeDep = maybeIncludeDependency({
358
+ currentDependencies,
359
+ dependency,
360
+ selectedRelease
361
+ });
362
+ if (!shouldUpgradeDep)
363
+ continue;
364
+ args.push(`${dependency[0]}@${getAbsoluteVersion(dependency[1])}`);
365
+ }
366
+ for (const dependency of Object.entries(selectedRelease.devDependencies)) {
367
+ const shouldUpgradeDep = maybeIncludeDependency({
368
+ currentDependencies,
369
+ dependency,
370
+ selectedRelease
371
+ });
372
+ if (!shouldUpgradeDep)
373
+ continue;
374
+ args.push(`${dependency[0]}@${getAbsoluteVersion(dependency[1])}`);
375
+ }
376
+ const currentRemix = Object.entries(currentDependencies).find(isRemixDependency);
377
+ const selectedRemix = Object.entries(selectedRelease.dependencies).find(
378
+ isRemixDependency
379
+ );
380
+ if (currentRemix && selectedRemix) {
381
+ const shouldUpgradeRemix = semver.lt(
382
+ getAbsoluteVersion(currentRemix[1]),
383
+ getAbsoluteVersion(selectedRemix[1])
384
+ );
385
+ if (shouldUpgradeRemix) {
386
+ args.push(
387
+ ...appendRemixDependencies({ currentDependencies, selectedRemix })
388
+ );
389
+ }
390
+ }
391
+ return args;
392
+ }
393
+ async function upgradeNodeModules({
394
+ appPath,
395
+ selectedRelease,
396
+ currentDependencies
397
+ }) {
398
+ await renderTasks(
399
+ [
400
+ {
401
+ title: `Upgrading dependencies`,
402
+ task: async () => {
403
+ await installNodeModules({
404
+ directory: appPath,
405
+ packageManager: await getPackageManager(appPath),
406
+ args: buildUpgradeCommandArgs({
407
+ selectedRelease,
408
+ currentDependencies
409
+ })
410
+ });
411
+ }
412
+ }
413
+ ],
414
+ {}
415
+ );
416
+ }
417
+ function appendRemixDependencies({
418
+ currentDependencies,
419
+ selectedRemix
420
+ }) {
421
+ const command = [];
422
+ for (const [name, version] of Object.entries(currentDependencies)) {
423
+ const isRemixPackage = isRemixDependency([name, version]);
424
+ if (!isRemixPackage) {
425
+ continue;
426
+ }
427
+ command.push(`${name}@${getAbsoluteVersion(selectedRemix[1])}`);
428
+ }
429
+ return command;
430
+ }
431
+ function getAbsoluteVersion(version) {
432
+ const result = semver.minVersion(version);
433
+ if (!result) {
434
+ throw new AbortError(`Invalid version: ${version}`);
435
+ }
436
+ return result.version;
437
+ }
438
+ async function promptUpgradeOptions(currentVersion, availableUpgrades) {
439
+ if (!availableUpgrades?.length) {
440
+ throw new AbortError("No upgrade options available");
441
+ }
442
+ const choices = availableUpgrades.map((release, index) => {
443
+ const { version, title } = release;
444
+ const tag = index === 0 ? "(latest)" : semver.patch(version) === 0 ? "(major)" : getAbsoluteVersion(currentVersion) === getAbsoluteVersion(version) ? "(outdated)" : "";
445
+ `${semver.major(version)}.${semver.minor(version)}`;
446
+ return {
447
+ // group: majorVersion,
448
+ label: `${version} ${tag} - ${cliTruncate(title, 54)}`,
449
+ value: release
450
+ };
451
+ });
452
+ return renderSelectPrompt({
453
+ message: `Available Hydrogen versions (current: ${currentVersion})`,
454
+ choices,
455
+ defaultValue: choices[0]?.value
456
+ // Latest version
457
+ });
458
+ }
459
+ async function displayUpgradeSummary({
460
+ appPath,
461
+ currentVersion,
462
+ selectedRelease,
463
+ instrunctionsFilePath
464
+ }) {
465
+ const updatedDependenciesList = [
466
+ ...Object.entries(selectedRelease.dependencies || {}).map(
467
+ ([name, version]) => `${name}@${version}`
468
+ ),
469
+ ...Object.entries(selectedRelease.devDependencies || {}).map(
470
+ ([name, version]) => `${name}@${version}`
471
+ )
472
+ ];
473
+ let nextSteps = [];
474
+ if (typeof instrunctionsFilePath === "string") {
475
+ let instructions = `Upgrade instructions created at:
476
+ file://${instrunctionsFilePath}`;
477
+ nextSteps.push(instructions);
478
+ }
479
+ const releaseNotesUrl = `https://hydrogen.shopify.dev/releases/${selectedRelease.version}`;
480
+ nextSteps.push(`Release notes:
481
+ ${releaseNotesUrl}`);
482
+ const currentPinnedVersion = getAbsoluteVersion(currentVersion);
483
+ const selectedPinnedVersion = getAbsoluteVersion(selectedRelease.version);
484
+ const upgradedDependenciesOnly = currentPinnedVersion === selectedPinnedVersion;
485
+ const fromToMsg = `${currentPinnedVersion} \u2192 ${selectedPinnedVersion}`;
486
+ const headline = upgradedDependenciesOnly ? `You've have upgraded Hydrogen ${selectedPinnedVersion} dependencies` : `You've have upgraded from ${fromToMsg}`;
487
+ const packageManager = await getPackageManager(appPath);
488
+ return renderSuccess({
489
+ headline,
490
+ // @ts-ignore we know that filter(Boolean) will always return an array
491
+ customSections: [
492
+ {
493
+ title: "Updated dependencies",
494
+ body: [
495
+ {
496
+ list: {
497
+ items: updatedDependenciesList
498
+ }
499
+ }
500
+ ]
501
+ },
502
+ {
503
+ title: "What\u2019s next?",
504
+ body: [
505
+ {
506
+ list: {
507
+ items: nextSteps
508
+ }
509
+ }
510
+ ]
511
+ },
512
+ {
513
+ title: "Undo these upgrades?",
514
+ body: [
515
+ {
516
+ list: {
517
+ items: [
518
+ `Run \`git restore . && git clean -df && ${packageManager} i\``
519
+ ]
520
+ }
521
+ }
522
+ ]
523
+ }
524
+ ].filter(Boolean)
525
+ });
526
+ }
527
+ async function validateUpgrade({
528
+ appPath,
529
+ selectedRelease
530
+ }) {
531
+ const dependencies = await getDependencies(
532
+ path.join(appPath, "package.json")
533
+ );
534
+ const updatedVersion = dependencies["@shopify/hydrogen"];
535
+ if (!updatedVersion) {
536
+ throw new AbortError("Hydrogen version not found in package.json");
537
+ }
538
+ const updatedPinnedVersion = getAbsoluteVersion(updatedVersion);
539
+ if (updatedPinnedVersion !== selectedRelease.version) {
540
+ throw new AbortError(
541
+ `Failed to upgrade to Hydrogen version ${selectedRelease.version}`,
542
+ `You are still on version ${updatedPinnedVersion}`
543
+ );
544
+ }
545
+ }
546
+ function generateStepMd(item) {
547
+ const { steps } = item;
548
+ const heading = `### ${item.title} [#${item.id}](${item.pr})
549
+ `;
550
+ const body = steps?.map((step, stepIndex) => {
551
+ const pr = item.pr ? `[#${item.id}](${item.pr})
552
+ ` : "";
553
+ const multiStep = steps.length > 1;
554
+ const title = multiStep ? `#### Step: ${stepIndex + 1}. ${step.title} ${pr}
555
+ ` : `#### ${step.title.trim()}
556
+ `;
557
+ const info = step.info ? `> ${step.info}
558
+ ` : "";
559
+ const code = step.code ? `${Buffer.from(step.code, "base64")}
560
+ ` : "";
561
+ const docs = item.docs ? `[docs](${item.docs})
562
+ ` : "";
563
+ return `${title}${info}${docs}${pr}${code}`;
564
+ }).join("\n");
565
+ return `${heading}
566
+ ${body}`;
567
+ }
568
+ async function generateUpgradeInstructionsFile({
569
+ appPath,
570
+ cumulativeRelease,
571
+ currentVersion,
572
+ selectedRelease
573
+ }) {
574
+ let filename = "";
575
+ const { featuresMd, breakingChangesMd } = cumulativeRelease.features.filter((feature) => feature.steps).reduce(
576
+ (acc, feature) => {
577
+ if (feature.breaking) {
578
+ acc.breakingChangesMd.push(generateStepMd(feature));
579
+ } else {
580
+ acc.featuresMd.push(generateStepMd(feature));
581
+ }
582
+ return acc;
583
+ },
584
+ { featuresMd: [], breakingChangesMd: [] }
585
+ );
586
+ const fixesMd = cumulativeRelease.fixes.filter((fixes) => fixes.steps).map(generateStepMd);
587
+ if (!featuresMd.length && !fixesMd.length) {
588
+ renderInfo({
589
+ headline: `No upgrade instructions generated`,
590
+ body: `There are no additional upgrade instructions for this version.`
591
+ });
592
+ return;
593
+ }
594
+ const absoluteFrom = getAbsoluteVersion(currentVersion);
595
+ const absoluteTo = getAbsoluteVersion(selectedRelease.version);
596
+ filename = `upgrade-${absoluteFrom}-to-${absoluteTo}.md`;
597
+ const instructionsFolderPath = path.join(appPath, INSTRUCTIONS_FOLDER);
598
+ const h1 = `# Hydrogen upgrade guide: ${absoluteFrom} to ${absoluteTo}`;
599
+ let md = `${h1}
600
+
601
+ ----
602
+ `;
603
+ if (breakingChangesMd.length) {
604
+ md += `
605
+ ## Breaking changes
606
+
607
+ ${breakingChangesMd.join("\n")}
608
+ ----
609
+ `;
610
+ }
611
+ if (featuresMd.length) {
612
+ md += `
613
+ ## Features
614
+
615
+ ${featuresMd.join("\n")}
616
+ ----
617
+ `;
618
+ }
619
+ if (fixesMd.length) {
620
+ md += `
621
+ ${featuresMd.length ? "----\n\n" : ""}## Fixes
622
+
623
+ ${fixesMd.join(
624
+ "\n"
625
+ )}`;
626
+ }
627
+ const filePath = path.join(instructionsFolderPath, filename);
628
+ try {
629
+ await isDirectory(instructionsFolderPath);
630
+ } catch (error) {
631
+ await mkdir(instructionsFolderPath);
632
+ }
633
+ if (!await fileExists(filePath)) {
634
+ await touchFile(filePath);
635
+ } else {
636
+ const overwriteMdFile = await renderConfirmationPrompt({
637
+ message: `A previous upgrade instructions file already exists for this version.
638
+ Do you want to overwrite it?`,
639
+ defaultValue: false
640
+ });
641
+ if (overwriteMdFile) {
642
+ await removeFile(`${filePath}.old`);
643
+ } else {
644
+ return;
645
+ }
646
+ await touchFile(filePath);
647
+ }
648
+ await writeFile(filePath, md);
649
+ return `${INSTRUCTIONS_FOLDER}/${filename}`;
650
+ }
651
+ async function displayDevUpgradeNotice({
652
+ targetPath
653
+ }) {
654
+ const appPath = targetPath ? path.resolve(targetPath) : process.cwd();
655
+ const { currentVersion } = await getHydrogenVersion({ appPath });
656
+ const isPrerelease = semver.prerelease(currentVersion);
657
+ if (isPrerelease) {
658
+ return;
659
+ }
660
+ const changelog = await getChangelog();
661
+ const { availableUpgrades, uniqueAvailableUpgrades } = getAvailableUpgrades({
662
+ releases: changelog.releases,
663
+ currentVersion
664
+ });
665
+ if (availableUpgrades.length === 0 || !availableUpgrades[0]?.version) {
666
+ return;
667
+ }
668
+ const pinnedLatestVersion = getAbsoluteVersion(availableUpgrades[0].version);
669
+ const pinnedCurrentVersion = getAbsoluteVersion(currentVersion);
670
+ const currentReleaseIndex = changelog.releases.findIndex((release) => {
671
+ const pinnedReleaseVersion = getAbsoluteVersion(release.version);
672
+ return pinnedReleaseVersion === pinnedCurrentVersion;
673
+ });
674
+ const uniqueNextReleases = changelog.releases.slice(0, currentReleaseIndex).reverse().reduce((acc, release) => {
675
+ if (acc[release.version])
676
+ return acc;
677
+ acc[release.version] = release;
678
+ return acc;
679
+ }, {});
680
+ const nextReleases = Object.keys(uniqueNextReleases).length ? Object.entries(uniqueNextReleases).map(([version, release]) => {
681
+ return `${version} - ${release.title}`;
682
+ }).slice(0, 5) : [];
683
+ let headline = Object.keys(uniqueAvailableUpgrades).length > 1 ? `There are ${Object.keys(uniqueAvailableUpgrades).length} new @shopify/hydrogen versions available.` : `There's a new @shopify/hydrogen version available.`;
684
+ renderInfo({
685
+ headline,
686
+ body: [`Current: ${currentVersion} | Latest: ${pinnedLatestVersion}`],
687
+ //@ts-ignore will always be an array
688
+ customSections: nextReleases.length ? [
689
+ {
690
+ title: `The next ${nextReleases.length} version(s) include`,
691
+ body: [
692
+ {
693
+ list: {
694
+ items: [
695
+ ...nextReleases,
696
+ availableUpgrades.length > 5 && `...more`
697
+ ].flat().filter(Boolean)
698
+ }
699
+ }
700
+ ].filter(Boolean)
701
+ },
702
+ {
703
+ title: "Next steps",
704
+ body: [
705
+ {
706
+ list: {
707
+ items: [
708
+ `Run \`h2 upgrade\` or \`h2 upgrade --version XXXX.X.XX\``,
709
+ ,
710
+ `Read release notes at https://hydrogen.shopify.dev/releases`
711
+ ]
712
+ }
713
+ }
714
+ ]
715
+ }
716
+ ] : []
717
+ });
718
+ }
719
+
720
+ export { buildUpgradeCommandArgs, Upgrade as default, displayConfirmation, displayDevUpgradeNotice, getAbsoluteVersion, getAvailableUpgrades, getChangelog, getCummulativeRelease, getHydrogenVersion, getSelectedRelease, hasOutdatedDependencies, isUpgradeableRelease, runUpgrade, upgradeNodeModules };