v3.14.0
release: v3.14.0 - resolve consumer-graph package exports in the… (#299)
Slothlet v3.14.0 Changelog
Release Date: August 2026
Release Type: Minor
Branch: release/3.14.0
Overview
Version 3.14.0 extends the browser importmap generator so it resolves
the exact exports subpaths of every package in the consumer's module
graph, not just @cldmv/slothlet's own surface (#297).
generateBrowserAssets already emitted a complete importmap for
slothlet's own modules; other packages in the graph got only a prefix
map. Because import maps do plain prefix substitution and never consult
a package's exports, a subpath that exports redirects —
@scope/ext/errors where exports["./errors"] points at
./src/lib/errors.mjs — resolved to a literal …/ext/errors URL and
404'd in the browser, even though it loaded fine in Node. Consumers
worked around this by hand-maintaining per-dependency allowlists.
The generator now closes that gap by construction. No public API
signature changed and there are no breaking changes; the new behavior is
purely additive.
✨ Features
Browser importmap resolves consumer-graph package exports (#297)
generateBrowserAssets(apiDir, { slothletBase }) now covers a third
kind of module the page must resolve: the third-party packages the
registered API leaves import. As before, slothlet's own modules are
collected by the focused, unchanged collectSlothletSpecifiers and
rebased onto slothletBase. In addition, the generator scans the API
directory for the packages its leaves import, resolves each from the
consumer's node_modules tree, reads that package's package.json
exports, and emits the exact redirected subpath keys a prefix map
can never produce (@scope/ext/errors →
…/@scope/ext/src/lib/errors.mjs). Without those keys a redirected
subpath 404s in the browser; with them a consumer can delete its
hand-maintained browser-dependency allowlists.
A new exported primitive, collectPackageSpecifiers(packageRoot),
does the package-agnostic half of the work: given any package's root
directory it reads that package's exports and returns the
bare-specifier → relative-target pairs the importmap needs. It handles
the same shapes the self-collector does, generalized — the package root
(.), flat (non-wildcard) subpaths, wildcard directories (./x/* →
every module file under the declared target directory, including nested
files), and conditional exports (it selects the browser / import /
module / default condition and never a node/require-only target,
so a browser importmap can't be pointed at a CommonJS or Node-only
file). Only ES-module targets are emitted; a package with no exports
is left to the page's prefix map.
Discovery is driven by the registered API paths, so nothing new has to
be declared: the generator finds the packages the apiDir leaves
actually import. Sibling packages are served next to @cldmv/slothlet
under a base derived from slothletBase — its node_modules or CDN
parent (https://cdn/@cldmv/slothlet@3/ → https://cdn/), falling back
to the conventional /node_modules/ root when slothletBase isn't the
standard package layout — so no separate per-package configuration is
required for the common case. Every emitted entry is verified to exist
on disk, so a generated importmap never carries a URL that 404s by
construction. This also closes slothlet's own latent instance of the
same gap: the moment slothlet's browser source imports a third-party
subpath whose exports redirects, the generator now covers it. See
docs/BROWSER.md.
🧪 Tests
- #297 — a staged consumer fixture whose registered API leaf imports
a sibling extension package'sexports-redirected subpaths reproduces
the original 404 gap and asserts the exact keys now appear: flat
redirects, the package root (.), wildcard directories including nested
files, and the browser condition of a conditional export chosen over the
node one. Skip paths are covered too — a node-only condition, a target
missing on disk, and a non-module target are all omitted; relative,
node:, package-internal (#), and unresolvable-bare specifiers
contribute nothing; and the base is derived correctly for a
node_modules-at-root layout, a CDN layout, and the"/"fallback. A
direct unit suite exercisescollectPackageSpecifiersacross every
exportsshape — string sugar, conditions-only sugar, condition arrays,
themodulecondition, null/non-module skips, and present/absent
wildcard directories. - Full coverage gate green across node and browser arms.
📚 Documentation
- NEW: docs/changelog/v3/v3.14.0.md — this
changelog. - docs/BROWSER.md — the third kind of module the
importmap resolves: consumer-graph packageexportssubpaths, why a
prefix map 404s them, and how the sibling-package base is derived from
slothletBase. - README — refreshed What's New.
🔧 Dependencies
No dependency updates.
Upgrade notes
- No breaking changes. No public API signature changed.
generateBrowserAssetsaccepts the same arguments and its importmap
simply gains the exactexports-subpath keys for the third-party
packages your API leaves import. - You can delete hand-maintained browser-dependency allowlists. If a
consumer previously hand-listed a dependency's redirected subpaths (or
added per-file importmap entries) to keep them from 404'ing in the
browser, those entries are now produced automatically from the
dependency's ownexports— remove the workaround and regenerate. - Sibling packages are served under a base derived from
slothletBase. By default that isslothletBase'snode_modules/CDN
parent; whenslothletBaseis served at a non-standard location (e.g.
"/"), the generator falls back to the conventional/node_modules/
root. If your extension packages are served somewhere else, serve them
alongside@cldmv/slothletunder that derived root.
| Metric | Coverage |
|---|---|
| Statements | 100.0% |
| Branches | 100.0% |
| Functions | 100.0% |
| Lines | 100.0% |
Avg: 100.0% · 46f9cb5 · Node lts/*