Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 44 additions & 42 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ not `behavior`, `licence` not `license`, `centre` not `center`).
`minimumReleaseAgeExclude` require a justification comment in the PR
- **Peer dependencies are a contract:** `p5`, `react`, and `react-dom` are peer
dependencies. The library code must never import anything beyond these at
runtime β€” `microdiff` and `react-error-boundary` are the only runtime
runtime β€” `@p5-wrapper/common` and `react-error-boundary` are the only runtime
dependencies

### Formatting and Linting
Expand All @@ -86,9 +86,8 @@ not `behavior`, `licence` not `license`, `centre` not `center`).
- **No comments:** Do not add comments to source files. The code should be
self-documenting. The only permitted exceptions are `@ts-expect-error` /
`@ts-ignore` suppressions with a `@see` reference (see
`src/utils/createP5CanvasInstance.ts` for the existing pattern) and JSDoc on
exported contracts where a URL reference adds value (see
`src/contracts/P5CanvasInstanceRef.ts`)
`src/components/P5CanvasWithSketch.tsx` for the existing pattern) and JSDoc on
exported contracts where a URL reference adds value
- **No comments rule does not apply to:** this file, `README.md`, workflow
files, and config files with existing comments

Expand All @@ -102,10 +101,9 @@ not `behavior`, `licence` not `license`, `centre` not `center`).
- **Import style:** Use `import { type Foo }` inline type imports, matching the
existing code. Imports of contracts across the alias boundaries follow the
sorted import order enforced by Prettier
- **Type assertions:** Avoid `as` casts in library code. The one existing
`@ts-expect-error` in `createP5CanvasInstance.ts` documents a known p5
upstream type inference issue β€” do not remove it without verifying against the
referenced p5 PR
- **Type assertions:** Avoid `as` casts in library code. No `@ts-expect-error`
or `@ts-ignore` suppressions currently exist; if one is ever needed, it must
carry a `@see` reference and be verified against upstream issue or PR links
- **Version pinned to 6.0.3:** `typescript` is an exact pin
(`"typescript": "6.0.3"`, no caret), deliberately held back from v7.
typescript-eslint does not currently support TypeScript 7 β€” its
Expand Down Expand Up @@ -183,45 +181,49 @@ project changes. Do not begin implementation until the plan is approved.
design. Components are function components; utilities are pure functions that
take arguments and return values β€” they never reach for globals or hidden
state. Keep it this way
- **Types as the public contract:** The `src/contracts/` directory is the type
contract between the library and its consumers. Every exported type is public
API via `src/main.tsx`. Generic defaults flow through `SketchProps` β€”
- **Types as the public contract:** Shared p5 contracts (`Sketch`,
`P5CanvasInstance`, `Updater`, `SketchProps`, refs, and the generic
`P5CanvasProps<Props, OutputNode>`) come from `@p5-wrapper/common` β€” this
repository's own contracts are the React bindings on top. The only local
contract file is `src/contracts/P5CanvasProps.ts`, which binds common's
`OutputNode` to `ReactNode`. Generic defaults flow through `SketchProps` β€”
understand the generic chain (`Sketch<Props>` β†’ `P5CanvasInstance<Props>` β†’
`Updater<Props>` β†’ `P5CanvasProps<Props>`) before touching any of them
- **One contract per file, one export per file:** Contracts live in
`src/contracts/`, one file per contract, named after the export. Utilities
live in `src/utils/`, one file per function, named after the function. Follow
this pattern for anything new
`Updater<Props>` β†’ `P5CanvasProps<Props, ReactNode>`) before touching any of
them
- **One contract per file, one export per file:** React-specific contracts live
in `src/contracts/`, one file per contract, named after the export. React
utilities live in `src/utils/`, one file per function, named after the
function. Anything shared across frameworks belongs in `@p5-wrapper/common`,
not here
- **No hidden dependencies:** The component tree is deliberately layered β€”
`P5Canvas` (memoisation) β†’ `P5CanvasGuard` (validation + error boundary +
suspense) β†’ `P5CanvasWithSketch` (lifecycle). Responsibilities stay in their
layer; utils never import components; contracts never import utils
- **Imperative p5, declarative React:** p5 instances are imperative and mutable
by nature. The bridge is contained entirely in `P5CanvasWithSketch` and the
`src/utils/` lifecycle functions (`createP5CanvasInstance`,
`updateP5CanvasInstance`, `removeP5CanvasInstance`). Do not leak imperative p5
patterns into the React layer above
by nature. The bridge is contained entirely in `P5CanvasWithSketch`, which
calls the lifecycle utilities (`createP5CanvasInstance`,
`updateP5CanvasInstance`, `removeP5CanvasInstance`) from `@p5-wrapper/common`.
Do not leak imperative p5 patterns into the React layer above
- **Lazy boundaries:** `P5CanvasGuard` and `react-error-boundary` are lazily
imported so consumers who never trigger them never pay the bundle cost. Any
new heavy dependency must follow the same `React.lazy` pattern

### Testing

- **Test first:** Tests for new behaviour are written before or alongside the
implementation, never as an afterthought. Every utility in `src/utils/` has a
corresponding test file in `tests/utils/`; every component in
`src/components/` has one in `tests/components/`. Keep this 1:1 mapping
implementation, never as an afterthought. Every component in `src/components/`
has one in `tests/components/`. Utility functions and shared
contracts/constants are owned and tested by `@p5-wrapper/common`, not here.
Keep the 1:1 component mapping
- **Black-box testing:** Test the rendered output and observable behaviour
(`data-testid` hooks: `canvas-container`, `loading`, `error`), not internal
implementation details
- **Environment:** Vitest with `happy-dom`, `vitest-canvas-mock` for the canvas
API, and `@testing-library/react`. `p5.disableFriendlyErrors = true` is set in
`tests/setup.ts` to stop p5's DOM scanning from causing unhandled rejections β€”
do not remove it. `afterEach` cleanup is also mandatory
- **Structure:** Tests mirror the `src/` directory structure
(`tests/components/`, `tests/constants/`, `tests/utils/`, plus
`tests/exports.test.tsx` guarding the public API surface). New `src/` files
must add the matching test file
- **Structure:** Tests live in `tests/components/`, mirroring `src/components/`.
New `src/` component files must add the matching test file
- **Coverage:** CI runs `pnpm test:coverage` and comments coverage deltas on
PRs. Do not reduce coverage of existing code
- **Skipped tests:** Two loading-UI tests are currently `it.skip`-ped due to
Expand Down Expand Up @@ -303,7 +305,7 @@ sibling `@p5-wrapper/next` package.

```
P5Canvas (src/components/P5Canvas.tsx)
React.memo + propsAreEqual (microdiff deep comparison)
React.memo + propsAreEqual (from @p5-wrapper/common, microdiff deep comparison)
└─ P5CanvasGuard (lazy)
sketch validation β†’ fallback UI or:
ErrorBoundary (lazy react-error-boundary) + Suspense
Expand All @@ -317,13 +319,13 @@ P5Canvas (src/components/P5Canvas.tsx)

Why the layers exist:

- `P5Canvas` only handles memoisation β€” a deep `microdiff` comparison so p5 is
not needlessly recreated
- `P5Canvas` only handles memoisation β€” a deep comparison (common's
`propsAreEqual`, powered by `microdiff`) so p5 is not needlessly recreated
- `P5CanvasGuard` handles absence (missing `sketch` β†’ `fallback`), errors
(`error` render prop β†’ error boundary), and async loading (`loading` β†’
suspense)
- `P5CanvasWithSketch` is the only place that touches the p5 instance lifecycle
via the utils
via common's utilities

### Public API

Expand All @@ -350,14 +352,10 @@ Everything exported from `src/main.tsx` is public API and semver-protected:
β”‚ β”‚ β”œβ”€β”€ P5Canvas.tsx
β”‚ β”‚ β”œβ”€β”€ P5CanvasGuard.tsx
β”‚ β”‚ └── P5CanvasWithSketch.tsx
β”‚ β”œβ”€β”€ constants/ # Exported constants
β”‚ β”œβ”€β”€ contracts/ # Public-facing types (one contract per file)
β”‚ └── utils/ # Pure functions (one function per file)
β”œβ”€β”€ tests/ # Vitest tests mirroring src/ structure
β”‚ └── contracts/ # React-specific contracts (common bindings)
β”‚ └── P5CanvasProps.ts # Binds common's OutputNode to ReactNode
β”œβ”€β”€ tests/ # Vitest tests for component behaviour
β”‚ β”œβ”€β”€ components/
β”‚ β”œβ”€β”€ constants/
β”‚ β”œβ”€β”€ utils/
β”‚ β”œβ”€β”€ exports.test.tsx # Guards the public API surface
β”‚ └── setup.ts # Test bootstrap (canvas mock, cleanup)
β”œβ”€β”€ config/
β”‚ β”œβ”€β”€ eslint/eslint.config.ts
Expand Down Expand Up @@ -392,9 +390,9 @@ Everything exported from `src/main.tsx` is public API and semver-protected:
Vite's `[format]` placeholder also changed from `esm` to `es` in Vite 8, so
the names must never come from the placeholder again. If you change one side,
change both in the same commit
- The library externals are `react`, `react/jsx-runtime`, `react-dom`, `p5` β€”
keep Rollup externals, TypeScript expectations, and peer dependencies in
agreement
- The library externals are `react`, `react/jsx-runtime`, `react-dom`, `p5`, and
`@p5-wrapper/common` β€” keep Rollup externals, TypeScript expectations, and
peer/runtime dependencies in agreement

## Commands

Expand Down Expand Up @@ -440,7 +438,7 @@ Everything exported from `src/main.tsx` is public API and semver-protected:
secret and runs only in CD
- **Never weaken the build contract:** the `exports` map, `files` field, ESM +
CJS dual output, and bundled types are what downstream consumers depend on
- **Never introduce a runtime dependency** beyond `microdiff` and
- **Never introduce a runtime dependency** beyond `@p5-wrapper/common` and
`react-error-boundary` without discussion β€” bundle size is a feature of this
library
- **Never disable or skip tests, lint rules, or type checks** to make a change
Expand All @@ -453,6 +451,10 @@ Everything exported from `src/main.tsx` is public API and semver-protected:

## Future Topics

- **@p5-wrapper/common adoption:** The shared contracts, utilities, and
constants now come from `@p5-wrapper/common` (adopted on the
`adopt-common-package` branch, pending merge). The only React-specific
contract that remains is `src/contracts/P5CanvasProps.ts`
- **Skipped loading-UI tests:** The two `it.skip` suspense tests in
`tests/components/P5Canvas.test.tsx` need a reliable strategy before being
re-enabled
Expand Down
11 changes: 9 additions & 2 deletions config/vite/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@ export function library(root: string): UserConfig {
formats: ["es", "cjs"]
},
rollupOptions: {
external: ["react", "react/jsx-runtime", "react-dom", "p5"],
external: [
"react",
"react/jsx-runtime",
"react-dom",
"p5",
"@p5-wrapper/common"
],
output: {
assetFileNames: "assets/[name][extname]",
dir: dist,
globals: {
p5: "p5",
react: "React",
"react/jsx-runtime": "jsxRuntime",
"react-dom": "ReactDom"
"react-dom": "ReactDom",
"@p5-wrapper/common": "P5WrapperCommon"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"url": "https://github.com/P5-wrapper/react/issues"
},
"dependencies": {
"microdiff": "^1.6.0",
"@p5-wrapper/common": "^0.1.0",
"react-error-boundary": "^6.1.4"
},
"peerDependencies": {
Expand Down
17 changes: 14 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ peerDependencyRules:

minimumReleaseAgeExclude:
- happy-dom@20.13.2
- "@p5-wrapper/common@0.1.0"
2 changes: 1 addition & 1 deletion src/components/P5Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { propsAreEqual } from "@utils/propsAreEqual";
import { propsAreEqual } from "@p5-wrapper/common";

const P5CanvasGuard = React.lazy(() => import("@components/P5CanvasGuard"));

Expand Down
2 changes: 1 addition & 1 deletion src/components/P5CanvasGuard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import P5CanvasWithSketch from "@components/P5CanvasWithSketch";
import { type P5CanvasProps } from "@contracts/P5CanvasProps";
import { logErrorBoundaryError } from "@utils/logErrorBoundaryError";
import { logErrorBoundaryError } from "@p5-wrapper/common";
import { ReactNode } from "react";
import { FallbackProps } from "react-error-boundary";

Expand Down
18 changes: 10 additions & 8 deletions src/components/P5CanvasWithSketch.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as React from "react";
import { CanvasContainerClassName } from "@constants/CanvasContainerClassName";
import { type CanvasContainerRef } from "@contracts/CanvasContainerRef";
import { type P5CanvasInstanceRef } from "@contracts/P5CanvasInstanceRef";
import { type Sketch } from "@contracts/Sketch";
import { type SketchProps } from "@contracts/SketchProps";
import { type Updater } from "@contracts/Updater";
import { removeP5CanvasInstance } from "@utils/removeP5CanvasInstance";
import { updateP5CanvasInstance } from "@utils/updateP5CanvasInstance";
import {
CanvasContainerClassName,
type CanvasContainerRef,
type P5CanvasInstanceRef,
removeP5CanvasInstance,
type Sketch,
type SketchProps,
updateP5CanvasInstance,
type Updater
} from "@p5-wrapper/common";
import { type ReactNode } from "react";

interface P5CanvasWithSketchProps {
Expand Down
1 change: 0 additions & 1 deletion src/constants/CanvasContainerClassName.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/contracts/CanvasContainer.ts

This file was deleted.

4 changes: 0 additions & 4 deletions src/contracts/CanvasContainerRef.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/contracts/P5CanvasInstance.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/contracts/P5CanvasInstanceRef.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/contracts/P5CanvasInternalProps.ts

This file was deleted.

7 changes: 4 additions & 3 deletions src/contracts/P5CanvasProps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type P5CanvasInternalProps } from "@contracts/P5CanvasInternalProps";
import { type SketchProps } from "@contracts/SketchProps";
import { type P5CanvasProps as CommonP5CanvasProps } from "@p5-wrapper/common";
import { type SketchProps } from "@p5-wrapper/common";
import { type ReactNode } from "react";

export type P5CanvasProps<Props extends SketchProps = SketchProps> =
P5CanvasInternalProps<Props> & Props;
CommonP5CanvasProps<Props, ReactNode>;
6 changes: 0 additions & 6 deletions src/contracts/Sketch.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/contracts/SketchProps.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/contracts/Updater.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/contracts/p5.ts

This file was deleted.

12 changes: 7 additions & 5 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export {
CanvasContainerClassName,
type P5CanvasInstance,
type Sketch,
type SketchProps,
type Updater
} from "@p5-wrapper/common";
export { P5Canvas } from "@components/P5Canvas";
export { CanvasContainerClassName } from "@constants/CanvasContainerClassName";
export { type P5CanvasInstance } from "@contracts/P5CanvasInstance";
export { type P5CanvasProps } from "@contracts/P5CanvasProps";
export { type Sketch } from "@contracts/Sketch";
export { type SketchProps } from "@contracts/SketchProps";
export { type Updater } from "@contracts/Updater";
14 changes: 0 additions & 14 deletions src/utils/createP5CanvasInstance.ts

This file was deleted.

Loading
Loading