@shopify/cli-hydrogen 5.1.1 → 5.2.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.js +10 -5
- package/dist/commands/hydrogen/check.js +1 -1
- package/dist/commands/hydrogen/codegen-unstable.js +3 -3
- package/dist/commands/hydrogen/dev.js +26 -17
- package/dist/commands/hydrogen/init.js +3 -0
- package/dist/commands/hydrogen/init.test.js +199 -51
- package/dist/commands/hydrogen/preview.js +4 -3
- package/dist/commands/hydrogen/setup.js +5 -2
- package/dist/commands/hydrogen/setup.test.js +62 -0
- package/dist/generator-templates/starter/app/components/Footer.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Header.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Search.tsx +3 -3
- package/dist/generator-templates/starter/app/root.tsx +24 -1
- package/dist/generator-templates/starter/app/routes/$.tsx +4 -0
- package/dist/generator-templates/starter/app/routes/_index.tsx +6 -2
- package/dist/generator-templates/starter/app/routes/account.$.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/account.addresses.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -7
- package/dist/generator-templates/starter/app/routes/account.profile.tsx +7 -2
- package/dist/generator-templates/starter/app/routes/account.tsx +4 -3
- package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +6 -2
- package/dist/generator-templates/starter/app/routes/account_.login.tsx +6 -2
- package/dist/generator-templates/starter/app/routes/account_.logout.tsx +2 -6
- package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/blogs._index.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/cart.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +2 -3
- package/dist/generator-templates/starter/app/routes/products.$handle.tsx +23 -15
- package/dist/generator-templates/starter/app/routes/search.tsx +1 -2
- package/dist/generator-templates/starter/package.json +4 -4
- package/dist/generator-templates/starter/remix.config.js +1 -0
- package/dist/generator-templates/starter/storefrontapi.generated.d.ts +9 -9
- package/dist/lib/ast.js +9 -0
- package/dist/lib/check-version.test.js +1 -0
- package/dist/lib/codegen.js +17 -7
- package/dist/lib/environment-variables.js +15 -11
- package/dist/lib/file.test.js +4 -5
- package/dist/lib/find-port.js +9 -0
- package/dist/lib/flags.js +3 -2
- package/dist/lib/format-code.js +3 -0
- package/dist/lib/live-reload.js +62 -0
- package/dist/lib/log.js +6 -1
- package/dist/lib/mini-oxygen.js +28 -18
- package/dist/lib/missing-routes.js +17 -1
- package/dist/lib/onboarding/common.js +5 -0
- package/dist/lib/onboarding/local.js +21 -8
- package/dist/lib/onboarding/remote.js +8 -3
- package/dist/lib/remix-config.js +2 -0
- package/dist/lib/remix-version-check.test.js +1 -0
- package/dist/lib/setups/css/replacers.js +7 -4
- package/dist/lib/setups/i18n/replacers.js +7 -5
- package/dist/lib/setups/routes/generate.js +4 -1
- package/dist/lib/setups/routes/generate.test.js +10 -11
- package/dist/lib/template-downloader.js +4 -0
- package/dist/lib/transpile-ts.js +1 -0
- package/dist/lib/virtual-routes.js +4 -1
- package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +26 -4
- package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +40 -10
- package/dist/virtual-routes/components/IconBanner.jsx +286 -44
- package/dist/virtual-routes/components/IconDiscord.jsx +17 -1
- package/dist/virtual-routes/components/IconError.jsx +55 -17
- package/dist/virtual-routes/components/IconGithub.jsx +19 -1
- package/dist/virtual-routes/components/IconTwitter.jsx +17 -1
- package/dist/virtual-routes/components/Layout.jsx +1 -1
- package/dist/virtual-routes/routes/index.jsx +110 -94
- package/dist/virtual-routes/virtual-root.jsx +7 -15
- package/oclif.manifest.json +1 -1
- package/package.json +9 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
3
|
import { outputInfo, outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
|
|
4
|
-
import { rmdir, fileSize, glob, removeFile, copyFile } from '@shopify/cli-kit/node/fs';
|
|
4
|
+
import { rmdir, fileSize, glob, removeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
|
|
5
5
|
import { resolvePath, relativePath, joinPath } from '@shopify/cli-kit/node/path';
|
|
6
6
|
import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
|
|
7
7
|
import colors from '@shopify/cli-kit/node/colors';
|
|
@@ -42,12 +42,12 @@ class Build extends Command {
|
|
|
42
42
|
await runBuild({
|
|
43
43
|
...flagsToCamelObject(flags),
|
|
44
44
|
useCodegen: flags["codegen-unstable"],
|
|
45
|
-
|
|
45
|
+
directory
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
async function runBuild({
|
|
50
|
-
|
|
50
|
+
directory,
|
|
51
51
|
useCodegen = false,
|
|
52
52
|
codegenConfigPath,
|
|
53
53
|
sourcemap = false,
|
|
@@ -56,7 +56,7 @@ async function runBuild({
|
|
|
56
56
|
if (!process.env.NODE_ENV) {
|
|
57
57
|
process.env.NODE_ENV = "production";
|
|
58
58
|
}
|
|
59
|
-
const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(
|
|
59
|
+
const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
|
|
60
60
|
await Promise.all([checkLockfileStatus(root), muteRemixLogs()]);
|
|
61
61
|
console.time(LOG_WORKER_BUILT);
|
|
62
62
|
outputInfo(`
|
|
@@ -125,9 +125,14 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
process.
|
|
128
|
+
if (!process.env.SHOPIFY_UNIT_TEST) {
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
129
131
|
}
|
|
130
132
|
async function copyPublicFiles(publicPath, buildPathClient) {
|
|
133
|
+
if (!await fileExists(publicPath)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
131
136
|
return copyFile(publicPath, buildPathClient);
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -29,17 +29,17 @@ class Codegen extends Command {
|
|
|
29
29
|
const directory = flags.path ? path.resolve(flags.path) : process.cwd();
|
|
30
30
|
await runCodegen({
|
|
31
31
|
...flagsToCamelObject(flags),
|
|
32
|
-
|
|
32
|
+
directory
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
async function runCodegen({
|
|
37
|
-
|
|
37
|
+
directory,
|
|
38
38
|
codegenConfigPath,
|
|
39
39
|
forceSfapiVersion,
|
|
40
40
|
watch
|
|
41
41
|
}) {
|
|
42
|
-
const { root } = getProjectPaths(
|
|
42
|
+
const { root } = getProjectPaths(directory);
|
|
43
43
|
const remixConfig = await getRemixConfig(root);
|
|
44
44
|
console.log("");
|
|
45
45
|
const generatedFiles = await codegen({
|
|
@@ -6,8 +6,8 @@ import { renderFatalError } from '@shopify/cli-kit/node/ui';
|
|
|
6
6
|
import colors from '@shopify/cli-kit/node/colors';
|
|
7
7
|
import { copyPublicFiles } from './build.js';
|
|
8
8
|
import { getProjectPaths, assertOxygenChecks, getRemixConfig } from '../../lib/remix-config.js';
|
|
9
|
-
import { muteDevLogs,
|
|
10
|
-
import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
|
|
9
|
+
import { muteDevLogs, createRemixLogger, enhanceH2Logs } from '../../lib/log.js';
|
|
10
|
+
import { commonFlags, deprecated, flagsToCamelObject, DEFAULT_PORT } from '../../lib/flags.js';
|
|
11
11
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
12
12
|
import { Flags } from '@oclif/core';
|
|
13
13
|
import { startMiniOxygen } from '../../lib/mini-oxygen.js';
|
|
@@ -16,6 +16,8 @@ import { addVirtualRoutes } from '../../lib/virtual-routes.js';
|
|
|
16
16
|
import { spawnCodegenProcess } from '../../lib/codegen.js';
|
|
17
17
|
import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
|
|
18
18
|
import { getConfig } from '../../lib/shopify-config.js';
|
|
19
|
+
import { findPort } from '../../lib/find-port.js';
|
|
20
|
+
import { setupLiveReload } from '../../lib/live-reload.js';
|
|
19
21
|
import { checkRemixVersions } from '../../lib/remix-version-check.js';
|
|
20
22
|
|
|
21
23
|
const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
|
|
@@ -56,7 +58,7 @@ class Dev extends Command {
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
async function runDev({
|
|
59
|
-
port,
|
|
61
|
+
port: portFlag = DEFAULT_PORT,
|
|
60
62
|
path: appPath,
|
|
61
63
|
useCodegen = false,
|
|
62
64
|
codegenConfigPath,
|
|
@@ -68,7 +70,6 @@ async function runDev({
|
|
|
68
70
|
if (!process.env.NODE_ENV)
|
|
69
71
|
process.env.NODE_ENV = "development";
|
|
70
72
|
muteDevLogs();
|
|
71
|
-
await muteRemixLogs();
|
|
72
73
|
if (debug)
|
|
73
74
|
(await import('node:inspector')).open();
|
|
74
75
|
const { root, publicPath, buildPathClient, buildPathWorkerFile } = getProjectPaths(appPath);
|
|
@@ -102,14 +103,15 @@ async function runDev({
|
|
|
102
103
|
let isInitialBuild = true;
|
|
103
104
|
let initialBuildDurationMs = 0;
|
|
104
105
|
let initialBuildStartTimeMs = Date.now();
|
|
106
|
+
const liveReload = remixConfig.future.v2_dev ? await setupLiveReload(remixConfig.devServerPort) : void 0;
|
|
105
107
|
let miniOxygen;
|
|
106
108
|
async function safeStartMiniOxygen() {
|
|
107
109
|
if (miniOxygen)
|
|
108
110
|
return;
|
|
109
111
|
miniOxygen = await startMiniOxygen({
|
|
110
112
|
root,
|
|
111
|
-
port,
|
|
112
|
-
watch:
|
|
113
|
+
port: await findPort(portFlag),
|
|
114
|
+
watch: !liveReload,
|
|
113
115
|
buildPathWorkerFile,
|
|
114
116
|
buildPathClient,
|
|
115
117
|
env: await envPromise
|
|
@@ -145,13 +147,15 @@ View GraphiQL API browser: ${graphiqlUrl}`)]
|
|
|
145
147
|
},
|
|
146
148
|
{
|
|
147
149
|
reloadConfig,
|
|
148
|
-
onBuildStart() {
|
|
150
|
+
onBuildStart(ctx) {
|
|
149
151
|
if (!isInitialBuild && !skipRebuildLogs) {
|
|
150
152
|
outputInfo(LOG_REBUILDING);
|
|
151
153
|
console.time(LOG_REBUILT);
|
|
152
154
|
}
|
|
155
|
+
liveReload?.onBuildStart(ctx);
|
|
153
156
|
},
|
|
154
|
-
|
|
157
|
+
onBuildManifest: liveReload?.onBuildManifest,
|
|
158
|
+
async onBuildFinish(context, duration, succeeded) {
|
|
155
159
|
if (isInitialBuild) {
|
|
156
160
|
await copyingFiles;
|
|
157
161
|
initialBuildDurationMs = Date.now() - initialBuildStartTimeMs;
|
|
@@ -162,16 +166,21 @@ View GraphiQL API browser: ${graphiqlUrl}`)]
|
|
|
162
166
|
if (!miniOxygen)
|
|
163
167
|
console.log("");
|
|
164
168
|
}
|
|
165
|
-
if (!miniOxygen) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
if (!miniOxygen && !await serverBundleExists()) {
|
|
170
|
+
return renderFatalError({
|
|
171
|
+
name: "BuildError",
|
|
172
|
+
type: 0,
|
|
173
|
+
message: "MiniOxygen cannot start because the server bundle has not been generated.",
|
|
174
|
+
tryMessage: "This is likely due to an error in your app and Remix is unable to compile. Try fixing the app and MiniOxygen will start."
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (succeeded) {
|
|
178
|
+
if (!miniOxygen) {
|
|
179
|
+
await safeStartMiniOxygen();
|
|
180
|
+
} else if (liveReload) {
|
|
181
|
+
await miniOxygen.reload({ worker: true });
|
|
173
182
|
}
|
|
174
|
-
|
|
183
|
+
liveReload?.onAppReady(context);
|
|
175
184
|
}
|
|
176
185
|
},
|
|
177
186
|
async onFileCreated(file) {
|
|
@@ -70,6 +70,9 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
|
|
|
70
70
|
supressNodeExperimentalWarnings();
|
|
71
71
|
options.git ??= true;
|
|
72
72
|
const showUpgrade = await checkHydrogenVersion(
|
|
73
|
+
// Resolving the CLI package from a local directory might fail because
|
|
74
|
+
// this code could be run from a global dependency (e.g. on `npm create`).
|
|
75
|
+
// Therefore, pass the known path to the package.json directly from here:
|
|
73
76
|
fileURLToPath(new URL("../../../package.json", import.meta.url)),
|
|
74
77
|
"cli"
|
|
75
78
|
);
|
|
@@ -1,39 +1,30 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
import { vi, describe, beforeEach, it, expect } from 'vitest';
|
|
3
|
-
import { temporaryDirectoryTask } from 'tempy';
|
|
4
3
|
import { runInit } from './init.js';
|
|
5
4
|
import { exec } from '@shopify/cli-kit/node/system';
|
|
6
5
|
import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { writeFile, inTemporaryDirectory, readFile, isDirectory, removeFile, fileExists } from '@shopify/cli-kit/node/fs';
|
|
7
|
+
import { joinPath, basename } from '@shopify/cli-kit/node/path';
|
|
9
8
|
import { checkHydrogenVersion } from '../../lib/check-version.js';
|
|
10
9
|
import { handleProjectLocation } from '../../lib/onboarding/common.js';
|
|
11
10
|
import glob from 'fast-glob';
|
|
12
11
|
import { getSkeletonSourceDir } from '../../lib/build.js';
|
|
13
12
|
import { execAsync } from '../../lib/process.js';
|
|
14
13
|
import { rmdir, symlink } from 'fs-extra';
|
|
14
|
+
import { runCheckRoutes } from './check.js';
|
|
15
|
+
import { runCodegen } from './codegen-unstable.js';
|
|
16
|
+
import { runBuild } from './build.js';
|
|
15
17
|
|
|
18
|
+
const { renderTasksHook } = vi.hoisted(() => ({ renderTasksHook: vi.fn() }));
|
|
19
|
+
vi.mock("../../lib/check-version.js");
|
|
16
20
|
vi.mock("../../lib/template-downloader.js", async () => ({
|
|
17
|
-
getLatestTemplates: () => Promise.resolve({
|
|
21
|
+
getLatestTemplates: () => Promise.resolve({
|
|
22
|
+
version: "",
|
|
23
|
+
templatesDir: fileURLToPath(
|
|
24
|
+
new URL("../../../../../templates", import.meta.url)
|
|
25
|
+
)
|
|
26
|
+
})
|
|
18
27
|
}));
|
|
19
|
-
vi.mock(
|
|
20
|
-
"@shopify/cli-kit/node/node-package-manager",
|
|
21
|
-
async (importOriginal) => {
|
|
22
|
-
const original = await importOriginal();
|
|
23
|
-
return {
|
|
24
|
-
...original,
|
|
25
|
-
installNodeModules: vi.fn(),
|
|
26
|
-
getPackageManager: () => Promise.resolve("npm"),
|
|
27
|
-
packageManagerUsedForCreating: () => Promise.resolve("npm")
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
vi.mock("../../lib/check-version.js");
|
|
32
|
-
const { renderTasksHook } = vi.hoisted(() => {
|
|
33
|
-
return {
|
|
34
|
-
renderTasksHook: vi.fn()
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
28
|
vi.mock("@shopify/cli-kit/node/ui", async () => {
|
|
38
29
|
const original = await vi.importActual("@shopify/cli-kit/node/ui");
|
|
39
30
|
return {
|
|
@@ -48,6 +39,30 @@ vi.mock("@shopify/cli-kit/node/ui", async () => {
|
|
|
48
39
|
})
|
|
49
40
|
};
|
|
50
41
|
});
|
|
42
|
+
vi.mock(
|
|
43
|
+
"@shopify/cli-kit/node/node-package-manager",
|
|
44
|
+
async (importOriginal) => {
|
|
45
|
+
const original = await importOriginal();
|
|
46
|
+
return {
|
|
47
|
+
...original,
|
|
48
|
+
getPackageManager: () => Promise.resolve("npm"),
|
|
49
|
+
packageManagerUsedForCreating: () => Promise.resolve("npm"),
|
|
50
|
+
installNodeModules: vi.fn(async ({ directory }) => {
|
|
51
|
+
renderTasksHook.mockImplementationOnce(async () => {
|
|
52
|
+
await writeFile(`${directory}/package-lock.json`, "{}");
|
|
53
|
+
});
|
|
54
|
+
await rmdir(joinPath(directory, "node_modules")).catch(() => {
|
|
55
|
+
});
|
|
56
|
+
await symlink(
|
|
57
|
+
fileURLToPath(
|
|
58
|
+
new URL("../../../../../node_modules", import.meta.url)
|
|
59
|
+
),
|
|
60
|
+
joinPath(directory, "node_modules")
|
|
61
|
+
);
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
);
|
|
51
66
|
vi.mock("../../lib/onboarding/common.js", async (importOriginal) => {
|
|
52
67
|
const original = await importOriginal();
|
|
53
68
|
return Object.keys(original).reduce((acc, item) => {
|
|
@@ -65,10 +80,11 @@ describe("init", () => {
|
|
|
65
80
|
const outputMock = mockAndCaptureOutput();
|
|
66
81
|
beforeEach(() => {
|
|
67
82
|
vi.clearAllMocks();
|
|
83
|
+
vi.unstubAllEnvs();
|
|
68
84
|
outputMock.clear();
|
|
69
85
|
});
|
|
70
86
|
it("checks Hydrogen version", async () => {
|
|
71
|
-
await
|
|
87
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
72
88
|
const showUpgradeMock = vi.fn((param) => ({
|
|
73
89
|
currentVersion: "1.0.0",
|
|
74
90
|
newVersion: "1.0.1"
|
|
@@ -83,9 +99,90 @@ describe("init", () => {
|
|
|
83
99
|
);
|
|
84
100
|
});
|
|
85
101
|
});
|
|
102
|
+
describe("remote templates", () => {
|
|
103
|
+
it("throws for unknown templates", async () => {
|
|
104
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
105
|
+
await expect(
|
|
106
|
+
runInit({
|
|
107
|
+
path: tmpDir,
|
|
108
|
+
git: false,
|
|
109
|
+
language: "ts",
|
|
110
|
+
template: "https://github.com/some/repo"
|
|
111
|
+
})
|
|
112
|
+
).rejects.toThrow("supported");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it("creates basic projects", async () => {
|
|
116
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
117
|
+
await runInit({
|
|
118
|
+
path: tmpDir,
|
|
119
|
+
git: false,
|
|
120
|
+
language: "ts",
|
|
121
|
+
template: "hello-world"
|
|
122
|
+
});
|
|
123
|
+
const helloWorldFiles = await glob("**/*", {
|
|
124
|
+
cwd: getSkeletonSourceDir().replace("skeleton", "hello-world"),
|
|
125
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
126
|
+
});
|
|
127
|
+
const projectFiles = await glob("**/*", { cwd: tmpDir });
|
|
128
|
+
const nonAppFiles = helloWorldFiles.filter(
|
|
129
|
+
(item) => !item.startsWith("app/")
|
|
130
|
+
);
|
|
131
|
+
expect(projectFiles).toEqual(expect.arrayContaining(nonAppFiles));
|
|
132
|
+
expect(projectFiles).toContain("app/root.tsx");
|
|
133
|
+
expect(projectFiles).toContain("app/entry.client.tsx");
|
|
134
|
+
expect(projectFiles).toContain("app/entry.server.tsx");
|
|
135
|
+
expect(projectFiles).not.toContain("app/components/Layout.tsx");
|
|
136
|
+
expect(projectFiles).not.toContain("app/routes/_index.tsx");
|
|
137
|
+
await expect(readFile(`${tmpDir}/package.json`)).resolves.toMatch(
|
|
138
|
+
`"name": "hello-world"`
|
|
139
|
+
);
|
|
140
|
+
const output = outputMock.info();
|
|
141
|
+
expect(output).toMatch("success");
|
|
142
|
+
expect(output).not.toMatch("warning");
|
|
143
|
+
expect(output).not.toMatch("Routes");
|
|
144
|
+
expect(output).toMatch(/Language:\s*TypeScript/);
|
|
145
|
+
expect(output).toMatch("Help");
|
|
146
|
+
expect(output).toMatch("Next steps");
|
|
147
|
+
expect(output).toMatch(
|
|
148
|
+
// Output contains banner characters. USe [^\w]*? to match them.
|
|
149
|
+
/Run `cd .*? &&[^\w]*?npm[^\w]*?install[^\w]*?&&[^\w]*?npm[^\w]*?run[^\w]*?dev`/ims
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
it("transpiles projects to JS", async () => {
|
|
154
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
155
|
+
await runInit({
|
|
156
|
+
path: tmpDir,
|
|
157
|
+
git: false,
|
|
158
|
+
language: "js",
|
|
159
|
+
template: "hello-world"
|
|
160
|
+
});
|
|
161
|
+
const helloWorldFiles = await glob("**/*", {
|
|
162
|
+
cwd: getSkeletonSourceDir().replace("skeleton", "hello-world"),
|
|
163
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
164
|
+
});
|
|
165
|
+
const projectFiles = await glob("**/*", { cwd: tmpDir });
|
|
166
|
+
expect(projectFiles).toEqual(
|
|
167
|
+
expect.arrayContaining(
|
|
168
|
+
helloWorldFiles.filter((item) => !item.endsWith(".d.ts")).map(
|
|
169
|
+
(item) => item.replace(/\.ts(x)?$/, ".js$1").replace(/tsconfig\.json$/, "jsconfig.json")
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
await expect(readFile(`${tmpDir}/server.js`)).resolves.toMatch(
|
|
174
|
+
/export default {\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/
|
|
175
|
+
);
|
|
176
|
+
const output = outputMock.info();
|
|
177
|
+
expect(output).toMatch("success");
|
|
178
|
+
expect(output).not.toMatch("warning");
|
|
179
|
+
expect(output).toMatch(/Language:\s*JavaScript/);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
86
183
|
describe("local templates", () => {
|
|
87
184
|
it("creates basic projects", async () => {
|
|
88
|
-
await
|
|
185
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
89
186
|
await runInit({
|
|
90
187
|
path: tmpDir,
|
|
91
188
|
git: false,
|
|
@@ -124,12 +221,13 @@ describe("init", () => {
|
|
|
124
221
|
expect(output).toMatch("Help");
|
|
125
222
|
expect(output).toMatch("Next steps");
|
|
126
223
|
expect(output).toMatch(
|
|
224
|
+
// Output contains banner characters. USe [^\w]*? to match them.
|
|
127
225
|
/Run `cd .*? &&[^\w]*?npm[^\w]*?install[^\w]*?&&[^\w]*?npm[^\w]*?run[^\w]*?dev`/ims
|
|
128
226
|
);
|
|
129
227
|
});
|
|
130
228
|
});
|
|
131
229
|
it("creates projects with route files", async () => {
|
|
132
|
-
await
|
|
230
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
133
231
|
await runInit({ path: tmpDir, git: false, routes: true, language: "ts" });
|
|
134
232
|
const skeletonFiles = await glob("**/*", {
|
|
135
233
|
cwd: getSkeletonSourceDir(),
|
|
@@ -152,7 +250,7 @@ describe("init", () => {
|
|
|
152
250
|
});
|
|
153
251
|
});
|
|
154
252
|
it("transpiles projects to JS", async () => {
|
|
155
|
-
await
|
|
253
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
156
254
|
await runInit({ path: tmpDir, git: false, routes: true, language: "js" });
|
|
157
255
|
const skeletonFiles = await glob("**/*", {
|
|
158
256
|
cwd: getSkeletonSourceDir(),
|
|
@@ -182,7 +280,7 @@ describe("init", () => {
|
|
|
182
280
|
});
|
|
183
281
|
describe("styling libraries", () => {
|
|
184
282
|
it("scaffolds Tailwind CSS", async () => {
|
|
185
|
-
await
|
|
283
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
186
284
|
await runInit({
|
|
187
285
|
path: tmpDir,
|
|
188
286
|
git: false,
|
|
@@ -207,7 +305,7 @@ describe("init", () => {
|
|
|
207
305
|
});
|
|
208
306
|
});
|
|
209
307
|
it("scaffolds CSS Modules", async () => {
|
|
210
|
-
await
|
|
308
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
211
309
|
await runInit({
|
|
212
310
|
path: tmpDir,
|
|
213
311
|
git: false,
|
|
@@ -229,7 +327,7 @@ describe("init", () => {
|
|
|
229
327
|
});
|
|
230
328
|
});
|
|
231
329
|
it("scaffolds Vanilla Extract", async () => {
|
|
232
|
-
await
|
|
330
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
233
331
|
await runInit({
|
|
234
332
|
path: tmpDir,
|
|
235
333
|
git: false,
|
|
@@ -253,7 +351,7 @@ describe("init", () => {
|
|
|
253
351
|
});
|
|
254
352
|
describe("i18n strategies", () => {
|
|
255
353
|
it("scaffolds i18n with domains strategy", async () => {
|
|
256
|
-
await
|
|
354
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
257
355
|
await runInit({
|
|
258
356
|
path: tmpDir,
|
|
259
357
|
git: false,
|
|
@@ -273,7 +371,7 @@ describe("init", () => {
|
|
|
273
371
|
});
|
|
274
372
|
});
|
|
275
373
|
it("scaffolds i18n with subdomains strategy", async () => {
|
|
276
|
-
await
|
|
374
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
277
375
|
await runInit({
|
|
278
376
|
path: tmpDir,
|
|
279
377
|
git: false,
|
|
@@ -293,7 +391,7 @@ describe("init", () => {
|
|
|
293
391
|
});
|
|
294
392
|
});
|
|
295
393
|
it("scaffolds i18n with subfolders strategy", async () => {
|
|
296
|
-
await
|
|
394
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
297
395
|
await runInit({
|
|
298
396
|
path: tmpDir,
|
|
299
397
|
git: false,
|
|
@@ -315,10 +413,7 @@ describe("init", () => {
|
|
|
315
413
|
});
|
|
316
414
|
describe("git", () => {
|
|
317
415
|
it("initializes a git repository and creates initial commits", async () => {
|
|
318
|
-
await
|
|
319
|
-
renderTasksHook.mockImplementationOnce(async () => {
|
|
320
|
-
await writeFile(`${tmpDir}/package-lock.json`, "{}");
|
|
321
|
-
});
|
|
416
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
322
417
|
await runInit({
|
|
323
418
|
path: tmpDir,
|
|
324
419
|
git: true,
|
|
@@ -345,10 +440,7 @@ describe("init", () => {
|
|
|
345
440
|
});
|
|
346
441
|
describe("project validity", () => {
|
|
347
442
|
it("typechecks the project", async () => {
|
|
348
|
-
await
|
|
349
|
-
renderTasksHook.mockImplementationOnce(async () => {
|
|
350
|
-
await writeFile(`${tmpDir}/package-lock.json`, "{}");
|
|
351
|
-
});
|
|
443
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
352
444
|
await runInit({
|
|
353
445
|
path: tmpDir,
|
|
354
446
|
git: true,
|
|
@@ -358,21 +450,77 @@ describe("init", () => {
|
|
|
358
450
|
routes: true,
|
|
359
451
|
installDeps: true
|
|
360
452
|
});
|
|
361
|
-
await rmdir(joinPath(tmpDir, "node_modules")).catch(() => {
|
|
362
|
-
});
|
|
363
|
-
await symlink(
|
|
364
|
-
fileURLToPath(
|
|
365
|
-
new URL("../../../../../node_modules", import.meta.url)
|
|
366
|
-
),
|
|
367
|
-
joinPath(tmpDir, "node_modules")
|
|
368
|
-
);
|
|
369
453
|
await expect(
|
|
370
|
-
exec("npm", ["run", "typecheck"], {
|
|
371
|
-
cwd: tmpDir
|
|
372
|
-
})
|
|
454
|
+
exec("npm", ["run", "typecheck"], { cwd: tmpDir })
|
|
373
455
|
).resolves.not.toThrow();
|
|
374
456
|
});
|
|
375
457
|
});
|
|
458
|
+
it("contains all standard routes", async () => {
|
|
459
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
460
|
+
await runInit({
|
|
461
|
+
path: tmpDir,
|
|
462
|
+
git: true,
|
|
463
|
+
language: "ts",
|
|
464
|
+
i18n: "subfolders",
|
|
465
|
+
routes: true,
|
|
466
|
+
installDeps: true
|
|
467
|
+
});
|
|
468
|
+
outputMock.clear();
|
|
469
|
+
await runCheckRoutes({ directory: tmpDir });
|
|
470
|
+
const output = outputMock.info();
|
|
471
|
+
expect(output).toMatch("success");
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
it("supports codegen", async () => {
|
|
475
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
476
|
+
await runInit({
|
|
477
|
+
path: tmpDir,
|
|
478
|
+
git: true,
|
|
479
|
+
language: "ts",
|
|
480
|
+
routes: true,
|
|
481
|
+
installDeps: true
|
|
482
|
+
});
|
|
483
|
+
outputMock.clear();
|
|
484
|
+
const codegenFile = `${tmpDir}/storefrontapi.generated.d.ts`;
|
|
485
|
+
const codegenFromTemplate = await readFile(codegenFile);
|
|
486
|
+
expect(codegenFromTemplate).toBeTruthy();
|
|
487
|
+
await removeFile(codegenFile);
|
|
488
|
+
expect(fileExists(codegenFile)).resolves.toBeFalsy();
|
|
489
|
+
await expect(runCodegen({ directory: tmpDir })).resolves.not.toThrow();
|
|
490
|
+
const output = outputMock.info();
|
|
491
|
+
expect(output).toMatch("success");
|
|
492
|
+
await expect(readFile(codegenFile)).resolves.toEqual(
|
|
493
|
+
codegenFromTemplate
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
it("builds the generated project", async () => {
|
|
498
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
499
|
+
await runInit({
|
|
500
|
+
path: tmpDir,
|
|
501
|
+
git: true,
|
|
502
|
+
language: "ts",
|
|
503
|
+
styling: "postcss",
|
|
504
|
+
i18n: "subfolders",
|
|
505
|
+
routes: true,
|
|
506
|
+
installDeps: true
|
|
507
|
+
});
|
|
508
|
+
outputMock.clear();
|
|
509
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
510
|
+
await expect(runBuild({ directory: tmpDir })).resolves.not.toThrow();
|
|
511
|
+
const expectedBundlePath = "dist/worker/index.js";
|
|
512
|
+
const output = outputMock.output();
|
|
513
|
+
expect(output).toMatch(expectedBundlePath);
|
|
514
|
+
expect(
|
|
515
|
+
fileExists(joinPath(tmpDir, expectedBundlePath))
|
|
516
|
+
).resolves.toBeTruthy();
|
|
517
|
+
const mb = Number(
|
|
518
|
+
output.match(/index\.js\s+([\d.]+)\s+MB/)?.[1] || ""
|
|
519
|
+
);
|
|
520
|
+
expect(mb).toBeGreaterThan(0);
|
|
521
|
+
expect(mb).toBeLessThan(1);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
376
524
|
});
|
|
377
525
|
});
|
|
378
526
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
2
|
import { muteDevLogs } from '../../lib/log.js';
|
|
3
3
|
import { getProjectPaths } from '../../lib/remix-config.js';
|
|
4
|
-
import { commonFlags } from '../../lib/flags.js';
|
|
4
|
+
import { commonFlags, DEFAULT_PORT } from '../../lib/flags.js';
|
|
5
5
|
import { startMiniOxygen } from '../../lib/mini-oxygen.js';
|
|
6
|
+
import { findPort } from '../../lib/find-port.js';
|
|
6
7
|
|
|
7
8
|
class Preview extends Command {
|
|
8
9
|
static description = "Runs a Hydrogen storefront in an Oxygen worker for production.";
|
|
@@ -16,7 +17,7 @@ class Preview extends Command {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
async function runPreview({
|
|
19
|
-
port,
|
|
20
|
+
port = DEFAULT_PORT,
|
|
20
21
|
path: appPath
|
|
21
22
|
}) {
|
|
22
23
|
if (!process.env.NODE_ENV)
|
|
@@ -25,7 +26,7 @@ async function runPreview({
|
|
|
25
26
|
const { root, buildPathWorkerFile, buildPathClient } = getProjectPaths(appPath);
|
|
26
27
|
const miniOxygen = await startMiniOxygen({
|
|
27
28
|
root,
|
|
28
|
-
port,
|
|
29
|
+
port: await findPort(port),
|
|
29
30
|
buildPathClient,
|
|
30
31
|
buildPathWorkerFile
|
|
31
32
|
});
|
|
@@ -44,7 +44,7 @@ async function runSetup(options) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
];
|
|
47
|
-
const i18nStrategy = options.
|
|
47
|
+
const i18nStrategy = options.markets ? options.markets : await renderI18nPrompt({
|
|
48
48
|
abortSignal: controller.signal,
|
|
49
49
|
extraChoices: { none: "Set up later" }
|
|
50
50
|
});
|
|
@@ -57,6 +57,8 @@ async function runSetup(options) {
|
|
|
57
57
|
const typescript = !!remixConfig.tsconfigPath;
|
|
58
58
|
backgroundWorkPromise = backgroundWorkPromise.then(
|
|
59
59
|
() => Promise.all([
|
|
60
|
+
// When starting from hello-world, the server entry point won't
|
|
61
|
+
// include all the cart logic from skeleton, so we need to copy it.
|
|
60
62
|
generateProjectFile("../server.ts", { ...remixConfig, typescript }),
|
|
61
63
|
...typescript ? [
|
|
62
64
|
copyFile(
|
|
@@ -71,6 +73,7 @@ async function runSetup(options) {
|
|
|
71
73
|
)
|
|
72
74
|
)
|
|
73
75
|
] : [],
|
|
76
|
+
// Copy app entries
|
|
74
77
|
generateProjectEntries({
|
|
75
78
|
rootDirectory: remixConfig.rootDirectory,
|
|
76
79
|
appDirectory: remixConfig.appDirectory,
|
|
@@ -130,4 +133,4 @@ async function runSetup(options) {
|
|
|
130
133
|
);
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
export { Setup as default };
|
|
136
|
+
export { Setup as default, runSetup };
|