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
77 changes: 19 additions & 58 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
name: CI

# Validates every pull request (and pushes to main) before merge. Prior to
# this workflow the only GitHub Action was the post-merge docs deploy, so a PR
# whose `tsc -b` / unit tests were red could still be merged green — exactly
# what happened with #114. This gate makes those failures block the merge.
#
# Two jobs:
# data — @adobe/data core: typecheck (tsc -b + WASM), lint, unit tests.
# packages — downstream published packages: build (tsc -b) + tests for each.
# Runs after `data` so a core failure surfaces once, not twice.
# @adobe/data is rebuilt here to provide dist/ for downstream tsc.
# Validates every pull request (and pushes to main) before merge. The gate is
# fully uniform: it never names individual packages. Each package owns how it
# builds/tests/typechecks via standard script names, and every step here is a
# single recursive invocation that pnpm runs in dependency (topological) order,
# skipping packages that don't define the script. Adding a package therefore
# needs no change to this file — see `scripts/check-workspace-conventions.mjs`
# and the "Workspace script conventions" section of CLAUDE.md.

on:
pull_request:
Expand All @@ -24,8 +21,8 @@ concurrency:
cancel-in-progress: true

jobs:
data:
name: "@adobe/data — typecheck, lint, test"
ci:
name: typecheck · lint · build · test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand All @@ -41,53 +38,17 @@ jobs:

- run: pnpm install --frozen-lockfile

- name: Type-check (tsc -b, project references)
run: pnpm --filter @adobe/data run typecheck
- name: Check workspace conventions
run: pnpm run check:workspace

- name: Lint
run: pnpm --filter @adobe/data run lint
- name: Type-check (every package, topological)
run: pnpm -r run typecheck

- name: Unit tests
run: pnpm --filter @adobe/data run test:ci
- name: Lint (every package that defines it)
run: pnpm -r run lint

packages:
name: "downstream packages — build, test"
runs-on: ubuntu-latest
needs: [data]
steps:
- uses: actions/checkout@v6

- uses: pnpm/action-setup@v6
with:
version: 9

- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Build @adobe/data (provides dist/ for downstream tsc)
run: pnpm --filter @adobe/data run typecheck

- name: Build every downstream package (type-check via tsc -b / vite)
# Builds ALL workspace packages except the core (already built above),
# including the private, unscoped sample apps (data-p2p-tictactoe,
# data-lit-todo, …). A previous `--filter '@adobe/data-*'` only matched
# scoped published packages, so a type/build error that surfaced only in
# a private sample (e.g. a stripped `.d.ts` member) merged green.
run: pnpm --filter '!@adobe/data' run build
- name: Build (every package, topological)
run: pnpm -r run build

- name: Test downstream packages
# Every workspace package except the core (tested in the `data` job) and
# data-persistence (whose `test` script pulls in Playwright — run its
# node project instead). pnpm skips packages that declare no `test`
# script. This now includes the private sample apps (data-lit-todo,
# data-lit-tictactoe, …); previously they were only built, so a broken
# sample unit test (e.g. the data-lit-todo conformance suite) merged green.
run: |
pnpm --filter '!@adobe/data' \
--filter '!@adobe/data-persistence' \
run test
pnpm --filter @adobe/data-persistence run test:node
- name: Test (each package runs its own complete suite)
run: pnpm -r run test
31 changes: 31 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,34 @@ Do not use `await` at module top level anywhere in this repo's package sources.
**Why:** Bundlers (Rolldown / Vite 8) wrap any module containing TLA — and every module that transitively imports it — in an async lazy-init function whose exports are only available via `await`. When those wrappers participate in an import cycle (very common through barrel re-exports), the cycle becomes a circular-await chain with no live-binding escape hatch, and the whole graph deadlocks silently. Native ESM in dev resolves cycles via live bindings, so dev appears fine; only the bundled output hangs, with no console error.

See `packages/data/src/cache/data-cache.ts` (`createGlobalDataCache`) for the lazy-init pattern, and `packages/data/src/cache/blob-store.ts` (`cachePromise` inside `createBlobStore`) for the same idea applied at factory scope.

## Workspace script conventions

The root and CI never name individual packages. They invoke uniform recursive
scripts (`pnpm -r run <script>`), which pnpm runs in dependency order and skips
for packages that don't define the script. Each package owns *how* it does each
step; the pipeline just calls the standard name. This is what keeps the monorepo
scalable — a new package "just works" with zero edits to any central list.

Standard script names a package may define:

- **`build`** — produce dist / bundle; must fail on type or bundler errors.
- **`typecheck`** — type-check (usually `tsc -b` or `tsc --noEmit`).
- **`test`** — run the package's *complete* suite, self-contained and runnable
in CI. If a package needs a browser it installs and drives it itself (see
`data-persistence`, whose `test` installs chromium and runs both projects).
Keep long/optional lanes under suffixed names (`test:browser`, `test:perf`)
that the uniform pipeline does not call.
- **`lint`** — lint the package.
- **`dev`** — local dev/watch.

Do **not** add a `publish-public` (or similar) script. Publishing is governed
*only* by the `private` field: `pnpm -r publish` publishes every package with
`private: false` and skips the rest. Every package must set `private`
explicitly — an absent `private` means "publishable" to npm, which we forbid so
nothing ships by accident. `scripts/check-workspace-conventions.mjs` (run in CI
via `pnpm run check:workspace`) enforces both rules.

To make a package publishable, set `private: false`; to hold it back, set
`private: true`. That one field is the single source of truth — there is no list
to update anywhere else.
8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@
"test": "pnpm -r run test",
"lint": "pnpm -r run lint",
"lint-fix": "pnpm -r run lint-fix",
"typecheck": "pnpm run typecheck:foundation && pnpm run typecheck:libraries && pnpm run typecheck:samples",
"typecheck:foundation": "pnpm --filter @adobe/data run typecheck",
"typecheck:libraries": "pnpm --parallel --filter '@adobe/data-*' --filter '!@adobe/data' run typecheck",
"typecheck:samples": "pnpm --parallel --filter data-lit-todo --filter data-react-hello --filter data-react-pixie --filter data-lit-tictactoe --filter data-p2p-tictactoe --filter data-solid-dashboard --filter data-gpu-samples run typecheck",
"typecheck": "pnpm -r run typecheck",
"check:workspace": "node scripts/check-workspace-conventions.mjs",
"dev": "pnpm -r --parallel run dev",
"dev:data": "pnpm --filter @adobe/data run dev",
"dev-gpu": "pnpm --parallel --filter @adobe/data --filter @adobe/data-gpu --filter data-gpu-samples run dev",
"link": "pnpm -r --filter @adobe/data* run link",
"publish": "sh -c 'for x in \"$@\"; do OTP=\"$x\"; done; export NPM_CONFIG_OTP=\"$OTP\"; pnpm -r --filter @adobe/data --filter @adobe/data-ai --filter @adobe/data-react --filter @adobe/data-lit --filter @adobe/data-solid --filter @adobe/data-gpu run publish-public' sh",
"publish": "sh -c 'for x in \"$@\"; do OTP=\"$x\"; done; export NPM_CONFIG_OTP=\"$OTP\"; pnpm -r run build && pnpm -r publish --no-git-checks --access public' sh",
"bump": "pnpm version patch --no-git-tag-version && V=$(node -p \"require('$PWD/package.json').version\") && pnpm -r exec pnpm version $V --no-git-tag-version --allow-same-version && node -e \"const fs=require('fs');const p='packages/data-ai/.claude-plugin/plugin.json';const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.argv[1];fs.writeFileSync(p,JSON.stringify(j,null,2)+'\\n')\" $V",
"release": "pnpm bump && pnpm publish",
"bp": "pnpm bump && pnpm run publish"
Expand Down
3 changes: 1 addition & 2 deletions packages/data-ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
"scripts": {
"build": "cp ../../LICENSE .",
"typecheck": "true",
"test": "vitest --run --passWithNoTests",
"publish-public": "pnpm build && pnpm publish --no-git-checks --access public"
"test": "vitest --run --passWithNoTests"
},
"keywords": [
"adobe",
Expand Down
3 changes: 1 addition & 2 deletions packages/data-gpu-hopper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"build": "vite build",
"dev": "vite",
"test": "vitest --run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit",
"publish-public": "true"
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion packages/data-gpu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"build": "cp ../../LICENSE . && tsc -b",
"dev": "tsc -b -w",
"test": "vitest --run --passWithNoTests",
"publish-public": "pnpm build && pnpm publish --no-git-checks --access public",
"typecheck": "tsc -b"
},
"sideEffects": false,
Expand Down
3 changes: 1 addition & 2 deletions packages/data-lit-space-rock-game/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"build": "vite build",
"dev": "vite",
"test": "vitest --run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit",
"publish-public": "true"
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
Expand Down
3 changes: 1 addition & 2 deletions packages/data-lit-tictactoe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"build": "vite build",
"dev": "vite",
"test": "vitest --run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit",
"publish-public": "true"
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
Expand Down
3 changes: 1 addition & 2 deletions packages/data-lit-todo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"build": "vite build",
"dev": "vite",
"test": "vitest --run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit",
"publish-public": "true"
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion packages/data-lit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"dev": "tsc -b -w",
"link": "pnpm build && pnpm link --global",
"test": "vitest --run --passWithNoTests",
"publish-public": "pnpm build && pnpm publish --no-git-checks --access public",
"typecheck": "tsc -b"
},
"dependencies": {
Expand Down
3 changes: 1 addition & 2 deletions packages/data-react-hello/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"scripts": {
"build": "vite build",
"dev": "vite",
"typecheck": "tsc -p tsconfig.json --noEmit",
"publish-public": "true"
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
Expand Down
3 changes: 1 addition & 2 deletions packages/data-react-pixie/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"scripts": {
"build": "vite build",
"dev": "vite",
"typecheck": "tsc -p tsconfig.json --noEmit",
"publish-public": "true"
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion packages/data-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"dev": "tsc -b -w",
"link": "pnpm build && pnpm link --global",
"test": "vitest --run --passWithNoTests",
"publish-public": "pnpm build && pnpm publish --no-git-checks --access public",
"typecheck": "tsc -b"
},
"dependencies": {
Expand Down
3 changes: 1 addition & 2 deletions packages/data-solid-dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"scripts": {
"build": "vite build",
"dev": "vite",
"typecheck": "tsc -p tsconfig.json --noEmit",
"publish-public": "true"
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@adobe/data": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion packages/data-solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"dev": "tsc -b -w",
"link": "pnpm build && pnpm link --global",
"test": "vitest --run --passWithNoTests",
"publish-public": "pnpm build && pnpm publish --no-git-checks --access public",
"typecheck": "tsc -b"
},
"dependencies": {
Expand Down
5 changes: 2 additions & 3 deletions packages/data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,10 @@
"link": "pnpm build && pnpm link --global",
"pre-commit": "lint-staged",
"prepublishOnly": "node scripts/copy-references.js",
"publish-public": "pnpm build && pnpm publish --no-git-checks --access public",
"perftest": "node dist/perftest/index.js",
"check:emit": "node scripts/emit-stripinternal/check.mjs",
"test": "vitest --run",
"test:ci": "SKIP_PERF=1 vitest --run",
"test": "SKIP_PERF=1 vitest --run",
"test:perf": "vitest --run",
"asbuild:debug": "asc assembly/index.ts -o dist/assembly/index.wasm --target debug --enable simd && echo built dist/assembly/index.wasm",
"asbuild:release": "asc assembly/index.ts -o dist/assembly/index.wasm --target release --enable simd --optimize && echo built dist/assembly/index.wasm",
"start": "npx serve ."
Expand Down
44 changes: 44 additions & 0 deletions scripts/check-workspace-conventions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// © 2026 Adobe. MIT License. See /LICENSE for details.
//
// Enforces the workspace script conventions so the build/test/publish pipeline
// never forks on package identity. Root and CI invoke uniform recursive scripts
// (`pnpm -r run <script>`); each package owns HOW it builds/tests/publishes.
// This guard keeps every package honest so a new package "just works" without
// editing any central list.
//
// Rules:
// 1. Every package declares `private` explicitly (true|false). npm treats an
// absent `private` as publishable, so we require an explicit decision —
// absent must never silently mean "publish".
// 2. Publishing is governed solely by `private` + `pnpm -r publish`. A
// per-package `publish-public` script would reintroduce identity-specific
// publish logic, so it is forbidden.

import { readFileSync, readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const packagesDir = join(root, "packages");

const errors = [];
for (const name of readdirSync(packagesDir)) {
let pkg;
try {
pkg = JSON.parse(readFileSync(join(packagesDir, name, "package.json"), "utf8"));
} catch {
continue; // not a package directory
}
if (typeof pkg.private !== "boolean") {
errors.push(`${name}: must set "private": true or false explicitly (absent must not mean publishable)`);
}
if (pkg.scripts?.["publish-public"]) {
errors.push(`${name}: remove the "publish-public" script — publishing is governed by the "private" field and \`pnpm -r publish\``);
}
}

if (errors.length > 0) {
console.error("Workspace convention violations:\n " + errors.join("\n "));
process.exit(1);
}
console.log("workspace conventions OK");