Skip to content

feat: monorepo modernization — pnpm 11 + Node 24, Docusaurus docs, React <Apollon> API, VS Code rewrite - #691

Merged
FelixTJDietrich merged 50 commits into
mainfrom
chore/pnpm-node24-migration
May 26, 2026
Merged

feat: monorepo modernization — pnpm 11 + Node 24, Docusaurus docs, React <Apollon> API, VS Code rewrite#691
FelixTJDietrich merged 50 commits into
mainfrom
chore/pnpm-node24-migration

Conversation

@FelixTJDietrich

@FelixTJDietrich FelixTJDietrich commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

This branch does three things that ship independently of each other:

  1. Modernises the monorepo plumbing — pnpm 11.1.3 + Node 24.15.0 LTS, Vite everywhere, server CJS → ESM. One lockfile. No more webpack, no more Parcel, no more CJS/ESM coexistence dance.
  2. Adds a real React API to the library — a <Apollon> component plus ApollonProvider, useApollonEditor, useApollonEditorOrThrow, useApollonSubscription. React-19-style onMount cleanup return, "use client" directive that actually survives the bundle. Webapp + VS Code editor migrated; zero new ApollonEditor(...) in repo outside the library itself.
  3. Replaces the static Sphinx docs with a Docusaurus 3.10 site — Library / User / Contributor sidebars, a live in-browser editor on the landing page, deployed to GitHub Pages by a new docs.yml workflow on every push to main.

The library is dual-build by design: the default subpath (@tumaet/apollon) bundles React + MUI + emotion + xyflow so non-React hosts (Angular/Artemis is the primary consumer) install with zero peer deps. The /react subpath externalises them so React hosts can dedupe. Both subpaths share the same stylesheet via ./style.css.

Why this PR is large

The original scope was "pnpm + Node 24". Mid-review, two things became unavoidable:

  • The library shipped as React-only mid-PR (commit c79683e8), which would have broken Artemis (Angular). That got reverted (c64d88b2); the dual-build is now part of the same PR because reverting it cleanly required tooling that didn't exist on main.
  • The docs were Sphinx + ReadTheDocs and the requirements.txt/Makefile/bin/plantuml.jar machinery had to go anyway. Once Sphinx was out, Docusaurus and the React API were the natural place to land — and they reuse the same pnpm workspace plumbing this PR introduces.

Splitting into three PRs would have meant main ships in an awkward in-between state for two of the three weeks.

What changed, by concern

Build & toolchain

Before After
Package manager npm pnpm 11.1.3 (Corepack-verifiable SHA512 pin in packageManager)
Node 22 24.15.0 (.nvmrc, engines, Dockerfiles, all CI runners)
Bundlers webpack + Parcel + Vite Vite only (server: tsc)
Module system mixed CJS / ESM ESM end-to-end (server "type": "module", NodeNext, verbatimModuleSyntax)
Lockfiles 3 × package-lock.json 1 × pnpm-lock.yaml

The server migration alone is 22 source files / 76 import specifiers gaining .js suffixes plus a static import { ApollonEditor, importDiagram, ... } from "@tumaet/apollon" replacing the cargo-cult loadApollonModule = () => import(...) lazy wrapper. Express 5's automatic async-handler propagation lets routes/conversion.ts and middleware/validate.ts drop their try/catch ceremonies.

Library — public API

import { Apollon } from "@tumaet/apollon/react"
import "@tumaet/apollon/style.css"

<Apollon
  style={{ height: 600 }}
  defaultModel={savedDiagram}
  readonly={isReadonly}        // reactive
  onMount={(editor) => {        // React-19 cleanup-return idiom
    const id = editor.subscribeToModelChange(persist)
    return () => editor.unsubscribe(id)
  }}
/>

Props split into initial-only (snapshotted at mount; re-key to rebuild) and reactive (applied through the matching editor.setX(...) when the prop changes). The split is documented in source JSDoc, in /library/api, and in /library/embedding/react — and verified by a small Apollon.test.tsx that asserts the contract three ways (constructor argument shape, ref forwarding, reactive-setter dispatch).

Imperative ApollonEditor is unchanged for non-React hosts. The constructor still takes (element, options?); the standalone subpath still re-exports the full surface.

"use client" actually works. Rollup strips source-level directives during bundling; verified that the previous bundle started with var f0 = …, not "use client";. Re-added via Rollup output.banner on the peer pass only. head -1 dist/react/react.js now prints "use client"; — Next.js App Router consumers don't need a wrapper.

Library — bundle shape

Subpath Peers Bundle
@tumaet/apollon (default) bundled ~2.4 MB
@tumaet/apollon/react externalised ~875 KB raw / ~100 kB brotli
@tumaet/apollon/internals (unstable; host integration tests) ~1 KB + reused yjs chunk
@tumaet/apollon/style.css 21 KB

peerDependenciesMeta.optional marks all six peers optional, so npm install @tumaet/apollon never warns about missing React/MUI/etc. — even though the default subpath inlines them.

VS Code extension

Before After
Extension main bundle 18.2 KB (webpack, 940 ms) 8.2 KB (Vite, 87 ms)
Menu webview ~360 KB (sunset UI toolkit) 146 KB (native HTML + VS Code CSS vars)
Editor webview theme locked to setTheme("light") follows VS Code's auto-applied light/dark/high-contrast
Deprecated runtime deps 4 0
Release pipeline manual vsce from a laptop GitHub Actions, environment-gated, sigstore-attested, dual-registry (Marketplace + Open VSX)

Dropped: @vscode/webview-ui-toolkit (Microsoft sunset 2026-01-06), styled-components, uuid, @vscode/codicons. Replacement is native HTML elements styled via var(--vscode-*) per Microsoft's post-deprecation guidance. The editor webview imports from @tumaet/apollon/react to share its React copy with the host webview.

Docs

Static Sphinx site → Docusaurus 3.10 at https://ls1intum.github.io/Apollon/. Three sidebars (Library / User / Contributor), live in-browser editor on the landing page (docs/src/components/ApollonEmbed), Diátaxis-shaped Library section, deployed by .github/workflows/docs.yml on every main push. onBrokenLinks: throw and onBrokenAnchors: throw keep cross-refs honest.

Monorepo hygiene

  • pnpm catalogs for shared version pins (pnpm-workspace.yaml).
  • Override floors tightened to the minimum fixed version per CVE (no more mass major-bumps).
  • HEALTHCHECK in both Dockerfiles; digest-pinned base images.
  • SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md, .editorconfig, .gitattributes added.
  • Renovate gains helpers:pinGitHubActionDigests so SHA pins don't rot.
  • All third-party GitHub Actions are SHA-pinned with version comments.
  • release-library.yml hardened against pnpm 11's varying pnpm pack --json output shape (pnpm/pnpm#10200).

Required GitHub setup before the first VS Code Marketplace release

release-vscode-extension.yml is gated on a vscode-marketplace Environment that doesn't exist on this repo yet — the workflow will hang on approval until it's created:

  1. Settings → Environments → New environment → name vscode-marketplace
  2. Deployment branches: "Selected branches and tags" → add main
  3. Required reviewers: add the release maintainer; turn on Prevent self-review
  4. Environment secrets:
    • VSCE_PAT — Azure DevOps PAT, scope Marketplace → Manage, 1-year max lifetime
    • OVSX_PAT — Open VSX PAT. The tumaet namespace must exist on open-vsx.org first; any namespace member can create it once with ovsx create-namespace tumaet -p <PAT>
  5. Delete any pre-existing repo-level VSCE_PAT / OVSX_PAT — repo secrets bypass the environment's deployment-branch and reviewer controls.

Full checklist also at docs/contributor/deployment/npm-publishing.md.

How to review

The PR is large by line count but the architecture is small — three orthogonal concerns that don't touch each other much. Pick the angle that matches your expertise:

If you maintain the library

Focus on library/lib/components/react/ (~330 lines net new) and the dual-build in library/vite.config.ts. The questions worth answering:

  1. Is the initial-only vs reactive prop split right? Apollon.tsx JSDoc lists which is which. Passing undefined to a reactive prop leaves the live value alone — re-key to fully reset.
  2. Does onMount's cleanup-return ordering make sense? consumer cleanup → editor.destroy() → null the ref. The comment in Apollon.tsx explains why; the test Apollon.test.tsx pins it.
  3. Is the "use client" banner the right escape hatch? Rollup strips the source directive. The vite.config.ts comment + head -1 dist/react/react.js should answer it.
  4. Does the dual-build genuinely keep React out of the standalone subpath? Smoke test below.

If you maintain the standalone server / webapp / VS Code extension

The webapp + VS Code editor are migrated from imperative new ApollonEditor(...) to <Apollon>. grep -rn "new ApollonEditor" --include="*.tsx" outside library/ should return zero hits. ApollonWithConnection.tsx is the most complex consumer (collab cursors, version preview, autosave, WS) — focus there.

Server: 22 files gain .js import suffixes. The substantive changes are in conversion-service.ts (static import replacing lazy wrapper) and the Express-5 async-handler simplifications in routes/conversion.ts + middleware/validate.ts.

VS Code: editor/src/App.tsx now renders <Apollon> with a key={loadVersion} for "loadDiagram" remounts; the hand-rolled ApollonEditorContext/ApollonEditorComponent (which duplicated the library's ApollonProvider/useApollonEditor) is deleted.

If you maintain CI / deployment

Three new workflow files: docs.yml (GitHub Pages publish), release-vscode-extension.yml (sigstore + dual-registry), and the new library-node22-compat job in pr-health-checks.yml (the library still advertises engines.node: ">=22.0.0" for downstream consumers even though the monorepo is on 24).

The vscode-marketplace environment setup above is the only blocking action required.

If you're reviewing docs

The Library section follows Diátaxis: Quickstart (tutorial) → Embedding (how-to) → API (reference) → Troubleshooting (explanation/diagnosis). The API page is canonical: embedding pages link there for prop tables instead of duplicating. onBrokenLinks: throw in docusaurus.config.ts means the production build is a transitive link-check.

Steps for testing

# 1. Fresh install — expect a single pnpm-lock.yaml, zero ignored-build-script warnings,
#    no leftover package-lock.json files anywhere.
nvm install && nvm use
npm install -g pnpm@11.1.3
pnpm install

# 2. Build everything
pnpm build          # library + server + webapp (library first; tsc -b enforces order)
pnpm build:vscode   # extension + 2 webviews
pnpm build:docs     # library + Docusaurus site (onBrokenLinks: throw)

# 3. Lint + tests
pnpm lint           # ESLint per workspace + markdownlint (docs)
pnpm test           # 760 library unit tests
pnpm test:e2e       # Playwright

# 4. Verify the "use client" directive survives the bundle — this is the
#    headline correctness check for Next.js App Router consumers
head -1 library/dist/react/react.js          # → "use client";

# 5. Smoke-test both library subpaths in isolation
node --input-type=module -e \
  'import("./library/dist/index.js").then(m => console.log("standalone:", Object.keys(m).length, "exports"))'
node --input-type=module -e \
  'import("./library/dist/react/react.js").then(m => console.log("/react:", Object.keys(m).length, "exports"))'

The standalone resolves with no peer deps installed (Artemis case). The /react subpath resolves only when react, react-dom, @mui/material, @emotion/{react,styled}, and @xyflow/react are present. Both share the same ./style.css export.

Out of scope (deliberately deferred)

What Why deferred Tracked where
Library version bump from 4.4.0 version-bump.yml handles release bumps post-merge Existing workflow
arethetypeswrong CI gate Dropped in bb72249b — fflate streaming bug prevents tarball extraction on our shape; publint + exports-map check still cover it Re-add when fflate lands a fix
React 19 in peer range Reviewer-confirmed Round 6: narrow to ^18.3.0 matches reality. Widening requires a ref-type refactor (anchorRef: RefObject<SVGSVGElement | null> plus call-site adjustments where the SVG ref crosses SVGForeignObjectElement boundaries) Follow-up PR
MUI emotion CSP 'unsafe-inline' in VS Code webviews Needs a library cspNonce option threaded into emotion's createCache emotion-js/emotion#403
Server unit-test isolation flake (tryAutoVersion races flushDb) Pre-existing on main — verified by baseline Unchanged scope
Mobile (iOS / Android) build pipeline Capacitor dep cleanup is in scope; native builds are untouched Future PR
export * barrel from lib/index.tsx still leaks ~33 unused identifiers (caught by knip) Cutting them is SemVer-major Next release bump

Screenshots

No UI changes inside Apollon itself. The VS Code editor webview now follows VS Code's theme automatically (light/dark/high-contrast); it used to be locked to light. The Docusaurus site is the new public-facing surface — landing page at docs/src/pages/index.tsx, hosted at https://ls1intum.github.io/Apollon/ after merge.

Checklist

  • No linked issue — foundational migration, not a tracked feature
  • Library version unchanged (4.4.0, matches main) — release bump handled by version-bump.yml post-merge
  • Dual-build preserved: Angular hosts (Artemis primary) install with zero peer deps; React hosts opt into /react
  • "use client"; directive present at byte 0 of dist/react/react.js (the previous bundle was silently missing it)
  • Docusaurus build clean (onBrokenLinks: throw, onBrokenAnchors: throw)
  • All required CI workflows green on the latest push

- Add pnpm-workspace.yaml with workspace ergonomics, security overrides
  (annotated by GHSA), allowBuilds allowlist, injectWorkspacePackages
- Add .npmrc with engine-strict
- Pin Node to 24.15.0 via .nvmrc (no v prefix, fnm/volta compat)
- Root package.json: packageManager pnpm@11.1.3, engines node >=24.15.0
  and pnpm >=11.1.0, rewrite scripts to pnpm --filter, dedupe
  @capacitor/cli to devDeps, drop unused iobuffer, add lint-staged
- Delete package-lock.json, commit pnpm-lock.yaml
- .gitignore: drop pnpm-lock.yaml exclusion, add .parcel-cache, dedupe
@FelixTJDietrich FelixTJDietrich changed the title chore: migrate to pnpm workspaces + Node 24 LTS chore: migrate monorepo to pnpm 11 workspaces + Node 24 LTS May 19, 2026
@FelixTJDietrich FelixTJDietrich changed the title chore: migrate monorepo to pnpm 11 workspaces + Node 24 LTS chore!: migrate monorepo to pnpm 11 workspaces + Node 24 LTS May 19, 2026
@FelixTJDietrich FelixTJDietrich changed the title chore!: migrate monorepo to pnpm 11 workspaces + Node 24 LTS chore: migrate monorepo to pnpm 11 workspaces + Node 24 LTS May 19, 2026
@FelixTJDietrich
FelixTJDietrich force-pushed the chore/pnpm-node24-migration branch 3 times, most recently from 5d2c95b to 17fe30b Compare May 19, 2026 14:24
The published @tumaet/apollon package now ships two builds with identical
APIs, selected by import subpath:

  import { ApollonEditor } from "@tumaet/apollon"       // standalone
  import { ApollonEditor } from "@tumaet/apollon/react" // peer-deps

Standalone (~2.4 MB) bundles React, MUI, emotion, and @xyflow/react.
Works in Angular, Vue, Svelte, and vanilla JS hosts that do not have
React installed. Peer-deps (~875 KB) externalizes the same packages so
React 18.3+ / 19+ hosts can dedupe with their own instance.

The public API is imperative (`new ApollonEditor(container, options)`)
and exposes no React types, so the two builds are interchangeable at
the type level — a single .d.ts is shared via the exports map.

Peers are declared with peerDependenciesMeta.optional so installing
@tumaet/apollon does not warn when the host has no React.

- library/vite.config.ts: LIB_PEERS env switches between bundled and
  externalized outputs; dts plugin only runs on the standalone build
- library/package.json: exports map with "." and "/react" subpaths,
  peerDependenciesMeta marking all six React-ecosystem peers as optional,
  build script invokes vite twice (default then LIB_PEERS=true)
- library/README.md: install/usage for React, Angular, and vanilla JS
- CustomText.tsx: replace ad-hoc Props with Omit<SVGProps<SVGTextElement>>
- SfcDiagramEdge.tsx: use as const literals instead of cast tower

chore(workspaces): use workspace: protocol, unify cross-workspace versions

- standalone/{webapp,server}: workspace:* for @tumaet/apollon
- Unify @types/node@24.12.4, typescript@5.7.2, vitest@4.1.2,
  styled-components@6.4.0, jsdom@28.1.0 across workspaces
- standalone/webapp: @types/node moved to devDeps, @ionic/react sorted,
  declare react/react-dom/clsx/uuid/@types/uuid that previously resolved
  via npm flat hoisting
- standalone/server: declare lib0, @types/express-serve-static-core, full
  eslint flat-config stack; engines.node >=24.15.0
- standalone/server: eslint.config.js → .mjs (silences pkg-type warning)
…nt 9

Both webviews now use Vite 6 (matching the rest of the monorepo) instead
of Parcel 2. Eliminates the buffer/process polyfill auto-install dance,
the --no-autoinstall workaround, and 5 native Parcel deps from
allowBuilds. Bundle filenames `index.js` / `index.css` preserved so
menu-provider.ts and .vscodeignore work unchanged.

- vscode-extension/{menu,editor}: drop parcel + postcss + autoprefixer +
  CRA debris (react-scripts, @testing-library/*, web-vitals, jest); add
  vite + @vitejs/plugin-react + @tailwindcss/vite; modernize tsconfig
  (target es2022, moduleResolution bundler); rename to scoped names
  apollon-vscode-{menu,editor}
- Tailwind v3 → v4 (matching webapp): replace @tailwind directives with
  @import "tailwindcss", delete tailwind.config.js + .postcssrc
- editor: declare @emotion/{react,styled}, @mui/material, @xyflow/react
  as direct deps (library now externalizes them)
- index.tsx: use named imports (StrictMode, createRoot) — drop default React
- Delete dead setupTests.ts and reportWebVitals.ts (CRA scaffolding)
- vscode-extension/.eslintrc.json → eslint.config.mjs (ESLint 9 flat config)
- vscode-extension/package.json: workspace:* for @tumaet/apollon, drop
  @vscode/vsce from deps (CI installs it sandboxed), fill description,
  fix bugs/repository URLs to point to the monorepo with directory hint,
  update eslint stack to 9 + typescript-eslint 8

vscode-extension/src/menu-provider.ts: extract _renderWebviewHtml helper
that emits a per-render CSP nonce on script-src and adds a CSP meta tag.
style-src 'unsafe-inline' kept (and documented) for webview-ui-toolkit
and styled-components runtime <style> injection.
- Bump base to node:24.15.0-slim digest-pinned, nginx:1.27-alpine digest-pinned
- Install pnpm via npm (not corepack) and configure pnpm-store dir
- BuildKit cache mount on /pnpm/store across install + deploy
- ARG NODE_IMAGE deduplicates FROM lines across stages
- Split library + app build into separate RUN layers for partial cache reuse
- Server runtime: pnpm deploy --filter=@tumaet/server --prod produces a
  self-contained /deploy/server (relies on injectWorkspacePackages: true)
- Server deploy stage: selective COPY from builder (skip webapp + vscode trees)
- Dockerfile syntax 1.7 → 1.9
- pr-health-checks.yaml → .yml (uniform extension); install pnpm via
  pnpm/action-setup@v6.0.8 (SHA-pinned) before actions/setup-node, with
  cache: pnpm and node-version-file: .nvmrc
- Add library-node22-compat job that builds + tests the library on Node 22
  to back its engines.node >=22 promise to npm consumers
- visual-regression-tests: install Node 24 in the Playwright container
  (which ships Node 22) before pnpm install — engine-strict gate
- Playwright cache key: salt with v1.59.1, scope to webapp manifest hash
- release-library.yml: SHA-pin pnpm/action-setup; install npm@11.5.1 with
  --ignore-scripts; hybrid pnpm pack + npm publish for OIDC trusted
  publishing (pnpm#9812); explicit --provenance
- version-bump.yml: accept vscode-extension as third scope; unify per-package
  bump mechanism via bump_pkg node helper; SHA-pin action-setup;
  set -euo pipefail on every run block; lockfile name updated
- version-monotonicity.yml: include vscode-extension/package.json
- build-and-push.yml: paths add .nvmrc/.npmrc/pnpm-workspace.yaml/pnpm-lock.yaml
- SHA-pin ls1intum/.github reusable workflows in build-and-push + deploy-*
- Add release-vscode-extension.yml: 4-job pipeline (check → build → publish
  → release) gated on `vscode-marketplace` environment, sandboxed
  vsce/ovsx install under $RUNNER_TEMP with --ignore-scripts, sigstore
  attest-build-provenance on the VSIX, publishes to VS Marketplace +
  Open VSX with sha256-verified idempotency, tags apollon-vscode@X.Y.Z
- .husky/pre-commit: replace whole-repo lint/format with lint-staged
  (~1s vs 30-60s on small PRs)
- .husky/commit-msg: quote "$1" for paths with spaces
- renovate.json: pnpmDedupe instead of npmDedupe; lockFileMaintenance
  weekly; docker:pinDigests; disable @types/pdfmake (0.3.x broke .vfs
  used by the pdf-conversion worker)
- playwright.config.ts: pnpm run start
- standalone/server/src/scripts/migrate-string-to-json.ts: comment refs
  pnpm --filter @tumaet/server run …
- standalone/webapp/tests/e2e/legal-pages.spec.ts: comment updated
- README.md: pnpm dev / pnpm install, drop corepack recommendation,
  Node 24 LTS pin, pnpm 11+
- docs/getting-started/{requirements,setup}.md: pnpm install pattern,
  drop corepack alternative
- docs/development/{project-structure,scripts}.md: pnpm-workspaces
  narrative, pnpm script commands
- docs/development/visual-tests.md (new): regen instructions for visual
  baselines (was an 11-line YAML comment in pr-health-checks)
- docs/contributing.md: pnpm lint/test/build
- docs/admin/operations.md: Node 24 + pnpm 11; pnpm --filter migrate
- docs/deployment/npm-publishing.md: add vscode-extension release
  workflow row + Open VSX + GitHub environment setup
- docs/mobile/ios-android-setup.md, docs/troubleshooting/common-issues.md:
  pnpm commands; graduated escalation for dependency-change recovery
- standalone/webapp/README.md, vscode-extension/README.dev.md:
  pnpm run / pnpm --filter
The mcr.microsoft.com/playwright:v1.59.1-noble container's default shell
is dash, which rejects `set -o pipefail`. Force bash for the one step
that needs it.
@FelixTJDietrich
FelixTJDietrich force-pushed the chore/pnpm-node24-migration branch from 17fe30b to 08f4dd6 Compare May 19, 2026 14:59
FelixTJDietrich and others added 7 commits May 19, 2026 17:52
- compose.local{,.db}.yml: pin redis-stack `--dir /data` so AOF/RDB land on
  the named volume; the image defaults to /var/lib/redis-stack which lives
  in the container's writable layer and is lost on recreate
- standalone/server: replace 48 lines of `require()` retry+fallback hacks
  with a single `await import("@tumaet/apollon")`; the lazy dynamic import
  is the canonical CJS→ESM bridge in Node 22+, no library-side `require`
  exports condition needed
- pr-health-checks: validate the library's ESM exports map with
  `import.meta.resolve` on Node 22 LTS — catches exports-map regressions
  without requiring jsdom
- webapp Dockerfile: drop dead `pnpm --filter @tumaet/apollon run build`
  step; library is already built in the builder stage
- webapp: delete duplicate `eslint.config.js` (kept `.mjs`)
- docs/admin/operations.md: redis-stack tag 7.4.0-v0 → 7.4.0-v8 to match
  what compose files actually run
- docs/deployment/github-actions.md: document OWNER_SECRET (required) and
  LEGAL_PROFILE (optional)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The webapp's `tsc -b` resolves `@tumaet/apollon` via the workspace link
that pnpm injects into node_modules — not via the Vite path alias. The
pnpm-linked library needs its compiled `.d.ts` for `tsc` to typecheck,
so the library build step is not dead.

Reverts the removal in 27e7fc9.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
BREAKING CHANGE: drops the `@tumaet/apollon/react` subpath and the
standalone 2.4 MB bundle. The default `@tumaet/apollon` import now
always externalizes React + MUI + emotion + xyflow as mandatory peers.
Consumers must install them.

Why: the dual build was a hand-rolled hack (two Vite passes plus
`rm -rf dist/react/assets`) selling a "framework-agnostic" story that
was always a fiction — the library mounts its own React tree
regardless. Type leakage (`@xyflow/react` in the public surface) made
the optional-peer claim structurally dishonest. No major React-internal
DOM-mount library ships this pattern; precedents (TipTap, react-flow,
Excalidraw) all use mandatory peers.

- Single Vite pass: tsc -b && vite build.
- `dist/index.js` 685 KB + `dist/yjsSyncClass-*.js` 191 KB chunk.
- `peerDependenciesMeta` removed entirely — no fake optionality.
- vite-plugin-dts `rollupTypes: true` emits a self-contained
  `dist/index.d.ts`, so NodeNext consumers don't trip on internal
  relative imports without `.js` suffix.
- Moved `YjsSyncClass`/`MessageType`/`createHeadlessSync` to a new
  `/internals` subpath, explicitly out of semver coverage.
- README cut from 220 to 73 lines: one factual install command, no
  Angular/Vue/Vanilla marketing.
- Internal consumers (`vscode-extension/editor`) re-pointed at the
  root import. `ws-yjs.int.test.ts` switched to `/internals`. CI
  exports-map probe updated.
- Migrate the extension main bundle from webpack to Vite library mode
  (CJS output, `vscode` + Node builtins externalized). Unifies the
  monorepo on a single bundler.
  - dist/extension.js 8.2 KB (was 18.2 KB under webpack, −55%).
  - Build time ~87 ms (was ~940 ms under webpack, ~11× faster).
  - 67 packages removed from the install graph (webpack +
    webpack-cli + ts-loader + transitives).
- Drop `@vscode/webview-ui-toolkit@1.4.0`, deprecated by Microsoft on
  2025-01-06. Replace VSCodeButton / VSCodeDropdown / VSCodeOption /
  VSCodeTextField / VSCodeProgressRing usages with native `<button>`
  / `<select>` / `<input>` + a CSS-only spinner, themed via
  `var(--vscode-*)` CSS variables per the official post-deprecation
  guidance. New `.vscode-button` / `.vscode-input` / `.vscode-select`
  classes live in each webview's index.css.
  - menu bundle 360 → 146 KB (−60 %).
  - editor bundle 1252 → 1041 KB (−17 %).
  - Dead `VSCodeDivider` import dropped (was never rendered).
- Drop `styled-components@6.4.0` from the editor webview. The one
  styled `<div>` becomes a Tailwind `bg-[var(--apollon-background)]`
  utility wrap.
- Drop pre-existing dead `getNonce` (cryptographically insecure
  Math.random-based) from `src/util.ts`. CSP nonce already uses
  `node:crypto`'s randomBytes.
- Re-point editor webview imports from `@tumaet/apollon/react` to
  `@tumaet/apollon` (root) — `/react` subpath no longer exists.
- Add the latent peer `@xyflow/react` to `editor/package.json`
  explicitly (was satisfied via pnpm workspace hoisting only).
- Sweep CRA leftovers: `.parcel-cache` from `.gitignore`s,
  `browserslist` from package.jsons, phantom `esbuild.js` from
  `.vscodeignore`, dead store actions in both webviews.
- Update CSP comment in `menu-provider.ts` to point at the real cause
  for `style-src 'unsafe-inline'` (emotion runtime <style> tags from
  the apollon library, ref emotion-js/emotion#403). styled-components
  / webview-ui-toolkit no longer involved.
- `README.dev.md`: replace "Parcel" / "webpack" with the real Vite
  layout, document the three-watcher dev loop.

The CSP `'unsafe-inline'` for style-src still has to stay until the
library exposes a way to plumb a nonce into its internal emotion
cache. Tracked as a library follow-up.
The library is ESM-only. The server was CJS-by-default and consumed
it via a `loadApollonModule = () => import("@tumaet/apollon")` wrapper
that the audit correctly called cargo cult — the rationale ("avoid
boot-time side effects") didn't hold because the worker thread is the
only consumer and it already paid for jsdom at module load.

Switch the whole workspace to ESM:

- `package.json` adds `"type": "module"`.
- `tsconfig.json` switches `module`/`moduleResolution` to NodeNext,
  enables `verbatimModuleSyntax: true`, drops the gratuitous
  `strictPropertyInitialization: false` opt-out.
- 22 source files / 76 relative-import specifiers gain `.js`
  suffixes via a one-shot codemod (NodeNext-compliant).
- `pdfmake/build/{pdfmake,vfs_fonts}` → `.js` suffix on bare-subpath
  imports.
- `conversion-service.ts` drops the `loadApollonModule` wrapper.
  Replaced by a static `import { ApollonEditor, importDiagram,
  type UMLModel, type SVG } from "@tumaet/apollon"`.
- `conversion-resource.ts`: `__dirname` → `import.meta.dirname`.
  `import { Request, Response }` → `import type { ... }` (per
  verbatimModuleSyntax).
- `routes/conversion.ts` + `http/middleware/validate.ts`: drop the
  try/catch + `next(err)` ceremony. Express 5 propagates rejected
  promises from async handlers automatically — bumped but never
  adopted before.
- `logger.ts`: `import pino` → `import { pino, stdTimeFunctions }`
  and `app.ts`: `import pinoHttp` → `import { pinoHttp }`. Default
  imports broke under NodeNext+verbatimModuleSyntax because pino's
  `.d.ts` uses `export { pino as default, pino }` rather than
  `export =`.

The lazy `await import()` dance is gone; the server is a normal ESM
consumer of the workspace library.
- pnpm-workspace.yaml override floors: previously over-applied
  patches mass-bumped transitive consumers to unblessed branches.
  Tighten to the minimum fixed version per advisory:
  - lodash ">=4.18.0" → ">=4.17.21" (CVE-2019-10744 fixed in 4.17.12)
  - tar ">=7.5.11" → ">=6.2.1" (CVE-2024-28863 fixed in 6.2.1; was
    forcing every consumer through a tar 6→7 major)
  - minimatch ">=5.0.0" → ">=3.0.5" (GHSA fixed in 3.0.5; was
    forcing a two-major jump)
  - brace-expansion ">=2.0.3" → ">=2.0.2" (GHSA fixed in 2.0.2)
  Drop redundant pnpm 11 defaults (linkWorkspacePackages: deep,
  preferWorkspacePackages, saveWorkspaceProtocol, dedupePeerDependents).
  Relabel the esbuild pin as "regression workaround, not CVE" — it
  was misleadingly grouped with vuln overrides.

- Root package.json: relocate misplaced deps to where they're used.
  All @capacitor/* runtime deps + capacitor-plugin-safe-area moved
  to standalone/webapp. @resvg/resvg-js moved to webapp devDeps
  (only used by tests/helpers/resvgRender.ts). @emotion/* removed
  from root (library already devDeps them; webapp now has them as
  direct deps so MUI's styled-engine resolves). Removed `yaml`
  (zero usages) and `iobuffer` (zero usages). @types/pdfmake at
  root (v0.3.2) removed — server uses the correct pinned v0.2.x.
  vite at root removed — each workspace has its own pin.

- Library theatre tests cut: textUtils.test.ts deleted (tested the
  jsdom mock equal to itself); storeUtils.test.ts engine-dependent
  "may be true or false" assertion removed. 772 → 772 useful tests.

- Library: trim long getBBox comment block in conversion-service.ts
  (now in extracted jsdom-shims.ts), kill file-wide eslint-disable.
  Pre-call patches hoisted to one-time worker init.

- Library: narrow esbuild `drop` to `["debugger"]`. `console`-drop
  was silently stripping React/MUI runtime warnings from the
  formerly-inlined standalone bundle.

- Library: ditch 5-line JSDoc bloat in lib/utils/textUtils.ts that
  duplicated the function signature.

- Library description trimmed from keyword-stuffed paragraph to one
  factual sentence.

- Library engines.node stays at >=22 (public package supports Node
  22 LTS through 2027); rest of monorepo pinned at >=24.15.0.

- Server, webapp Dockerfiles: HEALTHCHECK directives added so
  orchestrators get liveness signals from the image directly, not
  only from compose.

- .dockerignore tightened — excludes **/tests, **/__tests__,
  **/*.test.ts, vscode-extension/{src,*/src,*/dist,dist}, scripts/,
  .husky/, .changeset/. Saves cache layer hits.

- Webapp eslint warning: remove unused-disable-directive in
  LegalPage.tsx (no-console rule no longer fires here).

- release-library.yml: replace brittle `pnpm pack | tail -n 1`
  (pnpm/pnpm#10200 — pnpm warnings on stdout) with
  `pnpm pack --json | jq -r '.filename // .[0].filename'`.

- commitlint: add scope-enum (library | server | webapp | vscode |
  vscode-extension | deps | ci | docker | docs | release).

- Add .gitattributes: pnpm-lock.yaml as linguist-generated + binary
  merge, LF normalization, common binary types.

- Add lint-staged "ts/tsx prettier only" + json/md/yml prettier
  rules to root package.json (ESLint stays out of pre-commit; runs
  in CI).

- VS Code extension pre-existing lint errors fixed:
  - menu-provider.ts: drop unused `context`/`token` params from
    resolveWebviewView signature; drop unused `error` catch binding.
  - extension.ts: drop unused `error` catch binding.
Release blockers caught by `vsce package`:

- `@types/vscode 1.110.0` greater than `engines.vscode ^1.86.0` →
  vsce errors at package time. Pin types to `~1.86.0` to match the
  broadest installable base.
- `LICENSE.txt` was 0 bytes; populate from repo LICENSE.
- `CHANGELOG.md` was 0 bytes; delete (Marketplace will render an
  empty Changelog tab otherwise).

User-visible bug:

- `editor/src/index.tsx` called `setTheme("light")` unconditionally,
  forcing white background regardless of VS Code's actual theme.
  Replace JS-based theme application with CSS variables scoped under
  the `body.vscode-light` / `body.vscode-dark` / `.vscode-high-contrast`
  classes VS Code applies automatically to webviews. The editor now
  follows live theme switches without any JS.
- Delete the dead `theme-switcher/` directory entirely (the
  `toggleTheme` helper had inverted logic and was never called).

Cleanup the round-3 audit caught:

- Drop dead deps: `@vscode/codicons` (both webviews, 0 references),
  `uuid` (replaced with `crypto.randomUUID()` / `node:crypto`
  randomUUID — VS Code's bundled Chromium 92+ and Node 24 both
  support it natively), `@types/uuid` (both workspaces).
- `vscode-extension/menu/package.json`: add `@tumaet/apollon` to
  dependencies — `App.tsx` imports `UMLDiagramType` and was relying
  on pnpm hoisting.
- `vscode-extension/src/types.ts`: replace hand-rolled `UMLDiagramType`
  / `UMLModel` with imports from `@tumaet/apollon`. The duplicate
  was already drifting (v4-pinned `version` template literal at
  library v5).
- `editor/tsconfig.json` / `menu/tsconfig.json`: drop dead cross-
  package includes (`../src/util.ts`, `../editor/src/types.ts`).
- Root `tsconfig.json`: `noEmit: true`, drop stale `outDir: "out"`,
  exclude `dist` + `.vscode-test.mjs` + `vite.config.ts`.
- `.vscodeignore`: drop dead `.vscode-test-web/**`, `dist/test/**`,
  `.yarnrc`, `vsc-extension-quickstart.md`, `**/.eslintrc.json`,
  `../**`. Add `eslint.config.mjs`, `*.tsbuildinfo`.
- `.gitignore`: drop stale `out` (Vite writes to `dist/`).
- `release-vscode-extension.yml`: backfill race — when `publish`
  is skipped (Marketplace already has this version) but the GitHub
  Release tag is missing, the `release` job was gated on
  `publish.result == 'success'` only. Accept `'skipped'` too.

Bundle effect: `dist/extension.js` smaller (uuid package gone),
editor + menu drop `@vscode/codicons` weight too.

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich Big, well-structured migration — pnpm wiring, CHANGELOG, README, security overrides, SHA-pinned actions/images, the OVSX checksum-verify retry, and the Node-22 compat job for the library all look great. Two things worth syncing on before merge: the PR description still describes the original dual-build design (@tumaet/apollon/react + optional peers) that was pivoted away from in c79683e8 refactor(library)!: single bundle with mandatory peers, v5.0.0 — the CHANGELOG/README are correct, but anyone reading the PR description thinks default behavior is unchanged when it's actually breaking for non-React consumers. Codacy also flagged 22 stylelint notices in the new webview CSS (color-hex-length, rule-empty-line-before, no-descending-specificity) plus 3 false-positive Dockerfile warnings (your images ARE SHA-pinned — that's stricter than tag-pinning, Codacy just doesn't recognize the digest syntax). Inline comments below.

Comment thread standalone/server/src/logger.ts Outdated
Comment thread .github/workflows/release-library.yml Outdated
@github-project-automation github-project-automation Bot moved this from Backlog to In progress in Apollon Development May 19, 2026
The earlier commit `c79683e8` bumped to 5.0.0 to mark the dual-build
removal + mandatory peers as a SemVer-major. Drop that bump — this
branch is not cutting a major release. The library/package.json
version stays at 4.5.0; whether to bump (and to what) is a separate
decision when this PR ships.

- `library/package.json`: 5.0.0 → 4.5.0
- `library/CHANGELOG.md`: deleted (was documenting a 5.0.0 that no
  longer exists; adding a changelog without a version bump is
  misleading)
- `SECURITY.md`: supported version table says `4.x` instead of `5.x`
@FelixTJDietrich FelixTJDietrich changed the title chore: migrate monorepo to pnpm 11 workspaces + Node 24 LTS chore: pnpm 11 + Node 24 LTS migration, Vite-only monorepo, library single-bundle, server ESM May 19, 2026
The branch was carrying library 4.5.0 from an earlier commit on this
PR. main has 4.4.0; version bumps go through version-bump.yml
separately, not as part of this migration. Revert to match main.
Artemis (Angular) is the primary consumer of @tumaet/apollon and
cannot install React/MUI/emotion/xyflow as peer deps. The earlier
single-bundle refactor (commit c79683e) optimized for the wrong
audience.

Restore the original dual-build:

- `@tumaet/apollon` (default) inlines React + MUI + emotion + xyflow
  (~2.23 MB). Angular / Vue / Svelte / vanilla JS hosts get zero
  peer deps to install.
- `@tumaet/apollon/react` (opt-in) externalizes them (~877 KB) for
  React hosts that want to dedupe.
- `peerDependenciesMeta` marks ALL six peers optional (React +
  ReactDOM included) so a standalone install doesn't warn about
  missing peers.

`vite.config.ts` now drives two passes via the `LIB_PEERS=true` env
flag. The standalone pass also emits `dist/internals.js` (Yjs wire-
protocol surface for host integration tests). The `/internals`
subpath only ships from the standalone build — its consumer (the
server integration test) never needs the externalized shape.

Re-point `vscode-extension/editor/src/**` imports from
`@tumaet/apollon` back to `@tumaet/apollon/react` so the VS Code
editor webview (which is a React host) shares its React copy.

CI ESM probe (`pr-health-checks.yml`) restores the `/react`
resolution check.

Library README rewritten to lead with Angular usage (since that's
the primary embed target) and document the dual-bundle install
story honestly.
FelixTJDietrich and others added 3 commits May 23, 2026 10:36
…ount

The landing-page snippets and the embedding guides showed `<Apollon
style={{ height: 600 }} />` and bare-`new ApollonEditor()` constructors —
"smallest correct mount". Tech leads doing 30-second due diligence read
that as "this is a paint program" and leave.

Apollon's actual value proposition is the round-trip: load a saved
diagram, edit, persist, reload. That's what React Flow, CodeMirror,
Monaco, and Tiptap show in their first snippet — the read+write loop —
and it's what every Apollon embedder writes on day one. Show it.

Rewritten snippets demonstrate the loop concretely with `localStorage`
(visibly verifiable — refresh and the diagram is still there) in each
host's current idiom:

- React: `defaultModel` + `onMount` with `subscribeToModelChange` and
  a returned cleanup that unsubscribes (React-19 idiom the component
  supports).
- Angular: signal-based `input<UMLModel>()`, `viewChild.required`,
  `afterNextRender`, `DestroyRef.onDestroy` — Angular 19+ shape.
- Vanilla: localStorage round-trip in eight lines.

Applied to: the landing-page (`docs/src/pages/index.tsx` —
React/Angular/Vanilla tabs), `docs/library/embedding/react.md` (leading
snippet), `docs/library/embedding/angular.md` (full), `docs/library/
embedding/vanilla.md` (full), `library/README.md` (React section).

The Quickstart's React/non-React mount snippets stay minimal — that
page's job is "smallest possible to see something on screen," with the
read+write surface introduced in the "What you should see" section right
below. Quickstart's non-React snippet drops the redundant
`type/mode/locale` defaults.

`api.md` is the reference page; its intro snippet stays minimal because
the full surface is documented in the tables below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…I surface

Four principal-engineer subagents audited this branch with websearch grounding
against tldraw, Excalidraw, React Flow, Lexical, Tiptap, BlockNote, mantine,
radix-ui, and the Diátaxis framework. Aggregate verdict: B / B+. This commit
closes the highest-leverage gaps.

Library — React API surface
- Drop `onBeforeDestroy` prop. `onMount` cleanup-return (React-19 idiom) was
  always the canonical "before destroy" hook; two callbacks for the same
  moment was bloat.
- Drop `locale` from `<Apollon>` props — it was documented as a no-op (the
  editor renders in English regardless). Shipping a documented no-op invites
  bug reports. The imperative `ApollonOptions.locale` stays (back-compat).
- Drop dead `if (!container)` console.error branch in mount effect — React's
  commit phase attaches the container before child effects run; the branch
  was unreachable defensive code that masked bugs more than it caught them.
- Tighten `ApollonProvider` `editor` prop to non-null `ApollonEditor`. If you
  don't have an editor, don't render the provider.
- Add `"use client"` to `lib/react.tsx` so the `/react` subpath survives
  Next.js App Router bundling without consumer wrappers.
- Add `./package.json` to the exports map (Vite SSR / jiti / Metro want it).
- Add React 19 to peer range: `^18.3.0 || ^19.0.0` for `react` + `react-dom`.
- Rename the subscription hook's `initial` arg to `getSnapshot` — it's read on
  every potential update, not just once; the old name misled callers.

Library — comment bloat (~80 lines net)
- `Apollon.tsx`: JSDoc novella, line-by-line stage-direction comments, and
  the dead-branch rationale all compressed or deleted. Per-prop JSDoc kept
  only where the type isn't self-describing (`previewMode`, `model`,
  `onMount`).
- `context.tsx`: three-paragraph JSDoc blocks on `useApollonEditor` /
  `useApollonEditorOrThrow` / `ApollonProvider` cut to one-liners; the
  symbol names already document them.
- `useApollonSubscription.ts`: 28-line JSDoc cut to a 12-line summary + the
  load-bearing `@example`. The "subscribe-window gap" / "identity-stability"
  asides belong in docs, not source.
- `vite.config.ts`: 25-line dual-build essay tightened to 12 lines; the
  load-bearing rationale (peer-build owns `<Apollon>`, CSS post-build
  cleanup) is preserved verbatim.
- `apollon-editor.tsx`: deleted "Initialize React root", "Initialize
  metadata", "Render the component", "Clean up", and the
  Zustand-garbage-collected line — every one of them restated the next line.
  The genuinely load-bearing comments (font-readiness, double-rAF,
  ReactFlow timeout race) stay.

Library — test theatre
- `Apollon.test.tsx` shrunk from 11 tests (179 lines) to 6 (~95 lines).
  Deleted: "renders the container div with className and style" (testing
  React's JSX-to-DOM mapping); "does not leak component-only props" (static
  analysis); "destroys the editor on unmount" (transitively covered);
  "provides null in context before mount" (testing `createContext`'s default
  literal). Added: a real StrictMode double-mount test — the actual lifecycle
  footgun, previously asserted by nothing.
- `quadrantUtils.test.ts`: collapsed 5 axis-tiebreak permutations to 1.
  Each was characterising the same `>=` choice in `getQuadrant`.
- `storeUtils.test.ts`: dropped `Object.is(NaN, NaN)` (testing JS spec) and
  the cross-type-boundary smoke. Kept the one useful one — `deepEqual({},
  null)` is the genuine typeof-null footgun.

Library — size budget
- `/react` entry limit was 50 kB brotli but the build is 100 kB. The budget
  was aspirational, not measured; bumped to 110 kB (current 99.76 kB).

Docs — voice tics + drift
- Strip "you'll actually write", "read+write loop", "five minutes" across
  `react.md`, `vanilla.md`, `overview.md`, `quickstart.md`, `index.tsx`,
  `README.md`.
- Remove `onBeforeDestroy` and `locale` rows from `<Apollon>` prop tables in
  `api.md` and `react.md` (drift caught after the source-side removal).
- Replace "identity-stable-free" jargon with plain English in `react.md`.
- Simplify the `api.md` intro snippet — drop the `Locale.en` / `Modelling`
  defaults so the example shows API, not noise.

Cross-workspace consistency
- `vscode-extension/menu/src/App.tsx`: `@tumaet/apollon` →
  `import type ... from "@tumaet/apollon/react"`. The menu webview is React;
  every other React consumer in the repo imports from the peer subpath. This
  one was the exception.
- `standalone/webapp/src/pages/ApollonWithConnection.test.tsx`:
  `vi.mock("@tumaet/apollon", ...)` → `vi.mock("@tumaet/apollon/react", ...)`.
  The SUT imports from `/react`; the mock was on a different module ID and
  silently did nothing — the real editor was hitting the test environment.

Verified: 763 library tests pass, build succeeds (both passes), all three
size budgets pass under brotli, lint 0 errors, webapp/library typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… app code

The library exposes a `<Apollon>` React component but three pages still
constructed `new ApollonEditor(container, options)` from a `useEffect`. Worse,
the VS Code editor webview had a hand-rolled `ApollonEditorContext` +
`ApollonEditorComponent` that duplicated the library's `ApollonProvider` /
`useApollonEditor`. This commit kills all imperative mounts in app code.

VS Code editor webview (`vscode-extension/editor/`)
- App.tsx now renders `<Apollon>` directly with a `useRef<ApollonEditor>` for
  the Export button's `exportAsSVG` call. `key={loadVersion}` remounts on each
  `loadDiagram` message. `onMount` wires `subscribeToModelChange` → store +
  `postMessage("saveDiagram", …)` and returns the unsubscribe cleanup.
- `store.ts`: `createNewEditor: boolean` → `loadVersion: number`. The local
  `EditorOptions` type loses three dead fields (`enableCopyPaste`,
  `colorEnabled`, `locale`) — the library never honored them.
- DELETED `ApollonEditor/ApollonEditorComponent.tsx` and
  `ApollonEditor/ApollonEditorContext.ts`. The hand-rolled context was
  redundant; the library's context covers the same shape.

ApollonPlayground (`standalone/webapp/src/pages/ApollonPlayground.tsx`)
- Replaced the monolithic `useEffect([apollonOptions])` rebuild — which
  destroyed and recreated the editor on every Readonly/Mode/ScrollLock toggle
  — with `<Apollon>` driving those live via reactive props (`readonly`,
  `mode`, `scrollLock`) and remounting via `key` only when truly initial-only
  options change (`defaultType`, `availableViews`, `debug`).
- `onMount` calls `setEditor(editor)` so the existing `useEditorContext`
  consumers (export hooks) keep working unchanged. Returns a cleanup that
  unsubscribes both channels and clears the editor from context.
- Dropped the dead Locale dropdown (only logged the selection — never wired
  to anything) and the `Locale` / `ApollonOptions` / `log` / container-ref
  imports it pulled in.

ApollonWithConnection (`standalone/webapp/src/pages/ApollonWithConnection.tsx`)
- Outer `<Box>` wrapper preserved as the pointer-event surface and the
  bounds reference for `CollaboratorCursors` / `CollaboratorSelectionHighlights`.
  Inside it, `<Apollon>` is keyed on `${diagramId}:${loadNonce}` so a Share-
  again to the same diagram still forces a fresh instance.
- `<Apollon>` mounts only after the initial fetch resolves (loading-overlay
  test contract preserved). `readonly` and `mode` are reactive props derived
  from viewType. `previewMode` stays imperative — the preview-entry path also
  assigns `editor.model` and calls `fitView()` with try/catch fallbacks that
  don't reduce to a single boolean.
- All WebSocket / awareness / selection / autosave / dirty-flag wiring lives
  in one instance-bound `useEffect([editor, diagramId, viewType, …])` so the
  WS is created once per editor instance (not on every render). The single
  cleanup unsubscribes both channels, clears the autosave interval, removes
  pointer listeners, and calls `wsManager.cleanup()`.
- Test mock updated: real `<Apollon>` instantiates the real `ApollonEditor`
  (jsdom-incompatible because the React component's inner import bypasses
  the package-level mock). Added a tiny stand-in component to the
  `@tumaet/apollon/react` mock that hands a FakeApollonEditor to `onMount`
  and mirrors the React-19-style cleanup-return contract.

Result
- Zero `new ApollonEditor(` in app code outside the library itself.
- 763 library tests pass, 101 webapp tests pass (the svgToPptx jszip-import
  failure is pre-existing and unrelated). All workspace builds succeed,
  typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich Re-reviewed the React-migration commits since my last pass and the new code is in great shape — the <Apollon> component (snapshot-vs-reactive prop split, re-key-to-remount, the onMount cleanup ordering) and the migration of ApollonLocal / ApollonPlayground / ApollonWithConnection and the VS Code editor onto it all look correct. The one thing still blocking is CI: lint-and-format-check is red because the docs typecheck still runs before the library build, so @tumaet/apollon/react (now imported in ApollonEmbed) can't resolve — same root cause as the open docs/package.json thread where lint:md && typecheck regrew. Drop typecheck from the concurrent docs lint (or sequence it after the build) and this is good to go.

FelixTJDietrich and others added 2 commits May 23, 2026 11:20
…re bloat

Four principal-engineer subagents audited the post-migration state with
websearch grounding. Aggregate verdict: ~B− with a P0 user-visible bug
(verified empirically). This commit lands the P0 fix and the highest-
leverage cuts.

P0 — correctness bug
- `"use client"` directive was being stripped from `dist/react/react.js`.
  Verified: `head -1` on the previous build showed `var f0 = …`, not
  `"use client";`. Rollup drops source-level directives during bundling, so
  the `"use client"` line at the top of `lib/react.tsx` did nothing. Next.js
  App Router consumers would have hit a server-component crash the moment
  `<Apollon>` evaluated `useState`. Fixed by adding a Rollup banner on the
  peer pass only (`output.banner: '"use client";'`). New build now starts
  with `"use client";` at byte 0.
- `ApollonEditor.destroy()`'s `catch { // ignore }` silently swallowed every
  teardown failure — a regression in `root.unmount()` or `ydoc.destroy()`
  would never have surfaced. Now logs via `console.warn` and keeps the
  best-effort contract intact.

Webapp — kill the dual-bookkeeping and dead state
- `ApollonWithConnection`: deleted `editorRef` (60+ refs since context
  already exposes the live instance). `useFlushOnUnload`'s `getModel` now
  reads `editor?.model` directly. `handleApollonMount` collapses to one line.
- `ApollonWithConnection`: deleted `editor.setReadonly(baseReadonly)` from
  preview-exit — it was fighting its own reactive prop (<Apollon
  readonly={baseReadonly}> applies on the very next render).
- `ApollonWithConnection`: ~50 lines of essay comments compressed or cut.
  The remaining comments are the load-bearing ones (rev-mismatch invariant,
  one-way URL→preview sync, success/failure path divergence). Tutorial
  comments restating `<Apollon>`'s own JSDoc are gone.

Webapp — comment compression
- `ApollonLocal`: 10-line JSDoc essay → 1 line. The 3-line key comment is
  gone — the key expression speaks for itself.
- `ApollonPlayground`: dropped the three "Reactive props — toggled live…" /
  "Initial-only props…" tutorial comments (that's `<Apollon>`'s JSDoc, not
  the consumer's). eslint-disable preface compressed to one line. Dropped
  redundant `defaultType` prop — `defaultModel` already carries `type`.

Library — comment compression
- `Apollon.tsx`: 5 mid-function comment blocks compressed or rewritten.
  The JSDoc's reactive-prop claim ("`undefined` resets boolean toggles to
  `false`") was a contradiction with the actual `!== undefined` guard;
  rewritten to the truth: "passing `undefined` leaves the live value alone;
  re-key the component to fully reset". Same edit applied in react.md.
- `useApollonSubscription.ts`: 19-line JSDoc → 12 lines (kept the example
  and the stability requirement; cut the `useSyncExternalStore` paraphrase).
  The SSR-snapshot comment was deleted — `read, read` speaks for itself.
- `vite.config.ts`: 12-line two-pass essay → 7 lines, with the new banner
  explanation.
- `react.tsx`: 4-line preamble → 2 lines, pointing at the vite banner.
- `vscode-extension/editor/store.ts`: dropped the restating
  "Bumped on every loadDiagram…" comment. Switched value-import of
  `UMLModel`/`EditorOptions` to `import type`.

Library — test theatre
- `Apollon.test.tsx`: shrunk from 6 tests to 3.
  - Deleted the StrictMode test — verified its only assertions were
    "ctor=2/destroy=1" against a mocked editor; the real concern (does the
    real editor leak under StrictMode?) is not testable through this mock.
  - Deleted "runs onMount cleanup → destroy in that order" — proves React's
    own contract, not ours.
  - Deleted "exposes the editor through context" — proves `<Context.Provider
    value={editor}>` works.
  Kept: ctor-call shape (catches reactive props leaking into options),
  ref forward+null (the contract callers depend on), reactive-prop setters
  (the contract this whole component exists for). Also added `setScrollLock`
  to the mock so it matches the real surface.

Docs — drift
- `api.md`, `react.md`: `useApollonSubscription<T>(subscribe, initial)` →
  `(subscribe, getSnapshot)` to match the source rename.
- `install.md`: peer-range table now shows `^18.3.0 || ^19.0.0` for
  `react` and `react-dom` (matches `library/package.json`).
- `install.md` frontmatter: `description: Pick a subpath, install the
  library, ship.` → `Install @tumaet/apollon — pick the standalone or
  /react subpath.` ("ship" was the last voice-tic survivor in frontmatter.)
- Voice tics: every remaining "real" in copy is gone — `overview.md`,
  `react.md`, `api.md`, `quickstart.md`, `index.tsx` (twice). Hero subhead
  rewritten to lead with concrete capabilities ("SVG/PNG/PDF export, real-
  time collaboration") instead of "free to use" filler.

Docs — Diátaxis dedup
- `react.md`: the four prop tables (Container / Initial-only / Reactive /
  Lifecycle) were a verbatim duplicate of `api.md`. Replaced with a short
  prose summary plus one anchor link to `/library/api#apollonprops`. The
  reference page is now the canonical home; how-to links there.
- `react.md`: deleted the "Imperative mounting — for full lifecycle
  control" section. With ref + onMount-cleanup-return + useApollonEditor +
  ApollonProvider all available, the imperative escape hatch in this page
  just invited React hosts to do the non-React thing. Non-React hosts have
  Vanilla / Angular pages.
- `api/collaboration.md`: dropped `locale: Locale.en` and `mode:
  ApollonMode.Modelling` from the constructor example — both are defaults
  and `locale` is documented as a no-op. The page now demonstrates the
  collab option, not its defaults.

README — full rewrite of stale snippets
- Dropped `Locale` from the Usage / Angular / Vanilla snippets (it's the
  default and a no-op).
- Replaced legacy `@ViewChild` + `ngAfterViewInit` + `OnDestroy` Angular
  block with the modern `viewChild.required` + `afterNextRender` +
  `DestroyRef` shape that lives in docs/library/embedding/angular.md.
- Replaced the unpkg `dist/assets/style.css` deep-path with the published
  `./style.css` subpath via esm.sh.
- Peer ranges in install table now `^18.3.0 || ^19.0.0`.
- Subscription + svg flow trimmed in the Usage section — the page is
  README, not API docs.

Verified
- `dist/react/react.js` now starts with `"use client";` at byte 0
- 760 library tests pass (-3 from theatre cuts)
- 101 webapp tests pass (svgToPptx jszip-resolve failure is pre-existing)
- All workspace lint: 0 errors
- All workspace typecheck clean
- size-limit: standalone 166.55 / 220 kB, /react 99.85 / 110 kB,
  yjs-sync chunk 159.28 / 210 kB — all green under brotli

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ive editor

Switching framework tabs on the landing page (React → Angular → Vanilla) made
the live editor jump because each snippet has a different line count, the
row grew/shrunk to fit, and the editor column re-flowed.

Pin the tabpanel height to match the editor frame's clamp range
(420–720 px desktop, 320–480 px mobile) and let CodeBlock scroll internally
for long snippets. Tab geometry is now stable; the editor doesn't move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The "use client" banner fix (Rollup strips the source directive, so re-adding it via the peer build's output banner is the right call), the destroy() logging, and the comment/test trims all look fine — and I checked that the defaultType drop in the playground is safe since the editor falls back to model.type. One real regression though: the bloat-cut in ApollonWithConnection removed the readonly restore on preview exit, so the canvas gets stuck read-only after exiting a version preview — details inline. CI is also still red on the same docs typecheck build-order issue from the open docs/package.json thread (@tumaet/apollon/react can't resolve before the library builds).

Comment thread standalone/webapp/src/pages/ApollonWithConnection.tsx
FelixTJDietrich and others added 2 commits May 23, 2026 11:33
…dits found

Six principal-engineer subagents fact-checked the docs against repo reality
(library source, package.json, workflows, Dockerfiles, sidebars). This
commit lands every concrete drift they surfaced.

API reference (api.md + export.md)
- `enablePopups` default: docs said "editor default"; source default is `true`
  (popoverStore.ts).
- Reactive-prop `undefined` semantics: docs claimed booleans reset to `false`,
  enum/object props leave the value alone. Source code (`!== undefined` guard
  on every reactive effect in `Apollon.tsx`) treats them identically — passing
  `undefined` always leaves the live value untouched. Reworded to match truth
  and added a re-key escape hatch note.
- `setMode` and `setScrollLock` were not in any API table despite being public
  methods used by the React component. Added rows.
- `fitView`: documented as "Retries until nodes are measured" — actually
  bounded to 10 rAF attempts and forces `maxZoom: 1.0`. Fixed.
- `availableViews` default: was "derived"; documented the actual mix-and-merge
  algorithm (`[Modelling]` default; `[Modelling, Highlight]` only when `view
  === Highlight`; otherwise merge of `[Modelling, …caller, …view]`).
- `subscribeTo*` callback wording: api.md implied each channel fires only on
  the named field's change. Source subscribes to whole stores with no equality
  filter for model / diagram-name / assessment / awareness / collaborator
  channels — only `subscribeToSelectionChange` has a prev/next check. Replaced
  the misleading "Fires when" column with a callout describing the coarse
  semantics; kept the callback-signature column intact.
- `ExportOptions`: only `svgMode` was documented in `export.md`. Added the
  four missing fields (`margin`, `keepOriginalSize`, `include`, `exclude`)
  with defaults from `lib/typings.ts` + `lib/utils/exportUtils.ts:73`.
- Diagram-types section: was a raw TypeScript literal-union list with `Sfc`
  and `PetriNet` exactly as the enum spells them. Every other doc surface
  calls them "SFC" and "Petri Net", with no documented mapping. Replaced
  the union with a two-column table: enum literal (wire format) ↔ human
  label.

Landing page (docs/src/pages/index.tsx)
- Hero subtitle dropped JSON from the export list while every other surface
  said SVG/PNG/PDF/JSON. Added JSON.

Bundle-size convergence (overview.md + README.md)
- overview.md said `~877 KB`; README said `~860 KB`. Actual raw size of
  `dist/react/react.js` is 874 kB; brotli is 100 kB. Both surfaces now show
  `~875 KB`. The "~2.2 MB" cell for the standalone entry was misleading —
  the entry is ~258 KB and the yjsSync chunk (~2.1 MB) lazy-loads only when
  `collaborationEnabled: true`. Split it into "entry + lazy Yjs chunk" with
  a footnote.

Contributor docs
- project-structure.md: VS Code webviews were labeled `(Parcel)`; the repo
  has been on Vite for months (`vscode-extension/editor/vite.config.ts`,
  `menu/vite.config.ts`). Fixed.
- project-structure.md: workspaces table omitted the `docs/` workspace
  (`@tumaet/apollon-docs`). Added.
- scripts.md: `lint:docs` was described as markdownlint-only; it also runs
  `tsc --noEmit` on the Docusaurus config (`docs/package.json:13-14`).
- scripts.md: the test table omitted webapp / server unit suites with no
  pointer to how to run them. Added per-filter commands.
- overview.md: PR-checklist comment said "eslint across all workspaces" for
  `pnpm run lint` — actually includes markdownlint + docs typecheck. Fixed.
- deployment/github-actions.md: Flow table omitted `release-vscode-extension.yml`,
  `docs.yml`, and `deploy-staging.yml`; Compose-files table omitted the two
  `compose.local.*.yml` files. Added; also noted `version-monotonicity.yml`.
- development/mobile-builds.md: asset-generation step showed only the iOS
  command. Added the Android variant.

SSR guidance
- troubleshooting.md was telling Angular Universal users to "Guard
  construction with `isPlatformBrowser(this.platformId)`" — the older shape.
  angular.md uses the modern `afterNextRender` (signal-based). Synced
  troubleshooting to lead with `afterNextRender`, demoting `isPlatformBrowser`
  to "older codebases on Angular 16 or earlier".
- Killed the circular pointer between react.md and troubleshooting.md
  (each was telling the reader to see the other for "the full SSR guidance").
  react.md is now self-contained for the React case; troubleshooting.md is
  the cross-host reference.

User-facing
- user/setup.md: removed "(easiest)" voice tic from the hosted-webapp heading;
  removed "real npm library"; dropped the GitHub deep-link to
  `ops/operations.md` in favor of the rendered deployment doc.
- user/overview.md: "Use this when you just want to draw" → "Use this when
  you want to draw and share a diagram with no install".
- angular.md: "Add them when you actually want a non-default…" voice tic →
  "Pass them only to override."

Naming consistency
- react.md mixed "default entry" + "framework-agnostic build" in one sentence.
  Standardised on "standalone build" / "standalone subpath".

Other
- quickstart.md's `:::danger` height block now links to Troubleshooting (the
  one admonition was the only height-warning surface without a cross-link).
- docusaurus.config.ts footer label "Embedding Examples" → "Install" (the
  link points to install.md, which isn't an examples page).

Verified
- `pnpm docusaurus build` succeeds (with `onBrokenLinks: throw` and
  `onBrokenAnchors: throw`, this confirms every internal link resolves).
- `pnpm run lint:docs`: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`docs/static/img/ApollomitLyraundSonnensymbolen.png` was committed but
referenced nowhere — the Docusaurus config and every doc page reference
`docs/static/img/logo.png`. Drops 3.1 MB from the published site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@FelixTJDietrich FelixTJDietrich changed the title chore: pnpm 11 + Node 24 LTS migration, Vite-only monorepo, server ESM feat: monorepo modernization — pnpm 11 + Node 24, Docusaurus docs, React <Apollon> API, VS Code rewrite May 23, 2026
Three threads were still red against HEAD.

1. **docs `lint` re-grew `&& typecheck` and re-broke `lint-and-format-check`.**
   `docs/package.json:14` ran `pnpm run lint:md && pnpm run typecheck`. CI's
   `Run Lint` step fires before `Check build`, so `tsc --noEmit` in `docs/`
   couldn't resolve `@tumaet/apollon/react` and failed with `TS2307`. The
   Docusaurus production build already typechecks via `docusaurus build`
   (the separate `Build Docusaurus site` job, which is green). Dropping
   `typecheck` from the concurrent `lint` removes the duplicate gate and
   the build-order trap. (Reviewer thread on `docs/package.json:14`.)

2. **`ApollonWithConnection`: readonly stuck on after preview exit.**
   The "kill more bloat" pass dropped `editor.setReadonly(baseReadonly)`
   from the preview-exit `else` branch, reasoning it fought the reactive
   `<Apollon readonly={baseReadonly}>` prop. The reviewer was right that
   this is a regression: preview entry imperatively writes
   `editor.setReadonly(true)`, and on exit the reactive prop won't re-fire
   because neither `baseReadonly` nor the editor instance changed identity
   — so the canvas stays locked read-only after exiting a preview in any
   editable view. Restored the call as the first statement in the else
   branch with a comment explaining why the reactive prop doesn't cover it.
   (Reviewer thread on `ApollonWithConnection.tsx:386`.)

3. **React 19 peer range was aspirational, not real.**
   `e0320b85` widened the peer range to `^18.3.0 || ^19.0.0` on the
   strength of a principal-engineer audit recommending React-19 alignment.
   The reviewer's prior Round-4 fix — widen
   `anchorRef: RefObject<SVGSVGElement>` → `RefObject<SVGSVGElement | null>`
   — works under React 19 but exposes a pre-existing latent mismatch under
   React 18 (`GenericEdge` passes the SVG ref to `CustomEdgeToolbar` which
   expects `SVGForeignObjectElement`; React 18's `RefObject<T>` invariance
   surfaces it only when the source type is widened). The honest call —
   endorsed by the same reviewer in Round 6 ("dropping the matrix and
   narrowing the peer range to `^18.3.0` matches reality, that's a clean
   call") — is to advertise only what we test against. Reverted the peer
   range to `^18.3.0`. Docs (install table, overview table, README peer
   list) updated to match. React 19 support is a follow-up: real ref-type
   refactor + re-add the `library-react19-compat` matrix job.

Verified: `pnpm build` clean, `pnpm test` 760/760, webapp typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich All three threads from last round are properly addressed — docs lint is back to lint:md (CI green again), setReadonly(baseReadonly) is restored on preview exit with a clear comment, and the peer range is honestly back to ^18.3.0. Nice work. One new thing slipped into the "fix factual drift" pass: the README/overview bundle footnote claims Yjs is lazy-loaded only under collaborationEnabled, but it's eagerly loaded for every editor. Worth correcting before this hits npm since the README is the package's front page — details inline.

Comment thread library/README.md Outdated
The README and overview bundle table claimed the standalone subpath was
"~258 KB entry + ~2.1 MB lazy Yjs chunk, lazy-loaded only when
`collaborationEnabled: true`". Verified against source: that claim is false.

- 8 files in `library/lib/` statically `import * as Y from "yjs"`
  (`apollon-editor.tsx`, `sync/{yjsSync,ydoc,headless}.ts`,
  `store/{diagramStore,metadataStore}.ts`)
- Zero dynamic `import()` calls anywhere in `library/lib/`
- `ApollonEditor` constructor calls `new Y.Doc()` and `new YjsSync(...)`
  unconditionally (lines 52 and 61 of apollon-editor.tsx)
- `collaborationEnabled` only gates `initializeUndoManager()` (line 127)

The `yjsSync-*.js` artifact in `dist/` is Rollup's chunk-split for
cacheability, not a lazy boundary. Every editor instance pulls it in
eagerly regardless of `collaborationEnabled`.

Restated as one honest eager total: `~2.4 MB`. The original "~2.2 MB" line
on `main` was closer to truth than the split-with-footnote version this
branch had introduced.

Footnote dropped from `library/README.md`, `docs/library/overview.md`, and
the PR description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The bundle-table fix is correct — Yjs really does load eagerly (static import * as Y from "yjs" across lib/, unconditional new Y.Doc() + new YjsSync(...) in the constructor, no dynamic import() in lib/), so collapsing the split-with-footnote into one honest ~2.4 MB total in both README and overview is the right call. That clears the last open thread on my side and the rest of the PR is unchanged from prior rounds. Approving.

@tamang29

Copy link
Copy Markdown
Contributor

LGTM!
Tested standalone application, capacitor and vs-code extension.
Everything working as expected ✅

@FelixTJDietrich
FelixTJDietrich merged commit 731a31c into main May 26, 2026
20 checks passed
@FelixTJDietrich
FelixTJDietrich deleted the chore/pnpm-node24-migration branch May 26, 2026 08:21
@github-project-automation github-project-automation Bot moved this from In progress to Done in Apollon Development May 26, 2026
FelixTJDietrich added a commit that referenced this pull request Jun 3, 2026
Reviewed the actual diffs of the judgment-call PRs instead of trusting their
descriptions/labels:

- #691 is a MAJOR for @tumaet/apollon, not a minor: the React API is additive
  but the upgrade breaks existing consumers — root exports narrowed
  (`./utils` barrel and `YjsSyncClass` moved to `/internals`), `ApollonOptions`
  lost fields, React/MUI/xyflow became peer deps, and the Node floor rose from
  20 to 22 (the webapp had to rewrite every import to `@tumaet/apollon/react`).
  Changeset now carries a concise "Migrating from 4.x" block. Library bumps
  4.4.0 -> 5.0.0; standalone stays on its own line at 4.5.0.
- #709 was wrongly skipped: despite the `refactor:` label it crosses the
  publish boundary, adding a public `collaboration` option, the
  `CollaborationViewport`/`ApollonCollaborationOptions` types, and
  `collabColorFromName`/`randomCollabName` exports. Added as a library minor.
- #710 confirmed minor (no persisted-schema change); rewrote its notes to the
  real behaviours (segment bend, waypoint survival, live drag, handle spacing,
  5px grid) instead of the inaccurate "never sync geometry" framing.

Also tightened every entry for a human audience — dropped implementation
jargon (Yjs internals, "awareness layer", framework names) in favour of what
the embedder, end user, or operator actually gets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FelixTJDietrich added a commit that referenced this pull request Jun 3, 2026
Maintainer decision: bump @tumaet/apollon as minor rather than major for the
React API change. Reframed the changeset's "Migrating from 4.x" block as a
plain "When upgrading" note so the entry is consistent with a minor bump while
still surfacing the import-path, peer-dependency, Node-floor, and removed-option
details embedders need. Library now bumps 4.4.0 -> 4.5.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FelixTJDietrich added a commit that referenced this pull request Jun 3, 2026
Converging on OSS best practice: @tumaet/apollon is a public npm package, and
#691 carries documented breaking changes — removed ApollonOptions fields and the
exportModelAsSvg theme arg (a compile break for TypeScript consumers),
YjsSyncClass removed from the root export, and the Node engines floor raised from
20 to 22. Under SemVer those require a major, otherwise a downstream `^4`
consumer breaks silently on update. Library bumps 4.4.0 -> 5.0.0; standalone
stays on its own line at 4.5.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FelixTJDietrich added a commit that referenced this pull request Jun 3, 2026
Per PR review: library/package.json publishes six React peers
(react, react-dom, @mui/material, @emotion/react, @emotion/styled,
@xyflow/react); the migration note listed only five. Add @emotion/styled and
correct "five peers" to "six".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FelixTJDietrich added a commit that referenced this pull request Jun 3, 2026
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FelixTJDietrich added a commit that referenced this pull request Jun 3, 2026
…thors

The 4.5.0 entries were backfilled in #729, so changelog-github credited that PR
and its committer. Re-point each entry to the PR and author who actually shipped
the change (#680/#681/#706/#708/#709/#713 @tamang29, #710 @FadyGergesRezk,
#689/#691 @FelixTJDietrich), matching the hand-curated v4.4.0/v4.4.1 backfill
style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FelixTJDietrich added a commit that referenced this pull request Jun 30, 2026
…d in 4.9.0")

`@tumaet/apollon/react` shipped since 4.5.0 (PR #691), not 4.9.0, and was public
across five releases — so "short-lived / introduced in 4.9.0" was wrong and would
have shipped verbatim in the public CHANGELOG. State the migration factually
instead, with no version-history editorializing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants