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 301771390b..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 = { @@ -42,6 +43,7 @@ type UpstreamVersions = { type PnpmWorkspaceVersions = { vitest: string; tsdown: string; + lightningcss: string; oxcNodeCli: string; oxcNodeCore: string; oxfmt: string; @@ -131,20 +133,37 @@ 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`); } 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 data = await fetchNpmLatest(packageName); + 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'); @@ -212,6 +231,32 @@ 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, + }, { name: '@oxc-node/cli', pattern: /'@oxc-node\/cli': \^([\d.]+(?:-[\w.]+)?)/, @@ -516,6 +561,7 @@ console.log('Fetching latest versions…'); const [ vitestVersion, tsdownVersion, + lightningcssVersion, devtoolsVersion, oxcNodeCliVersion, oxcNodeCoreVersion, @@ -530,6 +576,8 @@ const [ ] = await Promise.all([ getLatestNpmVersion('vitest'), getLatestNpmVersion('tsdown'), + // Mirror exactly what the bundled @tsdown/css depends on. + getNpmDependencyRange('@tsdown/css', 'lightningcss'), getLatestNpmVersion('@vitejs/devtools'), getLatestNpmVersion('@oxc-node/cli'), getLatestNpmVersion('@oxc-node/core'), @@ -545,6 +593,7 @@ const [ console.log(`vitest: ${vitestVersion}`); console.log(`tsdown: ${tsdownVersion}`); +console.log(`lightningcss (from @tsdown/css): ${lightningcssVersion}`); console.log(`@vitejs/devtools: ${devtoolsVersion}`); console.log(`@oxc-node/cli: ${oxcNodeCliVersion}`); console.log(`@oxc-node/core: ${oxcNodeCoreVersion}`); @@ -561,6 +610,7 @@ await updateUpstreamVersions(); await updatePnpmWorkspace({ vitest: vitestVersion, tsdown: tsdownVersion, + lightningcss: lightningcssVersion, oxcNodeCli: oxcNodeCliVersion, oxcNodeCore: oxcNodeCoreVersion, oxfmt: oxfmtVersion, diff --git a/docs/guide/pack.md b/docs/guide/pack.md index b1714c48df..2bcc6c8da9 100644 --- a/docs/guide/pack.md +++ b/docs/guide/pack.md @@ -58,4 +58,12 @@ 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 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. + +## CSS Bundling + +`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/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..b9b0e487b6 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. 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', + '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,73 @@ 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. See https://github.com/voidzero-dev/vite-plus/issues/1586 +async function wireBundledTsdownExtensions() { + const tsdownDistDir = join(projectDir, '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. + // - `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; + // 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")'); + 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 (content.includes('import("./tsdown-css.js")')) { + cssLoadWired = true; + } + if (changed) { + await writeFile(chunkFile, content); + } + } + if (!exeWired) { + throw new Error('wireBundledTsdownExtensions: `importWithError("@tsdown/exe")` not found'); + } + if (!cssWired) { + throw new Error('wireBundledTsdownExtensions: `pkgExists("@tsdown/css")` not found'); + } + if (!cssLoadWired) { + throw new Error('wireBundledTsdownExtensions: bundled `./tsdown-css.js` is never imported'); } } @@ -662,6 +749,16 @@ 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` (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.bundledVersions = { ...destPkg.bundledVersions, vite: vitePkg.version, diff --git a/packages/core/package.json b/packages/core/package.json index 2920ac5fbd..4f1356f767 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -102,7 +102,7 @@ "dependencies": { "@oxc-project/runtime": "catalog:", "@oxc-project/types": "catalog:", - "lightningcss": "^1.30.2", + "lightningcss": "catalog:", "postcss": "^8.5.6" }, "devDependencies": { @@ -112,6 +112,8 @@ "@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", @@ -133,8 +135,6 @@ }, "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", @@ -156,12 +156,6 @@ "@arethetypeswrong/core": { "optional": true }, - "@tsdown/css": { - "optional": true - }, - "@tsdown/exe": { - "optional": true - }, "@vitejs/devtools": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9958a548b6..86b4221282 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 @@ -177,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 @@ -507,12 +516,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 @@ -526,7 +529,7 @@ importers: specifier: ^4.0.0 version: 4.4.2 lightningcss: - specifier: ^1.30.2 + specifier: 'catalog:' version: 1.32.0 postcss: specifier: ^8.5.6 @@ -583,6 +586,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) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ac9c9cde2d..b962b36f57 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 @@ -81,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 2a938f1e24..1974a5cd99 100644 --- a/rfcs/pack-command.md +++ b/rfcs/pack-command.md @@ -201,6 +201,36 @@ 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, 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) The `--exe` flag bundles the output as a Node.js Single Executable Application (SEA). @@ -263,13 +293,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, cannot be bundled) stays external; it ships as a regular core dependency. ### 4. Category C Delegation @@ -342,11 +373,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 +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 works out of the box, `lightningcss` is a core dependency that ships with the toolchain, so nothing extra needs installing. ## Exe Advanced Configuration @@ -382,7 +432,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