@shopify/cli-hydrogen 5.1.0 → 5.1.2

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.
@@ -5,11 +5,11 @@ import { rmdir, fileSize, glob, removeFile, copyFile } from '@shopify/cli-kit/no
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';
8
- import { getProjectPaths, getRemixConfig } from '../../lib/config.js';
8
+ import { getProjectPaths, getRemixConfig, assertOxygenChecks } from '../../lib/remix-config.js';
9
9
  import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
10
10
  import { checkLockfileStatus } from '../../lib/check-lockfile.js';
11
11
  import { findMissingRoutes } from '../../lib/missing-routes.js';
12
- import { warnOnce } from '../../lib/log.js';
12
+ import { muteRemixLogs, createRemixLogger } from '../../lib/log.js';
13
13
  import { codegen } from '../../lib/codegen.js';
14
14
 
15
15
  const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
@@ -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,8 +56,8 @@ 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);
60
- await checkLockfileStatus(root);
59
+ const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
60
+ await Promise.all([checkLockfileStatus(root), muteRemixLogs()]);
61
61
  console.time(LOG_WORKER_BUILT);
62
62
  outputInfo(`
63
63
  \u{1F3D7}\uFE0F Building in ${process.env.NODE_ENV} mode...`);
@@ -68,15 +68,16 @@ async function runBuild({
68
68
  import('@remix-run/dev/dist/compiler/fileWatchCache.js'),
69
69
  rmdir(buildPath, { force: true })
70
70
  ]);
71
+ assertOxygenChecks(remixConfig);
71
72
  await Promise.all([
72
73
  copyPublicFiles(publicPath, buildPathClient),
73
74
  build({
74
75
  config: remixConfig,
75
76
  options: {
76
77
  mode: process.env.NODE_ENV,
77
- onWarning: warnOnce,
78
78
  sourcemap
79
79
  },
80
+ logger: createRemixLogger(),
80
81
  fileWatchCache: createFileWatchCache()
81
82
  }).catch((thrown) => {
82
83
  logThrown(thrown);
@@ -124,7 +125,9 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
124
125
  );
125
126
  }
126
127
  }
127
- process.exit(0);
128
+ if (!process.env.SHOPIFY_UNIT_TEST) {
129
+ process.exit(0);
130
+ }
128
131
  }
129
132
  async function copyPublicFiles(publicPath, buildPathClient) {
130
133
  return copyFile(publicPath, buildPathClient);
@@ -1,7 +1,7 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
2
  import { resolvePath } from '@shopify/cli-kit/node/path';
3
3
  import { commonFlags } from '../../lib/flags.js';
4
- import { getRemixConfig } from '../../lib/config.js';
4
+ import { getRemixConfig } from '../../lib/remix-config.js';
5
5
  import { logMissingRoutes, findMissingRoutes } from '../../lib/missing-routes.js';
6
6
  import { Args } from '@oclif/core';
7
7
 
@@ -29,8 +29,8 @@ class GenerateRoute extends Command {
29
29
  }
30
30
  }
31
31
  async function runCheckRoutes({ directory }) {
32
- const remixConfig = await getRemixConfig(directory, true);
32
+ const remixConfig = await getRemixConfig(directory);
33
33
  logMissingRoutes(findMissingRoutes(remixConfig));
34
34
  }
35
35
 
36
- export { GenerateRoute as default };
36
+ export { GenerateRoute as default, runCheckRoutes };
@@ -2,7 +2,7 @@ import path from 'path';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
3
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
4
4
  import { Flags } from '@oclif/core';
5
- import { getProjectPaths, getRemixConfig } from '../../lib/config.js';
5
+ import { getProjectPaths, getRemixConfig } from '../../lib/remix-config.js';
6
6
  import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
7
7
  import { codegen } from '../../lib/codegen.js';
8
8
 
@@ -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({
@@ -5,8 +5,8 @@ import { fileExists } from '@shopify/cli-kit/node/fs';
5
5
  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
- import { getProjectPaths, getRemixConfig } from '../../lib/config.js';
9
- import { muteDevLogs, warnOnce, enhanceH2Logs } from '../../lib/log.js';
8
+ import { getProjectPaths, assertOxygenChecks, getRemixConfig } from '../../lib/remix-config.js';
9
+ import { muteDevLogs, muteRemixLogs, createRemixLogger, enhanceH2Logs } from '../../lib/log.js';
10
10
  import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
11
11
  import Command from '@shopify/cli-kit/node/base-command';
12
12
  import { Flags } from '@oclif/core';
@@ -16,6 +16,7 @@ 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 { checkRemixVersions } from '../../lib/remix-version-check.js';
19
20
 
20
21
  const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
21
22
  const LOG_REBUILT = "\u{1F680} Rebuilt";
@@ -67,6 +68,7 @@ async function runDev({
67
68
  if (!process.env.NODE_ENV)
68
69
  process.env.NODE_ENV = "development";
69
70
  muteDevLogs();
71
+ await muteRemixLogs();
70
72
  if (debug)
71
73
  (await import('node:inspector')).open();
72
74
  const { root, publicPath, buildPathClient, buildPathWorkerFile } = getProjectPaths(appPath);
@@ -86,7 +88,11 @@ async function runDev({
86
88
  return [fileRelative, path.resolve(root, fileRelative)];
87
89
  };
88
90
  const serverBundleExists = () => fileExists(buildPathWorkerFile);
89
- const { shop, storefront } = await getConfig(root);
91
+ const [remixConfig, { shop, storefront }] = await Promise.all([
92
+ reloadConfig(),
93
+ getConfig(root)
94
+ ]);
95
+ assertOxygenChecks(remixConfig);
90
96
  const fetchRemote = !!shop && !!storefront?.id;
91
97
  const envPromise = getAllEnvironmentVariables({ root, fetchRemote, envBranch });
92
98
  const [{ watch }, { createFileWatchCache }] = await Promise.all([
@@ -120,11 +126,11 @@ View GraphiQL API browser: ${graphiqlUrl}`)]
120
126
  if (useCodegen) {
121
127
  spawnCodegenProcess({ ...remixConfig, configFilePath: codegenConfigPath });
122
128
  }
129
+ checkRemixVersions();
123
130
  const showUpgrade = await checkingHydrogenVersion;
124
131
  if (showUpgrade)
125
132
  showUpgrade();
126
133
  }
127
- const remixConfig = await reloadConfig();
128
134
  const fileWatchCache = createFileWatchCache();
129
135
  let skipRebuildLogs = false;
130
136
  await watch(
@@ -132,10 +138,10 @@ View GraphiQL API browser: ${graphiqlUrl}`)]
132
138
  config: remixConfig,
133
139
  options: {
134
140
  mode: process.env.NODE_ENV,
135
- onWarning: warnOnce,
136
141
  sourcemap
137
142
  },
138
- fileWatchCache
143
+ fileWatchCache,
144
+ logger: createRemixLogger()
139
145
  },
140
146
  {
141
147
  reloadConfig,
@@ -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,89 @@ 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
+ /Run `cd .*? &&[^\w]*?npm[^\w]*?install[^\w]*?&&[^\w]*?npm[^\w]*?run[^\w]*?dev`/ims
149
+ );
150
+ });
151
+ });
152
+ it("transpiles projects to JS", async () => {
153
+ await inTemporaryDirectory(async (tmpDir) => {
154
+ await runInit({
155
+ path: tmpDir,
156
+ git: false,
157
+ language: "js",
158
+ template: "hello-world"
159
+ });
160
+ const helloWorldFiles = await glob("**/*", {
161
+ cwd: getSkeletonSourceDir().replace("skeleton", "hello-world"),
162
+ ignore: ["**/node_modules/**", "**/dist/**"]
163
+ });
164
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
165
+ expect(projectFiles).toEqual(
166
+ expect.arrayContaining(
167
+ helloWorldFiles.filter((item) => !item.endsWith(".d.ts")).map(
168
+ (item) => item.replace(/\.ts(x)?$/, ".js$1").replace(/tsconfig\.json$/, "jsconfig.json")
169
+ )
170
+ )
171
+ );
172
+ await expect(readFile(`${tmpDir}/server.js`)).resolves.toMatch(
173
+ /export default {\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/
174
+ );
175
+ const output = outputMock.info();
176
+ expect(output).toMatch("success");
177
+ expect(output).not.toMatch("warning");
178
+ expect(output).toMatch(/Language:\s*JavaScript/);
179
+ });
180
+ });
181
+ });
86
182
  describe("local templates", () => {
87
183
  it("creates basic projects", async () => {
88
- await temporaryDirectoryTask(async (tmpDir) => {
184
+ await inTemporaryDirectory(async (tmpDir) => {
89
185
  await runInit({
90
186
  path: tmpDir,
91
187
  git: false,
@@ -129,7 +225,7 @@ describe("init", () => {
129
225
  });
130
226
  });
131
227
  it("creates projects with route files", async () => {
132
- await temporaryDirectoryTask(async (tmpDir) => {
228
+ await inTemporaryDirectory(async (tmpDir) => {
133
229
  await runInit({ path: tmpDir, git: false, routes: true, language: "ts" });
134
230
  const skeletonFiles = await glob("**/*", {
135
231
  cwd: getSkeletonSourceDir(),
@@ -152,7 +248,7 @@ describe("init", () => {
152
248
  });
153
249
  });
154
250
  it("transpiles projects to JS", async () => {
155
- await temporaryDirectoryTask(async (tmpDir) => {
251
+ await inTemporaryDirectory(async (tmpDir) => {
156
252
  await runInit({ path: tmpDir, git: false, routes: true, language: "js" });
157
253
  const skeletonFiles = await glob("**/*", {
158
254
  cwd: getSkeletonSourceDir(),
@@ -182,7 +278,7 @@ describe("init", () => {
182
278
  });
183
279
  describe("styling libraries", () => {
184
280
  it("scaffolds Tailwind CSS", async () => {
185
- await temporaryDirectoryTask(async (tmpDir) => {
281
+ await inTemporaryDirectory(async (tmpDir) => {
186
282
  await runInit({
187
283
  path: tmpDir,
188
284
  git: false,
@@ -207,7 +303,7 @@ describe("init", () => {
207
303
  });
208
304
  });
209
305
  it("scaffolds CSS Modules", async () => {
210
- await temporaryDirectoryTask(async (tmpDir) => {
306
+ await inTemporaryDirectory(async (tmpDir) => {
211
307
  await runInit({
212
308
  path: tmpDir,
213
309
  git: false,
@@ -229,7 +325,7 @@ describe("init", () => {
229
325
  });
230
326
  });
231
327
  it("scaffolds Vanilla Extract", async () => {
232
- await temporaryDirectoryTask(async (tmpDir) => {
328
+ await inTemporaryDirectory(async (tmpDir) => {
233
329
  await runInit({
234
330
  path: tmpDir,
235
331
  git: false,
@@ -253,7 +349,7 @@ describe("init", () => {
253
349
  });
254
350
  describe("i18n strategies", () => {
255
351
  it("scaffolds i18n with domains strategy", async () => {
256
- await temporaryDirectoryTask(async (tmpDir) => {
352
+ await inTemporaryDirectory(async (tmpDir) => {
257
353
  await runInit({
258
354
  path: tmpDir,
259
355
  git: false,
@@ -273,7 +369,7 @@ describe("init", () => {
273
369
  });
274
370
  });
275
371
  it("scaffolds i18n with subdomains strategy", async () => {
276
- await temporaryDirectoryTask(async (tmpDir) => {
372
+ await inTemporaryDirectory(async (tmpDir) => {
277
373
  await runInit({
278
374
  path: tmpDir,
279
375
  git: false,
@@ -293,7 +389,7 @@ describe("init", () => {
293
389
  });
294
390
  });
295
391
  it("scaffolds i18n with subfolders strategy", async () => {
296
- await temporaryDirectoryTask(async (tmpDir) => {
392
+ await inTemporaryDirectory(async (tmpDir) => {
297
393
  await runInit({
298
394
  path: tmpDir,
299
395
  git: false,
@@ -315,10 +411,7 @@ describe("init", () => {
315
411
  });
316
412
  describe("git", () => {
317
413
  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
- });
414
+ await inTemporaryDirectory(async (tmpDir) => {
322
415
  await runInit({
323
416
  path: tmpDir,
324
417
  git: true,
@@ -345,10 +438,7 @@ describe("init", () => {
345
438
  });
346
439
  describe("project validity", () => {
347
440
  it("typechecks the project", async () => {
348
- await temporaryDirectoryTask(async (tmpDir) => {
349
- renderTasksHook.mockImplementationOnce(async () => {
350
- await writeFile(`${tmpDir}/package-lock.json`, "{}");
351
- });
441
+ await inTemporaryDirectory(async (tmpDir) => {
352
442
  await runInit({
353
443
  path: tmpDir,
354
444
  git: true,
@@ -358,21 +448,77 @@ describe("init", () => {
358
448
  routes: true,
359
449
  installDeps: true
360
450
  });
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
451
  await expect(
370
- exec("npm", ["run", "typecheck"], {
371
- cwd: tmpDir
372
- })
452
+ exec("npm", ["run", "typecheck"], { cwd: tmpDir })
373
453
  ).resolves.not.toThrow();
374
454
  });
375
455
  });
456
+ it("contains all standard routes", async () => {
457
+ await inTemporaryDirectory(async (tmpDir) => {
458
+ await runInit({
459
+ path: tmpDir,
460
+ git: true,
461
+ language: "ts",
462
+ i18n: "subfolders",
463
+ routes: true,
464
+ installDeps: true
465
+ });
466
+ outputMock.clear();
467
+ await runCheckRoutes({ directory: tmpDir });
468
+ const output = outputMock.info();
469
+ expect(output).toMatch("success");
470
+ });
471
+ });
472
+ it("supports codegen", async () => {
473
+ await inTemporaryDirectory(async (tmpDir) => {
474
+ await runInit({
475
+ path: tmpDir,
476
+ git: true,
477
+ language: "ts",
478
+ routes: true,
479
+ installDeps: true
480
+ });
481
+ outputMock.clear();
482
+ const codegenFile = `${tmpDir}/storefrontapi.generated.d.ts`;
483
+ const codegenFromTemplate = await readFile(codegenFile);
484
+ expect(codegenFromTemplate).toBeTruthy();
485
+ await removeFile(codegenFile);
486
+ expect(fileExists(codegenFile)).resolves.toBeFalsy();
487
+ await expect(runCodegen({ directory: tmpDir })).resolves.not.toThrow();
488
+ const output = outputMock.info();
489
+ expect(output).toMatch("success");
490
+ await expect(readFile(codegenFile)).resolves.toEqual(
491
+ codegenFromTemplate
492
+ );
493
+ });
494
+ });
495
+ it("builds the generated project", async () => {
496
+ await inTemporaryDirectory(async (tmpDir) => {
497
+ await runInit({
498
+ path: tmpDir,
499
+ git: true,
500
+ language: "ts",
501
+ styling: "postcss",
502
+ i18n: "subfolders",
503
+ routes: true,
504
+ installDeps: true
505
+ });
506
+ outputMock.clear();
507
+ vi.stubEnv("NODE_ENV", "production");
508
+ await expect(runBuild({ directory: tmpDir })).resolves.not.toThrow();
509
+ const expectedBundlePath = "dist/worker/index.js";
510
+ const output = outputMock.output();
511
+ expect(output).toMatch(expectedBundlePath);
512
+ expect(
513
+ fileExists(joinPath(tmpDir, expectedBundlePath))
514
+ ).resolves.toBeTruthy();
515
+ const mb = Number(
516
+ output.match(/index\.js\s+([\d.]+)\s+MB/)?.[1] || ""
517
+ );
518
+ expect(mb).toBeGreaterThan(0);
519
+ expect(mb).toBeLessThan(1);
520
+ });
521
+ });
376
522
  });
377
523
  });
378
524
  });
@@ -1,6 +1,6 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
2
  import { muteDevLogs } from '../../lib/log.js';
3
- import { getProjectPaths } from '../../lib/config.js';
3
+ import { getProjectPaths } from '../../lib/remix-config.js';
4
4
  import { commonFlags } from '../../lib/flags.js';
5
5
  import { startMiniOxygen } from '../../lib/mini-oxygen.js';
6
6
 
@@ -4,7 +4,7 @@ import Command from '@shopify/cli-kit/node/base-command';
4
4
  import { renderTasks, renderSuccess } from '@shopify/cli-kit/node/ui';
5
5
  import { getPackageManager, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
6
6
  import { Args } from '@oclif/core';
7
- import { getRemixConfig } from '../../../lib/config.js';
7
+ import { getRemixConfig } from '../../../lib/remix-config.js';
8
8
  import { SETUP_CSS_STRATEGIES, renderCssPrompt, setupCssStrategy, CSS_STRATEGY_NAME_MAP } from '../../../lib/setups/css/index.js';
9
9
 
10
10
  class SetupCSS extends Command {
@@ -3,7 +3,7 @@ import { commonFlags, flagsToCamelObject } from '../../../lib/flags.js';
3
3
  import Command from '@shopify/cli-kit/node/base-command';
4
4
  import { renderTasks, renderSuccess } from '@shopify/cli-kit/node/ui';
5
5
  import { Args } from '@oclif/core';
6
- import { getRemixConfig } from '../../../lib/config.js';
6
+ import { getRemixConfig } from '../../../lib/remix-config.js';
7
7
  import { SETUP_I18N_STRATEGIES, renderI18nPrompt, setupI18nStrategy, I18N_STRATEGY_NAME_MAP } from '../../../lib/setups/i18n/index.js';
8
8
 
9
9
  class SetupMarkets extends Command {
@@ -5,7 +5,7 @@ import { resolvePath, basename } from '@shopify/cli-kit/node/path';
5
5
  import { copyFile } from '@shopify/cli-kit/node/fs';
6
6
  import { commonFlags, overrideFlag, flagsToCamelObject } from '../../lib/flags.js';
7
7
  import { renderI18nPrompt, setupI18nStrategy } from '../../lib/setups/i18n/index.js';
8
- import { getRemixConfig } from '../../lib/config.js';
8
+ import { getRemixConfig } from '../../lib/remix-config.js';
9
9
  import { handleRouteGeneration, generateProjectEntries, handleCliShortcut, renderProjectReady } from '../../lib/onboarding/common.js';
10
10
  import { getCliCommand } from '../../lib/shell.js';
11
11
  import { generateProjectFile } from '../../lib/setups/routes/generate.js';
@@ -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
  });
@@ -130,4 +130,4 @@ async function runSetup(options) {
130
130
  );
131
131
  }
132
132
 
133
- export { Setup as default };
133
+ export { Setup as default, runSetup };
@@ -0,0 +1,62 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { vi, describe, beforeEach, it, expect } from 'vitest';
3
+ import { inTemporaryDirectory, copyFile, fileExists, readFile } from '@shopify/cli-kit/node/fs';
4
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
6
+ import { runSetup } from './setup.js';
7
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
8
+
9
+ vi.mock("../../lib/shell.js");
10
+ vi.mock("@shopify/cli-kit/node/ui", async () => {
11
+ const original = await vi.importActual("@shopify/cli-kit/node/ui");
12
+ return {
13
+ ...original,
14
+ renderConfirmationPrompt: vi.fn(),
15
+ renderSelectPrompt: vi.fn(),
16
+ renderTextPrompt: vi.fn(),
17
+ renderInfo: vi.fn()
18
+ };
19
+ });
20
+ describe("setup", () => {
21
+ const outputMock = mockAndCaptureOutput();
22
+ beforeEach(() => {
23
+ vi.resetAllMocks();
24
+ });
25
+ beforeEach(() => {
26
+ outputMock.clear();
27
+ });
28
+ it("sets up an i18n strategy and generates routes", async () => {
29
+ await inTemporaryDirectory(async (tmpDir) => {
30
+ await copyFile(
31
+ fileURLToPath(
32
+ new URL("../../../../../templates/hello-world", import.meta.url)
33
+ ),
34
+ tmpDir
35
+ );
36
+ await expect(
37
+ fileExists(joinPath(tmpDir, "app/routes/_index.tsx"))
38
+ ).resolves.toBeFalsy();
39
+ vi.mocked(renderConfirmationPrompt).mockResolvedValueOnce(true);
40
+ await expect(
41
+ runSetup({
42
+ directory: tmpDir,
43
+ markets: "subfolders",
44
+ installDeps: false
45
+ })
46
+ ).resolves.not.toThrow();
47
+ await expect(
48
+ fileExists(joinPath(tmpDir, "app/routes/($locale)._index.tsx"))
49
+ ).resolves.toBeTruthy();
50
+ const serverFile = await readFile(`${tmpDir}/server.ts`);
51
+ expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
52
+ expect(serverFile).toMatch(/url.pathname/);
53
+ const output = outputMock.info();
54
+ expect(output).toMatch("success");
55
+ expect(output).not.toMatch("warning");
56
+ expect(output).toMatch(/Markets:\s*Subfolders/);
57
+ expect(output).toMatch("Routes");
58
+ expect(output).toMatch("Home (/ & /:catchAll)");
59
+ expect(output).toMatch("Account (/account/*)");
60
+ });
61
+ });
62
+ });