@shopify/cli-hydrogen 7.0.0 → 7.1.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 (40) hide show
  1. package/dist/commands/hydrogen/build.js +3 -8
  2. package/dist/commands/hydrogen/deploy.js +79 -32
  3. package/dist/commands/hydrogen/deploy.test.js +162 -5
  4. package/dist/commands/hydrogen/generate/route.js +6 -1
  5. package/dist/commands/hydrogen/init.test.js +2 -1
  6. package/dist/commands/hydrogen/setup.js +17 -19
  7. package/dist/generator-templates/starter/CHANGELOG.md +45 -0
  8. package/dist/generator-templates/starter/README.md +25 -2
  9. package/dist/generator-templates/starter/app/components/Cart.tsx +2 -2
  10. package/dist/generator-templates/starter/app/components/Layout.tsx +9 -1
  11. package/dist/generator-templates/starter/app/components/Search.tsx +44 -15
  12. package/dist/generator-templates/starter/app/lib/search.ts +29 -0
  13. package/dist/generator-templates/starter/app/root.tsx +0 -2
  14. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -2
  15. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +1 -21
  16. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -5
  17. package/dist/generator-templates/starter/app/routes/search.tsx +8 -2
  18. package/dist/generator-templates/starter/app/styles/app.css +10 -15
  19. package/dist/generator-templates/starter/package.json +7 -7
  20. package/dist/generator-templates/starter/remix.config.js +1 -0
  21. package/dist/generator-templates/starter/remix.env.d.ts +6 -2
  22. package/dist/generator-templates/starter/server.ts +1 -0
  23. package/dist/hooks/init.js +3 -3
  24. package/dist/lib/codegen.js +1 -8
  25. package/dist/lib/file.js +4 -1
  26. package/dist/lib/flags.js +6 -0
  27. package/dist/lib/graphiql-url.js +3 -0
  28. package/dist/lib/mini-oxygen/assets.js +17 -1
  29. package/dist/lib/onboarding/common.js +4 -3
  30. package/dist/lib/onboarding/local.js +7 -7
  31. package/dist/lib/onboarding/remote.js +2 -1
  32. package/dist/lib/setups/i18n/replacers.test.js +3 -2
  33. package/dist/lib/setups/routes/generate.js +58 -10
  34. package/dist/lib/setups/routes/templates/locale-check.js +9 -0
  35. package/dist/lib/setups/routes/templates/locale-check.ts +16 -0
  36. package/dist/lib/shell.js +1 -1
  37. package/dist/lib/template-diff.js +13 -3
  38. package/dist/virtual-routes/components/RequestDetails.jsx +2 -2
  39. package/oclif.manifest.json +47 -8
  40. package/package.json +5 -5
@@ -22,22 +22,17 @@ class Build extends Command {
22
22
  static flags = {
23
23
  path: commonFlags.path,
24
24
  sourcemap: Flags.boolean({
25
- description: "Generate sourcemaps for the build.",
25
+ description: "Controls whether sourcemaps are generated. Default to true, use `--no-sourcemaps` to disable.",
26
26
  env: "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP",
27
27
  allowNo: true,
28
28
  default: true
29
29
  }),
30
30
  "bundle-stats": Flags.boolean({
31
- description: "Show a bundle size summary after building.",
32
- default: true,
33
- allowNo: true
34
- }),
35
- "lockfile-check": Flags.boolean({
36
- description: "Checks that there is exactly 1 valid lockfile in the project.",
37
- env: "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK",
31
+ description: "Show a bundle size summary after building. Defaults to true, use `--no-bundle-stats` to disable.",
38
32
  default: true,
39
33
  allowNo: true
40
34
  }),
35
+ "lockfile-check": commonFlags.lockfileCheck,
41
36
  "disable-route-warning": Flags.boolean({
42
37
  description: "Disable warning about missing standard routes.",
43
38
  env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_ROUTE_WARNING"
@@ -9,10 +9,12 @@ import { resolvePath } from '@shopify/cli-kit/node/path';
9
9
  import { renderFatalError, renderWarning, renderSelectPrompt, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
10
10
  import { ciPlatform } from '@shopify/cli-kit/node/context/local';
11
11
  import { parseToken, createDeploy } from '@shopify/oxygen-cli/deploy';
12
+ import { loadEnvironmentVariableFile } from '@shopify/oxygen-cli/utils';
12
13
  import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
13
14
  import { getOxygenDeploymentData } from '../../lib/get-oxygen-deployment-data.js';
14
15
  import { runBuild } from './build.js';
15
16
 
17
+ const DEPLOY_OUTPUT_FILE_HANDLE = "h2_deploy_log.json";
16
18
  const deploymentLogger = (message, level = "info") => {
17
19
  if (level === "error" || level === "warn") {
18
20
  outputWarn(message);
@@ -25,6 +27,10 @@ class Deploy extends Command {
25
27
  description: "Environment branch (tag) for environment to deploy to.",
26
28
  required: false
27
29
  }),
30
+ "env-file": Flags.string({
31
+ description: "Path to an environment file to override existing environment variables for the deployment.",
32
+ required: false
33
+ }),
28
34
  preview: Flags.boolean({
29
35
  description: "Deploys to the Preview environment. Overrides --env-branch and Git metadata.",
30
36
  required: false,
@@ -37,17 +43,28 @@ class Deploy extends Command {
37
43
  env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
38
44
  required: false
39
45
  }),
46
+ "no-verify": Flags.boolean({
47
+ description: "Skip the routability verification step after deployment.",
48
+ default: false,
49
+ required: false
50
+ }),
40
51
  "auth-bypass-token": Flags.boolean({
41
52
  description: "Generate an authentication bypass token, which can be used to perform end-to-end tests against the deployment.",
42
53
  required: false,
43
54
  default: false
44
55
  }),
56
+ "build-command": Flags.string({
57
+ description: "Specify a build command to run before deploying. If not specified, `shopify hydrogen build` will be used.",
58
+ required: false
59
+ }),
60
+ "lockfile-check": commonFlags.lockfileCheck,
45
61
  path: commonFlags.path,
46
62
  shop: commonFlags.shop,
47
- "no-json-output": Flags.boolean({
48
- description: "Prevents the command from creating a JSON file containing the deployment URL in CI environments.",
63
+ "json-output": Flags.boolean({
64
+ allowNo: true,
65
+ description: "Create a JSON file containing the deployment details in CI environments. Defaults to true, use `--no-json-output` to disable.",
49
66
  required: false,
50
- default: false
67
+ default: true
51
68
  }),
52
69
  token: Flags.string({
53
70
  char: "t",
@@ -92,6 +109,7 @@ class Deploy extends Command {
92
109
  ...camelFlags,
93
110
  defaultEnvironment: flags.preview,
94
111
  environmentTag: flags["env-branch"],
112
+ environmentFile: flags["env-file"],
95
113
  path: flags.path ? resolvePath(flags.path) : process.cwd()
96
114
  };
97
115
  }
@@ -99,10 +117,10 @@ class Deploy extends Command {
99
117
  function createUnexpectedAbortError(message) {
100
118
  return new AbortError(
101
119
  message || "The deployment failed due to an unexpected error.",
102
- "Retrying the deployement may succeed.",
120
+ "Retrying the deployment may succeed.",
103
121
  [
104
122
  [
105
- "If the issue persits, please check the",
123
+ "If the issue persists, please check the",
106
124
  {
107
125
  link: {
108
126
  label: "Shopify status page",
@@ -117,10 +135,14 @@ function createUnexpectedAbortError(message) {
117
135
  async function oxygenDeploy(options) {
118
136
  const {
119
137
  authBypassToken: generateAuthBypassToken,
138
+ buildCommand,
120
139
  defaultEnvironment,
121
140
  environmentTag,
141
+ environmentFile,
122
142
  force: forceOnUncommitedChanges,
123
- noJsonOutput,
143
+ noVerify,
144
+ lockfileCheck,
145
+ jsonOutput,
124
146
  path,
125
147
  shop,
126
148
  metadataUrl,
@@ -173,6 +195,16 @@ async function oxygenDeploy(options) {
173
195
  });
174
196
  metadataDescription = `${commitHash} with additional changes`;
175
197
  }
198
+ let overriddenEnvironmentVariables;
199
+ if (environmentFile) {
200
+ try {
201
+ overriddenEnvironmentVariables = loadEnvironmentVariableFile(environmentFile);
202
+ } catch (error) {
203
+ throw new AbortError(
204
+ `Could not load environment file at ${environmentFile}`
205
+ );
206
+ }
207
+ }
176
208
  if (!isCI) {
177
209
  deploymentData = await getOxygenDeploymentData({
178
210
  root: path,
@@ -244,11 +276,12 @@ async function oxygenDeploy(options) {
244
276
  ...metadataUser ? { user: metadataUser } : {},
245
277
  ...metadataVersion ? { version: metadataVersion } : {}
246
278
  },
247
- skipVerification: false,
279
+ skipVerification: noVerify,
248
280
  rootPath: path,
249
281
  skipBuild: false,
250
282
  workerOnly: false,
251
- workerDir: "dist/worker"
283
+ workerDir: "dist/worker",
284
+ overriddenEnvironmentVariables
252
285
  };
253
286
  let resolveUpload;
254
287
  const uploadPromise = new Promise((resolve) => {
@@ -272,17 +305,6 @@ async function oxygenDeploy(options) {
272
305
  rejectDeploy = reject;
273
306
  });
274
307
  const hooks = {
275
- buildFunction: async (assetPath) => {
276
- outputInfo(
277
- outputContent`${colors.whiteBright("Building project...")}`.value
278
- );
279
- await runBuild({
280
- directory: path,
281
- assetPath,
282
- sourcemap: true,
283
- useCodegen: false
284
- });
285
- },
286
308
  onDeploymentCompleted: () => resolveDeploymentCompletedVerification(),
287
309
  onVerificationComplete: () => resolveRoutableCheck(),
288
310
  onDeploymentCompletedVerificationError() {
@@ -309,6 +331,22 @@ async function oxygenDeploy(options) {
309
331
  );
310
332
  }
311
333
  };
334
+ if (buildCommand) {
335
+ config.buildCommand = buildCommand;
336
+ } else {
337
+ hooks.buildFunction = async (assetPath) => {
338
+ outputInfo(
339
+ outputContent`${colors.whiteBright("Building project...")}`.value
340
+ );
341
+ await runBuild({
342
+ directory: path,
343
+ assetPath,
344
+ lockfileCheck,
345
+ sourcemap: true,
346
+ useCodegen: false
347
+ });
348
+ };
349
+ }
312
350
  const uploadStart = async () => {
313
351
  outputInfo(
314
352
  outputContent`${colors.whiteBright("Deploying to Oxygen..\n")}`.value
@@ -324,7 +362,8 @@ async function oxygenDeploy(options) {
324
362
  },
325
363
  {
326
364
  title: "Verifying deployment is routable",
327
- task: async () => await routableCheckPromise
365
+ task: async () => await routableCheckPromise,
366
+ skip: () => noVerify
328
367
  }
329
368
  ]);
330
369
  };
@@ -333,27 +372,35 @@ async function oxygenDeploy(options) {
333
372
  rejectDeploy(createUnexpectedAbortError());
334
373
  return;
335
374
  }
336
- const nextSteps = [
337
- [
375
+ const nextSteps = [];
376
+ if (isCI) {
377
+ if (jsonOutput) {
378
+ nextSteps.push([
379
+ "View the deployment information in",
380
+ { subdued: DEPLOY_OUTPUT_FILE_HANDLE }
381
+ ]);
382
+ }
383
+ } else {
384
+ nextSteps.push([
338
385
  "Open",
339
386
  { link: { url: completedDeployment.url } },
340
- `in your browser to view your deployment.`
341
- ]
342
- ];
343
- if (completedDeployment?.authBypassToken) {
344
- nextSteps.push([
345
- "Use the",
346
- { subdued: completedDeployment.authBypassToken },
347
- "token to perform end-to-end tests against the deployment."
387
+ "in your browser to view your deployment."
348
388
  ]);
389
+ if (completedDeployment?.authBypassToken) {
390
+ nextSteps.push([
391
+ "Use the",
392
+ { subdued: completedDeployment.authBypassToken },
393
+ "token to perform end-to-end tests against the deployment."
394
+ ]);
395
+ }
349
396
  }
350
397
  renderSuccess({
351
398
  body: ["Successfully deployed to Oxygen"],
352
399
  nextSteps
353
400
  });
354
- if (isCI && !noJsonOutput) {
401
+ if (isCI && jsonOutput) {
355
402
  await writeFile(
356
- "h2_deploy_log.json",
403
+ DEPLOY_OUTPUT_FILE_HANDLE,
357
404
  JSON.stringify(completedDeployment)
358
405
  );
359
406
  }
@@ -8,12 +8,16 @@ import { ensureIsClean, GitDirectoryNotCleanError, getLatestGitCommit } from '@s
8
8
  import { oxygenDeploy, deploymentLogger } from './deploy.js';
9
9
  import { getOxygenDeploymentData } from '../../lib/get-oxygen-deployment-data.js';
10
10
  import { createDeploy, parseToken } from '@shopify/oxygen-cli/deploy';
11
+ import { loadEnvironmentVariableFile } from '@shopify/oxygen-cli/utils';
11
12
  import { ciPlatform } from '@shopify/cli-kit/node/context/local';
13
+ import { runBuild } from './build.js';
12
14
 
13
- vi.mock("../../lib/get-oxygen-deployment-data.js");
14
15
  vi.mock("@shopify/oxygen-cli/deploy");
16
+ vi.mock("@shopify/oxygen-cli/utils");
15
17
  vi.mock("@shopify/cli-kit/node/fs");
16
18
  vi.mock("@shopify/cli-kit/node/context/local");
19
+ vi.mock("../../lib/get-oxygen-deployment-data.js");
20
+ vi.mock("./build.js");
17
21
  vi.mock("../../lib/auth.js");
18
22
  vi.mock("../../lib/shopify-config.js");
19
23
  vi.mock("../../lib/graphql/admin/link-storefront.js");
@@ -69,7 +73,9 @@ describe("deploy", () => {
69
73
  authBypassToken: true,
70
74
  defaultEnvironment: false,
71
75
  force: false,
72
- noJsonOutput: false,
76
+ noVerify: true,
77
+ lockfileCheck: false,
78
+ jsonOutput: true,
73
79
  path: "./",
74
80
  shop: "snowdevil.myshopify.com",
75
81
  metadataUrl: "https://example.com",
@@ -98,7 +104,7 @@ describe("deploy", () => {
98
104
  user: deployParams.metadataUser,
99
105
  version: deployParams.metadataVersion
100
106
  },
101
- skipVerification: false,
107
+ skipVerification: true,
102
108
  rootPath: deployParams.path,
103
109
  skipBuild: false,
104
110
  workerOnly: false,
@@ -161,6 +167,46 @@ describe("deploy", () => {
161
167
  });
162
168
  expect(vi.mocked(renderSuccess)).toHaveBeenCalled;
163
169
  });
170
+ it("calls createDeploy with overridden variables in environment file", async () => {
171
+ const overriddenEnvironmentVariables = [
172
+ {
173
+ key: "fake-key",
174
+ value: "fake-value",
175
+ isSecret: true
176
+ }
177
+ ];
178
+ vi.mocked(loadEnvironmentVariableFile).mockReturnValue(
179
+ overriddenEnvironmentVariables
180
+ );
181
+ await oxygenDeploy({
182
+ ...deployParams,
183
+ environmentFile: "fake-env-file"
184
+ });
185
+ expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
186
+ config: {
187
+ ...expectedConfig,
188
+ overriddenEnvironmentVariables
189
+ },
190
+ hooks: expectedHooks,
191
+ logger: deploymentLogger
192
+ });
193
+ });
194
+ it("errors when supplied environment file does not exist", async () => {
195
+ vi.mocked(loadEnvironmentVariableFile).mockImplementation(
196
+ (_path) => {
197
+ throw new AbortError("File not found");
198
+ }
199
+ );
200
+ try {
201
+ await oxygenDeploy({
202
+ ...deployParams,
203
+ environmentFile: "fake-env-file"
204
+ });
205
+ expect(true).toBe(false);
206
+ } catch (err) {
207
+ expect(err).toBeInstanceOf(AbortError);
208
+ }
209
+ });
164
210
  it("errors when there are uncommited changes", async () => {
165
211
  vi.mocked(ensureIsClean).mockRejectedValue(
166
212
  new GitDirectoryNotCleanError("Uncommitted changes")
@@ -304,6 +350,42 @@ describe("deploy", () => {
304
350
  });
305
351
  });
306
352
  });
353
+ it("passes the lockfileCheck to the build function when the flag is set", async () => {
354
+ const params = {
355
+ ...deployParams,
356
+ lockfileCheck: false
357
+ };
358
+ vi.mocked(createDeploy).mockImplementationOnce((options) => {
359
+ options.hooks?.buildFunction?.("some-cool-asset-path");
360
+ return new Promise((resolve, _reject) => {
361
+ resolve({ url: "https://a-lovely-deployment.com" });
362
+ });
363
+ });
364
+ await oxygenDeploy(params);
365
+ expect(vi.mocked(runBuild)).toHaveBeenCalledWith({
366
+ assetPath: "some-cool-asset-path",
367
+ directory: params.path,
368
+ lockfileCheck: false,
369
+ sourcemap: true,
370
+ useCodegen: false
371
+ });
372
+ });
373
+ it("passes a build command to createDeploy when the build-command flag is used", async () => {
374
+ const params = {
375
+ ...deployParams,
376
+ buildCommand: "hocus pocus"
377
+ };
378
+ const { buildFunction: _, ...hooks } = expectedHooks;
379
+ await oxygenDeploy(params);
380
+ expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
381
+ config: {
382
+ ...expectedConfig,
383
+ buildCommand: "hocus pocus"
384
+ },
385
+ hooks,
386
+ logger: deploymentLogger
387
+ });
388
+ });
307
389
  it("writes a file with JSON content in CI environments", async () => {
308
390
  vi.mocked(ciPlatform).mockReturnValue({
309
391
  isCI: true,
@@ -325,7 +407,7 @@ describe("deploy", () => {
325
407
  })
326
408
  );
327
409
  vi.mocked(writeFile).mockClear();
328
- ciDeployParams.noJsonOutput = true;
410
+ ciDeployParams.jsonOutput = false;
329
411
  await oxygenDeploy(ciDeployParams);
330
412
  expect(vi.mocked(writeFile)).not.toHaveBeenCalled();
331
413
  });
@@ -401,10 +483,85 @@ describe("deploy", () => {
401
483
  } catch (err) {
402
484
  if (err instanceof AbortError) {
403
485
  expect(err.message).toBe("oh shit");
404
- expect(err.tryMessage).toBe("Retrying the deployement may succeed.");
486
+ expect(err.tryMessage).toBe("Retrying the deployment may succeed.");
405
487
  } else {
406
488
  expect(true).toBe(false);
407
489
  }
408
490
  }
409
491
  });
492
+ describe("next steps", () => {
493
+ it("renders a link to the deployment", async () => {
494
+ vi.mocked(createDeploy).mockResolvedValue({
495
+ url: "https://a-lovely-deployment.com"
496
+ });
497
+ await oxygenDeploy(deployParams);
498
+ expect(vi.mocked(renderSuccess)).toHaveBeenCalledWith({
499
+ body: ["Successfully deployed to Oxygen"],
500
+ nextSteps: [
501
+ [
502
+ "Open",
503
+ { link: { url: "https://a-lovely-deployment.com" } },
504
+ "in your browser to view your deployment."
505
+ ]
506
+ ]
507
+ });
508
+ });
509
+ it("renders a link to the deployment and shows auth bypass token when one is created", async () => {
510
+ vi.mocked(createDeploy).mockResolvedValue({
511
+ url: "https://a-lovely-deployment.com",
512
+ authBypassToken: "some-token"
513
+ });
514
+ await oxygenDeploy(deployParams);
515
+ expect(vi.mocked(renderSuccess)).toHaveBeenCalledWith({
516
+ body: ["Successfully deployed to Oxygen"],
517
+ nextSteps: [
518
+ [
519
+ "Open",
520
+ { link: { url: "https://a-lovely-deployment.com" } },
521
+ "in your browser to view your deployment."
522
+ ],
523
+ [
524
+ "Use the",
525
+ { subdued: "some-token" },
526
+ "token to perform end-to-end tests against the deployment."
527
+ ]
528
+ ]
529
+ });
530
+ });
531
+ describe("when in a CI environment", () => {
532
+ it("renders information about h2_deploy_log.json", async () => {
533
+ vi.mocked(ciPlatform).mockReturnValue({
534
+ isCI: true,
535
+ name: "github",
536
+ metadata: {}
537
+ });
538
+ await oxygenDeploy({ ...deployParams, token: "fake-token" });
539
+ expect(vi.mocked(renderSuccess)).toHaveBeenCalledWith({
540
+ body: ["Successfully deployed to Oxygen"],
541
+ nextSteps: [
542
+ [
543
+ "View the deployment information in",
544
+ { subdued: "h2_deploy_log.json" }
545
+ ]
546
+ ]
547
+ });
548
+ });
549
+ it("renders no next steps if jsonOutput is set to false", async () => {
550
+ vi.mocked(ciPlatform).mockReturnValue({
551
+ isCI: true,
552
+ name: "github",
553
+ metadata: {}
554
+ });
555
+ await oxygenDeploy({
556
+ ...deployParams,
557
+ token: "fake-token",
558
+ jsonOutput: false
559
+ });
560
+ expect(vi.mocked(renderSuccess)).toHaveBeenCalledWith({
561
+ body: ["Successfully deployed to Oxygen"],
562
+ nextSteps: []
563
+ });
564
+ });
565
+ });
566
+ });
410
567
  });
@@ -18,6 +18,10 @@ class GenerateRoute extends Command {
18
18
  description: "Generate TypeScript files",
19
19
  env: "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT"
20
20
  }),
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
+ }),
21
25
  force: commonFlags.force,
22
26
  path: commonFlags.path
23
27
  };
@@ -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
  }
@@ -171,8 +171,8 @@ describe("init", () => {
171
171
  const examplePath = templatePath.replace("templates", "examples").replace("skeleton", exampleName);
172
172
  const ignore = ["**/node_modules/**", "**/dist/**"];
173
173
  const resultFiles = await glob("**/*", { ignore, cwd: tmpDir });
174
- const templateFiles = await glob("**/*", { ignore, cwd: templatePath });
175
174
  const exampleFiles = await glob("**/*", { ignore, cwd: examplePath });
175
+ const templateFiles = (await glob("**/*", { ignore, cwd: templatePath })).filter((item) => !item.endsWith("CHANGELOG.md"));
176
176
  expect(resultFiles).toEqual(
177
177
  expect.arrayContaining([
178
178
  .../* @__PURE__ */ new Set([...templateFiles, ...exampleFiles])
@@ -462,6 +462,7 @@ describe("init", () => {
462
462
  });
463
463
  const resultFiles = await glob("**/*", { cwd: tmpDir });
464
464
  expect(resultFiles).toContain("app/routes/($locale)._index.tsx");
465
+ expect(resultFiles).toContain("app/routes/($locale).tsx");
465
466
  const serverFile = await readFile(`${tmpDir}/server.ts`);
466
467
  expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
467
468
  expect(serverFile).toMatch(/url.pathname/);
@@ -7,7 +7,7 @@ import { commonFlags, overrideFlag, flagsToCamelObject } from '../../lib/flags.j
7
7
  import { renderI18nPrompt, setupI18nStrategy } from '../../lib/setups/i18n/index.js';
8
8
  import { getRemixConfig } from '../../lib/remix-config.js';
9
9
  import { handleRouteGeneration, generateProjectEntries, handleCliShortcut, renderProjectReady } from '../../lib/onboarding/common.js';
10
- import { getCliCommand } from '../../lib/shell.js';
10
+ import { getCliCommand, ALIAS_NAME } from '../../lib/shell.js';
11
11
  import { getTemplateAppFile } from '../../lib/build.js';
12
12
 
13
13
  class Setup extends Command {
@@ -89,24 +89,22 @@ async function runSetup(options) {
89
89
  () => setupI18nStrategy(i18n, remixConfig)
90
90
  );
91
91
  }
92
- let hasCreatedShortcut = false;
93
- const cliCommand = await cliCommandPromise;
94
- const needsAlias = cliCommand !== "h2";
95
- if (needsAlias) {
96
- const { createShortcut, showShortcutBanner } = await handleCliShortcut(
97
- controller,
98
- await cliCommandPromise,
99
- options.shortcut
100
- );
101
- if (createShortcut) {
102
- backgroundWorkPromise = backgroundWorkPromise.then(async () => {
103
- hasCreatedShortcut = await createShortcut();
104
- });
105
- showShortcutBanner();
106
- }
107
- }
108
- if (!i18n && !needsRouteGeneration && !needsAlias)
92
+ let cliCommand = await Promise.resolve(cliCommandPromise);
93
+ const { createShortcut, showShortcutBanner } = await handleCliShortcut(
94
+ controller,
95
+ cliCommand,
96
+ options.shortcut
97
+ );
98
+ if (!i18n && !needsRouteGeneration && !createShortcut)
109
99
  return;
100
+ if (createShortcut) {
101
+ backgroundWorkPromise = backgroundWorkPromise.then(async () => {
102
+ if (await createShortcut()) {
103
+ cliCommand = ALIAS_NAME;
104
+ }
105
+ });
106
+ showShortcutBanner();
107
+ }
110
108
  await renderTasks(tasks);
111
109
  await renderProjectReady(
112
110
  {
@@ -115,7 +113,7 @@ async function runSetup(options) {
115
113
  directory: remixConfig.rootDirectory
116
114
  },
117
115
  {
118
- hasCreatedShortcut,
116
+ cliCommand,
119
117
  depsInstalled: true,
120
118
  packageManager: "npm",
121
119
  i18n,
@@ -1,5 +1,50 @@
1
1
  # skeleton
2
2
 
3
+ ## 1.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - This is an important fix to a bug with 404 routes and path-based i18n projects where some unknown routes would not properly render a 404. This fixes all new projects, but to fix existing projects, add a `($locale).tsx` route with the following contents: ([#1732](https://github.com/Shopify/hydrogen/pull/1732)) by [@blittle](https://github.com/blittle)
8
+
9
+ ```ts
10
+ import {type LoaderFunctionArgs} from '@remix-run/server-runtime';
11
+
12
+ export async function loader({params, context}: LoaderFunctionArgs) {
13
+ const {language, country} = context.storefront.i18n;
14
+
15
+ if (
16
+ params.locale &&
17
+ params.locale.toLowerCase() !== `${language}-${country}`.toLowerCase()
18
+ ) {
19
+ // If the locale URL param is defined, yet we still are still at the default locale
20
+ // then the the locale param must be invalid, send to the 404 page
21
+ throw new Response(null, {status: 404});
22
+ }
23
+
24
+ return null;
25
+ }
26
+ ```
27
+
28
+ - Add defensive null checks to the default cart implementation in the starter template ([#1746](https://github.com/Shopify/hydrogen/pull/1746)) by [@blittle](https://github.com/blittle)
29
+
30
+ - 🐛 Fix issue where customer login does not persist to checkout ([#1719](https://github.com/Shopify/hydrogen/pull/1719)) by [@michenly](https://github.com/michenly)
31
+
32
+ ✨ Add `customerAccount` option to `createCartHandler`. Where a `?logged_in=true` will be added to the checkoutUrl for cart query if a customer is logged in.
33
+
34
+ - Updated dependencies [[`faeba9f8`](https://github.com/Shopify/hydrogen/commit/faeba9f8947d6b9420b33274a0f39b62418ff2e5), [`6d585026`](https://github.com/Shopify/hydrogen/commit/6d585026623204e99d54a5f2efa3d1c74f690bb6), [`fcecfb23`](https://github.com/Shopify/hydrogen/commit/fcecfb2307210b9d73a7cc90ba865508937217ba), [`28864d6f`](https://github.com/Shopify/hydrogen/commit/28864d6ffbb19b62a5fb8f4c9bbe27568de62411), [`c0ec7714`](https://github.com/Shopify/hydrogen/commit/c0ec77141fb1d7a713d91219b8777bc541780ae8), [`226cf478`](https://github.com/Shopify/hydrogen/commit/226cf478a5bdef1cca33fe8f69832ae0e557d9d9), [`06d9fd91`](https://github.com/Shopify/hydrogen/commit/06d9fd91140bd52a8ee41a20bc114ce2e7fb67dc)]:
35
+ - @shopify/cli-hydrogen@7.1.0
36
+ - @shopify/hydrogen@2024.1.2
37
+
38
+ ## 1.0.3
39
+
40
+ ### Patch Changes
41
+
42
+ - ♻️ `CustomerClient` type is deprecated and replaced by `CustomerAccount` ([#1692](https://github.com/Shopify/hydrogen/pull/1692)) by [@michenly](https://github.com/michenly)
43
+
44
+ - Updated dependencies [[`02798786`](https://github.com/Shopify/hydrogen/commit/02798786bf8ae5c53f6430723a86d62b8e94d120), [`52b15df4`](https://github.com/Shopify/hydrogen/commit/52b15df457ce723bbc83ad594ded73a7b06447d6), [`a2664362`](https://github.com/Shopify/hydrogen/commit/a2664362a7d89b34835553a9b0eb7af55ca70ae4), [`eee5d927`](https://github.com/Shopify/hydrogen/commit/eee5d9274b72404dfb0ffef30d5503fd553be5fe), [`c7b2017f`](https://github.com/Shopify/hydrogen/commit/c7b2017f11a2cb4d280dfd8f170e65a908b9ea02), [`06320ee4`](https://github.com/Shopify/hydrogen/commit/06320ee48b94dbfece945461031a252f454fd0a3)]:
45
+ - @shopify/hydrogen@2024.1.1
46
+ - @shopify/cli-hydrogen@7.0.1
47
+
3
48
  ## 1.0.2
4
49
 
5
50
  ### Patch Changes
@@ -41,12 +41,18 @@ npm run dev
41
41
 
42
42
  ## Setup for using Customer Account API (`/account` section)
43
43
 
44
+ ### Enabled new Customer Account Experience
45
+
46
+ 1. Go to your Shopify admin => Settings => Customer accounts => New customer account
47
+
44
48
  ### Setup public domain using ngrok
45
49
 
46
50
  1. Setup a [ngrok](https://ngrok.com/) account and add a permanent domain (ie. `https://<your-ngrok-domain>.app`).
47
51
  1. Install the [ngrok CLI](https://ngrok.com/download) to use in terminal
48
52
  1. Start ngrok using `ngrok http --domain=<your-ngrok-domain>.app 3000`
49
53
 
54
+ > [!IMPORTANT]
55
+ > To successfully interact with the Customer Account API routes you will need to use the ngrok domain during development instead of localhost
50
56
  ### Include public domain in Customer Account API settings
51
57
 
52
58
  1. Go to your Shopify admin => `Hydrogen` or `Headless` app/channel => Customer Account API => Application setup
@@ -54,10 +60,27 @@ npm run dev
54
60
  1. Edit `Javascript origin(s)` to include your public domain `https://<your-ngrok-domain>.app` or keep it blank
55
61
  1. Edit `Logout URI` to include your public domain `https://<your-ngrok-domain>.app` or keep it blank
56
62
 
63
+ ### Add the ngrok domain to the CSP policy
64
+
65
+ Modify your `/app/entry.server.tsx` to allow the ngrok domain as a connect-src
66
+
67
+ ```diff
68
+ - const {nonce, header, NonceProvider} = createContentSecurityPolicy()
69
+ + const {nonce, header, NonceProvider} = createContentSecurityPolicy({
70
+ + connectSrc: [
71
+ + 'wss://<your-ngrok-domain>.app:*', // Your ngrok websocket domain
72
+ + ],
73
+ + });
74
+ ```
75
+
57
76
  ### Prepare Environment variables
58
77
 
59
78
  Run [`npx shopify hydrogen link`](https://shopify.dev/docs/custom-storefronts/hydrogen/cli#link) or [`npx shopify hydrogen env pull`](https://shopify.dev/docs/custom-storefronts/hydrogen/cli#env-pull) to link this app to your own test shop.
60
79
 
61
- Alternatly, the values of the required environment varaibles "PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID" and "PUBLIC_CUSTOMER_ACCOUNT_API_URL" can be found in customer account api settings in the Hydrogen admin channel.
80
+ Alternately, the values of the required environment variables "PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID" and "PUBLIC_CUSTOMER_ACCOUNT_API_URL" can be found in customer account api settings in the Hydrogen admin channel.
81
+
82
+ > [!IMPORTANT]
83
+ > Note that `mock.shop` doesn't supply these variables automatically and your own test shop is required for using Customer Account API
62
84
 
63
- 🗒️ Note that mock.shop doesn't supply these variables automatically.
85
+ > [!NOTE]
86
+ > B2B features such as contextual pricing is not available in SF API with Customer Account API login. If you require this feature, we suggest using the [legacy-customer-account-flow](https://github.com/Shopify/hydrogen/tree/main/examples/legacy-customer-account-flow). This feature should be available in the Customer Account API in the 2024-04 release.