Conversation
…d importmap generateBrowserAssets produced a correct importmap for @cldmv/slothlet's own surface, but every other package in the consumer's browser graph got only a prefix map. Import maps do plain prefix substitution and never consult a package's `exports`, so a subpath the exports map redirects (@scope/ext/errors -> ./src/lib/errors.mjs) resolved to a literal URL and 404'd in the browser — forcing consumers to hand-maintain allowlists. Add a package-agnostic collector (collectPackageSpecifiers) that reads any package's `exports` and emits the exact redirected subpath keys a prefix map can't produce — flat entries, wildcard directories, and the browser/import/ default condition of conditional exports. Wire generateBrowserAssets to scan the apiDir leaves for the packages they import, resolve each from the consumer node_modules tree, expand it, and merge the exact keys into the importmap; sibling packages are served under a base derived from slothletBase. Every entry is verified on disk so the map never carries a 404-by-construction URL. collectSlothletSpecifiers is kept exactly as-is. Fixes #297
🔒 Dependency Review
|
Contributor
Author
|
| File | Raw | Δ Raw | Gzipped | Δ Gzipped |
|---|---|---|---|---|
| dist/lib/builders/api-assignment.mjs | 15.5 kB | — | 3.4 kB | — |
| dist/lib/builders/api_builder.mjs | 48.8 kB | — | 9.9 kB | — |
| dist/lib/builders/builder.mjs | 2.7 kB | — | 1.2 kB | — |
| dist/lib/builders/modes-processor.mjs | 40.6 kB | — | 6.9 kB | — |
| dist/lib/errors.mjs | 4.4 kB | — | 1.7 kB | — |
| dist/lib/factories/component-base.mjs | 1.7 kB | — | 866 B | — |
| dist/lib/factories/context.mjs | 1010 B | — | 525 B | — |
| dist/lib/handlers/api-cache-manager.mjs | 3.4 kB | — | 1.4 kB | — |
| dist/lib/handlers/api-manager.mjs | 56.2 kB | — | 11.7 kB | — |
| dist/lib/handlers/context-async.mjs | 4.0 kB | — | 1.4 kB | — |
| dist/lib/handlers/context-live.mjs | 7.0 kB | — | 2.2 kB | — |
| dist/lib/handlers/framework-internals.mjs | 1.1 kB | — | 611 B | — |
| dist/lib/handlers/hook-manager.mjs | 19.6 kB | — | 4.9 kB | — |
| dist/lib/handlers/lifecycle-token.mjs | 959 B | — | 540 B | — |
| dist/lib/handlers/lifecycle.mjs | 2.2 kB | — | 1010 B | — |
| dist/lib/handlers/materialize-manager.mjs | 1.2 kB | — | 672 B | — |
| dist/lib/handlers/metadata.mjs | 12.2 kB | — | 3.2 kB | — |
| dist/lib/handlers/module-manager.mjs | 9.3 kB | — | 3.0 kB | — |
| dist/lib/handlers/ownership.mjs | 6.3 kB | — | 2.1 kB | — |
| dist/lib/handlers/permission-manager.mjs | 15.9 kB | — | 4.0 kB | — |
| dist/lib/handlers/trusted-root.mjs | 3.1 kB | — | 1.2 kB | — |
| dist/lib/handlers/unified-wrapper.mjs | 74.6 kB | — | 13.2 kB | — |
| dist/lib/handlers/version-manager.mjs | 14.4 kB | — | 3.7 kB | — |
| dist/lib/helpers/caller-pinning.mjs | 804 B | — | 496 B | — |
| dist/lib/helpers/class-instance-wrapper.mjs | 2.7 kB | — | 1.1 kB | — |
| dist/lib/helpers/config.mjs | 14.5 kB | — | 3.8 kB | — |
| dist/lib/helpers/eventemitter-context.mjs | 9.0 kB | — | 1.8 kB | — |
| dist/lib/helpers/eventtarget-context.mjs | 3.3 kB | — | 1.2 kB | — |
| dist/lib/helpers/generate-manifest.mjs | 10.6 kB | +3.8 kB (+55.8%) |
3.3 kB | +944 B |
| dist/lib/helpers/hint-detector.mjs | 1.3 kB | — | 756 B | — |
| dist/lib/helpers/manifest-resolver.mjs | 1004 B | — | 611 B | — |
| dist/lib/helpers/modes-utils.mjs | 1.1 kB | — | 696 B | — |
| dist/lib/helpers/module-discovery.mjs | 7.5 kB | — | 2.5 kB | — |
| dist/lib/helpers/module-manifest-validator.mjs | 8.7 kB | — | 1.9 kB | — |
| dist/lib/helpers/module-sort.mjs | 1.0 kB | — | 593 B | — |
| dist/lib/helpers/pattern-matcher.mjs | 2.6 kB | — | 1.2 kB | — |
| dist/lib/helpers/platform.mjs | 1.8 kB | — | 953 B | — |
| dist/lib/helpers/resolve-from-caller.mjs | 3.0 kB | — | 1.3 kB | — |
| dist/lib/helpers/sanitize.mjs | 7.7 kB | — | 2.3 kB | — |
| dist/lib/helpers/scheduler-context.mjs | 1.9 kB | — | 876 B | — |
| dist/lib/helpers/utilities.mjs | 1.7 kB | — | 909 B | — |
| dist/lib/i18n/languages/de-de.json | 64.3 kB | — | 18.3 kB | — |
| dist/lib/i18n/languages/en-gb.json | 56.7 kB | — | 16.0 kB | — |
| dist/lib/i18n/languages/en-us.json | 56.7 kB | — | 16.0 kB | — |
| dist/lib/i18n/languages/es-es.json | 63.8 kB | — | 17.8 kB | — |
| dist/lib/i18n/languages/es-mx.json | 63.8 kB | — | 17.8 kB | — |
| dist/lib/i18n/languages/fr-fr.json | 64.8 kB | — | 17.8 kB | — |
| dist/lib/i18n/languages/hi-in.json | 107.0 kB | — | 20.1 kB | — |
| dist/lib/i18n/languages/ja-jp.json | 76.3 kB | — | 18.8 kB | — |
| dist/lib/i18n/languages/ko-kr.json | 66.1 kB | — | 17.8 kB | — |
| dist/lib/i18n/languages/pt-br.json | 63.1 kB | — | 17.5 kB | — |
| dist/lib/i18n/languages/ru-ru.json | 77.8 kB | — | 18.9 kB | — |
| dist/lib/i18n/languages/zh-cn.json | 55.6 kB | — | 17.4 kB | — |
| dist/lib/i18n/translations.mjs | 5.0 kB | — | 1.8 kB | — |
| dist/lib/modes/eager.mjs | 2.0 kB | — | 1006 B | — |
| dist/lib/modes/lazy.mjs | 2.5 kB | — | 1.2 kB | — |
| dist/lib/processors/flatten.mjs | 9.9 kB | — | 2.4 kB | — |
| dist/lib/processors/loader.mjs | 12.3 kB | — | 4.0 kB | — |
| dist/lib/processors/type-generator.mjs | 6.7 kB | — | 2.6 kB | — |
| dist/lib/processors/typescript.mjs | 10.7 kB | — | 4.0 kB | — |
| dist/lib/runtime/runtime-asynclocalstorage.mjs | 3.0 kB | — | 1.1 kB | — |
| dist/lib/runtime/runtime-livebindings.mjs | 3.1 kB | — | 1.1 kB | — |
| dist/lib/runtime/runtime.mjs | 2.2 kB | — | 800 B | — |
| dist/lib/typegen/typegen.mjs | 2.1 kB | — | 1.0 kB | — |
| dist/slothlet.mjs | 17.9 kB | — | 5.1 kB | — |
| Total | 1.29 MB | +3.8 kB | 345.6 kB | +944 B |
📊 Generated by bundle-size. Brotli sizes also measured but omitted from the table for brevity.
…-manifest (#297) Close the full-coverage gaps the merged feature left in the new browser importmap collector: a bare "@scope" specifier with no package segment and a non-module file in the scanned apiDir, plus collectPackageSpecifiers shapes — an array whose only element resolves to null, a non-object exports value, a condition present but null (falls through to the next), a non-subpath key alongside subpath keys, a wildcard with no browser target, and the "./*" root wildcard. Restores the 100% coverage gate. No source change.
Document the browser-importmap consumer-graph exports feature (#297): the v3.14.0 minor changelog, and the README What's New Latest block bumped to v3.14.0 with v3.13.3 moved into Recent Releases (kept at four entries).
…#297) (#300) ## 🚀 What's Changed ### 💥 Breaking Changes _No breaking changes_ ### ✨ Features _No new features_ ### 🐛 Bug Fixes _No bug fixes_ ### 📦 Dependencies _No dependency updates_ ### 🔧 Other Changes - docs: add v3.14.0 changelog and refresh README What's New (#300) (6413344) - test(browser): cover remaining exports-collector branches in generate-manifest (#297) (6c7e3b3) <details> <summary>👥 Contributors</summary> - @Shinrai </details>
analyze-coverage.mjs only walked the statement (`s`) and branch (`b`) maps, so
it was blind to a never-called function whose body carries no statements of its
own — e.g. an empty `() => {}` callback. v8's `f`/`fnMap` is the only signal for
that case, and CI's coverage-summary.json counts it, so the local report read
"all clear" while the badge showed <100% functions. Add a functions pass that
reports each uncovered function (name + declaration line + context), and update
the summary wording to cover statements, branches, and functions.
…ze catch
The empty `.catch(() => {})` at unified-wrapper.mjs:3417 (getTrap's background
materialization kick-off) was the one uncovered function repo-wide — its
statement runs, but no test made that background `_materialize()` reject, so
the callback never fired. It is reachable: construct a lazy wrapper whose
materializeFunc rejects, read a plain property to trigger the getTrap branch,
and assert the rejection is swallowed (no unhandledRejection leaks) rather than
surfacing — the exact contract that catch provides. Brings functions coverage
to 100% alongside the existing 100% statements/branches/lines.
Shinrai
approved these changes
Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slothlet v3.14.0 Changelog
Release Date: August 2026
Release Type: Minor
Branch:
release/3.14.0Overview
Version 3.14.0 extends the browser importmap generator so it resolves the exact
exportssubpaths of every package in the consumer's module graph, not just@cldmv/slothlet's own surface (#297).generateBrowserAssetsalready 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'sexports, a subpath thatexportsredirects —@scope/ext/errorswhereexports["./errors"]points at./src/lib/errors.mjs— resolved to a literal…/ext/errorsURL 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, unchangedcollectSlothletSpecifiersand rebased ontoslothletBase. In addition, the generator scans the API directory for the packages its leaves import, resolves each from the consumer'snode_modulestree, reads that package'spackage.jsonexports, 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'sexportsand 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 conditionalexports(it selects thebrowser/import/module/defaultcondition and never anode/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 noexportsis 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
apiDirleaves actually import. Sibling packages are served next to@cldmv/slothletunder a base derived fromslothletBase— itsnode_modulesor CDN parent (https://cdn/@cldmv/slothlet@3/→https://cdn/), falling back to the conventional/node_modules/root whenslothletBaseisn'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 whoseexportsredirects, the generator now covers it. See docs/BROWSER.md.🧪 Tests
exports-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 anode_modules-at-root layout, a CDN layout, and the"/"fallback. A direct unit suite exercisescollectPackageSpecifiersacross everyexportsshape — string sugar, conditions-only sugar, condition arrays, themodulecondition, null/non-module skips, and present/absent wildcard directories.📚 Documentation
exportssubpaths, why a prefix map 404s them, and how the sibling-package base is derived fromslothletBase.🔧 Dependencies
No dependency updates.
Upgrade notes
generateBrowserAssetsaccepts the same arguments and its importmap simply gains the exactexports-subpath keys for the third-party packages your API leaves import.exports— remove the workaround and regenerate.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.👥 Contributors
Avg: 100.0% ·
46f9cb5· Node lts/*