Skip to content

Restore Next config after build. #364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
27 changes: 26 additions & 1 deletion packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": `{
Expand All @@ -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
Expand All @@ -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": "",
Expand Down Expand Up @@ -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",
Expand All @@ -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": `{
Expand Down Expand Up @@ -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": "",
Expand All @@ -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);
});
Expand Down
22 changes: 14 additions & 8 deletions packages/@apphosting/adapter-nextjs/src/bin/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
} 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";

Check failure on line 12 in packages/@apphosting/adapter-nextjs/src/bin/build.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `·addRouteOverrides,·overrideNextConfig,·restoreNextConfig,·validateNextConfigOverride·` with `⏎··addRouteOverrides,⏎··overrideNextConfig,⏎··restoreNextConfig,⏎··validateNextConfigOverride,⏎`

const root = process.cwd();
const opts = getBuildOptions();
Expand All @@ -19,29 +19,33 @@
// 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();

Check failure on line 44 in packages/@apphosting/adapter-nextjs/src/bin/build.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `⏎`


const adapterMetadata = getAdapterMetadata();
const nextBuildDirectory = join(opts.projectDirectory, originalConfig.distDir);
const nextBuildDirectory = join(opts.projectDirectory, nextConfig.distDir);
const outputBundleOptions = populateOutputBundleOptions(
root,
opts.projectDirectory,
Expand All @@ -50,7 +54,7 @@

await addRouteOverrides(
outputBundleOptions.outputDirectoryAppPath,
originalConfig.distDir,
nextConfig.distDir,
adapterMetadata,
);

Expand All @@ -64,3 +68,5 @@
adapterMetadata,
);
await validateOutputDirectory(outputBundleOptions, nextBuildDirectory);

await restoreNextConfig(root, nextConfig.configFileName);
28 changes: 28 additions & 0 deletions packages/@apphosting/adapter-nextjs/src/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@
}
}

/**
*
*/
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;

Check failure on line 170 in packages/@apphosting/adapter-nextjs/src/overrides.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `⏎`
}

/**
* Modifies the app's route manifest (routes-manifest.json) to add Firebase App Hosting
* specific overrides (i.e headers).
Expand Down
18 changes: 9 additions & 9 deletions packages/@apphosting/adapter-nextjs/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -131,30 +131,30 @@ export async function generateBuildOutput(
): Promise<void> {
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<void> {
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;
Expand Down
Loading