Files
OmniRoute/tests/unit/cli/windows-esm-import-paths.test.ts
Paco Cartones e32b9264e8 fix(cli): resolve dynamic imports to file:// URLs so the DB fallback works on Windows (#11238)
Validated on the combined 12-PR batch board: cli-combo-command + windows-esm-import-paths suites pass, typecheck:core clean. pathToFileURL on the four dynamic-import call sites unbreaks the CLI offline DB fallback on Windows. Thank you @pacocartones!
2026-08-23 14:24:50 -03:00

69 lines
2.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
// Regression guard for the Windows-only ESM loader failure:
//
// Error: Only URLs with a scheme in: file, data, and node are supported by
// the default ESM loader. On Windows, absolute paths must be valid file://
// URLs. Received protocol 'e:'
//
// `import()` resolves its specifier as a URL. A POSIX absolute path like
// /home/x/src/lib/db/combos.ts happens to also be a valid relative URL, so
// interpolating it works by accident. A Windows absolute path is
// E:\checkout\src\lib\db\combos.ts, whose leading drive letter the loader
// parses as the URL scheme `e:` and rejects. Every such call site must go
// through pathToFileURL().
//
// This broke `omniroute combo list/create/delete/switch` on Windows whenever
// the CLI fell back to direct DB access with the server offline.
const CLI_DIR = path.join(PROJECT_ROOT, "bin", "cli");
function collectMjsFiles(dir: string): string[] {
const out: string[] = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...collectMjsFiles(full));
else if (entry.name.endsWith(".mjs")) out.push(full);
}
return out;
}
test("bin/cli never passes an interpolated absolute path to dynamic import()", () => {
// Matches import(`${ANY_ROOT_CONST}/...`) — a raw filesystem path, not a URL.
const badImport = /\bimport\(\s*`\$\{[A-Za-z_$][\w$]*\}\//;
const offenders: string[] = [];
for (const file of collectMjsFiles(CLI_DIR)) {
const source = fs.readFileSync(file, "utf8");
source.split(/\r?\n/).forEach((line, i) => {
if (badImport.test(line)) {
offenders.push(`${path.relative(PROJECT_ROOT, file)}:${i + 1}: ${line.trim()}`);
}
});
}
assert.deepEqual(
offenders,
[],
"dynamic import() of an interpolated absolute path fails on Windows; " +
`wrap the path in pathToFileURL(...).href instead:\n${offenders.join("\n")}`,
);
});
test("runtime.mjs resolves db modules to a file:// URL", async () => {
const source = fs.readFileSync(path.join(CLI_DIR, "runtime.mjs"), "utf8");
assert.match(source, /pathToFileURL/, "runtime.mjs must build file:// URLs for dynamic imports");
// The real proof: the db fallback modules actually load on this platform.
const runtime = await import(pathToFileURL(path.join(CLI_DIR, "runtime.mjs")).href);
const ctx = await runtime.withDb(async (c: { kind: string; db: unknown }) => c);
assert.equal(ctx.kind, "db");
assert.ok(ctx.db);
});