@oliveryasuna/tsdown-config
v1.0.1
Published
Readme
@oliveryasuna/tsdown-config
A tiny set of composable tsdown presets for the three build targets
worth naming — node, browser, and library — behind one defineConfig
entry point.
The resolved config is a function of the platform you name and the overrides you
pass, and nothing else. Each preset is a plain UserConfig object you can import
and inspect; there is no probing, no environment sniffing, and no magic — if an
option is set, it is set because you can see it in a preset or in your own
override.
Status: the source is complete and dogfooded — this package builds itself with
defineConfig('library', {format: 'esm'}). Not yet covered: there are no tests and no CI workflow. See Not yet wired.
Install
bun add -D tsdown @oliveryasuna/tsdown-configtsdown is a peer dependency (^0.22.12) — you bring your own, and this package
never pulls a second copy into your tree.
Usage
// tsdown.config.ts
import {defineConfig} from '@oliveryasuna/tsdown-config';
export default defineConfig('library');That is the whole common case. Name a platform and you get that preset merged on top of the shared base.
Presets
Each preset is the base defaults plus a small platform layer. They are plain objects, so they are diffable, spreadable, and importable on their own.
| Platform | Intent | Format | target | Notable |
| --- | --- | --- | --- | --- |
| library | Platform-neutral package for both Node and the browser | ESM + CJS | es2020 | platform: 'neutral', fixedExtension |
| node | Node 20+ runtime code | ESM + CJS | node20 | shims, nodeProtocol, fixedExtension |
| browser | Modern-browser bundle | ESM | es2020 | minify, platform: 'browser' |
Passing anything else throws Unknown platform: <value> rather than silently
producing a config with the base defaults dropped — so a typo in a plain-JS
config file fails loudly instead of shipping a broken build.
Base defaults
Shared by every preset, from base:
| Option | Value | Why |
| --- | --- | --- |
| entry | ['src/index.ts'] | The conventional single entry; override for multi-entry packages |
| clean | true | Wipe dist/ before each build |
| dts | true | Emit type declarations |
| sourcemap | true | Emit source maps |
| treeshake | true | Drop dead code |
| hash | false | Stable, predictable output filenames |
| deps | {neverBundle: true} | Leave node_modules external; bundle only your own source |
format is deliberately not set in base — every preset names its own, so a
base value would only ever be overwritten.
Overrides
defineConfig(platform, overrides?) accepts a second argument in three shapes.
They compose, so you can reach for exactly as much power as a given tweak needs.
An object — merged one level deep onto the resolved preset (tsdown's own
mergeConfig), which is enough for almost everything:
defineConfig('library', {format: 'esm', minify: true});A function — receives the fully resolved config and returns the final one. This is the escape hatch for what a merge structurally cannot express: removing a preset default, or extending an array instead of replacing it.
// Append an entry rather than replacing the whole array
defineConfig('node', (config) => ({
...config,
entry: [...config.entry, 'src/cli.ts']
}));
// Remove a preset default
defineConfig('browser', ({minify, ...rest}) => rest);A function override is total: whatever it returns is the config. Spread
...config to keep the preset — return a bare {minify: true} and you have
thrown the preset away.
An array — a list of layers folded left-to-right, each an object or a function. Later layers see the accumulated result of the earlier ones, and the last write wins:
defineConfig('node', [
sharedOrgDefaults, // your org's base tweaks
(config) => ({...config, entry: [...config.entry, 'src/cli.ts']}),
{minify: false} // wins over anything above
]);This is how you build a shared middle tier on top of these presets without forking them.
A top-level array is always treated as layers, not as
tsdown's multiple-build array form. To override to a multi-build config, return it from a function layer:(config) => [buildA, buildB].
API
Everything is exported from the package root.
| Export | Kind | Description |
| --- | --- | --- |
| defineConfig | function | (platform, overrides?) => UserConfig — the entry point |
| base | object | The shared base config |
| library, node, browser | object | The three platform layers, for hand-composition |
| Platform | type | 'node' \| 'browser' \| 'library' |
| Override | type | Partial<UserConfig> \| ((resolved: UserConfig) => UserConfig) |
The raw presets are exported so you can compose them yourself when the factory's
shape doesn't fit — mergeConfig(base, node, myLayer) is always available.
Development
bun install
bun run lint # eslint, via @oliveryasuna/eslint-config
bun run typecheck # tsc --noEmit
bun run build # tsdown → dist/Bun is pinned by .bun-version and checked by check:bun; husky, lint-staged,
syncpack, and secretlint run on commit.
Build
tsdown bundles src/index.ts to ESM only — the entire consumer surface is an
ESM config file:
| Artifact | Referenced by |
| --- | --- |
| dist/index.mjs | main, module, exports["."].default |
| dist/index.d.mts | exports["."].types |
prepack runs the build, so npm publish cannot ship a stale or missing dist/
— which matters because dist/ is gitignored. Nothing runs on a consumer's
machine: prepare and prepack are skipped when the package is installed as a
dependency, and there are no preinstall/postinstall scripts.
Not yet wired
Honest gaps, so nobody finds them the hard way:
- No tests. A handful of snapshot tests over the resolved presets and the override-folding are planned but do not exist yet.
- No CI workflow, so
lint,typecheck, andbuildare not enforced anywhere. - No
LICENSEfile yet, thoughpackage.jsondeclares MIT.
License
MIT © Oliver Yasuna
