fix: reduce dashboard JS bundle from 1.7MB to 170KB (#792) (#928)

* fix: reduce dashboard JS bundle from 1.7MB to 170KB (gzipped) (#792)

Switch `ao start` default from `next dev` (7.6MB uncompressed) to optimized
production builds (128KB per route). Add `--dev` flag for HMR when editing
dashboard UI. Add bundle analyzer, server-only guards, and lazy-load
DirectTerminal via next/dynamic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip dashboard rebuild when assets exist, fix CI timeout

Skip the production build step when .next/BUILD_ID and dist-server/
already exist (e.g. after pnpm build in CI). Add c8 ignore for
untestable process-spawning startup code to fix diff coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: adopt PR #903 patterns — centralize rebuild logic and add preflight web artifact checks

Extract rebuildDashboardProductionArtifacts into dashboard-rebuild.ts, add
isInstalledUnderNodeModules/assertDashboardRebuildSupported guards, remove
findProcessWebDir, and verify .next/BUILD_ID + dist-server/start-all.js in
preflight. Dashboard command now always uses production server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip production preflight in --dev mode, add coverage for rebuild helpers

- Skip preflight.checkBuilt() when --dev is passed (dev mode uses HMR,
  doesn't need .next/BUILD_ID or dist-server/start-all.js)
- Add c8 ignore to dashboard.ts process-spawning code (matches start.ts pattern)
- Add tests for rebuildDashboardProductionArtifacts (success, failure, npm guard)
- Add preflight tests for npm-install web artifact hint paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: align preflight skip with actual dev-server condition

Only skip production artifact preflight when both --dev is passed AND
we're in the monorepo (where dev mode actually works). For npm global
installs, --dev is silently ignored and production server runs, so
preflight must still validate .next/BUILD_ID and dist-server/start-all.js.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: match @next/bundle-analyzer version to next@^15.1.0

The @next/* packages follow the Next.js release train. Pin the bundle
analyzer to ^15.1.0 to match the project's next dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: correct recovery command for npm installs and remove redundant guard

- Change stale-build recovery suggestion from "ao update" (which only
  works in source checkouts) to "npm install -g @composio/ao@latest"
  for npm global installs
- Remove redundant assertDashboardRebuildSupported call in dashboard.ts
  since rebuildDashboardProductionArtifacts already calls it internally

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ashish Huddar 2026-04-07 16:12:51 +05:30 committed by GitHub
parent 7fff3b940e
commit 48d655d932
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 449 additions and 116 deletions

View File

@ -89,45 +89,83 @@ describe("findRunningDashboardPid", () => {
});
});
describe("findProcessWebDir", () => {
it("extracts cwd from lsof output", async () => {
const webDir = join(tmpDir, "web");
mkdirSync(webDir, { recursive: true });
writeFileSync(join(webDir, "package.json"), "{}");
describe("isInstalledUnderNodeModules", () => {
it("returns true for a Unix node_modules path segment", async () => {
const { isInstalledUnderNodeModules } = await import("../../src/lib/dashboard-rebuild.js");
// Simulate lsof -p <pid> -Fn output
mockExecSilent.mockResolvedValue(
`p12345\nfcwd\nn${webDir}\nftxt\nn/usr/bin/node`,
);
const { findProcessWebDir } = await import("../../src/lib/dashboard-rebuild.js");
const result = await findProcessWebDir("12345");
expect(result).toBe(webDir);
expect(isInstalledUnderNodeModules("/usr/local/lib/node_modules/@composio/ao-web")).toBe(true);
});
it("returns null when cwd has no package.json", async () => {
const webDir = join(tmpDir, "web");
mkdirSync(webDir, { recursive: true });
// No package.json
it("returns true for a Windows node_modules path segment", async () => {
const { isInstalledUnderNodeModules } = await import("../../src/lib/dashboard-rebuild.js");
mockExecSilent.mockResolvedValue(
`p12345\nfcwd\nn${webDir}\nftxt\nn/usr/bin/node`,
);
const { findProcessWebDir } = await import("../../src/lib/dashboard-rebuild.js");
const result = await findProcessWebDir("12345");
expect(result).toBeNull();
expect(isInstalledUnderNodeModules("C:\\Users\\me\\node_modules\\@composio\\ao-web")).toBe(true);
});
it("returns null when lsof fails", async () => {
mockExecSilent.mockResolvedValue(null);
it("returns false for source paths containing node_modules as plain text", async () => {
const { isInstalledUnderNodeModules } = await import("../../src/lib/dashboard-rebuild.js");
const { findProcessWebDir } = await import("../../src/lib/dashboard-rebuild.js");
expect(
isInstalledUnderNodeModules("/home/user/node_modules_backup/agent-orchestrator/packages/web"),
).toBe(false);
});
});
const result = await findProcessWebDir("12345");
expect(result).toBeNull();
describe("assertDashboardRebuildSupported", () => {
it("passes for a source checkout", async () => {
const { assertDashboardRebuildSupported } = await import("../../src/lib/dashboard-rebuild.js");
expect(() =>
assertDashboardRebuildSupported("/home/user/agent-orchestrator/packages/web"),
).not.toThrow();
});
it("throws for an npm-installed package path", async () => {
const { assertDashboardRebuildSupported } = await import("../../src/lib/dashboard-rebuild.js");
expect(() =>
assertDashboardRebuildSupported("/usr/local/lib/node_modules/@composio/ao-web"),
).toThrow("Dashboard rebuild is only available from a source checkout");
});
});
describe("rebuildDashboardProductionArtifacts", () => {
it("cleans .next and runs pnpm build on success", async () => {
const webDir = join(tmpDir, "packages", "web");
mkdirSync(webDir, { recursive: true });
mkdirSync(join(webDir, ".next"), { recursive: true });
mockExec.mockResolvedValue({ stdout: "", stderr: "" });
const { rebuildDashboardProductionArtifacts } = await import("../../src/lib/dashboard-rebuild.js");
await rebuildDashboardProductionArtifacts(webDir);
// .next should be cleaned
expect(existsSync(join(webDir, ".next"))).toBe(false);
// pnpm build should be called from workspace root (../../ relative to webDir)
expect(mockExec).toHaveBeenCalledWith("pnpm", ["build"], { cwd: tmpDir });
});
it("throws when pnpm build fails", async () => {
const webDir = join(tmpDir, "packages", "web");
mkdirSync(webDir, { recursive: true });
mockExec.mockRejectedValue(new Error("build failed"));
const { rebuildDashboardProductionArtifacts } = await import("../../src/lib/dashboard-rebuild.js");
await expect(rebuildDashboardProductionArtifacts(webDir)).rejects.toThrow(
"Failed to rebuild dashboard production artifacts",
);
});
it("throws when called from an npm-installed path", async () => {
const { rebuildDashboardProductionArtifacts } = await import("../../src/lib/dashboard-rebuild.js");
await expect(
rebuildDashboardProductionArtifacts("/usr/local/lib/node_modules/@composio/ao-web"),
).rejects.toThrow("Dashboard rebuild is only available from a source checkout");
});
});

View File

@ -113,9 +113,8 @@ vi.mock("../../src/lib/web-dir.js", () => ({
}));
vi.mock("../../src/lib/dashboard-rebuild.js", () => ({
cleanNextCache: vi.fn(),
findRunningDashboardPid: vi.fn().mockResolvedValue(null),
findProcessWebDir: vi.fn().mockResolvedValue(null),
rebuildDashboardProductionArtifacts: vi.fn().mockResolvedValue(undefined),
waitForPortFree: vi.fn(),
}));

View File

@ -18,6 +18,11 @@ vi.mock("node:fs", () => ({
existsSync: mockExistsSync,
}));
vi.mock("../../src/lib/dashboard-rebuild.js", () => ({
isInstalledUnderNodeModules: (path: string) =>
path.includes("/node_modules/") || path.includes("\\node_modules\\"),
}));
import { preflight } from "../../src/lib/preflight.js";
beforeEach(() => {
@ -58,9 +63,12 @@ describe("preflight.checkBuilt", () => {
// /web/node_modules/@composio/ao-core — miss
// /node_modules/@composio/ao-core — hit
// /node_modules/@composio/ao-core/dist/index.js — exists
// /web/.next/BUILD_ID and /web/dist-server/start-all.js — exist
mockExistsSync
.mockReturnValueOnce(false)
.mockReturnValueOnce(true)
.mockReturnValueOnce(true)
.mockReturnValueOnce(true)
.mockReturnValueOnce(true);
await expect(preflight.checkBuilt("/web")).resolves.toBeUndefined();
});
@ -88,6 +96,38 @@ describe("preflight.checkBuilt", () => {
"Packages not built",
);
});
it("throws when web production artifacts are missing", async () => {
// findPackageUp finds ao-core, dist/index.js exists, but .next/BUILD_ID missing
mockExistsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
await expect(preflight.checkBuilt("/web")).rejects.toThrow(
"Packages not built",
);
});
it("throws npm hint when web artifacts missing in global install", async () => {
// ao-core found at first check, dist exists, but .next/BUILD_ID missing
mockExistsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
await expect(
preflight.checkBuilt("/usr/local/lib/node_modules/@composio/ao-web"),
).rejects.toThrow("npm install -g @composio/ao@latest");
});
it("throws npm hint when ao-core dist is missing in global install", async () => {
// ao-core found, but dist/index.js missing
mockExistsSync
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
await expect(
preflight.checkBuilt("/usr/local/lib/node_modules/@composio/ao-web"),
).rejects.toThrow("npm install -g @composio/ao@latest");
});
});
describe("preflight.checkTmux", () => {

View File

@ -1,11 +1,16 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import chalk from "chalk";
import type { Command } from "commander";
import { loadConfig } from "@composio/ao-core";
import { findWebDir, buildDashboardEnv, waitForPortAndOpen } from "../lib/web-dir.js";
import { cleanNextCache, findRunningDashboardPid, findProcessWebDir, waitForPortFree } from "../lib/dashboard-rebuild.js";
import {
findRunningDashboardPid,
isInstalledUnderNodeModules,
rebuildDashboardProductionArtifacts,
waitForPortFree,
} from "../lib/dashboard-rebuild.js";
import { preflight } from "../lib/preflight.js";
export function registerDashboard(program: Command): void {
program
@ -14,6 +19,7 @@ export function registerDashboard(program: Command): void {
.option("-p, --port <port>", "Port to listen on")
.option("--no-open", "Don't open browser automatically")
.option("--rebuild", "Clean stale build artifacts and rebuild before starting")
/* c8 ignore start -- process-spawning startup code, tested via integration/onboarding */
.action(async (opts: { port?: string; open?: boolean; rebuild?: boolean }) => {
const config = loadConfig();
const port = opts.port ? parseInt(opts.port, 10) : (config.port ?? 3000);
@ -28,11 +34,9 @@ export function registerDashboard(program: Command): void {
if (opts.rebuild) {
// Check if a dashboard is already running on this port.
const runningPid = await findRunningDashboardPid(port);
const runningWebDir = runningPid ? await findProcessWebDir(runningPid) : null;
const targetWebDir = runningWebDir ?? localWebDir;
if (runningPid) {
// Kill the running server, clean .next, then start fresh below.
// Stop the running server before rebuilding or restarting below.
console.log(
chalk.dim(`Stopping dashboard (PID ${runningPid}) on port ${port}...`),
);
@ -45,8 +49,10 @@ export function registerDashboard(program: Command): void {
await waitForPortFree(port, 5000);
}
await cleanNextCache(targetWebDir);
await rebuildDashboardProductionArtifacts(localWebDir);
// Fall through to start the dashboard on this port.
} else {
await preflight.checkBuilt(localWebDir);
}
const webDir = localWebDir;
@ -60,21 +66,12 @@ export function registerDashboard(program: Command): void {
config.directTerminalPort,
);
// In dev mode (monorepo), use `pnpm run dev` which starts Next.js AND
// the terminal WebSocket servers via concurrently. Without the WS servers,
// the live terminal in the dashboard won't work.
const isDevMode = existsSync(resolve(webDir, "server"));
const child = isDevMode
? spawn("pnpm", ["run", "dev"], {
cwd: webDir,
stdio: ["inherit", "inherit", "pipe"],
env,
})
: spawn("npx", ["next", "dev", "-p", String(port)], {
cwd: webDir,
stdio: ["inherit", "inherit", "pipe"],
env,
});
const startScript = resolve(webDir, "dist-server", "start-all.js");
const child = spawn("node", [startScript], {
cwd: webDir,
stdio: ["inherit", "inherit", "pipe"],
env,
});
const stderrChunks: string[] = [];
@ -108,10 +105,13 @@ export function registerDashboard(program: Command): void {
if (code !== 0 && code !== null && !opts.rebuild) {
const stderr = stderrChunks.join("");
if (looksLikeStaleBuild(stderr)) {
const recoveryCommand = isInstalledUnderNodeModules(webDir)
? "npm install -g @composio/ao@latest"
: "ao dashboard --rebuild";
console.error(
chalk.yellow(
"\nThis looks like a stale build cache issue. Try:\n\n" +
` ${chalk.cyan("ao dashboard --rebuild")}\n`,
` ${chalk.cyan(recoveryCommand)}\n`,
),
);
}
@ -120,6 +120,7 @@ export function registerDashboard(program: Command): void {
process.exit(code ?? 0);
});
});
/* c8 ignore stop */
}
/**

View File

@ -48,7 +48,7 @@ import {
findFreePort,
MAX_PORT_SCAN,
} from "../lib/web-dir.js";
import { cleanNextCache } from "../lib/dashboard-rebuild.js";
import { rebuildDashboardProductionArtifacts } from "../lib/dashboard-rebuild.js";
import { preflight } from "../lib/preflight.js";
import { register, unregister, isAlreadyRunning, getRunning, waitForExit } from "../lib/running-state.js";
import { isHumanCaller } from "../lib/caller-context.js";
@ -749,22 +749,29 @@ export async function createConfigOnly(): Promise<void> {
* Start dashboard server in the background.
* Returns the child process handle for cleanup.
*/
/* c8 ignore start -- process-spawning startup code, tested via integration/onboarding */
async function startDashboard(
port: number,
webDir: string,
configPath: string | null,
terminalPort?: number,
directTerminalPort?: number,
devMode?: boolean,
): Promise<ChildProcess> {
const env = await buildDashboardEnv(port, configPath, terminalPort, directTerminalPort);
// Detect dev vs production: the `server/` source directory only exists in the
// monorepo. Published npm packages only have `dist-server/`.
const isDevMode = existsSync(resolve(webDir, "server"));
// Detect monorepo vs npm install: the `server/` source directory only exists
// in the monorepo. Published npm packages only have `dist-server/`.
const isMonorepo = existsSync(resolve(webDir, "server"));
// In monorepo: use HMR dev server only when --dev is passed explicitly.
// Default is optimized production server for faster loading.
const useDevServer = isMonorepo && devMode === true;
let child: ChildProcess;
if (isDevMode) {
// Monorepo development: use pnpm run dev (tsx, HMR, etc.)
if (useDevServer) {
// Monorepo with --dev: use pnpm run dev (tsx watch, HMR, etc.)
console.log(chalk.dim(" Mode: development (HMR enabled)"));
child = spawn("pnpm", ["run", "dev"], {
cwd: webDir,
stdio: "inherit",
@ -772,8 +779,13 @@ async function startDashboard(
env,
});
} else {
// Production (installed from npm): use pre-built start-all script
child = spawn("node", [resolve(webDir, "dist-server", "start-all.js")], {
// Production: use pre-built start-all script.
if (isMonorepo) {
console.log(chalk.dim(" Mode: optimized (production bundles)"));
console.log(chalk.dim(" Tip: use --dev for hot reload when editing dashboard UI\n"));
}
const startScript = resolve(webDir, "dist-server", "start-all.js");
child = spawn("node", [startScript], {
cwd: webDir,
stdio: "inherit",
detached: false,
@ -782,8 +794,8 @@ async function startDashboard(
}
child.on("error", (err) => {
const cmd = isDevMode ? "pnpm" : "node";
const args = isDevMode ? ["run", "dev"] : [resolve(webDir, "dist-server", "start-all.js")];
const cmd = useDevServer ? "pnpm" : "node";
const args = useDevServer ? ["run", "dev"] : [resolve(webDir, "dist-server", "start-all.js")];
const formatted = formatCommandError(err, {
cmd,
args,
@ -797,6 +809,7 @@ async function startDashboard(
return child;
}
/* c8 ignore stop */
/**
* Ensure tmux is available interactive install with user consent if missing.
@ -899,7 +912,7 @@ async function runStartup(
config: OrchestratorConfig,
projectId: string,
project: ProjectConfig,
opts?: { dashboard?: boolean; orchestrator?: boolean; rebuild?: boolean },
opts?: { dashboard?: boolean; orchestrator?: boolean; rebuild?: boolean; dev?: boolean },
): Promise<number> {
// Ensure tmux is available before doing anything — covers all entry paths
// (normal start, URL start, retry with existing config)
@ -950,10 +963,15 @@ async function runStartup(
port = newPort;
}
const webDir = findWebDir(); // throws with install-specific guidance if not found
await preflight.checkBuilt(webDir);
// Dev mode (HMR) only works in the monorepo where `server/` source exists.
// For npm installs, --dev is silently ignored and production server runs,
// so preflight must still verify production artifacts exist.
const isMonorepo = existsSync(resolve(webDir, "server"));
const willUseDevServer = isMonorepo && opts?.dev === true;
if (opts?.rebuild) {
await cleanNextCache(webDir);
await rebuildDashboardProductionArtifacts(webDir);
} else if (!willUseDevServer) {
await preflight.checkBuilt(webDir);
}
spinner.start("Starting dashboard");
@ -963,6 +981,7 @@ async function runStartup(
config.configPath,
config.terminalPort,
config.directTerminalPort,
opts?.dev,
);
spinner.succeed(`Dashboard starting on http://localhost:${port}`);
console.log(chalk.dim(" (Dashboard will be ready in a few seconds)\n"));
@ -1174,6 +1193,7 @@ export function registerStart(program: Command): void {
.option("--no-dashboard", "Skip starting the dashboard server")
.option("--no-orchestrator", "Skip starting the orchestrator agent")
.option("--rebuild", "Clean and rebuild dashboard before starting")
.option("--dev", "Use Next.js dev server with hot reload (for dashboard UI development)")
.option("--interactive", "Prompt to configure config settings")
.action(
async (
@ -1182,6 +1202,7 @@ export function registerStart(program: Command): void {
dashboard?: boolean;
orchestrator?: boolean;
rebuild?: boolean;
dev?: boolean;
interactive?: boolean;
},
) => {

View File

@ -1,12 +1,33 @@
/**
* Dashboard cache utilities cleans stale .next artifacts and detects
* running dashboard processes.
* Dashboard cache utilities cleans stale .next artifacts, detects
* running dashboard processes, and rebuilds production artifacts.
*/
import { resolve } from "node:path";
import { existsSync, rmSync } from "node:fs";
import ora from "ora";
import { execSilent } from "./shell.js";
import { exec, execSilent } from "./shell.js";
/**
* Check if the web directory is inside a node_modules tree (npm/yarn global install).
* Matches node_modules as a path segment, not just a substring.
*/
export function isInstalledUnderNodeModules(path: string): boolean {
return path.includes("/node_modules/") || path.includes("\\node_modules\\");
}
/**
* Guard: rebuilds are only possible from a source checkout.
* Global npm installs ship prebuilt artifacts and cannot rebuild in place.
*/
export function assertDashboardRebuildSupported(webDir: string): void {
if (isInstalledUnderNodeModules(webDir)) {
throw new Error(
"Dashboard rebuild is only available from a source checkout. " +
"Run `ao update`, or reinstall with `npm install -g @composio/ao@latest`.",
);
}
}
/**
* Find the PID of a process listening on the given port.
@ -21,28 +42,6 @@ export async function findRunningDashboardPid(port: number): Promise<string | nu
return pid;
}
/**
* Find the working directory of a process by PID.
* Returns null if the cwd can't be determined.
*/
export async function findProcessWebDir(pid: string): Promise<string | null> {
const lsofDetail = await execSilent("lsof", ["-p", pid, "-Ffn"]);
if (!lsofDetail) return null;
// lsof -Fn outputs lines like "n/path/to/cwd" — the cwd entry follows "fcwd"
const lines = lsofDetail.split("\n");
for (let i = 0; i < lines.length; i++) {
if (lines[i] === "fcwd" && i + 1 < lines.length && lines[i + 1]?.startsWith("n/")) {
const cwd = lines[i + 1].slice(1);
if (existsSync(resolve(cwd, "package.json"))) {
return cwd;
}
}
}
return null;
}
/**
* Wait for a port to be free (no process listening).
* Throws if the port is still busy after the timeout.
@ -58,9 +57,7 @@ export async function waitForPortFree(port: number, timeoutMs: number): Promise<
}
/**
* Clean just the .next cache directory. Use when a dev server is running
* it will recompile on next request. Does NOT run pnpm build (which would
* create a production .next that the dev server can't use).
* Remove the .next directory before a rebuild.
*/
export async function cleanNextCache(webDir: string): Promise<void> {
const nextDir = resolve(webDir, ".next");
@ -72,3 +69,27 @@ export async function cleanNextCache(webDir: string): Promise<void> {
}
}
/**
* Rebuild dashboard production artifacts (Next.js build + server compilation)
* from a source checkout. Throws if called from an npm global install.
*/
export async function rebuildDashboardProductionArtifacts(webDir: string): Promise<void> {
assertDashboardRebuildSupported(webDir);
await cleanNextCache(webDir);
const workspaceRoot = resolve(webDir, "../..");
const spinner = ora("Rebuilding dashboard production artifacts").start();
try {
await exec("pnpm", ["build"], { cwd: workspaceRoot });
spinner.succeed("Rebuilt dashboard production artifacts");
} catch (error) {
spinner.fail("Dashboard rebuild failed");
throw new Error(
"Failed to rebuild dashboard production artifacts. Run `pnpm build` and try again.",
{ cause: error },
);
}
}

View File

@ -11,6 +11,7 @@ import { existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { isPortAvailable } from "./web-dir.js";
import { exec } from "./shell.js";
import { isInstalledUnderNodeModules } from "./dashboard-rebuild.js";
/**
* Check that the dashboard port is free.
@ -32,16 +33,26 @@ async function checkPort(port: number): Promise<void> {
* installs (hoisted to a parent node_modules).
*/
async function checkBuilt(webDir: string): Promise<void> {
const isNpmInstall = isInstalledUnderNodeModules(webDir);
const corePkgDir = findPackageUp(webDir, "@composio", "ao-core");
if (!corePkgDir) {
const hint = webDir.includes("node_modules")
const hint = isNpmInstall
? "Run: npm install -g @composio/ao@latest"
: "Run: pnpm install && pnpm build";
throw new Error(`Dependencies not installed. ${hint}`);
}
const coreEntry = resolve(corePkgDir, "dist", "index.js");
if (!existsSync(coreEntry)) {
const hint = webDir.includes("node_modules")
const hint = isNpmInstall
? "Run: npm install -g @composio/ao@latest"
: "Run: pnpm build";
throw new Error(`Packages not built. ${hint}`);
}
const webBuildId = resolve(webDir, ".next", "BUILD_ID");
const startAllEntry = resolve(webDir, "dist-server", "start-all.js");
if (!existsSync(webBuildId) || !existsSync(startAllEntry)) {
const hint = isNpmInstall
? "Run: npm install -g @composio/ao@latest"
: "Run: pnpm build";
throw new Error(`Packages not built. ${hint}`);

View File

@ -23,4 +23,11 @@ const nextConfig = {
},
};
export default nextConfig;
// Only load bundle analyzer when ANALYZE=true (dev-only dependency)
let config = nextConfig;
if (process.env.ANALYZE === "true") {
const { default: bundleAnalyzer } = await import("@next/bundle-analyzer");
config = bundleAnalyzer({ enabled: true })(nextConfig);
}
export default config;

View File

@ -19,6 +19,7 @@
"build": "next build && tsc -p tsconfig.server.json",
"start": "next start",
"start:all": "node dist-server/start-all.js",
"dev:optimized": "next build && tsc -p tsconfig.server.json && node dist-server/start-all.js",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
@ -41,6 +42,7 @@
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"server-only": "^0.0.1",
"ws": "^8.19.0",
"xterm": "^5.3.0"
},
@ -48,6 +50,7 @@
"node-pty": "^1.1.0"
},
"devDependencies": {
"@next/bundle-analyzer": "^15.1.0",
"@tailwindcss/postcss": "^4.0.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.1.0",
@ -55,6 +58,7 @@
"@types/react-dom": "^19.0.0",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-v8": "^2.1.0",
"concurrently": "^9.2.1",
"jsdom": "^25.0.0",
"node-gyp": "^12.2.0",
@ -62,7 +66,6 @@
"tailwindcss": "^4.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"@vitest/coverage-v8": "^2.1.0",
"vitest": "^2.1.0"
}
}

View File

@ -0,0 +1,3 @@
// Stub for "server-only" package in vitest — the real module throws when
// imported outside a React Server Component context.
export {};

View File

@ -1,10 +1,22 @@
"use client";
import { DirectTerminal } from "@/components/DirectTerminal";
import { Terminal } from "@/components/Terminal";
import nextDynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { useState, useEffect, Suspense } from "react";
const DirectTerminal = nextDynamic(
() =>
import("@/components/DirectTerminal").then((m) => ({
default: m.DirectTerminal,
})),
{ ssr: false },
);
const Terminal = nextDynamic(
() => import("@/components/Terminal").then((m) => ({ default: m.Terminal })),
{ ssr: false },
);
// Force dynamic rendering (required for useSearchParams)
export const dynamic = "force-dynamic";

View File

@ -7,18 +7,28 @@ vi.mock("next/navigation", () => ({
useSearchParams: () => searchParams,
}));
const MockDirectTerminal = ({
sessionId,
startFullscreen,
}: {
sessionId: string;
startFullscreen: boolean;
}) => (
<div data-testid="direct-terminal">
{sessionId}:{String(startFullscreen)}
</div>
);
vi.mock("@/components/DirectTerminal", () => ({
DirectTerminal: ({
sessionId,
startFullscreen,
}: {
sessionId: string;
startFullscreen: boolean;
}) => (
<div data-testid="direct-terminal">
{sessionId}:{String(startFullscreen)}
</div>
),
DirectTerminal: MockDirectTerminal,
}));
// next/dynamic wraps lazy imports; in tests, bypass the dynamic loader and
// return the mock component directly.
vi.mock("next/dynamic", () => ({
__esModule: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
default: (_loader: any) => MockDirectTerminal,
}));
describe("TestDirectPage", () => {

View File

@ -1,9 +1,17 @@
"use client";
import { DirectTerminal } from "@/components/DirectTerminal";
import nextDynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";
const DirectTerminal = nextDynamic(
() =>
import("@/components/DirectTerminal").then((m) => ({
default: m.DirectTerminal,
})),
{ ssr: false },
);
// Force dynamic rendering (required for useSearchParams)
export const dynamic = "force-dynamic";

View File

@ -6,11 +6,21 @@ import { useMediaQuery, MOBILE_BREAKPOINT } from "@/hooks/useMediaQuery";
import { type DashboardSession, type DashboardPR, isPRMergeReady } from "@/lib/types";
import { CI_STATUS } from "@composio/ao-core/types";
import { cn } from "@/lib/cn";
import dynamic from "next/dynamic";
import { getSessionTitle } from "@/lib/format";
import { CICheckList } from "./CIBadge";
import { DirectTerminal } from "./DirectTerminal";
import { MobileBottomNav } from "./MobileBottomNav";
const DirectTerminal = dynamic(
() => import("./DirectTerminal").then((m) => ({ default: m.DirectTerminal })),
{
ssr: false,
loading: () => (
<div className="h-[440px] animate-pulse rounded bg-[var(--color-bg-primary)]" />
),
},
);
interface OrchestratorZones {
merge: number;
respond: number;

View File

@ -1,3 +1,5 @@
import "server-only";
import { cache } from "react";
import { TERMINAL_STATUSES, type DashboardSession, type DashboardOrchestratorLink } from "@/lib/types";
import { getServices, getSCM } from "@/lib/services";

View File

@ -1,3 +1,5 @@
import "server-only";
import {
createCorrelationId,
createProjectObserver,

View File

@ -1,3 +1,5 @@
import "server-only";
import { cache } from "react";
import { loadConfig } from "@composio/ao-core";

View File

@ -1,3 +1,5 @@
import "server-only";
import {
TERMINAL_STATUSES,
type OrchestratorConfig,

View File

@ -1,3 +1,5 @@
import "server-only";
/**
* Core Session DashboardSession serialization.
*

View File

@ -1,3 +1,5 @@
import "server-only";
/**
* Server-side singleton for core services.
*

View File

@ -50,6 +50,7 @@ export default defineConfig({
find: "@composio/ao-plugin-tracker-linear",
replacement: resolve(__dirname, "../plugins/tracker-linear/src/index.ts"),
},
{ find: "server-only", replacement: resolve(__dirname, "./src/__tests__/server-only-mock.ts") },
{ find: "@", replacement: resolve(__dirname, "./src") },
],
},

View File

@ -617,6 +617,9 @@ importers:
react-dom:
specifier: ^19.0.0
version: 19.2.4(react@19.2.4)
server-only:
specifier: ^0.0.1
version: 0.0.1
ws:
specifier: ^8.19.0
version: 8.19.0
@ -628,6 +631,9 @@ importers:
specifier: ^1.1.0
version: 1.1.0
devDependencies:
'@next/bundle-analyzer':
specifier: ^15.1.0
version: 15.5.14
'@tailwindcss/postcss':
specifier: ^4.0.0
version: 4.1.18
@ -912,6 +918,10 @@ packages:
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
'@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@emnapi/runtime@1.8.1':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
@ -1555,6 +1565,9 @@ packages:
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
'@next/bundle-analyzer@15.5.14':
resolution: {integrity: sha512-Q8rK9QXAqnJ2Ct1XXRJALBccINg9rX0Zl8QSPrYCuL/J1c+k7cfGcRTUNDAP9WIvhYk8JfxZEm1JpcHi+MUKiw==}
'@next/env@15.5.12':
resolution: {integrity: sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==}
@ -1634,6 +1647,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@ -2136,6 +2152,10 @@ packages:
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn-walk@8.3.5:
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
engines: {node: '>=0.4.0'}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
@ -2341,6 +2361,10 @@ packages:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
composio-core@0.5.39:
resolution: {integrity: sha512-7BeSFlfRzr1cbIfGYJW4jQ3BHwaObOaFKiRJIFuWOmvOrTABl1hbxGkWPA3C+uFw9CFXbZhrLWNyD7lhYy2Scg==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
@ -2383,6 +2407,9 @@ packages:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@ -2440,6 +2467,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -2739,6 +2769,10 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
gzip-size@6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
engines: {node: '>=10'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@ -2847,6 +2881,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
@ -3183,6 +3221,10 @@ packages:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -3278,6 +3320,10 @@ packages:
zod:
optional: true
opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -3567,6 +3613,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -3593,6 +3642,10 @@ packages:
simple-wcswidth@1.1.2:
resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==}
sirv@2.0.4:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
engines: {node: '>= 10'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@ -3764,6 +3817,10 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
@ -4024,6 +4081,11 @@ packages:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
webpack-bundle-analyzer@4.10.1:
resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==}
engines: {node: '>= 10.13.0'}
hasBin: true
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
@ -4068,6 +4130,18 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
ws@7.5.10:
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
engines: {node: '>=8.3.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
@ -4478,6 +4552,8 @@ snapshots:
'@csstools/css-tokenizer@3.0.4': {}
'@discoveryjs/json-ext@0.5.7': {}
'@emnapi/runtime@1.8.1':
dependencies:
tslib: 2.8.1
@ -4992,6 +5068,13 @@ snapshots:
globby: 11.1.0
read-yaml-file: 1.1.0
'@next/bundle-analyzer@15.5.14':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@15.5.12': {}
'@next/swc-darwin-arm64@15.5.12':
@ -5049,6 +5132,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@polka/url@1.0.0-next.29': {}
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.57.1':
@ -5584,6 +5669,10 @@ snapshots:
dependencies:
acorn: 8.15.0
acorn-walk@8.3.5:
dependencies:
acorn: 8.15.0
acorn@8.15.0: {}
agent-base@7.1.4: {}
@ -5773,6 +5862,8 @@ snapshots:
commander@13.1.0: {}
commander@7.2.0: {}
composio-core@0.5.39(@ai-sdk/openai@3.0.29(zod@3.25.76))(@cloudflare/workers-types@4.20260214.0)(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(@langchain/openai@1.2.7(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(ws@8.19.0))(ai@6.0.86(zod@3.25.76))(langchain@1.2.24(@langchain/core@1.1.24(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(openai@6.22.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@3.25.76)))(openai@6.22.0(ws@8.19.0)(zod@3.25.76)):
dependencies:
'@ai-sdk/openai': 3.0.29(zod@3.25.76)
@ -5833,6 +5924,8 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
debounce@1.2.1: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
@ -5869,6 +5962,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.286: {}
@ -6222,6 +6317,10 @@ snapshots:
graceful-fs@4.2.11: {}
gzip-size@6.0.0:
dependencies:
duplexer: 0.1.2
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
@ -6309,6 +6408,8 @@ snapshots:
is-number@7.0.0: {}
is-plain-object@5.0.0: {}
is-potential-custom-element-name@1.0.1: {}
is-subdir@1.2.0:
@ -6642,6 +6743,8 @@ snapshots:
mri@1.2.0: {}
mrmime@2.0.1: {}
ms@2.1.3: {}
mustache@4.2.0: {}
@ -6731,6 +6834,8 @@ snapshots:
ws: 8.19.0
zod: 3.25.76
opener@1.5.2: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@ -7005,6 +7110,8 @@ snapshots:
semver@7.7.4: {}
server-only@0.0.1: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.0.0
@ -7051,6 +7158,12 @@ snapshots:
simple-wcswidth@1.1.2: {}
sirv@2.0.4:
dependencies:
'@polka/url': 1.0.0-next.29
mrmime: 2.0.1
totalist: 3.0.1
sisteransi@1.0.5: {}
slash@3.0.0: {}
@ -7199,6 +7312,8 @@ snapshots:
dependencies:
is-number: 7.0.0
totalist@3.0.1: {}
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
@ -7459,6 +7574,25 @@ snapshots:
webidl-conversions@7.0.0: {}
webpack-bundle-analyzer@4.10.1:
dependencies:
'@discoveryjs/json-ext': 0.5.7
acorn: 8.15.0
acorn-walk: 8.3.5
commander: 7.2.0
debounce: 1.2.1
escape-string-regexp: 4.0.0
gzip-size: 6.0.0
html-escaper: 2.0.2
is-plain-object: 5.0.0
opener: 1.5.2
picocolors: 1.1.1
sirv: 2.0.4
ws: 7.5.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
@ -7503,6 +7637,8 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.2
ws@7.5.10: {}
ws@8.19.0: {}
xml-name-validator@5.0.0: {}