Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
46e9f48
add function to override next config
mathu97 Mar 3, 2025
e4c1966
implement next config override logic to import in existing config and…
mathu97 Mar 3, 2025
7924e7b
add unit tests for next config overrides
mathu97 Mar 4, 2025
b670497
address some comments
mathu97 Mar 5, 2025
af99265
fix test
mathu97 Mar 5, 2025
c6626f9
use rename instead of read/write
mathu97 Mar 6, 2025
4a8401a
progress - e2e tests
mathu97 Mar 6, 2025
074d6a0
mostly working e2e tests
mathu97 Mar 6, 2025
8b72920
support async next configs
mathu97 Mar 6, 2025
141d46e
debug
mathu97 Mar 6, 2025
1e98f4f
fix up e2e tests
mathu97 Mar 6, 2025
70e1bd8
add additional tests and fix lint errors
mathu97 Mar 6, 2025
442a69a
fix lint
mathu97 Mar 6, 2025
cc64e3d
some debug logs
mathu97 Mar 6, 2025
2d6718a
Merge remote-tracking branch 'origin/main' into feat/override-next-co…
mathu97 Mar 6, 2025
560f0d8
remove console logs
mathu97 Mar 7, 2025
5a1ce3c
set debuggability for e2e test
mathu97 Mar 7, 2025
9d6ee74
remove usage of libraries to fix e2e
mathu97 Mar 7, 2025
be9d381
fix e2e
mathu97 Mar 7, 2025
9797602
add jsdocs
mathu97 Mar 7, 2025
2bdecf0
bump version
mathu97 Mar 8, 2025
85cea6e
address pr comments
mathu97 Mar 8, 2025
fff3505
fix errors
mathu97 Mar 8, 2025
a2dc2eb
add validate next config override function
mathu97 Mar 8, 2025
bd62abe
just loadconfig again instead of checking anything else
mathu97 Mar 8, 2025
726342e
fix breaking tests
mathu97 Mar 10, 2025
d7ce5da
add validation that override worked
mathu97 Mar 10, 2025
ec84af3
fix tests again
mathu97 Mar 10, 2025
7055350
add overrides validation unit tests
mathu97 Mar 10, 2025
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
7 changes: 4 additions & 3 deletions packages/@apphosting/adapter-nextjs/src/bin/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "../utils.js";
import { join } from "path";
import { getBuildOptions, runBuild } from "@apphosting/common";
import { addRouteOverrides } from "../overrides.js";
import { addRouteOverrides, overrideNextConfig } from "../overrides.js";

const root = process.cwd();
const opts = getBuildOptions();
Expand All @@ -20,11 +20,12 @@ process.env.NEXT_TELEMETRY_DISABLED = "1";
if (!process.env.FRAMEWORK_VERSION) {
throw new Error("Could not find the nextjs version of the application");
}

const { distDir, configFileName } = await loadConfig(root, opts.projectDirectory);
await overrideNextConfig(root, configFileName);
await runBuild();

const adapterMetadata = getAdapterMetadata();

const { distDir } = await loadConfig(root, opts.projectDirectory);
const nextBuildDirectory = join(opts.projectDirectory, distDir);
const outputBundleOptions = populateOutputBundleOptions(
root,
Expand Down
206 changes: 206 additions & 0 deletions packages/@apphosting/adapter-nextjs/src/overrides.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,209 @@ describe("route overrides", () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
});

describe("next config overrides", () => {
let tmpDir: string;

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

it("should set images.unoptimized to true - js normal config", async () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
// @ts-check

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

module.exports = nextConfig
`;

fs.writeFileSync(path.join(tmpDir, "next.config.js"), originalConfig);
await overrideNextConfig(tmpDir, "next.config.js");

const updatedConfig = fs.readFileSync(path.join(tmpDir, "next.config.js"), "utf-8");

assert.equal(
normalizeWhitespace(updatedConfig),
normalizeWhitespace(`
const originalConfig = require('./next.config.original.js');

// This file was automatically generated by Firebase App Hosting adapter
const config = typeof originalConfig === 'function'
? (...args) => {
const resolvedConfig = originalConfig(...args);
return {
...resolvedConfig,
images: {
...(resolvedConfig.images || {}),
unoptimized: true,
},
};
}
: {
...originalConfig,
images: {
...(originalConfig.images || {}),
unoptimized: true,
},
};

module.exports = config;
`),
);
});

it("should set images.unoptimized to true - ECMAScript Modules", async () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
// @ts-check

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

export default nextConfig
`;

fs.writeFileSync(path.join(tmpDir, "next.config.mjs"), originalConfig);
await overrideNextConfig(tmpDir, "next.config.mjs");

const updatedConfig = fs.readFileSync(path.join(tmpDir, "next.config.mjs"), "utf-8");
assert.equal(
normalizeWhitespace(updatedConfig),
normalizeWhitespace(`
import originalConfig from './next.config.original.mjs';

// This file was automatically generated by Firebase App Hosting adapter
const config = typeof originalConfig === 'function'
? (...args) => {
const resolvedConfig = originalConfig(...args);
return {
...resolvedConfig,
images: {
...(resolvedConfig.images || {}),
unoptimized: true,
},
};
}
: {
...originalConfig,
images: {
...(originalConfig.images || {}),
unoptimized: true,
},
};

export default config;
`),
);
});

it("should set images.unoptimized to true - ECMAScript Function", async () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
// @ts-check

export default (phase, { defaultConfig }) => {
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
/* config options here */
}
return nextConfig
}
`;

fs.writeFileSync(path.join(tmpDir, "next.config.mjs"), originalConfig);
await overrideNextConfig(tmpDir, "next.config.mjs");

const updatedConfig = fs.readFileSync(path.join(tmpDir, "next.config.mjs"), "utf-8");
assert.equal(
normalizeWhitespace(updatedConfig),
normalizeWhitespace(`
import originalConfig from './next.config.original.mjs';

// This file was automatically generated by Firebase App Hosting adapter
const config = typeof originalConfig === 'function'
? (...args) => {
const resolvedConfig = originalConfig(...args);
return {
...resolvedConfig,
images: {
...(resolvedConfig.images || {}),
unoptimized: true,
},
};
}
: {
...originalConfig,
images: {
...(originalConfig.images || {}),
unoptimized: true,
},
};

export default config;
`),
);
});

it("should set images.unoptimized to true - TypeScript", async () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
/* config options here */
}

export default nextConfig
`;

fs.writeFileSync(path.join(tmpDir, "next.config.ts"), originalConfig);
await overrideNextConfig(tmpDir, "next.config.ts");

const updatedConfig = fs.readFileSync(path.join(tmpDir, "next.config.ts"), "utf-8");
assert.equal(
normalizeWhitespace(updatedConfig),
normalizeWhitespace(`
import originalConfig from './next.config.original';

// This file was automatically generated by Firebase App Hosting adapter
const config = typeof originalConfig === 'function'
? (...args) => {
const resolvedConfig = originalConfig(...args);
return {
...resolvedConfig,
images: {
...(resolvedConfig.images || {}),
unoptimized: true,
},
};
}
: {
...originalConfig,
images: {
...(originalConfig.images || {}),
unoptimized: true,
},
};

module.exports = config;
`),
);
});
});

// Normalize whitespace for comparison
function normalizeWhitespace(str: string) {
return str.replace(/\s+/g, " ").trim();
}
80 changes: 77 additions & 3 deletions packages/@apphosting/adapter-nextjs/src/overrides.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
import { AdapterMetadata, MiddlewareManifest } from "./interfaces.js";
import { loadRouteManifest, writeRouteManifest, loadMiddlewareManifest } from "./utils.js";
import {
loadRouteManifest,
writeRouteManifest,
loadMiddlewareManifest,
exists,
writeFile,
} from "./utils.js";
import { join } from "path";
import { readFileSync } from "fs";

export async function overrideNextConfig(projectRoot: string, nextConfigFileName: string) {
// Check if the file exists in the current working directory
const configPath = join(projectRoot, nextConfigFileName);

if (!(await exists(configPath))) {
console.log(`No Next.js config file found at ${configPath}`);
return;
}

// Determine the file extension
const fileExtension = nextConfigFileName.split(".").pop() || "js";
const originalConfigName = `next.config.original.${fileExtension}`;
const newConfigName = `next.config.${fileExtension}`;

// Rename the original config file
try {
const originalContent = readFileSync(configPath, "utf-8");
await writeFile(join(projectRoot, originalConfigName), originalContent);

// Create a new config file with the appropriate import
let importStatement;
if (fileExtension === "js" || fileExtension === "cjs") {
importStatement = `const originalConfig = require('./${originalConfigName}');`;
} else if (fileExtension === "mjs") {
importStatement = `import originalConfig from './${originalConfigName}';`;
} else if (fileExtension === "ts") {
importStatement = `import originalConfig from './${originalConfigName.replace(".ts", "")}';`;
} else {
throw new Error(`Unsupported file extension: ${fileExtension}`);
}

// Create the new config content with our overrides
const newConfigContent = `${importStatement}

// This file was automatically generated by Firebase App Hosting adapter
const config = typeof originalConfig === 'function'
? (...args) => {
const resolvedConfig = originalConfig(...args);
return {
...resolvedConfig,
images: {
...(resolvedConfig.images || {}),
unoptimized: true,
},
};
}
: {
...originalConfig,
images: {
...(originalConfig.images || {}),
unoptimized: true,
},
};

${fileExtension === "mjs" ? "export default config;" : "module.exports = config;"}
`;

// Write the new config file
await writeFile(join(projectRoot, newConfigName), newConfigContent);
console.log(`Successfully created ${newConfigName} with Firebase App Hosting overrides`);
} catch (error) {
console.error(`Error overriding Next.js config: ${error}`);
throw error;
}
}

/**
* Modifies the app's route manifest (routes-manifest.json) to add Firebase App Hosting
Expand Down Expand Up @@ -36,8 +110,8 @@ export async function addRouteOverrides(
]
: []),
],
/*
NextJs converts the source string to a regex using path-to-regexp (https://github.com/pillarjs/path-to-regexp) at
/*
NextJs converts the source string to a regex using path-to-regexp (https://github.com/pillarjs/path-to-regexp) at
build time: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/index.ts#L1273.
This regex is then used to match the route against the request path.

Expand Down