Skip to content

feat(lint): serve the Oxlint plugin API from vite-plus - #2328

Draft
fengmk2 wants to merge 10 commits into
mainfrom
feat/lint-plugins-export
Draft

feat(lint): serve the Oxlint plugin API from vite-plus#2328
fengmk2 wants to merge 10 commits into
mainfrom
feat/lint-plugins-export

Conversation

@fengmk2

@fengmk2 fengmk2 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Vite+ bundles Oxlint, and vp migrate strips a standalone oxlint dependency. Your own Oxlint JS plugins import the authoring API by name, so that import stops resolving and vp lint fails to load the plugin:

x Failed to load JS plugin: ./lint/my-plugin.js
|   Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'oxlint' imported from .../lint/my-plugin.js

Adding @oxlint/plugins back as a direct dependency fixes that, but it pins a second copy against whatever Oxlint the bundled linter runs. cloudflare/kumo#658 pinned =1.73.0 while the bundled Oxlint moved on. That copy also does not resolve from a plugin file under pnpm's strict layout unless every package holding a plugin declares it.

So this serves the API from vite-plus instead, the same shape as vitest to vite-plus/test.

What it adds

  • vite-plus/lint/plugins re-exports defineRule, definePlugin, and eslintCompatPlugin from the @oxlint/plugins copy vite-plus already depends on. The API always matches the bundled linter, and you pin nothing.
  • vite-plus/lint/plugins-dev re-exports RuleTester. The subpath mirrors upstream's, so the mapping stays mechanical.
  • vp migrate repoints existing imports, and prefer-vite-plus-imports keeps them there.

What the rewrite deliberately does not touch

  • Statements mixing the two oxlint surfaces, such as import { defineConfig, defineRule } from 'oxlint'. The rewrite replaces the whole specifier, so moving it would leave the file invalid.
  • require(). The vite-plus/lint/* exports are ESM-only. Static import, export, and dynamic import() all rewrite.
  • Config-surface imports (defineConfig, OxlintConfig, OxlintOverride), and default, namespace, and side-effect imports, which name no binding.
  • Any package declaring oxlint or @oxlint/plugins in dependencies or peerDependencies. That shape marks a published Oxlint plugin whose consumers may run plain Oxlint. Its peer entry is preserved too.

Tests

Three PTY snapshot fixtures alongside the Rust and vitest units. lint_oxlint_plugin_api proves the export resolves and loads under real vp lint, using a plugin with no @oxlint/plugins dependency of its own. migration_oxlint_js_plugin_imports and migration_oxlint_published_plugin cover the rewrite and the exemption.

324 Rust tests, 988 vitest tests, 8 snapshot cases. Clippy clean.

Two things for a follow-up

skip_tsdown has the same latent flaw this PR fixes for skip_oxlint: rewritePackageJson strips tsdown before the import rewriter reads the manifests, so the signal is already gone. Left alone here to avoid changing unrelated behaviour.

sanitizeMigratedOxlintConfig derives a plugin's rule namespace from its package name. A relative-path JS plugin has none, so the .oxlintrc.json merge drops its rules while keeping the jsPlugins entry: the plugin loads but no rule runs. This hits exactly the repos this PR targets, since in-repo plugins are registered by path. The migrate snapshot records it with a comment so a fix shows up as a diff.

@netlify

netlify Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploy Preview for viteplus-preview canceled.

Name Link
🔨 Latest commit 93c0f22
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/6a79d8075af6e800082189bd

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

✅ Staging deployment successful!

Preview: https://viteplus-staging.void.app/
Commit: 93c0f22

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Native binary sizes (93c0f22)

Final release artifacts built by the canonical build-upstream and build-windows-cli actions.

Artifact Format Base PR Change
vp (Linux x64) Binary 10.52 MiB 10.52 MiB 0 B (0.00%)
vp (Linux x64) gzip -9 4.55 MiB 4.55 MiB 0 B (0.00%)
NAPI (Linux x64) Binary 33.68 MiB 33.69 MiB +12.00 KiB (+0.03%)
NAPI (Linux x64) gzip -9 13.08 MiB 13.08 MiB +2.09 KiB (+0.02%)
vp (macOS ARM64) Binary 7.87 MiB 7.87 MiB 0 B (0.00%)
vp (macOS ARM64) gzip -9 3.97 MiB 3.97 MiB 0 B (0.00%)
NAPI (macOS ARM64) Binary 41.00 MiB 41.02 MiB +16.13 KiB (+0.04%)
NAPI (macOS ARM64) gzip -9 17.31 MiB 17.31 MiB +2.12 KiB (+0.01%)
vp (Windows x64) Binary 8.41 MiB 8.41 MiB 0 B (0.00%)
vp (Windows x64) gzip -9 3.66 MiB 3.66 MiB -1 B (-0.00%)
NAPI (Windows x64) Binary 27.77 MiB 27.78 MiB +9.50 KiB (+0.03%)
NAPI (Windows x64) gzip -9 10.89 MiB 10.90 MiB +2.27 KiB (+0.02%)
Trampoline (Windows x64) Binary 205.00 KiB 205.00 KiB 0 B (0.00%)
Trampoline (Windows x64) gzip -9 99.00 KiB 99.00 KiB -1 B (-0.00%)
Installer (Windows x64) Binary 4.45 MiB 4.45 MiB 0 B (0.00%)
Installer (Windows x64) gzip -9 2.09 MiB 2.09 MiB -1 B (-0.00%)

@socket-security

socket-security Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​oxlint/​plugins@​1.78.0781009996100

View full report

@fengmk2
fengmk2 force-pushed the feat/lint-plugins-export branch from 5a10b7a to ee5e4e8 Compare August 6, 2026 08:43
@fengmk2 fengmk2 linked an issue Aug 9, 2026 that may be closed by this pull request
3 tasks
@fengmk2 fengmk2 self-assigned this Aug 9, 2026
Comment thread docs/guide/migrate-rules.md Outdated
@fengmk2
fengmk2 force-pushed the feat/lint-plugins-export branch from ee5e4e8 to c748fa1 Compare August 9, 2026 12:32
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c748fa165d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/package.json Outdated
Comment thread packages/cli/src/oxlint-plugin.ts Outdated
Comment thread packages/cli/src/oxlint-plugin.ts
Comment thread packages/cli/src/oxlint-plugin.ts
Comment thread crates/vp_migration/src/import_rewriter.rs
fengmk2 added a commit that referenced this pull request Aug 10, 2026
Seven fixes from review on #2328.

Correctness of the rewrite:

- A statement that mixes the two `oxlint` surfaces, such as
  `import { defineConfig, defineRule } from 'oxlint'`, is now left alone.
  The rewrite replaces the whole specifier, so moving it stripped
  `defineConfig` of its module. Splitting is the user's call.
- `require('@oxlint/plugins')` and `require('oxlint/plugins-dev')` no
  longer rewrite. The `vite-plus/lint/*` exports are ESM-only, so a
  rewritten `require()` failed with ERR_PACKAGE_PATH_NOT_EXPORTED. Static
  import, export, and dynamic `import()` still rewrite, because those
  resolve through the `import` condition.

The published-plugin exemption, which did not work end to end:

- `rewritePackageJson` strips `oxlint` before the import rewriter reads the
  manifests, so `SkipPackages::skip_oxlint` never saw the signal in a real
  migration. `collectOxlintOwnerDirs` now captures it before the edit and
  passes the directories through to the rewriter. Note that `skip_tsdown`
  has the same latent flaw, since `tsdown` is also in `REMOVE_PACKAGES`;
  this change does not touch it.
- `vp lint --fix` rewrote a published plugin's source unconditionally,
  undoing the exemption the migration had just honored. The rule now checks
  the nearest manifest, reusing the mtime-keyed cache shape already used
  for `@nuxt/test-utils`.
- The `oxlint` peer entry is no longer stripped from a package that owns
  the plugin API. A peer is a consumer contract, not a tool the package
  runs, and removing it left the source importing a package the manifest
  no longer declared.
- `declare module '@oxlint/plugins'` and the `oxlint` forms are preserved,
  the same way the rule already preserves Vitest-family augmentations. The
  re-exported types keep their upstream module identity, so a retargeted
  augmentation stopped merging.

Cleanup:

- The migration now drops a dead `@oxlint/plugins` devDependency, since
  nothing imports it after the rewrite. Only from devDependencies: a
  `dependencies` or `peerDependencies` edge marks a published plugin.

Tests: 3 Rust cases for the mixed, `require`, and dynamic-import rules; 8
lint-rule cases for the published-plugin and augmentation guards; and a new
`migration_oxlint_published_plugin` snapshot fixture covering the skip.
@fengmk2
fengmk2 force-pushed the feat/lint-plugins-export branch from c748fa1 to 43a9b87 Compare August 10, 2026 05:44
@fengmk2 fengmk2 added the preview-build Publish this PR's commits to the registry bridge as preview builds label Aug 10, 2026
fengmk2 added a commit that referenced this pull request Aug 10, 2026
Seven fixes from review on #2328.

Correctness of the rewrite:

- A statement that mixes the two `oxlint` surfaces, such as
  `import { defineConfig, defineRule } from 'oxlint'`, is now left alone.
  The rewrite replaces the whole specifier, so moving it stripped
  `defineConfig` of its module. Splitting is the user's call.
- `require('@oxlint/plugins')` and `require('oxlint/plugins-dev')` no
  longer rewrite. The `vite-plus/lint/*` exports are ESM-only, so a
  rewritten `require()` failed with ERR_PACKAGE_PATH_NOT_EXPORTED. Static
  import, export, and dynamic `import()` still rewrite, because those
  resolve through the `import` condition.

The published-plugin exemption, which did not work end to end:

- `rewritePackageJson` strips `oxlint` before the import rewriter reads the
  manifests, so `SkipPackages::skip_oxlint` never saw the signal in a real
  migration. `collectOxlintOwnerDirs` now captures it before the edit and
  passes the directories through to the rewriter. Note that `skip_tsdown`
  has the same latent flaw, since `tsdown` is also in `REMOVE_PACKAGES`;
  this change does not touch it.
- `vp lint --fix` rewrote a published plugin's source unconditionally,
  undoing the exemption the migration had just honored. The rule now checks
  the nearest manifest, reusing the mtime-keyed cache shape already used
  for `@nuxt/test-utils`.
- The `oxlint` peer entry is no longer stripped from a package that owns
  the plugin API. A peer is a consumer contract, not a tool the package
  runs, and removing it left the source importing a package the manifest
  no longer declared.
- `declare module '@oxlint/plugins'` and the `oxlint` forms are preserved,
  the same way the rule already preserves Vitest-family augmentations. The
  re-exported types keep their upstream module identity, so a retargeted
  augmentation stopped merging.

Cleanup:

- The migration now drops a dead `@oxlint/plugins` devDependency, since
  nothing imports it after the rewrite. Only from devDependencies: a
  `dependencies` or `peerDependencies` edge marks a published plugin.

Tests: 3 Rust cases for the mixed, `require`, and dynamic-import rules; 8
lint-rule cases for the published-plugin and augmentation guards; and a new
`migration_oxlint_published_plugin` snapshot fixture covering the skip.
@fengmk2
fengmk2 force-pushed the feat/lint-plugins-export branch from 43a9b87 to a7180fa Compare August 10, 2026 06:21
@fengmk2 fengmk2 added preview-build Publish this PR's commits to the registry bridge as preview builds and removed preview-build Publish this PR's commits to the registry bridge as preview builds labels Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Registry bridge build (9aa3b7d)

This commit build is published to the registry bridge, which serves these as ordinary npm versions (every other package proxies to npmjs):

Package Version
vite-plus 0.0.0-commit.9aa3b7d400901cdee9e1dd21f50a4640788d23f2
@voidzero-dev/vite-plus-core 0.0.0-commit.9aa3b7d400901cdee9e1dd21f50a4640788d23f2

Install the Vite+ CLI built from this commit, then migrate a project:

# macOS / Linux
curl -fsSL https://vite.plus | VP_PR_VERSION=2328 bash
# Windows (PowerShell)
$env:VP_PR_VERSION="2328"; irm https://vite.plus/ps1 | iex

After installing, upgrade the current project's vite-plus to this test build with:

vp migrate

Or point your package manager at the bridge registry https://registry-bridge.viteplus.dev/:

Package manager Registry config
npm / pnpm / Bun .npmrc: registry=https://registry-bridge.viteplus.dev/
Yarn (v2+) .yarnrc.yml: npmRegistryServer: "https://registry-bridge.viteplus.dev/"

Then pin the build (vite aliases to vite-plus-core; pnpm can use a catalog, npm an overrides entry):

{
  "devDependencies": {
    "vite-plus": "0.0.0-commit.9aa3b7d400901cdee9e1dd21f50a4640788d23f2",
    "vite": "npm:@voidzero-dev/vite-plus-core@0.0.0-commit.9aa3b7d400901cdee9e1dd21f50a4640788d23f2"
  }
}

fengmk2 added a commit that referenced this pull request Aug 10, 2026
The Docker preview job now fails outright:

  #14 exporting to GitHub Actions Cache
  #14 ERROR: error writing layer blob: failed to reserve cache
  #13 exporting to image ... CANCELED
  ERROR: failed to build: failed to solve: error writing layer blob

The cache export is fatal to the build, so it cancelled the image push. My
optimization broke the job it was meant to speed up, and the npm preview for
PR #2328 published fine while its Docker image did not.

Reverting rather than fixing it. Making it work would need `actions: write`
on the one job that installs and executes the preview package, which is the
job SR-5 says to keep as unprivileged as possible, and this was the only
`type=gha` usage in the repo so there was no working precedent to copy. The
benefit was 60-90s of apt on a path that already waits on a human approval
measured in minutes to days, so it was buying almost nothing.

`ignore-error=true` would keep the build green but the export would keep
failing, leaving a dead directive and a stack trace in every log.
fengmk2 added a commit that referenced this pull request Aug 10, 2026
Two fixes to `main`, both found by the first real runs of the publishing
workflow. It could not be exercised before merge, because `workflow_run`
only fires for workflow files already on the default branch.

**First, the good news: the design works.** PR #2328 published end to
end through the new path with an OIDC token, no admin token involved.
`authorize`, `Pkg Preview`, and the sticky comment all succeeded, and
`commit.a7180fa85c06fad48` is on the bridge:

```
commit.a7180fa85c06fad48 | pr: .../pull/2328 | at: 2026-08-10T07:35:31.617Z
```

## 1. Fork PRs could not be resolved at all

#2391 (from `liangmiQwQ`) failed in `authorize` with `no open PR of
voidzero-dev/vite-plus has head c7e51be…` while that PR was open with
exactly that head.

`listPullRequestsAssociatedWithCommit` returns **empty** for a fork PR's
head commit. Confirmed against the live API:

| commit | result |
| --- | --- |
| #2387 head (same-repo) | returns `#2387` |
| #2391 head (fork) | **empty** |

So it worked for every case reachable before merge and failed for the
only case this feature exists for. `workflow_run.pull_requests` is empty
for forks too, which is what sent me to the commit endpoint originally —
I swapped one fork-blind source for another.

Now resolves via `pulls?state=open&head=<head_owner>:<head_branch>`,
both GitHub-signed payload fields. The head-sha match is a separate step
so the message distinguishes "no such PR" from "the PR moved on":

```
fork PR 2391 (real failure) -> OK: #2391 labeled=true fork=true
stale head                  -> FAIL: PR #2391 now at c7e51be, built 0000000
no such branch              -> FAIL: no open PR from liangmiQwQ:does-not-exist
```

I re-checked the rest of the publishing workflow for the same blind
spot. Everything else keys off the PR number or the run id, which are
base-repo objects and fork-safe: the post-approval `pulls.get` re-check
returns correct state, head and labels for #2391, and the artifact
download and the `listWorkflowRunArtifacts` precondition both see that
run's 148MB `bridge-packages`.

## 2. The Docker gha cache broke the image push

```
#14 exporting to GitHub Actions Cache
#14 ERROR: error writing layer blob: failed to reserve cache
#13 exporting to image ... CANCELED
```

The cache export is fatal to the build, so it cancelled the push. I
added this in the cleanup pass; it broke the job it was meant to speed
up, and #2328's npm preview published while its Docker image did not.

Reverted rather than repaired. Making it work needs `actions: write` on
the one job that installs and executes the preview package, which is the
job SR-5 says to keep unprivileged, and this was the only `type=gha`
usage in the repo so there was no working precedent. It was saving
60-90s of apt on a path that already waits on a human approval measured
in minutes to days.

## 3. Terminology

"Trusted leg" and "build leg" were my own coinage and meant nothing to a
reader who was not in the design conversation. The two workflows are now
described as **the build workflow** and **the publishing workflow**, and
where trust was the point the property is stated rather than encoded in
a name.

This also surfaced something worth fixing later: `publish-preview.yml`
is named "Publish preview build" and no longer publishes anything.
Renaming it is the real fix, but the publishing workflow matches it by
`name:`, so that has to be a coordinated change. The header says so
outright for now.

The same terminology fix for the RFC and bridge docs is
voidzero-dev/pkg-pr-registry-bridge#93, which also corrects SR-1 for the
fork-blind endpoint above.

## After merging

Re-label #2391 to get the first genuine fork preview.
Vite+ bundles Oxlint, and `vp migrate` strips a standalone `oxlint`
dependency. A project's own Oxlint JS plugins import the authoring API by
name. That import then stops resolving, and `vp lint` fails to load the
plugin.

Adding `@oxlint/plugins` back as a direct dependency fixes it, but pins a
second copy against whatever Oxlint the bundled linter runs. It also does
not resolve from a plugin file under pnpm's strict layout, unless every
package that holds a plugin declares it.

Expose the API from vite-plus instead, and repoint the imports at it:

- `vite-plus/lint/plugins` re-exports `defineRule`, `definePlugin`, and
  `eslintCompatPlugin` from the `@oxlint/plugins` copy vite-plus already
  depends on. The API therefore always matches the bundled linter.
- `vite-plus/lint/plugins-dev` re-exports `RuleTester`, which lives in
  `oxlint/plugins-dev` and breaks the same way. The subpath mirrors
  upstream's, so the mapping stays mechanical and later additions to that
  entry still fit the name.
- `vp migrate` rewrites `@oxlint/plugins` and `oxlint/plugins-dev` in every
  statement form. It rewrites bare `oxlint` only when the import names a
  binding outside Oxlint's config surface. `defineConfig`, `OxlintConfig`,
  and `OxlintOverride` imports therefore stay as they are. Default,
  namespace, and side-effect imports name no binding, so the migration
  skips them.
- The migration skips a package that declares `oxlint` or
  `@oxlint/plugins` in `dependencies` or `peerDependencies`. That shape
  marks a published Oxlint plugin, and its consumers may not run Vite+.
- `prefer-vite-plus-imports` repeats the mapping, so the codemod has a
  standing backstop.
Two PTY fixtures for the `vite-plus/lint/plugins` work:

- `lint_oxlint_plugin_api`: a local JS plugin authored against
  `vite-plus/lint/plugins`. It declares no `@oxlint/plugins` dependency of
  its own. A reported diagnostic therefore proves the export resolves and
  loads under `vp lint`, which is the premise of the change. The same case
  covers the `prefer-vite-plus-imports` autofix for all three legacy
  authoring specifiers, and shows that the config surface stays clean.
- `migration_oxlint_js_plugin_imports`: `vp migrate` rewrites a plugin, a
  rule module, and a rule test. It leaves `defineConfig` and
  `OxlintOverride` imports alone.

The migrate snapshot also records a pre-existing gap, unrelated to this
change. `sanitizeMigratedOxlintConfig` derives a plugin's rule namespace
from its package name. A relative-path JS plugin has no package name, so
the `.oxlintrc.json` merge drops its rules. The snapshot records this with
a comment instead of hiding it, so a fix shows up as a snapshot diff.
Seven fixes from review on #2328.

Correctness of the rewrite:

- A statement that mixes the two `oxlint` surfaces, such as
  `import { defineConfig, defineRule } from 'oxlint'`, is now left alone.
  The rewrite replaces the whole specifier, so moving it stripped
  `defineConfig` of its module. Splitting is the user's call.
- `require('@oxlint/plugins')` and `require('oxlint/plugins-dev')` no
  longer rewrite. The `vite-plus/lint/*` exports are ESM-only, so a
  rewritten `require()` failed with ERR_PACKAGE_PATH_NOT_EXPORTED. Static
  import, export, and dynamic `import()` still rewrite, because those
  resolve through the `import` condition.

The published-plugin exemption, which did not work end to end:

- `rewritePackageJson` strips `oxlint` before the import rewriter reads the
  manifests, so `SkipPackages::skip_oxlint` never saw the signal in a real
  migration. `collectOxlintOwnerDirs` now captures it before the edit and
  passes the directories through to the rewriter. Note that `skip_tsdown`
  has the same latent flaw, since `tsdown` is also in `REMOVE_PACKAGES`;
  this change does not touch it.
- `vp lint --fix` rewrote a published plugin's source unconditionally,
  undoing the exemption the migration had just honored. The rule now checks
  the nearest manifest, reusing the mtime-keyed cache shape already used
  for `@nuxt/test-utils`.
- The `oxlint` peer entry is no longer stripped from a package that owns
  the plugin API. A peer is a consumer contract, not a tool the package
  runs, and removing it left the source importing a package the manifest
  no longer declared.
- `declare module '@oxlint/plugins'` and the `oxlint` forms are preserved,
  the same way the rule already preserves Vitest-family augmentations. The
  re-exported types keep their upstream module identity, so a retargeted
  augmentation stopped merging.

Cleanup:

- The migration now drops a dead `@oxlint/plugins` devDependency, since
  nothing imports it after the rewrite. Only from devDependencies: a
  `dependencies` or `peerDependencies` edge marks a published plugin.

Tests: 3 Rust cases for the mixed, `require`, and dynamic-import rules; 8
lint-rule cases for the published-plugin and augmentation guards; and a new
`migration_oxlint_published_plugin` snapshot fixture covering the skip.
@fengmk2
fengmk2 force-pushed the feat/lint-plugins-export branch from a7180fa to 9aa3b7d Compare August 10, 2026 08:40
@fengmk2 fengmk2 added preview-build Publish this PR's commits to the registry bridge as preview builds and removed preview-build Publish this PR's commits to the registry bridge as preview builds labels Aug 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🐳 Docker preview image

Built from this PR's registry bridge build:

Image Compressed size
ghcr.io/voidzero-dev/vite-plus:pr-2328 236MB
# remove any stale local copy from a previous run, then pull fresh
docker rmi ghcr.io/voidzero-dev/vite-plus:pr-2328 2>/dev/null; docker pull ghcr.io/voidzero-dev/vite-plus:pr-2328

Quick check:

docker run --rm ghcr.io/voidzero-dev/vite-plus:pr-2328 vp --version

See docs/guide/docker.md for usage.

@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

`vp check` failed with `unicorn(no-empty-file)`: Oxlint counts a file whose
only content is a comment as empty.

The file was never needed. `nearestPackageOwnsOxlintApi` walks up from the
filename to find the nearest package.json and never reads the file itself,
so the RuleTester cases only need a path, not a file on disk. This matches
the existing `fixtures/nuxt-test-utils/` fixture, which is a package.json
alone while the spec filenames it is referenced by do not exist.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9aa3b7d400

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/migration/migrator/package-json.ts Outdated
Comment thread packages/cli/src/migration/migrator/package-json.ts
Comment thread packages/cli/src/migration/migrator/package-json.ts
Comment thread packages/cli/src/oxlint-plugin.ts
Comment thread packages/cli/src/oxlint-plugin.ts
Five findings, two of them interactions between fixes from the first round.

- Deleting the dead `@oxlint/plugins` devDependency broke a preserved
  `require('@oxlint/plugins')`. Round one stopped rewriting `require()`
  because the target is ESM-only, which left the direct dependency as the
  only resolvable copy under pnpm's strict layout. The deletion is now
  gated on a source scan for those CommonJS forms.
- `collectOxlintOwnerDirs` treats `dependencies` or `peerDependencies` as
  ownership, but the manifest-preservation check only looked at peers. A
  published plugin declaring `oxlint` under `dependencies` kept its source
  but lost the edge that provided it. Both checks now match.
- A preserved `catalog:` peer would dangle, because the catalog rewrite
  still drops every `REMOVE_PACKAGES` entry. Such a reference is now
  resolved to the concrete range before the entry goes away.
- `import plugins = require('@oxlint/plugins')` was still autofixed. It has
  require semantics, so it is skipped like plain `require()`.
- `import oxlint, { defineRule } from 'oxlint'` was rewritten, but
  `vite-plus/lint/plugins` has no default export. A default or namespace
  binding now disqualifies the statement, in both implementations.

Tests: one Rust case for the default-binding guard, three lint-rule cases
for the import-equals and default-binding guards.
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f1f9a7578

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/migration/migrator/package-json.ts
Comment thread crates/vp_migration/src/import_rewriter.rs
Comment thread packages/cli/src/migration/migrator/source-scan.ts Outdated
Comment thread crates/vp_migration/src/import_rewriter.rs
Comment thread crates/vp_migration/src/import_rewriter.rs
Three of five findings were real.

- `export { defineRule } from 'oxlint'` was not rewritten. A named
  re-export identifies the surface exactly as an import does, so it now
  follows the same rules in both implementations. `export * from 'oxlint'`
  names nothing and stays put, as do config-surface re-exports.
- The CommonJS scan that guards the `@oxlint/plugins` deletion matched four
  fixed substrings, so `require( '@oxlint/plugins' )` and a line break
  before the argument slipped through. It is now a tolerant regex that also
  catches `createRequire(...)(...)`, and it errs toward keeping the
  dependency: a false positive leaves one unused entry, a false negative
  breaks a plugin at load time.
- The migrate fixture records that `oxlint` itself is still deleted while a
  config-surface import survives. That gap predates this PR, since those
  imports were never rewritten and `oxlint` was always in REMOVE_PACKAGES,
  so it is captured with a comment rather than fixed here.

The other two did not reproduce, and both are now pinned by tests:

- `import plugins = require('@oxlint/plugins')` is NOT rewritten by the
  Rust rule. tree-sitter does not surface it as a plain `import_statement`
  string, so the ESM rules never match it.
- `type C = import('@oxlint/plugins').Context` IS rewritten, and correctly:
  the shim re-exports the types, so the type resolves through it.
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c73a93c7f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/vp_migration/src/import_rewriter.rs
Comment thread packages/cli/src/migration/migrator/source-scan.ts Outdated
Comment thread crates/vp_migration/src/import_rewriter.rs
Fourth review round. One real finding, one trivial, one that did not
reproduce.

`vite-plus/lint/plugins` now ships a CJS build alongside the ESM one, and
its export gained a `require` condition. A `.cts` plugin, or a `.ts` one
compiled with `module: commonjs`, emits its import as `require()`, which the
ESM-only export rejected at runtime even though typechecking passed.
`@oxlint/plugins` ships CJS, so the shim can mirror it.

`lint/plugins-dev` deliberately stays ESM-only: `oxlint/plugins-dev` is
ESM-only upstream, so the shim mirrors exactly what upstream can offer
rather than inventing a capability.

The CommonJS scan that guards the dependency deletion now also tolerates
block comments inside the call, and separately catches the `createRequire`
indirection. Its comment previously claimed the latter without doing it.

`export const pluginApi = '@oxlint/plugins'` is NOT rewritten, contrary to
the report: `inside:` matches the immediate parent, and that string sits
under a lexical_declaration rather than directly under the export
statement. Pinned by a test so a rule loosened to `stopBy: end` cannot
start corrupting data.
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 729e7b3a71

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/migration/migrator/source-scan.ts Outdated
Comment thread packages/cli/src/migration/migrator/package-json.ts Outdated
Fifth review round found two more spellings the pre-rewrite scan missed:
a template-literal `require(`@oxlint/plugins`)` and a JSDoc
`@typedef {import('@oxlint/plugins').Context}`, which is a comment and so
invisible to every ast-grep string rule.

Both are symptoms of the same wrong shape. The scan ran BEFORE the import
rewrite, so it had to predict which specifiers the rewriter would leave
behind, and every missed spelling silently deleted a dependency that was
still load-bearing. Three rounds of regex patching did not converge.

The deletion now runs AFTER the rewrite, in `dropDeadOxlintPluginsDependency`.
By then every form the rewriter handles has already become a
`vite-plus/lint/*` specifier, so a plain substring scan for the package name
answers the only question that matters: does anything still need it? That
covers `require()`, import-equals, JSDoc, template literals, and plain
strings without enumerating any of them, and the tolerant-regex machinery
is gone.

Snapshots are unchanged, which is the point: same outcome for the cases that
were already right, and no longer wrong for the ones that were not.
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f160c0255a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/migration/migrator/source-scan.ts Outdated
Comment thread crates/vp_migration/src/import_rewriter.rs
Comment thread packages/cli/src/migration/migrator/source-scan.ts Outdated
Sixth review round, three findings, all real.

`export default '@oxlint/plugins'` had its exported DATA VALUE rewritten.
That literal is a direct child of the export statement, so an unconstrained
`inside: export_statement` matched it. The export rules now match the
`source` field, so only a genuine re-export qualifies.

My earlier probe of this claim tested `export const x = '...'` and a literal
nested in an object, both of which sit below a declaration and are safe. It
missed the bare direct-child form, so the disproof was wrong. Pinned now.

Moving the dependency deletion after the rewrite dropped the `needVitePlus`
signal it used to set. A monorepo leaf whose only migration signal was an
`@oxlint/plugins` devDependency would get its imports repointed at
`vite-plus/lint/plugins` without gaining a direct `vite-plus` edge. The edge
decision moves back to `rewritePackageJson`, where the pre-rewrite manifest
is still intact; only the deletion happens later.

The retention scan stopped at nested package.json boundaries, so a
non-workspace example or fixture directory holding a preserved
`require('@oxlint/plugins')` went unscanned while the root dependency it
resolved was deleted. That scan now crosses package boundaries, which is
correct for this question specifically: the nested file resolves the root's
dependency by walking up.
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbde05c23c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cli/src/migration/migrator/package-json.ts Outdated
Seventh review round, one finding.

`@oxlint/plugins` declared only in `optionalDependencies` still has its
imports repointed at `vite-plus/lint/plugins`, but it did not set
`needVitePlus`, so a standalone project or an isolated leaf could finish
with rewritten imports and no declared `vite-plus`.

An optional install edge provisions the API the same way a dev one does, so
both now feed the same signal, and the post-rewrite cleanup clears whichever
of the two declared it.
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93c0f22ce2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/vp_migration/src/import_rewriter.rs
Comment thread packages/cli/src/migration/migrator/package-json.ts
@fengmk2 fengmk2 added test: e2e Auto run e2e tests test: install-e2e run vite install e2e test test: create-e2e Run `vp create` e2e tests test: sfw labels Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview-build Publish this PR's commits to the registry bridge as preview builds test: create-e2e Run `vp create` e2e tests test: e2e Auto run e2e tests test: install-e2e run vite install e2e test test: sfw

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose oxlint/plugins-dev and oxlint/plugins exports via vite-plus

1 participant