@shopify/cli-hydrogen 4.0.9 → 4.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 (74) hide show
  1. package/dist/commands/hydrogen/build.d.ts +4 -4
  2. package/dist/commands/hydrogen/build.js +17 -16
  3. package/dist/commands/hydrogen/check.d.ts +3 -6
  4. package/dist/commands/hydrogen/check.js +10 -9
  5. package/dist/commands/hydrogen/dev.d.ts +3 -3
  6. package/dist/commands/hydrogen/dev.js +22 -21
  7. package/dist/commands/hydrogen/g.d.ts +10 -0
  8. package/dist/commands/hydrogen/g.js +17 -0
  9. package/dist/commands/hydrogen/generate/route.d.ts +7 -9
  10. package/dist/commands/hydrogen/generate/route.js +49 -47
  11. package/dist/commands/hydrogen/generate/route.test.js +48 -40
  12. package/dist/commands/hydrogen/generate/routes.d.ts +2 -2
  13. package/dist/commands/hydrogen/init.d.ts +3 -3
  14. package/dist/commands/hydrogen/init.js +74 -93
  15. package/dist/commands/hydrogen/init.test.js +71 -24
  16. package/dist/commands/hydrogen/preview.d.ts +2 -2
  17. package/dist/commands/hydrogen/preview.js +4 -4
  18. package/dist/commands/hydrogen/shortcut.d.ts +9 -0
  19. package/dist/commands/hydrogen/shortcut.js +74 -0
  20. package/dist/commands/hydrogen/shortcut.test.js +58 -0
  21. package/dist/generator-templates/routes/[robots.txt].tsx +35 -1
  22. package/dist/generator-templates/routes/[sitemap.xml].tsx +33 -2
  23. package/dist/generator-templates/routes/account/login.tsx +42 -13
  24. package/dist/generator-templates/routes/account/register.tsx +42 -13
  25. package/dist/generator-templates/routes/cart.tsx +42 -2
  26. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +44 -5
  27. package/dist/generator-templates/routes/index.tsx +33 -0
  28. package/dist/generator-templates/routes/pages/$pageHandle.tsx +48 -10
  29. package/dist/generator-templates/routes/policies/$policyHandle.tsx +67 -14
  30. package/dist/generator-templates/routes/policies/index.tsx +54 -4
  31. package/dist/generator-templates/routes/products/$productHandle.tsx +44 -9
  32. package/dist/hooks/init.js +2 -2
  33. package/dist/{utils → lib}/check-lockfile.js +7 -4
  34. package/dist/{utils → lib}/check-lockfile.test.js +19 -28
  35. package/dist/{utils → lib}/check-version.test.js +3 -2
  36. package/dist/lib/colors.d.ts +8 -0
  37. package/dist/lib/colors.js +8 -0
  38. package/dist/{utils → lib}/config.js +9 -18
  39. package/dist/{utils → lib}/flags.d.ts +3 -3
  40. package/dist/{utils → lib}/flags.js +4 -4
  41. package/dist/{utils → lib}/mini-oxygen.js +14 -12
  42. package/dist/lib/remix-version-interop.d.ts +11 -0
  43. package/dist/lib/remix-version-interop.js +54 -0
  44. package/dist/lib/remix-version-interop.test.d.ts +1 -0
  45. package/dist/lib/remix-version-interop.test.js +93 -0
  46. package/dist/lib/shell.d.ts +12 -0
  47. package/dist/lib/shell.js +73 -0
  48. package/dist/lib/template-downloader.d.ts +6 -0
  49. package/dist/{utils → lib}/template-downloader.js +21 -16
  50. package/dist/{utils → lib}/transpile-ts.js +5 -5
  51. package/dist/lib/virtual-routes.test.d.ts +1 -0
  52. package/dist/virtual-routes/routes/index.jsx +2 -15
  53. package/dist/virtual-routes/virtual-root.jsx +5 -6
  54. package/oclif.manifest.json +1 -1
  55. package/package.json +11 -10
  56. package/dist/utils/template-downloader.d.ts +0 -11
  57. /package/dist/{utils/check-lockfile.test.d.ts → commands/hydrogen/shortcut.test.d.ts} +0 -0
  58. /package/dist/{utils → lib}/check-lockfile.d.ts +0 -0
  59. /package/dist/{utils/check-version.test.d.ts → lib/check-lockfile.test.d.ts} +0 -0
  60. /package/dist/{utils → lib}/check-version.d.ts +0 -0
  61. /package/dist/{utils → lib}/check-version.js +0 -0
  62. /package/dist/{utils/flags.test.d.ts → lib/check-version.test.d.ts} +0 -0
  63. /package/dist/{utils → lib}/config.d.ts +0 -0
  64. /package/dist/{utils/virtual-routes.test.d.ts → lib/flags.test.d.ts} +0 -0
  65. /package/dist/{utils → lib}/flags.test.js +0 -0
  66. /package/dist/{utils → lib}/log.d.ts +0 -0
  67. /package/dist/{utils → lib}/log.js +0 -0
  68. /package/dist/{utils → lib}/mini-oxygen.d.ts +0 -0
  69. /package/dist/{utils → lib}/missing-routes.d.ts +0 -0
  70. /package/dist/{utils → lib}/missing-routes.js +0 -0
  71. /package/dist/{utils → lib}/transpile-ts.d.ts +0 -0
  72. /package/dist/{utils → lib}/virtual-routes.d.ts +0 -0
  73. /package/dist/{utils → lib}/virtual-routes.js +0 -0
  74. /package/dist/{utils → lib}/virtual-routes.test.js +0 -0
@@ -5,10 +5,10 @@ declare class GenerateRoutes extends Command {
5
5
  static description: string;
6
6
  static hidden: true;
7
7
  static flags: {
8
- adapter: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
8
+ adapter: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
9
  typescript: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
10
10
  force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
11
- path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
11
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
12
12
  };
13
13
  run(): Promise<void>;
14
14
  }
@@ -5,9 +5,9 @@ declare class Init extends Command {
5
5
  static description: string;
6
6
  static flags: {
7
7
  force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
8
- path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
9
- language: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
10
- template: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
8
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
+ language: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
10
+ template: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
11
11
  'install-deps': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
12
12
  };
13
13
  run(): Promise<void>;
@@ -1,12 +1,14 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
2
  import { packageManagerUsedForCreating, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
3
- import { renderFatalError } from '@shopify/cli-kit/node/ui';
4
- import Flags from '@oclif/core/lib/flags.js';
5
- import { output, path } from '@shopify/cli-kit';
6
- import { commonFlags, flagsToCamelObject, parseProcessFlags } from '../../utils/flags.js';
7
- import { transpileProject } from '../../utils/transpile-ts.js';
8
- import { getLatestTemplates } from '../../utils/template-downloader.js';
9
- import { checkHydrogenVersion } from '../../utils/check-version.js';
3
+ import { renderFatalError, renderSelectPrompt, renderTextPrompt, renderConfirmationPrompt, renderInfo, renderTasks, renderSuccess } from '@shopify/cli-kit/node/ui';
4
+ import { Flags } from '@oclif/core';
5
+ import { basename, resolvePath, joinPath } from '@shopify/cli-kit/node/path';
6
+ import { rmdir, copyFile, fileExists, isDirectory } from '@shopify/cli-kit/node/fs';
7
+ import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
8
+ import { commonFlags, flagsToCamelObject, parseProcessFlags } from '../../lib/flags.js';
9
+ import { transpileProject } from '../../lib/transpile-ts.js';
10
+ import { getLatestTemplates } from '../../lib/template-downloader.js';
11
+ import { checkHydrogenVersion } from '../../lib/check-version.js';
10
12
  import { readdir } from 'fs/promises';
11
13
  import { fileURLToPath } from 'url';
12
14
 
@@ -53,106 +55,77 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
53
55
  packageManager2 === "unknown" ? "" : `Please use the latest version with \`${packageManager2} create @shopify/hydrogen@latest\``
54
56
  );
55
57
  }
56
- const templatesPromise = getLatestTemplates().catch((error) => {
57
- output.info("\n\n\n");
58
+ let templatesDownloaded = false;
59
+ const templatesPromise = getLatestTemplates().then((result) => {
60
+ templatesDownloaded = true;
61
+ return result;
62
+ }).catch((error) => {
58
63
  renderFatalError(error);
59
64
  process.exit(1);
60
65
  });
61
- const { ui, file } = await import('@shopify/cli-kit');
62
- const { renderSuccess, renderInfo } = await import('@shopify/cli-kit/node/ui');
63
- const prompts = [];
64
- if (!options.template) {
65
- prompts.push({
66
- type: "select",
67
- name: "template",
68
- message: "Choose a template",
69
- choices: STARTER_TEMPLATES.map((value) => ({
70
- name: value.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
71
- value
72
- }))
73
- });
74
- }
75
- if (!options.language) {
76
- prompts.push({
77
- type: "select",
78
- name: "language",
79
- message: "Choose a language",
80
- choices: [
81
- { name: "JavaScript", value: "js" },
82
- { name: "TypeScript", value: "ts" }
83
- ],
84
- default: "js"
85
- });
86
- }
87
- if (!options.path) {
88
- prompts.push({
89
- type: "input",
90
- name: "path",
91
- message: "Where would you like to create your app?",
92
- default: "hydrogen-storefront"
93
- });
94
- }
95
- const {
96
- path: location = options.path,
97
- template: appTemplate = options.template,
98
- language = options.language ?? "js"
99
- } = prompts.length > 0 ? await ui.prompt(prompts) : options;
100
- const projectName = path.basename(location);
101
- const projectDir = path.resolve(process.cwd(), location);
66
+ const appTemplate = options.template ?? await renderSelectPrompt({
67
+ message: "Choose a template",
68
+ defaultValue: "hello-world",
69
+ choices: STARTER_TEMPLATES.map((value) => ({
70
+ label: value.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
71
+ value
72
+ }))
73
+ });
74
+ const language = options.language ?? await renderSelectPrompt({
75
+ message: "Choose a language",
76
+ choices: [
77
+ { label: "JavaScript", value: "js" },
78
+ { label: "TypeScript", value: "ts" }
79
+ ],
80
+ defaultValue: "js"
81
+ });
82
+ const location = options.path ?? await renderTextPrompt({
83
+ message: "Where would you like to create your app?",
84
+ defaultValue: "hydrogen-storefront"
85
+ });
86
+ const projectName = basename(location);
87
+ const projectDir = resolvePath(process.cwd(), location);
102
88
  if (await projectExists(projectDir)) {
103
89
  if (!options.force) {
104
- const { deleteFiles } = await ui.prompt([
105
- {
106
- type: "select",
107
- name: "deleteFiles",
108
- message: `${location} is not an empty directory. Do you want to delete the existing files and continue?`,
109
- choices: [
110
- { name: "Yes, delete the files", value: "true" },
111
- { name: "No, do not delete the files", value: "false" }
112
- ],
113
- default: "false"
114
- }
115
- ]);
116
- if (deleteFiles === "false") {
90
+ const deleteFiles = await renderConfirmationPrompt({
91
+ message: `${location} is not an empty directory. Do you want to delete the existing files and continue?`,
92
+ defaultValue: false
93
+ });
94
+ if (!deleteFiles) {
117
95
  renderInfo({
118
96
  headline: `Destination path ${location} already exists and is not an empty directory. You may use \`--force\` or \`-f\` to override it.`
119
97
  });
120
98
  return;
121
99
  }
122
100
  }
123
- await file.rmdir(projectDir, { force: true });
101
+ await rmdir(projectDir, { force: true });
102
+ }
103
+ if (!templatesDownloaded) {
104
+ await renderTasks([
105
+ {
106
+ title: "Downloading templates",
107
+ task: async () => {
108
+ await templatesPromise;
109
+ }
110
+ }
111
+ ]);
124
112
  }
125
- let downloaded = false;
126
- setTimeout(
127
- () => !downloaded && output.info("\n\u{1F4E5} Downloading templates..."),
128
- 150
129
- );
130
113
  const { templatesDir } = await templatesPromise;
131
- downloaded = true;
132
- await file.copy(path.join(templatesDir, appTemplate), projectDir);
114
+ await copyFile(joinPath(templatesDir, appTemplate), projectDir);
133
115
  if (language === "js") {
134
116
  try {
135
117
  await transpileProject(projectDir);
136
118
  } catch (error) {
137
- await file.rmdir(projectDir, { force: true });
119
+ await rmdir(projectDir, { force: true });
138
120
  throw error;
139
121
  }
140
122
  }
141
123
  let depsInstalled = false;
142
124
  let packageManager = await packageManagerUsedForCreating();
143
125
  if (packageManager !== "unknown") {
144
- const installDeps = options.installDeps ?? (await ui.prompt([
145
- {
146
- type: "select",
147
- name: "installDeps",
148
- message: `Install dependencies with ${packageManager}?`,
149
- choices: [
150
- { name: "Yes", value: "true" },
151
- { name: "No", value: "false" }
152
- ],
153
- default: "true"
154
- }
155
- ])).installDeps === "true";
126
+ const installDeps = options.installDeps ?? await renderConfirmationPrompt({
127
+ message: `Install dependencies with ${packageManager}?`
128
+ });
156
129
  if (installDeps) {
157
130
  await installNodeModules({
158
131
  directory: projectDir,
@@ -169,20 +142,28 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
169
142
  renderSuccess({
170
143
  headline: `${projectName} is ready to build.`,
171
144
  nextSteps: [
172
- `Run \`cd ${location}\`${depsInstalled ? "" : `, \`${packageManager} install\`,`} and \`${packageManager + (packageManager === "npm" ? " run" : "")} dev\` to start your local development server and start building.`
173
- ],
145
+ outputContent`Run ${outputToken.genericShellCommand(`cd ${location}`)}`.value,
146
+ depsInstalled ? void 0 : outputContent`Run ${outputToken.genericShellCommand(
147
+ `${packageManager} install`
148
+ )} to install the dependencies`.value,
149
+ outputContent`Run ${outputToken.packagejsonScript(
150
+ packageManager,
151
+ "dev"
152
+ )} to start your local development server and start building`.value
153
+ ].filter((step) => Boolean(step)),
174
154
  reference: [
175
- "Building with Hydrogen: https://shopify.dev/custom-storefronts/hydrogen"
155
+ "Building with Hydrogen: https://shopify.dev/docs/custom-storefronts/hydrogen/building/begin-development"
176
156
  ]
177
157
  });
178
- renderInfo({
179
- headline: `Your project will display inventory from the Hydrogen Demo Store.`,
180
- body: `To connect this project to your Shopify store\u2019s inventory, update \`${projectName}/.env\` with your store ID and Storefront API key.`
181
- });
158
+ if (appTemplate === "demo-store") {
159
+ renderInfo({
160
+ headline: `Your project will display inventory from the Hydrogen Demo Store.`,
161
+ body: `To connect this project to your Shopify store\u2019s inventory, update \`${projectName}/.env\` with your store ID and Storefront API key.`
162
+ });
163
+ }
182
164
  }
183
165
  async function projectExists(projectDir) {
184
- const { file } = await import('@shopify/cli-kit');
185
- return await file.exists(projectDir) && await file.isDirectory(projectDir) && (await readdir(projectDir)).length > 0;
166
+ return await fileExists(projectDir) && await isDirectory(projectDir) && (await readdir(projectDir)).length > 0;
186
167
  }
187
168
  function supressNodeExperimentalWarnings() {
188
169
  const warningListener = process.listeners("warning")[0];
@@ -1,21 +1,24 @@
1
1
  import { describe, beforeEach, vi, it, expect } from 'vitest';
2
2
  import { temporaryDirectoryTask } from 'tempy';
3
3
  import { runInit } from './init.js';
4
- import { ui } from '@shopify/cli-kit';
4
+ import { renderSelectPrompt, renderConfirmationPrompt, renderTextPrompt, renderInfo } from '@shopify/cli-kit/node/ui';
5
+ import { outputContent } from '@shopify/cli-kit/node/output';
5
6
  import { installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
6
7
 
7
8
  describe("init", () => {
8
9
  beforeEach(() => {
9
10
  vi.resetAllMocks();
10
- vi.mock("@shopify/cli-kit");
11
- vi.mock("../../utils/transpile-ts.js");
12
- vi.mock("../../utils/template-downloader.js", async () => ({
11
+ vi.mock("@shopify/cli-kit/node/output");
12
+ vi.mock("../../lib/transpile-ts.js");
13
+ vi.mock("../../lib/template-downloader.js", async () => ({
13
14
  getLatestTemplates: () => Promise.resolve({})
14
15
  }));
15
16
  vi.mock("@shopify/cli-kit/node/node-package-manager");
16
- vi.mocked(ui.prompt).mockImplementation(
17
- () => Promise.resolve({ installDeps: "false" })
18
- );
17
+ vi.mocked(outputContent).mockImplementation(() => ({
18
+ value: ""
19
+ }));
20
+ vi.mock("@shopify/cli-kit/node/ui");
21
+ vi.mock("@shopify/cli-kit/node/fs");
19
22
  });
20
23
  const defaultOptions = (stubs) => ({
21
24
  template: "hello-world",
@@ -24,11 +27,27 @@ describe("init", () => {
24
27
  ...stubs
25
28
  });
26
29
  describe.each([
27
- { flag: "template", value: "hello-world" },
28
- { flag: "installDeps", value: true },
29
- { flag: "language", value: "ts" },
30
- { flag: "path", value: "./my-app" }
31
- ])("flag $flag", ({ flag, value }) => {
30
+ {
31
+ flag: "template",
32
+ value: "hello-world",
33
+ condition: { fn: renderSelectPrompt, match: /template/i }
34
+ },
35
+ {
36
+ flag: "installDeps",
37
+ value: true,
38
+ condition: { fn: renderConfirmationPrompt, match: /install dependencies/i }
39
+ },
40
+ {
41
+ flag: "language",
42
+ value: "ts",
43
+ condition: { fn: renderSelectPrompt, match: /language/i }
44
+ },
45
+ {
46
+ flag: "path",
47
+ value: "./my-app",
48
+ condition: { fn: renderTextPrompt, match: /where/i }
49
+ }
50
+ ])("flag $flag", ({ flag, value, condition }) => {
32
51
  it(`does not prompt the user for ${flag} when a value is passed in options`, async () => {
33
52
  await temporaryDirectoryTask(async (tmpDir) => {
34
53
  const options = defaultOptions({
@@ -36,12 +55,10 @@ describe("init", () => {
36
55
  [flag]: value
37
56
  });
38
57
  await runInit(options);
39
- expect(ui.prompt).not.toHaveBeenCalledWith(
40
- expect.arrayContaining([
41
- expect.objectContaining({
42
- name: flag
43
- })
44
- ])
58
+ expect(condition.fn).not.toHaveBeenCalledWith(
59
+ expect.objectContaining({
60
+ message: expect.stringMatching(condition.match)
61
+ })
45
62
  );
46
63
  });
47
64
  });
@@ -52,12 +69,10 @@ describe("init", () => {
52
69
  [flag]: void 0
53
70
  });
54
71
  await runInit(options);
55
- expect(ui.prompt).toHaveBeenCalledWith(
56
- expect.arrayContaining([
57
- expect.objectContaining({
58
- name: flag
59
- })
60
- ])
72
+ expect(condition.fn).toHaveBeenCalledWith(
73
+ expect.objectContaining({
74
+ message: expect.stringMatching(condition.match)
75
+ })
61
76
  );
62
77
  });
63
78
  });
@@ -76,4 +91,36 @@ describe("init", () => {
76
91
  expect(installNodeModules).not.toHaveBeenCalled();
77
92
  });
78
93
  });
94
+ it("displays inventory information when using the demo-store template", async () => {
95
+ await temporaryDirectoryTask(async (tmpDir) => {
96
+ const options = defaultOptions({
97
+ installDeps: false,
98
+ path: tmpDir,
99
+ template: "demo-store"
100
+ });
101
+ await runInit(options);
102
+ expect(renderInfo).toHaveBeenCalledTimes(1);
103
+ expect(renderInfo).toHaveBeenCalledWith(
104
+ expect.objectContaining({
105
+ body: expect.stringContaining(
106
+ "To connect this project to your Shopify store\u2019s inventory"
107
+ ),
108
+ headline: expect.stringContaining(
109
+ "Your project will display inventory from the Hydrogen Demo Store"
110
+ )
111
+ })
112
+ );
113
+ });
114
+ });
115
+ it("does not display inventory information when using non-demo-store templates", async () => {
116
+ await temporaryDirectoryTask(async (tmpDir) => {
117
+ const options = defaultOptions({
118
+ installDeps: false,
119
+ path: tmpDir,
120
+ template: "pizza-store"
121
+ });
122
+ await runInit(options);
123
+ expect(renderInfo).toHaveBeenCalledTimes(0);
124
+ });
125
+ });
79
126
  });
@@ -4,8 +4,8 @@ import Command from '@shopify/cli-kit/node/base-command';
4
4
  declare class Preview extends Command {
5
5
  static description: string;
6
6
  static flags: {
7
- path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
8
- port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number>;
7
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
8
+ port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
9
  };
10
10
  run(): Promise<void>;
11
11
  }
@@ -1,8 +1,8 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
- import { muteDevLogs } from '../../utils/log.js';
3
- import { getProjectPaths } from '../../utils/config.js';
4
- import { commonFlags } from '../../utils/flags.js';
5
- import { startMiniOxygen } from '../../utils/mini-oxygen.js';
2
+ import { muteDevLogs } from '../../lib/log.js';
3
+ import { getProjectPaths } from '../../lib/config.js';
4
+ import { commonFlags } from '../../lib/flags.js';
5
+ import { startMiniOxygen } from '../../lib/mini-oxygen.js';
6
6
 
7
7
  class Preview extends Command {
8
8
  static description = "Runs a Hydrogen storefront in an Oxygen worker for production.";
@@ -0,0 +1,9 @@
1
+ import Command from '@shopify/cli-kit/node/base-command';
2
+
3
+ declare class Shortcut extends Command {
4
+ static description: string;
5
+ run(): Promise<void>;
6
+ }
7
+ declare function runCreateShortcut(): Promise<void>;
8
+
9
+ export { Shortcut as default, runCreateShortcut };
@@ -0,0 +1,74 @@
1
+ import Command from '@shopify/cli-kit/node/base-command';
2
+ import { renderSuccess, renderFatalError } from '@shopify/cli-kit/node/ui';
3
+ import { isWindows, isGitBash, supportsShell, hasAlias, shellWriteFile, homeFileExists, shellRunScript } from '../../lib/shell.js';
4
+
5
+ const ALIAS_NAME = "h2";
6
+ class Shortcut extends Command {
7
+ static description = `Creates a global \`${ALIAS_NAME}\` shortcut for the Hydrogen CLI`;
8
+ async run() {
9
+ await runCreateShortcut();
10
+ }
11
+ }
12
+ async function runCreateShortcut() {
13
+ const shortcuts = isWindows() && !isGitBash() ? await createShortcutsForWindows() : await createShortcutsForUnix();
14
+ if (shortcuts.length > 0) {
15
+ renderSuccess({
16
+ headline: `Shortcut ready for the following shells: ${shortcuts.join(
17
+ ", "
18
+ )}.
19
+ Restart your terminal session and run \`${ALIAS_NAME}\` from your local project.`
20
+ });
21
+ } else {
22
+ renderFatalError({
23
+ name: "error",
24
+ type: 0,
25
+ message: "No supported shell found.",
26
+ tryMessage: "Please create a shortcut manually."
27
+ });
28
+ }
29
+ }
30
+ const BASH_ZSH_COMMAND = `
31
+ # Shopify Hydrogen alias to local projects
32
+ alias ${ALIAS_NAME}='$(npm prefix -s)/node_modules/.bin/shopify hydrogen'`;
33
+ const FISH_FUNCTION = `
34
+ function ${ALIAS_NAME} --wraps='shopify hydrogen' --description 'Shortcut for the Hydrogen CLI'
35
+ set npmPrefix (npm prefix -s)
36
+ $npmPrefix/node_modules/.bin/shopify hydrogen $argv
37
+ end
38
+ `;
39
+ async function createShortcutsForUnix() {
40
+ const shells = [];
41
+ if (supportsShell("zsh") && (hasAlias(ALIAS_NAME, "~/.zshrc") || shellWriteFile("~/.zshrc", BASH_ZSH_COMMAND, true))) {
42
+ shells.push("zsh");
43
+ }
44
+ if (supportsShell("bash") && (hasAlias(ALIAS_NAME, "~/.bashrc") || shellWriteFile("~/.bashrc", BASH_ZSH_COMMAND, true))) {
45
+ shells.push("bash");
46
+ }
47
+ if (supportsShell("fish") && await homeFileExists("~/.config/fish/functions") && shellWriteFile(`~/.config/fish/functions/${ALIAS_NAME}.fish`, FISH_FUNCTION)) {
48
+ shells.push("fish");
49
+ }
50
+ return shells;
51
+ }
52
+ const PS_FUNCTION = `function Invoke-Local-H2 {$npmPrefix = npm prefix -s; Invoke-Expression "$npmPrefix\\node_modules\\.bin\\shopify.ps1 hydrogen $Args"}; Set-Alias -Name ${ALIAS_NAME} -Value Invoke-Local-H2`;
53
+ const PS_APPEND_PROFILE_COMMAND = `
54
+ if (!(Test-Path -Path $PROFILE)) {
55
+ New-Item -ItemType File -Path $PROFILE -Force
56
+ }
57
+
58
+ $profileContent = Get-Content -Path $PROFILE
59
+ if (!$profileContent -or $profileContent -NotLike '*Invoke-Local-H2*') {
60
+ Add-Content -Path $PROFILE -Value '${PS_FUNCTION}'
61
+ }
62
+ `;
63
+ async function createShortcutsForWindows() {
64
+ const shells = [];
65
+ if (shellRunScript(PS_APPEND_PROFILE_COMMAND, "powershell.exe")) {
66
+ shells.push("PowerShell");
67
+ }
68
+ if (shellRunScript(PS_APPEND_PROFILE_COMMAND, "pwsh.exe")) {
69
+ shells.push("PowerShell 7+");
70
+ }
71
+ return shells;
72
+ }
73
+
74
+ export { Shortcut as default, runCreateShortcut };
@@ -0,0 +1,58 @@
1
+ import { describe, beforeEach, vi, afterEach, expect, it } from 'vitest';
2
+ import { runCreateShortcut } from './shortcut.js';
3
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
4
+ import { supportsShell, isWindows, isGitBash } from '../../lib/shell.js';
5
+ import { execSync, exec } from 'child_process';
6
+
7
+ describe("shortcut", () => {
8
+ const outputMock = mockAndCaptureOutput();
9
+ beforeEach(() => {
10
+ vi.resetAllMocks();
11
+ vi.mock("child_process");
12
+ vi.mock("../../lib/shell.js", async () => {
13
+ return {
14
+ isWindows: vi.fn(),
15
+ isGitBash: vi.fn(),
16
+ supportsShell: vi.fn(),
17
+ shellWriteFile: () => true,
18
+ shellRunScript: () => true,
19
+ hasAlias: () => false,
20
+ homeFileExists: () => Promise.resolve(true)
21
+ };
22
+ });
23
+ vi.mocked(supportsShell).mockImplementation(
24
+ (shell) => !isWindows() || shell === "bash"
25
+ );
26
+ });
27
+ afterEach(() => {
28
+ outputMock.clear();
29
+ expect(execSync).toHaveBeenCalledTimes(0);
30
+ expect(exec).toHaveBeenCalledTimes(0);
31
+ });
32
+ it("creates aliases for Unix", async () => {
33
+ vi.mocked(isWindows).mockReturnValue(false);
34
+ await runCreateShortcut();
35
+ expect(outputMock.info()).toMatch(`zsh, bash, fish`);
36
+ expect(outputMock.error()).toBeFalsy();
37
+ });
38
+ it("creates aliases for Windows", async () => {
39
+ vi.mocked(isWindows).mockReturnValue(true);
40
+ await runCreateShortcut();
41
+ expect(outputMock.info()).toMatch(`PowerShell, PowerShell 7+`);
42
+ expect(outputMock.error()).toBeFalsy();
43
+ });
44
+ it("creates aliases for Windows in Git Bash", async () => {
45
+ vi.mocked(isWindows).mockReturnValue(true);
46
+ vi.mocked(isGitBash).mockReturnValueOnce(true);
47
+ await runCreateShortcut();
48
+ expect(outputMock.info()).toMatch("bash");
49
+ expect(outputMock.error()).toBeFalsy();
50
+ });
51
+ it("warns when not finding shells", async () => {
52
+ vi.mocked(isWindows).mockReturnValue(false);
53
+ vi.mocked(supportsShell).mockReturnValue(false);
54
+ await runCreateShortcut();
55
+ expect(outputMock.info()).toBeFalsy();
56
+ expect(outputMock.error()).toBeTruthy();
57
+ });
58
+ });
@@ -1,4 +1,8 @@
1
- import {type LoaderArgs} from '@shopify/remix-oxygen';
1
+ import {
2
+ type LoaderArgs,
3
+ type ErrorBoundaryComponent,
4
+ } from '@shopify/remix-oxygen';
5
+ import {useCatch, useRouteError, isRouteErrorResponse} from '@remix-run/react';
2
6
 
3
7
  export const loader = ({request}: LoaderArgs) => {
4
8
  const url = new URL(request.url);
@@ -14,6 +18,36 @@ export const loader = ({request}: LoaderArgs) => {
14
18
  });
15
19
  };
16
20
 
21
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
22
+ console.error(error);
23
+
24
+ return <div>There was an error.</div>;
25
+ };
26
+
27
+ export function CatchBoundary() {
28
+ const caught = useCatch();
29
+ console.error(caught);
30
+
31
+ return (
32
+ <div>
33
+ There was an error. Status: {caught.status}. Message:{' '}
34
+ {caught.data?.message}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function ErrorBoundary() {
40
+ const error = useRouteError();
41
+
42
+ if (isRouteErrorResponse(error)) {
43
+ console.error(error.status, error.statusText, error.data);
44
+ return <div>Route Error</div>;
45
+ } else {
46
+ console.error((error as Error).message);
47
+ return <div>Thrown Error</div>;
48
+ }
49
+ }
50
+
17
51
  function robotsTxtData({url}: {url: string}) {
18
52
  const sitemapUrl = url ? `${url}/sitemap.xml` : undefined;
19
53
 
@@ -1,5 +1,6 @@
1
1
  import {flattenConnection} from '@shopify/hydrogen';
2
- import type {LoaderArgs} from '@shopify/remix-oxygen';
2
+ import type {LoaderArgs, ErrorBoundaryComponent} from '@shopify/remix-oxygen';
3
+ import {useCatch, useRouteError, isRouteErrorResponse} from '@remix-run/react';
3
4
  import {
4
5
  CollectionConnection,
5
6
  PageConnection,
@@ -34,7 +35,7 @@ export async function loader({request, context: {storefront}}: LoaderArgs) {
34
35
  });
35
36
 
36
37
  if (!data) {
37
- throw new Response(null, {status: 404});
38
+ throw new Response('No data found', {status: 404});
38
39
  }
39
40
 
40
41
  return new Response(
@@ -50,6 +51,36 @@ export async function loader({request, context: {storefront}}: LoaderArgs) {
50
51
  );
51
52
  }
52
53
 
54
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
55
+ console.error(error);
56
+
57
+ return <div>There was an error.</div>;
58
+ };
59
+
60
+ export function CatchBoundary() {
61
+ const caught = useCatch();
62
+ console.error(caught);
63
+
64
+ return (
65
+ <div>
66
+ There was an error. Status: {caught.status}. Message:{' '}
67
+ {caught.data?.message}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ export function ErrorBoundary() {
73
+ const error = useRouteError();
74
+
75
+ if (isRouteErrorResponse(error)) {
76
+ console.error(error.status, error.statusText, error.data);
77
+ return <div>Route Error</div>;
78
+ } else {
79
+ console.error((error as Error).message);
80
+ return <div>Thrown Error</div>;
81
+ }
82
+ }
83
+
53
84
  function xmlEncode(string: string) {
54
85
  return string.replace(/[&<>'"]/g, (char) => `&#${char.charCodeAt(0)};`);
55
86
  }