diff --git a/apps/oxlint/src-js/bindings.d.ts b/apps/oxlint/src-js/bindings.d.ts index bb40670519105..33d1d72ba53fd 100644 --- a/apps/oxlint/src-js/bindings.d.ts +++ b/apps/oxlint/src-js/bindings.d.ts @@ -6,7 +6,7 @@ export type JsLintFileCb = /** JS callback to load a JS plugin. */ export type JsLoadPluginCb = - ((arg: string) => Promise) + ((arg0: string, arg1?: string | undefined | null) => Promise) /** * NAPI entry point. diff --git a/apps/oxlint/src-js/cli.ts b/apps/oxlint/src-js/cli.ts index 2244f8e010ead..77625c29bc109 100644 --- a/apps/oxlint/src-js/cli.ts +++ b/apps/oxlint/src-js/cli.ts @@ -6,13 +6,13 @@ import { lint } from './bindings.js'; let loadPlugin: typeof loadPluginWrapper | null = null; let lintFile: typeof lintFileWrapper | null = null; -function loadPluginWrapper(path: string): Promise { +function loadPluginWrapper(path: string, packageName?: string): Promise { if (loadPlugin === null) { const require = createRequire(import.meta.url); // `plugins.js` is in root of `dist`. See `tsdown.config.ts`. ({ loadPlugin, lintFile } = require('./plugins.js')); } - return loadPlugin(path); + return loadPlugin(path, packageName); } function lintFileWrapper(filePath: string, bufferId: number, buffer: Uint8Array | null, ruleIds: number[]): string { diff --git a/apps/oxlint/src-js/plugins/load.ts b/apps/oxlint/src-js/plugins/load.ts index a771e0f1f0f76..68b0c5c2df6cc 100644 --- a/apps/oxlint/src-js/plugins/load.ts +++ b/apps/oxlint/src-js/plugins/load.ts @@ -9,8 +9,8 @@ const ObjectKeys = Object.keys; // Linter plugin, comprising multiple rules export interface Plugin { - meta: { - name: string; + meta?: { + name?: string; }; rules: { [key: string]: Rule; @@ -80,11 +80,12 @@ interface PluginDetails { * containing try/catch. * * @param path - Absolute path of plugin file + * @param packageName - Optional package name from package.json (fallback if plugin.meta.name is missing) * @returns JSON result */ -export async function loadPlugin(path: string): Promise { +export async function loadPlugin(path: string, packageName?: string): Promise { try { - const res = await loadPluginImpl(path); + const res = await loadPluginImpl(path, packageName); return JSON.stringify({ Success: res }); } catch (err) { return JSON.stringify({ Failure: getErrorMessage(err) }); @@ -95,12 +96,13 @@ export async function loadPlugin(path: string): Promise { * Load a plugin. * * @param path - Absolute path of plugin file + * @param packageName - Optional package name from package.json (fallback if plugin.meta.name is missing) * @returns - Plugin details * @throws {Error} If plugin has already been registered * @throws {TypeError} If one of plugin's rules is malformed or its `createOnce` method returns invalid visitor * @throws {*} If plugin throws an error during import */ -async function loadPluginImpl(path: string): Promise { +async function loadPluginImpl(path: string, packageName?: string): Promise { if (registeredPluginPaths.has(path)) { throw new Error('This plugin has already been registered. This is a bug in Oxlint. Please report it.'); } @@ -110,7 +112,13 @@ async function loadPluginImpl(path: string): Promise { registeredPluginPaths.add(path); // TODO: Use a validation library to assert the shape of the plugin, and of rules - const pluginName = plugin.meta.name; + // Get plugin name from plugin.meta.name, or fall back to package name from package.json + const pluginName = plugin.meta?.name ?? packageName; + if (!pluginName) { + throw new TypeError( + 'Plugin must have either meta.name or be loaded from an npm package with a name field in package.json', + ); + } const offset = registeredRules.length; const { rules } = plugin; const ruleNames = ObjectKeys(rules); diff --git a/apps/oxlint/src/js_plugins/external_linter.rs b/apps/oxlint/src/js_plugins/external_linter.rs index a8fc20ca26e57..34d98eb073eac 100644 --- a/apps/oxlint/src/js_plugins/external_linter.rs +++ b/apps/oxlint/src/js_plugins/external_linter.rs @@ -37,11 +37,15 @@ pub fn create_external_linter( /// The returned function will panic if called outside of a Tokio runtime. fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb { let cb = Arc::new(cb); - Arc::new(move |plugin_path| { + Arc::new(move |plugin_path, package_name| { let cb = Arc::clone(&cb); tokio::task::block_in_place(move || { tokio::runtime::Handle::current().block_on(async move { - let result = cb.call_async(plugin_path).await?.into_future().await?; + let result = cb + .call_async(FnArgs::from((plugin_path, package_name))) + .await? + .into_future() + .await?; let plugin_load_result: PluginLoadResult = serde_json::from_str(&result)?; Ok(plugin_load_result) }) diff --git a/apps/oxlint/src/run.rs b/apps/oxlint/src/run.rs index bef2944bbc3f6..4df99aa1a4b3a 100644 --- a/apps/oxlint/src/run.rs +++ b/apps/oxlint/src/run.rs @@ -16,11 +16,11 @@ use crate::{lint::CliRunner, result::CliRunResult}; #[napi] pub type JsLoadPluginCb = ThreadsafeFunction< // Arguments - String, // Absolute path of plugin file + FnArgs<(String, Option)>, // Absolute path of plugin file, optional package name // Return value Promise, // `PluginLoadResult`, serialized to JSON // Arguments (repeated) - String, + FnArgs<(String, Option)>, // Error status Status, // CalleeHandled diff --git a/apps/oxlint/test/fixtures/load_paths/.oxlintrc.json b/apps/oxlint/test/fixtures/load_paths/.oxlintrc.json index 58d3dc41c30c4..e8e9ffefbcce0 100644 --- a/apps/oxlint/test/fixtures/load_paths/.oxlintrc.json +++ b/apps/oxlint/test/fixtures/load_paths/.oxlintrc.json @@ -12,7 +12,8 @@ "plugin10", "plugin11", "plugin12", - "plugin13" + "plugin13", + "plugin14" ], "rules": { "plugin1/no-debugger": "error", @@ -27,6 +28,7 @@ "plugin10/no-debugger": "error", "plugin11/no-debugger": "error", "plugin12/no-debugger": "error", - "plugin13/no-debugger": "error" + "plugin13/no-debugger": "error", + "plugin14/no-debugger": "error" } } diff --git a/apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/index.js b/apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/index.js new file mode 100644 index 0000000000000..59f41674d10c7 --- /dev/null +++ b/apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/index.js @@ -0,0 +1,17 @@ +// Plugin without meta.name - should fall back to package.json name +export default { + rules: { + "no-debugger": { + create(context) { + return { + DebuggerStatement(debuggerStatement) { + context.report({ + message: "Unexpected Debugger Statement", + node: debuggerStatement, + }); + }, + }; + }, + }, + }, +}; diff --git a/apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/package.json b/apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/package.json new file mode 100644 index 0000000000000..628bcb5e1ff5d --- /dev/null +++ b/apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/package.json @@ -0,0 +1,5 @@ +{ + "name": "plugin14", + "type": "module", + "main": "index.js" +} diff --git a/apps/oxlint/test/fixtures/load_paths/output.snap.md b/apps/oxlint/test/fixtures/load_paths/output.snap.md index 3dcbc3dd509be..7e2964afcc270 100644 --- a/apps/oxlint/test/fixtures/load_paths/output.snap.md +++ b/apps/oxlint/test/fixtures/load_paths/output.snap.md @@ -40,6 +40,12 @@ : ^^^^^^^^^ `---- + x plugin14(no-debugger): Unexpected Debugger Statement + ,-[files/index.js:1:1] + 1 | debugger; + : ^^^^^^^^^ + `---- + x plugin2(no-debugger): Unexpected Debugger Statement ,-[files/index.js:1:1] 1 | debugger; @@ -88,7 +94,7 @@ : ^^^^^^^^^ `---- -Found 1 warning and 13 errors. +Found 1 warning and 14 errors. Finished in Xms on 1 file using X threads. ``` diff --git a/crates/oxc_linter/src/config/config_builder.rs b/crates/oxc_linter/src/config/config_builder.rs index 7e99e449c85e0..acc7549a601d3 100644 --- a/crates/oxc_linter/src/config/config_builder.rs +++ b/crates/oxc_linter/src/config/config_builder.rs @@ -538,9 +538,12 @@ impl ConfigStoreBuilder { return Ok(()); } + // Extract package name from package.json if available + let package_name = resolved.package_json().and_then(|pkg| pkg.name().map(String::from)); + let result = { let plugin_path = plugin_path.clone(); - (external_linter.load_plugin)(plugin_path).map_err(|e| { + (external_linter.load_plugin)(plugin_path, package_name).map_err(|e| { ConfigBuilderError::PluginLoadFailed { plugin_specifier: plugin_specifier.to_string(), error: e.to_string(), diff --git a/crates/oxc_linter/src/external_linter.rs b/crates/oxc_linter/src/external_linter.rs index e64de38447166..d363cda7092df 100644 --- a/crates/oxc_linter/src/external_linter.rs +++ b/crates/oxc_linter/src/external_linter.rs @@ -5,7 +5,10 @@ use serde::Deserialize; use oxc_allocator::Allocator; pub type ExternalLinterLoadPluginCb = Arc< - dyn Fn(String) -> Result> + dyn Fn( + String, + Option, + ) -> Result> + Send + Sync, >;