Skip to content

Add multi-entry self-hosted dependencies to eliminate singleton hazard - #74

Merged
titouanmathis merged 1 commit into
mainfrom
feat/multi-entry-dependencies
Aug 6, 2026
Merged

Add multi-entry self-hosted dependencies to eliminate singleton hazard#74
titouanmathis merged 1 commit into
mainfrom
feat/multi-entry-dependencies

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

Problem

The PlaygroundDependenciesPlugin bundles each self-hosted dependency as its own tsdown build (one entry → one bundle at static/deps/<specifier>/). When a consumer registers multiple subpaths of the same workspace package as separate dependencies — e.g. @studiometa/ui (barrel) and @studiometa/ui/manifest (lazy) — each build independently bundles the shared modules (the ui component classes).

Result: the barrel's Action class and the manifest-lazy-loaded Action class are different module instances → a singleton/identity hazard. js-toolkit's component registry is keyed by class/name, instanceof breaks, and a component imported via the barrel is not the one autoloaded via the manifest. js-toolkit itself is fine (externalized to a single esm.sh instance), but the workspace package's own modules get duplicated across its subpath bundles.

Fix

Add an entries map to DependencyConfig, keyed by export subpath (Node exports-style: ., ./manifest, …):

{
  specifier: '@studiometa/ui',
  source: '../ui/**/*.ts',            // optional, dev file-watching only
  entries: {
    '.': '../ui/index.ts',
    './manifest': '../ui/manifest.ts',
  },
}

All entries are built together in one code-split tsdown build. rolldown hoists the modules shared between entries into a single shared chunk referenced by every entry → one runtime instance, no singleton hazard.

How shared chunks resolve across entries

Each entry is emitted verbatim under the base static/deps/<specifier>/ directory using its rolldown filename:

  • .index.js (keeps the existing contract) + index.d.ts
  • ./manifestmanifest.js + manifest.d.ts
  • shared modules → greeter-<hash>.js (emitted once)

Because entries and shared chunks live in the same base dir and every chunk keeps the exact filename rolldown assigned, the entries' relative import / import() of the shared chunks resolve unchanged — no code rewriting. Import-map entries and _headers x-typescript-types lines are generated per subpath.

End-to-end proof (demo build)

The demo-lib config is switched to multi-entry (. + ./manifest, both using a shared greeter module). npm run demo:build emits:

static/deps/demo-lib/index.js         → import { t as r } from "./greeter-QtQN3lXm.js"
static/deps/demo-lib/manifest.js      → import("./greeter-QtQN3lXm.js")
static/deps/demo-lib/greeter-QtQN3lXm.js   (single shared chunk)
static/deps/demo-lib/{index,manifest}.d.ts

Import map: "demo-lib": "…/index.js", "demo-lib/manifest": "…/manifest.js". _headers has a line per subpath.

Backward compatibility

Single-entry { specifier, source, entry } and esm.sh (string / { specifier }) forms are unchanged. All existing tests remain green.

Tests

  • Singleton fix (real tsdown build): a fixture package with a static-import barrel + dynamic-import manifest sharing one module; asserts the shared module is emitted once, its body is not inlined into either entry, and both entries reference the same shared chunk.
  • Resolution unit tests (one import-map key per subpath, single self-hosted build, side-effect subpaths, non-local entry skip + warning).
  • publicPath-prefixing test covering every subpath of a multi-entry dependency.

135 tests pass; build and lint clean.

Release

Would ship as 0.3.12 (patch/minor — additive, backward-compatible). Not published here.

🤖 Generated with Claude Code

Registering several subpaths of the same workspace package as separate
dependencies bundled each subpath in its own tsdown build, so shared
modules (e.g. component classes referenced by both a barrel and a lazy
manifest) were duplicated across bundles into distinct module instances
— a singleton/identity hazard for class-keyed registries and instanceof.

Add an `entries` map to `DependencyConfig` keyed by export subpath
(`.`, `./manifest`, …). All entries of a package are now built together
in a single code-split tsdown build: shared modules become one shared
chunk referenced by every entry, giving a single runtime instance.

Each entry is emitted verbatim under the base `static/deps/<specifier>/`
directory (the `.` entry keeps the `index.js` name for back-compat, other
subpaths use `<name>.js`), so the entries' relative import/import() of the
shared chunks resolve unchanged with no rewriting. Import-map entries and
`_headers` x-typescript-types lines are generated per subpath.

Single-entry `{ specifier, source, entry }` and esm.sh forms are
unchanged. Covers the fix with a real tsdown build asserting the shared
module is emitted once and referenced by both a static-import barrel and a
dynamic-import manifest, plus resolution/prefixing unit tests, and a
multi-entry demo-lib config exercising the whole webpack pipeline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FQxYGj2RqgcP8BkubpiNu
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying studiometa-playground with  Cloudflare Pages  Cloudflare Pages

Latest commit: b7be27e
Status: ✅  Deploy successful!
Preview URL: https://a21f8055.studiometa-playground.pages.dev
Branch Preview URL: https://feat-multi-entry-dependencie.studiometa-playground.pages.dev

View logs

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Size Change: +1.28 kB (+2.51%)

Total Size: 52.1 kB

📦 View Changed
Filename Size Change
packages/playground/dist/lib/plugins/PlaygroundDependenciesPlugin.js 3.8 kB +934 B (+32.57%) 🚨
packages/playground/dist/lib/utils/resolve-dependencies.js 1.59 kB +343 B (+27.42%) 🚨
ℹ️ View Unchanged
Filename Size
packages/playground-preview/dist/element.js 10 kB
packages/playground-preview/dist/index.js 10 kB
packages/playground/dist/front/css/app.css 4.26 kB
packages/playground/dist/front/js/app.js 139 B
packages/playground/dist/front/js/components/Editor.js 929 B
packages/playground/dist/front/js/components/Editors.js 265 B
packages/playground/dist/front/js/components/EditorVisibility.js 343 B
packages/playground/dist/front/js/components/HeaderSwitcher.js 436 B
packages/playground/dist/front/js/components/HtmlEditor.js 748 B
packages/playground/dist/front/js/components/Iframe.js 1.74 kB
packages/playground/dist/front/js/components/IframeReloader.js 182 B
packages/playground/dist/front/js/components/LayoutReactive.js 616 B
packages/playground/dist/front/js/components/LayoutSwitcher.js 341 B
packages/playground/dist/front/js/components/Playground.js 1.14 kB
packages/playground/dist/front/js/components/Resizable.js 992 B
packages/playground/dist/front/js/components/ResizableCursor.js 298 B
packages/playground/dist/front/js/components/ResizableSync.js 512 B
packages/playground/dist/front/js/components/ScriptEditor.js 520 B
packages/playground/dist/front/js/components/StyleEditor.js 247 B
packages/playground/dist/front/js/components/Switcher.js 334 B
packages/playground/dist/front/js/components/ThemeSwitcher.js 456 B
packages/playground/dist/front/js/create-playground.js 196 B
packages/playground/dist/front/js/loaders/html.js 124 B
packages/playground/dist/front/js/loaders/script.js 124 B
packages/playground/dist/front/js/loaders/style.js 124 B
packages/playground/dist/front/js/store/config.js 468 B
packages/playground/dist/front/js/store/content.js 400 B
packages/playground/dist/front/js/store/header.js 573 B
packages/playground/dist/front/js/store/index.js 109 B
packages/playground/dist/front/js/store/layout.js 606 B
packages/playground/dist/front/js/store/theme.js 516 B
packages/playground/dist/front/js/utils/js/index.js 436 B
packages/playground/dist/front/js/utils/monaco.js 844 B
packages/playground/dist/front/js/utils/patch-iframe-url.js 362 B
packages/playground/dist/front/js/utils/resolve-import-map-urls.js 246 B
packages/playground/dist/front/js/utils/storage/AbstractStorageProvider.js 138 B
packages/playground/dist/front/js/utils/storage/FallbackStorageProvider.js 266 B
packages/playground/dist/front/js/utils/storage/index.js 479 B
packages/playground/dist/front/js/utils/storage/LocalStorageProvider.js 254 B
packages/playground/dist/front/js/utils/storage/MemoryStorageProvider.js 206 B
packages/playground/dist/front/js/utils/storage/MultiStorageProvider.js 307 B
packages/playground/dist/front/js/utils/storage/StorageProviderInterface.js 73 B
packages/playground/dist/front/js/utils/storage/SyncedStorageProvider.js 292 B
packages/playground/dist/front/js/utils/storage/URLStorageProvider.js 365 B
packages/playground/dist/front/js/utils/storage/WatchableStore.js 307 B
packages/playground/dist/front/js/utils/storage/ZipStorageProvider.js 271 B
packages/playground/dist/front/js/utils/twig/index.js 435 B
packages/playground/dist/index.js 154 B
packages/playground/dist/lib/plugins/PlaygroundLoadersPlugin.js 424 B
packages/playground/dist/lib/presets/html-webpack-script-type-module.js 288 B
packages/playground/dist/lib/presets/playground.js 1.07 kB
packages/playground/dist/lib/presets/production-build.js 261 B
packages/playground/dist/lib/tailwind-config.js 509 B
packages/playground/dist/lib/utils/resolve-public-path.js 283 B
packages/playground/dist/lib/utils/zip.js 310 B
packages/playground/dist/preset.js 190 B
packages/playground/dist/tailwind.js 113 B

compressed-size-action

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.54545% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.96%. Comparing base (68e5b9a) to head (b7be27e).

Files with missing lines Patch % Lines
...nd/src/lib/plugins/PlaygroundDependenciesPlugin.ts 66.66% 10 Missing and 3 partials ⚠️
...s/playground/src/lib/utils/resolve-dependencies.ts 93.75% 0 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (74.54%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #74      +/-   ##
==========================================
+ Coverage   27.89%   29.96%   +2.06%     
==========================================
  Files          55       55              
  Lines         993     1038      +45     
  Branches      190      203      +13     
==========================================
+ Hits          277      311      +34     
- Misses        702      709       +7     
- Partials       14       18       +4     
Flag Coverage Δ
playground 29.96% <74.54%> (+2.06%) ⬆️
playground-preview 29.96% <74.54%> (+2.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@titouanmathis
titouanmathis merged commit 7b5c815 into main Aug 6, 2026
7 of 8 checks passed
@titouanmathis
titouanmathis deleted the feat/multi-entry-dependencies branch August 6, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant