diff --git a/.gitmodules b/.gitmodules index 1414ae8..1b6f54c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,7 @@ path = wxwidgets url = git@github.com:emergence-engineering/wxWidgets.git branch = wasm-port +[submodule "web/pcbjam-shared"] + path = web/pcbjam-shared + url = git@github.com:emergence-engineering/pcbjam-shared.git + branch = main diff --git a/web/apps/server/seed-data/demo.kicad_pcb b/tests/fixtures/demo/demo.kicad_pcb similarity index 100% rename from web/apps/server/seed-data/demo.kicad_pcb rename to tests/fixtures/demo/demo.kicad_pcb diff --git a/web/apps/server/seed-data/demo.kicad_sch b/tests/fixtures/demo/demo.kicad_sch similarity index 100% rename from web/apps/server/seed-data/demo.kicad_sch rename to tests/fixtures/demo/demo.kicad_sch diff --git a/web/apps/server/seed-data/demo.kicad_wks b/tests/fixtures/demo/demo.kicad_wks similarity index 100% rename from web/apps/server/seed-data/demo.kicad_wks rename to tests/fixtures/demo/demo.kicad_wks diff --git a/web/.env.example b/web/.env.example index 9940e95..06fd159 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,29 +1,30 @@ # --------------------------------------------------------------------------- -# Copy to web/.env and adjust as needed. docker-compose and the server both -# read this file. +# This is a standalone, frontend-only workspace: the `standalone` editor app +# and an example `backend`. Each app reads its OWN .env — see: +# standalone/.env.example (Vite frontend) +# backend/.env.example (thin reference backend) +# Copy each to a sibling `.env`. The values below are only a convenience +# overview; turbo passes these through to the apps. # --------------------------------------------------------------------------- -# --- Postgres (docker-compose) --- -POSTGRES_USER=kicad -POSTGRES_PASSWORD=kicad -POSTGRES_DB=kicad_web -# Non-default host port to avoid colliding with a local Postgres on 5432. -POSTGRES_PORT=54329 - -# --- Server --- -PORT=3050 -# Connection string MUST match the POSTGRES_* values + port above. -DATABASE_URL=postgres://kicad:kicad@localhost:54329/kicad_web -# Where uploaded project file bytes are stored by the local-disk FileStorage. -STORAGE_ROOT=./.data/storage -# Owner namespace used while there is no auth (seeded on first migrate). -DEFAULT_OWNER_SLUG=default -# Browser origin allowed to call the API (the Vite dev server, non-default port). -CORS_ORIGIN=http://localhost:3048 - -# --- Frontend (Vite, must be VITE_-prefixed) --- -VITE_API_BASE_URL=http://localhost:3050 -# SAME-ORIGIN: artifacts are smart-copied into public/wasm on `dev` and served by -# Vite at /wasm. Required because KiCad WASM pthread workers can't be cross-origin. -# For prod, set to a CDN URL whose origin also satisfies the worker/COEP rules. +# --- standalone editor (Vite, must be VITE_-prefixed) --- +# Backend implementing the @pcbjam/shared contract (the example backend, or any +# conforming backend such as the closed app's server). Leave unset to use only +# the local-folder loader. +VITE_API_BASE_URL=http://localhost:3060 +# Where the WASM glue/artifacts are served from. SAME-ORIGIN "/wasm" is required +# because KiCad WASM pthread workers cannot be created cross-origin; on `dev` the +# artifacts are symlinked into standalone/public/wasm and served by Vite at /wasm. +# For prod, set to an absolute URL whose origin also satisfies the COEP rules. VITE_WASM_ASSET_BASE_URL=/wasm +# Override the artifact source dir the dev symlink points at (default: +# /tests/apps/kicad, populated by tests/scripts/setup-kicad-wasm.sh). +# WASM_SRC_DIR= + +# --- example backend --- +# Absolute or relative path to a single KiCad project folder to serve. +PROJECT_DIR=../../tests/fixtures/demo +# Port the backend listens on (the standalone's VITE_API_BASE_URL must match). +PORT=3060 +# Browser origin allowed to call the backend (the Vite dev server). +CORS_ORIGIN=http://localhost:3048 diff --git a/web/README.md b/web/README.md index b4597af..f5fb8fc 100644 --- a/web/README.md +++ b/web/README.md @@ -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/// e.g. /p/project5/pcbnew/nyak.kicad_pcb +/p/// 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 (`.js/.wasm`, `wx.js`, `images.tar.gz`, plus the -`.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 (`.js/.wasm`, `wx.js`, `images.tar.gz`, `.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/.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` + `.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 `` 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. diff --git a/web/apps/frontend/src/components/UploadDropzone.tsx b/web/apps/frontend/src/components/UploadDropzone.tsx deleted file mode 100644 index 026f40b..0000000 --- a/web/apps/frontend/src/components/UploadDropzone.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import * as React from "react"; -import { useQueryClient } from "@tanstack/react-query"; -import { Files, FolderUp, Loader2, Package } from "lucide-react"; -import { uploadFiles, uploadZip, type UploadItem } from "@/lib/api"; -import { Button } from "@/components/ui/button"; - -export function UploadDropzone({ slug }: { slug: string }) { - const qc = useQueryClient(); - const [busy, setBusy] = React.useState(false); - const [error, setError] = React.useState(null); - const filesRef = React.useRef(null); - const folderRef = React.useRef(null); - const zipRef = React.useRef(null); - - // `webkitdirectory` isn't in the React input typings; set it imperatively. - React.useEffect(() => { - if (folderRef.current) { - folderRef.current.setAttribute("webkitdirectory", ""); - folderRef.current.setAttribute("directory", ""); - } - }, []); - - const refresh = () => qc.invalidateQueries({ queryKey: ["project", slug] }); - - const run = async (fn: () => Promise) => { - setBusy(true); - setError(null); - try { - await fn(); - await refresh(); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setBusy(false); - } - }; - - const onFiles = (e: React.ChangeEvent) => { - const list = e.target.files; - if (!list || list.length === 0) return; - const items: UploadItem[] = Array.from(list).map((file) => ({ - // folder picker → webkitRelativePath; multi-file picker → name - path: - (file as File & { webkitRelativePath?: string }).webkitRelativePath || - file.name, - file, - })); - void run(() => uploadFiles(slug, items)); - e.target.value = ""; - }; - - const onZip = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - void run(() => uploadZip(slug, file)); - e.target.value = ""; - }; - - return ( -
-
- - - - {busy && ( - - uploading… - - )} -
- {error &&

{error}

} - - - - -
- ); -} diff --git a/web/apps/frontend/src/lib/api.ts b/web/apps/frontend/src/lib/api.ts deleted file mode 100644 index 9ff5fbc..0000000 --- a/web/apps/frontend/src/lib/api.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - contract, - type Project, - type ProjectFile, - type ProjectWithFiles, - type UploadResponse, -} from "@kicad-web/contract"; -import { initClient } from "@ts-rest/core"; -import { - useMutation, - useQuery, - useQueryClient, -} from "@tanstack/react-query"; -import { API_BASE_URL } from "./config"; - -export const client = initClient(contract, { - baseUrl: API_BASE_URL, - baseHeaders: {}, -}); - -// --- queries --- - -export function useProjects() { - return useQuery({ - queryKey: ["projects"], - queryFn: async (): Promise => { - const res = await client.listProjects(); - if (res.status !== 200) throw new Error("failed to list projects"); - return res.body; - }, - }); -} - -export function useProject(slug: string) { - return useQuery({ - queryKey: ["project", slug], - queryFn: async (): Promise => { - const res = await client.getProject({ params: { project: slug } }); - if (res.status === 404) throw new Error("project not found"); - if (res.status !== 200) throw new Error("failed to load project"); - return res.body; - }, - }); -} - -// --- mutations --- - -export function useCreateProject() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: async (input: { - name: string; - slug?: string; - }): Promise => { - const res = await client.createProject({ body: input }); - if (res.status === 409) throw new Error("a project with that slug exists"); - if (res.status !== 201) throw new Error("failed to create project"); - return res.body; - }, - onSuccess: () => qc.invalidateQueries({ queryKey: ["projects"] }), - }); -} - -export function useDeleteProject() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: async (slug: string): Promise => { - const res = await client.deleteProject({ - params: { project: slug }, - body: {}, - }); - if (res.status !== 200) throw new Error("failed to delete project"); - }, - onSuccess: () => qc.invalidateQueries({ queryKey: ["projects"] }), - }); -} - -// --- raw binary endpoints (not in the ts-rest contract) --- - -export interface UploadItem { - /** project-relative path; folders use webkitRelativePath */ - path: string; - file: File; -} - -export async function uploadFiles( - slug: string, - items: UploadItem[], -): Promise { - const form = new FormData(); - for (const item of items) { - // Field name carries the relative path (server reads part.fieldname). - form.append(item.path, item.file, item.file.name); - } - const res = await fetch( - `${API_BASE_URL}/api/projects/${encodeURIComponent(slug)}/files`, - { method: "POST", body: form }, - ); - if (!res.ok) throw new Error(`upload failed: ${res.status}`); - return ((await res.json()) as UploadResponse).files; -} - -export async function uploadZip( - slug: string, - zip: File, -): Promise { - const form = new FormData(); - form.append("zip", zip, zip.name); - const res = await fetch( - `${API_BASE_URL}/api/projects/${encodeURIComponent(slug)}/files/zip`, - { method: "POST", body: form }, - ); - if (!res.ok) throw new Error(`zip upload failed: ${res.status}`); - return ((await res.json()) as UploadResponse).files; -} - -export function fileBytesUrl(slug: string, relPath: string): string { - const encoded = relPath - .split("/") - .map((seg) => encodeURIComponent(seg)) - .join("/"); - return `${API_BASE_URL}/api/projects/${encodeURIComponent(slug)}/files/${encoded}`; -} - -export async function fetchFileBytes( - slug: string, - relPath: string, -): Promise { - const res = await fetch(fileBytesUrl(slug, relPath)); - if (!res.ok) throw new Error(`download failed (${res.status}): ${relPath}`); - return new Uint8Array(await res.arrayBuffer()); -} diff --git a/web/apps/frontend/src/pages/ProjectsPage.tsx b/web/apps/frontend/src/pages/ProjectsPage.tsx deleted file mode 100644 index 3452be3..0000000 --- a/web/apps/frontend/src/pages/ProjectsPage.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import * as React from "react"; -import { Link } from "react-router-dom"; -import { Loader2, Plus, Trash2 } from "lucide-react"; -import { useCreateProject, useDeleteProject, useProjects } from "@/lib/api"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; - -function CreateProjectDialog() { - const create = useCreateProject(); - const [open, setOpen] = React.useState(false); - const [name, setName] = React.useState(""); - const [slug, setSlug] = React.useState(""); - - const submit = async () => { - if (!name.trim()) return; - await create.mutateAsync({ - name: name.trim(), - slug: slug.trim() || undefined, - }); - setName(""); - setSlug(""); - setOpen(false); - }; - - return ( - - - - - - - Create a project - - A project holds a tree of KiCad files you can open in the browser. - - -
-
- - setName(e.target.value)} - /> -
-
- - setSlug(e.target.value)} - /> -
- {create.error && ( -

- {(create.error as Error).message} -

- )} -
- - - -
-
- ); -} - -export function ProjectsPage() { - const { data: projects, isLoading, error } = useProjects(); - const del = useDeleteProject(); - - return ( -
-
-
-

Projects

-

- Create a project, upload KiCad files, open them in the browser. -

-
- -
- - {isLoading && ( -

- loading… -

- )} - {error && ( -

- Could not load projects: {(error as Error).message} -

- )} - -
- {projects?.map((p) => ( - - - {p.name} - /p/{p.slug} - - - - - - - ))} -
- - {projects && projects.length === 0 && !isLoading && ( -

No projects yet. Create one above.

- )} -
- ); -} diff --git a/web/apps/server/drizzle.config.ts b/web/apps/server/drizzle.config.ts deleted file mode 100644 index d7d68f0..0000000 --- a/web/apps/server/drizzle.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; -import { config } from "dotenv"; -import { defineConfig } from "drizzle-kit"; - -const here = path.dirname(fileURLToPath(import.meta.url)); -// web/.env is two levels up from apps/server. -config({ path: path.resolve(here, "../../.env") }); - -export default defineConfig({ - schema: "./src/db/schema.ts", - out: "./drizzle", - dialect: "postgresql", - dbCredentials: { - url: process.env.DATABASE_URL ?? "", - }, -}); diff --git a/web/apps/server/drizzle/0000_lucky_husk.sql b/web/apps/server/drizzle/0000_lucky_husk.sql deleted file mode 100644 index c0fd597..0000000 --- a/web/apps/server/drizzle/0000_lucky_husk.sql +++ /dev/null @@ -1,31 +0,0 @@ -CREATE TABLE "owner" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "slug" text NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "owner_slug_unique" UNIQUE("slug") -); ---> statement-breakpoint -CREATE TABLE "project_file" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "project_id" uuid NOT NULL, - "path" text NOT NULL, - "size" bigint NOT NULL, - "content_type" text NOT NULL, - "storage_key" text NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "project_file_path_uq" UNIQUE("project_id","path") -); ---> statement-breakpoint -CREATE TABLE "project" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "owner_id" uuid NOT NULL, - "slug" text NOT NULL, - "name" text NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "project_owner_slug_uq" UNIQUE("owner_id","slug") -); ---> statement-breakpoint -ALTER TABLE "project_file" ADD CONSTRAINT "project_file_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "project" ADD CONSTRAINT "project_owner_id_owner_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."owner"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/web/apps/server/drizzle/meta/0000_snapshot.json b/web/apps/server/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 0624013..0000000 --- a/web/apps/server/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,222 +0,0 @@ -{ - "id": "921143e5-34f7-42f8-8c3b-28918d8201fd", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.owner": { - "name": "owner", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "owner_slug_unique": { - "name": "owner_slug_unique", - "nullsNotDistinct": false, - "columns": [ - "slug" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.project_file": { - "name": "project_file", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "project_id": { - "name": "project_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "size": { - "name": "size", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "content_type": { - "name": "content_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "storage_key": { - "name": "storage_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "project_file_project_id_project_id_fk": { - "name": "project_file_project_id_project_id_fk", - "tableFrom": "project_file", - "tableTo": "project", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "project_file_path_uq": { - "name": "project_file_path_uq", - "nullsNotDistinct": false, - "columns": [ - "project_id", - "path" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.project": { - "name": "project", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "owner_id": { - "name": "owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "project_owner_id_owner_id_fk": { - "name": "project_owner_id_owner_id_fk", - "tableFrom": "project", - "tableTo": "owner", - "columnsFrom": [ - "owner_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "project_owner_slug_uq": { - "name": "project_owner_slug_uq", - "nullsNotDistinct": false, - "columns": [ - "owner_id", - "slug" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/web/apps/server/drizzle/meta/_journal.json b/web/apps/server/drizzle/meta/_journal.json deleted file mode 100644 index f39224e..0000000 --- a/web/apps/server/drizzle/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1780307401097, - "tag": "0000_lucky_husk", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/web/apps/server/package.json b/web/apps/server/package.json deleted file mode 100644 index ef6eede..0000000 --- a/web/apps/server/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@kicad-web/server", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "tsx watch src/server.ts", - "start": "node dist/server.js", - "build": "tsc -p tsconfig.json --noEmit false --declaration false --outDir dist", - "typecheck": "tsc --noEmit", - "db:generate": "drizzle-kit generate", - "db:migrate": "tsx src/db/migrate.ts", - "db:push": "drizzle-kit push", - "db:seed": "tsx src/db/seed.ts" - }, - "dependencies": { - "@fastify/cors": "^9.0.1", - "@fastify/multipart": "^8.3.0", - "@kicad-web/contract": "workspace:*", - "@kicad-web/storage": "workspace:*", - "@ts-rest/fastify": "^3.52.1", - "dotenv": "^16.4.7", - "drizzle-orm": "^0.38.3", - "fastify": "^4.29.0", - "pg": "^8.13.1", - "unzipper": "^0.12.3", - "zod": "^3.24.1" - }, - "devDependencies": { - "@types/node": "^22.10.5", - "@types/pg": "^8.11.10", - "@types/unzipper": "^0.10.10", - "drizzle-kit": "^0.30.1", - "tsx": "^4.19.2", - "typescript": "^5.7.3" - } -} diff --git a/web/apps/server/src/app.ts b/web/apps/server/src/app.ts deleted file mode 100644 index 393ca57..0000000 --- a/web/apps/server/src/app.ts +++ /dev/null @@ -1,32 +0,0 @@ -import cors from "@fastify/cors"; -import multipart from "@fastify/multipart"; -import Fastify, { type FastifyInstance } from "fastify"; -import { env } from "./env.js"; -import { apiPlugin } from "./routes/api.js"; -import { fileRoutes } from "./routes/files.js"; - -export async function buildApp(): Promise { - const app = Fastify({ - logger: true, - // Project files (whole KiCad trees) and zips can be large. - bodyLimit: 1024 * 1024 * 1024, - }); - - await app.register(cors, { - origin: env.CORS_ORIGIN === "*" ? true : env.CORS_ORIGIN.split(","), - }); - - await app.register(multipart, { - limits: { - fileSize: 1024 * 1024 * 1024, // 1 GiB per file - files: 5000, // a KiCad project can have many lib files - }, - }); - - app.get("/health", async () => ({ ok: true })); - - await app.register(apiPlugin); - await app.register(fileRoutes); - - return app; -} diff --git a/web/apps/server/src/db/index.ts b/web/apps/server/src/db/index.ts deleted file mode 100644 index 0038f9f..0000000 --- a/web/apps/server/src/db/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { drizzle } from "drizzle-orm/node-postgres"; -import pg from "pg"; -import { env } from "../env.js"; -import * as schema from "./schema.js"; - -export const pool = new pg.Pool({ connectionString: env.DATABASE_URL }); -export const db = drizzle(pool, { schema }); -export { schema }; -export type Db = typeof db; diff --git a/web/apps/server/src/db/migrate.ts b/web/apps/server/src/db/migrate.ts deleted file mode 100644 index 504a837..0000000 --- a/web/apps/server/src/db/migrate.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; -import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { db, pool } from "./index.js"; -import { seedDefaultOwner, seedDemoProject } from "./seed.js"; - -const here = path.dirname(fileURLToPath(import.meta.url)); - -async function main() { - await migrate(db, { - migrationsFolder: path.resolve(here, "../../drizzle"), - }); - const ownerId = await seedDefaultOwner(); - console.log(`migrations applied; default owner: ${ownerId}`); - // Give a fresh install a ready-to-open demo project (idempotent). - await seedDemoProject(); - await pool.end(); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/web/apps/server/src/db/schema.ts b/web/apps/server/src/db/schema.ts deleted file mode 100644 index a1b6e86..0000000 --- a/web/apps/server/src/db/schema.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - bigint, - pgTable, - text, - timestamp, - unique, - uuid, -} from "drizzle-orm/pg-core"; - -export const owners = pgTable("owner", { - id: uuid("id").primaryKey().defaultRandom(), - slug: text("slug").notNull().unique(), - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), -}); - -export const projects = pgTable( - "project", - { - id: uuid("id").primaryKey().defaultRandom(), - ownerId: uuid("owner_id") - .notNull() - .references(() => owners.id, { onDelete: "cascade" }), - slug: text("slug").notNull(), - name: text("name").notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .notNull() - .defaultNow(), - }, - (t) => [unique("project_owner_slug_uq").on(t.ownerId, t.slug)], -); - -export const projectFiles = pgTable( - "project_file", - { - id: uuid("id").primaryKey().defaultRandom(), - projectId: uuid("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - // POSIX project-relative path, e.g. "pcbnew/nyak.kicad_pcb". - path: text("path").notNull(), - size: bigint("size", { mode: "number" }).notNull(), - contentType: text("content_type").notNull(), - // Opaque key handed to FileStorage; decouples logical path from blob layout. - storageKey: text("storage_key").notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .notNull() - .defaultNow(), - }, - (t) => [unique("project_file_path_uq").on(t.projectId, t.path)], -); - -export type OwnerRow = typeof owners.$inferSelect; -export type ProjectRow = typeof projects.$inferSelect; -export type ProjectFileRow = typeof projectFiles.$inferSelect; diff --git a/web/apps/server/src/db/seed.ts b/web/apps/server/src/db/seed.ts deleted file mode 100644 index cdfcf52..0000000 --- a/web/apps/server/src/db/seed.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as fs from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { eq } from "drizzle-orm"; -import { env } from "../env.js"; -import { db, pool } from "./index.js"; -import { owners } from "./schema.js"; -import { - createProject, - getProjectRowBySlug, - writeProjectFile, -} from "../services/projects.js"; - -/** Ensure the default owner namespace exists (no-auth iteration). */ -export async function seedDefaultOwner(): Promise { - const existing = await db - .select() - .from(owners) - .where(eq(owners.slug, env.DEFAULT_OWNER_SLUG)) - .limit(1); - if (existing[0]) return existing[0].id; - - const inserted = await db - .insert(owners) - .values({ slug: env.DEFAULT_OWNER_SLUG }) - .onConflictDoNothing() - .returning(); - if (inserted[0]) return inserted[0].id; - - // Lost a race; re-read. - const row = await db - .select() - .from(owners) - .where(eq(owners.slug, env.DEFAULT_OWNER_SLUG)) - .limit(1); - if (!row[0]) throw new Error("failed to seed default owner"); - return row[0].id; -} - -/** - * Demo project bytes committed at /seed-data/. Resolved relative to this - * module so it works both under tsx (src/db/) and compiled (dist/db/) — both are - * two levels below the package root, where seed-data lives. - */ -const SEED_DATA_DIR = new URL("../../seed-data/", import.meta.url); - -const DEMO_SLUG = "demo"; -const DEMO_NAME = "Demo Project"; -/** Committed files → project-relative paths. One per openable tool. */ -const DEMO_FILES = [ - "demo.kicad_sch", // eeschema (Schematic Editor) - "demo.kicad_pcb", // pcbnew (PCB Editor) - "demo.kicad_wks", // pl_editor (Drawing Sheet Editor) -] as const; - -/** - * Seed a ready-to-open demo project so a freshly cloned + migrated install has - * something to click on (and to exercise the tool wiring). Idempotent: skips if - * the "demo" project already exists. Reads bytes from the committed seed-data/ - * dir, so it needs no submodule checkout at runtime. - */ -export async function seedDemoProject(): Promise { - if (await getProjectRowBySlug(DEMO_SLUG)) { - console.log(`demo project "${DEMO_SLUG}" already exists — skipping`); - return; - } - - const project = await createProject(DEMO_NAME, DEMO_SLUG); - const row = await getProjectRowBySlug(DEMO_SLUG); - if (!row) throw new Error("demo project vanished right after creation"); - - for (const name of DEMO_FILES) { - const data = await fs.readFile(fileURLToPath(new URL(name, SEED_DATA_DIR))); - await writeProjectFile({ project: row, rawPath: name, data }); - console.log(` seeded ${DEMO_SLUG}/${name} (${data.length} bytes)`); - } - console.log(`seeded demo project "${project.slug}" with ${DEMO_FILES.length} files`); -} - -// Allow running standalone: `pnpm db:seed`. -if (import.meta.url === `file://${process.argv[1]}`) { - seedDefaultOwner() - .then(async (id) => { - console.log(`seeded default owner: ${id}`); - await seedDemoProject(); - return pool.end(); - }) - .catch((err) => { - console.error(err); - process.exit(1); - }); -} diff --git a/web/apps/server/src/env.ts b/web/apps/server/src/env.ts deleted file mode 100644 index d795711..0000000 --- a/web/apps/server/src/env.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { config } from "dotenv"; -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; -import { z } from "zod"; - -const here = path.dirname(fileURLToPath(import.meta.url)); -// web/.env lives three levels up from apps/server/src. Standard precedence: -// real environment variables win over .env (don't override). -config({ path: path.resolve(here, "../../../.env") }); - -const envSchema = z.object({ - DATABASE_URL: z.string().min(1), - PORT: z.coerce.number().int().positive().default(3050), - STORAGE_DRIVER: z.string().default("local"), - STORAGE_ROOT: z.string().default("./.data/storage"), - CORS_ORIGIN: z.string().default("http://localhost:3048"), - DEFAULT_OWNER_SLUG: z.string().default("default"), -}); - -export const env = envSchema.parse(process.env); -export type Env = typeof env; diff --git a/web/apps/server/src/lib/paths.ts b/web/apps/server/src/lib/paths.ts deleted file mode 100644 index b8d83e0..0000000 --- a/web/apps/server/src/lib/paths.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as path from "node:path"; - -/** - * Normalize an arbitrary client-supplied relative path into a safe POSIX - * project-relative path. Strips leading slashes and any `..` traversal. - */ -export function sanitizeRelPath(input: string): string { - const posix = input.replace(/\\/g, "/"); - const normalized = path.posix - .normalize(posix) - .replace(/^(\.\.(\/|$))+/, "") - .replace(/^\/+/, ""); - if (!normalized || normalized === "." || normalized.startsWith("..")) { - throw new Error(`invalid file path: ${input}`); - } - return normalized; -} - -export function slugify(name: string): string { - const base = name - .toLowerCase() - .trim() - .replace(/[^a-z0-9._-]+/g, "-") - .replace(/^-+|-+$/g, "") - .replace(/-{2,}/g, "-"); - return base || "project"; -} - -const TEXT_EXT = new Set([ - ".kicad_pcb", - ".kicad_sch", - ".kicad_pro", - ".kicad_sym", - ".kicad_mod", - ".kicad_dru", - ".kicad_wks", - ".net", - ".txt", - ".csv", - ".json", -]); - -export function guessContentType(relPath: string): string { - const ext = path.posix.extname(relPath).toLowerCase(); - if (TEXT_EXT.has(ext)) return "text/plain; charset=utf-8"; - return "application/octet-stream"; -} diff --git a/web/apps/server/src/routes/api.ts b/web/apps/server/src/routes/api.ts deleted file mode 100644 index c890fd5..0000000 --- a/web/apps/server/src/routes/api.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { contract } from "@kicad-web/contract"; -import { initServer } from "@ts-rest/fastify"; -import * as svc from "../services/projects.js"; - -const s = initServer(); - -export const apiRouter = s.router(contract, { - listProjects: async () => ({ - status: 200, - body: await svc.listProjects(), - }), - - createProject: async ({ body }) => { - try { - const project = await svc.createProject(body.name, body.slug); - return { status: 201 as const, body: project }; - } catch (err) { - if (err instanceof svc.SlugConflictError) { - return { status: 409 as const, body: { message: err.message } }; - } - throw err; - } - }, - - getProject: async ({ params }) => { - const result = await svc.getProjectWithFiles(params.project); - if (!result) { - return { status: 404 as const, body: { message: "project not found" } }; - } - return { status: 200 as const, body: result }; - }, - - deleteProject: async ({ params }) => { - const id = await svc.deleteProject(params.project); - if (!id) { - return { status: 404 as const, body: { message: "project not found" } }; - } - return { status: 200 as const, body: { id } }; - }, - - listFiles: async ({ params }) => { - const row = await svc.getProjectRowBySlug(params.project); - if (!row) { - return { status: 404 as const, body: { message: "project not found" } }; - } - return { status: 200 as const, body: await svc.listFilesApi(row.id) }; - }, -}); - -/** Fastify plugin that mounts the ts-rest JSON API. */ -export const apiPlugin = s.plugin(apiRouter); diff --git a/web/apps/server/src/routes/files.ts b/web/apps/server/src/routes/files.ts deleted file mode 100644 index 892eb63..0000000 --- a/web/apps/server/src/routes/files.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { createWriteStream } from "node:fs"; -import * as fs from "node:fs/promises"; -import * as os from "node:os"; -import * as path from "node:path"; -import { pipeline } from "node:stream/promises"; -import type { ProjectFile } from "@kicad-web/contract"; -import type { FastifyInstance } from "fastify"; -import unzipper from "unzipper"; -import { sanitizeRelPath } from "../lib/paths.js"; -import * as svc from "../services/projects.js"; -import { storage } from "../storage.js"; - -/** - * Binary file routes that don't round-trip cleanly through ts-rest: - * POST /api/projects/:project/files (multipart; multi-file + folder) - * POST /api/projects/:project/files/zip (multipart; one zip) - * GET /api/projects/:project/files/* (stream raw bytes) - * - * For multi-file / folder uploads the client sends each file as a part whose - * FIELD NAME is the project-relative path (folder uploads pass - * webkitRelativePath). The zip route unpacks entries preserving their paths. - */ -export async function fileRoutes(app: FastifyInstance): Promise { - // --- multi-file / folder upload --- - app.post("/api/projects/:project/files", async (req, reply) => { - const slug = (req.params as { project: string }).project; - const project = await svc.getProjectRowBySlug(slug); - if (!project) { - return reply.code(404).send({ message: "project not found" }); - } - - const written: ProjectFile[] = []; - for await (const part of req.parts()) { - if (part.type !== "file") continue; - // Field name carries the relative path; fall back to the filename. - const rawPath = part.fieldname || part.filename || ""; - if (!rawPath) { - part.file.resume(); - continue; - } - written.push( - await svc.writeProjectFile({ - project, - rawPath, - data: part.file, - contentType: part.mimetype, - }), - ); - } - return reply.code(201).send({ files: written }); - }); - - // --- zip upload --- - app.post("/api/projects/:project/files/zip", async (req, reply) => { - const slug = (req.params as { project: string }).project; - const project = await svc.getProjectRowBySlug(slug); - if (!project) { - return reply.code(404).send({ message: "project not found" }); - } - - const zipPart = await req.file(); - if (!zipPart) { - return reply.code(400).send({ message: "no zip file in request" }); - } - - const tmp = path.join(os.tmpdir(), `kicad-upload-${randomUUID()}.zip`); - const written: ProjectFile[] = []; - try { - await pipeline(zipPart.file, createWriteStream(tmp)); - const directory = await unzipper.Open.file(tmp); - for (const entry of directory.files) { - if (entry.type !== "File") continue; - let relPath: string; - try { - relPath = sanitizeRelPath(entry.path); - } catch { - continue; // skip traversal / invalid entries - } - written.push( - await svc.writeProjectFile({ - project, - rawPath: relPath, - data: entry.stream(), - size: entry.uncompressedSize, - }), - ); - } - } finally { - await fs.rm(tmp, { force: true }); - } - return reply.code(201).send({ files: written }); - }); - - // --- download raw bytes --- - app.get("/api/projects/:project/files/*", async (req, reply) => { - const params = req.params as { project: string; "*": string }; - const project = await svc.getProjectRowBySlug(params.project); - if (!project) { - return reply.code(404).send({ message: "project not found" }); - } - let relPath: string; - try { - relPath = sanitizeRelPath(params["*"]); - } catch { - return reply.code(400).send({ message: "invalid path" }); - } - const file = await svc.getFileRow(project.id, relPath); - if (!file) { - return reply.code(404).send({ message: "file not found" }); - } - reply.header("Content-Type", file.contentType); - reply.header("Content-Length", file.size); - reply.header("Cache-Control", "no-cache"); - return reply.send(storage.createReadStream(file.storageKey)); - }); -} diff --git a/web/apps/server/src/server.ts b/web/apps/server/src/server.ts deleted file mode 100644 index 72ad8de..0000000 --- a/web/apps/server/src/server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { buildApp } from "./app.js"; -import { env } from "./env.js"; - -async function main() { - const app = await buildApp(); - await app.listen({ port: env.PORT, host: "0.0.0.0" }); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/web/apps/server/src/services/projects.ts b/web/apps/server/src/services/projects.ts deleted file mode 100644 index 48f217f..0000000 --- a/web/apps/server/src/services/projects.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { Project, ProjectFile } from "@kicad-web/contract"; -import type { Readable } from "node:stream"; -import { and, asc, eq } from "drizzle-orm"; -import { db } from "../db/index.js"; -import { - projectFiles, - projects, - type ProjectFileRow, - type ProjectRow, -} from "../db/schema.js"; -import { env } from "../env.js"; -import { owners } from "../db/schema.js"; -import { guessContentType, sanitizeRelPath, slugify } from "../lib/paths.js"; -import { fileStorageKey, projectStoragePrefix, storage } from "../storage.js"; - -export class SlugConflictError extends Error { - constructor(slug: string) { - super(`project slug already exists: ${slug}`); - this.name = "SlugConflictError"; - } -} - -let cachedOwnerId: string | null = null; - -export async function getDefaultOwnerId(): Promise { - if (cachedOwnerId) return cachedOwnerId; - const row = await db - .select() - .from(owners) - .where(eq(owners.slug, env.DEFAULT_OWNER_SLUG)) - .limit(1); - if (!row[0]) { - throw new Error( - `default owner "${env.DEFAULT_OWNER_SLUG}" not found — run db:migrate`, - ); - } - cachedOwnerId = row[0].id; - return cachedOwnerId; -} - -function toApiProject(row: ProjectRow): Project { - return { - id: row.id, - ownerId: row.ownerId, - slug: row.slug, - name: row.name, - createdAt: row.createdAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - }; -} - -function toApiFile(row: ProjectFileRow): ProjectFile { - return { - id: row.id, - projectId: row.projectId, - path: row.path, - size: row.size, - contentType: row.contentType, - createdAt: row.createdAt.toISOString(), - updatedAt: row.updatedAt.toISOString(), - }; -} - -export async function listProjects(): Promise { - const ownerId = await getDefaultOwnerId(); - const rows = await db - .select() - .from(projects) - .where(eq(projects.ownerId, ownerId)) - .orderBy(asc(projects.createdAt)); - return rows.map(toApiProject); -} - -async function slugExists(ownerId: string, slug: string): Promise { - const row = await db - .select({ id: projects.id }) - .from(projects) - .where(and(eq(projects.ownerId, ownerId), eq(projects.slug, slug))) - .limit(1); - return !!row[0]; -} - -export async function createProject( - name: string, - slug?: string, -): Promise { - const ownerId = await getDefaultOwnerId(); - - if (slug) { - if (await slugExists(ownerId, slug)) throw new SlugConflictError(slug); - } else { - const base = slugify(name); - slug = base; - for (let i = 2; await slugExists(ownerId, slug); i++) { - slug = `${base}-${i}`; - } - } - - const inserted = await db - .insert(projects) - .values({ ownerId, slug, name }) - .returning(); - return toApiProject(inserted[0]!); -} - -export async function getProjectRowBySlug( - slug: string, -): Promise { - const ownerId = await getDefaultOwnerId(); - const row = await db - .select() - .from(projects) - .where(and(eq(projects.ownerId, ownerId), eq(projects.slug, slug))) - .limit(1); - return row[0] ?? null; -} - -export async function listFiles(projectId: string): Promise { - return db - .select() - .from(projectFiles) - .where(eq(projectFiles.projectId, projectId)) - .orderBy(asc(projectFiles.path)); -} - -export async function listFilesApi(projectId: string): Promise { - return (await listFiles(projectId)).map(toApiFile); -} - -export async function getProjectWithFiles(slug: string): Promise<{ - project: Project; - files: ProjectFile[]; -} | null> { - const row = await getProjectRowBySlug(slug); - if (!row) return null; - const files = await listFiles(row.id); - return { project: toApiProject(row), files: files.map(toApiFile) }; -} - -export async function getFileRow( - projectId: string, - relPath: string, -): Promise { - const row = await db - .select() - .from(projectFiles) - .where( - and(eq(projectFiles.projectId, projectId), eq(projectFiles.path, relPath)), - ) - .limit(1); - return row[0] ?? null; -} - -export async function deleteProject(slug: string): Promise { - const row = await getProjectRowBySlug(slug); - if (!row) return null; - // Cascade removes project_file rows; storage prefix removed explicitly. - await db.delete(projects).where(eq(projects.id, row.id)); - await storage.deletePrefix(projectStoragePrefix(row.ownerId, row.id)); - return row.id; -} - -/** - * Stream/buffer a single file into storage and upsert its index row. Used by - * the upload routes (multi-file, folder, and zip entries). - */ -export async function writeProjectFile(opts: { - project: ProjectRow; - rawPath: string; - data: Uint8Array | Readable; - size?: number; - contentType?: string; -}): Promise { - const relPath = sanitizeRelPath(opts.rawPath); - const key = fileStorageKey(opts.project.ownerId, opts.project.id, relPath); - await storage.write(key, opts.data, { contentType: opts.contentType }); - - const size = opts.size ?? (await storage.stat(key)).size; - const contentType = opts.contentType ?? guessContentType(relPath); - - const inserted = await db - .insert(projectFiles) - .values({ - projectId: opts.project.id, - path: relPath, - size, - contentType, - storageKey: key, - }) - .onConflictDoUpdate({ - target: [projectFiles.projectId, projectFiles.path], - set: { size, contentType, storageKey: key, updatedAt: new Date() }, - }) - .returning(); - return toApiFile(inserted[0]!); -} diff --git a/web/apps/server/src/storage.ts b/web/apps/server/src/storage.ts deleted file mode 100644 index dc49cf7..0000000 --- a/web/apps/server/src/storage.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createFileStorage } from "@kicad-web/storage"; -import { env } from "./env.js"; - -export const storage = createFileStorage(env); - -export function projectStoragePrefix(ownerId: string, projectId: string): string { - return `owners/${ownerId}/projects/${projectId}`; -} - -export function fileStorageKey( - ownerId: string, - projectId: string, - relPath: string, -): string { - return `${projectStoragePrefix(ownerId, projectId)}/${relPath}`; -} diff --git a/web/apps/server/tsconfig.json b/web/apps/server/tsconfig.json deleted file mode 100644 index 76ac063..0000000 --- a/web/apps/server/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "types": ["node"], - "lib": ["ES2022"] - }, - "include": ["src", "drizzle.config.ts"] -} diff --git a/web/backend/.env.example b/web/backend/.env.example new file mode 100644 index 0000000..277d97d --- /dev/null +++ b/web/backend/.env.example @@ -0,0 +1,10 @@ +# Thin reference backend. Copy to backend/.env. + +# Absolute or relative path to the single KiCad project folder to serve. +PROJECT_DIR=../../tests/fixtures/demo + +# Port to listen on (the standalone's VITE_API_BASE_URL must match this). +PORT=3060 + +# Browser origin allowed to call this backend (the Vite dev server). +CORS_ORIGIN=http://localhost:3048 diff --git a/web/backend/package.json b/web/backend/package.json new file mode 100644 index 0000000..1df1fad --- /dev/null +++ b/web/backend/package.json @@ -0,0 +1,23 @@ +{ + "name": "@pcbjam/backend-example", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/server.ts", + "start": "tsx src/server.ts", + "build": "tsc -p tsconfig.json --noEmit false --declaration false --outDir dist", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@fastify/cors": "^9.0.1", + "@pcbjam/shared": "workspace:*", + "@ts-rest/fastify": "^3.52.1", + "fastify": "^4.29.0" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/web/backend/src/server.ts b/web/backend/src/server.ts new file mode 100644 index 0000000..e5516f9 --- /dev/null +++ b/web/backend/src/server.ts @@ -0,0 +1,175 @@ +// Thin REFERENCE backend for the @pcbjam/shared contract. +// +// It serves a SINGLE KiCad project straight off the local filesystem +// (PROJECT_DIR) with no database, no auth, and no uploads — just enough for the +// standalone editor to enumerate the project, read its file tree, and stream +// file bytes. It exists to (a) let the GPL editor run end-to-end on its own and +// (b) document the minimum a "real" backend must implement. Collaboration is +// browser-tab only (BroadcastChannel) and needs nothing from the server. + +import { createReadStream } from "node:fs"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { randomUUID } from "node:crypto"; +import cors from "@fastify/cors"; +import Fastify from "fastify"; +import { initServer } from "@ts-rest/fastify"; +import { + contract, + type Project, + type ProjectFile, +} from "@pcbjam/shared"; + +const PROJECT_DIR = path.resolve( + process.cwd(), + process.env.PROJECT_DIR ?? "./project", +); +const PORT = Number(process.env.PORT ?? 3060); +const CORS_ORIGIN = process.env.CORS_ORIGIN ?? "http://localhost:3048"; + +const TEXT_EXT = new Set([ + ".kicad_pcb", + ".kicad_sch", + ".kicad_pro", + ".kicad_sym", + ".kicad_mod", + ".kicad_dru", + ".kicad_wks", + ".net", + ".txt", + ".csv", + ".json", +]); + +function guessContentType(relPath: string): string { + const ext = path.posix.extname(relPath).toLowerCase(); + return TEXT_EXT.has(ext) + ? "text/plain; charset=utf-8" + : "application/octet-stream"; +} + +/** Reject paths that escape PROJECT_DIR (traversal guard). */ +function safeJoin(relPath: string): string { + const normalized = path.posix + .normalize(relPath.replace(/\\/g, "/")) + .replace(/^(\.\.(\/|$))+/, "") + .replace(/^\/+/, ""); + const abs = path.resolve(PROJECT_DIR, normalized); + if (abs !== PROJECT_DIR && !abs.startsWith(PROJECT_DIR + path.sep)) { + throw new Error(`path escapes project: ${relPath}`); + } + return abs; +} + +const SLUG = (path.basename(PROJECT_DIR) || "project") + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") || "project"; + +// Stable-per-process ids (no DB; the editor only needs them to be unique). +const PROJECT_ID = randomUUID(); +const fileIds = new Map(); +function fileId(relPath: string): string { + let id = fileIds.get(relPath); + if (!id) { + id = randomUUID(); + fileIds.set(relPath, id); + } + return id; +} + +async function walk(dir: string, prefix = ""): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const out: ProjectFile[] = []; + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; // skip dotfiles + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + const abs = path.join(dir, entry.name); + if (entry.isDirectory()) { + out.push(...(await walk(abs, rel))); + } else if (entry.isFile()) { + const st = await fs.stat(abs); + out.push({ + id: fileId(rel), + projectId: PROJECT_ID, + path: rel, + size: st.size, + contentType: guessContentType(rel), + createdAt: st.birthtime.toISOString(), + updatedAt: st.mtime.toISOString(), + }); + } + } + return out; +} + +async function project(): Promise { + const st = await fs.stat(PROJECT_DIR); + return { + id: PROJECT_ID, + slug: SLUG, + name: path.basename(PROJECT_DIR) || SLUG, + createdAt: st.birthtime.toISOString(), + updatedAt: st.mtime.toISOString(), + }; +} + +async function main(): Promise { + const app = Fastify({ logger: true, bodyLimit: 1024 * 1024 * 1024 }); + await app.register(cors, { + origin: CORS_ORIGIN === "*" ? true : CORS_ORIGIN.split(","), + }); + app.get("/health", async () => ({ ok: true })); + + const s = initServer(); + const router = s.router(contract, { + listProjects: async () => ({ status: 200, body: [await project()] }), + getProject: async ({ params }) => { + if (params.project !== SLUG) { + return { status: 404 as const, body: { message: "project not found" } }; + } + return { + status: 200 as const, + body: { project: await project(), files: await walk(PROJECT_DIR) }, + }; + }, + listFiles: async ({ params }) => { + if (params.project !== SLUG) { + return { status: 404 as const, body: { message: "project not found" } }; + } + return { status: 200 as const, body: await walk(PROJECT_DIR) }; + }, + }); + await app.register(s.plugin(router)); + + // Streamed file-byte download (binary; intentionally not a ts-rest endpoint). + app.get<{ Params: { project: string; "*": string } }>( + "/api/projects/:project/files/*", + async (req, reply) => { + if (req.params.project !== SLUG) { + return reply.code(404).send({ message: "project not found" }); + } + let abs: string; + try { + abs = safeJoin(req.params["*"]); + } catch { + return reply.code(400).send({ message: "invalid path" }); + } + const st = await fs.stat(abs).catch(() => null); + if (!st?.isFile()) { + return reply.code(404).send({ message: "file not found" }); + } + reply.header("Content-Type", guessContentType(req.params["*"])); + reply.header("Content-Length", st.size); + return reply.send(createReadStream(abs)); + }, + ); + + await app.listen({ port: PORT, host: "0.0.0.0" }); + app.log.info(`serving project "${SLUG}" from ${PROJECT_DIR}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/web/backend/tsconfig.json b/web/backend/tsconfig.json new file mode 100644 index 0000000..c69d1d7 --- /dev/null +++ b/web/backend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2022"], + "types": ["node"], + "moduleResolution": "Bundler", + "noEmit": true, + "verbatimModuleSyntax": false + }, + "include": ["src"] +} diff --git a/web/docker-compose.yml b/web/docker-compose.yml deleted file mode 100644 index e375a08..0000000 --- a/web/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Local infrastructure for the KiCad web app. -# -# NOTE: the Postgres host port defaults to a non-default value (54329) so it -# does not collide with a Postgres already listening on the standard 5432. -# Override with POSTGRES_PORT in web/.env if 54329 is taken. The container -# always listens on 5432 internally; only the published host port changes. -services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER:-kicad} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-kicad} - POSTGRES_DB: ${POSTGRES_DB:-kicad_web} - ports: - - "${POSTGRES_PORT:-54329}:5432" - volumes: - - kicad_web_pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-kicad} -d ${POSTGRES_DB:-kicad_web}"] - interval: 5s - timeout: 5s - retries: 10 - -volumes: - kicad_web_pgdata: diff --git a/web/package.json b/web/package.json index fd122dd..9069776 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,5 @@ { - "name": "kicad-web", + "name": "pcbjam-web", "version": "0.0.0", "private": true, "packageManager": "pnpm@10.33.0", @@ -10,12 +10,7 @@ "dev": "turbo run dev", "build": "turbo run build", "typecheck": "turbo run typecheck", - "lint": "turbo run lint", - "db:up": "docker compose up -d", - "db:down": "docker compose down", - "db:generate": "pnpm --filter @kicad-web/server db:generate", - "db:migrate": "pnpm --filter @kicad-web/server db:migrate", - "db:seed": "pnpm --filter @kicad-web/server db:seed" + "lint": "turbo run lint" }, "devDependencies": { "turbo": "^2.5.0", diff --git a/web/packages/contract/package.json b/web/packages/contract/package.json deleted file mode 100644 index 86f6caa..0000000 --- a/web/packages/contract/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@kicad-web/contract", - "version": "0.0.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@ts-rest/core": "^3.52.1", - "zod": "^3.24.1" - }, - "devDependencies": { - "typescript": "^5.7.3" - } -} diff --git a/web/packages/contract/src/index.ts b/web/packages/contract/src/index.ts deleted file mode 100644 index b71945b..0000000 --- a/web/packages/contract/src/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { initContract } from "@ts-rest/core"; -import { z } from "zod"; -import { - createProjectBody, - errorBody, - projectFileSchema, - projectSchema, - projectWithFiles, -} from "./schemas.js"; - -export * from "./schemas.js"; - -const c = initContract(); - -/** - * JSON API surface shared by the Fastify server and the React client. - * - * NOTE: binary upload (`POST .../files`, `.../files/zip`) and file-byte - * download (`GET .../files/*`) are intentionally NOT in this ts-rest contract — - * multipart/streamed-binary do not round-trip cleanly through ts-rest. They are - * plain Fastify routes; their response shapes are still shared via the Zod - * schemas in ./schemas.ts (e.g. `uploadResponse`). - */ -export const contract = c.router( - { - listProjects: { - method: "GET", - path: "/api/projects", - responses: { 200: z.array(projectSchema) }, - summary: "List all projects in the default owner namespace", - }, - createProject: { - method: "POST", - path: "/api/projects", - body: createProjectBody, - responses: { - 201: projectSchema, - 409: errorBody, - 400: errorBody, - }, - summary: "Create a project", - }, - getProject: { - method: "GET", - path: "/api/projects/:project", - pathParams: z.object({ project: z.string() }), - responses: { - 200: projectWithFiles, - 404: errorBody, - }, - summary: "Get a project and its file tree", - }, - deleteProject: { - method: "DELETE", - path: "/api/projects/:project", - body: c.type>(), - responses: { - 200: z.object({ id: z.string().uuid() }), - 404: errorBody, - }, - summary: "Delete a project and all its files", - }, - listFiles: { - method: "GET", - path: "/api/projects/:project/files", - pathParams: z.object({ project: z.string() }), - responses: { - 200: z.array(projectFileSchema), - 404: errorBody, - }, - summary: "List the files in a project", - }, - }, - { - strictStatusCodes: true, - }, -); - -export type Contract = typeof contract; diff --git a/web/packages/contract/src/schemas.ts b/web/packages/contract/src/schemas.ts deleted file mode 100644 index c8ebc79..0000000 --- a/web/packages/contract/src/schemas.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { z } from "zod"; - -/** WASM tools that can be selected by the `:tool` URL segment. */ -export const TOOLS = [ - "pcbnew", - "eeschema", - "calculator", - "pl_editor", - "symbol_editor", - "gerbview", -] as const; -export const toolSchema = z.enum(TOOLS); -export type Tool = z.infer; - -/** Human-readable labels for tools (UI links, status text). */ -export const TOOL_LABELS: Record = { - pcbnew: "PCB Editor", - eeschema: "Schematic Editor", - calculator: "PCB Calculator", - pl_editor: "Drawing Sheet Editor", - symbol_editor: "Symbol Editor", - gerbview: "Gerber Viewer", -}; - -/** Default file-extension → tool mapping (the explicit URL segment always wins). */ -export const EXTENSION_TOOL: Record = { - ".kicad_pcb": "pcbnew", - ".kicad_sch": "eeschema", - ".kicad_wks": "pl_editor", -}; - -/** - * Tools that do not take a file (booted standalone). The calculator has no file - * concept; the symbol editor opens libraries through its own UI (its frame does - * not implement OpenProjectFiles), so we boot it standalone rather than auto-open. - * The gerber viewer likewise opens gerber/drill files through its own File→Open - * UI — projects carry no gerber files to auto-open — so it boots standalone too. - */ -export const FILELESS_TOOLS: ReadonlySet = new Set([ - "calculator", - "symbol_editor", - "gerbview", -]); - -export const projectSlugSchema = z - .string() - .min(1) - .max(64) - .regex( - /^[a-z0-9][a-z0-9._-]*$/, - "slug must start alphanumeric and contain only lowercase letters, digits, '.', '_', '-'", - ); - -export const projectSchema = z.object({ - id: z.string().uuid(), - ownerId: z.string().uuid(), - slug: z.string(), - name: z.string(), - createdAt: z.string(), - updatedAt: z.string(), -}); -export type Project = z.infer; - -export const projectFileSchema = z.object({ - id: z.string().uuid(), - projectId: z.string().uuid(), - /** POSIX project-relative path, e.g. "pcbnew/nyak.kicad_pcb". */ - path: z.string(), - size: z.number().int().nonnegative(), - contentType: z.string(), - createdAt: z.string(), - updatedAt: z.string(), -}); -export type ProjectFile = z.infer; - -export const createProjectBody = z.object({ - name: z.string().min(1).max(200), - slug: projectSlugSchema.optional(), -}); -export type CreateProjectBody = z.infer; - -export const projectWithFiles = z.object({ - project: projectSchema, - files: z.array(projectFileSchema), -}); -export type ProjectWithFiles = z.infer; - -/** Shared response shape for the (raw-Fastify) upload endpoints. */ -export const uploadResponse = z.object({ - files: z.array(projectFileSchema), -}); -export type UploadResponse = z.infer; - -export const errorBody = z.object({ message: z.string() }); -export type ErrorBody = z.infer; diff --git a/web/packages/contract/tsconfig.json b/web/packages/contract/tsconfig.json deleted file mode 100644 index 564a599..0000000 --- a/web/packages/contract/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src"] -} diff --git a/web/packages/storage/package.json b/web/packages/storage/package.json deleted file mode 100644 index d485b23..0000000 --- a/web/packages/storage/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@kicad-web/storage", - "version": "0.0.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@types/node": "^22.10.5", - "typescript": "^5.7.3" - } -} diff --git a/web/packages/storage/src/index.ts b/web/packages/storage/src/index.ts deleted file mode 100644 index 9f4aaf3..0000000 --- a/web/packages/storage/src/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type { FileStorage, StatResult } from "./types.js"; -export { LocalDiskStorage } from "./local-disk.js"; - -import { LocalDiskStorage } from "./local-disk.js"; -import type { FileStorage } from "./types.js"; - -/** - * Build the storage backend from environment. Today only `local` is wired; an - * `s3` driver slots in here later behind the same FileStorage interface. - */ -export function createFileStorage(env: { - STORAGE_DRIVER?: string; - STORAGE_ROOT?: string; -}): FileStorage { - const driver = env.STORAGE_DRIVER ?? "local"; - switch (driver) { - case "local": - return new LocalDiskStorage(env.STORAGE_ROOT ?? "./.data/storage"); - default: - throw new Error(`unknown STORAGE_DRIVER: ${driver}`); - } -} diff --git a/web/packages/storage/src/local-disk.ts b/web/packages/storage/src/local-disk.ts deleted file mode 100644 index fe7e537..0000000 --- a/web/packages/storage/src/local-disk.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { createReadStream as fsCreateReadStream } from "node:fs"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import type { Readable } from "node:stream"; -import { pipeline } from "node:stream/promises"; -import { createWriteStream } from "node:fs"; -import type { FileStorage, StatResult } from "./types.js"; - -/** - * Stores blobs as files under a single root directory. `key` maps directly to a - * relative path inside the root; traversal outside the root is rejected. - */ -export class LocalDiskStorage implements FileStorage { - private readonly root: string; - - constructor(root: string) { - this.root = path.resolve(root); - } - - private resolve(key: string): string { - const normalized = path - .normalize(key) - .replace(/^(\.\.(\/|\\|$))+/, "") - .replace(/^[/\\]+/, ""); - const full = path.resolve(this.root, normalized); - if (full !== this.root && !full.startsWith(this.root + path.sep)) { - throw new Error(`storage key escapes root: ${key}`); - } - return full; - } - - async exists(key: string): Promise { - try { - await fs.access(this.resolve(key)); - return true; - } catch { - return false; - } - } - - async read(key: string): Promise { - return new Uint8Array(await fs.readFile(this.resolve(key))); - } - - createReadStream(key: string): Readable { - return fsCreateReadStream(this.resolve(key)); - } - - async stat(key: string): Promise { - const s = await fs.stat(this.resolve(key)); - return { size: s.size }; - } - - async list(prefix: string): Promise { - const base = this.resolve(prefix); - const out: string[] = []; - const walk = async (dir: string): Promise => { - let entries: import("node:fs").Dirent[]; - try { - entries = await fs.readdir(dir, { withFileTypes: true }); - } catch { - return; - } - for (const entry of entries) { - const abs = path.join(dir, entry.name); - if (entry.isDirectory()) { - await walk(abs); - } else if (entry.isFile()) { - out.push(path.relative(this.root, abs).split(path.sep).join("/")); - } - } - }; - await walk(base); - return out; - } - - async write( - key: string, - data: Uint8Array | Readable, - _opts?: { contentType?: string }, - ): Promise { - const full = this.resolve(key); - await fs.mkdir(path.dirname(full), { recursive: true }); - if (data instanceof Uint8Array) { - await fs.writeFile(full, data); - } else { - await pipeline(data, createWriteStream(full)); - } - } - - async delete(key: string): Promise { - await fs.rm(this.resolve(key), { force: true }); - } - - async deletePrefix(prefix: string): Promise { - await fs.rm(this.resolve(prefix), { recursive: true, force: true }); - } -} diff --git a/web/packages/storage/src/types.ts b/web/packages/storage/src/types.ts deleted file mode 100644 index c1c9b75..0000000 --- a/web/packages/storage/src/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Readable } from "node:stream"; - -export interface StatResult { - size: number; - contentType?: string; -} - -/** - * Pluggable blob store for project file bytes. - * - * `key` is an opaque string the caller chose (see the server's storage-key - * scheme); only the implementation interprets it. The current iteration is - * read-heavy — the write half exists so save-back (a later iteration) needs no - * redesign. - */ -export interface FileStorage { - // --- read --- - exists(key: string): Promise; - read(key: string): Promise; - createReadStream(key: string): Readable; - stat(key: string): Promise; - list(prefix: string): Promise; - - // --- write (used now by upload; save-back is a later iteration) --- - write( - key: string, - data: Uint8Array | Readable, - opts?: { contentType?: string }, - ): Promise; - delete(key: string): Promise; - /** Remove every key under a prefix (e.g. a whole project). */ - deletePrefix(prefix: string): Promise; -} diff --git a/web/packages/storage/tsconfig.json b/web/packages/storage/tsconfig.json deleted file mode 100644 index 374bac5..0000000 --- a/web/packages/storage/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "types": ["node"] - }, - "include": ["src"] -} diff --git a/web/pcbjam-shared b/web/pcbjam-shared new file mode 160000 index 0000000..b4993a4 --- /dev/null +++ b/web/pcbjam-shared @@ -0,0 +1 @@ +Subproject commit b4993a47e897afc6e3a986ae1d598cc2011b3c13 diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 44f0d33..19ba2c1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -15,11 +15,49 @@ importers: specifier: ^5.7.3 version: 5.9.3 - apps/frontend: + backend: dependencies: - '@kicad-web/contract': + '@fastify/cors': + specifier: ^9.0.1 + version: 9.0.1 + '@pcbjam/shared': specifier: workspace:* - version: link:../../packages/contract + version: link:../pcbjam-shared + '@ts-rest/fastify': + specifier: ^3.52.1 + version: 3.52.1(@ts-rest/core@3.52.1(@types/node@22.19.19)(zod@3.25.76))(fastify@4.29.1)(zod@3.25.76) + fastify: + specifier: ^4.29.0 + version: 4.29.1 + devDependencies: + '@types/node': + specifier: ^22.10.5 + version: 22.19.19 + tsx: + specifier: ^4.19.2 + version: 4.22.4 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + pcbjam-shared: + dependencies: + '@ts-rest/core': + specifier: ^3.52.1 + version: 3.52.1(@types/node@22.19.19)(zod@3.25.76) + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + standalone: + dependencies: + '@pcbjam/shared': + specifier: workspace:* + version: link:../pcbjam-shared '@radix-ui/react-dialog': specifier: ^1.1.4 version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.29))(@types/react@18.3.29)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -91,83 +129,6 @@ importers: specifier: ^6.0.7 version: 6.4.2(@types/node@22.19.19)(jiti@1.21.7)(tsx@4.22.4) - apps/server: - dependencies: - '@fastify/cors': - specifier: ^9.0.1 - version: 9.0.1 - '@fastify/multipart': - specifier: ^8.3.0 - version: 8.3.1 - '@kicad-web/contract': - specifier: workspace:* - version: link:../../packages/contract - '@kicad-web/storage': - specifier: workspace:* - version: link:../../packages/storage - '@ts-rest/fastify': - specifier: ^3.52.1 - version: 3.52.1(@ts-rest/core@3.52.1(@types/node@22.19.19)(zod@3.25.76))(fastify@4.29.1)(zod@3.25.76) - dotenv: - specifier: ^16.4.7 - version: 16.6.1 - drizzle-orm: - specifier: ^0.38.3 - version: 0.38.4(@types/pg@8.20.0)(@types/react@18.3.29)(pg@8.21.0)(react@18.3.1) - fastify: - specifier: ^4.29.0 - version: 4.29.1 - pg: - specifier: ^8.13.1 - version: 8.21.0 - unzipper: - specifier: ^0.12.3 - version: 0.12.3 - zod: - specifier: ^3.24.1 - version: 3.25.76 - devDependencies: - '@types/node': - specifier: ^22.10.5 - version: 22.19.19 - '@types/pg': - specifier: ^8.11.10 - version: 8.20.0 - '@types/unzipper': - specifier: ^0.10.10 - version: 0.10.11 - drizzle-kit: - specifier: ^0.30.1 - version: 0.30.6 - tsx: - specifier: ^4.19.2 - version: 4.22.4 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/contract: - dependencies: - '@ts-rest/core': - specifier: ^3.52.1 - version: 3.52.1(@types/node@22.19.19)(zod@3.25.76) - zod: - specifier: ^3.24.1 - version: 3.25.76 - devDependencies: - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - packages/storage: - devDependencies: - '@types/node': - specifier: ^22.10.5 - version: 22.19.19 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - packages: '@alloc/quick-lru@5.2.0': @@ -257,23 +218,6 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@drizzle-team/brocli@0.10.2': - resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild/aix-ppc64@0.19.12': - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -286,18 +230,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.19.12': - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -310,18 +242,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.19.12': - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -334,18 +254,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.19.12': - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -358,18 +266,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.19.12': - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -382,18 +278,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.19.12': - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -406,18 +290,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.19.12': - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -430,18 +302,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.19.12': - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -454,18 +314,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.19.12': - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -478,18 +326,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.19.12': - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -502,18 +338,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.19.12': - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -526,18 +350,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.19.12': - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -550,18 +362,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.19.12': - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -574,18 +374,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.19.12': - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -598,18 +386,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.19.12': - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -622,18 +398,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.19.12': - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -646,18 +410,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.19.12': - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -682,18 +434,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.19.12': - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -718,18 +458,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.19.12': - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -754,18 +482,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.19.12': - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -778,18 +494,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.19.12': - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -802,18 +506,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.19.12': - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -826,18 +518,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.19.12': - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -853,30 +533,18 @@ packages: '@fastify/ajv-compiler@3.6.0': resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} - '@fastify/busboy@3.2.0': - resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} - '@fastify/cors@9.0.1': resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} - '@fastify/deepmerge@2.0.2': - resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} - '@fastify/error@3.4.1': resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} - '@fastify/error@4.2.0': - resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} - '@fastify/fast-json-stringify-compiler@4.3.0': resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} '@fastify/merge-json-schemas@0.1.1': resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} - '@fastify/multipart@8.3.1': - resolution: {integrity: sha512-pncbnG28S6MIskFSVRtzTKE9dK+GrKAJl0NbaQ/CG8ded80okWFsYKzSlP9haaLNQhNRDOoHqmGQNvgbiPVpWQ==} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -905,9 +573,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@petamoriken/float16@3.9.3': - resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} - '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1339,9 +1004,6 @@ packages: '@types/node@22.19.19': resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} - '@types/pg@8.20.0': - resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -1353,9 +1015,6 @@ packages: '@types/react@18.3.29': resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==} - '@types/unzipper@0.10.11': - resolution: {integrity: sha512-D25im2zjyMCcgL9ag6N46+wbtJBnXIr7SI4zHf9eJD2Dw2tEB5e+p5MYkrxKIVRscs5QV0EhtU9rgXSPx90oJg==} - '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1421,9 +1080,6 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1433,9 +1089,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -1465,9 +1118,6 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1494,135 +1144,13 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - drizzle-kit@0.30.6: - resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==} - hasBin: true - - drizzle-orm@0.38.4: - resolution: {integrity: sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q==} - peerDependencies: - '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=4' - '@electric-sql/pglite': '>=0.2.0' - '@libsql/client': '>=0.10.0' - '@libsql/client-wasm': '>=0.10.0' - '@neondatabase/serverless': '>=0.10.0' - '@op-engineering/op-sqlite': '>=2' - '@opentelemetry/api': ^1.4.1 - '@planetscale/database': '>=1' - '@prisma/client': '*' - '@tidbcloud/serverless': '*' - '@types/better-sqlite3': '*' - '@types/pg': '*' - '@types/react': '>=18' - '@types/sql.js': '*' - '@vercel/postgres': '>=0.8.0' - '@xata.io/client': '*' - better-sqlite3: '>=7' - bun-types: '*' - expo-sqlite: '>=14.0.0' - knex: '*' - kysely: '*' - mysql2: '>=2' - pg: '>=8' - postgres: '>=3' - prisma: '*' - react: '>=18' - sql.js: '>=1' - sqlite3: '>=5' - peerDependenciesMeta: - '@aws-sdk/client-rds-data': - optional: true - '@cloudflare/workers-types': - optional: true - '@electric-sql/pglite': - optional: true - '@libsql/client': - optional: true - '@libsql/client-wasm': - optional: true - '@neondatabase/serverless': - optional: true - '@op-engineering/op-sqlite': - optional: true - '@opentelemetry/api': - optional: true - '@planetscale/database': - optional: true - '@prisma/client': - optional: true - '@tidbcloud/serverless': - optional: true - '@types/better-sqlite3': - optional: true - '@types/pg': - optional: true - '@types/react': - optional: true - '@types/sql.js': - optional: true - '@vercel/postgres': - optional: true - '@xata.io/client': - optional: true - better-sqlite3: - optional: true - bun-types: - optional: true - expo-sqlite: - optional: true - knex: - optional: true - kysely: - optional: true - mysql2: - optional: true - pg: - optional: true - postgres: - optional: true - prisma: - optional: true - react: - optional: true - sql.js: - optional: true - sqlite3: - optional: true - - duplexer2@0.1.4: - resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} - electron-to-chromium@1.5.364: resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} - env-paths@3.0.0: - resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -1695,10 +1223,6 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - fs-extra@11.3.5: - resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} - engines: {node: '>=14.14'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1707,11 +1231,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gel@2.2.0: - resolution: {integrity: sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==} - engines: {node: '>= 18.0.0'} - hasBin: true - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1720,9 +1239,6 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} - get-tsconfig@4.14.0: - resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1731,16 +1247,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1765,13 +1275,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isexe@3.1.5: - resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} - engines: {node: '>=18'} - isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} @@ -1798,9 +1301,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@6.2.1: - resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - lib0@0.2.117: resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} engines: {node: '>=16'} @@ -1850,9 +1350,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.46: resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} engines: {node: '>=18'} @@ -1879,40 +1376,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - pg-cloudflare@1.4.0: - resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} - - pg-connection-string@2.13.0: - resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} - - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-pool@3.14.0: - resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} - peerDependencies: - pg: '>=8.0' - - pg-protocol@1.14.0: - resolution: {integrity: sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - pg@8.21.0: - resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} - engines: {node: '>= 16.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - - pgpass@1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1989,25 +1452,6 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.1: - resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} @@ -2083,9 +1527,6 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2098,9 +1539,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -2125,9 +1563,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-regex2@3.1.0: resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} @@ -2153,10 +1588,6 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - shell-quote@1.8.4: - resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} - engines: {node: '>= 0.4'} - sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -2164,24 +1595,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - stream-wormhole@1.1.0: - resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} - engines: {node: '>=4.0.0'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -2249,13 +1666,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - unzipper@0.12.3: - resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -2325,15 +1735,6 @@ packages: yaml: optional: true - which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2460,213 +1861,102 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@drizzle-team/brocli@0.10.2': {} - - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.14.0 - - '@esbuild/aix-ppc64@0.19.12': - optional: true - '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/aix-ppc64@0.28.0': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - - '@esbuild/android-arm64@0.19.12': - optional: true - '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.28.0': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - - '@esbuild/android-arm@0.19.12': - optional: true - '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.28.0': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - - '@esbuild/android-x64@0.19.12': - optional: true - '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.28.0': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - - '@esbuild/darwin-arm64@0.19.12': - optional: true - '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.28.0': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - - '@esbuild/darwin-x64@0.19.12': - optional: true - '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.28.0': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - - '@esbuild/freebsd-arm64@0.19.12': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.28.0': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - - '@esbuild/freebsd-x64@0.19.12': - optional: true - '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.28.0': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - - '@esbuild/linux-arm64@0.19.12': - optional: true - '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.28.0': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - - '@esbuild/linux-arm@0.19.12': - optional: true - '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.28.0': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - - '@esbuild/linux-ia32@0.19.12': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.28.0': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - - '@esbuild/linux-loong64@0.19.12': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.28.0': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - - '@esbuild/linux-mips64el@0.19.12': - optional: true - '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.28.0': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - - '@esbuild/linux-ppc64@0.19.12': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.28.0': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - - '@esbuild/linux-riscv64@0.19.12': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.28.0': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - - '@esbuild/linux-s390x@0.19.12': - optional: true - '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.28.0': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - - '@esbuild/linux-x64@0.19.12': - optional: true - '@esbuild/linux-x64@0.25.12': optional: true @@ -2679,12 +1969,6 @@ snapshots: '@esbuild/netbsd-arm64@0.28.0': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - - '@esbuild/netbsd-x64@0.19.12': - optional: true - '@esbuild/netbsd-x64@0.25.12': optional: true @@ -2697,12 +1981,6 @@ snapshots: '@esbuild/openbsd-arm64@0.28.0': optional: true - '@esbuild/openbsd-x64@0.18.20': - optional: true - - '@esbuild/openbsd-x64@0.19.12': - optional: true - '@esbuild/openbsd-x64@0.25.12': optional: true @@ -2715,48 +1993,24 @@ snapshots: '@esbuild/openharmony-arm64@0.28.0': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - - '@esbuild/sunos-x64@0.19.12': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/sunos-x64@0.28.0': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - - '@esbuild/win32-arm64@0.19.12': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-arm64@0.28.0': optional: true - '@esbuild/win32-ia32@0.18.20': - optional: true - - '@esbuild/win32-ia32@0.19.12': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-ia32@0.28.0': optional: true - '@esbuild/win32-x64@0.18.20': - optional: true - - '@esbuild/win32-x64@0.19.12': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true @@ -2769,19 +2023,13 @@ snapshots: ajv-formats: 2.1.1(ajv@8.20.0) fast-uri: 2.4.0 - '@fastify/busboy@3.2.0': {} - '@fastify/cors@9.0.1': dependencies: fastify-plugin: 4.5.1 mnemonist: 0.39.6 - '@fastify/deepmerge@2.0.2': {} - '@fastify/error@3.4.1': {} - '@fastify/error@4.2.0': {} - '@fastify/fast-json-stringify-compiler@4.3.0': dependencies: fast-json-stringify: 5.16.1 @@ -2790,15 +2038,6 @@ snapshots: dependencies: fast-deep-equal: 3.1.3 - '@fastify/multipart@8.3.1': - dependencies: - '@fastify/busboy': 3.2.0 - '@fastify/deepmerge': 2.0.2 - '@fastify/error': 4.2.0 - fastify-plugin: 4.5.1 - secure-json-parse: 2.7.0 - stream-wormhole: 1.1.0 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2830,8 +2069,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@petamoriken/float16@3.9.3': {} - '@pinojs/redact@0.4.0': {} '@radix-ui/primitive@1.1.3': {} @@ -3145,12 +2382,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/pg@8.20.0': - dependencies: - '@types/node': 22.19.19 - pg-protocol: 1.14.0 - pg-types: 2.2.0 - '@types/prop-types@15.7.15': {} '@types/react-dom@18.3.7(@types/react@18.3.29)': @@ -3162,10 +2393,6 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 - '@types/unzipper@0.10.11': - dependencies: - '@types/node': 22.19.19 - '@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@22.19.19)(jiti@1.21.7)(tsx@4.22.4))': dependencies: '@babel/core': 7.29.7 @@ -3228,8 +2455,6 @@ snapshots: binary-extensions@2.3.0: {} - bluebird@3.7.2: {} - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3242,8 +2467,6 @@ snapshots: node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) - buffer-from@1.1.2: {} - camelcase-css@2.0.1: {} caniuse-lite@1.0.30001793: {} @@ -3272,8 +2495,6 @@ snapshots: cookie@0.7.2: {} - core-util-is@1.0.3: {} - cssesc@3.0.0: {} csstype@3.2.3: {} @@ -3288,93 +2509,10 @@ snapshots: dlv@1.1.3: {} - dotenv@16.6.1: {} - - drizzle-kit@0.30.6: - dependencies: - '@drizzle-team/brocli': 0.10.2 - '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.19.12 - esbuild-register: 3.6.0(esbuild@0.19.12) - gel: 2.2.0 - transitivePeerDependencies: - - supports-color - - drizzle-orm@0.38.4(@types/pg@8.20.0)(@types/react@18.3.29)(pg@8.21.0)(react@18.3.1): - optionalDependencies: - '@types/pg': 8.20.0 - '@types/react': 18.3.29 - pg: 8.21.0 - react: 18.3.1 - - duplexer2@0.1.4: - dependencies: - readable-stream: 2.3.8 - electron-to-chromium@1.5.364: {} - env-paths@3.0.0: {} - es-errors@1.3.0: {} - esbuild-register@3.6.0(esbuild@0.19.12): - dependencies: - debug: 4.4.3 - esbuild: 0.19.12 - transitivePeerDependencies: - - supports-color - - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - - esbuild@0.19.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -3510,36 +2648,15 @@ snapshots: fraction.js@5.3.4: {} - fs-extra@11.3.5: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - gel@2.2.0: - dependencies: - '@petamoriken/float16': 3.9.3 - debug: 4.4.3 - env-paths: 3.0.0 - semver: 7.8.1 - shell-quote: 1.8.4 - which: 4.0.0 - transitivePeerDependencies: - - supports-color - gensync@1.0.0-beta.2: {} get-nonce@1.0.1: {} - get-tsconfig@4.14.0: - dependencies: - resolve-pkg-maps: 1.0.0 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3548,14 +2665,10 @@ snapshots: dependencies: is-glob: 4.0.3 - graceful-fs@4.2.11: {} - hasown@2.0.4: dependencies: function-bind: 1.1.2 - inherits@2.0.4: {} - ipaddr.js@1.9.1: {} is-binary-path@2.1.0: @@ -3574,10 +2687,6 @@ snapshots: is-number@7.0.0: {} - isarray@1.0.0: {} - - isexe@3.1.5: {} - isomorphic.js@0.2.5: {} jiti@1.21.7: {} @@ -3594,12 +2703,6 @@ snapshots: json5@2.2.3: {} - jsonfile@6.2.1: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - lib0@0.2.117: dependencies: isomorphic.js: 0.2.5 @@ -3647,8 +2750,6 @@ snapshots: nanoid@3.3.12: {} - node-int64@0.4.0: {} - node-releases@2.0.46: {} normalize-path@3.0.0: {} @@ -3663,41 +2764,6 @@ snapshots: path-parse@1.0.7: {} - pg-cloudflare@1.4.0: - optional: true - - pg-connection-string@2.13.0: {} - - pg-int8@1.0.1: {} - - pg-pool@3.14.0(pg@8.21.0): - dependencies: - pg: 8.21.0 - - pg-protocol@1.14.0: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.1 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - pg@8.21.0: - dependencies: - pg-connection-string: 2.13.0 - pg-pool: 3.14.0(pg@8.21.0) - pg-protocol: 1.14.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.4.0 - - pgpass@1.0.5: - dependencies: - split2: 4.2.0 - picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -3766,18 +2832,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgres-array@2.0.0: {} - - postgres-bytea@1.0.1: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - - process-nextick-args@2.0.1: {} - process-warning@3.0.0: {} process-warning@5.0.0: {} @@ -3846,16 +2900,6 @@ snapshots: dependencies: pify: 2.3.0 - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readdirp@3.6.0: dependencies: picomatch: 2.3.2 @@ -3864,8 +2908,6 @@ snapshots: require-from-string@2.0.2: {} - resolve-pkg-maps@1.0.0: {} - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -3914,8 +2956,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.1.2: {} - safe-regex2@3.1.0: dependencies: ret: 0.4.3 @@ -3934,29 +2974,14 @@ snapshots: set-cookie-parser@2.7.2: {} - shell-quote@1.8.4: {} - sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - split2@4.2.0: {} - stream-wormhole@1.1.0: {} - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -4049,16 +3074,6 @@ snapshots: undici-types@6.21.0: {} - universalify@2.0.1: {} - - unzipper@0.12.3: - dependencies: - bluebird: 3.7.2 - duplexer2: 0.1.4 - fs-extra: 11.3.5 - graceful-fs: 4.2.11 - node-int64: 0.4.0 - update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -4096,12 +3111,6 @@ snapshots: jiti: 1.21.7 tsx: 4.22.4 - which@4.0.0: - dependencies: - isexe: 3.1.5 - - xtend@4.0.2: {} - yallist@3.1.1: {} yjs@13.6.31: diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml index 3ff5faa..ec771fe 100644 --- a/web/pnpm-workspace.yaml +++ b/web/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - - "apps/*" - - "packages/*" + - "standalone" + - "backend" + - "pcbjam-shared" diff --git a/web/standalone/.env.example b/web/standalone/.env.example new file mode 100644 index 0000000..8ae50c4 --- /dev/null +++ b/web/standalone/.env.example @@ -0,0 +1,16 @@ +# Standalone editor (Vite). Copy to standalone/.env. All vars must be VITE_-prefixed. + +# Backend implementing the @pcbjam/shared contract (the example backend, or any +# conforming backend). Leave it pointing anywhere unreachable to use only the +# local-folder loader on the home page. +VITE_API_BASE_URL=http://localhost:3060 + +# Where the WASM glue/artifacts are served from. SAME-ORIGIN "/wasm" is required +# (KiCad WASM pthread workers cannot be created cross-origin). On `dev` the +# artifacts are symlinked into public/wasm and served by Vite at /wasm. For prod, +# set an absolute URL whose origin also satisfies the COEP/COOP rules. +VITE_WASM_ASSET_BASE_URL=/wasm + +# Override the artifact source dir the dev symlink points at (default: +# /tests/apps/kicad). Useful when serving prebuilt artifacts from elsewhere. +# WASM_SRC_DIR= diff --git a/web/apps/frontend/index.html b/web/standalone/index.html similarity index 100% rename from web/apps/frontend/index.html rename to web/standalone/index.html diff --git a/web/apps/frontend/package.json b/web/standalone/package.json similarity index 93% rename from web/apps/frontend/package.json rename to web/standalone/package.json index d1655ec..d149cb8 100644 --- a/web/apps/frontend/package.json +++ b/web/standalone/package.json @@ -1,5 +1,5 @@ { - "name": "@kicad-web/frontend", + "name": "@pcbjam/standalone", "version": "0.0.0", "private": true, "type": "module", @@ -11,7 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@kicad-web/contract": "workspace:*", + "@pcbjam/shared": "workspace:*", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-slot": "^1.1.1", diff --git a/web/apps/frontend/postcss.config.js b/web/standalone/postcss.config.js similarity index 100% rename from web/apps/frontend/postcss.config.js rename to web/standalone/postcss.config.js diff --git a/web/apps/frontend/scripts/link-wasm.mjs b/web/standalone/scripts/link-wasm.mjs similarity index 95% rename from web/apps/frontend/scripts/link-wasm.mjs rename to web/standalone/scripts/link-wasm.mjs index 6724a06..52b3367 100644 --- a/web/apps/frontend/scripts/link-wasm.mjs +++ b/web/standalone/scripts/link-wasm.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // Make the WASM runtime artifacts available SAME-ORIGIN to the app without a -// third copy: symlink apps/frontend/public/wasm -> the synced artifact dir +// third copy: symlink standalone/public/wasm -> the synced artifact dir // (tests/apps/kicad, populated by tests/scripts/setup-kicad-wasm.sh). Vite then // serves them at /wasm from the app's own origin — required because KiCad WASM // (Asyncify build, COEP/cross-origin-isolated document) refuses to load its @@ -17,7 +17,8 @@ import { fileURLToPath } from "node:url"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const publicDir = path.resolve(scriptDir, "../public"); const linkPath = path.join(publicDir, "wasm"); -const repoRoot = path.resolve(scriptDir, "../../../.."); +// scriptDir = web/standalone/scripts → repo root is three levels up. +const repoRoot = path.resolve(scriptDir, "../../.."); const setupScript = path.join(repoRoot, "tests/scripts/setup-kicad-wasm.sh"); const targetAbs = process.env.WASM_SRC_DIR diff --git a/web/apps/frontend/src/App.tsx b/web/standalone/src/App.tsx similarity index 50% rename from web/apps/frontend/src/App.tsx rename to web/standalone/src/App.tsx index 8e6a5e0..ab49022 100644 --- a/web/apps/frontend/src/App.tsx +++ b/web/standalone/src/App.tsx @@ -1,13 +1,13 @@ import { Route, Routes } from "react-router-dom"; -import { ProjectsPage } from "@/pages/ProjectsPage"; -import { ProjectDetailPage } from "@/pages/ProjectDetailPage"; +import { HomePage } from "@/pages/HomePage"; +import { ProjectView } from "@/pages/ProjectView"; import { ToolPage } from "@/pages/ToolPage"; export default function App() { return ( - } /> - } /> + } /> + } /> } /> ); diff --git a/web/apps/frontend/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx similarity index 91% rename from web/apps/frontend/src/components/WasmTool.tsx rename to web/standalone/src/components/WasmTool.tsx index 2b7affe..44a3347 100644 --- a/web/apps/frontend/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -1,10 +1,9 @@ import * as React from "react"; -import type { ProjectFile, Tool } from "@kicad-web/contract"; +import type { Tool } from "@pcbjam/shared"; import { ChevronDown, ChevronUp } from "lucide-react"; -import { fetchFileBytes } from "@/lib/api"; import { WASM_ASSET_BASE_URL } from "@/lib/config"; import { bootKicadTool } from "@/wasm/boot"; -import { driveProjectIntoTool } from "@/wasm/kicad-runner"; +import { driveProjectIntoTool, type ToolFile } from "@/wasm/kicad-runner"; import type { CollabWindow } from "@/wasm/collab"; import { clog, cwarn } from "@/wasm/collab/debug"; @@ -79,11 +78,17 @@ export function WasmTool({ slug, files, targetPath, + fetchBytes, + assetBaseUrl, }: { tool: Tool; slug: string; - files: ProjectFile[]; + files: ToolFile[]; targetPath?: string; + /** Fetch one project-relative file's bytes (contract loader or local folder). */ + fetchBytes: (relPath: string) => Promise; + /** Where the WASM glue/artifacts are served from; defaults to VITE_WASM_ASSET_BASE_URL. */ + assetBaseUrl?: string; }) { const containerRef = React.useRef(null); const startedRef = React.useRef(false); @@ -91,7 +96,7 @@ export function WasmTool({ const [logs, setLogs] = React.useState([]); const [showLog, setShowLog] = React.useState(false); - const base = WASM_ASSET_BASE_URL.replace(/\/$/, ""); + const base = (assetBaseUrl ?? WASM_ASSET_BASE_URL).replace(/\/$/, ""); React.useEffect(() => { // Guard re-entry: the WASM runtime is process-global and must boot exactly @@ -117,7 +122,7 @@ export function WasmTool({ slug, files, targetPath, - fetchBytes: (relPath) => fetchFileBytes(slug, relPath), + fetchBytes, log: append, onStatus: setStatus, }); diff --git a/web/apps/frontend/src/components/ui/button.tsx b/web/standalone/src/components/ui/button.tsx similarity index 100% rename from web/apps/frontend/src/components/ui/button.tsx rename to web/standalone/src/components/ui/button.tsx diff --git a/web/apps/frontend/src/components/ui/card.tsx b/web/standalone/src/components/ui/card.tsx similarity index 100% rename from web/apps/frontend/src/components/ui/card.tsx rename to web/standalone/src/components/ui/card.tsx diff --git a/web/apps/frontend/src/components/ui/dialog.tsx b/web/standalone/src/components/ui/dialog.tsx similarity index 100% rename from web/apps/frontend/src/components/ui/dialog.tsx rename to web/standalone/src/components/ui/dialog.tsx diff --git a/web/apps/frontend/src/components/ui/input.tsx b/web/standalone/src/components/ui/input.tsx similarity index 100% rename from web/apps/frontend/src/components/ui/input.tsx rename to web/standalone/src/components/ui/input.tsx diff --git a/web/apps/frontend/src/components/ui/label.tsx b/web/standalone/src/components/ui/label.tsx similarity index 100% rename from web/apps/frontend/src/components/ui/label.tsx rename to web/standalone/src/components/ui/label.tsx diff --git a/web/apps/frontend/src/index.css b/web/standalone/src/index.css similarity index 100% rename from web/apps/frontend/src/index.css rename to web/standalone/src/index.css diff --git a/web/standalone/src/lib/api.ts b/web/standalone/src/lib/api.ts new file mode 100644 index 0000000..0cabab0 --- /dev/null +++ b/web/standalone/src/lib/api.ts @@ -0,0 +1,61 @@ +import { + contract, + type Project, + type ProjectWithFiles, +} from "@pcbjam/shared"; +import { initClient } from "@ts-rest/core"; +import { useQuery } from "@tanstack/react-query"; +import { API_BASE_URL } from "./config"; + +/** + * Read-only client over the shared contract. The standalone editor only ever + * READS projects from a backend (enumerate, get file tree, stream bytes) — it + * never creates/deletes/uploads. Those management concerns live in the closed + * application that hosts this editor. + */ +export const client = initClient(contract, { + baseUrl: API_BASE_URL, + baseHeaders: {}, +}); + +export function useProjects() { + return useQuery({ + queryKey: ["projects"], + queryFn: async (): Promise => { + const res = await client.listProjects(); + if (res.status !== 200) throw new Error("failed to list projects"); + return res.body; + }, + }); +} + +export function useProject(slug: string) { + return useQuery({ + queryKey: ["project", slug], + queryFn: async (): Promise => { + const res = await client.getProject({ params: { project: slug } }); + if (res.status === 404) throw new Error("project not found"); + if (res.status !== 200) throw new Error("failed to load project"); + return res.body; + }, + }); +} + +// --- raw file-byte download (streamed binary, not a ts-rest endpoint) --- + +export function fileBytesUrl(slug: string, relPath: string): string { + const encoded = relPath + .split("/") + .map((seg) => encodeURIComponent(seg)) + .join("/"); + return `${API_BASE_URL}/api/projects/${encodeURIComponent(slug)}/files/${encoded}`; +} + +export async function fetchFileBytes( + slug: string, + relPath: string, +): Promise { + const res = await fetch(fileBytesUrl(slug, relPath)); + if (!res.ok) throw new Error(`download failed (${res.status}): ${relPath}`); + return new Uint8Array(await res.arrayBuffer()); +} diff --git a/web/apps/frontend/src/lib/config.ts b/web/standalone/src/lib/config.ts similarity index 100% rename from web/apps/frontend/src/lib/config.ts rename to web/standalone/src/lib/config.ts diff --git a/web/apps/frontend/src/lib/utils.ts b/web/standalone/src/lib/utils.ts similarity index 100% rename from web/apps/frontend/src/lib/utils.ts rename to web/standalone/src/lib/utils.ts diff --git a/web/apps/frontend/src/main.tsx b/web/standalone/src/main.tsx similarity index 100% rename from web/apps/frontend/src/main.tsx rename to web/standalone/src/main.tsx diff --git a/web/standalone/src/pages/HomePage.tsx b/web/standalone/src/pages/HomePage.tsx new file mode 100644 index 0000000..1f41c7d --- /dev/null +++ b/web/standalone/src/pages/HomePage.tsx @@ -0,0 +1,187 @@ +import * as React from "react"; +import { Link } from "react-router-dom"; +import { + EXTENSION_TOOL, + FILELESS_TOOLS, + TOOL_LABELS, + TOOLS, + type Tool, +} from "@pcbjam/shared"; +import { FolderOpen, Loader2 } from "lucide-react"; +import { useProjects } from "@/lib/api"; +import { Button } from "@/components/ui/button"; +import type { ToolFile } from "@/wasm/kicad-runner"; +import { WasmTool } from "@/components/WasmTool"; + +/** A KiCad project picked from the local filesystem (no backend involved). */ +interface LocalProject { + name: string; + files: ToolFile[]; + fetchBytes: (relPath: string) => Promise; + defaultTool?: Tool; + defaultTarget?: string; +} + +function toolForPath(path: string): Tool | null { + const dot = path.lastIndexOf("."); + if (dot < 0) return null; + return EXTENSION_TOOL[path.slice(dot).toLowerCase()] ?? null; +} + +/** Build a LocalProject from a webkitdirectory FileList, stripping the top folder. */ +function buildLocalProject(fileList: FileList): LocalProject { + const map = new Map(); + const first = fileList[0]; + const topPrefix = + first?.webkitRelativePath?.includes("/") + ? first.webkitRelativePath.split("/")[0] + "/" + : ""; + for (const f of Array.from(fileList)) { + const rel = f.webkitRelativePath || f.name; + map.set(rel.startsWith(topPrefix) ? rel.slice(topPrefix.length) : rel, f); + } + const files: ToolFile[] = [...map.keys()].map((path) => ({ path })); + let defaultTool: Tool | undefined; + let defaultTarget: string | undefined; + for (const { path } of files) { + const tool = toolForPath(path); + if (tool) { + defaultTool = tool; + defaultTarget = path; + break; + } + } + return { + name: topPrefix ? topPrefix.slice(0, -1) : "local", + files, + defaultTool, + defaultTarget, + fetchBytes: async (relPath) => { + const f = map.get(relPath); + if (!f) throw new Error(`local file not found: ${relPath}`); + return new Uint8Array(await f.arrayBuffer()); + }, + }; +} + +export function HomePage() { + const { data: projects, isLoading, error } = useProjects(); + const inputRef = React.useRef(null); + const [local, setLocal] = React.useState(null); + const [tool, setTool] = React.useState(""); + const [launched, setLaunched] = React.useState(false); + + // is non-standard; set it imperatively. + React.useEffect(() => { + if (inputRef.current) inputRef.current.setAttribute("webkitdirectory", ""); + }, []); + + // Once launched, the WASM runtime is process-global and one-shot for this page + // load — render the editor full-screen and nothing else (no going back). + if (launched && local && tool) { + const target = tool === local.defaultTool ? local.defaultTarget : undefined; + return ( + + ); + } + + return ( +
+

PCBJam

+

+ Open KiCad files in the browser — from a backend, or straight from a + local folder. +

+ + {/* --- Local folder --- */} +
+

+ Open a local folder +

+

+ No upload — files stay in your browser. Pick a folder containing a + KiCad project. +

+ { + const fl = e.target.files; + if (!fl || fl.length === 0) return; + const proj = buildLocalProject(fl); + setLocal(proj); + setTool(proj.defaultTool ?? ""); + }} + /> + + {local && ( +
+ + {local.files.length} files + + + +
+ )} +
+ + {/* --- Backend projects --- */} +
+

Projects from the backend

+ {isLoading && ( +

+ loading… +

+ )} + {error && ( +

+ No backend reachable ({(error as Error).message}). Use a local folder + above, or configure VITE_API_BASE_URL. +

+ )} +
+ {projects?.map((p) => ( +
+
+

{p.name}

+

/p/{p.slug}

+
+ +
+ ))} + {projects && projects.length === 0 && ( +
+ The backend has no projects. +
+ )} +
+
+
+ ); +} diff --git a/web/apps/frontend/src/pages/ProjectDetailPage.tsx b/web/standalone/src/pages/ProjectView.tsx similarity index 78% rename from web/apps/frontend/src/pages/ProjectDetailPage.tsx rename to web/standalone/src/pages/ProjectView.tsx index b80ab68..ea21a61 100644 --- a/web/apps/frontend/src/pages/ProjectDetailPage.tsx +++ b/web/standalone/src/pages/ProjectView.tsx @@ -4,21 +4,24 @@ import { FILELESS_TOOLS, TOOL_LABELS, type Tool, -} from "@kicad-web/contract"; +} from "@pcbjam/shared"; import { ArrowLeft, ExternalLink, Loader2 } from "lucide-react"; import { useProject } from "@/lib/api"; import { formatBytes } from "@/lib/utils"; import { Button } from "@/components/ui/button"; -import { UploadDropzone } from "@/components/UploadDropzone"; function toolForPath(path: string): Tool | null { const dot = path.lastIndexOf("."); if (dot < 0) return null; - const ext = path.slice(dot).toLowerCase(); - return EXTENSION_TOOL[ext] ?? null; + return EXTENSION_TOOL[path.slice(dot).toLowerCase()] ?? null; } -export function ProjectDetailPage() { +/** + * Read-only view of a backend project: list its files and open them in a tool. + * The editor is GPL and intentionally has no create/delete/upload — those live + * in the closed application that hosts this editor. + */ +export function ProjectView() { const { project: slug = "" } = useParams(); const { data, isLoading, error } = useProject(slug); @@ -26,7 +29,7 @@ export function ProjectDetailPage() {
@@ -35,9 +38,7 @@ export function ProjectDetailPage() { loading…

)} - {error && ( -

{(error as Error).message}

- )} + {error &&

{(error as Error).message}

} {data && ( <> @@ -45,12 +46,14 @@ export function ProjectDetailPage() {

{data.project.name}

-

/p/{data.project.slug}

+

+ /p/{data.project.slug} +

- {/* Standalone (file-less) tools — launched without a target file. - Full reload (anchor) so Emscripten boots into a clean page. */} + {/* File-less tools — launched without a target file. Full reload + (anchor) so Emscripten boots into a clean page. */} {[...FILELESS_TOOLS].map((tool) => ( -
- -
-

Files ({data.files.length})

@@ -96,7 +95,7 @@ export function ProjectDetailPage() { })} {data.files.length === 0 && (
- No files yet — upload some above. + No files in this project.
)}
diff --git a/web/apps/frontend/src/pages/ToolPage.tsx b/web/standalone/src/pages/ToolPage.tsx similarity index 85% rename from web/apps/frontend/src/pages/ToolPage.tsx rename to web/standalone/src/pages/ToolPage.tsx index 154f4e1..472222b 100644 --- a/web/apps/frontend/src/pages/ToolPage.tsx +++ b/web/standalone/src/pages/ToolPage.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; -import { toolSchema } from "@kicad-web/contract"; -import { useProject } from "@/lib/api"; +import { toolSchema } from "@pcbjam/shared"; +import { fetchFileBytes, useProject } from "@/lib/api"; import { WasmTool } from "@/components/WasmTool"; export function ToolPage() { @@ -36,6 +36,7 @@ export function ToolPage() { slug={slug} files={data.files} targetPath={targetPath} + fetchBytes={(relPath) => fetchFileBytes(slug, relPath)} /> ); } diff --git a/web/apps/frontend/src/vite-env.d.ts b/web/standalone/src/vite-env.d.ts similarity index 100% rename from web/apps/frontend/src/vite-env.d.ts rename to web/standalone/src/vite-env.d.ts diff --git a/web/apps/frontend/src/wasm/boot.ts b/web/standalone/src/wasm/boot.ts similarity index 99% rename from web/apps/frontend/src/wasm/boot.ts rename to web/standalone/src/wasm/boot.ts index 986c28f..bd64122 100644 --- a/web/apps/frontend/src/wasm/boot.ts +++ b/web/standalone/src/wasm/boot.ts @@ -1,4 +1,4 @@ -import type { Tool } from "@kicad-web/contract"; +import type { Tool } from "@pcbjam/shared"; import { KICAD_CONFIG_DIR, RESOURCE_PATH, diff --git a/web/apps/frontend/src/wasm/collab/broadcast-transport.ts b/web/standalone/src/wasm/collab/broadcast-transport.ts similarity index 100% rename from web/apps/frontend/src/wasm/collab/broadcast-transport.ts rename to web/standalone/src/wasm/collab/broadcast-transport.ts diff --git a/web/apps/frontend/src/wasm/collab/debug.ts b/web/standalone/src/wasm/collab/debug.ts similarity index 100% rename from web/apps/frontend/src/wasm/collab/debug.ts rename to web/standalone/src/wasm/collab/debug.ts diff --git a/web/apps/frontend/src/wasm/collab/index.ts b/web/standalone/src/wasm/collab/index.ts similarity index 100% rename from web/apps/frontend/src/wasm/collab/index.ts rename to web/standalone/src/wasm/collab/index.ts diff --git a/web/apps/frontend/src/wasm/collab/reconciler.ts b/web/standalone/src/wasm/collab/reconciler.ts similarity index 100% rename from web/apps/frontend/src/wasm/collab/reconciler.ts rename to web/standalone/src/wasm/collab/reconciler.ts diff --git a/web/apps/frontend/src/wasm/collab/types.ts b/web/standalone/src/wasm/collab/types.ts similarity index 100% rename from web/apps/frontend/src/wasm/collab/types.ts rename to web/standalone/src/wasm/collab/types.ts diff --git a/web/apps/frontend/src/wasm/constants.ts b/web/standalone/src/wasm/constants.ts similarity index 97% rename from web/apps/frontend/src/wasm/constants.ts rename to web/standalone/src/wasm/constants.ts index 97c5ec0..a750c03 100644 --- a/web/apps/frontend/src/wasm/constants.ts +++ b/web/standalone/src/wasm/constants.ts @@ -1,4 +1,4 @@ -import type { Tool } from "@kicad-web/contract"; +import type { Tool } from "@pcbjam/shared"; /** * KiCad config/version dir baked into the WASM build. The File→Open dialog diff --git a/web/apps/frontend/src/wasm/global.d.ts b/web/standalone/src/wasm/global.d.ts similarity index 100% rename from web/apps/frontend/src/wasm/global.d.ts rename to web/standalone/src/wasm/global.d.ts diff --git a/web/apps/frontend/src/wasm/kicad-runner.ts b/web/standalone/src/wasm/kicad-runner.ts similarity index 85% rename from web/apps/frontend/src/wasm/kicad-runner.ts rename to web/standalone/src/wasm/kicad-runner.ts index c94c3cb..8dfca97 100644 --- a/web/apps/frontend/src/wasm/kicad-runner.ts +++ b/web/standalone/src/wasm/kicad-runner.ts @@ -1,12 +1,21 @@ -import type { ProjectFile, Tool } from "@kicad-web/contract"; -import { FILELESS_TOOLS } from "@kicad-web/contract"; +import type { Tool } from "@pcbjam/shared"; +import { FILELESS_TOOLS } from "@pcbjam/shared"; import { memfsFilePath, memfsProjectDir } from "./constants"; import { openFileInTool } from "./open-flow"; +/** + * The only thing the editor needs to know about a file to sync it into MEMFS: + * its project-relative POSIX path. Both the contract loader (whose ProjectFile + * is a superset of this) and the local-folder loader satisfy it. + */ +export interface ToolFile { + path: string; +} + export interface DriveOptions { tool: Tool; slug: string; - files: ProjectFile[]; + files: ToolFile[]; targetPath?: string; fetchBytes: (relPath: string) => Promise; log: (msg: string) => void; diff --git a/web/apps/frontend/src/wasm/open-flow.ts b/web/standalone/src/wasm/open-flow.ts similarity index 100% rename from web/apps/frontend/src/wasm/open-flow.ts rename to web/standalone/src/wasm/open-flow.ts diff --git a/web/apps/frontend/tailwind.config.js b/web/standalone/tailwind.config.js similarity index 100% rename from web/apps/frontend/tailwind.config.js rename to web/standalone/tailwind.config.js diff --git a/web/apps/frontend/tsconfig.json b/web/standalone/tsconfig.json similarity index 87% rename from web/apps/frontend/tsconfig.json rename to web/standalone/tsconfig.json index 60bed76..8826a9f 100644 --- a/web/apps/frontend/tsconfig.json +++ b/web/standalone/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../tsconfig.base.json", "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"], "jsx": "react-jsx", diff --git a/web/apps/frontend/vite.config.ts b/web/standalone/vite.config.ts similarity index 100% rename from web/apps/frontend/vite.config.ts rename to web/standalone/vite.config.ts diff --git a/web/turbo.json b/web/turbo.json index c305a13..d1d50a3 100644 --- a/web/turbo.json +++ b/web/turbo.json @@ -1,12 +1,9 @@ { "$schema": "https://turbo.build/schema.json", "globalEnv": [ - "DATABASE_URL", "PORT", - "STORAGE_ROOT", - "STORAGE_DRIVER", + "PROJECT_DIR", "CORS_ORIGIN", - "DEFAULT_OWNER_SLUG", "WASM_SRC_DIR", "VITE_API_BASE_URL", "VITE_WASM_ASSET_BASE_URL"