@shopify/cli-hydrogen 5.1.1 → 5.2.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 (71) hide show
  1. package/dist/commands/hydrogen/build.js +10 -5
  2. package/dist/commands/hydrogen/check.js +1 -1
  3. package/dist/commands/hydrogen/codegen-unstable.js +3 -3
  4. package/dist/commands/hydrogen/dev.js +26 -17
  5. package/dist/commands/hydrogen/init.js +3 -0
  6. package/dist/commands/hydrogen/init.test.js +199 -51
  7. package/dist/commands/hydrogen/preview.js +4 -3
  8. package/dist/commands/hydrogen/setup.js +5 -2
  9. package/dist/commands/hydrogen/setup.test.js +62 -0
  10. package/dist/generator-templates/starter/app/components/Footer.tsx +1 -1
  11. package/dist/generator-templates/starter/app/components/Header.tsx +1 -1
  12. package/dist/generator-templates/starter/app/components/Search.tsx +3 -3
  13. package/dist/generator-templates/starter/app/root.tsx +24 -1
  14. package/dist/generator-templates/starter/app/routes/$.tsx +4 -0
  15. package/dist/generator-templates/starter/app/routes/_index.tsx +6 -2
  16. package/dist/generator-templates/starter/app/routes/account.$.tsx +1 -2
  17. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +1 -1
  18. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -7
  19. package/dist/generator-templates/starter/app/routes/account.profile.tsx +7 -2
  20. package/dist/generator-templates/starter/app/routes/account.tsx +4 -3
  21. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +6 -2
  22. package/dist/generator-templates/starter/app/routes/account_.login.tsx +6 -2
  23. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +2 -6
  24. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +1 -2
  25. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -2
  26. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +1 -2
  27. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -2
  28. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -2
  29. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +1 -2
  30. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +2 -3
  31. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +23 -15
  32. package/dist/generator-templates/starter/app/routes/search.tsx +1 -2
  33. package/dist/generator-templates/starter/package.json +4 -4
  34. package/dist/generator-templates/starter/remix.config.js +1 -0
  35. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +9 -9
  36. package/dist/lib/ast.js +9 -0
  37. package/dist/lib/check-version.test.js +1 -0
  38. package/dist/lib/codegen.js +17 -7
  39. package/dist/lib/environment-variables.js +15 -11
  40. package/dist/lib/file.test.js +4 -5
  41. package/dist/lib/find-port.js +9 -0
  42. package/dist/lib/flags.js +3 -2
  43. package/dist/lib/format-code.js +3 -0
  44. package/dist/lib/live-reload.js +62 -0
  45. package/dist/lib/log.js +6 -1
  46. package/dist/lib/mini-oxygen.js +28 -18
  47. package/dist/lib/missing-routes.js +17 -1
  48. package/dist/lib/onboarding/common.js +5 -0
  49. package/dist/lib/onboarding/local.js +21 -8
  50. package/dist/lib/onboarding/remote.js +8 -3
  51. package/dist/lib/remix-config.js +2 -0
  52. package/dist/lib/remix-version-check.test.js +1 -0
  53. package/dist/lib/setups/css/replacers.js +7 -4
  54. package/dist/lib/setups/i18n/replacers.js +7 -5
  55. package/dist/lib/setups/routes/generate.js +4 -1
  56. package/dist/lib/setups/routes/generate.test.js +10 -11
  57. package/dist/lib/template-downloader.js +4 -0
  58. package/dist/lib/transpile-ts.js +1 -0
  59. package/dist/lib/virtual-routes.js +4 -1
  60. package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +26 -4
  61. package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +40 -10
  62. package/dist/virtual-routes/components/IconBanner.jsx +286 -44
  63. package/dist/virtual-routes/components/IconDiscord.jsx +17 -1
  64. package/dist/virtual-routes/components/IconError.jsx +55 -17
  65. package/dist/virtual-routes/components/IconGithub.jsx +19 -1
  66. package/dist/virtual-routes/components/IconTwitter.jsx +17 -1
  67. package/dist/virtual-routes/components/Layout.jsx +1 -1
  68. package/dist/virtual-routes/routes/index.jsx +110 -94
  69. package/dist/virtual-routes/virtual-root.jsx +7 -15
  70. package/oclif.manifest.json +1 -1
  71. package/package.json +9 -8
@@ -1,7 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
3
  import { outputInfo, outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
4
- import { rmdir, fileSize, glob, removeFile, copyFile } from '@shopify/cli-kit/node/fs';
4
+ import { rmdir, fileSize, glob, removeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
5
  import { resolvePath, relativePath, joinPath } from '@shopify/cli-kit/node/path';
6
6
  import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
7
7
  import colors from '@shopify/cli-kit/node/colors';
@@ -42,12 +42,12 @@ class Build extends Command {
42
42
  await runBuild({
43
43
  ...flagsToCamelObject(flags),
44
44
  useCodegen: flags["codegen-unstable"],
45
- path: directory
45
+ directory
46
46
  });
47
47
  }
48
48
  }
49
49
  async function runBuild({
50
- path: appPath,
50
+ directory,
51
51
  useCodegen = false,
52
52
  codegenConfigPath,
53
53
  sourcemap = false,
@@ -56,7 +56,7 @@ async function runBuild({
56
56
  if (!process.env.NODE_ENV) {
57
57
  process.env.NODE_ENV = "production";
58
58
  }
59
- const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(appPath);
59
+ const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
60
60
  await Promise.all([checkLockfileStatus(root), muteRemixLogs()]);
61
61
  console.time(LOG_WORKER_BUILT);
62
62
  outputInfo(`
@@ -125,9 +125,14 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
125
125
  );
126
126
  }
127
127
  }
128
- process.exit(0);
128
+ if (!process.env.SHOPIFY_UNIT_TEST) {
129
+ process.exit(0);
130
+ }
129
131
  }
130
132
  async function copyPublicFiles(publicPath, buildPathClient) {
133
+ if (!await fileExists(publicPath)) {
134
+ return;
135
+ }
131
136
  return copyFile(publicPath, buildPathClient);
132
137
  }
133
138
 
@@ -33,4 +33,4 @@ async function runCheckRoutes({ directory }) {
33
33
  logMissingRoutes(findMissingRoutes(remixConfig));
34
34
  }
35
35
 
36
- export { GenerateRoute as default };
36
+ export { GenerateRoute as default, runCheckRoutes };
@@ -29,17 +29,17 @@ class Codegen extends Command {
29
29
  const directory = flags.path ? path.resolve(flags.path) : process.cwd();
30
30
  await runCodegen({
31
31
  ...flagsToCamelObject(flags),
32
- path: directory
32
+ directory
33
33
  });
34
34
  }
35
35
  }
36
36
  async function runCodegen({
37
- path: appPath,
37
+ directory,
38
38
  codegenConfigPath,
39
39
  forceSfapiVersion,
40
40
  watch
41
41
  }) {
42
- const { root } = getProjectPaths(appPath);
42
+ const { root } = getProjectPaths(directory);
43
43
  const remixConfig = await getRemixConfig(root);
44
44
  console.log("");
45
45
  const generatedFiles = await codegen({
@@ -6,8 +6,8 @@ import { renderFatalError } from '@shopify/cli-kit/node/ui';
6
6
  import colors from '@shopify/cli-kit/node/colors';
7
7
  import { copyPublicFiles } from './build.js';
8
8
  import { getProjectPaths, assertOxygenChecks, getRemixConfig } from '../../lib/remix-config.js';
9
- import { muteDevLogs, muteRemixLogs, createRemixLogger, enhanceH2Logs } from '../../lib/log.js';
10
- import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
9
+ import { muteDevLogs, createRemixLogger, enhanceH2Logs } from '../../lib/log.js';
10
+ import { commonFlags, deprecated, flagsToCamelObject, DEFAULT_PORT } from '../../lib/flags.js';
11
11
  import Command from '@shopify/cli-kit/node/base-command';
12
12
  import { Flags } from '@oclif/core';
13
13
  import { startMiniOxygen } from '../../lib/mini-oxygen.js';
@@ -16,6 +16,8 @@ import { addVirtualRoutes } from '../../lib/virtual-routes.js';
16
16
  import { spawnCodegenProcess } from '../../lib/codegen.js';
17
17
  import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
18
18
  import { getConfig } from '../../lib/shopify-config.js';
19
+ import { findPort } from '../../lib/find-port.js';
20
+ import { setupLiveReload } from '../../lib/live-reload.js';
19
21
  import { checkRemixVersions } from '../../lib/remix-version-check.js';
20
22
 
21
23
  const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
@@ -56,7 +58,7 @@ class Dev extends Command {
56
58
  }
57
59
  }
58
60
  async function runDev({
59
- port,
61
+ port: portFlag = DEFAULT_PORT,
60
62
  path: appPath,
61
63
  useCodegen = false,
62
64
  codegenConfigPath,
@@ -68,7 +70,6 @@ async function runDev({
68
70
  if (!process.env.NODE_ENV)
69
71
  process.env.NODE_ENV = "development";
70
72
  muteDevLogs();
71
- await muteRemixLogs();
72
73
  if (debug)
73
74
  (await import('node:inspector')).open();
74
75
  const { root, publicPath, buildPathClient, buildPathWorkerFile } = getProjectPaths(appPath);
@@ -102,14 +103,15 @@ async function runDev({
102
103
  let isInitialBuild = true;
103
104
  let initialBuildDurationMs = 0;
104
105
  let initialBuildStartTimeMs = Date.now();
106
+ const liveReload = remixConfig.future.v2_dev ? await setupLiveReload(remixConfig.devServerPort) : void 0;
105
107
  let miniOxygen;
106
108
  async function safeStartMiniOxygen() {
107
109
  if (miniOxygen)
108
110
  return;
109
111
  miniOxygen = await startMiniOxygen({
110
112
  root,
111
- port,
112
- watch: true,
113
+ port: await findPort(portFlag),
114
+ watch: !liveReload,
113
115
  buildPathWorkerFile,
114
116
  buildPathClient,
115
117
  env: await envPromise
@@ -145,13 +147,15 @@ View GraphiQL API browser: ${graphiqlUrl}`)]
145
147
  },
146
148
  {
147
149
  reloadConfig,
148
- onBuildStart() {
150
+ onBuildStart(ctx) {
149
151
  if (!isInitialBuild && !skipRebuildLogs) {
150
152
  outputInfo(LOG_REBUILDING);
151
153
  console.time(LOG_REBUILT);
152
154
  }
155
+ liveReload?.onBuildStart(ctx);
153
156
  },
154
- async onBuildFinish() {
157
+ onBuildManifest: liveReload?.onBuildManifest,
158
+ async onBuildFinish(context, duration, succeeded) {
155
159
  if (isInitialBuild) {
156
160
  await copyingFiles;
157
161
  initialBuildDurationMs = Date.now() - initialBuildStartTimeMs;
@@ -162,16 +166,21 @@ View GraphiQL API browser: ${graphiqlUrl}`)]
162
166
  if (!miniOxygen)
163
167
  console.log("");
164
168
  }
165
- if (!miniOxygen) {
166
- if (!await serverBundleExists()) {
167
- return renderFatalError({
168
- name: "BuildError",
169
- type: 0,
170
- message: "MiniOxygen cannot start because the server bundle has not been generated.",
171
- tryMessage: "This is likely due to an error in your app and Remix is unable to compile. Try fixing the app and MiniOxygen will start."
172
- });
169
+ if (!miniOxygen && !await serverBundleExists()) {
170
+ return renderFatalError({
171
+ name: "BuildError",
172
+ type: 0,
173
+ message: "MiniOxygen cannot start because the server bundle has not been generated.",
174
+ tryMessage: "This is likely due to an error in your app and Remix is unable to compile. Try fixing the app and MiniOxygen will start."
175
+ });
176
+ }
177
+ if (succeeded) {
178
+ if (!miniOxygen) {
179
+ await safeStartMiniOxygen();
180
+ } else if (liveReload) {
181
+ await miniOxygen.reload({ worker: true });
173
182
  }
174
- await safeStartMiniOxygen();
183
+ liveReload?.onAppReady(context);
175
184
  }
176
185
  },
177
186
  async onFileCreated(file) {
@@ -70,6 +70,9 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
70
70
  supressNodeExperimentalWarnings();
71
71
  options.git ??= true;
72
72
  const showUpgrade = await checkHydrogenVersion(
73
+ // Resolving the CLI package from a local directory might fail because
74
+ // this code could be run from a global dependency (e.g. on `npm create`).
75
+ // Therefore, pass the known path to the package.json directly from here:
73
76
  fileURLToPath(new URL("../../../package.json", import.meta.url)),
74
77
  "cli"
75
78
  );
@@ -1,39 +1,30 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import { vi, describe, beforeEach, it, expect } from 'vitest';
3
- import { temporaryDirectoryTask } from 'tempy';
4
3
  import { runInit } from './init.js';
5
4
  import { exec } from '@shopify/cli-kit/node/system';
6
5
  import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
7
- import { readFile, writeFile, isDirectory } from '@shopify/cli-kit/node/fs';
8
- import { basename, joinPath } from '@shopify/cli-kit/node/path';
6
+ import { writeFile, inTemporaryDirectory, readFile, isDirectory, removeFile, fileExists } from '@shopify/cli-kit/node/fs';
7
+ import { joinPath, basename } from '@shopify/cli-kit/node/path';
9
8
  import { checkHydrogenVersion } from '../../lib/check-version.js';
10
9
  import { handleProjectLocation } from '../../lib/onboarding/common.js';
11
10
  import glob from 'fast-glob';
12
11
  import { getSkeletonSourceDir } from '../../lib/build.js';
13
12
  import { execAsync } from '../../lib/process.js';
14
13
  import { rmdir, symlink } from 'fs-extra';
14
+ import { runCheckRoutes } from './check.js';
15
+ import { runCodegen } from './codegen-unstable.js';
16
+ import { runBuild } from './build.js';
15
17
 
18
+ const { renderTasksHook } = vi.hoisted(() => ({ renderTasksHook: vi.fn() }));
19
+ vi.mock("../../lib/check-version.js");
16
20
  vi.mock("../../lib/template-downloader.js", async () => ({
17
- getLatestTemplates: () => Promise.resolve({})
21
+ getLatestTemplates: () => Promise.resolve({
22
+ version: "",
23
+ templatesDir: fileURLToPath(
24
+ new URL("../../../../../templates", import.meta.url)
25
+ )
26
+ })
18
27
  }));
19
- vi.mock(
20
- "@shopify/cli-kit/node/node-package-manager",
21
- async (importOriginal) => {
22
- const original = await importOriginal();
23
- return {
24
- ...original,
25
- installNodeModules: vi.fn(),
26
- getPackageManager: () => Promise.resolve("npm"),
27
- packageManagerUsedForCreating: () => Promise.resolve("npm")
28
- };
29
- }
30
- );
31
- vi.mock("../../lib/check-version.js");
32
- const { renderTasksHook } = vi.hoisted(() => {
33
- return {
34
- renderTasksHook: vi.fn()
35
- };
36
- });
37
28
  vi.mock("@shopify/cli-kit/node/ui", async () => {
38
29
  const original = await vi.importActual("@shopify/cli-kit/node/ui");
39
30
  return {
@@ -48,6 +39,30 @@ vi.mock("@shopify/cli-kit/node/ui", async () => {
48
39
  })
49
40
  };
50
41
  });
42
+ vi.mock(
43
+ "@shopify/cli-kit/node/node-package-manager",
44
+ async (importOriginal) => {
45
+ const original = await importOriginal();
46
+ return {
47
+ ...original,
48
+ getPackageManager: () => Promise.resolve("npm"),
49
+ packageManagerUsedForCreating: () => Promise.resolve("npm"),
50
+ installNodeModules: vi.fn(async ({ directory }) => {
51
+ renderTasksHook.mockImplementationOnce(async () => {
52
+ await writeFile(`${directory}/package-lock.json`, "{}");
53
+ });
54
+ await rmdir(joinPath(directory, "node_modules")).catch(() => {
55
+ });
56
+ await symlink(
57
+ fileURLToPath(
58
+ new URL("../../../../../node_modules", import.meta.url)
59
+ ),
60
+ joinPath(directory, "node_modules")
61
+ );
62
+ })
63
+ };
64
+ }
65
+ );
51
66
  vi.mock("../../lib/onboarding/common.js", async (importOriginal) => {
52
67
  const original = await importOriginal();
53
68
  return Object.keys(original).reduce((acc, item) => {
@@ -65,10 +80,11 @@ describe("init", () => {
65
80
  const outputMock = mockAndCaptureOutput();
66
81
  beforeEach(() => {
67
82
  vi.clearAllMocks();
83
+ vi.unstubAllEnvs();
68
84
  outputMock.clear();
69
85
  });
70
86
  it("checks Hydrogen version", async () => {
71
- await temporaryDirectoryTask(async (tmpDir) => {
87
+ await inTemporaryDirectory(async (tmpDir) => {
72
88
  const showUpgradeMock = vi.fn((param) => ({
73
89
  currentVersion: "1.0.0",
74
90
  newVersion: "1.0.1"
@@ -83,9 +99,90 @@ describe("init", () => {
83
99
  );
84
100
  });
85
101
  });
102
+ describe("remote templates", () => {
103
+ it("throws for unknown templates", async () => {
104
+ await inTemporaryDirectory(async (tmpDir) => {
105
+ await expect(
106
+ runInit({
107
+ path: tmpDir,
108
+ git: false,
109
+ language: "ts",
110
+ template: "https://github.com/some/repo"
111
+ })
112
+ ).rejects.toThrow("supported");
113
+ });
114
+ });
115
+ it("creates basic projects", async () => {
116
+ await inTemporaryDirectory(async (tmpDir) => {
117
+ await runInit({
118
+ path: tmpDir,
119
+ git: false,
120
+ language: "ts",
121
+ template: "hello-world"
122
+ });
123
+ const helloWorldFiles = await glob("**/*", {
124
+ cwd: getSkeletonSourceDir().replace("skeleton", "hello-world"),
125
+ ignore: ["**/node_modules/**", "**/dist/**"]
126
+ });
127
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
128
+ const nonAppFiles = helloWorldFiles.filter(
129
+ (item) => !item.startsWith("app/")
130
+ );
131
+ expect(projectFiles).toEqual(expect.arrayContaining(nonAppFiles));
132
+ expect(projectFiles).toContain("app/root.tsx");
133
+ expect(projectFiles).toContain("app/entry.client.tsx");
134
+ expect(projectFiles).toContain("app/entry.server.tsx");
135
+ expect(projectFiles).not.toContain("app/components/Layout.tsx");
136
+ expect(projectFiles).not.toContain("app/routes/_index.tsx");
137
+ await expect(readFile(`${tmpDir}/package.json`)).resolves.toMatch(
138
+ `"name": "hello-world"`
139
+ );
140
+ const output = outputMock.info();
141
+ expect(output).toMatch("success");
142
+ expect(output).not.toMatch("warning");
143
+ expect(output).not.toMatch("Routes");
144
+ expect(output).toMatch(/Language:\s*TypeScript/);
145
+ expect(output).toMatch("Help");
146
+ expect(output).toMatch("Next steps");
147
+ expect(output).toMatch(
148
+ // Output contains banner characters. USe [^\w]*? to match them.
149
+ /Run `cd .*? &&[^\w]*?npm[^\w]*?install[^\w]*?&&[^\w]*?npm[^\w]*?run[^\w]*?dev`/ims
150
+ );
151
+ });
152
+ });
153
+ it("transpiles projects to JS", async () => {
154
+ await inTemporaryDirectory(async (tmpDir) => {
155
+ await runInit({
156
+ path: tmpDir,
157
+ git: false,
158
+ language: "js",
159
+ template: "hello-world"
160
+ });
161
+ const helloWorldFiles = await glob("**/*", {
162
+ cwd: getSkeletonSourceDir().replace("skeleton", "hello-world"),
163
+ ignore: ["**/node_modules/**", "**/dist/**"]
164
+ });
165
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
166
+ expect(projectFiles).toEqual(
167
+ expect.arrayContaining(
168
+ helloWorldFiles.filter((item) => !item.endsWith(".d.ts")).map(
169
+ (item) => item.replace(/\.ts(x)?$/, ".js$1").replace(/tsconfig\.json$/, "jsconfig.json")
170
+ )
171
+ )
172
+ );
173
+ await expect(readFile(`${tmpDir}/server.js`)).resolves.toMatch(
174
+ /export default {\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/
175
+ );
176
+ const output = outputMock.info();
177
+ expect(output).toMatch("success");
178
+ expect(output).not.toMatch("warning");
179
+ expect(output).toMatch(/Language:\s*JavaScript/);
180
+ });
181
+ });
182
+ });
86
183
  describe("local templates", () => {
87
184
  it("creates basic projects", async () => {
88
- await temporaryDirectoryTask(async (tmpDir) => {
185
+ await inTemporaryDirectory(async (tmpDir) => {
89
186
  await runInit({
90
187
  path: tmpDir,
91
188
  git: false,
@@ -124,12 +221,13 @@ describe("init", () => {
124
221
  expect(output).toMatch("Help");
125
222
  expect(output).toMatch("Next steps");
126
223
  expect(output).toMatch(
224
+ // Output contains banner characters. USe [^\w]*? to match them.
127
225
  /Run `cd .*? &&[^\w]*?npm[^\w]*?install[^\w]*?&&[^\w]*?npm[^\w]*?run[^\w]*?dev`/ims
128
226
  );
129
227
  });
130
228
  });
131
229
  it("creates projects with route files", async () => {
132
- await temporaryDirectoryTask(async (tmpDir) => {
230
+ await inTemporaryDirectory(async (tmpDir) => {
133
231
  await runInit({ path: tmpDir, git: false, routes: true, language: "ts" });
134
232
  const skeletonFiles = await glob("**/*", {
135
233
  cwd: getSkeletonSourceDir(),
@@ -152,7 +250,7 @@ describe("init", () => {
152
250
  });
153
251
  });
154
252
  it("transpiles projects to JS", async () => {
155
- await temporaryDirectoryTask(async (tmpDir) => {
253
+ await inTemporaryDirectory(async (tmpDir) => {
156
254
  await runInit({ path: tmpDir, git: false, routes: true, language: "js" });
157
255
  const skeletonFiles = await glob("**/*", {
158
256
  cwd: getSkeletonSourceDir(),
@@ -182,7 +280,7 @@ describe("init", () => {
182
280
  });
183
281
  describe("styling libraries", () => {
184
282
  it("scaffolds Tailwind CSS", async () => {
185
- await temporaryDirectoryTask(async (tmpDir) => {
283
+ await inTemporaryDirectory(async (tmpDir) => {
186
284
  await runInit({
187
285
  path: tmpDir,
188
286
  git: false,
@@ -207,7 +305,7 @@ describe("init", () => {
207
305
  });
208
306
  });
209
307
  it("scaffolds CSS Modules", async () => {
210
- await temporaryDirectoryTask(async (tmpDir) => {
308
+ await inTemporaryDirectory(async (tmpDir) => {
211
309
  await runInit({
212
310
  path: tmpDir,
213
311
  git: false,
@@ -229,7 +327,7 @@ describe("init", () => {
229
327
  });
230
328
  });
231
329
  it("scaffolds Vanilla Extract", async () => {
232
- await temporaryDirectoryTask(async (tmpDir) => {
330
+ await inTemporaryDirectory(async (tmpDir) => {
233
331
  await runInit({
234
332
  path: tmpDir,
235
333
  git: false,
@@ -253,7 +351,7 @@ describe("init", () => {
253
351
  });
254
352
  describe("i18n strategies", () => {
255
353
  it("scaffolds i18n with domains strategy", async () => {
256
- await temporaryDirectoryTask(async (tmpDir) => {
354
+ await inTemporaryDirectory(async (tmpDir) => {
257
355
  await runInit({
258
356
  path: tmpDir,
259
357
  git: false,
@@ -273,7 +371,7 @@ describe("init", () => {
273
371
  });
274
372
  });
275
373
  it("scaffolds i18n with subdomains strategy", async () => {
276
- await temporaryDirectoryTask(async (tmpDir) => {
374
+ await inTemporaryDirectory(async (tmpDir) => {
277
375
  await runInit({
278
376
  path: tmpDir,
279
377
  git: false,
@@ -293,7 +391,7 @@ describe("init", () => {
293
391
  });
294
392
  });
295
393
  it("scaffolds i18n with subfolders strategy", async () => {
296
- await temporaryDirectoryTask(async (tmpDir) => {
394
+ await inTemporaryDirectory(async (tmpDir) => {
297
395
  await runInit({
298
396
  path: tmpDir,
299
397
  git: false,
@@ -315,10 +413,7 @@ describe("init", () => {
315
413
  });
316
414
  describe("git", () => {
317
415
  it("initializes a git repository and creates initial commits", async () => {
318
- await temporaryDirectoryTask(async (tmpDir) => {
319
- renderTasksHook.mockImplementationOnce(async () => {
320
- await writeFile(`${tmpDir}/package-lock.json`, "{}");
321
- });
416
+ await inTemporaryDirectory(async (tmpDir) => {
322
417
  await runInit({
323
418
  path: tmpDir,
324
419
  git: true,
@@ -345,10 +440,7 @@ describe("init", () => {
345
440
  });
346
441
  describe("project validity", () => {
347
442
  it("typechecks the project", async () => {
348
- await temporaryDirectoryTask(async (tmpDir) => {
349
- renderTasksHook.mockImplementationOnce(async () => {
350
- await writeFile(`${tmpDir}/package-lock.json`, "{}");
351
- });
443
+ await inTemporaryDirectory(async (tmpDir) => {
352
444
  await runInit({
353
445
  path: tmpDir,
354
446
  git: true,
@@ -358,21 +450,77 @@ describe("init", () => {
358
450
  routes: true,
359
451
  installDeps: true
360
452
  });
361
- await rmdir(joinPath(tmpDir, "node_modules")).catch(() => {
362
- });
363
- await symlink(
364
- fileURLToPath(
365
- new URL("../../../../../node_modules", import.meta.url)
366
- ),
367
- joinPath(tmpDir, "node_modules")
368
- );
369
453
  await expect(
370
- exec("npm", ["run", "typecheck"], {
371
- cwd: tmpDir
372
- })
454
+ exec("npm", ["run", "typecheck"], { cwd: tmpDir })
373
455
  ).resolves.not.toThrow();
374
456
  });
375
457
  });
458
+ it("contains all standard routes", async () => {
459
+ await inTemporaryDirectory(async (tmpDir) => {
460
+ await runInit({
461
+ path: tmpDir,
462
+ git: true,
463
+ language: "ts",
464
+ i18n: "subfolders",
465
+ routes: true,
466
+ installDeps: true
467
+ });
468
+ outputMock.clear();
469
+ await runCheckRoutes({ directory: tmpDir });
470
+ const output = outputMock.info();
471
+ expect(output).toMatch("success");
472
+ });
473
+ });
474
+ it("supports codegen", async () => {
475
+ await inTemporaryDirectory(async (tmpDir) => {
476
+ await runInit({
477
+ path: tmpDir,
478
+ git: true,
479
+ language: "ts",
480
+ routes: true,
481
+ installDeps: true
482
+ });
483
+ outputMock.clear();
484
+ const codegenFile = `${tmpDir}/storefrontapi.generated.d.ts`;
485
+ const codegenFromTemplate = await readFile(codegenFile);
486
+ expect(codegenFromTemplate).toBeTruthy();
487
+ await removeFile(codegenFile);
488
+ expect(fileExists(codegenFile)).resolves.toBeFalsy();
489
+ await expect(runCodegen({ directory: tmpDir })).resolves.not.toThrow();
490
+ const output = outputMock.info();
491
+ expect(output).toMatch("success");
492
+ await expect(readFile(codegenFile)).resolves.toEqual(
493
+ codegenFromTemplate
494
+ );
495
+ });
496
+ });
497
+ it("builds the generated project", async () => {
498
+ await inTemporaryDirectory(async (tmpDir) => {
499
+ await runInit({
500
+ path: tmpDir,
501
+ git: true,
502
+ language: "ts",
503
+ styling: "postcss",
504
+ i18n: "subfolders",
505
+ routes: true,
506
+ installDeps: true
507
+ });
508
+ outputMock.clear();
509
+ vi.stubEnv("NODE_ENV", "production");
510
+ await expect(runBuild({ directory: tmpDir })).resolves.not.toThrow();
511
+ const expectedBundlePath = "dist/worker/index.js";
512
+ const output = outputMock.output();
513
+ expect(output).toMatch(expectedBundlePath);
514
+ expect(
515
+ fileExists(joinPath(tmpDir, expectedBundlePath))
516
+ ).resolves.toBeTruthy();
517
+ const mb = Number(
518
+ output.match(/index\.js\s+([\d.]+)\s+MB/)?.[1] || ""
519
+ );
520
+ expect(mb).toBeGreaterThan(0);
521
+ expect(mb).toBeLessThan(1);
522
+ });
523
+ });
376
524
  });
377
525
  });
378
526
  });
@@ -1,8 +1,9 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
2
  import { muteDevLogs } from '../../lib/log.js';
3
3
  import { getProjectPaths } from '../../lib/remix-config.js';
4
- import { commonFlags } from '../../lib/flags.js';
4
+ import { commonFlags, DEFAULT_PORT } from '../../lib/flags.js';
5
5
  import { startMiniOxygen } from '../../lib/mini-oxygen.js';
6
+ import { findPort } from '../../lib/find-port.js';
6
7
 
7
8
  class Preview extends Command {
8
9
  static description = "Runs a Hydrogen storefront in an Oxygen worker for production.";
@@ -16,7 +17,7 @@ class Preview extends Command {
16
17
  }
17
18
  }
18
19
  async function runPreview({
19
- port,
20
+ port = DEFAULT_PORT,
20
21
  path: appPath
21
22
  }) {
22
23
  if (!process.env.NODE_ENV)
@@ -25,7 +26,7 @@ async function runPreview({
25
26
  const { root, buildPathWorkerFile, buildPathClient } = getProjectPaths(appPath);
26
27
  const miniOxygen = await startMiniOxygen({
27
28
  root,
28
- port,
29
+ port: await findPort(port),
29
30
  buildPathClient,
30
31
  buildPathWorkerFile
31
32
  });
@@ -44,7 +44,7 @@ async function runSetup(options) {
44
44
  }
45
45
  }
46
46
  ];
47
- const i18nStrategy = options.i18n ? options.i18n : await renderI18nPrompt({
47
+ const i18nStrategy = options.markets ? options.markets : await renderI18nPrompt({
48
48
  abortSignal: controller.signal,
49
49
  extraChoices: { none: "Set up later" }
50
50
  });
@@ -57,6 +57,8 @@ async function runSetup(options) {
57
57
  const typescript = !!remixConfig.tsconfigPath;
58
58
  backgroundWorkPromise = backgroundWorkPromise.then(
59
59
  () => Promise.all([
60
+ // When starting from hello-world, the server entry point won't
61
+ // include all the cart logic from skeleton, so we need to copy it.
60
62
  generateProjectFile("../server.ts", { ...remixConfig, typescript }),
61
63
  ...typescript ? [
62
64
  copyFile(
@@ -71,6 +73,7 @@ async function runSetup(options) {
71
73
  )
72
74
  )
73
75
  ] : [],
76
+ // Copy app entries
74
77
  generateProjectEntries({
75
78
  rootDirectory: remixConfig.rootDirectory,
76
79
  appDirectory: remixConfig.appDirectory,
@@ -130,4 +133,4 @@ async function runSetup(options) {
130
133
  );
131
134
  }
132
135
 
133
- export { Setup as default };
136
+ export { Setup as default, runSetup };