@shopify/cli-hydrogen 5.2.2 → 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 +49 -25
- package/dist/commands/hydrogen/deploy.js +171 -0
- package/dist/commands/hydrogen/deploy.test.js +185 -0
- package/dist/commands/hydrogen/dev.js +27 -14
- 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 +4 -4
- package/dist/generator-templates/starter/remix.env.d.ts +12 -3
- package/dist/generator-templates/starter/server.ts +22 -19
- 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/live-reload.js +2 -1
- 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/node.js +110 -0
- 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 +24 -13
- package/dist/lib/onboarding/local.js +1 -1
- package/dist/lib/remix-config.js +12 -2
- package/dist/lib/remix-version-check.js +7 -4
- package/dist/lib/remix-version-check.test.js +1 -1
- package/dist/lib/render-errors.js +1 -1
- package/dist/lib/request-events.js +84 -0
- package/dist/lib/setups/routes/generate.js +3 -3
- package/dist/lib/transpile-ts.js +21 -23
- package/dist/lib/virtual-routes.js +11 -9
- package/dist/virtual-routes/components/FlameChartWrapper.jsx +125 -0
- package/dist/virtual-routes/routes/debug-network.jsx +289 -0
- package/dist/virtual-routes/routes/index.jsx +4 -4
- package/dist/virtual-routes/virtual-root.jsx +7 -4
- package/oclif.manifest.json +81 -3
- package/package.json +35 -12
- package/dist/lib/mini-oxygen.js +0 -108
|
@@ -1,18 +1,21 @@
|
|
|
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
|
-
import { getProjectPaths, getRemixConfig, assertOxygenChecks } from '../../lib/remix-config.js';
|
|
8
|
+
import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
|
|
9
9
|
import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
|
|
10
10
|
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,21 +60,28 @@ 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);
|
|
62
76
|
outputInfo(`
|
|
63
77
|
\u{1F3D7}\uFE0F Building in ${process.env.NODE_ENV} mode...`);
|
|
64
|
-
const [remixConfig, { build }, { logThrown }, { createFileWatchCache }] = await Promise.all([
|
|
78
|
+
const [remixConfig, [{ build }, { logThrown }, { createFileWatchCache }]] = await Promise.all([
|
|
65
79
|
getRemixConfig(root),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
Promise.all([
|
|
81
|
+
import('@remix-run/dev/dist/compiler/build.js'),
|
|
82
|
+
import('@remix-run/dev/dist/compiler/utils/log.js'),
|
|
83
|
+
import('@remix-run/dev/dist/compiler/fileWatchCache.js')
|
|
84
|
+
]).catch(handleRemixImportFail),
|
|
69
85
|
rmdir(buildPath, { force: true })
|
|
70
86
|
]);
|
|
71
87
|
assertOxygenChecks(remixConfig);
|
|
@@ -88,29 +104,37 @@ async function runBuild({
|
|
|
88
104
|
if (process.env.NODE_ENV !== "development") {
|
|
89
105
|
console.timeEnd(LOG_WORKER_BUILT);
|
|
90
106
|
const sizeMB = await fileSize(buildPathWorkerFile) / (1024 * 1024);
|
|
107
|
+
const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
|
|
91
108
|
outputInfo(
|
|
92
109
|
outputContent` ${colors.dim(
|
|
93
110
|
relativePath(root, buildPathWorkerFile)
|
|
94
|
-
)} ${outputToken.
|
|
111
|
+
)} ${outputToken.link(
|
|
112
|
+
colors.yellow(sizeMB.toFixed(2) + " MB"),
|
|
113
|
+
bundleAnalysisPath
|
|
114
|
+
)}\n`
|
|
95
115
|
);
|
|
96
|
-
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) {
|
|
97
133
|
outputWarn(
|
|
98
|
-
`\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."}
|
|
99
135
|
`
|
|
100
136
|
);
|
|
101
137
|
}
|
|
102
|
-
if (sourcemap) {
|
|
103
|
-
if (process.env.HYDROGEN_ASSET_BASE_URL) {
|
|
104
|
-
const filepaths = await glob(joinPath(buildPathClient, "**/*.js.map"));
|
|
105
|
-
for (const filepath of filepaths) {
|
|
106
|
-
await removeFile(filepath);
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
outputWarn(
|
|
110
|
-
"\u{1F6A8} Sourcemaps are enabled in production! Use this only for testing.\n"
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
138
|
}
|
|
115
139
|
if (!disableRouteWarning) {
|
|
116
140
|
const missingRoutes = findMissingRoutes(remixConfig);
|
|
@@ -125,7 +149,7 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
|
|
|
125
149
|
);
|
|
126
150
|
}
|
|
127
151
|
}
|
|
128
|
-
if (!process.env.SHOPIFY_UNIT_TEST) {
|
|
152
|
+
if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
|
|
129
153
|
process.exit(0);
|
|
130
154
|
}
|
|
131
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
|
+
});
|
|
@@ -5,12 +5,12 @@ import { fileExists } from '@shopify/cli-kit/node/fs';
|
|
|
5
5
|
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
|
-
import { getProjectPaths, assertOxygenChecks, getRemixConfig } from '../../lib/remix-config.js';
|
|
8
|
+
import { getProjectPaths, assertOxygenChecks, handleRemixImportFail, getRemixConfig } from '../../lib/remix-config.js';
|
|
9
9
|
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,
|
|
@@ -98,7 +101,7 @@ async function runDev({
|
|
|
98
101
|
const [{ watch }, { createFileWatchCache }] = await Promise.all([
|
|
99
102
|
import('@remix-run/dev/dist/compiler/watch.js'),
|
|
100
103
|
import('@remix-run/dev/dist/compiler/fileWatchCache.js')
|
|
101
|
-
]);
|
|
104
|
+
]).catch(handleRemixImportFail);
|
|
102
105
|
let isInitialBuild = true;
|
|
103
106
|
let initialBuildDurationMs = 0;
|
|
104
107
|
let initialBuildStartTimeMs = Date.now();
|
|
@@ -107,22 +110,32 @@ 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`;
|
|
125
|
+
const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
|
|
119
126
|
enhanceH2Logs({ graphiqlUrl, ...remixConfig });
|
|
120
127
|
miniOxygen.showBanner({
|
|
121
128
|
appName: storefront ? colors.cyan(storefront?.title) : void 0,
|
|
122
129
|
headlinePrefix: initialBuildDurationMs > 0 ? `Initial build: ${initialBuildDurationMs}ms
|
|
123
130
|
` : "",
|
|
124
|
-
extraLines: [
|
|
125
|
-
|
|
131
|
+
extraLines: [
|
|
132
|
+
colors.dim(`
|
|
133
|
+
View GraphiQL API browser: ${graphiqlUrl}`),
|
|
134
|
+
workerRuntime ? "" : colors.dim(
|
|
135
|
+
`
|
|
136
|
+
View server-side network requests: ${debugNetworkUrl}`
|
|
137
|
+
)
|
|
138
|
+
].filter(Boolean)
|
|
126
139
|
});
|
|
127
140
|
if (useCodegen) {
|
|
128
141
|
spawnCodegenProcess({ ...remixConfig, configFilePath: codegenConfigPath });
|
|
@@ -177,7 +190,7 @@ View GraphiQL API browser: ${graphiqlUrl}`)]
|
|
|
177
190
|
if (!miniOxygen) {
|
|
178
191
|
await safeStartMiniOxygen();
|
|
179
192
|
} else if (liveReload) {
|
|
180
|
-
await miniOxygen.reload(
|
|
193
|
+
await miniOxygen.reload();
|
|
181
194
|
}
|
|
182
195
|
liveReload?.onAppReady(context);
|
|
183
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
|
});
|