refactor(web): split into GPL standalone editor + thin reference backend

Reduce web/ to a single generic editor app plus a minimal example backend,
moving all project-specific/app code out to the closed root repo:

- apps/frontend -> standalone (@pcbjam/standalone): the GPL editor. Strips the
  project-management UI (ProjectsPage/ProjectDetailPage/UploadDropzone and the
  write half of lib/api.ts). WasmTool now takes fetchBytes + assetBaseUrl as
  injected props (decoupled from the API client) so it can be driven by either
  a backend or a local folder. New HomePage (local-folder loader + backend
  project list) and read-only ProjectView. BroadcastChannel collab unchanged.
- backend (@pcbjam/backend-example): thin Fastify+ts-rest reference impl of the
  @pcbjam/shared contract — serves a single project off the local filesystem
  (PROJECT_DIR), no DB/auth/uploads.
- packages/contract -> web/pcbjam-shared (git submodule, MIT @pcbjam/shared).
- Remove apps/server, packages/storage, packages/contract, docker-compose
  (server + storage move to the closed root repo; preserved via subtree
  branches). Rewrite pnpm-workspace/turbo/.env for the frontend-only layout.
- Add tests/fixtures/demo so the editor + example backend run out of the box.

Standalone typechecks + builds; backend serves the contract end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergő Törcsvári 2026-06-09 12:35:46 +02:00
commit b6a77ee93c
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
80 changed files with 671 additions and 2992 deletions

View file

@ -1,116 +1,83 @@
# KiCad Web
# PCBJam Web — standalone editor (GPL)
Single web app to create/open KiCad projects, upload files, and open them in the
WASM tools (pcbnew / eeschema / calculator) by URL:
A self-contained, GPL web app that opens KiCad projects in the WASM tools
(pcbnew / eeschema / pl_editor / …) — from a local folder, or from any backend
that implements the MIT [`@pcbjam/shared`](./pcbjam-shared) contract. It opens a
tool by URL:
```
/p/<project>/<tool>/<file-path> e.g. /p/project5/pcbnew/nyak.kicad_pcb
/p/<project>/<tool>/<file-path> e.g. /p/demo/pcbnew/nyak.kicad_pcb
```
Design + decisions: [`../docs/features/web-init/0001-web-app-spec.md`](../docs/features/web-init/0001-web-app-spec.md).
This workspace contains **only** the generic editor and a thin reference
backend. All project-specific concerns (accounts, project management, uploads,
auth) live in the separate closed application, which reuses this editor by
hosting it standalone and redirecting to it (it must not link the GPL editor).
## Stack
- **Monorepo**: pnpm + turbo
- **Frontend**: Vite + React + TypeScript + shadcn/ui (`apps/frontend`)
- **Backend**: Fastify + ts-rest + Zod (`apps/server`)
- **DB**: Postgres + Drizzle (project/file metadata)
- **Storage**: pluggable `FileStorage` (local disk now, S3 later) (`packages/storage`)
- **Shared types**: ts-rest contract + Zod (`packages/contract`)
## Layout
```
web/
├── apps/
│ ├── frontend/ # Vite React app
│ └── server/ # Fastify API + WASM static + Drizzle
└── packages/
├── contract/ # ts-rest contract + Zod schemas (FE + BE share this)
└── storage/ # FileStorage interface + LocalDiskStorage
├── standalone/ # @pcbjam/standalone — the GPL editor (Vite + React)
├── backend/ # @pcbjam/backend-example — thin reference @pcbjam/shared impl
└── pcbjam-shared/ # @pcbjam/shared — the FE↔BE contract (git submodule, MIT)
```
- **Editor**: Vite + React + TypeScript. Boots a tool directly in the document
(no iframe), syncs the project tree into MEMFS, drives File→Open, and runs
same-tab collaboration over BroadcastChannel.
- **Example backend**: Fastify + ts-rest serving a single project off the local
filesystem (`PROJECT_DIR`). No DB, no auth, no uploads — the minimum the editor
needs, and a worked example of the contract.
## Quick start
```bash
cd web
cp .env.example .env # Postgres host port defaults to 54329 (non-default)
pnpm install
git submodule update --init web/pcbjam-shared # if not already populated
pnpm db:up # start Postgres (docker compose)
pnpm db:migrate # apply migrations + seed the default owner
cp standalone/.env.example standalone/.env
cp backend/.env.example backend/.env # PROJECT_DIR=../../tests/fixtures/demo
pnpm dev # turbo: server :3050 + frontend :3048
pnpm dev # turbo: backend :3060 + editor :3048
```
Open http://localhost:3048 — create a project, upload files (multi / folder /
.zip), then open a `.kicad_pcb` / `.kicad_sch` in its tool.
Open http://localhost:3048 — either **open a local folder** (no backend needed)
or open the backend's project. The editor can point at any conforming backend
via `VITE_API_BASE_URL`.
## WASM artifacts
The runtime artifacts (`<tool>.js/.wasm`, `wx.js`, `images.tar.gz`, plus the
`<tool>.html` harness pages) are build outputs, **not** committed here. The
complete set is synced into `tests/apps/kicad/` by
`tests/scripts/setup-kicad-wasm.sh` from repo-root `output/` (+ `wx.js` from
`wxwidgets/`; `output/` alone lacks `wx.js`). That script is a real **sync** —
it skips files already byte-identical at the destination, so re-running it does
not rewrite the multi-hundred-MB `.wasm`.
The runtime artifacts (`<tool>.js/.wasm`, `wx.js`, `images.tar.gz`, `<tool>.html`)
are build outputs, **not** committed. They are synced into `tests/apps/kicad/` by
`tests/scripts/setup-kicad-wasm.sh` (from repo-root `output/`).
**They must be served same-origin as the app.** Under the document's COEP/
**They must be served same-origin as the app.** Under the document's COEP /
cross-origin-isolation (set by the Vite dev server), KiCad WASM refuses to load
its glue/wasm from a different origin. So the app serves them from its own
origin with **no extra copy**: `pnpm dev` runs `scripts/link-wasm.mjs`, which
**symlinks** `apps/frontend/public/wasm → tests/apps/kicad`. Vite then serves
them at `/wasm` (same origin). `VITE_WASM_ASSET_BASE_URL` defaults to `/wasm`.
its glue/wasm from a different origin. `pnpm dev` runs `scripts/link-wasm.mjs`,
which **symlinks** `standalone/public/wasm → tests/apps/kicad`; Vite serves them
at `/wasm`. `VITE_WASM_ASSET_BASE_URL` defaults to `/wasm`.
- Point the symlink elsewhere with
`WASM_SRC_DIR=/path pnpm --filter @kicad-web/frontend link-wasm`.
- If the tool won't load, the target dir is probably empty — run
`WASM_SRC_DIR=/path pnpm --filter @pcbjam/standalone link-wasm`.
- If a tool won't load, the target dir is probably empty — run
`tests/scripts/setup-kicad-wasm.sh` to populate `tests/apps/kicad/`.
The tool view (`WasmTool.tsx` + `src/wasm/boot.ts`) boots the tool **directly in
the React document** — no iframe. It replicates the proven harness HTML
(`tests/apps/kicad/<tool>.html`): builds the same global Emscripten `Module`
config and preRun steps (create canvas, write `images.tar.gz`, seed config), then
injects the same `wx.js` + `<tool>.js` artifacts into the page. It then syncs the
project tree into MEMFS and drives File→Open. The build is non-modularized
(global `Module`/`FS`) and pthread-based, so only **one** tool runs per page load;
switching tools requires a full navigation. `locateFile` resolves the wasm and
the pthread worker against `<base>` so they load regardless of the SPA route.
**prod**: point `VITE_WASM_ASSET_BASE_URL` at a CDN URL — but that origin must
itself satisfy the same-origin / COEP constraints (e.g. served under the app's
own origin/path).
- **prod**: point `VITE_WASM_ASSET_BASE_URL` at a URL whose origin also satisfies
the same-origin / COEP constraints.
## Scripts
| Command | What |
|---|---|
| `pnpm dev` | server + frontend (turbo) |
| `pnpm db:up` / `pnpm db:down` | start/stop Postgres |
| `pnpm db:generate` | generate Drizzle migration SQL from schema |
| `pnpm db:migrate` | apply migrations + seed default owner |
| `pnpm db:seed` | (re)seed the default owner |
| `pnpm typecheck` | typecheck all packages |
| `pnpm dev` | editor + example backend (turbo) |
| `pnpm build` | build all packages |
| `pnpm typecheck` | typecheck all packages |
## API (shared via `packages/contract`)
## Contract (`@pcbjam/shared`, MIT)
JSON (ts-rest): `GET/POST /api/projects`, `GET/DELETE /api/projects/:project`,
`GET /api/projects/:project/files`.
Binary (raw Fastify, response shapes still shared via Zod):
`POST /api/projects/:project/files` (multi-file + folder),
`POST /api/projects/:project/files/zip`,
`GET /api/projects/:project/files/*` (stream bytes).
## Status / next iteration
Working end-to-end: create / open / upload (files, folder, zip) / file
download / WASM static serving / project list & detail UI / URL routing.
Booting a tool syncs the **whole** project tree into MEMFS, then opens the
target file. The open step (`apps/frontend/src/wasm/open-flow.ts`) prefers a
programmatic hook (`Module.kicadOpenFile`) and falls back to EXPERIMENTAL UI
automation ported from the e2e tests — this needs in-browser validation against
built artifacts, and exposing a real embind open-entry-point is the intended
follow-up (spec §11.2). Lazy/partial MEMFS loading and save-back land together
in a later iteration (spec §§9, 12).
The editor reads from a backend over the shared contract:
`GET /api/projects`, `GET /api/projects/:project`,
`GET /api/projects/:project/files`, and the streamed
`GET /api/projects/:project/files/*` (raw bytes). Management/write operations and
ownership are **not** part of this contract — they belong to the closed app.