fix(core): make lingui macro transform match Vite's normalized paths on Windows - #2462
Conversation
…on Windows The dev-mode Lingui macro transform compared its id.startsWith(adminSourcePath) using a backslash-separated, launch-cwd-cased path against Vite's forward-slash, on-disk-cased module ids. On Windows this comparison silently failed for every admin source file, so macro compilation never ran and raw @lingui/*/macro imports shipped to the browser — throwing during hydration and leaving the admin UI hung on "Loading EmDash..." with no visible error. Also fixes a second, previously-masked bug in the same code path: a raw Windows drive-letter path passed to a dynamic import(), which Node's ESM loader rejects with ERR_UNSUPPORTED_ESM_URL_SCHEME. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 2cdd60d The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
This is the right fix for a real, narrow problem. Normalizing adminSourcePath to Vite’s forward-slash/true-casing convention and wrapping the Babel core resolution in pathToFileURL directly address the two Windows-only failure modes described in #1862. The change is small, scoped to the dev-mode source-aliasing path, and doesn’t affect production builds or external-app dist usage.
I checked the diff, the full vite-config.ts implementation, the existing vite-config tests, and the changeset against AGENTS.md. The code itself looks correct and there are no obvious logic or security regressions. The main issues are in the changeset and test commentary:
- The changeset is written as PR prose and includes internal mechanics (
Windows-style backslash path,dynamic import(),ERR_UNSUPPORTED_ESM_URL_SCHEME, etc.).AGENTS.mdsays changesets are user-facing release notes and should leave out implementation details. - The new regression test is a valid Windows guard, but its comment is long and duplicative of the
it()title and the source comment. It also won’t fail on non-Windows CI if the normalization is reverted, so its protective value depends on running tests on Windows.
I’m leaving this as comment rather than request_changes because the underlying fix is sound and the findings are polish/test-hygiene items, not bugs that would ship broken behavior.
| "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. |
There was a problem hiding this comment.
[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.
| 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. |
| // 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. |
There was a problem hiding this comment.
[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.
| // 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", () => { |
| // 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", () => { | ||
| const config = buildConfig(monorepoDemoRoot); |
There was a problem hiding this comment.
[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.
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
What does this PR do?
On Windows, the admin UI can hang forever on "Loading EmDash…" in local dev with no visible error — no console error, no failed network request, nothing. This reproduces reliably in this environment via the documented
pnpm devcontributor workflow (aliasing@emdash-cms/adminto source for HMR).Root cause, found via live reproduction with browser devtools:
The dev-mode
linguiMacroPlugininpackages/core/src/astro/integration/vite-config.tsgates its Babel transform onid.startsWith(adminSourcePath).adminSourcePathis built withpath.resolve, which on Windows returns a backslash-separated path in whatever casing the process happened to launch with. Vite, however, normalizes moduleids it passes to plugin hooks to forward slashes and to the file's true on-disk casing. Whenever those two don't literally match — which is easy to hit (e.g. any case difference between how a shell was invoked and the real directory casing) — thestartsWithcheck silently fails for every admin source file, so the Lingui macro compilation step never runs. Raw, uncompiled@lingui/react/macro/@lingui/core/macroimports then ship straight to the browser. Lingui's macro runtime throws by design when it isn't run through the Babel transform, and Astro'sastro-islandhydration error handler swallows that throw to the console with no user-visible fallback — so the page is left stuck on its static pre-hydration "Loading EmDash…" placeholder forever.This normalizes
adminSourcePathto match Vite's own convention (realpathSyncfor casing, forward slashes for separators), so the comparison actually works.Fixing that exposed a second, previously-unreached bug in the same function: once the transform actually runs, it does
await import(babelCorePath)with a raw filesystem path resolved viarequire.resolve. Node's ESM loader rejects that on Windows withERR_UNSUPPORTED_ESM_URL_SCHEME(absolute paths need afile://URL). Fixed by wrapping the resolved path withpathToFileURL(...).href.I believe this is the underlying cause of #1862 ("React admin UI silently hangs on 'Loading EmDash...' — createRoot().render() runs but produces zero DOM output, zero errors, zero network activity") — that symptom (including "zero errors" for a reporter who likely didn't check the console) matches exactly what I observed before this fix.
Verified by reproducing live: before the fix, the admin UI hangs indefinitely with a
[astro-island] Error hydrating ... Could not resolve "babel-plugin-macros"console error; after the fix, the dashboard loads and renders normally with real API calls succeeding, no console errors.Closes #1862
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
Before fix (hung, console error):
After fix: admin dashboard renders normally,
/_emdash/api/manifest,/_emdash/api/auth/me,/_emdash/api/dashboardall return 200, zero console errors.