Skip to content

Commit 4ea67de

Browse files
committed
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.
1 parent fdaca97 commit 4ea67de

File tree

10 files changed

+82
-10
lines changed

10 files changed

+82
-10
lines changed

apps/oxfmt/src-js/cli/js_config.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { basename as pathBasename } from "node:path";
22
import { pathToFileURL } from "node:url";
33

4+
const isObject = (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v);
5+
46
const VITE_CONFIG_NAME = "vite.config.ts";
57
const VITE_OXFMT_CONFIG_FIELD = "fmt";
68

@@ -23,23 +25,29 @@ export async function loadJsConfig(path: string): Promise<object | null> {
2325
const { default: config } = await import(fileUrl.href);
2426

2527
if (config === undefined) throw new Error(`Configuration file has no default export: ${path}`);
26-
if (typeof config !== "object" || config === null || Array.isArray(config)) {
27-
throw new Error(`Configuration file must have a default export that is an object: ${path}`);
28-
}
2928

3029
// Vite config: extract `.fmt` field
3130
if (pathBasename(path) === VITE_CONFIG_NAME) {
31+
// NOTE: Vite configs may export a function via `defineConfig(() => ({ ... }))`,
32+
// but we don't know the arguments to call the function.
33+
// Treat non-object exports as "no config" and skip.
34+
if (!isObject(config)) return null;
35+
3236
const fmtConfig = (config as Record<string, unknown>)[VITE_OXFMT_CONFIG_FIELD];
3337
// NOTE: return `null` if missing (signals "skip" to Rust side)
3438
if (fmtConfig === undefined) return null;
3539

36-
if (typeof fmtConfig !== "object" || fmtConfig === null || Array.isArray(fmtConfig)) {
40+
if (!isObject(fmtConfig)) {
3741
throw new Error(
3842
`The \`${VITE_OXFMT_CONFIG_FIELD}\` field in the default export must be an object: ${path}`,
3943
);
4044
}
4145
return fmtConfig;
4246
}
4347

48+
if (!isObject(config)) {
49+
throw new Error(`Configuration file must have a default export that is an object: ${path}`);
50+
}
51+
4452
return config;
4553
}

apps/oxfmt/test/cli/vite_config/__snapshots__/vite_config.test.ts.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ Finished in <variable>ms on 1 files using 1 threads.
4747
--------------------"
4848
`;
4949
50+
exports[`vite_config > skip: auto-discovered vite.config.ts with function export uses defaults 1`] = `
51+
"--------------------
52+
arguments: --check test.ts
53+
working directory: vite_config/fixtures/skip_fn_export
54+
exit code: 0
55+
--- STDOUT ---------
56+
Checking formatting...
57+
58+
All matched files use the correct format.
59+
Finished in <variable>ms on 1 files using 1 threads.
60+
--- STDERR ---------
61+
62+
--------------------"
63+
`;
64+
5065
exports[`vite_config > skip: auto-discovered vite.config.ts without fmt field uses defaults 1`] = `
5166
"--------------------
5267
arguments: --check test.ts
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const a = 1;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const defineConfig = (config: unknown) => config;
2+
3+
export default defineConfig(() => ({
4+
plugins: [],
5+
}));

apps/oxfmt/test/cli/vite_config/vite_config.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ describe("vite_config", () => {
3434
expect(snapshot).toMatchSnapshot();
3535
});
3636

37+
it("skip: auto-discovered vite.config.ts with function export uses defaults", async () => {
38+
const cwd = join(fixturesDir, "skip_fn_export");
39+
const snapshot = await runAndSnapshot(cwd, [["--check", "test.ts"]]);
40+
expect(snapshot).toMatchSnapshot();
41+
});
42+
3743
it("priority: oxfmt.config.ts takes precedence over vite.config.ts", async () => {
3844
// `oxfmt.config.ts` has `semi: false`, `vite.config.ts` has `semi: true`
3945
// oxfmt.config.ts should win, so `const a = 1;` (with semicolon) should be flagged

apps/oxlint/src-js/js_config.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface JsConfigResult {
99
config: unknown; // Will be validated as Oxlintrc on Rust side, `null` means "skip this config"
1010
}
1111

12+
const isObject = (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v);
13+
1214
const VITE_CONFIG_NAME = "vite.config.ts";
1315
const VITE_OXLINT_CONFIG_FIELD = "lint";
1416

@@ -57,7 +59,7 @@ function validateConfigExtends(root: object): void {
5759
}
5860
for (let i = 0; i < maybeExtends.length; i++) {
5961
const item = maybeExtends[i];
60-
if (typeof item !== "object" || item === null || Array.isArray(item)) {
62+
if (!isObject(item)) {
6163
throw new Error(
6264
`\`extends[${i}]\` must be a config object (strings/paths are not supported).`,
6365
);
@@ -106,19 +108,22 @@ export async function loadJsConfigs(paths: string[]): Promise<string> {
106108
throw new Error(`Configuration file has no default export.`);
107109
}
108110

109-
if (typeof config !== "object" || config === null || Array.isArray(config)) {
110-
throw new Error(`Configuration file must have a default export that is an object.`);
111-
}
112-
113111
// Vite config: extract `.lint` field, skip `defineConfig()` validation
114112
if (pathBasename(path) === VITE_CONFIG_NAME) {
113+
// NOTE: Vite configs may export a function via `defineConfig(() => ({ ... }))`,
114+
// but we don't know the arguments to call the function.
115+
// Treat non-object exports as "no config" and skip.
116+
if (!isObject(config)) {
117+
return { path, config: null };
118+
}
119+
115120
const lintConfig = (config as Record<string, unknown>)[VITE_OXLINT_CONFIG_FIELD];
116121
// NOTE: return `null` if `.lint` is missing which signals "skip" this
117122
if (lintConfig === undefined) {
118123
return { path, config: null };
119124
}
120125

121-
if (typeof lintConfig !== "object" || lintConfig === null || Array.isArray(lintConfig)) {
126+
if (!isObject(lintConfig)) {
122127
throw new Error(
123128
`The \`${VITE_OXLINT_CONFIG_FIELD}\` field in the default export must be an object.`,
124129
);
@@ -127,6 +132,10 @@ export async function loadJsConfigs(paths: string[]): Promise<string> {
127132
return { path, config: lintConfig };
128133
}
129134

135+
if (!isObject(config)) {
136+
throw new Error(`Configuration file must have a default export that is an object.`);
137+
}
138+
130139
if (!isDefineConfig(config)) {
131140
throw new Error(
132141
`Configuration file must wrap its default export with defineConfig() from "oxlint".`,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
debugger;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"singleThread": true
3+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Exit code
2+
0
3+
4+
# stdout
5+
```
6+
! eslint(no-debugger): `debugger` statement is not allowed
7+
,-[files/test.js:1:1]
8+
1 | debugger;
9+
: ^^^^^^^^^
10+
`----
11+
help: Remove the debugger statement
12+
13+
Found 1 warning and 0 errors.
14+
Finished in Xms on 1 file with 93 rules using X threads.
15+
```
16+
17+
# stderr
18+
```
19+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const defineConfig = (config: unknown) => config;
2+
3+
export default defineConfig(() => ({
4+
plugins: [],
5+
}));

0 commit comments

Comments
 (0)