@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
@@ -1,25 +1,28 @@
1
- import { Response, Miniflare, NoOpLog, Request } from 'miniflare';
2
- import { resolvePath } from '@shopify/cli-kit/node/path';
3
- import { glob, readFile, createFileReadStream, fileSize } from '@shopify/cli-kit/node/fs';
1
+ import { Response, Miniflare, Request, fetch, NoOpLog } from 'miniflare';
2
+ import { resolvePath, dirname } from '@shopify/cli-kit/node/path';
3
+ import { readFile } from '@shopify/cli-kit/node/fs';
4
4
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
5
- import { lookupMimeType } from '@shopify/cli-kit/node/mimes';
6
- import { findInspectorUrl, connectToInspector } from './workerd-inspector.js';
7
- import { DEFAULT_PORT } from '../flags.js';
5
+ import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
6
+ import colors from '@shopify/cli-kit/node/colors';
7
+ import { createInspectorConnector } from './workerd-inspector.js';
8
8
  import { findPort } from '../find-port.js';
9
9
  import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
10
- import { setConstructors, handleDebugNetworkRequest, H2O_BINDING_NAME, logRequestEvent } from '../request-events.js';
10
+ import { setConstructors, handleDebugNetworkRequest, H2O_BINDING_NAME, createLogRequestEvent } from '../request-events.js';
11
+ import { STATIC_ASSET_EXTENSIONS, createAssetsServer, buildAssetsUrl } from './assets.js';
11
12
 
12
- const DEFAULT_INSPECTOR_PORT = 8787;
13
+ const PRIVATE_WORKERD_INSPECTOR_PORT = 9222;
13
14
  async function startWorkerdServer({
14
15
  root,
15
- port = DEFAULT_PORT,
16
+ port: appPort,
17
+ inspectorPort: publicInspectorPort,
18
+ assetsPort,
19
+ debug = false,
16
20
  watch = false,
17
21
  buildPathWorkerFile,
18
22
  buildPathClient,
19
23
  env
20
24
  }) {
21
- const appPort = await findPort(port);
22
- const inspectorPort = await findPort(DEFAULT_INSPECTOR_PORT);
25
+ const privateInspectorPort = await findPort(PRIVATE_WORKERD_INSPECTOR_PORT);
23
26
  const oxygenHeadersMap = Object.values(OXYGEN_HEADERS_MAP).reduce(
24
27
  (acc, item) => {
25
28
  acc[item.name] = item.defaultValue;
@@ -28,44 +31,57 @@ async function startWorkerdServer({
28
31
  {}
29
32
  );
30
33
  setConstructors({ Response });
34
+ const absoluteBundlePath = resolvePath(root, buildPathWorkerFile);
35
+ const handleAssets = createAssetHandler(assetsPort);
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
+ }
31
44
  const buildMiniOxygenOptions = async () => ({
32
45
  cf: false,
33
46
  verbose: false,
34
47
  port: appPort,
35
- inspectorPort,
48
+ inspectorPort: privateInspectorPort,
36
49
  log: new NoOpLog(),
37
50
  liveReload: watch,
38
51
  host: "localhost",
52
+ handleRuntimeStdio() {
53
+ },
39
54
  workers: [
40
55
  {
41
56
  name: "mini-oxygen",
42
57
  modules: true,
43
- script: `export default { fetch: ${miniOxygenHandler.toString()} }`,
58
+ script: `export default { fetch: ${stringifiedOxygenHandler} }`,
44
59
  bindings: {
45
- initialAssets: await glob("**/*", { cwd: buildPathClient }),
60
+ staticAssetExtensions,
46
61
  oxygenHeadersMap
47
62
  },
48
63
  serviceBindings: {
49
64
  hydrogen: "hydrogen",
50
- assets: createAssetHandler(buildPathClient),
65
+ assets: handleAssets,
51
66
  debugNetwork: handleDebugNetworkRequest,
52
67
  logRequest
53
68
  }
54
69
  },
55
70
  {
56
71
  name: "hydrogen",
72
+ modulesRoot: dirname(absoluteBundlePath),
57
73
  modules: [
58
74
  {
59
75
  type: "ESModule",
60
- path: resolvePath(root, buildPathWorkerFile),
61
- contents: await readFile(resolvePath(root, buildPathWorkerFile))
76
+ path: absoluteBundlePath,
77
+ contents: await readFile(absoluteBundlePath)
62
78
  }
63
79
  ],
64
80
  compatibilityFlags: ["streams_enable_constructors"],
65
81
  compatibilityDate: "2022-10-31",
66
82
  bindings: { ...env },
67
83
  serviceBindings: {
68
- [H2O_BINDING_NAME]: logRequestEvent
84
+ [H2O_BINDING_NAME]: createLogRequestEvent({ absoluteBundlePath })
69
85
  }
70
86
  }
71
87
  ]
@@ -74,8 +90,16 @@ async function startWorkerdServer({
74
90
  const miniOxygen = new Miniflare(miniOxygenOptions);
75
91
  const listeningAt = (await miniOxygen.ready).origin;
76
92
  const sourceMapPath = buildPathWorkerFile + ".map";
77
- let inspectorUrl = await findInspectorUrl(inspectorPort);
78
- let cleanupInspector = inspectorUrl ? connectToInspector({ inspectorUrl, sourceMapPath }) : void 0;
93
+ const reconnect = createInspectorConnector({
94
+ debug,
95
+ sourceMapPath,
96
+ absoluteBundlePath,
97
+ privateInspectorPort,
98
+ publicInspectorPort
99
+ });
100
+ await reconnect();
101
+ const assetsServer = createAssetsServer(buildPathClient);
102
+ assetsServer.listen(assetsPort);
79
103
  return {
80
104
  port: appPort,
81
105
  listeningAt,
@@ -89,25 +113,31 @@ async function startWorkerdServer({
89
113
  hydrogen.bindings = { ...nextOptions?.env ?? env };
90
114
  }
91
115
  }
92
- cleanupInspector?.();
93
- await miniOxygen.setOptions(miniOxygenOptions);
94
- inspectorUrl ??= await findInspectorUrl(inspectorPort);
95
- if (inspectorUrl) {
96
- cleanupInspector = connectToInspector({ inspectorUrl, sourceMapPath });
97
- }
116
+ await reconnect(() => miniOxygen.setOptions(miniOxygenOptions));
98
117
  },
99
118
  showBanner(options) {
100
119
  console.log("");
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;
101
128
  renderSuccess({
102
- headline: `${options?.headlinePrefix ?? ""}MiniOxygen (Unstable Worker Runtime) ${options?.mode ?? "development"} server running.`,
129
+ headline: `${options?.headlinePrefix ?? ""}MiniOxygen (Worker Runtime) ${options?.mode ?? "development"} server running.`,
103
130
  body: [
104
131
  `View ${options?.appName ?? "Hydrogen"} app: ${listeningAt}`,
105
- ...options?.extraLines ?? []
132
+ ...options?.extraLines ?? [],
133
+ ...debug ? [{ warn: debuggerMessage }] : []
106
134
  ]
107
135
  });
108
136
  console.log("");
109
137
  },
110
138
  async close() {
139
+ assetsServer.closeAllConnections();
140
+ assetsServer.close();
111
141
  await miniOxygen.dispose();
112
142
  }
113
143
  };
@@ -118,7 +148,11 @@ async function miniOxygenHandler(request, env, context) {
118
148
  return env.debugNetwork.fetch(request);
119
149
  }
120
150
  if (request.method === "GET") {
121
- if (new Set(env.initialAssets).has(pathname.slice(1))) {
151
+ const staticAssetExtensions = new Set(env.staticAssetExtensions);
152
+ const wellKnown = pathname.startsWith("/.well-known");
153
+ const extension = pathname.split(".").at(-1) ?? "";
154
+ const isAsset = wellKnown || !!staticAssetExtensions.has(extension.toUpperCase());
155
+ if (isAsset) {
122
156
  const response2 = await env.assets.fetch(
123
157
  new Request(request.url, {
124
158
  signal: request.signal,
@@ -154,28 +188,18 @@ async function miniOxygenHandler(request, env, context) {
154
188
  );
155
189
  return response;
156
190
  }
157
- function createAssetHandler(buildPathClient) {
191
+ function createAssetHandler(assetsPort) {
192
+ const assetsServerOrigin = buildAssetsUrl(assetsPort);
158
193
  return async (request) => {
159
- const relativeAssetPath = new URL(request.url).pathname.replace("/", "");
160
- if (relativeAssetPath) {
161
- try {
162
- const absoluteAssetPath = resolvePath(
163
- buildPathClient,
164
- relativeAssetPath
165
- );
166
- return new Response(createFileReadStream(absoluteAssetPath), {
167
- headers: {
168
- "Content-Type": lookupMimeType(relativeAssetPath) || "text/plain",
169
- "Content-Length": String(await fileSize(absoluteAssetPath))
170
- }
171
- });
172
- } catch (error) {
173
- if (error.code !== "ENOENT") {
174
- throw error;
175
- }
176
- }
177
- }
178
- return new Response("Not Found", { status: 404 });
194
+ return fetch(
195
+ new Request(
196
+ request.url.replace(
197
+ new URL(request.url).origin + "/",
198
+ assetsServerOrigin
199
+ ),
200
+ request
201
+ )
202
+ );
179
203
  };
180
204
  }
181
205
  async function logRequest(request) {
@@ -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
@@ -14,6 +14,7 @@ function renderMissingStorefront({
14
14
  message: outputContent`${outputToken.errorText(
15
15
  "Couldn\u2019t find Hydrogen storefront."
16
16
  )}`.value,
17
+ skipOclifErrorHandling: true,
17
18
  tryMessage: outputContent`Couldn’t find ${storefront.title} (ID: ${parseGid(
18
19
  storefront.id
19
20
  )}) on ${session.storeFqdn}. Check that the storefront exists and run ${outputToken.genericShellCommand(
@@ -29,6 +30,7 @@ function renderMissingLink({ session, cliCommand }) {
29
30
  name: "NoLinkedStorefrontError",
30
31
  type: 0,
31
32
  message: `No linked Hydrogen storefront on ${session.storeFqdn}`,
33
+ skipOclifErrorHandling: true,
32
34
  tryMessage: [
33
35
  "To pull environment variables or to deploy to Oxygen, link this project to a Hydrogen storefront. To select a storefront to link, run",
34
36
  { command: `${cliCommand} link` }
@@ -1,14 +1,15 @@
1
+ import path from 'node:path';
1
2
  import { EventEmitter } from 'node:events';
2
3
  import { ReadableStream } from 'node:stream/web';
3
4
  import { getGraphiQLUrl } from './graphiql-url.js';
4
- import { Response } from '@shopify/mini-oxygen';
5
+ import { mapSourcePosition } from 'source-map-support';
5
6
 
6
7
  const H2O_BINDING_NAME = "H2O_LOG_EVENT";
7
- let ResponseConstructor = Response;
8
+ let ResponseConstructor;
8
9
  function setConstructors(constructors) {
9
10
  ResponseConstructor = constructors.Response;
10
11
  }
11
- const DEV_ROUTES = /* @__PURE__ */ new Set(["/graphiql", "/debug-network"]);
12
+ const DEV_ROUTES = /* @__PURE__ */ new Set(["/graphiql", "/subrequest-profiler"]);
12
13
  const EVENT_MAP = {
13
14
  request: "Request",
14
15
  subrequest: "Sub request"
@@ -16,9 +17,9 @@ const EVENT_MAP = {
16
17
  async function getRequestInfo(request) {
17
18
  const data = await request.json();
18
19
  return {
19
- id: data.requestId ?? "",
20
+ ...data,
21
+ requestId: data.requestId ?? "",
20
22
  eventType: data.eventType || "unknown",
21
- startTime: data.startTime,
22
23
  endTime: data.endTime || Date.now(),
23
24
  purpose: data.purpose === "prefetch" ? "(prefetch)" : "",
24
25
  cacheStatus: data.cacheStatus ?? "",
@@ -34,33 +35,66 @@ async function clearHistory(request) {
34
35
  eventHistory.length = 0;
35
36
  return createResponse();
36
37
  }
37
- async function logRequestEvent(request) {
38
- const url = new URL(request.url);
39
- if (DEV_ROUTES.has(url.pathname)) {
40
- return createResponse();
41
- }
42
- const { eventType, purpose, graphql, ...data } = await getRequestInfo(request);
43
- let graphiqlLink = "";
44
- let description = request.url;
45
- if (eventType === "subrequest") {
46
- description = graphql?.query.match(/(query|mutation)\s+(\w+)/)?.[0]?.replace(/\s+/, " ") || decodeURIComponent(url.search.slice(1));
47
- if (graphql) {
48
- graphiqlLink = getGraphiQLUrl({ graphql });
38
+ function createLogRequestEvent(options) {
39
+ return async function logRequestEvent(request) {
40
+ const url = new URL(request.url);
41
+ if (DEV_ROUTES.has(url.pathname)) {
42
+ return createResponse();
43
+ }
44
+ const {
45
+ url: displayUrl,
46
+ displayName: displayNameData,
47
+ eventType,
48
+ purpose,
49
+ graphql,
50
+ stackInfo,
51
+ ...data
52
+ } = await getRequestInfo(request);
53
+ let graphiqlLink = "";
54
+ let descriptionUrl = request.url;
55
+ let displayName = displayNameData;
56
+ if (eventType === "subrequest") {
57
+ displayName = displayName || graphql?.query.match(/(query|mutation)\s+(\w+)/)?.[0]?.replace(/\s+/, " ");
58
+ descriptionUrl = displayUrl || request.url;
59
+ if (graphql) {
60
+ graphiqlLink = getGraphiQLUrl({ graphql });
61
+ }
62
+ }
63
+ let stackLine = null;
64
+ let stackLink = null;
65
+ if (stackInfo?.file) {
66
+ if (!path.isAbsolute(stackInfo.file) && options?.absoluteBundlePath) {
67
+ stackInfo.file = options.absoluteBundlePath;
68
+ }
69
+ const { source, line, column } = mapSourcePosition({
70
+ source: stackInfo.file,
71
+ line: stackInfo.line ?? 0,
72
+ column: stackInfo.column ?? 0
73
+ });
74
+ stackLine = `${source}:${line}:${column + 1}`;
75
+ stackLink = `vscode://${path.join("file", stackLine)}`;
76
+ stackLine = stackLine.split(path.sep + "app" + path.sep)[1] ?? stackLine;
77
+ if (stackInfo.func) {
78
+ stackLine = `${stackInfo.func.replace(/\d+$/, "")} (${stackLine})`;
79
+ }
49
80
  }
50
- }
51
- const event = {
52
- event: EVENT_MAP[eventType] || eventType,
53
- data: JSON.stringify({
54
- ...data,
55
- url: `${purpose} ${description}`.trim(),
56
- graphiqlLink
57
- })
81
+ const event = {
82
+ event: EVENT_MAP[eventType] || eventType,
83
+ data: JSON.stringify({
84
+ ...data,
85
+ displayName,
86
+ url: `${purpose} ${descriptionUrl}`.trim(),
87
+ graphiqlLink,
88
+ stackLine,
89
+ stackLink
90
+ })
91
+ };
92
+ eventHistory.push(event);
93
+ if (eventHistory.length > 100)
94
+ eventHistory.shift();
95
+ eventEmitter.emit("request", event);
96
+ return createResponse();
58
97
  };
59
- eventHistory.push(event);
60
- if (eventHistory.length > 100)
61
- eventHistory.shift();
62
- eventEmitter.emit("request", event);
63
- return createResponse();
64
98
  }
65
99
  function streamRequestEvents(request) {
66
100
  const stream = new ReadableStream({
@@ -101,4 +135,4 @@ function handleDebugNetworkRequest(request) {
101
135
  return request.method === "DELETE" ? clearHistory() : streamRequestEvents(request);
102
136
  }
103
137
 
104
- export { DEV_ROUTES, H2O_BINDING_NAME, handleDebugNetworkRequest, logRequestEvent, setConstructors };
138
+ export { DEV_ROUTES, H2O_BINDING_NAME, createLogRequestEvent, handleDebugNetworkRequest, setConstructors };