From e5015b0a8061ac68339b29b640a8b3810a93e160 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sun, 5 Jul 2026 18:30:31 +0800 Subject: [PATCH] fix(vite): resolve @vue-tui/vite from CommonJS Vite configs (#243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(vite): resolve @vue-tui/vite from CommonJS Vite configs When a consumer project is CommonJS — package.json "type": "commonjs", or no "type" field at all (Node defaults to CommonJS) — Vite loads vite.config.ts as CommonJS and resolves its imports under the `require` condition. @vue-tui/vite's exports used an `import`-only conditions object with no fallback, so nothing matched under `require` and Vite threw: [plugin externalize-deps] Failed to resolve "@vue-tui/vite". This package is ESM only but it was tried to load by `require`. Switch the export to a bare string (plus "./package.json"), matching the other @vue-tui/* packages and @vitejs/plugin-vue. A string export is condition-agnostic, so it resolves under both `import` and `require`; Node then loads the ESM file via require(ESM), which every supported Node (>=22.18) provides. No CommonJS build is added — this mirrors how the Vite plugin ecosystem ships today (all ESM-only). Add a regression test (test/cjs-config.test.ts): a `type: commonjs` fixture whose vite.config.ts imports @vue-tui/vite by name, loaded through the real config loader via resolveConfig(). It fails with the exact #238 error on the old exports and passes with this change. * chore(lint): exclude test fixtures from linting Test fixtures are test INPUTS, not shipped source, and some are deliberately broken: the overlay / full-reload dev-server tests transiently overwrite a fixture's app.vue with a syntax error (to exercise the error overlay / failed HMR), then restore it. Because `vp run ci` runs lint concurrently with those tests, a linter that read a fixture inside that broken window failed with a spurious "Unexpected token" — a flaky race whose surfacing depended on test timing. Add lint.ignorePatterns ["**/test/fixtures/**"] to remove the race (and the wasted lint work on test data). --- packages/vite/package.json | 6 +-- packages/vite/test/cjs-config.test.ts | 44 +++++++++++++++++++ .../test/fixtures/cjs-config/package.json | 4 ++ .../test/fixtures/cjs-config/vite.config.ts | 11 +++++ vite.config.ts | 7 +++ 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 packages/vite/test/cjs-config.test.ts create mode 100644 packages/vite/test/fixtures/cjs-config/package.json create mode 100644 packages/vite/test/fixtures/cjs-config/vite.config.ts diff --git a/packages/vite/package.json b/packages/vite/package.json index 6e7e66b..fb142a5 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -10,10 +10,8 @@ ], "type": "module", "exports": { - ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" - } + ".": "./dist/index.mjs", + "./package.json": "./package.json" }, "publishConfig": { "access": "public" diff --git a/packages/vite/test/cjs-config.test.ts b/packages/vite/test/cjs-config.test.ts new file mode 100644 index 0000000..dc45fb4 --- /dev/null +++ b/packages/vite/test/cjs-config.test.ts @@ -0,0 +1,44 @@ +// Regression test for #238: @vue-tui/vite must be loadable from a CommonJS consumer project. +// +// When a consumer's nearest package.json is `type: commonjs` (or has no `type` field — Node +// defaults to CommonJS), Vite bundles+loads vite.config.ts as CommonJS and resolves its imports +// under the `require` condition. @vue-tui/vite once exported only an `import` condition with no +// fallback, so nothing matched under `require` and config loading threw: +// +// [plugin externalize-deps] Failed to resolve "@vue-tui/vite". +// This package is ESM only but it was tried to load by `require`. +// +// The fixture (test/fixtures/cjs-config) has its own `type: commonjs` package.json to force the +// CJS config path, and imports @vue-tui/vite BY NAME. We symlink the real package into the +// fixture's node_modules first, so the bare import resolves against the built package and this +// exercises its published exports map under `require` — exactly a real consumer's layout. +// resolveConfig() runs the real config loader (the code path that threw) without a server/build. +import { test, expect, beforeAll, afterAll } from "vite-plus/test"; +import { fileURLToPath } from "node:url"; +import { mkdirSync, rmSync, symlinkSync } from "node:fs"; +import { resolveConfig } from "vite"; + +const root = fileURLToPath(new URL("./fixtures/cjs-config", import.meta.url)); +const pkgRoot = fileURLToPath(new URL("../", import.meta.url)); // packages/vite +const fixtureModules = `${root}/node_modules`; + +beforeAll(() => { + rmSync(fixtureModules, { recursive: true, force: true }); + mkdirSync(`${fixtureModules}/@vue-tui`, { recursive: true }); + // "junction" keeps this cross-platform (absolute dir link on Windows, plain symlink on POSIX). + symlinkSync(pkgRoot, `${fixtureModules}/@vue-tui/vite`, "junction"); +}); + +afterAll(() => { + rmSync(fixtureModules, { recursive: true, force: true }); +}); + +test("loads a config that imports @vue-tui/vite from a CommonJS project (#238)", async () => { + const config = await resolveConfig( + { root, configFile: `${root}/vite.config.ts`, logLevel: "silent" }, + "serve", + ); + + // The config resolved AND executed vueTui() under the CJS `require` path: its plugins are here. + expect(config.plugins.some((p) => p.name?.startsWith("vue-tui"))).toBe(true); +}); diff --git a/packages/vite/test/fixtures/cjs-config/package.json b/packages/vite/test/fixtures/cjs-config/package.json new file mode 100644 index 0000000..0fef86e --- /dev/null +++ b/packages/vite/test/fixtures/cjs-config/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "commonjs" +} diff --git a/packages/vite/test/fixtures/cjs-config/vite.config.ts b/packages/vite/test/fixtures/cjs-config/vite.config.ts new file mode 100644 index 0000000..bc8ed14 --- /dev/null +++ b/packages/vite/test/fixtures/cjs-config/vite.config.ts @@ -0,0 +1,11 @@ +// Fixture for the #238 regression test (test/cjs-config.test.ts). +// +// The sibling package.json declares `type: commonjs`, so Vite bundles+loads THIS config as +// CommonJS and resolves its imports under the `require` condition. Importing @vue-tui/vite BY +// NAME exercises the published package's exports map: if it isn't resolvable under `require`, +// config loading throws "This package is ESM only but it was tried to load by `require`". +import { vueTui } from "@vue-tui/vite"; + +export default { + plugins: [vueTui()], +}; diff --git a/vite.config.ts b/vite.config.ts index 4961baf..f4035ec 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,13 @@ export default defineConfig({ }, fmt: {}, lint: { + // Don't lint test fixtures. They are test INPUTS, not shipped source, and some are + // deliberately broken: the overlay / full-reload dev-server tests transiently overwrite a + // fixture's app.vue with a syntax error (to exercise the error overlay / failed HMR), then + // restore it. Because `vp run ci` runs lint CONCURRENTLY with those tests, a linter that read + // a fixture inside that broken window failed with a spurious "Unexpected token" (a flaky race, + // surfaced by test timing). Excluding fixtures removes the race and the wasted lint work. + ignorePatterns: ["**/test/fixtures/**"], options: { typeAware: true, typeCheck: false }, rules: { // This is a terminal UI library: parsing keyboard escape sequences and