diff --git a/.changeset/vite-dev-error-overlay.md b/.changeset/vite-dev-error-overlay.md
new file mode 100644
index 00000000..1c6404a9
--- /dev/null
+++ b/.changeset/vite-dev-error-overlay.md
@@ -0,0 +1,5 @@
+---
+"@inlang/paraglide-js": minor
+---
+
+Show compilation and config errors in Vite's development overlay, including in newly opened tabs. Once compilation and config loading recover, the plugin requests a page reload to clear the overlay. The dev server keeps serving the last compiled output, as before.
diff --git a/docs/configuration.md b/docs/configuration.md
index 4e7e1b6c..ab7eeabf 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -107,6 +107,12 @@ When things go wrong:
as if there had never been a config. Recreating any config file is picked
up again.
+In Vite development mode, compilation and config errors also appear in the
+Vite's error overlay, including in tabs opened while an error is pending.
+After both compilation and config loading recover, the plugin requests a page
+reload to clear the overlay. This reload discards client state and can occur
+alongside Vite's normal file updates.
+
One limitation: esbuild does not support registering additional watch
files, so config changes require an esbuild process restart.
diff --git a/src/bundler-plugins/unplugin.test.ts b/src/bundler-plugins/unplugin.test.ts
index 2003d72a..4ae41361 100644
--- a/src/bundler-plugins/unplugin.test.ts
+++ b/src/bundler-plugins/unplugin.test.ts
@@ -3,6 +3,7 @@ import consola from "consola";
import { memfs } from "memfs";
import path from "node:path";
import realFs from "node:fs";
+import type { HotPayload, ViteDevServer } from "vite";
import {
loadProjectInMemory,
newProject,
@@ -60,7 +61,9 @@ async function createProjectFixture(
}> {
const { mkdtemp, mkdir, writeFile } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
- const workspace = await mkdtemp(path.join(tmpdir(), "paraglide-plugin-"));
+ const workspace = realFs.realpathSync(
+ await mkdtemp(path.join(tmpdir(), "paraglide-plugin-"))
+ );
testDirectories.push(workspace);
const projectDir = path.join(workspace, "project.inlang");
await mkdir(projectDir, { recursive: true });
@@ -108,6 +111,437 @@ function setViteRoot(plugin: any, root: string): void {
const mockContext = { addWatchFile: () => {} };
+function configureViteServer(plugin: any) {
+ const ws = {
+ send: vi.fn(),
+ clients: new Set([{}]),
+ on: vi.fn(),
+ };
+ plugin.configureServer?.({ ws });
+ return ws;
+}
+
+async function connectViteClient(server: ViteDevServer) {
+ const url = new URL(server.resolvedUrls!.local[0]!);
+ url.protocol = "ws:";
+ url.searchParams.set("token", server.config.webSocketToken);
+ const socket = new WebSocket(url, "vite-hmr");
+ const messages: HotPayload[] = [];
+ socket.addEventListener("message", (event) => {
+ messages.push(JSON.parse(String(event.data)));
+ });
+ await vi.waitFor(() => {
+ expect(messages).toContainEqual({ type: "connected" });
+ });
+ // A message sent after connection callbacks provides a transport barrier.
+ server.ws.send({ type: "custom", event: "paraglide:test-barrier" });
+ await vi.waitFor(() => {
+ expect(messages).toContainEqual({
+ type: "custom",
+ event: "paraglide:test-barrier",
+ });
+ });
+ return { socket, messages };
+}
+
+async function startOverlayServer(invalidSettings = false) {
+ process.env.NODE_ENV = "development";
+ const { createServer } = await import("vite");
+ const { paraglideVitePlugin } = await import("./vite.js");
+ const fixture = await createProjectFixture();
+ const { workspace, projectDir, outdir } = fixture;
+ const file = path.join(projectDir, "settings.json");
+ const original = await realFs.promises.readFile(file, "utf8");
+ if (invalidSettings) await realFs.promises.writeFile(file, "{");
+ await realFs.promises.writeFile(
+ path.join(workspace, "index.html"),
+ "
Translation preview"
+ );
+ const server = await createServer({
+ root: workspace,
+ configFile: false,
+ logLevel: "silent",
+ plugins: [paraglideVitePlugin({ project: projectDir, outdir })],
+ server: { host: "127.0.0.1", port: 0, watch: null },
+ optimizeDeps: { noDiscovery: true, include: [] },
+ });
+ try {
+ await server.listen();
+ } catch (error) {
+ await server.close();
+ throw error;
+ }
+ return { ...fixture, server, file, original };
+}
+
+test.each(["settings", "config"])(
+ "vite error overlay reports invalid %s at startup and clears after recovery",
+ async (source) => {
+ process.env.NODE_ENV = "development";
+ const { projectDir, outdir } = await createProjectFixture();
+ const file = path.join(
+ projectDir,
+ source === "settings" ? "settings.json" : "paraglide.config.js"
+ );
+ const original = await realFs.promises.readFile(file, "utf8");
+ await realFs.promises.writeFile(
+ file,
+ source === "settings" ? "{" : "export default { outdir: 42 }"
+ );
+ const { paraglideVitePlugin } = await import("./vite.js");
+ const plugin = paraglideVitePlugin({ project: projectDir, outdir }) as any;
+ const { send } = configureViteServer(plugin);
+
+ await expect(plugin.buildStart.call(mockContext)).resolves.toBeUndefined();
+ expect(send).toHaveBeenCalledWith({
+ type: "error",
+ err: {
+ message: expect.any(String),
+ stack: expect.any(String),
+ plugin: "unplugin-paraglide-js",
+ },
+ });
+ expect(send.mock.calls[0]![0].err.message.length).toBeGreaterThan(0);
+
+ await realFs.promises.writeFile(file, original);
+ await plugin.watchChange.call(mockContext, file);
+ expect(send).toHaveBeenLastCalledWith({ type: "full-reload" });
+ expect(await realFs.promises.readdir(outdir)).toContain("runtime.js");
+ }
+);
+
+test.each(["settings", "config"])(
+ "vite error overlay recovers from invalid %s and preserves the last output until recovery",
+ async (source) => {
+ process.env.NODE_ENV = "development";
+ const { projectDir, outdir } = await createProjectFixture();
+ const { paraglideVitePlugin } = await import("./vite.js");
+ const writeFile = vi.spyOn(realFs.promises, "writeFile");
+ const plugin = paraglideVitePlugin({ project: projectDir, outdir }) as any;
+ const { send } = configureViteServer(plugin);
+ await plugin.buildStart.call(mockContext);
+ expect(send).not.toHaveBeenCalled();
+ const runtime = await realFs.promises.readFile(
+ path.join(outdir, "runtime.js"),
+ "utf8"
+ );
+ const file = path.join(
+ projectDir,
+ source === "settings" ? "settings.json" : "paraglide.config.js"
+ );
+ const original = await realFs.promises.readFile(file, "utf8");
+ await realFs.promises.writeFile(
+ file,
+ source === "settings" ? "{" : "export default { outdir: 42 }"
+ );
+ await expect(
+ plugin.watchChange.call(mockContext, file)
+ ).resolves.toBeUndefined();
+ expect(send).toHaveBeenLastCalledWith({
+ type: "error",
+ err: expect.objectContaining({ message: expect.any(String) }),
+ });
+ expect(
+ await realFs.promises.readFile(path.join(outdir, "runtime.js"), "utf8")
+ ).toBe(runtime);
+
+ await plugin.watchChange.call(mockContext, path.join(outdir, "runtime.js"));
+ expect(send).toHaveBeenCalledTimes(1);
+ await realFs.promises.writeFile(file, original);
+ writeFile.mockClear();
+ await plugin.watchChange.call(mockContext, file);
+ expect(send).toHaveBeenLastCalledWith({ type: "full-reload" });
+ const outputWrites = writeFile.mock.calls.filter(([file]) =>
+ String(file).startsWith(outdir)
+ );
+ if (source === "config") {
+ expect(outputWrites).toHaveLength(0);
+ } else {
+ expect(outputWrites.length).toBeGreaterThan(0);
+ }
+ await plugin.watchChange.call(mockContext, file);
+ expect(send).toHaveBeenCalledTimes(2);
+ }
+);
+
+test("vite error overlay prevents generated HMR while the config remains invalid", async () => {
+ const { server, workspace, projectDir, outdir, file, original } =
+ await startOverlayServer();
+ try {
+ const main = path.join(workspace, "main.js");
+ await realFs.promises.writeFile(
+ main,
+ "import './test-output/runtime.js'; import.meta.hot.accept(); import.meta.hot.accept('./test-output/runtime.js', () => {});"
+ );
+ await server.transformRequest("/main.js");
+ await server.transformRequest("/test-output/runtime.js");
+ const client = await connectViteClient(server);
+ server.watcher.emit("change", main);
+ await vi.waitFor(() =>
+ expect(client.messages.some((message) => message.type === "update")).toBe(
+ true
+ )
+ );
+ await server.transformRequest("/main.js");
+
+ const configFile = path.join(projectDir, "paraglide.config.js");
+ const config = await realFs.promises.readFile(configFile, "utf8");
+ await realFs.promises.writeFile(
+ configFile,
+ "export default { outdir: 42 }"
+ );
+ await server.pluginContainer.watchChange(configFile, { event: "update" });
+ await vi.waitFor(() =>
+ expect(client.messages.some((message) => message.type === "error")).toBe(
+ true
+ )
+ );
+ const runtimeFile = path.join(outdir, "runtime.js");
+ const outputBefore = await realFs.promises.readFile(runtimeFile, "utf8");
+ const settings = JSON.parse(original);
+ settings.locales.push("fr");
+ await realFs.promises.writeFile(file, JSON.stringify(settings));
+ await server.pluginContainer.watchChange(file, { event: "update" });
+ const outputAfter = await realFs.promises.readFile(runtimeFile, "utf8");
+ // The fixture disables file watching; deliver the event a changed output produces.
+ if (outputAfter !== outputBefore) {
+ server.watcher.emit("change", runtimeFile);
+ await vi.waitFor(() =>
+ expect(
+ client.messages.filter((message) => message.type === "update")
+ ).toHaveLength(2)
+ );
+ }
+ server.ws.send({ type: "custom", event: "paraglide:input-processed" });
+ await vi.waitFor(() =>
+ expect(client.messages).toContainEqual({
+ type: "custom",
+ event: "paraglide:input-processed",
+ })
+ );
+ expect(
+ client.messages
+ .filter((message) =>
+ ["update", "error", "full-reload"].includes(message.type)
+ )
+ .at(-1)?.type
+ ).toBe("error");
+ expect(outputAfter).toBe(outputBefore);
+ expect((await fetch(server.resolvedUrls!.local[0]!)).status).toBe(200);
+
+ await realFs.promises.writeFile(configFile, config);
+ await server.pluginContainer.watchChange(configFile, { event: "update" });
+ await vi.waitFor(() =>
+ expect(client.messages).toContainEqual({ type: "full-reload" })
+ );
+ expect(await realFs.promises.readFile(runtimeFile, "utf8")).not.toBe(
+ outputBefore
+ );
+ } finally {
+ await server.close();
+ }
+});
+
+test("vite error overlay still reports failures after restarting an inline plugin", async () => {
+ const { server, file } = await startOverlayServer();
+ try {
+ await server.restart();
+ const client = await connectViteClient(server);
+ await realFs.promises.writeFile(file, "{");
+ await server.pluginContainer.watchChange(file, { event: "update" });
+ await vi.waitFor(() =>
+ expect(client.messages.some((message) => message.type === "error")).toBe(
+ true
+ )
+ );
+ } finally {
+ await server.close();
+ }
+});
+
+test("vite error overlay retains errors without buffering them in Vite", async () => {
+ process.env.NODE_ENV = "development";
+ const { projectDir, outdir } = await createProjectFixture();
+ const file = path.join(projectDir, "settings.json");
+ const original = await realFs.promises.readFile(file, "utf8");
+ await realFs.promises.writeFile(file, "{");
+ const { paraglideVitePlugin } = await import("./vite.js");
+ const plugin = paraglideVitePlugin({ project: projectDir, outdir }) as any;
+ const ws = configureViteServer(plugin);
+ ws.clients.clear();
+ await plugin.buildStart.call(mockContext);
+ expect(ws.send).not.toHaveBeenCalled();
+ expect(ws.on).toHaveBeenCalledWith("connection", expect.any(Function));
+ const onConnection = ws.on.mock.calls[0]![1];
+ const socket = { send: vi.fn() };
+ onConnection(socket);
+ expect(JSON.parse(socket.send.mock.calls[0]![0])).toMatchObject({
+ type: "error",
+ err: { message: expect.any(String), plugin: "unplugin-paraglide-js" },
+ });
+ await realFs.promises.writeFile(file, original);
+ await plugin.watchChange.call(mockContext, file);
+ expect(ws.send).not.toHaveBeenCalled();
+ socket.send.mockClear();
+ onConnection(socket);
+ expect(socket.send).not.toHaveBeenCalled();
+});
+
+test("vite error overlay preserves compiler diagnostics and clears after a successful buildStart", async () => {
+ process.env.NODE_ENV = "development";
+ const actual = await vi.importActual(
+ "../compiler/compile.js"
+ );
+ const error = new SyntaxError("Invalid message pattern in messages/en.json");
+ const compile = vi.fn(actual.compile).mockRejectedValueOnce(error);
+ vi.doMock("../compiler/compile.js", () => ({ ...actual, compile }));
+ try {
+ const { projectDir, outdir } = await createProjectFixture();
+ const { paraglideVitePlugin } = await import("./vite.js");
+ const plugin = paraglideVitePlugin({ project: projectDir, outdir }) as any;
+ const { send } = configureViteServer(plugin);
+ await plugin.buildStart.call(mockContext);
+ expect(send).toHaveBeenCalledWith({
+ type: "error",
+ err: {
+ message: error.message,
+ stack: error.stack,
+ plugin: "unplugin-paraglide-js",
+ },
+ });
+ await plugin.buildStart.call(mockContext);
+ expect(send).toHaveBeenLastCalledWith({ type: "full-reload" });
+ await plugin.buildStart.call(mockContext);
+ expect(send).toHaveBeenCalledTimes(2);
+ } finally {
+ vi.doUnmock("../compiler/compile.js");
+ }
+});
+
+test("vite error overlay reports non-Error compiler failures during watchChange", async () => {
+ process.env.NODE_ENV = "development";
+ const actual = await vi.importActual(
+ "../compiler/compile.js"
+ );
+ const compile = vi.fn(actual.compile);
+ vi.doMock("../compiler/compile.js", () => ({ ...actual, compile }));
+ try {
+ const { projectDir, outdir } = await createProjectFixture();
+ const { paraglideVitePlugin } = await import("./vite.js");
+ const plugin = paraglideVitePlugin({ project: projectDir, outdir }) as any;
+ const { send } = configureViteServer(plugin);
+ await plugin.buildStart.call(mockContext);
+ compile.mockRejectedValueOnce("Invalid translation");
+ const file = path.join(projectDir, "settings.json");
+ await plugin.watchChange.call(mockContext, file);
+ expect(send).toHaveBeenLastCalledWith({
+ type: "error",
+ err: {
+ message: "Invalid translation",
+ stack: expect.any(String),
+ plugin: "unplugin-paraglide-js",
+ },
+ });
+ await plugin.watchChange.call(mockContext, file);
+ expect(send).toHaveBeenLastCalledWith({ type: "full-reload" });
+ } finally {
+ vi.doUnmock("../compiler/compile.js");
+ }
+});
+
+test("vite error overlay keeps serving requests while the dev watcher recovers", async () => {
+ const { server, file, original } = await startOverlayServer();
+ try {
+ const send = vi.spyOn(server.ws, "send");
+ const client = await connectViteClient(server);
+ const url = server.resolvedUrls!.local[0]!;
+ await realFs.promises.writeFile(file, "{");
+ server.watcher.emit("change", file);
+ await vi.waitFor(
+ () => {
+ expect(send).toHaveBeenCalledWith({
+ type: "error",
+ err: expect.objectContaining({ message: expect.any(String) }),
+ });
+ },
+ { timeout: 10000 }
+ );
+ await vi.waitFor(() =>
+ expect(client.messages.some((message) => message.type === "error")).toBe(
+ true
+ )
+ );
+ const response = await fetch(url);
+ expect(response.status).toBe(200);
+ expect(await response.text()).toContain("Translation preview");
+ send.mockClear();
+ await realFs.promises.writeFile(file, original);
+ server.watcher.emit("change", file);
+ await vi.waitFor(
+ () => expect(send).toHaveBeenCalledWith({ type: "full-reload" }),
+ { timeout: 10000 }
+ );
+ } finally {
+ await server.close();
+ }
+});
+
+// Vite 7 and earlier can retain a buffered error after a full reload.
+test("vite error overlay does not replay an error recovered before the first connection", async () => {
+ const { server, file, original, outdir } = await startOverlayServer(true);
+ try {
+ await realFs.promises.writeFile(file, original);
+ await server.pluginContainer.watchChange(file, { event: "update" });
+ expect(await realFs.promises.readdir(outdir)).toContain("runtime.js");
+ const client = await connectViteClient(server);
+ expect(client.messages.some((message) => message.type === "error")).toBe(
+ false
+ );
+ } finally {
+ await server.close();
+ }
+});
+
+test("vite error overlay reaches new tabs and reconnecting clients after a startup failure", async () => {
+ const { server, file, original } = await startOverlayServer(true);
+ try {
+ const first = await connectViteClient(server);
+ await vi.waitFor(() => {
+ expect(first.messages.some((message) => message.type === "error")).toBe(
+ true
+ );
+ });
+ const second = await connectViteClient(server);
+ expect(second.messages.some((message) => message.type === "error")).toBe(
+ true
+ );
+ await Promise.all(
+ [first, second].map(
+ ({ socket }) =>
+ new Promise((resolve) => {
+ socket.addEventListener("close", () => resolve(), { once: true });
+ socket.close();
+ })
+ )
+ );
+ const reconnected = await connectViteClient(server);
+ expect(
+ reconnected.messages.some((message) => message.type === "error")
+ ).toBe(true);
+ await realFs.promises.writeFile(file, original);
+ await server.pluginContainer.watchChange(file, { event: "update" });
+ await vi.waitFor(() => {
+ expect(reconnected.messages).toContainEqual({ type: "full-reload" });
+ });
+ const recovered = await connectViteClient(server);
+ expect(recovered.messages.some((message) => message.type === "error")).toBe(
+ false
+ );
+ } finally {
+ await server.close();
+ }
+});
+
test("watch mode follows config precedence flips", async () => {
process.env.NODE_ENV = "development";
const { writeFile, rm } = await import("node:fs/promises");
@@ -178,7 +612,9 @@ test("vite plugin throws on compilation errors at build time", async () => {
}) as any;
// In production mode - should throw the error
+ const { send } = configureViteServer(plugin);
await expect(plugin.buildStart?.call(mockContext)).rejects.toThrow();
+ expect(send).not.toHaveBeenCalled();
});
// Regression test for https://github.com/opral/inlang-paraglide-js/issues/659:
diff --git a/src/bundler-plugins/unplugin.ts b/src/bundler-plugins/unplugin.ts
index a8592ea8..3bb94f53 100644
--- a/src/bundler-plugins/unplugin.ts
+++ b/src/bundler-plugins/unplugin.ts
@@ -22,6 +22,7 @@ import {
resolveConfigCandidate,
type LoadedParaglideConfig,
} from "../services/config/index.js";
+import type { ErrorPayload, ViteDevServer, WebSocketClient } from "vite";
const PLUGIN_NAME = "unplugin-paraglide-js";
@@ -94,6 +95,7 @@ type PluginState = {
readFiles: Set;
clearReadFiles: () => void;
previousCompilation: CompilationResult | undefined;
+ compilationError: Error | undefined;
/**
* Digest of the input files and options of the last successful compilation.
*
@@ -124,6 +126,7 @@ function getPluginState(args: Pick): PluginState {
readFiles: tracked.readFiles,
clearReadFiles: tracked.clearReadFiles,
previousCompilation: undefined,
+ compilationError: undefined,
previousInputsDigest: undefined,
previousProject: undefined,
previousOutdir: undefined,
@@ -462,6 +465,7 @@ async function runCompilation(args: {
const { state } = args;
const outputStructure = resolveOutputStructure(compilerOptions);
maybeInvalidateForOptionsChange(state, compilerOptions);
+ state.compilationError = undefined;
try {
// Stabilize project directory listings before the first digest. The
// ignored cache directory must not invalidate the cache that creates it.
@@ -510,6 +514,8 @@ async function runCompilation(args: {
await writePersistentCache(state, compilerOptions, outputStructure);
logger.success(`Compilation complete (${outputStructure})`);
} catch (error) {
+ state.compilationError =
+ error instanceof Error ? error : new Error(String(error));
state.previousInputsDigest = undefined;
logger.error("Failed to compile project:", (error as Error).message);
logger.info("Please check your translation files for syntax errors.");
@@ -573,6 +579,39 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
// Set once a bundler context rejects watch-file registrations (esbuild);
// further registration attempts are skipped to avoid warning floods.
let watchRegistrationUnsupported = false;
+ let viteServer: ViteDevServer | undefined;
+ let viteError: ErrorPayload | undefined;
+ let configLoadFailed = false;
+
+ function reportViteError(
+ error: unknown,
+ source: "config" | "compilation" = "compilation"
+ ): void {
+ if (!viteServer || process.env.NODE_ENV === "production") return;
+ if (configLoadFailed && source === "compilation") return;
+ if (source === "config") configLoadFailed = true;
+ const failure = error instanceof Error ? error : new Error(String(error));
+ viteError = {
+ type: "error",
+ err: {
+ message: failure.message,
+ stack: failure.stack ?? "",
+ plugin: PLUGIN_NAME,
+ },
+ };
+ // Older Vite versions buffer errors without clients, even after recovery.
+ if (viteServer.ws.clients.size > 0) viteServer.ws.send(viteError);
+ }
+
+ function clearViteError(): void {
+ if (!viteServer || !viteError || configLoadFailed) return;
+ viteError = undefined;
+ // An invalid config can recover without writing generated files,
+ // leaving no HMR update to dismiss the error overlay.
+ if (viteServer.ws.clients.size > 0) {
+ viteServer.ws.send({ type: "full-reload" });
+ }
+ }
const getProjectDir = () =>
nodeNormalizePath(
@@ -617,8 +656,8 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
/**
* Rebuild for a single watch event. Config-named events reload the
- * config; input events reuse the last known options — options cannot
- * have changed without a config event.
+ * config; input events reuse the last known options unless the config
+ * failed to load.
*/
async function performWatchRecompile(path: string): Promise {
const normalizedPath = nodeNormalizePath(path);
@@ -630,13 +669,17 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
let args: CompilerOptions;
if (!configEvent && lastOptions !== undefined) {
- args = lastOptions;
- const targets = getWatchTargets(readFiles, { outdir: args.outdir });
+ const targets = getWatchTargets(readFiles, {
+ outdir: lastOptions.outdir,
+ });
if (targets.isIgnoredPath(normalizedPath)) return;
const shouldCompile =
targets.files.has(normalizedPath) ||
isPathWithinDirectories(normalizedPath, targets.directories);
if (!shouldCompile) return;
+ }
+ if (!configEvent && lastOptions !== undefined && !configLoadFailed) {
+ args = lastOptions;
} else {
if (configEvent) {
logger.info("Paraglide configuration changed, re-compiling.");
@@ -648,6 +691,7 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
} catch (error) {
// Invalid config: skip the compilation entirely and surface
// the problem. The previously compiled output stays on disk.
+ reportViteError(error, "config");
logger.error((error as Error).message);
logger.error(
"Skipping compilation because of the invalid paraglide config."
@@ -655,6 +699,7 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
return;
}
lastOptions = args;
+ configLoadFailed = false;
}
maybeInvalidateForOptionsChange(state, args);
@@ -678,6 +723,7 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
});
await writePersistentCache(state, args, outputStructure);
logger.success(`Re-compilation complete (${outputStructure})`);
+ clearViteError();
const nextTargets = getWatchTargets(readFiles, { outdir: args.outdir });
for (const filePath of nextTargets.files)
@@ -685,6 +731,7 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
for (const directoryPath of nextTargets.directories)
safeAddWatchFile(undefined, directoryPath);
} catch (e) {
+ reportViteError(e);
clearReadFiles();
for (const filePath of previouslyReadFiles) readFiles.add(filePath);
state.previousCompilation = undefined;
@@ -701,6 +748,7 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
try {
const loaded = await loadActiveConfig();
const args = resolveOptionsWith(loaded);
+ configLoadFailed = false;
lastOptions = args;
await runCompilation({ state, compilerOptions: args });
} catch (error) {
@@ -713,6 +761,7 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
"Failed to prepare paraglide compilation:",
(error as Error).message
);
+ reportViteError(error, "config");
return;
} finally {
if (lastOptions !== undefined) {
@@ -735,6 +784,11 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
);
}
}
+ if (state.compilationError) {
+ reportViteError(state.compilationError);
+ } else {
+ clearViteError();
+ }
},
async watchChange(path) {
// Serialize rebuilds: bundlers may deliver several watchChange
@@ -772,6 +826,12 @@ export const unpluginFactory: UnpluginFactoryWithOptions = (userArgs) => {
discoveryRoot = config.root;
},
},
+ configureServer(server) {
+ viteServer = server;
+ server.ws.on("connection", (socket: WebSocketClient["socket"]) => {
+ if (viteError) socket.send(JSON.stringify(viteError));
+ });
+ },
},
webpack(compiler) {
discoveryRoot = compiler.context;