Skip to content

gitignore .apphosting #374

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 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as assert from "assert";
import { posix } from "path";
import { posix, join } from "path";

Check failure on line 2 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

'join' is defined but never used
import fsExtra from "fs-extra";


Check failure on line 5 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `⏎`
const host = process.env.HOST;
if (!host) {
throw new Error("HOST environment variable expected");
Expand All @@ -27,9 +28,47 @@
".next",
);

const standalonePath = posix.join(

Check failure on line 31 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `⏎··process.cwd(),⏎··"e2e",⏎··"runs",⏎··runId,⏎··".next",⏎··"standalone",⏎` with `process.cwd(),·"e2e",·"runs",·runId,·".next",·"standalone"`
process.cwd(),
"e2e",
"runs",
runId,
".next",
"standalone",
);

const appPath = posix.join(

Check failure on line 40 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `⏎··process.cwd(),⏎··"e2e",⏎··"runs",⏎··runId,⏎` with `process.cwd(),·"e2e",·"runs",·runId`
process.cwd(),
"e2e",
"runs",
runId,
);

const requiredServerFilePath = posix.join(compiledFilesPath, "required-server-files.json");

describe("next.config override", () => {
it("Should not overwrite original next config", async function () {
if (
scenario.includes("with-empty-config") ||
scenario.includes("with-images-unoptimized-false") ||
scenario.includes("with-custom-image-loader")
) {
this.skip();

Check failure on line 56 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected 'this'
}
const files = await fsExtra.readdir(appPath);
const configRegex = /^next\.config\..*$/g;
const configOriginalRegex = /^next\.config\.(?!original).*$/g;
const configFiles = files

Check failure on line 61 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `⏎······.filter((file)·=>··` with `.filter((file)·=>`
.filter((file) => file.match(configRegex));
assert.strictEqual(configFiles.length, 1);
assert.ok(configFiles[0].match(configOriginalRegex), "found original config file in root");

const standaloneFiles = await fsExtra.readdir(standalonePath);
const standaloneConfigFiles = standaloneFiles

Check failure on line 67 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Delete `⏎······`
.filter((file) => file.match(configRegex));
assert.strictEqual(standaloneConfigFiles.length, 2);
assert.ok(configFiles.some((file) => file.match(configOriginalRegex)), "no original config found in standalone");

Check failure on line 70 in packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `configFiles.some((file)·=>·file.match(configOriginalRegex)),·"no·original·config·found·in·standalone"` with `⏎······configFiles.some((file)·=>·file.match(configOriginalRegex)),⏎······"no·original·config·found·in·standalone",⏎····`
});
it("should have images optimization disabled", async function () {
if (
scenario.includes("with-empty-config") ||
Expand Down
66 changes: 37 additions & 29 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,48 +19,56 @@
// 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();
try {
await runBuild();

Check failure on line 45 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 outputBundleOptions = populateOutputBundleOptions(
root,
opts.projectDirectory,
nextBuildDirectory,
);
const adapterMetadata = getAdapterMetadata();
const nextBuildDirectory = join(opts.projectDirectory, nextConfig.distDir);
const outputBundleOptions = populateOutputBundleOptions(
root,
opts.projectDirectory,
nextBuildDirectory,
);

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

const nextjsVersion = process.env.FRAMEWORK_VERSION || "unspecified";
await generateBuildOutput(
root,
opts.projectDirectory,
outputBundleOptions,
nextBuildDirectory,
nextjsVersion,
adapterMetadata,
);
await validateOutputDirectory(outputBundleOptions, nextBuildDirectory);
const nextjsVersion = process.env.FRAMEWORK_VERSION || "unspecified";
await generateBuildOutput(
root,
opts.projectDirectory,
outputBundleOptions,
nextBuildDirectory,
nextjsVersion,
adapterMetadata,
);
await validateOutputDirectory(outputBundleOptions, nextBuildDirectory);
} finally {
await restoreNextConfig(root, nextConfig.configFileName);
}
61 changes: 61 additions & 0 deletions packages/@apphosting/adapter-nextjs/src/overrides.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,67 @@ describe("validateNextConfigOverride", () => {
});
});

describe("next config restore", () => {
let tmpDir: string;
const nextConfigOriginalBody = `
// @ts-check

/** @type {import('next').NextConfig} */
const nextConfig = {
/* config options here */
}

module.exports = nextConfig
`;
const nextConfigBody = `
// This file was automatically generated by Firebase App Hosting adapter
const fahOptimizedConfig = (config) => ({
...config,
images: {
...(config.images || {}),
...(config.images?.unoptimized === undefined && config.images?.loader === undefined
? { unoptimized: true }
: {}),
},
});

const config = typeof originalConfig === 'function'
? async (...args) => {
const resolvedConfig = await originalConfig(...args);
return fahOptimizedConfig(resolvedConfig);
}
: fahOptimizedConfig(originalConfig);
`;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-overrides"));
});

it("handle no original config file found", async () => {
const { restoreNextConfig } = await importOverrides;
fs.writeFileSync(path.join(tmpDir, "next.config.mjs"), nextConfigBody);
await restoreNextConfig(tmpDir, "next.config.mjs");

const restoredConfig = fs.readFileSync(path.join(tmpDir, "next.config.mjs"), "utf-8");
assert.equal(restoredConfig, nextConfigBody);
});

it("handle no config file found", async () => {
const { restoreNextConfig } = await importOverrides;
assert.doesNotReject(restoreNextConfig(tmpDir, "next.config.mjs"));
});

it("original config file restored", async () => {
const { restoreNextConfig } = await importOverrides;
fs.writeFileSync(path.join(tmpDir, "next.config.mjs"), nextConfigBody);
fs.writeFileSync(path.join(tmpDir, "next.config.original.mjs"), nextConfigOriginalBody);
await restoreNextConfig(tmpDir, "next.config.mjs");

const restoredConfig = fs.readFileSync(path.join(tmpDir, "next.config.mjs"), "utf-8");
assert.equal(restoredConfig, nextConfigOriginalBody);
});
});

// Normalize whitespace for comparison
function normalizeWhitespace(str: string) {
return str.replace(/\s+/g, " ").trim();
Expand Down
29 changes: 29 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,35 @@ export async function validateNextConfigOverride(
}
}

/**
* Restores the user's original Next Config file (next.config.original.[ts|js|mjs])
* to leave user code the way we found it.
*/
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).
Expand Down
7 changes: 4 additions & 3 deletions packages/@apphosting/adapter-nextjs/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
MiddlewareManifest,
} from "./interfaces.js";
import { NextConfigComplete } from "next/dist/server/config-shared.js";
import { OutputBundleConfig } from "@apphosting/common";
import { OutputBundleConfig, UpdateOrCreateGitignore } from "@apphosting/common";

// fs-extra is CJS, readJson can't be imported using shorthand
export const { copy, exists, writeFile, readJson, readdir, readFileSync, existsSync, mkdir } =
export const { copy, exists, writeFile, readJson, readdir, readFileSync, existsSync, ensureDir } =
fsExtra;

// Loads the user's next.config.js file.
Expand Down Expand Up @@ -181,7 +181,7 @@ async function generateBundleYaml(
nextVersion: string,
adapterMetadata: AdapterMetadata,
): Promise<void> {
await mkdir(opts.outputDirectoryBasePath);
await ensureDir(opts.outputDirectoryBasePath);
const outputBundle: OutputBundleConfig = {
version: "v1",
runConfig: {
Expand All @@ -203,6 +203,7 @@ async function generateBundleYaml(
}

await writeFile(opts.bundleYamlPath, yamlStringify(outputBundle));
UpdateOrCreateGitignore(cwd, ["/.apphosting/"]);
return;
}

Expand Down
27 changes: 27 additions & 0 deletions packages/@apphosting/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { spawn } from "child_process";
import * as path from "node:path";
import * as fs from "fs-extra";


// Output bundle metadata specifications to be written to bundle.yaml
export interface OutputBundleConfig {
Expand Down Expand Up @@ -139,3 +142,27 @@ export function getBuildOptions(): BuildOptions {
projectDirectory: process.cwd(),
};
}


/**
* Updates or creates a .gitignore file with the given entries in the given path
*/
export function UpdateOrCreateGitignore(dirPath: string, entries: string[]) {
const gitignorePath = path.join(dirPath, ".gitignore");

if (!fs.existsSync(gitignorePath)) {
console.log(`creating ${gitignorePath} with entries: ${entries.join("\n")}`);
fs.writeFileSync(gitignorePath, entries.join("\n"));
return;
}

let content = fs.readFileSync(gitignorePath, "utf-8");
for (const entry of entries) {
if (!content.includes(entry)) {
console.log(`adding ${entry} to ${gitignorePath}`);
content += `\n${entry}\n`;
}
}

fs.writeFileSync(gitignorePath, content);
}
Loading