Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions packages/stack-cli/scripts/copy-runtime-assets.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { execFileSync } from "child_process";
import { chmodSync, cpSync, existsSync, mkdirSync, rmSync } from "fs";
import { dirname, join, resolve } from "path";
import { chmodSync, cpSync, existsSync, mkdirSync, readlinkSync, readdirSync, rmSync } from "fs";
import { dirname, join, relative, resolve } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -39,6 +39,57 @@ function copyEmulatorAssets() {
console.log(`Copied emulator assets into ${emulatorDist} (+ .env.development into ${distDir}).`);
}

function shouldCopyDashboardFile(path) {
return existsSync(path);
}

function copyDashboardSymlinkTarget(src, dest) {
rmSync(dest, { recursive: true, force: true });
cpSync(src, dest, { recursive: true, dereference: true, filter: shouldCopyDashboardFile });
}

function splitDashboardPath(root, path) {
return relative(root, path).split(/[\\/]+/);
}

function getDashboardDependencyName(pnpmRoot, path) {
const parts = splitDashboardPath(pnpmRoot, path);
const nodeModulesIndex = parts.lastIndexOf("node_modules");
if (nodeModulesIndex < 0) {
return undefined;
}
const dependencyParts = parts.slice(nodeModulesIndex + 1);
if (dependencyParts.length === 1) {
return dependencyParts[0];
}
if (dependencyParts.length === 2 && dependencyParts[0].startsWith("@")) {
return join(dependencyParts[0], dependencyParts[1]);
}
return undefined;
}

function copyDashboardHoistedDependencies(pnpmRoot, current = pnpmRoot) {
Copy link
Copy Markdown

@vercel vercel Bot May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copyDashboardHoistedDependencies function now properly checks if the directory exists before calling readdirSync() ## Bug Explanation (FIXED)

The copyDashboardHoistedDependencies function at line 72 was calling readdirSync(current) without first checking if the directory exists. The function is invoked on line 110 with the path join(dashboardStandaloneSrc, "node_modules/.pnpm"), which may not exist in all build scenarios.

When readdirSync() is called on a non-existent directory, it throws an ENOENT (Error: no such file or directory) error, causing the entire build process to fail. This can happen when there are no hoisted dependencies in the dashboard standalone build or the .pnpm directory wasn't created during the Next.js build process.

Fix Explanation

An early return check has been added at the beginning of the copyDashboardHoistedDependencies function:

if (!existsSync(current)) {
  return;
}

This check gracefully handles the case where the directory doesn't exist by simply returning without attempting to read its contents. The existsSync function is already imported from the fs module. This allows the build to continue successfully even when the .pnpm directory is absent, which is a valid and expected scenario in some build configurations.

Fix on Vercel

for (const entry of readdirSync(current, { withFileTypes: true })) {
const path = join(current, entry.name);
if (entry.isDirectory()) {
copyDashboardHoistedDependencies(pnpmRoot, path);
continue;
}
if (!entry.isSymbolicLink() || !existsSync(path)) {
continue;
}
const dependencyName = getDashboardDependencyName(pnpmRoot, path);
if (dependencyName == null) {
continue;
}
const target = resolve(current, readlinkSync(path));
Comment thread
vercel[bot] marked this conversation as resolved.
const parts = splitDashboardPath(pnpmRoot, path);
if (parts[0] !== "node_modules" && existsSync(join(target, "package.json"))) {
copyDashboardSymlinkTarget(target, join(dashboardDist, "node_modules", dependencyName));
}
}
}

function copyDashboardAssets() {
assertExists(
join(dashboardStandaloneSrc, "apps/dashboard/server.js"),
Expand All @@ -50,11 +101,12 @@ function copyDashboardAssets() {
);

rmSync(dashboardDist, { recursive: true, force: true });
cpSync(dashboardStandaloneSrc, dashboardDist, { recursive: true });
cpSync(dashboardStandaloneSrc, dashboardDist, { recursive: true, dereference: true, filter: shouldCopyDashboardFile });
cpSync(dashboardStaticSrc, join(dashboardDist, "apps/dashboard/.next/static"), { recursive: true });
if (existsSync(dashboardPublicSrc)) {
cpSync(dashboardPublicSrc, join(dashboardDist, "apps/dashboard/public"), { recursive: true });
}
copyDashboardHoistedDependencies(join(dashboardStandaloneSrc, "node_modules/.pnpm"));

console.log(`Copied dashboard standalone runtime into ${dashboardDist}.`);
}
Expand Down
Loading