mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(linter/config): allow aliasing plugin names to allow names the same as builtin plugins (#15569)
fixes https://github.com/oxc-project/oxc/issues/15413 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
This commit is contained in:
17 files changed
+832
-116
No files matched your search
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["jsdoc"],
|
||||
"jsPlugins": [{ "name": "jsPluginJsDoc", "specifier": "./plugin.ts" }],
|
||||
"categories": { "correctness": "off" },
|
||||
"rules": {
|
||||
"jsPluginJsDoc/no-debugger": "error",
|
||||
"jsdoc/rule-name": "error"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
debugger;
|
||||
@@ -0,0 +1,20 @@
|
||||
# Exit code
|
||||
1
|
||||
|
||||
# stdout
|
||||
```
|
||||
x jsPluginJsDoc(no-debugger): Unexpected Debugger Statement
|
||||
,-[files/index.js:1:1]
|
||||
1 | debugger;
|
||||
: ^^^^^^^^^
|
||||
`----
|
||||
|
||||
Found 0 warnings and 1 error.
|
||||
Finished in Xms on 1 file using X threads.
|
||||
```
|
||||
|
||||
# stderr
|
||||
```
|
||||
WARNING: JS plugins are experimental and not subject to semver.
|
||||
Breaking changes are possible while JS plugins support is under development.
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Plugin } from "../../../dist/index.js";
|
||||
|
||||
const plugin: Plugin = {
|
||||
meta: {
|
||||
name: "jsdoc",
|
||||
},
|
||||
rules: {
|
||||
"no-debugger": {
|
||||
create(context) {
|
||||
return {
|
||||
DebuggerStatement(debuggerStatement) {
|
||||
context.report({
|
||||
message: "Unexpected Debugger Statement",
|
||||
node: debuggerStatement,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": ["jsdoc"],
|
||||
"jsPlugins": [{ "name": "jsdoc", "specifier": "./plugin.ts" }],
|
||||
"rules": {}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
debugger;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Exit code
|
||||
1
|
||||
|
||||
# stdout
|
||||
```
|
||||
Failed to parse configuration file.
|
||||
|
||||
x Plugin name 'jsdoc' is reserved, and cannot be used for JS plugins.
|
||||
|
|
||||
| The 'jsdoc' plugin is already implemented natively in Rust within oxlint.
|
||||
| Using both the native and JS versions would create ambiguity about which rules to use.
|
||||
|
|
||||
| To use an external 'jsdoc' plugin instead, provide a custom alias:
|
||||
|
|
||||
| "jsPlugins": [{ "name": "jsdoc-js", "specifier": "eslint-plugin-jsdoc" }]
|
||||
|
|
||||
| Then reference rules using your alias:
|
||||
|
|
||||
| "rules": {
|
||||
| "jsdoc-js/rule-name": "error"
|
||||
| }
|
||||
|
|
||||
| See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html
|
||||
```
|
||||
|
||||
# stderr
|
||||
```
|
||||
WARNING: JS plugins are experimental and not subject to semver.
|
||||
Breaking changes are possible while JS plugins support is under development.
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Plugin } from "../../../dist/index.js";
|
||||
|
||||
const plugin: Plugin = {
|
||||
meta: {
|
||||
name: "jsdoc",
|
||||
},
|
||||
rules: {
|
||||
"no-debugger": {
|
||||
create(context) {
|
||||
return {
|
||||
DebuggerStatement(debuggerStatement) {
|
||||
context.report({
|
||||
message: "Unexpected Debugger Statement",
|
||||
node: debuggerStatement,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
+16
-1
@@ -5,7 +5,22 @@
|
||||
```
|
||||
Failed to parse configuration file.
|
||||
|
||||
x Plugin name 'import' is reserved, and cannot be used for JS plugins
|
||||
x Plugin name 'import' is reserved, and cannot be used for JS plugins.
|
||||
|
|
||||
| The 'import' plugin is already implemented natively in Rust within oxlint.
|
||||
| Using both the native and JS versions would create ambiguity about which rules to use.
|
||||
|
|
||||
| To use an external 'import' plugin instead, provide a custom alias:
|
||||
|
|
||||
| "jsPlugins": [{ "name": "import-js", "specifier": "eslint-plugin-import" }]
|
||||
|
|
||||
| Then reference rules using your alias:
|
||||
|
|
||||
| "rules": {
|
||||
| "import-js/rule-name": "error"
|
||||
| }
|
||||
|
|
||||
| See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html
|
||||
```
|
||||
|
||||
# stderr
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::{
|
||||
RuleCategory, RuleEnum,
|
||||
config::{
|
||||
ESLintRule, OxlintOverrides, OxlintRules,
|
||||
external_plugins::ExternalPluginEntry,
|
||||
overrides::OxlintOverride,
|
||||
plugins::{LintPlugins, normalize_plugin_name},
|
||||
},
|
||||
@@ -150,16 +151,15 @@ impl ConfigStoreBuilder {
|
||||
let (oxlintrc, extended_paths) = resolve_oxlintrc_config(oxlintrc)?;
|
||||
|
||||
// Collect external plugins from both base config and overrides
|
||||
let mut external_plugins: FxHashSet<(&PathBuf, &str)> = FxHashSet::default();
|
||||
let mut external_plugins: FxHashSet<&ExternalPluginEntry> = FxHashSet::default();
|
||||
|
||||
if let Some(base_external_plugins) = &oxlintrc.external_plugins {
|
||||
external_plugins.extend(base_external_plugins.iter().map(|(k, v)| (k, v.as_str())));
|
||||
external_plugins.extend(base_external_plugins.iter());
|
||||
}
|
||||
|
||||
for r#override in &oxlintrc.overrides {
|
||||
if let Some(override_external_plugins) = &r#override.external_plugins {
|
||||
external_plugins
|
||||
.extend(override_external_plugins.iter().map(|(k, v)| (k, v.as_str())));
|
||||
external_plugins.extend(override_external_plugins.iter());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,9 +169,9 @@ impl ConfigStoreBuilder {
|
||||
if !external_plugins.is_empty() && external_plugin_store.is_enabled() {
|
||||
let Some(external_linter) = external_linter else {
|
||||
#[expect(clippy::missing_panics_doc, reason = "infallible")]
|
||||
let (_, original_specifier) = external_plugins.iter().next().unwrap();
|
||||
let first_plugin = external_plugins.iter().next().unwrap();
|
||||
return Err(ConfigBuilderError::NoExternalLinterConfigured {
|
||||
plugin_specifier: (*original_specifier).to_string(),
|
||||
plugin_specifier: first_plugin.specifier.clone(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -180,10 +180,11 @@ impl ConfigStoreBuilder {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
for (config_path, specifier) in &external_plugins {
|
||||
for entry in &external_plugins {
|
||||
Self::load_external_plugin(
|
||||
config_path,
|
||||
specifier,
|
||||
&entry.config_dir,
|
||||
&entry.specifier,
|
||||
entry.name.as_deref(),
|
||||
external_linter,
|
||||
&resolver,
|
||||
external_plugin_store,
|
||||
@@ -523,6 +524,7 @@ impl ConfigStoreBuilder {
|
||||
fn load_external_plugin(
|
||||
resolve_dir: &Path,
|
||||
plugin_specifier: &str,
|
||||
alias: Option<&str>,
|
||||
external_linter: &ExternalLinter,
|
||||
resolver: &Resolver,
|
||||
external_plugin_store: &mut ExternalPluginStore,
|
||||
@@ -562,8 +564,13 @@ impl ConfigStoreBuilder {
|
||||
}
|
||||
})?;
|
||||
|
||||
// Normalize plugin name (e.g., "eslint-plugin-foo" -> "foo", "@foo/eslint-plugin" -> "@foo")
|
||||
let plugin_name = normalize_plugin_name(&result.name).into_owned();
|
||||
// Use alias if provided, otherwise normalize plugin name
|
||||
let plugin_name = if let Some(alias_name) = alias {
|
||||
alias_name.to_string()
|
||||
} else {
|
||||
// Normalize plugin name (e.g., "eslint-plugin-foo" -> "foo", "@foo/eslint-plugin" -> "@foo")
|
||||
normalize_plugin_name(&result.name).into_owned()
|
||||
};
|
||||
|
||||
if LintPlugins::try_from(plugin_name.as_str()).is_err() {
|
||||
external_plugin_store.register_plugin(
|
||||
@@ -652,7 +659,22 @@ impl Display for ConfigBuilderError {
|
||||
ConfigBuilderError::ReservedExternalPluginName { plugin_name } => {
|
||||
write!(
|
||||
f,
|
||||
"Plugin name '{plugin_name}' is reserved, and cannot be used for JS plugins",
|
||||
"Plugin name '{plugin_name}' is reserved, and cannot be used for JS plugins.\n\
|
||||
\n\
|
||||
The '{plugin_name}' plugin is already implemented natively in Rust within oxlint.\n\
|
||||
Using both the native and JS versions would create ambiguity about which rules to use.\n\
|
||||
\n\
|
||||
To use an external '{plugin_name}' plugin instead, provide a custom alias:\n\
|
||||
\n\
|
||||
\"jsPlugins\": [{{ \"name\": \"{plugin_name}-js\", \"specifier\": \"eslint-plugin-{plugin_name}\" }}]\n\
|
||||
\n\
|
||||
Then reference rules using your alias:\n\
|
||||
\n\
|
||||
\"rules\": {{\n\
|
||||
\"{plugin_name}-js/rule-name\": \"error\"\n\
|
||||
}}\n\
|
||||
\n\
|
||||
See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use schemars::{
|
||||
JsonSchema, SchemaGenerator,
|
||||
schema::{
|
||||
ArrayValidation, InstanceType, Metadata, ObjectValidation, Schema, SchemaObject,
|
||||
SubschemaValidation,
|
||||
},
|
||||
};
|
||||
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
de::{Error, SeqAccess, Visitor},
|
||||
};
|
||||
|
||||
/// External plugin entry containing the plugin specifier and optional custom name
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExternalPluginEntry {
|
||||
/// Directory containing the config file that specified this plugin
|
||||
pub config_dir: PathBuf,
|
||||
/// Plugin specifier (path, package name, or URL)
|
||||
pub specifier: String,
|
||||
/// Optional custom name/alias for the plugin
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
/// Custom deserializer for external plugins
|
||||
/// Supports:
|
||||
/// - Array of strings: `["plugin1", "plugin2"]`
|
||||
/// - Array of objects: `[{ "name": "alias", "specifier": "plugin" }]`
|
||||
/// - Mixed array: `["plugin1", { "name": "alias", "specifier": "plugin2" }]`
|
||||
pub fn deserialize_external_plugins<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<FxHashSet<ExternalPluginEntry>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct PluginSetVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for PluginSetVisitor {
|
||||
type Value = Option<FxHashSet<ExternalPluginEntry>>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("null or an array of plugin entries")
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E>
|
||||
where
|
||||
E: Error,
|
||||
{
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
let mut plugins = FxHashSet::default();
|
||||
|
||||
while let Some(entry) = seq.next_element::<ExternalPluginEntry>()? {
|
||||
plugins.insert(entry);
|
||||
}
|
||||
|
||||
Ok(Some(plugins))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(PluginSetVisitor)
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ExternalPluginEntry {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PluginObject {
|
||||
name: String,
|
||||
specifier: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PluginEntry {
|
||||
String(String),
|
||||
Object(PluginObject),
|
||||
}
|
||||
|
||||
let entry = PluginEntry::deserialize(deserializer)?;
|
||||
|
||||
Ok(match entry {
|
||||
PluginEntry::String(specifier) => {
|
||||
ExternalPluginEntry { config_dir: PathBuf::default(), specifier, name: None }
|
||||
}
|
||||
PluginEntry::Object(obj) => ExternalPluginEntry {
|
||||
config_dir: PathBuf::default(),
|
||||
specifier: obj.specifier,
|
||||
name: Some(obj.name),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ExternalPluginEntry {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
enum PluginEntry<'a> {
|
||||
String(&'a str),
|
||||
Object { name: &'a str, specifier: &'a str },
|
||||
}
|
||||
|
||||
if let Some(alias_name) = &self.name {
|
||||
PluginEntry::Object { name: alias_name.as_str(), specifier: self.specifier.as_str() }
|
||||
.serialize(serializer)
|
||||
} else {
|
||||
PluginEntry::String(&self.specifier).serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom JSON schema generator for external plugins that includes uniqueItems constraint
|
||||
pub fn external_plugins_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
let entry_schema = generator.subschema_for::<ExternalPluginEntry>();
|
||||
|
||||
let array_schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::Array.into()),
|
||||
array: Some(Box::new(ArrayValidation {
|
||||
items: Some(entry_schema.into()),
|
||||
unique_items: Some(true),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
any_of: Some(vec![
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::Null.into()),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
array_schema.into(),
|
||||
]),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl JsonSchema for ExternalPluginEntry {
|
||||
fn schema_name() -> String {
|
||||
"ExternalPluginEntry".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
// Schema represents: string | { name: string, specifier: string }
|
||||
let string_schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some("Path or package name of the plugin".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut object_properties = schemars::Map::new();
|
||||
object_properties.insert(
|
||||
"name".to_string(),
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"Custom name/alias for the plugin.\n\n\
|
||||
Note: The following plugin names are reserved because they are implemented \
|
||||
natively in Rust within oxlint and cannot be used for JS plugins:\n\
|
||||
- react (includes react-hooks)\n\
|
||||
- unicorn\n\
|
||||
- typescript\n\
|
||||
- oxc\n\
|
||||
- import (includes import-x)\n\
|
||||
- jsdoc\n\
|
||||
- jest\n\
|
||||
- vitest\n\
|
||||
- jsx-a11y\n\
|
||||
- nextjs\n\
|
||||
- react-perf\n\
|
||||
- promise\n\
|
||||
- node\n\
|
||||
- regex\n\
|
||||
- vue\n\
|
||||
- eslint\n\n\
|
||||
If you need to use the JavaScript version of any of these plugins, \
|
||||
provide a custom alias to avoid conflicts."
|
||||
.to_string()
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
object_properties.insert(
|
||||
"specifier".to_string(),
|
||||
SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some("Path or package name of the plugin".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
||||
let object_schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some("Plugin with custom name/alias".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
properties: object_properties,
|
||||
required: vec!["name".to_string(), "specifier".to_string()].into_iter().collect(),
|
||||
additional_properties: Some(Box::new(Schema::Bool(false))),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
any_of: Some(vec![string_schema.into(), object_schema.into()]),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize() {
|
||||
#[derive(Deserialize)]
|
||||
struct TestConfig {
|
||||
#[serde(
|
||||
rename = "jsPlugins",
|
||||
default,
|
||||
deserialize_with = "deserialize_external_plugins"
|
||||
)]
|
||||
plugins: Option<FxHashSet<ExternalPluginEntry>>,
|
||||
}
|
||||
|
||||
let json = serde_json::json!({
|
||||
"jsPlugins": [
|
||||
"./plugin.ts",
|
||||
{ "name": "custom", "specifier": "./plugin2.ts" }
|
||||
]
|
||||
});
|
||||
let config: TestConfig = serde_json::from_value(json).unwrap();
|
||||
let plugins = config.plugins.as_ref().unwrap();
|
||||
assert_eq!(plugins.len(), 2);
|
||||
assert_eq!(plugins.iter().filter(|e| e.name.is_some()).count(), 1);
|
||||
|
||||
// Null
|
||||
let json = serde_json::json!({ "jsPlugins": null });
|
||||
let config: TestConfig = serde_json::from_value(json).unwrap();
|
||||
assert!(config.plugins.is_none());
|
||||
|
||||
// Empty array
|
||||
let json = serde_json::json!({ "jsPlugins": [] });
|
||||
let config: TestConfig = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(config.plugins.unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_mixed_formats() {
|
||||
#[derive(Deserialize)]
|
||||
struct TestConfig {
|
||||
#[serde(
|
||||
rename = "jsPlugins",
|
||||
default,
|
||||
deserialize_with = "deserialize_external_plugins"
|
||||
)]
|
||||
plugins: Option<FxHashSet<ExternalPluginEntry>>,
|
||||
}
|
||||
|
||||
// Mix string and object formats
|
||||
let json = serde_json::json!({
|
||||
"jsPlugins": [
|
||||
"eslint-plugin-import",
|
||||
{ "name": "custom", "specifier": "./plugin.ts" }
|
||||
]
|
||||
});
|
||||
let config: TestConfig = serde_json::from_value(json).unwrap();
|
||||
let plugins = config.plugins.as_ref().unwrap();
|
||||
assert_eq!(plugins.len(), 2);
|
||||
assert_eq!(plugins.iter().filter(|e| e.name.is_some()).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_rejects_invalid() {
|
||||
#[derive(Deserialize)]
|
||||
struct TestConfig {
|
||||
#[expect(dead_code)]
|
||||
#[serde(
|
||||
rename = "jsPlugins",
|
||||
default,
|
||||
deserialize_with = "deserialize_external_plugins"
|
||||
)]
|
||||
plugins: Option<FxHashSet<ExternalPluginEntry>>,
|
||||
}
|
||||
|
||||
// Extra fields should be rejected
|
||||
let json = serde_json::json!({
|
||||
"jsPlugins": [
|
||||
{ "name": "x", "specifier": "y", "extra": "z" }
|
||||
]
|
||||
});
|
||||
assert!(serde_json::from_value::<TestConfig>(json).is_err());
|
||||
|
||||
// Missing required fields should be rejected
|
||||
let json = serde_json::json!({ "jsPlugins": [{ "name": "x" }] });
|
||||
assert!(serde_json::from_value::<TestConfig>(json).is_err());
|
||||
|
||||
// Object with single arbitrary field should be rejected
|
||||
let json = serde_json::json!({ "jsPlugins": [{ "alias": "plugin" }] });
|
||||
assert!(serde_json::from_value::<TestConfig>(json).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize() {
|
||||
let mut plugins = FxHashSet::default();
|
||||
plugins.insert(ExternalPluginEntry {
|
||||
config_dir: PathBuf::default(),
|
||||
specifier: "./plugin.ts".to_string(),
|
||||
name: None,
|
||||
});
|
||||
plugins.insert(ExternalPluginEntry {
|
||||
config_dir: PathBuf::default(),
|
||||
specifier: "./plugin2.ts".to_string(),
|
||||
name: Some("custom".to_string()),
|
||||
});
|
||||
|
||||
let json = serde_json::to_value(Some(plugins)).unwrap();
|
||||
let arr = json.as_array().unwrap();
|
||||
assert_eq!(arr.len(), 2);
|
||||
|
||||
// Null
|
||||
let json = serde_json::to_value(&None::<FxHashSet<ExternalPluginEntry>>).unwrap();
|
||||
assert!(json.is_null());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod categories;
|
||||
mod config_builder;
|
||||
mod config_store;
|
||||
mod env;
|
||||
mod external_plugins;
|
||||
mod globals;
|
||||
mod ignore_matcher;
|
||||
mod overrides;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
use schemars::{JsonSchema, r#gen, schema::Schema};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
use crate::{LintPlugins, OxlintEnv, OxlintGlobals, config::OxlintRules};
|
||||
|
||||
use super::external_plugins::{
|
||||
ExternalPluginEntry, deserialize_external_plugins, external_plugins_schema,
|
||||
};
|
||||
|
||||
// nominal wrapper required to add JsonSchema impl
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||
pub struct OxlintOverrides(Vec<OxlintOverride>);
|
||||
@@ -101,15 +104,12 @@ pub struct OxlintOverride {
|
||||
/// They are not supported in language server at present.
|
||||
#[serde(
|
||||
rename = "jsPlugins",
|
||||
deserialize_with = "deserialize_external_plugins_override",
|
||||
serialize_with = "serialize_external_plugins_override",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_external_plugins"
|
||||
)]
|
||||
#[schemars(with = "Option<FxHashSet<String>>")]
|
||||
pub external_plugins: Option<
|
||||
FxHashSet<(PathBuf /* config file directory */, String /* plugin specifier */)>,
|
||||
>,
|
||||
#[schemars(schema_with = "external_plugins_schema")]
|
||||
pub external_plugins: Option<FxHashSet<ExternalPluginEntry>>,
|
||||
|
||||
#[serde(default)]
|
||||
pub rules: OxlintRules,
|
||||
@@ -150,32 +150,6 @@ impl GlobSet {
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_external_plugins_override<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<FxHashSet<(PathBuf, String)>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let opt_set: Option<FxHashSet<String>> = Option::deserialize(deserializer)?;
|
||||
Ok(opt_set
|
||||
.map(|set| set.into_iter().map(|specifier| (PathBuf::default(), specifier)).collect()))
|
||||
}
|
||||
|
||||
#[expect(clippy::ref_option)]
|
||||
fn serialize_external_plugins_override<S>(
|
||||
plugins: &Option<FxHashSet<(PathBuf, String)>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Serialize as an array of original specifiers (the values in the map)
|
||||
match plugins {
|
||||
Some(set) => serializer.collect_seq(set.iter().map(|(_, specifier)| specifier)),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::config::{globals::GlobalValue, plugins::LintPlugins};
|
||||
|
||||
@@ -5,15 +5,22 @@ use std::{
|
||||
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use schemars::{JsonSchema, schema_for};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
|
||||
use crate::{LintPlugins, utils::read_to_string};
|
||||
|
||||
use super::{
|
||||
categories::OxlintCategories, env::OxlintEnv, globals::OxlintGlobals,
|
||||
overrides::OxlintOverrides, rules::OxlintRules, settings::OxlintSettings,
|
||||
categories::OxlintCategories,
|
||||
env::OxlintEnv,
|
||||
external_plugins::{
|
||||
ExternalPluginEntry, deserialize_external_plugins, external_plugins_schema,
|
||||
},
|
||||
globals::OxlintGlobals,
|
||||
overrides::OxlintOverrides,
|
||||
rules::OxlintRules,
|
||||
settings::OxlintSettings,
|
||||
};
|
||||
|
||||
/// Oxlint Configuration File
|
||||
@@ -76,13 +83,12 @@ pub struct Oxlintrc {
|
||||
/// They are not supported in language server at present.
|
||||
#[serde(
|
||||
rename = "jsPlugins",
|
||||
deserialize_with = "deserialize_external_plugins",
|
||||
serialize_with = "serialize_external_plugins",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_external_plugins"
|
||||
)]
|
||||
#[schemars(with = "Option<FxHashSet<String>>")]
|
||||
pub external_plugins: Option<FxHashSet<(PathBuf, String)>>,
|
||||
#[schemars(schema_with = "external_plugins_schema")]
|
||||
pub external_plugins: Option<FxHashSet<ExternalPluginEntry>>,
|
||||
pub categories: OxlintCategories,
|
||||
/// Example
|
||||
///
|
||||
@@ -170,7 +176,10 @@ impl Oxlintrc {
|
||||
if let Some(external_plugins) = &mut config.external_plugins {
|
||||
*external_plugins = std::mem::take(external_plugins)
|
||||
.into_iter()
|
||||
.map(|(_, specifier)| (config_dir.to_path_buf(), specifier))
|
||||
.map(|mut entry| {
|
||||
entry.config_dir = config_dir.to_path_buf();
|
||||
entry
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
@@ -178,7 +187,10 @@ impl Oxlintrc {
|
||||
if let Some(external_plugins) = &mut override_config.external_plugins {
|
||||
*external_plugins = std::mem::take(external_plugins)
|
||||
.into_iter()
|
||||
.map(|(_, specifier)| (config_dir.to_path_buf(), specifier))
|
||||
.map(|mut entry| {
|
||||
entry.config_dir = config_dir.to_path_buf();
|
||||
entry
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
@@ -315,7 +327,7 @@ impl Oxlintrc {
|
||||
|
||||
let external_plugins = match (&self.external_plugins, &other.external_plugins) {
|
||||
(Some(self_external), Some(other_external)) => {
|
||||
Some(self_external.iter().chain(other_external).cloned().collect())
|
||||
Some(self_external.iter().chain(other_external.iter()).cloned().collect())
|
||||
}
|
||||
(Some(self_external), None) => Some(self_external.clone()),
|
||||
(None, Some(other_external)) => Some(other_external.clone()),
|
||||
@@ -342,37 +354,12 @@ fn is_json_ext(ext: &str) -> bool {
|
||||
ext == "json" || ext == "jsonc"
|
||||
}
|
||||
|
||||
fn deserialize_external_plugins<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<FxHashSet<(PathBuf, String)>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let opt_set: Option<FxHashSet<String>> = Option::deserialize(deserializer)?;
|
||||
Ok(opt_set
|
||||
.map(|set| set.into_iter().map(|specifier| (PathBuf::default(), specifier)).collect()))
|
||||
}
|
||||
|
||||
#[expect(clippy::ref_option)]
|
||||
fn serialize_external_plugins<S>(
|
||||
plugins: &Option<FxHashSet<(PathBuf, String)>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
// Serialize as an array of original specifiers (the values in the map)
|
||||
match plugins {
|
||||
Some(set) => serializer.collect_seq(set.iter().map(|(_, specifier)| specifier)),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rustc_hash::FxHashSet;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::config::plugins::LintPlugins;
|
||||
use crate::config::{external_plugins::ExternalPluginEntry, plugins::LintPlugins};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -435,4 +422,70 @@ mod test {
|
||||
let config: Oxlintrc = serde_json::from_str(r#"{"extends": []}"#).unwrap();
|
||||
assert_eq!(0, config.extends.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oxlintrc_js_plugins() {
|
||||
let config: Oxlintrc = serde_json::from_str(
|
||||
r#"{"jsPlugins": ["./plugin.ts", { "name": "custom", "specifier": "./plugin2.ts" }]}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(config.external_plugins.as_ref().unwrap().len(), 2);
|
||||
|
||||
// None
|
||||
let config: Oxlintrc = serde_json::from_str(r"{}").unwrap();
|
||||
assert!(config.external_plugins.is_none());
|
||||
|
||||
// Empty array
|
||||
let config: Oxlintrc = serde_json::from_str(r#"{"jsPlugins": []}"#).unwrap();
|
||||
assert_eq!(config.external_plugins.as_ref().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oxlintrc_js_plugins_rejects_invalid() {
|
||||
// Extra fields should be rejected
|
||||
assert!(
|
||||
serde_json::from_str::<Oxlintrc>(
|
||||
r#"{"jsPlugins": [{ "name": "x", "specifier": "y", "extra": "z" }]}"#
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Missing required fields should be rejected
|
||||
assert!(serde_json::from_str::<Oxlintrc>(r#"{"jsPlugins": [{ "name": "x" }]}"#).is_err());
|
||||
|
||||
// Object with arbitrary field should be rejected
|
||||
assert!(
|
||||
serde_json::from_str::<Oxlintrc>(r#"{"jsPlugins": [{ "myAlias": "my-plugin" }]}"#)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oxlintrc_js_plugins_roundtrip() {
|
||||
let mut config = Oxlintrc::default();
|
||||
let mut plugins = FxHashSet::default();
|
||||
plugins.insert(ExternalPluginEntry {
|
||||
config_dir: PathBuf::default(),
|
||||
specifier: "./plugin.ts".to_string(),
|
||||
name: None,
|
||||
});
|
||||
plugins.insert(ExternalPluginEntry {
|
||||
config_dir: PathBuf::default(),
|
||||
specifier: "./plugin2.ts".to_string(),
|
||||
name: Some("custom".to_string()),
|
||||
});
|
||||
config.external_plugins = Some(plugins);
|
||||
|
||||
let serialized = serde_json::to_string(&config).unwrap();
|
||||
let deserialized: Oxlintrc = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(config.external_plugins, deserialized.external_plugins);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oxlintrc_js_plugins_merge() {
|
||||
let config1: Oxlintrc = serde_json::from_str(r#"{"jsPlugins": ["./plugin1.ts"]}"#).unwrap();
|
||||
let config2: Oxlintrc = serde_json::from_str(r#"{"jsPlugins": ["./plugin2.ts"]}"#).unwrap();
|
||||
let merged = config1.merge(config2);
|
||||
assert_eq!(merged.external_plugins.unwrap().len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -57,14 +57,18 @@ expression: json
|
||||
},
|
||||
"jsPlugins": {
|
||||
"description": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ExternalPluginEntry"
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"markdownDescription": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
|
||||
},
|
||||
"overrides": {
|
||||
@@ -226,6 +230,37 @@ expression: json
|
||||
},
|
||||
"markdownDescription": "See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html)"
|
||||
},
|
||||
"ExternalPluginEntry": {
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Path or package name of the plugin",
|
||||
"type": "string",
|
||||
"markdownDescription": "Path or package name of the plugin"
|
||||
},
|
||||
{
|
||||
"description": "Plugin with custom name/alias",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"specifier"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.",
|
||||
"type": "string",
|
||||
"markdownDescription": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts."
|
||||
},
|
||||
"specifier": {
|
||||
"description": "Path or package name of the plugin",
|
||||
"type": "string",
|
||||
"markdownDescription": "Path or package name of the plugin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"markdownDescription": "Plugin with custom name/alias"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GlobSet": {
|
||||
"description": "A set of glob patterns.",
|
||||
"type": "array",
|
||||
@@ -484,14 +519,18 @@ expression: json
|
||||
},
|
||||
"jsPlugins": {
|
||||
"description": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ExternalPluginEntry"
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"markdownDescription": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -53,14 +53,18 @@
|
||||
},
|
||||
"jsPlugins": {
|
||||
"description": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ExternalPluginEntry"
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"markdownDescription": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
|
||||
},
|
||||
"overrides": {
|
||||
@@ -222,6 +226,37 @@
|
||||
},
|
||||
"markdownDescription": "See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html)"
|
||||
},
|
||||
"ExternalPluginEntry": {
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Path or package name of the plugin",
|
||||
"type": "string",
|
||||
"markdownDescription": "Path or package name of the plugin"
|
||||
},
|
||||
{
|
||||
"description": "Plugin with custom name/alias",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"specifier"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.",
|
||||
"type": "string",
|
||||
"markdownDescription": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts."
|
||||
},
|
||||
"specifier": {
|
||||
"description": "Path or package name of the plugin",
|
||||
"type": "string",
|
||||
"markdownDescription": "Path or package name of the plugin"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"markdownDescription": "Plugin with custom name/alias"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GlobSet": {
|
||||
"description": "A set of glob patterns.",
|
||||
"type": "array",
|
||||
@@ -480,14 +515,18 @@
|
||||
},
|
||||
"jsPlugins": {
|
||||
"description": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ExternalPluginEntry"
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"markdownDescription": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -198,7 +198,7 @@ Globs to ignore during linting. These are resolved from the configuration file p
|
||||
|
||||
## jsPlugins
|
||||
|
||||
type: `string[]`
|
||||
type: `array | null`
|
||||
|
||||
|
||||
JS plugins.
|
||||
@@ -207,6 +207,50 @@ Note: JS plugins are experimental and not subject to semver.
|
||||
They are not supported in language server at present.
|
||||
|
||||
|
||||
### jsPlugins[n]
|
||||
|
||||
type: `object | string`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### jsPlugins[n].name
|
||||
|
||||
type: `string`
|
||||
|
||||
|
||||
Custom name/alias for the plugin.
|
||||
|
||||
Note: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:
|
||||
- react (includes react-hooks)
|
||||
- unicorn
|
||||
- typescript
|
||||
- oxc
|
||||
- import (includes import-x)
|
||||
- jsdoc
|
||||
- jest
|
||||
- vitest
|
||||
- jsx-a11y
|
||||
- nextjs
|
||||
- react-perf
|
||||
- promise
|
||||
- node
|
||||
- regex
|
||||
- vue
|
||||
- eslint
|
||||
|
||||
If you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.
|
||||
|
||||
|
||||
#### jsPlugins[n].specifier
|
||||
|
||||
type: `string`
|
||||
|
||||
|
||||
Path or package name of the plugin
|
||||
|
||||
|
||||
## overrides
|
||||
|
||||
type: `array`
|
||||
@@ -249,7 +293,7 @@ Enabled or disabled specific global variables.
|
||||
|
||||
#### overrides[n].jsPlugins
|
||||
|
||||
type: `string[]`
|
||||
type: `array | null`
|
||||
|
||||
|
||||
JS plugins for this override.
|
||||
@@ -258,6 +302,50 @@ Note: JS plugins are experimental and not subject to semver.
|
||||
They are not supported in language server at present.
|
||||
|
||||
|
||||
##### overrides[n].jsPlugins[n]
|
||||
|
||||
type: `object | string`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###### overrides[n].jsPlugins[n].name
|
||||
|
||||
type: `string`
|
||||
|
||||
|
||||
Custom name/alias for the plugin.
|
||||
|
||||
Note: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:
|
||||
- react (includes react-hooks)
|
||||
- unicorn
|
||||
- typescript
|
||||
- oxc
|
||||
- import (includes import-x)
|
||||
- jsdoc
|
||||
- jest
|
||||
- vitest
|
||||
- jsx-a11y
|
||||
- nextjs
|
||||
- react-perf
|
||||
- promise
|
||||
- node
|
||||
- regex
|
||||
- vue
|
||||
- eslint
|
||||
|
||||
If you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.
|
||||
|
||||
|
||||
###### overrides[n].jsPlugins[n].specifier
|
||||
|
||||
type: `string`
|
||||
|
||||
|
||||
Path or package name of the plugin
|
||||
|
||||
|
||||
#### overrides[n].plugins
|
||||
|
||||
type: `array | null`
|
||||
|
||||
Reference in new issue
Block a user