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
10 changes: 9 additions & 1 deletion ecs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ conversation, the diff or the alternatives. Tell them what it is and what it doe
- "X, not B, C or D": say what is done;
- "previously", "now", "instead of", "moved from", "whose source of truth is…".

## Layout

A feature's or package's root folder holds only its public exports, one file per export, named after it, and their
tests. Its implementation lives in folders below. Nothing outside a feature imports below its root, and within a
package only its entry file re-exports.

## Never use top-level await

Do not use `await` at module top level in any package's sources. Defer async work to a lazy-init pattern: store the
Expand Down Expand Up @@ -63,7 +69,9 @@ Standard script names a package may define:
every package's `pages`.

Publishing is governed only by the `private` field: `pnpm -r publish` publishes every package with `private: false` and
skips the rest. Every package sets `private` explicitly, and samples are `private: true`. Don't add a `publish-public`
skips the rest. Every package sets `private` explicitly. Samples are `private: true`, and every library is published
publicly from the start: `private: false`, `publishConfig` with `"access": "public"`, `independentVersion: true`, and
`LICENSE` among its files, as `ecs-gpu`'s are. Don't add a `publish-public`
or similar script. `scripts/check-workspace-conventions.mjs` (`pnpm run check:workspace`, run in CI) enforces these.

## Checks
Expand Down
2 changes: 2 additions & 0 deletions ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ A high-performance, multithreaded Entity Component System for web-based applicat
- **[`@adobe/ecs`](packages/ecs)**: the ECS and its build plugin, `@adobe/ecs/compiler`.
- **[`@adobe/ecs-gpu`](packages/ecs-gpu)**: WebGPU devices, canvases, render targets, views and frame passes for the
ECS.
- **[`@adobe/ecs-persistence`](packages/ecs-persistence)**: saving, loading and copying the entities of worlds, as
binary columns.
- **[`ecs-todo-vanillajs`](packages/ecs-todo-vanillajs)**: a todo app whose world drives the DOM through systems,
with vanilla custom elements.
- **[`ecs-particles-webgpu`](packages/ecs-particles-webgpu)**: particles simulated by kernels, on the CPU and with
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-gpu-views/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GPU Views tab.

- **Main** (`main.ts`) owns the page. It starts the world's worker and joins the world. It moves each `<canvas>` to the
world's thread as the input of a run of `Surface.attach`, and `Surface.observe` sends each canvas's size in device
pixels whenever it changes. It spawns the views and the points, and adds `show`, an observer system writing the
pixels whenever it changes. It spawns the views and the points, and adds `show`, a readonly system writing the
frame count into the overlay.
- **The world's worker** (`world.ts`) creates the world, which ticks every frame, keeps it supplied with a device with
`Device.keep`, and shares it on the port main sends.
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-gpu-views/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let drawn = 0;
/** Whether the world on the worker has shut down, after the page said it was leaving. */
let left = false;
// Added on this thread, it runs here after each of the world's frames.
const show = System.create({ observer: true, resources: { changed: [frames] } }, ({ resources }) => {
const show = System.create({ readonly: true, resources: { changed: [frames] } }, ({ resources }) => {
drawn = resources[frames];
overlay(`frame ${drawn} · drawn on the world's worker into two canvases: two views side by side, and one from above`);
});
Expand Down
21 changes: 21 additions & 0 deletions ecs/packages/ecs-gpu/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025-2026 Adobe, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
12 changes: 9 additions & 3 deletions ecs/packages/ecs-gpu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
"license": "MIT",
"type": "module",
"sideEffects": true,
"private": true,
"private": false,
"independentVersion": true,
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"files": [
"dist",
"src",
"!src/**/*.test.ts",
"!src/**/*.test-helper.ts",
"!src/**/*.test-worker.ts"
"!src/**/*.test-worker.ts",
"LICENSE"
],
"scripts": {
"build": "tsc -p tsconfig.build.json && vite build",
"build": "cp ../../LICENSE . && tsc -p tsconfig.build.json && vite build",
"typecheck": "tsc && tsc -p tsconfig.build.json",
"test": "pnpm exec playwright install chromium && vitest --run"
},
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-guide/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ and so are the builds, a service that checks and compiles off the main thread an
are rounded panels on the base background, a gutter apart, and the editor's colors are Spectrum's palette, whose
tokens flip with the theme.

Each feature's `model.ts` holds systems with input, one per thing that happens, and `dom.ts` the observer systems that
Each feature's `model.ts` holds systems with input, one per thing that happens, and `dom.ts` the readonly systems that
bind elements, with the ticking systems a feature needs, as one composite the app adds. `events.ts` turns DOM events,
the builds' results and the frame's messages into runs. Typing stays smooth: each keystroke is one run and the
bindings it changes, while checking, compiling and running happen in workers and the frame, after the code has been
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-guide/src/app/compiled.page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ it("marks each of chapter 14's systems above its call with what it compiles to,
await statusOf(page);
const lenses = await lensesReading(page, chipLabels);
expect(lenses.map(lens => lens.line.trim().slice(0, 30))).toEqual([
"const move = System.create({ c", "const drift = System.create({ ", "const slow = System.create({ k", "const read = System.create({ o", "const spawn = System.create(({",
"const move = System.create({ c", "const drift = System.create({ ", "const slow = System.create({ k", "const read = System.create({ r", "const spawn = System.create(({",
]);
expect(lenses[0]?.label).toBe("move: JS compiled, WASM compiled, WGSL if declared kernel: \"gpu\". Show what it compiles to.");
await (await chipOf(page, "spawn")).hover();
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-guide/src/app/hover.page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ it.each(["dark", "light"] as const)("shows the types of what's hovered, System.c
await away(page);
await hoverWord(page, "await using world = World.create()", "create");
const world = await hovered(page);
expect(world.signature).toMatch(/^\(property\) create: /);
expect(world.signature).toMatch(/^const create: /);
expect(errors).toEqual([]);
} finally {
await page.close();
Expand Down
8 changes: 4 additions & 4 deletions ecs/packages/ecs-guide/src/features/appearance/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@ declare module "@adobe/ecs" {

/** Colors the page light or dark, as the system prefers. */
export const bindTheme = System.create({
observer: true,
readonly: true,
resources: { is: [theme], changed: [dark] },
}, ({ resources }) => {
resources[theme].setAttribute("color", resources[dark] ? "dark" : "light");
});

/** Says whether this browser has WebGPU. */
export const bindWebgpu = System.create({
observer: true,
readonly: true,
resources: { is: [note], changed: [webgpu] },
}, ({ resources }) => {
resources[note].webgpu = resources[webgpu];
});

/** Shows the note in the chapter on GPU kernels only. */
export const bindNote = System.create({
observer: true,
readonly: true,
components: { with: [shown, chapterNumber], changed: [shown] },
resources: { is: [note] },
}, (row, { resources }) => {
Expand All @@ -49,7 +49,7 @@ export const bindNote = System.create({
* without an example, there's nothing to resize.
*/
export const bindColumns = System.create({
observer: true,
readonly: true,
components: { with: [shown, sources] },
resources: { is: [columnsElement] },
}, (row, { resources }) => {
Expand Down
18 changes: 9 additions & 9 deletions ecs/packages/ecs-guide/src/features/builds/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ declare module "@adobe/ecs" {

/** Sends each revision of a chapter's files to be checked and compiled. */
export const requestBuilds = System.create({
observer: true,
readonly: true,
components: { with: [revision, sources], changed: [revision] },
resources: { is: [builds] },
}, (row, { resources }) => {
Expand All @@ -45,7 +45,7 @@ export const requestBuilds = System.create({

/** Lists the shown chapter's problems, and marks the shown file's in the editor. */
export const bindProblems = System.create({
observer: true,
readonly: true,
components: { with: [shown, typeProblems, buildProblems, sources, fileShown], changed: [shown, typeProblems, buildProblems, fileShown] },
resources: { is: [problemsElement, editor] },
}, (row, { resources }) => {
Expand All @@ -57,7 +57,7 @@ export const bindProblems = System.create({

/** Shows where the shown chapter's run is. */
export const bindStatus = System.create({
observer: true,
readonly: true,
components: { with: [shown, runState], changed: [shown, runState] },
resources: { is: [statusElement] },
}, (row, { resources }) => {
Expand All @@ -66,7 +66,7 @@ export const bindStatus = System.create({

/** Shows what the shown chapter's run logged. */
export const bindConsole = System.create({
observer: true,
readonly: true,
components: { with: [shown, logged], changed: [shown, logged] },
resources: { is: [consoleElement] },
}, (row, { resources }) => {
Expand All @@ -78,7 +78,7 @@ export const bindConsole = System.create({
* was last shown, and the Problems, each saying whether an error is among them.
*/
export const bindTabs = System.create({
observer: true,
readonly: true,
components: { with: [shown, tab, logged, consoleSeen, typeProblems, buildProblems], changed: [shown, tab, logged, consoleSeen, typeProblems, buildProblems] },
resources: { is: [tabsElement] },
}, (row, { resources }) => {
Expand All @@ -96,7 +96,7 @@ export const bindTabs = System.create({

/** Lists what the shown chapter's systems compile to, and opens the one the reader chose. */
export const bindCompiled = System.create({
observer: true,
readonly: true,
components: { with: [shown, described, systemOpened], changed: [shown, described, systemOpened] },
resources: { is: [compiledElement] },
}, (row, { resources }) => {
Expand All @@ -107,7 +107,7 @@ export const bindCompiled = System.create({

/** Lets the editor ask TypeScript what's at a place in the shown chapter's files. */
export const bindQuickInfo = System.create({
observer: true,
readonly: true,
components: { with: [shown], changed: [shown] },
resources: { is: [editor, builds] },
}, (row, { resources }) => {
Expand All @@ -118,7 +118,7 @@ export const bindQuickInfo = System.create({

/** Marks the shown file's systems in the editor at their calls. */
export const bindSystemMarks = System.create({
observer: true,
readonly: true,
components: { with: [shown, described, sources, fileShown], changed: [shown, described, fileShown] },
resources: { is: [editor] },
}, (row, { resources }) => {
Expand All @@ -129,7 +129,7 @@ export const bindSystemMarks = System.create({

/** Frames the shown chapter's latest run. */
export const bindOutput = System.create({
observer: true,
readonly: true,
components: { with: [shown, page], changed: [shown, page] },
resources: { is: [outputElement] },
}, (row, { resources }) => {
Expand Down
4 changes: 2 additions & 2 deletions ecs/packages/ecs-guide/src/features/chapters/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ declare module "@adobe/ecs" {

/** Lists the chapters in the navigation, in order, whenever a chapter is added. */
export const bindChapters = System.create({
observer: true,
readonly: true,
resources: { is: [nav] },
lookup: { chapters: { index: inOrder, changed: true } },
}, ({ resources, lookup }) => {
Expand All @@ -32,7 +32,7 @@ export const bindChapters = System.create({

/** Shows the shown chapter: selected in the navigation, its prose, and its title in the page's. */
export const bindShown = System.create({
observer: true,
readonly: true,
components: { with: [shown, slug, chapterNumber, title, prose], changed: [shown] },
resources: { is: [nav, proseElement] },
}, (row, { resources }) => {
Expand Down
6 changes: 3 additions & 3 deletions ecs/packages/ecs-guide/src/features/examples/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ declare module "@adobe/ecs" {

/** Puts the shown chapter's shown file in the editor, and its files in the tabs; a chapter without examples hides them. */
export const bindEditor = System.create({
observer: true,
readonly: true,
components: { with: [shown, sources, fileShown], changed: [shown, sources, fileShown] },
resources: { is: [editor, filesElement, exampleArea] },
}, (row, { resources }) => {
Expand All @@ -37,7 +37,7 @@ export const bindEditor = System.create({

/** Shows the lines asked for in the editor. */
export const bindRevealed = System.create({
observer: true,
readonly: true,
components: { with: [shown, revealed], changed: [shown, revealed] },
resources: { is: [editor] },
}, (row, { resources }) => {
Expand All @@ -46,7 +46,7 @@ export const bindRevealed = System.create({

/** Lets the reset button reset only a chapter edited. */
export const bindReset = System.create({
observer: true,
readonly: true,
components: { with: [shown, sources, originals], changed: [shown, sources] },
resources: { is: [resetElement] },
}, (row, { resources }) => {
Expand Down
4 changes: 2 additions & 2 deletions ecs/packages/ecs-guide/src/features/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Read = {
};
const read: Read[] = [];
const readChapters = System.create({
observer: true,
readonly: true,
components: { with: [slug, chapterNumber, sources, revision, fileShown, revealed, runState, logged, page, typeProblems, buildProblems, tab, consoleSeen], optional: [shown] },
}, row => {
read.push({
Expand Down Expand Up @@ -190,7 +190,7 @@ it("frames a chapter shown again only once its new revision compiles, not its la
/** What a chapter's systems compile to, and the system opened, as tests read them. */
const compiledOf = async (world: World): Promise<unknown> => {
let found: unknown = null;
await world.run(System.create({ observer: true, components: { with: [shown, described, systemOpened, tab, tabPicked] } }, row => {
await world.run(System.create({ readonly: true, components: { with: [shown, described, systemOpened, tab, tabPicked] } }, row => {
found = { described: row[described], opened: row[systemOpened], tab: row[tab], picked: row[tabPicked] };
}));
return found;
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-particles-webgpu/src/gpu/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const start = async (): Promise<void> => {
await world.run(spawn, { count, width, height });
/** For automated checks: a few drawn particles' positions, which an observer system reads back from the GPU. */
const probed: (readonly number[])[] = [];
const probe = System.create({ observer: true, components: { with: [position, visible, enabled] } }, row => {
const probe = System.create({ readonly: true, components: { with: [position, visible, enabled] } }, row => {
if (probed.length < 3) probed.push(row[position]);
});
Reflect.set(globalThis, "sampleProbe", async () => {
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-particles-webgpu/src/particles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe("particles", () => {
let positions: readonly (readonly number[])[] = [];
for (const seconds of frames) positions = await frame(world, seconds);
let spawned = 0;
await world.run(System.create({ observer: true }, ({ resources }) => { spawned = resources[count]; }));
await world.run(System.create({ readonly: true }, ({ resources }) => { spawned = resources[count]; }));
expect(spawned).toBe(100);
const expected = reference(frames);
expect(positions).toHaveLength(expected.length);
Expand Down
2 changes: 1 addition & 1 deletion ecs/packages/ecs-particles-webgpu/src/particles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const main = System.create({ systems: [spawn, gravity, integrate] });
// Reads every position the frame left, in spawn order.
const positions: Vec3[] = [];
const read = System.create({
observer: true,
readonly: true,
components: { with: [position] },
}, row => {
positions.push(row[position]);
Expand Down
21 changes: 21 additions & 0 deletions ecs/packages/ecs-persistence/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025-2026 Adobe, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading