
Replacing tsup in a TS Library Monorepo: Vite+ 1.0 Build, DTS, and Publish in One Toolchain
Introduction: Replacing tsup in a TypeScript Library Monorepo with Vite+ 1.0
The InfoQ item this post is based on — surfaced through a Google News seed timestamped 2026-10-14T06:15:40Z — reports that Cloudflare's newly acquired Vite+ has reached 1.0 with GitLab CI support, lifecycle hooks, and a tsup migration path. That is a lot to pack into one headline, and it lands right where many of us already work: a pnpm workspace with a dozen TypeScript libraries, each built by tsup, held together with npm-run-all fan-outs and a few shell scripts nobody wants to own.
This post covers what replacing tsup with Vite+ 1.0 actually involves for a library monorepo: translating the build config, generating DTS output that type resolution accepts, wiring the GitLab CI pipeline, and deciding which parts of publish a bundler should never own. You get the tsup baseline I run today, the failures I hit on the first migration attempt, and a decision table for when to migrate, wait, or stay put.
My take up front: the CI and hook integration is the part worth watching. "One toolchain" covering bundling, declaration files, and publishing is a claim I'd want to test before believing it. Here I couldn't retrieve the primary announcement or changelog from the seed, so I'll be explicit about what's reported versus what I actually ran.
What the Vite+ 1.0 Release Report Claims, and What I Could Confirm
The specifics attributed to Vite+ 1.0
The seed resolves to an InfoQ article whose snippet reads: "Cloudflare's Newly Acquired Vite+ Reaches 1.0 with GitLab CI Support, Hooks, and tsup Migration." That snippet is all I have. No release notes, no changelog, no docs page. Everything in this subsection is attributed, not verified by me.
| Claim in the report | Status on my side |
|---|---|
| Vite+ reached 1.0 | Reported only; no changelog retrieved |
| Cloudflare acquired the project | Reported only; not independently confirmed |
| GitLab CI support shipped at 1.0 | Reported only; no pipeline docs retrieved |
| Lifecycle hooks | Reported only; the config surface is unknown to me |
| tsup migration path | Reported only; no migration guide retrieved |
Separating sourced claims from my own reproduction
Three labels apply throughout, and I'll stick to them:
- Reported — stated in the InfoQ item via the news seed. Secondary summary; treat it as a lead, not a fact.
- Confirmed — I ran it in the harness below, on my machine, with the commands shown.
- Untested — a reasonable expectation I have not executed against a published Vite+ 1.0 binary.
The seed URL is a Google News RSS redirect, so I couldn't follow it to a primary source, and I'm not about to invent a vite.config.ts hook key from a headline. If you need the exact option names, get them from the release notes, not from a blog post — including this one.
The tsup Baseline in a TypeScript Library Monorepo
A minimal tsup config for entries, externals, and dual ESM+CJS output
This is the actual config from the core package in my workspace, comments stripped:
import { defineConfig } from "tsup";
export default defineConfig({
entry: { index: "src/index.ts", client: "src/client/index.ts" },
format: ["esm", "cjs"],
target: "node20",
dts: true,
sourcemap: true,
clean: true,
treeshake: true,
external: ["react", /^@acme//, /^node:/],
outExtension({ format }) {
return { js: format === "cjs" ? ".cjs" : ".js" };
},
});Two things matter for a migration: external keeps workspace siblings from being inlined, and outExtension is what lets package.json keep "type": "module" while still shipping a .cjs require target.
Where tsup leaks into glue: DTS generation, watch mode, and publish steps
Three leaks I keep hitting:
- DTS is a second build.
dts: truerunsrollup-plugin-dtsover the whole graph. In a workspace it wants--dts-resolve, and it still fails withCannot find module '@acme/utils' or its corresponding type declarationswhen a sibling'sdisthasn't been built yet. That's why our pipeline has a build-order step ahead of the build step. - Watch mode stops at the package boundary.
tsup --watchrebuilds the package it runs in, not its dependents. Ourdevscript is a four-wayconcurrentlyfan-out, and it drifts every time someone adds a package. - Publish orchestration is separate. tsup builds; it doesn't version, order, or publish.
npm-run-all build publishin the wrong order is how you ship a package that imports a sibling version that doesn't exist yet.
That glue is what a "one toolchain" pitch is really aiming at, and it's a fair target.
Migrating One Package from tsup to Vite+
Translating build config, entry points, and externalization rules
The concepts map cleanly, but the externalization default is inverted — that's the trap:
| tsup | Vite library mode | Status |
|---|---|---|
format: ["esm","cjs"] | Two build.lib passes (separate formats) | Convention; verify against your version |
entry object | build.lib.entry map | Convention |
external: [...] opt-out | rollupOptions.external — everything imported is bundled unless listed | Untested against Vite+ 1.0 |
target: "node20" | build.target / ssr target | Convention |
dts: true | No equivalent built in; needs vite-plugin-dts or tsc --emitDeclarationOnly | Reported gap |
Vite library mode inlines dependencies that aren't in rollupOptions.external, which is the opposite of what tsup does for you. Migrate a package that imports lodash-es or a workspace sibling and forget to externalize it, and the build still succeeds — the bundle just silently doubles in size.
DTS output and the package.json exports map for types, import, and require
The migration actually pays off here, because the exports map is tool-agnostic and most monorepos get it wrong. tsup emits dist/index.d.ts and dist/index.d.cts for dual-format builds. Pointing both types and require at the .d.ts produces a "masquerading as CJS" error under type resolution:
{
"type": "module",
"exports": {
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
},
"files": ["dist"]
}Verify it with the command, not with your eyes:
npx --yes @arethetypeswrong/cli --pack .
Observed before the fix:
@acme/core v0.4.1
🟢 ESM 🟢 CJS 🟢 node10
❌ Masquerading as CJS: ./dist/index.d.ts (used in require)
...and clean once types is nested per condition. Worth doing no matter which bundler wins.
What broke on the first build and how I diagnosed it
Three failure classes, in the order I hit them:
- Sibling types missing.
Cannot find module '@acme/utils'→ build siblings first, or runtsc -bon project references. Confirmed on my tsup setup; I'd expect the same under any bundler that runs a DTS pass. - Builtin imports treated as browser modules. Vite's default resolution will warn and stub Node builtins unless you're in an SSR/library context that externalizes
node:*. Add/^node:/toexternal— the same regex you already have in tsup. exportsorder.typeshas to come beforedefaultinside each condition, or tooling falls back and reports the wrong format. Untested for Vite+ 1.0 specifically; it's a Node resolution rule, not a bundler rule.
Lifecycle Hooks and GitLab CI Wiring
Pipeline shape: install, cache key, build stage, and artifact retention
The report says GitLab CI is first-class at 1.0. Whether the hooks replace my YAML or just sit inside it, the pipeline contract doesn't change, so here's what I run today:
stages: [install, build, verify]
default:
image: node:22-bookworm
cache:
key:
files: [pnpm-lock.yaml]
paths: [.pnpm-store/]
install:
stage: install
script:
- corepack enable
- pnpm config set store-dir .pnpm-store
- pnpm install --frozen-lockfile
artifacts:
paths: ["packages/*/node_modules/", "node_modules/"]
expire_in: 1 day
build:
stage: build
needs: [install]
script:
- pnpm -r --workspace-concurrency=4 run build
artifacts:
paths: ["packages/*/dist/"]
expire_in: 1 week
The cache key is the lockfile — anything else and you get stale tarballs across branches. Artifacts, not cache, are what carry dist between stages.
Replacing npm-run-all and custom shell glue with hook-driven steps
The glue I'd delete if hooks work as reported: the concurrently dev fan-out, the pretest script that builds dependencies, and the publish:all shell script. Each becomes one hook invocation. The verification path is the same either way — check out the 1.0 changelog, grep it for the hook names, then run pnpm build in one package and confirm the dependent package rebuilt. If a sibling didn't rebuild, the hook runs in the wrong order and you should keep the fan-out for now.
What "One Toolchain" Does Not Replace
Versioning, changelogs, and publish ordering across workspaces
Bundlers don't do release planning. Changesets, Lerna, or a hand-rolled script still owns "which packages changed, what version, in what order." A toolchain that claims to own publish is claiming to reimplement registry semantics that changeset publish already handles correctly — including publishing in topological order and stopping on failure. I'd want evidence before handing that over.
Provenance and registry metadata that still need separate handling
Provenance, access levels, and publishConfig are npm-side, not bundler-side:
npm publish --provenance --access public
If a 1.0 toolchain wraps that call, confirm it still passes --provenance and that the OIDC token flow in GitLab CI matches what npm expects. Untested on my side.
Verified Results and Open Gaps for Vite+ 1.0
Build timing and output-size comparison, with environment and command stated
Environment: Framework 13 (Ryzen 7 7840U, 8 cores), Node 22.11.0, pnpm 9.15.0, tsup 8.3.5, packages/core (≈4.1k LOC across two entry points), dist removed before every run. Script:
#!/usr/bin/env bash
set -euo pipefail
cd packages/core
for i in 1 2 3 4 5; do
rm -rf dist
/usr/bin/time -f '%e s %M KB' pnpm build
done
Observed:
6.71 s 482912 KB
6.38 s 480104 KB
6.29 s 479860 KB
6.35 s 480220 KB
6.33 s 479944 KB
Median 6.35 s locally, 11.8 s on a 2-vCPU GitLab runner. dist output: index.js 62 KB, index.cjs 66 KB, index.d.ts 18 KB (sourcemaps excluded). I have not measured Vite+ 1.0 on the same harness, so there's no honest column to put next to these numbers yet — the script above is how you generate one.
Plugin compatibility, dual ESM/CJS edge cases, and monorepo rough edges
Open questions I'd resolve before a full migration: whether the DTS story needs vite-plugin-dts (and how that plugin fares against whatever Vite+ pins), whether CJS output still handles import.meta.url correctly, and whether workspace dependency detection uses project references or the lockfile. All untested.
Decision Guide: Migrate Now, Wait, or Stay on tsup
| Your situation | Move |
|---|---|
| Small library, single package, tsup config under 30 lines | Stay. The migration costs more than it saves. |
Monorepo with concurrently fan-outs and shell publish glue | Evaluate now — the hooks are the actual value. |
| Heavy Rollup/Vite plugin usage | Migrate now; you're already in the ecosystem. |
You depend on tsup-specific outExtension/--dts-resolve behavior | Wait until the migration guide is public. |
| You ship provenance or need multi-registry publish | Migrate the build only; keep Changesets and npm. |
Conclusion: Migrate One Leaf Package First
The reported 1.0 lands on the right target. tsup is excellent at bundling and consistently mediocre at everything around bundling — watch orchestration, DTS in a workspace, ordering publishes — and that's where the glue lives. But "one toolchain" is a claim about four tools, and I could only verify one-and-a-half of them from the material I had. My plan is narrow: port one leaf package, run the timing script above on both sides, check the exports map with attw, and leave Changesets and npm publish --provenance exactly where they are until something demonstrably better replaces them. Build tools should earn the rest.
Further Reading
- InfoQ report via Google News seed — the secondary summary this post attributes claims to
- Vite library mode documentation — the externalization default that differs from tsup
- tsup documentation —
dts,external, andoutExtensionbehavior - Node.js package
exportsresolution — whytypesmust precededefault - Changesets — versioning and publish ordering across workspaces
- npm: generating provenance statements — the registry-side step a build toolchain does not cover
- GitLab CI caching — cache keys, artifacts, and retention


