npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sanity/tsdown-config

v0.28.1

Published

Shared configuration for tsdown

Readme

Shared config for tsdown

pnpm add --save-dev @sanity/tsdown-config tsdown

Create a tsdown.config.ts file with:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({tsconfig: 'tsconfig.dist.json'})

React Compiler

The same React Compiler feature as @sanity/pkg-utils is available. It runs the React Compiler on the source files before they are bundled, so published components are memoized automatically. The compiler needs to be installed separately:

pnpm add --save-dev oxc-transform-react

Then enable it:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  reactCompiler: true,
})

Pass an object to configure the compiler, using the same options as babel-plugin-react-compiler:

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  reactCompiler: {target: '18'},
})

Transforms (transform)

transform picks which implementation of the compiler runs:

| transform | Runs | Install | | ----------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | | 'oxc' (default) | oxc-transform-react, the Rust port | pnpm add -D oxc-transform-react | | 'babel' | babel-plugin-react-compiler | pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler |

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  reactCompiler: {target: '19', transform: 'babel'},
})

Good to know about the default 'oxc' (via @vitejs/plugin-react's compiler option):

  • One native pass compiles, strips TypeScript, and lowers JSX. Custom jsxImportSource? Opt into 'babel'.
  • Options are the serializable subset: no logger, no function-valued sources.

'babel' runs the reference implementation and covers those cases. It's opt-in: babel never lands in node_modules unless you set transform: 'babel' and install the toolchain. Only the implementation you use needs to be installed — the other one's options simply stay untyped (no declare module stubs needed).

React Server Components (reactServer)

React Server Components refuse to load React Compiler output — react/compiler-runtime throws in the react-server environment, since memoization can never pay off for components that render exactly once. The React team's guidance for libraries that ship compiled code is to publish two entrypoints: the compiled one for the client, and an uncompiled one under the react-server export condition.

The reactServer option of reactCompiler (an option of this config, never forwarded to the compiler) bakes that pattern in. It's experimental (@alpha) and not covered by semver: it can change behavior or be removed entirely in a minor version.

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  reactCompiler: {target: '19', reactServer: true},
})

Every entry is built twice from the same source, and the only difference is that React Compiler auto-memoization is applied to the non-react-server output. The uncompiled build lands next to the compiled one with .react-server inserted before the extension (dist/index.jsdist/index.react-server.js), only the compiled build emits the .d.ts files, and — when the exports feature is enabled — every entry export gains a react-server condition, with a types condition specified before it (the .react-server. files have no declaration siblings, so the explicit types condition points every resolution mode — including consumers with react-server in their customConditions — at the compiled build's declarations):

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "react-server": "./dist/index.react-server.js",
      "default": "./dist/index.js"
    }
  }
}

React Server Components resolve the uncompiled build, everything else (client components, SSR, plain Node) resolves the compiled one. The .react-server. files never become export subpaths of their own.

Nothing is stripped from either output, so pair reactServer with deleting manual useMemo/useCallback calls from the source: server components stop paying for memoization that cannot pay off, and client components get the compiler's finer-grained auto-memoization instead of the hand-written hooks.

reactServer is meant for isomorphic libraries that render in both worlds without a 'use client' directive (pure renderers like @portabletext/react). Client-only packages that ship 'use client' don't need it — they always load through the default condition anyway. Also note the general dual-entrypoint caveat: the two conditions are two module instances, so module-scope identity (e.g. createContext) is not shared between server and client trees — which is exactly the boundary React draws for such libraries anyway.

With reactServer the config resolves to two tsdown configs, so the isolated declarations annotation becomes satisfies Promise<UserConfig[]>.

styled-components

If your package uses styled-components, enable the same styledComponents transform that @sanity/pkg-utils has:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  styledComponents: true,
})

It adds displayName (better debugging) and componentId (avoids SSR hydration mismatches) to your styled components, and minifies the CSS in tagged template literals. Unlike @sanity/pkg-utils it doesn't require installing babel-plugin-styled-components, as it uses oxc's native port of the babel plugin.

Pass an object to customize the transform, using the same options as babel-plugin-styled-components:

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  styledComponents: {namespace: 'my-package'},
})

vanilla-extract

The same vanillaExtract feature as @sanity/pkg-utils is available. It extracts the CSS from .css.ts files into a separate file (dist/bundle.css by default), minified and lowered with lightningcss for the @sanity/browserslist-config browsers by default. Under the hood it uses @sanity/vanilla-extract-tsdown-plugin, a tsdown-native port of @vanilla-extract/rollup-plugin, so enabling it doesn't pull rollup into your project. Start by installing @vanilla-extract/css for authoring the .css.ts files:

pnpm add --save-dev @vanilla-extract/css
import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  vanillaExtract: true,
})

By default (inject: true with exports: {nodeCompat: true}) the conditional CSS export pattern is wired up automatically:

  • injects the self-referential import "<pkg>/bundle.css" into the entry chunks that use vanilla-extract styles,
  • emits a no-op bundle-css.js shim (plus bundle-css.d.ts) for runtimes that cannot import .css files, and
  • writes the conditional "./bundle.css" export to package.json (types → the shim's .d.ts, browser/style → the real CSS, node/default → the shim).

The result is that import "<pkg>/bundle.css" resolves to the real CSS in bundlers/browsers and to the no-op shim in Node and similar runtimes. Make sure the extracted CSS survives tree-shaking in consumers by adding it to sideEffects in package.json:

{
  "sideEffects": ["*.css"]
}

Pass an options object instead of true to customize - the options are modeled after the css options of @tsdown/css (e.g. fileName, minify, target, lightningcss). inject and exports are independent: set exports: true for a plain (browser-only) "./bundle.css" export without the shim, exports: false for a relative import "./bundle.css" instead of the export pattern, or inject: false to publish the CSS without importing it automatically (for packages whose consumers import the subpath themselves).

[!NOTE] inject: {nodeCompat: true} is deprecated. It means {inject: true, exports: {nodeCompat: true}} and still works, with a warning: nodeCompat configures how the CSS file is published, not how the import is injected, so it moved to exports.

Two Sanity-flavored defaults diverge from the bare plugins (which match @tsdown/css exactly):

  • minify defaults to true - published Sanity libraries ship minified CSS. Set minify: false for readable output.
  • The CSS syntax lowering target defaults to tsdown's top-level target, and when the effective target is undefined or names no browsers (e.g. 'node20', also what tsdown derives from engines.node - it speaks to the JS runtime, not the browsers the extracted CSS runs in), the lowering targets are resolved from @sanity/browserslist-config and passed through lightningcss.targets. The bare plugins (like @tsdown/css) would skip lowering in that case. target: false disables lowering entirely, and a user-provided lightningcss.targets wins over the fallback.

vanillaExtract can be combined with the css option below when a package also uses CSS modules (or other @tsdown/css features) — the two pipelines write to different files by default (bundle.css vs style.css) and do not interfere with each other.

css

tsdown's experimental css option enables the @tsdown/css pipeline for plain CSS, CSS modules, preprocessors, and Lightning CSS / PostCSS. Install @tsdown/css in the project first — it is an optional peer dependency:

pnpm add --save-dev @tsdown/css
import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  css: {
    modules: {localsConvention: 'camelCase'},
  },
})

The options are @tsdown/css's, plus an exports option this config implements on top, with the same two Sanity-flavored defaults as vanillaExtract:

  • exports defaults to {nodeCompat: true}, wiring up the same conditional CSS export pattern: the self-referential import "<pkg>/style.css", a no-op style-css.js shim with its style-css.d.ts declaration, and the conditional "./style.css" export in package.json. @tsdown/css has no equivalent — its own inject emits a relative import "./style.css", which throws in runtimes that cannot load .css files. Set exports: true for a plain (browser-only) CSS export, or exports: false to fall back to the relative injection.
  • minify defaults to true, and browserless CSS syntax lowering targets fall back to @sanity/browserslist-config — exactly as described for vanillaExtract above.

Both css and vanillaExtract can be enabled in the same config. Use that when a package authors styles with vanilla-extract and CSS modules (.module.css):

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  vanillaExtract: true,
  css: {
    modules: {localsConvention: 'camelCase'},
  },
})

vanilla-extract extracts into dist/bundle.css, while @tsdown/css merges other CSS — including scoped CSS modules — into dist/style.css. Each pipeline publishes its own conditional export and emits its own shim, so they never collide. See the InlineConfig.css API reference for the full option surface.

dts

tsdown's dts option is passed through as-is. By default tsdown auto-detects it from package.json (it's enabled when a types field or a types condition in exports is present). Pass an object to customize how the .d.ts files are generated, for example to use tsgo (the same feature as the tsgo option in @sanity/pkg-utils, requires either typescript v7 or @typescript/native-preview to be installed — with typescript v7 it's enabled automatically):

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  dts: {generator: 'tsgo'},
})

tsdoc

Runs API Extractor after the build (via tsdown's build:done hook) to check that TSDoc tags are valid and release tags are correct. Off by default — set tsdoc: true to enable, or pass an options object to customize rules and custom tags:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  tsdoc: true,
})
export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  tsdoc: {
    rules: {
      // do not require internal members to be prefixed with `_`
      'ae-internal-missing-underscore': 'off',
    },
  },
})

The check runs against every entry .d.ts / .d.mts / .d.cts file the build emitted. API Extractor is loaded only when that hook runs (or when you import the programmatic API), so enabling tsdoc does not pull those dependencies into the root @sanity/tsdown-config entry.

It is also available as checkTsdoc from @sanity/tsdown-config/tsdoc for hosts that want to run it outside the build:

import {checkTsdoc} from '@sanity/tsdown-config/tsdoc'

outDir

tsdown's outDir option is passed through as-is. When left undefined, tsdown writes to dist (the same default as @sanity/pkg-utils):

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  outDir: 'lib',
})

clean

tsdown's clean option is passed through as-is. When left undefined, tsdown defaults to true and removes outDir (dist by default) before each build.

Prefer an array of folders over a separate "clean" script in package.json. That way tsdown / pnpm build clears the directories itself — packages don't need rimraf, a clean script, or prebuild / run-s clean build wiring:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  // Instead of `"clean": "rimraf dist coverage"` (and running it before build):
  clean: ['dist', 'coverage'],
})

A string[] replaces tsdown's default (true → clean outDir), so include outDir (usually 'dist') in the array when you still want it cleaned alongside other folders. Pass false to skip cleaning entirely.

Isolated declarations

If you're using the @sanity/tsconfig/isolated-declarations preset — which makes tsdown generate the .d.ts files with oxc's fast isolated declarations transform — annotate the default export of tsdown.config.ts with satisfies Promise<UserConfig>:

import {defineConfig} from '@sanity/tsdown-config'
import type {UserConfig} from 'tsdown'

export default defineConfig() satisfies Promise<UserConfig>

Without the annotation, type-checking tsdown.config.ts with the @sanity/tsconfig presets (they enable declaration) fails with TS2883 in pnpm projects: the inferred type of the default export can only be named through @sanity/tsdown-config's own copy of tsdown, which isn't portable. satisfies Promise<UserConfig> names the type through your own tsdown dependency instead. With reactCompiler.reactServer the config resolves to two tsdown configs, so the annotation becomes satisfies Promise<UserConfig[]>.

Keep the isolated-declarations preset scoped to the tsconfig that tsdown builds with (e.g. a tsconfig.dist.json that only includes ./src). If isolatedDeclarations covers tsdown.config.ts itself, the default export can't be inferred at all (TS9037), and the config has to move into an explicitly annotated variable instead:

import {defineConfig} from '@sanity/tsdown-config'
import type {UserConfig} from 'tsdown'

const config: Promise<UserConfig> = defineConfig()

export default config

define

tsdown's define option is also passed through as-is. It replaces global identifiers with constant expressions at build time (the same feature as the define option in @sanity/pkg-utils):

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  define: {'process.env.NODE_ENV': JSON.stringify('production')},
})

sourcemap

tsdown's sourcemap option is forwarded with a true default (the same as @sanity/pkg-utils). tsdown itself defaults to false and does not read sourceMap from the tsconfig:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  sourcemap: false,
})

minify

@sanity/tsdown-config sets tsdown's minify option to compression only, with function and class names preserved:

{
  compress: {keepNames: {function: true, class: true}},
  mangle: false,
  codegen: false,
}

Consumers' production builds minify node_modules again anyway, so mangling identifiers or stripping whitespace in the published dist would not shrink final app bundles - it would only hurt debuggability. The compress pass still applies constant folding and dead code elimination (e.g. evaluating defined branches away), and keepNames stops it from stripping otherwise-unreferenced function/class names such as the inner name in forwardRef(function Button(…) {…}). React DevTools reads that name via Function.name - the tree-shakeable alternative to a top-level Button.displayName = '…' assignment, which is a side effect that pins unused components into consumer bundles (sanity-io/ui#2435). Names stripped at publish time are unrecoverable in userland, while keeping them costs a fraction of a kilobyte.

Override it by merging over the returned config, like everything else:

import {defineConfig} from '@sanity/tsdown-config'
import {mergeConfig} from 'tsdown'

export default mergeConfig(await defineConfig({tsconfig: 'tsconfig.dist.json'}), {
  // e.g. drop the names again for the smallest possible dist:
  minify: {compress: true},
})

deps

tsdown's deps option is forwarded. When platform is 'neutral' (the default), neverBundle always includes /^node:/ so node built-ins stay external, and userland neverBundle entries are appended rather than replacing that default (tsdown's mergeConfig would replace the array):

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  // Resulting neverBundle: [/^node:/, /^my-package(\/|$)/]
  deps: {neverBundle: [/^my-package(\/|$)/]},
})

'neutral' also restores inputOptions.resolve.mainFields: ['module', 'main'] for inlined dependencies that ship no exports map. Prefer it over 'node' for packages that also run in the browser - 'node' makes CommonJS-interop emit a module-scope createRequire(import.meta.url) for inlined CJS deps, which crashes browser-bundled consumers.

target

tsdown's target option is also passed through as-is. It downlevels JS syntax for the given runtimes (esbuild-style target strings), and doubles as the default CSS syntax lowering target when vanillaExtract is enabled (browserless targets like node20 don't affect the CSS - it falls back to @sanity/browserslist-config):

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  target: ['chrome90', 'safari16'],
})

exports

tsdown's exports option is forwarded with different defaults, suited for publishing Sanity libraries:

  • enabled: true - the exports map in package.json is generated on every build, whether CI is set or not. Gating on 'local-only'/'ci-only' surprised environments like Cursor Cloud that set CI=true without intending to skip package.json rewrites, and
  • devExports: true when pnpm is detected - the local exports map points at the source files (so monorepo siblings and editors resolve them directly), while publishConfig.exports receives the built files. This default is omitted for other or unknown package managers because they do not all reliably apply publishConfig.exports when publishing.

Userland values apply with tsdown's mergeConfig semantics: an object deep-merges over the defaults (so individual fields can be overridden), while any other value - false to disable exports generation, or a bare CI condition ('ci-only'/'local-only') - replaces them entirely:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  exports: {all: true},
})

The package-manager detection behind the devExports default only runs when that default can still apply - it is skipped when the userland value replaces the defaults (false, true, a bare CI condition) or sets devExports explicitly, so those configs behave identically across package managers without any filesystem probing.

cwd

tsdown's cwd option is forwarded as-is, and also used for the package-manager detection behind the exports devExports default (instead of process.cwd()). Config files can leave it unset; set it when driving builds programmatically for a package in another directory, e.g. from a monorepo script or a tool composing this config:

import {defineConfig} from '@sanity/tsdown-config'

const config = await defineConfig({
  cwd: '/path/to/package',
  tsconfig: 'tsconfig.dist.json',
})

bundleAnalyzer

Enables Rolldown's experimental bundleAnalyzerPlugin to emit a report of what the package itself bundles — chunks, modules, dependency chains — next to the build output. The plugin is currently experimental (exported from rolldown/experimental); this option is @alpha to match.

Analysis adds work to the build, so the option stays off by default. Typical usage is an env-gated opt-in so everyday pnpm build / publish is unchanged:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  bundleAnalyzer: process.env.ENABLE_BUNDLE_ANALYZER === 'true',
})

true selects format: 'md' — an LLM-friendly markdown report (analyze-data.md in outDir) — rather than the plugin's own 'json' default. Pass an object to customize:

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  bundleAnalyzer: {
    format: 'json',
    fileName: 'bundle-analysis.json',
  },
})

The report is not a publishable artifact. Exclude it from package.json files so an accidental analyze build cannot ship it:

{
  "files": ["dist", "!dist/analyze-data.md"]
}

With reactCompiler.reactServer only the compiled (default) variant is analyzed — the react-server variant skips it so the two parallel builds don't overwrite one report in the shared outDir.

checks

Rolldown's checks.circularDependency warning is enabled by default (Rolldown itself defaults it to false). Circular imports inflate bundle size and can cause execution-order issues, so library builds surface them as warnings. Cycles that only exist between declaration files are filtered out - see suppressWarnings.

To turn the warning off, merge over the returned config:

import {defineConfig} from '@sanity/tsdown-config'
import {mergeConfig} from 'tsdown'

export default mergeConfig(await defineConfig({tsconfig: 'tsconfig.dist.json'}), {
  checks: {circularDependency: false},
})

suppressWarnings

The checks.circularDependency check above also applies to the declaration bundling pass, where it reports cycles between the emitted .d.ts modules. Those imports are type-only and erased at runtime, so the cycles carry none of the hazards the check exists to surface - and they're unavoidable for mutually referencing public types, e.g. a DocumentNode whose fields are FieldNodes that point back at their parent document. In sanity-io/sanity#13753 109 of 136 cycle warnings were declaration-only, drowning out the 27 real ones.

So defineConfig sets tsdown's suppressWarnings to drop CIRCULAR_DEPENDENCY warnings whose entire cycle consists of declaration files (.d.ts/.d.mts/.d.cts). A cycle that includes even one runtime module still warns.

Adding your own suppressions - strings (matched with includes), regular expressions (matched with test), or a predicate - goes through the suppressWarnings option, which is OR'd with the built-in one rather than replacing it:

import {defineConfig} from '@sanity/tsdown-config'

export default defineConfig({
  tsconfig: 'tsconfig.dist.json',
  suppressWarnings: [/EMPTY_BUNDLE/, 'node_modules/some-dep'],
})

Suppression happens before failOnWarn turns warnings into errors, so a suppressed warning never fails the build. To drop the built-in suppression instead of adding to it, merge over the returned config - mergeConfig replaces functions:

import {defineConfig} from '@sanity/tsdown-config'
import {mergeConfig} from 'tsdown'

export default mergeConfig(await defineConfig({tsconfig: 'tsconfig.dist.json'}), {
  // Restores every warning, including the declaration-only cycles
  suppressWarnings: () => false,
})

Everything else: mergeConfig

defineConfig deliberately only exposes options you're likely to change. For anything else, merge tsdown options over the returned config with tsdown's own mergeConfig - defineConfig returns a promise, so await it first:

import {defineConfig} from '@sanity/tsdown-config'
import {mergeConfig} from 'tsdown'

export default mergeConfig(await defineConfig({tsconfig: 'tsconfig.dist.json'}), {
  // Any tsdown option, e.g. opting out of hashed chunk filenames:
  hash: false,
})

Programmatic composition

defineConfig() output is a mergeConfig-safe base, and that is a supported contract - tools like @sanity/pkg-utils compose their own opinions over it programmatically. mergeConfig applies with tsdown's semantics:

  • plugins (top-level, inputOptions, outputOptions) are appended, so layering your own plugins never clobbers the ones this config sets up (React Compiler, vanilla-extract, bundle analyzer),
  • plain objects deep-merge over the defaults (e.g. minify: {mangle: true} keeps the compress/codegen defaults), and
  • scalars and non-plugin arrays replace (e.g. publint: false, format: ['esm']).

Combined with cwd, a host can resolve a package-specific config without touching process.cwd():

import {defineConfig} from '@sanity/tsdown-config'
import {build, mergeConfig} from 'tsdown'

const config = mergeConfig(await defineConfig({cwd, tsconfig: 'tsconfig.dist.json'}), {
  // the host's own opinions, e.g. computed targets or extra plugins
})

await build({...config, config: false})