From 4ea67decf50f753b42278191c9926c59aedc14d1 Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Thu, 12 Mar 2026 04:56:57 +0000 Subject: [PATCH] fix(oxlint,oxfmt): Skip `vite.config.ts` exports `defineConfig(fn)` (#20260) `vite.config.ts` may return function from `defineConfig(({ mode }) => ({ ... }))`, but we can't evaluate. Just skip it like no `.lint|fmt` config. --- apps/oxfmt/src-js/cli/js_config.ts | 16 ++++++++++---- .../__snapshots__/vite_config.test.ts.snap | 15 +++++++++++++ .../fixtures/skip_fn_export/test.ts | 1 + .../fixtures/skip_fn_export/vite.config.ts | 5 +++++ .../test/cli/vite_config/vite_config.test.ts | 6 ++++++ apps/oxlint/src-js/js_config.ts | 21 +++++++++++++------ .../vite_config_fn_export/files/test.js | 1 + .../vite_config_fn_export/options.json | 3 +++ .../vite_config_fn_export/output.snap.md | 19 +++++++++++++++++ .../vite_config_fn_export/vite.config.ts | 5 +++++ 10 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/test.ts create mode 100644 apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/vite.config.ts create mode 100644 apps/oxlint/test/fixtures/vite_config_fn_export/files/test.js create mode 100644 apps/oxlint/test/fixtures/vite_config_fn_export/options.json create mode 100644 apps/oxlint/test/fixtures/vite_config_fn_export/output.snap.md create mode 100644 apps/oxlint/test/fixtures/vite_config_fn_export/vite.config.ts diff --git a/apps/oxfmt/src-js/cli/js_config.ts b/apps/oxfmt/src-js/cli/js_config.ts index 494d86e499794..e7822b255223c 100644 --- a/apps/oxfmt/src-js/cli/js_config.ts +++ b/apps/oxfmt/src-js/cli/js_config.ts @@ -1,6 +1,8 @@ import { basename as pathBasename } from "node:path"; import { pathToFileURL } from "node:url"; +const isObject = (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v); + const VITE_CONFIG_NAME = "vite.config.ts"; const VITE_OXFMT_CONFIG_FIELD = "fmt"; @@ -23,17 +25,19 @@ export async function loadJsConfig(path: string): Promise { const { default: config } = await import(fileUrl.href); if (config === undefined) throw new Error(`Configuration file has no default export: ${path}`); - if (typeof config !== "object" || config === null || Array.isArray(config)) { - throw new Error(`Configuration file must have a default export that is an object: ${path}`); - } // Vite config: extract `.fmt` field if (pathBasename(path) === VITE_CONFIG_NAME) { + // NOTE: Vite configs may export a function via `defineConfig(() => ({ ... }))`, + // but we don't know the arguments to call the function. + // Treat non-object exports as "no config" and skip. + if (!isObject(config)) return null; + const fmtConfig = (config as Record)[VITE_OXFMT_CONFIG_FIELD]; // NOTE: return `null` if missing (signals "skip" to Rust side) if (fmtConfig === undefined) return null; - if (typeof fmtConfig !== "object" || fmtConfig === null || Array.isArray(fmtConfig)) { + if (!isObject(fmtConfig)) { throw new Error( `The \`${VITE_OXFMT_CONFIG_FIELD}\` field in the default export must be an object: ${path}`, ); @@ -41,5 +45,9 @@ export async function loadJsConfig(path: string): Promise { return fmtConfig; } + if (!isObject(config)) { + throw new Error(`Configuration file must have a default export that is an object: ${path}`); + } + return config; } diff --git a/apps/oxfmt/test/cli/vite_config/__snapshots__/vite_config.test.ts.snap b/apps/oxfmt/test/cli/vite_config/__snapshots__/vite_config.test.ts.snap index 08f8e030aba0a..922f266a834c9 100644 --- a/apps/oxfmt/test/cli/vite_config/__snapshots__/vite_config.test.ts.snap +++ b/apps/oxfmt/test/cli/vite_config/__snapshots__/vite_config.test.ts.snap @@ -47,6 +47,21 @@ Finished in ms on 1 files using 1 threads. --------------------" `; +exports[`vite_config > skip: auto-discovered vite.config.ts with function export uses defaults 1`] = ` +"-------------------- +arguments: --check test.ts +working directory: vite_config/fixtures/skip_fn_export +exit code: 0 +--- STDOUT --------- +Checking formatting... + +All matched files use the correct format. +Finished in ms on 1 files using 1 threads. +--- STDERR --------- + +--------------------" +`; + exports[`vite_config > skip: auto-discovered vite.config.ts without fmt field uses defaults 1`] = ` "-------------------- arguments: --check test.ts diff --git a/apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/test.ts b/apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/test.ts new file mode 100644 index 0000000000000..54b82a09ad543 --- /dev/null +++ b/apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/test.ts @@ -0,0 +1 @@ +const a = 1; diff --git a/apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/vite.config.ts b/apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/vite.config.ts new file mode 100644 index 0000000000000..69a08fbf5deab --- /dev/null +++ b/apps/oxfmt/test/cli/vite_config/fixtures/skip_fn_export/vite.config.ts @@ -0,0 +1,5 @@ +const defineConfig = (config: unknown) => config; + +export default defineConfig(() => ({ + plugins: [], +})); diff --git a/apps/oxfmt/test/cli/vite_config/vite_config.test.ts b/apps/oxfmt/test/cli/vite_config/vite_config.test.ts index c2d81a7e05b37..d8edf11b7b3bd 100644 --- a/apps/oxfmt/test/cli/vite_config/vite_config.test.ts +++ b/apps/oxfmt/test/cli/vite_config/vite_config.test.ts @@ -34,6 +34,12 @@ describe("vite_config", () => { expect(snapshot).toMatchSnapshot(); }); + it("skip: auto-discovered vite.config.ts with function export uses defaults", async () => { + const cwd = join(fixturesDir, "skip_fn_export"); + const snapshot = await runAndSnapshot(cwd, [["--check", "test.ts"]]); + expect(snapshot).toMatchSnapshot(); + }); + it("priority: oxfmt.config.ts takes precedence over vite.config.ts", async () => { // `oxfmt.config.ts` has `semi: false`, `vite.config.ts` has `semi: true` // oxfmt.config.ts should win, so `const a = 1;` (with semicolon) should be flagged diff --git a/apps/oxlint/src-js/js_config.ts b/apps/oxlint/src-js/js_config.ts index 63628d744b500..2010e71d4d749 100644 --- a/apps/oxlint/src-js/js_config.ts +++ b/apps/oxlint/src-js/js_config.ts @@ -9,6 +9,8 @@ interface JsConfigResult { config: unknown; // Will be validated as Oxlintrc on Rust side, `null` means "skip this config" } +const isObject = (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v); + const VITE_CONFIG_NAME = "vite.config.ts"; const VITE_OXLINT_CONFIG_FIELD = "lint"; @@ -57,7 +59,7 @@ function validateConfigExtends(root: object): void { } for (let i = 0; i < maybeExtends.length; i++) { const item = maybeExtends[i]; - if (typeof item !== "object" || item === null || Array.isArray(item)) { + if (!isObject(item)) { throw new Error( `\`extends[${i}]\` must be a config object (strings/paths are not supported).`, ); @@ -106,19 +108,22 @@ export async function loadJsConfigs(paths: string[]): Promise { throw new Error(`Configuration file has no default export.`); } - if (typeof config !== "object" || config === null || Array.isArray(config)) { - throw new Error(`Configuration file must have a default export that is an object.`); - } - // Vite config: extract `.lint` field, skip `defineConfig()` validation if (pathBasename(path) === VITE_CONFIG_NAME) { + // NOTE: Vite configs may export a function via `defineConfig(() => ({ ... }))`, + // but we don't know the arguments to call the function. + // Treat non-object exports as "no config" and skip. + if (!isObject(config)) { + return { path, config: null }; + } + const lintConfig = (config as Record)[VITE_OXLINT_CONFIG_FIELD]; // NOTE: return `null` if `.lint` is missing which signals "skip" this if (lintConfig === undefined) { return { path, config: null }; } - if (typeof lintConfig !== "object" || lintConfig === null || Array.isArray(lintConfig)) { + if (!isObject(lintConfig)) { throw new Error( `The \`${VITE_OXLINT_CONFIG_FIELD}\` field in the default export must be an object.`, ); @@ -127,6 +132,10 @@ export async function loadJsConfigs(paths: string[]): Promise { return { path, config: lintConfig }; } + if (!isObject(config)) { + throw new Error(`Configuration file must have a default export that is an object.`); + } + if (!isDefineConfig(config)) { throw new Error( `Configuration file must wrap its default export with defineConfig() from "oxlint".`, diff --git a/apps/oxlint/test/fixtures/vite_config_fn_export/files/test.js b/apps/oxlint/test/fixtures/vite_config_fn_export/files/test.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_fn_export/files/test.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/test/fixtures/vite_config_fn_export/options.json b/apps/oxlint/test/fixtures/vite_config_fn_export/options.json new file mode 100644 index 0000000000000..c6d966f1b525b --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_fn_export/options.json @@ -0,0 +1,3 @@ +{ + "singleThread": true +} diff --git a/apps/oxlint/test/fixtures/vite_config_fn_export/output.snap.md b/apps/oxlint/test/fixtures/vite_config_fn_export/output.snap.md new file mode 100644 index 0000000000000..d5673ed74d025 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_fn_export/output.snap.md @@ -0,0 +1,19 @@ +# Exit code +0 + +# stdout +``` + ! eslint(no-debugger): `debugger` statement is not allowed + ,-[files/test.js:1:1] + 1 | debugger; + : ^^^^^^^^^ + `---- + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in Xms on 1 file with 93 rules using X threads. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/vite_config_fn_export/vite.config.ts b/apps/oxlint/test/fixtures/vite_config_fn_export/vite.config.ts new file mode 100644 index 0000000000000..69a08fbf5deab --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_fn_export/vite.config.ts @@ -0,0 +1,5 @@ +const defineConfig = (config: unknown) => config; + +export default defineConfig(() => ({ + plugins: [], +}));