diff --git a/package.json b/package.json index fbedad57..4f5f8f95 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/copy-standalone-assets.mjs b/scripts/copy-standalone-assets.mjs new file mode 100644 index 00000000..bfaf6e0d --- /dev/null +++ b/scripts/copy-standalone-assets.mjs @@ -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(); +} diff --git a/tests/unit/standalone-assets.test.js b/tests/unit/standalone-assets.test.js new file mode 100644 index 00000000..94951325 --- /dev/null +++ b/tests/unit/standalone-assets.test.js @@ -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(); + }); +});