@shopify/cli-hydrogen 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/assets/hydrogen/starter/CHANGELOG.md +166 -0
  2. package/dist/assets/hydrogen/starter/app/components/AddToCartButton.tsx +37 -0
  3. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +150 -0
  4. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +68 -0
  5. package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +101 -0
  6. package/dist/assets/hydrogen/starter/app/components/Header.tsx +3 -3
  7. package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +2 -2
  8. package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +80 -0
  9. package/dist/assets/hydrogen/starter/app/components/ProductImage.tsx +23 -0
  10. package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +27 -0
  11. package/dist/assets/hydrogen/starter/app/lib/session.ts +5 -0
  12. package/dist/assets/hydrogen/starter/app/root.tsx +23 -36
  13. package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +1 -5
  14. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +12 -70
  15. package/dist/assets/hydrogen/starter/app/routes/account.orders.$id.tsx +7 -14
  16. package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +1 -8
  17. package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +5 -22
  18. package/dist/assets/hydrogen/starter/app/routes/account.tsx +0 -1
  19. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +1 -3
  20. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +51 -232
  21. package/dist/assets/hydrogen/starter/package.json +10 -11
  22. package/dist/assets/hydrogen/starter/server.ts +4 -0
  23. package/dist/assets/hydrogen/tailwind/package.json +1 -6
  24. package/dist/assets/hydrogen/tailwind/tailwind.css +6 -3
  25. package/dist/assets/hydrogen/vanilla-extract/package.json +2 -3
  26. package/dist/assets/hydrogen/virtual-routes/components/{Layout.jsx → PageLayout.jsx} +2 -2
  27. package/dist/assets/hydrogen/virtual-routes/components/RequestDetails.jsx +1 -2
  28. package/dist/assets/hydrogen/virtual-routes/components/RequestTable.jsx +1 -2
  29. package/dist/assets/hydrogen/virtual-routes/routes/index.jsx +1 -2
  30. package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +8 -30
  31. package/dist/commands/hydrogen/build.js +33 -10
  32. package/dist/commands/hydrogen/customer-account/push.js +3 -6
  33. package/dist/commands/hydrogen/debug/cpu.js +3 -3
  34. package/dist/commands/hydrogen/deploy.js +14 -3
  35. package/dist/commands/hydrogen/dev.js +3 -6
  36. package/dist/commands/hydrogen/env/list.js +1 -2
  37. package/dist/commands/hydrogen/env/pull.js +2 -4
  38. package/dist/commands/hydrogen/env/push.js +6 -12
  39. package/dist/commands/hydrogen/init.d.ts +18 -15
  40. package/dist/commands/hydrogen/init.js +12 -24
  41. package/dist/commands/hydrogen/link.js +1 -2
  42. package/dist/commands/hydrogen/preview.js +4 -6
  43. package/dist/commands/hydrogen/setup/css.js +29 -12
  44. package/dist/commands/hydrogen/setup/vite.js +3 -6
  45. package/dist/commands/hydrogen/setup.js +8 -7
  46. package/dist/commands/hydrogen/upgrade.js +16 -32
  47. package/dist/hooks/init.js +50 -6
  48. package/dist/index.d.ts +46 -46
  49. package/dist/lib/auth.js +1 -2
  50. package/dist/lib/build.js +1 -2
  51. package/dist/lib/bundle/analyzer.js +39 -24
  52. package/dist/lib/bundle/vite-plugin.js +161 -0
  53. package/dist/lib/check-cli-version.js +61 -0
  54. package/dist/lib/check-lockfile.js +2 -2
  55. package/dist/lib/classic-compiler/build.js +3 -3
  56. package/dist/lib/classic-compiler/dev.js +5 -10
  57. package/dist/lib/codegen.js +8 -16
  58. package/dist/lib/defer.js +2 -4
  59. package/dist/lib/environment-variables.js +2 -4
  60. package/dist/lib/file.js +15 -7
  61. package/dist/lib/flags.js +10 -0
  62. package/dist/lib/get-oxygen-deployment-data.js +1 -2
  63. package/dist/lib/graphiql-url.js +1 -2
  64. package/dist/lib/import-utils.js +3 -2
  65. package/dist/lib/log.js +11 -22
  66. package/dist/lib/mini-oxygen/common.js +1 -2
  67. package/dist/lib/mini-oxygen/node.js +1 -2
  68. package/dist/lib/missing-routes.js +1 -2
  69. package/dist/lib/onboarding/common.js +60 -15
  70. package/dist/lib/onboarding/local.js +14 -13
  71. package/dist/lib/onboarding/remote.js +16 -9
  72. package/dist/lib/onboarding/setup-template.mocks.js +6 -3
  73. package/dist/lib/remix-config.js +2 -4
  74. package/dist/lib/remix-version-check.js +1 -2
  75. package/dist/lib/request-events.js +3 -6
  76. package/dist/lib/setups/css/assets.js +1 -1
  77. package/dist/lib/setups/css/index.js +17 -10
  78. package/dist/lib/setups/css/replacers.js +74 -76
  79. package/dist/lib/setups/css/tailwind.js +16 -20
  80. package/dist/lib/setups/css/vanilla-extract.js +8 -5
  81. package/dist/lib/setups/i18n/replacers.js +1 -2
  82. package/dist/lib/setups/routes/generate.js +18 -19
  83. package/dist/lib/shell.js +5 -10
  84. package/dist/lib/template-diff.js +83 -104
  85. package/dist/lib/template-downloader.js +2 -2
  86. package/dist/lib/transpile/morph/functions.js +3 -6
  87. package/dist/lib/transpile/morph/index.js +2 -4
  88. package/dist/lib/transpile/morph/typedefs.js +3 -6
  89. package/dist/lib/transpile/morph/utils.js +2 -4
  90. package/dist/lib/transpile/project.js +4 -3
  91. package/oclif.manifest.json +51 -4
  92. package/package.json +8 -12
  93. package/dist/assets/hydrogen/css-modules/package.json +0 -6
  94. package/dist/assets/hydrogen/postcss/package.json +0 -10
  95. package/dist/assets/hydrogen/postcss/postcss.config.js +0 -8
  96. package/dist/assets/hydrogen/starter/app/components/Cart.tsx +0 -364
  97. package/dist/assets/hydrogen/tailwind/postcss.config.js +0 -10
  98. package/dist/assets/hydrogen/tailwind/tailwind.config.js +0 -8
  99. package/dist/lib/check-version.js +0 -75
  100. package/dist/lib/setups/css/css-modules.js +0 -23
  101. package/dist/lib/setups/css/postcss.js +0 -31
package/dist/lib/file.js CHANGED
@@ -6,8 +6,7 @@ import { formatCode } from './format-code.js';
6
6
 
7
7
  async function replaceFileContent(filepath, formatConfig, replacer) {
8
8
  let content = await replacer(await readFile(filepath));
9
- if (typeof content !== "string")
10
- return;
9
+ if (typeof content !== "string") return;
11
10
  if (formatConfig) {
12
11
  content = await formatCode(content, formatConfig, filepath);
13
12
  }
@@ -62,8 +61,7 @@ async function mergePackageJson(sourceDir, targetDir, options) {
62
61
  (key) => !MANAGED_PACKAGE_JSON_KEYS.includes(key)
63
62
  );
64
63
  for (const key of unmanagedKeys) {
65
- if (ignoredKeys.has(key))
66
- continue;
64
+ if (ignoredKeys.has(key)) continue;
67
65
  const sourceValue = sourcePkgJson[key];
68
66
  const targetValue = targetPkgJson[key];
69
67
  const newValue = Array.isArray(sourceValue) && Array.isArray(targetValue) ? [...targetValue, ...sourceValue] : typeof sourceValue === "object" && typeof targetValue === "object" ? { ...targetValue, ...sourceValue } : sourceValue;
@@ -73,8 +71,7 @@ async function mergePackageJson(sourceDir, targetDir, options) {
73
71
  ([dep]) => dep.startsWith("@remix-run/")
74
72
  )?.[1];
75
73
  for (const key of MANAGED_PACKAGE_JSON_KEYS) {
76
- if (ignoredKeys.has(key))
77
- continue;
74
+ if (ignoredKeys.has(key)) continue;
78
75
  if (sourcePkgJson[key]) {
79
76
  targetPkgJson[key] = [
80
77
  .../* @__PURE__ */ new Set([
@@ -96,5 +93,16 @@ async function mergePackageJson(sourceDir, targetDir, options) {
96
93
  options?.onResult?.(targetPkgJson) ?? targetPkgJson
97
94
  );
98
95
  }
96
+ async function mergeTsConfig(sourceDir, targetDir) {
97
+ const sourceTsConfig = await readFile(joinPath(sourceDir, "tsconfig.json"));
98
+ const sourceTsTypes = sourceTsConfig.match(/"types": \[(.*?)\]/)?.[1];
99
+ if (sourceTsTypes) {
100
+ replaceFileContent(
101
+ joinPath(targetDir, "tsconfig.json"),
102
+ false,
103
+ (content) => content.replace(/"types":\s*\[[^\]]*\]/, `"types": [${sourceTsTypes}]`)
104
+ );
105
+ }
106
+ }
99
107
 
100
- export { findFileWithExtension, mergePackageJson, replaceFileContent };
108
+ export { findFileWithExtension, mergePackageJson, mergeTsConfig, replaceFileContent };
package/dist/lib/flags.js CHANGED
@@ -3,6 +3,7 @@ import { camelize } from '@shopify/cli-kit/common/string';
3
3
  import { renderInfo } from '@shopify/cli-kit/node/ui';
4
4
  import { normalizeStoreFqdn } from '@shopify/cli-kit/node/context/fqdn';
5
5
  import colors from '@shopify/cli-kit/node/colors';
6
+ import { STYLING_CHOICES } from './setups/css/index.js';
6
7
  import { I18N_CHOICES } from './setups/i18n/index.js';
7
8
 
8
9
  const DEFAULT_APP_PORT = 3e3;
@@ -86,6 +87,15 @@ const commonFlags = {
86
87
  dependsOn: ["codegen"]
87
88
  })
88
89
  },
90
+ styling: {
91
+ styling: Flags.string({
92
+ description: `Sets the styling strategy to use. One of ${STYLING_CHOICES.map(
93
+ (item) => `\`${item}\``
94
+ ).join(", ")}.`,
95
+ choices: STYLING_CHOICES,
96
+ env: "SHOPIFY_HYDROGEN_FLAG_STYLING"
97
+ })
98
+ },
89
99
  markets: {
90
100
  markets: Flags.string({
91
101
  description: `Sets the URL structure to support multiple markets. Must be one of: ${I18N_CHOICES.map(
@@ -18,8 +18,7 @@ async function getOxygenDeploymentData({
18
18
  config,
19
19
  cliCommand
20
20
  });
21
- if (!linkedStorefront)
22
- return;
21
+ if (!linkedStorefront) return;
23
22
  config.storefront = linkedStorefront;
24
23
  const { storefront } = await getOxygenData(session, config.storefront.id);
25
24
  if (!storefront) {
@@ -5,8 +5,7 @@ function getGraphiQLUrl({
5
5
  let url = `${host.endsWith("/") ? host.slice(0, -1) : host}/graphiql`;
6
6
  if (graphql) {
7
7
  let { query, variables } = graphql;
8
- if (typeof variables !== "string")
9
- variables = JSON.stringify(variables);
8
+ if (typeof variables !== "string") variables = JSON.stringify(variables);
10
9
  url += `?query=${encodeURIComponent(query)}${variables ? `&variables=${encodeURIComponent(variables)}` : ""}`;
11
10
  if (graphql.schema) {
12
11
  url += `&schema=${graphql.schema}`;
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
+ import { pathToFileURL } from 'node:url';
2
3
  import { findUpAndReadPackageJson } from '@shopify/cli-kit/node/node-package-manager';
3
4
  import { joinPath, dirname } from '@shopify/cli-kit/node/path';
4
5
 
@@ -11,11 +12,11 @@ async function importVite(root) {
11
12
  dirname(vitePackageJson.path),
12
13
  viteNodeIndexFile
13
14
  );
14
- return import(viteNodePath);
15
+ return import(pathToFileURL(viteNodePath).href);
15
16
  }
16
17
  function importLocal(packageName, path) {
17
18
  const realPath = require2.resolve(packageName, { paths: [path] });
18
- return import(realPath);
19
+ return import(pathToFileURL(realPath).href);
19
20
  }
20
21
 
21
22
  export { importLocal, importVite };
package/dist/lib/log.js CHANGED
@@ -25,8 +25,7 @@ function debounceMessage(args, debounceFor) {
25
25
  const message = item?.message ?? item;
26
26
  return typeof message === "string" ? message : "";
27
27
  }).filter(Boolean).join("");
28
- if (printedMessages.has(key))
29
- return true;
28
+ if (printedMessages.has(key)) return true;
30
29
  printedMessages.add(key);
31
30
  if (debounceFor !== true) {
32
31
  setTimeout(() => printedMessages.delete(key), debounceFor ?? 1e3);
@@ -47,18 +46,15 @@ function injectLogReplacer(method, debouncer) {
47
46
  return;
48
47
  }
49
48
  const replacers = messageReplacers.reduce((acc, [matcher, replacer]) => {
50
- if (matcher(args, acc.length))
51
- acc.push(replacer);
49
+ if (matcher(args, acc.length)) acc.push(replacer);
52
50
  return acc;
53
51
  }, []);
54
- if (replacers.length === 0)
55
- return originalConsole[method](...args);
52
+ if (replacers.length === 0) return originalConsole[method](...args);
56
53
  const result = replacers.reduce(
57
54
  (resultArgs, replacer) => resultArgs && replacer(resultArgs),
58
55
  args
59
56
  );
60
- if (result)
61
- return originalConsole[method](...result);
57
+ if (result) return originalConsole[method](...result);
62
58
  };
63
59
  }
64
60
  }
@@ -200,21 +196,17 @@ function muteAuthLogs({
200
196
  if (process.stdout.write === originalWrite) {
201
197
  const write = originalWrite.bind(process.stdout);
202
198
  process.stdout.write = (item, cb) => {
203
- if (typeof item !== "string")
204
- return write(item, cb);
199
+ if (typeof item !== "string") return write(item, cb);
205
200
  const replacers = messageReplacers.reduce((acc, [matcher, replacer]) => {
206
- if (matcher([item], acc.length))
207
- acc.push(replacer);
201
+ if (matcher([item], acc.length)) acc.push(replacer);
208
202
  return acc;
209
203
  }, []);
210
- if (replacers.length === 0)
211
- return write(item, cb);
204
+ if (replacers.length === 0) return write(item, cb);
212
205
  const result = replacers.reduce(
213
206
  (resultArgs, replacer) => resultArgs && replacer(resultArgs),
214
207
  [item]
215
208
  );
216
- if (result)
217
- return write(result[0], cb);
209
+ if (result) return write(result[0], cb);
218
210
  };
219
211
  }
220
212
  addMessageReplacers(
@@ -225,8 +217,7 @@ function muteAuthLogs({
225
217
  const content = first.replace(" to Shopify Partners", "");
226
218
  const link = content.match(/(https?:\/\/.*)Log in/)?.[1];
227
219
  onKeyTimeout(link);
228
- if (link)
229
- return;
220
+ if (link) return;
230
221
  return [content];
231
222
  }
232
223
  ],
@@ -266,8 +257,7 @@ function enhanceH2Logs(options) {
266
257
  stringArg += "\nRun `h2 link` to link your store.";
267
258
  }
268
259
  const [, type, scope, message] = stringArg.match(/\[h2:([^:]+):([^\]]+)\]\s+(.*)$/ims) || [];
269
- if (!type || !scope || !message)
270
- return args;
260
+ if (!type || !scope || !message) return args;
271
261
  const headline = `In Hydrogen's \`${scope.trim()}\`:
272
262
 
273
263
  `;
@@ -281,8 +271,7 @@ function enhanceH2Logs(options) {
281
271
  colors.magentaBright(`\`${options.cliCommand ?? "$1"} $2\``)
282
272
  );
283
273
  }
284
- if (hasLinks || hasCommands)
285
- lines.pop();
274
+ if (hasLinks || hasCommands) lines.pop();
286
275
  if (type === "error" || errorObject) {
287
276
  let tryMessage = hasLinks || hasCommands ? lastLine : void 0;
288
277
  let stack = errorObject?.stack;
@@ -18,8 +18,7 @@ function logRequestLine({
18
18
  }) {
19
19
  try {
20
20
  const url = new URL(request.url);
21
- if (DEV_ROUTES.has(url.pathname) || url.pathname === "/favicon.ico")
22
- return;
21
+ if (DEV_ROUTES.has(url.pathname) || url.pathname === "/favicon.ico") return;
23
22
  const isDataRequest = url.searchParams.has("_data");
24
23
  let route = request.url.replace(url.origin, "");
25
24
  let info = "";
@@ -42,8 +42,7 @@ async function startNodeServer({
42
42
  }
43
43
  };
44
44
  if (debug) {
45
- if (!inspectorPort)
46
- inspectorPort = await findPort(DEFAULT_INSPECTOR_PORT);
45
+ if (!inspectorPort) inspectorPort = await findPort(DEFAULT_INSPECTOR_PORT);
47
46
  (await import('node:inspector')).open(inspectorPort);
48
47
  }
49
48
  const readWorkerFile = () => readFile(buildPathWorkerFile).catch((error) => {
@@ -51,8 +51,7 @@ function findMissingRoutes(config, requiredRoutes = REQUIRED_ROUTES) {
51
51
  const parentRoute = userRoutes.find(
52
52
  (r) => r.id === currentRoute.parentId
53
53
  );
54
- if (!parentRoute)
55
- break;
54
+ if (!parentRoute) break;
56
55
  currentRoute.path = `${parentRoute.path}/${currentRoute.path}`;
57
56
  currentRoute.parentId = parentRoute.parentId;
58
57
  }
@@ -1,22 +1,24 @@
1
- import { readdir } from 'node:fs/promises';
1
+ import { symlink, readdir } from 'node:fs/promises';
2
2
  import { packageManagerFromUserAgent, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
3
3
  import { renderConfirmationPrompt, renderInfo, renderTextPrompt, renderSelectPrompt, renderFatalError, renderWarning, renderSuccess } from '@shopify/cli-kit/node/ui';
4
4
  import { hyphenate, capitalize } from '@shopify/cli-kit/common/string';
5
5
  import { joinPath, resolvePath, basename } from '@shopify/cli-kit/node/path';
6
6
  import { initializeGitRepository, addAllToGitFromDirectory, createGitCommit } from '@shopify/cli-kit/node/git';
7
7
  import { AbortError } from '@shopify/cli-kit/node/error';
8
- import { rmdir, writeFile, fileExists, isDirectory } from '@shopify/cli-kit/node/fs';
8
+ import { rmdir, copyFile, writeFile, fileExists, isDirectory } from '@shopify/cli-kit/node/fs';
9
9
  import { outputDebug, formatPackageManagerCommand } from '@shopify/cli-kit/node/output';
10
+ import { currentProcessIsGlobal } from '@shopify/cli-kit/node/is-global';
10
11
  import colors from '@shopify/cli-kit/node/colors';
11
12
  import { login, renderLoginSuccess } from '../auth.js';
12
13
  import { renderI18nPrompt, setupI18nStrategy, I18N_STRATEGY_NAME_MAP } from '../setups/i18n/index.js';
13
14
  import { titleize } from '../string.js';
14
15
  import { ALIAS_NAME, createPlatformShortcut } from '../shell.js';
15
16
  import { transpileProject } from '../transpile/index.js';
16
- import { CSS_STRATEGY_NAME_MAP } from '../setups/css/index.js';
17
+ import { renderCssPrompt, setupCssStrategy, CSS_STRATEGY_NAME_MAP } from '../setups/css/index.js';
17
18
  import { renderRoutePrompt, generateRoutes, generateProjectFile } from '../setups/routes/generate.js';
18
19
  import { execAsync } from '../process.js';
19
20
  import { getStorefronts } from '../graphql/admin/link-storefront.js';
21
+ import { isHydrogenMonorepo, getSkeletonSourceDir, getRepoNodeModules } from '../build.js';
20
22
 
21
23
  const LANGUAGES = {
22
24
  js: "JavaScript",
@@ -46,7 +48,7 @@ async function handleRouteGeneration(controller, flagRoutes) {
46
48
  const needsRouteGeneration = routesToScaffold === "all" || routesToScaffold.length > 0;
47
49
  return {
48
50
  needsRouteGeneration,
49
- setupRoutes: async (directory, language, i18nStrategy) => {
51
+ setupRoutes: async (directory, language, options) => {
50
52
  if (needsRouteGeneration) {
51
53
  const result = await generateRoutes(
52
54
  {
@@ -54,8 +56,9 @@ async function handleRouteGeneration(controller, flagRoutes) {
54
56
  directory,
55
57
  force: true,
56
58
  typescript: language === "ts",
57
- localePrefix: i18nStrategy === "subfolders" ? "locale" : false,
58
- signal: controller.signal
59
+ localePrefix: options?.i18nStrategy === "subfolders" ? "locale" : false,
60
+ signal: controller.signal,
61
+ ...options
59
62
  },
60
63
  {
61
64
  rootDirectory: directory,
@@ -75,8 +78,7 @@ function generateProjectEntries(options) {
75
78
  );
76
79
  }
77
80
  async function handleCliShortcut(controller, cliCommand, flagShortcut) {
78
- if (cliCommand === ALIAS_NAME)
79
- return {};
81
+ if (cliCommand === ALIAS_NAME) return {};
80
82
  const shouldCreateShortcut = flagShortcut ?? await renderConfirmationPrompt({
81
83
  confirmationMessage: "Yes",
82
84
  cancellationMessage: "No",
@@ -89,8 +91,7 @@ async function handleCliShortcut(controller, cliCommand, flagShortcut) {
89
91
  ],
90
92
  abortSignal: controller.signal
91
93
  });
92
- if (!shouldCreateShortcut)
93
- return {};
94
+ if (!shouldCreateShortcut) return {};
94
95
  return {
95
96
  createShortcut: async () => {
96
97
  try {
@@ -223,7 +224,33 @@ async function handleLanguage(projectDir, controller, flagLanguage) {
223
224
  };
224
225
  }
225
226
  async function handleCssStrategy(projectDir, controller, flagStyling) {
226
- return {};
227
+ const selection = flagStyling ?? await renderCssPrompt({
228
+ abortSignal: controller.signal,
229
+ extraChoices: { none: "Skip and set up later" }
230
+ });
231
+ const cssStrategy = selection === "none" ? void 0 : selection;
232
+ return {
233
+ cssStrategy,
234
+ async setupCss() {
235
+ if (cssStrategy) {
236
+ if (cssStrategy === "postcss" || cssStrategy === "css-modules") {
237
+ return;
238
+ }
239
+ const result = await setupCssStrategy(
240
+ cssStrategy,
241
+ {
242
+ rootDirectory: projectDir,
243
+ appDirectory: joinPath(projectDir, "app")
244
+ // Default value in new projects
245
+ },
246
+ true
247
+ );
248
+ if (result) {
249
+ await result.workPromise;
250
+ }
251
+ }
252
+ }
253
+ };
227
254
  }
228
255
  async function handleDependencies(projectDir, controller, packageManagerFromFlag, shouldInstallDeps) {
229
256
  const detectedPackageManager = packageManagerFromFlag ?? packageManagerFromUserAgent();
@@ -257,6 +284,20 @@ async function handleDependencies(projectDir, controller, packageManagerFromFlag
257
284
  });
258
285
  }
259
286
  }
287
+ if (isHydrogenMonorepo) {
288
+ await copyFile(
289
+ joinPath(getSkeletonSourceDir(), ".npmrc"),
290
+ joinPath(projectDir, ".npmrc")
291
+ ).catch(() => {
292
+ });
293
+ if (!shouldInstallDeps) {
294
+ await symlink(
295
+ await getRepoNodeModules(),
296
+ joinPath(projectDir, "node_modules")
297
+ ).catch(() => {
298
+ });
299
+ }
300
+ }
260
301
  return {
261
302
  packageManager: actualPackageManager,
262
303
  shouldInstallDeps,
@@ -292,7 +333,7 @@ async function createInitialCommit(directory) {
292
333
  return commitAll(directory, "Scaffold Storefront");
293
334
  } catch (error) {
294
335
  outputDebug(
295
- "Failed to initialize Git.\n" + error?.stack
336
+ "Failed to initialize Git.\n" + (error?.stack ?? error?.message ?? error)
296
337
  );
297
338
  }
298
339
  }
@@ -302,7 +343,7 @@ async function commitAll(directory, message) {
302
343
  await createGitCommit(message, { directory });
303
344
  } catch (error) {
304
345
  outputDebug(
305
- "Failed to commit code.\n" + error?.stack
346
+ "Failed to commit code.\n" + (error?.stack ?? error?.message ?? error)
306
347
  );
307
348
  }
308
349
  }
@@ -395,7 +436,7 @@ async function renderProjectReady(project, {
395
436
  command: [
396
437
  project.directory === process.cwd() ? void 0 : `cd ${project.location.replace(/^\.\//, "")}`,
397
438
  depsInstalled ? void 0 : `${packageManager} install`,
398
- formatPackageManagerCommand(packageManager, "dev")
439
+ currentProcessIsGlobal() ? "npm run dev" : formatPackageManagerCommand(packageManager, "dev")
399
440
  ].filter(Boolean).join(" && ")
400
441
  }
401
442
  ]
@@ -417,10 +458,14 @@ function createAbortHandler(controller, project) {
417
458
  }
418
459
  renderFatalError(
419
460
  new AbortError(
420
- "Failed to initialize project: " + error?.message,
461
+ "Failed to initialize project: " + (error?.message ?? ""),
421
462
  error?.tryMessage ?? error?.stack
422
463
  )
423
464
  );
465
+ if (process.env.SHOPIFY_UNIT_TEST && process.exit.name !== "spy") {
466
+ console.error("Error during test before process.exit:", error);
467
+ throw error;
468
+ }
424
469
  process.exit(1);
425
470
  };
426
471
  }
@@ -1,4 +1,4 @@
1
- import { copy } from 'fs-extra/esm';
1
+ import { cp } from 'node:fs/promises';
2
2
  import { writeFile } from '@shopify/cli-kit/node/fs';
3
3
  import { relativePath, joinPath } from '@shopify/cli-kit/node/path';
4
4
  import { hyphenate } from '@shopify/cli-kit/common/string';
@@ -32,22 +32,21 @@ async function setupLocalStarterTemplate(options, controller) {
32
32
  storefrontInfo,
33
33
  controller
34
34
  });
35
- if (!project)
36
- return;
37
- if (templateAction === "mock")
38
- project.storefrontTitle = "Mock.shop";
35
+ if (!project) return;
36
+ if (templateAction === "mock") project.storefrontTitle = "Mock.shop";
39
37
  const abort = createAbortHandler(controller, project);
40
38
  const createStorefrontPromise = storefrontInfo && !storefrontInfo.id && createStorefront(storefrontInfo.session, storefrontInfo.title).then(async ({ storefront, jobId }) => {
41
- if (jobId)
42
- await waitForJob(storefrontInfo.session, jobId);
39
+ if (jobId) await waitForJob(storefrontInfo.session, jobId);
43
40
  return storefront;
44
41
  }).catch(abort);
45
42
  const templateDir = await getStarterDir();
46
- let backgroundWorkPromise = copy(
43
+ let backgroundWorkPromise = cp(
47
44
  templateDir,
48
45
  project.directory,
49
46
  // Filter out the `app` directory and server.ts, which will be generated later
50
47
  {
48
+ force: true,
49
+ recursive: true,
51
50
  filter: (filepath) => !/^(app\/|dist\/|node_modules\/|server\.ts)/i.test(
52
51
  relativePath(templateDir, filepath)
53
52
  )
@@ -120,10 +119,7 @@ async function setupLocalStarterTemplate(options, controller) {
120
119
  // Set required env vars
121
120
  writeFile(
122
121
  joinPath(project.directory, ".env"),
123
- envLeadingComment + "\n" + [
124
- ["SESSION_SECRET", "foobar"],
125
- ["PUBLIC_STORE_DOMAIN", "mock.shop"]
126
- ].map(([key, value]) => `${key}="${value}"`).join("\n") + "\n"
122
+ envLeadingComment + "\n" + [["SESSION_SECRET", "foobar"]].map(([key, value]) => `${key}="${value}"`).join("\n") + "\n"
127
123
  )
128
124
  );
129
125
  }
@@ -236,7 +232,12 @@ async function setupLocalStarterTemplate(options, controller) {
236
232
  ).catch((error) => {
237
233
  setupSummary.i18nError = error;
238
234
  });
239
- await setupRoutes(project.directory, language, i18nStrategy).then((routes) => {
235
+ await setupRoutes(project.directory, language, {
236
+ i18nStrategy,
237
+ // The init process might have added and modified files. Do not overwrite them.
238
+ // E.g. CSS imports might have been added to the root.
239
+ overwriteFileDeps: false
240
+ }).then((routes) => {
240
241
  setupSummary.routes = routes;
241
242
  if (options.git && routes) {
242
243
  return commitAll(
@@ -7,6 +7,7 @@ import { renderTasks, renderInfo } from '@shopify/cli-kit/node/ui';
7
7
  import { downloadExternalRepo, downloadMonorepoTemplates } from '../template-downloader.js';
8
8
  import { applyTemplateDiff } from '../template-diff.js';
9
9
  import { getCliCommand } from '../shell.js';
10
+ import { replaceFileContent } from '../file.js';
10
11
  import { createAbortHandler, handleProjectLocation, handleLanguage, createInitialCommit, handleDependencies, commitAll, renderProjectReady } from './common.js';
11
12
 
12
13
  const DEMO_STORE_REPO = "shopify/hydrogen-demo-store";
@@ -15,15 +16,12 @@ async function setupRemoteTemplate(options, controller) {
15
16
  let abort = createAbortHandler(controller);
16
17
  const backgroundDownloadPromise = appTemplate.includes("/") ? getExternalTemplate(appTemplate, controller.signal).catch(abort) : getMonorepoTemplate(appTemplate, controller.signal).catch(abort);
17
18
  const project = await handleProjectLocation({ ...options, controller });
18
- if (!project)
19
- return;
19
+ if (!project) return;
20
20
  abort = createAbortHandler(controller, project);
21
21
  const downloaded = await backgroundDownloadPromise;
22
- if (controller.signal.aborted)
23
- return;
22
+ if (controller.signal.aborted) return;
24
23
  let backgroundWorkPromise = Promise.resolve().then(async () => {
25
- if (controller.signal.aborted)
26
- return;
24
+ if (controller.signal.aborted) return;
27
25
  const { sourcePath, skeletonPath } = downloaded;
28
26
  const pkgJson = await readAndParsePackageJson(
29
27
  joinPath(sourcePath, "package.json")
@@ -31,7 +29,17 @@ async function setupRemoteTemplate(options, controller) {
31
29
  if (pkgJson.scripts?.dev?.includes("--diff")) {
32
30
  return applyTemplateDiff(project.directory, sourcePath, skeletonPath);
33
31
  }
34
- return copyFile(sourcePath, project.directory);
32
+ await copyFile(sourcePath, project.directory);
33
+ await replaceFileContent(
34
+ joinPath(project.directory, "package.json"),
35
+ false,
36
+ (content) => (
37
+ // Remove the cli plugin dependency from the package.json because it's
38
+ // only used for monorepo development. This line is present in non-diff
39
+ // examples like `express` when scaffolding a new project:
40
+ content.replace(/^\s*"@shopify\/cli-hydrogen": "[^"]+",?\n/m, "")
41
+ )
42
+ );
35
43
  }).catch(abort);
36
44
  const supportsTranspilation = await fileExists(
37
45
  joinPath(downloaded.sourcePath, "tsconfig.json")
@@ -79,8 +87,7 @@ async function setupRemoteTemplate(options, controller) {
79
87
  }
80
88
  });
81
89
  }
82
- if (controller.signal.aborted)
83
- return;
90
+ if (controller.signal.aborted) return;
84
91
  await renderTasks(tasks);
85
92
  if (options.git) {
86
93
  await commitAll(project.directory, "Lockfile");
@@ -1,5 +1,5 @@
1
+ import { rm, symlink } from 'node:fs/promises';
1
2
  import { vi } from 'vitest';
2
- import { remove, createSymlink } from 'fs-extra/esm';
3
3
  import { writeFile } from '@shopify/cli-kit/node/fs';
4
4
  import { dirname, joinPath } from '@shopify/cli-kit/node/path';
5
5
  import { getSkeletonSourceDir, getRepoNodeModules } from '../build.js';
@@ -43,9 +43,12 @@ vi.mock(
43
43
  renderTasksHook.mockImplementationOnce(async () => {
44
44
  await writeFile(`${directory}/package-lock.json`, "{}");
45
45
  });
46
- await remove(joinPath(directory, "node_modules")).catch(() => {
46
+ await rm(joinPath(directory, "node_modules"), {
47
+ force: true,
48
+ recursive: true
49
+ }).catch(() => {
47
50
  });
48
- await createSymlink(
51
+ await symlink(
49
52
  await getRepoNodeModules(),
50
53
  joinPath(directory, "node_modules")
51
54
  );
@@ -42,8 +42,7 @@ function handleRemixImportFail() {
42
42
  }
43
43
  function getRawRemixConfig(root) {
44
44
  return findFileWithExtension(root, "remix.config").then(({ filepath }) => {
45
- if (!filepath)
46
- throw new AbortError("No remix.config.js file found.");
45
+ if (!filepath) throw new AbortError("No remix.config.js file found.");
47
46
  return createRequire(import.meta.url)(filepath);
48
47
  });
49
48
  }
@@ -152,8 +151,7 @@ async function assertEntryFileExists(root, fileRelative) {
152
151
  const { name, ext } = path.parse(file);
153
152
  return name === path.basename(fileAbsolute) && /^\.[jt]s$/.test(ext);
154
153
  });
155
- if (exists2)
156
- return;
154
+ if (exists2) return;
157
155
  }
158
156
  throw new AbortError(
159
157
  `Entry file "${fileRelative}" not found.`,
@@ -17,8 +17,7 @@ function checkRemixVersions(projectPath, requiredVersionInHydrogen = REQUIRED_RE
17
17
  const outOfSyncPkgs = pkgs.filter(
18
18
  (pkg) => pkg.version && !satisfiesSemver(pkg.version, requiredVersionInHydrogen)
19
19
  );
20
- if (outOfSyncPkgs.length === 0)
21
- return;
20
+ if (outOfSyncPkgs.length === 0) return;
22
21
  const items = outOfSyncPkgs.reduce((acc, item) => {
23
22
  if (item.version) {
24
23
  acc.push(`${item.name}@${item.version}`);
@@ -95,8 +95,7 @@ function createLogRequestEvent(options) {
95
95
  })
96
96
  };
97
97
  eventHistory.push(event);
98
- if (eventHistory.length > 100)
99
- eventHistory.shift();
98
+ if (eventHistory.length > 100) eventHistory.shift();
100
99
  eventEmitter.emit("request", event);
101
100
  return createResponse();
102
101
  };
@@ -116,16 +115,14 @@ function streamRequestEvents(request) {
116
115
  eventEmitter.addListener("request", enqueueEvent);
117
116
  let closed = false;
118
117
  function close() {
119
- if (closed)
120
- return;
118
+ if (closed) return;
121
119
  closed = true;
122
120
  request.signal.removeEventListener("abort", close);
123
121
  eventEmitter.removeListener("request", enqueueEvent);
124
122
  controller.close();
125
123
  }
126
124
  request.signal.addEventListener("abort", close);
127
- if (request.signal.aborted)
128
- return close();
125
+ if (request.signal.aborted) return close();
129
126
  }
130
127
  });
131
128
  return createResponse(stream, {
@@ -5,8 +5,8 @@ import { getAssetsDir } from '../../build.js';
5
5
 
6
6
  const SETUP_CSS_STRATEGIES = [
7
7
  "tailwind",
8
- "css-modules",
9
8
  "vanilla-extract",
9
+ "css-modules",
10
10
  "postcss"
11
11
  ];
12
12
  async function copyAssets(feature, assets, rootDirectory, replacer = (content, filename) => content) {
@@ -2,27 +2,34 @@ import { renderSelectPrompt } from '@shopify/cli-kit/node/ui';
2
2
  import { SETUP_CSS_STRATEGIES } from './assets.js';
3
3
  export { SETUP_CSS_STRATEGIES } from './assets.js';
4
4
  import { setupTailwind } from './tailwind.js';
5
- import { setupPostCss } from './postcss.js';
6
- import { setupCssModules } from './css-modules.js';
7
5
  import { setupVanillaExtract } from './vanilla-extract.js';
8
6
 
9
7
  const STYLING_CHOICES = [...SETUP_CSS_STRATEGIES, "none"];
10
8
  const CSS_STRATEGY_NAME_MAP = {
11
- tailwind: "Tailwind",
12
- "css-modules": "CSS Modules",
9
+ tailwind: "Tailwind (v4 alpha)",
13
10
  "vanilla-extract": "Vanilla Extract",
14
- postcss: "CSS"
11
+ "css-modules": "CSS Modules",
12
+ postcss: "PostCSS"
13
+ };
14
+ const CSS_STRATEGY_HELP_URL_MAP = {
15
+ postcss: "https://vitejs.dev/guide/features.html#postcss",
16
+ "css-modules": "https://vitejs.dev/guide/features.html#css-modules",
17
+ "vanilla-extract": "https://vanilla-extract.style/documentation/styling/",
18
+ tailwind: "https://tailwindcss.com/docs/configuration"
15
19
  };
16
20
  function setupCssStrategy(strategy, options, force) {
17
21
  switch (strategy) {
18
22
  case "tailwind":
19
23
  return setupTailwind(options, force);
20
- case "postcss":
21
- return setupPostCss(options, force);
22
- case "css-modules":
23
- return setupCssModules(options);
24
24
  case "vanilla-extract":
25
25
  return setupVanillaExtract(options);
26
+ case "postcss":
27
+ case "css-modules":
28
+ return {
29
+ workPromise: Promise.resolve(),
30
+ generatedAssets: [],
31
+ needsInstallDeps: false
32
+ };
26
33
  default:
27
34
  throw new Error("Unknown strategy");
28
35
  }
@@ -43,4 +50,4 @@ async function renderCssPrompt(options) {
43
50
  });
44
51
  }
45
52
 
46
- export { CSS_STRATEGY_NAME_MAP, STYLING_CHOICES, renderCssPrompt, setupCssStrategy };
53
+ export { CSS_STRATEGY_HELP_URL_MAP, CSS_STRATEGY_NAME_MAP, STYLING_CHOICES, renderCssPrompt, setupCssStrategy };