Files
oxc-project__oxc/apps/oxlint/src-js/cli.ts
leaysgur f99ecdaa76 feat(oxlint): Gate vite.config.ts recognition behind VP_VERSION env var (#21298)
Fixes #20416 for Oxlint

- Same `VP_VERSION` gating for config discovery, LSP file watchers, and JS config loading
- Refactor JS config loader into `resolveJsConfig` / `resolveVitePlusConfig` with shared `importConfig` + `loadConfigs`
- Split test fixtures into `vite_config_*` (non VP mode) and `vite_plus_*` (VP mode)
- Add `env` field to fixture `options.json` for passing env vars to test runs
- Fix repo `oxlintrc.json` ignore glob (`*/files/**` → `**/files/**`) for nested fixture paths like monorepo usecase
2026-04-13 01:10:46 +00:00

199 lines
7.9 KiB
TypeScript

import { lint } from "./bindings.js";
import { debugAssertIsNonNull, debugAssertIsNotUndefined } from "./utils/asserts.ts";
// Lazy-loaded JS plugin-related functions.
// The type annotations below use `typeof import("./plugins/index.ts").<fn>` so that the lazy-loaded variables
// always have the exact same function signatures as the implementations exported from the plugins module.
let loadPlugin: typeof import("./plugins/index.ts").loadPlugin | null = null;
let setupRuleConfigs: typeof import("./plugins/index.ts").setupRuleConfigs | null = null;
let lintFile: typeof import("./plugins/index.ts").lintFile | null = null;
let createWorkspace: typeof import("./workspace/index.ts").createWorkspace | null = null;
let destroyWorkspace: typeof import("./workspace/index.ts").destroyWorkspace | null = null;
// Lazy-loaded JS/TS config loader (experimental)
let resolvedConfigLoader: import("./js_config.ts").ConfigLoader | null = null;
/**
* Load a plugin.
*
* Lazy-loads plugins code on first call, so that overhead is skipped if user doesn't use JS plugins.
*
* @param path - Absolute path of plugin file
* @param pluginName - Plugin name (either alias or package name)
* @param pluginNameIsAlias - `true` if plugin name is an alias (takes priority over name that plugin defines itself)
* @param workspaceUri - Workspace URI (`null` in CLI mode, `string` in LSP mode)
* @returns Plugin details or error serialized to JSON string
*/
function loadPluginWrapper(
path: string,
pluginName: string | null | undefined,
pluginNameIsAlias: boolean,
workspaceUri: string | null | undefined,
): Promise<string> {
// Type generated by NAPI-RS for `lint` is inaccurate.
// `Option::None` on Rust side becomes `null` on JS side, not `undefined`.
debugAssertIsNotUndefined(pluginName, "`pluginName` should not be `undefined`");
debugAssertIsNotUndefined(workspaceUri, "`workspaceUri` should not be `undefined`");
if (loadPlugin === null) {
// Use promises here instead of making `loadPluginWrapper` an async function,
// to avoid a micro-tick and extra wrapper `Promise` in all later calls to `loadPluginWrapper`
return import("./plugins/index.ts").then((mod) => {
({ loadPlugin, lintFile, setupRuleConfigs } = mod);
return loadPlugin(path, pluginName, pluginNameIsAlias, workspaceUri);
});
}
return loadPlugin(path, pluginName, pluginNameIsAlias, workspaceUri);
}
/**
* Bootstrap configuration options.
*
* Delegates to `setupRuleConfigs`, which was lazy-loaded by `loadPluginWrapper`.
*
* @param optionsJSON - Array of all rule options across all configurations, serialized as JSON
* @returns `null` if success, or error message string
*/
function setupRuleConfigsWrapper(optionsJSON: string): string | null {
debugAssertIsNonNull(setupRuleConfigs);
return setupRuleConfigs(optionsJSON);
}
/**
* Lint a file.
*
* Delegates to `lintFile`, which was lazy-loaded by `loadPluginWrapper`.
*
* @param filePath - Absolute path of file being linted
* @param bufferId - ID of buffer containing file data
* @param buffer - Buffer containing file data, or `null` if buffer with this ID was previously sent to JS
* @param ruleIds - IDs of rules to run on this file
* @param optionsIds - IDs of options to use for rules on this file, in same order as `ruleIds`
* @param settingsJSON - Settings for file, as JSON
* @param globalsJSON - Globals for file, as JSON
* @param workspaceUri - Workspace URI (`null` in CLI mode, `string` in LSP mode)
* @returns Diagnostics or error serialized to JSON string
*/
function lintFileWrapper(
filePath: string,
bufferId: number,
buffer: Uint8Array | null | undefined,
ruleIds: number[],
optionsIds: number[],
settingsJSON: string,
globalsJSON: string,
workspaceUri: string | null | undefined,
): string | null {
// Type generated by NAPI-RS for `lint` is inaccurate.
// `Option::None` on Rust side becomes `null` on JS side, not `undefined`.
debugAssertIsNotUndefined(buffer, "`buffer` should not be `undefined`");
debugAssertIsNotUndefined(workspaceUri, "`workspaceUri` should not be `undefined`");
// `lintFileWrapper` is never called without `loadPluginWrapper` being called first,
// so `lintFile` must be defined here
debugAssertIsNonNull(lintFile);
return lintFile(
filePath,
bufferId,
buffer,
ruleIds,
optionsIds,
settingsJSON,
globalsJSON,
workspaceUri,
);
}
/**
* Create a new workspace.
*
* Lazy-loads workspace code on first call, so that overhead is skipped if user doesn't use JS plugins.
*
* @param workspace - Workspace URI
* @returns Promise which resolves when workspace is created
*/
function createWorkspaceWrapper(workspace: string): Promise<undefined> {
if (createWorkspace === null) {
// Use promises here instead of making `createWorkspaceWrapper` an async function,
// to avoid a micro-tick and extra wrapper `Promise` in all later calls to `createWorkspaceWrapper`
return import("./workspace/index.ts").then((mod) => {
({ createWorkspace, destroyWorkspace } = mod);
return createWorkspace(workspace);
});
}
return Promise.resolve(createWorkspace(workspace));
}
/**
* Destroy a workspace.
*
* Delegates to `destroyWorkspace`, which was lazy-loaded by `createWorkspaceWrapper`.
*
* @param workspace - Workspace URI
* @returns `undefined`
*/
function destroyWorkspaceWrapper(workspace: string): undefined {
// `destroyWorkspaceWrapper` is never called without `createWorkspaceWrapper` being called first,
// so `destroyWorkspace` must be defined here
debugAssertIsNonNull(destroyWorkspace);
destroyWorkspace(workspace);
}
/**
* Load JavaScript/TypeScript config files (experimental).
*
* Lazy-loads the js_config module on first call.
* Uses native Node.js TypeScript support to import config files.
*
* @param paths - Array of absolute paths to JavaScript/TypeScript config files
* @returns JSON-stringified result with all configs or error
*/
function loadJsConfigsWrapper(paths: string[]): Promise<string> {
if (resolvedConfigLoader === null) {
return import("./js_config.ts").then(({ loadJsConfigs, loadVitePlusConfigs }) => {
resolvedConfigLoader = process.env.VP_VERSION ? loadVitePlusConfigs : loadJsConfigs;
return resolvedConfigLoader(paths);
});
}
return resolvedConfigLoader(paths);
}
// Get command line arguments, skipping first 2 (node binary and script path)
const args = process.argv.slice(2);
// Node.js sets non-TTY `stdio` to non-blocking mode, which causes "Resource temporarily unavailable" errors
// in language server when passing a lot of data via stdin/stdout.
// https://github.com/oxc-project/oxc/issues/19265
// See also issue related to this workaround in `oxfmt` CLI:
// https://github.com/oxc-project/oxc/issues/17939
//
// As a workaround, if used with pipe, set blocking mode before calling NAPI bindings.
// See: https://github.com/napi-rs/napi-rs/issues/1630
if (!process.stdout.isTTY) {
// @ts-expect-error: `_handle` is an internal API
process.stdin._handle?.setBlocking?.(true);
// @ts-expect-error: `_handle` is an internal API
process.stdout._handle?.setBlocking?.(true);
}
// LSP uses stdout for communication, so write logs to stderr to avoid breaking the protocol.
// Since LSP is handled on the Rust side, we have to check the flag here. (`lint()` starts the server and waits)
// Also, for Oxlint, this actually affects:
// - loading JS/TS config files
// - Execution of JS plugins
if (args.includes("--lsp")) process.stdout.write = process.stderr.write.bind(process.stderr);
// Call Rust, passing callbacks and CLI arguments
const success = await lint(
args,
loadPluginWrapper,
setupRuleConfigsWrapper,
lintFileWrapper,
createWorkspaceWrapper,
destroyWorkspaceWrapper,
loadJsConfigsWrapper,
);
// Note: It's recommended to set `process.exitCode` instead of calling `process.exit()`.
// `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies.
// https://nodejs.org/api/process.html#processexitcode
if (!success) process.exitCode = 1;