Skip to content

vp create doesn't rewrite vite imports when adding a third-party template to a monorepo #3281

Description

@DaniilKubarenko

Describe the bug

When vp create adds a third-party template to an existing Vite+ monorepo, the new package's
vite.config.ts keeps import { defineConfig } from "vite" while lazyPlugins is imported from
vite-plus, so vp check fails on vite-plus/prefer-vite-plus-imports. vp migrate rewrites this
import; vp create doesn't.

Expected: defineConfig is imported from vite-plus, as after vp migrate.
Actual (apps/web/vite.config.ts):

import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
import { lazyPlugins } from "vite-plus";

Not framework-specific: adding a React app the same way
(vp create vite --no-interactive -- react-app --template react-ts) gives
import { defineConfig } from "vite" + import { lazyPlugins } from "vite-plus" too.
Outside a monorepo, the same vp create svelte@1.1.1 … command is correct
(import { defineConfig, lazyPlugins } from "vite-plus"), which points at the monorepo path.

Cause: rewriteMonorepoProject (packages/cli/src/migration/migrator/orchestrators.ts) never calls
rewriteAllImports. rewriteStandaloneProject does, and rewriteMonorepo runs it once for the whole
workspace (it calls rewriteMonorepoProject with deferLazyPluginWrapping = true). vp create adding a
package to a monorepo only calls rewriteMonorepoProject (create/bin.ts), which wraps lazy plugins
without having rewritten imports.

Suggested fix: in rewriteMonorepoProject, when deferLazyPluginWrapping is false, call
rewriteAllImports(projectPath, …) before wrapLazyPluginsInViteConfig, as rewriteStandaloneProject
does. Happy to send a PR if that helps.

Reproduction

https://github.com/DaniilKubarenko/vp-create-monorepo-vite-imports

Steps to reproduce

vp create vite:monorepo --no-interactive --directory repro --no-hooks --package-manager pnpm
cd repro
vp create svelte@1.1.1 --no-interactive -- web --template minimal --types ts --no-add-ons
cd apps/web
vp check

The linked repo is the unmodified output of the first three commands; `apps/website` and
`packages/utils` are the monorepo template's samples and unrelated.

### System Info

```shell
Node.js:
  Version    26.13.0
  Source     engines.node
  Bin Path   C:\Users\<user>\AppData\Local\vite-plus\data\js_runtime\node\26.13.0\node.exe
  Installed  true
  Mode       managed

Package Manager:
  Name       pnpm
  Version    12.16.1
  Source     devEngines.packageManager
  Installed  true
  Mode       managed

vp v1.4.0

Local vite-plus:
  vite-plus  v1.4.0

Tools:
  vite             v8.3.5
  rolldown         v1.2.15
  vitest           v5.0.5
  oxfmt            v0.74.0
  oxlint           v1.89.0
  oxlint-tsgolint  v7.0.2004
  tsdown           v0.23.1

Used Package Manager

pnpm

Logs

x vite-plus(prefer-vite-plus-imports): Use 'vite-plus' instead of 'vite' in Vite+ projects.
   ,-[vite.config.ts:3:30]
 2 | import { sveltekit } from "@sveltejs/kit/vite";
 3 | import { defineConfig } from "vite";
   :                              ^^^^^^

Validations

Activity

  1. voidzero-bot commented on Oct 29, 2026

    @voidzero-bot
    • Reproduced: Yes
    • Exploration: Yes — View branch
    • Unit Test: Yes — packages/cli/src/migration/__tests__/migrator.spec.ts
    • Priority: P2: Has Workaround. This only affects adding a third-party template to an existing Vite+ monorepo; the resulting vite.config.ts can be fixed manually or by running vp migrate on the package, so it doesn't block core installs or builds.

    The root cause is in packages/cli/src/migration/migrator/orchestrators.ts. vp create calls rewriteMonorepoProject(...) when integrating a new package into a monorepo, but that function only wraps plugins and never runs rewriteAllImports. Standalone vp create and vp migrate both rewrite imports before wrapping, so this path is the only one missing the step.

    I found a potential fix for this issue. The branch adds an oxlintOwnerDirs collection before rewritePackageJson and calls rewriteAllImports(projectPath, ...) before wrapLazyPluginsInViteConfig when deferLazyPluginWrapping is false. That mirrors the standalone and full-monorepo orchestrators, and it includes a regression test that fails before the fix and passes after.

    Full Triage Report

    Triage Report: #3281

    Issue Summary

    When vp create adds a third-party template package to an existing Vite+ monorepo, the new
    package's vite.config.ts keeps import { defineConfig } from "vite" while also importing
    lazyPlugins from "vite-plus". This causes vp check to fail on the
    vite-plus/prefer-vite-plus-imports rule. The same template outside of a monorepo rewrites the
    import to "vite-plus" correctly.

    Environment

    Reproduction Path

    1. Clone the reporter's reproduction repository:

      cd /tmp/vite-plus-triage/gh-3281
      git clone --depth 1 https://github.com/DaniilKubarenko/vp-create-monorepo-vite-imports repro-npm
      cd repro-npm
    2. Install dependencies from npm (the lockfile pins vite-plus@1.4.0, matching the current
      checkout's package version):

      pnpm install
    3. Run vp check in the newly added Svelte app:

      cd apps/web
      vp check

    Observed Result

    apps/web/vite.config.ts contains:

    import adapter from "@sveltejs/adapter-auto";
    import { sveltekit } from "@sveltejs/kit/vite";
    import { defineConfig } from "vite";
    import { lazyPlugins } from "vite-plus";

    vp check exits with code 1 and reports the exact rule violation described in the issue:

    error: Lint or type issues found
    ! unicorn(no-empty-file): Empty files are not allowed.
       ,-[src/lib/index.ts:1:1]
     1 | // place files you want to import through the `#lib` alias in this folder.
       `----
    
      x vite-plus(prefer-vite-plus-imports): Use 'vite-plus' instead of 'vite' in Vite+ projects.
       ,-[vite.config.ts:3:30]
     2 | import { sveltekit } from "@sveltejs/kit/vite";
     3 | import { defineConfig } from "vite";
       :                              ^^^^^^
     4 | import { lazyPlugins } from "vite-plus";
       `----
    
    Found 1 error and 1 warning in 5 files
    

    Control: packages/utils/vite.config.ts in the same monorepo (a Vite+ template package, not a
    third-party template) imports { defineConfig } from "vite-plus". Running vp check in
    packages/utils passes with no prefer-vite-plus-imports error, confirming the failure is scoped to
    the newly added third-party project.

    Source-Code Observation

    packages/cli/src/migration/migrator/orchestrators.ts confirms the reporter's analysis:

    • rewriteStandaloneProject calls rewriteAllImports(...) before
      wrapLazyPluginsInViteConfig(...).
    • rewriteMonorepo calls rewriteAllImports(...) once for the whole workspace, then loops over
      packages calling rewriteMonorepoProject(..., deferLazyPluginWrapping = true) before wrapping
      plugins per-project.
    • rewriteMonorepoProject does not call rewriteAllImports(...) when
      deferLazyPluginWrapping is false. vp create calls this function with the default
      deferLazyPluginWrapping = false, so the third-party project's imports are wrapped but never
      rewritten.

    Diagnosis

    Affected files and lines

    • packages/cli/src/create/bin.ts:1261 and :1325
      • The "add to existing monorepo" branch calls rewriteMonorepoProject(...).
      • The standalone branch calls rewriteStandaloneProject(...), which does rewrite imports.
    • packages/cli/src/migration/migrator/orchestrators.ts:344-346
      • rewriteStandaloneProject runs rewriteAllImports and then wrapLazyPluginsInViteConfig.
    • packages/cli/src/migration/migrator/orchestrators.ts:505-515
      • rewriteMonorepo runs rewriteAllImports once for the whole workspace and then passes
        deferLazyPluginWrapping = true to each per-project rewriteMonorepoProject call.
    • packages/cli/src/migration/migrator/orchestrators.ts:529-649
      • rewriteMonorepoProject performs package.json/tsconfig merging and then runs
        wrapLazyPluginsInViteConfig at line 642, but it never calls rewriteAllImports.
    • packages/cli/src/create/templates/monorepo.ts:123 and :158
      • The built-in monorepo template also calls rewriteMonorepoProject without
        deferLazyPluginWrapping, so default apps/libraries are affected too (latent).

    Incorrect logic

    rewriteMonorepoProject is designed to work in two modes:

    1. Called from rewriteMonorepo with deferLazyPluginWrapping = true — the workspace-wide
      rewriteAllImports has already run, so the project only needs plugin wrapping.
    2. Called directly (e.g. from vp create or the monorepo template generator) with the default
      deferLazyPluginWrapping = false — there is no preceding rewriteAllImports call anywhere, so
      the project's Vite imports are left untouched.

    Because plugin wrapping (wrapLazyPluginsInViteConfig) only rewrites the plugin array and adds a
    lazyPlugins import from vite-plus, the original defineConfig import stays pointed at vite.
    The resulting file exactly matches the reproduction:

    import { defineConfig } from "vite";
    import { lazyPlugins } from "vite-plus";

    Why this causes the observed behavior

    The monorepo root enables vite-plus/prefer-vite-plus-imports (seen in the reproduction's root
    vite.config.ts). The newly added package is a workspace member, so the rule applies to it. Because
    rewriteMonorepoProject did not rewrite the vite import to vite-plus, vp check reports a
    violation on the generated vite.config.ts.

    Suggested fix approach

    In rewriteMonorepoProject, collect oxlintOwnerDirs and, when deferLazyPluginWrapping is
    false, call rewriteAllImports(projectPath, silent, report, true, oxlintOwnerDirs) before
    wrapLazyPluginsInViteConfig. This mirrors rewriteStandaloneProject and preserves the existing
    rewriteMonorepo behavior because deferLazyPluginWrapping stays true there.

    Edge cases

    • Default monorepo apps/libraries: The same fix will also rewrite imports for the initial app and
      library created by the vite:monorepo template. This is desirable — every Vite+ workspace member
      should use vite-plus imports.
    • Yarn/Bun monorepos: The fix is package-manager-agnostic because rewriteMonorepoProject is
      shared.
    • Double rewrite guard: Must keep deferLazyPluginWrapping = true path unchanged to avoid
      redundant or conflicting rewrites from the workspace-level call.

    Regression evidence

    packages/cli/src/migration/__tests__/migrator.spec.ts has a test
    'wraps direct monorepo project rewrites used by create-monorepo flows', but its fixture already
    imports defineConfig from vite-plus, so the missing rewriteAllImports call is not exercised.
    No existing test drives a defineConfig import from vite through rewriteMonorepoProject.

    Verification

    Claim

    The reporter observed that vp create, when adding a third-party template to an existing Vite+
    monorepo, leaves defineConfig imported from "vite" in the new package's vite.config.ts. The
    expected behavior is the same migration that vp migrate performs: rewriting the import to
    "vite-plus" so vp check passes.

    Evidence of intent

    • docs/guide/migrate.md states that vp migrate "Rewrites imports where needed" as a core
      migration step.

    • packages/cli/src/migration/migrator/orchestrators.ts:343 and :504 both contain the explicit
      comment:

      "rewrite imports in all TypeScript/JavaScript files before lazy plugin import merging"

      This comment precedes rewriteAllImports in both rewriteStandaloneProject and rewriteMonorepo,
      but the equivalent call is missing in rewriteMonorepoProject.

    • rewriteStandaloneProject (standalone vp create path) and rewriteMonorepo (vp migrate path)
      already perform the rewrite; only the direct rewriteMonorepoProject path used by vp create
      inside an existing monorepo skips it.

    • The default monorepo root config in the reproduction enables
      vite-plus/prefer-vite-plus-imports: "error", so leaving the vite import is a clear lint
      violation.

    • git blame shows the per-project wrapLazyPluginsInViteConfig call at line 515-516 was added in
      a later commit (688fe4aa, Charles Wang, 2026-09-25), while the import-rewrite step was never
      brought into rewriteMonorepoProject. This looks like an oversight, not an intentional omission.

    • gh search issues --repo voidzero-dev/vite-plus finds no prior issue or design decision stating
      that third-party monorepo apps should keep upstream vite imports.

    Verdict

    Bug. The behavior contradicts the established migration pattern, the explicit code comments, and
    the lint rules that vp create itself generates for Vite+ monorepos.

    Confidence

    high

    Fix

    Changed files

    • packages/cli/src/migration/migrator/orchestrators.ts
    • packages/cli/src/migration/__tests__/migrator.spec.ts

    Rationale

    rewriteMonorepoProject is used both as a per-project helper inside rewriteMonorepo
    (deferLazyPluginWrapping = true) and as a direct integration step when vp create adds a package
    to a monorepo (deferLazyPluginWrapping = false). In the latter case there is no preceding
    call to rewriteAllImports, so the new package's vite imports remain unchanged.

    the fix adds an oxlintOwnerDirs collection before rewritePackageJson (to preserve Oxlint plugin
    ownership signals) and, when deferLazyPluginWrapping is false, calls rewriteAllImports before
    wrapLazyPluginsInViteConfig. This mirrors the standalone and full-monorepo orchestrators.

    Diff

    --- a/packages/cli/src/migration/migrator/orchestrators.ts
    +++ b/packages/cli/src/migration/migrator/orchestrators.ts
    @@ -575,6 +575,14 @@ export function rewriteMonorepoProject(
           ? findYarnWorkspaceHoisting(workspaceContext?.rootDir ?? projectPath)
           : undefined;
     
    +  // Capture Oxlint plugin owner directories before `rewritePackageJson` strips
    +  // `oxlint` from the package manifest. The import rewriter needs these signals
    +  // to preserve legitimate plugin imports.
    +  const oxlintOwnerDirs = collectOxlintOwnerDirs(
    +    workspaceContext?.rootDir ?? projectPath,
    +    workspaceContext?.packages,
    +  );
    +
       let extractedStagedConfig: Record<string, string | string[]> | null = null;
    @@ -639,6 +647,8 @@ export function rewriteMonorepoProject(
       }
     
       if (!deferLazyPluginWrapping) {
    +    // rewrite imports in all TypeScript/JavaScript files before lazy plugin import merging
    +    rewriteAllImports(projectPath, silent, report, true, oxlintOwnerDirs);
         wrapLazyPluginsInViteConfig(projectPath, silent, report);
         migrateTaskCacheConfigInViteConfig(projectPath, silent, report);
       }

    Regression test

    A new unit test in migrator.spec.ts sets up a vite.config.ts with import { defineConfig } from 'vite' and an inline plugins: [react()] array, then calls rewriteMonorepoProject directly. It
    expects:

    • the import to be rewritten to 'vite-plus';
    • no remaining 'vite' module specifier imports;
    • the plugin array to be wrapped with lazyPlugins;
    • report.rewrittenImportFileCount to be 1 and report.wrappedPluginConfigCount to be 1.

    This test fails before the fix (the import stays 'vite') and passes after.

    Validation

    • pnpm -F vite-plus build-ts rebuilt the CLI package without errors.
    • Focused test: pnpm test:unit -- packages/cli/src/migration/__tests__/migrator.spec.ts -t "rewrites vite imports" passed.
    • Migration suite: pnpm test:unit -- packages/cli/src/migration passed — 28 files, 1670 tests.
    • vp check packages/cli/src/migration/migrator/orchestrators.ts packages/cli/src/migration/__tests__/migrator.spec.ts passed.
    • git status --short --untracked-files=all shows only the intended two source files modified.

    Alternatives considered

    • Calling rewriteAllImports unconditionally inside rewriteMonorepoProject regardless of
      deferLazyPluginWrapping. Rejected because rewriteMonorepo already rewrites the whole workspace
      before per-project wrapLazyPluginsInViteConfig; duplicating the rewrite per package would be
      inefficient and could re-process already-relocated files.

    Final Result

    • Reproduced: yes
    • Skipped: no
    • Confidence: high
    • Verdict: bug
    • Fixed: yes

    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: #3282

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Target date

    None yet

    Start date

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions