You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
vp migrate adds a second vite-plus import when it rewrites a vite import #3430
When vp migrate rewrites an import from vite to vite-plus, it does not merge it into an existing vite-plus import in the same file. The file then has two vite-plus imports, and import/no-duplicates reports an error.
vp migrate merges the rewritten import into the existing one:
import{defineConfig,typePlugin}from"vite-plus";
Notes
The 1.6.0 release candidate behaves the same. In ecosystem testing, a project's vp check failed after the upgrade with import(no-duplicates): Module 'vite-plus' is imported more than once in this file.
Unit Test: Yes — crates/vp_migration/src/import_merge.rs and crates/vp_migration/src/import_rewriter.rs
Priority:P3: minor bug — This only affects files that already import from vite-plus alongside a vite import that migration rewrites. It leaves an import/no-duplicates lint failure, but users can manually merge the imports or rely on a linter autofix as a short-term workaround.
The migration's import rewriter applies local ast-grep rules that replace one import specifier at a time. After rewriting import type { Plugin } from 'vite' to vite-plus, no later pass detects that the file now has two imports from vite-plus, so the declarations stay separate.
I found a potential fix for this issue. The branch adds an oxc-based post-processing pass in crates/vp_migration/src/import_rewriter.rs that collapses adjacent duplicate imports after the ast-grep rewrites run, merging value and type-only named imports and emitting inline type specifiers when needed. It skips namespace imports, side-effect imports, imports with attributes, and conflicting bindings. The regression test uses the exact reproducer from the issue.
The migration's import rewriting entry point is in crates/vp_migration/src/import_rewriter.rs:
rewrite_imports_in_directory_with_options (line 2284) walks all TS/JS files.
For each file it calls rewrite_import (line 2399).
rewrite_import calls rewrite_import_content_full (line 2480) which applies pre-loaded ast-grep rules via ast_grep::apply_loaded_rules.
For vite imports, the rewrite-vite-import rule (defined around line 21 of import_rewriter.rs) matches the string literal inside any import_statement whose value is exactly 'vite' or "vite" and replaces the specifier with vite-plus.
Why duplicates are produced
ast-grep rules are local transformations: they only see and rewrite one match at a time.
They have no notion of other import statements in the same file.
There is no later pass that detects two imports from the same module and merges their named specifiers. vp fmt/oxfmt formats the code but does not merge duplicate import declarations, so the import/no-duplicates lint error remains.
Suggested fix
Add a post-processing step after the ast-grep rules have run (inside rewrite_import_content_full or just before returning from rewrite_import) that:
Parses the rewritten source with an AST parser (e.g., oxc).
Finds groups of import declarations that share the same source string.
For each group, merges compatible imports:
Combine named imports into one declaration.
Preserve a default import if only one default exists per source.
Preserve side-effect imports separately.
Preserve namespace imports separately (do not merge with named imports).
When merging a pure type-only import with a value import, emit inline type specifiers (e.g., import { defineConfig, type Plugin } from "vite-plus").
Edge cases
Separate import type + value imports from the same module should collapse into one value import with inline type specifiers.
Default imports, namespace imports (import * as ...), and side-effect imports should not be merged with named imports.
Comments attached to the removed import declarations should ideally be preserved (attached to the merged import or kept as a leading comment).
The same problem can happen for vitest → vite-plus/test rewrites if both vitest and vite-plus/test imports exist in one file.
The problem can happen for tsdown → vite-plus/pack rewrites as well.
Confidence
high
Verify
Claim
Observed: vp migrate rewrites import type { Plugin } from 'vite' to import type { Plugin } from "vite-plus" in a file that already contains import { defineConfig } from "vite-plus", leaving two imports from the same
module.
Expected: the migrated file has a single merged import such as import { defineConfig, type Plugin } from "vite-plus".
Research
Documentation (docs/guide/migrate.md) says migration "Rewrites imports where needed" but does not state that duplicate imports are intentionally left unmerged or that they will be merged.
crates/vp_migration/src/import_rewriter.rs uses local ast-grep rules that replace a single specifier string per match. There is no post-processing pass that merges import declarations.
No tests in crates/vp_migration/src/import_rewriter.rs cover a pre-existing vite-plus import alongside a vite import.
packages/cli/src/oxlint-plugin.ts mirrors the migration's specifier rewrites but also does not merge duplicate declarations.
vp fmt/oxfmt formats the file but does not merge duplicate imports.
A GitHub search for "duplicate import" / "no-duplicates" did not find prior maintainers stating that this behavior is intentional.
Verdict
bug
The migration produces code that violates common lint rules (import/no-duplicates) which the recommended post-migration workflow (vp check) can run against. There is no documented or tested intent to leave such duplicates unmerged, and the merge is semantically safe (import type { Plugin } ≡ inline type Plugin).
Confidence
high
Fix
Changed files
crates/vp_migration/src/import_merge.rs (new)
Post-processes rewritten source to merge adjacent import declarations that now reference the same module specifier.
Uses the oxc parser already available in the crate.
Handles named imports (including inline type specifiers) and a single default import.
Calls import_merge::merge_duplicate_imports after the ast-grep rewrites.
Only runs the merge step when the ast-grep rules actually changed the file.
Updated two existing oxlint-plugin rewrite tests whose expected outputs now reflect the merged result.
Added regression test test_rewrite_import_content_vite_plus_duplicate_merge using the exact input from the issue.
crates/vp_migration/src/lib.rs
Declared the new import_merge module.
Rationale
The ast-grep rules are intentionally local: each rule matches one occurrence of a string literal and rewrites it. Adding import merging to ast-grep rules is not practical. A small oxc-based post-processing pass keeps the migration's actual rewrites unchanged while collapsing the duplicate import statements those rewrites sometimes produce.
The full CLI vp migrate reproduction could not be completed because the workspace node_modules are missing oxfmt/oxlint in this environment, causing the migration's format/editor step to fail before/after the fix. The bug and fix are verified at the Rust unit-test level and through the NAPI binding smoke test.
pnpm -F vite-plus build could not be run because pnpm-lock.yaml is out of sync with package.json in this environment; the binding was rebuilt directly via cargo build --features rolldown,oxc-node in packages/cli/binding.
This report was made by an LLM. The analysis may be wrong, and the potential fix might not work, but is intended as a starting point for exploring the issue.
Activity
voidzero-bot commentedon Nov 12, 2026
crates/vp_migration/src/import_merge.rsandcrates/vp_migration/src/import_rewriter.rsvite-plusalongside aviteimport that migration rewrites. It leaves animport/no-duplicateslint failure, but users can manually merge the imports or rely on a linter autofix as a short-term workaround.The migration's import rewriter applies local ast-grep rules that replace one import specifier at a time. After rewriting
import type { Plugin } from 'vite'tovite-plus, no later pass detects that the file now has two imports fromvite-plus, so the declarations stay separate.I found a potential fix for this issue. The branch adds an oxc-based post-processing pass in
crates/vp_migration/src/import_rewriter.rsthat collapses adjacent duplicate imports after the ast-grep rewrites run, merging value and type-only named imports and emitting inlinetypespecifiers when needed. It skips namespace imports, side-effect imports, imports with attributes, and conflicting bindings. The regression test uses the exact reproducer from the issue.Full Triage Report
Triage Report for Issue #3430
Reproduce
Reproduced: yes
After
vp migrate --no-interactive,vite.config.tscontains twovite-plusimports instead of one merged import.Actual result:
Expected result:
Diagnose
Code path
The migration's import rewriting entry point is in
crates/vp_migration/src/import_rewriter.rs:rewrite_imports_in_directory_with_options(line 2284) walks all TS/JS files.rewrite_import(line 2399).rewrite_importcallsrewrite_import_content_full(line 2480) which applies pre-loaded ast-grep rules viaast_grep::apply_loaded_rules.viteimports, therewrite-vite-importrule (defined around line 21 ofimport_rewriter.rs) matches the string literal inside anyimport_statementwhose value is exactly'vite'or"vite"and replaces the specifier withvite-plus.Why duplicates are produced
ast-grep rules are local transformations: they only see and rewrite one match at a time.
They have no notion of other import statements in the same file.
Given:
rewrite-vite-importrewrites the first string to"vite-plus", producing:There is no later pass that detects two imports from the same module and merges their named specifiers.
vp fmt/oxfmtformats the code but does not merge duplicate import declarations, so theimport/no-duplicateslint error remains.Suggested fix
Add a post-processing step after the ast-grep rules have run (inside
rewrite_import_content_fullor just before returning fromrewrite_import) that:typespecifiers (e.g.,import { defineConfig, type Plugin } from "vite-plus").Edge cases
import type+ value imports from the same module should collapse into one value import with inlinetypespecifiers.import * as ...), and side-effect imports should not be merged with named imports.vitest→vite-plus/testrewrites if bothvitestandvite-plus/testimports exist in one file.tsdown→vite-plus/packrewrites as well.Confidence
high
Verify
Claim
Observed:
vp migraterewritesimport type { Plugin } from 'vite'toimport type { Plugin } from "vite-plus"in a file that already containsimport { defineConfig } from "vite-plus", leaving two imports from the samemodule.
Expected: the migrated file has a single merged import such as
import { defineConfig, type Plugin } from "vite-plus".Research
docs/guide/migrate.md) says migration "Rewrites imports where needed" but does not state that duplicate imports are intentionally left unmerged or that they will be merged.crates/vp_migration/src/import_rewriter.rsuses local ast-grep rules that replace a single specifier string per match. There is no post-processing pass that merges import declarations.crates/vp_migration/src/import_rewriter.rscover a pre-existingvite-plusimport alongside aviteimport.packages/cli/src/oxlint-plugin.tsmirrors the migration's specifier rewrites but also does not merge duplicate declarations.vp fmt/oxfmtformats the file but does not merge duplicate imports.Verdict
bug
The migration produces code that violates common lint rules (
import/no-duplicates) which the recommended post-migration workflow (vp check) can run against. There is no documented or tested intent to leave such duplicates unmerged, and the merge is semantically safe (import type { Plugin }≡ inlinetype Plugin).Confidence
high
Fix
Changed files
crates/vp_migration/src/import_merge.rs(new)typespecifiers) and a single default import.crates/vp_migration/src/import_rewriter.rsimport_merge::merge_duplicate_importsafter the ast-grep rewrites.test_rewrite_import_content_vite_plus_duplicate_mergeusing the exact input from the issue.crates/vp_migration/src/lib.rsimport_mergemodule.Rationale
The ast-grep rules are intentionally local: each rule matches one occurrence of a string literal and rewrites it. Adding import merging to ast-grep rules is not practical. A small oxc-based post-processing pass keeps the migration's actual rewrites unchanged while collapsing the duplicate import statements those rewrites sometimes produce.
Validation
cargo test -p vp_migration— all 417 tests pass.cargo fmt -p vp_migration -- --check— clean.cargo clippy -p vp_migration --all-targets— clean.rewriteImportsInDirectoryon the reproduction file produced:Regression tests
New tests added directly exercise the merge pass:
import_merge::tests::merges_value_and_type_only_vite_plus_importsimport_merge::tests::merges_two_value_imports_from_same_sourceimport_merge::tests::merges_two_type_only_importsimport_merge::tests::does_not_merge_namespace_importsimport_merge::tests::does_not_merge_default_with_type_only_declimport_merge::tests::does_not_merge_different_sourcesimport_merge::tests::does_not_merge_conflicting_local_bindingsEnd-to-end regression test in
import_rewriter::tests:test_rewrite_import_content_vite_plus_duplicate_mergeSkipped checks and notes
vp migratereproduction could not be completed because the workspacenode_modulesare missingoxfmt/oxlintin this environment, causing the migration's format/editor step to fail before/after the fix. The bug and fix are verified at the Rust unit-test level and through the NAPI binding smoke test.pnpm -F vite-plus buildcould not be run becausepnpm-lock.yamlis out of sync withpackage.jsonin this environment; the binding was rebuilt directly viacargo build --features rolldown,oxc-nodeinpackages/cli/binding.This report was made by an LLM. The analysis may be wrong, and the potential fix might not work, but is intended as a starting point for exploring the issue.
I've opened a pull request with this fix: #3439
fix(migrate): safely merge duplicate imports (voidzero-dev#3439)