Skip to content

Commit a109836

Browse files
committed
rethrowAsUserError for knows user code errors
1 parent 3e2e1b8 commit a109836

2 files changed

Lines changed: 150 additions & 98 deletions

File tree

packages/cli/src/commands/hydrogen/build.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,42 @@ import {
88
import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
99
import {joinPath} from '@shopify/cli-kit/node/path';
1010
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output';
11-
import {runBuild} from './build.js';
11+
import {runBuild, rethrowAsUserError} from './build.js';
12+
import {AbortError} from '@shopify/cli-kit/node/error';
1213
import {setupTemplate} from '../../lib/onboarding/index.js';
1314
import {BUNDLE_ANALYZER_HTML_FILE} from '../../lib/bundle/analyzer.js';
1415
import path from 'node:path';
1516
import {mkdirSync} from 'node:fs';
1617

18+
describe('rethrowAsUserError', () => {
19+
it('converts known user-code Vite errors to AbortError', () => {
20+
const error = new Error(
21+
'Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.',
22+
);
23+
error.stack = 'Error: ...\n at something.js:1:1';
24+
25+
expect(() => rethrowAsUserError(error)).toThrow(AbortError);
26+
try {
27+
rethrowAsUserError(error);
28+
} catch (e) {
29+
expect(e).toBeInstanceOf(AbortError);
30+
expect((e as AbortError).message).toBe(error.message);
31+
expect((e as AbortError).stack).toBe(error.stack);
32+
}
33+
});
34+
35+
it('re-throws unknown errors unchanged', () => {
36+
const error = new Error('some unexpected vite internal error');
37+
38+
try {
39+
rethrowAsUserError(error);
40+
} catch (e) {
41+
expect(e).toBe(error);
42+
expect(e).not.toBeInstanceOf(AbortError);
43+
}
44+
});
45+
});
46+
1747
describe('build', () => {
1848
const outputMock = mockAndCaptureOutput();
1949

packages/cli/src/commands/hydrogen/build.ts

Lines changed: 119 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ import {deferPromise, type DeferredPromise} from '../../lib/defer.js';
3434
import {setupResourceCleanup} from '../../lib/resource-cleanup.js';
3535
import {AbortError} from '@shopify/cli-kit/node/error';
3636

37+
const USER_CODE_ERROR_PATTERNS = ['Failed to parse source for import analysis'];
38+
39+
export function rethrowAsUserError(error: unknown): never {
40+
if (
41+
error instanceof Error &&
42+
USER_CODE_ERROR_PATTERNS.some((pattern) => error.message.includes(pattern))
43+
) {
44+
const abortError = new AbortError(error.message);
45+
abortError.stack = error.stack;
46+
throw abortError;
47+
}
48+
throw error;
49+
}
50+
3751
export default class Build extends Command {
3852
static descriptionWithMarkdown = `Builds a Hydrogen storefront for production. The client and app worker files are compiled to a \`/dist\` folder in your Hydrogen project directory.`;
3953

@@ -168,111 +182,119 @@ export async function runBuild({
168182
let clientBuildStatus: DeferredPromise;
169183

170184
// Client build first
171-
const clientBuild = await vite.build({
172-
...commonConfig,
173-
build: {
174-
emptyOutDir: true,
175-
copyPublicDir: true,
176-
// Disable client sourcemaps in production by default
177-
sourcemap:
178-
forceClientSourcemap ??
179-
(process.env.NODE_ENV !== 'production' && sourcemap),
180-
watch: watch ? {} : null,
181-
},
182-
server: {
183-
watch: watch ? {} : null,
184-
},
185-
plugins: [
186-
{
187-
name: 'hydrogen:cli:client',
188-
buildStart() {
189-
clientBuildStatus?.resolve();
190-
clientBuildStatus = deferPromise();
191-
},
192-
buildEnd(error) {
193-
if (error) clientBuildStatus.reject(error);
194-
},
195-
writeBundle() {
196-
clientBuildStatus.resolve();
197-
},
198-
closeWatcher() {
199-
// End build process if watcher is closed
200-
this.error(new Error('Process exited before client build finished.'));
201-
},
185+
const clientBuild = await vite
186+
.build({
187+
...commonConfig,
188+
build: {
189+
emptyOutDir: true,
190+
copyPublicDir: true,
191+
// Disable client sourcemaps in production by default
192+
sourcemap:
193+
forceClientSourcemap ??
194+
(process.env.NODE_ENV !== 'production' && sourcemap),
195+
watch: watch ? {} : null,
196+
},
197+
server: {
198+
watch: watch ? {} : null,
202199
},
203-
],
204-
});
200+
plugins: [
201+
{
202+
name: 'hydrogen:cli:client',
203+
buildStart() {
204+
clientBuildStatus?.resolve();
205+
clientBuildStatus = deferPromise();
206+
},
207+
buildEnd(error) {
208+
if (error) clientBuildStatus.reject(error);
209+
},
210+
writeBundle() {
211+
clientBuildStatus.resolve();
212+
},
213+
closeWatcher() {
214+
// End build process if watcher is closed
215+
this.error(
216+
new Error('Process exited before client build finished.'),
217+
);
218+
},
219+
},
220+
],
221+
})
222+
.catch(rethrowAsUserError);
205223

206224
console.log('');
207225

208226
let serverBuildStatus: DeferredPromise;
209227

210228
// Server/SSR build
211-
const serverBuild = await vite.build({
212-
...commonConfig,
213-
build: {
214-
sourcemap,
215-
ssr: ssrEntry ?? true,
216-
emptyOutDir: false,
217-
copyPublicDir: false,
218-
minify: serverMinify,
219-
// Ensure the server rebuild start after the client one
220-
watch: watch ? {buildDelay: 100} : null,
221-
},
222-
server: {
223-
watch: watch ? {} : null,
224-
},
225-
plugins: [
226-
{
227-
name: 'hydrogen:cli:server',
228-
async buildStart() {
229-
// Wait for the client build to finish in watch mode
230-
// before starting the server build to access the
231-
// Remix manifest from file disk.
232-
await clientBuildStatus.promise;
233-
234-
// Keep track of server builds to wait for them to finish
235-
// before cleaning up resources in watch mode. Otherwise,
236-
// it might complain about missing files and loop infinitely.
237-
serverBuildStatus?.resolve();
238-
serverBuildStatus = deferPromise();
239-
await onServerBuildStart?.();
240-
},
241-
async writeBundle() {
242-
if (serverBuildStatus?.state !== 'rejected') {
243-
await onServerBuildFinish?.();
244-
}
245-
246-
serverBuildStatus.resolve();
247-
},
248-
closeWatcher() {
249-
// End build process if watcher is closed
250-
this.error(new Error('Process exited before server build finished.'));
251-
},
229+
const serverBuild = await vite
230+
.build({
231+
...commonConfig,
232+
build: {
233+
sourcemap,
234+
ssr: ssrEntry ?? true,
235+
emptyOutDir: false,
236+
copyPublicDir: false,
237+
minify: serverMinify,
238+
// Ensure the server rebuild start after the client one
239+
watch: watch ? {buildDelay: 100} : null,
252240
},
253-
...(bundleStats
254-
? [
255-
hydrogenBundleAnalyzer({
256-
minify: serverMinify
257-
? (code, filepath) =>
258-
vite
259-
.transformWithEsbuild(code, filepath, {
260-
minify: true,
261-
minifyWhitespace: true,
262-
minifySyntax: true,
263-
minifyIdentifiers: true,
264-
sourcemap: false,
265-
treeShaking: false, // Tree-shaking would drop most exports in routes
266-
legalComments: 'none',
267-
target: 'esnext',
268-
})
269-
.then((result) => result.code)
270-
: undefined,
271-
}),
272-
]
273-
: []),
274-
],
275-
});
241+
server: {
242+
watch: watch ? {} : null,
243+
},
244+
plugins: [
245+
{
246+
name: 'hydrogen:cli:server',
247+
async buildStart() {
248+
// Wait for the client build to finish in watch mode
249+
// before starting the server build to access the
250+
// Remix manifest from file disk.
251+
await clientBuildStatus.promise;
252+
253+
// Keep track of server builds to wait for them to finish
254+
// before cleaning up resources in watch mode. Otherwise,
255+
// it might complain about missing files and loop infinitely.
256+
serverBuildStatus?.resolve();
257+
serverBuildStatus = deferPromise();
258+
await onServerBuildStart?.();
259+
},
260+
async writeBundle() {
261+
if (serverBuildStatus?.state !== 'rejected') {
262+
await onServerBuildFinish?.();
263+
}
264+
265+
serverBuildStatus.resolve();
266+
},
267+
closeWatcher() {
268+
// End build process if watcher is closed
269+
this.error(
270+
new Error('Process exited before server build finished.'),
271+
);
272+
},
273+
},
274+
...(bundleStats
275+
? [
276+
hydrogenBundleAnalyzer({
277+
minify: serverMinify
278+
? (code, filepath) =>
279+
vite
280+
.transformWithEsbuild(code, filepath, {
281+
minify: true,
282+
minifyWhitespace: true,
283+
minifySyntax: true,
284+
minifyIdentifiers: true,
285+
sourcemap: false,
286+
treeShaking: false, // Tree-shaking would drop most exports in routes
287+
legalComments: 'none',
288+
target: 'esnext',
289+
})
290+
.then((result) => result.code)
291+
: undefined,
292+
}),
293+
]
294+
: []),
295+
],
296+
})
297+
.catch(rethrowAsUserError);
276298

277299
if (!watch) {
278300
await Promise.all([

0 commit comments

Comments
 (0)