Skip to content
Closed
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
2 changes: 0 additions & 2 deletions .cargo/config.toml

This file was deleted.

18 changes: 9 additions & 9 deletions .github/workflows/tauri-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ jobs:
pnpm -C browser install --frozen-lockfile
# The workspace root build, not `-C browser/data-browser build`.
# data-browser imports @tomic/lib, which is a workspace package that
# has to be BUILT before anything can resolve it — the root script
# does `--filter "@tomic/lib" run build` first for exactly that
# reason. Building the leaf alone fails with
# has to be BUILT before anything can resolve it — the root script's
# `pnpm run -r build` builds it first (workspace topological order)
# for exactly that reason. Building the leaf alone fails with
# `Rolldown failed to resolve import "@tomic/lib"`. Same command
# release.yml uses to build the assets it embeds in the crate.
pnpm -C browser build
Expand Down Expand Up @@ -442,9 +442,9 @@ jobs:
pnpm -C browser install --frozen-lockfile
# The workspace root build, not `-C browser/data-browser build`.
# data-browser imports @tomic/lib, which is a workspace package that
# has to be BUILT before anything can resolve it — the root script
# does `--filter "@tomic/lib" run build` first for exactly that
# reason. Building the leaf alone fails with
# has to be BUILT before anything can resolve it — the root script's
# `pnpm run -r build` builds it first (workspace topological order)
# for exactly that reason. Building the leaf alone fails with
# `Rolldown failed to resolve import "@tomic/lib"`. Same command
# release.yml uses to build the assets it embeds in the crate.
pnpm -C browser build
Expand Down Expand Up @@ -585,9 +585,9 @@ jobs:
pnpm -C browser install --frozen-lockfile
# The workspace root build, not `-C browser/data-browser build`.
# data-browser imports @tomic/lib, which is a workspace package that
# has to be BUILT before anything can resolve it — the root script
# does `--filter "@tomic/lib" run build` first for exactly that
# reason. Building the leaf alone fails with
# has to be BUILT before anything can resolve it — the root script's
# `pnpm run -r build` builds it first (workspace topological order)
# for exactly that reason. Building the leaf alone fails with
# `Rolldown failed to resolve import "@tomic/lib"`. Same command
# release.yml uses to build the assets it embeds in the crate.
pnpm -C browser build
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ scratchpad
node_modules/
# Raw footage and cuts from browser/e2e/scripts/record-*.mjs
/browser/e2e/recordings/

# Bundled by browser/data-browser/scripts/build-website-runtime.mjs, which
# vite.config.ts runs on every vite / vitest start.
/browser/data-browser/src/chunks/Website/runtime/search-view.html
/browser/data-browser/src/chunks/Website/runtime/website-runtime.min.js
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"ATOMICSERVER_SKIP_JS_BUILD": "true"
},
// The check command runs build scripts: without this, atomic-server's
// build.rs shells out to `cargo bin wasm-pack build`, whose nested cargo
// build.rs shells out to wasm-pack (via the `tools/cargo-bin` shim), whose nested cargo
// builds into the shared `target/` and grabs its lock — blocking terminal
// `cargo build/test`. `cargo.extraEnv` does not reliably reach the check
// command, so set it on the check too.
Expand Down
2 changes: 1 addition & 1 deletion .zed/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"command": "clippy",
"targetDir": "target/rust-analyzer",
// The check command runs build scripts: without this, atomic-server's
// build.rs shells out to `cargo bin wasm-pack build`, whose NESTED
// build.rs shells out to wasm-pack (via the `tools/cargo-bin` shim), whose NESTED
// cargo builds into the shared `target/` (ignoring the targetDir
// above) and grabs its lock — blocking terminal `cargo build/test`
// with "Blocking waiting for file lock on artifact directory".
Expand Down
28 changes: 13 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,25 @@ The frontend auto-updates via HMR. If changes don't appear, reload the page. If

## Fresh Worktrees

A new `git worktree` starts with no installed `browser/node_modules` and no
built `dist/` for the internal JS packages — none of that is git-tracked, so
it doesn't come along with the checkout. Before your first `git commit` that
touches any `.rs`, `Cargo.toml`/`Cargo.lock` file in a fresh worktree, run a
full build so both sides are warm:
A new `git worktree` has no `browser/node_modules`, no built `dist/` and no
WASM; none of that is tracked. Set it up with:

```
cd browser && pnpm install && pnpm run -r build
cd ../server && cargo build
```

Skipping this turns a setup problem into a confusing commit-time failure:
`scripts/pre-commit.mjs`'s Rust step (any staged `.rs`/`Cargo.*` file) runs
`cargo clippy` against a snapshot containing only staged, git-tracked files —
no `node_modules`, no `assets_tmp`. `server/build.rs` then has to run its own
`pnpm install && pnpm run build` from scratch inside that snapshot to embed
the frontend. If this worktree's own `browser/` was never fully installed and
built, that inner build fails too, and it fails with errors that look
unrelated to your change (a missing package, a stale/missing export from a
workspace package) rather than anything obviously about "run pnpm install."
Warming the worktree once up front avoids that entirely.
That is all; no workarounds needed. The build includes the WASM, and the
first run compiles wasm-pack into this worktree's own `.bin/` (about a
minute). Don't borrow the main checkout's wasm-pack or `public/wasm/`, and
don't build `@tomic/lib` separately first: `pnpm run -r build` orders the
packages itself. The Website runtime bundles (`search-view.html`,
`website-runtime.min.js`) are gitignored and regenerated by vite, so builds
leave `git status` clean. Node 22 (CI) through 26 work, vitest included.

The pre-commit hook lints and clippies a snapshot of the staged files. It
borrows this worktree's `node_modules` (so `pnpm install` must have run) and
skips the frontend build, so it needs nothing else.

## Planning

Expand Down
8 changes: 4 additions & 4 deletions browser/data-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,17 @@
},
"scripts": {
"build:website-runtime": "node scripts/build-website-runtime.mjs",
"build": "node scripts/build-website-runtime.mjs && if [ \"$SKIP_WASM_BUILD\" = \"1\" ]; then echo 'SKIP_WASM_BUILD=1, skipping wasm-pack'; else pnpm build:wasm; fi && vite build",
"build": "if [ \"$SKIP_WASM_BUILD\" = \"1\" ]; then echo 'SKIP_WASM_BUILD=1, skipping wasm-pack'; else pnpm build:wasm; fi && vite build",
"build:tauri": "pnpm --filter \"@tomic/lib\" --filter \"@tomic/react\" run build && pnpm build:wasm && TAURI=1 vite build",
"build:wasm": "cd ../../wasm && export CARGO_TARGET_DIR=../target/wasm-pack && cargo bin wasm-pack --version && env -u CARGO_ENCODED_RUSTFLAGS -u RUSTFLAGS cargo bin wasm-pack build --target web --out-dir pkg && cp pkg/atomic_wasm.js pkg/atomic_wasm_bg.wasm ../browser/data-browser/public/wasm/",
"dev": "node scripts/build-website-runtime.mjs && vite",
"build:wasm": "cd ../../wasm && export CARGO_TARGET_DIR=../target/wasm-pack && cargo run -q --package cargo-bin -- wasm-pack --version && env -u CARGO_ENCODED_RUSTFLAGS -u RUSTFLAGS cargo run -q --package cargo-bin -- wasm-pack build --target web --out-dir pkg && cp pkg/atomic_wasm.js pkg/atomic_wasm_bg.wasm ../browser/data-browser/public/wasm/",
"dev": "vite",
"dev:tauri": "pnpm --filter \"@tomic/lib\" --filter \"@tomic/react\" run build && TAURI=1 vite",
"format-check": "oxfmt -c ../.oxfmtrc.json --check ./src",
"format": "oxfmt -c ../.oxfmtrc.json ./src",
"lint": "oxlint -c ../.oxlintrc.json . && pnpm format-check",
"lint-fix": "oxlint -c ../.oxlintrc.json --fix . && pnpm format",
"preview": "vite preview",
"start": "node scripts/build-website-runtime.mjs && vite",
"start": "vite",
"test": "vitest run --exclude scripts/integration-mcp.test.mjs && node --test scripts/integration-mcp.test.mjs",
"typecheck": "pnpm exec tsc --noEmit -p tsconfig.json",
"clean-translations": "pnpm exec wuchale --clean"
Expand Down
20 changes: 16 additions & 4 deletions browser/data-browser/scripts/build-website-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@ import { build } from 'esbuild';
import { createHash } from 'node:crypto';
import { writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';

// Bundles the Website chunk's sandboxed runtime into `search-view.html` and
// `website-runtime.min.js`. Both outputs are gitignored build artifacts:
// `vite.config.ts` calls this on every vite / vitest start, so dev, build,
// tests and `server/build.rs` (via `pnpm run build`) all regenerate them.
// Committing them let the copies drift from their sources, and every build
// then rewrote tracked files.
const root = new URL('../src/chunks/Website/runtime/', import.meta.url);
const bundle = async name => (await build({ entryPoints: [fileURLToPath(new URL(name + '.ts', root))], bundle: true, write: false, format: 'iife', platform: 'browser', target: 'es2022', minify: true })).outputFiles[0].text;
const search = await bundle('searchView');
const hash = createHash('sha256').update(search).digest('base64');
await writeFile(new URL('search-view.html', root), `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-${hash}'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"><title>Search content</title><style>body{font:16px/1.6 system-ui;color:#213b34;margin:0;padding:1rem;background:#f4f3eb}label{display:block;font-weight:650}input{display:block;width:100%;box-sizing:border-box;padding:.8rem;border:1px solid #668478;border-radius:8px;font:inherit;margin:.6rem 0}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}.card{padding:1.5rem;border:1px solid #a9b8ad;border-radius:12px;background:white;margin:0}dt{font-size:.75rem;text-transform:uppercase;color:#637d6d}dd{margin:0 0 1rem;white-space:pre-wrap}</style></head><body><p>Loading search…</p><script>${search}</script></body></html>`);
await writeFile(new URL('website-runtime.min.js', root), '/* eslint-disable */\n// @wc-ignore-file\n' + await bundle('websiteRuntime'));

export async function buildWebsiteRuntime() {
const search = await bundle('searchView');
const hash = createHash('sha256').update(search).digest('base64');
await writeFile(new URL('search-view.html', root), `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-${hash}'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"><title>Search content</title><style>body{font:16px/1.6 system-ui;color:#213b34;margin:0;padding:1rem;background:#f4f3eb}label{display:block;font-weight:650}input{display:block;width:100%;box-sizing:border-box;padding:.8rem;border:1px solid #668478;border-radius:8px;font:inherit;margin:.6rem 0}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}.card{padding:1.5rem;border:1px solid #a9b8ad;border-radius:12px;background:white;margin:0}dt{font-size:.75rem;text-transform:uppercase;color:#637d6d}dd{margin:0 0 1rem;white-space:pre-wrap}</style></head><body><p>Loading search…</p><script>${search}</script></body></html>`);
await writeFile(new URL('website-runtime.min.js', root), '/* eslint-disable */\n// @wc-ignore-file\n' + await bundle('websiteRuntime'));
}

if (process.argv[1] === fileURLToPath(import.meta.url)) await buildWebsiteRuntime();

This file was deleted.

This file was deleted.

19 changes: 19 additions & 0 deletions browser/data-browser/src/test-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Node 25+ ships Web Storage on `globalThis` (`--experimental-webstorage` is on
// by default), and without `--localstorage-file` its `localStorage` is
// undefined. Vitest's jsdom environment only copies window keys the global
// does not already have, so jsdom's working Storage never reaches `window`
// and every `// @vitest-environment jsdom` test that touches storage throws.
// Put jsdom's back. A no-op under Node 22/24 and in the node environment.
const dom = (globalThis as { jsdom?: { window: Window } }).jsdom;

if (dom) {
for (const key of ['localStorage', 'sessionStorage'] as const) {
if (!globalThis[key]) {
Object.defineProperty(globalThis, key, {
value: dom.window[key],
configurable: true,
writable: true,
});
}
}
}
18 changes: 18 additions & 0 deletions browser/data-browser/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { execSync } from 'node:child_process';
import { buildWebsiteRuntime } from './scripts/build-website-runtime.mjs';

// TAURI=1 produces a Tauri-compatible bundle: no CSP nonces (Tauri serves
// HTML verbatim, so the server's runtime ATOMICSERVER_NONCE substitution
Expand Down Expand Up @@ -174,6 +175,20 @@ export default defineConfig(({ mode }) => {
},
plugins: [
wasm(),
{
// `src/chunks/Website/runtime/{search-view.html,website-runtime.min.js}`
// are gitignored bundles of the runtime's `.ts` sources. `config` runs
// for dev, build and vitest alike, so every consumer finds them fresh.
name: 'website-runtime',
async config() {
await buildWebsiteRuntime();
},
async handleHotUpdate({ file }) {
if (/Website\/runtime\/[^/]+\.ts$/.test(file)) {
await buildWebsiteRuntime();
}
},
},
{
// index.html preloads the wasm pair to warm the worker's fetch, so those
// hrefs have to carry the same `?v=` the app requests — a preload for a
Expand Down Expand Up @@ -452,6 +467,9 @@ export default defineConfig(({ mode }) => {
// boot pre-optimizes everything, so first-open is warm and e2e is stable.
entries: ['./index.html', './src/chunks/**/*.{ts,tsx}'],
},
test: {
setupFiles: ['./src/test-setup.ts'],
},
build: {
target: 'baseline-widely-available',
outDir: isTauri ? 'dist-tauri' : 'dist',
Expand Down
3 changes: 1 addition & 2 deletions browser/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.2",
"@microsoft/api-extractor": "^7.58.7",
"@tomic/cli": "workspace:*",
"loro-crdt": "^1.12.1",
"oxfmt": "^0.51.0",
"tslib": "^2.8.1",
Expand Down Expand Up @@ -72,7 +71,7 @@
},
"scripts": {
"build": "tsup",
"generate-ontologies": "ad-generate ontologies",
"generate-ontologies": "pnpm --filter @tomic/cli run build && node ../cli/bin/src/index.js ontologies",
"lint-package": "pnpm publint && pnpm attw",
"lint": "oxlint -c ../.oxlintrc.json ./src && pnpm format-check",
"lint-fix": "oxlint -c ../.oxlintrc.json --fix ./src && pnpm format",
Expand Down
2 changes: 1 addition & 1 deletion browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"lint-package": "pnpm run -r lint-package",
"format-check": "pnpm run -r format-check",
"format": "pnpm run -r format",
"build": "pnpm --filter \"@tomic/lib\" run build && pnpm --filter=!./lib run -r build ",
"build": "pnpm run -r build",
"test": "pnpm run -r test",
"test-e2e": "pnpm run --filter @tomic/e2e test-e2e",
"test-e2e:light": "pnpm run --filter @tomic/e2e test-e2e:light",
Expand Down
Loading