diff --git a/apps/oxlint/src-js/js_config.ts b/apps/oxlint/src-js/js_config.ts index 0e98f8954b177..b8fd3376c2aa9 100644 --- a/apps/oxlint/src-js/js_config.ts +++ b/apps/oxlint/src-js/js_config.ts @@ -2,6 +2,7 @@ import { basename as pathBasename } from "node:path"; import { getErrorMessage } from "./utils/utils.ts"; import { DateNow, JSONStringify } from "./utils/globals.ts"; +import { getUnsupportedTypeScriptModuleLoadHintForError } from "./utils/node_version.ts"; interface JsConfigResult { path: string; @@ -147,7 +148,15 @@ export async function loadJsConfigs(paths: string[]): Promise { if (result.status === "fulfilled") { successes.push(result.value); } else { - errors.push({ path: paths[i], error: getErrorMessage(result.reason) }); + const path = paths[i]; + const unsupportedNodeHint = getUnsupportedTypeScriptModuleLoadHintForError( + result.reason, + path, + ); + errors.push({ + path, + error: unsupportedNodeHint ?? getErrorMessage(result.reason), + }); } } diff --git a/apps/oxlint/src-js/utils/node_version.ts b/apps/oxlint/src-js/utils/node_version.ts new file mode 100644 index 0000000000000..b24297f4ac33d --- /dev/null +++ b/apps/oxlint/src-js/utils/node_version.ts @@ -0,0 +1,41 @@ +import { extname as pathExtname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getErrorMessage } from "./utils.ts"; + +export const NODE_TYPESCRIPT_SUPPORT_RANGE = "^20.19.0 || >=22.18.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); +} + +export function getUnsupportedTypeScriptModuleLoadHintForError( + err: unknown, + specifier: string, + nodeVersion: string = process.version, +): string | null { + if (!isTypeScriptModuleSpecifier(specifier) || !isUnknownFileExtensionError(err)) { + return null; + } + + return `${getErrorMessage(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.`; +}