diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/index.html b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/index.html
new file mode 100644
index 0000000000..4d5cb11b47
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/package.json
new file mode 100644
index 0000000000..fcf5005964
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "command-dev-bundled-dev-test",
+ "private": true
+}
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots.toml
new file mode 100644
index 0000000000..449c743099
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots.toml
@@ -0,0 +1,14 @@
+# Regression test for PR #2384: the core build rewrites CLIENT_ENTRY and
+# ENV_ENTRY to dist/vite/client/, but it left BUNDLED_DEV_CLIENT_ENTRY at
+# dist/client/. With `experimental.bundledDev` enabled, Vite reads that
+# path with fs.readFileSync, so `vp dev` fails at startup with ENOENT.
+[[case]]
+name = "command_dev_bundled_dev"
+vp = "local"
+skip-platforms = ["windows"]
+steps = [
+ { argv = ["vp", "dev", "--host", "127.0.0.1", "--port", "0"], comment = "dev server with experimental.bundledDev enabled starts and serves the bundled dev client", continue-on-failure = true, interactions = [
+ { "expect-milestone" = "dev-server:ready" },
+ { "write-key" = "ctrl-c" },
+ ] },
+]
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev.md
new file mode 100644
index 0000000000..dc14b84a18
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/snapshots/command_dev_bundled_dev.md
@@ -0,0 +1,28 @@
+# command_dev_bundled_dev
+
+## `vp dev --host 127.0.0.1 --port 0`
+
+dev server with experimental.bundledDev enabled starts and serves the bundled dev client
+
+**Exit code:** 130
+
+**→ expect-milestone:** `dev-server:ready`
+
+```
+
+ VITE+
+
+ ➜ Local: http://127.0.0.1:/
+ ➜ press h + enter to show help
+```
+
+**← write-key:** `ctrl-c`
+
+```
+
+ VITE+
+
+ ➜ Local: http://127.0.0.1:/
+ ➜ press h + enter to show help
+
+```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/src/main.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/src/main.ts
new file mode 100644
index 0000000000..5561718e53
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/src/main.ts
@@ -0,0 +1 @@
+document.querySelector('#app')!.textContent = 'hello';
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/vite.config.ts
new file mode 100644
index 0000000000..afda1c4c1f
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_dev_bundled_dev/vite.config.ts
@@ -0,0 +1,20 @@
+export default {
+ clearScreen: false,
+ experimental: {
+ bundledDev: true,
+ },
+ plugins: [
+ {
+ name: 'dev-server-ready-milestone',
+ configureServer(server) {
+ server.httpServer?.once('listening', () => {
+ // Let Vite print its startup banner after server.listen() resolves.
+ setImmediate(() => {
+ const name = Buffer.from('dev-server:ready').toString('base64url');
+ process.stdout.write(`\x1b]2;pty-terminal-test:${'0'.repeat(32)}:${name}\x1b\\`);
+ });
+ });
+ },
+ },
+ ],
+};
diff --git a/packages/core/BUNDLING.md b/packages/core/BUNDLING.md
index 48148b37b2..02633169a4 100644
--- a/packages/core/BUNDLING.md
+++ b/packages/core/BUNDLING.md
@@ -55,7 +55,7 @@ This is the most complex step, using the upstream `vite-rolldown.config` with mo
1. **Filter externals** - Bundles `picomatch`, `tinyglobby`, `fdir`, `rolldown`, `yaml` instead of keeping them external
2. **Add RewriteImportsPlugin** - Rewrites vite/rolldown imports at build time
-3. **Rewrite static paths** - Fixes `VITE_PACKAGE_DIR`, `CLIENT_ENTRY`, `ENV_ENTRY` constants
+3. **Rewrite static paths** - Fixes `VITE_PACKAGE_DIR`, `CLIENT_ENTRY`, `BUNDLED_DEV_CLIENT_ENTRY`, `ENV_ENTRY` constants
4. **Copy additional files** - `misc/`, `.d.ts` files, `types/`, `client.d.ts`
**Input**: `vite/packages/vite/`
@@ -219,6 +219,7 @@ dist/
│ │ ├── module-runner.js
│ │ └── chunks/
│ ├── client/
+│ │ ├── bundledDevClient.mjs
│ │ ├── client.mjs
│ │ └── env.mjs
│ ├── misc/
diff --git a/packages/core/build.ts b/packages/core/build.ts
index 69e46bbdf1..dddd165f2d 100644
--- a/packages/core/build.ts
+++ b/packages/core/build.ts
@@ -180,6 +180,13 @@ async function buildVite() {
)`,
`export const CLIENT_ENTRY = path.join(VITE_PACKAGE_DIR, 'dist/vite/client/client.mjs')`,
);
+ magicString.replace(
+ `export const BUNDLED_DEV_CLIENT_ENTRY: string = resolve(
+ VITE_PACKAGE_DIR,
+ 'dist/client/bundledDevClient.mjs',
+)`,
+ `export const BUNDLED_DEV_CLIENT_ENTRY = path.join(VITE_PACKAGE_DIR, 'dist/vite/client/bundledDevClient.mjs')`,
+ );
magicString.replace(
`export const ENV_ENTRY: string = resolve(
VITE_PACKAGE_DIR,