Skip to content

feat(build): migrate plugin and CLI builds to tsdown - #858

Merged
aXenDeveloper merged 4 commits into
canaryfrom
feat/tsdown-migration
Oct 10, 2026
Merged

aXenDeveloper merged 4 commits into
canaryfrom
feat/tsdown-migration

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Improving Documentation

  • Lint (oxlint --type-aware) and formatting (oxfmt --check) pass on the changed packages.
  • Docs follow the contribution guide: cli/build.mdx, cli/dev.mdx and cli/index.mdx updated; new plugins/build.mdx (how plugin builds and watch mode work) and plugins/migrate-to-tsdown.mdx (migrating an existing plugin from SWC). Both new pages compile in the web build and render with HTTP 200.

Description

What?

Replaces the plugin/package pipeline (tsc + SWC + tsc-alias) and the core CLI build (tsup) with tsdown 0.23.0.

vitnode build in a plugin or adapter package

  • One VitNode-owned tsdown config (scripts/cli/builder/package-build.ts); plugin authors write no config.
  • Unbundle mode: every .ts/.tsx under src (except tests, src/tests, __tests__, __fixtures__) compiles one-to-one into dist/src, so modules not reachable from an index are still built.
  • Contract preserved: ESM with .js extensions, automatic JSX runtime, *.server.js names, dynamic imports, side effects (no tree shaking), @/ aliases rewritten in JS and .d.ts, all package imports external. Non-code files (JSON, CSS) are copied and their imports keep import attributes. Production output is minified with declaration maps and no JS maps, the same file set as before.
  • Declarations use tsdown's normal TypeScript path with dts.eager. Without it, global.d.ts and types/api-registry.gen.d.ts are ignored and inferred fetcher types silently become any; a test covers this.
  • tsdown doesn't report type errors, so tsc --noEmit -p tsconfig.build.json runs concurrently and still fails the build.
  • A build writes only files whose content changed and, after success, prunes stale files inside dist/src only. The CLI build cleans only dist/scripts, so the two never delete each other's output.

vitnode dev in a plugin package

  • Runs three child processes: tsc --watch --noEmit, tsdown for JavaScript, and tsdown for declarations. They are separate processes because the declaration rebuild blocks its thread and delayed JS rebuilds to ~3s when shared.
  • VitNode watches src itself and restarts tsdown when files are added or removed. tsdown's own glob restart has a race that left builds failing (UNRESOLVED_ENTRY) after a quick add-then-delete.
  • Ctrl+C stops every watcher; a crashing watcher stops the rest.

Other changes

  • tsdown.config.ts replaces tsup.config.ts and also builds the package-watch.js runner. The createRequire banner was dropped after confirming the bundle has no require( and the built CLI runs.
  • Package detection: tsconfig.build.json plus exports pointing into ./dist/src/ (no .swcrc). It classifies every package in the repo the same way as before.
  • .swcrc files, @swc/cli, @swc/core, tsup and (outside API apps) tsc-alias removed. Generator template, versions, core's optional tsdown peer dependency and the lockfile updated; the lockfile only drops SWC/tsup packages and adds tsdown's. dist/*.tsbuildinfo is excluded from published packages.
  • Standalone API builds (tsc + tsc-alias) and Vite app builds are unchanged.

Verification

  • Core suite: 562 files / 8,914 tests pass, including new integration tests covering real tsdown builds of a template-based fixture, Node ESM execution, declaration imports and maps, failing type and syntax errors, pruning, the full watch lifecycle with no leftover processes, the built CLI, scripts/package output coexistence, and the generated plugin passing validation.
  • Type tests, create-vitnode-app tests, typecheck, lint and format pass. turbo build:plugins --force succeeds (10/10).
  • A plugin generated by create-vitnode-app builds, runs in watch mode and passes vitnode plugin validate.
  • The web app type-checks against the new declarations, and its production build succeeds with the same 588 bundler warnings as with the old output.
  • Packed @vitnode/core and @vitnode/blog in an isolated consumer: no type errors in @vitnode/* declarations, fetcher inference intact, and the modules and CLI run under Node.

Performance (same machine, A/B)

SWC/tsup tsdown
CLI build 1.29s 1.19s
Core cold build 54.8s 55.8s
Core repeated build ~16.5s ~47.8s
Example plugin cold / repeat 7.0s / ~7.1s 7.6s / ~8.0s
Blog plugin cold / repeat 7.6s / ~7.6s 8.9s / ~8.5s
Dev rebuild, JS (example) 54–78ms 58–85ms
Dev rebuild, .d.ts (example) 2.2–3.2s 3.8–4.5s

Known limitations

  • Repeated core builds are ~3× slower: tsc reused its incremental declaration output, tsdown regenerates every declaration. Its incremental mode got the repeat to 22.6s but made the cold build worse (65s) and needs a generated tsconfig plus a cache; not adopted here.
  • Core JS rebuilds in dev take ~2s because rolldown re-renders all ~1,600 modules (plugins stay under 100ms). Adding or deleting a core file rebuilds all its declarations (~45s, in the background).
  • Writes aren't atomic; a dev server can briefly see a partly written file, but only for files that changed.
  • Some re-export-only index.ts files no longer get a .d.ts.map.
  • tsdown requires Node ^22.18, ^24.11 or >=26.

Why?

Moves plugin builds and dev watch mode to one coordinated tool. tsdown handles alias rewriting, JavaScript and declarations together, so there's no separate alias pass racing against compiler output, and plugin authors need no build config of their own.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DspbmiWQwLSS7JbZQrPYJT


Generated by Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T08:08:10.660878Z 55ccfc8 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

React Doctor found 1 new issue in 1 file · 1 warning · score 93 / 100 (Great) · 0 fixed · vs canary

1 warning

scripts/cli/builder/compiler-build.ts

  • ⚠️ L111 await inside a loop async-await-in-loop

Reviewed by React Doctor for commit 55ccfc8. See inline comments for fixes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f993eb2561

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"oxlint-tsgolint": versionsPackageJson.oxlintTsgolint,
}),
"tsc-alias": versionsPackageJson.tscAlias,
tsdown: versionsPackageJson.tsdown,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align generated plugins with the supported Node range

Generated plugins now install tsdown@^0.23.0, whose lockfile declares Node ^22.18.0 || ^24.11.0 || >=26.0.0, while the main setup documentation still supports every Node 22 release. On Node 22.0–22.17, engine-strict package managers reject the generated project, and otherwise its required build tool runs on an unsupported runtime, so vitnode build/dev cannot be relied on. Either select a compatible tsdown release or raise and enforce the project's Node requirement consistently.

Useful? React with 👍 / 👎.

… tsdown

- vitnode build compiles plugin/adapter packages with tsdown in unbundle mode
  (shared VitNode-owned config), with tsc --noEmit type checking alongside
- vitnode dev runs tsc --watch plus separate tsdown watchers for JavaScript
  and declarations, restarting on added/removed sources
- core CLI/scripts build with tsdown.config.ts instead of tsup
- plugin detection no longer requires .swcrc; .swcrc files, SWC, tsc-alias
  (outside API apps) and tsup removed; generator and versions updated
- integration tests for builds, watch lifecycle, built CLI and generated plugins
- docs: plugin build/watch explanation and SWC migration guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DspbmiWQwLSS7JbZQrPYJT
@aXenDeveloper
aXenDeveloper force-pushed the feat/tsdown-migration branch from f993eb2 to c3e4e0b Compare October 9, 2026 20:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c3e4e0b108

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

*/
export const listPackageEntries = (root: string): string[] =>
listSourceFiles(root).filter(
file => /\.tsx?$/.test(file) && !DECLARATION.test(file),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compile all TypeScript module extensions classified as code

When a plugin contains an unreferenced .mts or .cts module, this filter omits it from tsdown's entries, while SCRIPT also prevents it from being copied as an asset. The type check can therefore pass and the build report success without producing the module under dist/src—and pruning can delete its previous output—even though these extensions are explicitly recognized elsewhere as scripts and declarations. Include the supported TypeScript module extensions in the entry filter.

Useful? React with 👍 / 👎.

Comment on lines +425 to +426
try {
await sync();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize the initial watcher build with queued restarts

If an entry or asset changes while the initial tsdown build is pending for longer than the 200 ms debounce, the filesystem callback queues sync() on the already-resolved pending promise while this direct sync() is still running. Both builds can then overlap; because current is assigned only after each build resolves, one handle can be overwritten without being closed, leaving duplicate watchers rebuilding and writing the same output. Put the initial synchronization onto the same pending chain used by restarts.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a63546afb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +145 to +150
for (const entry of readdirSync(current)) {
const path = join(current, entry);
if (statSync(path).isDirectory() && removeEmpty(path)) continue;
empty = false;
}
if (empty && current !== join(outDir, dir)) rmdirSync(current);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make directory pruning safe across parallel watchers

When vitnode dev deletes the last source in a nested directory, the JavaScript and declaration watcher processes both call prune concurrently on the same output tree. One process can delete a file or directory after the other's readdirSync but before this statSync/rmdirSync, causing an ENOENT that fails that rebuild. Tolerate already-removed paths or centralize empty-directory cleanup so the two watchers cannot race.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68c97a5bbb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +143 to +146
const typeCheck = runStep(check, project).then(result => ({
...result,
duration: Date.now() - startedAt,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle type-check rejection before starting tsdown

When runStep rejects before the tsdown build finishes—for example, because typescript is missing and resolveBin throws, or because the child process cannot be spawned—the derived typeCheck promise has no rejection handler until after await tsdown.build(...). Node can therefore treat it as an unhandled rejection and terminate the CLI instead of returning the intended configuration/runtime error. Attach a rejection handler immediately or store an always-settled result before starting the concurrent build.

Useful? React with 👍 / 👎.

);
}
task.succeed();
finishStep(ui, task, step, await runStep(step, project));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

@aXenDeveloper
aXenDeveloper merged commit 597ffa7 into canary Oct 10, 2026
7 checks passed
@aXenDeveloper
aXenDeveloper deleted the feat/tsdown-migration branch October 10, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants