@shopify/cli-hydrogen 6.1.0 → 7.0.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 +40 -78
- package/dist/commands/hydrogen/codegen.js +8 -3
- package/dist/commands/hydrogen/deploy.js +107 -35
- package/dist/commands/hydrogen/deploy.test.js +83 -13
- package/dist/commands/hydrogen/dev.js +30 -15
- package/dist/commands/hydrogen/init.js +1 -1
- package/dist/commands/hydrogen/init.test.js +155 -53
- package/dist/commands/hydrogen/link.js +5 -21
- package/dist/commands/hydrogen/link.test.js +10 -10
- package/dist/commands/hydrogen/preview.js +7 -6
- package/dist/commands/hydrogen/setup.js +0 -4
- package/dist/commands/hydrogen/setup.test.js +0 -1
- package/dist/commands/hydrogen/upgrade.js +15 -0
- package/dist/generator-templates/starter/.graphqlrc.yml +12 -1
- package/dist/generator-templates/starter/CHANGELOG.md +56 -0
- package/dist/generator-templates/starter/README.md +23 -0
- package/dist/generator-templates/starter/app/components/Cart.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Header.tsx +5 -1
- package/dist/generator-templates/starter/app/components/Layout.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Search.tsx +1 -1
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +39 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
- package/dist/generator-templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
- package/dist/generator-templates/starter/app/lib/fragments.ts +102 -0
- package/dist/generator-templates/starter/app/lib/session.ts +67 -0
- package/dist/generator-templates/starter/app/root.tsx +11 -45
- package/dist/generator-templates/starter/app/routes/account.$.tsx +8 -4
- package/dist/generator-templates/starter/app/routes/account._index.tsx +5 -0
- package/dist/generator-templates/starter/app/routes/account.addresses.tsx +215 -206
- package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +56 -163
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +32 -109
- package/dist/generator-templates/starter/app/routes/account.profile.tsx +40 -180
- package/dist/generator-templates/starter/app/routes/account.tsx +20 -135
- package/dist/generator-templates/starter/app/routes/account_.authorize.tsx +5 -0
- package/dist/generator-templates/starter/app/routes/account_.login.tsx +3 -140
- package/dist/generator-templates/starter/app/routes/account_.logout.tsx +5 -24
- package/dist/generator-templates/starter/app/routes/cart.tsx +7 -5
- package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/products.$handle.tsx +2 -2
- package/dist/generator-templates/starter/app/routes/search.tsx +1 -1
- package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +506 -0
- package/dist/generator-templates/starter/package.json +11 -11
- package/dist/generator-templates/starter/remix.config.js +4 -0
- package/dist/generator-templates/starter/remix.env.d.ts +4 -11
- package/dist/generator-templates/starter/server.ts +24 -167
- package/dist/generator-templates/starter/storefrontapi.generated.d.ts +104 -881
- package/dist/hooks/init.js +4 -4
- package/dist/lib/auth.js +5 -10
- package/dist/lib/build.js +6 -1
- package/dist/lib/bundle/analyzer.js +36 -26
- package/dist/lib/codegen.js +58 -18
- package/dist/lib/defer.js +12 -0
- package/dist/lib/file.js +52 -3
- package/dist/lib/flags.js +15 -8
- package/dist/lib/get-oxygen-deployment-data.test.js +4 -2
- package/dist/lib/graphql/admin/client.test.js +2 -2
- package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
- package/dist/lib/log.js +31 -14
- package/dist/lib/mini-oxygen/index.js +4 -5
- package/dist/lib/mini-oxygen/mini-oxygen.test.js +214 -0
- package/dist/lib/mini-oxygen/node.js +4 -2
- package/dist/lib/mini-oxygen/workerd-inspector-logs.js +2 -2
- package/dist/lib/mini-oxygen/workerd.js +27 -10
- package/dist/lib/missing-routes.js +6 -3
- package/dist/lib/onboarding/common.js +40 -9
- package/dist/lib/onboarding/local.js +19 -11
- package/dist/lib/onboarding/remote.js +48 -28
- package/dist/lib/request-events.js +65 -31
- package/dist/lib/setups/css/assets.js +1 -46
- package/dist/lib/setups/css/css-modules.js +3 -2
- package/dist/lib/setups/css/postcss.js +4 -2
- package/dist/lib/setups/css/tailwind.js +4 -2
- package/dist/lib/setups/css/vanilla-extract.js +3 -2
- package/dist/lib/setups/i18n/replacers.test.js +54 -38
- package/dist/lib/template-diff.js +89 -0
- package/dist/lib/template-downloader.js +3 -2
- package/dist/lib/transpile/project.js +1 -1
- package/dist/virtual-routes/assets/debug-network.css +592 -0
- package/dist/virtual-routes/assets/favicon-dark.svg +20 -0
- package/dist/virtual-routes/components/FlameChartWrapper.jsx +8 -10
- package/dist/virtual-routes/components/IconClose.jsx +38 -0
- package/dist/virtual-routes/components/IconDiscard.jsx +44 -0
- package/dist/virtual-routes/components/RequestDetails.jsx +179 -0
- package/dist/virtual-routes/components/RequestTable.jsx +92 -0
- package/dist/virtual-routes/components/RequestWaterfall.jsx +151 -0
- package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +176 -0
- package/dist/virtual-routes/routes/subrequest-profiler.jsx +243 -0
- package/oclif.manifest.json +54 -61
- package/package.json +14 -11
- package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +0 -161
- package/dist/generator-templates/starter/app/routes/account_.recover.tsx +0 -129
- package/dist/generator-templates/starter/app/routes/account_.register.tsx +0 -207
- package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +0 -136
- package/dist/virtual-routes/routes/debug-network.jsx +0 -289
- /package/dist/generator-templates/starter/app/{utils.ts → lib/variants.ts} +0 -0
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
|
-
import { outputInfo,
|
|
3
|
+
import { outputInfo, outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
|
|
4
4
|
import { rmdir, fileSize, glob, readFile, writeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
|
|
5
|
-
import { resolvePath,
|
|
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';
|
|
8
8
|
import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
|
|
9
|
-
import { commonFlags,
|
|
9
|
+
import { commonFlags, 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 {
|
|
15
|
-
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
14
|
+
import { buildBundleAnalysis, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
|
|
16
15
|
import { isCI } from '../../lib/is-ci.js';
|
|
16
|
+
import { prepareDiffDirectory, copyDiffBuild } from '../../lib/template-diff.js';
|
|
17
17
|
|
|
18
18
|
const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
|
|
19
|
-
const
|
|
19
|
+
const WORKER_BUILD_SIZE_LIMIT = 5;
|
|
20
20
|
class Build extends Command {
|
|
21
21
|
static description = "Builds a Hydrogen storefront for production.";
|
|
22
22
|
static flags = {
|
|
@@ -44,18 +44,24 @@ class Build extends Command {
|
|
|
44
44
|
}),
|
|
45
45
|
codegen: commonFlags.codegen,
|
|
46
46
|
"codegen-config-path": commonFlags.codegenConfigPath,
|
|
47
|
-
|
|
48
|
-
entry: deprecated("--entry")(),
|
|
49
|
-
target: deprecated("--target")()
|
|
47
|
+
diff: commonFlags.diff
|
|
50
48
|
};
|
|
51
49
|
async run() {
|
|
52
50
|
const { flags } = await this.parse(Build);
|
|
53
|
-
const
|
|
51
|
+
const originalDirectory = flags.path ? resolvePath(flags.path) : process.cwd();
|
|
52
|
+
let directory = originalDirectory;
|
|
53
|
+
if (flags.diff) {
|
|
54
|
+
directory = await prepareDiffDirectory(originalDirectory, false);
|
|
55
|
+
}
|
|
54
56
|
await runBuild({
|
|
55
57
|
...flagsToCamelObject(flags),
|
|
56
58
|
useCodegen: flags.codegen,
|
|
57
59
|
directory
|
|
58
60
|
});
|
|
61
|
+
if (flags.diff) {
|
|
62
|
+
await copyDiffBuild(directory, originalDirectory);
|
|
63
|
+
}
|
|
64
|
+
process.exit(0);
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
async function runBuild({
|
|
@@ -104,28 +110,37 @@ async function runBuild({
|
|
|
104
110
|
fileWatchCache: createFileWatchCache()
|
|
105
111
|
}).catch((thrown) => {
|
|
106
112
|
logThrown(thrown);
|
|
107
|
-
process.
|
|
113
|
+
if (process.env.SHOPIFY_UNIT_TEST) {
|
|
114
|
+
throw thrown;
|
|
115
|
+
} else {
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
108
118
|
}),
|
|
109
119
|
useCodegen && codegen({ ...remixConfig, configFilePath: codegenConfigPath })
|
|
110
120
|
]);
|
|
111
121
|
if (process.env.NODE_ENV !== "development") {
|
|
112
122
|
console.timeEnd(LOG_WORKER_BUILT);
|
|
123
|
+
const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
|
|
113
124
|
const sizeMB = await fileSize(buildPathWorkerFile) / (1024 * 1024);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
root,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
const formattedSize = colors.yellow(sizeMB.toFixed(2) + " MB");
|
|
126
|
+
outputInfo(
|
|
127
|
+
outputContent` ${colors.dim(
|
|
128
|
+
relativePath(root, buildPathWorkerFile)
|
|
129
|
+
)} ${bundleAnalysisPath ? outputToken.link(formattedSize, bundleAnalysisPath) : formattedSize}\n`
|
|
130
|
+
);
|
|
131
|
+
if (bundleStats && bundleAnalysisPath) {
|
|
132
|
+
outputInfo(
|
|
133
|
+
outputContent`${await getBundleAnalysisSummary(buildPathWorkerFile) || "\n"}\n │\n └─── ${outputToken.link(
|
|
134
|
+
"Complete analysis: " + bundleAnalysisPath,
|
|
135
|
+
bundleAnalysisPath
|
|
136
|
+
)}\n\n`
|
|
122
137
|
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
}
|
|
139
|
+
if (sizeMB >= WORKER_BUILD_SIZE_LIMIT) {
|
|
140
|
+
outputWarn(
|
|
141
|
+
`\u{1F6A8} Smaller worker bundles are faster to deploy and run.${remixConfig.serverMinify ? "" : "\n Minify your bundle by adding `serverMinify: true` to remix.config.js."}
|
|
142
|
+
Learn more about optimizing your worker bundle file: https://h2o.fyi/debugging/bundle-size
|
|
143
|
+
`
|
|
129
144
|
);
|
|
130
145
|
}
|
|
131
146
|
}
|
|
@@ -145,9 +160,6 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
|
|
|
145
160
|
if (process.env.NODE_ENV !== "development") {
|
|
146
161
|
await cleanClientSourcemaps(buildPathClient);
|
|
147
162
|
}
|
|
148
|
-
if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
|
|
149
|
-
process.exit(0);
|
|
150
|
-
}
|
|
151
163
|
}
|
|
152
164
|
async function cleanClientSourcemaps(buildPathClient) {
|
|
153
165
|
const bundleFiles = await glob(joinPath(buildPathClient, "**/*.js"));
|
|
@@ -161,56 +173,6 @@ async function cleanClientSourcemaps(buildPathClient) {
|
|
|
161
173
|
})
|
|
162
174
|
);
|
|
163
175
|
}
|
|
164
|
-
async function writeBundleAnalysis(buildPath, root, buildPathWorkerFile, sizeMB, bundleStats, remixConfig) {
|
|
165
|
-
const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
|
|
166
|
-
outputInfo(
|
|
167
|
-
outputContent` ${colors.dim(
|
|
168
|
-
relativePath(root, buildPathWorkerFile)
|
|
169
|
-
)} ${outputToken.link(
|
|
170
|
-
colors.yellow(sizeMB.toFixed(2) + " MB"),
|
|
171
|
-
bundleAnalysisPath
|
|
172
|
-
)}\n`
|
|
173
|
-
);
|
|
174
|
-
if (bundleStats && sizeMB < MAX_WORKER_BUNDLE_SIZE) {
|
|
175
|
-
outputInfo(
|
|
176
|
-
outputContent`${await getBundleAnalysisSummary(buildPathWorkerFile) || "\n"}\n │\n └─── ${outputToken.link(
|
|
177
|
-
"Complete analysis: " + bundleAnalysisPath,
|
|
178
|
-
bundleAnalysisPath
|
|
179
|
-
)}\n\n`
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
|
|
183
|
-
throw new AbortError(
|
|
184
|
-
"\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB.",
|
|
185
|
-
outputContent`See the bundle analysis for a breakdown of what is contributing to the bundle size:\n${outputToken.link(
|
|
186
|
-
bundleAnalysisPath,
|
|
187
|
-
bundleAnalysisPath
|
|
188
|
-
)}`
|
|
189
|
-
);
|
|
190
|
-
} else if (sizeMB >= 5) {
|
|
191
|
-
outputWarn(
|
|
192
|
-
`\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."}
|
|
193
|
-
`
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
async function writeSimpleBuildStatus(root, buildPathWorkerFile, sizeMB, remixConfig) {
|
|
198
|
-
outputInfo(
|
|
199
|
-
outputContent` ${colors.dim(
|
|
200
|
-
relativePath(root, buildPathWorkerFile)
|
|
201
|
-
)} ${colors.yellow(sizeMB.toFixed(2) + " MB")}\n`
|
|
202
|
-
);
|
|
203
|
-
if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
|
|
204
|
-
throw new AbortError(
|
|
205
|
-
"\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB."
|
|
206
|
-
);
|
|
207
|
-
} else if (sizeMB >= 5) {
|
|
208
|
-
outputWarn(
|
|
209
|
-
`\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."}
|
|
210
|
-
`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
176
|
async function copyPublicFiles(publicPath, buildPathClient) {
|
|
215
177
|
if (!await fileExists(publicPath)) {
|
|
216
178
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
3
|
import { renderSuccess } from '@shopify/cli-kit/node/ui';
|
|
4
|
+
import colors from '@shopify/cli-kit/node/colors';
|
|
4
5
|
import { Flags } from '@oclif/core';
|
|
5
6
|
import { getProjectPaths, getRemixConfig } from '../../lib/remix-config.js';
|
|
6
7
|
import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
|
|
@@ -24,8 +25,6 @@ class Codegen extends Command {
|
|
|
24
25
|
default: false
|
|
25
26
|
})
|
|
26
27
|
};
|
|
27
|
-
static aliases = ["codegen-unstable"];
|
|
28
|
-
static deprecateAliases = true;
|
|
29
28
|
async run() {
|
|
30
29
|
const { flags } = await this.parse(Codegen);
|
|
31
30
|
const directory = flags.path ? path.resolve(flags.path) : process.cwd();
|
|
@@ -53,7 +52,13 @@ async function runCodegen({
|
|
|
53
52
|
if (!watch) {
|
|
54
53
|
renderSuccess({
|
|
55
54
|
headline: "Generated types for GraphQL:",
|
|
56
|
-
body:
|
|
55
|
+
body: {
|
|
56
|
+
list: {
|
|
57
|
+
items: Object.entries(generatedFiles).map(
|
|
58
|
+
([key, value]) => key + "\n" + value.map((item) => colors.dim(`- ${item}`)).join("\n")
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
57
62
|
});
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -19,12 +19,17 @@ const deploymentLogger = (message, level = "info") => {
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
class Deploy extends Command {
|
|
22
|
+
static description = "Builds and deploys a Hydrogen storefront to Oxygen.";
|
|
22
23
|
static flags = {
|
|
23
24
|
"env-branch": Flags.string({
|
|
24
|
-
|
|
25
|
-
description: "Environment branch (tag) for environment to deploy to",
|
|
25
|
+
description: "Environment branch (tag) for environment to deploy to.",
|
|
26
26
|
required: false
|
|
27
27
|
}),
|
|
28
|
+
preview: Flags.boolean({
|
|
29
|
+
description: "Deploys to the Preview environment. Overrides --env-branch and Git metadata.",
|
|
30
|
+
required: false,
|
|
31
|
+
default: false
|
|
32
|
+
}),
|
|
28
33
|
force: Flags.boolean({
|
|
29
34
|
char: "f",
|
|
30
35
|
description: "Forces a deployment to proceed if there are uncommited changes in its Git repository.",
|
|
@@ -32,22 +37,21 @@ class Deploy extends Command {
|
|
|
32
37
|
env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
|
|
33
38
|
required: false
|
|
34
39
|
}),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"public-deployment": Flags.boolean({
|
|
38
|
-
env: "SHOPIFY_HYDROGEN_FLAG_PUBLIC_DEPLOYMENT",
|
|
39
|
-
description: "Marks a preview deployment as publicly accessible.",
|
|
40
|
+
"auth-bypass-token": Flags.boolean({
|
|
41
|
+
description: "Generate an authentication bypass token, which can be used to perform end-to-end tests against the deployment.",
|
|
40
42
|
required: false,
|
|
41
43
|
default: false
|
|
42
44
|
}),
|
|
45
|
+
path: commonFlags.path,
|
|
46
|
+
shop: commonFlags.shop,
|
|
43
47
|
"no-json-output": Flags.boolean({
|
|
44
|
-
description: "Prevents the command from creating a JSON file containing the deployment URL
|
|
48
|
+
description: "Prevents the command from creating a JSON file containing the deployment URL in CI environments.",
|
|
45
49
|
required: false,
|
|
46
50
|
default: false
|
|
47
51
|
}),
|
|
48
52
|
token: Flags.string({
|
|
49
53
|
char: "t",
|
|
50
|
-
description: "Oxygen deployment token",
|
|
54
|
+
description: "Oxygen deployment token. Defaults to the linked storefront's token if available.",
|
|
51
55
|
env: "SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN",
|
|
52
56
|
required: false
|
|
53
57
|
}),
|
|
@@ -57,9 +61,9 @@ class Deploy extends Command {
|
|
|
57
61
|
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_DESCRIPTION"
|
|
58
62
|
}),
|
|
59
63
|
"metadata-url": Flags.string({
|
|
60
|
-
description: "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
|
|
61
64
|
required: false,
|
|
62
|
-
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL"
|
|
65
|
+
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL",
|
|
66
|
+
hidden: true
|
|
63
67
|
}),
|
|
64
68
|
"metadata-user": Flags.string({
|
|
65
69
|
description: "User that initiated the deployment. Will be saved and displayed in the Shopify admin",
|
|
@@ -67,12 +71,11 @@ class Deploy extends Command {
|
|
|
67
71
|
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_USER"
|
|
68
72
|
}),
|
|
69
73
|
"metadata-version": Flags.string({
|
|
70
|
-
description: "A version identifier for the deployment. Will be saved and displayed in the Shopify admin",
|
|
71
74
|
required: false,
|
|
72
|
-
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION"
|
|
75
|
+
env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION",
|
|
76
|
+
hidden: true
|
|
73
77
|
})
|
|
74
78
|
};
|
|
75
|
-
static hidden = true;
|
|
76
79
|
async run() {
|
|
77
80
|
const { flags } = await this.parse(Deploy);
|
|
78
81
|
const deploymentOptions = this.flagsToOxygenDeploymentOptions(flags);
|
|
@@ -87,19 +90,39 @@ class Deploy extends Command {
|
|
|
87
90
|
const camelFlags = flagsToCamelObject(flags);
|
|
88
91
|
return {
|
|
89
92
|
...camelFlags,
|
|
93
|
+
defaultEnvironment: flags.preview,
|
|
90
94
|
environmentTag: flags["env-branch"],
|
|
91
95
|
path: flags.path ? resolvePath(flags.path) : process.cwd()
|
|
92
96
|
};
|
|
93
97
|
}
|
|
94
98
|
}
|
|
99
|
+
function createUnexpectedAbortError(message) {
|
|
100
|
+
return new AbortError(
|
|
101
|
+
message || "The deployment failed due to an unexpected error.",
|
|
102
|
+
"Retrying the deployement may succeed.",
|
|
103
|
+
[
|
|
104
|
+
[
|
|
105
|
+
"If the issue persits, please check the",
|
|
106
|
+
{
|
|
107
|
+
link: {
|
|
108
|
+
label: "Shopify status page",
|
|
109
|
+
url: "https://status.shopify.com/"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"for any known issues."
|
|
113
|
+
]
|
|
114
|
+
]
|
|
115
|
+
);
|
|
116
|
+
}
|
|
95
117
|
async function oxygenDeploy(options) {
|
|
96
118
|
const {
|
|
119
|
+
authBypassToken: generateAuthBypassToken,
|
|
120
|
+
defaultEnvironment,
|
|
97
121
|
environmentTag,
|
|
98
122
|
force: forceOnUncommitedChanges,
|
|
99
123
|
noJsonOutput,
|
|
100
124
|
path,
|
|
101
125
|
shop,
|
|
102
|
-
publicDeployment,
|
|
103
126
|
metadataUrl,
|
|
104
127
|
metadataUser,
|
|
105
128
|
metadataVersion
|
|
@@ -168,12 +191,17 @@ async function oxygenDeploy(options) {
|
|
|
168
191
|
] : `Could not obtain an Oxygen deployment token, please try again or contact Shopify support.`;
|
|
169
192
|
throw new AbortError(errMessage);
|
|
170
193
|
}
|
|
171
|
-
if (!isCI && !environmentTag && deploymentData?.environments) {
|
|
194
|
+
if (!isCI && !defaultEnvironment && !environmentTag && deploymentData?.environments) {
|
|
172
195
|
if (deploymentData.environments.length > 1) {
|
|
173
196
|
const choices = [
|
|
174
|
-
...deploymentData.environments.map(({ name, branch: branch2 }) => ({
|
|
197
|
+
...deploymentData.environments.map(({ name, branch: branch2, type }) => ({
|
|
175
198
|
label: name,
|
|
176
|
-
|
|
199
|
+
// The preview environment will never have an associated branch so
|
|
200
|
+
// we're using a custom string here to identify it later in our code.
|
|
201
|
+
// Using a period at the end of the value is an invalid branch name
|
|
202
|
+
// in Git so we can be sure that this won't conflict with a merchant's
|
|
203
|
+
// repository.
|
|
204
|
+
value: type === "PREVIEW" ? "shopify-preview-environment." : branch2
|
|
177
205
|
}))
|
|
178
206
|
];
|
|
179
207
|
deploymentEnvironmentTag = await renderSelectPrompt({
|
|
@@ -194,12 +222,21 @@ async function oxygenDeploy(options) {
|
|
|
194
222
|
"Using a custom deployment service. Don't do this in production!"
|
|
195
223
|
);
|
|
196
224
|
}
|
|
225
|
+
let fallbackEnvironmentTag = branch;
|
|
226
|
+
let isPreview = false;
|
|
227
|
+
if (deploymentEnvironmentTag === "shopify-preview-environment.") {
|
|
228
|
+
fallbackEnvironmentTag = void 0;
|
|
229
|
+
deploymentEnvironmentTag = void 0;
|
|
230
|
+
isPreview = true;
|
|
231
|
+
}
|
|
197
232
|
const config = {
|
|
198
233
|
assetsDir: "dist/client",
|
|
199
234
|
bugsnag: true,
|
|
200
235
|
deploymentUrl,
|
|
236
|
+
defaultEnvironment: defaultEnvironment || isPreview,
|
|
201
237
|
deploymentToken: parseToken(token),
|
|
202
|
-
environmentTag: environmentTag || deploymentEnvironmentTag ||
|
|
238
|
+
environmentTag: environmentTag || deploymentEnvironmentTag || fallbackEnvironmentTag,
|
|
239
|
+
generateAuthBypassToken,
|
|
203
240
|
verificationMaxDuration: 180,
|
|
204
241
|
metadata: {
|
|
205
242
|
...metadataDescription ? { description: metadataDescription } : {},
|
|
@@ -207,7 +244,6 @@ async function oxygenDeploy(options) {
|
|
|
207
244
|
...metadataUser ? { user: metadataUser } : {},
|
|
208
245
|
...metadataVersion ? { version: metadataVersion } : {}
|
|
209
246
|
},
|
|
210
|
-
publicDeployment,
|
|
211
247
|
skipVerification: false,
|
|
212
248
|
rootPath: path,
|
|
213
249
|
skipBuild: false,
|
|
@@ -218,10 +254,16 @@ async function oxygenDeploy(options) {
|
|
|
218
254
|
const uploadPromise = new Promise((resolve) => {
|
|
219
255
|
resolveUpload = resolve;
|
|
220
256
|
});
|
|
221
|
-
let
|
|
222
|
-
const
|
|
223
|
-
|
|
257
|
+
let resolveRoutableCheck;
|
|
258
|
+
const routableCheckPromise = new Promise((resolve) => {
|
|
259
|
+
resolveRoutableCheck = resolve;
|
|
224
260
|
});
|
|
261
|
+
let resolveDeploymentCompletedVerification;
|
|
262
|
+
const deploymentCompletedVerificationPromise = new Promise(
|
|
263
|
+
(resolve) => {
|
|
264
|
+
resolveDeploymentCompletedVerification = resolve;
|
|
265
|
+
}
|
|
266
|
+
);
|
|
225
267
|
let deployError = null;
|
|
226
268
|
let resolveDeploy;
|
|
227
269
|
let rejectDeploy;
|
|
@@ -237,11 +279,21 @@ async function oxygenDeploy(options) {
|
|
|
237
279
|
await runBuild({
|
|
238
280
|
directory: path,
|
|
239
281
|
assetPath,
|
|
240
|
-
sourcemap:
|
|
282
|
+
sourcemap: true,
|
|
241
283
|
useCodegen: false
|
|
242
284
|
});
|
|
243
285
|
},
|
|
244
|
-
|
|
286
|
+
onDeploymentCompleted: () => resolveDeploymentCompletedVerification(),
|
|
287
|
+
onVerificationComplete: () => resolveRoutableCheck(),
|
|
288
|
+
onDeploymentCompletedVerificationError() {
|
|
289
|
+
deployError = new AbortError(
|
|
290
|
+
"Unable to verify the deployment was completed successfully",
|
|
291
|
+
"Please verify the deployment status in the Shopify Admin and retry deploying if necessary."
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
onDeploymentFailed: (details) => {
|
|
295
|
+
deployError = createUnexpectedAbortError(details.error || details.status);
|
|
296
|
+
},
|
|
245
297
|
onUploadFilesStart: () => uploadStart(),
|
|
246
298
|
onUploadFilesComplete: () => resolveUpload(),
|
|
247
299
|
onVerificationError: (error) => {
|
|
@@ -267,23 +319,43 @@ async function oxygenDeploy(options) {
|
|
|
267
319
|
task: async () => await uploadPromise
|
|
268
320
|
},
|
|
269
321
|
{
|
|
270
|
-
title: "Verifying deployment",
|
|
271
|
-
task: async () => await
|
|
322
|
+
title: "Verifying deployment has been completed",
|
|
323
|
+
task: async () => await deploymentCompletedVerificationPromise
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
title: "Verifying deployment is routable",
|
|
327
|
+
task: async () => await routableCheckPromise
|
|
272
328
|
}
|
|
273
329
|
]);
|
|
274
330
|
};
|
|
275
|
-
await createDeploy({ config, hooks, logger: deploymentLogger }).then(async (
|
|
276
|
-
|
|
331
|
+
await createDeploy({ config, hooks, logger: deploymentLogger }).then(async (completedDeployment) => {
|
|
332
|
+
if (!completedDeployment) {
|
|
333
|
+
rejectDeploy(createUnexpectedAbortError());
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const nextSteps = [
|
|
337
|
+
[
|
|
338
|
+
"Open",
|
|
339
|
+
{ link: { url: completedDeployment.url } },
|
|
340
|
+
`in your browser to view your deployment.`
|
|
341
|
+
]
|
|
342
|
+
];
|
|
343
|
+
if (completedDeployment?.authBypassToken) {
|
|
344
|
+
nextSteps.push([
|
|
345
|
+
"Use the",
|
|
346
|
+
{ subdued: completedDeployment.authBypassToken },
|
|
347
|
+
"token to perform end-to-end tests against the deployment."
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
277
350
|
renderSuccess({
|
|
278
351
|
body: ["Successfully deployed to Oxygen"],
|
|
279
|
-
nextSteps
|
|
280
|
-
[
|
|
281
|
-
`Open ${url} in your browser to view your ${deploymentType} deployment`
|
|
282
|
-
]
|
|
283
|
-
]
|
|
352
|
+
nextSteps
|
|
284
353
|
});
|
|
285
354
|
if (isCI && !noJsonOutput) {
|
|
286
|
-
await writeFile(
|
|
355
|
+
await writeFile(
|
|
356
|
+
"h2_deploy_log.json",
|
|
357
|
+
JSON.stringify(completedDeployment)
|
|
358
|
+
);
|
|
287
359
|
}
|
|
288
360
|
resolveDeploy();
|
|
289
361
|
}).catch((error) => {
|
|
@@ -66,11 +66,12 @@ describe("deploy", () => {
|
|
|
66
66
|
};
|
|
67
67
|
const originalExit = process.exit;
|
|
68
68
|
const deployParams = {
|
|
69
|
+
authBypassToken: true,
|
|
70
|
+
defaultEnvironment: false,
|
|
69
71
|
force: false,
|
|
70
72
|
noJsonOutput: false,
|
|
71
73
|
path: "./",
|
|
72
74
|
shop: "snowdevil.myshopify.com",
|
|
73
|
-
publicDeployment: false,
|
|
74
75
|
metadataUrl: "https://example.com",
|
|
75
76
|
metadataUser: "user",
|
|
76
77
|
metadataVersion: "1.0.0"
|
|
@@ -87,15 +88,16 @@ describe("deploy", () => {
|
|
|
87
88
|
const expectedConfig = {
|
|
88
89
|
assetsDir: "dist/client",
|
|
89
90
|
bugsnag: true,
|
|
91
|
+
defaultEnvironment: false,
|
|
90
92
|
deploymentUrl: "https://oxygen.shopifyapps.com",
|
|
91
93
|
deploymentToken: mockToken,
|
|
94
|
+
generateAuthBypassToken: true,
|
|
92
95
|
verificationMaxDuration: 180,
|
|
93
96
|
metadata: {
|
|
94
97
|
url: deployParams.metadataUrl,
|
|
95
98
|
user: deployParams.metadataUser,
|
|
96
99
|
version: deployParams.metadataVersion
|
|
97
100
|
},
|
|
98
|
-
publicDeployment: deployParams.publicDeployment,
|
|
99
101
|
skipVerification: false,
|
|
100
102
|
rootPath: deployParams.path,
|
|
101
103
|
skipBuild: false,
|
|
@@ -104,10 +106,13 @@ describe("deploy", () => {
|
|
|
104
106
|
};
|
|
105
107
|
const expectedHooks = {
|
|
106
108
|
buildFunction: expect.any(Function),
|
|
109
|
+
onDeploymentCompleted: expect.any(Function),
|
|
110
|
+
onDeploymentFailed: expect.any(Function),
|
|
111
|
+
onDeploymentCompletedVerificationError: expect.any(Function),
|
|
107
112
|
onVerificationComplete: expect.any(Function),
|
|
113
|
+
onVerificationError: expect.any(Function),
|
|
108
114
|
onUploadFilesStart: expect.any(Function),
|
|
109
115
|
onUploadFilesComplete: expect.any(Function),
|
|
110
|
-
onVerificationError: expect.any(Function),
|
|
111
116
|
onUploadFilesError: expect.any(Function)
|
|
112
117
|
};
|
|
113
118
|
beforeEach(async () => {
|
|
@@ -125,9 +130,10 @@ describe("deploy", () => {
|
|
|
125
130
|
}
|
|
126
131
|
]);
|
|
127
132
|
vi.mocked(renderSelectPrompt).mockResolvedValue(FULL_SHOPIFY_CONFIG.shop);
|
|
128
|
-
vi.mocked(createDeploy).mockResolvedValue(
|
|
129
|
-
"
|
|
130
|
-
|
|
133
|
+
vi.mocked(createDeploy).mockResolvedValue({
|
|
134
|
+
authBypassToken: "some-token",
|
|
135
|
+
url: "https://a-lovely-deployment.com"
|
|
136
|
+
});
|
|
131
137
|
vi.mocked(getOxygenDeploymentData).mockResolvedValue({
|
|
132
138
|
oxygenDeploymentToken: "some-encoded-token",
|
|
133
139
|
environments: []
|
|
@@ -252,19 +258,52 @@ describe("deploy", () => {
|
|
|
252
258
|
vi.mocked(getOxygenDeploymentData).mockResolvedValue({
|
|
253
259
|
oxygenDeploymentToken: "some-encoded-token",
|
|
254
260
|
environments: [
|
|
255
|
-
{ name: "
|
|
256
|
-
{ name: "
|
|
261
|
+
{ name: "Production", branch: "main", type: "PRODUCTION" },
|
|
262
|
+
{ name: "Preview", branch: null, type: "PREVIEW" }
|
|
257
263
|
]
|
|
258
264
|
});
|
|
259
265
|
await oxygenDeploy(deployParams);
|
|
260
266
|
expect(vi.mocked(renderSelectPrompt)).toHaveBeenCalledWith({
|
|
261
267
|
message: "Select an environment to deploy to",
|
|
262
268
|
choices: [
|
|
263
|
-
{ label: "
|
|
264
|
-
{ label: "
|
|
269
|
+
{ label: "Production", value: "main" },
|
|
270
|
+
{ label: "Preview", value: "shopify-preview-environment." }
|
|
265
271
|
]
|
|
266
272
|
});
|
|
267
273
|
});
|
|
274
|
+
describe("when Preview is selected", () => {
|
|
275
|
+
it("calls createDeploy with defaultEnvironment and an undefined environmentTag", async () => {
|
|
276
|
+
vi.mocked(getLatestGitCommit).mockResolvedValue({
|
|
277
|
+
hash: "123",
|
|
278
|
+
message: "test commit",
|
|
279
|
+
date: "2021-01-01",
|
|
280
|
+
author_name: "test author",
|
|
281
|
+
author_email: "test@author.com",
|
|
282
|
+
body: "test body",
|
|
283
|
+
refs: "HEAD -> main"
|
|
284
|
+
});
|
|
285
|
+
vi.mocked(getOxygenDeploymentData).mockResolvedValue({
|
|
286
|
+
oxygenDeploymentToken: "some-encoded-token",
|
|
287
|
+
environments: [
|
|
288
|
+
{ name: "Production", branch: "main", type: "PRODUCTION" },
|
|
289
|
+
{ name: "Preview", branch: null, type: "PREVIEW" }
|
|
290
|
+
]
|
|
291
|
+
});
|
|
292
|
+
vi.mocked(renderSelectPrompt).mockResolvedValue(
|
|
293
|
+
"shopify-preview-environment."
|
|
294
|
+
);
|
|
295
|
+
await oxygenDeploy(deployParams);
|
|
296
|
+
expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
|
|
297
|
+
config: {
|
|
298
|
+
...expectedConfig,
|
|
299
|
+
defaultEnvironment: true,
|
|
300
|
+
environmentTag: void 0
|
|
301
|
+
},
|
|
302
|
+
hooks: expectedHooks,
|
|
303
|
+
logger: deploymentLogger
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
});
|
|
268
307
|
it("writes a file with JSON content in CI environments", async () => {
|
|
269
308
|
vi.mocked(ciPlatform).mockReturnValue({
|
|
270
309
|
isCI: true,
|
|
@@ -274,12 +313,16 @@ describe("deploy", () => {
|
|
|
274
313
|
const ciDeployParams = {
|
|
275
314
|
...deployParams,
|
|
276
315
|
token: "some-token",
|
|
277
|
-
metadataDescription: "cool new stuff"
|
|
316
|
+
metadataDescription: "cool new stuff",
|
|
317
|
+
generateAuthBypassToken: true
|
|
278
318
|
};
|
|
279
319
|
await oxygenDeploy(ciDeployParams);
|
|
280
320
|
expect(vi.mocked(writeFile)).toHaveBeenCalledWith(
|
|
281
321
|
"h2_deploy_log.json",
|
|
282
|
-
JSON.stringify({
|
|
322
|
+
JSON.stringify({
|
|
323
|
+
authBypassToken: "some-token",
|
|
324
|
+
url: "https://a-lovely-deployment.com"
|
|
325
|
+
})
|
|
283
326
|
);
|
|
284
327
|
vi.mocked(writeFile).mockClear();
|
|
285
328
|
ciDeployParams.noJsonOutput = true;
|
|
@@ -311,7 +354,7 @@ describe("deploy", () => {
|
|
|
311
354
|
}
|
|
312
355
|
}
|
|
313
356
|
});
|
|
314
|
-
it("handles error during deployment verification", async () => {
|
|
357
|
+
it("handles error during deployment routability verification", async () => {
|
|
315
358
|
const mockRenderFatalError = vi.fn();
|
|
316
359
|
vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
|
|
317
360
|
const error = new Error("Cloudflare is down!");
|
|
@@ -337,4 +380,31 @@ describe("deploy", () => {
|
|
|
337
380
|
}
|
|
338
381
|
}
|
|
339
382
|
});
|
|
383
|
+
it("handles error during deployment completion verification", async () => {
|
|
384
|
+
const mockRenderFatalError = vi.fn();
|
|
385
|
+
vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
|
|
386
|
+
vi.mocked(createDeploy).mockImplementation((options) => {
|
|
387
|
+
options.hooks?.onUploadFilesStart?.();
|
|
388
|
+
options.hooks?.onUploadFilesComplete?.();
|
|
389
|
+
options.hooks?.onDeploymentCompletedVerificationStart?.();
|
|
390
|
+
options.hooks?.onDeploymentFailed?.({
|
|
391
|
+
status: "oh shit",
|
|
392
|
+
url: "https://a-lovely-deployment.com"
|
|
393
|
+
});
|
|
394
|
+
return new Promise((_resolve, reject) => {
|
|
395
|
+
reject();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
try {
|
|
399
|
+
await oxygenDeploy(deployParams);
|
|
400
|
+
expect(true).toBe(false);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (err instanceof AbortError) {
|
|
403
|
+
expect(err.message).toBe("oh shit");
|
|
404
|
+
expect(err.tryMessage).toBe("Retrying the deployement may succeed.");
|
|
405
|
+
} else {
|
|
406
|
+
expect(true).toBe(false);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
340
410
|
});
|