@shopify/cli-hydrogen 13.0.0 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,13 @@
1
1
  # skeleton
2
2
 
3
+ ## 2026.4.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Add an explicit `~` app alias to new Hydrogen projects. ([#3852](https://github.com/Shopify/hydrogen/pull/3852)) by [@andguy95](https://github.com/andguy95)
8
+
9
+ JavaScript projects use `jsconfig.json`, which is not reliably covered by Vite's native `resolve.tsconfigPaths` behavior. New projects now define Hydrogen's `~/` import convention directly in the Vite config so imports like `~/assets/favicon.svg` work in both TypeScript and JavaScript projects.
10
+
3
11
  ## 2026.4.4
4
12
 
5
13
  ### Patch Changes
@@ -1,13 +1,13 @@
1
1
  # Hydrogen template: Skeleton
2
2
 
3
- Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
3
+ Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [React Router](https://reactrouter.com/), the modern multi-strategy router for React. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
4
4
 
5
5
  [Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
6
- [Get familiar with Remix](https://remix.run/docs/en/v1)
6
+ [Get familiar with React Router](https://reactrouter.com/start/framework/routing)
7
7
 
8
8
  ## What's included
9
9
 
10
- - Remix
10
+ - React Router
11
11
  - Hydrogen
12
12
  - Oxygen
13
13
  - Vite
@@ -22,7 +22,7 @@ Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dov
22
22
 
23
23
  **Requirements:**
24
24
 
25
- - Node.js version 18.0.0 or higher
25
+ - Node.js version 22.x or 24.x
26
26
 
27
27
  ```bash
28
28
  npm create @shopify/hydrogen@latest
@@ -2,7 +2,7 @@
2
2
  "name": "skeleton",
3
3
  "private": true,
4
4
  "sideEffects": false,
5
- "version": "2026.4.4",
5
+ "version": "2026.4.5",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "shopify hydrogen build --codegen",
@@ -1,3 +1,4 @@
1
+ import {fileURLToPath} from 'node:url';
1
2
  import {defineConfig} from 'vite';
2
3
  import {hydrogen} from '@shopify/hydrogen/vite';
3
4
  import {oxygen} from '@shopify/mini-oxygen/vite';
@@ -6,6 +7,11 @@ import {reactRouter} from '@react-router/dev/vite';
6
7
  export default defineConfig({
7
8
  plugins: [hydrogen(), oxygen(), reactRouter()],
8
9
  resolve: {
10
+ alias: {
11
+ // Vite's native tsconfig path resolver does not cover JavaScript
12
+ // projects that use jsconfig.json, so define Hydrogen's app alias here.
13
+ '~': fileURLToPath(new URL('./app', import.meta.url)),
14
+ },
9
15
  tsconfigPaths: true,
10
16
  },
11
17
  build: {
@@ -47,7 +47,7 @@ class Deploy extends Command {
47
47
  }),
48
48
  force: Flags.boolean({
49
49
  char: "f",
50
- description: "Forces a deployment to proceed if there are uncommitted changes in its Git repository.",
50
+ description: "Forces a deployment to proceed if there are uncommitted changes in its Git repository, and skips confirmation prompts for non-preview environments.",
51
51
  default: false,
52
52
  env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
53
53
  required: false
@@ -166,7 +166,7 @@ async function runDeploy(options) {
166
166
  env: envHandle,
167
167
  envBranch,
168
168
  environmentFile,
169
- force: forceOnUncommittedChanges,
169
+ force,
170
170
  forceClientSourcemap = false,
171
171
  noVerify,
172
172
  lockfileCheck,
@@ -188,7 +188,7 @@ async function runDeploy(options) {
188
188
  if (error instanceof GitDirectoryNotCleanError) {
189
189
  isCleanGit = false;
190
190
  }
191
- if (!forceOnUncommittedChanges && !isCleanGit) {
191
+ if (!force && !isCleanGit) {
192
192
  let errorMessage = "Uncommitted changes detected";
193
193
  let changedFiles = void 0;
194
194
  const nextSteps = [
@@ -355,11 +355,11 @@ ${changedFiles.trimEnd()}`;
355
355
  workerDir = workerDirFlag ?? workerDir;
356
356
  } else {
357
357
  const viteConfig = await getViteConfig(root, ssrEntry).catch(() => null);
358
- const outputDirs = await resolveDeploymentOutputDirs({
358
+ const outputDirs = resolveDeploymentOutputDirs({
359
359
  root,
360
- viteConfig,
361
- assetsDir: assetsDirFlag,
362
- workerDir: workerDirFlag
360
+ viteOutputDirs: viteConfig,
361
+ assetsDirFlag,
362
+ workerDirFlag
363
363
  });
364
364
  assetsDir = outputDirs.assetsDir;
365
365
  workerDir = outputDirs.workerDir;
@@ -388,7 +388,7 @@ ${changedFiles.trimEnd()}`;
388
388
  workerDir,
389
389
  overriddenEnvironmentVariables
390
390
  };
391
- if (!isCI && !config.defaultEnvironment && (userProvidedEnvironmentTag || userChosenEnvironmentTag)) {
391
+ if (!isCI && !config.defaultEnvironment && (userProvidedEnvironmentTag || userChosenEnvironmentTag) && !force) {
392
392
  let chosenEnvironment = findEnvironmentByBranchOrThrow(
393
393
  deploymentData.environments,
394
394
  config.environmentTag
@@ -422,6 +422,7 @@ Continue?`.value
422
422
  }
423
423
  );
424
424
  let deployError = null;
425
+ let buildError = null;
425
426
  let resolveDeploy;
426
427
  let rejectDeploy;
427
428
  const deployPromise = new Promise((resolve, reject) => {
@@ -466,21 +467,26 @@ Continue?`.value
466
467
  config.buildCommand = buildCommand ?? DEFAULT_BUILD_COMMAND;
467
468
  } else {
468
469
  hooks.buildFunction = async (assetPath) => {
469
- outputInfo(
470
- outputContent`${colors.whiteBright("Building project...")}`.value
471
- );
472
- if (isClassicCompiler) {
473
- throw new AbortError(REMIX_COMPILER_ERROR_MESSAGE);
470
+ try {
471
+ outputInfo(
472
+ outputContent`${colors.whiteBright("Building project...")}`.value
473
+ );
474
+ if (isClassicCompiler) {
475
+ throw new AbortError(REMIX_COMPILER_ERROR_MESSAGE);
476
+ }
477
+ await runBuild({
478
+ directory: root,
479
+ assetPath,
480
+ lockfileCheck,
481
+ sourcemap: true,
482
+ forceClientSourcemap,
483
+ useCodegen: false,
484
+ entry: ssrEntry
485
+ });
486
+ } catch (error) {
487
+ buildError = error;
488
+ throw error;
474
489
  }
475
- await runBuild({
476
- directory: root,
477
- assetPath,
478
- lockfileCheck,
479
- sourcemap: true,
480
- forceClientSourcemap,
481
- useCodegen: false,
482
- entry: ssrEntry
483
- });
484
490
  };
485
491
  }
486
492
  const uploadStart = async () => {
@@ -542,27 +548,21 @@ Continue?`.value
542
548
  }
543
549
  resolveDeploy();
544
550
  }).catch((error) => {
545
- rejectDeploy(deployError || error);
551
+ rejectDeploy(deployError || buildError || error);
546
552
  });
547
553
  return deployPromise;
548
554
  }
549
- async function resolveDeploymentOutputDirs({
555
+ function resolveDeploymentOutputDirs({
550
556
  root,
551
- viteConfig,
552
- assetsDir,
553
- workerDir
557
+ viteOutputDirs,
558
+ assetsDirFlag,
559
+ workerDirFlag
554
560
  }) {
555
- const fallbackOutputDirs = {
556
- assetsDir: "dist/client",
557
- workerDir: "dist/server"
558
- };
559
- const viteOutputDirs = viteConfig ? {
560
- assetsDir: relativePath(root, viteConfig.clientOutDir),
561
- workerDir: relativePath(root, viteConfig.serverOutDir)
562
- } : void 0;
561
+ const viteAssetsDir = viteOutputDirs && relativePath(root, viteOutputDirs.clientOutDir);
562
+ const viteWorkerDir = viteOutputDirs && relativePath(root, viteOutputDirs.serverOutDir);
563
563
  return {
564
- assetsDir: assetsDir ?? viteOutputDirs?.assetsDir ?? fallbackOutputDirs.assetsDir,
565
- workerDir: workerDir ?? viteOutputDirs?.workerDir ?? fallbackOutputDirs.workerDir
564
+ assetsDir: assetsDirFlag ?? viteAssetsDir ?? "dist/client",
565
+ workerDir: workerDirFlag ?? viteWorkerDir ?? "dist/server"
566
566
  };
567
567
  }
568
568
  function isHydrogenPreviewVersion(version) {
@@ -1,4 +1,5 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
+ import { Flags } from '@oclif/core';
2
3
  import { diffLines } from 'diff';
3
4
  import { commonFlags, flagsToCamelObject } from '../../../lib/flags.js';
4
5
  import { login } from '../../../lib/auth.js';
@@ -19,7 +20,17 @@ class EnvPush extends Command {
19
20
  static flags = {
20
21
  ...commonFlags.env,
21
22
  ...commonFlags.envFile,
22
- ...commonFlags.path
23
+ ...commonFlags.path,
24
+ force: Flags.boolean({
25
+ char: "f",
26
+ description: "Push environment variable changes without confirmation.",
27
+ env: "SHOPIFY_HYDROGEN_FLAG_FORCE"
28
+ }),
29
+ "dry-run": Flags.boolean({
30
+ description: "Preview environment variable changes without pushing them.",
31
+ env: "SHOPIFY_HYDROGEN_FLAG_DRY_RUN",
32
+ exclusive: ["force"]
33
+ })
23
34
  };
24
35
  async run() {
25
36
  const { flags } = await this.parse(EnvPush);
@@ -27,8 +38,10 @@ class EnvPush extends Command {
27
38
  }
28
39
  }
29
40
  async function runEnvPush({
41
+ dryRun = false,
30
42
  env: envHandle,
31
43
  envFile,
44
+ force = false,
32
45
  path = process.cwd()
33
46
  }) {
34
47
  let validatedEnvironment;
@@ -114,8 +127,18 @@ async function runEnvPush({
114
127
  body: "No changes to your environment variables."
115
128
  });
116
129
  return;
117
- } else {
118
- const diff = diffLines(comparableRemoteVars, compareableLocalVars);
130
+ }
131
+ const diff = diffLines(comparableRemoteVars, compareableLocalVars);
132
+ if (dryRun) {
133
+ renderInfo({
134
+ body: outputContent`The following changes would be made to your environment variables for ${validatedEnvironment.name}:
135
+
136
+ ${outputToken.linesDiff(diff)}
137
+ No changes were pushed because --dry-run was used.`.value
138
+ });
139
+ return;
140
+ }
141
+ if (!force) {
119
142
  const confirmPush = await renderConfirmationPrompt({
120
143
  confirmationMessage: "Yes, confirm changes",
121
144
  cancellationMessage: "No, make changes later",
@@ -1,8 +1,8 @@
1
- import { createRequire } from 'node:module';
2
1
  import { spawnSync } from 'node:child_process';
3
2
  import { outputNewline, outputDebug } from '@shopify/cli-kit/node/output';
4
- import { cwd, resolvePath, joinPath } from '@shopify/cli-kit/node/path';
3
+ import { cwd, resolvePath } from '@shopify/cli-kit/node/path';
5
4
  import { renderWarning } from '@shopify/cli-kit/node/ui';
5
+ import { isHydrogenProject, applyHydrogenCommandPolicy } from '../lib/hydrogen-command-policy.js';
6
6
 
7
7
  const hook = async function(options) {
8
8
  if (!options.id?.startsWith("hydrogen:") || options.id === "hydrogen:init") {
@@ -36,6 +36,9 @@ const hook = async function(options) {
36
36
  });
37
37
  process.exit(1);
38
38
  }
39
+ if (applyHydrogenCommandPolicy({ id: options.id, projectPath })) {
40
+ process.exit(1);
41
+ }
39
42
  if (commandNeedsVM(options.id, options.argv) && !process.execArgv.includes(EXPERIMENTAL_VM_MODULES_FLAG) && !(process.env.NODE_OPTIONS ?? "").includes(EXPERIMENTAL_VM_MODULES_FLAG)) {
40
43
  outputDebug(
41
44
  `Restarting CLI process with ${EXPERIMENTAL_VM_MODULES_FLAG} flag.`
@@ -50,18 +53,6 @@ const EXPERIMENTAL_VM_MODULES_FLAG = "--experimental-vm-modules";
50
53
  function commandNeedsVM(id = "", argv = []) {
51
54
  return id === "hydrogen:debug:cpu";
52
55
  }
53
- function isHydrogenProject(projectPath) {
54
- try {
55
- const require2 = createRequire(import.meta.url);
56
- const { dependencies, scripts } = require2(
57
- joinPath(projectPath, "package.json")
58
- );
59
- return !!dependencies?.["@shopify/hydrogen"] || // Diff examples don't have dependencies:
60
- !!scripts?.dev?.includes("--diff");
61
- } catch {
62
- return false;
63
- }
64
- }
65
56
  var init_default = hook;
66
57
 
67
58
  export { init_default as default };
package/dist/index.d.ts CHANGED
@@ -126,6 +126,8 @@ declare class EnvPull extends Command {
126
126
  declare class EnvPush extends Command {
127
127
  static description: string;
128
128
  static flags: {
129
+ force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
130
+ 'dry-run': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
129
131
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
130
132
  'env-file': _oclif_core_lib_interfaces_parser_js.OptionFlag<string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
131
133
  env: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
@@ -0,0 +1,45 @@
1
+ import { createRequire } from 'node:module';
2
+ import { outputNewline } from '@shopify/cli-kit/node/output';
3
+ import { joinPath } from '@shopify/cli-kit/node/path';
4
+ import { renderError } from '@shopify/cli-kit/node/ui';
5
+
6
+ function applyHydrogenCommandPolicy({
7
+ id,
8
+ projectPath
9
+ }) {
10
+ if (!id || !isHydrogenProject(projectPath) || !isHydrogenCommandDisabled(projectPath, id)) {
11
+ return false;
12
+ }
13
+ outputNewline();
14
+ renderError({
15
+ headline: `\`shopify ${id.replace(/:/g, " ")}\` is not supported by this version of Hydrogen`,
16
+ body: "The installed version of @shopify/hydrogen disables this command.",
17
+ nextSteps: ["Use your framework or package tooling instead."]
18
+ });
19
+ return true;
20
+ }
21
+ function isHydrogenCommandDisabled(projectPath, id) {
22
+ try {
23
+ const require2 = createRequire(joinPath(projectPath, "package.json"));
24
+ const hydrogenPackageJson = require2("@shopify/hydrogen/package.json");
25
+ const disabledCommands = hydrogenPackageJson?.shopify?.cli?.disabledCommands;
26
+ return Array.isArray(disabledCommands) && disabledCommands.includes(id);
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ function isHydrogenProject(projectPath) {
32
+ try {
33
+ const require2 = createRequire(import.meta.url);
34
+ const projectPackageJson = require2(joinPath(projectPath, "package.json"));
35
+ return [
36
+ projectPackageJson?.dependencies,
37
+ projectPackageJson?.devDependencies,
38
+ projectPackageJson?.peerDependencies
39
+ ].some((dependencies) => !!dependencies?.["@shopify/hydrogen"]);
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ export { applyHydrogenCommandPolicy, isHydrogenCommandDisabled, isHydrogenProject };
@@ -37,9 +37,7 @@ async function getViteConfig(root, ssrEntryFlag) {
37
37
  mode,
38
38
  mode
39
39
  );
40
- const { appDirectory, serverBuildFile, routes } = getReactRouterOrRemixConfigFromVite(resolvedViteConfig);
41
- const serverOutDir = resolvedViteConfig.environments.ssr?.build.outDir ?? resolvedViteConfig.build.outDir;
42
- const clientOutDir = resolvedViteConfig.environments.client?.build.outDir ?? serverOutDir.replace(/server$/, "client");
40
+ const { appDirectory, clientOutDir, serverBuildFile, serverOutDir, routes } = getBuildConfigFromVite(resolvedViteConfig);
43
41
  const rollupOutput = resolvedViteConfig.build.rollupOptions.output;
44
42
  const { entryFileNames } = (Array.isArray(rollupOutput) ? rollupOutput[0] : rollupOutput) ?? {};
45
43
  const serverOutFile = joinPath(
@@ -68,33 +66,58 @@ async function getViteConfig(root, ssrEntryFlag) {
68
66
  }
69
67
  };
70
68
  }
71
- function getReactRouterOrRemixConfigFromVite(viteConfig) {
72
- try {
73
- const reactRouterConfig = getReactRouterConfigFromVite(viteConfig);
74
- return reactRouterConfig;
75
- } catch (error) {
76
- const remixConfig = getRemixConfigFromVite(viteConfig);
77
- return remixConfig;
69
+ function resolveViteOutputDirs({
70
+ routingBuildDirectory,
71
+ serverEnvironmentOutDir,
72
+ clientEnvironmentOutDir,
73
+ buildOutDir
74
+ }) {
75
+ if (routingBuildDirectory) {
76
+ return {
77
+ clientOutDir: joinPath(routingBuildDirectory, "client"),
78
+ serverOutDir: joinPath(routingBuildDirectory, "server")
79
+ };
78
80
  }
81
+ const serverOutDir = serverEnvironmentOutDir ?? buildOutDir;
82
+ const clientOutDir = clientEnvironmentOutDir && clientEnvironmentOutDir !== serverOutDir ? clientEnvironmentOutDir : serverOutDir.replace(/server$/, "client");
83
+ return {
84
+ clientOutDir,
85
+ serverOutDir
86
+ };
87
+ }
88
+ function getBuildConfigFromVite(viteConfig) {
89
+ const buildConfig = getReactRouterConfigFromVite(viteConfig) ?? getRemixConfigFromVite(viteConfig);
90
+ const { clientOutDir, serverOutDir } = resolveViteOutputDirs({
91
+ routingBuildDirectory: buildConfig?.buildDirectory,
92
+ serverEnvironmentOutDir: viteConfig.environments?.ssr?.build.outDir,
93
+ clientEnvironmentOutDir: viteConfig.environments?.client?.build.outDir,
94
+ buildOutDir: viteConfig.build.outDir
95
+ });
96
+ return {
97
+ ...buildConfig,
98
+ clientOutDir,
99
+ serverOutDir
100
+ };
79
101
  }
80
102
  function getReactRouterConfigFromVite(viteConfig) {
81
- if (!viteConfig.__reactRouterPluginContext) {
82
- throw new Error("Could not resolve React Router config");
83
- }
84
- const { appDirectory, serverBuildFile, routes } = viteConfig.__reactRouterPluginContext.reactRouterConfig;
103
+ if (!viteConfig.__reactRouterPluginContext) return;
104
+ const { appDirectory, buildDirectory, serverBuildFile, routes } = viteConfig.__reactRouterPluginContext.reactRouterConfig;
85
105
  return {
86
106
  appDirectory,
107
+ buildDirectory,
87
108
  serverBuildFile,
88
109
  routes
89
110
  };
90
111
  }
91
112
  function getRemixConfigFromVite(viteConfig) {
92
113
  const { remixConfig } = findHydrogenPlugin(viteConfig)?.api?.getPluginOptions() ?? {};
93
- return remixConfig ? {
94
- appDirectory: remixConfig.appDirectory,
95
- serverBuildFile: remixConfig.serverBuildFile,
96
- routes: remixConfig.routes
97
- } : {};
114
+ if (remixConfig) {
115
+ return {
116
+ appDirectory: remixConfig.appDirectory,
117
+ serverBuildFile: remixConfig.serverBuildFile,
118
+ routes: remixConfig.routes
119
+ };
120
+ }
98
121
  }
99
122
  function findPlugin(config, name) {
100
123
  return config.plugins.find((plugin) => plugin.name === name);
@@ -353,7 +353,7 @@
353
353
  },
354
354
  "force": {
355
355
  "char": "f",
356
- "description": "Forces a deployment to proceed if there are uncommitted changes in its Git repository.",
356
+ "description": "Forces a deployment to proceed if there are uncommitted changes in its Git repository, and skips confirmation prompts for non-preview environments.",
357
357
  "env": "SHOPIFY_HYDROGEN_FLAG_FORCE",
358
358
  "name": "force",
359
359
  "required": false,
@@ -805,6 +805,24 @@
805
805
  "hasDynamicHelp": false,
806
806
  "multiple": false,
807
807
  "type": "option"
808
+ },
809
+ "force": {
810
+ "char": "f",
811
+ "description": "Push environment variable changes without confirmation.",
812
+ "env": "SHOPIFY_HYDROGEN_FLAG_FORCE",
813
+ "name": "force",
814
+ "allowNo": false,
815
+ "type": "boolean"
816
+ },
817
+ "dry-run": {
818
+ "description": "Preview environment variable changes without pushing them.",
819
+ "env": "SHOPIFY_HYDROGEN_FLAG_DRY_RUN",
820
+ "exclusive": [
821
+ "force"
822
+ ],
823
+ "name": "dry-run",
824
+ "allowNo": false,
825
+ "type": "boolean"
808
826
  }
809
827
  },
810
828
  "hasDynamicHelp": false,
@@ -1693,5 +1711,5 @@
1693
1711
  ]
1694
1712
  }
1695
1713
  },
1696
- "version": "13.0.0"
1714
+ "version": "13.0.1"
1697
1715
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "@shopify:registry": "https://registry.npmjs.org"
6
6
  },
7
- "version": "13.0.0",
7
+ "version": "13.0.1",
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "repository": {