fix: bypass repeated vite html transforms in dev

This commit is contained in:
Dotta committed 2026-04-14 11:52:57 -05:00
1 parent 2e7db9e202
commit d330076bb2
5 files changed
+202 -59

No files matched your search

@@ -1,43 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import express from "express";
import request from "supertest";
import { actorMiddleware } from "../middleware/auth.js";
function createApp(resolveSession: ReturnType<typeof vi.fn>) {
const app = express();
app.use(
actorMiddleware({} as never, {
deploymentMode: "authenticated",
resolveSession,
}),
);
app.get("/", (_req, res) => {
res.status(200).send("ok");
});
app.get("/api/ping", (_req, res) => {
res.status(200).json({ ok: true });
});
return app;
}
describe("actorMiddleware", () => {
it("skips session resolution for non-api page requests", async () => {
const resolveSession = vi.fn(async () => null);
const app = createApp(resolveSession);
const res = await request(app).get("/");
expect(res.status).toBe(200);
expect(resolveSession).not.toHaveBeenCalled();
});
it("still resolves sessions for api requests", async () => {
const resolveSession = vi.fn(async () => null);
const app = createApp(resolveSession);
const res = await request(app).get("/api/ping");
expect(res.status).toBe(200);
expect(resolveSession).toHaveBeenCalledOnce();
});
});
@@ -0,0 +1,88 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createCachedViteHtmlRenderer, type ViteWatcherHost } from "../vite-html-renderer.js";
function createWatcher() {
const listeners = new Map<string, Set<(file: string) => void>>();
return {
on(event: string, listener: (file: string) => void) {
if (!listeners.has(event)) listeners.set(event, new Set());
listeners.get(event)?.add(listener);
},
off(event: string, listener: (file: string) => void) {
listeners.get(event)?.delete(listener);
},
emit(event: string, file: string) {
for (const listener of listeners.get(event) ?? []) {
listener(file);
}
},
};
}
describe("createCachedViteHtmlRenderer", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
it("reuses the injected dev html shell until a watched file changes", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-vite-html-"));
tempDirs.push(tempDir);
const indexPath = path.join(tempDir, "index.html");
fs.writeFileSync(
indexPath,
'<html><body>v1<script type="module" src="/src/main.tsx"></script></body></html>',
"utf8",
);
const watcher = createWatcher();
const vite: ViteWatcherHost = {
watcher,
};
const renderer = createCachedViteHtmlRenderer({ vite, uiRoot: tempDir });
await expect(renderer.render("/")).resolves.toContain("/@vite/client");
const first = await renderer.render("/");
const second = await renderer.render("/issues");
expect(first).toBe(second);
expect(first.match(/\/@vite\/client/g)?.length).toBe(1);
fs.writeFileSync(
indexPath,
'<html><body>v2<script type="module" src="/src/main.tsx"></script></body></html>',
"utf8",
);
watcher.emit("change", indexPath);
await expect(renderer.render("/")).resolves.toContain("v2");
renderer.dispose();
});
it("does not duplicate the vite client tag when it already exists", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-vite-html-"));
tempDirs.push(tempDir);
fs.writeFileSync(
path.join(tempDir, "index.html"),
'<html><body><script type="module" src="/@vite/client"></script></body></html>',
"utf8",
);
const vite: ViteWatcherHost = {
watcher: createWatcher(),
};
const renderer = createCachedViteHtmlRenderer({ vite, uiRoot: tempDir });
const html = await renderer.render("/");
expect(html.match(/\/@vite\/client/g)?.length).toBe(1);
});
});
+39 -4
View File
@@ -49,9 +49,27 @@ import { createPluginHostServiceCleanup } from "./services/plugin-host-service-c
import { pluginRegistryService } from "./services/plugin-registry.js";
import { createHostClientHandlers } from "@paperclipai/plugin-sdk";
import type { BetterAuthSessionResult } from "./auth/better-auth.js";
import { createCachedViteHtmlRenderer } from "./vite-html-renderer.js";
type UiMode = "none" | "static" | "vite-dev";
const FEEDBACK_EXPORT_FLUSH_INTERVAL_MS = 5_000;
const VITE_DEV_ASSET_PREFIXES = [
"/@fs/",
"/@id/",
"/@react-refresh",
"/@vite/",
"/assets/",
"/node_modules/",
"/src/",
];
const VITE_DEV_STATIC_PATHS = new Set([
"/apple-touch-icon.png",
"/favicon-16x16.png",
"/favicon-32x32.png",
"/favicon.ico",
"/favicon.svg",
"/site.webmanifest",
]);
export function resolveViteHmrPort(serverPort: number): number {
if (serverPort <= 55_535) {
@@ -60,6 +78,13 @@ export function resolveViteHmrPort(serverPort: number): number {
return Math.max(1_024, serverPort - 10_000);
}
function shouldServeViteDevHtml(req: ExpressRequest): boolean {
const pathname = req.path;
if (VITE_DEV_STATIC_PATHS.has(pathname)) return false;
if (VITE_DEV_ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix))) return false;
return req.accepts(["html"]) === "html";
}
export async function createApp(
db: Db,
opts: {
@@ -193,6 +218,7 @@ export async function createApp(
jobStore,
});
const hostServiceCleanup = createPluginHostServiceCleanup(lifecycle, hostServicesDisposers);
let viteHtmlRenderer: ReturnType<typeof createCachedViteHtmlRenderer> | null = null;
const loader = pluginLoader(
db,
{ localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR },
@@ -285,18 +311,26 @@ export async function createApp(
allowedHosts: privateHostnameGateEnabled ? Array.from(privateHostnameAllowSet) : undefined,
},
});
viteHtmlRenderer = createCachedViteHtmlRenderer({
vite,
uiRoot,
brandHtml: applyUiBranding,
});
const renderViteHtml = viteHtmlRenderer;
app.use(vite.middlewares);
app.get(/.*/, async (req, res, next) => {
if (!shouldServeViteDevHtml(req)) {
next();
return;
}
try {
const templatePath = path.resolve(uiRoot, "index.html");
const template = fs.readFileSync(templatePath, "utf-8");
const html = applyUiBranding(await vite.transformIndexHtml(req.originalUrl, template));
const html = await renderViteHtml.render(req.originalUrl);
res.status(200).set({ "Content-Type": "text/html" }).end(html);
} catch (err) {
next(err);
}
});
app.use(vite.middlewares);
}
app.use(errorHandler);
@@ -338,6 +372,7 @@ export async function createApp(
process.once("exit", () => {
if (feedbackExportTimer) clearInterval(feedbackExportTimer);
devWatcher?.close();
viteHtmlRenderer?.dispose();
hostServiceCleanup.disposeAll();
hostServiceCleanup.teardown();
});
+1 -12
View File
@@ -18,13 +18,6 @@ interface ActorMiddlewareOptions {
resolveSession?: (req: Request) => Promise<BetterAuthSessionResult | null>;
}
function shouldResolveBoardSession(req: Request): boolean {
// Only API routes consume req.actor. Skipping session resolution for
// document/static requests avoids unnecessary Better Auth work on each
// Vite-served page load in authenticated dev mode.
return req.path.startsWith("/api");
}
export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHandler {
const boardAuth = boardAuthService(db);
return async (req, _res, next) => {
@@ -37,11 +30,7 @@ export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHa
const authHeader = req.header("authorization");
if (!authHeader?.toLowerCase().startsWith("bearer ")) {
if (
opts.deploymentMode === "authenticated" &&
opts.resolveSession &&
shouldResolveBoardSession(req)
) {
if (opts.deploymentMode === "authenticated" && opts.resolveSession) {
let session: BetterAuthSessionResult | null = null;
try {
session = await opts.resolveSession(req);
+74
View File
@@ -0,0 +1,74 @@
import fs from "node:fs";
import path from "node:path";
type ViteWatcherEvent = "add" | "change" | "unlink";
export interface ViteWatcherHost {
watcher?: {
on?: (event: ViteWatcherEvent, listener: (file: string) => void) => unknown;
off?: (event: ViteWatcherEvent, listener: (file: string) => void) => unknown;
};
}
export interface CachedViteHtmlRenderer {
render(_url: string): Promise<string>;
dispose(): void;
}
const WATCHER_EVENTS: ViteWatcherEvent[] = ["add", "change", "unlink"];
const MAIN_ENTRY_TAG = '<script type="module" src="/src/main.tsx"></script>';
const VITE_CLIENT_TAG = '<script type="module" src="/@vite/client"></script>';
function injectViteClient(html: string): string {
if (html.includes(VITE_CLIENT_TAG)) return html;
if (html.includes(MAIN_ENTRY_TAG)) {
return html.replace(MAIN_ENTRY_TAG, `${VITE_CLIENT_TAG}\n ${MAIN_ENTRY_TAG}`);
}
return html.replace("</body>", ` ${VITE_CLIENT_TAG}\n </body>`);
}
export function createCachedViteHtmlRenderer(opts: {
vite: ViteWatcherHost;
uiRoot: string;
brandHtml?: (html: string) => string;
}): CachedViteHtmlRenderer {
const uiRoot = path.resolve(opts.uiRoot);
const templatePath = path.resolve(uiRoot, "index.html");
const brandHtml = opts.brandHtml ?? ((html: string) => html);
let cachedHtml: string | null = null;
function loadHtml(): string {
if (cachedHtml === null) {
const rawTemplate = fs.readFileSync(templatePath, "utf-8");
cachedHtml = brandHtml(injectViteClient(rawTemplate));
}
return cachedHtml;
}
function invalidate(): void {
cachedHtml = null;
}
function onWatchEvent(filePath: string): void {
const resolvedPath = path.resolve(filePath);
if (resolvedPath === templatePath || resolvedPath.startsWith(`${uiRoot}${path.sep}`)) {
invalidate();
}
}
for (const eventName of WATCHER_EVENTS) {
opts.vite.watcher?.on?.(eventName, onWatchEvent);
}
return {
render(): Promise<string> {
return Promise.resolve(loadHtml());
},
dispose(): void {
for (const eventName of WATCHER_EVENTS) {
opts.vite.watcher?.off?.(eventName, onWatchEvent);
}
},
};
}