CodeVetter · Repo Unpack · Contributor handoff
excalidraw/excalidraw
Work in packages/* for editor behavior and excalidraw-app/ for collab, Firebase, share links, and PWA. Read AGENTS.md before touching DOM or types. Run yarn test:typecheck and the relevant vitest file; refresh snapshots with yarn test:update only when the assertion change is intentional. Do not import jotai directly, do not bump element version by hand, and do not assume the collab socket server or Firebase rules are covered by tests.
- AGENTS.md requires new DOM access to go through app.ownerDocument and app.ownerWindow (or the mounted node's ownerDocument and defaultView), not global document/window. Type overrides must use Merge<Base, Overrides> from @excalidraw/common/utility-types rather than Omit intersections. The App class already exposes ownerDocument and ownerWindow getters that follow that rule.
- ESLint rule no-restricted-imports errors on importing 'jotai'. Use packages/excalidraw/editor-jotai.ts inside the editor and excalidraw-app/app-jotai.ts in the app. import/order is warn and wants a newline between groups, with @excalidraw/** treated as external.
- Safe first reads for an editor change: packages/excalidraw/components/App.tsx (constructor and the specific handler), packages/excalidraw/actions/types.ts plus the action file, packages/element/src/types.ts, and packages/element/src/store.ts. For an app change: excalidraw-app/App.tsx initializeScene and onChange, excalidraw-app/collab/Collab.tsx, excalidraw-app/data/firebase.ts, and excalidraw-app/app_constants.ts.
- Before committing, CLAUDE.md says yarn test:update (vitest with snapshot update, watch off) and yarn test:typecheck (tsc). yarn fix runs prettier then eslint --fix. yarn test:all is the full local gate but CI splits it: PRs do not run test:app unless the coverage workflow does. Prefer a targeted vitest path while iterating.
- Do not hand-edit version or versionNonce. mutateElement / newElementWith / bumpVersion own them. History intentionally excludes those fields when applying undo so the result still collabs. Remote scene writes must pass CaptureUpdateAction.NEVER or they enter the local undo stack.
- LocalData.save is paused for the entire collaboration session (pauseSave('collaboration') in startCollaboration, resumeSave in destroySocketClient). A change that writes localStorage during collab will fight that locker. isSavePaused is also true while document.hidden, and flushSave is what unload and blur call.
- Fractional index bugs throw in dev and test (validateFractionalIndices shouldThrow). If a change reorders elements, it must go through syncMovedIndices or syncInvalidIndices. Collab reconcile also checks bound text. Do not sort the elements array ad hoc.
- StrictMode is on in excalidraw-app/index.tsx. The editor comments that the imperative API is invalidated on unmount and must be recreated in componentDidMount. After unmount, get* methods throw if isDestroyed is not checked. Tests and dev code should use window.h.app rather than a stale API reference.
- Collab and Firebase are easy to break without a failing test. excalidraw-app/tests/collab.test.tsx mocks both firebase.ts and socket.io-client. There is no in-repo server. Manual checks need VITE_APP_WS_SERVER_URL, VITE_APP_FIREBASE_CONFIG, and the backend V2 URLs from vite-env.d.ts. Docker builds set VITE_APP_DISABLE_SENTRY=true so container smoke tests will not report errors.
- Package builds are esbuild via yarn build:esm (scripts/buildPackage.js) and must be run before the browser-script example. The app and tests alias source, so you usually do not need a package build to iterate inside the monorepo. Publishing is scripts/release.js; do not bump package.json versions by hand unless you are cutting a release.
- Known traps: image cannot be set via props.activeTool; #room= keys must be length 22; database JSON strips files so images must be saved separately; clearAppStateForLocalStorage drops transient UI such as the canvas search sidebar; opening the search tab is not persisted. Feature flag COMPLEX_BINDINGS defaults off and lives only in localStorage.
- Type imports must use import type (verbatim style, separate type imports) or eslint errors. The repo is TypeScript strict. When overriding a field, follow AGENTS.md and use Merge. Tests that render the editor should go through packages/excalidraw/tests/test-utils and helpers/api.ts (the collab test imports API, act, render, and waitFor from there).