fix(build): include assets in standalone output
With output: "standalone", next build writes server.js under .next/standalone but leaves generated static/public assets in the project root, so starting the standalone server directly (e.g. via PM2) 404s on JS/CSS/font/favicon requests and /login stays stuck loading. Add a postbuild step that copies .next/static and public into the standalone directory, skipping the workspace-traced CLI build which already copies its own assets.
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
"dev": "next dev --port 20127",
|
||||
"dev:webpack": "next dev --webpack --port 20127",
|
||||
"build": "next build --webpack",
|
||||
"postbuild": "node scripts/copy-standalone-assets.mjs",
|
||||
"postbuild:bun": "node scripts/copy-standalone-assets.mjs",
|
||||
"start": "next start --port 20127",
|
||||
"dev:bun": "bun --bun next dev --webpack --port 20127",
|
||||
"build:bun": "bun --bun next build --webpack",
|
||||
|
||||
36
scripts/copy-standalone-assets.mjs
Normal file
36
scripts/copy-standalone-assets.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
import { cpSync, existsSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
export function copyStandaloneAssets({ projectRoot = process.cwd(), distDir = process.env.NEXT_DIST_DIR || ".next" } = {}) {
|
||||
if (process.env.NEXT_TRACING_ROOT_MODE === "workspace") {
|
||||
console.log("[standalone-assets] Skipping workspace-traced CLI build; CLI packaging handles assets");
|
||||
return;
|
||||
}
|
||||
|
||||
const buildDir = resolve(projectRoot, distDir);
|
||||
const standaloneDir = resolve(buildDir, "standalone");
|
||||
|
||||
if (!existsSync(standaloneDir)) {
|
||||
console.log(`[standalone-assets] No standalone build found at ${standaloneDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const staticSource = resolve(buildDir, "static");
|
||||
const staticDestination = resolve(standaloneDir, distDir, "static");
|
||||
if (existsSync(staticSource)) {
|
||||
cpSync(staticSource, staticDestination, { recursive: true, force: true });
|
||||
console.log(`[standalone-assets] Copied static assets to ${staticDestination}`);
|
||||
}
|
||||
|
||||
const publicSource = resolve(projectRoot, "public");
|
||||
const publicDestination = resolve(standaloneDir, "public");
|
||||
if (existsSync(publicSource)) {
|
||||
cpSync(publicSource, publicDestination, { recursive: true, force: true });
|
||||
console.log(`[standalone-assets] Copied public assets to ${publicDestination}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === resolve(dirname(fileURLToPath(import.meta.url)), "copy-standalone-assets.mjs")) {
|
||||
copyStandaloneAssets();
|
||||
}
|
||||
55
tests/unit/standalone-assets.test.js
Normal file
55
tests/unit/standalone-assets.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { copyStandaloneAssets } from "../../scripts/copy-standalone-assets.mjs";
|
||||
|
||||
function createBuildFixture(distDir) {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), "9router-standalone-assets-"));
|
||||
const buildRoot = join(projectRoot, distDir);
|
||||
mkdirSync(join(buildRoot, "standalone"), { recursive: true });
|
||||
mkdirSync(join(buildRoot, "static", "chunks"), { recursive: true });
|
||||
mkdirSync(join(projectRoot, "public"), { recursive: true });
|
||||
writeFileSync(join(buildRoot, "static", "chunks", "app.js"), "static asset");
|
||||
writeFileSync(join(projectRoot, "public", "favicon.svg"), "public asset");
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
describe("standalone build assets", () => {
|
||||
it("copies static and public assets into the default standalone layout", () => {
|
||||
const projectRoot = createBuildFixture(".next");
|
||||
|
||||
copyStandaloneAssets({ projectRoot, distDir: ".next" });
|
||||
|
||||
expect(readFileSync(join(projectRoot, ".next", "standalone", ".next", "static", "chunks", "app.js"), "utf8"))
|
||||
.toBe("static asset");
|
||||
expect(readFileSync(join(projectRoot, ".next", "standalone", "public", "favicon.svg"), "utf8"))
|
||||
.toBe("public asset");
|
||||
});
|
||||
|
||||
it("uses a custom Next dist directory", () => {
|
||||
const projectRoot = createBuildFixture(".next-cli-build");
|
||||
|
||||
copyStandaloneAssets({ projectRoot, distDir: ".next-cli-build" });
|
||||
|
||||
expect(readFileSync(join(projectRoot, ".next-cli-build", "standalone", ".next-cli-build", "static", "chunks", "app.js"), "utf8"))
|
||||
.toBe("static asset");
|
||||
});
|
||||
|
||||
it("does not modify workspace-traced CLI builds", () => {
|
||||
const projectRoot = createBuildFixture(".next-cli-build");
|
||||
const previousMode = process.env.NEXT_TRACING_ROOT_MODE;
|
||||
process.env.NEXT_TRACING_ROOT_MODE = "workspace";
|
||||
|
||||
try {
|
||||
copyStandaloneAssets({ projectRoot, distDir: ".next-cli-build" });
|
||||
} finally {
|
||||
if (previousMode === undefined) delete process.env.NEXT_TRACING_ROOT_MODE;
|
||||
else process.env.NEXT_TRACING_ROOT_MODE = previousMode;
|
||||
}
|
||||
|
||||
expect(() => readFileSync(join(projectRoot, ".next-cli-build", "standalone", ".next-cli-build", "static", "chunks", "app.js")))
|
||||
.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user