From 558b16ca0b7f45e93f512a570757f2044a3ceec1 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 23 Jun 2026 13:57:51 +0800 Subject: [PATCH 1/8] fix(pack): bundle @tsdown/exe and @tsdown/css into core @tsdown/exe and @tsdown/css hard-peer-depend on tsdown and import tsdown/internal, but Vite+ bundles tsdown internally with no resolvable top-level tsdown package, so installing them failed with "Failed to import module @tsdown/exe". Bundle both into core (tsdown-exe.js, tsdown-css.js) so they resolve tsdown/internal at build time and work without any extra install. lightningcss (native) stays external as an optional peer, loaded lazily with an actionable error when it is missing. Closes #1586 --- docs/guide/pack.md | 16 ++ .../snap-tests/command-pack-css/package.json | 5 + .../cli/snap-tests/command-pack-css/snap.txt | 16 ++ .../snap-tests/command-pack-css/src/index.ts | 3 + .../snap-tests/command-pack-css/src/style.css | 7 + .../snap-tests/command-pack-css/steps.json | 7 + .../package.json | 5 + .../command-pack-tsdown-extensions/snap.txt | 3 + .../command-pack-tsdown-extensions/steps.json | 6 + .../verify-extensions.mjs | 22 +++ packages/core/build.ts | 151 ++++++++++++++++-- packages/core/package.json | 16 +- pnpm-lock.yaml | 24 +-- pnpm-workspace.yaml | 2 + rfcs/pack-command.md | 64 +++++++- 15 files changed, 311 insertions(+), 36 deletions(-) create mode 100644 packages/cli/snap-tests/command-pack-css/package.json create mode 100644 packages/cli/snap-tests/command-pack-css/snap.txt create mode 100644 packages/cli/snap-tests/command-pack-css/src/index.ts create mode 100644 packages/cli/snap-tests/command-pack-css/src/style.css create mode 100644 packages/cli/snap-tests/command-pack-css/steps.json create mode 100644 packages/cli/snap-tests/command-pack-tsdown-extensions/package.json create mode 100644 packages/cli/snap-tests/command-pack-tsdown-extensions/snap.txt create mode 100644 packages/cli/snap-tests/command-pack-tsdown-extensions/steps.json create mode 100644 packages/cli/snap-tests/command-pack-tsdown-extensions/verify-extensions.mjs diff --git a/docs/guide/pack.md b/docs/guide/pack.md index b1714c48df..18c70cf6ab 100644 --- a/docs/guide/pack.md +++ b/docs/guide/pack.md @@ -58,4 +58,20 @@ export default defineConfig({ }); ``` +Executable support is bundled into Vite+, so you do not need to install `@tsdown/exe` separately. + +Building executables uses Node's [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) support and requires Node.js 25.7.0 or later. Switch the active runtime with `vp env use 25` if `vp pack --exe` reports an unsupported version. + See the official [tsdown executable docs](https://tsdown.dev/options/exe#executable) for details about configuring custom file names, embedded assets, and cross-platform targets. + +## CSS Bundling + +`vp pack` can transform and bundle CSS (including CSS Modules and Lightning CSS optimizations) for your entry points. This support is bundled into Vite+, so you do not need to install `@tsdown/css`. + +CSS transforms are powered by [Lightning CSS](https://lightningcss.dev/), which ships as a native module and is an optional peer dependency. Install it when you bundle CSS: + +```bash +vp add -D lightningcss +``` + +If it is missing, `vp pack` prints an actionable error telling you to install it. diff --git a/packages/cli/snap-tests/command-pack-css/package.json b/packages/cli/snap-tests/command-pack-css/package.json new file mode 100644 index 0000000000..6b91fc3570 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-pack-css", + "version": "1.0.0", + "type": "module" +} diff --git a/packages/cli/snap-tests/command-pack-css/snap.txt b/packages/cli/snap-tests/command-pack-css/snap.txt new file mode 100644 index 0000000000..a4b43ee8d9 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css/snap.txt @@ -0,0 +1,16 @@ +> vp pack src/index.ts --minify # bundles CSS via the bundled @tsdown/css + lightningcss (issue #1586) +ℹ entry: src/index.ts +ℹ Build start +ℹ dist/index.mjs kB │ gzip: kB +ℹ dist/style.css kB │ gzip: kB +ℹ 2 files, total: kB +✔ Build complete in ms + +> cat dist/style.css # lightningcss-optimized output proves @tsdown/css ran +.foo { + color: red; +} + +.bar { + margin: 0; +} diff --git a/packages/cli/snap-tests/command-pack-css/src/index.ts b/packages/cli/snap-tests/command-pack-css/src/index.ts new file mode 100644 index 0000000000..add68f6159 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css/src/index.ts @@ -0,0 +1,3 @@ +import './style.css'; + +export const hello = 'world'; diff --git a/packages/cli/snap-tests/command-pack-css/src/style.css b/packages/cli/snap-tests/command-pack-css/src/style.css new file mode 100644 index 0000000000..d5aba93ecd --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css/src/style.css @@ -0,0 +1,7 @@ +.foo { + color: #ff0000; +} + +.bar { + margin: 0px 0px 0px 0px; +} diff --git a/packages/cli/snap-tests/command-pack-css/steps.json b/packages/cli/snap-tests/command-pack-css/steps.json new file mode 100644 index 0000000000..437afe16f8 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css/steps.json @@ -0,0 +1,7 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "vp pack src/index.ts --minify # bundles CSS via the bundled @tsdown/css + lightningcss (issue #1586)", + "cat dist/style.css # lightningcss-optimized output proves @tsdown/css ran" + ] +} diff --git a/packages/cli/snap-tests/command-pack-tsdown-extensions/package.json b/packages/cli/snap-tests/command-pack-tsdown-extensions/package.json new file mode 100644 index 0000000000..77cd0bb956 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-tsdown-extensions/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-pack-tsdown-extensions", + "version": "1.0.0", + "type": "module" +} diff --git a/packages/cli/snap-tests/command-pack-tsdown-extensions/snap.txt b/packages/cli/snap-tests/command-pack-tsdown-extensions/snap.txt new file mode 100644 index 0000000000..af5ce06923 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-tsdown-extensions/snap.txt @@ -0,0 +1,3 @@ +> node verify-extensions.mjs # bundled @tsdown/exe and @tsdown/css load without a top-level tsdown (issue #1586) +tsdown-exe.js: getCacheDir, getCachedBinaryPath, getTargetSuffix, resolveNodeBinary +tsdown-css.js: CssPlugin, resolveCssOptions diff --git a/packages/cli/snap-tests/command-pack-tsdown-extensions/steps.json b/packages/cli/snap-tests/command-pack-tsdown-extensions/steps.json new file mode 100644 index 0000000000..b00e80a479 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-tsdown-extensions/steps.json @@ -0,0 +1,6 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "node verify-extensions.mjs # bundled @tsdown/exe and @tsdown/css load without a top-level tsdown (issue #1586)" + ] +} diff --git a/packages/cli/snap-tests/command-pack-tsdown-extensions/verify-extensions.mjs b/packages/cli/snap-tests/command-pack-tsdown-extensions/verify-extensions.mjs new file mode 100644 index 0000000000..93dbe6cdb1 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-tsdown-extensions/verify-extensions.mjs @@ -0,0 +1,22 @@ +// Issue #1586: `@tsdown/exe` and `@tsdown/css` have a hard peer dependency on +// `tsdown` and import `tsdown/internal`, but Vite+ bundles tsdown internally and +// does not expose a resolvable top-level `tsdown` package. Before the fix, the +// bundled tsdown loaded them as external top-level packages, so `vp pack --exe` +// (and CSS bundling) failed with `Failed to import module "@tsdown/exe"`. +// +// They are now bundled into core, so this loads the bundled extension chunks +// directly to prove they resolve `tsdown/internal` against the bundled tsdown. +// `vp pack --exe` itself needs Node >= 25.7 (SEA), so it cannot run end-to-end +// in CI; this check is Node-version independent. +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const require = createRequire(import.meta.url); +const packEntry = require.resolve('@voidzero-dev/vite-plus-core/pack'); +const tsdownDir = path.dirname(packEntry); + +for (const chunk of ['tsdown-exe.js', 'tsdown-css.js']) { + const mod = await import(pathToFileURL(path.join(tsdownDir, chunk)).href); + console.log(`${chunk}: ${Object.keys(mod).sort().join(', ')}`); +} diff --git a/packages/core/build.ts b/packages/core/build.ts index c44383c9c9..cd012a4106 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -1,5 +1,6 @@ import { existsSync } from 'node:fs'; import { copyFile, cp, mkdir, readFile, stat, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; import path from 'node:path'; import { dirname, join, parse, resolve, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -54,6 +55,7 @@ await bundleRolldown(); await buildVite(); await bundleTsdown(); await brandTsdown(); +await wireBundledTsdownExtensions(); await bundleVitepress(); generateLicenseFile({ title: 'Vite-Plus core license', @@ -379,23 +381,47 @@ async function bundleRolldown() { async function bundleTsdown() { await mkdir(join(projectDir, 'dist/tsdown/dist'), { recursive: true }); - const tsdownExternal = Object.keys(pkgJson.peerDependencies); + const require = createRequire(import.meta.url); + + // `@tsdown/exe` and `@tsdown/css` are bundled directly into core instead of + // being externalized. They have a hard peer dependency on `tsdown` and import + // `tsdown/internal`, but Vite+ bundles tsdown inside core rather than exposing + // a resolvable top-level `tsdown` package. Bundling them here resolves + // `tsdown/internal` against the bundled tsdown at build time, so `vp pack + // --exe` and CSS bundling work without users installing anything extra. + // See https://github.com/voidzero-dev/vite-plus/issues/1586 + const bundledTsdownPackages = new Set(['@tsdown/exe', '@tsdown/css']); + + // Everything else in tsdown's peer dependencies stays external. `lightningcss` + // (a native module) and `postcss` also stay external: `@tsdown/css` pulls them + // in, and they cannot be bundled. `lightningcss` becomes an optional peer with + // a friendly loader injected by `wireBundledTsdownExtensions()`. + const tsdownExternal = [ + ...Object.keys(pkgJson.peerDependencies).filter((name) => !bundledTsdownPackages.has(name)), + 'lightningcss', + 'postcss', + ]; + const isExternal = (id: string) => tsdownExternal.some((e) => id === e || id.startsWith(`${e}/`)); const thirdPartyCjsModules = new Set(); - // Re-build tsdown cli + // Re-build tsdown cli plus the bundled `@tsdown/exe` and `@tsdown/css` + // extensions as stable named entries (`tsdown-exe.js`, `tsdown-css.js`). await build({ input: { run: join(tsdownSourceDir, 'dist/run.mjs'), index: join(tsdownSourceDir, 'dist/index.mjs'), + 'tsdown-exe': require.resolve('@tsdown/exe'), + 'tsdown-css': require.resolve('@tsdown/css'), }, output: { format: 'esm', cleanDir: true, dir: join(projectDir, 'dist/tsdown'), + entryFileNames: '[name].js', }, platform: 'node', - external: (id: string) => tsdownExternal.some((e) => id.startsWith(e)), + external: isExternal, plugins: [ RewriteImportsPlugin, { @@ -425,7 +451,7 @@ async function bundleTsdown() { format: 'esm', dir: join(projectDir, 'dist/tsdown'), }, - external: (id: string) => tsdownExternal.some((e) => id.startsWith(e)), + external: isExternal, plugins: [ RewriteImportsPlugin, dts({ @@ -448,13 +474,14 @@ async function bundleTsdown() { async function brandTsdown() { const tsdownDistDir = join(projectDir, 'dist/tsdown'); const buildFiles = await glob(toPosixPath(join(tsdownDistDir, 'build-*.js')), { absolute: true }); - const mainFiles = await glob(toPosixPath(join(tsdownDistDir, 'main-*.js')), { absolute: true }); + // The logger code lives in a shared chunk whose name depends on rolldown's + // chunking (e.g. `main-*.js` or `debug-*.js`), so scan every chunk for it. + const loggerCandidateFiles = await glob(toPosixPath(join(tsdownDistDir, '*.js')), { + absolute: true, + }); if (buildFiles.length === 0) { throw new Error('brandTsdown: no build chunk found in dist/tsdown/'); } - if (mainFiles.length === 0) { - throw new Error('brandTsdown: no main chunk found in dist/tsdown/'); - } const search = '"tsdown "'; const replacement = '"vp pack "'; @@ -534,8 +561,8 @@ async function brandTsdown() { ]; let loggerPatched = false; - for (const mainFile of mainFiles) { - let content = await readFile(mainFile, 'utf-8'); + for (const candidateFile of loggerCandidateFiles) { + let content = await readFile(candidateFile, 'utf-8'); let changed = false; for (const { search, replacement } of loggerPatches) { if (content.includes(search)) { @@ -546,13 +573,91 @@ async function brandTsdown() { if (!changed) { continue; } - await writeFile(mainFile, content, 'utf-8'); - console.log(`Branded tsdown logger prefixes in ${mainFile}`); + await writeFile(candidateFile, content, 'utf-8'); + console.log(`Branded tsdown logger prefixes in ${candidateFile}`); loggerPatched = true; } if (!loggerPatched) { - throw new Error('brandTsdown: logger prefix patterns not found in any main chunk'); + throw new Error('brandTsdown: logger prefix patterns not found in any chunk'); + } +} + +// Wire the bundled `@tsdown/exe` and `@tsdown/css` extensions into the bundled +// tsdown so they load from the local chunks instead of resolving external +// top-level packages, and make `lightningcss` an optional peer with a friendly +// loader. See https://github.com/voidzero-dev/vite-plus/issues/1586 +async function wireBundledTsdownExtensions() { + const tsdownDistDir = join(projectDir, 'dist/tsdown'); + const buildFiles = await glob(toPosixPath(join(tsdownDistDir, 'build-*.js')), { absolute: true }); + if (buildFiles.length === 0) { + throw new Error('wireBundledTsdownExtensions: no build chunk found in dist/tsdown/'); + } + + // 1) Route `@tsdown/exe` and `@tsdown/css` to the bundled entries. + // - `importWithError("@tsdown/exe")` dynamically imports a runtime string, + // which rolldown cannot follow, so rewrite the call site to the chunk. + // - `pkgExists("@tsdown/css")` resolves the top-level package at runtime; + // since it is bundled now, force it on and point the import at the chunk. + let exeWired = false; + let cssWired = false; + for (const buildFile of buildFiles) { + let content = await readFile(buildFile, 'utf-8'); + let changed = false; + if (content.includes('importWithError("@tsdown/exe")')) { + content = content.replaceAll('importWithError("@tsdown/exe")', 'import("./tsdown-exe.js")'); + exeWired = true; + changed = true; + } + if (content.includes('pkgExists("@tsdown/css")')) { + content = content.replaceAll('pkgExists("@tsdown/css")', 'true'); + cssWired = true; + changed = true; + } + if (content.includes('import("@tsdown/css")')) { + content = content.replaceAll('import("@tsdown/css")', 'import("./tsdown-css.js")'); + changed = true; + } + if (changed) { + await writeFile(buildFile, content); + } + } + if (!exeWired) { + throw new Error('wireBundledTsdownExtensions: `importWithError("@tsdown/exe")` not found'); + } + if (!cssWired) { + throw new Error('wireBundledTsdownExtensions: `pkgExists("@tsdown/css")` not found'); + } + + // 2) `lightningcss` is a native module and an optional peer. Wrap its dynamic + // import so a missing install produces an actionable error instead of a raw + // `Cannot find package 'lightningcss'`. + const helper = + 'async function __vpImportLightningcss() {\n' + + ' try {\n' + + ' return await import("lightningcss");\n' + + ' } catch (cause) {\n' + + ' throw new Error(' + + '"Cannot find package \\"lightningcss\\". CSS bundling with `vp pack` requires it. ' + + 'Install it with `vp add -D lightningcss`.", { cause });\n' + + ' }\n' + + '}\n'; + const allChunks = await glob(toPosixPath(join(tsdownDistDir, '*.js')), { absolute: true }); + let lightningcssWired = false; + for (const chunk of allChunks) { + let content = await readFile(chunk, 'utf-8'); + if (!content.includes('import("lightningcss")')) { + continue; + } + // Rewrite the call sites first, then prepend the helper (whose own + // `import("lightningcss")` must not be rewritten). + content = content.replaceAll('import("lightningcss")', '__vpImportLightningcss()'); + content = helper + content; + await writeFile(chunk, content); + lightningcssWired = true; + } + if (!lightningcssWired) { + throw new Error('wireBundledTsdownExtensions: `import("lightningcss")` not found'); } } @@ -650,6 +755,14 @@ async function mergePackageJson() { const vitePkg = JSON.parse(await readFile(vitePkgPath, 'utf-8')); const destPkg = JSON.parse(await readFile(destPkgPath, 'utf-8')); + // Track the bundled `@tsdown/css` lightningcss dependency so the optional peer + // range stays in lockstep with what the bundled CSS code expects. + const require = createRequire(import.meta.url); + const tsdownCssPkg = JSON.parse( + await readFile(require.resolve('@tsdown/css/package.json'), 'utf-8'), + ); + const lightningcssPeerRange: string = tsdownCssPkg.dependencies?.lightningcss ?? '^1.30.2'; + // Merge peerDependencies from tsdown and vite destPkg.peerDependencies = { ...tsdownPkg.peerDependencies, @@ -662,6 +775,18 @@ async function mergePackageJson() { ...vitePkg.peerDependenciesMeta, }; + // `@tsdown/exe` and `@tsdown/css` are bundled into core (see bundleTsdown), + // so they must not be advertised as peers anymore. `lightningcss` is a native + // module pulled in by the bundled `@tsdown/css` (and Vite's lightningcss + // transformer); keep it external as an optional peer so users only install it + // when they actually bundle CSS. + for (const bundled of ['@tsdown/exe', '@tsdown/css']) { + delete destPkg.peerDependencies[bundled]; + delete destPkg.peerDependenciesMeta[bundled]; + } + destPkg.peerDependencies['lightningcss'] = lightningcssPeerRange; + destPkg.peerDependenciesMeta['lightningcss'] = { optional: true }; + destPkg.bundledVersions = { ...destPkg.bundledVersions, vite: vitePkg.version, diff --git a/packages/core/package.json b/packages/core/package.json index 2920ac5fbd..eaa797872b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -102,7 +102,6 @@ "dependencies": { "@oxc-project/runtime": "catalog:", "@oxc-project/types": "catalog:", - "lightningcss": "^1.30.2", "postcss": "^8.5.6" }, "devDependencies": { @@ -112,9 +111,12 @@ "@babel/types": "^7.28.5", "@oxc-node/cli": "catalog:", "@oxc-node/core": "catalog:", + "@tsdown/css": "catalog:", + "@tsdown/exe": "catalog:", "@vitejs/devtools": "^0.3.3", "es-module-lexer": "^1.7.0", "hookable": "^6.0.1", + "lightningcss": "^1.32.0", "magic-string": "^0.30.21", "oxc-parser": "catalog:", "oxfmt": "catalog:", @@ -133,13 +135,12 @@ }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", - "@tsdown/css": "0.22.3", - "@tsdown/exe": "0.22.3", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", + "lightningcss": "^1.32.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", @@ -156,12 +157,6 @@ "@arethetypeswrong/core": { "optional": true }, - "@tsdown/css": { - "optional": true - }, - "@tsdown/exe": { - "optional": true - }, "@vitejs/devtools": { "optional": true }, @@ -209,6 +204,9 @@ }, "yaml": { "optional": true + }, + "lightningcss": { + "optional": true } }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9958a548b6..97246307a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,12 @@ catalogs: '@rollup/plugin-node-resolve': specifier: ^16.0.0 version: 16.0.3 + '@tsdown/css': + specifier: ^0.22.3 + version: 0.22.3 + '@tsdown/exe': + specifier: ^0.22.3 + version: 0.22.3 '@types/babel__core': specifier: 7.20.5 version: 7.20.5 @@ -507,12 +513,6 @@ importers: '@oxc-project/types': specifier: 'catalog:' version: 0.136.0 - '@tsdown/css': - specifier: 0.22.3 - version: 0.22.3(jiti@2.7.0)(postcss-import@16.1.1(postcss@8.5.15))(postcss-modules@6.0.1(postcss@8.5.15))(postcss@8.5.15)(sass-embedded@1.100.0(source-map-js@1.2.1))(sass@1.100.0)(tsdown@0.22.3)(tsx@4.22.4)(yaml@2.9.0) - '@tsdown/exe': - specifier: 0.22.3 - version: 0.22.3(tsdown@0.22.3) '@types/node': specifier: ^20.19.0 || >=22.12.0 version: 24.12.4 @@ -525,9 +525,6 @@ importers: less: specifier: ^4.0.0 version: 4.4.2 - lightningcss: - specifier: ^1.30.2 - version: 1.32.0 postcss: specifier: ^8.5.6 version: 8.5.15 @@ -583,6 +580,12 @@ importers: '@oxc-node/core': specifier: 'catalog:' version: 0.1.0 + '@tsdown/css': + specifier: 'catalog:' + version: 0.22.3(jiti@2.7.0)(postcss-import@16.1.1(postcss@8.5.15))(postcss-modules@6.0.1(postcss@8.5.15))(postcss@8.5.15)(sass-embedded@1.100.0(source-map-js@1.2.1))(sass@1.100.0)(tsdown@0.22.3)(tsx@4.22.4)(yaml@2.9.0) + '@tsdown/exe': + specifier: 'catalog:' + version: 0.22.3(tsdown@0.22.3) '@vitejs/devtools': specifier: ^0.3.3 version: 0.3.3(typescript@6.0.2)(vite@packages+core) @@ -592,6 +595,9 @@ importers: hookable: specifier: ^6.0.1 version: 6.1.1 + lightningcss: + specifier: ^1.32.0 + version: 1.32.0 magic-string: specifier: ^0.30.21 version: 0.30.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ac9c9cde2d..7fafd253ba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,6 +23,8 @@ catalog: '@rollup/plugin-commonjs': ^29.0.0 '@rollup/plugin-json': ^6.1.0 '@rollup/plugin-node-resolve': ^16.0.0 + '@tsdown/css': ^0.22.3 + '@tsdown/exe': ^0.22.3 '@types/babel__core': 7.20.5 '@types/connect': ^3.4.38 '@types/cross-spawn': ^6.0.6 diff --git a/rfcs/pack-command.md b/rfcs/pack-command.md index 2a938f1e24..4693aeac7c 100644 --- a/rfcs/pack-command.md +++ b/rfcs/pack-command.md @@ -201,6 +201,40 @@ tsdown is bundled inside `@voidzero-dev/vite-plus-core/pack`: - `packages/core/package.json` tracks `bundledVersions.tsdown` - Re-exported via `packages/cli/src/pack.ts` +The `@tsdown/exe` (executables) and `@tsdown/css` (CSS bundling) extensions are +bundled alongside tsdown as well, so `vp pack --exe` and CSS bundling work +without users installing anything extra. See [tsdown Extensions](#bundled-tsdown-extensions). + +### Bundled tsdown Extensions + +This covers the `@tsdown/exe` (executables) and `@tsdown/css` (CSS bundling) extensions. + +Standalone `@tsdown/exe` and `@tsdown/css` hard-peer-depend on `tsdown` and import +`tsdown/internal`. Because Vite+ bundles tsdown internally rather than exposing a +resolvable top-level `tsdown` package, installing them at the project level fails +to resolve `tsdown/internal` (`Failed to import module "@tsdown/exe"`). See +[issue #1586](https://github.com/voidzero-dev/vite-plus/issues/1586). + +To fix this, both extensions are bundled into core instead of left as peers: + +- `packages/core/build.ts` (`bundleTsdown`) builds them as stable named entries + `dist/tsdown/tsdown-exe.js` and `dist/tsdown/tsdown-css.js`, so `tsdown/internal` + resolves against the bundled tsdown at build time. +- `wireBundledTsdownExtensions` rewrites the bundled tsdown call sites to load the + local chunks (`importWithError("@tsdown/exe")` → `import("./tsdown-exe.js")`, + `pkgExists("@tsdown/css")` → `true`, `import("@tsdown/css")` → `import("./tsdown-css.js")`). +- `mergePackageJson` drops `@tsdown/exe`/`@tsdown/css` from the published + `peerDependencies`; their types are inlined into `dist/tsdown/index-types.d.ts`. + +`lightningcss` (which `@tsdown/css` uses for CSS transforms) is a native module and +cannot be bundled. It is an **optional `peerDependency`**: the bundled CSS code +loads it through an injected `__vpImportLightningcss()` helper that raises an +actionable error when it is missing: + +``` +Cannot find package "lightningcss". CSS bundling with `vp pack` requires it. Install it with `vp add -D lightningcss`. +``` + ## `--exe` Feature (Experimental) The `--exe` flag bundles the output as a Node.js Single Executable Application (SEA). @@ -263,13 +297,14 @@ These are distinct commands: ### 3. tsdown Bundled Inside Core -**Decision**: tsdown is bundled inside `@voidzero-dev/vite-plus-core/pack` rather than used as a direct dependency. +**Decision**: tsdown is bundled inside `@voidzero-dev/vite-plus-core/pack` rather than used as a direct dependency. Its `@tsdown/exe` and `@tsdown/css` extensions are bundled the same way (see [tsdown Extensions](#bundled-tsdown-extensions)). **Rationale**: - Ensures consistent tsdown version across all vite-plus users - Avoids version conflicts in monorepos - The core build process bundles JS, CJS deps, and types together +- The extensions peer-depend on `tsdown`/`tsdown/internal`, which Vite+ does not expose as a top-level package, so bundling them is the only way they resolve (issue #1586). Only `lightningcss` (native) stays external as an optional peer. ### 4. Category C Delegation @@ -342,11 +377,29 @@ Options: Tests `vp pack -h` (help output includes all options including `--exe`) and `vp run pack` (build and cache hit). -### Local CLI Test: `command-pack-exe` +### Local CLI Test: `command-pack-css` + +**Location**: `packages/cli/snap-tests/command-pack-css/` + +Tests `vp pack src/index.ts --minify` on a CSS entry: the bundled `@tsdown/css` plus `lightningcss` transform the CSS (e.g. `#ff0000` → `red`), proving CSS bundling works without installing `@tsdown/css` (issue #1586). + +### Local CLI Test: `command-pack-tsdown-extensions` + +**Location**: `packages/cli/snap-tests/command-pack-tsdown-extensions/` + +Loads the bundled `dist/tsdown/tsdown-exe.js` and `dist/tsdown/tsdown-css.js` chunks to prove they resolve `tsdown/internal` against the bundled tsdown without a top-level `tsdown` package. This is Node-version independent (it does not run the SEA build), so it catches the import-resolution regression on every CI Node version. + +### Global CLI Test: `command-pack-exe` + +**Location**: `packages/cli/snap-tests-global/command-pack-exe/` + +Pins Node.js 25.7.0 (`engines.node`) and builds a real SEA executable end-to-end (`vp pack --exe`, then runs `./build/index`), exercising the bundled `@tsdown/exe`. + +### Global CLI Test: `command-pack-exe-error` -**Location**: `packages/cli/snap-tests/command-pack-exe/` +**Location**: `packages/cli/snap-tests-global/command-pack-exe-error/` -Tests `vp pack src/index.ts --exe` error behavior when Node.js version is below 25.7.0. +Tests `vp pack src/index.ts --exe` error behavior when the active Node.js version is below 25.7.0. ## Backward Compatibility @@ -355,6 +408,7 @@ This RFC documents an existing command with no breaking changes: - All existing `vp pack` options continue to work - The new `--exe` flag is purely additive - Config format in `vite.config.ts` is unchanged +- Bundling `@tsdown/exe`/`@tsdown/css` is a strict improvement: projects that previously installed them keep working and no longer need to. CSS bundling now only requires the optional `lightningcss` peer (`vp add -D lightningcss`), which is loaded lazily with an actionable error when missing. ## Exe Advanced Configuration @@ -382,7 +436,7 @@ export default { ### Cross-Platform Executable Building -Cross-platform builds are supported via the `@tsdown/exe` package (optional peer dependency). The `targets` option accepts an array of `{ platform, arch, nodeVersion }` objects to build executables for different platforms from a single host. +Cross-platform builds are powered by `@tsdown/exe`, which is bundled into core (no separate install needed; see [tsdown Extensions](#bundled-tsdown-extensions)). The `targets` option accepts an array of `{ platform, arch, nodeVersion }` objects to build executables for different platforms from a single host. ## Conclusion From 67b133c10c8632eb75c08e61b65c31dc4bb513a7 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 23 Jun 2026 14:21:04 +0800 Subject: [PATCH 2/8] test(pack): cover the missing lightningcss optional peer error Add the command-pack-css-missing-lightningcss snap test: a resolve hook makes lightningcss unresolvable (the dev/CI monorepo always has it), so vp pack surfaces the actionable install hint. Locks in that the error tells users how to fix it. --- .../block-lightningcss.mjs | 11 +++++++++++ .../package.json | 5 +++++ .../register-block.mjs | 5 +++++ .../command-pack-css-missing-lightningcss/snap.txt | 9 +++++++++ .../src/index.ts | 3 +++ .../src/style.css | 3 +++ .../command-pack-css-missing-lightningcss/steps.json | 6 ++++++ rfcs/pack-command.md | 10 ++++++++++ 8 files changed, 52 insertions(+) create mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs create mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json create mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs create mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt create mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts create mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css create mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs new file mode 100644 index 0000000000..b92f59cc58 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs @@ -0,0 +1,11 @@ +// Simulate `lightningcss` not being installed: the dev/CI monorepo always has it +// resolvable from core (it is a transitive dependency), so a resolve hook is the +// deterministic way to exercise the missing optional-peer path. See issue #1586. +export async function resolve(specifier, context, nextResolve) { + if (specifier === 'lightningcss') { + const error = new Error("Cannot find package 'lightningcss'"); + error.code = 'ERR_MODULE_NOT_FOUND'; + throw error; + } + return nextResolve(specifier, context); +} diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json new file mode 100644 index 0000000000..642a49fdbc --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-pack-css-missing-lightningcss", + "version": "1.0.0", + "type": "module" +} diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs new file mode 100644 index 0000000000..fa004d4181 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs @@ -0,0 +1,5 @@ +import { register } from 'node:module'; + +// Loaded via `NODE_OPTIONS=--import=./register-block.mjs` so the resolve hook +// applies to the `vp pack` process (and its managed-Node child). +register('./block-lightningcss.mjs', import.meta.url); diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt new file mode 100644 index 0000000000..8a6f395405 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt @@ -0,0 +1,9 @@ +[1]> NODE_OPTIONS=--import=./register-block.mjs vp pack src/index.ts --minify # missing optional lightningcss peer prints an actionable install hint (issue #1586) +ℹ entry: src/index.ts +ℹ Build start +error: Build failed with 1 error: + +[plugin @tsdown/css] /src/style.css +Error: Cannot find package "lightningcss". CSS bundling with `vp pack` requires it. Install it with `vp add -D lightningcss`. +Caused by: + Error: Cannot find package 'lightningcss' diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts new file mode 100644 index 0000000000..add68f6159 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts @@ -0,0 +1,3 @@ +import './style.css'; + +export const hello = 'world'; diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css new file mode 100644 index 0000000000..97ff765eff --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css @@ -0,0 +1,3 @@ +.foo { + color: #ff0000; +} diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json new file mode 100644 index 0000000000..7a9df9d520 --- /dev/null +++ b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json @@ -0,0 +1,6 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "NODE_OPTIONS=--import=./register-block.mjs vp pack src/index.ts --minify # missing optional lightningcss peer prints an actionable install hint (issue #1586)" + ] +} diff --git a/rfcs/pack-command.md b/rfcs/pack-command.md index 4693aeac7c..e6ff983390 100644 --- a/rfcs/pack-command.md +++ b/rfcs/pack-command.md @@ -389,6 +389,16 @@ Tests `vp pack src/index.ts --minify` on a CSS entry: the bundled `@tsdown/css` Loads the bundled `dist/tsdown/tsdown-exe.js` and `dist/tsdown/tsdown-css.js` chunks to prove they resolve `tsdown/internal` against the bundled tsdown without a top-level `tsdown` package. This is Node-version independent (it does not run the SEA build), so it catches the import-resolution regression on every CI Node version. +### Local CLI Test: `command-pack-css-missing-lightningcss` + +**Location**: `packages/cli/snap-tests/command-pack-css-missing-lightningcss/` + +Runs `vp pack --minify` with a resolve hook (`NODE_OPTIONS=--import`) that makes `lightningcss` unresolvable, simulating the uninstalled optional peer (the dev/CI monorepo always has it). Asserts the actionable error tells the user how to fix it: + +``` +Cannot find package "lightningcss". CSS bundling with `vp pack` requires it. Install it with `vp add -D lightningcss`. +``` + ### Global CLI Test: `command-pack-exe` **Location**: `packages/cli/snap-tests-global/command-pack-exe/` From 5862b1343e1973881e56fe1d95ba3a4ec68e0fcb Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 23 Jun 2026 14:27:17 +0800 Subject: [PATCH 3/8] ci(upgrade-deps): keep @tsdown/css and @tsdown/exe in lockstep with tsdown They are bundled into core and exact-peer-depend on the same tsdown version, so the catalog entries must track tsdown on every upgrade run. --- .github/scripts/upgrade-deps.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts index 301771390b..dbbe25a20e 100644 --- a/.github/scripts/upgrade-deps.ts +++ b/.github/scripts/upgrade-deps.ts @@ -212,6 +212,21 @@ async function updatePnpmWorkspace(versions: PnpmWorkspaceVersions): Promise Date: Tue, 23 Jun 2026 15:19:35 +0800 Subject: [PATCH 4/8] refactor(pack): ship lightningcss as a core dependency, not an optional peer lightningcss is already a core dependency (Vite's lightningcss transformer uses it), so the bundled @tsdown/css can use it directly and CSS bundling works with no extra install. Revert the optional-peer + loader churn: keep lightningcss in dependencies (via catalog) and drop the __vpImportLightningcss helper and the missing-lightningcss snap test. --- docs/guide/pack.md | 10 +-- .../block-lightningcss.mjs | 11 --- .../package.json | 5 -- .../register-block.mjs | 5 -- .../snap.txt | 9 --- .../src/index.ts | 3 - .../src/style.css | 3 - .../steps.json | 6 -- packages/core/build.ts | 70 ++++--------------- packages/core/package.json | 6 +- pnpm-lock.yaml | 9 ++- pnpm-workspace.yaml | 2 + rfcs/pack-command.md | 20 +----- 13 files changed, 28 insertions(+), 131 deletions(-) delete mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs delete mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json delete mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs delete mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt delete mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts delete mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css delete mode 100644 packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json diff --git a/docs/guide/pack.md b/docs/guide/pack.md index 18c70cf6ab..338a4b0e32 100644 --- a/docs/guide/pack.md +++ b/docs/guide/pack.md @@ -66,12 +66,4 @@ See the official [tsdown executable docs](https://tsdown.dev/options/exe#executa ## CSS Bundling -`vp pack` can transform and bundle CSS (including CSS Modules and Lightning CSS optimizations) for your entry points. This support is bundled into Vite+, so you do not need to install `@tsdown/css`. - -CSS transforms are powered by [Lightning CSS](https://lightningcss.dev/), which ships as a native module and is an optional peer dependency. Install it when you bundle CSS: - -```bash -vp add -D lightningcss -``` - -If it is missing, `vp pack` prints an actionable error telling you to install it. +`vp pack` can transform and bundle CSS (including CSS Modules and [Lightning CSS](https://lightningcss.dev/) optimizations) for your entry points. This support is bundled into Vite+, so you do not need to install `@tsdown/css` or `lightningcss` separately, it works out of the box. diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs deleted file mode 100644 index b92f59cc58..0000000000 --- a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/block-lightningcss.mjs +++ /dev/null @@ -1,11 +0,0 @@ -// Simulate `lightningcss` not being installed: the dev/CI monorepo always has it -// resolvable from core (it is a transitive dependency), so a resolve hook is the -// deterministic way to exercise the missing optional-peer path. See issue #1586. -export async function resolve(specifier, context, nextResolve) { - if (specifier === 'lightningcss') { - const error = new Error("Cannot find package 'lightningcss'"); - error.code = 'ERR_MODULE_NOT_FOUND'; - throw error; - } - return nextResolve(specifier, context); -} diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json deleted file mode 100644 index 642a49fdbc..0000000000 --- a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "command-pack-css-missing-lightningcss", - "version": "1.0.0", - "type": "module" -} diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs deleted file mode 100644 index fa004d4181..0000000000 --- a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/register-block.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import { register } from 'node:module'; - -// Loaded via `NODE_OPTIONS=--import=./register-block.mjs` so the resolve hook -// applies to the `vp pack` process (and its managed-Node child). -register('./block-lightningcss.mjs', import.meta.url); diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt deleted file mode 100644 index 8a6f395405..0000000000 --- a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/snap.txt +++ /dev/null @@ -1,9 +0,0 @@ -[1]> NODE_OPTIONS=--import=./register-block.mjs vp pack src/index.ts --minify # missing optional lightningcss peer prints an actionable install hint (issue #1586) -ℹ entry: src/index.ts -ℹ Build start -error: Build failed with 1 error: - -[plugin @tsdown/css] /src/style.css -Error: Cannot find package "lightningcss". CSS bundling with `vp pack` requires it. Install it with `vp add -D lightningcss`. -Caused by: - Error: Cannot find package 'lightningcss' diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts deleted file mode 100644 index add68f6159..0000000000 --- a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './style.css'; - -export const hello = 'world'; diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css deleted file mode 100644 index 97ff765eff..0000000000 --- a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/src/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.foo { - color: #ff0000; -} diff --git a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json b/packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json deleted file mode 100644 index 7a9df9d520..0000000000 --- a/packages/cli/snap-tests/command-pack-css-missing-lightningcss/steps.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ignoredPlatforms": ["win32"], - "commands": [ - "NODE_OPTIONS=--import=./register-block.mjs vp pack src/index.ts --minify # missing optional lightningcss peer prints an actionable install hint (issue #1586)" - ] -} diff --git a/packages/core/build.ts b/packages/core/build.ts index cd012a4106..5950e8cf59 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -394,8 +394,8 @@ async function bundleTsdown() { // Everything else in tsdown's peer dependencies stays external. `lightningcss` // (a native module) and `postcss` also stay external: `@tsdown/css` pulls them - // in, and they cannot be bundled. `lightningcss` becomes an optional peer with - // a friendly loader injected by `wireBundledTsdownExtensions()`. + // in and they cannot be bundled. Both are core `dependencies`, so they resolve + // at runtime and CSS bundling works without users installing anything. const tsdownExternal = [ ...Object.keys(pkgJson.peerDependencies).filter((name) => !bundledTsdownPackages.has(name)), 'lightningcss', @@ -585,8 +585,7 @@ async function brandTsdown() { // Wire the bundled `@tsdown/exe` and `@tsdown/css` extensions into the bundled // tsdown so they load from the local chunks instead of resolving external -// top-level packages, and make `lightningcss` an optional peer with a friendly -// loader. See https://github.com/voidzero-dev/vite-plus/issues/1586 +// top-level packages. See https://github.com/voidzero-dev/vite-plus/issues/1586 async function wireBundledTsdownExtensions() { const tsdownDistDir = join(projectDir, 'dist/tsdown'); const buildFiles = await glob(toPosixPath(join(tsdownDistDir, 'build-*.js')), { absolute: true }); @@ -594,11 +593,13 @@ async function wireBundledTsdownExtensions() { throw new Error('wireBundledTsdownExtensions: no build chunk found in dist/tsdown/'); } - // 1) Route `@tsdown/exe` and `@tsdown/css` to the bundled entries. - // - `importWithError("@tsdown/exe")` dynamically imports a runtime string, - // which rolldown cannot follow, so rewrite the call site to the chunk. - // - `pkgExists("@tsdown/css")` resolves the top-level package at runtime; - // since it is bundled now, force it on and point the import at the chunk. + // Route `@tsdown/exe` and `@tsdown/css` to the bundled entries. + // - `importWithError("@tsdown/exe")` dynamically imports a runtime string, + // which rolldown cannot follow, so rewrite the call site to the chunk. + // - `pkgExists("@tsdown/css")` resolves the top-level package at runtime; + // since it is bundled now, force it on and point the import at the chunk. + // `@tsdown/css` still imports `lightningcss` (a native module that cannot be + // bundled), which resolves to core's own `lightningcss` dependency. let exeWired = false; let cssWired = false; for (const buildFile of buildFiles) { @@ -628,37 +629,6 @@ async function wireBundledTsdownExtensions() { if (!cssWired) { throw new Error('wireBundledTsdownExtensions: `pkgExists("@tsdown/css")` not found'); } - - // 2) `lightningcss` is a native module and an optional peer. Wrap its dynamic - // import so a missing install produces an actionable error instead of a raw - // `Cannot find package 'lightningcss'`. - const helper = - 'async function __vpImportLightningcss() {\n' + - ' try {\n' + - ' return await import("lightningcss");\n' + - ' } catch (cause) {\n' + - ' throw new Error(' + - '"Cannot find package \\"lightningcss\\". CSS bundling with `vp pack` requires it. ' + - 'Install it with `vp add -D lightningcss`.", { cause });\n' + - ' }\n' + - '}\n'; - const allChunks = await glob(toPosixPath(join(tsdownDistDir, '*.js')), { absolute: true }); - let lightningcssWired = false; - for (const chunk of allChunks) { - let content = await readFile(chunk, 'utf-8'); - if (!content.includes('import("lightningcss")')) { - continue; - } - // Rewrite the call sites first, then prepend the helper (whose own - // `import("lightningcss")` must not be rewritten). - content = content.replaceAll('import("lightningcss")', '__vpImportLightningcss()'); - content = helper + content; - await writeFile(chunk, content); - lightningcssWired = true; - } - if (!lightningcssWired) { - throw new Error('wireBundledTsdownExtensions: `import("lightningcss")` not found'); - } } // Actually do nothing now, we will polish it in the future when `vitepress` is ready @@ -755,14 +725,6 @@ async function mergePackageJson() { const vitePkg = JSON.parse(await readFile(vitePkgPath, 'utf-8')); const destPkg = JSON.parse(await readFile(destPkgPath, 'utf-8')); - // Track the bundled `@tsdown/css` lightningcss dependency so the optional peer - // range stays in lockstep with what the bundled CSS code expects. - const require = createRequire(import.meta.url); - const tsdownCssPkg = JSON.parse( - await readFile(require.resolve('@tsdown/css/package.json'), 'utf-8'), - ); - const lightningcssPeerRange: string = tsdownCssPkg.dependencies?.lightningcss ?? '^1.30.2'; - // Merge peerDependencies from tsdown and vite destPkg.peerDependencies = { ...tsdownPkg.peerDependencies, @@ -775,17 +737,15 @@ async function mergePackageJson() { ...vitePkg.peerDependenciesMeta, }; - // `@tsdown/exe` and `@tsdown/css` are bundled into core (see bundleTsdown), - // so they must not be advertised as peers anymore. `lightningcss` is a native - // module pulled in by the bundled `@tsdown/css` (and Vite's lightningcss - // transformer); keep it external as an optional peer so users only install it - // when they actually bundle CSS. + // `@tsdown/exe` and `@tsdown/css` are bundled into core (see bundleTsdown), so + // they must not be advertised as peers anymore. `lightningcss` (which the + // bundled `@tsdown/css` and Vite's lightningcss transformer use) stays a core + // `dependency`, kept in lockstep with `@tsdown/css` by the upgrade-deps script, + // so CSS bundling works without any extra install. for (const bundled of ['@tsdown/exe', '@tsdown/css']) { delete destPkg.peerDependencies[bundled]; delete destPkg.peerDependenciesMeta[bundled]; } - destPkg.peerDependencies['lightningcss'] = lightningcssPeerRange; - destPkg.peerDependenciesMeta['lightningcss'] = { optional: true }; destPkg.bundledVersions = { ...destPkg.bundledVersions, diff --git a/packages/core/package.json b/packages/core/package.json index eaa797872b..4f1356f767 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -102,6 +102,7 @@ "dependencies": { "@oxc-project/runtime": "catalog:", "@oxc-project/types": "catalog:", + "lightningcss": "catalog:", "postcss": "^8.5.6" }, "devDependencies": { @@ -116,7 +117,6 @@ "@vitejs/devtools": "^0.3.3", "es-module-lexer": "^1.7.0", "hookable": "^6.0.1", - "lightningcss": "^1.32.0", "magic-string": "^0.30.21", "oxc-parser": "catalog:", "oxfmt": "catalog:", @@ -140,7 +140,6 @@ "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.32.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", @@ -204,9 +203,6 @@ }, "yaml": { "optional": true - }, - "lightningcss": { - "optional": true } }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97246307a8..86b4221282 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,9 @@ catalogs: jsonc-parser: specifier: ^3.3.1 version: 3.3.1 + lightningcss: + specifier: ^1.32.0 + version: 1.32.0 lint-staged: specifier: ^16.2.6 version: 16.4.0 @@ -525,6 +528,9 @@ importers: less: specifier: ^4.0.0 version: 4.4.2 + lightningcss: + specifier: 'catalog:' + version: 1.32.0 postcss: specifier: ^8.5.6 version: 8.5.15 @@ -595,9 +601,6 @@ importers: hookable: specifier: ^6.0.1 version: 6.1.1 - lightningcss: - specifier: ^1.32.0 - version: 1.32.0 magic-string: specifier: ^0.30.21 version: 0.30.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7fafd253ba..b962b36f57 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -83,6 +83,8 @@ catalog: glob: ^13.0.0 husky: ^9.1.7 jsonc-parser: ^3.3.1 + # Kept in lockstep with the bundled @tsdown/css by .github/scripts/upgrade-deps.ts + lightningcss: ^1.32.0 lint-staged: ^16.2.6 lodash-es: ^4.17.21 micromatch: ^4.0.9 diff --git a/rfcs/pack-command.md b/rfcs/pack-command.md index e6ff983390..8429224a13 100644 --- a/rfcs/pack-command.md +++ b/rfcs/pack-command.md @@ -227,13 +227,9 @@ To fix this, both extensions are bundled into core instead of left as peers: `peerDependencies`; their types are inlined into `dist/tsdown/index-types.d.ts`. `lightningcss` (which `@tsdown/css` uses for CSS transforms) is a native module and -cannot be bundled. It is an **optional `peerDependency`**: the bundled CSS code -loads it through an injected `__vpImportLightningcss()` helper that raises an -actionable error when it is missing: - -``` -Cannot find package "lightningcss". CSS bundling with `vp pack` requires it. Install it with `vp add -D lightningcss`. -``` +cannot be bundled, so it stays external. It is already a core `dependency` (Vite's +lightningcss transformer uses it too), so the bundled `@tsdown/css` resolves it +automatically and CSS bundling works without any extra install. ## `--exe` Feature (Experimental) @@ -389,16 +385,6 @@ Tests `vp pack src/index.ts --minify` on a CSS entry: the bundled `@tsdown/css` Loads the bundled `dist/tsdown/tsdown-exe.js` and `dist/tsdown/tsdown-css.js` chunks to prove they resolve `tsdown/internal` against the bundled tsdown without a top-level `tsdown` package. This is Node-version independent (it does not run the SEA build), so it catches the import-resolution regression on every CI Node version. -### Local CLI Test: `command-pack-css-missing-lightningcss` - -**Location**: `packages/cli/snap-tests/command-pack-css-missing-lightningcss/` - -Runs `vp pack --minify` with a resolve hook (`NODE_OPTIONS=--import`) that makes `lightningcss` unresolvable, simulating the uninstalled optional peer (the dev/CI monorepo always has it). Asserts the actionable error tells the user how to fix it: - -``` -Cannot find package "lightningcss". CSS bundling with `vp pack` requires it. Install it with `vp add -D lightningcss`. -``` - ### Global CLI Test: `command-pack-exe` **Location**: `packages/cli/snap-tests-global/command-pack-exe/` From e107015598fa608d9127c8440bb7dd0437e0bb5f Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 23 Jun 2026 15:19:47 +0800 Subject: [PATCH 5/8] ci(upgrade-deps): sync lightningcss with @tsdown/css and ignore the extensions in Renovate lightningcss is a core dependency consumed by the bundled @tsdown/css, so upgrade-deps now mirrors @tsdown/css's lightningcss range into the catalog when bumping tsdown. Also ignore @tsdown/css, @tsdown/exe and lightningcss in Renovate so they stay in lockstep with tsdown instead of drifting. --- .github/renovate.json | 3 +++ .github/scripts/upgrade-deps.ts | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/.github/renovate.json b/.github/renovate.json index defc88aa94..afdda01716 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -21,7 +21,10 @@ "/^oxc-.*/", "@oxc-node/*", "@oxc-project/*", + "@tsdown/css", + "@tsdown/exe", "@vitejs/devtools", + "lightningcss", "oxfmt", "oxlint", "oxlint-tsgolint", diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts index dbbe25a20e..9de95e42cd 100644 --- a/.github/scripts/upgrade-deps.ts +++ b/.github/scripts/upgrade-deps.ts @@ -42,6 +42,7 @@ type UpstreamVersions = { type PnpmWorkspaceVersions = { vitest: string; tsdown: string; + lightningcss: string; oxcNodeCli: string; oxcNodeCore: string; oxfmt: string; @@ -145,6 +146,25 @@ async function getLatestNpmVersion(packageName: string): Promise { return data.version; } +// Read a dependency range from the latest published version of `packageName`, +// e.g. the `lightningcss` range that the bundled `@tsdown/css` depends on. +async function getNpmDependencyRange(packageName: string, dependencyName: string): Promise { + const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`); + if (!res.ok) { + throw new Error( + `Failed to fetch npm metadata for ${packageName}: ${res.status} ${res.statusText}`, + ); + } + const data = (await res.json()) as { dependencies?: Record }; + const range = data.dependencies?.[dependencyName]; + if (typeof range !== 'string') { + throw new Error( + `Invalid npm response for ${packageName}: missing dependencies.${dependencyName}`, + ); + } + return range; +} + // ============ Update .upstream-versions.json ============ async function updateUpstreamVersions(): Promise { const filePath = path.join(ROOT, 'packages/tools/.upstream-versions.json'); @@ -227,6 +247,15 @@ async function updatePnpmWorkspace(versions: PnpmWorkspaceVersions): Promise Date: Tue, 23 Jun 2026 20:35:43 +0800 Subject: [PATCH 6/8] fix(pack): harden tsdown extension wiring and lightningcss version sync - wireBundledTsdownExtensions: scan every emitted chunk (not just build-*.js) and assert the bundled ./tsdown-css.js is referenced, so a rolldown chunking change can't silently ship an unrewritten import. - upgrade-deps: match any lightningcss range value so a non-caret range from @tsdown/css can't poison the next run's pattern. - rfc: lightningcss is a core dependency, not an optional peer. --- .github/scripts/upgrade-deps.ts | 4 +++- packages/core/build.ts | 24 ++++++++++++++++++------ rfcs/pack-command.md | 4 ++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts index 9de95e42cd..92863a5133 100644 --- a/.github/scripts/upgrade-deps.ts +++ b/.github/scripts/upgrade-deps.ts @@ -252,7 +252,9 @@ async function updatePnpmWorkspace(versions: PnpmWorkspaceVersions): Promise=`, compound ranges) on the next run. + pattern: /\n {2}lightningcss: ([^\n]+)\n/, replacement: `\n lightningcss: ${versions.lightningcss}\n`, newVersion: versions.lightningcss, }, diff --git a/packages/core/build.ts b/packages/core/build.ts index 5950e8cf59..b9b0e487b6 100644 --- a/packages/core/build.ts +++ b/packages/core/build.ts @@ -588,9 +588,11 @@ async function brandTsdown() { // top-level packages. See https://github.com/voidzero-dev/vite-plus/issues/1586 async function wireBundledTsdownExtensions() { const tsdownDistDir = join(projectDir, 'dist/tsdown'); - const buildFiles = await glob(toPosixPath(join(tsdownDistDir, 'build-*.js')), { absolute: true }); - if (buildFiles.length === 0) { - throw new Error('wireBundledTsdownExtensions: no build chunk found in dist/tsdown/'); + // Scan every emitted chunk, not just `build-*.js`: which chunk rolldown hoists + // these call sites into depends on its chunking heuristics, so don't pin to one. + const chunkFiles = await glob(toPosixPath(join(tsdownDistDir, '*.js')), { absolute: true }); + if (chunkFiles.length === 0) { + throw new Error('wireBundledTsdownExtensions: no chunk found in dist/tsdown/'); } // Route `@tsdown/exe` and `@tsdown/css` to the bundled entries. @@ -602,8 +604,12 @@ async function wireBundledTsdownExtensions() { // bundled), which resolves to core's own `lightningcss` dependency. let exeWired = false; let cssWired = false; - for (const buildFile of buildFiles) { - let content = await readFile(buildFile, 'utf-8'); + // The `import("@tsdown/css")` call site may already be deduped to the bundled + // entry by rolldown; track whether the bundled load ends up referenced either + // way so a silent miss (no rewrite and no dedup) fails the build. + let cssLoadWired = false; + for (const chunkFile of chunkFiles) { + let content = await readFile(chunkFile, 'utf-8'); let changed = false; if (content.includes('importWithError("@tsdown/exe")')) { content = content.replaceAll('importWithError("@tsdown/exe")', 'import("./tsdown-exe.js")'); @@ -619,8 +625,11 @@ async function wireBundledTsdownExtensions() { content = content.replaceAll('import("@tsdown/css")', 'import("./tsdown-css.js")'); changed = true; } + if (content.includes('import("./tsdown-css.js")')) { + cssLoadWired = true; + } if (changed) { - await writeFile(buildFile, content); + await writeFile(chunkFile, content); } } if (!exeWired) { @@ -629,6 +638,9 @@ async function wireBundledTsdownExtensions() { if (!cssWired) { throw new Error('wireBundledTsdownExtensions: `pkgExists("@tsdown/css")` not found'); } + if (!cssLoadWired) { + throw new Error('wireBundledTsdownExtensions: bundled `./tsdown-css.js` is never imported'); + } } // Actually do nothing now, we will polish it in the future when `vitepress` is ready diff --git a/rfcs/pack-command.md b/rfcs/pack-command.md index 8429224a13..1974a5cd99 100644 --- a/rfcs/pack-command.md +++ b/rfcs/pack-command.md @@ -300,7 +300,7 @@ These are distinct commands: - Ensures consistent tsdown version across all vite-plus users - Avoids version conflicts in monorepos - The core build process bundles JS, CJS deps, and types together -- The extensions peer-depend on `tsdown`/`tsdown/internal`, which Vite+ does not expose as a top-level package, so bundling them is the only way they resolve (issue #1586). Only `lightningcss` (native) stays external as an optional peer. +- The extensions peer-depend on `tsdown`/`tsdown/internal`, which Vite+ does not expose as a top-level package, so bundling them is the only way they resolve (issue #1586). Only `lightningcss` (native, cannot be bundled) stays external; it ships as a regular core dependency. ### 4. Category C Delegation @@ -404,7 +404,7 @@ This RFC documents an existing command with no breaking changes: - All existing `vp pack` options continue to work - The new `--exe` flag is purely additive - Config format in `vite.config.ts` is unchanged -- Bundling `@tsdown/exe`/`@tsdown/css` is a strict improvement: projects that previously installed them keep working and no longer need to. CSS bundling now only requires the optional `lightningcss` peer (`vp add -D lightningcss`), which is loaded lazily with an actionable error when missing. +- Bundling `@tsdown/exe`/`@tsdown/css` is a strict improvement: projects that previously installed them keep working and no longer need to. CSS bundling works out of the box, `lightningcss` is a core dependency that ships with the toolchain, so nothing extra needs installing. ## Exe Advanced Configuration From ec3b9b28dca9b46e4629b13db3cb32d5b8b11484 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 23 Jun 2026 20:51:50 +0800 Subject: [PATCH 7/8] refactor(upgrade-deps): share npm registry fetch helper getLatestNpmVersion and getNpmDependencyRange duplicated the same registry fetch + ok-check + json block; extract fetchNpmLatest so the registry URL and error wording live in one place. --- .github/scripts/upgrade-deps.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts index 92863a5133..3efe521527 100644 --- a/.github/scripts/upgrade-deps.ts +++ b/.github/scripts/upgrade-deps.ts @@ -28,6 +28,7 @@ type LatestTagOptions = { type NpmLatestResponse = { version?: unknown; + dependencies?: Record; }; type UpstreamVersions = { @@ -132,14 +133,18 @@ async function getLatestTag( } // ============ npm Registry ============ -async function getLatestNpmVersion(packageName: string): Promise { +async function fetchNpmLatest(packageName: string): Promise { const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`); if (!res.ok) { throw new Error( - `Failed to fetch npm version for ${packageName}: ${res.status} ${res.statusText}`, + `Failed to fetch npm metadata for ${packageName}: ${res.status} ${res.statusText}`, ); } - const data = (await res.json()) as NpmLatestResponse; + return (await res.json()) as NpmLatestResponse; +} + +async function getLatestNpmVersion(packageName: string): Promise { + const data = await fetchNpmLatest(packageName); if (typeof data.version !== 'string') { throw new Error(`Invalid npm response for ${packageName}: missing version field`); } @@ -149,13 +154,7 @@ async function getLatestNpmVersion(packageName: string): Promise { // Read a dependency range from the latest published version of `packageName`, // e.g. the `lightningcss` range that the bundled `@tsdown/css` depends on. async function getNpmDependencyRange(packageName: string, dependencyName: string): Promise { - const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`); - if (!res.ok) { - throw new Error( - `Failed to fetch npm metadata for ${packageName}: ${res.status} ${res.statusText}`, - ); - } - const data = (await res.json()) as { dependencies?: Record }; + const data = await fetchNpmLatest(packageName); const range = data.dependencies?.[dependencyName]; if (typeof range !== 'string') { throw new Error( From 12560ea3509b77709b7a5ce368b6505117fb1d1e Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 23 Jun 2026 20:55:30 +0800 Subject: [PATCH 8/8] docs(pack): suggest `vp env use 26` for the exe SEA build Node 26 is the version the exe targets default to and clears the 25.7 SEA floor; `vp env use 25` could resolve below it. --- docs/guide/pack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/pack.md b/docs/guide/pack.md index 338a4b0e32..2bcc6c8da9 100644 --- a/docs/guide/pack.md +++ b/docs/guide/pack.md @@ -60,7 +60,7 @@ export default defineConfig({ Executable support is bundled into Vite+, so you do not need to install `@tsdown/exe` separately. -Building executables uses Node's [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) support and requires Node.js 25.7.0 or later. Switch the active runtime with `vp env use 25` if `vp pack --exe` reports an unsupported version. +Building executables uses Node's [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) support and requires Node.js 25.7.0 or later. Switch the active runtime with `vp env use 26` if `vp pack --exe` reports an unsupported version. See the official [tsdown executable docs](https://tsdown.dev/options/exe#executable) for details about configuring custom file names, embedded assets, and cross-platform targets.