@shopify/cli-hydrogen 4.0.9 → 4.1.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 (74) hide show
  1. package/dist/commands/hydrogen/build.d.ts +4 -4
  2. package/dist/commands/hydrogen/build.js +17 -16
  3. package/dist/commands/hydrogen/check.d.ts +3 -6
  4. package/dist/commands/hydrogen/check.js +10 -9
  5. package/dist/commands/hydrogen/dev.d.ts +3 -3
  6. package/dist/commands/hydrogen/dev.js +22 -21
  7. package/dist/commands/hydrogen/g.d.ts +10 -0
  8. package/dist/commands/hydrogen/g.js +17 -0
  9. package/dist/commands/hydrogen/generate/route.d.ts +7 -9
  10. package/dist/commands/hydrogen/generate/route.js +49 -47
  11. package/dist/commands/hydrogen/generate/route.test.js +48 -40
  12. package/dist/commands/hydrogen/generate/routes.d.ts +2 -2
  13. package/dist/commands/hydrogen/init.d.ts +3 -3
  14. package/dist/commands/hydrogen/init.js +74 -93
  15. package/dist/commands/hydrogen/init.test.js +71 -24
  16. package/dist/commands/hydrogen/preview.d.ts +2 -2
  17. package/dist/commands/hydrogen/preview.js +4 -4
  18. package/dist/commands/hydrogen/shortcut.d.ts +9 -0
  19. package/dist/commands/hydrogen/shortcut.js +74 -0
  20. package/dist/commands/hydrogen/shortcut.test.js +58 -0
  21. package/dist/generator-templates/routes/[robots.txt].tsx +35 -1
  22. package/dist/generator-templates/routes/[sitemap.xml].tsx +33 -2
  23. package/dist/generator-templates/routes/account/login.tsx +42 -13
  24. package/dist/generator-templates/routes/account/register.tsx +42 -13
  25. package/dist/generator-templates/routes/cart.tsx +42 -2
  26. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +44 -5
  27. package/dist/generator-templates/routes/index.tsx +33 -0
  28. package/dist/generator-templates/routes/pages/$pageHandle.tsx +48 -10
  29. package/dist/generator-templates/routes/policies/$policyHandle.tsx +67 -14
  30. package/dist/generator-templates/routes/policies/index.tsx +54 -4
  31. package/dist/generator-templates/routes/products/$productHandle.tsx +44 -9
  32. package/dist/hooks/init.js +2 -2
  33. package/dist/{utils → lib}/check-lockfile.js +7 -4
  34. package/dist/{utils → lib}/check-lockfile.test.js +19 -28
  35. package/dist/{utils → lib}/check-version.test.js +3 -2
  36. package/dist/lib/colors.d.ts +8 -0
  37. package/dist/lib/colors.js +8 -0
  38. package/dist/{utils → lib}/config.js +9 -18
  39. package/dist/{utils → lib}/flags.d.ts +3 -3
  40. package/dist/{utils → lib}/flags.js +4 -4
  41. package/dist/{utils → lib}/mini-oxygen.js +14 -12
  42. package/dist/lib/remix-version-interop.d.ts +11 -0
  43. package/dist/lib/remix-version-interop.js +54 -0
  44. package/dist/lib/remix-version-interop.test.d.ts +1 -0
  45. package/dist/lib/remix-version-interop.test.js +93 -0
  46. package/dist/lib/shell.d.ts +12 -0
  47. package/dist/lib/shell.js +73 -0
  48. package/dist/lib/template-downloader.d.ts +6 -0
  49. package/dist/{utils → lib}/template-downloader.js +21 -16
  50. package/dist/{utils → lib}/transpile-ts.js +5 -5
  51. package/dist/lib/virtual-routes.test.d.ts +1 -0
  52. package/dist/virtual-routes/routes/index.jsx +2 -15
  53. package/dist/virtual-routes/virtual-root.jsx +5 -6
  54. package/oclif.manifest.json +1 -1
  55. package/package.json +11 -10
  56. package/dist/utils/template-downloader.d.ts +0 -11
  57. /package/dist/{utils/check-lockfile.test.d.ts → commands/hydrogen/shortcut.test.d.ts} +0 -0
  58. /package/dist/{utils → lib}/check-lockfile.d.ts +0 -0
  59. /package/dist/{utils/check-version.test.d.ts → lib/check-lockfile.test.d.ts} +0 -0
  60. /package/dist/{utils → lib}/check-version.d.ts +0 -0
  61. /package/dist/{utils → lib}/check-version.js +0 -0
  62. /package/dist/{utils/flags.test.d.ts → lib/check-version.test.d.ts} +0 -0
  63. /package/dist/{utils → lib}/config.d.ts +0 -0
  64. /package/dist/{utils/virtual-routes.test.d.ts → lib/flags.test.d.ts} +0 -0
  65. /package/dist/{utils → lib}/flags.test.js +0 -0
  66. /package/dist/{utils → lib}/log.d.ts +0 -0
  67. /package/dist/{utils → lib}/log.js +0 -0
  68. /package/dist/{utils → lib}/mini-oxygen.d.ts +0 -0
  69. /package/dist/{utils → lib}/missing-routes.d.ts +0 -0
  70. /package/dist/{utils → lib}/missing-routes.js +0 -0
  71. /package/dist/{utils → lib}/transpile-ts.d.ts +0 -0
  72. /package/dist/{utils → lib}/virtual-routes.d.ts +0 -0
  73. /package/dist/{utils → lib}/virtual-routes.js +0 -0
  74. /package/dist/{utils → lib}/virtual-routes.test.js +0 -0
@@ -1,4 +1,6 @@
1
- import { file, path, git } from '@shopify/cli-kit';
1
+ import { fileExists } from '@shopify/cli-kit/node/fs';
2
+ import { resolvePath } from '@shopify/cli-kit/node/path';
3
+ import { checkIfIgnoredInGitRepository } from '@shopify/cli-kit/node/git';
2
4
  import { renderWarning } from '@shopify/cli-kit/node/ui';
3
5
  import { lockfiles } from '@shopify/cli-kit/node/node-package-manager';
4
6
 
@@ -54,7 +56,7 @@ async function checkLockfileStatus(directory) {
54
56
  return;
55
57
  const availableLockfiles = [];
56
58
  for (const lockFileName of lockfiles) {
57
- if (await file.exists(path.resolve(directory, lockFileName))) {
59
+ if (await fileExists(resolvePath(directory, lockFileName))) {
58
60
  availableLockfiles.push(lockFileName);
59
61
  }
60
62
  }
@@ -65,9 +67,10 @@ async function checkLockfileStatus(directory) {
65
67
  return multipleLockfilesWarning(availableLockfiles);
66
68
  }
67
69
  try {
68
- const repo = git.factory(directory);
69
70
  const lockfile = availableLockfiles[0];
70
- const ignoredLockfile = await repo.checkIgnore([lockfile]);
71
+ const ignoredLockfile = await checkIfIgnoredInGitRepository(directory, [
72
+ lockfile
73
+ ]);
71
74
  if (ignoredLockfile.length) {
72
75
  lockfileIgnoredWarning(lockfile);
73
76
  }
@@ -1,34 +1,28 @@
1
1
  import { checkLockfileStatus } from './check-lockfile.js';
2
- import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
3
- import { git, outputMocker, file, path } from '@shopify/cli-kit';
2
+ import { describe, vi, beforeEach, afterEach, it, expect } from 'vitest';
3
+ import { inTemporaryDirectory, writeFile } from '@shopify/cli-kit/node/fs';
4
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { checkIfIgnoredInGitRepository } from '@shopify/cli-kit/node/git';
6
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
4
7
 
5
- vi.mock("@shopify/cli-kit", async () => {
6
- const cliKit = await vi.importActual("@shopify/cli-kit");
7
- return {
8
- ...cliKit,
9
- git: {
10
- factory: vi.fn()
11
- }
12
- };
13
- });
14
8
  describe("checkLockfileStatus()", () => {
15
9
  const checkIgnoreMock = vi.fn();
16
- const gitFactoryMock = {
17
- checkIgnore: checkIgnoreMock
18
- };
10
+ const outputMock = mockAndCaptureOutput();
19
11
  beforeEach(() => {
20
- vi.mocked(git.factory).mockReturnValue(gitFactoryMock);
12
+ vi.mock("@shopify/cli-kit/node/git");
13
+ vi.mocked(checkIfIgnoredInGitRepository).mockImplementation(
14
+ checkIgnoreMock
15
+ );
21
16
  vi.mocked(checkIgnoreMock).mockResolvedValue([]);
22
17
  });
23
18
  afterEach(() => {
24
19
  vi.restoreAllMocks();
25
- outputMocker.mockAndCaptureOutput().clear();
20
+ outputMock.clear();
26
21
  });
27
22
  describe("when a lockfile is present", () => {
28
23
  it("does not call displayLockfileWarning", async () => {
29
- await file.inTemporaryDirectory(async (tmpDir) => {
30
- await file.write(path.join(tmpDir, "package-lock.json"), "");
31
- const outputMock = outputMocker.mockAndCaptureOutput();
24
+ await inTemporaryDirectory(async (tmpDir) => {
25
+ await writeFile(joinPath(tmpDir, "package-lock.json"), "");
32
26
  await checkLockfileStatus(tmpDir);
33
27
  expect(outputMock.warn()).toBe("");
34
28
  });
@@ -38,9 +32,8 @@ describe("checkLockfileStatus()", () => {
38
32
  vi.mocked(checkIgnoreMock).mockResolvedValue(["package-lock.json"]);
39
33
  });
40
34
  it("renders a warning", async () => {
41
- await file.inTemporaryDirectory(async (tmpDir) => {
42
- await file.write(path.join(tmpDir, "package-lock.json"), "");
43
- const outputMock = outputMocker.mockAndCaptureOutput();
35
+ await inTemporaryDirectory(async (tmpDir) => {
36
+ await writeFile(joinPath(tmpDir, "package-lock.json"), "");
44
37
  await checkLockfileStatus(tmpDir);
45
38
  expect(outputMock.warn()).toMatch(
46
39
  / warning .+ Lockfile ignored by Git .+/is
@@ -51,10 +44,9 @@ describe("checkLockfileStatus()", () => {
51
44
  });
52
45
  describe("when there are multiple lockfiles", () => {
53
46
  it("renders a warning", async () => {
54
- await file.inTemporaryDirectory(async (tmpDir) => {
55
- await file.write(path.join(tmpDir, "package-lock.json"), "");
56
- await file.write(path.join(tmpDir, "pnpm-lock.yaml"), "");
57
- const outputMock = outputMocker.mockAndCaptureOutput();
47
+ await inTemporaryDirectory(async (tmpDir) => {
48
+ await writeFile(joinPath(tmpDir, "package-lock.json"), "");
49
+ await writeFile(joinPath(tmpDir, "pnpm-lock.yaml"), "");
58
50
  await checkLockfileStatus(tmpDir);
59
51
  expect(outputMock.warn()).toMatch(
60
52
  / warning .+ Multiple lockfiles found .+/is
@@ -64,8 +56,7 @@ describe("checkLockfileStatus()", () => {
64
56
  });
65
57
  describe("when a lockfile is missing", () => {
66
58
  it("renders a warning", async () => {
67
- await file.inTemporaryDirectory(async (tmpDir) => {
68
- const outputMock = outputMocker.mockAndCaptureOutput();
59
+ await inTemporaryDirectory(async (tmpDir) => {
69
60
  await checkLockfileStatus(tmpDir);
70
61
  expect(outputMock.warn()).toMatch(/ warning .+ No lockfile found .+/is);
71
62
  });
@@ -1,6 +1,6 @@
1
1
  import { checkHydrogenVersion } from './check-version.js';
2
2
  import { vi, describe, afterEach, it, expect, beforeEach } from 'vitest';
3
- import { outputMocker } from '@shopify/cli-kit';
3
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
4
4
  import { checkForNewVersion } from '@shopify/cli-kit/node/node-package-manager';
5
5
 
6
6
  vi.mock("@shopify/cli-kit/node/node-package-manager", () => {
@@ -9,8 +9,10 @@ vi.mock("@shopify/cli-kit/node/node-package-manager", () => {
9
9
  };
10
10
  });
11
11
  describe("checkHydrogenVersion()", () => {
12
+ const outputMock = mockAndCaptureOutput();
12
13
  afterEach(() => {
13
14
  vi.restoreAllMocks();
15
+ outputMock.clear();
14
16
  });
15
17
  describe("when a current version is available", () => {
16
18
  it("calls checkForNewVersion", async () => {
@@ -36,7 +38,6 @@ describe("checkHydrogenVersion()", () => {
36
38
  expect(await checkHydrogenVersion("dir")).toBeInstanceOf(Function);
37
39
  });
38
40
  it("outputs a message to the user with the new version", async () => {
39
- const outputMock = outputMocker.mockAndCaptureOutput();
40
41
  const showUpgrade = await checkHydrogenVersion("dir");
41
42
  const { currentVersion, newVersion } = showUpgrade();
42
43
  expect(outputMock.info()).toMatch(
@@ -0,0 +1,8 @@
1
+ import ansiColors from 'ansi-colors';
2
+
3
+ declare const colors: {
4
+ dim: ansiColors.StyleFunction;
5
+ bold: ansiColors.StyleFunction;
6
+ };
7
+
8
+ export { colors };
@@ -0,0 +1,8 @@
1
+ import ansiColors from 'ansi-colors';
2
+
3
+ const colors = {
4
+ dim: ansiColors.dim,
5
+ bold: ansiColors.bold
6
+ };
7
+
8
+ export { colors };
@@ -1,6 +1,6 @@
1
1
  import { renderFatalError } from '@shopify/cli-kit/node/ui';
2
- import { output, file } from '@shopify/cli-kit';
3
- import { createRequire } from 'module';
2
+ import { outputWarn } from '@shopify/cli-kit/node/output';
3
+ import { fileExists } from '@shopify/cli-kit/node/fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import path from 'path';
6
6
  import fs from 'fs/promises';
@@ -8,6 +8,7 @@ import fs from 'fs/promises';
8
8
  const BUILD_DIR = "dist";
9
9
  const CLIENT_SUBDIR = "client";
10
10
  const WORKER_SUBDIR = "worker";
11
+ const oxygenServerMainFields = ["browser", "module", "main"];
11
12
  function getProjectPaths(appPath, entry) {
12
13
  const root = appPath ?? process.cwd();
13
14
  const publicPath = path.join(root, "public");
@@ -25,13 +26,6 @@ function getProjectPaths(appPath, entry) {
25
26
  async function getRemixConfig(root, mode = process.env.NODE_ENV) {
26
27
  const { readConfig } = await import('@remix-run/dev/dist/config.js');
27
28
  const config = await readConfig(root, mode);
28
- if (!config.serverConditions) {
29
- const require2 = createRequire(import.meta.url);
30
- const actualConfigFile = require2(path.join(root, "remix.config"));
31
- config.serverBuildTarget = "cloudflare-workers";
32
- config.serverConditions = actualConfigFile.serverConditions;
33
- config.serverMainFields = actualConfigFile.serverMainFields;
34
- }
35
29
  if (!config.serverEntryPoint) {
36
30
  throwConfigError(
37
31
  "Could not find a server entry point.",
@@ -61,17 +55,14 @@ async function getRemixConfig(root, mode = process.env.NODE_ENV) {
61
55
  );
62
56
  }
63
57
  if (process.env.NODE_ENV === "development" && !config.serverConditions?.includes("development")) {
64
- output.warn(
58
+ outputWarn(
65
59
  "Add `process.env.NODE_ENV` value to serverConditions in remix.config.js to enable debugging features in development."
66
60
  );
67
61
  }
68
- const expectedServerMainFields = ["browser", "module", "main"];
69
- if (!config.serverMainFields || !expectedServerMainFields.every(
70
- (v, i) => config.serverMainFields?.[i] === v
71
- )) {
62
+ if (!config.serverMainFields || !oxygenServerMainFields.every((v, i) => config.serverMainFields?.[i] === v)) {
72
63
  throwConfigError(
73
64
  `The serverMainFields in remix.config.js must be ${JSON.stringify(
74
- expectedServerMainFields
65
+ oxygenServerMainFields
75
66
  )}.`
76
67
  );
77
68
  }
@@ -124,13 +115,13 @@ function throwConfigError(message, tryMessage = null) {
124
115
  }
125
116
  async function assertEntryFileExists(root, fileRelative) {
126
117
  const fileAbsolute = path.resolve(root, fileRelative);
127
- const exists = await file.exists(fileAbsolute);
118
+ const exists = await fileExists(fileAbsolute);
128
119
  if (!exists) {
129
120
  if (!path.extname(fileAbsolute)) {
130
121
  const { readdir } = await import('fs/promises');
131
122
  const files = await readdir(path.dirname(fileAbsolute));
132
- const exists2 = files.some((file2) => {
133
- const { name, ext } = path.parse(file2);
123
+ const exists2 = files.some((file) => {
124
+ const { name, ext } = path.parse(file);
134
125
  return name === path.basename(fileAbsolute) && /^\.[jt]s$/.test(ext);
135
126
  });
136
127
  if (exists2)
@@ -1,8 +1,8 @@
1
1
  import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
2
 
3
3
  declare const commonFlags: {
4
- path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
5
- port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number>;
4
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
5
+ port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
6
6
  force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
7
7
  };
8
8
  declare function flagsToCamelObject(obj: Record<string, any>): any;
@@ -19,6 +19,6 @@ declare function parseProcessFlags(processArgv: string[], flagMap?: Record<strin
19
19
  * Displays an info message when the flag is used.
20
20
  * @param name The name of the flag.
21
21
  */
22
- declare function deprecated(name: string): _oclif_core_lib_interfaces_parser_js.Definition<unknown, Record<string, unknown>>;
22
+ declare function deprecated(name: string): _oclif_core_lib_interfaces_parser_js.FlagDefinition<unknown, Record<string, unknown>>;
23
23
 
24
24
  export { commonFlags, deprecated, flagsToCamelObject, parseProcessFlags };
@@ -1,7 +1,7 @@
1
- import Flags from '@oclif/core/lib/flags.js';
2
- import { string } from '@shopify/cli-kit';
1
+ import { Flags } from '@oclif/core';
2
+ import { camelize } from '@shopify/cli-kit/common/string';
3
3
  import { renderInfo } from '@shopify/cli-kit/node/ui';
4
- import colors from '@shopify/cli-kit/node/colors';
4
+ import { colors } from './colors.js';
5
5
 
6
6
  const commonFlags = {
7
7
  path: Flags.string({
@@ -21,7 +21,7 @@ const commonFlags = {
21
21
  };
22
22
  function flagsToCamelObject(obj) {
23
23
  return Object.entries(obj).reduce((acc, [key, value]) => {
24
- acc[string.camelize(key)] = value;
24
+ acc[camelize(key)] = value;
25
25
  return acc;
26
26
  }, {});
27
27
  }
@@ -1,5 +1,7 @@
1
- import { path, file, output } from '@shopify/cli-kit';
2
- import colors from '@shopify/cli-kit/node/colors';
1
+ import { outputInfo, outputContent, outputToken } from '@shopify/cli-kit/node/output';
2
+ import { resolvePath } from '@shopify/cli-kit/node/path';
3
+ import { fileExists } from '@shopify/cli-kit/node/fs';
4
+ import { colors } from './colors.js';
3
5
 
4
6
  async function startMiniOxygen({
5
7
  root,
@@ -10,7 +12,7 @@ async function startMiniOxygen({
10
12
  }) {
11
13
  const { default: miniOxygen } = await import('@shopify/mini-oxygen');
12
14
  const miniOxygenPreview = miniOxygen.default ?? miniOxygen;
13
- const dotenvPath = path.resolve(root, ".env");
15
+ const dotenvPath = resolvePath(root, ".env");
14
16
  const { port: actualPort } = await miniOxygenPreview({
15
17
  workerFile: buildPathWorkerFile,
16
18
  assetsDir: buildPathClient,
@@ -20,18 +22,18 @@ async function startMiniOxygen({
20
22
  autoReload: watch,
21
23
  modules: true,
22
24
  env: process.env,
23
- envPath: await file.exists(dotenvPath) ? dotenvPath : void 0,
25
+ envPath: await fileExists(dotenvPath) ? dotenvPath : void 0,
24
26
  log: () => {
25
27
  },
26
- buildWatchPaths: watch ? [path.resolve(root, buildPathWorkerFile)] : void 0,
28
+ buildWatchPaths: watch ? [resolvePath(root, buildPathWorkerFile)] : void 0,
27
29
  onResponse: (request, response) => logResponse(
28
30
  request,
29
31
  response
30
32
  )
31
33
  });
32
34
  const listeningAt = `http://localhost:${actualPort}`;
33
- output.info(
34
- output.content`🚥 MiniOxygen server started at ${output.token.link(
35
+ outputInfo(
36
+ outputContent`🚥 MiniOxygen server started at ${outputToken.link(
35
37
  listeningAt,
36
38
  listeningAt
37
39
  )}\n`
@@ -57,15 +59,15 @@ function logResponse(request, response) {
57
59
  route = url.pathname;
58
60
  info = `[${dataParam}]`;
59
61
  }
60
- const colorizeStatus = response.status < 300 ? output.token.green : response.status < 400 ? output.token.cyan : output.token.errorText;
61
- output.info(
62
- output.content`${request.method.padStart(6)} ${colorizeStatus(
62
+ const colorizeStatus = response.status < 300 ? outputToken.green : response.status < 400 ? outputToken.cyan : outputToken.errorText;
63
+ outputInfo(
64
+ outputContent`${request.method.padStart(6)} ${colorizeStatus(
63
65
  String(response.status)
64
- )} ${output.token.italic(type.padEnd(7, " "))} ${route}${info ? " " + colors.dim(info) : ""} ${request.headers.get("purpose") === "prefetch" ? output.token.italic("(prefetch)") : ""}`
66
+ )} ${outputToken.italic(type.padEnd(7, " "))} ${route}${info ? " " + colors.dim(info) : ""} ${request.headers.get("purpose") === "prefetch" ? outputToken.italic("(prefetch)") : ""}`
65
67
  );
66
68
  } catch {
67
69
  if (request && response) {
68
- output.info(`${request.method} ${response.status} ${request.url}`);
70
+ outputInfo(`${request.method} ${response.status} ${request.url}`);
69
71
  }
70
72
  }
71
73
  }
@@ -0,0 +1,11 @@
1
+ declare function isRemixV2(): boolean;
2
+ declare function getV2Flags(root: string): Promise<{
3
+ isV2Meta: boolean;
4
+ isV2ErrorBoundary: boolean;
5
+ isV2RouteConvention: boolean;
6
+ }>;
7
+ type RemixV2Flags = Partial<Awaited<ReturnType<typeof getV2Flags>>>;
8
+ declare function convertRouteToV2(route: string): string;
9
+ declare function convertTemplateToRemixVersion(template: string, { isV2Meta, isV2ErrorBoundary }: RemixV2Flags): string;
10
+
11
+ export { RemixV2Flags, convertRouteToV2, convertTemplateToRemixVersion, getV2Flags, isRemixV2 };
@@ -0,0 +1,54 @@
1
+ import { createRequire } from 'module';
2
+ import { getRemixConfig } from './config.js';
3
+
4
+ function isRemixV2() {
5
+ try {
6
+ const require2 = createRequire(import.meta.url);
7
+ const version = require2("@remix-run/server-runtime/package.json")?.version ?? "";
8
+ return version.startsWith("2.");
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ async function getV2Flags(root) {
14
+ const isV2 = isRemixV2();
15
+ const futureFlags = {
16
+ ...!isV2 && (await getRemixConfig(root)).future
17
+ };
18
+ return {
19
+ isV2Meta: isV2 || !!futureFlags.v2_meta,
20
+ isV2ErrorBoundary: isV2 || !!futureFlags.v2_errorBoundary,
21
+ isV2RouteConvention: isV2 ? !isV1RouteConventionInstalled() : !!futureFlags.v2_routeConvention
22
+ };
23
+ }
24
+ function convertRouteToV2(route) {
25
+ return route.replace(/\/index$/, "/_index").replace(/(?<!^)\//g, ".");
26
+ }
27
+ function convertTemplateToRemixVersion(template, { isV2Meta, isV2ErrorBoundary }) {
28
+ template = isV2Meta ? convertToMetaV2(template) : convertToMetaV1(template);
29
+ template = isV2ErrorBoundary ? convertToErrorBoundaryV2(template) : convertToErrorBoundaryV1(template);
30
+ return template;
31
+ }
32
+ function convertToMetaV2(template) {
33
+ return template.replace(/type MetaFunction\s*,?/, "").replace(/export (const|function) metaV1.+?\n};?\n/s, "").replace(/import \{\s*\} from '@shopify\/remix-oxygen';/, "");
34
+ }
35
+ function convertToMetaV1(template) {
36
+ return template.replace(/type V2_MetaFunction\s*,?/, "").replace(/export (const|function) meta[^V].+?\n};?\n/s, "").replace(/(const|function) metaV1/, "$1 meta").replace(/import \{\s*\} from '@remix-run\/react';/, "");
37
+ }
38
+ function convertToErrorBoundaryV2(template) {
39
+ return template.replace(/type ErrorBoundaryComponent\s*,?/s, "").replace(/useCatch\s*,?/s, "").replace(/export function CatchBoundary.+?\n}/s, "").replace(/export (const|function) ErrorBoundaryV1.+?\n};?/s, "").replace(/import \{\s*\} from '@shopify\/remix-oxygen';/, "").replace(/import \{\s*\} from '@remix-run\/react';/, "");
40
+ }
41
+ function convertToErrorBoundaryV1(template) {
42
+ return template.replace(/useRouteError\s*,?/s, "").replace(/isRouteErrorResponse\s*,?/s, "").replace(/export function ErrorBoundary[^V].+?\n}/s, "").replace(/(const|function) ErrorBoundaryV1/, "$1 ErrorBoundary").replace(/import \{\s*\} from '@remix-run\/react';/, "");
43
+ }
44
+ function isV1RouteConventionInstalled() {
45
+ try {
46
+ const require2 = createRequire(import.meta.url);
47
+ require2.resolve("@remix-run/v1-route-convention/package.json");
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ export { convertRouteToV2, convertTemplateToRemixVersion, getV2Flags, isRemixV2 };
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { convertTemplateToRemixVersion } from './remix-version-interop.js';
3
+
4
+ describe("remix-version-interop", () => {
5
+ describe("v2_meta", () => {
6
+ const META_TEMPLATE = `
7
+ import {type MetaFunction} from '@shopify/remix-oxygen';
8
+ import {type V2_MetaFunction} from '@remix-run/react';
9
+ export const metaV1: MetaFunction = ({data}) => {
10
+ const title = 'title';
11
+ return {title};
12
+ };
13
+ export const meta: V2_MetaFunction = ({data}) => {
14
+ const title = 'title';
15
+ return [{title}];
16
+ };
17
+ `.replace(/^\s{4}/gm, "");
18
+ it("transforms meta exports to v2", async () => {
19
+ const result = convertTemplateToRemixVersion(META_TEMPLATE, {
20
+ isV2Meta: true
21
+ });
22
+ expect(result).toContain("type V2_MetaFunction");
23
+ expect(result).not.toContain("type MetaFunction");
24
+ expect(result).not.toContain("@shopify/remix-oxygen");
25
+ expect(result).toMatch(/return \[\{title\}\];/);
26
+ expect(result).not.toMatch(/return \{title\};/);
27
+ });
28
+ it("transforms meta exports to v1", async () => {
29
+ const result = convertTemplateToRemixVersion(META_TEMPLATE, {
30
+ isV2Meta: false
31
+ });
32
+ expect(result).toContain("type MetaFunction");
33
+ expect(result).not.toContain("type V2_MetaFunction");
34
+ expect(result).not.toContain("@remix-run/react");
35
+ expect(result).toMatch(/return \{title\};/);
36
+ expect(result).not.toMatch(/return \[\{title\}\];/);
37
+ });
38
+ });
39
+ describe("v2_errorBoundary", () => {
40
+ const ERROR_BOUNDARY_TEMPLATE = `
41
+ import {useCatch, isRouteErrorResponse, useRouteError} from "@remix-run/react";
42
+ import {type ErrorBoundaryComponent} from '@shopify/remix-oxygen';
43
+
44
+ export function CatchBoundary() {
45
+ const caught = useCatch();
46
+ console.error(caught);
47
+
48
+ return <div>stuff</div>;
49
+ }
50
+
51
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
52
+ console.error(error);
53
+
54
+ return <div>There was an error.</div>;
55
+ };
56
+
57
+ export function ErrorBoundary() {
58
+ const error = useRouteError();
59
+
60
+ if (isRouteErrorResponse(error)) {
61
+ return <div>RouteError</div>;
62
+ } else {
63
+ return <h1>Unknown Error</h1>;
64
+ }
65
+ }
66
+ `.replace(/^\s{4}/gm, "");
67
+ it("transforms ErrorBoundary exports to v2", async () => {
68
+ const result = convertTemplateToRemixVersion(ERROR_BOUNDARY_TEMPLATE, {
69
+ isV2ErrorBoundary: true
70
+ });
71
+ expect(result).toContain("export function ErrorBoundary");
72
+ expect(result).not.toContain("export const ErrorBoundary");
73
+ expect(result).not.toMatch("export function CatchBoundary");
74
+ expect(result).not.toContain("type ErrorBoundaryComponent");
75
+ expect(result).not.toContain("@shopify/remix-oxygen");
76
+ expect(result).toContain("useRouteError");
77
+ expect(result).toContain("isRouteErrorResponse");
78
+ expect(result).not.toContain("useCatch");
79
+ });
80
+ it("transforms ErrorBoundary exports to v1", async () => {
81
+ const result = convertTemplateToRemixVersion(ERROR_BOUNDARY_TEMPLATE, {
82
+ isV2ErrorBoundary: false
83
+ });
84
+ expect(result).toContain("export const ErrorBoundary");
85
+ expect(result).not.toContain("export function ErrorBoundary");
86
+ expect(result).toMatch("export function CatchBoundary");
87
+ expect(result).toContain("type ErrorBoundaryComponent");
88
+ expect(result).toContain("useCatch");
89
+ expect(result).not.toContain("useRouteError");
90
+ expect(result).not.toContain("isRouteErrorResponse");
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,12 @@
1
+ type UnixShell = 'zsh' | 'bash' | 'fish';
2
+ type WindowsShell = 'PowerShell' | 'PowerShell 7+' | 'CMD';
3
+ type Shell = UnixShell | WindowsShell;
4
+ declare const isWindows: () => boolean;
5
+ declare const isGitBash: () => boolean;
6
+ declare function homeFileExists(filepath: string): false | Promise<boolean>;
7
+ declare function supportsShell(shell: UnixShell): boolean;
8
+ declare function hasAlias(aliasName: string, filepath: string): boolean;
9
+ declare function shellWriteFile(filepath: string, content: string, append?: boolean): boolean;
10
+ declare function shellRunScript(script: string, shellBin: string): boolean;
11
+
12
+ export { Shell, UnixShell, WindowsShell, hasAlias, homeFileExists, isGitBash, isWindows, shellRunScript, shellWriteFile, supportsShell };
@@ -0,0 +1,73 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { fileExists } from '@shopify/cli-kit/node/fs';
5
+ import { outputDebug } from '@shopify/cli-kit/node/output';
6
+
7
+ const isWindows = () => process.platform === "win32";
8
+ const isGitBash = () => !!process.env.MINGW_PREFIX;
9
+ function resolveFromHome(filepath) {
10
+ if (filepath[0] === "~") {
11
+ return path.join(os.homedir(), filepath.slice(1));
12
+ }
13
+ return filepath;
14
+ }
15
+ function homeFileExists(filepath) {
16
+ try {
17
+ return fileExists(resolveFromHome(filepath));
18
+ } catch (error) {
19
+ return false;
20
+ }
21
+ }
22
+ function supportsShell(shell) {
23
+ try {
24
+ execSync(`which ${shell}`, { stdio: "ignore" });
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ function hasAlias(aliasName, filepath) {
31
+ try {
32
+ const result = execSync(
33
+ `grep 'alias ${aliasName}' ${resolveFromHome(filepath)}`,
34
+ { stdio: "pipe" }
35
+ ).toString();
36
+ return !!result;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ function shellWriteFile(filepath, content, append = false) {
42
+ content = `"${content}"`;
43
+ content = content.replaceAll("\n", "\\n");
44
+ if (!isWindows()) {
45
+ content = content.replaceAll("$", "\\$");
46
+ }
47
+ try {
48
+ execSync(
49
+ `printf ${content} ${append ? ">>" : ">"} ${resolveFromHome(filepath)}`
50
+ );
51
+ return true;
52
+ } catch (error) {
53
+ outputDebug(
54
+ `Could not create or modify ${filepath}:
55
+ ` + error.stack
56
+ );
57
+ return false;
58
+ }
59
+ }
60
+ function shellRunScript(script, shellBin) {
61
+ try {
62
+ execSync(script, { shell: shellBin, stdio: "ignore" });
63
+ return true;
64
+ } catch (error) {
65
+ outputDebug(
66
+ `Could not run shell script for ${shellBin}:
67
+ ` + error.stack
68
+ );
69
+ return false;
70
+ }
71
+ }
72
+
73
+ export { hasAlias, homeFileExists, isGitBash, isWindows, shellRunScript, shellWriteFile, supportsShell };
@@ -0,0 +1,6 @@
1
+ declare function getLatestTemplates(): Promise<{
2
+ version: string;
3
+ templatesDir: string;
4
+ }>;
5
+
6
+ export { getLatestTemplates };