diff --git a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts index eb81b226..8fb1177f 100644 --- a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts +++ b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts @@ -32,6 +32,10 @@ describe("build commands", () => { it("expects all output bundle files to be generated", async () => { const { generateBuildOutput, validateOutputDirectory } = await importUtils; const files = { + // .next/standalone/.next/ must be created beforehand otherwise + // generateBuildOutput will attempt to copy + // .next/ into .next/standalone/.next + ".next/standalone/.next/package.json": "", ".next/standalone/server.js": "", ".next/static/staticfile": "", ".next/routes-manifest.json": `{ @@ -53,7 +57,13 @@ describe("build commands", () => { const expectedFiles = { ".next/standalone/.next/static/staticfile": "", + ".next/static/staticfile": "", ".next/standalone/server.js": "", + ".next/routes-manifest.json": `{ + "headers":[], + "rewrites":[], + "redirects":[] + }`, ".apphosting/bundle.yaml": `version: v1 runConfig: runCommand: node .next/standalone/server.js @@ -71,7 +81,7 @@ outputFiles: validateTestFiles(tmpDir, expectedFiles); }); - it("moves files into correct location in a monorepo setup", async () => { + it("copies files into correct location in a monorepo setup", async () => { const { generateBuildOutput } = await importUtils; const files = { ".next/standalone/apps/next-app/standalonefile": "", @@ -113,6 +123,7 @@ outputFiles: const expectedFiles = { ".next/standalone/apps/next-app/.next/static/staticfile": "", ".next/standalone/apps/next-app/standalonefile": "", + ".next/static/staticfile": "", }; const expectedPartialYaml = { version: "v1", @@ -125,6 +136,10 @@ outputFiles: it("test failed validateOutputDirectory", async () => { const { generateBuildOutput, validateOutputDirectory } = await importUtils; const files = { + // .next/standalone/.next/ must be created beforehand otherwise + // generateBuildOutput will attempt to copy + // .next/ into .next/standalone/.next + ".next/standalone/.next/package.json": "", ".next/standalone/notserver.js": "", ".next/static/staticfile": "", ".next/routes-manifest.json": `{ @@ -152,6 +167,10 @@ outputFiles: it("expects directories and other files to be copied over", async () => { const { generateBuildOutput, validateOutputDirectory } = await importUtils; const files = { + // .next/standalone/.next/ must be created beforehand otherwise + // generateBuildOutput will attempt to copy + // .next/ into .next/standalone/.next + ".next/standalone/.next/package.json": "", ".next/standalone/server.js": "", ".next/static/staticfile": "", "public/publicfile": "", @@ -178,9 +197,15 @@ outputFiles: const expectedFiles = { ".next/standalone/.next/static/staticfile": "", + ".next/static/staticfile": "", ".next/standalone/server.js": "", ".next/standalone/public/publicfile": "", ".next/standalone/extrafile": "", + ".next/routes-manifest.json": `{ + "headers":[], + "rewrites":[], + "redirects":[] + }`, }; validateTestFiles(tmpDir, expectedFiles); }); diff --git a/packages/@apphosting/adapter-nextjs/src/bin/build.ts b/packages/@apphosting/adapter-nextjs/src/bin/build.ts index f1cc826c..43b6d844 100644 --- a/packages/@apphosting/adapter-nextjs/src/bin/build.ts +++ b/packages/@apphosting/adapter-nextjs/src/bin/build.ts @@ -9,7 +9,7 @@ import { } from "../utils.js"; import { join } from "path"; import { getBuildOptions, runBuild } from "@apphosting/common"; -import { addRouteOverrides, overrideNextConfig, validateNextConfigOverride } from "../overrides.js"; +import { addRouteOverrides, overrideNextConfig, restoreNextConfig, validateNextConfigOverride } from "../overrides.js"; const root = process.cwd(); const opts = getBuildOptions(); @@ -19,29 +19,33 @@ process.env.NEXT_PRIVATE_STANDALONE = "true"; // Opt-out sending telemetry to Vercel process.env.NEXT_TELEMETRY_DISABLED = "1"; -const originalConfig = await loadConfig(root, opts.projectDirectory); +const nextConfig = await loadConfig(root, opts.projectDirectory); /** * Override user's Next Config to optimize the app for Firebase App Hosting * and validate that the override resulted in a valid config that Next.js can * load. * + * We restore the user's Next Config at the end of the build, after the config file has been + * copied over to the output directory, so that the user's original code is not modified. + * * If the app does not have a next.config.[js|mjs|ts] file in the first place, * then can skip config override. * * Note: loadConfig always returns a fileName (default: next.config.js) even if * one does not exist in the app's root: https://github.com/vercel/next.js/blob/23681508ca34b66a6ef55965c5eac57de20eb67f/packages/next/src/server/config.ts#L1115 */ -const originalConfigPath = join(root, originalConfig.configFileName); -if (await exists(originalConfigPath)) { - await overrideNextConfig(root, originalConfig.configFileName); - await validateNextConfigOverride(root, opts.projectDirectory, originalConfig.configFileName); +const nextConfigPath = join(root, nextConfig.configFileName); +if (await exists(nextConfigPath)) { + await overrideNextConfig(root, nextConfig.configFileName); + await validateNextConfigOverride(root, opts.projectDirectory, nextConfig.configFileName); } await runBuild(); + const adapterMetadata = getAdapterMetadata(); -const nextBuildDirectory = join(opts.projectDirectory, originalConfig.distDir); +const nextBuildDirectory = join(opts.projectDirectory, nextConfig.distDir); const outputBundleOptions = populateOutputBundleOptions( root, opts.projectDirectory, @@ -50,7 +54,7 @@ const outputBundleOptions = populateOutputBundleOptions( await addRouteOverrides( outputBundleOptions.outputDirectoryAppPath, - originalConfig.distDir, + nextConfig.distDir, adapterMetadata, ); @@ -64,3 +68,5 @@ await generateBuildOutput( adapterMetadata, ); await validateOutputDirectory(outputBundleOptions, nextBuildDirectory); + +await restoreNextConfig(root, nextConfig.configFileName); diff --git a/packages/@apphosting/adapter-nextjs/src/overrides.ts b/packages/@apphosting/adapter-nextjs/src/overrides.ts index f8f8e992..143f376c 100644 --- a/packages/@apphosting/adapter-nextjs/src/overrides.ts +++ b/packages/@apphosting/adapter-nextjs/src/overrides.ts @@ -142,6 +142,34 @@ export async function validateNextConfigOverride( } } +/** + * + */ +export async function restoreNextConfig(projectRoot: string, nextConfigFileName: string) { + // Check if the file exists in the current working directory + const configPath = join(projectRoot, nextConfigFileName); + if (!(await exists(configPath))) { + return; + } + + // Determine the file extension + const fileExtension = extname(nextConfigFileName); + const originalConfigPath = join(projectRoot, `next.config.original${fileExtension}`); + if (!(await exists(originalConfigPath))) { + console.warn(`next config may have been overwritten but original contents not found`); + return; + } + console.log(`Restoring original next config in project root`); + + try { + await renamePromise(originalConfigPath, configPath); + } catch (error) { + console.error(`Error restoring Next config: ${error}`); + } + return; + +} + /** * Modifies the app's route manifest (routes-manifest.json) to add Firebase App Hosting * specific overrides (i.e headers). diff --git a/packages/@apphosting/adapter-nextjs/src/utils.ts b/packages/@apphosting/adapter-nextjs/src/utils.ts index aaf5ead8..ed6cbc0a 100644 --- a/packages/@apphosting/adapter-nextjs/src/utils.ts +++ b/packages/@apphosting/adapter-nextjs/src/utils.ts @@ -15,7 +15,7 @@ import { NextConfigComplete } from "next/dist/server/config-shared.js"; import { OutputBundleConfig } from "@apphosting/common"; // fs-extra is CJS, readJson can't be imported using shorthand -export const { move, exists, writeFile, readJson, readdir, readFileSync, existsSync, mkdir } = +export const { copy, exists, writeFile, readJson, readdir, readFileSync, existsSync, mkdir } = fsExtra; // Loads the user's next.config.js file. @@ -116,7 +116,7 @@ export function populateOutputBundleOptions( } /** - * Moves static assets and other resources into the standlone directory, also generates the bundle.yaml + * Copy static assets and other resources into the standlone directory, also generates the bundle.yaml * @param rootDir The root directory of the uploaded source code. * @param outputBundleOptions The target location of built artifacts in the output bundle. * @param nextBuildDirectory The location of the .next directory. @@ -131,30 +131,30 @@ export async function generateBuildOutput( ): Promise { const staticDirectory = join(nextBuildDirectory, "static"); await Promise.all([ - move(staticDirectory, opts.outputStaticDirectoryPath, { overwrite: true }), - moveResources(appDir, opts.outputDirectoryAppPath, opts.bundleYamlPath), + copy(staticDirectory, opts.outputStaticDirectoryPath, { overwrite: true }), + copyResources(appDir, opts.outputDirectoryAppPath, opts.bundleYamlPath), generateBundleYaml(opts, rootDir, nextVersion, adapterMetadata), ]); return; } -// Move all files and directories to apphosting output directory. +// Copy all files and directories to apphosting output directory. // Files are skipped if there is already a file with the same name in the output directory -async function moveResources( +async function copyResources( appDir: string, outputBundleAppDir: string, bundleYamlPath: string, ): Promise { const appDirExists = await exists(appDir); if (!appDirExists) return; - const pathsToMove = await readdir(appDir); - for (const path of pathsToMove) { + const pathsToCopy = await readdir(appDir); + for (const path of pathsToCopy) { const isbundleYamlDir = join(appDir, path) === dirname(bundleYamlPath); const existsInOutputBundle = await exists(join(outputBundleAppDir, path)); // Keep apphosting.yaml files in the root directory still, as later steps expect them to be there const isApphostingYaml = path === "apphosting_preprocessed" || path === "apphosting.yaml"; if (!isbundleYamlDir && !existsInOutputBundle && !isApphostingYaml) { - await move(join(appDir, path), join(outputBundleAppDir, path)); + await copy(join(appDir, path), join(outputBundleAppDir, path)); } } return;