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
16 changes: 12 additions & 4 deletions apps/oxfmt/src-js/cli/js_config.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -23,23 +25,29 @@ export async function loadJsConfig(path: string): Promise<object | null> {
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<string, unknown>)[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}`,
);
}
return fmtConfig;
}

if (!isObject(config)) {
throw new Error(`Configuration file must have a default export that is an object: ${path}`);
}

return config;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ Finished in <variable>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 <variable>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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const a = 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const defineConfig = (config: unknown) => config;

export default defineConfig(() => ({
plugins: [],
}));
6 changes: 6 additions & 0 deletions apps/oxfmt/test/cli/vite_config/vite_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 15 additions & 6 deletions apps/oxlint/src-js/js_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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).`,
);
Expand Down Expand Up @@ -106,19 +108,22 @@ export async function loadJsConfigs(paths: string[]): Promise<string> {
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<string, unknown>)[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.`,
);
Expand All @@ -127,6 +132,10 @@ export async function loadJsConfigs(paths: string[]): Promise<string> {
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".`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
debugger;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleThread": true
}
19 changes: 19 additions & 0 deletions apps/oxlint/test/fixtures/vite_config_fn_export/output.snap.md
Original file line number Diff line number Diff line change
@@ -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
```
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const defineConfig = (config: unknown) => config;

export default defineConfig(() => ({
plugins: [],
}));
Loading