A pnpm + moon monorepo of the DXOS team's plugins for Composer, developed and released outside the
dxos/dxos monorepo. Each plugin is packages/<name>/ (its own package.json, dx.config.ts,
vite.config.ts, src/). All plugins share one DXOS SDK version via the dxos catalog in
pnpm-workspace.yaml.
Always test your work after each step. If unsure how to implement something, ask — and when you ask an a-or-b question, number the options.
pnpm install, thenmoon run :build(the npm library) and/ormoon run :bundle(the registry bundle). Task names mirror dxos/dxos:build= library,bundle= vite bundle.- Two build outputs, two audiences.
:buildemits the npm library intodist/(dist/libJS with dependencies externalised,dist/typesdeclarations fromtsc).:bundlebundles the plugin for the registry intoout/, with every dependency inlined so Composer loads one self-contained artifact. Onlydistandsrcship to npm —out/must never reach the tarball, or consumers get a second copy of React, Effect and the SDK. - A plugin compiles its own stylesheet. Composer's CSS is generated from the dxos monorepo's
sources, so a registry-loaded plugin is never scanned: any class it uses that Composer does not
already emit — every arbitrary value like
max-w-[30rem]— silently resolves to nothing. Each plugin therefore hassrc/theme.css, imports it from its plugin entry, and runstailwindcss()in both vite configs. Full wiring and the post-build check →composer-pluginsskill, "Styling". - Tasks are tag-based, never per-project: a plugin's
moon.ymldeclarestags, and each tag inherits the matching/.moon/tasks/tag-<tag>.yml. Available tags → tasks:typecheck→typecheck·ts-vite-build→build·ts-test→test,test-watch·vite→bundle,dev,preview·storybook→storybook. Add a task by editing the tag file, so every plugin carrying that tag gets it — do not addtasks:to a plugin'smoon.yml. - Toolchain (node/pnpm/moon) is pinned in
.prototools— runproto installonce. - Format and lint before every commit:
pnpm format(oxfmt) andpnpm lint(oxlint--fix). CI runsoxfmt --checkandmoon run :lint, and a single unformatted file fails the job.
- Every dependency version lives in a catalog in
pnpm-workspace.yaml; apackage.jsonnever carries a literal version. Two catalogs: the defaultcatalog:for external packages, and the namedcatalog:dxosfor@dxos/*. - Never hand-edit the
dxoscatalog — usenode scripts/set-sdk.mjs. The SDK ships as one unit and moves in lockstep. - Add a shared external dep with
pnpm add --filter <plugin> --save-catalog <package>, which writes the version to the default catalog andcatalog:to the plugin. - A dep the SDK also resolves must match the pinned SDK build, not dxos main.
effect,@automerge/automerge,reactandreact-domare declared by@dxos/*too; pick a version the pinned build does not also resolve and pnpm installs a second copy. Effect and Automerge brand their types nominally, so the duplicate surfaces asProperty '[TypeId]' is missingacross every schema. Check withnode -p "require('./node_modules/@dxos/echo/package.json').peerDependencies". - When editing
pnpm-workspace.yaml, preserve the comments.
Deep, task-specific guidance lives in .agents/skills/* — follow the relevant one:
code-style— namespace exports, internal-module imports, class-member ordering, options-bag types, the no-cast rule, the comment rule, test structure.composer-plugins— plugin structure, capabilities, containers vs components, operations.composer-ui— theme tokens,@dxos/react-ui*primitives, forms, lists, toolbars, storybook.
These are ported from the dxos/dxos monorepo. Where they cite a source path, @dxos/<pkg>/src/...
refers to the published package — readable under node_modules/@dxos/<pkg>/src/, since the SDK
tarballs ship their sources.
Summary below; the code-style skill is authoritative.
- TypeScript, single quotes. Prefer functional programming and arrow functions.
- Import order: builtin → external →
@dxos→ internal → parent → sibling (blank line between groups). Inline type imports:import { type Foo } from 'bar'. Max line 120, trailing commas, JSX single quotes. - Avoid single-letter variable names (
space, nots). Avoid default exports unless required. Prefer ES#privateover the TypeScriptprivatekeyword in new code. - Use the
Optionssuffix for constructor/function option-bag types (notOpts/Props/Config). - JSDoc public functions; comments end with a period. Comments state why the code is necessary (the invariant/constraint it satisfies) — never narrate the change, reference a conversation, or use before/after framing ("we used to…", "rather than X we now Y").
- When moving code, don't leave compatibility re-exports/shims behind — update every call site in the same change.
- React: arrow-function components, TailwindCSS, proper event-handler types. Import React symbols as
named imports (
useMemo,type Ref— notReact.useMemo); name a forwarded refforwardedRef.
Fix the type at its source (inference, signature, generic), not the call site that surfaced it. A red typecheck is a finding, not something to paper over.
- "Cast" =
as T,as any,as unknown as T, non-null!, or a widened/anysignature added to silence the checker.as constis not a cast (it narrows a literal) — always fine. - Casts are acceptable only at genuine type-system boundaries (external/untyped data, deliberate coercions) and must carry a one-line comment saying why no typed alternative exists.
- Logging:
import { log } from '@dxos/log'— structured metadata,log.info('msg', { key }). - Assertions:
import { invariant } from '@dxos/invariant'—invariant(cond, 'message'). - Error handling: Effect-TS patterns where applicable.
- Place tests next to the module as
module.test.ts. Use vitest withdescribe/test(notit), and prefertest('foo', ({ expect }) => ...). - Test the public API (the plugin's exported surface / capabilities), not private internals. Prefer extending an existing suite over adding a fragmented new one.
A changeset is a markdown file in .changeset/. Write the file directly (deterministic; don't rely
on the interactive pnpm changeset). Create .changeset/<short-kebab-summary>.md:
---
'@dxos/plugin-tictactoe': patch
---
One-line, user-facing summary of the change.- Key = the plugin's
package.jsonname(one line per plugin touched). - Bump:
patch= fix/refactor/dep bump (no API/behavior change) ·minor= new backward-compatible feature or a breaking change ·major= reserved for a deliberate1.0.0cut. Plugins are pre-1.0, so breaking rides the minor (0.1.28 → 0.2.0); writingmajorwould ship1.0.0by accident. - Skip it for non-plugin changes (repo tooling, CI, root docs, tests). Don't hand-write the
coordinated SDK bump — that's
scripts/changeset-all.mjs.
Trunk-based on main; there is no release branch. Merging a changeset to main opens a
"Version Packages" PR, and merging that PR is the release — it tags each version and publishes to
the registry. Plugins carry independent version lines but share one Version PR, so a release ships
whatever accumulated. See RELEASING.md for the full flow and the SDK upgrade train.
- New packages stay
"private": trueuntil npm has seen them. npm auth is a per-package trusted publisher (OIDC), which cannot exist before the package does — so a publishable plugin npm has never seen failschangeset publishand takes the whole release with it. Drop the flag only after the first manual publish and trusted-publisher setup;pnpm check-packages-publishedenforces this in CI. Plugins reach Composer viadx registry publish; npm is the secondary channel. - A publishable plugin needs the
ts-vite-buildtag (:build, the npm library) and thevitetag (:bundle, the registry artifact), and itsexports/importsmaps must point at what:buildemits. It also needsrepository.urlin itspackage.json: npm validates the provenance statement against that field, and rejects the publish without it.pnpm check-packages-publishedenforces both this and theprivaterule above. - PR titles use Conventional Commits:
feat(tictactoe): …,fix: …,refactor: …,docs: …. - Before committing, run
git statusand account for every modified/untracked file.