diff --git a/packages/wxt/src/core/create-server.ts b/packages/wxt/src/core/create-server.ts index 74b12f1ab..9abf0348d 100644 --- a/packages/wxt/src/core/create-server.ts +++ b/packages/wxt/src/core/create-server.ts @@ -1,4 +1,5 @@ import { debounce } from 'perfect-debounce'; +import chokidar from 'chokidar'; import { BuildStepOutput, EntrypointGroup, @@ -28,6 +29,7 @@ import { mapWxtOptionsToRegisteredContentScript, } from './utils/content-scripts'; import { createKeyboardShortcuts } from './keyboard-shortcuts'; +import { isBabelSyntaxError, logBabelSyntaxError } from './utils/syntax-errors'; /** * Creates a dev server and pre-builds all the files that need to exist before loading the extension. @@ -156,8 +158,25 @@ async function createServerInternal(): Promise { const keyboardShortcuts = createKeyboardShortcuts(server); const buildAndOpenBrowser = async () => { - // Build after starting the dev server so it can be used to transform HTML files - server.currentOutput = await internalBuild(); + try { + // Build after starting the dev server so it can be used to transform HTML files + server.currentOutput = await internalBuild(); + } catch (err) { + if (!isBabelSyntaxError(err)) { + throw err; + } + logBabelSyntaxError(err); + wxt.logger.info('Waiting for syntax error to be fixed...'); + await new Promise((resolve) => { + const watcher = chokidar.watch(err.id, { ignoreInitial: true }); + watcher.on('all', () => { + watcher.close(); + wxt.logger.info('Syntax error resolved, rebuilding...'); + resolve(); + }); + }); + return buildAndOpenBrowser(); + } // Add file watchers for files not loaded by the dev server. See // https://github.com/wxt-dev/wxt/issues/428#issuecomment-1944731870 @@ -187,7 +206,7 @@ function createFileReloader(server: WxtDevServer) { const cb = async (event: string, path: string) => { changeQueue.push([event, path]); - await fileChangedMutex.runExclusive(async () => { + const reloading = fileChangedMutex.runExclusive(async () => { if (server.currentOutput == null) return; const fileChanges = changeQueue @@ -256,6 +275,14 @@ function createFileReloader(server: WxtDevServer) { // Catch build errors instead of crashing. Don't log error either, builder should have already logged it } }); + + await reloading.catch((error) => { + if (!isBabelSyntaxError(error)) { + throw error; + } + // Log syntax errors without crashing the server. + logBabelSyntaxError(error); + }); }; return debounce(cb, wxt.config.dev.server!.watchDebounce, { diff --git a/packages/wxt/src/core/utils/syntax-errors.ts b/packages/wxt/src/core/utils/syntax-errors.ts new file mode 100644 index 000000000..d0aef21a9 --- /dev/null +++ b/packages/wxt/src/core/utils/syntax-errors.ts @@ -0,0 +1,32 @@ +import { relative } from 'node:path'; +import pc from 'picocolors'; +import { wxt } from '../wxt'; + +export interface BabelSyntaxError extends SyntaxError { + code: 'BABEL_PARSER_SYNTAX_ERROR'; + frame?: string; + id: string; + loc: { line: number; column: number }; +} + +export function isBabelSyntaxError(error: unknown): error is BabelSyntaxError { + return ( + error instanceof SyntaxError && + (error as any).code === 'BABEL_PARSER_SYNTAX_ERROR' + ); +} + +export function logBabelSyntaxError(error: BabelSyntaxError) { + let filename = relative(wxt.config.root, error.id); + if (filename.startsWith('..')) { + filename = error.id; + } + let message = error.message.replace( + /\(\d+:\d+\)$/, + `(${filename}:${error.loc.line}:${error.loc.column + 1})`, + ); + if (error.frame) { + message += '\n\n' + pc.red(error.frame); + } + wxt.logger.error(message); +}