@shopify/cli-hydrogen 0.0.0-next-744c490-20230315110711
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/LICENSE.md +7 -0
- package/README.md +14 -0
- package/dist/commands/hydrogen/build.d.ts +23 -0
- package/dist/commands/hydrogen/build.js +100 -0
- package/dist/commands/hydrogen/check.d.ts +18 -0
- package/dist/commands/hydrogen/check.js +35 -0
- package/dist/commands/hydrogen/dev.d.ts +15 -0
- package/dist/commands/hydrogen/dev.js +134 -0
- package/dist/commands/hydrogen/generate/route.d.ts +34 -0
- package/dist/commands/hydrogen/generate/route.js +166 -0
- package/dist/commands/hydrogen/generate/route.test.d.ts +1 -0
- package/dist/commands/hydrogen/generate/route.test.js +126 -0
- package/dist/commands/hydrogen/generate/routes.d.ts +16 -0
- package/dist/commands/hydrogen/generate/routes.js +22 -0
- package/dist/commands/hydrogen/init.d.ts +24 -0
- package/dist/commands/hydrogen/init.js +199 -0
- package/dist/commands/hydrogen/init.test.d.ts +1 -0
- package/dist/commands/hydrogen/init.test.js +79 -0
- package/dist/commands/hydrogen/preview.d.ts +17 -0
- package/dist/commands/hydrogen/preview.js +34 -0
- package/dist/create-app.d.ts +1 -0
- package/dist/create-app.js +4 -0
- package/dist/generator-templates/routes/[robots.txt].tsx +41 -0
- package/dist/generator-templates/routes/[sitemap.xml].tsx +204 -0
- package/dist/generator-templates/routes/account/login.tsx +74 -0
- package/dist/generator-templates/routes/account/register.tsx +74 -0
- package/dist/generator-templates/routes/cart.tsx +41 -0
- package/dist/generator-templates/routes/collections/$collectionHandle.tsx +65 -0
- package/dist/generator-templates/routes/graphiql.tsx +10 -0
- package/dist/generator-templates/routes/index.tsx +7 -0
- package/dist/generator-templates/routes/pages/$pageHandle.tsx +74 -0
- package/dist/generator-templates/routes/policies/$policyHandle.tsx +87 -0
- package/dist/generator-templates/routes/policies/index.tsx +67 -0
- package/dist/generator-templates/routes/products/$productHandle.tsx +57 -0
- package/dist/hooks/init.d.ts +5 -0
- package/dist/hooks/init.js +18 -0
- package/dist/utils/check-lockfile.d.ts +3 -0
- package/dist/utils/check-lockfile.js +78 -0
- package/dist/utils/check-lockfile.test.d.ts +1 -0
- package/dist/utils/check-lockfile.test.js +74 -0
- package/dist/utils/check-version.d.ts +16 -0
- package/dist/utils/check-version.js +55 -0
- package/dist/utils/check-version.test.d.ts +1 -0
- package/dist/utils/check-version.test.js +60 -0
- package/dist/utils/config.d.ts +18 -0
- package/dist/utils/config.js +146 -0
- package/dist/utils/flags.d.ts +24 -0
- package/dist/utils/flags.js +60 -0
- package/dist/utils/flags.test.d.ts +1 -0
- package/dist/utils/flags.test.js +43 -0
- package/dist/utils/log.d.ts +5 -0
- package/dist/utils/log.js +20 -0
- package/dist/utils/mini-oxygen.d.ts +11 -0
- package/dist/utils/mini-oxygen.js +73 -0
- package/dist/utils/missing-routes.d.ts +6 -0
- package/dist/utils/missing-routes.js +71 -0
- package/dist/utils/template-downloader.d.ts +11 -0
- package/dist/utils/template-downloader.js +70 -0
- package/dist/utils/transpile-ts.d.ts +16 -0
- package/dist/utils/transpile-ts.js +169 -0
- package/dist/utils/virtual-routes.d.ts +7 -0
- package/dist/utils/virtual-routes.js +44 -0
- package/dist/utils/virtual-routes.test.d.ts +1 -0
- package/dist/utils/virtual-routes.test.js +49 -0
- package/dist/virtual-routes/assets/favicon.svg +28 -0
- package/dist/virtual-routes/assets/inter-variable-font.woff2 +0 -0
- package/dist/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
- package/dist/virtual-routes/assets/styles.css +238 -0
- package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +7 -0
- package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +13 -0
- package/dist/virtual-routes/components/IconBanner.jsx +47 -0
- package/dist/virtual-routes/components/IconDiscord.jsx +4 -0
- package/dist/virtual-routes/components/IconError.jsx +20 -0
- package/dist/virtual-routes/components/IconGithub.jsx +4 -0
- package/dist/virtual-routes/components/IconTwitter.jsx +4 -0
- package/dist/virtual-routes/components/Layout.jsx +6 -0
- package/dist/virtual-routes/routes/graphiql.jsx +5 -0
- package/dist/virtual-routes/routes/index.jsx +163 -0
- package/dist/virtual-routes/virtual-root.jsx +38 -0
- package/oclif.manifest.json +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,74 @@
|
|
|
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';
|
|
4
|
+
|
|
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
|
+
describe("checkLockfileStatus()", () => {
|
|
15
|
+
const checkIgnoreMock = vi.fn();
|
|
16
|
+
const gitFactoryMock = {
|
|
17
|
+
checkIgnore: checkIgnoreMock
|
|
18
|
+
};
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.mocked(git.factory).mockReturnValue(gitFactoryMock);
|
|
21
|
+
vi.mocked(checkIgnoreMock).mockResolvedValue([]);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
outputMocker.mockAndCaptureOutput().clear();
|
|
26
|
+
});
|
|
27
|
+
describe("when a lockfile is present", () => {
|
|
28
|
+
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();
|
|
32
|
+
await checkLockfileStatus(tmpDir);
|
|
33
|
+
expect(outputMock.warn()).toBe("");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("and it is being ignored by Git", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.mocked(checkIgnoreMock).mockResolvedValue(["package-lock.json"]);
|
|
39
|
+
});
|
|
40
|
+
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();
|
|
44
|
+
await checkLockfileStatus(tmpDir);
|
|
45
|
+
expect(outputMock.warn()).toMatch(
|
|
46
|
+
/ warning .+ Lockfile ignored by Git .+/is
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("when there are multiple lockfiles", () => {
|
|
53
|
+
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();
|
|
58
|
+
await checkLockfileStatus(tmpDir);
|
|
59
|
+
expect(outputMock.warn()).toMatch(
|
|
60
|
+
/ warning .+ Multiple lockfiles found .+/is
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("when a lockfile is missing", () => {
|
|
66
|
+
it("renders a warning", async () => {
|
|
67
|
+
await file.inTemporaryDirectory(async (tmpDir) => {
|
|
68
|
+
const outputMock = outputMocker.mockAndCaptureOutput();
|
|
69
|
+
await checkLockfileStatus(tmpDir);
|
|
70
|
+
expect(outputMock.warn()).toMatch(/ warning .+ No lockfile found .+/is);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const PACKAGE_NAMES: {
|
|
2
|
+
readonly main: "@shopify/hydrogen";
|
|
3
|
+
readonly cli: "@shopify/cli-hydrogen";
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param resolveFrom Path to a directory to resolve from, or directly the path to a package.json file.
|
|
8
|
+
* @param pkgKey Package to check for updates.
|
|
9
|
+
* @returns A function to show the update information if any update is available.
|
|
10
|
+
*/
|
|
11
|
+
declare function checkHydrogenVersion(resolveFrom: string, pkgKey?: keyof typeof PACKAGE_NAMES): Promise<((extraMessage?: string) => {
|
|
12
|
+
currentVersion: string;
|
|
13
|
+
newVersion: string;
|
|
14
|
+
}) | undefined>;
|
|
15
|
+
|
|
16
|
+
export { checkHydrogenVersion };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { checkForNewVersion } from '@shopify/cli-kit/node/node-package-manager';
|
|
4
|
+
import { renderInfo } from '@shopify/cli-kit/node/ui';
|
|
5
|
+
|
|
6
|
+
const PACKAGE_NAMES = {
|
|
7
|
+
main: "@shopify/hydrogen",
|
|
8
|
+
cli: "@shopify/cli-hydrogen"
|
|
9
|
+
};
|
|
10
|
+
async function checkHydrogenVersion(resolveFrom, pkgKey = "main") {
|
|
11
|
+
if (process.env.LOCAL_DEV)
|
|
12
|
+
return;
|
|
13
|
+
const pkgName = PACKAGE_NAMES[pkgKey];
|
|
14
|
+
const require2 = createRequire(import.meta.url);
|
|
15
|
+
const pkgJsonPath = resolveFrom.endsWith("package.json") ? locateDependency(require2, resolveFrom) : locateDependency(
|
|
16
|
+
require2,
|
|
17
|
+
path.join(pkgName, "package.json"),
|
|
18
|
+
resolveFrom
|
|
19
|
+
);
|
|
20
|
+
if (!pkgJsonPath)
|
|
21
|
+
return;
|
|
22
|
+
const currentVersion = require2(pkgJsonPath).version;
|
|
23
|
+
if (!currentVersion)
|
|
24
|
+
return;
|
|
25
|
+
const newVersionAvailable = await checkForNewVersion(pkgName, currentVersion);
|
|
26
|
+
if (!newVersionAvailable)
|
|
27
|
+
return;
|
|
28
|
+
return (extraMessage = "") => {
|
|
29
|
+
renderInfo({
|
|
30
|
+
headline: "Upgrade available",
|
|
31
|
+
body: `Version ${newVersionAvailable} of ${pkgName} is now available.
|
|
32
|
+
You are currently running v${currentVersion}.` + (extraMessage ? "\n\n" : "") + extraMessage,
|
|
33
|
+
reference: [
|
|
34
|
+
{
|
|
35
|
+
link: {
|
|
36
|
+
label: "Hydrogen releases",
|
|
37
|
+
url: "https://github.com/Shopify/hydrogen/releases"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
return { currentVersion, newVersion: newVersionAvailable };
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function locateDependency(require2, nameToResolve, resolveFrom) {
|
|
46
|
+
try {
|
|
47
|
+
return require2.resolve(nameToResolve, {
|
|
48
|
+
paths: [resolveFrom ?? process.cwd()]
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { checkHydrogenVersion };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { checkHydrogenVersion } from './check-version.js';
|
|
2
|
+
import { vi, describe, afterEach, it, expect, beforeEach } from 'vitest';
|
|
3
|
+
import { outputMocker } from '@shopify/cli-kit';
|
|
4
|
+
import { checkForNewVersion } from '@shopify/cli-kit/node/node-package-manager';
|
|
5
|
+
|
|
6
|
+
vi.mock("@shopify/cli-kit/node/node-package-manager", () => {
|
|
7
|
+
return {
|
|
8
|
+
checkForNewVersion: vi.fn()
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
describe("checkHydrogenVersion()", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
describe("when a current version is available", () => {
|
|
16
|
+
it("calls checkForNewVersion", async () => {
|
|
17
|
+
await checkHydrogenVersion("dir");
|
|
18
|
+
expect(checkForNewVersion).toHaveBeenCalledWith(
|
|
19
|
+
"@shopify/hydrogen",
|
|
20
|
+
expect.stringMatching(/20\d{2}\.\d{1,2}\.\d{1,3}/)
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
describe("and it is up to date", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.mocked(checkForNewVersion).mockResolvedValue(void 0);
|
|
26
|
+
});
|
|
27
|
+
it("returns undefined", async () => {
|
|
28
|
+
expect(await checkHydrogenVersion("dir")).toBe(void 0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe("and a new version is available", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.mocked(checkForNewVersion).mockResolvedValue("2023.1.5");
|
|
34
|
+
});
|
|
35
|
+
it("returns a function", async () => {
|
|
36
|
+
expect(await checkHydrogenVersion("dir")).toBeInstanceOf(Function);
|
|
37
|
+
});
|
|
38
|
+
it("outputs a message to the user with the new version", async () => {
|
|
39
|
+
const outputMock = outputMocker.mockAndCaptureOutput();
|
|
40
|
+
const showUpgrade = await checkHydrogenVersion("dir");
|
|
41
|
+
const { currentVersion, newVersion } = showUpgrade();
|
|
42
|
+
expect(outputMock.info()).toMatch(
|
|
43
|
+
new RegExp(
|
|
44
|
+
` info .+ Upgrade available .+ Version ${newVersion.replaceAll(
|
|
45
|
+
".",
|
|
46
|
+
"\\."
|
|
47
|
+
)}.+ running v${currentVersion.replaceAll(".", "\\.")}`,
|
|
48
|
+
"is"
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("when no current version can be found", () => {
|
|
55
|
+
it("returns undefined and does not call checkForNewVersion", async () => {
|
|
56
|
+
expect(await checkHydrogenVersion("/fake-absolute-dir")).toBe(void 0);
|
|
57
|
+
expect(checkForNewVersion).not.toHaveBeenCalled();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ServerMode } from '@remix-run/dev/dist/config/serverModes.js';
|
|
2
|
+
import { RemixConfig } from '@remix-run/dev/dist/config.js';
|
|
3
|
+
|
|
4
|
+
declare function getProjectPaths(appPath?: string, entry?: string): {
|
|
5
|
+
root: string;
|
|
6
|
+
buildPath: string;
|
|
7
|
+
buildPathClient: string;
|
|
8
|
+
buildPathWorkerFile: string;
|
|
9
|
+
publicPath: string;
|
|
10
|
+
};
|
|
11
|
+
declare function getRemixConfig(root: string, mode?: ServerMode): Promise<RemixConfig & {
|
|
12
|
+
serverConditions?: string[] | undefined;
|
|
13
|
+
serverMainFields?: string[] | undefined;
|
|
14
|
+
serverDependenciesToBundle?: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
declare function assertEntryFileExists(root: string, fileRelative: string): Promise<void>;
|
|
17
|
+
|
|
18
|
+
export { assertEntryFileExists, getProjectPaths, getRemixConfig };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { renderFatalError } from '@shopify/cli-kit/node/ui';
|
|
2
|
+
import { output, file } from '@shopify/cli-kit';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
|
|
8
|
+
const BUILD_DIR = "dist";
|
|
9
|
+
const CLIENT_SUBDIR = "client";
|
|
10
|
+
const WORKER_SUBDIR = "worker";
|
|
11
|
+
function getProjectPaths(appPath, entry) {
|
|
12
|
+
const root = appPath ?? process.cwd();
|
|
13
|
+
const publicPath = path.join(root, "public");
|
|
14
|
+
const buildPath = path.join(root, BUILD_DIR);
|
|
15
|
+
const buildPathClient = path.join(buildPath, CLIENT_SUBDIR);
|
|
16
|
+
const buildPathWorkerFile = path.join(buildPath, WORKER_SUBDIR, "index.js");
|
|
17
|
+
return {
|
|
18
|
+
root,
|
|
19
|
+
buildPath,
|
|
20
|
+
buildPathClient,
|
|
21
|
+
buildPathWorkerFile,
|
|
22
|
+
publicPath
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function getRemixConfig(root, mode = process.env.NODE_ENV) {
|
|
26
|
+
const { readConfig } = await import('@remix-run/dev/dist/config.js');
|
|
27
|
+
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
|
+
if (!config.serverEntryPoint) {
|
|
36
|
+
throwConfigError(
|
|
37
|
+
"Could not find a server entry point.",
|
|
38
|
+
"Please add a server option to your remix.config.js pointing to an Oxygen worker entry file."
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
assertEntryFileExists(config.rootDirectory, config.serverEntryPoint);
|
|
42
|
+
}
|
|
43
|
+
if (config.serverPlatform !== "neutral") {
|
|
44
|
+
throwConfigError(
|
|
45
|
+
'The serverPlatform in remix.config.js must be "neutral".'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (config.serverModuleFormat !== "esm") {
|
|
49
|
+
throwConfigError(
|
|
50
|
+
'The serverModuleFormat in remix.config.js must be "esm".'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (config.serverDependenciesToBundle !== "all") {
|
|
54
|
+
throwConfigError(
|
|
55
|
+
'The serverDependenciesToBundle in remix.config.js must be "all".'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (!config.serverConditions?.includes("worker")) {
|
|
59
|
+
throwConfigError(
|
|
60
|
+
'The serverConditions in remix.config.js must include "worker".'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (process.env.NODE_ENV === "development" && !config.serverConditions?.includes("development")) {
|
|
64
|
+
output.warn(
|
|
65
|
+
"Add `process.env.NODE_ENV` value to serverConditions in remix.config.js to enable debugging features in development."
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const expectedServerMainFields = ["browser", "module", "main"];
|
|
69
|
+
if (!config.serverMainFields || !expectedServerMainFields.every(
|
|
70
|
+
(v, i) => config.serverMainFields?.[i] === v
|
|
71
|
+
)) {
|
|
72
|
+
throwConfigError(
|
|
73
|
+
`The serverMainFields in remix.config.js must be ${JSON.stringify(
|
|
74
|
+
expectedServerMainFields
|
|
75
|
+
)}.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const cdnUrl = process.env.HYDROGEN_ASSET_BASE_URL;
|
|
79
|
+
if (cdnUrl && !config.publicPath.startsWith(cdnUrl)) {
|
|
80
|
+
throwConfigError(
|
|
81
|
+
"The publicPath in remix.config.js must be prepended with the value of `process.env.HYDROGEN_ASSET_BASE_URL`."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const expectedServerBuildPath = path.join(
|
|
85
|
+
BUILD_DIR,
|
|
86
|
+
WORKER_SUBDIR,
|
|
87
|
+
"index.js"
|
|
88
|
+
);
|
|
89
|
+
if (config.serverBuildPath !== path.resolve(root, expectedServerBuildPath)) {
|
|
90
|
+
throwConfigError(
|
|
91
|
+
`The serverBuildPath in remix.config.js must be "${expectedServerBuildPath}".`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const expectedAssetsBuildDirectory = path.join(BUILD_DIR, CLIENT_SUBDIR);
|
|
95
|
+
if (!config.assetsBuildDirectory.startsWith(
|
|
96
|
+
path.resolve(root, expectedAssetsBuildDirectory)
|
|
97
|
+
)) {
|
|
98
|
+
throwConfigError(
|
|
99
|
+
`The assetsBuildDirectory in remix.config.js must be in "${expectedAssetsBuildDirectory}".`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (process.env.LOCAL_DEV) {
|
|
103
|
+
const packagesPath = fileURLToPath(new URL("../../..", import.meta.url));
|
|
104
|
+
config.watchPaths ??= [];
|
|
105
|
+
config.watchPaths.push(
|
|
106
|
+
...(await fs.readdir(packagesPath)).map(
|
|
107
|
+
(pkg) => pkg === "hydrogen-react" ? path.resolve(packagesPath, pkg, "dist", "browser-dev", "index.mjs") : path.resolve(packagesPath, pkg, "dist", "development", "index.js")
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
config.watchPaths.push(
|
|
111
|
+
path.join(packagesPath, "cli", "dist", "virtual-routes", "**", "*")
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return config;
|
|
115
|
+
}
|
|
116
|
+
function throwConfigError(message, tryMessage = null) {
|
|
117
|
+
renderFatalError({
|
|
118
|
+
name: "ConfigError",
|
|
119
|
+
type: 0,
|
|
120
|
+
message,
|
|
121
|
+
tryMessage
|
|
122
|
+
});
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
async function assertEntryFileExists(root, fileRelative) {
|
|
126
|
+
const fileAbsolute = path.resolve(root, fileRelative);
|
|
127
|
+
const exists = await file.exists(fileAbsolute);
|
|
128
|
+
if (!exists) {
|
|
129
|
+
if (!path.extname(fileAbsolute)) {
|
|
130
|
+
const { readdir } = await import('fs/promises');
|
|
131
|
+
const files = await readdir(path.dirname(fileAbsolute));
|
|
132
|
+
const exists2 = files.some((file2) => {
|
|
133
|
+
const { name, ext } = path.parse(file2);
|
|
134
|
+
return name === path.basename(fileAbsolute) && /^\.[jt]s$/.test(ext);
|
|
135
|
+
});
|
|
136
|
+
if (exists2)
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
throwConfigError(
|
|
140
|
+
`Entry file "${fileRelative}" not found.`,
|
|
141
|
+
"Please ensure the file exists and that the path is correctly added to the `server` property in remix.config.js."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { assertEntryFileExists, getProjectPaths, getRemixConfig };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
|
|
2
|
+
|
|
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>;
|
|
6
|
+
force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
declare function flagsToCamelObject(obj: Record<string, any>): any;
|
|
9
|
+
/**
|
|
10
|
+
* Parse process arguments into an object for use in the cli as flags.
|
|
11
|
+
* This is used when starting the init command from create-hydrogen without Oclif.
|
|
12
|
+
* @example
|
|
13
|
+
* input: `node ./bin --force --no-install-deps --language js`
|
|
14
|
+
* output: { force: true, installDeps: false, language: 'js' }
|
|
15
|
+
*/
|
|
16
|
+
declare function parseProcessFlags(processArgv: string[], flagMap?: Record<string, string>): any;
|
|
17
|
+
/**
|
|
18
|
+
* Create a deprecated flag to prevent the CLI from crashing when a deprecated flag is used.
|
|
19
|
+
* Displays an info message when the flag is used.
|
|
20
|
+
* @param name The name of the flag.
|
|
21
|
+
*/
|
|
22
|
+
declare function deprecated(name: string): _oclif_core_lib_interfaces_parser_js.Definition<unknown, Record<string, unknown>>;
|
|
23
|
+
|
|
24
|
+
export { commonFlags, deprecated, flagsToCamelObject, parseProcessFlags };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Flags from '@oclif/core/lib/flags.js';
|
|
2
|
+
import { string } from '@shopify/cli-kit';
|
|
3
|
+
import { renderInfo } from '@shopify/cli-kit/node/ui';
|
|
4
|
+
import colors from '@shopify/cli-kit/node/colors';
|
|
5
|
+
|
|
6
|
+
const commonFlags = {
|
|
7
|
+
path: Flags.string({
|
|
8
|
+
description: "The path to the directory of the Hydrogen storefront. The default is the current directory.",
|
|
9
|
+
env: "SHOPIFY_HYDROGEN_FLAG_PATH"
|
|
10
|
+
}),
|
|
11
|
+
port: Flags.integer({
|
|
12
|
+
description: "Port to run the server on.",
|
|
13
|
+
env: "SHOPIFY_HYDROGEN_FLAG_PORT",
|
|
14
|
+
default: 3e3
|
|
15
|
+
}),
|
|
16
|
+
force: Flags.boolean({
|
|
17
|
+
description: "Overwrite the destination directory and files if they already exist.",
|
|
18
|
+
env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
|
|
19
|
+
char: "f"
|
|
20
|
+
})
|
|
21
|
+
};
|
|
22
|
+
function flagsToCamelObject(obj) {
|
|
23
|
+
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
24
|
+
acc[string.camelize(key)] = value;
|
|
25
|
+
return acc;
|
|
26
|
+
}, {});
|
|
27
|
+
}
|
|
28
|
+
function parseProcessFlags(processArgv, flagMap = {}) {
|
|
29
|
+
const [, , ...args] = processArgv;
|
|
30
|
+
const options = {};
|
|
31
|
+
for (let i = 0; i < args.length; i++) {
|
|
32
|
+
const arg = args[i];
|
|
33
|
+
const nextArg = args[i + 1];
|
|
34
|
+
if (arg?.startsWith("-")) {
|
|
35
|
+
let key = arg.replace(/^\-{1,2}/, "");
|
|
36
|
+
let value = !nextArg || nextArg.startsWith("-") ? true : nextArg;
|
|
37
|
+
if (value === true && key.startsWith("no-")) {
|
|
38
|
+
value = false;
|
|
39
|
+
key = key.replace("no-", "");
|
|
40
|
+
}
|
|
41
|
+
options[flagMap[key] || key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return flagsToCamelObject(options);
|
|
45
|
+
}
|
|
46
|
+
function deprecated(name) {
|
|
47
|
+
return Flags.custom({
|
|
48
|
+
parse: () => {
|
|
49
|
+
renderInfo({
|
|
50
|
+
headline: `The ${colors.bold(
|
|
51
|
+
name
|
|
52
|
+
)} flag is deprecated and will be removed in a future version of Shopify CLI.`
|
|
53
|
+
});
|
|
54
|
+
return Promise.resolve(" ");
|
|
55
|
+
},
|
|
56
|
+
hidden: true
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { commonFlags, deprecated, flagsToCamelObject, parseProcessFlags };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flagsToCamelObject, parseProcessFlags } from './flags.js';
|
|
3
|
+
|
|
4
|
+
describe("CLI flag utils", () => {
|
|
5
|
+
it("turns kebab-case flags to camelCase", async () => {
|
|
6
|
+
expect(
|
|
7
|
+
flagsToCamelObject({
|
|
8
|
+
"kebab-case": "value",
|
|
9
|
+
"another-kebab-case": "value",
|
|
10
|
+
flag: "value"
|
|
11
|
+
})
|
|
12
|
+
).toMatchObject({
|
|
13
|
+
kebabCase: "value",
|
|
14
|
+
anotherKebabCase: "value",
|
|
15
|
+
flag: "value"
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
it("parses flags from process.argv", async () => {
|
|
19
|
+
expect(
|
|
20
|
+
parseProcessFlags(
|
|
21
|
+
"node ./bin --force --install-deps --template hello-world --path test --language ts".split(
|
|
22
|
+
" "
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
).toMatchObject({
|
|
26
|
+
force: true,
|
|
27
|
+
installDeps: true,
|
|
28
|
+
template: "hello-world",
|
|
29
|
+
path: "test",
|
|
30
|
+
language: "ts"
|
|
31
|
+
});
|
|
32
|
+
expect(
|
|
33
|
+
parseProcessFlags(
|
|
34
|
+
"node ./bin -f --no-install-deps --language js".split(" "),
|
|
35
|
+
{ f: "force" }
|
|
36
|
+
)
|
|
37
|
+
).toMatchObject({
|
|
38
|
+
force: true,
|
|
39
|
+
installDeps: false,
|
|
40
|
+
language: "js"
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
let isFirstWorkerReload = true;
|
|
2
|
+
function muteDevLogs({ workerReload } = {}) {
|
|
3
|
+
const log = console.log.bind(console);
|
|
4
|
+
console.log = (first, ...rest) => {
|
|
5
|
+
if (typeof first === "string" && first.includes("[mf:")) {
|
|
6
|
+
if (workerReload !== false && first.includes("Worker reloaded")) {
|
|
7
|
+
if (isFirstWorkerReload)
|
|
8
|
+
isFirstWorkerReload = false;
|
|
9
|
+
else
|
|
10
|
+
return log(first.replace("[mf:inf] ", "\u{1F504} ") + "\n", ...rest);
|
|
11
|
+
}
|
|
12
|
+
if (!first.includes("[mf:err]")) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return log(first, ...rest);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { muteDevLogs };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type MiniOxygenOptions = {
|
|
2
|
+
root: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
watch?: boolean;
|
|
5
|
+
buildPathClient: string;
|
|
6
|
+
buildPathWorkerFile: string;
|
|
7
|
+
};
|
|
8
|
+
declare function startMiniOxygen({ root, port, watch, buildPathWorkerFile, buildPathClient, }: MiniOxygenOptions): Promise<void>;
|
|
9
|
+
declare function logResponse(request: Request, response: Response): void;
|
|
10
|
+
|
|
11
|
+
export { logResponse, startMiniOxygen };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { path, file, output } from '@shopify/cli-kit';
|
|
2
|
+
import colors from '@shopify/cli-kit/node/colors';
|
|
3
|
+
|
|
4
|
+
async function startMiniOxygen({
|
|
5
|
+
root,
|
|
6
|
+
port = 3e3,
|
|
7
|
+
watch = false,
|
|
8
|
+
buildPathWorkerFile,
|
|
9
|
+
buildPathClient
|
|
10
|
+
}) {
|
|
11
|
+
const { default: miniOxygen } = await import('@shopify/mini-oxygen');
|
|
12
|
+
const miniOxygenPreview = miniOxygen.default ?? miniOxygen;
|
|
13
|
+
const dotenvPath = path.resolve(root, ".env");
|
|
14
|
+
const { port: actualPort } = await miniOxygenPreview({
|
|
15
|
+
workerFile: buildPathWorkerFile,
|
|
16
|
+
assetsDir: buildPathClient,
|
|
17
|
+
publicPath: "",
|
|
18
|
+
port,
|
|
19
|
+
watch,
|
|
20
|
+
autoReload: watch,
|
|
21
|
+
modules: true,
|
|
22
|
+
env: process.env,
|
|
23
|
+
envPath: await file.exists(dotenvPath) ? dotenvPath : void 0,
|
|
24
|
+
log: () => {
|
|
25
|
+
},
|
|
26
|
+
buildWatchPaths: watch ? [path.resolve(root, buildPathWorkerFile)] : void 0,
|
|
27
|
+
onResponse: (request, response) => logResponse(
|
|
28
|
+
request,
|
|
29
|
+
response
|
|
30
|
+
)
|
|
31
|
+
});
|
|
32
|
+
const listeningAt = `http://localhost:${actualPort}`;
|
|
33
|
+
output.info(
|
|
34
|
+
output.content`🚥 MiniOxygen server started at ${output.token.link(
|
|
35
|
+
listeningAt,
|
|
36
|
+
listeningAt
|
|
37
|
+
)}\n`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function logResponse(request, response) {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(request.url);
|
|
43
|
+
if (["/graphiql"].includes(url.pathname))
|
|
44
|
+
return;
|
|
45
|
+
const isProxy = !!response.url && response.url !== request.url;
|
|
46
|
+
const isDataRequest = !isProxy && url.searchParams.has("_data");
|
|
47
|
+
let route = request.url.replace(url.origin, "");
|
|
48
|
+
let info = "";
|
|
49
|
+
let type = "render";
|
|
50
|
+
if (isProxy) {
|
|
51
|
+
type = "proxy";
|
|
52
|
+
info = `[${response.url}]`;
|
|
53
|
+
}
|
|
54
|
+
if (isDataRequest) {
|
|
55
|
+
type = request.method === "GET" ? "loader" : "action";
|
|
56
|
+
const dataParam = url.searchParams.get("_data")?.replace("routes/", "");
|
|
57
|
+
route = url.pathname;
|
|
58
|
+
info = `[${dataParam}]`;
|
|
59
|
+
}
|
|
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(
|
|
63
|
+
String(response.status)
|
|
64
|
+
)} ${output.token.italic(type.padEnd(7, " "))} ${route}${info ? " " + colors.dim(info) : ""} ${request.headers.get("purpose") === "prefetch" ? output.token.italic("(prefetch)") : ""}`
|
|
65
|
+
);
|
|
66
|
+
} catch {
|
|
67
|
+
if (request && response) {
|
|
68
|
+
output.info(`${request.method} ${response.status} ${request.url}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { logResponse, startMiniOxygen };
|