@shopify/cli-hydrogen 6.1.0 → 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 (97) 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 +107 -35
  4. package/dist/commands/hydrogen/deploy.test.js +83 -13
  5. package/dist/commands/hydrogen/dev.js +30 -15
  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 +7 -6
  11. package/dist/commands/hydrogen/setup.js +0 -4
  12. package/dist/commands/hydrogen/setup.test.js +0 -1
  13. package/dist/commands/hydrogen/upgrade.js +15 -0
  14. package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
  15. package/dist/generator-templates/starter/CHANGELOG.md +56 -0
  16. package/dist/generator-templates/starter/README.md +23 -0
  17. package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
  18. package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
  19. package/dist/generator-templates/starter/app/components/Layout.tsx +1 -1
  20. package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
  21. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
  22. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
  23. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
  24. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
  25. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
  26. package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
  27. package/dist/generator-templates/starter/app/lib/session.ts +67 -0
  28. package/dist/generator-templates/starter/app/root.tsx +11 -45
  29. package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
  30. package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
  31. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
  32. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
  33. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
  34. package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
  35. package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
  36. package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
  37. package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
  38. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
  39. package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
  40. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
  41. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
  42. package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
  43. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
  44. package/dist/generator-templates/starter/package.json +11 -11
  45. package/dist/generator-templates/starter/remix.config.js +4 -0
  46. package/dist/generator-templates/starter/remix.env.d.ts +4 -11
  47. package/dist/generator-templates/starter/server.ts +24 -167
  48. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
  49. package/dist/hooks/init.js +4 -4
  50. package/dist/lib/auth.js +5 -10
  51. package/dist/lib/build.js +6 -1
  52. package/dist/lib/bundle/analyzer.js +36 -26
  53. package/dist/lib/codegen.js +58 -18
  54. package/dist/lib/defer.js +12 -0
  55. package/dist/lib/file.js +52 -3
  56. package/dist/lib/flags.js +15 -8
  57. package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
  58. package/dist/lib/graphql/admin/client.test.js +2 -2
  59. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  60. package/dist/lib/log.js +31 -14
  61. package/dist/lib/mini-oxygen/index.js +4 -5
  62. package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
  63. package/dist/lib/mini-oxygen/node.js +4 -2
  64. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +2 -2
  65. package/dist/lib/mini-oxygen/workerd.js +27 -10
  66. package/dist/lib/missing-routes.js +6 -3
  67. package/dist/lib/onboarding/common.js +40 -9
  68. package/dist/lib/onboarding/local.js +19 -11
  69. package/dist/lib/onboarding/remote.js +48 -28
  70. package/dist/lib/request-events.js +65 -31
  71. package/dist/lib/setups/css/assets.js +1 -46
  72. package/dist/lib/setups/css/css-modules.js +3 -2
  73. package/dist/lib/setups/css/postcss.js +4 -2
  74. package/dist/lib/setups/css/tailwind.js +4 -2
  75. package/dist/lib/setups/css/vanilla-extract.js +3 -2
  76. package/dist/lib/setups/i18n/replacers.test.js +54 -38
  77. package/dist/lib/template-diff.js +89 -0
  78. package/dist/lib/template-downloader.js +3 -2
  79. package/dist/lib/transpile/project.js +1 -1
  80. package/dist/virtual-routes/assets/debug-network.css +592 -0
  81. package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
  82. package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
  83. package/dist/virtual-routes/components/IconClose.jsx +38 -0
  84. package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
  85. package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
  86. package/dist/virtual-routes/components/RequestTable.jsx +92 -0
  87. package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
  88. package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
  89. package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
  90. package/oclif.manifest.json +54 -61
  91. package/package.json +14 -11
  92. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
  93. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
  94. package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
  95. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
  96. package/dist/virtual-routes/routes/debug-network.jsx +0 -289
  97. /package/dist/generator-templates/starter/app/{utils.ts → lib/variants.ts} +0 -0
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { transformWithEsbuild } from 'vite';
3
+ import { startMiniOxygen, buildAssetsUrl } from './index.js';
4
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { inTemporaryDirectory, removeFile, touchFile, writeFile } from '@shopify/cli-kit/node/fs';
6
+ import getPort from 'get-port';
7
+
8
+ describe("MiniOxygen Worker Runtime", () => {
9
+ it("receives HTML from test worker", async () => {
10
+ await withFixtures(
11
+ async ({ writeHandler }) => {
12
+ await writeHandler(() => {
13
+ return new Response("<html><body>Hello, world</body></html>", {
14
+ headers: { "content-type": "text/html" }
15
+ });
16
+ });
17
+ },
18
+ async ({ fetch: fetch2 }) => {
19
+ const response = await fetch2("/");
20
+ expect(response.headers.get("content-type")).toEqual("text/html");
21
+ await expect(response.text()).resolves.toEqual(
22
+ "<html><body>Hello, world</body></html>"
23
+ );
24
+ }
25
+ );
26
+ });
27
+ it("reloads script", async () => {
28
+ await withFixtures(
29
+ async ({ writeHandler }) => {
30
+ await writeHandler((req, env) => new Response("foo"));
31
+ },
32
+ async ({ fetch: fetch2, writeHandler, miniOxygen }) => {
33
+ let response = await fetch2("/");
34
+ await expect(response.text()).resolves.toEqual("foo");
35
+ await writeHandler((req, env) => new Response("bar"));
36
+ await miniOxygen.reload();
37
+ response = await fetch2("/");
38
+ await expect(response.text()).resolves.toEqual("bar");
39
+ }
40
+ );
41
+ });
42
+ it("reloads environment variables", async () => {
43
+ await withFixtures(
44
+ async ({ writeHandler }) => {
45
+ await writeHandler((req, env) => new Response(env.TEST));
46
+ return { env: { TEST: "foo" } };
47
+ },
48
+ async ({ fetch: fetch2, miniOxygen }) => {
49
+ let response = await fetch2("/");
50
+ await expect(response.text()).resolves.toEqual("foo");
51
+ await miniOxygen.reload({ env: { TEST: "bar" } });
52
+ response = await fetch2("/");
53
+ await expect(response.text()).resolves.toEqual("bar");
54
+ }
55
+ );
56
+ });
57
+ it("serves a static asset via proxy", async () => {
58
+ await withFixtures(
59
+ async ({ writeHandler, writeAsset }) => {
60
+ await writeAsset(
61
+ "star.svg",
62
+ '<svg><polygon points="100,10 40,198 190,78 10,78 160,198" style="fill:gold;"/></svg>'
63
+ );
64
+ await writeHandler(() => new Response("ok"));
65
+ },
66
+ async ({ fetch: fetch2, fetchAsset }) => {
67
+ const asset = await (await fetchAsset("/star.svg")).text();
68
+ expect(asset).toEqual(
69
+ '<svg><polygon points="100,10 40,198 190,78 10,78 160,198" style="fill:gold;"/></svg>'
70
+ );
71
+ const response = await fetch2("/star.svg");
72
+ const result = await response.text();
73
+ expect(response.headers.get("content-type")).toEqual("image/svg+xml");
74
+ expect(result).toEqual(asset);
75
+ }
76
+ );
77
+ });
78
+ it("adds Oxygen request headers", async () => {
79
+ await withFixtures(
80
+ async ({ writeHandler }) => {
81
+ await writeHandler(
82
+ (req) => new Response(
83
+ JSON.stringify(Object.fromEntries(req.headers.entries())),
84
+ { headers: { "content-type": "application/json" } }
85
+ )
86
+ );
87
+ },
88
+ async ({ fetch: fetch2 }) => {
89
+ const response = await fetch2("/");
90
+ await expect(response.json()).resolves.toMatchObject({
91
+ "request-id": expect.stringMatching(/^[a-z0-9-]{36}$/),
92
+ "oxygen-buyer-ip": "127.0.0.1"
93
+ });
94
+ }
95
+ );
96
+ });
97
+ it("applies sourcemaps to error stack traces", async () => {
98
+ await withFixtures(
99
+ async ({ writeHandler }) => {
100
+ await writeHandler(
101
+ () => {
102
+ function doStuff() {
103
+ throw new Error("test");
104
+ }
105
+ try {
106
+ doStuff();
107
+ } catch (error) {
108
+ console.error(error);
109
+ throw error;
110
+ }
111
+ return new Response("ok");
112
+ },
113
+ { sourcemap: true }
114
+ );
115
+ },
116
+ async ({ fetch: fetch2, miniOxygen, miniOxygenOptions }) => {
117
+ const spy = vi.spyOn(console, "error").mockImplementation((error) => {
118
+ });
119
+ const response = await fetch2("/");
120
+ expect(response.status).toEqual(500);
121
+ await expect(response.text()).resolves.toEqual("Error: test");
122
+ await vi.waitFor(
123
+ () => expect(spy.mock.calls.length).toBeGreaterThan(1)
124
+ // At least 2 calls
125
+ );
126
+ expect(spy, "Logged with sourcemaps").toHaveBeenCalledWith(
127
+ expect.objectContaining({
128
+ stack: expect.stringMatching(
129
+ // Shows `doStuff` and the offending line by mapping
130
+ // the minified code with sourcemaps:
131
+ /Error: test\nthrow new Error\("test"\);\n.*at doStuff \(/s
132
+ )
133
+ })
134
+ );
135
+ expect(spy).toHaveBeenCalledWith(new Error("test"));
136
+ spy.mockClear();
137
+ await removeFile(miniOxygenOptions.buildPathWorkerFile + ".map");
138
+ await miniOxygen.reload();
139
+ await fetch2("/");
140
+ await vi.waitFor(
141
+ () => expect(spy.mock.calls.length).toBeGreaterThan(1)
142
+ // At least 2 calls
143
+ );
144
+ expect(spy, "Logged without sourcemaps").toHaveBeenCalledWith(
145
+ expect.objectContaining({
146
+ stack: expect.stringMatching(
147
+ // Doesn't show `doStuff` because it's minified
148
+ /Error: test\n\s+at \w .*at Object\.fetch/s
149
+ )
150
+ })
151
+ );
152
+ expect(spy).toHaveBeenCalledWith(new Error("test"));
153
+ spy.mockRestore();
154
+ }
155
+ );
156
+ });
157
+ });
158
+ function withFixtures(setup, runTest) {
159
+ return inTemporaryDirectory(async (tmpDir) => {
160
+ const relativeDistClient = joinPath("dist", "client");
161
+ const relativeDistWorker = joinPath("dist", "worker");
162
+ const relativeWorkerEntry = joinPath(relativeDistWorker, "index.js");
163
+ const writeFixture = async (filename, content) => {
164
+ const filepath = joinPath(tmpDir, filename);
165
+ await touchFile(filepath);
166
+ await writeFile(filepath, content);
167
+ };
168
+ const writeAsset = (filepath, content) => writeFixture(joinPath(relativeDistClient, filepath), content);
169
+ const writeHandler = async (handler, { sourcemap = false } = {}) => {
170
+ let code = `export default { fetch: ${handler.toString()} }`;
171
+ if (sourcemap) {
172
+ const result = await transformWithEsbuild(code, relativeWorkerEntry, {
173
+ minify: true,
174
+ sourcemap: true
175
+ });
176
+ code = result.code;
177
+ await writeFixture(
178
+ relativeWorkerEntry + ".map",
179
+ JSON.stringify(result.map)
180
+ );
181
+ }
182
+ await writeFixture(relativeWorkerEntry, code);
183
+ };
184
+ const optionsFromSetup = await setup({
185
+ writeFixture,
186
+ writeAsset,
187
+ writeHandler
188
+ });
189
+ const miniOxygenOptions = {
190
+ root: tmpDir,
191
+ port: await getPort(),
192
+ buildPathWorkerFile: joinPath(tmpDir, relativeWorkerEntry),
193
+ buildPathClient: joinPath(tmpDir, relativeDistClient),
194
+ inspectorPort: 9229,
195
+ assetsPort: 1347,
196
+ env: {},
197
+ ...optionsFromSetup
198
+ };
199
+ const miniOxygen = await startMiniOxygen(miniOxygenOptions);
200
+ try {
201
+ await runTest({
202
+ writeFixture,
203
+ writeHandler,
204
+ writeAsset,
205
+ miniOxygen,
206
+ miniOxygenOptions,
207
+ fetch: (pathname) => fetch(miniOxygen.listeningAt + pathname),
208
+ fetchAsset: (pathname) => fetch(buildAssetsUrl(miniOxygenOptions.assetsPort) + pathname)
209
+ });
210
+ } finally {
211
+ await miniOxygen.close();
212
+ }
213
+ });
214
+ }
@@ -2,10 +2,10 @@ import { randomUUID } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { readFile } from '@shopify/cli-kit/node/fs';
4
4
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
5
- import { startServer, Request } from '@shopify/mini-oxygen';
5
+ import { Response, startServer, Request } from '@shopify/mini-oxygen';
6
6
  import { DEFAULT_PORT } from '../flags.js';
7
7
  import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
8
- import { handleDebugNetworkRequest, H2O_BINDING_NAME, logRequestEvent } from '../request-events.js';
8
+ import { setConstructors, createLogRequestEvent, handleDebugNetworkRequest, H2O_BINDING_NAME } from '../request-events.js';
9
9
 
10
10
  async function startNodeServer({
11
11
  port = DEFAULT_PORT,
@@ -21,6 +21,8 @@ async function startNodeServer({
21
21
  return [key, value.defaultValue];
22
22
  })
23
23
  );
24
+ setConstructors({ Response });
25
+ const logRequestEvent = createLogRequestEvent();
24
26
  const asyncLocalStorage = new AsyncLocalStorage();
25
27
  const serviceBindings = {
26
28
  [H2O_BINDING_NAME]: {
@@ -179,12 +179,12 @@ function formatStructuredError(sourceMapConsumer, message, frames) {
179
179
  lines.push(message);
180
180
  frames?.forEach(({ functionName, lineNumber, columnNumber }, i) => {
181
181
  try {
182
- if (lineNumber) {
182
+ if (typeof lineNumber === "number") {
183
183
  const pos = sourceMapConsumer.originalPositionFor({
184
184
  line: lineNumber + 1,
185
185
  column: columnNumber
186
186
  });
187
- if (i === 0 && pos.source && pos.line) {
187
+ if (i === 0 && pos.source && pos.line !== null) {
188
188
  const fileSource = sourceMapConsumer.sourceContentFor(pos.source);
189
189
  const fileSourceLine = fileSource?.split("\n")[pos.line - 1] || "";
190
190
  lines.push(fileSourceLine.trim());
@@ -1,12 +1,13 @@
1
- import crypto from 'node:crypto';
2
- import { Response, Miniflare, fetch, Request, NoOpLog } from 'miniflare';
1
+ import { Response, Miniflare, Request, fetch, NoOpLog } from 'miniflare';
3
2
  import { resolvePath, dirname } from '@shopify/cli-kit/node/path';
4
3
  import { readFile } from '@shopify/cli-kit/node/fs';
5
4
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
5
+ import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
6
+ import colors from '@shopify/cli-kit/node/colors';
6
7
  import { createInspectorConnector } from './workerd-inspector.js';
7
8
  import { findPort } from '../find-port.js';
8
9
  import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
9
- import { setConstructors, handleDebugNetworkRequest, H2O_BINDING_NAME, logRequestEvent } from '../request-events.js';
10
+ import { setConstructors, handleDebugNetworkRequest, H2O_BINDING_NAME, createLogRequestEvent } from '../request-events.js';
10
11
  import { STATIC_ASSET_EXTENSIONS, createAssetsServer, buildAssetsUrl } from './assets.js';
11
12
 
12
13
  const PRIVATE_WORKERD_INSPECTOR_PORT = 9222;
@@ -33,6 +34,13 @@ async function startWorkerdServer({
33
34
  const absoluteBundlePath = resolvePath(root, buildPathWorkerFile);
34
35
  const handleAssets = createAssetHandler(assetsPort);
35
36
  const staticAssetExtensions = STATIC_ASSET_EXTENSIONS.slice();
37
+ let stringifiedOxygenHandler = miniOxygenHandler.toString();
38
+ if (process.env.NODE_ENV === "test") {
39
+ stringifiedOxygenHandler = stringifiedOxygenHandler.replace(
40
+ /\w*vite_ssr_import[\w\d]*\./g,
41
+ ""
42
+ );
43
+ }
36
44
  const buildMiniOxygenOptions = async () => ({
37
45
  cf: false,
38
46
  verbose: false,
@@ -41,11 +49,13 @@ async function startWorkerdServer({
41
49
  log: new NoOpLog(),
42
50
  liveReload: watch,
43
51
  host: "localhost",
52
+ handleRuntimeStdio() {
53
+ },
44
54
  workers: [
45
55
  {
46
56
  name: "mini-oxygen",
47
57
  modules: true,
48
- script: `export default { fetch: ${miniOxygenHandler.toString()} }`,
58
+ script: `export default { fetch: ${stringifiedOxygenHandler} }`,
49
59
  bindings: {
50
60
  staticAssetExtensions,
51
61
  oxygenHeadersMap
@@ -71,7 +81,7 @@ async function startWorkerdServer({
71
81
  compatibilityDate: "2022-10-31",
72
82
  bindings: { ...env },
73
83
  serviceBindings: {
74
- [H2O_BINDING_NAME]: logRequestEvent
84
+ [H2O_BINDING_NAME]: createLogRequestEvent({ absoluteBundlePath })
75
85
  }
76
86
  }
77
87
  ]
@@ -107,10 +117,14 @@ async function startWorkerdServer({
107
117
  },
108
118
  showBanner(options) {
109
119
  console.log("");
110
- const debuggerMessage = `
111
-
112
- Debug mode enabled. Attach a ${process.env.TERM_PROGRAM === "vscode" ? "VSCode " : ""}debugger to port ${publicInspectorPort}
113
- or open DevTools in http://localhost:${publicInspectorPort}`;
120
+ const isVSCode = process.env.TERM_PROGRAM === "vscode";
121
+ const debuggingDocsLink = "https://h2o.fyi/debugging/server-code" + (isVSCode ? "#visual-studio-code" : "#step-2-attach-a-debugger");
122
+ const debuggerMessage = outputContent`\n\nDebugging enabled on port ${String(
123
+ publicInspectorPort
124
+ )}.\nAttach a ${outputToken.link(
125
+ colors.yellow(isVSCode ? "VSCode debugger" : "debugger"),
126
+ debuggingDocsLink
127
+ )} or open DevTools in http://localhost:${String(publicInspectorPort)}.`.value;
114
128
  renderSuccess({
115
129
  headline: `${options?.headlinePrefix ?? ""}MiniOxygen (Worker Runtime) ${options?.mode ?? "development"} server running.`,
116
130
  body: [
@@ -179,7 +193,10 @@ function createAssetHandler(assetsPort) {
179
193
  return async (request) => {
180
194
  return fetch(
181
195
  new Request(
182
- request.url.replace(new URL(request.url).origin, assetsServerOrigin),
196
+ request.url.replace(
197
+ new URL(request.url).origin + "/",
198
+ assetsServerOrigin
199
+ ),
183
200
  request
184
201
  )
185
202
  );
@@ -23,12 +23,15 @@ const REQUIRED_ROUTES = [
23
23
  // 'discount/:discountCode', => Handled in storefrontRedirect
24
24
  "account",
25
25
  "account/login",
26
- "account/register",
27
26
  // 'account/addresses',
28
27
  // 'account/orders',
29
28
  "account/orders/:orderId",
30
- "account/reset/:id/:token",
31
- "account/activate/:id/:token"
29
+ // -- Added for CAAPI:
30
+ "account/authorize"
31
+ // -- These were removed when migrating to CAAPI:
32
+ // 'account/register',
33
+ // 'account/reset/:id/:token',
34
+ // 'account/activate/:id/:token',
32
35
  // 'password',
33
36
  // 'opening_soon',
34
37
  ];
@@ -16,6 +16,7 @@ import { transpileProject } from '../transpile/index.js';
16
16
  import { renderCssPrompt, setupCssStrategy, CSS_STRATEGY_NAME_MAP } from '../setups/css/index.js';
17
17
  import { renderRoutePrompt, generateRoutes, generateProjectFile } from '../setups/routes/generate.js';
18
18
  import { execAsync } from '../process.js';
19
+ import { getStorefronts } from '../graphql/admin/link-storefront.js';
19
20
 
20
21
  const LANGUAGES = {
21
22
  js: "JavaScript",
@@ -68,7 +69,7 @@ async function handleRouteGeneration(controller, flagRoutes) {
68
69
  }
69
70
  function generateProjectEntries(options) {
70
71
  return Promise.all(
71
- ["root", "entry.server", "entry.client"].map(
72
+ ["root", "entry.server", "entry.client", "../server.ts"].map(
72
73
  (filename) => generateProjectFile(filename, options)
73
74
  )
74
75
  );
@@ -108,12 +109,41 @@ async function handleCliShortcut(controller, cliCommand, flagShortcut) {
108
109
  async function handleStorefrontLink(controller) {
109
110
  const { session, config } = await login();
110
111
  renderLoginSuccess(config);
111
- const title = await renderTextPrompt({
112
- message: "New storefront name",
113
- defaultValue: titleize(config.shopName),
114
- abortSignal: controller.signal
112
+ const storefronts = await getStorefronts(session);
113
+ let selectedStorefront = await handleStorefrontSelection(storefronts);
114
+ let title;
115
+ if (selectedStorefront) {
116
+ title = selectedStorefront.title;
117
+ } else {
118
+ title = await renderTextPrompt({
119
+ message: "New storefront name",
120
+ defaultValue: titleize(config.shopName),
121
+ abortSignal: controller.signal
122
+ });
123
+ }
124
+ return {
125
+ ...config,
126
+ id: selectedStorefront?.id,
127
+ title,
128
+ session
129
+ };
130
+ }
131
+ async function handleStorefrontSelection(storefronts) {
132
+ const choices = [
133
+ {
134
+ label: "Create a new storefront",
135
+ value: null
136
+ },
137
+ ...storefronts.map(({ id, title, productionUrl }) => ({
138
+ label: `${title} (${productionUrl})`,
139
+ value: id
140
+ }))
141
+ ];
142
+ const storefrontId = await renderSelectPrompt({
143
+ message: "Select a Hydrogen storefront to link",
144
+ choices
115
145
  });
116
- return { ...config, title, session };
146
+ return storefrontId ? storefronts.find(({ id }) => id === storefrontId) : void 0;
117
147
  }
118
148
  async function handleProjectLocation({
119
149
  storefrontInfo,
@@ -380,7 +410,7 @@ async function renderProjectReady(project, {
380
410
  {
381
411
  link: {
382
412
  label: "Guides",
383
- url: "https://shopify.dev/docs/custom-storefronts/hydrogen/building"
413
+ url: "https://h2o.fyi/building"
384
414
  }
385
415
  },
386
416
  {
@@ -434,7 +464,8 @@ async function renderProjectReady(project, {
434
464
  function createAbortHandler(controller, project) {
435
465
  return async function abort(error) {
436
466
  controller.abort();
437
- if (typeof project !== "undefined") {
467
+ await Promise.resolve();
468
+ if (project?.directory) {
438
469
  await rmdir(project.directory, { force: true }).catch(() => {
439
470
  });
440
471
  }
@@ -457,4 +488,4 @@ function normalizeRoutePath(routePath) {
457
488
  return routePath.replace(/(^|\.)_index$/, "").replace(/((^|\.)[^\.]+)_\./g, "$1.").replace(/\.(?!\w+\])/g, "/").replace(/\$$/g, ":catchAll").replace(/\$/g, ":").replace(/[\[\]]/g, "").replace(/:\w*Handle/i, ":handle");
458
489
  }
459
490
 
460
- export { LANGUAGES, commitAll, createAbortHandler, createInitialCommit, generateProjectEntries, handleCliShortcut, handleCssStrategy, handleDependencies, handleI18n, handleLanguage, handleProjectLocation, handleRouteGeneration, handleStorefrontLink, renderProjectReady };
491
+ export { LANGUAGES, commitAll, createAbortHandler, createInitialCommit, generateProjectEntries, handleCliShortcut, handleCssStrategy, handleDependencies, handleI18n, handleLanguage, handleProjectLocation, handleRouteGeneration, handleStorefrontLink, handleStorefrontSelection, renderProjectReady };
@@ -37,7 +37,7 @@ async function setupLocalStarterTemplate(options, controller) {
37
37
  if (templateAction === "mock")
38
38
  project.storefrontTitle = "Mock.shop";
39
39
  const abort = createAbortHandler(controller, project);
40
- const createStorefrontPromise = storefrontInfo && createStorefront(storefrontInfo.session, storefrontInfo.title).then(async ({ storefront, jobId }) => {
40
+ const createStorefrontPromise = storefrontInfo && !storefrontInfo.id && createStorefront(storefrontInfo.session, storefrontInfo.title).then(async ({ storefront, jobId }) => {
41
41
  if (jobId)
42
42
  await waitForJob(storefrontInfo.session, jobId);
43
43
  return storefront;
@@ -46,9 +46,9 @@ async function setupLocalStarterTemplate(options, controller) {
46
46
  let backgroundWorkPromise = copy(
47
47
  templateDir,
48
48
  project.directory,
49
- // Filter out the `app` directory, which will be generated later
49
+ // Filter out the `app` directory and server.ts, which will be generated later
50
50
  {
51
- filter: (filepath) => !/^(app|dist|node_modules)\//i.test(
51
+ filter: (filepath) => !/^(app\/|dist\/|node_modules\/|server\.ts)/i.test(
52
52
  relativePath(templateDir, filepath)
53
53
  )
54
54
  }
@@ -90,19 +90,23 @@ async function setupLocalStarterTemplate(options, controller) {
90
90
  )
91
91
  ];
92
92
  const envLeadingComment = "# The variables added in this file are only available locally in MiniOxygen.\n# Run `h2 link` to also inject environment variables from your storefront,\n# or `h2 env pull` to populate this file.";
93
- if (storefrontInfo && createStorefrontPromise) {
93
+ let storefrontToLink;
94
+ if (storefrontInfo) {
94
95
  promises.push(
95
96
  // Save linked storefront in project
96
97
  setUserAccount(project.directory, storefrontInfo),
97
- createStorefrontPromise.then(
98
- (storefront) => (
99
- // Save linked storefront in project
100
- setStorefront(project.directory, storefront)
101
- )
102
- ),
103
98
  // Write empty dotenv file to fallback to remote Oxygen variables
104
99
  writeFile(joinPath(project.directory, ".env"), envLeadingComment)
105
100
  );
101
+ if (storefrontInfo.id) {
102
+ storefrontToLink = { id: storefrontInfo.id, title: storefrontInfo.title };
103
+ } else if (createStorefrontPromise) {
104
+ promises.push(
105
+ createStorefrontPromise.then((createdStorefront) => {
106
+ storefrontToLink = createdStorefront;
107
+ })
108
+ );
109
+ }
106
110
  } else if (templateAction === "mock") {
107
111
  promises.push(
108
112
  // Set required env vars
@@ -115,7 +119,11 @@ async function setupLocalStarterTemplate(options, controller) {
115
119
  )
116
120
  );
117
121
  }
118
- return Promise.all(promises).catch(abort);
122
+ return Promise.all(promises).then(() => {
123
+ if (storefrontToLink) {
124
+ return setStorefront(project.directory, storefrontToLink);
125
+ }
126
+ }).catch(abort);
119
127
  });
120
128
  const { language, transpileProject } = await handleLanguage(
121
129
  project.directory,
@@ -1,38 +1,58 @@
1
1
  import { AbortError } from '@shopify/cli-kit/node/error';
2
- import { copyFile } from '@shopify/cli-kit/node/fs';
2
+ import { fileExists, copyFile } from '@shopify/cli-kit/node/fs';
3
+ import { readAndParsePackageJson } from '@shopify/cli-kit/node/node-package-manager';
3
4
  import { joinPath } from '@shopify/cli-kit/node/path';
4
5
  import { renderTasks, renderInfo } from '@shopify/cli-kit/node/ui';
5
6
  import { getLatestTemplates } from '../template-downloader.js';
6
- import { handleProjectLocation, createAbortHandler, handleLanguage, createInitialCommit, handleDependencies, commitAll, renderProjectReady } from './common.js';
7
+ import { applyTemplateDiff } from '../template-diff.js';
8
+ import { createAbortHandler, handleProjectLocation, handleLanguage, createInitialCommit, handleDependencies, commitAll, renderProjectReady } from './common.js';
7
9
 
8
10
  async function setupRemoteTemplate(options, controller) {
9
- const isOfficialTemplate = options.template === "demo-store" || options.template === "hello-world";
10
- if (!isOfficialTemplate) {
11
- throw new AbortError(
12
- "Only `demo-store` and `hello-world` are supported in --template flag for now.",
13
- "Skip the --template flag to run the setup flow."
14
- );
15
- }
16
11
  const appTemplate = options.template;
12
+ let abort = createAbortHandler(controller);
17
13
  const backgroundDownloadPromise = getLatestTemplates({
18
14
  signal: controller.signal
19
- }).catch((error) => {
20
- throw abort(error);
21
- });
15
+ }).then(async ({ templatesDir, examplesDir }) => {
16
+ const templatePath = joinPath(templatesDir, appTemplate);
17
+ const examplePath = joinPath(examplesDir, appTemplate);
18
+ if (await fileExists(templatePath)) {
19
+ return { templatesDir, sourcePath: templatePath };
20
+ }
21
+ if (await fileExists(examplePath)) {
22
+ return { templatesDir, sourcePath: examplePath };
23
+ }
24
+ throw new AbortError(
25
+ "Unknown value in --template flag.",
26
+ "Skip the --template flag or provide the name of a template or example in the Hydrogen repository."
27
+ );
28
+ }).catch(abort);
22
29
  const project = await handleProjectLocation({ ...options, controller });
23
30
  if (!project)
24
31
  return;
25
- const abort = createAbortHandler(controller, project);
26
- let backgroundWorkPromise = backgroundDownloadPromise.then(
27
- ({ templatesDir }) => copyFile(joinPath(templatesDir, appTemplate), project.directory).catch(
28
- abort
29
- )
30
- );
31
- const { language, transpileProject } = await handleLanguage(
32
- project.directory,
33
- controller,
34
- options.language
32
+ abort = createAbortHandler(controller, project);
33
+ let backgroundWorkPromise = backgroundDownloadPromise.then(async (result) => {
34
+ if (controller.signal.aborted)
35
+ return;
36
+ const { sourcePath: sourcePath2, templatesDir } = result;
37
+ const pkgJson = await readAndParsePackageJson(
38
+ joinPath(sourcePath2, "package.json")
39
+ );
40
+ if (pkgJson.scripts?.dev?.includes("--diff")) {
41
+ return applyTemplateDiff(
42
+ project.directory,
43
+ sourcePath2,
44
+ joinPath(templatesDir, "skeleton")
45
+ );
46
+ }
47
+ return copyFile(sourcePath2, project.directory);
48
+ }).catch(abort);
49
+ if (controller.signal.aborted)
50
+ return;
51
+ const { sourcePath } = await backgroundDownloadPromise;
52
+ const supportsTranspilation = await fileExists(
53
+ joinPath(sourcePath, "tsconfig.json")
35
54
  );
55
+ const { language, transpileProject } = supportsTranspilation ? await handleLanguage(project.directory, controller, options.language) : { language: "js", transpileProject: () => Promise.resolve() };
36
56
  backgroundWorkPromise = backgroundWorkPromise.then(() => transpileProject().catch(abort)).then(
37
57
  () => options.git ? createInitialCommit(project.directory) : void 0
38
58
  );
@@ -74,17 +94,17 @@ async function setupRemoteTemplate(options, controller) {
74
94
  }
75
95
  });
76
96
  }
97
+ if (controller.signal.aborted)
98
+ return;
77
99
  await renderTasks(tasks);
78
100
  if (options.git) {
79
101
  await commitAll(project.directory, "Lockfile");
80
102
  }
81
103
  await renderProjectReady(project, setupSummary);
82
- if (isOfficialTemplate) {
83
- renderInfo({
84
- headline: `Your project will display inventory from ${options.template === "demo-store" ? "the Hydrogen Demo Store" : "Mock.shop"}.`,
85
- body: `To connect this project to your Shopify store\u2019s inventory, update \`${project.name}/.env\` with your store ID and Storefront API key.`
86
- });
87
- }
104
+ renderInfo({
105
+ headline: `Your project will display inventory from ${options.template === "demo-store" ? "the Hydrogen Demo Store" : "Mock.shop"}.`,
106
+ body: `To connect this project to your Shopify store\u2019s inventory, update \`${project.name}/.env\` with your store ID and Storefront API key.`
107
+ });
88
108
  return {
89
109
  ...project,
90
110
  ...setupSummary