@shopify/cli-hydrogen 5.1.2 → 5.2.1

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 (73) hide show
  1. package/dist/commands/hydrogen/build.js +4 -1
  2. package/dist/commands/hydrogen/dev.js +25 -17
  3. package/dist/commands/hydrogen/generate/route.test.js +0 -1
  4. package/dist/commands/hydrogen/init.js +6 -3
  5. package/dist/commands/hydrogen/init.test.js +2 -0
  6. package/dist/commands/hydrogen/preview.js +2 -2
  7. package/dist/commands/hydrogen/setup.js +3 -0
  8. package/dist/generator-templates/starter/app/components/Footer.tsx +1 -1
  9. package/dist/generator-templates/starter/app/components/Header.tsx +1 -1
  10. package/dist/generator-templates/starter/app/components/Search.tsx +3 -3
  11. package/dist/generator-templates/starter/app/entry.server.tsx +9 -1
  12. package/dist/generator-templates/starter/app/root.tsx +31 -5
  13. package/dist/generator-templates/starter/app/routes/$.tsx +4 -0
  14. package/dist/generator-templates/starter/app/routes/_index.tsx +6 -2
  15. package/dist/generator-templates/starter/app/routes/account.$.tsx +1 -2
  16. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +1 -1
  17. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -7
  18. package/dist/generator-templates/starter/app/routes/account.profile.tsx +7 -2
  19. package/dist/generator-templates/starter/app/routes/account.tsx +4 -3
  20. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +6 -2
  21. package/dist/generator-templates/starter/app/routes/account_.login.tsx +6 -2
  22. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +2 -6
  23. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +1 -2
  24. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -2
  25. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +2 -3
  26. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -2
  27. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -2
  28. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +1 -2
  29. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +2 -3
  30. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +23 -15
  31. package/dist/generator-templates/starter/app/routes/search.tsx +1 -2
  32. package/dist/generator-templates/starter/package.json +5 -5
  33. package/dist/generator-templates/starter/remix.config.js +1 -0
  34. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +9 -9
  35. package/dist/generator-templates/starter/tsconfig.json +1 -0
  36. package/dist/lib/ast.js +9 -0
  37. package/dist/lib/check-version.test.js +1 -0
  38. package/dist/lib/codegen.js +17 -7
  39. package/dist/lib/environment-variables.js +15 -11
  40. package/dist/lib/file.js +1 -1
  41. package/dist/lib/find-port.js +9 -0
  42. package/dist/lib/flags.js +6 -5
  43. package/dist/lib/format-code.js +7 -4
  44. package/dist/lib/graphql/admin/client.js +18 -0
  45. package/dist/lib/graphql/admin/client.test.js +28 -3
  46. package/dist/lib/live-reload.js +62 -0
  47. package/dist/lib/log.js +6 -1
  48. package/dist/lib/mini-oxygen.js +28 -18
  49. package/dist/lib/missing-routes.js +17 -1
  50. package/dist/lib/onboarding/common.js +5 -0
  51. package/dist/lib/onboarding/local.js +21 -8
  52. package/dist/lib/remix-config.js +2 -0
  53. package/dist/lib/remix-version-check.test.js +1 -0
  54. package/dist/lib/setups/css/index.js +4 -2
  55. package/dist/lib/setups/css/replacers.js +7 -4
  56. package/dist/lib/setups/i18n/replacers.js +7 -5
  57. package/dist/lib/setups/routes/generate.js +15 -29
  58. package/dist/lib/setups/routes/generate.test.js +1 -3
  59. package/dist/lib/template-downloader.js +4 -0
  60. package/dist/lib/transpile-ts.js +5 -3
  61. package/dist/lib/virtual-routes.js +4 -1
  62. package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +29 -4
  63. package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +44 -10
  64. package/dist/virtual-routes/components/IconBanner.jsx +289 -44
  65. package/dist/virtual-routes/components/IconDiscord.jsx +18 -1
  66. package/dist/virtual-routes/components/IconError.jsx +58 -17
  67. package/dist/virtual-routes/components/IconGithub.jsx +20 -1
  68. package/dist/virtual-routes/components/IconTwitter.jsx +18 -1
  69. package/dist/virtual-routes/components/Layout.jsx +2 -1
  70. package/dist/virtual-routes/routes/index.jsx +199 -94
  71. package/dist/virtual-routes/virtual-root.jsx +62 -16
  72. package/oclif.manifest.json +3 -3
  73. package/package.json +8 -7
@@ -1,27 +1,28 @@
1
1
  import { outputToken, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
2
2
  import { resolvePath } from '@shopify/cli-kit/node/path';
3
- import { fileExists } from '@shopify/cli-kit/node/fs';
3
+ import { readFile, fileExists } from '@shopify/cli-kit/node/fs';
4
4
  import colors from '@shopify/cli-kit/node/colors';
5
5
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
6
+ import { startServer } from '@shopify/mini-oxygen';
7
+ import { DEFAULT_PORT } from './flags.js';
6
8
 
7
9
  async function startMiniOxygen({
8
10
  root,
9
- port = 3e3,
11
+ port = DEFAULT_PORT,
10
12
  watch = false,
13
+ autoReload = watch,
11
14
  buildPathWorkerFile,
12
15
  buildPathClient,
13
16
  env
14
17
  }) {
15
- const { default: miniOxygenImport } = await import('@shopify/mini-oxygen');
16
- const miniOxygenPreview = miniOxygenImport.default ?? miniOxygenImport;
17
18
  const dotenvPath = resolvePath(root, ".env");
18
- const miniOxygen = await miniOxygenPreview({
19
- workerFile: buildPathWorkerFile,
19
+ const miniOxygen = await startServer({
20
+ script: await readFile(buildPathWorkerFile),
20
21
  assetsDir: buildPathClient,
21
22
  publicPath: "",
22
23
  port,
23
24
  watch,
24
- autoReload: watch,
25
+ autoReload,
25
26
  modules: true,
26
27
  env: {
27
28
  ...env,
@@ -30,23 +31,31 @@ async function startMiniOxygen({
30
31
  envPath: !env && await fileExists(dotenvPath) ? dotenvPath : void 0,
31
32
  log: () => {
32
33
  },
33
- buildWatchPaths: watch ? [resolvePath(root, buildPathWorkerFile)] : void 0,
34
- onResponse: (request, response) => logResponse(
35
- request,
36
- response
34
+ onResponse: (request, response) => (
35
+ // 'Request' and 'Response' types in MiniOxygen comes from
36
+ // Miniflare and are slightly different from standard types.
37
+ logResponse(
38
+ request,
39
+ response
40
+ )
37
41
  )
38
42
  });
39
43
  const listeningAt = `http://localhost:${miniOxygen.port}`;
40
44
  return {
41
45
  listeningAt,
42
46
  port: miniOxygen.port,
43
- reload(nextOptions) {
44
- return miniOxygen.reload({
45
- env: {
46
- ...nextOptions?.env ?? env,
47
+ async reload(options = {}) {
48
+ const nextOptions = {};
49
+ if (options.env) {
50
+ nextOptions.env = {
51
+ ...options.env,
47
52
  ...process.env
48
- }
49
- });
53
+ };
54
+ }
55
+ if (options.worker) {
56
+ nextOptions.script = await readFile(buildPathWorkerFile);
57
+ }
58
+ return miniOxygen.reload(nextOptions);
50
59
  },
51
60
  showBanner(options) {
52
61
  console.log("");
@@ -64,8 +73,9 @@ async function startMiniOxygen({
64
73
  function logResponse(request, response) {
65
74
  try {
66
75
  const url = new URL(request.url);
67
- if (["/graphiql"].includes(url.pathname))
76
+ if (["/graphiql"].includes(url.pathname)) {
68
77
  return;
78
+ }
69
79
  const isProxy = !!response.url && response.url !== request.url;
70
80
  const isDataRequest = !isProxy && url.searchParams.has("_data");
71
81
  let route = request.url.replace(url.origin, "");
@@ -3,20 +3,34 @@ import { renderWarning, renderSuccess } from '@shopify/cli-kit/node/ui';
3
3
  const REQUIRED_ROUTES = [
4
4
  "",
5
5
  "cart",
6
+ // 'products',
6
7
  "products/:productHandle",
7
8
  "collections",
9
+ // 'collections/all',
8
10
  "collections/:collectionHandle",
11
+ // 'collections/:collectionHandle/:constraint',
12
+ // 'collections/:collectionHandle/products/:productHandle',
9
13
  "sitemap.xml",
10
14
  "robots.txt",
11
15
  "pages/:pageHandle",
16
+ // 'blogs/:blogHandle/tagged/:tagHandle',
17
+ // 'blogs/:blogHandle/:articleHandle',
18
+ // 'blogs/:blogHandle/:articleHandle/comments',
12
19
  "policies/:policyHandle",
20
+ // 'variants/:variantId',
13
21
  "search",
22
+ // 'gift_cards/:storeId/:cardId',
23
+ // 'discount/:discountCode', => Handled in storefrontRedirect
14
24
  "account",
15
25
  "account/login",
16
26
  "account/register",
27
+ // 'account/addresses',
28
+ // 'account/orders',
17
29
  "account/orders/:orderId",
18
30
  "account/reset/:id/:token",
19
31
  "account/activate/:id/:token"
32
+ // 'password',
33
+ // 'opening_soon',
20
34
  ];
21
35
  function findMissingRoutes(config, requiredRoutes = REQUIRED_ROUTES) {
22
36
  const userRoutes = Object.values(config.routes);
@@ -40,7 +54,9 @@ function findMissingRoutes(config, requiredRoutes = REQUIRED_ROUTES) {
40
54
  currentRoute.parentId = parentRoute.parentId;
41
55
  }
42
56
  const optionalSegment = ":?[^\\/\\?]+\\?";
43
- const reString = `^(${optionalSegment}\\/)?` + requiredRoute.replaceAll(".", "\\.").replace(/\//g, `\\/(${optionalSegment}\\/)?`).replace(/:[^/)?]+/g, ":[^\\/]+") + `(\\/${optionalSegment})?$`;
57
+ const reString = `^(${optionalSegment}\\/)?` + // Starts with an optional segment
58
+ requiredRoute.replaceAll(".", "\\.").replace(/\//g, `\\/(${optionalSegment}\\/)?`).replace(/:[^/)?]+/g, ":[^\\/]+") + // Replace params with regex
59
+ `(\\/${optionalSegment})?$`;
44
60
  if (new RegExp(reString).test(currentRoute.path)) {
45
61
  missingRoutes.delete(requiredRoute);
46
62
  }
@@ -163,7 +163,9 @@ async function handleProjectLocation({
163
163
  return {
164
164
  name: basename(location),
165
165
  location,
166
+ // User input. E.g. "./hydrogen-storefront"
166
167
  directory,
168
+ // Absolute path to location
167
169
  storefrontTitle: storefrontInfo?.title
168
170
  };
169
171
  }
@@ -201,6 +203,7 @@ async function handleCssStrategy(projectDir, controller, flagStyling) {
201
203
  {
202
204
  rootDirectory: projectDir,
203
205
  appDirectory: joinPath(projectDir, "app")
206
+ // Default value in new projects
204
207
  },
205
208
  true
206
209
  );
@@ -344,6 +347,8 @@ async function renderProjectReady(project, {
344
347
  body: bodyLines.map(
345
348
  ([label, value]) => ` ${(label + ":").padEnd(padMin, " ")} ${colors.dim(value)}`
346
349
  ).join("\n") + routeSummary,
350
+ // Use `customSections` instead of `nextSteps` and `references`
351
+ // here to enforce a newline between title and items.
347
352
  customSections: [
348
353
  hasErrors && {
349
354
  title: "Warnings\n",
@@ -18,7 +18,7 @@ async function setupLocalStarterTemplate(options, controller) {
18
18
  message: "Connect to Shopify",
19
19
  choices: [
20
20
  {
21
- label: "Use sample data from Mock.shop (no login required)",
21
+ label: "Use sample data from mock.shop (You can connect a Shopify account later)",
22
22
  value: "mock"
23
23
  },
24
24
  { label: "Link your Shopify account", value: "link" }
@@ -46,17 +46,22 @@ 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
50
  {
50
51
  filter: (filepath) => !/^(app|dist|node_modules)\//i.test(
51
52
  relativePath(templateDir, filepath)
52
53
  )
53
54
  }
54
55
  ).then(
55
- () => generateProjectEntries({
56
- rootDirectory: project.directory,
57
- appDirectory: joinPath(project.directory, "app"),
58
- typescript: true
59
- })
56
+ () => (
57
+ // Generate project entries and their file dependencies
58
+ generateProjectEntries({
59
+ rootDirectory: project.directory,
60
+ appDirectory: joinPath(project.directory, "app"),
61
+ typescript: true
62
+ // Will be transpiled later
63
+ })
64
+ )
60
65
  ).catch(abort);
61
66
  const tasks = [
62
67
  {
@@ -74,6 +79,7 @@ async function setupLocalStarterTemplate(options, controller) {
74
79
  ];
75
80
  backgroundWorkPromise = backgroundWorkPromise.then(() => {
76
81
  const promises = [
82
+ // Add project name to package.json
77
83
  replaceFileContent(
78
84
  joinPath(project.directory, "package.json"),
79
85
  false,
@@ -83,17 +89,23 @@ async function setupLocalStarterTemplate(options, controller) {
83
89
  )
84
90
  )
85
91
  ];
86
- const envLeadingComment = "# The variables added in this file are only available locally in MiniOxygen\n";
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.";
87
93
  if (storefrontInfo && createStorefrontPromise) {
88
94
  promises.push(
95
+ // Save linked storefront in project
89
96
  setUserAccount(project.directory, storefrontInfo),
90
97
  createStorefrontPromise.then(
91
- (storefront) => setStorefront(project.directory, storefront)
98
+ (storefront) => (
99
+ // Save linked storefront in project
100
+ setStorefront(project.directory, storefront)
101
+ )
92
102
  ),
103
+ // Write empty dotenv file to fallback to remote Oxygen variables
93
104
  writeFile(joinPath(project.directory, ".env"), envLeadingComment)
94
105
  );
95
106
  } else if (templateAction === "mock") {
96
107
  promises.push(
108
+ // Set required env vars
97
109
  writeFile(
98
110
  joinPath(project.directory, ".env"),
99
111
  envLeadingComment + "\n" + [
@@ -188,6 +200,7 @@ async function setupLocalStarterTemplate(options, controller) {
188
200
  const { setupRoutes } = await handleRouteGeneration(
189
201
  controller,
190
202
  options.routes || true
203
+ // TODO: Remove default value when multi-select UI component is available
191
204
  );
192
205
  setupSummary.i18n = i18nStrategy;
193
206
  backgroundWorkPromise = backgroundWorkPromise.then(async () => {
@@ -5,6 +5,7 @@ import { readdir } from 'node:fs/promises';
5
5
  import { AbortError } from '@shopify/cli-kit/node/error';
6
6
  import { outputWarn } from '@shopify/cli-kit/node/output';
7
7
  import { fileExists } from '@shopify/cli-kit/node/fs';
8
+ import { muteRemixLogs } from './log.js';
8
9
 
9
10
  const BUILD_DIR = "dist";
10
11
  const CLIENT_SUBDIR = "client";
@@ -25,6 +26,7 @@ function getProjectPaths(appPath, entry) {
25
26
  };
26
27
  }
27
28
  async function getRemixConfig(root, mode = process.env.NODE_ENV) {
29
+ await muteRemixLogs();
28
30
  const { readConfig } = await import('@remix-run/dev/dist/config.js');
29
31
  const config = await readConfig(root, mode);
30
32
  if (process.env.LOCAL_DEV) {
@@ -26,6 +26,7 @@ describe("remix-version-check", () => {
26
26
  it("warns when versions are out of sync", () => {
27
27
  const expectedVersion = "42.0.0-test";
28
28
  vi.mocked(requireMock).mockReturnValueOnce({
29
+ // Hydrogen expected version
29
30
  dependencies: { "@remix-run/dev": expectedVersion }
30
31
  });
31
32
  const outputMock = mockAndCaptureOutput();
@@ -1,10 +1,12 @@
1
1
  import { renderSelectPrompt } from '@shopify/cli-kit/node/ui';
2
+ import { SETUP_CSS_STRATEGIES } from './assets.js';
3
+ export { SETUP_CSS_STRATEGIES } from './assets.js';
2
4
  import { setupTailwind } from './tailwind.js';
3
5
  import { setupPostCss } from './postcss.js';
4
6
  import { setupCssModules } from './css-modules.js';
5
7
  import { setupVanillaExtract } from './vanilla-extract.js';
6
- export { SETUP_CSS_STRATEGIES } from './assets.js';
7
8
 
9
+ const STYLING_CHOICES = [...SETUP_CSS_STRATEGIES, "none"];
8
10
  const CSS_STRATEGY_NAME_MAP = {
9
11
  tailwind: "Tailwind",
10
12
  "css-modules": "CSS Modules",
@@ -41,4 +43,4 @@ async function renderCssPrompt(options) {
41
43
  });
42
44
  }
43
45
 
44
- export { CSS_STRATEGY_NAME_MAP, renderCssPrompt, setupCssStrategy };
46
+ export { CSS_STRATEGY_NAME_MAP, STYLING_CHOICES, renderCssPrompt, setupCssStrategy };
@@ -1,8 +1,7 @@
1
1
  import { AbortError } from '@shopify/cli-kit/node/error';
2
- import { ts, tsx, js, jsx } from '@ast-grep/napi';
3
2
  import { findFileWithExtension, replaceFileContent } from '../../file.js';
3
+ import { importLangAstGrep } from '../../ast.js';
4
4
 
5
- const astGrep = { ts, tsx, js, jsx };
6
5
  async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
7
6
  const { filepath, astType } = await findFileWithExtension(
8
7
  rootDirectory,
@@ -14,7 +13,8 @@ async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
14
13
  );
15
14
  }
16
15
  await replaceFileContent(filepath, formatConfig, async (content) => {
17
- const root = astGrep[astType].parse(content).root();
16
+ const astGrep = await importLangAstGrep(astType);
17
+ const root = astGrep.parse(content).root();
18
18
  const remixConfigNode = root.find({
19
19
  rule: {
20
20
  kind: "object",
@@ -22,9 +22,11 @@ async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
22
22
  any: [
23
23
  {
24
24
  kind: "export_statement"
25
+ // ESM
25
26
  },
26
27
  {
27
28
  kind: "assignment_expression",
29
+ // CJS
28
30
  has: {
29
31
  kind: "member_expression",
30
32
  field: "left",
@@ -77,7 +79,8 @@ async function replaceRootLinks(appDirectory, formatConfig, importer) {
77
79
  if (content.includes(importStatement.split("from")[0])) {
78
80
  return;
79
81
  }
80
- const root = astGrep[astType].parse(content).root();
82
+ const astGrep = await importLangAstGrep(astType);
83
+ const root = astGrep.parse(content).root();
81
84
  const lastImportNode = root.findAll({ rule: { kind: "import_statement" } }).pop();
82
85
  const linksReturnNode = root.find({
83
86
  utils: {
@@ -1,17 +1,17 @@
1
1
  import { AbortError } from '@shopify/cli-kit/node/error';
2
2
  import { joinPath, relativePath } from '@shopify/cli-kit/node/path';
3
3
  import { fileExists } from '@shopify/cli-kit/node/fs';
4
- import { ts, tsx, js, jsx } from '@ast-grep/napi';
5
4
  import { replaceFileContent, findFileWithExtension } from '../../file.js';
5
+ import { importLangAstGrep } from '../../ast.js';
6
6
 
7
- const astGrep = { ts, tsx, js, jsx };
8
7
  async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" }, formatConfig, localeExtractImplementation) {
9
8
  const { filepath, astType } = await findEntryFile({
10
9
  rootDirectory,
11
10
  serverEntryPoint
12
11
  });
13
12
  await replaceFileContent(filepath, formatConfig, async (content) => {
14
- const root = astGrep[astType].parse(content).root();
13
+ const astGrep = await importLangAstGrep(astType);
14
+ const root = astGrep.parse(content).root();
15
15
  const requestIdentifier = root.find({
16
16
  rule: {
17
17
  kind: "identifier",
@@ -58,6 +58,7 @@ async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" },
58
58
  has: {
59
59
  kind: "identifier",
60
60
  regex: `^${hydrogenImportName}`
61
+ // could be appended with " as ..."
61
62
  }
62
63
  }
63
64
  });
@@ -154,10 +155,11 @@ async function replaceRemixEnv({ rootDirectory, serverEntryPoint }, formatConfig
154
155
  rootDirectory,
155
156
  entryFilepath
156
157
  ).replace(/.[tj]sx?$/, "");
157
- await replaceFileContent(remixEnvPath, formatConfig, (content) => {
158
+ await replaceFileContent(remixEnvPath, formatConfig, async (content) => {
158
159
  if (content.includes(`Storefront<`))
159
160
  return;
160
- const root = astGrep.ts.parse(content).root();
161
+ const astGrep = await importLangAstGrep("ts");
162
+ const root = astGrep.parse(content).root();
161
163
  const storefrontTypeNode = root.find({
162
164
  rule: {
163
165
  kind: "property_signature",
@@ -64,8 +64,7 @@ async function generateRoutes(options) {
64
64
  options,
65
65
  v2Flags.isV2RouteConvention
66
66
  );
67
- const typescript = options.typescript ?? !!tsconfigPath;
68
- const transpilerOptions = typescript ? void 0 : await getJsTranspilerOptions(rootDirectory);
67
+ const typescript = !!(options.typescript ?? tsconfigPath?.endsWith("tsconfig.json"));
69
68
  const routes = [];
70
69
  for (const route of routesArray) {
71
70
  routes.push(
@@ -76,7 +75,6 @@ async function generateRoutes(options) {
76
75
  rootDirectory,
77
76
  appDirectory,
78
77
  formatOptions,
79
- transpilerOptions,
80
78
  v2Flags
81
79
  })
82
80
  );
@@ -85,7 +83,6 @@ async function generateRoutes(options) {
85
83
  routes,
86
84
  routeGroups,
87
85
  isTypescript: typescript,
88
- transpilerOptions,
89
86
  v2Flags,
90
87
  formatOptions
91
88
  };
@@ -98,12 +95,12 @@ async function getLocalePrefix(appDirectory, { localePrefix, routeName }, isV2Ro
98
95
  const existingFiles = await readdir(joinPath(appDirectory, "routes")).catch(
99
96
  () => []
100
97
  );
101
- const homeRouteWithLocaleRE = isV2RouteConvention ? /^\(\$(\w+)\)\._index.[jt]sx?$/ : /^\(\$(\w+)\)$/;
102
- const homeRouteWithLocale = existingFiles.find(
103
- (file) => homeRouteWithLocaleRE.test(file)
98
+ const coreRouteWithLocaleRE = isV2RouteConvention ? /^\(\$(\w+)\)\.(_index|\$|cart).[jt]sx?$/ : /^\(\$(\w+)\)$/;
99
+ const coreRouteWithLocale = existingFiles.find(
100
+ (file) => coreRouteWithLocaleRE.test(file)
104
101
  );
105
- if (homeRouteWithLocale) {
106
- return homeRouteWithLocale.match(homeRouteWithLocaleRE)?.[1];
102
+ if (coreRouteWithLocale) {
103
+ return coreRouteWithLocale.match(coreRouteWithLocaleRE)?.[1];
107
104
  }
108
105
  }
109
106
  async function generateProjectFile(routeFrom, {
@@ -113,7 +110,6 @@ async function generateProjectFile(routeFrom, {
113
110
  force,
114
111
  adapter,
115
112
  templatesRoot = getStarterDir(),
116
- transpilerOptions,
117
113
  formatOptions,
118
114
  localePrefix,
119
115
  v2Flags = {},
@@ -159,19 +155,17 @@ async function generateProjectFile(routeFrom, {
159
155
  if (!await fileExists(dirname(destinationPath))) {
160
156
  await mkdir(dirname(destinationPath));
161
157
  }
158
+ const templateAppFilePath = getTemplateAppFile(filePath, templatesRoot);
162
159
  if (!/\.[jt]sx?$/.test(filePath)) {
163
- await copyFile(
164
- getTemplateAppFile(filePath, templatesRoot),
165
- destinationPath
166
- );
160
+ await copyFile(templateAppFilePath, destinationPath);
167
161
  continue;
168
162
  }
169
163
  let templateContent = convertTemplateToRemixVersion(
170
- await readFile(getTemplateAppFile(filePath, templatesRoot)),
164
+ await readFile(templateAppFilePath),
171
165
  v2Flags
172
166
  );
173
167
  if (!typescript) {
174
- templateContent = transpileFile(templateContent, transpilerOptions);
168
+ templateContent = await transpileFile(templateContent);
175
169
  }
176
170
  if (adapter) {
177
171
  templateContent = templateContent.replace(
@@ -179,7 +173,7 @@ async function generateProjectFile(routeFrom, {
179
173
  adapter
180
174
  );
181
175
  }
182
- templateContent = formatCode(
176
+ templateContent = await formatCode(
183
177
  templateContent,
184
178
  formatOptions,
185
179
  destinationPath
@@ -191,7 +185,9 @@ async function generateProjectFile(routeFrom, {
191
185
  function getDestinationRoute(routeFrom, localePrefix, v2Flags) {
192
186
  const routePath = routeFrom.replace(GENERATOR_ROUTE_DIR + "/", "");
193
187
  const filePrefix = localePrefix && !NO_LOCALE_PATTERNS.some((pattern) => pattern.test(routePath)) ? `($${localePrefix})` + (v2Flags.isV2RouteConvention ? "." : "/") : "";
194
- return GENERATOR_ROUTE_DIR + "/" + filePrefix + (v2Flags.isV2RouteConvention ? routePath : convertRouteToV1(routePath));
188
+ return GENERATOR_ROUTE_DIR + "/" + filePrefix + // The template file uses the v2 route convention, so we need to convert
189
+ // it to v1 if the user is not using v2.
190
+ (v2Flags.isV2RouteConvention ? routePath : convertRouteToV1(routePath));
195
191
  }
196
192
  async function findRouteDependencies(routeFilePath, appDirectory) {
197
193
  const filesToCheck = /* @__PURE__ */ new Set([routeFilePath]);
@@ -203,6 +199,7 @@ async function findRouteDependencies(routeFilePath, appDirectory) {
203
199
  continue;
204
200
  match = match.replace(
205
201
  "~",
202
+ // import from '~/components/...'
206
203
  relativePath(dirname(filePath), appDirectory) || "."
207
204
  );
208
205
  const resolvedMatchPath = resolvePath(dirname(filePath), match);
@@ -220,17 +217,6 @@ async function findRouteDependencies(routeFilePath, appDirectory) {
220
217
  }
221
218
  return [...fileDependencies];
222
219
  }
223
- async function getJsTranspilerOptions(rootDirectory) {
224
- const jsConfigPath = joinPath(rootDirectory, "jsconfig.json");
225
- if (!await fileExists(jsConfigPath))
226
- return;
227
- return JSON.parse(
228
- (await readFile(jsConfigPath, { encoding: "utf8" })).replace(
229
- /^\s*\/\/.*$/gm,
230
- ""
231
- )
232
- )?.compilerOptions;
233
- }
234
220
  async function renderRoutePrompt(options) {
235
221
  const generateAll = await renderConfirmationPrompt({
236
222
  message: "Scaffold all standard route files? " + Object.keys(ROUTE_MAP).join(", "),
@@ -39,7 +39,6 @@ describe("generate/route", () => {
39
39
  expect(result).toMatchObject(
40
40
  expect.objectContaining({
41
41
  isTypescript: false,
42
- transpilerOptions: { test: "js" },
43
42
  formatOptions: { singleQuote: false },
44
43
  routes: expect.any(Array)
45
44
  })
@@ -61,7 +60,7 @@ describe("generate/route", () => {
61
60
  });
62
61
  vi.mocked(getRemixConfig).mockResolvedValue({
63
62
  ...directories,
64
- tsconfigPath: "somewhere",
63
+ tsconfigPath: "somewhere/tsconfig.json",
65
64
  future: {
66
65
  v2_routeConvention: true
67
66
  }
@@ -74,7 +73,6 @@ describe("generate/route", () => {
74
73
  expect(result).toMatchObject(
75
74
  expect.objectContaining({
76
75
  isTypescript: true,
77
- transpilerOptions: void 0,
78
76
  routes: expect.any(Array),
79
77
  formatOptions: expect.any(Object)
80
78
  })
@@ -19,6 +19,7 @@ async function getLatestReleaseDownloadUrl(signal) {
19
19
  }
20
20
  const release = await response.json();
21
21
  return {
22
+ // @shopify/package-name@version => package-name@version
22
23
  version: release.name.split("/").pop() ?? release.name,
23
24
  url: release.tarball_url
24
25
  };
@@ -32,8 +33,11 @@ async function downloadTarball(url, storageDir, signal) {
32
33
  );
33
34
  }
34
35
  await pipeline(
36
+ // Download
35
37
  response.body,
38
+ // Decompress
36
39
  gunzipMaybe(),
40
+ // Unpack
37
41
  extract(storageDir, {
38
42
  strip: 1,
39
43
  filter: (name) => {
@@ -1,6 +1,5 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
- import ts from 'typescript';
4
3
  import glob from 'fast-glob';
5
4
  import { outputDebug } from '@shopify/cli-kit/node/output';
6
5
  import { getCodeFormatOptions, formatCode } from './format-code.js';
@@ -18,12 +17,15 @@ const DEFAULT_TS_CONFIG = {
18
17
  forceConsistentCasingInFileNames: true,
19
18
  skipLibCheck: true
20
19
  };
21
- function transpileFile(code, config = DEFAULT_TS_CONFIG) {
20
+ async function transpileFile(code, config = DEFAULT_TS_CONFIG) {
21
+ const tsImport = await import('typescript');
22
+ const ts = tsImport.default ?? tsImport;
22
23
  const withArtificialNewLines = escapeNewLines(code);
23
24
  const compiled = ts.transpileModule(withArtificialNewLines, {
24
25
  reportDiagnostics: false,
25
26
  compilerOptions: {
26
27
  ...config,
28
+ // '1' tells TypeScript to preserve the JSX syntax.
27
29
  jsx: 1,
28
30
  removeComments: false
29
31
  }
@@ -81,7 +83,7 @@ async function transpileProject(projectDir) {
81
83
  continue;
82
84
  }
83
85
  const tsx = await fs.readFile(entry, "utf8");
84
- const mjs = formatCode(transpileFile(tsx), formatConfig);
86
+ const mjs = await formatCode(await transpileFile(tsx), formatConfig);
85
87
  await fs.rm(entry);
86
88
  await fs.writeFile(entry.replace(/\.ts(x?)$/, ".js$1"), mjs, "utf8");
87
89
  }
@@ -12,7 +12,10 @@ async function addVirtualRoutes(config) {
12
12
  const relativeFilePath = path.relative(virtualRoutesPath, absoluteFilePath);
13
13
  const routePath = relativeFilePath.replace(/\.[jt]sx?$/, "").replaceAll("\\", "/");
14
14
  const isIndex = /(^|\/)index$/.test(routePath);
15
- const normalizedVirtualRoutePath = isIndex ? routePath.slice(0, -"index".length).replace(/\/$/, "") || void 0 : routePath.replace(/\$/g, ":").replace(/[\[\]]/g, "");
15
+ const normalizedVirtualRoutePath = isIndex ? routePath.slice(0, -"index".length).replace(/\/$/, "") || void 0 : (
16
+ // TODO: support v2 flat routes?
17
+ routePath.replace(/\$/g, ":").replace(/[\[\]]/g, "")
18
+ );
16
19
  const hasUserRoute = userRouteList.some(
17
20
  (r) => r.parentId === "root" && r.path === normalizedVirtualRoutePath
18
21
  );
@@ -1,7 +1,32 @@
1
- const HydrogenLogoBaseBW = (props) => <svg width={81} height={82} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
2
- <path d="M39.955 81.28 2.138 61.19l12.933-6.818 14.562 7.733 12.218-6.441L27.29 47.93l12.933-6.833L78.04 61.189l-12.934 6.817L51.35 60.7l-12.236 6.457 13.774 7.308-12.933 6.817Z" fill="#000" />
3
- <path fillRule="evenodd" clipRule="evenodd" d="m40.225 0 39.953 21.227-15.073 7.945-13.756-7.308-10.096 5.328 13.775 7.309-15.075 7.945L0 21.22l15.073-7.945 14.562 7.732 10.078-5.313-14.56-7.731L40.225 0ZM29.426 7.967l14.564 7.734L29.63 23.27 15.07 15.537l-10.794 5.69 35.68 18.956 10.793-5.688-13.773-7.307L51.352 19.6l13.757 7.308 10.794-5.69-35.68-18.956-10.797 5.704Z" fill="#000" />
4
- </svg>;
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ const HydrogenLogoBaseBW = (props) => /* @__PURE__ */ jsxs(
3
+ "svg",
4
+ {
5
+ width: 81,
6
+ height: 82,
7
+ fill: "none",
8
+ xmlns: "http://www.w3.org/2000/svg",
9
+ ...props,
10
+ children: [
11
+ /* @__PURE__ */ jsx(
12
+ "path",
13
+ {
14
+ d: "M39.955 81.28 2.138 61.19l12.933-6.818 14.562 7.733 12.218-6.441L27.29 47.93l12.933-6.833L78.04 61.189l-12.934 6.817L51.35 60.7l-12.236 6.457 13.774 7.308-12.933 6.817Z",
15
+ fill: "#000"
16
+ }
17
+ ),
18
+ /* @__PURE__ */ jsx(
19
+ "path",
20
+ {
21
+ fillRule: "evenodd",
22
+ clipRule: "evenodd",
23
+ d: "m40.225 0 39.953 21.227-15.073 7.945-13.756-7.308-10.096 5.328 13.775 7.309-15.075 7.945L0 21.22l15.073-7.945 14.562 7.732 10.078-5.313-14.56-7.731L40.225 0ZM29.426 7.967l14.564 7.734L29.63 23.27 15.07 15.537l-10.794 5.69 35.68 18.956 10.793-5.688-13.773-7.307L51.352 19.6l13.757 7.308 10.794-5.69-35.68-18.956-10.797 5.704Z",
24
+ fill: "#000"
25
+ }
26
+ )
27
+ ]
28
+ }
29
+ );
5
30
  export {
6
31
  HydrogenLogoBaseBW
7
32
  };