Building a library of components, a directory per component means every entry point is called index — and every one of them compiled to index.css, or nested a level deep under esbuild's outbase. A glob-matched index.* is now named after the directory that holds it, so one glob turns a tree of component directories into a flat set of named bundles. Brace patterns like index.{js,ts} count as globs too.
A glob-matched index.* is named after its directory. This one is for
building libraries of components. One directory per component is the obvious
way to lay a library out — the accordion's markup, styles and script live
together, and each one is called index. Point a glob at them and the naming
falls apart, differently for each pipeline. Styles compiled every match to
<out>/index.css, so the last directory to build won and the rest were
overwritten in silence. Scripts fared better but not well: esbuild nests entry
points from different directories under their common ancestor, so you got
dist/accordion/index.js where you wanted dist/accordion.js.
{
"scripts": { "in": "src/elements/*/index.{js,mjs,cjs,jsx,ts,tsx}", "out": "dist/js/" },
"styles": { "in": "src/elements/*/index.{scss,sass,css}", "out": "dist/css/" }
}
src/elements/accordion/index.scss → dist/css/accordion.css
src/elements/accordion/index.ts → dist/js/accordion.js
src/elements/tabs/index.scss → dist/css/tabs.css
src/elements/tabs/index.ts → dist/js/tabs.js
Two globs, and the whole library builds to a flat set of bundles named after the components — add a directory, get a bundle, no config change.
The rename only applies to entries a glob matched. A literal
"in": "src/index.js" still writes dist/index.js, and
"in": "src/scss/index.scss" still writes dist/index.css — you named that
entry point yourself, and moving it to src.js or scss.css because of a
rule about globs would be a rename you never asked for. Same for an explicit
out file path: that always wins.
The name is placed relative to the glob's static prefix — everything
before its first magic segment — which is what keeps the flat case flat
without making same-named components collide. src/elements/*/index.scss
has the prefix src/elements, shared by every match, so nothing is left to
nest under. Widen the glob and the part it no longer pins down is kept:
"src/*/accordion/index.scss"
src/blocks/accordion/index.scss → dist/css/blocks/accordion.css
src/elements/accordion/index.scss → dist/css/elements/accordion.css
Two accordion directories, two stylesheets, no overwrite — and no config
to keep in sync, because the prefix is read off the pattern rather than off
whatever happened to match. Add or remove a component and the layout of the
rest doesn't move.
Brace patterns count as globs. hasMagic doesn't treat braces as magic by
default, so a pattern with alternates but no wildcard —
src/elements/accordion/index.{scss,sass,css} — was read as a literal file
path, and failed with Entry does not exist: naming a file that was never
going to exist. It now resolves as the glob it obviously is, which is what
makes "whichever extension this component happens to use" expressible for a
single component and not only across a *.
The watcher learned the same thing. A brace pattern in a copy, images or
markup in is now matched as a glob when deciding whether a changed file
belongs to that task, instead of falling through to a path-segment compare
that could never match it.