Skip to content

Repository files navigation

lightningcss-build

Package Node CI License

An opinionated CSS bundler for libraries, built on the lightningcss Node API.

Overview

lightningcss-build adds the three things lightningcss-cli does not support — glob entry patterns, watch mode, and preserving your source layout in the output — on top of always-bundle, always-minify, and always-target-browserslist defaults. One command replaces ad hoc combinations of lightningcss, npm-run-all, and custom watcher scripts.

It always:

  • Minifies the output.
  • Bundles @imports into each entry.
  • Targets the project's browserslist config (or browserslist's defaults if none is present).
  • Preserves source directory structure in the output.
  • Expands glob patterns consistently across shells (macOS, Linux, and Windows), so quoting your patterns behaves the same everywhere.

In watch mode, it rebuilds only the entries affected by a change — editing a shared @import partial rebuilds only the entries that import it.

Install

npm install --save-dev lightningcss-build

Requires Node 22+.

Usage

lightningcss-build [options] [entries...]

Each entry is a file path or a glob pattern (resolved relative to the current working directory). If no entries are specified, it defaults to <input-dir>/*.css — top-level .css files in the source root, which is a common case for libraries.

Option Default Description
-i, --input-dir <dir> src Source root
-o, --output-dir <dir> dist Output directory
-w, --watch Rebuild on file changes
-s, --silent Suppress non-error output
-v, --version Show version
-h, --help Show help

Every resolved entry must live under --input-dir — anything outside is a hard error. Output mirrors the layout beneath it: each entry (or each file matched by an entry glob) produces its own file at <output-dir>/<relative-path-from-input-dir>.

Entries are never combined — N entries resolve to N outputs. Bundling refers to inlining each entry's @imports into that entry's output, not to merging entries with each other.

Files reached only via @import (not listed as entries) are inlined into the importer and never emitted as standalone outputs. A file that is both listed as an entry and @imported by another entry is emitted standalone and inlined into its importer.

Examples

Default. Top-level stylesheets in src/:

{
  "scripts": {
    "build:css": "lightningcss-build"
  }
}

→ each src/*.css writes to dist/.

Glob over a tree. Multiple stylesheets in a nested source tree:

{
  "scripts": {
    "build:css": "lightningcss-build \"src/**/*.css\""
  }
}

→ writes one output per matched file, preserving the directory layout.

Quote your globs. Without quotes, some shells expand globs themselves (and inconsistently — sh does not understand **). Quoting delegates expansion to lightningcss-build, which behaves identically across macOS, Linux, and Windows.

Custom input and output directories. Stylesheets under styles/, output to build/css/:

{
  "scripts": {
    "build:css": "lightningcss-build -i styles -o build/css \"styles/**/*.css\""
  }
}

styles/index.css writes to build/css/index.css;
styles/components/button.css writes to build/css/components/button.css.

Watch mode. Re-run on change, scoped to the affected entries:

{
  "scripts": {
    "watch:css": "lightningcss-build -w \"src/**/*.css\""
  }
}

Ctrl+C (SIGINT) or SIGTERM shuts the watcher down cleanly.

Only files under --input-dir are watched. @imports may still reference files outside it (they are bundled normally), but changes to those external files do not trigger a rebuild — re-save the importing entry to pick them up.

The set of entries is fixed at startup. Creating a new file that matches an entry glob does not add it to the watch set; restart lightningcss-build to pick up new entries. Deleting an entry removes its output; recreating the same file later (e.g. via git checkout) re-registers it and rebuilds.

Browserslist

Browserslist targets come from your project's browserslist config. The tool looks in the standard locations, in the usual order of precedence:

  1. browserslist field in package.json
  2. .browserslistrc
  3. Browserslist's built-in defaults (> 0.5%, last 2 versions, Firefox ESR, not dead)

Example package.json:

{
  "browserslist": ["> 0.25%", "last 2 versions", "not dead"]
}

There is no --targets flag. If you need a different query per build, set the BROWSERSLIST env var inline or switch configs with BROWSERSLIST_ENV.

Exit codes

Code Meaning
0 Build succeeded
1 Build error (parse error, missing @import, etc.)
2 Usage error (no entries, unknown flag, or entry outside --input-dir)

In watch mode, build errors stay scoped to the failing entry — previous good outputs are preserved, the watcher keeps running, and the error is logged as file:line:col.

Non-goals

  • Not a lightningcss-cli replacement for generic use cases. The opinions (always bundle, always minify, always respect Browserslist, and always preserve structure) cannot be turned off.
  • Not a CSS Modules / custom-media / custom-syntax configurator. If you need those lightningcss features, use lightningcss-cli or the API directly.
  • Not a config-file tool. All options are passed via CLI argv or resolved from standard project files.
  • Not a general bundler. No JS, no assets, no CSS Modules JSON, no HMR.

License

MIT © 2026 Igor Danchenko

About

Opinionated CSS build tool for libraries, powered by lightningcss

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Sponsor this project

Used by

Contributors

Languages