Skip to content

Latest commit

 

History

History
146 lines (117 loc) · 8.83 KB

File metadata and controls

146 lines (117 loc) · 8.83 KB

Agent guide — composer-plugins

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.

Build / dev

  • pnpm install, then moon run :build (the npm library) and/or moon run :bundle (the registry bundle). Task names mirror dxos/dxos: build = library, bundle = vite bundle.
  • Two build outputs, two audiences. :build emits the npm library into dist/ (dist/lib JS with dependencies externalised, dist/types declarations from tsc). :bundle bundles the plugin for the registry into out/, with every dependency inlined so Composer loads one self-contained artifact. Only dist and src ship 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 has src/theme.css, imports it from its plugin entry, and runs tailwindcss() in both vite configs. Full wiring and the post-build check → composer-plugins skill, "Styling".
  • Tasks are tag-based, never per-project: a plugin's moon.yml declares tags, and each tag inherits the matching /.moon/tasks/tag-<tag>.yml. Available tags → tasks: typechecktypecheck · ts-vite-buildbuild · ts-testtest, test-watch · vitebundle, dev, preview · storybookstorybook. Add a task by editing the tag file, so every plugin carrying that tag gets it — do not add tasks: to a plugin's moon.yml.
  • Toolchain (node/pnpm/moon) is pinned in .prototools — run proto install once.
  • Format and lint before every commit: pnpm format (oxfmt) and pnpm lint (oxlint --fix). CI runs oxfmt --check and moon run :lint, and a single unformatted file fails the job.

Dependencies

  • Every dependency version lives in a catalog in pnpm-workspace.yaml; a package.json never carries a literal version. Two catalogs: the default catalog: for external packages, and the named catalog:dxos for @dxos/*.
  • Never hand-edit the dxos catalog — use node 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 and catalog: to the plugin.
  • A dep the SDK also resolves must match the pinned SDK build, not dxos main. effect, @automerge/automerge, react and react-dom are 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 as Property '[TypeId]' is missing across every schema. Check with node -p "require('./node_modules/@dxos/echo/package.json').peerDependencies".
  • When editing pnpm-workspace.yaml, preserve the comments.

Skills

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.

Code style

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, not s). Avoid default exports unless required. Prefer ES #private over the TypeScript private keyword in new code.
  • Use the Options suffix for constructor/function option-bag types (not Opts/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 — not React.useMemo); name a forwarded ref forwardedRef.

Don't cast to fix type errors

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/any signature added to silence the checker. as const is 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.

Common DXOS patterns

  • 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.

Testing

  • Place tests next to the module as module.test.ts. Use vitest with describe/test (not it), and prefer test('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.

Changesets — add one in every PR that changes a plugin

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.json name (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 deliberate 1.0.0 cut. Plugins are pre-1.0, so breaking rides the minor (0.1.28 → 0.2.0); writing major would ship 1.0.0 by 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.

Releasing

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.

Conventions

  • New packages stay "private": true until 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 fails changeset publish and takes the whole release with it. Drop the flag only after the first manual publish and trusted-publisher setup; pnpm check-packages-published enforces this in CI. Plugins reach Composer via dx registry publish; npm is the secondary channel.
  • A publishable plugin needs the ts-vite-build tag (:build, the npm library) and the vite tag (:bundle, the registry artifact), and its exports/imports maps must point at what :build emits. It also needs repository.url in its package.json: npm validates the provenance statement against that field, and rejects the publish without it. pnpm check-packages-published enforces both this and the private rule above.
  • PR titles use Conventional Commits: feat(tictactoe): …, fix: …, refactor: …, docs: ….
  • Before committing, run git status and account for every modified/untracked file.