@shopify/cli-hydrogen 11.1.11 → 11.1.13

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ The Hydrogen extension for the [Shopify CLI](https://shopify.dev/apps/tools/cli)
8
8
 
9
9
  The most common way to test the cli changes locally is to do the following:
10
10
 
11
- - Run `npm run build` in this directory (`packages/cli` from the root of the repo).
11
+ - Run `pnpm run build` in this directory (`packages/cli` from the root of the repo).
12
12
  - Run `npx shopify hydrogen` anywhere else in the monorepo, for example `npx shopify hydrogen init`.
13
13
  - If you want to test a command inside of a template, run the command from within that template or use the `--path` flag to point to another template or any Hydrogen app.
14
14
  - If you want to make changes to a file that is generated when running `npx shopify hydrogen generate`, make changes to that file from inside of the `templates/skeleton` directory.
@@ -1,5 +1,29 @@
1
1
  # skeleton
2
2
 
3
+ ## 2026.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Remove redundant Storefront API proxy route from skeleton template. The server now automatically proxies requests to `/api/:version/graphql.json` via `createRequestHandler` with `proxyStandardRoutes: true` (enabled by default since December 2025). ([#3572](https://github.com/Shopify/hydrogen/pull/3572)) by [@itsjustriley](https://github.com/itsjustriley)
8
+
9
+ Developers no longer need a manual route file for the tokenless Storefront API. Existing apps with this route can safely delete it - the server-level proxy provides the same functionality with better cookie forwarding and analytics integration.
10
+
11
+ - Updated dependencies [[`b0a75c1d759706931876f056662de2497cb3e688`](https://github.com/Shopify/hydrogen/commit/b0a75c1d759706931876f056662de2497cb3e688), [`a44ee3566b9bb9a7f43f05dfaae6f1f2ab1d548f`](https://github.com/Shopify/hydrogen/commit/a44ee3566b9bb9a7f43f05dfaae6f1f2ab1d548f)]:
12
+ - @shopify/hydrogen@2026.1.4
13
+
14
+ ## 2026.1.3
15
+
16
+ ### Patch Changes
17
+
18
+ - Improve screen reader experience for paginated product grids by hiding decorative arrow characters from assistive technology. ([#3557](https://github.com/Shopify/hydrogen/pull/3557)) by [@itsjustriley](https://github.com/itsjustriley)
19
+
20
+ - Fix broken `aria-label` on territory code input in address form. The label was the raw developer string `"territoryCode"` instead of a human-readable `"Country code"`. ([#3607](https://github.com/Shopify/hydrogen/pull/3607)) by [@itsjustriley](https://github.com/itsjustriley)
21
+
22
+ - Add aria-label to ProductPrice for improved screen reader accessibility ([#3558](https://github.com/Shopify/hydrogen/pull/3558)) by [@itsjustriley](https://github.com/itsjustriley)
23
+
24
+ - Updated dependencies [[`108243003a7f36349a446478f4e8ab0cade3e13a`](https://github.com/Shopify/hydrogen/commit/108243003a7f36349a446478f4e8ab0cade3e13a)]:
25
+ - @shopify/hydrogen@2026.1.3
26
+
3
27
  ## 2026.1.2
4
28
 
5
29
  ### Patch Changes
@@ -2,15 +2,17 @@ import * as React from 'react';
2
2
  import {Pagination} from '@shopify/hydrogen';
3
3
 
4
4
  /**
5
- * <PaginatedResourceSection > is a component that encapsulate how the previous and next behaviors throughout your application.
5
+ * <PaginatedResourceSection> encapsulates the previous and next pagination behaviors throughout your application.
6
6
  */
7
7
  export function PaginatedResourceSection<NodesType>({
8
8
  connection,
9
9
  children,
10
+ ariaLabel,
10
11
  resourcesClassName,
11
12
  }: {
12
13
  connection: React.ComponentProps<typeof Pagination<NodesType>>['connection'];
13
14
  children: React.FunctionComponent<{node: NodesType; index: number}>;
15
+ ariaLabel?: string;
14
16
  resourcesClassName?: string;
15
17
  }) {
16
18
  return (
@@ -23,15 +25,33 @@ export function PaginatedResourceSection<NodesType>({
23
25
  return (
24
26
  <div>
25
27
  <PreviousLink>
26
- {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
28
+ {isLoading ? (
29
+ 'Loading...'
30
+ ) : (
31
+ <span>
32
+ <span aria-hidden="true">↑</span> Load previous
33
+ </span>
34
+ )}
27
35
  </PreviousLink>
28
36
  {resourcesClassName ? (
29
- <div className={resourcesClassName}>{resourcesMarkup}</div>
37
+ <div
38
+ aria-label={ariaLabel}
39
+ className={resourcesClassName}
40
+ role={ariaLabel ? 'region' : undefined}
41
+ >
42
+ {resourcesMarkup}
43
+ </div>
30
44
  ) : (
31
45
  resourcesMarkup
32
46
  )}
33
47
  <NextLink>
34
- {isLoading ? 'Loading...' : <span>Load more ↓</span>}
48
+ {isLoading ? (
49
+ 'Loading...'
50
+ ) : (
51
+ <span>
52
+ Load more <span aria-hidden="true">↓</span>
53
+ </span>
54
+ )}
35
55
  </NextLink>
36
56
  </div>
37
57
  );
@@ -9,7 +9,7 @@ export function ProductPrice({
9
9
  compareAtPrice?: MoneyV2 | null;
10
10
  }) {
11
11
  return (
12
- <div className="product-price">
12
+ <div aria-label="Price" className="product-price" role="group">
13
13
  {compareAtPrice ? (
14
14
  <div className="product-price-on-sale">
15
15
  {price ? <Money data={price} /> : null}
@@ -468,7 +468,7 @@ export function AddressForm({
468
468
  />
469
469
  <label htmlFor="territoryCode">Country Code*</label>
470
470
  <input
471
- aria-label="territoryCode"
471
+ aria-label="Country code"
472
472
  autoComplete="country"
473
473
  defaultValue={address?.territoryCode ?? ''}
474
474
  id="territoryCode"
@@ -2,7 +2,7 @@
2
2
  "name": "skeleton",
3
3
  "private": true,
4
4
  "sideEffects": false,
5
- "version": "2026.1.2",
5
+ "version": "2026.1.4",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "shopify hydrogen build --codegen",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "prettier": "@shopify/prettier-config",
16
16
  "dependencies": {
17
- "@shopify/hydrogen": "2026.1.2",
17
+ "@shopify/hydrogen": "2026.1.4",
18
18
  "graphql": "^16.10.0",
19
19
  "graphql-tag": "^2.12.6",
20
20
  "isbot": "^5.1.22",
@@ -32,7 +32,7 @@
32
32
  "@react-router/fs-routes": "7.12.0",
33
33
  "@shopify/cli": "3.91.1",
34
34
  "@shopify/hydrogen-codegen": "0.3.3",
35
- "@shopify/mini-oxygen": "4.0.1",
35
+ "@shopify/mini-oxygen": "4.0.2",
36
36
  "@shopify/oxygen-workers-types": "^4.1.6",
37
37
  "@shopify/prettier-config": "^1.1.2",
38
38
  "@total-typescript/ts-reset": "^0.6.1",
@@ -54,7 +54,7 @@
54
54
  "graphql-config": "^5.0.3",
55
55
  "prettier": "^3.4.2",
56
56
  "typescript": "^5.9.2",
57
- "vite": "^6.2.4",
57
+ "vite": "^6.4.2",
58
58
  "vite-tsconfig-paths": "^4.3.1"
59
59
  },
60
60
  "engines": {
@@ -377,13 +377,6 @@ export type FooterQuery = {
377
377
  >;
378
378
  };
379
379
 
380
- export type StoreRobotsQueryVariables = StorefrontAPI.Exact<{
381
- country?: StorefrontAPI.InputMaybe<StorefrontAPI.CountryCode>;
382
- language?: StorefrontAPI.InputMaybe<StorefrontAPI.LanguageCode>;
383
- }>;
384
-
385
- export type StoreRobotsQuery = {shop: Pick<StorefrontAPI.Shop, 'id'>};
386
-
387
380
  export type FeaturedCollectionFragment = Pick<
388
381
  StorefrontAPI.Collection,
389
382
  'id' | 'title' | 'handle'
@@ -1283,10 +1276,6 @@ interface GeneratedQueryTypes {
1283
1276
  return: FooterQuery;
1284
1277
  variables: FooterQueryVariables;
1285
1278
  };
1286
- '#graphql\n query StoreRobots($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n shop {\n id\n }\n }\n': {
1287
- return: StoreRobotsQuery;
1288
- variables: StoreRobotsQueryVariables;
1289
- };
1290
1279
  '#graphql\n fragment FeaturedCollection on Collection {\n id\n title\n image {\n id\n url\n altText\n width\n height\n }\n handle\n }\n query FeaturedCollection($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n collections(first: 1, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...FeaturedCollection\n }\n }\n }\n': {
1291
1280
  return: FeaturedCollectionQuery;
1292
1281
  variables: FeaturedCollectionQueryVariables;
@@ -7,7 +7,7 @@ import { renderSuccess, renderInfo } from '@shopify/cli-kit/node/ui';
7
7
  import { AbortError } from '@shopify/cli-kit/node/error';
8
8
  import { removeFile } from '@shopify/cli-kit/node/fs';
9
9
  import { setH2OVerbose, isH2Verbose, muteDevLogs, enhanceH2Logs } from '../../lib/log.js';
10
- import { commonFlags, overrideFlag, flagsToCamelObject, DEFAULT_INSPECTOR_PORT, DEFAULT_APP_PORT } from '../../lib/flags.js';
10
+ import { commonFlags, overrideFlag, flagsToCamelObject, DEFAULT_APP_PORT, DEFAULT_INSPECTOR_PORT } from '../../lib/flags.js';
11
11
  import { spawnCodegenProcess } from '../../lib/codegen.js';
12
12
  import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
13
13
  import { displayDevUpgradeNotice } from './upgrade.js';
@@ -103,6 +103,9 @@ async function runDev({
103
103
  if (!process.env.NODE_ENV) process.env.NODE_ENV = "development";
104
104
  if (verbose) setH2OVerbose();
105
105
  if (!isH2Verbose()) muteDevLogs();
106
+ if (appPort === 0) {
107
+ appPort = await findPort(DEFAULT_APP_PORT);
108
+ }
106
109
  const root = appPath ?? process.cwd();
107
110
  const cliCommandPromise = getCliCommand(root);
108
111
  const backgroundPromise = getDevConfigInBackground(
@@ -6,7 +6,7 @@ import { ensureInsideGitDirectory, isClean } from '@shopify/cli-kit/node/git';
6
6
  import Command from '@shopify/cli-kit/node/base-command';
7
7
  import { renderSuccess, renderInfo, renderConfirmationPrompt, renderTasks, renderSelectPrompt, renderWarning } from '@shopify/cli-kit/node/ui';
8
8
  import { isDirectory, readFile, mkdir, fileExists, touchFile, removeFile, writeFile } from '@shopify/cli-kit/node/fs';
9
- import { getPackageManager, getDependencies, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
9
+ import { getPackageManager, getDependencies } from '@shopify/cli-kit/node/node-package-manager';
10
10
  import { exec } from '@shopify/cli-kit/node/system';
11
11
  import { AbortError } from '@shopify/cli-kit/node/error';
12
12
  import { resolvePath, joinPath, dirname } from '@shopify/cli-kit/node/path';
@@ -106,7 +106,7 @@ async function runUpgrade({
106
106
  availableUpgrades,
107
107
  currentDependencies
108
108
  });
109
- cumulativeRelease = getCummulativeRelease({
109
+ cumulativeRelease = getCumulativeRelease({
110
110
  availableUpgrades,
111
111
  currentVersion,
112
112
  currentDependencies,
@@ -128,7 +128,11 @@ async function runUpgrade({
128
128
  appPath,
129
129
  selectedRelease,
130
130
  currentDependencies,
131
- targetVersion
131
+ targetVersion,
132
+ cumulativeRemoveDependencies: cumulativeRelease.removeDependencies,
133
+ cumulativeRemoveDevDependencies: cumulativeRelease.removeDevDependencies,
134
+ cumulativeDependencies: cumulativeRelease.dependencies,
135
+ cumulativeDevDependencies: cumulativeRelease.devDependencies
132
136
  });
133
137
  await validateUpgrade({
134
138
  appPath,
@@ -336,20 +340,31 @@ async function getSelectedRelease({
336
340
  ) : void 0;
337
341
  return targetRelease ?? promptUpgradeOptions(currentVersion, availableUpgrades);
338
342
  }
339
- function getCummulativeRelease({
343
+ function getCumulativeRelease({
340
344
  availableUpgrades,
341
345
  selectedRelease,
342
346
  currentVersion,
343
347
  currentDependencies
344
348
  }) {
345
349
  const currentPinnedVersion = getAbsoluteVersion(currentVersion);
346
- if (!availableUpgrades?.length) {
347
- return { features: [], fixes: [] };
348
- }
350
+ const empty = {
351
+ features: [],
352
+ fixes: [],
353
+ removeDependencies: [],
354
+ removeDevDependencies: [],
355
+ dependencies: {},
356
+ devDependencies: {}
357
+ };
358
+ if (!availableUpgrades?.length) return empty;
349
359
  if (selectedRelease.dependencies?.["@shopify/hydrogen"] === "next") {
350
360
  return {
361
+ ...empty,
351
362
  features: selectedRelease.features || [],
352
- fixes: selectedRelease.fixes || []
363
+ fixes: selectedRelease.fixes || [],
364
+ removeDependencies: selectedRelease.removeDependencies ?? [],
365
+ removeDevDependencies: selectedRelease.removeDevDependencies ?? [],
366
+ dependencies: selectedRelease.dependencies ?? {},
367
+ devDependencies: selectedRelease.devDependencies ?? {}
353
368
  };
354
369
  }
355
370
  const upgradingReleases = availableUpgrades.filter((release) => {
@@ -359,14 +374,65 @@ function getCummulativeRelease({
359
374
  if (!isSameHydrogenVersion || !currentDependencies) return false;
360
375
  return hasOutdatedDependencies({ release, currentDependencies });
361
376
  });
362
- return upgradingReleases.reduce(
363
- (acc, release) => {
364
- acc.features = [...acc.features, ...release.features];
365
- acc.fixes = [...acc.fixes, ...release.fixes];
366
- return acc;
367
- },
368
- { features: [], fixes: [] }
377
+ const features = upgradingReleases.flatMap((r) => r.features);
378
+ const fixes = upgradingReleases.flatMap((r) => r.fixes);
379
+ const releasesByVersion = [...upgradingReleases].sort(
380
+ (a, b) => semver.compare(a.version, b.version)
369
381
  );
382
+ const removedDepsAt = /* @__PURE__ */ new Map();
383
+ const removedDevDepsAt = /* @__PURE__ */ new Map();
384
+ releasesByVersion.forEach((release, i) => {
385
+ release.removeDependencies?.forEach((dep) => {
386
+ removedDepsAt.set(dep, i);
387
+ });
388
+ release.removeDevDependencies?.forEach((dep) => {
389
+ removedDevDepsAt.set(dep, i);
390
+ });
391
+ });
392
+ const reinstalledDeps = /* @__PURE__ */ new Set();
393
+ const reinstalledDevDeps = /* @__PURE__ */ new Set();
394
+ for (let i = 0; i < releasesByVersion.length; i++) {
395
+ const release = releasesByVersion[i];
396
+ if (!release) continue;
397
+ const dependencies2 = release.dependencies ?? {};
398
+ const devDependencies2 = release.devDependencies ?? {};
399
+ Object.keys(dependencies2).forEach((dep) => {
400
+ const removalI = removedDepsAt.get(dep);
401
+ if (removalI !== void 0 && i >= removalI) {
402
+ reinstalledDeps.add(dep);
403
+ }
404
+ });
405
+ Object.keys(devDependencies2).forEach((dep) => {
406
+ const removalI = removedDevDepsAt.get(dep);
407
+ if (removalI !== void 0 && i >= removalI) {
408
+ reinstalledDevDeps.add(dep);
409
+ }
410
+ });
411
+ }
412
+ const removeDependencies = [
413
+ ...new Set(
414
+ releasesByVersion.flatMap((r) => r.removeDependencies ?? []).filter((dep) => !reinstalledDeps.has(dep))
415
+ )
416
+ ];
417
+ const removeDevDependencies = [
418
+ ...new Set(
419
+ releasesByVersion.flatMap((r) => r.removeDevDependencies ?? []).filter((dep) => !reinstalledDevDeps.has(dep))
420
+ )
421
+ ];
422
+ const dependencies = {};
423
+ const devDependencies = {};
424
+ for (const release of releasesByVersion) {
425
+ Object.assign(dependencies, release.dependencies ?? {});
426
+ Object.assign(devDependencies, release.devDependencies ?? {});
427
+ }
428
+ return {
429
+ features,
430
+ fixes,
431
+ removeDependencies,
432
+ removeDevDependencies,
433
+ dependencies,
434
+ devDependencies
435
+ };
370
436
  }
371
437
  function displayConfirmation({
372
438
  cumulativeRelease,
@@ -377,7 +443,7 @@ function displayConfirmation({
377
443
  if (features.length || fixes.length) {
378
444
  renderInfo({
379
445
  headline: `Included in this upgrade:`,
380
- //@ts-ignore we know that filter(Boolean) will always return an array
446
+ // @ts-expect-error - filter(Boolean) removes falsy values, leaving only objects
381
447
  customSections: [
382
448
  features.length && {
383
449
  title: "Features",
@@ -461,10 +527,20 @@ function maybeIncludeDependency({
461
527
  function buildUpgradeCommandArgs({
462
528
  selectedRelease,
463
529
  currentDependencies,
464
- targetVersion
530
+ targetVersion,
531
+ cumulativeDependencies,
532
+ cumulativeDevDependencies
465
533
  }) {
466
534
  const args = [];
467
- for (const dependency of Object.entries(selectedRelease.dependencies)) {
535
+ const effectiveDependencies = {
536
+ ...cumulativeDependencies ?? {},
537
+ ...selectedRelease.dependencies
538
+ };
539
+ const effectiveDevDependencies = {
540
+ ...cumulativeDevDependencies ?? {},
541
+ ...selectedRelease.devDependencies
542
+ };
543
+ for (const dependency of Object.entries(effectiveDependencies)) {
468
544
  const shouldUpgradeDep = maybeIncludeDependency({
469
545
  currentDependencies,
470
546
  dependency,
@@ -480,7 +556,7 @@ function buildUpgradeCommandArgs({
480
556
  )}`
481
557
  );
482
558
  }
483
- for (const dependency of Object.entries(selectedRelease.devDependencies)) {
559
+ for (const dependency of Object.entries(effectiveDevDependencies)) {
484
560
  const shouldUpgradeDep = maybeIncludeDependency({
485
561
  currentDependencies,
486
562
  dependency,
@@ -497,7 +573,7 @@ function buildUpgradeCommandArgs({
497
573
  );
498
574
  }
499
575
  const currentRemix = Object.entries(currentDependencies).find(isRemixDependency);
500
- const selectedRemix = Object.entries(selectedRelease.dependencies).find(
576
+ const selectedRemix = Object.entries(effectiveDependencies).find(
501
577
  isRemixDependency
502
578
  );
503
579
  if (currentRemix && selectedRemix) {
@@ -514,7 +590,7 @@ function buildUpgradeCommandArgs({
514
590
  const currentReactRouter = Object.entries(currentDependencies).find(
515
591
  isReactRouterDependency
516
592
  );
517
- const selectedReactRouter = Object.entries(selectedRelease.dependencies).find(
593
+ const selectedReactRouter = Object.entries(effectiveDependencies).find(
518
594
  isReactRouterDependency
519
595
  );
520
596
  if (selectedReactRouter) {
@@ -537,12 +613,16 @@ async function upgradeNodeModules({
537
613
  appPath,
538
614
  selectedRelease,
539
615
  currentDependencies,
540
- targetVersion
616
+ targetVersion,
617
+ cumulativeRemoveDependencies,
618
+ cumulativeRemoveDevDependencies,
619
+ cumulativeDependencies,
620
+ cumulativeDevDependencies
541
621
  }) {
542
622
  const tasks = [];
543
623
  const depsToRemove = [
544
- ...selectedRelease.removeDependencies || [],
545
- ...selectedRelease.removeDevDependencies || []
624
+ ...cumulativeRemoveDependencies,
625
+ ...cumulativeRemoveDevDependencies
546
626
  ].filter((dep) => dep in currentDependencies);
547
627
  if (depsToRemove.length > 0) {
548
628
  tasks.push({
@@ -559,17 +639,24 @@ async function upgradeNodeModules({
559
639
  const upgradeArgs = buildUpgradeCommandArgs({
560
640
  selectedRelease,
561
641
  currentDependencies,
562
- targetVersion
642
+ targetVersion,
643
+ cumulativeDependencies,
644
+ cumulativeDevDependencies
563
645
  });
564
646
  if (upgradeArgs.length > 0) {
565
647
  tasks.push({
566
648
  title: `Upgrading dependencies`,
567
649
  task: async () => {
568
- await installNodeModules({
569
- directory: appPath,
570
- packageManager: await getPackageManager(appPath),
571
- args: upgradeArgs
572
- });
650
+ const packageManager = await getPackageManager(appPath);
651
+ const command = packageManager === "npm" ? "install" : packageManager === "yarn" ? "add" : packageManager === "pnpm" ? "add" : packageManager === "bun" ? "install" : "install";
652
+ const extraArgs = packageManager === "npm" || packageManager === "unknown" ? ["--legacy-peer-deps"] : [];
653
+ await exec(
654
+ resolvePackageManagerName(packageManager),
655
+ [command, ...extraArgs, ...upgradeArgs],
656
+ {
657
+ cwd: appPath
658
+ }
659
+ );
573
660
  }
574
661
  });
575
662
  }
@@ -577,6 +664,9 @@ async function upgradeNodeModules({
577
664
  await renderTasks(tasks, {});
578
665
  }
579
666
  }
667
+ function resolvePackageManagerName(packageManager) {
668
+ return packageManager === "unknown" ? "npm" : packageManager;
669
+ }
580
670
  async function uninstallNodeModules({
581
671
  directory,
582
672
  packageManager,
@@ -584,8 +674,12 @@ async function uninstallNodeModules({
584
674
  }) {
585
675
  if (args.length === 0) return;
586
676
  const command = packageManager === "npm" ? "uninstall" : packageManager === "yarn" ? "remove" : packageManager === "pnpm" ? "remove" : packageManager === "bun" ? "remove" : "uninstall";
587
- const actualPackageManager = packageManager === "unknown" ? "npm" : packageManager;
588
- await exec(actualPackageManager, [command, ...args], { cwd: directory });
677
+ const extraArgs = packageManager === "npm" || packageManager === "unknown" ? ["--legacy-peer-deps"] : [];
678
+ await exec(
679
+ resolvePackageManagerName(packageManager),
680
+ [command, ...extraArgs, ...args],
681
+ { cwd: directory }
682
+ );
589
683
  }
590
684
  function appendRemixDependencies({
591
685
  currentDependencies,
@@ -954,4 +1048,4 @@ async function displayDevUpgradeNotice({
954
1048
  }
955
1049
  }
956
1050
 
957
- export { buildUpgradeCommandArgs, Upgrade as default, displayConfirmation, displayDevUpgradeNotice, getAbsoluteVersion, getAvailableUpgrades, getChangelog, getCummulativeRelease, getHydrogenVersion, getPackageVersion, getSelectedRelease, isRunningFromHydrogenMonorepo, runUpgrade, upgradeNodeModules, validateUpgrade };
1051
+ export { buildUpgradeCommandArgs, Upgrade as default, displayConfirmation, displayDevUpgradeNotice, getAbsoluteVersion, getAvailableUpgrades, getChangelog, getCumulativeRelease, getHydrogenVersion, getPackageVersion, getSelectedRelease, isRunningFromHydrogenMonorepo, runUpgrade, upgradeNodeModules, validateUpgrade };
@@ -1,6 +1,50 @@
1
1
  import { AbortError } from '@shopify/cli-kit/node/error';
2
2
  import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
3
3
 
4
+ const KNOWN_USER_ERRORS = [
5
+ {
6
+ pattern: "app is not installed",
7
+ abort: [
8
+ "Hydrogen sales channel isn't installed",
9
+ "Install the Hydrogen sales channel on your store to start creating and linking Hydrogen storefronts: https://apps.shopify.com/hydrogen"
10
+ ]
11
+ },
12
+ {
13
+ pattern: "Access denied for hydrogenStorefrontCreate field",
14
+ abort: [
15
+ "Couldn't connect storefront to Shopify",
16
+ [
17
+ "Common reasons for this error include:",
18
+ {
19
+ list: {
20
+ items: [
21
+ "The Hydrogen sales channel isn't installed on the store.",
22
+ "You don't have the required account permission to manage apps or channels.",
23
+ "You're trying to connect to an ineligible store type (Trial, Development store)"
24
+ ]
25
+ }
26
+ }
27
+ ]
28
+ ]
29
+ },
30
+ {
31
+ pattern: "Access denied for hydrogenStorefronts field",
32
+ abort: [
33
+ "Couldn't access Hydrogen storefronts",
34
+ [
35
+ "Common reasons for this error include:",
36
+ {
37
+ list: {
38
+ items: [
39
+ "You don't have full access to apps or access to the Hydrogen channel.",
40
+ "The Hydrogen sales channel isn't installed on the store."
41
+ ]
42
+ }
43
+ }
44
+ ]
45
+ ]
46
+ }
47
+ ];
4
48
  async function adminRequest(query, session, variables) {
5
49
  const api = "Admin";
6
50
  const url = `https://${session.storeFqdn}/admin/api/unstable/graphql.json`;
@@ -14,29 +58,10 @@ async function adminRequest(query, session, variables) {
14
58
  });
15
59
  } catch (error) {
16
60
  const errors = error.errors;
17
- if (errors?.some?.((error2) => error2.message.includes("app is not installed"))) {
18
- throw new AbortError(
19
- "Hydrogen sales channel isn't installed",
20
- "Install the Hydrogen sales channel on your store to start creating and linking Hydrogen storefronts: https://apps.shopify.com/hydrogen"
21
- );
22
- }
23
- if (errors?.some?.(
24
- (error2) => error2.message.includes(
25
- "Access denied for hydrogenStorefrontCreate field"
26
- )
27
- )) {
28
- throw new AbortError("Couldn't connect storefront to Shopify", [
29
- "Common reasons for this error include:",
30
- {
31
- list: {
32
- items: [
33
- "The Hydrogen sales channel isn't installed on the store.",
34
- "You don't have the required account permission to manage apps or channels.",
35
- "You're trying to connect to an ineligible store type (Trial, Development store)"
36
- ]
37
- }
38
- }
39
- ]);
61
+ for (const { pattern, abort } of KNOWN_USER_ERRORS) {
62
+ if (errors?.some?.((e) => e.message.includes(pattern))) {
63
+ throw new AbortError(...abort);
64
+ }
40
65
  }
41
66
  throw error;
42
67
  }
@@ -0,0 +1,198 @@
1
+ import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ function isErrnoException(err) {
5
+ return err instanceof Error && "code" in err;
6
+ }
7
+ const MARKER = "// [hydrogen-monorepo-patch]";
8
+ function getRunJsPath(root) {
9
+ return resolve(root, "node_modules", "@shopify", "cli", "bin", "run.js");
10
+ }
11
+ function isPatchApplied(content) {
12
+ return content.includes(MARKER);
13
+ }
14
+ function generatePatchedContent() {
15
+ return `#!/usr/bin/env node
16
+ ${MARKER}
17
+
18
+ // Mirrors upstream @shopify/cli/bin/run.js behavior.
19
+ // This is not introduced by the patch \u2014 @shopify/cli ships with this line.
20
+ // Changing it here would create a behavioral divergence between dev and production.
21
+ process.removeAllListeners('warning')
22
+
23
+ // --- Monorepo detection ---
24
+ // Walk up from cwd to find the hydrogen monorepo root.
25
+ // We look for packages/cli/package.json with the correct package name \u2014
26
+ // if it matches, we know we're inside the monorepo and should load the
27
+ // local @shopify/cli-hydrogen
28
+ const {existsSync, readFileSync} = await import('node:fs');
29
+ const {resolve, dirname} = await import('node:path');
30
+
31
+ let monorepoRoot = null;
32
+ let dir = process.cwd();
33
+ while (true) {
34
+ const candidate = resolve(dir, 'packages', 'cli', 'package.json');
35
+ if (existsSync(candidate)) {
36
+ try {
37
+ const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
38
+ if (pkg.name === '@shopify/cli-hydrogen') {
39
+ monorepoRoot = dir;
40
+ break;
41
+ }
42
+ } catch {
43
+ // Ignore malformed package.json and keep walking
44
+ }
45
+ }
46
+ const parent = dirname(dir);
47
+ if (parent === dir) break; // reached filesystem root
48
+ dir = parent;
49
+ }
50
+
51
+ if (monorepoRoot) {
52
+ // We're in the hydrogen monorepo. Start @shopify/cli normally but
53
+ // inject pluginAdditions so oclif loads @shopify/cli-hydrogen from
54
+ // the monorepo's workspace (packages/cli) instead of the version
55
+ // bundled inside @shopify/cli/dist.
56
+ //
57
+ // This uses the same pluginAdditions mechanism that ShopifyConfig
58
+ // uses internally
59
+ const {fileURLToPath} = await import('node:url');
60
+ const {Config, run, flush} = await import('@oclif/core');
61
+
62
+ // root must point to @shopify/cli's installed location so oclif
63
+ // can find its package.json, oclif config, and bundled commands.
64
+ const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
65
+
66
+ const c = '\\x1b[38;5;209m';
67
+ const d = '\\x1b[2m';
68
+ const r = '\\x1b[0m';
69
+ console.log('');
70
+ console.log(c + ' \u250C\u2500\u2500 hydrogen-monorepo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510' + r);
71
+ console.log(c + ' \u2502' + r + ' ' + c + '\u2502' + r);
72
+ console.log(c + ' \u2502' + r + ' Using local cli-hydrogen plugin from packages/cli ' + c + '\u2502' + r);
73
+ console.log(c + ' \u2502' + r + d + ' Bundled commands replaced with local source ' + r + c + '\u2502' + r);
74
+ console.log(c + ' \u2502' + r + ' ' + c + '\u2502' + r);
75
+ console.log(c + ' \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518' + r);
76
+ console.log('');
77
+
78
+ // Tell ShopifyConfig to skip its own monorepo detection since we
79
+ // are handling pluginAdditions ourselves.
80
+ process.env.IGNORE_HYDROGEN_MONOREPO = '1';
81
+
82
+ const config = new Config({
83
+ root: cliRoot,
84
+ // pluginAdditions tells oclif's plugin loader to read the
85
+ // monorepo root's package.json, find @shopify/cli-hydrogen in
86
+ // its dependencies (workspace:*), and load that as a core plugin.
87
+ // Because workspace:* symlinks to packages/cli, oclif loads the
88
+ // local source code \u2014 exactly what we want for development.
89
+ pluginAdditions: {
90
+ core: ['@shopify/cli-hydrogen'],
91
+ path: monorepoRoot,
92
+ },
93
+ // Skip the oclif manifest cache so commands are loaded fresh from
94
+ // disk rather than from a potentially stale oclif.manifest.json.
95
+ ignoreManifest: true,
96
+ });
97
+
98
+ await config.load();
99
+
100
+ // --- Post-load command replacement ---
101
+ // After loading, both the bundled hydrogen commands (from @shopify/cli's
102
+ // root plugin) and the local ones (from pluginAdditions) are registered.
103
+ // Since @shopify/cli@3.83.0 (which upgraded to oclif v4),
104
+ // determinePriority is no longer an overridable instance method, so we
105
+ // manually replace the bundled commands with the external plugin's
106
+ // versions using oclif's private _commands Map.
107
+ const externalPlugin = Array.from(config.plugins.values()).find(
108
+ (p) => p.name === '@shopify/cli-hydrogen' && !p.isRoot,
109
+ );
110
+
111
+ if (!externalPlugin) {
112
+ throw new Error(
113
+ '[hydrogen-monorepo] Could not find local @shopify/cli-hydrogen plugin. ' +
114
+ 'The patch may need updating for the current @shopify/cli version.'
115
+ );
116
+ }
117
+
118
+ const cmds = config._commands;
119
+ if (!cmds || !(cmds instanceof Map)) {
120
+ throw new Error(
121
+ '[hydrogen-monorepo] Cannot replace bundled commands \u2014 oclif internals changed. ' +
122
+ 'The patch may need updating for the current oclif version.'
123
+ );
124
+ }
125
+
126
+ // Delete bundled hydrogen commands (canonical IDs + aliases + hidden aliases)
127
+ for (const command of externalPlugin.commands) {
128
+ if (!command.id.startsWith('hydrogen')) continue;
129
+ cmds.delete(command.id);
130
+ for (const alias of [...(command.aliases ?? []), ...(command.hiddenAliases ?? [])]) {
131
+ cmds.delete(alias);
132
+ }
133
+ }
134
+ // Re-insert commands from the local plugin. loadCommands handles
135
+ // alias registration and command permutations correctly.
136
+ config.loadCommands(externalPlugin);
137
+
138
+ await run(process.argv.slice(2), config);
139
+ await flush();
140
+ } else {
141
+ // Not in the monorepo \u2014 run the standard @shopify/cli entrypoint.
142
+ const {default: runCLI} = await import('../dist/index.js');
143
+ runCLI({development: false});
144
+ }
145
+ `;
146
+ }
147
+ function generateOriginalContent() {
148
+ return `#!/usr/bin/env node
149
+
150
+ process.removeAllListeners('warning')
151
+
152
+ import runCLI from '../dist/index.js'
153
+
154
+ runCLI({development: false})
155
+ `;
156
+ }
157
+ function applyPatch(runJsPath) {
158
+ let current;
159
+ try {
160
+ current = readFileSync(runJsPath, "utf8");
161
+ } catch (err) {
162
+ if (isErrnoException(err) && err.code === "ENOENT") {
163
+ throw new Error(
164
+ `@shopify/cli is not installed (${runJsPath} not found). Run 'pnpm install' first.`
165
+ );
166
+ }
167
+ throw err;
168
+ }
169
+ if (isPatchApplied(current)) return false;
170
+ const backupPath = runJsPath + ".backup";
171
+ if (!existsSync(backupPath)) {
172
+ writeFileSync(backupPath, current);
173
+ }
174
+ writeFileSync(runJsPath, generatePatchedContent());
175
+ return true;
176
+ }
177
+ function removePatch(runJsPath) {
178
+ let current;
179
+ try {
180
+ current = readFileSync(runJsPath, "utf8");
181
+ } catch (err) {
182
+ if (isErrnoException(err) && err.code === "ENOENT") {
183
+ return false;
184
+ }
185
+ throw err;
186
+ }
187
+ if (!isPatchApplied(current)) return false;
188
+ const backupPath = runJsPath + ".backup";
189
+ const hasBackup = existsSync(backupPath);
190
+ const original = hasBackup ? readFileSync(backupPath, "utf8") : generateOriginalContent();
191
+ writeFileSync(runJsPath, original);
192
+ if (hasBackup) {
193
+ unlinkSync(backupPath);
194
+ }
195
+ return true;
196
+ }
197
+
198
+ export { MARKER, applyPatch, generateOriginalContent, generatePatchedContent, getRunJsPath, isPatchApplied, removePatch };
@@ -22,8 +22,7 @@ const ROUTE_MAP = {
22
22
  account: "account*",
23
23
  search: ["search", "api.predictive-search"],
24
24
  robots: "[robots.txt]",
25
- sitemap: ["[sitemap.xml]", "sitemap.$type.$page[.xml]"],
26
- tokenlessApi: "api.$version.[graphql.json]"
25
+ sitemap: ["[sitemap.xml]", "sitemap.$type.$page[.xml]"]
27
26
  };
28
27
  let allRouteTemplateFiles = [];
29
28
  async function getResolvedRoutes(routeKeys = Object.keys(ROUTE_MAP)) {
@@ -1,8 +1,14 @@
1
+ import { createReadStream } from 'node:fs';
1
2
  import { execFileSync } from 'node:child_process';
2
3
  import { readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
+ import { join, isAbsolute } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
4
6
  import { tmpdir } from 'node:os';
7
+ import gunzipMaybe from 'gunzip-maybe';
8
+ import { extract } from 'tar-fs';
5
9
 
10
+ const WINDOWS_SHELL_OPTS = process.platform === "win32" ? { shell: true } : {};
11
+ const PNPM_PACK_TIMEOUT_IN_MS = 6e4;
6
12
  const DEPENDENCY_SECTIONS = [
7
13
  "dependencies",
8
14
  "devDependencies",
@@ -20,7 +26,9 @@ async function getPackedTemplatePackageJson(sourceTemplateDir) {
20
26
  ["pack", "--pack-destination", tempDir, "--json"],
21
27
  {
22
28
  cwd: sourceTemplateDir,
23
- encoding: "utf8"
29
+ encoding: "utf8",
30
+ timeout: PNPM_PACK_TIMEOUT_IN_MS,
31
+ ...WINDOWS_SHELL_OPTS
24
32
  }
25
33
  );
26
34
  const parsedResult = JSON.parse(rawPackResult.trim());
@@ -28,14 +36,15 @@ async function getPackedTemplatePackageJson(sourceTemplateDir) {
28
36
  if (!packedTarball) {
29
37
  throw new Error("pnpm pack did not return a tarball filename.");
30
38
  }
31
- const packedManifestRaw = execFileSync(
32
- "tar",
33
- [
34
- "-xOf",
35
- packedTarball.startsWith("/") ? packedTarball : join(tempDir, packedTarball),
36
- "package/package.json"
37
- ],
38
- { encoding: "utf8" }
39
+ const tarballPath = isAbsolute(packedTarball) ? packedTarball : join(tempDir, packedTarball);
40
+ await pipeline(
41
+ createReadStream(tarballPath),
42
+ gunzipMaybe(),
43
+ extract(tempDir)
44
+ );
45
+ const packedManifestRaw = await readFile(
46
+ join(tempDir, "package", "package.json"),
47
+ "utf8"
39
48
  );
40
49
  return JSON.parse(packedManifestRaw);
41
50
  } finally {
@@ -81,4 +90,4 @@ async function replaceWorkspaceProtocolVersions({
81
90
  );
82
91
  }
83
92
 
84
- export { replaceWorkspaceProtocolVersions };
93
+ export { WINDOWS_SHELL_OPTS, replaceWorkspaceProtocolVersions };
@@ -0,0 +1,50 @@
1
+ const DEPENDENCY_SECTIONS = [
2
+ "dependencies",
3
+ "devDependencies"
4
+ ];
5
+ function parseCatalogFromWorkspaceYaml(yamlContent) {
6
+ const catalogVersions = {};
7
+ const catalogMatch = yamlContent.match(/^catalog:\s*\n((?:[ \t]+.+\n?)*)/m);
8
+ const catalogSection = catalogMatch?.[1];
9
+ if (!catalogSection) return catalogVersions;
10
+ for (const line of catalogSection.split("\n")) {
11
+ const match = line.match(
12
+ /^\s+['"]?([^'":]+?)['"]?\s*:\s*['"]?([^'"]+?)['"]?\s*$/
13
+ );
14
+ if (match?.[1] && match[2]) {
15
+ catalogVersions[match[1].trim()] = match[2].trim();
16
+ }
17
+ }
18
+ return catalogVersions;
19
+ }
20
+ function parseWorkspacePackagesFromYaml(yamlContent) {
21
+ const packagesMatch = yamlContent.match(
22
+ /^packages:\s*\n((?:[ \t]+-.+\n?)*)/m
23
+ );
24
+ const packagesSection = packagesMatch?.[1];
25
+ if (!packagesSection) return [];
26
+ return packagesSection.split("\n").map((line) => line.match(/^\s+-\s+(.+)/)?.[1]?.trim()).filter((path) => Boolean(path));
27
+ }
28
+ async function resolveWorkspaceProtocols({
29
+ packageJson,
30
+ catalogVersions,
31
+ resolveWorkspaceVersion,
32
+ fallbackVersion
33
+ }) {
34
+ const resolved = JSON.parse(JSON.stringify(packageJson));
35
+ for (const section of DEPENDENCY_SECTIONS) {
36
+ const deps = resolved[section];
37
+ if (!deps) continue;
38
+ for (const [name, version] of Object.entries(deps)) {
39
+ if (version.startsWith("workspace:")) {
40
+ const resolvedVersion = await resolveWorkspaceVersion(name);
41
+ deps[name] = resolvedVersion ?? fallbackVersion;
42
+ } else if (version === "catalog:" && catalogVersions[name]) {
43
+ deps[name] = catalogVersions[name];
44
+ }
45
+ }
46
+ }
47
+ return resolved;
48
+ }
49
+
50
+ export { parseCatalogFromWorkspaceYaml, parseWorkspacePackagesFromYaml, resolveWorkspaceProtocols };
@@ -832,7 +832,7 @@
832
832
  "aliases": [],
833
833
  "args": {
834
834
  "routeName": {
835
- "description": "The route to generate. One of home,page,cart,products,collections,policies,blogs,account,search,robots,sitemap,tokenlessApi,all.",
835
+ "description": "The route to generate. One of home,page,cart,products,collections,policies,blogs,account,search,robots,sitemap,all.",
836
836
  "name": "routeName",
837
837
  "options": [
838
838
  "home",
@@ -846,7 +846,6 @@
846
846
  "search",
847
847
  "robots",
848
848
  "sitemap",
849
- "tokenlessApi",
850
849
  "all"
851
850
  ],
852
851
  "required": true
@@ -1676,5 +1675,5 @@
1676
1675
  ]
1677
1676
  }
1678
1677
  },
1679
- "version": "11.1.11"
1678
+ "version": "11.1.13"
1680
1679
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "@shopify:registry": "https://registry.npmjs.org"
6
6
  },
7
- "version": "11.1.11",
7
+ "version": "11.1.13",
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "repository": {
@@ -16,7 +16,7 @@
16
16
  "@react-router/dev": "7.12.0",
17
17
  "@types/diff": "^5.0.2",
18
18
  "@types/gunzip-maybe": "^1.4.0",
19
- "@types/node": "^22",
19
+ "@types/node": "22.19.15",
20
20
  "esbuild": "^0.25.0",
21
21
  "@types/prettier": "^2.7.2",
22
22
  "@types/source-map-support": "^0.5.10",
@@ -27,7 +27,7 @@
27
27
  "flame-chart-js": "2.3.2",
28
28
  "get-port": "^7.0.0",
29
29
  "type-fest": "^4.33.0",
30
- "vite": "^6.2.4",
30
+ "vite": "^6.4.2",
31
31
  "vitest": "^1.0.4"
32
32
  },
33
33
  "dependencies": {
@@ -59,7 +59,7 @@
59
59
  "graphql-config": "^5.0.3",
60
60
  "vite": "^5.1.0 || ^6.2.0",
61
61
  "@shopify/hydrogen-codegen": "0.3.3",
62
- "@shopify/mini-oxygen": "4.0.1"
62
+ "@shopify/mini-oxygen": "4.0.2"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "@graphql-codegen/cli": {
@@ -1,14 +0,0 @@
1
- import type {Route} from './+types/api.$version.[graphql.json]';
2
-
3
- export async function action({params, context, request}: Route.ActionArgs) {
4
- const response = await fetch(
5
- `https://${context.env.PUBLIC_CHECKOUT_DOMAIN}/api/${params.version}/graphql.json`,
6
- {
7
- method: 'POST',
8
- body: request.body,
9
- headers: request.headers,
10
- },
11
- );
12
-
13
- return new Response(response.body, {headers: new Headers(response.headers)});
14
- }