@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.
- package/dist/commands/hydrogen/build.d.ts +4 -4
- package/dist/commands/hydrogen/build.js +17 -16
- package/dist/commands/hydrogen/check.d.ts +3 -6
- package/dist/commands/hydrogen/check.js +10 -9
- package/dist/commands/hydrogen/dev.d.ts +3 -3
- package/dist/commands/hydrogen/dev.js +22 -21
- package/dist/commands/hydrogen/g.d.ts +10 -0
- package/dist/commands/hydrogen/g.js +17 -0
- package/dist/commands/hydrogen/generate/route.d.ts +7 -9
- package/dist/commands/hydrogen/generate/route.js +49 -47
- package/dist/commands/hydrogen/generate/route.test.js +48 -40
- package/dist/commands/hydrogen/generate/routes.d.ts +2 -2
- package/dist/commands/hydrogen/init.d.ts +3 -3
- package/dist/commands/hydrogen/init.js +74 -93
- package/dist/commands/hydrogen/init.test.js +71 -24
- package/dist/commands/hydrogen/preview.d.ts +2 -2
- package/dist/commands/hydrogen/preview.js +4 -4
- package/dist/commands/hydrogen/shortcut.d.ts +9 -0
- package/dist/commands/hydrogen/shortcut.js +74 -0
- package/dist/commands/hydrogen/shortcut.test.js +58 -0
- package/dist/generator-templates/routes/[robots.txt].tsx +35 -1
- package/dist/generator-templates/routes/[sitemap.xml].tsx +33 -2
- package/dist/generator-templates/routes/account/login.tsx +42 -13
- package/dist/generator-templates/routes/account/register.tsx +42 -13
- package/dist/generator-templates/routes/cart.tsx +42 -2
- package/dist/generator-templates/routes/collections/$collectionHandle.tsx +44 -5
- package/dist/generator-templates/routes/index.tsx +33 -0
- package/dist/generator-templates/routes/pages/$pageHandle.tsx +48 -10
- package/dist/generator-templates/routes/policies/$policyHandle.tsx +67 -14
- package/dist/generator-templates/routes/policies/index.tsx +54 -4
- package/dist/generator-templates/routes/products/$productHandle.tsx +44 -9
- package/dist/hooks/init.js +2 -2
- package/dist/{utils → lib}/check-lockfile.js +7 -4
- package/dist/{utils → lib}/check-lockfile.test.js +19 -28
- package/dist/{utils → lib}/check-version.test.js +3 -2
- package/dist/lib/colors.d.ts +8 -0
- package/dist/lib/colors.js +8 -0
- package/dist/{utils → lib}/config.js +9 -18
- package/dist/{utils → lib}/flags.d.ts +3 -3
- package/dist/{utils → lib}/flags.js +4 -4
- package/dist/{utils → lib}/mini-oxygen.js +14 -12
- package/dist/lib/remix-version-interop.d.ts +11 -0
- package/dist/lib/remix-version-interop.js +54 -0
- package/dist/lib/remix-version-interop.test.d.ts +1 -0
- package/dist/lib/remix-version-interop.test.js +93 -0
- package/dist/lib/shell.d.ts +12 -0
- package/dist/lib/shell.js +73 -0
- package/dist/lib/template-downloader.d.ts +6 -0
- package/dist/{utils → lib}/template-downloader.js +21 -16
- package/dist/{utils → lib}/transpile-ts.js +5 -5
- package/dist/lib/virtual-routes.test.d.ts +1 -0
- package/dist/virtual-routes/routes/index.jsx +2 -15
- package/dist/virtual-routes/virtual-root.jsx +5 -6
- package/oclif.manifest.json +1 -1
- package/package.json +11 -10
- package/dist/utils/template-downloader.d.ts +0 -11
- /package/dist/{utils/check-lockfile.test.d.ts → commands/hydrogen/shortcut.test.d.ts} +0 -0
- /package/dist/{utils → lib}/check-lockfile.d.ts +0 -0
- /package/dist/{utils/check-version.test.d.ts → lib/check-lockfile.test.d.ts} +0 -0
- /package/dist/{utils → lib}/check-version.d.ts +0 -0
- /package/dist/{utils → lib}/check-version.js +0 -0
- /package/dist/{utils/flags.test.d.ts → lib/check-version.test.d.ts} +0 -0
- /package/dist/{utils → lib}/config.d.ts +0 -0
- /package/dist/{utils/virtual-routes.test.d.ts → lib/flags.test.d.ts} +0 -0
- /package/dist/{utils → lib}/flags.test.js +0 -0
- /package/dist/{utils → lib}/log.d.ts +0 -0
- /package/dist/{utils → lib}/log.js +0 -0
- /package/dist/{utils → lib}/mini-oxygen.d.ts +0 -0
- /package/dist/{utils → lib}/missing-routes.d.ts +0 -0
- /package/dist/{utils → lib}/missing-routes.js +0 -0
- /package/dist/{utils → lib}/transpile-ts.d.ts +0 -0
- /package/dist/{utils → lib}/virtual-routes.d.ts +0 -0
- /package/dist/{utils → lib}/virtual-routes.js +0 -0
- /package/dist/{utils → lib}/virtual-routes.test.js +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
|
17
|
-
checkIgnore: checkIgnoreMock
|
|
18
|
-
};
|
|
10
|
+
const outputMock = mockAndCaptureOutput();
|
|
19
11
|
beforeEach(() => {
|
|
20
|
-
vi.
|
|
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
|
-
|
|
20
|
+
outputMock.clear();
|
|
26
21
|
});
|
|
27
22
|
describe("when a lockfile is present", () => {
|
|
28
23
|
it("does not call displayLockfileWarning", async () => {
|
|
29
|
-
await
|
|
30
|
-
await
|
|
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
|
|
42
|
-
await
|
|
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
|
|
55
|
-
await
|
|
56
|
-
await
|
|
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
|
|
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 {
|
|
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(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderFatalError } from '@shopify/cli-kit/node/ui';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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((
|
|
133
|
-
const { name, ext } = path.parse(
|
|
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.
|
|
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
|
|
2
|
-
import {
|
|
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 '
|
|
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[
|
|
24
|
+
acc[camelize(key)] = value;
|
|
25
25
|
return acc;
|
|
26
26
|
}, {});
|
|
27
27
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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 =
|
|
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
|
|
25
|
+
envPath: await fileExists(dotenvPath) ? dotenvPath : void 0,
|
|
24
26
|
log: () => {
|
|
25
27
|
},
|
|
26
|
-
buildWatchPaths: watch ? [
|
|
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
|
-
|
|
34
|
-
|
|
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 ?
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
)} ${
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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 };
|