AI agents

Pitfalls and invariants

Rules that must stay true when integrating or changing @skaile/forge-editor: CSS order, mandatory context, SSR, mount-time props, bundle isolation, entries.

Check this list before and after every change to an integration. Each bullet is a failure that does not announce itself at compile time.

  • CSS order. core.css after Tailwind and Nuxt UI, feature sheets after it, themes.css last, and @source so Tailwind sees the library. The exact order per host is in CSS and UI-provider requirements.
  • Editor context is mandatory. EditorSurface, SimpleEditor, AdvancedEditor and EditorToolbar throw without app.use(createEditor()) (the Nuxt module does this in its client plugin); the React components throw without <EditorContextProvider context={createEditorContext()}>, and the React host owns (disposes) that context.
  • Notices. Features send user-facing messages through ctx.notify / the context's notify (Notifier, silentNotifier drops them). Vue: the queued notifier hands them to Nuxt UI toasts once a surface mounted (needs <UApp>). React: pass createEditorContext({ notify: reactNotifier }) and mount sonner's <Toaster /> (exported by /react) once; without it no notice is visible.
  • UI provider. Wrap the page in <UApp>; plain Vue also needs app.use(ui) (@nuxt/ui/vue-plugin) and the ui({ router: false }) Vite plugin.
  • SSR. Editors are client-only. Nuxt apps in this repository run ssr: false; with SSR enabled wrap editors in <ClientOnly>.
  • Mount-time props. The preset (schema, features), collaboration, abilities, extensions, commands, blockActions, blockTypes, blockUi, coreContent, documentId are read once at setup. Change them by remounting with a new key. editable, metadata and the model stay reactive. Build presets in <script setup> (Vue) or a component/useMemo (React), not on every render.
  • Stable props. Pass metadata, abilities and extensions as constants; a new object per render replaces the lists each time.
  • Schema match. A simple editor rejects advanced JSON through the contentError event (the last valid document stays). collaboration needs a preset containing collab; commentsFeature() needs collab.
  • Bundle isolation. core/settings import no feature; a feature imports core, settings and its own schema/protocol, never another feature; integrations import only the features they connect; the simple bundle contains no feature code, Yjs, AI SDK, Nuxt or server modules. Keep it that way when adding code, and prove it with check:editor-entries. Rules: Dependency rules.
  • Entries. Server code imports only framework-free entries (/schema, /protocol, /types, /settings) or /server. A new public entry needs an explicit package.json#exports row; there is no wildcard.
  • Icons. i-lucide-* names resolve at build time inside the Vite root; a workspace library lies outside it. Bundle them (see examples/vue-simple/vite.config.ts) to avoid Iconify API requests at runtime.
  • React hosts. CSS: Tailwind, tw-animate-css, then either .../react/styles/ui-vars.css, core.css and the feature sheets, or for the advanced editor the single .../react/styles/advanced.css; the shadcn theme block, and @source to the whole dist of the package (node_modules/@skaile/forge-editor/dist; packages/editor/src in this repository), never just react: the Features' components are in features/*/react; icons: addCollection(lucide) from @iconify/react before rendering; <Toaster /> for notices. The preset, metadata, abilities, extensions and commands are mount-time: keep them module constants (new objects per render replace the lists). editor from onReady goes in useState, and EditorToolbar takes it as editor. Examples: Quick start: React and Advanced editor in React.
  • Settings keys. Only keys of EditorSettings persist; unknown ones are dropped on read and write.
  • Design tokens. @skaile/forge-editor/styles/tokens.css declares the shared --sk-* variables (--sk-bg, --sk-fg, --sk-muted, --sk-border, --sk-primary, --sk-primary-fg, --sk-success, --sk-warning, --sk-danger, --sk-radius, --sk-font) under :where(:root) (dark values under :where(.dark)). Later UI work in this package reads --sk-*; map them once in the host.
  • Module order. @nuxt/ui before @skaile/forge-editor/nuxt in modules.
  • Adapters tier. Entries under adapters/, anchors, comments/{server,json,drizzle,client} and server/nitro supply storage and wiring; they import no application code. Vue is reachable only from ./adapters/vue, React only from ./adapters/react (and the framework layers), Nitro (h3) only from ./server/nitro, drizzle-orm only from the two drizzle entries; ./adapters is types only. ./server/nest and ./adapters/prisma are reserved and not exported. The Nitro host and the HTTP backend (createHttpBackend, which useEditorDocument reads documents, collaboration and comments through) speak one wire contract: GET/PUT /api/editor/documents/:id (TipTap JSON, 404 = missing, PUT 204 and only for adapters that manage documents), GET/POST /api/editor/comments/:id (handleCommentsRequest, which trusts its caller), /collab/:id (Hocuspocus). Every route authenticates through ports.identity.authenticate({ headers, token }); null answers 401; the default outside production is an anonymous dev user, in production nobody. The JSON and forge adapters and the JSON comment store are single-node. Mount the surface with :key="id" and v-if="doc.ready.value" (React: key={id} and doc.ready), bind with v-bind="doc.bind.value" (React: {...doc.bind}). Runnable reference: examples/vue-nitro-json (bun run proto:vue-nitro-json, port 3046).
Copyright © 2026