@shopify/cli-hydrogen 5.2.3 → 5.3.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 +42 -20
- package/dist/commands/hydrogen/deploy.js +171 -0
- package/dist/commands/hydrogen/deploy.test.js +185 -0
- package/dist/commands/hydrogen/dev.js +21 -13
- package/dist/commands/hydrogen/init.js +10 -6
- package/dist/commands/hydrogen/init.test.js +16 -1
- package/dist/commands/hydrogen/preview.js +27 -11
- package/dist/generator-templates/starter/app/root.tsx +6 -4
- package/dist/generator-templates/starter/app/routes/account.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/cart.$lines.tsx +70 -0
- package/dist/generator-templates/starter/app/routes/cart.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/discount.$code.tsx +43 -0
- package/dist/generator-templates/starter/app/routes/products.$handle.tsx +3 -1
- package/dist/generator-templates/starter/package.json +3 -3
- package/dist/generator-templates/starter/remix.env.d.ts +11 -3
- package/dist/generator-templates/starter/server.ts +21 -18
- package/dist/generator-templates/starter/tsconfig.json +1 -1
- package/dist/lib/bundle/analyzer.js +56 -0
- package/dist/lib/bundle/bundle-analyzer.html +2045 -0
- package/dist/lib/flags.js +4 -0
- package/dist/lib/get-oxygen-token.js +47 -0
- package/dist/lib/get-oxygen-token.test.js +104 -0
- package/dist/lib/graphql/admin/oxygen-token.js +21 -0
- package/dist/lib/log.js +56 -13
- package/dist/lib/mini-oxygen/common.js +58 -0
- package/dist/lib/mini-oxygen/index.js +12 -0
- package/dist/lib/{mini-oxygen.js → mini-oxygen/node.js} +27 -52
- package/dist/lib/mini-oxygen/types.js +1 -0
- package/dist/lib/mini-oxygen/workerd-inspector.js +392 -0
- package/dist/lib/mini-oxygen/workerd.js +182 -0
- package/dist/lib/onboarding/common.js +4 -4
- package/dist/lib/onboarding/local.js +1 -1
- package/dist/lib/render-errors.js +1 -1
- package/dist/lib/setups/routes/generate.js +1 -1
- package/dist/virtual-routes/routes/index.jsx +4 -4
- package/oclif.manifest.json +81 -3
- package/package.json +12 -4
|
@@ -1,8 +1,8 @@
|
|
|
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,
|
|
5
|
-
import { resolvePath, relativePath
|
|
4
|
+
import { rmdir, fileSize, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
|
|
5
|
+
import { resolvePath, relativePath } 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';
|
|
8
8
|
import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
|
|
@@ -11,8 +11,11 @@ import { checkLockfileStatus } from '../../lib/check-lockfile.js';
|
|
|
11
11
|
import { findMissingRoutes } from '../../lib/missing-routes.js';
|
|
12
12
|
import { muteRemixLogs, createRemixLogger } from '../../lib/log.js';
|
|
13
13
|
import { codegen } from '../../lib/codegen.js';
|
|
14
|
+
import { buildBundleAnalysis, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
|
|
15
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
14
16
|
|
|
15
17
|
const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
|
|
18
|
+
const MAX_WORKER_BUNDLE_SIZE = 10;
|
|
16
19
|
class Build extends Command {
|
|
17
20
|
static description = "Builds a Hydrogen storefront for production.";
|
|
18
21
|
static flags = {
|
|
@@ -20,7 +23,13 @@ class Build extends Command {
|
|
|
20
23
|
sourcemap: Flags.boolean({
|
|
21
24
|
description: "Generate sourcemaps for the build.",
|
|
22
25
|
env: "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP",
|
|
23
|
-
|
|
26
|
+
allowNo: true,
|
|
27
|
+
default: true
|
|
28
|
+
}),
|
|
29
|
+
["bundle-stats"]: Flags.boolean({
|
|
30
|
+
description: "Show a bundle size summary after building.",
|
|
31
|
+
default: true,
|
|
32
|
+
allowNo: true
|
|
24
33
|
}),
|
|
25
34
|
"disable-route-warning": Flags.boolean({
|
|
26
35
|
description: "Disable warning about missing standard routes.",
|
|
@@ -51,11 +60,16 @@ async function runBuild({
|
|
|
51
60
|
useCodegen = false,
|
|
52
61
|
codegenConfigPath,
|
|
53
62
|
sourcemap = false,
|
|
54
|
-
disableRouteWarning = false
|
|
63
|
+
disableRouteWarning = false,
|
|
64
|
+
bundleStats = true,
|
|
65
|
+
assetPath
|
|
55
66
|
}) {
|
|
56
67
|
if (!process.env.NODE_ENV) {
|
|
57
68
|
process.env.NODE_ENV = "production";
|
|
58
69
|
}
|
|
70
|
+
if (assetPath) {
|
|
71
|
+
process.env.HYDROGEN_ASSET_BASE_URL = assetPath;
|
|
72
|
+
}
|
|
59
73
|
const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
|
|
60
74
|
await Promise.all([checkLockfileStatus(root), muteRemixLogs()]);
|
|
61
75
|
console.time(LOG_WORKER_BUILT);
|
|
@@ -90,29 +104,37 @@ async function runBuild({
|
|
|
90
104
|
if (process.env.NODE_ENV !== "development") {
|
|
91
105
|
console.timeEnd(LOG_WORKER_BUILT);
|
|
92
106
|
const sizeMB = await fileSize(buildPathWorkerFile) / (1024 * 1024);
|
|
107
|
+
const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
|
|
93
108
|
outputInfo(
|
|
94
109
|
outputContent` ${colors.dim(
|
|
95
110
|
relativePath(root, buildPathWorkerFile)
|
|
96
|
-
)} ${outputToken.
|
|
111
|
+
)} ${outputToken.link(
|
|
112
|
+
colors.yellow(sizeMB.toFixed(2) + " MB"),
|
|
113
|
+
bundleAnalysisPath
|
|
114
|
+
)}\n`
|
|
97
115
|
);
|
|
98
|
-
if (sizeMB
|
|
116
|
+
if (bundleStats && sizeMB < MAX_WORKER_BUNDLE_SIZE) {
|
|
117
|
+
outputInfo(
|
|
118
|
+
outputContent`${await getBundleAnalysisSummary(buildPathWorkerFile) || "\n"}\n │\n └─── ${outputToken.link(
|
|
119
|
+
"Complete analysis: " + bundleAnalysisPath,
|
|
120
|
+
bundleAnalysisPath
|
|
121
|
+
)}\n\n`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
|
|
125
|
+
throw new AbortError(
|
|
126
|
+
"\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB.",
|
|
127
|
+
outputContent`See the bundle analysis for a breakdown of what is contributing to the bundle size:\n${outputToken.link(
|
|
128
|
+
bundleAnalysisPath,
|
|
129
|
+
bundleAnalysisPath
|
|
130
|
+
)}`
|
|
131
|
+
);
|
|
132
|
+
} else if (sizeMB >= 5) {
|
|
99
133
|
outputWarn(
|
|
100
|
-
`\u{1F6A8} Worker bundle exceeds
|
|
134
|
+
`\u{1F6A8} Worker bundle exceeds 5 MB! This can delay your worker response.${remixConfig.serverMinify ? "" : " Minify your bundle by adding `serverMinify: true` to remix.config.js."}
|
|
101
135
|
`
|
|
102
136
|
);
|
|
103
137
|
}
|
|
104
|
-
if (sourcemap) {
|
|
105
|
-
if (process.env.HYDROGEN_ASSET_BASE_URL) {
|
|
106
|
-
const filepaths = await glob(joinPath(buildPathClient, "**/*.js.map"));
|
|
107
|
-
for (const filepath of filepaths) {
|
|
108
|
-
await removeFile(filepath);
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
outputWarn(
|
|
112
|
-
"\u{1F6A8} Sourcemaps are enabled in production! Use this only for testing.\n"
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
138
|
}
|
|
117
139
|
if (!disableRouteWarning) {
|
|
118
140
|
const missingRoutes = findMissingRoutes(remixConfig);
|
|
@@ -127,7 +149,7 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
|
|
|
127
149
|
);
|
|
128
150
|
}
|
|
129
151
|
}
|
|
130
|
-
if (!process.env.SHOPIFY_UNIT_TEST) {
|
|
152
|
+
if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
|
|
131
153
|
process.exit(0);
|
|
132
154
|
}
|
|
133
155
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
|
+
import colors from '@shopify/cli-kit/node/colors';
|
|
4
|
+
import { outputWarn, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
|
|
5
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
6
|
+
import { resolvePath } from '@shopify/cli-kit/node/path';
|
|
7
|
+
import { renderFatalError, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
|
|
8
|
+
import { parseToken, createDeploy } from '@shopify/oxygen-cli/deploy';
|
|
9
|
+
import { commonFlags } from '../../lib/flags.js';
|
|
10
|
+
import { getOxygenDeploymentToken } from '../../lib/get-oxygen-token.js';
|
|
11
|
+
import { runBuild } from './build.js';
|
|
12
|
+
|
|
13
|
+
const deploymentLogger = (message, level = "info") => {
|
|
14
|
+
if (level === "error" || level === "warn") {
|
|
15
|
+
outputWarn(message);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
class Deploy extends Command {
|
|
19
|
+
static flags = {
|
|
20
|
+
path: commonFlags.path,
|
|
21
|
+
shop: commonFlags.shop,
|
|
22
|
+
publicDeployment: Flags.boolean({
|
|
23
|
+
env: "SHOPIFY_HYDROGEN_FLAG_PUBLIC_DEPLOYMENT",
|
|
24
|
+
description: "Marks a preview deployment as publicly accessible.",
|
|
25
|
+
required: false,
|
|
26
|
+
default: false
|
|
27
|
+
}),
|
|
28
|
+
metadataUrl: Flags.string({
|
|
29
|
+
description: "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
|
|
30
|
+
required: false,
|
|
31
|
+
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL"
|
|
32
|
+
}),
|
|
33
|
+
metadataUser: Flags.string({
|
|
34
|
+
description: "User that initiated the deployment. Will be saved and displayed in the Shopify admin",
|
|
35
|
+
required: false,
|
|
36
|
+
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_USER"
|
|
37
|
+
}),
|
|
38
|
+
metadataVersion: Flags.string({
|
|
39
|
+
description: "A version identifier for the deployment. Will be saved and displayed in the Shopify admin",
|
|
40
|
+
required: false,
|
|
41
|
+
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION"
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
static hidden = true;
|
|
45
|
+
async run() {
|
|
46
|
+
const { flags } = await this.parse(Deploy);
|
|
47
|
+
const actualPath = flags.path ? resolvePath(flags.path) : process.cwd();
|
|
48
|
+
await oxygenDeploy({
|
|
49
|
+
path: actualPath,
|
|
50
|
+
shop: flags.shop,
|
|
51
|
+
publicDeployment: flags.publicDeployment,
|
|
52
|
+
metadataUrl: flags.metadataUrl,
|
|
53
|
+
metadataUser: flags.metadataUser,
|
|
54
|
+
metadataVersion: flags.metadataVersion
|
|
55
|
+
}).catch((error) => {
|
|
56
|
+
renderFatalError(error);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}).finally(() => {
|
|
59
|
+
process.exit(0);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function oxygenDeploy(options) {
|
|
64
|
+
const {
|
|
65
|
+
path,
|
|
66
|
+
shop,
|
|
67
|
+
publicDeployment,
|
|
68
|
+
metadataUrl,
|
|
69
|
+
metadataUser,
|
|
70
|
+
metadataVersion
|
|
71
|
+
} = options;
|
|
72
|
+
const token = await getOxygenDeploymentToken({
|
|
73
|
+
root: path,
|
|
74
|
+
flagShop: shop
|
|
75
|
+
});
|
|
76
|
+
if (!token) {
|
|
77
|
+
throw new AbortError("Could not obtain Oxygen deployment token");
|
|
78
|
+
}
|
|
79
|
+
const config = {
|
|
80
|
+
assetsDir: "dist/client",
|
|
81
|
+
deploymentUrl: "https://oxygen.shopifyapps.com",
|
|
82
|
+
deploymentToken: parseToken(token),
|
|
83
|
+
healthCheckMaxDuration: 180,
|
|
84
|
+
metadata: {
|
|
85
|
+
...metadataUrl ? { url: metadataUrl } : {},
|
|
86
|
+
...metadataUser ? { user: metadataUser } : {},
|
|
87
|
+
...metadataVersion ? { version: metadataVersion } : {}
|
|
88
|
+
},
|
|
89
|
+
publicDeployment,
|
|
90
|
+
skipHealthCheck: false,
|
|
91
|
+
rootPath: path,
|
|
92
|
+
skipBuild: false,
|
|
93
|
+
workerOnly: false,
|
|
94
|
+
workerDir: "dist/worker"
|
|
95
|
+
};
|
|
96
|
+
let resolveUpload;
|
|
97
|
+
const uploadPromise = new Promise((resolve) => {
|
|
98
|
+
resolveUpload = resolve;
|
|
99
|
+
});
|
|
100
|
+
let resolveHealthCheck;
|
|
101
|
+
const healthCheckPromise = new Promise((resolve) => {
|
|
102
|
+
resolveHealthCheck = resolve;
|
|
103
|
+
});
|
|
104
|
+
let deployError = null;
|
|
105
|
+
let resolveDeploy;
|
|
106
|
+
let rejectDeploy;
|
|
107
|
+
const deployPromise = new Promise((resolve, reject) => {
|
|
108
|
+
resolveDeploy = resolve;
|
|
109
|
+
rejectDeploy = reject;
|
|
110
|
+
});
|
|
111
|
+
const hooks = {
|
|
112
|
+
buildFunction: async (assetPath) => {
|
|
113
|
+
outputInfo(
|
|
114
|
+
outputContent`${colors.whiteBright("Building project...")}`.value
|
|
115
|
+
);
|
|
116
|
+
await runBuild({
|
|
117
|
+
directory: path,
|
|
118
|
+
assetPath,
|
|
119
|
+
sourcemap: false,
|
|
120
|
+
useCodegen: false
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
onHealthCheckComplete: () => resolveHealthCheck(),
|
|
124
|
+
onUploadFilesStart: () => uploadStart(),
|
|
125
|
+
onUploadFilesComplete: () => resolveUpload(),
|
|
126
|
+
onHealthCheckError: (error) => {
|
|
127
|
+
deployError = new AbortError(
|
|
128
|
+
error.message,
|
|
129
|
+
"Please verify the deployment status in the Shopify Admin and retry deploying if necessary."
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
onUploadFilesError: (error) => {
|
|
133
|
+
deployError = new AbortError(
|
|
134
|
+
error.message,
|
|
135
|
+
"Check your connection and try again. If the problem persists, try again later or contact support."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const uploadStart = async () => {
|
|
140
|
+
outputInfo(
|
|
141
|
+
outputContent`${colors.whiteBright("Deploying to Oxygen..\n")}`.value
|
|
142
|
+
);
|
|
143
|
+
await renderTasks([
|
|
144
|
+
{
|
|
145
|
+
title: "Uploading files",
|
|
146
|
+
task: async () => await uploadPromise
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
title: "Performing health check",
|
|
150
|
+
task: async () => await healthCheckPromise
|
|
151
|
+
}
|
|
152
|
+
]);
|
|
153
|
+
};
|
|
154
|
+
await createDeploy({ config, hooks, logger: deploymentLogger }).then((url) => {
|
|
155
|
+
const deploymentType = config.publicDeployment ? "public" : "private";
|
|
156
|
+
renderSuccess({
|
|
157
|
+
body: ["Successfully deployed to Oxygen"],
|
|
158
|
+
nextSteps: [
|
|
159
|
+
[
|
|
160
|
+
`Open ${url} in your browser to view your ${deploymentType} deployment`
|
|
161
|
+
]
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
resolveDeploy();
|
|
165
|
+
}).catch((error) => {
|
|
166
|
+
rejectDeploy(deployError || error);
|
|
167
|
+
});
|
|
168
|
+
return deployPromise;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { Deploy as default, deploymentLogger, oxygenDeploy };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
|
|
2
|
+
import { login } from '../../lib/auth.js';
|
|
3
|
+
import { getStorefronts } from '../../lib/graphql/admin/link-storefront.js';
|
|
4
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
5
|
+
import { renderSelectPrompt, renderSuccess, renderFatalError } from '@shopify/cli-kit/node/ui';
|
|
6
|
+
import { oxygenDeploy, deploymentLogger } from './deploy.js';
|
|
7
|
+
import { getOxygenDeploymentToken } from '../../lib/get-oxygen-token.js';
|
|
8
|
+
import { createDeploy, parseToken } from '@shopify/oxygen-cli/deploy';
|
|
9
|
+
|
|
10
|
+
vi.mock("../../lib/get-oxygen-token.js");
|
|
11
|
+
vi.mock("@shopify/oxygen-cli/deploy");
|
|
12
|
+
vi.mock("../../lib/auth.js");
|
|
13
|
+
vi.mock("../../lib/shopify-config.js");
|
|
14
|
+
vi.mock("../../lib/graphql/admin/link-storefront.js");
|
|
15
|
+
vi.mock("../../lib/graphql/admin/create-storefront.js");
|
|
16
|
+
vi.mock("../../lib/graphql/admin/fetch-job.js");
|
|
17
|
+
vi.mock("../../lib/shell.js", () => ({ getCliCommand: () => "h2" }));
|
|
18
|
+
vi.mock("@shopify/cli-kit/node/output", async () => {
|
|
19
|
+
return {
|
|
20
|
+
outputContent: () => ({ value: "" }),
|
|
21
|
+
outputInfo: () => {
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
vi.mock("@shopify/cli-kit/node/ui", async () => {
|
|
26
|
+
return {
|
|
27
|
+
renderFatalError: vi.fn(),
|
|
28
|
+
renderSelectPrompt: vi.fn(),
|
|
29
|
+
renderSuccess: vi.fn(),
|
|
30
|
+
renderTasks: vi.fn()
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
describe("deploy", () => {
|
|
34
|
+
const ADMIN_SESSION = {
|
|
35
|
+
token: "abc123",
|
|
36
|
+
storeFqdn: "my-shop.myshopify.com"
|
|
37
|
+
};
|
|
38
|
+
const FULL_SHOPIFY_CONFIG = {
|
|
39
|
+
shop: "my-shop.myshopify.com",
|
|
40
|
+
shopName: "My Shop",
|
|
41
|
+
email: "email",
|
|
42
|
+
storefront: {
|
|
43
|
+
id: "gid://shopify/HydrogenStorefront/1",
|
|
44
|
+
title: "Hydrogen"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const UNLINKED_SHOPIFY_CONFIG = {
|
|
48
|
+
...FULL_SHOPIFY_CONFIG,
|
|
49
|
+
storefront: void 0
|
|
50
|
+
};
|
|
51
|
+
const originalExit = process.exit;
|
|
52
|
+
const deployParams = {
|
|
53
|
+
path: "./",
|
|
54
|
+
shop: "snowdevil.myshopify.com",
|
|
55
|
+
publicDeployment: false,
|
|
56
|
+
metadataUrl: "https://example.com",
|
|
57
|
+
metadataUser: "user",
|
|
58
|
+
metadataVersion: "1.0.0"
|
|
59
|
+
};
|
|
60
|
+
const mockToken = {
|
|
61
|
+
accessToken: "some-token",
|
|
62
|
+
allowedResource: "some-resource",
|
|
63
|
+
appId: "1",
|
|
64
|
+
client: "1",
|
|
65
|
+
expiresAt: "some-time",
|
|
66
|
+
namespace: "some-namespace",
|
|
67
|
+
namespaceId: "1"
|
|
68
|
+
};
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
process.exit = vi.fn();
|
|
71
|
+
vi.mocked(login).mockResolvedValue({
|
|
72
|
+
session: ADMIN_SESSION,
|
|
73
|
+
config: UNLINKED_SHOPIFY_CONFIG
|
|
74
|
+
});
|
|
75
|
+
vi.mocked(getStorefronts).mockResolvedValue([
|
|
76
|
+
{
|
|
77
|
+
...FULL_SHOPIFY_CONFIG.storefront,
|
|
78
|
+
parsedId: "1",
|
|
79
|
+
productionUrl: "https://example.com"
|
|
80
|
+
}
|
|
81
|
+
]);
|
|
82
|
+
vi.mocked(renderSelectPrompt).mockResolvedValue(FULL_SHOPIFY_CONFIG.shop);
|
|
83
|
+
vi.mocked(createDeploy).mockResolvedValue(
|
|
84
|
+
"https://a-lovely-deployment.com"
|
|
85
|
+
);
|
|
86
|
+
vi.mocked(getOxygenDeploymentToken).mockResolvedValue("some-encoded-token");
|
|
87
|
+
vi.mocked(parseToken).mockReturnValue(mockToken);
|
|
88
|
+
});
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
vi.resetAllMocks();
|
|
91
|
+
process.exit = originalExit;
|
|
92
|
+
});
|
|
93
|
+
it("calls getOxygenDeploymentToken with the correct parameters", async () => {
|
|
94
|
+
await oxygenDeploy(deployParams);
|
|
95
|
+
expect(getOxygenDeploymentToken).toHaveBeenCalledWith({
|
|
96
|
+
root: "./",
|
|
97
|
+
flagShop: "snowdevil.myshopify.com"
|
|
98
|
+
});
|
|
99
|
+
expect(getOxygenDeploymentToken).toHaveBeenCalledTimes(1);
|
|
100
|
+
});
|
|
101
|
+
it("calls createDeploy with the correct parameters", async () => {
|
|
102
|
+
await oxygenDeploy(deployParams);
|
|
103
|
+
const expectedConfig = {
|
|
104
|
+
assetsDir: "dist/client",
|
|
105
|
+
deploymentUrl: "https://oxygen.shopifyapps.com",
|
|
106
|
+
deploymentToken: mockToken,
|
|
107
|
+
healthCheckMaxDuration: 180,
|
|
108
|
+
metadata: {
|
|
109
|
+
url: deployParams.metadataUrl,
|
|
110
|
+
user: deployParams.metadataUser,
|
|
111
|
+
version: deployParams.metadataVersion
|
|
112
|
+
},
|
|
113
|
+
publicDeployment: deployParams.publicDeployment,
|
|
114
|
+
skipHealthCheck: false,
|
|
115
|
+
rootPath: deployParams.path,
|
|
116
|
+
skipBuild: false,
|
|
117
|
+
workerOnly: false,
|
|
118
|
+
workerDir: "dist/worker"
|
|
119
|
+
};
|
|
120
|
+
expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
|
|
121
|
+
config: expectedConfig,
|
|
122
|
+
hooks: {
|
|
123
|
+
buildFunction: expect.any(Function),
|
|
124
|
+
onHealthCheckComplete: expect.any(Function),
|
|
125
|
+
onUploadFilesStart: expect.any(Function),
|
|
126
|
+
onUploadFilesComplete: expect.any(Function),
|
|
127
|
+
onHealthCheckError: expect.any(Function),
|
|
128
|
+
onUploadFilesError: expect.any(Function)
|
|
129
|
+
},
|
|
130
|
+
logger: deploymentLogger
|
|
131
|
+
});
|
|
132
|
+
expect(vi.mocked(renderSuccess)).toHaveBeenCalled;
|
|
133
|
+
});
|
|
134
|
+
it("handles error during uploadFiles", async () => {
|
|
135
|
+
const mockRenderFatalError = vi.fn();
|
|
136
|
+
vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
|
|
137
|
+
const error = new Error("Wonky internet!");
|
|
138
|
+
vi.mocked(createDeploy).mockImplementation((options) => {
|
|
139
|
+
options.hooks?.onUploadFilesStart?.();
|
|
140
|
+
options.hooks?.onUploadFilesError?.(error);
|
|
141
|
+
return new Promise((_resolve, reject) => {
|
|
142
|
+
reject(error);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
try {
|
|
146
|
+
await oxygenDeploy(deployParams);
|
|
147
|
+
expect(true).toBe(false);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof AbortError) {
|
|
150
|
+
expect(err.message).toBe(error.message);
|
|
151
|
+
expect(err.tryMessage).toBe(
|
|
152
|
+
"Check your connection and try again. If the problem persists, try again later or contact support."
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
expect(true).toBe(false);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
it("handles error during health check", async () => {
|
|
160
|
+
const mockRenderFatalError = vi.fn();
|
|
161
|
+
vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
|
|
162
|
+
const error = new Error("Cloudflare is down!");
|
|
163
|
+
vi.mocked(createDeploy).mockImplementation((options) => {
|
|
164
|
+
options.hooks?.onUploadFilesStart?.();
|
|
165
|
+
options.hooks?.onUploadFilesComplete?.();
|
|
166
|
+
options.hooks?.onHealthCheckError?.(error);
|
|
167
|
+
return new Promise((_resolve, reject) => {
|
|
168
|
+
reject(error);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
await oxygenDeploy(deployParams);
|
|
173
|
+
expect(true).toBe(false);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err instanceof AbortError) {
|
|
176
|
+
expect(err.message).toBe(error.message);
|
|
177
|
+
expect(err.tryMessage).toBe(
|
|
178
|
+
"Please verify the deployment status in the Shopify Admin and retry deploying if necessary."
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
expect(true).toBe(false);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -10,7 +10,7 @@ import { muteDevLogs, createRemixLogger, enhanceH2Logs } from '../../lib/log.js'
|
|
|
10
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
|
-
import { startMiniOxygen } from '../../lib/mini-oxygen.js';
|
|
13
|
+
import { startMiniOxygen } from '../../lib/mini-oxygen/index.js';
|
|
14
14
|
import { checkHydrogenVersion } from '../../lib/check-version.js';
|
|
15
15
|
import { addVirtualRoutes } from '../../lib/virtual-routes.js';
|
|
16
16
|
import { spawnCodegenProcess } from '../../lib/codegen.js';
|
|
@@ -26,6 +26,7 @@ class Dev extends Command {
|
|
|
26
26
|
static flags = {
|
|
27
27
|
path: commonFlags.path,
|
|
28
28
|
port: commonFlags.port,
|
|
29
|
+
["worker-unstable"]: commonFlags.workerRuntime,
|
|
29
30
|
["codegen-unstable"]: Flags.boolean({
|
|
30
31
|
description: "Generate types for the Storefront API queries found in your project. It updates the types on file save.",
|
|
31
32
|
required: false,
|
|
@@ -52,6 +53,7 @@ class Dev extends Command {
|
|
|
52
53
|
await runDev({
|
|
53
54
|
...flagsToCamelObject(flags),
|
|
54
55
|
useCodegen: flags["codegen-unstable"],
|
|
56
|
+
workerRuntime: flags["worker-unstable"],
|
|
55
57
|
path: directory
|
|
56
58
|
});
|
|
57
59
|
}
|
|
@@ -60,6 +62,7 @@ async function runDev({
|
|
|
60
62
|
port: portFlag = DEFAULT_PORT,
|
|
61
63
|
path: appPath,
|
|
62
64
|
useCodegen = false,
|
|
65
|
+
workerRuntime = false,
|
|
63
66
|
codegenConfigPath,
|
|
64
67
|
disableVirtualRoutes,
|
|
65
68
|
envBranch,
|
|
@@ -107,14 +110,17 @@ async function runDev({
|
|
|
107
110
|
async function safeStartMiniOxygen() {
|
|
108
111
|
if (miniOxygen)
|
|
109
112
|
return;
|
|
110
|
-
miniOxygen = await startMiniOxygen(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
miniOxygen = await startMiniOxygen(
|
|
114
|
+
{
|
|
115
|
+
root,
|
|
116
|
+
port: portFlag,
|
|
117
|
+
watch: !liveReload,
|
|
118
|
+
buildPathWorkerFile,
|
|
119
|
+
buildPathClient,
|
|
120
|
+
env: await envPromise
|
|
121
|
+
},
|
|
122
|
+
workerRuntime
|
|
123
|
+
);
|
|
118
124
|
const graphiqlUrl = `${miniOxygen.listeningAt}/graphiql`;
|
|
119
125
|
const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
|
|
120
126
|
enhanceH2Logs({ graphiqlUrl, ...remixConfig });
|
|
@@ -125,9 +131,11 @@ async function runDev({
|
|
|
125
131
|
extraLines: [
|
|
126
132
|
colors.dim(`
|
|
127
133
|
View GraphiQL API browser: ${graphiqlUrl}`),
|
|
128
|
-
colors.dim(
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
workerRuntime ? "" : colors.dim(
|
|
135
|
+
`
|
|
136
|
+
View server-side network requests: ${debugNetworkUrl}`
|
|
137
|
+
)
|
|
138
|
+
].filter(Boolean)
|
|
131
139
|
});
|
|
132
140
|
if (useCodegen) {
|
|
133
141
|
spawnCodegenProcess({ ...remixConfig, configFilePath: codegenConfigPath });
|
|
@@ -182,7 +190,7 @@ View server-side network requests: ${debugNetworkUrl}`)
|
|
|
182
190
|
if (!miniOxygen) {
|
|
183
191
|
await safeStartMiniOxygen();
|
|
184
192
|
} else if (liveReload) {
|
|
185
|
-
await miniOxygen.reload(
|
|
193
|
+
await miniOxygen.reload();
|
|
186
194
|
}
|
|
187
195
|
liveReload?.onAppReady(context);
|
|
188
196
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import {
|
|
3
|
+
import { packageManagerFromUserAgent } from '@shopify/cli-kit/node/node-package-manager';
|
|
4
4
|
import { Flags } from '@oclif/core';
|
|
5
5
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
6
6
|
import { AbortController } from '@shopify/cli-kit/node/abort';
|
|
@@ -42,7 +42,8 @@ class Init extends Command {
|
|
|
42
42
|
routes: Flags.boolean({
|
|
43
43
|
description: "Generate routes for all pages.",
|
|
44
44
|
env: "SHOPIFY_HYDROGEN_FLAG_ROUTES",
|
|
45
|
-
hidden: true
|
|
45
|
+
hidden: true,
|
|
46
|
+
allowNo: true
|
|
46
47
|
}),
|
|
47
48
|
git: Flags.boolean({
|
|
48
49
|
description: "Init Git and create initial commits.",
|
|
@@ -52,10 +53,13 @@ class Init extends Command {
|
|
|
52
53
|
})
|
|
53
54
|
};
|
|
54
55
|
async run() {
|
|
55
|
-
const {
|
|
56
|
-
|
|
56
|
+
const {
|
|
57
|
+
flags: { markets, ..._flags }
|
|
58
|
+
} = await this.parse(Init);
|
|
59
|
+
const flags = { ..._flags, i18n: markets };
|
|
60
|
+
if (flags.i18n && !I18N_CHOICES.includes(flags.i18n)) {
|
|
57
61
|
throw new AbortError(
|
|
58
|
-
`Invalid URL structure strategy: ${flags.
|
|
62
|
+
`Invalid URL structure strategy: ${flags.i18n}. Must be one of ${I18N_CHOICES.join(", ")}`
|
|
59
63
|
);
|
|
60
64
|
}
|
|
61
65
|
if (flags.styling && !STYLING_CHOICES.includes(flags.styling)) {
|
|
@@ -77,7 +81,7 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
|
|
|
77
81
|
"cli"
|
|
78
82
|
);
|
|
79
83
|
if (showUpgrade) {
|
|
80
|
-
const packageManager =
|
|
84
|
+
const packageManager = packageManagerFromUserAgent();
|
|
81
85
|
showUpgrade(
|
|
82
86
|
packageManager === "unknown" ? "" : `Please use the latest version with \`${packageManager} create @shopify/hydrogen@latest\``
|
|
83
87
|
);
|
|
@@ -46,7 +46,7 @@ vi.mock(
|
|
|
46
46
|
return {
|
|
47
47
|
...original,
|
|
48
48
|
getPackageManager: () => Promise.resolve("npm"),
|
|
49
|
-
|
|
49
|
+
packageManagerFromUserAgent: () => "npm",
|
|
50
50
|
installNodeModules: vi.fn(async ({ directory }) => {
|
|
51
51
|
renderTasksHook.mockImplementationOnce(async () => {
|
|
52
52
|
await writeFile(`${directory}/package-lock.json`, "{}");
|
|
@@ -519,6 +519,21 @@ describe("init", () => {
|
|
|
519
519
|
);
|
|
520
520
|
expect(mb).toBeGreaterThan(0);
|
|
521
521
|
expect(mb).toBeLessThan(1);
|
|
522
|
+
expect(output).toMatch("Complete analysis: file://");
|
|
523
|
+
const clientAnalysisPath = "dist/worker/client-bundle-analyzer.html";
|
|
524
|
+
const workerAnalysisPath = "dist/worker/worker-bundle-analyzer.html";
|
|
525
|
+
expect(
|
|
526
|
+
fileExists(joinPath(tmpDir, clientAnalysisPath))
|
|
527
|
+
).resolves.toBeTruthy();
|
|
528
|
+
expect(
|
|
529
|
+
fileExists(joinPath(tmpDir, workerAnalysisPath))
|
|
530
|
+
).resolves.toBeTruthy();
|
|
531
|
+
expect(await readFile(joinPath(tmpDir, clientAnalysisPath))).toMatch(
|
|
532
|
+
/globalThis\.METAFILE = '.+';/g
|
|
533
|
+
);
|
|
534
|
+
expect(await readFile(joinPath(tmpDir, workerAnalysisPath))).toMatch(
|
|
535
|
+
/globalThis\.METAFILE = '.+';/g
|
|
536
|
+
);
|
|
522
537
|
});
|
|
523
538
|
});
|
|
524
539
|
});
|
|
@@ -1,34 +1,50 @@
|
|
|
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, DEFAULT_PORT } from '../../lib/flags.js';
|
|
5
|
-
import { startMiniOxygen } from '../../lib/mini-oxygen.js';
|
|
4
|
+
import { commonFlags, flagsToCamelObject, DEFAULT_PORT } from '../../lib/flags.js';
|
|
5
|
+
import { startMiniOxygen } from '../../lib/mini-oxygen/index.js';
|
|
6
|
+
import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
|
|
7
|
+
import { getConfig } from '../../lib/shopify-config.js';
|
|
6
8
|
|
|
7
9
|
class Preview extends Command {
|
|
8
10
|
static description = "Runs a Hydrogen storefront in an Oxygen worker for production.";
|
|
9
11
|
static flags = {
|
|
10
12
|
path: commonFlags.path,
|
|
11
|
-
port: commonFlags.port
|
|
13
|
+
port: commonFlags.port,
|
|
14
|
+
["worker-unstable"]: commonFlags.workerRuntime,
|
|
15
|
+
["env-branch"]: commonFlags.envBranch
|
|
12
16
|
};
|
|
13
17
|
async run() {
|
|
14
18
|
const { flags } = await this.parse(Preview);
|
|
15
|
-
await runPreview({
|
|
19
|
+
await runPreview({
|
|
20
|
+
...flagsToCamelObject(flags),
|
|
21
|
+
workerRuntime: flags["worker-unstable"]
|
|
22
|
+
});
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
async function runPreview({
|
|
19
26
|
port = DEFAULT_PORT,
|
|
20
|
-
path: appPath
|
|
27
|
+
path: appPath,
|
|
28
|
+
workerRuntime = false,
|
|
29
|
+
envBranch
|
|
21
30
|
}) {
|
|
22
31
|
if (!process.env.NODE_ENV)
|
|
23
32
|
process.env.NODE_ENV = "production";
|
|
24
33
|
muteDevLogs({ workerReload: false });
|
|
25
34
|
const { root, buildPathWorkerFile, buildPathClient } = getProjectPaths(appPath);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const { shop, storefront } = await getConfig(root);
|
|
36
|
+
const fetchRemote = !!shop && !!storefront?.id;
|
|
37
|
+
const env = await getAllEnvironmentVariables({ root, fetchRemote, envBranch });
|
|
38
|
+
const miniOxygen = await startMiniOxygen(
|
|
39
|
+
{
|
|
40
|
+
root,
|
|
41
|
+
port,
|
|
42
|
+
buildPathClient,
|
|
43
|
+
buildPathWorkerFile,
|
|
44
|
+
env
|
|
45
|
+
},
|
|
46
|
+
workerRuntime
|
|
47
|
+
);
|
|
32
48
|
miniOxygen.showBanner({ mode: "preview" });
|
|
33
49
|
}
|
|
34
50
|
|