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.
This commit is contained in:
georgelichen 2026-08-27 16:07:16 +08:00 committed by GitHub
parent ed336ec3dd
commit bdd68469c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 147 additions and 29 deletions

View File

@ -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 <pattern> # e.g. pnpm rstest run my-component
### Logs
- Docker stack: `make docker-logs` (or `docker compose -f docker/... logs -f <svc>`).
- 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.

View File

@ -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`.

View File

@ -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

View File

@ -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() {

View File

@ -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",

71
frontend/scripts/dev.mjs Normal file
View File

@ -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<string, string | undefined>} 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<string, string | undefined>} 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();
}

View File

@ -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"]);
});
});