@shopify/cli-hydrogen 7.0.1 → 7.1.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.
Files changed (87) hide show
  1. package/dist/commands/hydrogen/build-vite.js +131 -0
  2. package/dist/commands/hydrogen/build.js +7 -21
  3. package/dist/commands/hydrogen/check.js +1 -1
  4. package/dist/commands/hydrogen/codegen.js +3 -3
  5. package/dist/commands/hydrogen/debug/cpu.js +1 -1
  6. package/dist/commands/hydrogen/deploy.js +113 -51
  7. package/dist/commands/hydrogen/deploy.test.js +162 -19
  8. package/dist/commands/hydrogen/dev-vite.js +159 -0
  9. package/dist/commands/hydrogen/dev.js +11 -14
  10. package/dist/commands/hydrogen/env/list.js +1 -1
  11. package/dist/commands/hydrogen/env/pull.js +3 -3
  12. package/dist/commands/hydrogen/env/pull.test.js +2 -0
  13. package/dist/commands/hydrogen/env/push__unstable.js +190 -0
  14. package/dist/commands/hydrogen/env/push__unstable.test.js +383 -0
  15. package/dist/commands/hydrogen/generate/route.js +8 -3
  16. package/dist/commands/hydrogen/init.d.ts +69 -0
  17. package/dist/commands/hydrogen/init.js +5 -5
  18. package/dist/commands/hydrogen/init.test.js +3 -2
  19. package/dist/commands/hydrogen/link.js +2 -2
  20. package/dist/commands/hydrogen/list.js +1 -1
  21. package/dist/commands/hydrogen/login.js +2 -9
  22. package/dist/commands/hydrogen/logout.js +1 -1
  23. package/dist/commands/hydrogen/preview.js +15 -7
  24. package/dist/commands/hydrogen/setup/css.js +3 -3
  25. package/dist/commands/hydrogen/setup/markets.js +4 -4
  26. package/dist/commands/hydrogen/setup/vite.js +209 -0
  27. package/dist/commands/hydrogen/setup.js +8 -6
  28. package/dist/commands/hydrogen/unlink.js +1 -1
  29. package/dist/commands/hydrogen/upgrade.js +5 -3
  30. package/dist/generator-templates/assets/vite/package.json +15 -0
  31. package/dist/generator-templates/assets/vite/vite.config.js +13 -0
  32. package/dist/generator-templates/starter/CHANGELOG.md +69 -0
  33. package/dist/generator-templates/starter/README.md +25 -2
  34. package/dist/generator-templates/starter/app/components/Cart.tsx +2 -2
  35. package/dist/generator-templates/starter/app/components/Layout.tsx +9 -1
  36. package/dist/generator-templates/starter/app/components/Search.tsx +44 -15
  37. package/dist/generator-templates/starter/app/lib/search.ts +29 -0
  38. package/dist/generator-templates/starter/app/root.tsx +1 -4
  39. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -2
  40. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +1 -21
  41. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -5
  42. package/dist/generator-templates/starter/app/routes/search.tsx +8 -2
  43. package/dist/generator-templates/starter/app/styles/app.css +10 -15
  44. package/dist/generator-templates/starter/package.json +9 -8
  45. package/dist/generator-templates/starter/public/.gitkeep +0 -0
  46. package/dist/generator-templates/starter/remix.config.js +1 -0
  47. package/dist/generator-templates/starter/server.ts +1 -0
  48. package/dist/hooks/init.js +3 -3
  49. package/dist/lib/build.js +2 -1
  50. package/dist/lib/codegen.js +8 -3
  51. package/dist/lib/environment-variables.test.js +4 -2
  52. package/dist/lib/flags.js +149 -89
  53. package/dist/lib/graphql/admin/pull-variables.js +1 -0
  54. package/dist/lib/graphql/admin/pull-variables.test.js +7 -1
  55. package/dist/lib/graphql/admin/push-variables.js +35 -0
  56. package/dist/lib/log.js +1 -0
  57. package/dist/lib/mini-oxygen/common.js +2 -1
  58. package/dist/lib/mini-oxygen/node.js +2 -2
  59. package/dist/lib/mini-oxygen/workerd-inspector.js +1 -1
  60. package/dist/lib/mini-oxygen/workerd.js +29 -17
  61. package/dist/lib/onboarding/common.js +0 -3
  62. package/dist/lib/onboarding/local.js +4 -1
  63. package/dist/lib/onboarding/remote.js +16 -11
  64. package/dist/lib/remix-config.js +1 -1
  65. package/dist/lib/request-events.js +3 -3
  66. package/dist/lib/setups/css/assets.js +7 -2
  67. package/dist/lib/setups/i18n/replacers.test.js +1 -0
  68. package/dist/lib/setups/routes/generate.js +58 -10
  69. package/dist/lib/setups/routes/templates/locale-check.js +9 -0
  70. package/dist/lib/setups/routes/templates/locale-check.ts +16 -0
  71. package/dist/lib/template-diff.js +26 -11
  72. package/dist/lib/template-downloader.js +11 -2
  73. package/dist/lib/vite/hydrogen-middleware.js +82 -0
  74. package/dist/lib/vite/mini-oxygen.js +152 -0
  75. package/dist/lib/vite/plugins.d.ts +27 -0
  76. package/dist/lib/vite/plugins.js +139 -0
  77. package/dist/lib/vite/shared.js +10 -0
  78. package/dist/lib/vite/utils.js +55 -0
  79. package/dist/lib/vite/worker-entry.js +1518 -0
  80. package/dist/lib/vite-config.js +45 -0
  81. package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +4 -2
  82. package/dist/virtual-routes/routes/index.jsx +5 -5
  83. package/dist/virtual-routes/routes/subrequest-profiler.jsx +1 -1
  84. package/dist/virtual-routes/virtual-root.jsx +1 -1
  85. package/oclif.manifest.json +1146 -474
  86. package/package.json +36 -11
  87. /package/dist/generator-templates/starter/{public → app/assets}/favicon.svg +0 -0
@@ -0,0 +1,190 @@
1
+ import Command from '@shopify/cli-kit/node/base-command';
2
+ import { diffLines } from 'diff';
3
+ import { Flags } from '@oclif/core';
4
+ import { commonFlags, flagsToCamelObject } from '../../../lib/flags.js';
5
+ import { login } from '../../../lib/auth.js';
6
+ import { getCliCommand } from '../../../lib/shell.js';
7
+ import { resolvePath } from '@shopify/cli-kit/node/path';
8
+ import { renderConfirmationPrompt, renderSelectPrompt, renderInfo, renderSuccess } from '@shopify/cli-kit/node/ui';
9
+ import { outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
10
+ import { renderMissingLink } from '../../../lib/render-errors.js';
11
+ import { getStorefrontEnvironments } from '../../../lib/graphql/admin/list-environments.js';
12
+ import { linkStorefront } from '../link.js';
13
+ import { getStorefrontEnvVariables } from '../../../lib/graphql/admin/pull-variables.js';
14
+ import { pushStorefrontEnvVariables } from '../../../lib/graphql/admin/push-variables.js';
15
+ import { AbortError } from '@shopify/cli-kit/node/error';
16
+ import { readAndParseDotEnv } from '@shopify/cli-kit/node/dot-env';
17
+
18
+ class EnvPush extends Command {
19
+ static description = "Push environment variables from the local .env file to your linked Hydrogen storefront.";
20
+ static hidden = true;
21
+ static flags = {
22
+ ...commonFlags.env,
23
+ "env-file": Flags.string({
24
+ description: "Specify the environment variable file name. Default value is '.env'.",
25
+ env: "SHOPIFY_HYDROGEN_ENVIRONMENT_FILENAME"
26
+ }),
27
+ ...commonFlags.path
28
+ };
29
+ async run() {
30
+ const { flags } = await this.parse(EnvPush);
31
+ await runEnvPush({ ...flagsToCamelObject(flags) });
32
+ }
33
+ }
34
+ async function runEnvPush({
35
+ env: environmentName,
36
+ envFile = ".env",
37
+ path = process.cwd()
38
+ }) {
39
+ let validatedEnvironment = {};
40
+ const dotEnvPath = resolvePath(path, envFile);
41
+ const { variables: localVariables } = await readAndParseDotEnv(dotEnvPath);
42
+ const [{ session, config }, cliCommand] = await Promise.all([
43
+ login(path),
44
+ getCliCommand()
45
+ ]);
46
+ if (!config.storefront?.id) {
47
+ renderMissingLink({ session, cliCommand });
48
+ const runLink = await renderConfirmationPrompt({
49
+ message: outputContent`Run ${outputToken.genericShellCommand(
50
+ `${cliCommand} link`
51
+ )}?`.value
52
+ });
53
+ if (!runLink)
54
+ return;
55
+ config.storefront = await linkStorefront(path, session, config, {
56
+ cliCommand
57
+ });
58
+ }
59
+ if (!config.storefront?.id)
60
+ return;
61
+ const { environments: environmentsData } = await getStorefrontEnvironments(session, config.storefront.id) ?? {};
62
+ if (!environmentsData) {
63
+ throw new AbortError("Failed to fetch environments");
64
+ }
65
+ const environments = [
66
+ ...environmentsData.filter((environment) => environment.type === "PREVIEW"),
67
+ ...environmentsData.filter((environment) => environment.type === "CUSTOM"),
68
+ ...environmentsData.filter(
69
+ (environment) => environment.type === "PRODUCTION"
70
+ )
71
+ ];
72
+ if (environments.length === 0) {
73
+ throw new AbortError("No environments found");
74
+ }
75
+ if (environmentName) {
76
+ const matchedEnvironments = environments.filter(
77
+ ({ name }) => name === environmentName
78
+ );
79
+ if (matchedEnvironments.length === 0) {
80
+ throw new AbortError(
81
+ "Environment not found",
82
+ `We could not find an environment matching the name '${environmentName}'.`
83
+ );
84
+ } else if (matchedEnvironments.length === 1) {
85
+ const { id, name, branch, type } = matchedEnvironments[0] ?? {};
86
+ validatedEnvironment = { id, name, branch, type };
87
+ } else {
88
+ const selection = await renderSelectPrompt({
89
+ message: `There were multiple environments found with the name ${environmentName}:`,
90
+ choices: [
91
+ ...matchedEnvironments.map(({ id: id2, name: name2, branch: branch2, type: type2, url }) => ({
92
+ label: `${name2} (${branch2}) ${type2} ${url}`,
93
+ value: id2
94
+ }))
95
+ ]
96
+ });
97
+ const { id, name, branch, type } = matchedEnvironments.find(({ id: id2 }) => id2 === selection) ?? {};
98
+ validatedEnvironment = { id, name, branch, type };
99
+ }
100
+ } else {
101
+ const choices = [
102
+ ...environments.map(({ id: id2, name: name2, branch: branch2 }) => ({
103
+ label: branch2 ? `${name2} (${branch2})` : name2,
104
+ value: id2
105
+ }))
106
+ ];
107
+ const pushToBranchSelection = await renderSelectPrompt({
108
+ message: "Select a set of environment variables to overwrite:",
109
+ choices
110
+ });
111
+ const { id, name, branch, type } = environments.find(({ id: id2 }) => id2 === pushToBranchSelection) ?? {};
112
+ validatedEnvironment = { id, name, branch, type };
113
+ }
114
+ const { environmentVariables = [] } = await getStorefrontEnvVariables(
115
+ session,
116
+ config.storefront.id,
117
+ validatedEnvironment.branch ?? void 0
118
+ ) ?? {};
119
+ const remoteVars = environmentVariables.filter(
120
+ ({ isSecret, readOnly }) => !isSecret && !readOnly
121
+ );
122
+ const comparableRemoteVars = remoteVars.sort((a, b) => a.key.localeCompare(b.key)).map(({ key, value }) => `${key}=${value}`).join("\n") + "\n";
123
+ const compareableLocalVars = Object.keys(localVariables).sort((a, b) => a.localeCompare(b)).reduce((acc, key) => {
124
+ const { isSecret, readOnly } = environmentVariables.find((variable) => variable.key === key) ?? {};
125
+ if (isSecret || readOnly)
126
+ return acc;
127
+ return [...acc, `${key}=${localVariables[key]}`];
128
+ }, []).join("\n") + "\n";
129
+ if (!validatedEnvironment.name)
130
+ throw new AbortError("Missing environment name");
131
+ const remoteReadOnlyOrSecrets = environmentVariables.reduce(
132
+ (acc, { isSecret, readOnly, key }) => {
133
+ if (!isSecret && !readOnly)
134
+ return acc;
135
+ const localVar = localVariables[key];
136
+ const remoteVar = environmentVariables.find(
137
+ (variable) => variable.key === key
138
+ );
139
+ if (localVar === remoteVar?.value)
140
+ return acc;
141
+ return [...acc, key];
142
+ },
143
+ []
144
+ );
145
+ if (remoteReadOnlyOrSecrets.length) {
146
+ outputWarn(
147
+ `Variables that are read only or contain secret values cannot be pushed from the CLI: ${remoteReadOnlyOrSecrets.join(
148
+ ", "
149
+ )}.
150
+ `
151
+ );
152
+ }
153
+ if (compareableLocalVars === comparableRemoteVars) {
154
+ renderInfo({
155
+ body: "No changes to your environment variables."
156
+ });
157
+ return;
158
+ } else {
159
+ const diff = diffLines(comparableRemoteVars, compareableLocalVars);
160
+ const confirmPush = await renderConfirmationPrompt({
161
+ confirmationMessage: "Yes, confirm changes",
162
+ cancellationMessage: "No, make changes later",
163
+ message: outputContent`We'll make the following changes to your environment variables for ${validatedEnvironment.name}:
164
+
165
+ ${outputToken.linesDiff(diff)}
166
+ Continue?`.value
167
+ });
168
+ if (!confirmPush)
169
+ return;
170
+ }
171
+ if (!validatedEnvironment.id)
172
+ throw new AbortError("Missing environment ID");
173
+ const { userErrors } = await pushStorefrontEnvVariables(
174
+ session,
175
+ config.storefront.id,
176
+ validatedEnvironment.id,
177
+ Object.entries(localVariables).map(([key, value]) => ({ key, value }))
178
+ );
179
+ if (userErrors.length) {
180
+ throw new AbortError(
181
+ "Failed to upload and save environment variables.",
182
+ userErrors[0]?.message
183
+ );
184
+ }
185
+ renderSuccess({
186
+ body: `Environment variables push to ${validatedEnvironment.name ?? "Preview"} was successful.`
187
+ });
188
+ }
189
+
190
+ export { EnvPush as default, runEnvPush };
@@ -0,0 +1,383 @@
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
3
+ import { inTemporaryDirectory, writeFile } from '@shopify/cli-kit/node/fs';
4
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { renderConfirmationPrompt, renderSelectPrompt } from '@shopify/cli-kit/node/ui';
6
+ import { login } from '../../../lib/auth.js';
7
+ import { getStorefrontEnvVariables } from '../../../lib/graphql/admin/pull-variables.js';
8
+ import { getStorefrontEnvironments } from '../../../lib/graphql/admin/list-environments.js';
9
+ import { pushStorefrontEnvVariables } from '../../../lib/graphql/admin/push-variables.js';
10
+ import { runEnvPush } from './push__unstable.js';
11
+
12
+ vi.mock("@shopify/cli-kit/node/ui", async () => {
13
+ const original = await vi.importActual("@shopify/cli-kit/node/ui");
14
+ return {
15
+ ...original,
16
+ renderConfirmationPrompt: vi.fn(),
17
+ renderSelectPrompt: vi.fn()
18
+ };
19
+ });
20
+ vi.mock("../link.js");
21
+ vi.mock("../../../lib/auth.js");
22
+ vi.mock("../../../lib/render-errors.js");
23
+ vi.mock("../../../lib/graphql/admin/pull-variables.js");
24
+ vi.mock("../../../lib/graphql/admin/list-environments.js");
25
+ vi.mock("../../../lib/graphql/admin/push-variables.js");
26
+ const ADMIN_SESSION = {
27
+ token: "abc123",
28
+ storeFqdn: "my-shop"
29
+ };
30
+ const SHOPIFY_CONFIG = {
31
+ shop: "my-shop",
32
+ shopName: "My Shop",
33
+ email: "email",
34
+ storefront: {
35
+ id: "gid://shopify/HydrogenStorefront/2",
36
+ title: "Existing Link"
37
+ }
38
+ };
39
+ const PRODUCTION_ENV = {
40
+ id: "gid://shopify/HydrogenStorefrontEnvironment/1",
41
+ createdAt: "2024-01-01",
42
+ branch: "main",
43
+ name: "Production",
44
+ type: "PRODUCTION",
45
+ url: "production.com"
46
+ };
47
+ const PREVIEW_ENV = {
48
+ id: "gid://shopify/HydrogenStorefrontEnvironment/2",
49
+ createdAt: "2024-01-01",
50
+ branch: null,
51
+ name: "Preview",
52
+ type: "PREVIEW",
53
+ url: null
54
+ };
55
+ const CUSTOM_ENV = {
56
+ id: "gid://shopify/HydrogenStorefrontEnvironment/3",
57
+ createdAt: "2024-01-01",
58
+ branch: "staging",
59
+ name: "Staging",
60
+ type: "CUSTOM",
61
+ url: "custom.com"
62
+ };
63
+ const outputMock = mockAndCaptureOutput();
64
+ const processExit = vi.spyOn(process, "exit");
65
+ describe("pushVariables", () => {
66
+ beforeEach(async () => {
67
+ processExit.mockImplementation(() => {
68
+ throw "mockExit";
69
+ });
70
+ vi.mocked(login).mockResolvedValue({
71
+ session: ADMIN_SESSION,
72
+ config: SHOPIFY_CONFIG
73
+ });
74
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
75
+ id: SHOPIFY_CONFIG.storefront.id,
76
+ environmentVariables: [
77
+ {
78
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
79
+ key: "PUBLIC_API_TOKEN",
80
+ value: "abc123",
81
+ readOnly: true,
82
+ isSecret: false
83
+ },
84
+ {
85
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/2",
86
+ key: "PRIVATE_API_TOKEN",
87
+ value: "",
88
+ readOnly: true,
89
+ isSecret: true
90
+ }
91
+ ]
92
+ });
93
+ vi.mocked(getStorefrontEnvironments).mockResolvedValue({
94
+ id: SHOPIFY_CONFIG.storefront.id,
95
+ productionUrl: "prod.com",
96
+ environments: [PRODUCTION_ENV, PREVIEW_ENV, CUSTOM_ENV]
97
+ });
98
+ vi.mocked(pushStorefrontEnvVariables).mockResolvedValue({
99
+ userErrors: []
100
+ });
101
+ });
102
+ afterEach(() => {
103
+ outputMock.clear();
104
+ vi.clearAllMocks();
105
+ vi.resetAllMocks();
106
+ });
107
+ it("calls getStorefrontEnvironments", async () => {
108
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
109
+ await inTemporaryDirectory(async (tmpDir) => {
110
+ const filePath = joinPath(tmpDir, ".env");
111
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
112
+ await expect(
113
+ runEnvPush({ path: tmpDir, env: "Preview" })
114
+ ).resolves.not.toThrow();
115
+ expect(getStorefrontEnvironments).toHaveBeenCalledWith(
116
+ ADMIN_SESSION,
117
+ SHOPIFY_CONFIG.storefront.id
118
+ );
119
+ });
120
+ });
121
+ it("errors if no environments data", async () => {
122
+ vi.mocked(getStorefrontEnvironments).mockResolvedValue({
123
+ id: SHOPIFY_CONFIG.storefront.id,
124
+ productionUrl: "prod.com",
125
+ environments: []
126
+ });
127
+ await inTemporaryDirectory(async (tmpDir) => {
128
+ const filePath = joinPath(tmpDir, ".env");
129
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
130
+ await expect(
131
+ runEnvPush({ path: tmpDir, env: "Preview" })
132
+ ).rejects.toThrowError("No environments found");
133
+ });
134
+ });
135
+ it("prompts the user to select an environment", async () => {
136
+ vi.mocked(renderSelectPrompt).mockResolvedValue(PREVIEW_ENV.id);
137
+ await inTemporaryDirectory(async (tmpDir) => {
138
+ const filePath = joinPath(tmpDir, ".env");
139
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
140
+ await expect(runEnvPush({ path: tmpDir })).resolves.not.toThrow();
141
+ expect(renderSelectPrompt).toHaveBeenCalledWith({
142
+ message: "Select a set of environment variables to overwrite:",
143
+ choices: [
144
+ expect.objectContaining({ label: expect.stringContaining("Preview") }),
145
+ expect.objectContaining({ label: expect.stringContaining("Staging") }),
146
+ expect.objectContaining({
147
+ label: expect.stringContaining("Production")
148
+ })
149
+ ]
150
+ });
151
+ });
152
+ });
153
+ describe("when an environment is passed", () => {
154
+ it("errors when the environment does not match graphql", async () => {
155
+ await inTemporaryDirectory(async (tmpDir) => {
156
+ const filePath = joinPath(tmpDir, ".env");
157
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
158
+ await expect(
159
+ runEnvPush({ path: tmpDir, env: "Something random" })
160
+ ).rejects.toThrowError("Environment not found");
161
+ });
162
+ });
163
+ it("prompts the user if there are multiple matches", async () => {
164
+ vi.mocked(renderSelectPrompt).mockResolvedValue(PREVIEW_ENV.id);
165
+ vi.mocked(getStorefrontEnvironments).mockResolvedValue({
166
+ id: SHOPIFY_CONFIG.storefront.id,
167
+ productionUrl: "prod.com",
168
+ environments: [
169
+ PRODUCTION_ENV,
170
+ PREVIEW_ENV,
171
+ { ...CUSTOM_ENV, name: "Preview" }
172
+ ]
173
+ });
174
+ await inTemporaryDirectory(async (tmpDir) => {
175
+ const filePath = joinPath(tmpDir, ".env");
176
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
177
+ await expect(
178
+ runEnvPush({ path: tmpDir, env: "Preview" })
179
+ ).resolves.not.toThrow();
180
+ expect(renderSelectPrompt).toHaveBeenCalledWith({
181
+ message: "There were multiple environments found with the name Preview:",
182
+ choices: expect.any(Array)
183
+ });
184
+ });
185
+ });
186
+ });
187
+ it("exits if variables are identical", async () => {
188
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
189
+ id: SHOPIFY_CONFIG.storefront.id,
190
+ environmentVariables: [
191
+ {
192
+ id: "1",
193
+ key: "EXISTING_TOKEN",
194
+ value: "1",
195
+ isSecret: false,
196
+ readOnly: false
197
+ },
198
+ {
199
+ id: "2",
200
+ key: "SECOND_TOKEN",
201
+ value: "2",
202
+ isSecret: false,
203
+ readOnly: false
204
+ }
205
+ ]
206
+ });
207
+ await inTemporaryDirectory(async (tmpDir) => {
208
+ const filePath = joinPath(tmpDir, ".env");
209
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
210
+ await expect(
211
+ runEnvPush({ path: tmpDir, env: "Preview" })
212
+ ).resolves.not.toThrow();
213
+ expect(outputMock.info()).toMatch(
214
+ /No changes to your environment variables/
215
+ );
216
+ });
217
+ });
218
+ it("renders a diff when a variable is updated", async () => {
219
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
220
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
221
+ id: SHOPIFY_CONFIG.storefront.id,
222
+ environmentVariables: [
223
+ {
224
+ id: "1",
225
+ key: "EXISTING_TOKEN",
226
+ value: "1",
227
+ isSecret: false,
228
+ readOnly: false
229
+ },
230
+ {
231
+ id: "2",
232
+ key: "SECOND_TOKEN",
233
+ value: "updated value",
234
+ isSecret: false,
235
+ readOnly: false
236
+ }
237
+ ]
238
+ });
239
+ await inTemporaryDirectory(async (tmpDir) => {
240
+ const filePath = joinPath(tmpDir, ".env");
241
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
242
+ await expect(
243
+ runEnvPush({ path: tmpDir, env: "Preview" })
244
+ ).resolves.not.toThrow();
245
+ expect(renderConfirmationPrompt).toHaveBeenCalled();
246
+ });
247
+ });
248
+ it("ignores comparison against secrets", async () => {
249
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
250
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
251
+ id: SHOPIFY_CONFIG.storefront.id,
252
+ environmentVariables: [
253
+ {
254
+ id: "1",
255
+ key: "EXISTING_TOKEN",
256
+ value: "1",
257
+ isSecret: false,
258
+ readOnly: false
259
+ },
260
+ {
261
+ id: "2",
262
+ key: "SECOND_TOKEN",
263
+ value: "updated value",
264
+ isSecret: true,
265
+ readOnly: false
266
+ }
267
+ ]
268
+ });
269
+ await inTemporaryDirectory(async (tmpDir) => {
270
+ const filePath = joinPath(tmpDir, ".env");
271
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
272
+ await expect(
273
+ runEnvPush({ path: tmpDir, env: "Preview" })
274
+ ).resolves.not.toThrow();
275
+ });
276
+ expect(outputMock.info()).toMatch(
277
+ /No changes to your environment variables/
278
+ );
279
+ });
280
+ it("ignores comparison against read only variables", async () => {
281
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
282
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
283
+ id: SHOPIFY_CONFIG.storefront.id,
284
+ environmentVariables: [
285
+ {
286
+ id: "1",
287
+ key: "EXISTING_TOKEN",
288
+ value: "1",
289
+ isSecret: false,
290
+ readOnly: false
291
+ },
292
+ {
293
+ id: "2",
294
+ key: "SECOND_TOKEN",
295
+ value: "updated value",
296
+ isSecret: false,
297
+ readOnly: true
298
+ }
299
+ ]
300
+ });
301
+ await inTemporaryDirectory(async (tmpDir) => {
302
+ const filePath = joinPath(tmpDir, ".env");
303
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
304
+ await expect(
305
+ runEnvPush({ path: tmpDir, env: "Preview" })
306
+ ).resolves.not.toThrow();
307
+ expect(outputMock.info()).toMatch(
308
+ /No changes to your environment variables/
309
+ );
310
+ });
311
+ });
312
+ it("exits when diff is not confirmed", async () => {
313
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(false);
314
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
315
+ id: SHOPIFY_CONFIG.storefront.id,
316
+ environmentVariables: [
317
+ {
318
+ id: "1",
319
+ key: "EXISTING_TOKEN",
320
+ value: "1",
321
+ isSecret: false,
322
+ readOnly: false
323
+ },
324
+ {
325
+ id: "2",
326
+ key: "SECOND_TOKEN",
327
+ value: "updated value",
328
+ isSecret: false,
329
+ readOnly: true
330
+ }
331
+ ]
332
+ });
333
+ await inTemporaryDirectory(async (tmpDir) => {
334
+ const filePath = joinPath(tmpDir, ".env");
335
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
336
+ await expect(
337
+ runEnvPush({ path: tmpDir, env: "Preview" })
338
+ ).resolves.not.toThrow();
339
+ expect(pushStorefrontEnvVariables).not.toHaveBeenCalled();
340
+ });
341
+ });
342
+ it("calls pushStorefrontEnvVariables when diff is confirmed", async () => {
343
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
344
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
345
+ id: SHOPIFY_CONFIG.storefront.id,
346
+ environmentVariables: [
347
+ {
348
+ id: "1",
349
+ key: "EXISTING_TOKEN",
350
+ value: "1",
351
+ isSecret: false,
352
+ readOnly: false
353
+ },
354
+ {
355
+ id: "2",
356
+ key: "SECOND_TOKEN",
357
+ value: "2",
358
+ isSecret: false,
359
+ readOnly: false
360
+ }
361
+ ]
362
+ });
363
+ await inTemporaryDirectory(async (tmpDir) => {
364
+ const filePath = joinPath(tmpDir, ".env");
365
+ await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=NEW_VALUE");
366
+ await expect(
367
+ runEnvPush({ path: tmpDir, env: "Preview" })
368
+ ).resolves.not.toThrow();
369
+ expect(pushStorefrontEnvVariables).toHaveBeenCalledWith(
370
+ { storeFqdn: "my-shop", token: "abc123" },
371
+ "gid://shopify/HydrogenStorefront/2",
372
+ "gid://shopify/HydrogenStorefrontEnvironment/2",
373
+ [
374
+ { key: "EXISTING_TOKEN", value: "1" },
375
+ { key: "SECOND_TOKEN", value: "NEW_VALUE" }
376
+ ]
377
+ );
378
+ expect(outputMock.info()).toMatch(
379
+ /Environment variables push to Preview was successful/
380
+ );
381
+ });
382
+ });
383
+ });
@@ -18,8 +18,12 @@ class GenerateRoute extends Command {
18
18
  description: "Generate TypeScript files",
19
19
  env: "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT"
20
20
  }),
21
- force: commonFlags.force,
22
- path: commonFlags.path
21
+ "locale-param": Flags.string({
22
+ description: "The param name in Remix routes for the i18n locale, if any. Example: `locale` becomes ($locale).",
23
+ env: "SHOPIFY_HYDROGEN_FLAG_ADAPTER"
24
+ }),
25
+ ...commonFlags.force,
26
+ ...commonFlags.path
23
27
  };
24
28
  static hidden;
25
29
  static args = {
@@ -40,7 +44,8 @@ class GenerateRoute extends Command {
40
44
  await runGenerate({
41
45
  ...flags,
42
46
  directory,
43
- routeName
47
+ routeName,
48
+ localePrefix: flags["locale-param"]
44
49
  });
45
50
  }
46
51
  }
@@ -0,0 +1,69 @@
1
+ import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+
4
+ declare const GENERATOR_SETUP_ASSETS_SUB_DIRS: readonly ["tailwind", "css-modules", "vanilla-extract", "postcss", "vite"];
5
+ type AssetDir = (typeof GENERATOR_SETUP_ASSETS_SUB_DIRS)[number];
6
+
7
+ type CssStrategy = Exclude<AssetDir, 'vite'>;
8
+
9
+ declare const I18N_CHOICES: readonly ["subfolders", "domains", "subdomains", "none"];
10
+ type I18nChoice = (typeof I18N_CHOICES)[number];
11
+
12
+ declare const STYLING_CHOICES: readonly [...CssStrategy[], "none"];
13
+ type StylingChoice = (typeof STYLING_CHOICES)[number];
14
+
15
+ type InitOptions = {
16
+ path?: string;
17
+ template?: string;
18
+ language?: Language;
19
+ mockShop?: boolean;
20
+ styling?: StylingChoice;
21
+ i18n?: I18nChoice;
22
+ token?: string;
23
+ force?: boolean;
24
+ routes?: boolean;
25
+ shortcut?: boolean;
26
+ installDeps?: boolean;
27
+ git?: boolean;
28
+ };
29
+ declare const LANGUAGES: {
30
+ readonly js: "JavaScript";
31
+ readonly ts: "TypeScript";
32
+ };
33
+ type Language = keyof typeof LANGUAGES;
34
+
35
+ declare class Init extends Command {
36
+ static description: string;
37
+ static flags: {
38
+ routes: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
39
+ git: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
40
+ shortcut: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
41
+ markets: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
42
+ styling: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
43
+ 'mock-shop': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
44
+ 'install-deps': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
45
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
46
+ language: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
47
+ template: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
48
+ force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
49
+ };
50
+ run(): Promise<void>;
51
+ }
52
+ declare function runInit(options?: InitOptions): Promise<{
53
+ language?: "js" | "ts" | undefined;
54
+ packageManager: "npm" | "pnpm" | "yarn" | "bun" | "unknown";
55
+ cssStrategy?: CssStrategy | undefined;
56
+ cliCommand: "h2" | "pnpm shopify hydrogen" | "yarn shopify hydrogen" | "bun shopify hydrogen" | "npx shopify hydrogen";
57
+ depsInstalled: boolean;
58
+ depsError?: Error | undefined;
59
+ i18n?: "subfolders" | "domains" | "subdomains" | undefined;
60
+ i18nError?: Error | undefined;
61
+ routes?: Record<string, string | string[]> | undefined;
62
+ routesError?: Error | undefined;
63
+ location: string;
64
+ name: string;
65
+ directory: string;
66
+ storefrontTitle?: string | undefined;
67
+ } | undefined>;
68
+
69
+ export { Init as default, runInit };
@@ -16,7 +16,7 @@ const FLAG_MAP = { f: "force" };
16
16
  class Init extends Command {
17
17
  static description = "Creates a new Hydrogen storefront.";
18
18
  static flags = {
19
- force: commonFlags.force,
19
+ ...commonFlags.force,
20
20
  path: Flags.string({
21
21
  description: "The path to the directory of the new Hydrogen storefront.",
22
22
  env: "SHOPIFY_HYDROGEN_FLAG_PATH"
@@ -30,15 +30,15 @@ class Init extends Command {
30
30
  description: "Scaffolds project based on an existing template or example from the Hydrogen repository.",
31
31
  env: "SHOPIFY_HYDROGEN_FLAG_TEMPLATE"
32
32
  }),
33
- "install-deps": commonFlags.installDeps,
33
+ ...commonFlags.installDeps,
34
34
  "mock-shop": Flags.boolean({
35
35
  description: "Use mock.shop as the data source for the storefront.",
36
36
  default: false,
37
37
  env: "SHOPIFY_HYDROGEN_FLAG_MOCK_DATA"
38
38
  }),
39
- styling: commonFlags.styling,
40
- markets: commonFlags.markets,
41
- shortcut: commonFlags.shortcut,
39
+ ...commonFlags.styling,
40
+ ...commonFlags.markets,
41
+ ...commonFlags.shortcut,
42
42
  routes: Flags.boolean({
43
43
  description: "Generate routes for all pages.",
44
44
  env: "SHOPIFY_HYDROGEN_FLAG_ROUTES",