@ttsc/unplugin
v0.21.0
Published
Bundler adapters for ttsc plugins.
Maintainers
Readme
@ttsc/unplugin

Bundler adapter for ttsc plugins.
Use it when Vite, Rollup, esbuild, Webpack, Rspack, Next.js, Farm, or Bun owns the build but the project still needs ttsc plugins.
Setup
Install ttsc and TypeScript-Go first. Then install the bundler adapter:
npm install -D ttsc typescript
npm install -D @ttsc/unpluginChoose your bundler and add the adapter.
Vite
// vite.config.ts
import ttsc from "@ttsc/unplugin/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [ttsc()],
});Rollup
// rollup.config.ts
import ttsc from "@ttsc/unplugin/rollup";
export default {
input: "src/index.ts",
output: {
dir: "dist",
format: "esm",
},
plugins: [ttsc()],
};Rolldown
// rolldown.config.ts
import ttsc from "@ttsc/unplugin/rolldown";
export default {
input: "src/index.ts",
output: {
dir: "dist",
format: "esm",
},
plugins: [ttsc()],
};esbuild
// esbuild.config.ts
import { build } from "esbuild";
import ttsc from "@ttsc/unplugin/esbuild";
await build({
entryPoints: ["src/index.ts"],
outdir: "dist",
bundle: true,
plugins: [ttsc()],
});Webpack
// webpack.config.mjs
import ttsc from "@ttsc/unplugin/webpack";
export default {
entry: "./src/index.ts",
output: {
path: new URL("./dist", import.meta.url).pathname,
},
plugins: [ttsc()],
};Rspack
// rspack.config.mjs
import ttsc from "@ttsc/unplugin/rspack";
export default {
entry: "./src/index.ts",
plugins: [ttsc()],
};Next.js
// next.config.mjs
import withTtsc from "@ttsc/unplugin/next";
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default withTtsc(nextConfig);withTtsc covers Next.js builds that go through webpack. When Next.js runs on Turbopack (next dev --turbopack), wire the Turbopack loader rules below instead.
Turbopack
Turbopack has no JS plugin API, but it runs webpack loaders through turbopack.rules, and a ttsc transform is exactly loader-shaped (TypeScript source in, transformed source out). @ttsc/unplugin/turbopack is that standalone loader:
// next.config.mjs
/** @type {import("next").NextConfig} */
const nextConfig = {
turbopack: {
rules: {
"*.ts": { loaders: ["@ttsc/unplugin/turbopack"] },
"*.tsx": { loaders: ["@ttsc/unplugin/turbopack"] },
},
},
};
export default nextConfig;Pass options through the rule's options object: { loader: "@ttsc/unplugin/turbopack", options: { project: "tsconfig.build.json" } }. The loader keeps a per-worker compiler cache (Turbopack runs loaders in a worker pool) and returns the source unchanged for declaration files, node_modules paths, and transforms that produce no change.
Farm
// farm.config.ts
import ttsc from "@ttsc/unplugin/farm";
import { defineConfig } from "@farmfe/core";
export default defineConfig({
plugins: [ttsc()],
});Bun
// build.ts
import ttsc from "@ttsc/unplugin/bun";
await Bun.build({
entrypoints: ["src/index.ts"],
outdir: "dist",
plugins: [ttsc()],
});Under Bun.build, the adapter yields to the next loader for declarations, node_modules, source that ttsc leaves unchanged, and entries supplied through Bun.build({ files }). In-memory entries remain with Bun because ttsc transforms filesystem-backed project inputs. Bun's runtime onLoad contract does not accept an undefined result, so Bun.plugin() explicitly passes excluded and unchanged filesystem files through with their original source. Bun.build clears the project generation through its onStart lifecycle on every build. The runtime API has no corresponding hook, so one setup is treated as one immutable module-loading session: restart the Bun process after changing source, tsconfig, or plugin inputs.
Configuration
By default, @ttsc/unplugin finds the nearest tsconfig.json from the file being transformed and uses that project's plugin settings, including directly installed plugin packages.
If that is already the config you want, ttsc() is enough.
Project Selection
Use project when the bundler should read a different config file:
import ttsc from "@ttsc/unplugin/vite";
export default {
plugins: [
ttsc({
project: "tsconfig.bundle.json",
}),
],
};The project path is resolved from process.cwd().
Inline Compiler Options
Use compilerOptions when the bundler needs a small override without another config file:
import ttsc from "@ttsc/unplugin/vite";
export default {
plugins: [
ttsc({
compilerOptions: {
plugins: [
{
transform: "@ttsc/lint",
configFile: "./lint.config.ts",
},
{
transform: "typia/lib/transform",
finite: true,
},
],
},
}),
],
};compilerOptions is layered on top of the selected project config. Existing settings stay in place, and only the fields you pass here are changed for the bundler build.
Plugin-owned keys (typia's finite here) are forwarded to the plugin verbatim. @ttsc/lint reads only configFile from its entry; rules live in the referenced lint.config.* file (or one discovered by upward walk from the tsconfig directory when configFile is absent).
Plugin Overrides
Use the top-level plugins option inside ttsc(...) when the bundler should use a different plugin list from tsconfig.json:
import ttsc from "@ttsc/unplugin/vite";
export default {
plugins: [
ttsc({
plugins: [
{ transform: "@ttsc/lint", configFile: "./lint.config.ts" },
{ transform: "typia/lib/transform" },
],
}),
],
};Explicit adapter options override the plugin list read from the selected project config.
Set plugins: false to run the adapter without loading project plugins.
Next.js Options
Pass adapter options as the second argument:
// next.config.mjs
import withTtsc from "@ttsc/unplugin/next";
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default withTtsc(nextConfig, {
project: "tsconfig.bundle.json",
});Adapter Entrypoints
Import the entrypoint that matches your bundler:
import ttsc from "@ttsc/unplugin/vite";Supported entrypoints are:
@ttsc/unplugin/vite@ttsc/unplugin/esbuild@ttsc/unplugin/rollup@ttsc/unplugin/rolldown@ttsc/unplugin/webpack@ttsc/unplugin/rspack@ttsc/unplugin/turbopack@ttsc/unplugin/farm@ttsc/unplugin/next@ttsc/unplugin/bun
Each entrypoint supports ESM import and CJS require. In CommonJS configs, read the default export from require("@ttsc/unplugin/vite").default. @ttsc/unplugin/turbopack is not an unplugin factory but a standalone webpack loader; reference it by module name inside turbopack.rules instead of calling it.
Options
import type { TtscUnpluginOptions } from "@ttsc/unplugin";
const options: TtscUnpluginOptions = {
project: "tsconfig.json",
compilerOptions: {
baseUrl: ".",
},
plugins: false,
};project: path to thetsconfig.jsonused by the bundler.compilerOptions: temporary override layered on the selected project config.plugins: directttscplugin list override, orfalseto disable plugins.
Cache and Watch Invalidation
The transform graph also records module-resolution candidates that would outrank a selected target. The adapter registers candidates for importers in the transformed file's reachability closure, so creating or changing a higher-priority probe invalidates watch and persistent caches without an importer or tsconfig edit.
The transform host reports the program's reference graph (the transform envelope's graph section: per-file direct resolved references with type-only edges included, global-scope files, and the tsconfig extends chain — see the plugin protocol). Per transformed file, the adapter registers the graph's reachability closure, the globals, and the configs with the bundler via addWatchFile, so editing a type that only a generated validator depends on invalidates the module — in watch mode, in webpack's filesystem cache, and in Turbopack's fileDependencies — even though the bundler erased the type-only import from its own graph.
Transform plugins may additionally report, per file, the source files they consulted (the envelope's dependencies field); the adapter registers those as watch files too, union semantics. A plugin that declares such a list complete for a file narrows the registration instead: only its own list plus the tsconfig chain, so files the transform never consulted stop invalidating it. Files a plugin declares volatile (output depending on non-file inputs such as environment or time) bypass the adapter's transform cache and are marked uncacheable where the bundler exposes that control.
The transform cache uses the same input set: every regular file reached by the non-following project walk plus graph-reported inputs outside that walk (node_modules declarations, monorepo sibling sources, files reached through symlinks or Windows junctions, and out-of-root extends ancestry). One whole-project compile already contains every module's output. Adapters with a guaranteed build boundary clear the previous generation there, then check only the supplied source on each module's first delivery in that build; a repeated delivery validates the complete snapshots. This avoids one complete project walk per module on an initial build. Bun runtime setup defines one process-scoped loading session. Long-lived hosts without either boundary (Metro workers, the Turbopack loader, and Vite's development server, whose initial buildStart spans later HMR edits) validate the complete project and external-input snapshots on every generation hit.
Sponsors
Thanks for your support.
Your donation encourages ttsc development.
References
Inspired by @ryoppippi/unplugin-typia.
