Part of #18610.
Our intent is to move `definePlugin`, `defineRule`, and `eslintCompatPlugin` to a separate package `@oxlint/plugins` (plural to avoid confusion with naming convention for plugins `oxlint-plugin-xxx`).
So rename the current entry point which exports these functions from `oxlint/plugin` to `oxlint/plugins` (plural).
Closes#18611.
Previously `definePlugin` and `defineRule` were doing double duty:
1. Providing type safety (standard `defineConfig` pattern).
2. Converting a plugin/rule to be ESLint compatible
Combining the two duties is probably confusing to users. Users are familiar with the `defineConfig` pattern and these functions generally don't do anything at runtime - just require a specific type as param and return the input untouched.
This PR adds a separate function `eslintCompatPlugin` which does the conversion to an ESLint-compatible plugin.
`definePlugin` and `defineRule` are now no-ops at runtime - they just pass through the plugin/rule unchanged.
As we prepare for alpha release of JS plugins, it feels like we can remove the warning logged to stderr.
```
WARNING: JS plugins are experimental and not subject to semver.
Breaking changes are possible while JS plugins support is under development.
```
They do still remain experimental, but I don't think we need to shout that quite as loudly now.
This is technically breaking (only for types), but custom plugins are experimental.
The ruletester/defineRule/definePlugin are re-exported but with a `@deprecated` tag which I think is a good enough trade off
I want to avoid users bringing in a ton of custom plugin code when doing `import { defineConfig } from 'oxlint'`
This ensures that we print the expected number of rules for users that are using JS Plugins.
Fix#17519.
Based on #17517, because otherwise we'd end up with weird snapshot differences when trying to merge these anyway.
AI Disclosure: The fix here was generated with Copilot + GitHub's Raptor mini model.
For a while, we've been skipping the printing of rule runs unless `--disable-nested-config` was passed. This was a result of the nested_configs value including the base config (see #16356). When we were checking whether that value was empty, it would always have at least 1 item (unless nested configs were entirely disabled).
As part of this, I've updated some configs in the fixtures to disable correctness rules, otherwise we will end up with a bunch of fixture snapshots needing updates every time a new default rule is added.
It's probably easiest to review this one commit at a time.
Remove the need to list every fixture in `e2e.test.ts` file by automatically running tests on every fixture in `test/fixtures` directory.
Running Oxlint with `--fix` and running fixtures with ESLint is now controlled by adding an `options.json` file to the fixture directory contining e.g. `{ "fix": true }` or `{ "eslint": true }`.
Now we can add a new fixture just by creating a new directory in `fixtures`.
When replacing absolute paths in fixture snapshots, replace paths anywhere in the output, rather than only in certain specified patterns.
Additionally, introduce `<fixtures>` and `<fixture>` replacements to shorten paths in snapshots.
```diff
- File path: <root>/apps/oxlint/test/fixtures/fixture_name/files/1.js
+ File path: <fixture>/files/1.js
- File path: <root>/apps/oxlint/test/fixtures
+ File path: <fixtures>
```
This allows removing the `relativePath` helper function from some test plugins, outputting paths (e.g. `cwd`) anywhere, and generally will avoid problems with snapshots being system-dependent, and dependent on directory layout of fixtures.
Also, amend ESLint `stdout` output in snapshots to align all rule names, to reduce churn in snapshots, and avoid mismatches between local and CI (presumably due to differences in terminal width).
```diff
- filename: <fixture>/files/1.js define-plugin-plugin/create-once
- filename: <fixture>/files/long_filename.js define-plugin-plugin/create-once
- filename: <fixture>/files/very_long_filename.js define-plugin-plugin/create-once
+ filename: <fixture>/files/1.js define-plugin-plugin/create-once
+ filename: <fixture>/files/long_filename.js define-plugin-plugin/create-once
+ filename: <fixture>/files/very_long_filename.js define-plugin-plugin/create-once
```
If rule is defined using `createOnce`, and it returns an empty visitor (`before` / `after` hooks only), then don't run the hooks - the rule is a no-op.
Reason this is important is forward-compatibility with an optimization we could make where we calculate on Rust side whether AST contains any nodes that rules act on, and if it doesn't, skip calling into JS entirely. In that case, the `before` hook wouldn't run on those files.
We can't emulate that behavior entirely, but we can at least avoid users creating rules which *only* contain a `before` hook, and then finding that when we apply that optimization later on, it stops behaving as expected.
i.e. we make the `before` hook never be called for a rule defined like this:
```js
export default {
createOnce(context) {
return {
before() {
if (content.filename.endsWith('.tsx') {
context.report({ message: 'No TSX!', node: context.sourceCode.ast });
}
},
};
},
};
```
Add `loc` to all AST nodes.
`loc` is computed lazily, as it's quite an expensive operation - because it requires searching the source text for line breaks and building a translation table.
We can consider making `loc` a "real" property once we have a faster translation table built on Rust side.
Add `range` field to AST nodes, using the support for `range` in raw transfer added in #14319.
Use `range` field rather than `start` + `end` in nodes passed to `context.report` and fixer methods - for closer alignment with ESLint.
Add a test to make sure `createOnce` can return an object with only `before` and `after` hooks (no visitor functions) and that both hooks are called for each file in this case.
`oxlint` package export TS types. `defineRule` and `definePlugin` now give you type-safety and intellisense.
Writing plugins in TS is only supported on versions of NodeJS which support type-stripping. Have not added a library (e.g. Jiti) to support older NodeJS versions.
Probably some of the types are wrong. But I think this is a decent start.
"Are the types wrong" (added in #14177) is reporting an error in `index.d.ts`:
```ts
import { VisitorObject as Visitor } from "../dist/generated/visit/visitor.js";
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```
because the file in `dist/generated/visit` is actually called `visitor.d.ts` (`.d.ts`, not `.js`).
I'm not sure if that's a mistake in Rolldown's bundling, "Are the types wrong" being too strict, or a genuine problem.
But regardless, TS seems to pick up the types from `visitor.d.ts`, so I think we can probably merge this and look into / fix it later.
Include `stderr` output in test snapshots. This makes sure that the "JS plugins are experimental" warning is not output if user doesn't use JS plugins.
Trim leading/trailing line breaks from `stdout` in test snapshots and convert `\r\n` to `\n`. This ensures tests will pass on Windows, and removes extraneous line breaks from snapshots.
Fixes#12432.
JS plugins were previously configured by adding them to `plugins` array in config file. Instead, move them to a separate `jsPlugins` array.
Before:
```json
{
"plugins": ["oxc", "react", "./custom-plugin.js"],
}
```
After:
```json
{
"plugins": ["oxc", "react"],
"jsPlugins": ["./custom-plugin.js"],
}
```
Pure refactor. Remove `"ignorePatterns": ["**/*", "!files/**"]` from `.oxlintrc.json` in every test fixture, by running `oxlint` with `oxlint --js-plugins files`.
Pure refactor of tests. Standardize the directory structure of all test fixtures.
* Move files to be linted into `files` subdirectory.
* Move plugin to a file called `plugin.js` (except 1 fixture which has multiple plugins).
* Change `ignorePatterns` in `.oxlintrc.json` config files to whitelist files in `files` dir, instead of blacklisting the plugin file.
Export a method `definePlugin` from `oxlint` package, to go with `defineRule`. Used as follows:
```js
import { definePlugin, defineRule } from 'oxlint';
const myRule = defineRule({
create(context) { /* rule impl */ },
});
export default definePlugin({
meta: { name: "my-plugin" },
rules: {
'my-rule': myRule,
},
});
```
At present this method doesn't have much utility, as we don't export TS types for it yet, but it's part of the intended API, so may as well add it now.
Add `defineRule` method for defining rules. This is the 2nd part of the "alternative API" mentioned in https://github.com/oxc-project/oxc/issues/9905#issuecomment-3208424180.
```js
import { defineRule } from 'oxlint';
export default defineRule({
meta: { /* ... */ },
create(context) {
// Rule implementation
}
});
```
or, using the alternative `createOnce` API:
```js
export default defineRule({
meta: { /* ... */ },
createOnce(context) {
// Rule implementation
}
});
```
When using `createOnce`, `defineRule` creates a `create` method and adds it to the rule object. So the rule remains ESLint-compatible, while getting a potential speed boost when the rule is used with Oxlint. This PR includes a test that runs an Oxlint plugin that uses `defineRule` + `createOnce` in ESLint.
Not done yet: We need to export TS type defs, so the type checking benefit of `defineRule` is realized. I'll do that in a later PR.
---
`defineRule` is intended to be used when defining JS plugins, so we should move it into a separate NPM package (`oxlint-tools`?), so that someone publishing a plugin to NPM doesn't need their plugin to have a dependency on `oxlint` itself, which includes large binaries.
But for now, for simplicity, just include it in `oxlint` package.