@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.
Files changed (81) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +14 -0
  3. package/dist/commands/hydrogen/build.d.ts +23 -0
  4. package/dist/commands/hydrogen/build.js +100 -0
  5. package/dist/commands/hydrogen/check.d.ts +18 -0
  6. package/dist/commands/hydrogen/check.js +35 -0
  7. package/dist/commands/hydrogen/dev.d.ts +15 -0
  8. package/dist/commands/hydrogen/dev.js +134 -0
  9. package/dist/commands/hydrogen/generate/route.d.ts +34 -0
  10. package/dist/commands/hydrogen/generate/route.js +166 -0
  11. package/dist/commands/hydrogen/generate/route.test.d.ts +1 -0
  12. package/dist/commands/hydrogen/generate/route.test.js +126 -0
  13. package/dist/commands/hydrogen/generate/routes.d.ts +16 -0
  14. package/dist/commands/hydrogen/generate/routes.js +22 -0
  15. package/dist/commands/hydrogen/init.d.ts +24 -0
  16. package/dist/commands/hydrogen/init.js +199 -0
  17. package/dist/commands/hydrogen/init.test.d.ts +1 -0
  18. package/dist/commands/hydrogen/init.test.js +79 -0
  19. package/dist/commands/hydrogen/preview.d.ts +17 -0
  20. package/dist/commands/hydrogen/preview.js +34 -0
  21. package/dist/create-app.d.ts +1 -0
  22. package/dist/create-app.js +4 -0
  23. package/dist/generator-templates/routes/[robots.txt].tsx +41 -0
  24. package/dist/generator-templates/routes/[sitemap.xml].tsx +204 -0
  25. package/dist/generator-templates/routes/account/login.tsx +74 -0
  26. package/dist/generator-templates/routes/account/register.tsx +74 -0
  27. package/dist/generator-templates/routes/cart.tsx +41 -0
  28. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +65 -0
  29. package/dist/generator-templates/routes/graphiql.tsx +10 -0
  30. package/dist/generator-templates/routes/index.tsx +7 -0
  31. package/dist/generator-templates/routes/pages/$pageHandle.tsx +74 -0
  32. package/dist/generator-templates/routes/policies/$policyHandle.tsx +87 -0
  33. package/dist/generator-templates/routes/policies/index.tsx +67 -0
  34. package/dist/generator-templates/routes/products/$productHandle.tsx +57 -0
  35. package/dist/hooks/init.d.ts +5 -0
  36. package/dist/hooks/init.js +18 -0
  37. package/dist/utils/check-lockfile.d.ts +3 -0
  38. package/dist/utils/check-lockfile.js +78 -0
  39. package/dist/utils/check-lockfile.test.d.ts +1 -0
  40. package/dist/utils/check-lockfile.test.js +74 -0
  41. package/dist/utils/check-version.d.ts +16 -0
  42. package/dist/utils/check-version.js +55 -0
  43. package/dist/utils/check-version.test.d.ts +1 -0
  44. package/dist/utils/check-version.test.js +60 -0
  45. package/dist/utils/config.d.ts +18 -0
  46. package/dist/utils/config.js +146 -0
  47. package/dist/utils/flags.d.ts +24 -0
  48. package/dist/utils/flags.js +60 -0
  49. package/dist/utils/flags.test.d.ts +1 -0
  50. package/dist/utils/flags.test.js +43 -0
  51. package/dist/utils/log.d.ts +5 -0
  52. package/dist/utils/log.js +20 -0
  53. package/dist/utils/mini-oxygen.d.ts +11 -0
  54. package/dist/utils/mini-oxygen.js +73 -0
  55. package/dist/utils/missing-routes.d.ts +6 -0
  56. package/dist/utils/missing-routes.js +71 -0
  57. package/dist/utils/template-downloader.d.ts +11 -0
  58. package/dist/utils/template-downloader.js +70 -0
  59. package/dist/utils/transpile-ts.d.ts +16 -0
  60. package/dist/utils/transpile-ts.js +169 -0
  61. package/dist/utils/virtual-routes.d.ts +7 -0
  62. package/dist/utils/virtual-routes.js +44 -0
  63. package/dist/utils/virtual-routes.test.d.ts +1 -0
  64. package/dist/utils/virtual-routes.test.js +49 -0
  65. package/dist/virtual-routes/assets/favicon.svg +28 -0
  66. package/dist/virtual-routes/assets/inter-variable-font.woff2 +0 -0
  67. package/dist/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
  68. package/dist/virtual-routes/assets/styles.css +238 -0
  69. package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +7 -0
  70. package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +13 -0
  71. package/dist/virtual-routes/components/IconBanner.jsx +47 -0
  72. package/dist/virtual-routes/components/IconDiscord.jsx +4 -0
  73. package/dist/virtual-routes/components/IconError.jsx +20 -0
  74. package/dist/virtual-routes/components/IconGithub.jsx +4 -0
  75. package/dist/virtual-routes/components/IconTwitter.jsx +4 -0
  76. package/dist/virtual-routes/components/Layout.jsx +6 -0
  77. package/dist/virtual-routes/routes/graphiql.jsx +5 -0
  78. package/dist/virtual-routes/routes/index.jsx +163 -0
  79. package/dist/virtual-routes/virtual-root.jsx +38 -0
  80. package/oclif.manifest.json +1 -0
  81. 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,5 @@
1
+ declare function muteDevLogs({ workerReload }?: {
2
+ workerReload?: boolean;
3
+ }): void;
4
+
5
+ export { muteDevLogs };
@@ -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 };
@@ -0,0 +1,6 @@
1
+ import { RemixConfig } from '@remix-run/dev/dist/config.js';
2
+
3
+ declare function findMissingRoutes(config: RemixConfig): string[];
4
+ declare function logMissingRoutes(routes: string[]): void;
5
+
6
+ export { findMissingRoutes, logMissingRoutes };