feat(vite)!: don't bundle @vitejs/plugin-vue; compose it explicitly (0.2.0) (#234)

* feat(vite)!: don't bundle @vitejs/plugin-vue; compose it explicitly

BREAKING: vueTui() no longer includes @vitejs/plugin-vue. Add your own SFC/JSX
compiler alongside it — `[vueTui(), vue()]` for SFCs (JSX was already explicit:
`[vueTui(), vueJsx()]`). This makes SFC and JSX consistent, matches Vite
convention (your framework plugin is visible in the config), removes the "is
plugin-vue already bundled?" ambiguity, and keeps @vue-tui/vite focused on the
terminal dev server (HMR) + build wiring.

The dev plugin already force-client-compiles whichever plugin-vue/plugin-vue-jsx
is present (matched by name in configResolved), so user-provided plugins work
identically to the previously-bundled one.

- index.ts: drop the bundled vue() and the `vue` passthrough option.
- package.json: @vitejs/plugin-vue moves from dependencies to devDependencies +
  an OPTIONAL peerDependency (^6) — optional because JSX (plugin-vue-jsx) and
  h() users don't need it; the version is still validated when present.
  Bump 0.1.2 -> 0.2.0.
- Update all in-repo SFC configs (examples + fixtures) and the dev/build tests
  to `[vueTui(), vue()]`; update basic-template's README.

vp run ready green (vite suite 129 tests, runtime examples 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(vite): use the public [vueTui(), vue()] form in dev tests

The decouple migration left the tests on the spread `[...vueTui(), vue()]` while
configs/examples/docs use `[vueTui(), vue()]` (Vite flattens the nested plugin
array — verified it works in createServer too). Align the tests to the
documented consumer form for consistency. Vite suite green (12 files, 28 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(vite): compiler plugin first in order — [vue(), vueTui()]

Convention: list the SFC/JSX compiler first and vueTui() last
([vue(), vueTui()] / [vueJsx(), vueTui()]). Order is functionally irrelevant
(devPlugin force-compiles whichever plugin-vue/vue-jsx is present by name in
configResolved), so this is purely stylistic consistency across configs,
examples, fixtures, tests, and docs.

Also refresh @vue-tui/vite's own README, which still showed the pre-decouple
[vueTui()] form and documented the now-removed `vue` passthrough option.

Vite suite green (28 tests, incl. the JSX path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-06-28 22:41:28 +08:00
committed by GitHub
parent 9f2529ba31
commit 14d5e5f9ad
22 changed files with 83 additions and 50 deletions
+1 -1
View File
@@ -6,5 +6,5 @@ import { vueTui } from "@vue-tui/vite";
// is added alongside vueTui(); the `entry` option points both the dev launcher and
// the production build at src/main.tsx instead of the default src/main.ts.
export default defineConfig({
plugins: [vueTui({ entry: "/src/main.tsx" }), vueJsx()],
plugins: [vueJsx(), vueTui({ entry: "/src/main.tsx" })],
});
+2 -1
View File
@@ -8,10 +8,11 @@ canonical reference for wiring up the `@vue-tui/vite` plugin.
```ts
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "@vue-tui/vite";
export default defineConfig({
plugins: [vueTui()],
plugins: [vue(), vueTui()],
});
```
+2 -1
View File
@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "@vue-tui/vite";
export default defineConfig({
plugins: [vueTui()],
plugins: [vue(), vueTui()],
});
+2 -1
View File
@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "@vue-tui/vite";
export default defineConfig({
plugins: [vueTui()],
plugins: [vue(), vueTui()],
});
+10 -9
View File
@@ -6,19 +6,23 @@ with HMR, plus a production build, for Vue apps that render to the terminal via
## Install
```sh
npm install -D @vue-tui/vite
npm install -D @vue-tui/vite @vitejs/plugin-vue
# peer deps: vite ^8, @vue-tui/runtime
```
## Usage
`vueTui()` adds the terminal dev server (HMR) and the production build. Bring your own SFC/JSX
compiler alongside it — `@vitejs/plugin-vue` for SFCs (or `@vitejs/plugin-vue-jsx` for JSX):
```ts
// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "@vue-tui/vite";
export default defineConfig({
plugins: [vueTui()],
plugins: [vue(), vueTui()],
});
```
@@ -31,20 +35,16 @@ export default defineConfig({
```ts
vueTui({
entry: "src/main.ts", // default; the app entry (a .ts/.tsx file, not an index.html)
vue: {
// options forwarded to @vitejs/plugin-vue
/* … */
},
});
```
For a JSX/TSX entry, add `@vitejs/plugin-vue-jsx` alongside it and point `entry` at the `.tsx` file:
For a JSX/TSX entry, use `@vitejs/plugin-vue-jsx` and point `entry` at the `.tsx` file:
```ts
import vueJsx from "@vitejs/plugin-vue-jsx";
export default defineConfig({
plugins: [vueTui({ entry: "/src/main.tsx" }), vueJsx()],
plugins: [vueJsx(), vueTui({ entry: "/src/main.tsx" })],
});
```
@@ -59,10 +59,11 @@ bundled but Node builtins — e.g. toward a standalone binary), set your own bui
`vite.config.ts` and the plugin yields to them:
```ts
import vue from "@vitejs/plugin-vue";
import { isBuiltin } from "node:module";
export default defineConfig({
plugins: [vueTui()],
plugins: [vue(), vueTui()],
build: {
// Vite 8 is Rolldown-powered: the field is `rolldownOptions` (`rollupOptions` is the alias).
rolldownOptions: {
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@vue-tui/vite",
"version": "0.1.2",
"version": "0.2.0",
"description": "Vite plugin for vue-tui: in-process terminal dev server with HMR + production build.",
"license": "MIT",
"files": [
@@ -25,11 +25,9 @@
"check:type": "tsc --noEmit",
"prepublishOnly": "vp run build"
},
"dependencies": {
"@vitejs/plugin-vue": "^6"
},
"devDependencies": {
"@types/node": "catalog:",
"@vitejs/plugin-vue": "^6",
"@vitejs/plugin-vue-jsx": "catalog:",
"@vue-tui/runtime": "workspace:*",
"typescript": "^6.0.3",
@@ -37,10 +35,14 @@
"vue": "^3.4.0"
},
"peerDependencies": {
"@vitejs/plugin-vue": "^6",
"@vue-tui/runtime": "workspace:^",
"vite": "^8"
},
"peerDependenciesMeta": {
"@vitejs/plugin-vue": {
"optional": true
},
"vite": {
"optional": true
}
+7 -11
View File
@@ -1,27 +1,23 @@
import vue from "@vitejs/plugin-vue";
import type { Plugin } from "vite";
import { devVmodPlugin } from "./dev-vmod.ts";
import { devPlugin } from "./dev.ts";
import { buildConfigPlugin } from "./build.ts";
export interface VueTuiOptions {
vue?: Parameters<typeof vue>[0];
entry?: string;
}
export function vueTui(options: VueTuiOptions = {}): Plugin[] {
// devPlugin (apply:"serve") and buildConfigPlugin (apply:"build") never run together — Vite
// picks the right set per mode. normalizeEntry() (below) derives the entry string each needs.
// plugin-vue here (and any plugin-vue-jsx the user adds) is force-client-compiled in
// devPlugin's configResolved, so it runs in the SSR dev environment but emits CLIENT
// render functions for the terminal renderer.
//
// Bring your own SFC/JSX compiler alongside vueTui() — `[vue(), vueTui()]` for SFCs, or
// `[vueJsx(), vueTui()]` for JSX. devPlugin's configResolved finds whichever is present (by
// plugin name) and force-client-compiles it, so it emits CLIENT render functions for the
// terminal renderer even in Vite's SSR dev environment. vueTui deliberately does NOT bundle
// @vitejs/plugin-vue: the app's authoring format is the consumer's choice, kept explicit.
const { dev, build } = normalizeEntry(options.entry);
return [
devPlugin({ entry: dev }),
buildConfigPlugin({ entry: build }),
devVmodPlugin(),
vue(options.vue) as Plugin,
];
return [devPlugin({ entry: dev }), buildConfigPlugin({ entry: build }), devVmodPlugin()];
}
// Reconcile the entry for dev (matched against the absolute module id via endsWith) and build
+8 -2
View File
@@ -17,6 +17,7 @@ import { fileURLToPath } from "node:url";
import { existsSync, rmSync } from "node:fs";
import { isBuiltin } from "node:module";
import { build, type Rollup } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../src/index.ts";
const root = fileURLToPath(new URL("./fixtures/build", import.meta.url));
@@ -28,7 +29,12 @@ afterEach(() => {
test("vite build emits a single self-contained Node entry with deps externalized", async () => {
rmSync(dist, { recursive: true, force: true });
const output = await build({ root, configFile: false, plugins: vueTui(), logLevel: "silent" });
const output = await build({
root,
configFile: false,
plugins: [vue(), vueTui()],
logLevel: "silent",
});
// A single, named input resolves to one RollupOutput (not a watcher).
const result = (Array.isArray(output) ? output[0] : output) as Rollup.RollupOutput;
@@ -57,7 +63,7 @@ test("a consumer's own rolldownOptions.external overrides the plugin default (se
const output = await build({
root,
configFile: false,
plugins: vueTui(),
plugins: [vue(), vueTui()],
logLevel: "silent",
build: {
rolldownOptions: {
@@ -4,6 +4,7 @@
import { test, expect, afterEach } from "vite-plus/test";
import { fileURLToPath } from "node:url";
import { createServer, type ViteDevServer } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../src/index.ts";
import { capture, waitFor } from "./helpers.ts";
@@ -24,7 +25,12 @@ afterEach(async () => {
// server._shortcutsState (the readline). Assert vueTui leaves no _shortcutsState.
test("vueTui disables Vite's CLI keyboard shortcuts so they can't hijack the TUI's stdin", async () => {
const read = capture();
server = await createServer({ root, logLevel: "silent", configFile: false, plugins: vueTui() });
server = await createServer({
root,
logLevel: "silent",
configFile: false,
plugins: [vue(), vueTui()],
});
await server.listen(); // a real httpServer is required for bindCLIShortcuts to act
await waitFor(read, "count="); // let the app mount with the normal (non-TTY) stdin first
+7 -1
View File
@@ -18,6 +18,7 @@
import { test, expect, afterEach } from "vite-plus/test";
import { fileURLToPath } from "node:url";
import { createServer, type ViteDevServer } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../src/index.ts";
import { capture, waitFor } from "./helpers.ts";
@@ -37,7 +38,12 @@ afterEach(async () => {
test("#214: dev-mode <Text color> emits real ANSI color", async () => {
const read = capture();
server = await createServer({ root, logLevel: "silent", configFile: false, plugins: vueTui() });
server = await createServer({
root,
logLevel: "silent",
configFile: false,
plugins: [vue(), vueTui()],
});
await server.listen();
await waitFor(read, "COLORTEST");
// Asserting the SGR (not just the text) is what distinguishes the fix from #214 — under the
+3 -2
View File
@@ -13,6 +13,7 @@ import { test, expect, afterEach } from "vite-plus/test";
import { fileURLToPath } from "node:url";
import { writeFileSync, readFileSync } from "node:fs";
import { createServer, type ViteDevServer } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../src/index.ts";
import { capture, waitFor } from "./helpers.ts";
@@ -34,7 +35,7 @@ test("boots the app in-process and renders a frame", async () => {
root,
logLevel: "silent",
configFile: false,
plugins: vueTui(),
plugins: [vue(), vueTui()],
});
await server.listen();
await waitFor(read, "LABEL-A");
@@ -47,7 +48,7 @@ test("template-only edit hot-swaps with state preserved (no reload)", async () =
root,
logLevel: "silent",
configFile: false,
plugins: vueTui(),
plugins: [vue(), vueTui()],
});
await server.listen();
await waitFor(read, "count=3"); // let the counter tick
+2 -1
View File
@@ -1,3 +1,4 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../../../src/index.ts";
export default defineConfig({ plugins: [vueTui()] });
export default defineConfig({ plugins: [vue(), vueTui()] });
+2 -1
View File
@@ -1,3 +1,4 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../../../src/index.ts";
export default defineConfig({ plugins: [vueTui()] });
export default defineConfig({ plugins: [vue(), vueTui()] });
+2 -1
View File
@@ -1,3 +1,4 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../../../src/index.ts";
export default defineConfig({ plugins: [vueTui()] });
export default defineConfig({ plugins: [vue(), vueTui()] });
+2 -1
View File
@@ -1,3 +1,4 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../../../src/index.ts";
export default defineConfig({ plugins: [vueTui()] });
export default defineConfig({ plugins: [vue(), vueTui()] });
+1 -1
View File
@@ -1,4 +1,4 @@
import { defineConfig } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import { vueTui } from "../../../src/index.ts";
export default defineConfig({ plugins: [vueTui({ entry: "/src/main.tsx" }), vueJsx()] });
export default defineConfig({ plugins: [vueJsx(), vueTui({ entry: "/src/main.tsx" })] });
+2 -1
View File
@@ -1,3 +1,4 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../../../src/index.ts";
export default defineConfig({ plugins: [vueTui()] });
export default defineConfig({ plugins: [vue(), vueTui()] });
+2 -1
View File
@@ -1,3 +1,4 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../../../src/index.ts";
export default defineConfig({ plugins: [vueTui()] });
export default defineConfig({ plugins: [vue(), vueTui()] });
@@ -12,6 +12,7 @@ import { test, expect, afterEach } from "vite-plus/test";
import { fileURLToPath } from "node:url";
import { writeFileSync, readFileSync } from "node:fs";
import { createServer, type ViteDevServer } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../src/index.ts";
import { capture, waitUntil } from "./helpers.ts";
@@ -38,7 +39,12 @@ function counts(chunk: string): number[] {
test("an entry-level (non-HMR) change restarts the app in-process with no zombie", async () => {
const read = capture();
server = await createServer({ root, logLevel: "silent", configFile: false, plugins: vueTui() });
server = await createServer({
root,
logLevel: "silent",
configFile: false,
plugins: [vue(), vueTui()],
});
await server.listen();
// Let the original app boot and tick a few times.
@@ -100,7 +106,7 @@ test("survives a SECOND full reload when @vue-tui/runtime is EXTERNALIZED (the p
root,
logLevel: "silent",
configFile: false,
plugins: vueTui(),
plugins: [vue(), vueTui()],
ssr: { external: ["@vue-tui/runtime", "@vue-tui/runtime/internal"] },
server: { middlewareMode: true },
});
@@ -145,7 +151,7 @@ test("a genuine app exit closes the in-process dev server", async () => {
root: exitRoot,
logLevel: "silent",
configFile: false,
plugins: vueTui(),
plugins: [vue(), vueTui()],
});
// Spy on close so we can detect the runtime-driven teardown. dev.ts captured
// `server` and calls server.close() at exit time, so it sees this wrapper.
+1 -1
View File
@@ -27,7 +27,7 @@ test("JSX (.tsx) components render in the in-process dev server", async () => {
root,
logLevel: "silent",
configFile: false,
plugins: [vueTui({ entry: "/src/main.tsx" }), vueJsx()],
plugins: [vueJsx(), vueTui({ entry: "/src/main.tsx" })],
});
await server.listen();
await waitFor(read, "JSX-LABEL");
@@ -16,6 +16,7 @@ import { test, expect, afterEach } from "vite-plus/test";
import { fileURLToPath } from "node:url";
import { writeFileSync, readFileSync } from "node:fs";
import { createServer, type ViteDevServer } from "vite";
import vue from "@vitejs/plugin-vue";
import { vueTui } from "../src/index.ts";
import { capture, waitFor } from "./helpers.ts";
@@ -37,7 +38,7 @@ test("a build error renders the in-process dev overlay", async () => {
root,
logLevel: "silent",
configFile: false,
plugins: vueTui(),
plugins: [vue(), vueTui()],
});
await server.listen();
await waitFor(read, "LABEL-A");
+3 -4
View File
@@ -338,14 +338,13 @@ importers:
version: 3.5.34(typescript@6.0.3)
packages/vite:
dependencies:
'@vitejs/plugin-vue':
specifier: ^6
version: 6.0.7(vite@8.1.0(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3))(vue@3.5.34(typescript@6.0.3))
devDependencies:
'@types/node':
specifier: ^24.12.4
version: 24.12.4
'@vitejs/plugin-vue':
specifier: ^6
version: 6.0.7(vite@8.1.0(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3))(vue@3.5.34(typescript@6.0.3))
'@vitejs/plugin-vue-jsx':
specifier: 'catalog:'
version: 5.1.5(vite@8.1.0(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3))(vue@3.5.34(typescript@6.0.3))