@shopify/cli-hydrogen 12.0.0 → 12.0.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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/hydrogen/routes/locale-check.ts +3 -3
  3. package/dist/assets/hydrogen/starter/.graphqlrc.ts +4 -2
  4. package/dist/assets/hydrogen/starter/CHANGELOG.md +271 -0
  5. package/dist/assets/hydrogen/starter/app/components/Aside.tsx +4 -2
  6. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +67 -38
  7. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +49 -8
  8. package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +133 -57
  9. package/dist/assets/hydrogen/starter/app/components/Header.tsx +3 -3
  10. package/dist/assets/hydrogen/starter/app/components/MockShopNotice.tsx +20 -0
  11. package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +24 -4
  12. package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +1 -1
  13. package/dist/assets/hydrogen/starter/app/entry.client.tsx +2 -3
  14. package/dist/assets/hydrogen/starter/app/lib/context.ts +5 -0
  15. package/dist/assets/hydrogen/starter/app/lib/fragments.ts +8 -0
  16. package/dist/assets/hydrogen/starter/app/routes/[robots.txt].tsx +14 -53
  17. package/dist/assets/hydrogen/starter/app/routes/_index.tsx +15 -9
  18. package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +1 -1
  19. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +14 -14
  20. package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +1 -1
  21. package/dist/assets/hydrogen/starter/app/routes/account.tsx +4 -7
  22. package/dist/assets/hydrogen/starter/app/routes/account_.login.tsx +10 -0
  23. package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +2 -9
  24. package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +1 -4
  25. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +3 -11
  26. package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +1 -3
  27. package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +2 -8
  28. package/dist/assets/hydrogen/starter/app/routes/policies.$handle.tsx +1 -4
  29. package/dist/assets/hydrogen/starter/app/routes/policies._index.tsx +1 -1
  30. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +2 -9
  31. package/dist/assets/hydrogen/starter/app/routes/search.tsx +18 -12
  32. package/dist/assets/hydrogen/starter/app/styles/app.css +72 -1
  33. package/dist/assets/hydrogen/starter/eslint.config.js +1 -0
  34. package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +3 -3
  35. package/dist/assets/hydrogen/starter/guides/search/search.md +1 -1
  36. package/dist/assets/hydrogen/starter/package.json +16 -16
  37. package/dist/assets/hydrogen/starter/server.ts +5 -7
  38. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +89 -11
  39. package/dist/assets/hydrogen/starter/vite.config.ts +10 -4
  40. package/dist/assets/hydrogen/vite/package.json +1 -2
  41. package/dist/assets/hydrogen/vite/vite.config.js +4 -3
  42. package/dist/commands/hydrogen/deploy.js +19 -13
  43. package/dist/commands/hydrogen/dev.js +6 -6
  44. package/dist/commands/hydrogen/generate/route.js +1 -1
  45. package/dist/commands/hydrogen/init.d.ts +2 -4
  46. package/dist/commands/hydrogen/init.js +2 -8
  47. package/dist/commands/hydrogen/preview.js +1 -1
  48. package/dist/commands/hydrogen/setup/css.js +1 -1
  49. package/dist/commands/hydrogen/setup/markets.js +1 -1
  50. package/dist/commands/hydrogen/setup/vite.js +1 -1
  51. package/dist/commands/hydrogen/setup.js +31 -33
  52. package/dist/commands/hydrogen/upgrade.js +173 -39
  53. package/dist/lib/build.js +4 -5
  54. package/dist/lib/bundle/vite-plugin.js +7 -21
  55. package/dist/lib/check-lockfile.js +16 -7
  56. package/dist/lib/codegen.js +2 -2
  57. package/dist/lib/format-code.js +3 -3
  58. package/dist/lib/graphql/admin/client.js +48 -23
  59. package/dist/lib/mini-oxygen/workerd.js +3 -5
  60. package/dist/lib/onboarding/common.js +21 -26
  61. package/dist/lib/onboarding/local.js +46 -59
  62. package/dist/lib/package-managers.js +2 -0
  63. package/dist/lib/patch-cli.js +198 -0
  64. package/dist/lib/setups/routes/generate.js +5 -3
  65. package/dist/lib/template-pack.js +93 -0
  66. package/dist/lib/transpile/morph/functions.js +1 -1
  67. package/dist/lib/transpile/morph/typedefs.js +2 -3
  68. package/dist/lib/upgrade-e2e-utils.js +50 -0
  69. package/oclif.manifest.json +7 -15
  70. package/package.json +22 -19
  71. package/dist/assets/hydrogen/starter/app/routes/api.$version.[graphql.json].tsx +0 -14
  72. package/dist/lib/react-router-version-check.js +0 -82
@@ -1,6 +1,6 @@
1
1
  import { symlink, readdir } from 'node:fs/promises';
2
2
  import { packageManagerFromUserAgent, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
3
- import { renderConfirmationPrompt, renderInfo, renderTextPrompt, renderSelectPrompt, renderWarning, renderSuccess, renderFatalError } from '@shopify/cli-kit/node/ui';
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';
@@ -15,7 +15,7 @@ import { titleize } from '../string.js';
15
15
  import { ALIAS_NAME, createPlatformShortcut } from '../shell.js';
16
16
  import { transpileProject } from '../transpile/index.js';
17
17
  import { renderCssPrompt, setupCssStrategy, CSS_STRATEGY_NAME_MAP } from '../setups/css/index.js';
18
- import { renderRoutePrompt, generateRoutes, generateProjectFile } from '../setups/routes/generate.js';
18
+ import { generateRoutes, generateProjectFile } from '../setups/routes/generate.js';
19
19
  import { execAsync } from '../process.js';
20
20
  import { getStorefronts } from '../graphql/admin/link-storefront.js';
21
21
  import { isHydrogenMonorepo, getSkeletonSourceDir, getRepoNodeModules } from '../build.js';
@@ -42,32 +42,25 @@ async function handleI18n(controller, cliCommand, flagI18n) {
42
42
  }
43
43
  };
44
44
  }
45
- async function handleRouteGeneration(controller, flagRoutes) {
46
- const routesToScaffold = flagRoutes === true ? "all" : flagRoutes === false ? [] : await renderRoutePrompt({
47
- abortSignal: controller.signal
48
- });
49
- const needsRouteGeneration = routesToScaffold === "all" || routesToScaffold.length > 0;
45
+ function handleRouteGeneration(controller) {
50
46
  return {
51
- needsRouteGeneration,
52
47
  setupRoutes: async (directory, language, options) => {
53
- if (needsRouteGeneration) {
54
- const result = await generateRoutes(
55
- {
56
- routeName: routesToScaffold,
57
- directory,
58
- force: true,
59
- typescript: language === "ts",
60
- localePrefix: options?.i18nStrategy === "subfolders" ? "locale" : false,
61
- signal: controller.signal,
62
- ...options
63
- },
64
- {
65
- rootDirectory: directory,
66
- appDirectory: joinPath(directory, "app")
67
- }
68
- );
69
- return result.routeGroups;
70
- }
48
+ const result = await generateRoutes(
49
+ {
50
+ routeName: "all",
51
+ directory,
52
+ force: true,
53
+ typescript: language === "ts",
54
+ localePrefix: options?.i18nStrategy === "subfolders" ? "locale" : false,
55
+ signal: controller.signal,
56
+ ...options
57
+ },
58
+ {
59
+ rootDirectory: directory,
60
+ appDirectory: joinPath(directory, "app")
61
+ }
62
+ );
63
+ return result.routeGroups;
71
64
  }
72
65
  };
73
66
  }
@@ -284,6 +277,8 @@ async function handleDependencies(projectDir, controller, packageManagerFromFlag
284
277
  cancellationMessage: "No",
285
278
  abortSignal: controller.signal
286
279
  });
280
+ } else {
281
+ actualPackageManager = detectedPackageManager;
287
282
  }
288
283
  }
289
284
  if (isHydrogenMonorepo) {
@@ -2,8 +2,7 @@ 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';
5
- import colors from '@shopify/cli-kit/node/colors';
6
- import { renderSelectPrompt, renderSuccess, renderConfirmationPrompt, renderTasks } from '@shopify/cli-kit/node/ui';
5
+ import { renderSelectPrompt, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
7
6
  import { handleStorefrontLink, handleProjectLocation, createAbortHandler, generateProjectEntries, handleLanguage, createInitialCommit, handleCssStrategy, commitAll, handleDependencies, handleCliShortcut, handleI18n, handleRouteGeneration, renderProjectReady } from './common.js';
8
7
  import { createStorefront } from '../graphql/admin/create-storefront.js';
9
8
  import { waitForJob } from '../graphql/admin/fetch-job.js';
@@ -49,7 +48,7 @@ async function setupLocalStarterTemplate(options, controller) {
49
48
  {
50
49
  force: true,
51
50
  recursive: true,
52
- filter: (filepath) => !/^(app\/|dist\/|node_modules\/|server\.ts|\.shopify\/)/i.test(
51
+ filter: (filepath) => !/^(app(\/|$)|dist(\/|$)|node_modules(\/|$)|server\.ts$|\.shopify(\/|$))/i.test(
53
52
  relativePath(templateDir, filepath)
54
53
  )
55
54
  }
@@ -217,65 +216,53 @@ async function setupLocalStarterTemplate(options, controller) {
217
216
  ]
218
217
  });
219
218
  }
220
- const continueWithSetup = (options.i18n ?? options.routes) !== void 0 || await renderConfirmationPrompt({
221
- message: "Do you want to scaffold routes and core functionality?",
222
- confirmationMessage: "Yes, set up now",
223
- cancellationMessage: "No, set up later " + colors.dim(`(run \`${setupSummary.cliCommand} setup\`)`),
224
- abortSignal: controller.signal
225
- });
226
- if (continueWithSetup) {
227
- const { i18nStrategy, setupI18n } = await handleI18n(
228
- controller,
229
- setupSummary.cliCommand,
230
- options.i18n
231
- );
232
- const { setupRoutes } = await handleRouteGeneration(
233
- controller,
234
- options.routes ?? true
235
- // TODO: Remove default value when multi-select UI component is available
236
- );
237
- setupSummary.i18n = i18nStrategy;
238
- backgroundWorkPromise = backgroundWorkPromise.then(async () => {
239
- await setupI18n({
240
- rootDirectory: project.directory,
241
- contextCreate: language === "ts" ? "app/lib/context.ts" : "app/lib/context.js"
242
- }).then(
243
- () => options.git ? commitAll(
244
- project.directory,
245
- `Setup markets support using ${i18nStrategy}`
246
- ) : void 0
247
- ).catch((error) => {
248
- setupSummary.i18nError = error;
249
- });
250
- await setupRoutes(project.directory, language, {
251
- i18nStrategy,
252
- // The init process might have added and modified files. Do not overwrite them.
253
- // E.g. CSS imports might have been added to the root.
254
- overwriteFileDeps: false
255
- }).then(async (routes) => {
256
- setupSummary.routes = routes;
257
- if (language === "ts" && setupSummary.depsInstalled) {
258
- try {
259
- await execAsync("npx react-router typegen", {
260
- cwd: project.directory
261
- });
262
- } catch (error) {
263
- outputWarn(
264
- "Failed to generate React Router types after route generation. You may need to run `npx react-router typegen` manually."
265
- );
266
- }
267
- }
268
- if (options.git && routes) {
269
- return commitAll(
270
- project.directory,
271
- `Generate routes for core functionality`
219
+ const { i18nStrategy, setupI18n } = await handleI18n(
220
+ controller,
221
+ setupSummary.cliCommand,
222
+ options.i18n
223
+ );
224
+ const { setupRoutes } = handleRouteGeneration(controller);
225
+ setupSummary.i18n = i18nStrategy;
226
+ backgroundWorkPromise = backgroundWorkPromise.then(async () => {
227
+ await setupI18n({
228
+ rootDirectory: project.directory,
229
+ contextCreate: language === "ts" ? "app/lib/context.ts" : "app/lib/context.js"
230
+ }).then(
231
+ () => options.git ? commitAll(
232
+ project.directory,
233
+ `Setup markets support using ${i18nStrategy}`
234
+ ) : void 0
235
+ ).catch((error) => {
236
+ setupSummary.i18nError = error;
237
+ });
238
+ await setupRoutes(project.directory, language, {
239
+ i18nStrategy,
240
+ // The init process might have added and modified files. Do not overwrite them.
241
+ // E.g. CSS imports might have been added to the root.
242
+ overwriteFileDeps: false
243
+ }).then(async (routes) => {
244
+ setupSummary.routes = routes;
245
+ if (language === "ts" && setupSummary.depsInstalled) {
246
+ try {
247
+ await execAsync("npx react-router typegen", {
248
+ cwd: project.directory
249
+ });
250
+ } catch (error) {
251
+ outputWarn(
252
+ "Failed to generate React Router types after route generation. You may need to run `npx react-router typegen` manually."
272
253
  );
273
254
  }
274
- }).catch((error) => {
275
- setupSummary.routesError = error;
276
- });
255
+ }
256
+ if (options.git && routes) {
257
+ return commitAll(
258
+ project.directory,
259
+ `Generate routes for core functionality`
260
+ );
261
+ }
262
+ }).catch((error) => {
263
+ setupSummary.routesError = error;
277
264
  });
278
- }
265
+ });
279
266
  await renderTasks(tasks);
280
267
  if (options.git) {
281
268
  await commitAll(project.directory, "Lockfile");
@@ -2,6 +2,7 @@ const packageManagers = [
2
2
  {
3
3
  name: "npm",
4
4
  lockfile: "package-lock.json",
5
+ alternativeLockfiles: ["npm-shrinkwrap.json"],
5
6
  installCommand: "npm ci"
6
7
  },
7
8
  {
@@ -17,6 +18,7 @@ const packageManagers = [
17
18
  {
18
19
  name: "bun",
19
20
  lockfile: "bun.lockb",
21
+ alternativeLockfiles: ["bun.lock"],
20
22
  installCommand: "bun install --frozen-lockfile"
21
23
  }
22
24
  ];
@@ -0,0 +1,198 @@
1
+ import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ function isErrnoException(err) {
5
+ return err instanceof Error && "code" in err;
6
+ }
7
+ const MARKER = "// [hydrogen-monorepo-patch]";
8
+ function getRunJsPath(root) {
9
+ return resolve(root, "node_modules", "@shopify", "cli", "bin", "run.js");
10
+ }
11
+ function isPatchApplied(content) {
12
+ return content.includes(MARKER);
13
+ }
14
+ function generatePatchedContent() {
15
+ return `#!/usr/bin/env node
16
+ ${MARKER}
17
+
18
+ // Mirrors upstream @shopify/cli/bin/run.js behavior.
19
+ // This is not introduced by the patch \u2014 @shopify/cli ships with this line.
20
+ // Changing it here would create a behavioral divergence between dev and production.
21
+ process.removeAllListeners('warning')
22
+
23
+ // --- Monorepo detection ---
24
+ // Walk up from cwd to find the hydrogen monorepo root.
25
+ // We look for packages/cli/package.json with the correct package name \u2014
26
+ // if it matches, we know we're inside the monorepo and should load the
27
+ // local @shopify/cli-hydrogen
28
+ const {existsSync, readFileSync} = await import('node:fs');
29
+ const {resolve, dirname} = await import('node:path');
30
+
31
+ let monorepoRoot = null;
32
+ let dir = process.cwd();
33
+ while (true) {
34
+ const candidate = resolve(dir, 'packages', 'cli', 'package.json');
35
+ if (existsSync(candidate)) {
36
+ try {
37
+ const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
38
+ if (pkg.name === '@shopify/cli-hydrogen') {
39
+ monorepoRoot = dir;
40
+ break;
41
+ }
42
+ } catch {
43
+ // Ignore malformed package.json and keep walking
44
+ }
45
+ }
46
+ const parent = dirname(dir);
47
+ if (parent === dir) break; // reached filesystem root
48
+ dir = parent;
49
+ }
50
+
51
+ if (monorepoRoot) {
52
+ // We're in the hydrogen monorepo. Start @shopify/cli normally but
53
+ // inject pluginAdditions so oclif loads @shopify/cli-hydrogen from
54
+ // the monorepo's workspace (packages/cli) instead of the version
55
+ // bundled inside @shopify/cli/dist.
56
+ //
57
+ // This uses the same pluginAdditions mechanism that ShopifyConfig
58
+ // uses internally
59
+ const {fileURLToPath} = await import('node:url');
60
+ const {Config, run, flush} = await import('@oclif/core');
61
+
62
+ // root must point to @shopify/cli's installed location so oclif
63
+ // can find its package.json, oclif config, and bundled commands.
64
+ const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
65
+
66
+ const c = '\\x1b[38;5;209m';
67
+ const d = '\\x1b[2m';
68
+ const r = '\\x1b[0m';
69
+ console.log('');
70
+ console.log(c + ' \u250C\u2500\u2500 hydrogen-monorepo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510' + r);
71
+ console.log(c + ' \u2502' + r + ' ' + c + '\u2502' + r);
72
+ console.log(c + ' \u2502' + r + ' Using local cli-hydrogen plugin from packages/cli ' + c + '\u2502' + r);
73
+ console.log(c + ' \u2502' + r + d + ' Bundled commands replaced with local source ' + r + c + '\u2502' + r);
74
+ console.log(c + ' \u2502' + r + ' ' + c + '\u2502' + r);
75
+ console.log(c + ' \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518' + r);
76
+ console.log('');
77
+
78
+ // Tell ShopifyConfig to skip its own monorepo detection since we
79
+ // are handling pluginAdditions ourselves.
80
+ process.env.IGNORE_HYDROGEN_MONOREPO = '1';
81
+
82
+ const config = new Config({
83
+ root: cliRoot,
84
+ // pluginAdditions tells oclif's plugin loader to read the
85
+ // monorepo root's package.json, find @shopify/cli-hydrogen in
86
+ // its dependencies (workspace:*), and load that as a core plugin.
87
+ // Because workspace:* symlinks to packages/cli, oclif loads the
88
+ // local source code \u2014 exactly what we want for development.
89
+ pluginAdditions: {
90
+ core: ['@shopify/cli-hydrogen'],
91
+ path: monorepoRoot,
92
+ },
93
+ // Skip the oclif manifest cache so commands are loaded fresh from
94
+ // disk rather than from a potentially stale oclif.manifest.json.
95
+ ignoreManifest: true,
96
+ });
97
+
98
+ await config.load();
99
+
100
+ // --- Post-load command replacement ---
101
+ // After loading, both the bundled hydrogen commands (from @shopify/cli's
102
+ // root plugin) and the local ones (from pluginAdditions) are registered.
103
+ // Since @shopify/cli@3.83.0 (which upgraded to oclif v4),
104
+ // determinePriority is no longer an overridable instance method, so we
105
+ // manually replace the bundled commands with the external plugin's
106
+ // versions using oclif's private _commands Map.
107
+ const externalPlugin = Array.from(config.plugins.values()).find(
108
+ (p) => p.name === '@shopify/cli-hydrogen' && !p.isRoot,
109
+ );
110
+
111
+ if (!externalPlugin) {
112
+ throw new Error(
113
+ '[hydrogen-monorepo] Could not find local @shopify/cli-hydrogen plugin. ' +
114
+ 'The patch may need updating for the current @shopify/cli version.'
115
+ );
116
+ }
117
+
118
+ const cmds = config._commands;
119
+ if (!cmds || !(cmds instanceof Map)) {
120
+ throw new Error(
121
+ '[hydrogen-monorepo] Cannot replace bundled commands \u2014 oclif internals changed. ' +
122
+ 'The patch may need updating for the current oclif version.'
123
+ );
124
+ }
125
+
126
+ // Delete bundled hydrogen commands (canonical IDs + aliases + hidden aliases)
127
+ for (const command of externalPlugin.commands) {
128
+ if (!command.id.startsWith('hydrogen')) continue;
129
+ cmds.delete(command.id);
130
+ for (const alias of [...(command.aliases ?? []), ...(command.hiddenAliases ?? [])]) {
131
+ cmds.delete(alias);
132
+ }
133
+ }
134
+ // Re-insert commands from the local plugin. loadCommands handles
135
+ // alias registration and command permutations correctly.
136
+ config.loadCommands(externalPlugin);
137
+
138
+ await run(process.argv.slice(2), config);
139
+ await flush();
140
+ } else {
141
+ // Not in the monorepo \u2014 run the standard @shopify/cli entrypoint.
142
+ const {default: runCLI} = await import('../dist/index.js');
143
+ runCLI({development: false});
144
+ }
145
+ `;
146
+ }
147
+ function generateOriginalContent() {
148
+ return `#!/usr/bin/env node
149
+
150
+ process.removeAllListeners('warning')
151
+
152
+ import runCLI from '../dist/index.js'
153
+
154
+ runCLI({development: false})
155
+ `;
156
+ }
157
+ function applyPatch(runJsPath) {
158
+ let current;
159
+ try {
160
+ current = readFileSync(runJsPath, "utf8");
161
+ } catch (err) {
162
+ if (isErrnoException(err) && err.code === "ENOENT") {
163
+ throw new Error(
164
+ `@shopify/cli is not installed (${runJsPath} not found). Run 'pnpm install' first.`
165
+ );
166
+ }
167
+ throw err;
168
+ }
169
+ if (isPatchApplied(current)) return false;
170
+ const backupPath = runJsPath + ".backup";
171
+ if (!existsSync(backupPath)) {
172
+ writeFileSync(backupPath, current);
173
+ }
174
+ writeFileSync(runJsPath, generatePatchedContent());
175
+ return true;
176
+ }
177
+ function removePatch(runJsPath) {
178
+ let current;
179
+ try {
180
+ current = readFileSync(runJsPath, "utf8");
181
+ } catch (err) {
182
+ if (isErrnoException(err) && err.code === "ENOENT") {
183
+ return false;
184
+ }
185
+ throw err;
186
+ }
187
+ if (!isPatchApplied(current)) return false;
188
+ const backupPath = runJsPath + ".backup";
189
+ const hasBackup = existsSync(backupPath);
190
+ const original = hasBackup ? readFileSync(backupPath, "utf8") : generateOriginalContent();
191
+ writeFileSync(runJsPath, original);
192
+ if (hasBackup) {
193
+ unlinkSync(backupPath);
194
+ }
195
+ return true;
196
+ }
197
+
198
+ export { MARKER, applyPatch, generateOriginalContent, generatePatchedContent, getRunJsPath, isPatchApplied, removePatch };
@@ -22,8 +22,7 @@ const ROUTE_MAP = {
22
22
  account: "account*",
23
23
  search: ["search", "api.predictive-search"],
24
24
  robots: "[robots.txt]",
25
- sitemap: ["[sitemap.xml]", "sitemap.$type.$page[.xml]"],
26
- tokenlessApi: "api.$version.[graphql.json]"
25
+ sitemap: ["[sitemap.xml]", "sitemap.$type.$page[.xml]"]
27
26
  };
28
27
  let allRouteTemplateFiles = [];
29
28
  async function getResolvedRoutes(routeKeys = Object.keys(ROUTE_MAP)) {
@@ -187,7 +186,10 @@ async function generateProjectFile(routeFrom, {
187
186
  return result;
188
187
  }
189
188
  function replaceAdapters(templateContent, adapter) {
190
- return templateContent.replace(/@shopify\/remix-oxygen/g, adapter);
189
+ return templateContent.replace(
190
+ /(from\s+['"])react-router(['"])/g,
191
+ `$1${adapter}$2`
192
+ );
191
193
  }
192
194
  function getDestinationRoute(routeFrom, localePrefix, options) {
193
195
  const routePath = routeFrom.replace(ASSETS_STARTER_DIR_ROUTES + "/", "");
@@ -0,0 +1,93 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { readFile, writeFile, mkdtemp, rm } from 'node:fs/promises';
4
+ import { join, isAbsolute } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import gunzipMaybe from 'gunzip-maybe';
8
+ import { extract } from 'tar-fs';
9
+
10
+ const WINDOWS_SHELL_OPTS = process.platform === "win32" ? { shell: true } : {};
11
+ const PNPM_PACK_TIMEOUT_IN_MS = 6e4;
12
+ const DEPENDENCY_SECTIONS = [
13
+ "dependencies",
14
+ "devDependencies",
15
+ "peerDependencies",
16
+ "optionalDependencies"
17
+ ];
18
+ function hasReplaceableProtocol(version) {
19
+ return version.startsWith("workspace:") || version.startsWith("catalog:");
20
+ }
21
+ async function getPackedTemplatePackageJson(sourceTemplateDir) {
22
+ const tempDir = await mkdtemp(join(tmpdir(), "hydrogen-template-pack-"));
23
+ try {
24
+ const rawPackResult = execFileSync(
25
+ "pnpm",
26
+ ["pack", "--pack-destination", tempDir, "--json"],
27
+ {
28
+ cwd: sourceTemplateDir,
29
+ encoding: "utf8",
30
+ timeout: PNPM_PACK_TIMEOUT_IN_MS,
31
+ ...WINDOWS_SHELL_OPTS
32
+ }
33
+ );
34
+ const parsedResult = JSON.parse(rawPackResult.trim());
35
+ const packedTarball = Array.isArray(parsedResult) ? parsedResult[0]?.filename : parsedResult.filename;
36
+ if (!packedTarball) {
37
+ throw new Error("pnpm pack did not return a tarball filename.");
38
+ }
39
+ const tarballPath = isAbsolute(packedTarball) ? packedTarball : join(tempDir, packedTarball);
40
+ await pipeline(
41
+ createReadStream(tarballPath),
42
+ gunzipMaybe(),
43
+ extract(tempDir)
44
+ );
45
+ const packedManifestRaw = await readFile(
46
+ join(tempDir, "package", "package.json"),
47
+ "utf8"
48
+ );
49
+ return JSON.parse(packedManifestRaw);
50
+ } finally {
51
+ await rm(tempDir, { recursive: true, force: true });
52
+ }
53
+ }
54
+ function replaceWorkspaceProtocolVersionsInPackageJson({
55
+ packedManifest,
56
+ targetPackageJson
57
+ }) {
58
+ for (const section of DEPENDENCY_SECTIONS) {
59
+ const targetDeps = targetPackageJson[section];
60
+ if (!targetDeps) continue;
61
+ for (const [name, version] of Object.entries(targetDeps)) {
62
+ if (!hasReplaceableProtocol(version)) continue;
63
+ const packedVersion = packedManifest[section]?.[name];
64
+ if (!packedVersion) {
65
+ throw new Error(
66
+ `Unable to resolve ${name} from ${section} in packed template manifest.`
67
+ );
68
+ }
69
+ targetDeps[name] = packedVersion;
70
+ }
71
+ }
72
+ }
73
+ async function replaceWorkspaceProtocolVersions({
74
+ sourceTemplateDir,
75
+ targetTemplateDir
76
+ }) {
77
+ const packedManifest = await getPackedTemplatePackageJson(sourceTemplateDir);
78
+ const targetPackageJsonPath = join(targetTemplateDir, "package.json");
79
+ const targetPackageJson = JSON.parse(
80
+ await readFile(targetPackageJsonPath, "utf8")
81
+ );
82
+ replaceWorkspaceProtocolVersionsInPackageJson({
83
+ packedManifest,
84
+ targetPackageJson
85
+ });
86
+ await writeFile(
87
+ targetPackageJsonPath,
88
+ `${JSON.stringify(targetPackageJson, null, 2)}
89
+ `
90
+ );
91
+ }
92
+
93
+ export { WINDOWS_SHELL_OPTS, replaceWorkspaceProtocolVersions };
@@ -35,7 +35,7 @@ function isAnnotableType(type) {
35
35
  }
36
36
  function normalizeType(type) {
37
37
  const normalizedType = type.replace(/^\(\)\s+=>\s+/, "").replace(/typeof import\("[^"]+"\)\./, "typeof ");
38
- return normalizedType === "SerializeFrom<any>" ? `{SerializeFrom<loader>}` : `{${normalizedType}}`;
38
+ return normalizedType === "SerializeFrom<any>" ? `{ReturnType<typeof useLoaderData<typeof loader>>}` : `{${normalizedType}}`;
39
39
  }
40
40
  function generateParameterDocumentation(functionNode, docNode) {
41
41
  const params = functionNode.getParameters();
@@ -28,7 +28,6 @@ function generateTypeDefs(sourceFile, code) {
28
28
  typedefs.push("");
29
29
  const knownGenerics = {
30
30
  MetaFunction: "T",
31
- SerializeFrom: "T",
32
31
  Fetcher: "T",
33
32
  PartialPredictiveSearchResult: "ItemType, ExtraProps",
34
33
  PartialSearchResult: "ItemType"
@@ -52,7 +51,7 @@ function generateTypeDefs(sourceFile, code) {
52
51
  }
53
52
  if (/(function loader\(|const loader =)/.test(source)) {
54
53
  typedefs.push(
55
- `/** @typedef {import('@shopify/remix-oxygen').SerializeFrom<typeof loader>} LoaderReturnData */`
54
+ `/** @typedef {ReturnType<typeof useLoaderData<typeof loader>>} LoaderReturnData */`
56
55
  );
57
56
  code = code.replace(
58
57
  /^\s+?const\s+([\w\d]+|\{[\w\d\s,.]+\})\s+?=\s+?useLoaderData\(\)/gms,
@@ -72,7 +71,7 @@ function generateTypeDefs(sourceFile, code) {
72
71
  }
73
72
  if (/(function action\(|const action =)/.test(source)) {
74
73
  typedefs.push(
75
- `/** @typedef {import('@shopify/remix-oxygen').SerializeFrom<typeof action>} ActionReturnData */`
74
+ `/** @typedef {ReturnType<typeof useActionData<typeof action>>} ActionReturnData */`
76
75
  );
77
76
  code = code.replace(
78
77
  /^\s+?const\s+([\w\d]+|\{[\w\d\s,.]+\})\s+?=\s+?useActionData\(\)/gms,
@@ -0,0 +1,50 @@
1
+ const DEPENDENCY_SECTIONS = [
2
+ "dependencies",
3
+ "devDependencies"
4
+ ];
5
+ function parseCatalogFromWorkspaceYaml(yamlContent) {
6
+ const catalogVersions = {};
7
+ const catalogMatch = yamlContent.match(/^catalog:\s*\n((?:[ \t]+.+\n?)*)/m);
8
+ const catalogSection = catalogMatch?.[1];
9
+ if (!catalogSection) return catalogVersions;
10
+ for (const line of catalogSection.split("\n")) {
11
+ const match = line.match(
12
+ /^\s+['"]?([^'":]+?)['"]?\s*:\s*['"]?([^'"]+?)['"]?\s*$/
13
+ );
14
+ if (match?.[1] && match[2]) {
15
+ catalogVersions[match[1].trim()] = match[2].trim();
16
+ }
17
+ }
18
+ return catalogVersions;
19
+ }
20
+ function parseWorkspacePackagesFromYaml(yamlContent) {
21
+ const packagesMatch = yamlContent.match(
22
+ /^packages:\s*\n((?:[ \t]+-.+\n?)*)/m
23
+ );
24
+ const packagesSection = packagesMatch?.[1];
25
+ if (!packagesSection) return [];
26
+ return packagesSection.split("\n").map((line) => line.match(/^\s+-\s+(.+)/)?.[1]?.trim()).filter((path) => Boolean(path));
27
+ }
28
+ async function resolveWorkspaceProtocols({
29
+ packageJson,
30
+ catalogVersions,
31
+ resolveWorkspaceVersion,
32
+ fallbackVersion
33
+ }) {
34
+ const resolved = JSON.parse(JSON.stringify(packageJson));
35
+ for (const section of DEPENDENCY_SECTIONS) {
36
+ const deps = resolved[section];
37
+ if (!deps) continue;
38
+ for (const [name, version] of Object.entries(deps)) {
39
+ if (version.startsWith("workspace:")) {
40
+ const resolvedVersion = await resolveWorkspaceVersion(name);
41
+ deps[name] = resolvedVersion ?? fallbackVersion;
42
+ } else if (version === "catalog:" && catalogVersions[name]) {
43
+ deps[name] = catalogVersions[name];
44
+ }
45
+ }
46
+ }
47
+ return resolved;
48
+ }
49
+
50
+ export { parseCatalogFromWorkspaceYaml, parseWorkspacePackagesFromYaml, resolveWorkspaceProtocols };