feat(library): add a fully-external build entry and externalize Yjs#782
Conversation
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche The build change itself checks out locally: library build and size passed, and the generated artifacts keep Yjs imports external. I am requesting changes because the public install docs still promise a zero-peer standalone setup, which is no longer true with these required Yjs peers.
The standalone `.` build inlined yjs and y-protocols, so hosts that already use Yjs (e.g. Artemis, which runs a Yjs-backed collaborative code editor) shipped two Yjs copies at different versions — wasted payload plus a cross-instance correctness hazard, since Yjs relies on module-level singletons (instanceof checks, shared Doc/transaction state). Externalize yjs and y-protocols in both the default and peer build passes (mirroring how React/MUI are already externalized) and promote them to required peerDependencies, so the host provides exactly one Yjs instance. Also enable build source maps so the dependencies Apollon still inlines (React, MUI, @dnd-kit, zustand, …) stay attributable in consumers' bundle analyzers and SBOM tooling. Update the README and docs (install pages, overview, quickstart, Angular guide, landing page) to document yjs/y-protocols as required peers of both builds — the standalone build is no longer "zero peer deps". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5c13f18 to
66427fd
Compare
The README and docs documented the React-family peers as React 18.3 (`react`/`react-dom` `^18.3.0`, `@emotion/*` `^11.11.0`, `@xyflow/react` `^12.3.0`), but the library is React-19-only: `library/package.json` declares `react`/`react-dom` `^19.0.0`, `@emotion/*` `^11.12.0`, `@xyflow/react` `^12.9.0`, and the React Compiler targets 19. Align the peer tables and prose in the README and the docs install/overview pages to the actual `peerDependencies` ranges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a third build pass (LIB_EXTERNAL) exposing `@tumaet/apollon/external`: the same imperative ApollonEditor API as the default entry, but with EVERY runtime dependency external (the React family plus @dnd-kit/zustand/uuid/ @chenglou/pretext; yjs/y-protocols are already external). A bundler host of any framework then resolves, de-duplicates, and gets full SBOM attribution for each dependency from its own node_modules, instead of a copy inlined invisibly into Apollon's bundle. This is what actually delivers SBOM visibility and single-copy guarantees to bundler hosts like Artemis; source maps could not — a consumer with `sourceMap.vendor: false` ignores vendor maps, and inlined deps that aren't in the consumer's dependency tree would break its SBOM filter. So the earlier `sourcemap: true` change is dropped (it added ~12 MB to the tarball for no benefit here). The default `@tumaet/apollon` (self-contained) and `/react` entries are unchanged. README + docs updated; changeset covers the new entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche The npm install docs are mostly updated now, but the CDN/vanilla path is still missing the peer-resolution guidance from the original thread. Leaving this as changes requested until those no-bundler snippets explain or make explicit how yjs and y-protocols resolve.
|
Refreshed the title and description to match where this landed. TL;DR — two packaging changes for hosts that embed Apollon with a bundler: Source maps were dropped on purpose — they don't deliver the SBOM goal (a consumer with Action required for consumers: the default entry is no longer zero-peer — install CI status (transparent): everything is green except For Artemis (separate, host-side): import |
The root README's "Use the library" snippet still showed a bare `npm install @tumaet/apollon`, and the CDN/no-bundler examples (the docs Vanilla page and the published library README) imported Apollon from esm.sh without resolving its now-required Yjs peers. Add `yjs`/`y-protocols` to the root install command, and give the CDN path an import map plus `?external=yjs,y-protocols` so the page shares a single Yjs instance instead of silently loading a second copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Pushed
Docs-only, so no changeset. The three other entries' install instructions and the peer tables were already correct. |
The previous commit's CDN example tried to externalize the Yjs peers with an import map plus `?external=yjs,y-protocols`. Verified in a headless browser against the built bundle: that produces TWO Yjs copies (the "Yjs was already imported" error — the very problem this PR prevents), and a correct import map would additionally need `*y-protocols/` and `lib0/`, version-aligned — too fragile to document. esm.sh already resolves Apollon's `yjs`/`y-protocols` peers from the bare import URL and shims `process`, so revert the CDN examples (docs Vanilla page + published library README) to the plain import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche Most of the docs are fixed now, including the dedicated vanilla guide and library README. I am keeping changes requested because the homepage Vanilla JS CDN snippet still lacks the peer-resolution note from the open thread.
The peer-dependency notes called yjs/y-protocols "the collaboration engine," which implies they're only needed when collaboration is on. They are actually Apollon's document model and undo/redo engine, instantiated for every editor (new Y.Doc() in the constructor), so they are required whether or not you collaborate. Reword across the README, library README, and docs (overview, quickstart, install) to say so, and fix install.md's stale "both builds" (there are now three entries). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche The homepage npm install line is updated, but the open CDN/Vanilla JS point is still not addressed in the tab that shows the snippet. Keeping this as changes requested until that snippet explains the esm.sh peer resolution or imports the peers explicitly.
… tab The homepage Vanilla JS snippet imported Apollon from esm.sh without noting how its required yjs/y-protocols peers resolve on the no-bundler path. Add a one-line comment next to the import (esm.sh serves them automatically), matching the vanilla embedding guide and the library README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche All requested docs feedback is addressed now. The thread is resolved and the latest clarification looks good to me.
FelixTJDietrich
left a comment
There was a problem hiding this comment.
Changes look good and work as expected, I ensured that the examples in the docs are working. I will do one more minor adjustment to the docs before I merge
The published README and the overview table tied the new `/external` entry to Artemis, but Artemis imports the default `@tumaet/apollon` entry everywhere and does not use `/external` at all. Remove the claim (and the matching "e.g. Artemis" in the build table) — build-selection guidance shouldn't hinge on one consumer, and in this case the claim was simply untrue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche The docs follow-up is resolved, but the new /external build still bundles Zustand subpath helpers because the external list only matches the package root. I left the concrete fix inline.
RUNTIME_DEPS externalized only the exact id "zustand", but the source imports zustand/middleware (7x) and zustand/shallow (120x). Rollup treats subpaths as distinct ids, so the /external build still inlined that code. Match zustand and its subpaths with a regex (mirroring the @dnd-kit entry). Verified: after the change dist/external/index.js imports "zustand", "zustand/middleware", and "zustand/shallow" as bare externals. The other exact-string externals (uuid, @chenglou/pretext, @emotion/*, @xyflow/react) import no subpaths and stay clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claudia-Anthropica
left a comment
There was a problem hiding this comment.
@krusche All requested feedback is addressed now. I verified the library build, the generated external bundle imports the Zustand subpaths as bare externals, and the size budget still passes.
Summary
Apollon's default build is a single self-contained file — perfect for a
<script>tag or CDN, but it inlines React, MUI, xyflow, and Apollon's own runtime deps. For a host that embeds Apollon with a bundler (Angular, Vue, Svelte, React), that means shipping — and being unable to de-duplicate or audit — a second private copy of each. And because Yjs is a CRDT whose runtime relies on module-level singletons, a host that already uses Yjs ends up running two Yjs copies on the same page.This PR fixes both, without changing the existing entries:
@tumaet/apollon/externalentry — the same imperativeApollonEditorAPI as the default entry, but with every runtime dependency external (the React family plus@dnd-kit/zustand/uuid/@chenglou/pretext;yjs/y-protocolswere already external). The host's own bundler then resolves, de-duplicates, and gets full supply-chain / SBOM attribution for each one from itsnode_modules, instead of a copy buried invisibly in Apollon's bundle.yjsandy-protocolsare now required peer dependencies of every build. They are Apollon's document model and undo/redo engine (used by every editor, collaboration or not), and they rely on module-level singletons — so one shared instance is the correct contract for any host that already uses Yjs.The default
@tumaet/apollon(self-contained) and@tumaet/apollon/reactentries are behaviourally unchanged.@tumaet/apollon(default)<script>, CDN.@tumaet/apollon/react<Apollon>component and shares its own React/MUI.@tumaet/apollon/external(new)Release note
Two packaging improvements for embedding hosts. First,
yjsandy-protocolsare now required peer dependencies instead of being bundled, so your app and Apollon share a single Yjs instance — no duplicate payload and no cross-instance document errors. Second, a new@tumaet/apollon/externalentry exposes the same imperativeApollonEditorAPI as the default entry but leaves every dependency external (React, MUI, emotion, xyflow,@dnd-kit,zustand,uuid,@chenglou/pretext, …) — so a bundler host of any framework resolves and de-duplicates each one from its ownnode_modulesand gets full supply-chain / SBOM visibility, instead of a copy inlined invisibly into the bundle. The default@tumaet/apollon(self-contained) and@tumaet/apollon/reactentries are unchanged. Action required only if you adopt the new entry or the Yjs peer: install the corresponding peers (most package managers do this automatically).Implementation notes
vite.config.ts— a third build pass (LIB_EXTERNAL=true) emitsdist/external/index.js, externalizing the React family and Apollon's own runtime deps. It reuses the rootdist/index.d.ts(same public surface as the default entry), so no second declaration file is emitted.yjs/y-protocolsare external in all three passes.library/package.json— adds the./externalexport; movesyjs/y-protocolsfromdependenciesto requiredpeerDependencies(the React-family peers remainoptional), and keeps them indevDependenciesso the workspace still builds and tests. Adds asize-limitbudget for the new entry (≈38 KB brotli — everything is external).sourcemap: true, but that does not deliver the SBOM goal: a consumer that disables vendor source maps ignores them, and a dependency inlined-but-undeclared has no version to match against an advisory. It also added ~12 MB to the published tarball. The/externalentry is the structural fix instead.minor). The default entry's old "zero peers" promise becomes "installyjs+y-protocols." npm 7+ and pnpm 8+ add missing peers automatically; Yarn (all versions) and pnpm < 8 do not, and CDN / import-map users must declare them explicitly. A missing peer surfaces as a clear module-not-found error, not silent breakage.@tumaet/apollon/externaland provides the now-external peers it doesn't already have. Its existing SBOM tooling (cdxgen, Syft, …) then attributes every Apollon dependency natively from its ownnode_modules— no Apollon-side SBOM artifact needed.Steps for testing
pnpm --filter @tumaet/apollon build→ emitsdist/index.js,dist/react/react.js, and the newdist/external/index.js(withdist/index.d.tsshared by.and/external). (verified locally — green)pnpm --filter @tumaet/apollon size→ all five budgets pass; the/externalentry is ≈38 KB brotli. (verified locally — green)dist/external/index.jsimportsreact,@mui/material,yjs,@dnd-kit/*,zustand, … as bare specifiers (no inlined copy).pnpm --filter @tumaet/apollon lintand… test— green in CI (lint-and-format-check,library-node22-compat,visual-regression-tests).import { ApollonEditor } from "@tumaet/apollon/external", then confirm exactly one resolved copy of each dependency in the lockfile.Screenshots / screencasts
n/a — packaging / build + docs change, no UI.
Checklist
feat— a new public entry)