From bdd68469c156877daa3facb547314b8391cabf73 Mon Sep 17 00:00:00 2001 From: georgelichen Date: Thu, 27 Aug 2026 16:07:16 +0800 Subject: [PATCH] fix(frontend): stabilize Windows development startup (#5036) * Stabilize Windows frontend development startup Next.js Turbopack can infer a user-level workspace root and hit an internal Rust task panic on Windows. Keep the frontend root explicit and select Webpack on Windows while preserving Turbopack elsewhere. Constraint: The existing non-Windows development workflow should keep Turbopack. Rejected: Disable Turbopack on every platform | unnecessary performance regression. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Revisit the Windows Webpack fallback after the upstream Turbopack panic is resolved. Tested: Prettier, ESLint, TypeScript, 139 frontend test files with 1060 tests, and Windows pnpm dev smoke test on port 3010. Not-tested: Production build and browser E2E suite. Related: #4957 * Make the frontend bundler fallback diagnosable Keep the Windows Webpack fallback while allowing local diagnosis to select either supported Next.js bundler. Resolve the Turbopack root through the Node 20-compatible URL API so the explicit workspace boundary remains effective across the declared Next.js engine range. Constraint: Windows defaults to Webpack because of the observed Turbopack panic, while non-Windows defaults to Turbopack. Rejected: Accept arbitrary bundler values | Next.js only supports the two explicit flags and invalid values should fail early. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Revisit the default Windows fallback when the upstream TaskGuard issue is resolved; the override is intentionally retained for diagnosis. Tested: Frontend format, ESLint, TypeScript, 139-file/1062-test Rstest suite, target launcher tests, and next.config.js absolute-root load check. Not-tested: Production build and browser E2E suite. --- AGENTS.md | 4 +- frontend/AGENTS.md | 26 ++++----- frontend/README.md | 28 +++++----- frontend/next.config.js | 5 ++ frontend/package.json | 2 +- frontend/scripts/dev.mjs | 71 +++++++++++++++++++++++++ frontend/tests/unit/scripts/dev.test.ts | 40 ++++++++++++++ 7 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 frontend/scripts/dev.mjs create mode 100644 frontend/tests/unit/scripts/dev.test.ts diff --git a/AGENTS.md b/AGENTS.md index 410798224..3b1a1e496 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -151,7 +151,7 @@ cd backend && make lint # ruff check cd backend && make format # ruff format # Frontend (see frontend/AGENTS.md for the full set) -cd frontend && pnpm dev # Dev server with Turbopack (port 3000) +cd frontend && pnpm dev # Dev server: Webpack on Windows, Turbopack elsewhere (override with DEER_FLOW_DEV_BUNDLER) cd frontend && pnpm check # Lint + type check (run before committing) cd frontend && pnpm test # Unit tests ``` @@ -188,7 +188,7 @@ cd frontend && pnpm rstest run # e.g. pnpm rstest run my-component ### Logs - Docker stack: `make docker-logs` (or `docker compose -f docker/... logs -f `). -- Local `make dev`: each service logs to its own terminal pane. Frontend Turbopack +- Local `make dev`: each service logs to its own terminal pane. Frontend dev-server errors surface in the browser console at `localhost:3000`; backend tracebacks appear in the Gateway terminal. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 8edd8087b..7de7cbd33 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -17,21 +17,23 @@ DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It commu ## Commands -| Command | Purpose | -| ---------------- | ------------------------------------------------- | -| `pnpm dev` | Dev server with Turbopack (http://localhost:3000) | -| `pnpm build` | Production build | -| `pnpm check` | Lint + type check (run before committing) | -| `pnpm lint` | ESLint only | -| `pnpm lint:fix` | ESLint with auto-fix | -| `pnpm format` | Prettier check (`pnpm format:write` to apply) | -| `pnpm test` | Run unit tests with Rstest | -| `pnpm test:e2e` | Run E2E tests with Playwright (Chromium) | -| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) | -| `pnpm start` | Start production server | +| Command | Purpose | +| ---------------- | ------------------------------------------------------------------- | +| `pnpm dev` | Platform-aware dev server (Webpack on Windows, Turbopack elsewhere) | +| `pnpm build` | Production build | +| `pnpm check` | Lint + type check (run before committing) | +| `pnpm lint` | ESLint only | +| `pnpm lint:fix` | ESLint with auto-fix | +| `pnpm format` | Prettier check (`pnpm format:write` to apply) | +| `pnpm test` | Run unit tests with Rstest | +| `pnpm test:e2e` | Run E2E tests with Playwright (Chromium) | +| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) | +| `pnpm start` | Start production server | Unit tests live under `tests/unit/` and mirror the `src/` layout (e.g., `tests/unit/core/api/stream-mode.test.ts` tests `src/core/api/stream-mode.ts`). Powered by Rstest; import source modules via the `@/` path alias. +Use `DEER_FLOW_DEV_BUNDLER=turbo` or `DEER_FLOW_DEV_BUNDLER=webpack` with `pnpm dev` to override the platform default when diagnosing a local Next.js bundler issue. + Rstest runs them as two projects (`rstest.config.ts`). `*.test.ts` / `*.test.tsx` run in a plain **node** environment — that is nearly the whole suite, and it is the default for anything that is pure logic. `*.dom.test.ts` / `*.dom.test.tsx` run in **happy-dom**, for tests that need a document: hooks driven through `renderHook` from `@testing-library/react`, and components. Keep the split — a DOM environment costs roughly 3x the runtime of the node suite, so tests that do not render should not opt into it. A hook whose behavior only exists under real React (effect ordering, cleanup on unmount, re-render on store change) belongs in a `.dom.test.*` file rather than a node test that mocks `react` itself. E2E tests live under `tests/e2e/` and use Playwright with Chromium. They mock all backend APIs via `page.route()` network interception and test real page interactions (navigation, chat input, streaming responses). Config: `playwright.config.ts`. diff --git a/frontend/README.md b/frontend/README.md index 7660d8e34..e234cf873 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -127,24 +127,24 @@ src/ ## Scripts -| Command | Description | -| ------------------- | --------------------------------------- | -| `pnpm dev` | Start development server with Turbopack | -| `pnpm build` | Build for production | -| `pnpm start` | Start production server | -| `pnpm test` | Run unit tests with Rstest | -| `pnpm test:e2e` | Run E2E tests with Playwright | -| `pnpm format` | Check formatting with Prettier | -| `pnpm format:write` | Apply formatting with Prettier | -| `pnpm lint` | Run ESLint | -| `pnpm lint:fix` | Fix ESLint issues | -| `pnpm typecheck` | Run TypeScript type checking | -| `pnpm check` | Run both lint and typecheck | +| Command | Description | +| ------------------- | ------------------------------------------------------------------ | +| `pnpm dev` | Start development server (Webpack on Windows, Turbopack elsewhere) | +| `pnpm build` | Build for production | +| `pnpm start` | Start production server | +| `pnpm test` | Run unit tests with Rstest | +| `pnpm test:e2e` | Run E2E tests with Playwright | +| `pnpm format` | Check formatting with Prettier | +| `pnpm format:write` | Apply formatting with Prettier | +| `pnpm lint` | Run ESLint | +| `pnpm lint:fix` | Fix ESLint issues | +| `pnpm typecheck` | Run TypeScript type checking | +| `pnpm check` | Run both lint and typecheck | ## Development Notes - Uses pnpm workspaces (see `packageManager` in package.json) -- Turbopack enabled by default in development for faster builds +- Turbopack is used by default in development except on Windows, where Webpack avoids known Turbopack runtime instability. Set `DEER_FLOW_DEV_BUNDLER=turbo` or `DEER_FLOW_DEV_BUNDLER=webpack` to override the platform default for local diagnosis. - Environment validation can be skipped with `SKIP_ENV_VALIDATION=1` (useful for Docker) - Backend API URLs are optional; nginx proxy is used by default in development diff --git a/frontend/next.config.js b/frontend/next.config.js index c1b984c2f..f2cf384b1 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,3 +1,5 @@ +import { fileURLToPath } from "node:url"; + /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful * for Docker builds. @@ -25,6 +27,9 @@ const config = { locales: ["en", "zh"], defaultLocale: "en", }, + turbopack: { + root: fileURLToPath(new URL(".", import.meta.url)), + }, devIndicators: false, allowedDevOrigins: getAllowedDevOrigins(), async rewrites() { diff --git a/frontend/package.json b/frontend/package.json index b1202a057..1ede350f4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "demo:save": "node scripts/save-demo.js", "build": "next build", "check": "eslint . --ext .ts,.tsx && tsc --noEmit", - "dev": "next dev --turbo", + "dev": "node scripts/dev.mjs", "format": "prettier --check .", "format:write": "prettier --write .", "lint": "eslint . --ext .ts,.tsx", diff --git a/frontend/scripts/dev.mjs b/frontend/scripts/dev.mjs new file mode 100644 index 000000000..2299a4b73 --- /dev/null +++ b/frontend/scripts/dev.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * @param {string} platform + * @param {Record} env + */ +export function getDevBundler(platform = process.platform, env = process.env) { + const override = env.DEER_FLOW_DEV_BUNDLER?.trim(); + if (override) { + if (override !== "turbo" && override !== "webpack") { + throw new Error( + 'DEER_FLOW_DEV_BUNDLER must be either "turbo" or "webpack"', + ); + } + return override; + } + return platform === "win32" ? "webpack" : "turbo"; +} + +/** + * @param {string} platform + * @param {string[]} extraArgs + * @param {Record} env + */ +export function getNextDevArgs( + platform = process.platform, + extraArgs = [], + env = process.env, +) { + const nextArgs = extraArgs[0] === "--" ? extraArgs.slice(1) : extraArgs; + return ["dev", `--${getDevBundler(platform, env)}`, ...nextArgs]; +} + +function startDevServer() { + const frontendDir = fileURLToPath(new URL("..", import.meta.url)); + const nextBin = fileURLToPath( + new URL("../node_modules/next/dist/bin/next", import.meta.url), + ); + const child = spawn( + process.execPath, + [nextBin, ...getNextDevArgs(process.platform, process.argv.slice(2))], + { + cwd: frontendDir, + env: process.env, + stdio: "inherit", + }, + ); + + child.on("error", (error) => { + console.error(`Failed to start Next.js: ${error.message}`); + process.exitCode = 1; + }); + child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exitCode = code ?? 1; + }); +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + startDevServer(); +} diff --git a/frontend/tests/unit/scripts/dev.test.ts b/frontend/tests/unit/scripts/dev.test.ts new file mode 100644 index 000000000..403a255ef --- /dev/null +++ b/frontend/tests/unit/scripts/dev.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "@rstest/core"; + +import { getDevBundler, getNextDevArgs } from "../../../scripts/dev.mjs"; + +describe("frontend dev launcher", () => { + test("uses webpack on Windows to avoid Turbopack runtime instability", () => { + expect(getDevBundler("win32")).toBe("webpack"); + expect(getNextDevArgs("win32")).toEqual(["dev", "--webpack"]); + }); + + test("allows an explicit bundler override on every platform", () => { + expect(getDevBundler("win32", { DEER_FLOW_DEV_BUNDLER: "turbo" })).toBe( + "turbo", + ); + expect( + getNextDevArgs("linux", [], { DEER_FLOW_DEV_BUNDLER: "webpack" }), + ).toEqual(["dev", "--webpack"]); + }); + + test("rejects an unsupported bundler override", () => { + expect(() => + getDevBundler("linux", { DEER_FLOW_DEV_BUNDLER: "invalid" }), + ).toThrow('DEER_FLOW_DEV_BUNDLER must be either "turbo" or "webpack"'); + }); + + test("passes through extra Next.js arguments", () => { + expect(getNextDevArgs("win32", ["--", "--port", "3302"])).toEqual([ + "dev", + "--webpack", + "--port", + "3302", + ]); + }); + + test("keeps Turbopack for non-Windows development", () => { + expect(getDevBundler("linux")).toBe("turbo"); + expect(getDevBundler("darwin")).toBe("turbo"); + expect(getNextDevArgs("linux")).toEqual(["dev", "--turbo"]); + }); +});