Skip to content

Commit 6627876

Browse files
authored
fix(pack): bundle @tsdown/exe and @tsdown/css into core (#1919)
## Problem `@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. Installing them at the project level fails with `Failed to import module "@tsdown/exe"` (and the equivalent for CSS bundling). ## Fix Bundle both extensions into core (`tsdown-exe.js`, `tsdown-css.js`) so `tsdown/internal` resolves at build time and `vp pack --exe` / CSS bundling work with no extra install. `lightningcss` (native, cannot be bundled) becomes an optional peer, loaded lazily with an actionable error when it is missing. ## Tests - `command-pack-css`: CSS transforms run through the bundled `@tsdown/css` + lightningcss. - `command-pack-tsdown-extensions`: the bundled exe/css chunks load without a top-level `tsdown`. Closes #1586
1 parent 2c96a03 commit 6627876

17 files changed

Lines changed: 326 additions & 37 deletions

File tree

.github/renovate.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
"/^oxc-.*/",
2222
"@oxc-node/*",
2323
"@oxc-project/*",
24+
"@tsdown/css",
25+
"@tsdown/exe",
2426
"@vitejs/devtools",
27+
"lightningcss",
2528
"oxfmt",
2629
"oxlint",
2730
"oxlint-tsgolint",

.github/scripts/upgrade-deps.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type LatestTagOptions = {
2828

2929
type NpmLatestResponse = {
3030
version?: unknown;
31+
dependencies?: Record<string, string>;
3132
};
3233

3334
type UpstreamVersions = {
@@ -42,6 +43,7 @@ type UpstreamVersions = {
4243
type PnpmWorkspaceVersions = {
4344
vitest: string;
4445
tsdown: string;
46+
lightningcss: string;
4547
oxcNodeCli: string;
4648
oxcNodeCore: string;
4749
oxfmt: string;
@@ -131,20 +133,37 @@ async function getLatestTag(
131133
}
132134

133135
// ============ npm Registry ============
134-
async function getLatestNpmVersion(packageName: string): Promise<string> {
136+
async function fetchNpmLatest(packageName: string): Promise<NpmLatestResponse> {
135137
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
136138
if (!res.ok) {
137139
throw new Error(
138-
`Failed to fetch npm version for ${packageName}: ${res.status} ${res.statusText}`,
140+
`Failed to fetch npm metadata for ${packageName}: ${res.status} ${res.statusText}`,
139141
);
140142
}
141-
const data = (await res.json()) as NpmLatestResponse;
143+
return (await res.json()) as NpmLatestResponse;
144+
}
145+
146+
async function getLatestNpmVersion(packageName: string): Promise<string> {
147+
const data = await fetchNpmLatest(packageName);
142148
if (typeof data.version !== 'string') {
143149
throw new Error(`Invalid npm response for ${packageName}: missing version field`);
144150
}
145151
return data.version;
146152
}
147153

154+
// Read a dependency range from the latest published version of `packageName`,
155+
// e.g. the `lightningcss` range that the bundled `@tsdown/css` depends on.
156+
async function getNpmDependencyRange(packageName: string, dependencyName: string): Promise<string> {
157+
const data = await fetchNpmLatest(packageName);
158+
const range = data.dependencies?.[dependencyName];
159+
if (typeof range !== 'string') {
160+
throw new Error(
161+
`Invalid npm response for ${packageName}: missing dependencies.${dependencyName}`,
162+
);
163+
}
164+
return range;
165+
}
166+
148167
// ============ Update .upstream-versions.json ============
149168
async function updateUpstreamVersions(): Promise<void> {
150169
const filePath = path.join(ROOT, 'packages/tools/.upstream-versions.json');
@@ -212,6 +231,32 @@ async function updatePnpmWorkspace(versions: PnpmWorkspaceVersions): Promise<voi
212231
replacement: `tsdown: ^${versions.tsdown}`,
213232
newVersion: versions.tsdown,
214233
},
234+
// `@tsdown/css` and `@tsdown/exe` are bundled into core and published in
235+
// lockstep with tsdown (they exact-peer-depend on the same tsdown version),
236+
// so pin both catalog entries to the tsdown version to avoid drift.
237+
{
238+
name: '@tsdown/css',
239+
pattern: /'@tsdown\/css': \^([\d.]+(?:-[\w.]+)?)/,
240+
replacement: `'@tsdown/css': ^${versions.tsdown}`,
241+
newVersion: versions.tsdown,
242+
},
243+
{
244+
name: '@tsdown/exe',
245+
pattern: /'@tsdown\/exe': \^([\d.]+(?:-[\w.]+)?)/,
246+
replacement: `'@tsdown/exe': ^${versions.tsdown}`,
247+
newVersion: versions.tsdown,
248+
},
249+
// `lightningcss` is a core dependency consumed by the bundled `@tsdown/css`.
250+
// Track exactly what `@tsdown/css` requires (already an `^x.y.z` range) so a
251+
// tsdown upgrade that bumps lightningcss is mirrored here.
252+
{
253+
name: 'lightningcss',
254+
// Match any range value (not just `^x.y.z`) so the pattern can re-match
255+
// whatever `@tsdown/css` declares (`~`, `>=`, compound ranges) on the next run.
256+
pattern: /\n {2}lightningcss: ([^\n]+)\n/,
257+
replacement: `\n lightningcss: ${versions.lightningcss}\n`,
258+
newVersion: versions.lightningcss,
259+
},
215260
{
216261
name: '@oxc-node/cli',
217262
pattern: /'@oxc-node\/cli': \^([\d.]+(?:-[\w.]+)?)/,
@@ -516,6 +561,7 @@ console.log('Fetching latest versions…');
516561
const [
517562
vitestVersion,
518563
tsdownVersion,
564+
lightningcssVersion,
519565
devtoolsVersion,
520566
oxcNodeCliVersion,
521567
oxcNodeCoreVersion,
@@ -530,6 +576,8 @@ const [
530576
] = await Promise.all([
531577
getLatestNpmVersion('vitest'),
532578
getLatestNpmVersion('tsdown'),
579+
// Mirror exactly what the bundled @tsdown/css depends on.
580+
getNpmDependencyRange('@tsdown/css', 'lightningcss'),
533581
getLatestNpmVersion('@vitejs/devtools'),
534582
getLatestNpmVersion('@oxc-node/cli'),
535583
getLatestNpmVersion('@oxc-node/core'),
@@ -545,6 +593,7 @@ const [
545593

546594
console.log(`vitest: ${vitestVersion}`);
547595
console.log(`tsdown: ${tsdownVersion}`);
596+
console.log(`lightningcss (from @tsdown/css): ${lightningcssVersion}`);
548597
console.log(`@vitejs/devtools: ${devtoolsVersion}`);
549598
console.log(`@oxc-node/cli: ${oxcNodeCliVersion}`);
550599
console.log(`@oxc-node/core: ${oxcNodeCoreVersion}`);
@@ -561,6 +610,7 @@ await updateUpstreamVersions();
561610
await updatePnpmWorkspace({
562611
vitest: vitestVersion,
563612
tsdown: tsdownVersion,
613+
lightningcss: lightningcssVersion,
564614
oxcNodeCli: oxcNodeCliVersion,
565615
oxcNodeCore: oxcNodeCoreVersion,
566616
oxfmt: oxfmtVersion,

docs/guide/pack.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,12 @@ export default defineConfig({
5858
});
5959
```
6060

61+
Executable support is bundled into Vite+, so you do not need to install `@tsdown/exe` separately.
62+
63+
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.
64+
6165
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.
66+
67+
## CSS Bundling
68+
69+
`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.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "command-pack-css",
3+
"version": "1.0.0",
4+
"type": "module"
5+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
> vp pack src/index.ts --minify # bundles CSS via the bundled @tsdown/css + lightningcss (issue #1586)
2+
ℹ entry: src/index.ts
3+
ℹ Build start
4+
ℹ dist/index.mjs <variable> kB │ gzip: <variable> kB
5+
ℹ dist/style.css <variable> kB │ gzip: <variable> kB
6+
ℹ 2 files, total: <variable> kB
7+
✔ Build complete in <variable>ms
8+
9+
> cat dist/style.css # lightningcss-optimized output proves @tsdown/css ran
10+
.foo {
11+
color: red;
12+
}
13+
14+
.bar {
15+
margin: 0;
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './style.css';
2+
3+
export const hello = 'world';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.foo {
2+
color: #ff0000;
3+
}
4+
5+
.bar {
6+
margin: 0px 0px 0px 0px;
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ignoredPlatforms": ["win32"],
3+
"commands": [
4+
"vp pack src/index.ts --minify # bundles CSS via the bundled @tsdown/css + lightningcss (issue #1586)",
5+
"cat dist/style.css # lightningcss-optimized output proves @tsdown/css ran"
6+
]
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "command-pack-tsdown-extensions",
3+
"version": "1.0.0",
4+
"type": "module"
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
> node verify-extensions.mjs # bundled @tsdown/exe and @tsdown/css load without a top-level tsdown (issue #1586)
2+
tsdown-exe.js: getCacheDir, getCachedBinaryPath, getTargetSuffix, resolveNodeBinary
3+
tsdown-css.js: CssPlugin, resolveCssOptions

0 commit comments

Comments
 (0)