@shopify/cli-hydrogen 5.3.0 → 5.4.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 +98 -36
- package/dist/commands/hydrogen/dev.js +8 -4
- package/dist/generator-templates/starter/app/components/Aside.tsx +2 -2
- package/dist/generator-templates/starter/app/components/Search.tsx +3 -5
- package/dist/generator-templates/starter/app/root.tsx +4 -5
- package/dist/generator-templates/starter/app/routes/_index.tsx +2 -1
- package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +0 -2
- package/dist/generator-templates/starter/app/routes/cart.$lines.tsx +4 -5
- package/dist/generator-templates/starter/app/routes/collections._index.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/discount.$code.tsx +3 -4
- package/dist/generator-templates/starter/package.json +3 -3
- package/dist/lib/bundle/analyzer.js +9 -3
- package/dist/lib/check-lockfile.js +52 -47
- package/dist/lib/check-lockfile.test.js +16 -0
- package/dist/lib/graphiql-url.js +15 -0
- package/dist/lib/is-ci.js +6 -0
- package/dist/lib/log.js +6 -5
- package/dist/lib/log.test.js +7 -7
- package/dist/lib/mini-oxygen/node.js +6 -9
- package/dist/lib/request-events.js +28 -12
- package/oclif.manifest.json +7 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
|
-
import { outputInfo, outputContent, outputToken
|
|
4
|
-
import { rmdir, fileSize, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
|
|
5
|
-
import { resolvePath, relativePath } from '@shopify/cli-kit/node/path';
|
|
3
|
+
import { outputInfo, outputWarn, outputContent, outputToken } from '@shopify/cli-kit/node/output';
|
|
4
|
+
import { rmdir, fileSize, glob, readFile, writeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
|
|
5
|
+
import { resolvePath, joinPath, 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,9 @@ 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';
|
|
14
|
+
import { hasMetafile, buildBundleAnalysis, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
|
|
15
15
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
16
|
+
import { isCI } from '../../lib/is-ci.js';
|
|
16
17
|
|
|
17
18
|
const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
|
|
18
19
|
const MAX_WORKER_BUNDLE_SIZE = 10;
|
|
@@ -26,21 +27,27 @@ class Build extends Command {
|
|
|
26
27
|
allowNo: true,
|
|
27
28
|
default: true
|
|
28
29
|
}),
|
|
29
|
-
|
|
30
|
+
"bundle-stats": Flags.boolean({
|
|
30
31
|
description: "Show a bundle size summary after building.",
|
|
31
32
|
default: true,
|
|
32
33
|
allowNo: true
|
|
33
34
|
}),
|
|
35
|
+
"lockfile-check": Flags.boolean({
|
|
36
|
+
description: "Checks that there is exactly 1 valid lockfile in the project.",
|
|
37
|
+
env: "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK",
|
|
38
|
+
default: true,
|
|
39
|
+
allowNo: true
|
|
40
|
+
}),
|
|
34
41
|
"disable-route-warning": Flags.boolean({
|
|
35
42
|
description: "Disable warning about missing standard routes.",
|
|
36
43
|
env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_ROUTE_WARNING"
|
|
37
44
|
}),
|
|
38
|
-
|
|
45
|
+
"codegen-unstable": Flags.boolean({
|
|
39
46
|
description: "Generate types for the Storefront API queries found in your project.",
|
|
40
47
|
required: false,
|
|
41
48
|
default: false
|
|
42
49
|
}),
|
|
43
|
-
|
|
50
|
+
"codegen-config-path": commonFlags.codegenConfigPath,
|
|
44
51
|
base: deprecated("--base")(),
|
|
45
52
|
entry: deprecated("--entry")(),
|
|
46
53
|
target: deprecated("--target")()
|
|
@@ -62,6 +69,7 @@ async function runBuild({
|
|
|
62
69
|
sourcemap = false,
|
|
63
70
|
disableRouteWarning = false,
|
|
64
71
|
bundleStats = true,
|
|
72
|
+
lockfileCheck = true,
|
|
65
73
|
assetPath
|
|
66
74
|
}) {
|
|
67
75
|
if (!process.env.NODE_ENV) {
|
|
@@ -71,7 +79,10 @@ async function runBuild({
|
|
|
71
79
|
process.env.HYDROGEN_ASSET_BASE_URL = assetPath;
|
|
72
80
|
}
|
|
73
81
|
const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
|
|
74
|
-
|
|
82
|
+
if (lockfileCheck) {
|
|
83
|
+
await checkLockfileStatus(root, isCI());
|
|
84
|
+
}
|
|
85
|
+
await muteRemixLogs();
|
|
75
86
|
console.time(LOG_WORKER_BUILT);
|
|
76
87
|
outputInfo(`
|
|
77
88
|
\u{1F3D7}\uFE0F Building in ${process.env.NODE_ENV} mode...`);
|
|
@@ -104,35 +115,21 @@ async function runBuild({
|
|
|
104
115
|
if (process.env.NODE_ENV !== "development") {
|
|
105
116
|
console.timeEnd(LOG_WORKER_BUILT);
|
|
106
117
|
const sizeMB = await fileSize(buildPathWorkerFile) / (1024 * 1024);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
);
|
|
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`
|
|
118
|
+
if (await hasMetafile(buildPath)) {
|
|
119
|
+
await writeBundleAnalysis(
|
|
120
|
+
buildPath,
|
|
121
|
+
root,
|
|
122
|
+
buildPathWorkerFile,
|
|
123
|
+
sizeMB,
|
|
124
|
+
bundleStats,
|
|
125
|
+
remixConfig
|
|
122
126
|
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
bundleAnalysisPath
|
|
130
|
-
)}`
|
|
131
|
-
);
|
|
132
|
-
} else if (sizeMB >= 5) {
|
|
133
|
-
outputWarn(
|
|
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."}
|
|
135
|
-
`
|
|
127
|
+
} else {
|
|
128
|
+
await writeSimpleBuildStatus(
|
|
129
|
+
root,
|
|
130
|
+
buildPathWorkerFile,
|
|
131
|
+
sizeMB,
|
|
132
|
+
remixConfig
|
|
136
133
|
);
|
|
137
134
|
}
|
|
138
135
|
}
|
|
@@ -149,10 +146,75 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
|
|
|
149
146
|
);
|
|
150
147
|
}
|
|
151
148
|
}
|
|
149
|
+
if (process.env.NODE_ENV !== "development") {
|
|
150
|
+
await cleanClientSourcemaps(buildPathClient);
|
|
151
|
+
}
|
|
152
152
|
if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
|
|
153
153
|
process.exit(0);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
async function cleanClientSourcemaps(buildPathClient) {
|
|
157
|
+
const bundleFiles = await glob(joinPath(buildPathClient, "**/*.js"));
|
|
158
|
+
await Promise.all(
|
|
159
|
+
bundleFiles.map(async (filePath) => {
|
|
160
|
+
const file = await readFile(filePath);
|
|
161
|
+
return await writeFile(
|
|
162
|
+
filePath,
|
|
163
|
+
file.replace(/\/\/# sourceMappingURL=.+\.js\.map$/gm, "")
|
|
164
|
+
);
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
async function writeBundleAnalysis(buildPath, root, buildPathWorkerFile, sizeMB, bundleStats, remixConfig) {
|
|
169
|
+
const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
|
|
170
|
+
outputInfo(
|
|
171
|
+
outputContent` ${colors.dim(
|
|
172
|
+
relativePath(root, buildPathWorkerFile)
|
|
173
|
+
)} ${outputToken.link(
|
|
174
|
+
colors.yellow(sizeMB.toFixed(2) + " MB"),
|
|
175
|
+
bundleAnalysisPath
|
|
176
|
+
)}\n`
|
|
177
|
+
);
|
|
178
|
+
if (bundleStats && sizeMB < MAX_WORKER_BUNDLE_SIZE) {
|
|
179
|
+
outputInfo(
|
|
180
|
+
outputContent`${await getBundleAnalysisSummary(buildPathWorkerFile) || "\n"}\n │\n └─── ${outputToken.link(
|
|
181
|
+
"Complete analysis: " + bundleAnalysisPath,
|
|
182
|
+
bundleAnalysisPath
|
|
183
|
+
)}\n\n`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
|
|
187
|
+
throw new AbortError(
|
|
188
|
+
"\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB.",
|
|
189
|
+
outputContent`See the bundle analysis for a breakdown of what is contributing to the bundle size:\n${outputToken.link(
|
|
190
|
+
bundleAnalysisPath,
|
|
191
|
+
bundleAnalysisPath
|
|
192
|
+
)}`
|
|
193
|
+
);
|
|
194
|
+
} else if (sizeMB >= 5) {
|
|
195
|
+
outputWarn(
|
|
196
|
+
`\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."}
|
|
197
|
+
`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function writeSimpleBuildStatus(root, buildPathWorkerFile, sizeMB, remixConfig) {
|
|
202
|
+
outputInfo(
|
|
203
|
+
outputContent` ${colors.dim(
|
|
204
|
+
relativePath(root, buildPathWorkerFile)
|
|
205
|
+
)} ${colors.yellow(sizeMB.toFixed(2) + " MB")}\n`
|
|
206
|
+
);
|
|
207
|
+
if (sizeMB >= MAX_WORKER_BUNDLE_SIZE) {
|
|
208
|
+
throw new AbortError(
|
|
209
|
+
"\u{1F6A8} Worker bundle exceeds 10 MB! Oxygen has a maximum worker bundle size of 10 MB."
|
|
210
|
+
);
|
|
211
|
+
} else if (sizeMB >= 5) {
|
|
212
|
+
outputWarn(
|
|
213
|
+
`\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."}
|
|
214
|
+
`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
156
218
|
async function copyPublicFiles(publicPath, buildPathClient) {
|
|
157
219
|
if (!await fileExists(publicPath)) {
|
|
158
220
|
return;
|
|
@@ -18,6 +18,7 @@ import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
|
|
|
18
18
|
import { getConfig } from '../../lib/shopify-config.js';
|
|
19
19
|
import { setupLiveReload } from '../../lib/live-reload.js';
|
|
20
20
|
import { checkRemixVersions } from '../../lib/remix-version-check.js';
|
|
21
|
+
import { getGraphiQLUrl } from '../../lib/graphiql-url.js';
|
|
21
22
|
|
|
22
23
|
const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
|
|
23
24
|
const LOG_REBUILT = "\u{1F680} Rebuilt";
|
|
@@ -121,16 +122,19 @@ async function runDev({
|
|
|
121
122
|
},
|
|
122
123
|
workerRuntime
|
|
123
124
|
);
|
|
124
|
-
const graphiqlUrl = `${miniOxygen.listeningAt}/graphiql`;
|
|
125
125
|
const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
|
|
126
|
-
enhanceH2Logs({
|
|
126
|
+
enhanceH2Logs({ host: miniOxygen.listeningAt, ...remixConfig });
|
|
127
127
|
miniOxygen.showBanner({
|
|
128
128
|
appName: storefront ? colors.cyan(storefront?.title) : void 0,
|
|
129
129
|
headlinePrefix: initialBuildDurationMs > 0 ? `Initial build: ${initialBuildDurationMs}ms
|
|
130
130
|
` : "",
|
|
131
131
|
extraLines: [
|
|
132
|
-
colors.dim(
|
|
133
|
-
|
|
132
|
+
colors.dim(
|
|
133
|
+
`
|
|
134
|
+
View GraphiQL API browser: ${getGraphiQLUrl({
|
|
135
|
+
host: miniOxygen.listeningAt
|
|
136
|
+
})}`
|
|
137
|
+
),
|
|
134
138
|
workerRuntime ? "" : colors.dim(
|
|
135
139
|
`
|
|
136
140
|
View server-side network requests: ${debugNetworkUrl}`
|
|
@@ -450,13 +450,11 @@ function usePredictiveSearch(): UseSearchReturn {
|
|
|
450
450
|
|
|
451
451
|
/**
|
|
452
452
|
* Converts a plural search type to a singular search type
|
|
453
|
-
* @param type - The plural search type
|
|
454
|
-
* @returns The singular search type
|
|
455
453
|
*
|
|
456
454
|
* @example
|
|
457
|
-
* ```
|
|
458
|
-
* pluralToSingularSearchType('articles') // => 'ARTICLE'
|
|
459
|
-
* pluralToSingularSearchType(['articles', 'products']) // => 'ARTICLE,PRODUCT'
|
|
455
|
+
* ```js
|
|
456
|
+
* pluralToSingularSearchType('articles'); // => 'ARTICLE'
|
|
457
|
+
* pluralToSingularSearchType(['articles', 'products']); // => 'ARTICLE,PRODUCT'
|
|
460
458
|
* ```
|
|
461
459
|
*/
|
|
462
460
|
function pluralToSingularSearchType(
|
|
@@ -189,14 +189,13 @@ export function CatchBoundary() {
|
|
|
189
189
|
* @see https://shopify.dev/docs/api/storefront/latest/objects/CustomerAccessToken
|
|
190
190
|
*
|
|
191
191
|
* @example
|
|
192
|
-
* ```
|
|
193
|
-
* //
|
|
192
|
+
* ```js
|
|
194
193
|
* const {isLoggedIn, headers} = await validateCustomerAccessToken(
|
|
195
194
|
* customerAccessToken,
|
|
196
195
|
* session,
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
|
|
196
|
+
* );
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
200
199
|
async function validateCustomerAccessToken(
|
|
201
200
|
session: HydrogenSession,
|
|
202
201
|
customerAccessToken?: CustomerAccessToken,
|
|
@@ -108,8 +108,6 @@ async function fetchPredictiveSearchResults({
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Normalize results and apply tracking qurery parameters to each result url
|
|
111
|
-
* @param predictiveSearch
|
|
112
|
-
* @param locale
|
|
113
111
|
*/
|
|
114
112
|
export function normalizePredictiveSearchResults(
|
|
115
113
|
predictiveSearch: PredictiveSearchQuery['predictiveSearch'],
|
|
@@ -3,21 +3,20 @@ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
|
|
|
3
3
|
/**
|
|
4
4
|
* Automatically creates a new cart based on the URL and redirects straight to checkout.
|
|
5
5
|
* Expected URL structure:
|
|
6
|
-
* ```
|
|
6
|
+
* ```js
|
|
7
7
|
* /cart/<variant_id>:<quantity>
|
|
8
8
|
*
|
|
9
9
|
* ```
|
|
10
|
+
*
|
|
10
11
|
* More than one `<variant_id>:<quantity>` separated by a comma, can be supplied in the URL, for
|
|
11
12
|
* carts with more than one product variant.
|
|
12
13
|
*
|
|
13
|
-
* @param `?discount` an optional discount code to apply to the cart
|
|
14
14
|
* @example
|
|
15
|
-
* Example path creating a cart with two product variants, different quantities, and a discount code:
|
|
16
|
-
* ```
|
|
15
|
+
* Example path creating a cart with two product variants, different quantities, and a discount code in the querystring:
|
|
16
|
+
* ```js
|
|
17
17
|
* /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD
|
|
18
18
|
*
|
|
19
19
|
* ```
|
|
20
|
-
* @preserve
|
|
21
20
|
*/
|
|
22
21
|
export async function loader({request, context, params}: LoaderArgs) {
|
|
23
22
|
const {cart} = context;
|
|
@@ -3,14 +3,13 @@ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
|
|
|
3
3
|
/**
|
|
4
4
|
* Automatically applies a discount found on the url
|
|
5
5
|
* If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @example
|
|
8
|
-
* Example path applying a discount and redirecting
|
|
9
|
-
* ```
|
|
8
|
+
* Example path applying a discount and optional redirecting (defaults to the home page)
|
|
9
|
+
* ```js
|
|
10
10
|
* /discount/FREESHIPPING?redirect=/products
|
|
11
11
|
*
|
|
12
12
|
* ```
|
|
13
|
-
* @preserve
|
|
14
13
|
*/
|
|
15
14
|
export async function loader({request, context, params}: LoaderArgs) {
|
|
16
15
|
const {cart} = context;
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@remix-run/react": "1.19.1",
|
|
17
17
|
"@shopify/cli": "3.49.2",
|
|
18
|
-
"@shopify/cli-hydrogen": "^5.
|
|
19
|
-
"@shopify/hydrogen": "^2023.7.
|
|
18
|
+
"@shopify/cli-hydrogen": "^5.4.0",
|
|
19
|
+
"@shopify/hydrogen": "^2023.7.9",
|
|
20
20
|
"@shopify/remix-oxygen": "^1.1.4",
|
|
21
21
|
"graphql": "^16.6.0",
|
|
22
22
|
"graphql-tag": "^2.12.6",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@shopify/prettier-config": "^1.1.2",
|
|
31
31
|
"@total-typescript/ts-reset": "^0.4.2",
|
|
32
32
|
"@types/eslint": "^8.4.10",
|
|
33
|
-
"@types/react": "^18.2.
|
|
33
|
+
"@types/react": "^18.2.22",
|
|
34
34
|
"@types/react-dom": "^18.2.7",
|
|
35
35
|
"eslint": "^8.20.0",
|
|
36
36
|
"eslint-plugin-hydrogen": "0.12.2",
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { joinPath, dirname } from '@shopify/cli-kit/node/path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { readFile, writeFile } from '@shopify/cli-kit/node/fs';
|
|
3
|
+
import { fileExists, readFile, writeFile } from '@shopify/cli-kit/node/fs';
|
|
4
4
|
import colors from '@shopify/cli-kit/node/colors';
|
|
5
5
|
|
|
6
|
+
async function hasMetafile(buildPath) {
|
|
7
|
+
return (await Promise.all([
|
|
8
|
+
fileExists(joinPath(buildPath, "worker", "metafile.server.json")),
|
|
9
|
+
fileExists(joinPath(buildPath, "worker", "metafile.js.json"))
|
|
10
|
+
])).every(Boolean);
|
|
11
|
+
}
|
|
6
12
|
async function buildBundleAnalysis(buildPath) {
|
|
7
13
|
await Promise.all([
|
|
8
14
|
writeBundleAnalyzerFile(
|
|
@@ -46,11 +52,11 @@ async function getBundleAnalysisSummary(bundlePath) {
|
|
|
46
52
|
color: true
|
|
47
53
|
})).split("\n").filter((line) => {
|
|
48
54
|
const match = line.match(
|
|
49
|
-
/(.*)
|
|
55
|
+
/(.*)(node_modules\/|server-assets-manifest:|server-entry-module:)(react-dom|@remix-run|@shopify\/hydrogen|react-router|react-router-dom)\/(.*)/g
|
|
50
56
|
);
|
|
51
57
|
return !match;
|
|
52
58
|
}).slice(2, 12).join("\n").replace(/dist\/worker\/_assets\/.*$/ms, "\n").replace(/\n/g, "\n ").replace(/(\.\.\/)+node_modules\//g, (match) => colors.dim(match));
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
export { buildBundleAnalysis, getBundleAnalysisSummary };
|
|
62
|
+
export { buildBundleAnalysis, getBundleAnalysisSummary, hasMetafile };
|
|
@@ -2,24 +2,28 @@ import { fileExists } from '@shopify/cli-kit/node/fs';
|
|
|
2
2
|
import { resolvePath } from '@shopify/cli-kit/node/path';
|
|
3
3
|
import { checkIfIgnoredInGitRepository } from '@shopify/cli-kit/node/git';
|
|
4
4
|
import { renderWarning } from '@shopify/cli-kit/node/ui';
|
|
5
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
5
6
|
import { lockfiles } from '@shopify/cli-kit/node/node-package-manager';
|
|
6
7
|
|
|
7
|
-
function missingLockfileWarning() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
function missingLockfileWarning(shouldExit) {
|
|
9
|
+
const headline = "No lockfile found";
|
|
10
|
+
const body = `If you don\u2019t commit a lockfile, then your app might install the wrong package versions when deploying. To avoid versioning issues, generate a new lockfile and commit it to your repository.`;
|
|
11
|
+
const nextSteps = [
|
|
12
|
+
[
|
|
13
|
+
"Generate a lockfile. Run",
|
|
14
|
+
{
|
|
15
|
+
command: "npm|yarn|pnpm install"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"Commit the new file to your repository"
|
|
19
|
+
];
|
|
20
|
+
if (shouldExit) {
|
|
21
|
+
throw new AbortError(headline, body, nextSteps);
|
|
22
|
+
} else {
|
|
23
|
+
renderWarning({ headline, body, nextSteps });
|
|
24
|
+
}
|
|
21
25
|
}
|
|
22
|
-
function multipleLockfilesWarning(lockfiles2) {
|
|
26
|
+
function multipleLockfilesWarning(lockfiles2, shouldExit) {
|
|
23
27
|
const packageManagers = {
|
|
24
28
|
"yarn.lock": "yarn",
|
|
25
29
|
"package-lock.json": "npm",
|
|
@@ -28,30 +32,32 @@ function multipleLockfilesWarning(lockfiles2) {
|
|
|
28
32
|
const lockfileList = lockfiles2.map((lockfile) => {
|
|
29
33
|
return `${lockfile} (created by ${packageManagers[lockfile]})`;
|
|
30
34
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`Your project contains more than one lockfile. This can cause version conflicts when installing and deploying your app. The following lockfiles were detected:
|
|
35
|
+
const headline = "Multiple lockfiles found";
|
|
36
|
+
const body = [
|
|
37
|
+
`Your project contains more than one lockfile. This can cause version conflicts when installing and deploying your app. The following lockfiles were detected:
|
|
35
38
|
`,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
{ list: { items: lockfileList } }
|
|
40
|
+
];
|
|
41
|
+
const nextSteps = [
|
|
42
|
+
"Delete any unneeded lockfiles",
|
|
43
|
+
"Commit the change to your repository"
|
|
44
|
+
];
|
|
45
|
+
if (shouldExit) {
|
|
46
|
+
throw new AbortError(headline, body, nextSteps);
|
|
47
|
+
} else {
|
|
48
|
+
renderWarning({ headline, body, nextSteps });
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
function lockfileIgnoredWarning(lockfile) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
52
|
+
const headline = "Lockfile ignored by Git";
|
|
53
|
+
const body = `Your project\u2019s lockfile isn\u2019t being tracked by Git. If you don\u2019t commit a lockfile, then your app might install the wrong package versions when deploying.`;
|
|
54
|
+
const nextSteps = [
|
|
55
|
+
`In your project\u2019s .gitignore file, delete any references to ${lockfile}`,
|
|
56
|
+
"Commit the change to your repository"
|
|
57
|
+
];
|
|
58
|
+
renderWarning({ headline, body, nextSteps });
|
|
53
59
|
}
|
|
54
|
-
async function checkLockfileStatus(directory) {
|
|
60
|
+
async function checkLockfileStatus(directory, shouldExit = false) {
|
|
55
61
|
if (process.env.LOCAL_DEV)
|
|
56
62
|
return;
|
|
57
63
|
const availableLockfiles = [];
|
|
@@ -60,21 +66,20 @@ async function checkLockfileStatus(directory) {
|
|
|
60
66
|
availableLockfiles.push(lockFileName);
|
|
61
67
|
}
|
|
62
68
|
}
|
|
63
|
-
if (
|
|
64
|
-
return missingLockfileWarning();
|
|
69
|
+
if (availableLockfiles.length === 0) {
|
|
70
|
+
return missingLockfileWarning(shouldExit);
|
|
65
71
|
}
|
|
66
72
|
if (availableLockfiles.length > 1) {
|
|
67
|
-
return multipleLockfilesWarning(availableLockfiles);
|
|
73
|
+
return multipleLockfilesWarning(availableLockfiles, shouldExit);
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} catch {
|
|
75
|
+
const lockfile = availableLockfiles[0];
|
|
76
|
+
const ignoredLockfile = await checkIfIgnoredInGitRepository(directory, [
|
|
77
|
+
lockfile
|
|
78
|
+
]).catch(() => {
|
|
79
|
+
return [];
|
|
80
|
+
});
|
|
81
|
+
if (ignoredLockfile.length > 0) {
|
|
82
|
+
lockfileIgnoredWarning(lockfile);
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -53,6 +53,15 @@ describe("checkLockfileStatus()", () => {
|
|
|
53
53
|
);
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
+
it("throws when shouldExit is true", async () => {
|
|
57
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
58
|
+
await writeFile(joinPath(tmpDir, "package-lock.json"), "");
|
|
59
|
+
await writeFile(joinPath(tmpDir, "pnpm-lock.yaml"), "");
|
|
60
|
+
await expect(checkLockfileStatus(tmpDir, true)).rejects.toThrow(
|
|
61
|
+
/Multiple lockfiles found/is
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
56
65
|
});
|
|
57
66
|
describe("when a lockfile is missing", () => {
|
|
58
67
|
it("renders a warning", async () => {
|
|
@@ -61,5 +70,12 @@ describe("checkLockfileStatus()", () => {
|
|
|
61
70
|
expect(outputMock.warn()).toMatch(/ warning .+ No lockfile found .+/is);
|
|
62
71
|
});
|
|
63
72
|
});
|
|
73
|
+
it("throws when shouldExit is true", async () => {
|
|
74
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
75
|
+
await expect(checkLockfileStatus(tmpDir, true)).rejects.toThrow(
|
|
76
|
+
/No lockfile found/is
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
64
80
|
});
|
|
65
81
|
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function getGraphiQLUrl({
|
|
2
|
+
host = "",
|
|
3
|
+
graphql
|
|
4
|
+
}) {
|
|
5
|
+
let url = `${host.endsWith("/") ? host.slice(0, -1) : host}/graphiql`;
|
|
6
|
+
if (graphql) {
|
|
7
|
+
let { query, variables } = graphql;
|
|
8
|
+
if (typeof variables !== "string")
|
|
9
|
+
variables = JSON.stringify(variables);
|
|
10
|
+
url += `?query=${encodeURIComponent(query)}${variables ? `&variables=${encodeURIComponent(variables)}` : ""}`;
|
|
11
|
+
}
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { getGraphiQLUrl };
|
package/dist/lib/log.js
CHANGED
|
@@ -2,6 +2,7 @@ import { renderFatalError, renderWarning, renderInfo } from '@shopify/cli-kit/no
|
|
|
2
2
|
import { BugError } from '@shopify/cli-kit/node/error';
|
|
3
3
|
import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
|
|
4
4
|
import colors from '@shopify/cli-kit/node/colors';
|
|
5
|
+
import { getGraphiQLUrl } from './graphiql-url.js';
|
|
5
6
|
|
|
6
7
|
const originalConsole = { ...console };
|
|
7
8
|
const methodsReplaced = /* @__PURE__ */ new Set();
|
|
@@ -194,11 +195,11 @@ function enhanceH2Logs(options) {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
if (typeof cause !== "string" && !!cause?.graphql?.query) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const [, queryType, queryName] = query.match(/(query|mutation)\s+(\w+)/) || [];
|
|
198
|
+
const link = getGraphiQLUrl({
|
|
199
|
+
host: options.host,
|
|
200
|
+
graphql: cause.graphql
|
|
201
|
+
});
|
|
202
|
+
const [, queryType, queryName] = cause.graphql.query.match(/(query|mutation)\s+(\w+)/) || [];
|
|
202
203
|
tryMessage = (tryMessage ? `${tryMessage}
|
|
203
204
|
|
|
204
205
|
` : "") + outputContent`To debug the ${queryType || "query"}${queryName ? ` \`${colors.whiteBright(queryName)}\`` : ""}, try it in ${outputToken.link(colors.bold("GraphiQL"), link)}.`.value;
|
package/dist/lib/log.test.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resetAllLogs, enhanceH2Logs } from './log.js';
|
|
|
5
5
|
|
|
6
6
|
describe("log replacer", () => {
|
|
7
7
|
describe("enhanceH2Logs", () => {
|
|
8
|
-
const
|
|
8
|
+
const host = "http://localhost:3000";
|
|
9
9
|
const rootDirectory = fileURLToPath(import.meta.url);
|
|
10
10
|
const outputMock = mockAndCaptureOutput();
|
|
11
11
|
beforeEach(() => {
|
|
@@ -18,7 +18,7 @@ describe("log replacer", () => {
|
|
|
18
18
|
});
|
|
19
19
|
describe("enhances h2:info pattern", () => {
|
|
20
20
|
it("renders in an info banner", () => {
|
|
21
|
-
enhanceH2Logs({
|
|
21
|
+
enhanceH2Logs({ host, rootDirectory });
|
|
22
22
|
console.warn("[h2:info:storefront.query] Tip");
|
|
23
23
|
const message = outputMock.info();
|
|
24
24
|
expect(message).not.toMatch("h2");
|
|
@@ -29,7 +29,7 @@ describe("log replacer", () => {
|
|
|
29
29
|
});
|
|
30
30
|
describe("enhances h2:warn pattern", () => {
|
|
31
31
|
it("renders in a warning banner", () => {
|
|
32
|
-
enhanceH2Logs({
|
|
32
|
+
enhanceH2Logs({ host, rootDirectory });
|
|
33
33
|
console.warn("[h2:warn:storefront.query] Wrong query 1");
|
|
34
34
|
const warning = outputMock.warn();
|
|
35
35
|
expect(warning).not.toMatch("h2");
|
|
@@ -38,7 +38,7 @@ describe("log replacer", () => {
|
|
|
38
38
|
expect(warning).toMatch("Wrong query");
|
|
39
39
|
});
|
|
40
40
|
it("shows links from the last line as a list", () => {
|
|
41
|
-
enhanceH2Logs({
|
|
41
|
+
enhanceH2Logs({ host, rootDirectory });
|
|
42
42
|
console.warn(
|
|
43
43
|
"[h2:warn:storefront.query] Wrong query.\nhttps://docs.com/something"
|
|
44
44
|
);
|
|
@@ -50,7 +50,7 @@ describe("log replacer", () => {
|
|
|
50
50
|
});
|
|
51
51
|
describe("enhances h2:error pattern", () => {
|
|
52
52
|
it("renders in an error banner", () => {
|
|
53
|
-
enhanceH2Logs({
|
|
53
|
+
enhanceH2Logs({ host, rootDirectory });
|
|
54
54
|
console.error(new Error("[h2:error:storefront.query] Wrong query 2"));
|
|
55
55
|
const error = outputMock.error();
|
|
56
56
|
expect(error.split("stack trace:")[0]).not.toMatch("h2");
|
|
@@ -59,7 +59,7 @@ describe("log replacer", () => {
|
|
|
59
59
|
expect(error).toMatch("Wrong query");
|
|
60
60
|
});
|
|
61
61
|
it("shows a GraphiQL link when the error is related to a GraphQL query", () => {
|
|
62
|
-
enhanceH2Logs({
|
|
62
|
+
enhanceH2Logs({ host, rootDirectory });
|
|
63
63
|
console.error(
|
|
64
64
|
new Error("[h2:error:storefront.query] Wrong query 3", {
|
|
65
65
|
cause: {
|
|
@@ -75,7 +75,7 @@ describe("log replacer", () => {
|
|
|
75
75
|
`);
|
|
76
76
|
});
|
|
77
77
|
it("trims stack traces when the error is related to a GraphQL query", () => {
|
|
78
|
-
enhanceH2Logs({
|
|
78
|
+
enhanceH2Logs({ host, rootDirectory });
|
|
79
79
|
console.error(
|
|
80
80
|
new Error("[h2:error:storefront.query] Wrong query 4", {
|
|
81
81
|
cause: { graphql: { query: "query test {}" } }
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
-
import { resolvePath } from '@shopify/cli-kit/node/path';
|
|
4
3
|
import { readFile } from '@shopify/cli-kit/node/fs';
|
|
5
4
|
import { renderSuccess } from '@shopify/cli-kit/node/ui';
|
|
6
5
|
import { startServer, Request } from '@shopify/mini-oxygen';
|
|
@@ -9,14 +8,12 @@ import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
|
|
|
9
8
|
import { clearHistory, streamRequestEvents, logRequestEvent } from '../request-events.js';
|
|
10
9
|
|
|
11
10
|
async function startNodeServer({
|
|
12
|
-
root,
|
|
13
11
|
port = DEFAULT_PORT,
|
|
14
12
|
watch = false,
|
|
15
13
|
buildPathWorkerFile,
|
|
16
14
|
buildPathClient,
|
|
17
15
|
env
|
|
18
16
|
}) {
|
|
19
|
-
resolvePath(root, ".env");
|
|
20
17
|
const oxygenHeaders = Object.fromEntries(
|
|
21
18
|
Object.entries(OXYGEN_HEADERS_MAP).map(([key, value]) => {
|
|
22
19
|
return [key, value.defaultValue];
|
|
@@ -25,13 +22,13 @@ async function startNodeServer({
|
|
|
25
22
|
const asyncLocalStorage = new AsyncLocalStorage();
|
|
26
23
|
const serviceBindings = {
|
|
27
24
|
H2O_LOG_EVENT: {
|
|
28
|
-
fetch: (request) => logRequestEvent(
|
|
25
|
+
fetch: async (request) => logRequestEvent(
|
|
29
26
|
new Request(request.url, {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
...await request.json(),
|
|
33
30
|
...asyncLocalStorage.getStore()
|
|
34
|
-
}
|
|
31
|
+
})
|
|
35
32
|
})
|
|
36
33
|
)
|
|
37
34
|
}
|
|
@@ -65,7 +62,7 @@ async function startNodeServer({
|
|
|
65
62
|
}
|
|
66
63
|
const startTimeMs = Date.now();
|
|
67
64
|
const response = await asyncLocalStorage.run(
|
|
68
|
-
{
|
|
65
|
+
{ requestId, purpose: request.headers.get("purpose") },
|
|
69
66
|
() => defaultDispatcher(request)
|
|
70
67
|
);
|
|
71
68
|
logRequestLine(request, {
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { Response } from '@shopify/mini-oxygen';
|
|
4
|
+
import { getGraphiQLUrl } from './graphiql-url.js';
|
|
4
5
|
|
|
5
6
|
const DEV_ROUTES = /* @__PURE__ */ new Set(["/graphiql", "/debug-network"]);
|
|
6
7
|
const EVENT_MAP = {
|
|
7
8
|
request: "Request",
|
|
8
9
|
subrequest: "Sub request"
|
|
9
10
|
};
|
|
10
|
-
function getRequestInfo(request) {
|
|
11
|
+
async function getRequestInfo(request) {
|
|
12
|
+
const data = await request.json();
|
|
11
13
|
return {
|
|
12
|
-
id:
|
|
13
|
-
eventType:
|
|
14
|
-
startTime:
|
|
15
|
-
endTime:
|
|
16
|
-
purpose:
|
|
17
|
-
cacheStatus:
|
|
14
|
+
id: data.requestId ?? "",
|
|
15
|
+
eventType: data.eventType || "unknown",
|
|
16
|
+
startTime: data.startTime,
|
|
17
|
+
endTime: data.endTime || Date.now(),
|
|
18
|
+
purpose: data.purpose === "prefetch" ? "(prefetch)" : "",
|
|
19
|
+
cacheStatus: data.cacheStatus ?? "",
|
|
20
|
+
stackLine: data.stackLine ?? "",
|
|
21
|
+
graphql: data.graphql ? JSON.parse(data.graphql) : null
|
|
18
22
|
};
|
|
19
23
|
}
|
|
20
24
|
const eventEmitter = new EventEmitter();
|
|
@@ -24,24 +28,36 @@ async function clearHistory() {
|
|
|
24
28
|
return new Response("ok");
|
|
25
29
|
}
|
|
26
30
|
async function logRequestEvent(request) {
|
|
27
|
-
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
if (DEV_ROUTES.has(url.pathname)) {
|
|
28
33
|
return new Response("ok");
|
|
29
34
|
}
|
|
30
|
-
const { eventType, purpose, ...data } = getRequestInfo(request);
|
|
35
|
+
const { eventType, purpose, stackLine, graphql, ...data } = await getRequestInfo(request);
|
|
36
|
+
let originFile = "";
|
|
37
|
+
let graphiqlLink = "";
|
|
31
38
|
let description = request.url;
|
|
32
39
|
if (eventType === "subrequest") {
|
|
33
|
-
description =
|
|
40
|
+
description = graphql?.query.match(/(query|mutation)\s+(\w+)/)?.[0]?.replace(/\s+/, " ") || decodeURIComponent(url.search.slice(1));
|
|
41
|
+
const [, fnName, filePath] = stackLine?.match(/\s+at ([^\s]+) \(.*?\/(app\/[^\n]*)\)/) || [];
|
|
42
|
+
if (fnName && filePath) {
|
|
43
|
+
originFile = `${fnName}:${filePath}`;
|
|
44
|
+
}
|
|
45
|
+
if (graphql) {
|
|
46
|
+
graphiqlLink = getGraphiQLUrl({ graphql });
|
|
47
|
+
}
|
|
34
48
|
}
|
|
35
49
|
const event = {
|
|
36
50
|
event: EVENT_MAP[eventType] || eventType,
|
|
37
51
|
data: JSON.stringify({
|
|
38
52
|
...data,
|
|
39
|
-
url: `${purpose} ${description}`.trim()
|
|
53
|
+
url: `${purpose} ${description}`.trim(),
|
|
54
|
+
graphiqlLink,
|
|
55
|
+
originFile
|
|
40
56
|
})
|
|
41
57
|
};
|
|
58
|
+
eventHistory.push(event);
|
|
42
59
|
if (eventHistory.length > 100)
|
|
43
60
|
eventHistory.shift();
|
|
44
|
-
eventHistory.push(event);
|
|
45
61
|
eventEmitter.emit("request", event);
|
|
46
62
|
return new Response("ok");
|
|
47
63
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.4.0",
|
|
3
3
|
"commands": {
|
|
4
4
|
"hydrogen:build": {
|
|
5
5
|
"id": "hydrogen:build",
|
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
"description": "Show a bundle size summary after building.",
|
|
29
29
|
"allowNo": true
|
|
30
30
|
},
|
|
31
|
+
"lockfile-check": {
|
|
32
|
+
"name": "lockfile-check",
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"description": "Checks that there is exactly 1 valid lockfile in the project.",
|
|
35
|
+
"allowNo": true
|
|
36
|
+
},
|
|
31
37
|
"disable-route-warning": {
|
|
32
38
|
"name": "disable-route-warning",
|
|
33
39
|
"type": "boolean",
|