diff --git a/apps/oxfmt/package.json b/apps/oxfmt/package.json index f392d2ba30718..3da7114770db5 100644 --- a/apps/oxfmt/package.json +++ b/apps/oxfmt/package.json @@ -14,7 +14,7 @@ "build-napi-test": "pnpm run build-napi --profile coverage", "build-napi-release": "pnpm run build-napi --release --features allocator", "build-js": "node scripts/build.js", - "test": "vitest --dir test run", + "test": "vitest run", "conformance": "node conformance/run.ts", "download-fixtures": "node conformance/download-fixtures.js", "generate-config-types": "node scripts/generate-config-types.ts" diff --git a/apps/oxfmt/src-js/cli.ts b/apps/oxfmt/src-js/cli.ts index 978a09286ff23..d68dec8ab2569 100644 --- a/apps/oxfmt/src-js/cli.ts +++ b/apps/oxfmt/src-js/cli.ts @@ -7,7 +7,7 @@ import { formatEmbeddedDoc, sortTailwindClasses, } from "./cli/worker-proxy"; -import { loadJsConfig } from "./cli/js_config"; +import { loadJsConfig } from "./cli/js_config/index"; // napi-JS `oxfmt` CLI entry point // See also `run_cli()` function in `./src/main_napi.rs` diff --git a/apps/oxfmt/src-js/cli/js_config.ts b/apps/oxfmt/src-js/cli/js_config/index.ts similarity index 86% rename from apps/oxfmt/src-js/cli/js_config.ts rename to apps/oxfmt/src-js/cli/js_config/index.ts index b81acdc15c328..23642256f705d 100644 --- a/apps/oxfmt/src-js/cli/js_config.ts +++ b/apps/oxfmt/src-js/cli/js_config/index.ts @@ -1,6 +1,6 @@ import { basename as pathBasename } from "node:path"; import { pathToFileURL } from "node:url"; -import { getUnsupportedTypeScriptModuleLoadHintForError } from "./node_version"; +import { getUnsupportedTypeScriptModuleLoadHint } from "./node_version"; const isObject = (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v); @@ -24,13 +24,11 @@ export async function loadJsConfig(path: string): Promise { const fileUrl = pathToFileURL(path); fileUrl.searchParams.set("cache", Date.now().toString()); - let config: unknown; - try { - ({ default: config } = await import(fileUrl.href)); - } catch (err) { - const unsupportedNodeHint = getUnsupportedTypeScriptModuleLoadHintForError(err, path); - throw unsupportedNodeHint ?? err; - } + const { default: config } = await import(fileUrl.href).catch((err) => { + const hint = getUnsupportedTypeScriptModuleLoadHint(err, path); + if (hint && err instanceof Error) err.message += `\n\n${hint}`; + throw err; + }); if (config === undefined) throw new Error("Configuration file has no default export."); diff --git a/apps/oxfmt/src-js/cli/js_config/node_version.ts b/apps/oxfmt/src-js/cli/js_config/node_version.ts new file mode 100644 index 0000000000000..b0c592aa2e3d3 --- /dev/null +++ b/apps/oxfmt/src-js/cli/js_config/node_version.ts @@ -0,0 +1,76 @@ +import { extname as pathExtname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const NODE_TYPESCRIPT_SUPPORT_RANGE = "^20.19.0 || >=22.12.0"; +const TS_MODULE_EXTENSIONS = new Set([".ts", ".mts", ".cts"]); + +export function getUnsupportedTypeScriptModuleLoadHint( + err: unknown, + specifier: string, + nodeVersion: string = process.version, +): string | null { + if (!isTypeScriptModuleSpecifier(specifier) || !isUnknownFileExtensionError(err)) return null; + + return `TypeScript config files require Node.js ${NODE_TYPESCRIPT_SUPPORT_RANGE}.\nDetected Node.js ${nodeVersion}.\nPlease upgrade Node.js or use a JSON config file instead.`; +} + +// --- + +function isTypeScriptModuleSpecifier(specifier: string): boolean { + const ext = pathExtname(normalizeModuleSpecifierPath(specifier)).toLowerCase(); + return TS_MODULE_EXTENSIONS.has(ext); +} + +function normalizeModuleSpecifierPath(specifier: string): string { + if (!specifier.startsWith("file:")) return specifier; + + try { + return fileURLToPath(specifier); + } catch { + return specifier; + } +} + +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); +} + +// --- + +if (import.meta.vitest) { + const { it, expect } = import.meta.vitest; + + 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("returns 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(getUnsupportedTypeScriptModuleLoadHint(err, "/tmp/oxfmt.config.ts", "v22.11.0")).toBe( + `TypeScript 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(getUnsupportedTypeScriptModuleLoadHint(err, "/tmp/oxfmt.config.ts")).toBeNull(); + + const unknownExtension = new TypeError('Unknown file extension ".ts"'); + expect( + getUnsupportedTypeScriptModuleLoadHint(unknownExtension, "/tmp/oxfmt.config.js"), + ).toBeNull(); + }); +} diff --git a/apps/oxfmt/src-js/cli/node_version.ts b/apps/oxfmt/src-js/cli/node_version.ts deleted file mode 100644 index 955aefceaaa6e..0000000000000 --- a/apps/oxfmt/src-js/cli/node_version.ts +++ /dev/null @@ -1,49 +0,0 @@ -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.`; -} diff --git a/apps/oxfmt/test/cli/js_config/node_version.test.ts b/apps/oxfmt/test/cli/js_config/node_version.test.ts deleted file mode 100644 index b387576d37fcd..0000000000000 --- a/apps/oxfmt/test/cli/js_config/node_version.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -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(); - }); -}); diff --git a/apps/oxfmt/tsconfig.json b/apps/oxfmt/tsconfig.json index 34ee727bb88df..b562f255554cd 100644 --- a/apps/oxfmt/tsconfig.json +++ b/apps/oxfmt/tsconfig.json @@ -6,7 +6,8 @@ "noEmit": true, "target": "ESNext", "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["vitest/importMeta"] }, "include": ["scripts", "conformance/*.ts", "src-js", "test/**/*.ts"], "exclude": ["test/**/fixtures"] diff --git a/apps/oxfmt/tsdown.config.ts b/apps/oxfmt/tsdown.config.ts index d4cf7b6570160..b7e651c263e36 100644 --- a/apps/oxfmt/tsdown.config.ts +++ b/apps/oxfmt/tsdown.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ outDir: "dist", shims: false, fixedExtension: false, + define: { "import.meta.vitest": "undefined" }, deps: { // Optional peer plugins that `prettier-plugin-tailwindcss` tries to dynamic import. // They are not installed and not needed for us, diff --git a/apps/oxfmt/vitest.config.ts b/apps/oxfmt/vitest.config.ts index e54090d76d406..07d891405926d 100644 --- a/apps/oxfmt/vitest.config.ts +++ b/apps/oxfmt/vitest.config.ts @@ -1,8 +1,9 @@ -import { configDefaults, defineConfig } from "vitest/config"; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - exclude: [...configDefaults.exclude], + include: ["./test/**/*.test.ts"], + includeSource: ["./src-js/**/*.ts"], snapshotFormat: { escapeString: false, printBasicPrototype: false,