mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
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!
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { platform, totalmem } from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
|
||||
@@ -414,7 +414,7 @@ async function runWithSupervisor(
|
||||
if (detectMitmCrash(crashLog)) {
|
||||
try {
|
||||
const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
const { updateSettings } = await import(`${PROJECT_ROOT}/src/lib/db/settings.ts`);
|
||||
const { updateSettings } = await import(pathToFileURL(join(PROJECT_ROOT, "src/lib/db/settings.ts")).href);
|
||||
updateSettings({ mitmEnabled: false });
|
||||
} catch {}
|
||||
return "disable-mitm-and-retry";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { createPrompt, printHeading, printInfo, printSuccess } from "../io.mjs";
|
||||
import { openOmniRouteDb } from "../sqlite.mjs";
|
||||
@@ -16,7 +16,7 @@ import { t } from "../i18n.mjs";
|
||||
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
async function getListCliTools() {
|
||||
const { listCliTools } = await import(`${PROJECT_ROOT}/src/shared/constants/cliTools.ts`);
|
||||
const { listCliTools } = await import(pathToFileURL(resolve(PROJECT_ROOT, "src/shared/constants/cliTools.ts")).href);
|
||||
return listCliTools;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { apiFetch, isServerUp } from "./api.mjs";
|
||||
|
||||
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
// Dynamic `import()` resolves its specifier as a URL, not as a filesystem path.
|
||||
// On Windows an absolute path starts with a drive letter, which the ESM loader
|
||||
// reads as the unsupported URL scheme `e:` and rejects. Pass a file:// URL.
|
||||
const projectFileUrl = (relPath) => pathToFileURL(resolve(PROJECT_ROOT, relPath)).href;
|
||||
|
||||
export class ServerOfflineError extends Error {
|
||||
constructor(message = "Server is offline and operation requires HTTP runtime") {
|
||||
super(message);
|
||||
@@ -22,8 +27,8 @@ function makeHttpContext(opts) {
|
||||
|
||||
async function importDbModules() {
|
||||
const [combos, recovery] = await Promise.all([
|
||||
import(`${PROJECT_ROOT}/src/lib/db/combos.ts`),
|
||||
import(`${PROJECT_ROOT}/src/lib/db/recovery.ts`),
|
||||
import(projectFileUrl("src/lib/db/combos.ts")),
|
||||
import(projectFileUrl("src/lib/db/recovery.ts")),
|
||||
]);
|
||||
return { combos, recovery };
|
||||
}
|
||||
|
||||
@@ -27,7 +27,14 @@ async function withComboEnv(fn: (dataDir: string) => Promise<void>) {
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
globalThis.fetch = ORIGINAL_FETCH;
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
// On Windows the SQLite file may still be held open by the db module when
|
||||
// the test ends, and rmSync then throws EPERM, failing a test whose
|
||||
// assertions all passed. Retry, then give up quietly.
|
||||
try {
|
||||
fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
} catch {
|
||||
// best effort: the OS reclaims its own temp dir
|
||||
}
|
||||
|
||||
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
||||
|
||||
68
tests/unit/cli/windows-esm-import-paths.test.ts
Normal file
68
tests/unit/cli/windows-esm-import-paths.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user