Library builds

Shell
esdev build --lib src            # src/** → dist/**.js + dist/**.d.ts

A library is an input to somebody else's build, so application defaults such as NODE_ENV, worker, and asset URLs are the consumer's to make.

A directory, not an entryEvery module under it is built. Your exports map decides what a consumer may import.
The output is replacedThe build owns it; a failed build replaces nothing.
Nothing is tree-shakenAn export no current caller uses is still the API.
Dependencies stay externalConsumers can dedupe, override or patch them.
Module structure preservedAn exports subpath is a real file and a stack trace names a module.
.d.ts beside each moduleDerived from the annotations your source already carries.
Stylesheets keep their namessrc/styles.css is dist/styles.css, so an exports map can name it exactly.

Skipped: *.test.* and .d.ts files. An --out that holds your source or project is refused rather than replaced. A single-entry application build replaces nothing; whole-project application builds replace their outdirs.

Library stylesheets are CSS passthrough files: their source-relative paths and their relative @imports are kept intact. A library cannot hash a stylesheet whose exact path its consumer's exports map must name.

For example, publish src/styles.css directly from the path the consumer imports:

JSON
{
  "exports": {
    ".": "./dist/index.js",
    "./styles.css": "./dist/styles.css"
  }
}
Shell
esdev build --lib src
# src/styles.css and src/theme.css → dist/styles.css and dist/theme.css

This is deliberately passthrough rather than a hashed deploy asset: a library's consumer controls caching and where its CSS is served.

A component that imports its stylesheet

A module may import its own stylesheet, so the two stay together while you work on them:

JavaScript
// src/button.js
import "./button.css";
export function Button() { /* … */ }

The stylesheet is copied like any other, and the import is removed from the published module. A .css import works only in a bundler that understands it; in Node, in a server render or under esrun it fails. The consumer imports the stylesheet instead, through your exports:

JavaScript
import { Button } from "my-lib";
import "my-lib/button.css";

That subpath has to be in exports. One line covers every stylesheet:

JSON
{
  "exports": {
    ".": "./dist/index.js",
    "./*.css": "./dist/*.css"
  }
}

esdev does not edit package.json. When exports cannot reach a copied stylesheet, the build warns and names the line to add. A package with no exports field gets no warning, because every file in it is importable.

CSS Modules

A .module.css that a module imports ships scoped: the class names in dist/button.module.css are the ones the published JavaScript hands out, such as btn_c2e58308. A module it composes from is imported at the top of the file, so importing one stylesheet brings every rule its class names use.

Images and fonts a stylesheet uses

A file a library stylesheet names with url() ships with it. By default it is copied into dist/assets/ under a content-hashed name, and the url() points at the copy. To inline every such file into the stylesheet as a data: URL instead, set css-urls:

Shell
esdev build --lib src --css-urls=inline
JSON
{ "build": { "targets": { "kit": { "entry": "src", "lib": true, "outdir": "dist", "css-urls": "inline" } } } }
css-urlsurl(./bg.png) becomesShips
copy (default)url("../assets/bg-1a2b3c4d.png")dist/assets/bg-1a2b3c4d.png
inlineurl("data:image/png;base64,…")nothing extra

Inlining keeps a package to its stylesheets, at the cost of a larger CSS file: base64 is about a third bigger than the file, and an image used twice is embedded twice. Remote, rooted and data: URLs, @imports, and a path that names no file are left as written. Everything else in the stylesheet is unchanged.

Declarations

Declarations are derived from what the source says, never from what a checker infers. An exported signature has to state its type:

TypeScript
export const driver = defineDriver({ … });                    // ✗
export const driver: Driver<Conn, Opts> = defineDriver({ … }); // ✓

A signature that does not meet this rule fails the build with the list of locations. This is TypeScript's isolatedDeclarations contract. --no-types skips declarations.

Imports in declarations are rewritten to the files the build writes: ./pool and ./pool.ts become ./pool.js, and a .d.cts names its .cjs sibling.

CommonJS output

esrun loads ES modules and nothing else. CommonJS is an output for consumers that still use require():

Shell
esdev build --lib src --format=esm,cjs
esdev build --lib src --format=cjs
.cjs, whatever "type" saysBoth trees in one directory are told apart by extension.
.d.cts beside each .cjsUnder node16, a .cjs is typed by that declaration.
Named exportsrequire() returns the namespace, so a default export is .default.
All or nothingNo format lands unless every format succeeds.

types goes inside each exports condition:

JSON
{
  "exports": {
    ".": {
      "import":  { "types": "./dist/index.d.ts",  "default": "./dist/index.js" },
      "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
    }
  }
}

Top-level await has no CommonJS form and stops the build. A runtime: import stays external in both trees.

One declaration file

For a package with one public entry, use a declaration bundle:

Shell
esdev build --lib src --dts-bundle    # → dist/index.d.ts

Everything the entry exports transitively is reachable and linked; types needed only through a public type are inlined without widening the API. Collisions are renamed consistently, dependencies stay imports, and JSDoc is preserved.

Keep per-module declarations when your exports map has subpaths. Unsupported constructs such as module augmentations stop the build and name themselves.

Library options

FlagIn esdev.json
--lib"lib": trueBuild a library
--format=<list>"format": ["esm", "cjs"]esm (default), cjs, or both
--no-types"types": falseSkip .d.ts files
--dts-bundle[=<entry>]"dts-bundle": true or an entryOne .d.ts instead of one per module
--css-urls=<copy|inline>"css-urls": "inline"What a stylesheet's url() files become; copy (default)
--out=<dir>"outdir": "dist"Where to write it
JSON
{
  "build": {
    "targets": {
      "lib": {
        "entry": "src",
        "lib": true,
        "format": ["esm", "cjs"],
        "outdir": "dist",
        "types": true,
        "assets": ["README.md", "LICENSE"]
      }
    }
  }
}

Library keys mean nothing on an application target and are refused rather than ignored, and a library is built without the project's aliases. Types are derived, not checked; esdev typecheck checks them.

Last updated on
Edit this page