feat: monorepo modernization — pnpm 11 + Node 24, Docusaurus docs, React <Apollon> API, VS Code rewrite - #691
Conversation
- 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
5d2c95b to
17fe30b
Compare
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.
17fe30b to
08f4dd6
Compare
- 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
left a comment
There was a problem hiding this comment.
@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.
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`
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.
…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
left a comment
There was a problem hiding this comment.
@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.
…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
left a comment
There was a problem hiding this comment.
@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).
…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>
<Apollon> API, VS Code rewrite
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
left a comment
There was a problem hiding this comment.
@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.
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
left a comment
There was a problem hiding this comment.
@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.
|
LGTM! |
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>
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>
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>
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>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
…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>
Summary
This branch does three things that ship independently of each other:
<Apollon>component plusApollonProvider,useApollonEditor,useApollonEditorOrThrow,useApollonSubscription. React-19-styleonMountcleanup return,"use client"directive that actually survives the bundle. Webapp + VS Code editor migrated; zeronew ApollonEditor(...)in repo outside the library itself.docs.ymlworkflow on every push tomain.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/reactsubpath 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:
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 onmain.requirements.txt/Makefile/bin/plantuml.jarmachinery 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
mainships in an awkward in-between state for two of the three weeks.What changed, by concern
Build & toolchain
packageManager).nvmrc,engines, Dockerfiles, all CI runners)tsc)"type": "module",NodeNext,verbatimModuleSyntax)package-lock.jsonpnpm-lock.yamlThe server migration alone is 22 source files / 76 import specifiers gaining
.jssuffixes plus a staticimport { ApollonEditor, importDiagram, ... } from "@tumaet/apollon"replacing the cargo-cultloadApollonModule = () => import(...)lazy wrapper. Express 5's automatic async-handler propagation letsroutes/conversion.tsandmiddleware/validate.tsdrop their try/catch ceremonies.Library — public API
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 smallApollon.test.tsxthat asserts the contract three ways (constructor argument shape, ref forwarding, reactive-setter dispatch).Imperative
ApollonEditoris 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 withvar f0 = …, not"use client";. Re-added via Rollupoutput.banneron the peer pass only.head -1 dist/react/react.jsnow prints"use client";— Next.js App Router consumers don't need a wrapper.Library — bundle shape
@tumaet/apollon(default)@tumaet/apollon/react@tumaet/apollon/internals@tumaet/apollon/style.csspeerDependenciesMeta.optionalmarks all six peers optional, sonpm install @tumaet/apollonnever warns about missing React/MUI/etc. — even though the default subpath inlines them.VS Code extension
setTheme("light")vscefrom a laptopDropped:
@vscode/webview-ui-toolkit(Microsoft sunset 2026-01-06),styled-components,uuid,@vscode/codicons. Replacement is native HTML elements styled viavar(--vscode-*)per Microsoft's post-deprecation guidance. The editor webview imports from@tumaet/apollon/reactto 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.ymlon everymainpush.onBrokenLinks: throwandonBrokenAnchors: throwkeep cross-refs honest.Monorepo hygiene
pnpm-workspace.yaml).HEALTHCHECKin both Dockerfiles; digest-pinned base images.SECURITY.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.editorconfig,.gitattributesadded.helpers:pinGitHubActionDigestsso SHA pins don't rot.release-library.ymlhardened against pnpm 11's varyingpnpm pack --jsonoutput shape (pnpm/pnpm#10200).Required GitHub setup before the first VS Code Marketplace release
release-vscode-extension.ymlis gated on avscode-marketplaceEnvironment that doesn't exist on this repo yet — the workflow will hang on approval until it's created:vscode-marketplacemainVSCE_PAT— Azure DevOps PAT, scope Marketplace → Manage, 1-year max lifetimeOVSX_PAT— Open VSX PAT. Thetumaetnamespace must exist on open-vsx.org first; any namespace member can create it once withovsx create-namespace tumaet -p <PAT>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 inlibrary/vite.config.ts. The questions worth answering:Apollon.tsxJSDoc lists which is which. Passingundefinedto a reactive prop leaves the live value alone — re-key to fully reset.onMount's cleanup-return ordering make sense?consumer cleanup → editor.destroy() → null the ref. The comment inApollon.tsxexplains why; the testApollon.test.tsxpins it."use client"banner the right escape hatch? Rollup strips the source directive. Thevite.config.tscomment +head -1 dist/react/react.jsshould answer it.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"outsidelibrary/should return zero hits.ApollonWithConnection.tsxis the most complex consumer (collab cursors, version preview, autosave, WS) — focus there.Server: 22 files gain
.jsimport suffixes. The substantive changes are inconversion-service.ts(static import replacing lazy wrapper) and the Express-5 async-handler simplifications inroutes/conversion.ts+middleware/validate.ts.VS Code:
editor/src/App.tsxnow renders<Apollon>with akey={loadVersion}for "loadDiagram" remounts; the hand-rolledApollonEditorContext/ApollonEditorComponent(which duplicated the library'sApollonProvider/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 newlibrary-node22-compatjob inpr-health-checks.yml(the library still advertisesengines.node: ">=22.0.0"for downstream consumers even though the monorepo is on 24).The
vscode-marketplaceenvironment 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: throwindocusaurus.config.tsmeans the production build is a transitive link-check.Steps for testing
The standalone resolves with no peer deps installed (Artemis case). The
/reactsubpath resolves only whenreact,react-dom,@mui/material,@emotion/{react,styled}, and@xyflow/reactare present. Both share the same./style.cssexport.Out of scope (deliberately deferred)
version-bump.ymlhandles release bumps post-mergearethetypeswrongCI gatebb72249b— fflate streaming bug prevents tarball extraction on our shape;publint+ exports-map check still cover it^18.3.0matches reality. Widening requires a ref-type refactor (anchorRef: RefObject<SVGSVGElement | null>plus call-site adjustments where the SVG ref crossesSVGForeignObjectElementboundaries)'unsafe-inline'in VS Code webviewscspNonceoption threaded into emotion'screateCachetryAutoVersionracesflushDb)main— verified by baselineexport *barrel fromlib/index.tsxstill leaks ~33 unused identifiers (caught byknip)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
main) — release bump handled byversion-bump.ymlpost-merge/react"use client";directive present at byte 0 ofdist/react/react.js(the previous bundle was silently missing it)onBrokenLinks: throw,onBrokenAnchors: throw)