From 9234bac79e1c21879e3fd4c1ae4f5afc33bd9d9b Mon Sep 17 00:00:00 2001 From: tianma-if Date: Mon, 19 Oct 2026 18:26:11 +0800 Subject: [PATCH] fix(poster): isolate automatic thumbnail generation in Node --- Dockerfile | 2 +- apps/web/poster-thumbnails.ts | 4 ++-- .../poster-template-previews/manifest.json | 2 +- scripts/docker-release.test.mjs | 2 +- scripts/prepare-poster-thumbnails.d.mts | 1 + scripts/prepare-poster-thumbnails.mjs | 23 ++++++++++++++++--- scripts/verify-poster-thumbnail-updates.mjs | 17 +++----------- 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index ac225fa5..b80746bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN bun install --frozen-lockfile \ FROM dependencies AS build # Only stale template previews need the build browser; it stays out of runtime. -RUN apk add --no-cache chromium +RUN apk add --no-cache chromium nodejs ENV EDGE_EVER_POSTER_CHROME_EXECUTABLE=/usr/bin/chromium COPY apps/api apps/api COPY apps/web apps/web diff --git a/apps/web/poster-thumbnails.ts b/apps/web/poster-thumbnails.ts index 470cdfc5..3fb631fd 100644 --- a/apps/web/poster-thumbnails.ts +++ b/apps/web/poster-thumbnails.ts @@ -1,9 +1,9 @@ import path from "node:path"; import type { Plugin, ViteDevServer } from "vite"; -import { ensurePosterThumbnails, projectRoot, sources } from "../../scripts/prepare-poster-thumbnails.mjs"; +import { runPosterThumbnailGeneration, projectRoot, sources } from "../../scripts/prepare-poster-thumbnails.mjs"; /** Generate on build/startup and coalesce template saves while rendering. */ -export function automaticPosterThumbnails({ root = projectRoot, generate = ensurePosterThumbnails } = {}): Plugin { +export function automaticPosterThumbnails({ root = projectRoot, generate = runPosterThumbnailGeneration } = {}): Plugin { let command: "build" | "serve"; let server: ViteDevServer | undefined; let timer: ReturnType | undefined; diff --git a/apps/web/src/assets/poster-template-previews/manifest.json b/apps/web/src/assets/poster-template-previews/manifest.json index 3aa83e50..5313cd80 100644 --- a/apps/web/src/assets/poster-template-previews/manifest.json +++ b/apps/web/src/assets/poster-template-previews/manifest.json @@ -1,6 +1,6 @@ { "version": 2, - "sourceHash": "9cd2f217b58072425f856730ab245e9723160c7fb387b02300824887e91a955e", + "sourceHash": "014dc1441fe7b47b32069e372c8820d70a8b58aa353f6232efaf7f4e1330f508", "painterHash": "21dcb74339638258001efc7f15a679376cf51ff95ccbcf2cc31026d4f2a94a96", "width": 480, "height": 640, diff --git a/scripts/docker-release.test.mjs b/scripts/docker-release.test.mjs index 840c7b6a..b1d59bc0 100644 --- a/scripts/docker-release.test.mjs +++ b/scripts/docker-release.test.mjs @@ -80,7 +80,7 @@ describe("Docker release contract", () => { ); expect(dockerfile).toContain("RUN bun run build:web && bun run build:self-hosted"); expect(dockerfile).toContain("scripts/prepare-poster-thumbnails.mjs scripts/prepare-poster-thumbnails.d.mts scripts/"); - expect(dockerfile).toContain("RUN apk add --no-cache chromium"); + expect(dockerfile).toContain("RUN apk add --no-cache chromium nodejs"); expect(dockerfile).toContain("EDGE_EVER_POSTER_CHROME_EXECUTABLE=/usr/bin/chromium"); expect(dockerfile).toContain("COPY tests/browser/poster-thumbnail-generator.html tests/browser/poster-thumbnail-generator.ts tests/browser/"); expect(runtimeStage).toContain( diff --git a/scripts/prepare-poster-thumbnails.d.mts b/scripts/prepare-poster-thumbnails.d.mts index 13e36024..1edb2bd4 100644 --- a/scripts/prepare-poster-thumbnails.d.mts +++ b/scripts/prepare-poster-thumbnails.d.mts @@ -5,3 +5,4 @@ export function ensurePosterThumbnails(options?: { output?: string; log?: (message: string) => void; }): Promise<{ generated: number; skipped: boolean }>; +export function runPosterThumbnailGeneration(options?: { root?: string }): Promise<{ generated: number; skipped: boolean }>; diff --git a/scripts/prepare-poster-thumbnails.mjs b/scripts/prepare-poster-thumbnails.mjs index 25c34667..a86ebfec 100644 --- a/scripts/prepare-poster-thumbnails.mjs +++ b/scripts/prepare-poster-thumbnails.mjs @@ -243,12 +243,29 @@ export async function ensurePosterThumbnails({ root = projectRoot, output = path } } +// Playwright's process/transport lifecycle is supported by Node. Keep browser +// generation out of the application's Vite/Bun process, including Docker builds. +export async function runPosterThumbnailGeneration({ root = projectRoot } = {}) { + if (await thumbnailsCurrent(path.join(root, "apps/web/src/assets/poster-template-previews"), await sourceState(root))) return { generated: 0, skipped: true }; + const { stdout, stderr } = await promisify(execFile)("node", [fileURLToPath(import.meta.url), "--json", "--root", root], { + cwd: root, maxBuffer: 8 * 1024 * 1024, + }); + if (stderr) process.stderr.write(stderr); + return JSON.parse(stdout); +} + if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + const rootIndex = process.argv.indexOf("--root"); + const root = rootIndex < 0 ? projectRoot : path.resolve(process.argv[rootIndex + 1]); if (process.argv.includes("--check")) { - if (!await thumbnailsCurrent(path.join(projectRoot, "apps/web/src/assets/poster-template-previews"), await sourceState())) throw new Error("Poster thumbnails are stale; normal dev/build will regenerate them automatically."); + if (!await thumbnailsCurrent(path.join(root, "apps/web/src/assets/poster-template-previews"), await sourceState(root))) throw new Error("Poster thumbnails are stale; normal dev/build will regenerate them automatically."); console.log("Poster thumbnails are current"); } else { - const result = await ensurePosterThumbnails(); - if (result.skipped) console.log("[poster thumbnails] Already current; no renderer needed"); + const json = process.argv.includes("--json"); + const result = process.versions.bun + ? await runPosterThumbnailGeneration({ root }) + : await ensurePosterThumbnails({ root, log: json ? message => console.error(message) : console.log }); + if (json) console.log(JSON.stringify(result)); + else if (result.skipped) console.log("[poster thumbnails] Already current; no renderer needed"); } } diff --git a/scripts/verify-poster-thumbnail-updates.mjs b/scripts/verify-poster-thumbnail-updates.mjs index d9c50224..f32a04e5 100644 --- a/scripts/verify-poster-thumbnail-updates.mjs +++ b/scripts/verify-poster-thumbnail-updates.mjs @@ -1,4 +1,4 @@ -// Run with Bun. A temporary source fixture and Chrome exercise the real Vite +// Run with Node (native TypeScript support). A temporary source fixture and Chrome exercise the real Vite // watcher and renderer; no working-tree templates or user notes are changed. import assert from "node:assert/strict"; import { mkdtemp, mkdir, cp, readFile, writeFile, symlink, rm, stat } from "node:fs/promises"; @@ -6,7 +6,7 @@ import path from "node:path"; import { tmpdir } from "node:os"; import { createServer } from "vite"; import { automaticPosterThumbnails } from "../apps/web/poster-thumbnails.ts"; -import { sources, projectRoot, ensurePosterThumbnails, openRenderer, sourceState, thumbnailsCurrent } from "./prepare-poster-thumbnails.mjs"; +import { sources, projectRoot, ensurePosterThumbnails, sourceState, thumbnailsCurrent } from "./prepare-poster-thumbnails.mjs"; await ensurePosterThumbnails(); const root = await mkdtemp(path.join(tmpdir(), "edgeever-poster-watch-")); @@ -34,18 +34,7 @@ try { await symlink(path.join(projectRoot, "apps/web/node_modules"), path.join(root, "apps/web/node_modules"), process.platform === "win32" ? "junction" : "dir"); server = await createServer({ configFile: false, root, logLevel: "error", appType: "mpa", - plugins: [automaticPosterThumbnails({ root, generate: options => ensurePosterThumbnails({ - ...options, - renderer: async root => { - const runtime = await openRenderer(root); - const prepare = runtime.prepare; - runtime.prepare = async (id, language) => { - if (language === "zh") console.log(`[watch test] Inspecting ${id}`); - return prepare(id, language); - }; - return runtime; - }, - }) })], + plugins: [automaticPosterThumbnails({ root })], server: { host: "127.0.0.1", port: 0, fs: { allow: [root, projectRoot] } }, optimizeDeps: { noDiscovery: true }, });