@shopify/cli-hydrogen 6.0.2 → 7.0.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 (108) hide show
  1. package/dist/commands/hydrogen/build.js +40 -78
  2. package/dist/commands/hydrogen/codegen.js +8 -3
  3. package/dist/commands/hydrogen/deploy.js +173 -37
  4. package/dist/commands/hydrogen/deploy.test.js +192 -20
  5. package/dist/commands/hydrogen/dev.js +56 -31
  6. package/dist/commands/hydrogen/init.js +1 -1
  7. package/dist/commands/hydrogen/init.test.js +155 -53
  8. package/dist/commands/hydrogen/link.js +5 -21
  9. package/dist/commands/hydrogen/link.test.js +10 -10
  10. package/dist/commands/hydrogen/preview.js +22 -11
  11. package/dist/commands/hydrogen/setup.js +0 -4
  12. package/dist/commands/hydrogen/setup.test.js +0 -1
  13. package/dist/commands/hydrogen/shortcut.js +1 -0
  14. package/dist/commands/hydrogen/upgrade.js +720 -0
  15. package/dist/commands/hydrogen/upgrade.test.js +786 -0
  16. package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
  17. package/dist/generator-templates/starter/CHANGELOG.md +126 -0
  18. package/dist/generator-templates/starter/README.md +23 -0
  19. package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
  20. package/dist/generator-templates/starter/app/components/Footer.tsx +3 -1
  21. package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
  22. package/dist/generator-templates/starter/app/components/Layout.tsx +14 -11
  23. package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
  24. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
  25. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
  26. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
  27. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
  28. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
  29. package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
  30. package/dist/generator-templates/starter/app/lib/session.ts +67 -0
  31. package/dist/generator-templates/starter/app/root.tsx +11 -45
  32. package/dist/generator-templates/starter/app/routes/[robots.txt].tsx +0 -27
  33. package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
  34. package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
  35. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
  36. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
  37. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
  38. package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
  39. package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
  40. package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
  41. package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
  42. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
  43. package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
  44. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
  45. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
  46. package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
  47. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
  48. package/dist/generator-templates/starter/package.json +11 -10
  49. package/dist/generator-templates/starter/remix.config.js +4 -0
  50. package/dist/generator-templates/starter/remix.env.d.ts +6 -11
  51. package/dist/generator-templates/starter/server.ts +24 -167
  52. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
  53. package/dist/hooks/init.js +4 -4
  54. package/dist/lib/auth.js +5 -10
  55. package/dist/lib/build.js +6 -1
  56. package/dist/lib/bundle/analyzer.js +36 -26
  57. package/dist/lib/check-lockfile.js +1 -0
  58. package/dist/lib/codegen.js +59 -18
  59. package/dist/lib/defer.js +12 -0
  60. package/dist/lib/file.js +52 -3
  61. package/dist/lib/flags.js +27 -9
  62. package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
  63. package/dist/lib/graphql/admin/client.test.js +2 -2
  64. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  65. package/dist/lib/log.js +32 -14
  66. package/dist/lib/mini-oxygen/assets.js +118 -0
  67. package/dist/lib/mini-oxygen/common.js +2 -1
  68. package/dist/lib/mini-oxygen/index.js +7 -5
  69. package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
  70. package/dist/lib/mini-oxygen/node.js +19 -5
  71. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +227 -0
  72. package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +200 -0
  73. package/dist/lib/mini-oxygen/workerd-inspector.js +62 -235
  74. package/dist/lib/mini-oxygen/workerd.js +74 -50
  75. package/dist/lib/missing-routes.js +6 -3
  76. package/dist/lib/onboarding/common.js +40 -9
  77. package/dist/lib/onboarding/local.js +19 -11
  78. package/dist/lib/onboarding/remote.js +48 -28
  79. package/dist/lib/render-errors.js +2 -0
  80. package/dist/lib/request-events.js +65 -31
  81. package/dist/lib/setups/css/assets.js +1 -46
  82. package/dist/lib/setups/css/css-modules.js +3 -2
  83. package/dist/lib/setups/css/postcss.js +4 -2
  84. package/dist/lib/setups/css/tailwind.js +4 -2
  85. package/dist/lib/setups/css/vanilla-extract.js +3 -2
  86. package/dist/lib/setups/i18n/replacers.test.js +56 -38
  87. package/dist/lib/shell.js +1 -1
  88. package/dist/lib/template-diff.js +89 -0
  89. package/dist/lib/template-downloader.js +3 -2
  90. package/dist/lib/transpile/project.js +1 -1
  91. package/dist/virtual-routes/assets/debug-network.css +592 -0
  92. package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
  93. package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
  94. package/dist/virtual-routes/components/IconClose.jsx +38 -0
  95. package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
  96. package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
  97. package/dist/virtual-routes/components/RequestTable.jsx +92 -0
  98. package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
  99. package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
  100. package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
  101. package/oclif.manifest.json +134 -59
  102. package/package.json +18 -26
  103. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
  104. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
  105. package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
  106. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
  107. package/dist/virtual-routes/routes/debug-network.jsx +0 -289
  108. /package/dist/generator-templates/starter/app/{utils.ts → lib/variants.ts} +0 -0
@@ -3,17 +3,19 @@ import { vi, describe, beforeEach, it, expect } from 'vitest';
3
3
  import { runInit } from './init.js';
4
4
  import { exec } from '@shopify/cli-kit/node/system';
5
5
  import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
6
+ import { readAndParsePackageJson } from '@shopify/cli-kit/node/node-package-manager';
6
7
  import { writeFile, inTemporaryDirectory, readFile, isDirectory, removeFile, fileExists } from '@shopify/cli-kit/node/fs';
7
8
  import { joinPath, basename } from '@shopify/cli-kit/node/path';
8
9
  import { checkHydrogenVersion } from '../../lib/check-version.js';
9
10
  import { handleProjectLocation } from '../../lib/onboarding/common.js';
10
11
  import glob from 'fast-glob';
11
- import { getSkeletonSourceDir } from '../../lib/build.js';
12
+ import { getRepoNodeModules, getSkeletonSourceDir } from '../../lib/build.js';
12
13
  import { execAsync } from '../../lib/process.js';
13
- import { rmdir, symlink } from 'fs-extra';
14
+ import { remove, createSymlink } from 'fs-extra/esm';
14
15
  import { runCheckRoutes } from './check.js';
15
16
  import { runCodegen } from './codegen.js';
16
17
  import { runBuild } from './build.js';
18
+ import { runDev } from './dev.js';
17
19
 
18
20
  const { renderTasksHook } = vi.hoisted(() => ({ renderTasksHook: vi.fn() }));
19
21
  vi.mock("../../lib/check-version.js");
@@ -22,6 +24,9 @@ vi.mock("../../lib/template-downloader.js", async () => ({
22
24
  version: "",
23
25
  templatesDir: fileURLToPath(
24
26
  new URL("../../../../../templates", import.meta.url)
27
+ ),
28
+ examplesDir: fileURLToPath(
29
+ new URL("../../../../../examples", import.meta.url)
25
30
  )
26
31
  })
27
32
  }));
@@ -51,12 +56,10 @@ vi.mock(
51
56
  renderTasksHook.mockImplementationOnce(async () => {
52
57
  await writeFile(`${directory}/package-lock.json`, "{}");
53
58
  });
54
- await rmdir(joinPath(directory, "node_modules")).catch(() => {
59
+ await remove(joinPath(directory, "node_modules")).catch(() => {
55
60
  });
56
- await symlink(
57
- fileURLToPath(
58
- new URL("../../../../../node_modules", import.meta.url)
59
- ),
61
+ await createSymlink(
62
+ await getRepoNodeModules(),
60
63
  joinPath(directory, "node_modules")
61
64
  );
62
65
  })
@@ -101,6 +104,8 @@ describe("init", () => {
101
104
  });
102
105
  describe("remote templates", () => {
103
106
  it("throws for unknown templates", async () => {
107
+ const processExit = vi.spyOn(process, "exit").mockImplementationOnce(() => {
108
+ });
104
109
  await inTemporaryDirectory(async (tmpDir) => {
105
110
  await expect(
106
111
  runInit({
@@ -109,8 +114,11 @@ describe("init", () => {
109
114
  language: "ts",
110
115
  template: "https://github.com/some/repo"
111
116
  })
112
- ).rejects.toThrow("supported");
117
+ ).resolves;
113
118
  });
119
+ expect(outputMock.error()).toMatch("--template");
120
+ expect(processExit).toHaveBeenCalledWith(1);
121
+ processExit.mockRestore();
114
122
  });
115
123
  it("creates basic projects", async () => {
116
124
  await inTemporaryDirectory(async (tmpDir) => {
@@ -120,20 +128,20 @@ describe("init", () => {
120
128
  language: "ts",
121
129
  template: "hello-world"
122
130
  });
123
- const helloWorldFiles = await glob("**/*", {
131
+ const templateFiles = await glob("**/*", {
124
132
  cwd: getSkeletonSourceDir().replace("skeleton", "hello-world"),
125
133
  ignore: ["**/node_modules/**", "**/dist/**"]
126
134
  });
127
- const projectFiles = await glob("**/*", { cwd: tmpDir });
128
- const nonAppFiles = helloWorldFiles.filter(
135
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
136
+ const nonAppFiles = templateFiles.filter(
129
137
  (item) => !item.startsWith("app/")
130
138
  );
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");
139
+ expect(resultFiles).toEqual(expect.arrayContaining(nonAppFiles));
140
+ expect(resultFiles).toContain("app/root.tsx");
141
+ expect(resultFiles).toContain("app/entry.client.tsx");
142
+ expect(resultFiles).toContain("app/entry.server.tsx");
143
+ expect(resultFiles).not.toContain("app/components/Layout.tsx");
144
+ expect(resultFiles).not.toContain("app/routes/_index.tsx");
137
145
  await expect(readFile(`${tmpDir}/package.json`)).resolves.toMatch(
138
146
  `"name": "hello-world"`
139
147
  );
@@ -150,6 +158,62 @@ describe("init", () => {
150
158
  );
151
159
  });
152
160
  });
161
+ it("applies diff for examples", async () => {
162
+ await inTemporaryDirectory(async (tmpDir) => {
163
+ const exampleName = "third-party-queries-caching";
164
+ await runInit({
165
+ path: tmpDir,
166
+ git: false,
167
+ language: "ts",
168
+ template: exampleName
169
+ });
170
+ const templatePath = getSkeletonSourceDir();
171
+ const examplePath = templatePath.replace("templates", "examples").replace("skeleton", exampleName);
172
+ const ignore = ["**/node_modules/**", "**/dist/**"];
173
+ const resultFiles = await glob("**/*", { ignore, cwd: tmpDir });
174
+ const templateFiles = await glob("**/*", { ignore, cwd: templatePath });
175
+ const exampleFiles = await glob("**/*", { ignore, cwd: examplePath });
176
+ expect(resultFiles).toEqual(
177
+ expect.arrayContaining([
178
+ .../* @__PURE__ */ new Set([...templateFiles, ...exampleFiles])
179
+ ])
180
+ );
181
+ const templatePkgJson = await readAndParsePackageJson(
182
+ `${templatePath}/package.json`
183
+ );
184
+ const examplePkgJson = await readAndParsePackageJson(
185
+ `${examplePath}/package.json`
186
+ );
187
+ const resultPkgJson = await readAndParsePackageJson(
188
+ `${tmpDir}/package.json`
189
+ );
190
+ expect(resultPkgJson.name).toMatch(exampleName);
191
+ expect(resultPkgJson.scripts).toEqual(
192
+ expect.objectContaining(templatePkgJson.scripts)
193
+ );
194
+ expect(resultPkgJson.dependencies).toEqual(
195
+ expect.objectContaining({
196
+ ...templatePkgJson.dependencies,
197
+ ...examplePkgJson.dependencies
198
+ })
199
+ );
200
+ expect(resultPkgJson.devDependencies).toEqual(
201
+ expect.objectContaining({
202
+ ...templatePkgJson.devDependencies,
203
+ ...examplePkgJson.devDependencies
204
+ })
205
+ );
206
+ expect(resultPkgJson.peerDependencies).toEqual(
207
+ expect.objectContaining({
208
+ ...templatePkgJson.peerDependencies,
209
+ ...examplePkgJson.peerDependencies
210
+ })
211
+ );
212
+ expect(await readFile(joinPath(templatePath, "tsconfig.json"))).toEqual(
213
+ await readFile(joinPath(tmpDir, "tsconfig.json"))
214
+ );
215
+ });
216
+ });
153
217
  it("transpiles projects to JS", async () => {
154
218
  await inTemporaryDirectory(async (tmpDir) => {
155
219
  await runInit({
@@ -158,14 +222,14 @@ describe("init", () => {
158
222
  language: "js",
159
223
  template: "hello-world"
160
224
  });
161
- const helloWorldFiles = await glob("**/*", {
225
+ const templateFiles = await glob("**/*", {
162
226
  cwd: getSkeletonSourceDir().replace("skeleton", "hello-world"),
163
227
  ignore: ["**/node_modules/**", "**/dist/**"]
164
228
  });
165
- const projectFiles = await glob("**/*", { cwd: tmpDir });
166
- expect(projectFiles).toEqual(
229
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
230
+ expect(resultFiles).toEqual(
167
231
  expect.arrayContaining(
168
- helloWorldFiles.filter((item) => !item.endsWith(".d.ts")).map(
232
+ templateFiles.filter((item) => !item.endsWith(".d.ts")).map(
169
233
  (item) => item.replace(/\.ts(x)?$/, ".js$1").replace(/tsconfig\.json$/, "jsconfig.json")
170
234
  )
171
235
  )
@@ -189,20 +253,20 @@ describe("init", () => {
189
253
  language: "ts",
190
254
  mockShop: true
191
255
  });
192
- const skeletonFiles = await glob("**/*", {
256
+ const templateFiles = await glob("**/*", {
193
257
  cwd: getSkeletonSourceDir(),
194
258
  ignore: ["**/node_modules/**", "**/dist/**"]
195
259
  });
196
- const projectFiles = await glob("**/*", { cwd: tmpDir });
197
- const nonAppFiles = skeletonFiles.filter(
260
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
261
+ const nonAppFiles = templateFiles.filter(
198
262
  (item) => !item.startsWith("app/")
199
263
  );
200
- expect(projectFiles).toEqual(expect.arrayContaining(nonAppFiles));
201
- expect(projectFiles).toContain("app/root.tsx");
202
- expect(projectFiles).toContain("app/entry.client.tsx");
203
- expect(projectFiles).toContain("app/entry.server.tsx");
204
- expect(projectFiles).toContain("app/components/Layout.tsx");
205
- expect(projectFiles).not.toContain("app/routes/_index.tsx");
264
+ expect(resultFiles).toEqual(expect.arrayContaining(nonAppFiles));
265
+ expect(resultFiles).toContain("app/root.tsx");
266
+ expect(resultFiles).toContain("app/entry.client.tsx");
267
+ expect(resultFiles).toContain("app/entry.server.tsx");
268
+ expect(resultFiles).toContain("app/components/Layout.tsx");
269
+ expect(resultFiles).not.toContain("app/routes/_index.tsx");
206
270
  await expect(readFile(`${tmpDir}/server.ts`)).resolves.toEqual(
207
271
  await readFile(`${getSkeletonSourceDir()}/server.ts`)
208
272
  );
@@ -229,13 +293,13 @@ describe("init", () => {
229
293
  it("creates projects with route files", async () => {
230
294
  await inTemporaryDirectory(async (tmpDir) => {
231
295
  await runInit({ path: tmpDir, git: false, routes: true, language: "ts" });
232
- const skeletonFiles = await glob("**/*", {
296
+ const templateFiles = await glob("**/*", {
233
297
  cwd: getSkeletonSourceDir(),
234
298
  ignore: ["**/node_modules/**", "**/dist/**"]
235
299
  });
236
- const projectFiles = await glob("**/*", { cwd: tmpDir });
237
- expect(projectFiles).toEqual(expect.arrayContaining(skeletonFiles));
238
- expect(projectFiles).toContain("app/routes/_index.tsx");
300
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
301
+ expect(resultFiles).toEqual(expect.arrayContaining(templateFiles));
302
+ expect(resultFiles).toContain("app/routes/_index.tsx");
239
303
  await expect(readFile(`${tmpDir}/server.ts`)).resolves.toEqual(
240
304
  await readFile(`${getSkeletonSourceDir()}/server.ts`)
241
305
  );
@@ -252,19 +316,19 @@ describe("init", () => {
252
316
  it("transpiles projects to JS", async () => {
253
317
  await inTemporaryDirectory(async (tmpDir) => {
254
318
  await runInit({ path: tmpDir, git: false, routes: true, language: "js" });
255
- const skeletonFiles = await glob("**/*", {
319
+ const templateFiles = await glob("**/*", {
256
320
  cwd: getSkeletonSourceDir(),
257
321
  ignore: ["**/node_modules/**", "**/dist/**"]
258
322
  });
259
- const projectFiles = await glob("**/*", { cwd: tmpDir });
260
- expect(projectFiles).toEqual(
323
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
324
+ expect(resultFiles).toEqual(
261
325
  expect.arrayContaining(
262
- skeletonFiles.filter((item) => !item.endsWith(".d.ts")).map(
326
+ templateFiles.filter((item) => !item.endsWith(".d.ts")).map(
263
327
  (item) => item.replace(/\.ts(x)?$/, ".js$1").replace(/tsconfig\.json$/, "jsconfig.json")
264
328
  )
265
329
  )
266
330
  );
267
- expect(projectFiles).toContain("app/routes/_index.jsx");
331
+ expect(resultFiles).toContain("app/routes/_index.jsx");
268
332
  await expect(readFile(`${tmpDir}/server.js`)).resolves.toMatch(
269
333
  /export default {\n\s+\/\*\*.*?\*\/\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/s
270
334
  );
@@ -356,8 +420,8 @@ describe("init", () => {
356
420
  i18n: "domains",
357
421
  routes: true
358
422
  });
359
- const projectFiles = await glob("**/*", { cwd: tmpDir });
360
- expect(projectFiles).toContain("app/routes/_index.tsx");
423
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
424
+ expect(resultFiles).toContain("app/routes/_index.tsx");
361
425
  const serverFile = await readFile(`${tmpDir}/server.ts`);
362
426
  expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
363
427
  expect(serverFile).toMatch(/domain = url.hostname/);
@@ -376,8 +440,8 @@ describe("init", () => {
376
440
  i18n: "subdomains",
377
441
  routes: true
378
442
  });
379
- const projectFiles = await glob("**/*", { cwd: tmpDir });
380
- expect(projectFiles).toContain("app/routes/_index.tsx");
443
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
444
+ expect(resultFiles).toContain("app/routes/_index.tsx");
381
445
  const serverFile = await readFile(`${tmpDir}/server.ts`);
382
446
  expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
383
447
  expect(serverFile).toMatch(/firstSubdomain = url.hostname/);
@@ -396,8 +460,8 @@ describe("init", () => {
396
460
  i18n: "subfolders",
397
461
  routes: true
398
462
  });
399
- const projectFiles = await glob("**/*", { cwd: tmpDir });
400
- expect(projectFiles).toContain("app/routes/($locale)._index.tsx");
463
+ const resultFiles = await glob("**/*", { cwd: tmpDir });
464
+ expect(resultFiles).toContain("app/routes/($locale)._index.tsx");
401
465
  const serverFile = await readFile(`${tmpDir}/server.ts`);
402
466
  expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
403
467
  expect(serverFile).toMatch(/url.pathname/);
@@ -519,18 +583,56 @@ describe("init", () => {
519
583
  expect(output).toMatch("Complete analysis: file://");
520
584
  const clientAnalysisPath = "dist/worker/client-bundle-analyzer.html";
521
585
  const workerAnalysisPath = "dist/worker/worker-bundle-analyzer.html";
522
- expect(
586
+ await expect(
523
587
  fileExists(joinPath(tmpDir, clientAnalysisPath))
524
588
  ).resolves.toBeTruthy();
525
- expect(
589
+ await expect(
526
590
  fileExists(joinPath(tmpDir, workerAnalysisPath))
527
591
  ).resolves.toBeTruthy();
528
- expect(await readFile(joinPath(tmpDir, clientAnalysisPath))).toMatch(
529
- /globalThis\.METAFILE = '.+';/g
530
- );
531
- expect(await readFile(joinPath(tmpDir, workerAnalysisPath))).toMatch(
532
- /globalThis\.METAFILE = '.+';/g
533
- );
592
+ await expect(
593
+ readFile(joinPath(tmpDir, clientAnalysisPath))
594
+ ).resolves.toMatch(/globalThis\.METAFILE = '.+';/g);
595
+ await expect(
596
+ readFile(joinPath(tmpDir, workerAnalysisPath))
597
+ ).resolves.toMatch(/globalThis\.METAFILE = '.+';/g);
598
+ });
599
+ });
600
+ it("runs dev in the generated project", async () => {
601
+ await inTemporaryDirectory(async (tmpDir) => {
602
+ await runInit({
603
+ path: tmpDir,
604
+ git: true,
605
+ language: "ts",
606
+ styling: "postcss",
607
+ i18n: "subfolders",
608
+ routes: true,
609
+ installDeps: true
610
+ });
611
+ outputMock.clear();
612
+ const port = 1337;
613
+ const { close } = await runDev({
614
+ path: tmpDir,
615
+ port,
616
+ inspectorPort: 9e3,
617
+ disableVirtualRoutes: true,
618
+ disableVersionCheck: true
619
+ });
620
+ try {
621
+ await vi.waitFor(
622
+ () => expect(outputMock.output()).toMatch("success"),
623
+ { timeout: 5e3 }
624
+ );
625
+ expect(outputMock.output()).toMatch(/View Hydrogen app/i);
626
+ await expect(
627
+ fileExists(joinPath(tmpDir, "dist", "worker", "index.js"))
628
+ ).resolves.toBeTruthy();
629
+ const response = await fetch(`http://localhost:${port}`);
630
+ expect(response.status).toEqual(200);
631
+ expect(response.headers.get("content-type")).toEqual("text/html");
632
+ await expect(response.text()).resolves.toMatch("Mock.shop");
633
+ } finally {
634
+ await close();
635
+ }
534
636
  });
535
637
  });
536
638
  });
@@ -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 { basename } from '@shopify/cli-kit/node/path';
4
- import { renderSuccess, renderConfirmationPrompt, renderWarning, renderSelectPrompt, renderTextPrompt, renderTasks } from '@shopify/cli-kit/node/ui';
4
+ import { renderSuccess, renderConfirmationPrompt, renderWarning, renderTextPrompt, renderTasks } from '@shopify/cli-kit/node/ui';
5
5
  import { AbortError } from '@shopify/cli-kit/node/error';
6
6
  import { commonFlags } from '../../lib/flags.js';
7
7
  import { getStorefronts } from '../../lib/graphql/admin/link-storefront.js';
@@ -11,6 +11,7 @@ import { waitForJob } from '../../lib/graphql/admin/fetch-job.js';
11
11
  import { titleize } from '../../lib/string.js';
12
12
  import { getCliCommand } from '../../lib/shell.js';
13
13
  import { login } from '../../lib/auth.js';
14
+ import { handleStorefrontSelection } from '../../lib/onboarding/common.js';
14
15
 
15
16
  class Link extends Command {
16
17
  static description = "Link a local project to one of your shop's Hydrogen storefronts.";
@@ -93,29 +94,12 @@ async function linkStorefront(root, session, config, {
93
94
  return;
94
95
  }
95
96
  } else {
96
- const choices = [
97
- {
98
- label: "Create a new storefront",
99
- value: null
100
- },
101
- ...storefronts.map(({ id, title, productionUrl }) => ({
102
- label: `${title} (${productionUrl})`,
103
- value: id
104
- }))
105
- ];
106
- const storefrontId = await renderSelectPrompt({
107
- message: "Select a Hydrogen storefront to link",
108
- choices
109
- });
110
- if (storefrontId) {
111
- selectedStorefront = storefronts.find(({ id }) => id === storefrontId);
112
- } else {
97
+ selectedStorefront = await handleStorefrontSelection(storefronts);
98
+ if (!selectedStorefront) {
113
99
  selectedStorefront = await createNewStorefront(root, session);
114
100
  }
115
101
  }
116
- if (selectedStorefront) {
117
- await setStorefront(root, selectedStorefront);
118
- }
102
+ await setStorefront(root, selectedStorefront);
119
103
  return selectedStorefront;
120
104
  }
121
105
  async function createNewStorefront(root, session) {
@@ -42,6 +42,8 @@ describe("link", () => {
42
42
  ...FULL_SHOPIFY_CONFIG,
43
43
  storefront: void 0
44
44
  };
45
+ const expectedStorefrontName = "New Storefront";
46
+ const expectedJobId = "gid://shopify/Job/1";
45
47
  beforeEach(async () => {
46
48
  vi.mocked(login).mockResolvedValue({
47
49
  session: ADMIN_SESSION,
@@ -55,6 +57,14 @@ describe("link", () => {
55
57
  }
56
58
  ]);
57
59
  vi.mocked(renderSelectPrompt).mockResolvedValue(FULL_SHOPIFY_CONFIG.shop);
60
+ vi.mocked(createStorefront).mockResolvedValue({
61
+ storefront: {
62
+ id: "gid://shopify/HydrogenStorefront/1",
63
+ title: expectedStorefrontName,
64
+ productionUrl: "https://example.com"
65
+ },
66
+ jobId: expectedJobId
67
+ });
58
68
  });
59
69
  afterEach(() => {
60
70
  vi.resetAllMocks();
@@ -100,18 +110,8 @@ describe("link", () => {
100
110
  });
101
111
  });
102
112
  describe("when you want to link a new Hydrogen storefront", () => {
103
- const expectedStorefrontName = "New Storefront";
104
- const expectedJobId = "gid://shopify/Job/1";
105
113
  beforeEach(async () => {
106
114
  vi.mocked(renderSelectPrompt).mockResolvedValue(null);
107
- vi.mocked(createStorefront).mockResolvedValue({
108
- storefront: {
109
- id: "gid://shopify/HydrogenStorefront/1",
110
- title: expectedStorefrontName,
111
- productionUrl: "https://example.com"
112
- },
113
- jobId: expectedJobId
114
- });
115
115
  });
116
116
  it("chooses to create a new storefront given the directory path", async () => {
117
117
  await runLink({ path: "my-path" });
@@ -1,32 +1,37 @@
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, flagsToCamelObject, DEFAULT_PORT } from '../../lib/flags.js';
4
+ import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
5
5
  import { startMiniOxygen } from '../../lib/mini-oxygen/index.js';
6
6
  import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
7
7
  import { getConfig } from '../../lib/shopify-config.js';
8
+ import { findPort } from '../../lib/find-port.js';
8
9
 
9
10
  class Preview extends Command {
10
11
  static description = "Runs a Hydrogen storefront in an Oxygen worker for production.";
11
12
  static flags = {
12
13
  path: commonFlags.path,
13
14
  port: commonFlags.port,
14
- ["worker-unstable"]: commonFlags.workerRuntime,
15
- ["env-branch"]: commonFlags.envBranch
15
+ worker: deprecated("--worker", { isBoolean: true }),
16
+ "legacy-runtime": commonFlags.legacyRuntime,
17
+ "env-branch": commonFlags.envBranch,
18
+ "inspector-port": commonFlags.inspectorPort,
19
+ debug: commonFlags.debug
16
20
  };
17
21
  async run() {
18
22
  const { flags } = await this.parse(Preview);
19
23
  await runPreview({
20
- ...flagsToCamelObject(flags),
21
- workerRuntime: flags["worker-unstable"]
24
+ ...flagsToCamelObject(flags)
22
25
  });
23
26
  }
24
27
  }
25
28
  async function runPreview({
26
- port = DEFAULT_PORT,
29
+ port: appPort,
27
30
  path: appPath,
28
- workerRuntime = false,
29
- envBranch
31
+ legacyRuntime = false,
32
+ envBranch,
33
+ inspectorPort,
34
+ debug
30
35
  }) {
31
36
  if (!process.env.NODE_ENV)
32
37
  process.env.NODE_ENV = "production";
@@ -35,15 +40,21 @@ async function runPreview({
35
40
  const { shop, storefront } = await getConfig(root);
36
41
  const fetchRemote = !!shop && !!storefront?.id;
37
42
  const env = await getAllEnvironmentVariables({ root, fetchRemote, envBranch });
43
+ appPort = legacyRuntime ? appPort : await findPort(appPort);
44
+ inspectorPort = debug ? await findPort(inspectorPort) : inspectorPort;
45
+ const assetsPort = legacyRuntime ? 0 : await findPort(appPort + 100);
38
46
  const miniOxygen = await startMiniOxygen(
39
47
  {
40
48
  root,
41
- port,
49
+ port: appPort,
50
+ assetsPort,
51
+ env,
42
52
  buildPathClient,
43
53
  buildPathWorkerFile,
44
- env
54
+ inspectorPort,
55
+ debug
45
56
  },
46
- workerRuntime
57
+ legacyRuntime
47
58
  );
48
59
  miniOxygen.showBanner({ mode: "preview" });
49
60
  }
@@ -8,7 +8,6 @@ import { renderI18nPrompt, setupI18nStrategy } from '../../lib/setups/i18n/index
8
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
- import { generateProjectFile } from '../../lib/setups/routes/generate.js';
12
11
  import { getTemplateAppFile } from '../../lib/build.js';
13
12
 
14
13
  class Setup extends Command {
@@ -57,9 +56,6 @@ async function runSetup(options) {
57
56
  const typescript = !!remixConfig.tsconfigPath;
58
57
  backgroundWorkPromise = backgroundWorkPromise.then(
59
58
  () => 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.
62
- generateProjectFile("../server.ts", { ...remixConfig, typescript }),
63
59
  ...typescript ? [
64
60
  copyFile(
65
61
  getTemplateAppFile("../remix.env.d.ts"),
@@ -56,7 +56,6 @@ describe("setup", () => {
56
56
  expect(output).toMatch(/Markets:\s*Subfolders/);
57
57
  expect(output).toMatch("Routes");
58
58
  expect(output).toMatch("Home (/ & /:catchAll)");
59
- expect(output).toMatch("Account (/account/*)");
60
59
  });
61
60
  });
62
61
  });
@@ -22,6 +22,7 @@ Restart your terminal session and run \`${ALIAS_NAME}\` from your local project.
22
22
  name: "error",
23
23
  type: 0,
24
24
  message: "No supported shell found.",
25
+ skipOclifErrorHandling: true,
25
26
  tryMessage: "Please create a shortcut manually."
26
27
  });
27
28
  }