@shopify/cli-hydrogen 4.0.8 → 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 (76) hide show
  1. package/README.md +9 -0
  2. package/dist/commands/hydrogen/build.d.ts +4 -1
  3. package/dist/commands/hydrogen/build.js +21 -17
  4. package/dist/commands/hydrogen/check.d.ts +3 -6
  5. package/dist/commands/hydrogen/check.js +10 -9
  6. package/dist/commands/hydrogen/dev.d.ts +3 -2
  7. package/dist/commands/hydrogen/dev.js +24 -22
  8. package/dist/commands/hydrogen/g.d.ts +10 -0
  9. package/dist/commands/hydrogen/g.js +17 -0
  10. package/dist/commands/hydrogen/generate/route.d.ts +7 -9
  11. package/dist/commands/hydrogen/generate/route.js +49 -47
  12. package/dist/commands/hydrogen/generate/route.test.js +48 -40
  13. package/dist/commands/hydrogen/generate/routes.d.ts +2 -2
  14. package/dist/commands/hydrogen/init.d.ts +3 -3
  15. package/dist/commands/hydrogen/init.js +76 -95
  16. package/dist/commands/hydrogen/init.test.js +126 -0
  17. package/dist/commands/hydrogen/preview.d.ts +2 -2
  18. package/dist/commands/hydrogen/preview.js +4 -4
  19. package/dist/commands/hydrogen/shortcut.d.ts +9 -0
  20. package/dist/commands/hydrogen/shortcut.js +74 -0
  21. package/dist/commands/hydrogen/shortcut.test.js +58 -0
  22. package/dist/generator-templates/routes/[robots.txt].tsx +35 -1
  23. package/dist/generator-templates/routes/[sitemap.xml].tsx +45 -10
  24. package/dist/generator-templates/routes/account/login.tsx +42 -13
  25. package/dist/generator-templates/routes/account/register.tsx +42 -13
  26. package/dist/generator-templates/routes/cart.tsx +42 -2
  27. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +44 -5
  28. package/dist/generator-templates/routes/index.tsx +33 -0
  29. package/dist/generator-templates/routes/pages/$pageHandle.tsx +48 -10
  30. package/dist/generator-templates/routes/policies/$policyHandle.tsx +67 -14
  31. package/dist/generator-templates/routes/policies/index.tsx +54 -4
  32. package/dist/generator-templates/routes/products/$productHandle.tsx +44 -9
  33. package/dist/hooks/init.js +2 -2
  34. package/dist/{utils → lib}/check-lockfile.js +7 -4
  35. package/dist/{utils → lib}/check-lockfile.test.js +19 -28
  36. package/dist/{utils → lib}/check-version.test.js +3 -2
  37. package/dist/lib/colors.d.ts +8 -0
  38. package/dist/lib/colors.js +8 -0
  39. package/dist/{utils → lib}/config.js +10 -19
  40. package/dist/{utils → lib}/flags.d.ts +9 -3
  41. package/dist/{utils → lib}/flags.js +19 -4
  42. package/dist/lib/flags.test.d.ts +1 -0
  43. package/dist/{utils → lib}/mini-oxygen.js +14 -12
  44. package/dist/{utils → lib}/missing-routes.js +1 -1
  45. package/dist/lib/remix-version-interop.d.ts +11 -0
  46. package/dist/lib/remix-version-interop.js +54 -0
  47. package/dist/lib/remix-version-interop.test.d.ts +1 -0
  48. package/dist/lib/remix-version-interop.test.js +93 -0
  49. package/dist/lib/shell.d.ts +12 -0
  50. package/dist/lib/shell.js +73 -0
  51. package/dist/lib/template-downloader.d.ts +6 -0
  52. package/dist/{utils → lib}/template-downloader.js +21 -16
  53. package/dist/{utils → lib}/transpile-ts.js +5 -5
  54. package/dist/lib/virtual-routes.test.d.ts +1 -0
  55. package/dist/virtual-routes/routes/index.jsx +2 -15
  56. package/dist/virtual-routes/virtual-root.jsx +5 -6
  57. package/oclif.manifest.json +1 -1
  58. package/package.json +11 -10
  59. package/dist/utils/template-downloader.d.ts +0 -11
  60. /package/dist/{utils/check-lockfile.test.d.ts → commands/hydrogen/init.test.d.ts} +0 -0
  61. /package/dist/{utils/check-version.test.d.ts → commands/hydrogen/shortcut.test.d.ts} +0 -0
  62. /package/dist/{utils → lib}/check-lockfile.d.ts +0 -0
  63. /package/dist/{utils/flags.test.d.ts → lib/check-lockfile.test.d.ts} +0 -0
  64. /package/dist/{utils → lib}/check-version.d.ts +0 -0
  65. /package/dist/{utils → lib}/check-version.js +0 -0
  66. /package/dist/{utils/virtual-routes.test.d.ts → lib/check-version.test.d.ts} +0 -0
  67. /package/dist/{utils → lib}/config.d.ts +0 -0
  68. /package/dist/{utils → lib}/flags.test.js +0 -0
  69. /package/dist/{utils → lib}/log.d.ts +0 -0
  70. /package/dist/{utils → lib}/log.js +0 -0
  71. /package/dist/{utils → lib}/mini-oxygen.d.ts +0 -0
  72. /package/dist/{utils → lib}/missing-routes.d.ts +0 -0
  73. /package/dist/{utils → lib}/transpile-ts.d.ts +0 -0
  74. /package/dist/{utils → lib}/virtual-routes.d.ts +0 -0
  75. /package/dist/{utils → lib}/virtual-routes.js +0 -0
  76. /package/dist/{utils → lib}/virtual-routes.test.js +0 -0
@@ -1,24 +1,16 @@
1
1
  import { describe, beforeEach, vi, it, expect } from 'vitest';
2
2
  import { temporaryDirectoryTask } from 'tempy';
3
3
  import { runGenerate, GENERATOR_TEMPLATES_DIR } from './route.js';
4
- import { file, path, ui } from '@shopify/cli-kit';
4
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
5
+ import { readFile, mkdir, writeFile } from '@shopify/cli-kit/node/fs';
6
+ import { joinPath, dirname } from '@shopify/cli-kit/node/path';
7
+ import { convertRouteToV2 } from '../../../lib/remix-version-interop.js';
5
8
 
6
9
  describe("generate/route", () => {
7
10
  beforeEach(() => {
8
11
  vi.resetAllMocks();
9
- vi.mock("@shopify/cli-kit", async () => {
10
- const cliKit = await vi.importActual("@shopify/cli-kit");
11
- return {
12
- ...cliKit,
13
- output: {
14
- ...cliKit.output,
15
- success: vi.fn()
16
- },
17
- ui: {
18
- prompt: vi.fn()
19
- }
20
- };
21
- });
12
+ vi.mock("@shopify/cli-kit/node/output");
13
+ vi.mock("@shopify/cli-kit/node/ui");
22
14
  });
23
15
  it("generates a route file", async () => {
24
16
  await temporaryDirectoryTask(async (tmpDir) => {
@@ -27,12 +19,30 @@ describe("generate/route", () => {
27
19
  files: [],
28
20
  templates: [[route, `const str = "hello world"`]]
29
21
  });
30
- await runGenerate(route, {
22
+ await runGenerate(route, route, {
31
23
  directory: appRoot,
32
24
  templatesRoot
33
25
  });
34
26
  expect(
35
- await file.read(path.join(appRoot, "app/routes", `${route}.jsx`))
27
+ await readFile(joinPath(appRoot, "app/routes", `${route}.jsx`))
28
+ ).toContain(`const str = 'hello world'`);
29
+ });
30
+ });
31
+ it("generates a route file for Remix v2", async () => {
32
+ await temporaryDirectoryTask(async (tmpDir) => {
33
+ const route = "custom/path/$handle/index";
34
+ const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
35
+ files: [],
36
+ templates: [[route, `const str = "hello world"`]]
37
+ });
38
+ await runGenerate(route, convertRouteToV2(route), {
39
+ directory: appRoot,
40
+ templatesRoot
41
+ });
42
+ expect(
43
+ await readFile(
44
+ joinPath(appRoot, "app/routes", `custom.path.$handle._index.jsx`)
45
+ )
36
46
  ).toContain(`const str = 'hello world'`);
37
47
  });
38
48
  });
@@ -43,55 +53,53 @@ describe("generate/route", () => {
43
53
  files: [],
44
54
  templates: [[route, 'const str = "hello typescript"']]
45
55
  });
46
- await runGenerate(route, {
56
+ await runGenerate(route, route, {
47
57
  directory: appRoot,
48
58
  templatesRoot,
49
59
  typescript: true
50
60
  });
51
61
  expect(
52
- await file.read(path.join(appRoot, "app/routes", `${route}.tsx`))
62
+ await readFile(joinPath(appRoot, "app/routes", `${route}.tsx`))
53
63
  ).toContain(`const str = 'hello typescript'`);
54
64
  });
55
65
  });
56
66
  it("prompts the user if there the file already exists", async () => {
57
67
  await temporaryDirectoryTask(async (tmpDir) => {
58
- vi.mocked(ui.prompt).mockImplementationOnce(async () => {
59
- return { value: "overwrite" };
60
- });
68
+ vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
69
+ async () => true
70
+ );
61
71
  const route = "page/$pageHandle";
62
72
  const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
63
73
  files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
64
74
  templates: [[route, 'const str = "hello world"']]
65
75
  });
66
- await runGenerate(route, {
76
+ await runGenerate(route, route, {
67
77
  directory: appRoot,
68
78
  templatesRoot
69
79
  });
70
- expect(ui.prompt).toHaveBeenCalledWith(
71
- expect.arrayContaining([
72
- expect.objectContaining({
73
- message: expect.stringContaining("already exists")
74
- })
75
- ])
80
+ expect(renderConfirmationPrompt).toHaveBeenCalledWith(
81
+ expect.objectContaining({
82
+ message: expect.stringContaining("already exists")
83
+ })
76
84
  );
77
85
  });
78
86
  });
79
87
  it("does not prompt the user if the force property is true", async () => {
80
88
  await temporaryDirectoryTask(async (tmpDir) => {
81
- vi.mocked(ui.prompt).mockImplementationOnce(async () => {
82
- return { value: "overwrite" };
83
- });
89
+ vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
90
+ async () => true
91
+ );
84
92
  const route = "page/$pageHandle";
85
93
  const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
86
94
  files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
87
95
  templates: [[route, 'const str = "hello world"']]
88
96
  });
89
- await runGenerate(route, {
97
+ await runGenerate(route, route, {
90
98
  directory: appRoot,
91
99
  templatesRoot,
92
100
  force: true
93
101
  });
94
- expect(ui.prompt).not.toHaveBeenCalled();
102
+ expect(renderConfirmationPrompt).not.toHaveBeenCalled();
95
103
  });
96
104
  });
97
105
  });
@@ -104,23 +112,23 @@ async function createHydrogen(directory, {
104
112
  }) {
105
113
  for (const item of files) {
106
114
  const [filePath, fileContent] = item;
107
- const fullFilePath = path.join(directory, "app", filePath);
108
- await file.mkdir(path.dirname(fullFilePath));
109
- await file.write(fullFilePath, fileContent);
115
+ const fullFilePath = joinPath(directory, "app", filePath);
116
+ await mkdir(dirname(fullFilePath));
117
+ await writeFile(fullFilePath, fileContent);
110
118
  }
111
119
  for (const item of templates) {
112
120
  const [filePath, fileContent] = item;
113
- const fullFilePath = path.join(
121
+ const fullFilePath = joinPath(
114
122
  directory,
115
123
  GENERATOR_TEMPLATES_DIR,
116
124
  "routes",
117
125
  `${filePath}.tsx`
118
126
  );
119
- await file.mkdir(path.dirname(fullFilePath));
120
- await file.write(fullFilePath, fileContent);
127
+ await mkdir(dirname(fullFilePath));
128
+ await writeFile(fullFilePath, fileContent);
121
129
  }
122
130
  return {
123
- appRoot: path.join(directory, "app"),
131
+ appRoot: joinPath(directory, "app"),
124
132
  templatesRoot: directory
125
133
  };
126
134
  }
@@ -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, 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
 
@@ -32,13 +34,13 @@ class Init extends Command {
32
34
  }),
33
35
  "install-deps": Flags.boolean({
34
36
  description: "Auto install dependencies using the active package manager",
35
- env: "SHOPIFY_HYDROGEN_INSTALL_DEPS",
37
+ env: "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS",
36
38
  allowNo: true
37
39
  })
38
40
  };
39
41
  async run() {
40
42
  const { flags } = await this.parse(Init);
41
- await runInit({ ...flags });
43
+ await runInit(flagsToCamelObject(flags));
42
44
  }
43
45
  }
44
46
  async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
@@ -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];
@@ -0,0 +1,126 @@
1
+ import { describe, beforeEach, vi, it, expect } from 'vitest';
2
+ import { temporaryDirectoryTask } from 'tempy';
3
+ import { runInit } from './init.js';
4
+ import { renderSelectPrompt, renderConfirmationPrompt, renderTextPrompt, renderInfo } from '@shopify/cli-kit/node/ui';
5
+ import { outputContent } from '@shopify/cli-kit/node/output';
6
+ import { installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
7
+
8
+ describe("init", () => {
9
+ beforeEach(() => {
10
+ vi.resetAllMocks();
11
+ vi.mock("@shopify/cli-kit/node/output");
12
+ vi.mock("../../lib/transpile-ts.js");
13
+ vi.mock("../../lib/template-downloader.js", async () => ({
14
+ getLatestTemplates: () => Promise.resolve({})
15
+ }));
16
+ vi.mock("@shopify/cli-kit/node/node-package-manager");
17
+ vi.mocked(outputContent).mockImplementation(() => ({
18
+ value: ""
19
+ }));
20
+ vi.mock("@shopify/cli-kit/node/ui");
21
+ vi.mock("@shopify/cli-kit/node/fs");
22
+ });
23
+ const defaultOptions = (stubs) => ({
24
+ template: "hello-world",
25
+ language: "js",
26
+ path: "path/to/project",
27
+ ...stubs
28
+ });
29
+ describe.each([
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 }) => {
51
+ it(`does not prompt the user for ${flag} when a value is passed in options`, async () => {
52
+ await temporaryDirectoryTask(async (tmpDir) => {
53
+ const options = defaultOptions({
54
+ path: tmpDir,
55
+ [flag]: value
56
+ });
57
+ await runInit(options);
58
+ expect(condition.fn).not.toHaveBeenCalledWith(
59
+ expect.objectContaining({
60
+ message: expect.stringMatching(condition.match)
61
+ })
62
+ );
63
+ });
64
+ });
65
+ it(`prompts the user for ${flag} when no value is passed in options`, async () => {
66
+ await temporaryDirectoryTask(async (tmpDir) => {
67
+ const options = defaultOptions({
68
+ path: tmpDir,
69
+ [flag]: void 0
70
+ });
71
+ await runInit(options);
72
+ expect(condition.fn).toHaveBeenCalledWith(
73
+ expect.objectContaining({
74
+ message: expect.stringMatching(condition.match)
75
+ })
76
+ );
77
+ });
78
+ });
79
+ });
80
+ it("installs dependencies when installDeps is true", async () => {
81
+ await temporaryDirectoryTask(async (tmpDir) => {
82
+ const options = defaultOptions({ installDeps: true, path: tmpDir });
83
+ await runInit(options);
84
+ expect(installNodeModules).toHaveBeenCalled();
85
+ });
86
+ });
87
+ it("does not install dependencies when installDeps is false", async () => {
88
+ await temporaryDirectoryTask(async (tmpDir) => {
89
+ const options = defaultOptions({ installDeps: false, path: tmpDir });
90
+ await runInit(options);
91
+ expect(installNodeModules).not.toHaveBeenCalled();
92
+ });
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
+ });
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 };