Skip to content

Commit d97a27a

Browse files
authored
feat(lint): export Oxlint plugin APIs from vite-plus (#2328)
`vp migrate` can break custom Oxlint plugins when it removes the dependency that provides their API. This PR adds two entry points for the API that matches the bundled linter: - `vite-plus/lint/plugins` exports helpers such as `definePlugin` and `defineRule`. - `vite-plus/lint/plugins-dev` exports `RuleTester`. Both entry points support ESM and CommonJS. Projects can use them through their `vite-plus` dependency, including with `pnpm`'s strict dependency layout. The migrator updates supported imports from `@oxlint/plugins`, `oxlint/plugins-dev`, and the legacy plugin API in `oxlint`. The `prefer-vite-plus-imports` lint rule applies the same changes. The rewrite leaves `oxlint` config imports and ambiguous imports unchanged. It also leaves `require()` calls unchanged. Packages that declare `oxlint` or `@oxlint/plugins` in `dependencies` or `peerDependencies` keep their upstream plugin imports and dependencies. The same exemption applies to `@oxlint/plugins` in `optionalDependencies`. This protects published plugins whose consumers do not use `vite-plus`. The cleanup removes `@oxlint/plugins` from `devDependencies` only when no references remain in the project. It checks source files, package import aliases, and ignored build output such as `dist`, `build`, and `out`.
1 parent 13e1e5a commit d97a27a

63 files changed

Lines changed: 2682 additions & 88 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e-test.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,13 @@ jobs:
327327
vp check --fix
328328
vp run check
329329
vp test run --project unit --shard=1/3
330+
- name: videojs-v10
331+
node-version: 24
332+
command: |
333+
node $GITHUB_WORKSPACE/ecosystem-ci/verify-videojs-v10.ts
334+
# The upstream preset keeps existing diagnostics at warning level.
335+
vp lint --quiet
336+
vp test run tools/oxlint/anti-slop/rules/tests
330337
- name: reactive-resume
331338
node-version: 24
332339
command: |
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Authored against the API vite-plus re-exports, with no `@oxlint/plugins`
2+
// dependency of its own: the point of the test is that this resolves and loads.
3+
import { definePlugin, defineRule } from 'vite-plus/lint/plugins';
4+
5+
const noFoo = defineRule({
6+
meta: { messages: { noFoo: 'Do not name things "foo".' } },
7+
create(context) {
8+
return {
9+
Identifier(node) {
10+
if (node.name === 'foo') {
11+
context.report({ node, messageId: 'noFoo' });
12+
}
13+
},
14+
};
15+
},
16+
});
17+
18+
export default definePlugin({
19+
meta: { name: 'local' },
20+
rules: { 'no-foo': noFoo },
21+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "lint-oxlint-plugin-api",
3+
"version": "0.0.0",
4+
"private": true
5+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[[case]]
2+
name = "lint_oxlint_plugin_api"
3+
vp = "local"
4+
skip-platforms = [{ os = "linux", libc = "musl" }]
5+
steps = [
6+
{ argv = [
7+
"vp",
8+
"lint",
9+
"src/uses-foo.ts",
10+
], comment = "the local JS plugin imports its API from vite-plus/lint/plugins. It declares no @oxlint/plugins dependency. A reported diagnostic therefore proves the export resolved and loaded", continue-on-failure = true },
11+
{ argv = [
12+
"vp",
13+
"lint",
14+
"src/legacy-imports.ts",
15+
], comment = "prefer-vite-plus-imports reports the three legacy authoring specifiers", continue-on-failure = true },
16+
{ argv = [
17+
"vp",
18+
"lint",
19+
"src/config-surface.ts",
20+
], comment = "oxlint still owns defineConfig and OxlintOverride, so these are clean", continue-on-failure = true },
21+
{ argv = [
22+
"vp",
23+
"lint",
24+
"--fix",
25+
"src/legacy-imports.ts",
26+
], comment = "the autofix matches what vp migrate rewrites", continue-on-failure = true },
27+
{ argv = [
28+
"vpt",
29+
"print-file",
30+
"src/legacy-imports.ts",
31+
], continue-on-failure = true },
32+
{ argv = [
33+
"vp",
34+
"lint",
35+
"src/legacy-imports.ts",
36+
], comment = "confirm the rewritten file is clean", continue-on-failure = true },
37+
]
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# lint_oxlint_plugin_api
2+
3+
## `vp lint src/uses-foo.ts`
4+
5+
the local JS plugin imports its API from vite-plus/lint/plugins. It declares no @oxlint/plugins dependency. A reported diagnostic therefore proves the export resolved and loaded
6+
7+
**Exit code:** 1
8+
9+
```
10+
11+
× local(no-foo): Do not name things "foo".
12+
╭─[src/uses-foo.ts:1:14]
13+
1 │ export const foo = 1;
14+
· ───
15+
2 │ export const bar = 2;
16+
╰────
17+
18+
Found 0 warnings and 1 error.
19+
Finished in <duration> on 1 file with <n> rules using <n> threads.
20+
```
21+
22+
## `vp lint src/legacy-imports.ts`
23+
24+
prefer-vite-plus-imports reports the three legacy authoring specifiers
25+
26+
**Exit code:** 1
27+
28+
```
29+
30+
× vite-plus(prefer-vite-plus-imports): Use 'vite-plus/lint/plugins' instead of 'oxlint' in Vite+ projects.
31+
╭─[src/legacy-imports.ts:1:28]
32+
1 │ import { defineRule } from 'oxlint';
33+
· ────────
34+
2 │ import { definePlugin } from '@oxlint/plugins';
35+
╰────
36+
37+
× vite-plus(prefer-vite-plus-imports): Use 'vite-plus/lint/plugins' instead of '@oxlint/plugins' in Vite+ projects.
38+
╭─[src/legacy-imports.ts:2:30]
39+
1 │ import { defineRule } from 'oxlint';
40+
2 │ import { definePlugin } from '@oxlint/plugins';
41+
· ─────────────────
42+
3 │ import { RuleTester } from 'oxlint/plugins-dev';
43+
╰────
44+
45+
× vite-plus(prefer-vite-plus-imports): Use 'vite-plus/lint/plugins-dev' instead of 'oxlint/plugins-dev' in Vite+ projects.
46+
╭─[src/legacy-imports.ts:3:28]
47+
2 │ import { definePlugin } from '@oxlint/plugins';
48+
3 │ import { RuleTester } from 'oxlint/plugins-dev';
49+
· ────────────────────
50+
4 │
51+
╰────
52+
53+
× vite-plus(prefer-vite-plus-imports): Use 'vite-plus/lint/plugins' instead of 'oxlint' in Vite+ projects.
54+
╭─[src/legacy-imports.ts:6:38]
55+
5 │ export { defineRule, definePlugin, RuleTester };
56+
6 │ export { 'defineRule' as rule } from 'oxlint';
57+
· ────────
58+
7 │ export type { 'Context' as RuleContext } from 'oxlint';
59+
╰────
60+
61+
× vite-plus(prefer-vite-plus-imports): Use 'vite-plus/lint/plugins' instead of 'oxlint' in Vite+ projects.
62+
╭─[src/legacy-imports.ts:7:47]
63+
6 │ export { 'defineRule' as rule } from 'oxlint';
64+
7 │ export type { 'Context' as RuleContext } from 'oxlint';
65+
· ────────
66+
╰────
67+
68+
Found 0 warnings and 5 errors.
69+
Finished in <duration> on 1 file with <n> rules using <n> threads.
70+
```
71+
72+
## `vp lint src/config-surface.ts`
73+
74+
oxlint still owns defineConfig and OxlintOverride, so these are clean
75+
76+
```
77+
Found 0 warnings and 0 errors.
78+
Finished in <duration> on 1 file with <n> rules using <n> threads.
79+
```
80+
81+
## `vp lint --fix src/legacy-imports.ts`
82+
83+
the autofix matches what vp migrate rewrites
84+
85+
```
86+
Found 0 warnings and 0 errors.
87+
Finished in <duration> on 1 file with <n> rules using <n> threads.
88+
```
89+
90+
## `vpt print-file src/legacy-imports.ts`
91+
92+
```
93+
import { defineRule } from 'vite-plus/lint/plugins';
94+
import { definePlugin } from 'vite-plus/lint/plugins';
95+
import { RuleTester } from 'vite-plus/lint/plugins-dev';
96+
97+
export { defineRule, definePlugin, RuleTester };
98+
export { 'defineRule' as rule } from 'vite-plus/lint/plugins';
99+
export type { 'Context' as RuleContext } from 'vite-plus/lint/plugins';
100+
```
101+
102+
## `vp lint src/legacy-imports.ts`
103+
104+
confirm the rewritten file is clean
105+
106+
```
107+
Found 0 warnings and 0 errors.
108+
Finished in <duration> on 1 file with <n> rules using <n> threads.
109+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from 'oxlint';
2+
import type { OxlintOverride } from 'oxlint';
3+
4+
export const override: OxlintOverride = { files: ['**/*.ts'] };
5+
6+
export default defineConfig({ overrides: [override] });
7+
export { 'defineConfig' as config } from 'oxlint';
8+
export type { 'OxlintOverride' as Override } from 'oxlint';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineRule } from 'oxlint';
2+
import { definePlugin } from '@oxlint/plugins';
3+
import { RuleTester } from 'oxlint/plugins-dev';
4+
5+
export { defineRule, definePlugin, RuleTester };
6+
export { 'defineRule' as rule } from 'oxlint';
7+
export type { 'Context' as RuleContext } from 'oxlint';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const foo = 1;
2+
export const bar = 2;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from 'vite-plus';
2+
3+
export default defineConfig({
4+
lint: {
5+
jsPlugins: [
6+
'./lint/plugin.js',
7+
{ name: 'vite-plus', specifier: 'vite-plus/oxlint-plugin' },
8+
],
9+
rules: {
10+
'local/no-foo': 'error',
11+
'vite-plus/prefer-vite-plus-imports': 'error',
12+
},
13+
},
14+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
node_modules/

0 commit comments

Comments
 (0)