Releases: stamat/poops
Release list
v3.0.0
A ```mermaid fence came out of the build as syntax-highlighted CSS. highlight.js has no mermaid grammar, so the fence fell through to auto-detection, which took graph TD; A-->B; for selectors and wrapped the diagram source in hljs-selector-tag spans. Mermaid could still read it — textContent drops the spans — but only if you told it to look at code.language-mermaid, and the page flashed the wrongly-coloured source first.
Added
- A
mermaidfence compiles to<pre class="mermaid">, with no<code>element and the diagram source escaped inside. That is the markup mermaid documents and the default itsrun()selects on, so loading mermaid on the page is all it takes. The fence info string keeps working:```mermaid wide tab=depsstill becomesclass="mermaid wide" data-tab="deps".
Changed
-
book-of-spells1.5 → 2.5,poops-docs-theme4.2 → 4.5. The one that shows:removeAccentsin book-of-spells now folds letters whose mark is written through the glyph —Đ Ł Ø Ħ Þ ßand friends — whichslugifyinherits, andslugifyis what Poops builds heading anchors, collection slugs and permalinks from.Đorđe Balaševićused to slug toore-balasevicbecause the unfolded letter was deleted outright; it isdorde-balasevicnow. A site with those letters in a heading or a title will move the URLs they generate — worth a redirect if anything links to the old ones. -
mermaidis never syntax-highlighted, through fences, the{% highlight %}tag and thehighlightfilter alike. Only the language name is special — a JavaScript fence that mentions mermaid is still highlighted as JavaScript.
Poops ships no mermaid and injects no script, so a page with no diagram carries no library, and a diagram on a page that never loads one shows its source as readable text. The trade against the code-fence story is real and stated in the README: highlighting happens at build time to avoid layout shift, and a browser-rendered diagram pops in after paint.
Full Changelog: v2.5.0...v3.0.0
v2.5.0
A page whose body was built from template tags described itself with their
source. # {{ site.title }} over {{ site.description }} shipped an
og:description reading "site.description" and an anchor at #site-title,
while the page itself rendered the real title and the real description right
underneath. The build stayed green through both, and the only symptom was a link
preview nobody looks at until someone shares the page.
Added
- A
descriptionfilter emits<meta name="description">, escaped.
{{ page | description(site) }}(Liquid:{{ page | description: site }})
reads the same chainogandjsonlddo — front matterdescription, then
the page's auto-excerpt, thensite.description— so the three cannot
disagree, and emits nothing when none of them is set. Poops renders with
autoescape off, so a layout writing that tag by hand ships the front matter
verbatim: one"in a sentence closes the attribute and truncates the
description to the words before it, silently, on a green build. Poops' own
v2.4.0 post described itself asAfor exactly that reason. Escaping now
happens in one named place rather than in every layout that remembers.
Fixed
-
page.excerptis taken after the template engine has run. A first
paragraph written as{{ site.description }}, or supplied by an
{% include %}ahead of the prose, is resolved before the text is taken, so
the description is the one a reader gets. The excerpt now reads the rendered
body's first<p>rather than guessing at the markdown source — headings,
comments, code fences and tables are skipped because they are not paragraphs,
and link text comes through without its URL. When nothing usable resolves the
excerpt is empty andog/jsonld/<meta name="description">fall through to
site.description— the old behaviour was to strip the braces and ship
site.descriptionas a literal string, which then beat the very value it was
standing in front of. Only pages whose first paragraph is a tag pay for the
extra render.A collection item is read without a page context, so its
excerptis empty in
that case rather than resolved; listings and feeds fall back to the item's
description. The item's own page still resolves it. -
Heading
ids are slugged from the rendered heading.# {{ site.title }},
# {{ page.title }}and# My Sitenow all anchor at#my-site, so swapping
which variable feeds a heading no longer remints every bookmarked URL. The
tocfilter slugs from the same text, so its links still land.
Full Changelog: v2.4.0...v2.5.0
v2.4.0
A "last updated" line under a post had two ways to get its date and both were
bad. Keep updated current by hand and it is one more thing to forget in every
edit. Read the file's modification time instead and git clone erases it — CI
checks out every file at once, so the whole site reads as edited today.
Added
markup.options.lastUpdatedgives every page anupdateddate, without
shelling out to git. Set it totrueand Poops keeps an index of content
hashes at.poops-updates.json(a string names a different file). A page's date
moves only when its body actually changes — front matter is outside the hash,
so retitling or retagging a post is not editing it — and every build after
that reads the same date back. Templates getpage.updatedlike any other
front-matter field; it also feedsdateModified,article:modified_timeand
the sitemap. A page carrying its ownupdatedkeeps it and stays out of the
index. The index file has to be committed, and a build has to run before
the commit — it is the only memory of when a page was edited, so a build says
Updated dates changed for 3 pages — commit .poops-updates.jsonwhenever it moves.
Fixed
- The sitemap's
<lastmod>now means last modified. It read front matter
date— a publication date — so a post revised for a year still advertised
itself to crawlers as untouched since the day it went up.updatedwins where
a page has one, withdateas the fallback. - A date front matter cannot fail the build. A
date(orupdated) that
is not a date reachedtoISOString()on an Invalid Date while the sitemap was
being written, and took the whole build down with aRangeErrorthat named no
page. The entry now goes out without a<lastmod>.
Full Changelog: v2.3.0...v2.4.0
v2.3.0
A footer wanting to print your version had nowhere to get it. poops.json is
JSON, so a value cannot compute one, and a theme rendering site.footer renders
it verbatim — which left the number hardcoded in a second file, silently wrong
from the next release on.
Added
-
exechooks can run before a stage, not only after it. A generator that has to write
before the stage reads — fetching posts the templates render, writing a data file — had
nowhere to go. Hijacking an earlier stage's hook worked in a build, where the pipeline order
put it first, and then silently stopped working in watch, because a markup-only edit never
fires the earlier stage. Every stage now also takespre:<stage>, fired in build and watch
alike:"pre:markup": "node script/fetch-posts.mjs".pre:buildruns once before the
pipeline starts, which is where clearingdistor fetching content belongs, andpre:styles
fires ahead of Sass — the pair brackets the whole style pipeline, sincestylesfires past
PostCSS.post:<stage>is accepted as the explicit spelling of a bare<stage>, so a config
can readpre:markupabovepost:markupinstead of above something that looks like a typo. -
markup.options.sitevalues are filled frompackage.json, with the same tokens a
banner uses."footer": "MyLib v{{ version }} — MIT licensed"now prints the version
the manifest holds, filled at build time from the one place it lives.{{ name }},
{{ homepage }},{{ license }},{{ author }},{{ description }}and{{ year }}
fill the same way, at any depth — atitleinsidesite.linksgets them too — and the
pkgoption still says whichpackage.jsonto read. -
A page can join a sidebar section without moving. The nav tree is built from urls, so
grouping pages that already sit side by side meant a subdirectory — and a subdirectory
changes their url, every link pointing at them and every search result that has been
indexed, to buy one heading in a sidebar.navGroup: "No APG pattern"in front matter
synthesizes that section beside the page's ungrouped siblings and nests the page inside it,
url untouched. The label is used as written rather than humanized, the section sorts where
its first child would, and grouping is per directory — the same name in two directories
opens a section in each, instead of one reaching across the tree. A page that is itself a
section (a directory's index page, or the homepage) is refused with a warning: its
subpages would stay behind on the ungrouped path and split the section in two.
Fixed
-
A banner token with no matching
package.jsonfield no longer printsundefined.
{{ homepage }}in a package that declares none put that literal word at the top of
every built file, which reads as Poops being broken rather than as a field you never
filled in. The token is now left as written, naming what is missing. -
A PostCSS pass no longer leaves a stale source map beside its output. Poops wrote the
processed CSS and threw away the map PostCSS had composed for it, so whatever map the Sass
stage left on disk stayed there describing lines the pass had since moved — a debugger
pointing confidently at the wrong rule. The composed map is now written next to the output,
shifted past the banner line like every other stage. An input with no map still produces none.
Full Changelog: v2.2.0...v2.3.0
v2.2.0
A companion's block passed in silence only when you depended on the companion by
name, which a package that arrives through another one never is. Separately, the
permalink anchor beside every heading was hidden from screen readers and still
reachable by keyboard — a tab stop with nothing to announce, once per heading,
on every page Poops has built.
Added
- A direct dependency can vouch for a companion's config key. A
septicblock
passed in silence only whensepticitself sat in your package.json — but laxative
brings septic, so a laxative app declareslaxativeand the warning fired on every
build. A dependency now vouches through its own manifest,
"poops": { "companionKeys": ["septic"] }, read one directory deep and never loaded.
Nothing declared and nothing vouched still warns, which is what catches the typo.
Fixed
-
The heading permalink is no longer a tab stop on nothing. Every heading gets an
anchor, and it carriedaria-hidden="true"while staying focusable — so a keyboard
reached it, and a screen reader had nothing to announce when it landed. That is a link
with no accessible name in the tab order, once per heading, on every page Poops has ever
built. It also carriedaria-label="Permalink", whicharia-hiddenhad already made
unreadable to anyone.The anchor now says the same thing three ways instead of three different things: it is
decoration, so it takestabindex="-1"beside thearia-hiddenit always had, and the
deadaria-labelis gone. The heading beside it is already the name of the place.
DOM change:<a class="heading-anchor" href="#id" tabindex="-1" aria-hidden="true">,
where it wasaria-label="Permalink" aria-hidden="true". Nothing changes for a mouse —
the "#" a theme reveals through.heading-anchor::beforestill clicks — and a theme
styling[aria-label="Permalink"]rather than the class is the one selector this breaks. -
Config Poops has always accepted is finally written down.
exechad no section in
the README at all, only two passing mentions of a key nothing introduced.watch: true,
the per-entrynodePaths,markup.options.autoescapeanddateFormat, thefeed's
content, thetocfilter and the banner's{{ year }}were each real, tested and
undocumented — some in the README, some on the docs site,nodePathsand{{ year }}
in neither. Two engine-contract listings also still advertisedfileExtensionand
renderString, which the pipeline stopped calling, while omitting theinvalidate/
pagesDependingOnpair that incremental rebuilds actually run on. Nothing changed in the
code; the pages now say what it does. A new test walks every link in the README and the
documentation site, so the eleven dead ones found writing this are the last of them.
Full Changelog: v2.1.0...v2.2.0
v2.1.0
The config file now tells your editor what belongs in it — and tells Poops,
which no longer lets a key misspelt inside a block pass in silence. It also
stops calling a companion package's block a mistake. Separately, llms was the one index
feature that could not turn itself on, and the corpus it writes carried the
machinery of the pages it was made from.
Added
-
A JSON Schema for
poops.json, so the editor catches a typo you would
otherwise find in the output. A top-level"stlyes"was warned about and
ignored, and a mistyped script option reaches esbuild, which rejects it — but
"minfiy"in a style's options is read by nothing and warned about by nobody.
The build stays green and the.min.cssis never written.Poops now ships
schema/poops.schema.json, covering every key, with the
documentation for each one inline. Point$schemaat it and the editor
completes and validates as you type:{ "$schema": "./node_modules/poops/schema/poops.schema.json" }The same file is published at
https://stamat.info/poops/poops.schema.json
for a project that has not installed Poops yet, and can be attached by
filename from VS Code'sjson.schemassetting instead of by editing the
config. The$schemakey is inert to Poops — it is recognised and otherwise
ignored, and nothing was added to what Poops installs into your project.The schema is hand-written, so Poops' own test suite holds it to the code: it
is validated against the draft-07 meta-schema, andpoops.jsonplus every
complete config example in the README and on the documentation site is
validated against it, so an example that stops being valid config now fails
the build. -
A key misspelt inside a block is now named too. The unknown-key warning
stopped at the top level:"stlyes"was caught,"inn"in a styles entry was
not. That entry compiled nothing,poops -bexited 0, and the file that never
appeared was the only sign. Poops now checks every block it owns against the
schema it ships, at startup:[info][warn] Unknown key "inn" in styles[0] — ignored. Valid: in, out, optionsKey names only, and only where Poops owns them:
imagesbelongs to
poops-images,siteis yours to name, and a companion's top-level block is
left alone — an unrecognised key in any of those passes without comment.
Types are not checked, so"minify": "yes"still reaches the compiler and
fails there.execkeeps its own warning, which says the more useful thing —
that the stage never runs. The schema read is the copy inside
node_modules/poops, so where$schemapoints changes nothing.The walk is unknown-keys, a new
dependency and the only one this release adds — zero dependencies of its own,
written for this and published separately because poops-images and septic read
the same file and want the same warning.
Changed
-
A top-level key naming a package you depend on is no longer called
unknown.poops.jsonis shared — septic
reads asepticblock out of the same file — but every build printed
Unknown config key "septic" — ignored, a warning about something working
exactly as designed. The name is now checked against yourdependencies,
devDependencies,peerDependenciesandoptionalDependenciesfirst, and a
match passes in silence. Declaring the package is enough; Poops still never
loads it or reads its block. Nothing by that name declared, and the warning
is unchanged — which is what still catches"stlyes".Your editor cannot see
node_modules, so the schema cannot make that call: it
allows an object under any name it does not know and rejects everything else,
meaning"stlyes": [ … ]is flagged there but"srve": { … }is not. The CLI
catches what the editor lets through.
Fixed
-
llmsalone now generates its files. Page entries were collected only
whensearchIndex,sitemap,navorfeedwas configured, so a markup
config whose only index feature wasllmscompiled the site and wrote
nothing — no warning, no output. Pairing it with a sitemap was the
workaround; there is nothing to pair it with now. -
llms-full.txtno longer carries a page's machinery. The corpus is built
from the Markdown source, which is read before the template engine runs, so it
held whatever the source held: template comments, tags and output expressions,
and every inline<style>and<script>on the page. A page that wraps its
body in a{% set body %}…{% endset %}capture and styles itself in a
<style>block could hand an LLM more plumbing than prose. All of it is
stripped now, and the prose a capture wrapped stays. Fenced blocks, inline
code spans and{% raw %}bodies are left alone — a sample documenting
template syntax is content. A feed's article HTML is built from the same
stripped source.
What's Changed
- Bump the actions group across 1 directory with 4 updates by @dependabot[bot] in #52
Full Changelog: v2.0.0...v2.1.0
v2.0.0
Poops 2.0 has no new features. It is the release where the core loop — watch,
build, reload — stops carrying old dependencies and old spellings, and where the
compat shims that survived the whole 1.x line finally go.
Changed
-
Poops now requires Node.js 22 or newer. Node 20 reached end of life in
April 2026, and the dependencies the dev loop is built on — the file watcher,
esbuild — assume a modern runtime anyway. CI tests on Node 22 and 24, on both
Ubuntu and Windows. -
Live reload is now served by Poops itself, on the server port. The
livereloadpackage is gone, and with it a second port, an old websocket
stack, and the snippet you had to paste into your templates."livereload": truealongside"serve"is all it takes. Poops answers
/__poops_reloadas a server-sent events stream and appends a small client
script to each HTML page it serves — appends it to the response, so nothing
lands in your build output. Behaviour is unchanged otherwise: one reload per
save once the build settles, CSS-only builds swap stylesheets in place
instead of reloading, and the browser reconnects by itself after a restart.Migrating: delete the
livereload.jssnippet from your templates (a leftover
one is harmless — it will 404 quietly), and droplivereload.port,
livereload.exclude,livereload.extraExtsandlivereload.extsfrom your
config. The last three had already stopped doing anything when the reload
server stopped watching files in 1.5.1. -
The file watcher moved from chokidar 3 to chokidar 5. Two majors of
watcher fixes, and one fewer legacy dependency tree undernode_modules.
Nothing in your config changes:watchhas always been a list of directories,
never globs, so chokidar 4 dropping glob support costs nothing here. Watch
mode behaves as before — one rebuild per save, CSS still hot-swaps, deletions
still remove their output. -
esbuild moved from 0.25 to 0.28, and the default
targetfromes2019to
es2020. esbuild is pre-1.0, so its minor bumps shift output by design;
absorbing that is what a major of ours is for. Expect small differences in
your bundles — the CommonJS interop helper is more careful around a throwing
module, andSymbol.forcalls are annotated as side-effect free, which makes
minified output slightly smaller.The new default target is the visible half: optional chaining (
?.) and
nullish coalescing (??) are ES2020, and compiling them down for browsers
that have shipped them since 2020 only made bundles bigger. Entries that set
their owntargetare untouched. -
timeDateFormatis nowdateFormat. It sets the default format for the
datefilter; the old name said the same thing twice. Deprecated and still
read through 2.x, gone in 3.0.Engines see the rename too:
registerFilters({ timeDateFormat, markupOut })
is nowregisterFilters({ dateFormat, markupOut }). This is a hard rename —
it lands before the engine interface becomes public API in this same release,
so a custom engine must update its parameter name for 2.0. -
outputis nowoutin the markup sub-features.llms,nav,feed,
searchIndex,sitemapandrobotsname their output file without, the
same word every entry in the config already uses:{ "nav": { "out": "nav.json", "root": "docs" } }outputis deprecated but still honoured through 2.x, warning once per
feature, and stops working in 3.0. The string shorthand ("nav": "nav.json")
is unaffected. -
Markup settings belong under
markup.options.markupnow has the shape
every other entry has —in,out, and everything else inoptions:{ "markup": { "in": "src/markup", "out": "dist", "options": { "engine": "nunjucks", "site": { "title": "My Site" }, "searchIndex": "search-index.json" } } }1.x read
engine,site,data,includePaths,timeDateFormat,
collections,searchIndex,sitemap,llms,robots,feed,nav,
baseURLandautoescapeat themarkup.level as well. That placement is
deprecated but still honoured through 2.x — each stray key logs a warning
naming its new home — and stops working in 3.0. Where both are set,options
wins.
Added
-
The markup engine interface is public API as of 2.0.
markup.options.engine
has always accepted any importable module, and
poops-shopify ships a production
engine against it — but nothing said the interface was stable, so a rename in
a patch release could have broken it silently. It is documented in
the engine API reference, semver applies to it from here on,
and a contract test asserts both builtin engines still expose the shape — a
failing test is a breaking change caught before it ships. -
serve.basedefaults to the markupoutdirectory. Nearly every config
set it to the path it had just built into. Set it only when the server should
serve somewhere else; an explicit value still wins, and a project with no
markupstill serves the working directory.
Removed
-
The
ssgconfig key. It has been an alias forreactorsince the rename,
and it was the last compatibility shim in the codebase. A config still using
it now gets told what to call it instead:[info] Config key "ssg" is renamed to "reactor" in 2.0 — ignored.
Internal
- Dependency bumps.
book-of-spells1.3 → 1.4 and apostcsspatch, both
routine. Dev-only:neostandard0.12 → 0.13, andsulphuris2 → 4 for the
example site's styles. ESLint stays on 9 — neostandard 0.13 still declares a
peer ofeslint@^9, so 10 has to wait for it.
Fixed
-
"Edit this page on GitHub" links work for collection items on Windows. A
collection item'sfilePathkept native separators while a regular page's was
posix, so on Windows the link for a post came out as
…/edit/main/src/posts\hello.md. Both are posix now. -
The image cache is read correctly across platforms. Its lookups all key on
posix paths, so a cache written on Windows — committed next to the images it
describes, or built in CI — matched nothing and galleries came up empty. Keys
and variant paths are normalized on read. The output-directory containment
check also no longer accepts a sibling directory whose name merely starts with
the output dir's (dist-oldfordist). -
A style edit hot-swaps the stylesheet the page actually links. With
minifyon, the reload chain was told aboutsite.csswhile the page linked
site.min.css— no stylesheet matched, so every style edit reloaded the whole
page instead of swapping the CSS. Both spellings are now reported (and with
justMinified, only the minified one, since the other is deleted).
Full Changelog: v1.9.8...v2.0.0
v1.9.8
A fence could say more than its language, but only the language survived to the HTML — anything marking a fence for a later stage had to be an HTML comment next to it in the Markdown. The rest of the info string now lands on the code element as classes and data attributes, and all six places that render a code block finally agree on what one looks like.
Added
-
The rest of a fence's info string becomes classes and
data-attributes.
1.9.7 stopped meta words leaking into the language class, which fixed the
highlighting bug but threw the words away. So a fence could be labelled and
nothing downstream could see the label — the marker for "this block is a live
demo" had to live beside the fence as an HTML comment, one thing to keep in
sync with another.Everything after the language now rides along: a bare word becomes a class, a
key=valuetoken becomes a data attribute.```html preview tab=options widths=375,768 <my-element></my-element> ```
<pre><code class="hljs language-html preview" data-tab="options" data-widths="375,768">…</code></pre>
A post-
markupexecscript then matchescode.previewand reads the
settings off the element, with nothing in the Markdown but the fence itself.
Values are single tokens — no quotes, no spaces — which keeps the parser a
split; anything needing a sentence belongs in the prose around the fence,
not in its opening line.A bare word is a class rather than a valueless attribute, since a class is
what the consumer selects on. When you do want the attribute — a flag read
withhasAttributeinstead of offclassList— write the key with nothing
after the=:```html preview expanded= <my-element></my-element> ```
<pre><code class="hljs language-html preview" data-expanded="">…</code></pre>
Backwards compatible in the strict sense: a single-token info string renders
exactly as before, which is every fence on every site today. Both the
{% highlight %}tag and thehighlightfilter take the
same info string.
Fixed
- All six renderers agree on what a code block is. The same three lines were
copy-pasted across the Markdown renderer, the standalone highlight renderer,
and the filter and tag in each engine — and 1.9.7 only fixed two of them. The
engine copies still interpolated the whole info string into the class
attribute, so they disagreed with the Markdown renderer about a fence's output
and were right aboutlanguage-html previewpurely by accident. One
codeBlockhelper now emits every code block, so there is one place left to
get wrong.
Full Changelog: v1.9.7...v1.9.8
v1.9.7
Added
-
site.jsonldsets a JSON-LD default for the whole site. Thejsonld
filter picksBlogPostingfor a dated page andWebPagefor everything else,
and ajsonldobject in front matter overrode any of it. That escape hatch was
per page, which is the wrong shape when the whole site is one type: a docs site
isTechArticle, a knowledge base isFAQPage, a product catalogue is
Product. The only way to say so was the same four lines of front matter in
every file — and the failure mode is quiet, since a page that misses them still
emits valid JSON-LD, just the generic type.The same object now works in your
sitedata:{% raw %}{ "markup": { "site": { "jsonld": { "@type": "TechArticle" } } } }{% endraw %}Every page renders as
TechArticle— no front matter, nothing to forget. It
isn't limited to@type; any key you would have set per page works site-wide,
which is where the fields that genuinely don't vary belong:{% raw %}"jsonld": { "@type": "TechArticle", "license": "https://opensource.org/licenses/MIT", "isAccessibleForFree": true }{% endraw %}Precedence is defaults →
site.jsonld→page.jsonld, so a single page still
opts out of the site-wide type while keeping the rest of it — aFAQPage
inside aTechArticlesite keeps thelicenseand only replaces@type:--- title: Frequently asked questions jsonld: "@type": FAQPage ---
Two things it deliberately doesn't do. A site-wide
@typebeats the
auto-detectedBlogPostingas well — it's a default you set, not one poops
guessed — so on a site that mixes docs with a blog, set the type per page
rather than site-wide, or the posts stop being articles. And it merges into
the page's own block only: theWebSiteblock on the homepage and the
auto-appendedBreadcrumbListon nested pages are structural, and a site-wide
@typehas no business rewriting them.Same shallow merge as the front-matter object, for the same reason — nested
schema is rare, and the escape hatch is meant for whole-key replacement.
Full Changelog: v1.9.6...v1.9.7
v1.9.6
Added
-
outcan be a template, for styles and scripts alike.v1.9.5named a
glob-matchedindex.*after its directory, which fixed the case a component
library actually hits. It did nothing for anything else: point a glob at
src/elements/*/theme.scssand every match still falls back to its own
basename, so the components overwrite each other in the output directory and
the last one to build wins. There was no way to say what you wanted the files
called.Now there is. An
outcarrying{% raw %}{{dir}}{% endraw %}or
{% raw %}{{name}}{% endraw %}resolves per entry point instead of naming one
shared destination:{% raw %}{ "styles": { "in": "src/elements/*/theme.scss", "out": "dist/css/{{dir}}-{{name}}.css" }, "scripts": { "in": "src/elements/*/widget.ts", "out": "dist/js/{{dir}}-{{name}}.js" } }{% endraw %}src/elements/accordion/theme.scss → dist/css/accordion-theme.css src/elements/tabs/theme.scss → dist/css/tabs-theme.css src/elements/accordion/widget.ts → dist/js/accordion-widget.js src/elements/tabs/widget.ts → dist/js/tabs-widget.js{% raw %}{{dir}}{% endraw %}is the match's directory relative to the glob's
static prefix — the same name anindex.*entry gets, so the two rules
agree on what a component is called, and widening the glob keeps the segments
it no longer pins down.{% raw %}{{name}}{% endraw %}is the basename
without its extension. Whitespace inside the braces is fine
({% raw %}{{ dir }}{% endraw %}), matching thebannertemplates.Tokens work in directory segments too, so the flat layout isn't the only one
available:{% raw %}{ "in": "src/elements/*/theme.scss", "out": "dist/css/{{dir}}/theme.css" }{% endraw %}A literal entry fills
{% raw %}{{dir}}{% endraw %}with its own directory
name, which keeps mixed arrays of globs and plain paths working:{% raw %}"out": "dist/css/{{dir}}.css"{% endraw %} src/scss/main.scss → dist/css/scss.css src/elements/accordion/index.scss → dist/css/accordion.cssFor scripts the template's extension is honoured as well, which is the cheap
way to ship one format per entry point rather than one bundle per format:{% raw %}{ "in": "src/elements/*/index.ts", "out": "dist/esm/{{dir}}.mjs", "options": { "format": "esm" } }{% endraw %}A template wins over the
index.*rename — you named these outputs, and
renaming them behind your back is the thing globs were already doing wrong.
It's also exempt from the "more than one entry file needs a directoryout"
guard, since a template already resolves to a different file per entry rather
than to one file everything overwrites. What it does not do is invent
uniqueness:{% raw %}"out": "dist/{{name}}.css"{% endraw %}across component
directories collides exactly like a plain directoryoutwould, and its
scripts equivalent fails the build outright with esbuild's "Two output files
share the same path". That's what{% raw %}{{dir}}{% endraw %}is there
for.
Fixed
-
Live CSS reload no longer guesses the output path. The watch chain fed
livereload a path derived from the config — a directoryoutjoined with the
basename ofin. For a single named entry point that guess was right. For a
glob it was the pattern's own basename, sosrc/elements/*/index.scss
reporteddist/css/index.css, a file that was never written; a templated
outreported the template verbatim, braces and all. The livereload client
looks for a loaded stylesheet matching the path it is handed and, finding
none, falls back to reloading the whole page — so editing a component's Sass
flashed a full reload instead of swapping the stylesheet in place, and any
scroll position or open state went with it.The styles compiler now records the files it wrote and the watch chain
reloads exactly those. One entry, one glob or twenty templated outputs, the
reported paths are the ones on disk, because nothing derives them a second
time. -
The watcher's output zones understand templates. A compiler writing into
a watched directory must not retrigger itself, which the watcher prevents by
zoning each task'sout. A templatedoutis only a fixed path up to its
first token, sodist/{{dir}}/theme.csszoned a directory literally named
{{dir}}, protecting nothing. Zones are now taken from the static prefix.
Full Changelog: v1.9.5...v1.9.6