@inlang/paraglide-js
Version:
[](https://www.npmjs.com/package/@inlang/paraglide-js) [ => {
originalNodeEnv = process.env.NODE_ENV;
// Reset module state between tests so the module-scoped
// `previousCompilation` doesn't leak across tests.
vi.resetModules();
// Mock logging methods to suppress error messages in tests
consola.mockTypes(() => vi.fn());
});
afterEach(() => {
if (originalNodeEnv !== undefined) {
process.env.NODE_ENV = originalNodeEnv;
}
else {
delete process.env.NODE_ENV;
}
});
test("vite plugin does not throw when compilation is successful", async () => {
// Dynamic import so beforeEach's vi.resetModules() yields a fresh module
// (a static import would share module state across tests).
const { paraglideVitePlugin } = await import("../bundler-plugins/vite.js");
// Create and save a viable project to the virtual file system
const project = await loadProjectInMemory({
blob: await newProject({
settings: {
baseLocale: "en",
locales: ["en", "de", "fr"],
},
}),
});
const fs = memfs().fs;
await saveProjectToDirectory({
project,
path: "/project.inlang",
fs: fs.promises,
});
const plugin = paraglideVitePlugin({
project: "/project.inlang",
outdir: "/test-output",
fs: fs,
});
const mockContext = {
addWatchFile: () => { },
};
await expect(plugin.buildStart?.call(mockContext)).resolves.toBeUndefined();
});
test("vite plugin does not throw on compilation errors in development", async () => {
const { paraglideVitePlugin } = await import("../bundler-plugins/vite.js");
process.env.NODE_ENV = "development";
// Use memfs with no project (simulates missing project)
const fs = memfs().fs;
const plugin = paraglideVitePlugin({
project: "/non-existent-project.inlang",
outdir: "/test-output",
fs: fs,
});
const mockContext = {
addWatchFile: () => { },
};
// In development mode - should catch errors and NOT throw
await expect(plugin.buildStart?.call(mockContext)).resolves.toBeUndefined();
});
test("vite plugin throws on compilation errors at build time", async () => {
const { paraglideVitePlugin } = await import("../bundler-plugins/vite.js");
process.env.NODE_ENV = "production";
// Use memfs with no project (simulates missing project)
const fs = memfs().fs;
const plugin = paraglideVitePlugin({
project: "/non-existent-project.inlang",
outdir: "/test-output",
fs: fs,
});
const mockContext = {
addWatchFile: () => { },
};
// In production mode - should throw the error
await expect(plugin.buildStart?.call(mockContext)).rejects.toThrow();
});
// Regression test for https://github.com/opral/inlang-paraglide-js/issues/659:
// the bundler plugins used to wipe `outdir` on every fresh process, racing
// concurrent reads (SSR/prerender modules, sibling Vite processes, the
// config-watcher reload) and producing ENOENT/MISSING_EXPORT/ERR_LOAD_URL.
test("vite plugin does not wipe outdir during first buildStart (#659)", async () => {
const { paraglideVitePlugin: vitePlugin } = await import("../bundler-plugins/vite.js");
const project = await loadProjectInMemory({
blob: await newProject({
settings: {
baseLocale: "en",
locales: ["en", "de", "fr"],
},
}),
});
const fs = memfs().fs;
await saveProjectToDirectory({
project,
path: "/project.inlang",
fs: fs.promises,
});
// Pre-seed outdir with a sentinel file mimicking a prior compilation
// still on disk from an earlier process.
await fs.promises.mkdir("/test-output", { recursive: true });
await fs.promises.writeFile("/test-output/__sentinel__.txt", "kept");
const rmSpy = vi.spyOn(fs.promises, "rm");
const plugin = vitePlugin({
project: "/project.inlang",
outdir: "/test-output",
cleanOutdir: true,
fs: fs,
});
const mockContext = { addWatchFile: () => { } };
await plugin.buildStart?.call(mockContext);
// The plugin must not recursively remove the outdir during the first
// compile, even though previousCompilation is undefined. Concurrent
// readers may be holding file handles into outdir.
for (const call of rmSpy.mock.calls) {
expect(call[0]).not.toBe("/test-output");
}
});
test("vite plugin warm-restart writes nothing when inputs unchanged (#659)", async () => {
const project = await loadProjectInMemory({
blob: await newProject({
settings: {
baseLocale: "en",
locales: ["en", "de", "fr"],
},
}),
});
const fs = memfs().fs;
await saveProjectToDirectory({
project,
path: "/project.inlang",
fs: fs.promises,
});
const mockContext = { addWatchFile: () => { } };
// First process: compile and persist files to outdir.
{
const { paraglideVitePlugin: vitePlugin } = await import("../bundler-plugins/vite.js");
const plugin = vitePlugin({
project: "/project.inlang",
outdir: "/test-output",
fs: fs,
});
await plugin.buildStart?.call(mockContext);
}
// Second process: simulate a fresh node process by resetting modules so
// the module-scoped `previousCompilation` is undefined again. The plugin
// should seed from on-disk hashes and write zero files (no race window).
vi.resetModules();
const { paraglideVitePlugin: vitePluginFresh } = await import("../bundler-plugins/vite.js");
const plugin2 = vitePluginFresh({
project: "/project.inlang",
outdir: "/test-output",
fs: fs,
});
const writeFileSpy = vi.spyOn(fs.promises, "writeFile");
await plugin2.buildStart?.call(mockContext);
expect(writeFileSpy).not.toHaveBeenCalled();
});
// Regression test for https://github.com/opral/paraglide-js/issues/693:
// `vite build` fires buildStart once per environment (client, ssr) and each
// run used to do a full compile() — project loading + message compilation —
// even though the inputs hadn't changed.
test("vite plugin skips compile() when buildStart fires again with unchanged inputs (#693)", async () => {
const actualCompileModule = await vi.importActual("../compiler/compile.js");
const compileSpy = vi.fn(actualCompileModule.compile);
vi.doMock("../compiler/compile.js", () => ({
...actualCompileModule,
compile: compileSpy,
}));
try {
const { paraglideVitePlugin: vitePlugin } = await import("../bundler-plugins/vite.js");
const project = await loadProjectInMemory({
blob: await newProject({
settings: {
baseLocale: "en",
locales: ["en", "de", "fr"],
},
}),
});
const fs = memfs().fs;
await saveProjectToDirectory({
project,
path: "/project.inlang",
fs: fs.promises,
});
const plugin = vitePlugin({
project: "/project.inlang",
outdir: "/test-output",
fs: fs,
});
const mockContext = { addWatchFile: () => { } };
// First environment (e.g. client) compiles.
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(1);
// Second environment (e.g. ssr) — inputs unchanged, compile skipped.
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(1);
// Changing an input file invalidates the digest and recompiles.
const settingsPath = "/project.inlang/settings.json";
const settings = JSON.parse(await fs.promises.readFile(settingsPath, "utf-8"));
settings.locales = ["en", "de", "fr", "es"];
await fs.promises.writeFile(settingsPath, JSON.stringify(settings));
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(2);
}
finally {
vi.doUnmock("../compiler/compile.js");
}
});
// A new file added next to tracked inputs must invalidate the digest via
// the directory-listing part of the hash, not only edits to known files.
test("vite plugin recompiles when a file is added to a tracked directory (#693)", async () => {
const actualCompileModule = await vi.importActual("../compiler/compile.js");
const compileSpy = vi.fn(actualCompileModule.compile);
vi.doMock("../compiler/compile.js", () => ({
...actualCompileModule,
compile: compileSpy,
}));
try {
const { paraglideVitePlugin: vitePlugin } = await import("../bundler-plugins/vite.js");
const project = await loadProjectInMemory({
blob: await newProject({
settings: { baseLocale: "en", locales: ["en", "de"] },
}),
});
const fs = memfs().fs;
await saveProjectToDirectory({
project,
path: "/project.inlang",
fs: fs.promises,
});
const plugin = vitePlugin({
project: "/project.inlang",
outdir: "/test-output",
fs: fs,
});
const mockContext = { addWatchFile: () => { } };
await plugin.buildStart?.call(mockContext);
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(1);
// No tracked file changed, but the listing of a tracked directory did.
await fs.promises.writeFile("/project.inlang/added-later.txt", "new");
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(2);
}
finally {
vi.doUnmock("../compiler/compile.js");
}
});
// After a failed compile the stored digest must not be reused: restoring an
// input to its previous content has to trigger a real recompile, because the
// failed attempt may have left no (or partial) output.
test("vite plugin does not reuse a digest from before a failed compile (#693)", async () => {
process.env.NODE_ENV = "development";
const actualCompileModule = await vi.importActual("../compiler/compile.js");
const compileSpy = vi.fn(actualCompileModule.compile);
vi.doMock("../compiler/compile.js", () => ({
...actualCompileModule,
compile: compileSpy,
}));
try {
const { paraglideVitePlugin: vitePlugin } = await import("../bundler-plugins/vite.js");
const project = await loadProjectInMemory({
blob: await newProject({
settings: { baseLocale: "en", locales: ["en", "de"] },
}),
});
const fs = memfs().fs;
await saveProjectToDirectory({
project,
path: "/project.inlang",
fs: fs.promises,
});
const plugin = vitePlugin({
project: "/project.inlang",
outdir: "/test-output",
fs: fs,
});
const mockContext = { addWatchFile: () => { } };
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(1);
const settingsPath = "/project.inlang/settings.json";
const originalSettings = await fs.promises.readFile(settingsPath, "utf-8");
// Break the settings file — the compile attempt fails (dev mode
// swallows the error) and must clear the stored digest.
await fs.promises.writeFile(settingsPath, "{ not json");
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(2);
// Restore the exact original content. The digest now matches the one
// from before the failure — it must have been cleared, so this
// buildStart has to compile again instead of skipping.
await fs.promises.writeFile(settingsPath, originalSettings);
await plugin.buildStart?.call(mockContext);
expect(compileSpy).toHaveBeenCalledTimes(3);
}
finally {
vi.doUnmock("../compiler/compile.js");
}
});
// Two plugin instances with different filesystems must not share the tracked
// fs wrapper or cached compilation state — each compiles into its own fs.
test("vite plugin instances with different fs compile against their own fs (#693)", async () => {
const { paraglideVitePlugin: vitePlugin } = await import("../bundler-plugins/vite.js");
const makeProjectFs = async () => {
const project = await loadProjectInMemory({
blob: await newProject({
settings: { baseLocale: "en", locales: ["en"] },
}),
});
const fs = memfs().fs;
await saveProjectToDirectory({
project,
path: "/project.inlang",
fs: fs.promises,
});
return fs;
};
const fsA = await makeProjectFs();
const fsB = await makeProjectFs();
const mockContext = { addWatchFile: () => { } };
const pluginA = vitePlugin({
project: "/project.inlang",
outdir: "/test-output",
fs: fsA,
});
await pluginA.buildStart?.call(mockContext);
const pluginB = vitePlugin({
project: "/project.inlang",
outdir: "/test-output",
fs: fsB,
});
await pluginB.buildStart?.call(mockContext);
// B's output must exist in B's filesystem — with a shared tracked fs it
// would have been read from and written into A's filesystem instead.
const outputB = await fsB.promises.readdir("/test-output");
expect(outputB.length).toBeGreaterThan(0);
});
//# sourceMappingURL=unplugin.test.js.map