diff --git a/docker/app-dev.sh b/docker/app-dev.sh index 08de7cb0..dcce5a1c 100755 --- a/docker/app-dev.sh +++ b/docker/app-dev.sh @@ -1,22 +1,28 @@ #!/bin/sh -# Start the Vite dev server for the compose dev stack, with annotator, media and -# ui-core rebuilding as they are edited. No install: docker/app.Dockerfile already did it. +# Start the Vite dev server for the compose dev stack, with browser-inference, annotator, +# media and ui-core rebuilding as they are edited. No install: docker/app.Dockerfile already +# did it. set -eu -# All three libraries are consumed through their `dist/`, so vite cannot resolve them +# All four libraries are consumed through their `dist/`, so vite cannot resolve them # from source. A blocking build first, because a watcher's first pass is asynchronous and # vite would race an empty `dist/`. Topological order matters here (unlike the watch # below): ui-core imports @visionset/media, so media must build before ui-core does. The # app itself is not built: nothing in dev reads it. -echo "app-dev: building @visionset/annotator, @visionset/media and @visionset/ui-core" -pnpm --filter @visionset/annotator --filter @visionset/media --filter @visionset/ui-core build +# browser-inference also has to build before Vite starts: its ORT artifact directory is +# served by frontend/app/vite.config.ts rather than being copied into this source tree. +echo "app-dev: building @visionset/browser-inference, @visionset/annotator, @visionset/media and @visionset/ui-core" +pnpm --filter @visionset/browser-inference --filter @visionset/annotator --filter @visionset/media --filter @visionset/ui-core build # tsup's watcher is chokidar underneath, so it already honours CHOKIDAR_USEPOLLING from -# compose.yaml — no polling flags to pass here, unlike tsc before #839. `--watch` alone; -# tsup accepts no `--preserveWatchOutput`/`--watchFile`/`--watchDirectory` (those were -# tsc's) and rejects unknown flags outright. -echo "app-dev: starting watch builds for annotator + media + ui-core" -pnpm --filter @visionset/annotator --filter @visionset/media --filter @visionset/ui-core --parallel run build --watch & +# compose.yaml — no polling flags to pass here, unlike tsc before #839. Every package's +# regular build uses `clean: true`, which is correct for the blocking build above but +# wrong for a long-lived watcher: Vite may resolve a library between its clean and emit, +# or read browser-inference's ORT directory before its copy script restores it. Watch +# tsup directly with cleaning disabled. The blocking build provides a clean output and +# browser-inference's immutable ORT assets only change when its configuration is rebuilt. +echo "app-dev: starting watch builds for browser-inference + annotator + media + ui-core" +pnpm --filter @visionset/browser-inference --filter @visionset/annotator --filter @visionset/media --filter @visionset/ui-core --parallel exec tsup --watch --clean=false & WATCH_PID=$! # Backgrounded rather than `exec`, so this shell stays PID 1 and keeps the trap. diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile index 056409bd..6269b088 100644 --- a/docker/app.Dockerfile +++ b/docker/app.Dockerfile @@ -19,6 +19,7 @@ WORKDIR /workspace # Manifests and lockfile only, so a source edit does not re-run the install. COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ COPY frontend/annotator/package.json ./frontend/annotator/ +COPY frontend/browser-inference/package.json ./frontend/browser-inference/ COPY frontend/ui-core/package.json ./frontend/ui-core/ COPY frontend/media/package.json ./frontend/media/ COPY frontend/app/package.json ./frontend/app/ @@ -37,8 +38,12 @@ RUN --mount=type=cache,target=/pnpm-store \ # purpose (Playwright, which this image never runs). tsup.config.ts is required too: # app-dev.sh builds these packages with tsup (#839), and it reads no CLI entry, so # without the config file on disk it fails with "No input files". ui-core imports -# @visionset/media (#847), so it needs the same treatment. +# @visionset/media (#847), and app's Vite configuration reads the ORT assets that +# @visionset/browser-inference emits, so both need the same treatment. COPY frontend/annotator/tsconfig*.json frontend/annotator/tsup.config.ts ./frontend/annotator/ +COPY frontend/browser-inference/tsconfig*.json frontend/browser-inference/tsup.config.ts ./frontend/browser-inference/ +COPY frontend/browser-inference/scripts ./frontend/browser-inference/scripts +COPY frontend/browser-inference/src ./frontend/browser-inference/src COPY frontend/ui-core/tsconfig*.json frontend/ui-core/tsup.config.ts ./frontend/ui-core/ COPY frontend/media/tsconfig*.json frontend/media/tsup.config.ts ./frontend/media/ COPY frontend/app/tsconfig.json frontend/app/vite.config.ts frontend/app/index.html ./frontend/app/ diff --git a/docker/compose.yaml b/docker/compose.yaml index 38c2c976..22f70454 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -81,6 +81,7 @@ services: # image's node_modules. Manifests, tsconfigs, vite.config.ts and index.html are # baked into the image, so editing one needs `build`. - ../frontend/annotator/src:/workspace/frontend/annotator/src + - ../frontend/browser-inference/src:/workspace/frontend/browser-inference/src - ../frontend/media/src:/workspace/frontend/media/src - ../frontend/ui-core/src:/workspace/frontend/ui-core/src - ../frontend/app/src:/workspace/frontend/app/src diff --git a/tests/scripts/compose_browser_inference.test.mjs b/tests/scripts/compose_browser_inference.test.mjs new file mode 100644 index 00000000..990c6806 --- /dev/null +++ b/tests/scripts/compose_browser_inference.test.mjs @@ -0,0 +1,56 @@ +// Run with: pnpm test:scripts (also part of the root `pnpm test`) +// +// The Compose app image starts Vite, whose configuration serves the ORT assets emitted +// by @visionset/browser-inference. That package is consumed through `dist/`, so merely +// installing its dependencies is insufficient: the image needs its build inputs and +// app-dev.sh must build it before Vite reads the configuration. +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const read = (...parts) => readFileSync(join(ROOT, ...parts), "utf8"); + +test("the Compose app image builds browser-inference before Vite starts", () => { + const dockerfile = read("docker", "app.Dockerfile"); + const devScript = read("docker", "app-dev.sh"); + const compose = read("docker", "compose.yaml"); + + assert.match( + dockerfile, + /^COPY frontend\/browser-inference\/package\.json \.\/frontend\/browser-inference\/$/m, + "docker/app.Dockerfile must copy browser-inference's manifest before pnpm install", + ); + assert.match( + dockerfile, + /^COPY frontend\/browser-inference\/src \.\/frontend\/browser-inference\/src$/m, + "docker/app.Dockerfile must bake browser-inference's build source into the app image", + ); + assert.match( + dockerfile, + /^COPY frontend\/browser-inference\/scripts \.\/frontend\/browser-inference\/scripts$/m, + "docker/app.Dockerfile must bake browser-inference's ORT-copy build script into the app image", + ); + assert.match( + dockerfile, + /^COPY frontend\/browser-inference\/tsconfig\*\.json frontend\/browser-inference\/tsup\.config\.ts \.\/frontend\/browser-inference\/$/m, + "docker/app.Dockerfile must bake browser-inference's tsup configuration into the app image", + ); + assert.match( + devScript, + /pnpm --filter @visionset\/browser-inference(?:\s|$).* build/m, + "docker/app-dev.sh must build browser-inference before starting Vite", + ); + assert.match( + devScript, + /pnpm --filter @visionset\/browser-inference --filter @visionset\/annotator --filter @visionset\/media --filter @visionset\/ui-core --parallel exec tsup --watch --clean=false/m, + "docker/app-dev.sh must watch every library without clearing its dist output", + ); + assert.match( + compose, + /^\s*- \.\.\/frontend\/browser-inference\/src:\/workspace\/frontend\/browser-inference\/src$/m, + "docker/compose.yaml must mount browser-inference source for the watcher", + ); +});