Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lingui-macro-windows-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"emdash": patch
---

Fixes the admin UI hanging indefinitely on "Loading EmDash…" in local dev on Windows, with no visible error. The Lingui macro compiler used to build admin source in dev mode compared file paths using a Windows-style backslash path against Vite's forward-slash-normalized module ids, so the comparison always failed and macro compilation silently never ran — shipping raw, uncompiled `@lingui/*/macro` imports to the browser, which then threw during hydration. A second, previously-masked bug in the same code path passed a raw Windows drive-letter path to a dynamic `import()`, which Node's ESM loader rejects.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] This changeset reads like a PR description: it walks through the root cause (Windows-style backslash path, Vite's forward-slash-normalized module ids, ERR_UNSUPPORTED_ESM_URL_SCHEME) rather than telling a user what changed. AGENTS.md says changesets are release notes that should describe the observable effect and leave out internal mechanics.

Suggested change
Fixes the admin UI hanging indefinitely on "Loading EmDash…" in local dev on Windows, with no visible error. The Lingui macro compiler used to build admin source in dev mode compared file paths using a Windows-style backslash path against Vite's forward-slash-normalized module ids, so the comparison always failed and macro compilation silently never ran — shipping raw, uncompiled `@lingui/*/macro` imports to the browser, which then threw during hydration. A second, previously-masked bug in the same code path passed a raw Windows drive-letter path to a dynamic `import()`, which Node's ESM loader rejects.
Fixes the admin UI hanging indefinitely on "Loading EmDash…" during local dev on Windows.

20 changes: 14 additions & 6 deletions packages/core/src/astro/integration/vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* Vite-specific configuration for EmDash.
*/

import { existsSync } from "node:fs";
import { existsSync, realpathSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, isAbsolute, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

import type { AstroConfig } from "astro";
import type { Plugin } from "vite";
Expand Down Expand Up @@ -79,7 +79,9 @@ const LOCALE_MESSAGES_RE = /[/\\]([a-z]{2}(?:-[A-Z]{2})?)[/\\]messages\.mjs$/;
function linguiMacroPlugin(adminSourcePath: string, adminDistPath: string): Plugin {
// Resolve @babel/core from admin's devDependencies, not core's.
const adminRequire = createRequire(resolve(adminDistPath, "index.js"));
const babelCorePath = adminRequire.resolve("@babel/core");
// import() requires a file:// URL for absolute paths on Windows — a raw
// drive-letter path throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
const babelCoreUrl = pathToFileURL(adminRequire.resolve("@babel/core")).href;

return {
name: "emdash-lingui-macro",
Expand All @@ -96,7 +98,7 @@ function linguiMacroPlugin(adminSourcePath: string, adminDistPath: string): Plug
},
async transform(code, id) {
if (!id.startsWith(adminSourcePath) || !code.includes("@lingui")) return;
const { transformAsync } = (await import(babelCorePath)) as typeof import("@babel/core");
const { transformAsync } = (await import(babelCoreUrl)) as typeof import("@babel/core");
const result = await transformAsync(code, {
filename: id,
plugins: ["@lingui/babel-plugin-lingui-macro"],
Expand Down Expand Up @@ -143,7 +145,13 @@ function resolveAdminSource(projectRoot: string): string | undefined {

try {
if (existsSync(srcEntry) && isInside(repoRoot, projectRoot)) {
return resolve(packageRoot, "src");
// Vite normalizes module ids to forward slashes and their on-disk
// casing before calling plugin hooks; on Windows this can differ
// from path.resolve's backslash output and from the casing the
// current process happened to launch with. Match both normalizations
// here so id.startsWith(adminSourcePath) in linguiMacroPlugin
// doesn't silently fail the prefix check.
return realpathSync(resolve(packageRoot, "src")).split(sep).join("/");
}
} catch {
// Not in local repo — fall back to dist
Expand Down
13 changes: 13 additions & 0 deletions packages/core/tests/unit/astro/vite-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ describe("createViteConfig admin aliasing", () => {
expect(replacement).toMatch(adminSourcePattern);
});

// Regression: Vite normalizes module ids to forward slashes (and, on
// case-insensitive filesystems, to their on-disk casing) before calling
// plugin hooks. The Lingui macro transform compares its own id against
// this same alias path with a plain id.startsWith(adminSourcePath) check,
// so a backslash-separated path here makes that check always fail —
// silently skipping macro compilation for every admin source file.
Comment on lines +57 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This block is a detailed regression narrative that largely restates the it() title and the production-code comment. Under AGENTS.md, comments should explain a non-obvious "why" to a future reader, not summarize the PR. The test name is already descriptive, so the comment can be removed.

Suggested change
// Regression: Vite normalizes module ids to forward slashes (and, on
// case-insensitive filesystems, to their on-disk casing) before calling
// plugin hooks. The Lingui macro transform compares its own id against
// this same alias path with a plain id.startsWith(adminSourcePath) check,
// so a backslash-separated path here makes that check always fail —
// silently skipping macro compilation for every admin source file.
it("returns the admin source path with forward slashes only", () => {

it("returns the admin source path with forward slashes only", () => {
const config = buildConfig(monorepoDemoRoot);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This assertion only guards the regression on Windows. On Unix CI the path is already forward-slash, so reverting the normalization would still pass here and the bug could silently regress again. Consider either gating the test to Windows (it.skipIf(process.platform !== "win32")) or adding a stronger assertion that exercises the normalization directly, so the test fails on any platform if the transform comparison breaks.

const replacement = getAdminAliasReplacement(config);

expect(replacement).not.toMatch(/\\/);
});

it("uses built admin dist for external app dev", () => {
const config = buildConfig(externalProjectRoot);
const replacement = getAdminAliasReplacement(config);
Expand Down
Loading