Skip to content
Merged
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
9 changes: 8 additions & 1 deletion apps/oxfmt/src-js/cli/js_config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { basename as pathBasename } from "node:path";
import { pathToFileURL } from "node:url";
import { getUnsupportedTypeScriptModuleLoadHintForError } from "./node_version";

const isObject = (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v);

Expand All @@ -23,7 +24,13 @@ export async function loadJsConfig(path: string): Promise<object | null> {
const fileUrl = pathToFileURL(path);
fileUrl.searchParams.set("cache", Date.now().toString());

const { default: config } = await import(fileUrl.href);
let config: unknown;
try {
({ default: config } = await import(fileUrl.href));
} catch (err) {
const unsupportedNodeHint = getUnsupportedTypeScriptModuleLoadHintForError(err, path);
throw unsupportedNodeHint ?? err;
}

if (config === undefined) throw new Error("Configuration file has no default export.");

Expand Down
49 changes: 49 additions & 0 deletions apps/oxfmt/src-js/cli/node_version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { extname as pathExtname } from "node:path";
import { fileURLToPath } from "node:url";

export const NODE_TYPESCRIPT_SUPPORT_RANGE = "^20.19.0 || >=22.12.0";

const TS_MODULE_EXTENSIONS = new Set([".ts", ".mts", ".cts"]);

function normalizeModuleSpecifierPath(specifier: string): string {
if (!specifier.startsWith("file:")) return specifier;

try {
return fileURLToPath(specifier);
} catch {
return specifier;
}
}

export function isTypeScriptModuleSpecifier(specifier: string): boolean {
const ext = pathExtname(normalizeModuleSpecifierPath(specifier)).toLowerCase();
return TS_MODULE_EXTENSIONS.has(ext);
}

function isUnknownFileExtensionError(err: unknown): boolean {
if ((err as { code?: unknown })?.code === "ERR_UNKNOWN_FILE_EXTENSION") return true;

const message = (err as { message?: unknown })?.message;
return typeof message === "string" && /unknown(?: or unsupported)? file extension/i.test(message);
}

function getErrorSummary(err: unknown): string {
if (err instanceof Error) return String(err);

const message = (err as { message?: unknown })?.message;
if (typeof message === "string" && message !== "") return message;

return "Unknown error";
}

export function getUnsupportedTypeScriptModuleLoadHintForError(
err: unknown,
specifier: string,
nodeVersion: string = process.version,
): string | null {
if (!isTypeScriptModuleSpecifier(specifier) || !isUnknownFileExtensionError(err)) {
return null;
}

return `${getErrorSummary(err)}\n\nTypeScript config files require Node.js ${NODE_TYPESCRIPT_SUPPORT_RANGE}.\nDetected Node.js ${nodeVersion}.\nPlease upgrade Node.js or use a JSON config file instead.`;
}
41 changes: 41 additions & 0 deletions apps/oxfmt/test/cli/js_config/node_version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import {
NODE_TYPESCRIPT_SUPPORT_RANGE,
getUnsupportedTypeScriptModuleLoadHintForError,
isTypeScriptModuleSpecifier,
} from "../../../src-js/cli/node_version";

describe("node_version", () => {
it("detects supported TypeScript config specifiers", () => {
expect(isTypeScriptModuleSpecifier("/tmp/oxfmt.config.ts")).toBe(true);
expect(isTypeScriptModuleSpecifier("/tmp/oxfmt.config.mts")).toBe(true);
expect(isTypeScriptModuleSpecifier("/tmp/oxfmt.config.cts")).toBe(true);
expect(isTypeScriptModuleSpecifier("file:///tmp/oxfmt.config.ts")).toBe(true);
expect(isTypeScriptModuleSpecifier("/tmp/oxfmt.config.js")).toBe(false);
});

it("adds a node version hint for unsupported TypeScript module loading", () => {
const err = new TypeError(
'Unknown file extension ".ts" for /tmp/oxfmt.config.ts',
) as TypeError & {
code?: string;
};
err.code = "ERR_UNKNOWN_FILE_EXTENSION";

expect(
getUnsupportedTypeScriptModuleLoadHintForError(err, "/tmp/oxfmt.config.ts", "v22.11.0"),
).toBe(
`TypeError: Unknown file extension ".ts" for /tmp/oxfmt.config.ts\n\nTypeScript config files require Node.js ${NODE_TYPESCRIPT_SUPPORT_RANGE}.\nDetected Node.js v22.11.0.\nPlease upgrade Node.js or use a JSON config file instead.`,
);
});

it("does not add the hint for non-TypeScript specifiers or unrelated errors", () => {
const err = new Error("Cannot find package");
expect(getUnsupportedTypeScriptModuleLoadHintForError(err, "/tmp/oxfmt.config.ts")).toBeNull();

const unknownExtension = new TypeError('Unknown file extension ".ts"');
expect(
getUnsupportedTypeScriptModuleLoadHintForError(unknownExtension, "/tmp/oxfmt.config.js"),
).toBeNull();
});
});
Loading