fix(desktop): make the test loader work on Windows (#2758)

The resolve hook hands nextResolve absolute filesystem paths. Node's ESM
resolver requires URLs or relative specifiers: POSIX absolute paths
happen to be coerced, but a Windows path like C:\... parses as a URL
with protocol 'c:', so every desktop unit-test run on Windows dies
immediately with ERR_UNSUPPORTED_ESM_URL_SCHEME - on a clean tree,
before any test executes. CI never sees it (Linux runners).

Convert absolute paths to file:// URLs (pathToFileURL) at the three
nextResolve call sites. On POSIX the resulting URL is identical to what
node coerced before; on Windows the loader now works.

With this change the full desktop suite (318 files, 3487 tests) passes
on Windows 11 / node 24.14.1. Independently reported by another Windows
contributor in #2634's testing notes.


Claude-Session: https://claude.ai/code/session_01YFkHsUe1UUBBuvL81Zoe3n

---------

Signed-off-by: technicallybrantley <77166260+technicallybrantley@users.noreply.github.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
technicallybrantley
2026-07-27 13:00:55 -04:00
committed by GitHub
co-authored by Claude Fable 5 Will Pfleger
parent 545bb46b82
commit 8bb43d5191
+18 -4
View File
@@ -1,4 +1,4 @@
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import fs from "node:fs";
import path from "node:path";
import ts from "typescript";
@@ -13,6 +13,17 @@ const repoRoot = path.resolve(
"..",
);
// `nextResolve` requires specifiers to be URLs or relative paths. Passing an
// absolute filesystem path happens to work on POSIX (node coerces it), but on
// Windows an absolute path like `C:\...` parses as a URL with protocol `c:`
// and every test run dies with ERR_UNSUPPORTED_ESM_URL_SCHEME. Hand absolute
// paths to node as proper file:// URLs on all platforms.
function toFileSpecifier(candidatePath) {
return path.isAbsolute(candidatePath)
? pathToFileURL(candidatePath).href
: candidatePath;
}
function resolveSourcePath(basePath) {
// Existence decides, not path.extname — a dotted basename like
// `ProfileAvatarEditor.utils` (→ .utils.ts on disk) looks like an
@@ -76,7 +87,7 @@ export function resolve(specifier, context, nextResolve) {
}
if (specifier === "@features-manifest") {
const resolved = path.join(repoRoot, "preview-features.json");
return nextResolve(resolved, context);
return nextResolve(toFileSpecifier(resolved), context);
}
if (specifier.startsWith("@/")) {
const stripped = specifier.slice(2);
@@ -86,7 +97,10 @@ export function resolve(specifier, context, nextResolve) {
// Otherwise paths like `@/.../foo.mjs` would be coerced into `foo.mjs.ts`
// and fail to resolve.
const resolved = resolveSourcePath(`${srcRoot}/${stripped}`);
return nextResolve(resolved ?? `${srcRoot}/${stripped}`, context);
return nextResolve(
toFileSpecifier(resolved ?? `${srcRoot}/${stripped}`),
context,
);
}
// Resolve extensionless relative TS imports (e.g. `./parseImeta`) — the app's
// bundler adds the extension, but node's ESM resolver does not. Without this,
@@ -103,7 +117,7 @@ export function resolve(specifier, context, nextResolve) {
path.resolve(path.dirname(parentPath), specifier),
);
if (resolved) {
return nextResolve(resolved, context);
return nextResolve(toFileSpecifier(resolved), context);
}
return nextResolve(specifier, context);
}