Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,4 @@ tests:

module.exports = nextConfig;
file: next.config.js
- name: without-a-next-config
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const compiledFilesPath = posix.join(
const requiredServerFilePath = posix.join(compiledFilesPath, "required-server-files.json");

describe("next.config override", () => {
it("should have images optimization disabled", async function () {
it("should have image optimization disabled", async function () {
if (
scenario.includes("with-empty-config") ||
scenario.includes("with-images-unoptimized-false") ||
Expand All @@ -53,7 +53,7 @@ describe("next.config override", () => {
});

it("should preserve other user set next configs", async function () {
if (scenario.includes("with-empty-config")) {
if (scenario.includes("with-empty-config") || scenario.includes("without-a-next-config")) {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
this.skip();
}
Expand Down
9 changes: 7 additions & 2 deletions packages/@apphosting/adapter-nextjs/e2e/run-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@ const scenarios: Scenario[] = [
tests: ["middleware.spec.ts"], // Only run middleware-specific tests
},
...configOverrideTestScenarios.map(
(scenario: { name: string; config: string; file: string }) => ({
(scenario: { name: string; config?: string; file?: string }) => ({
name: scenario.name,
setup: async (cwd: string) => {
const configContent = scenario.config;
const files = await fsExtra.readdir(cwd);
const configFiles = files
.filter((file) => file.startsWith("next.config."))
Expand All @@ -67,6 +66,12 @@ const scenarios: Scenario[] = [
console.log(`Removed existing config file: ${file}`);
}

// skip creating the test config if data is not provided
if (!scenario.config || !scenario.file) {
return;
}

const configContent = scenario.config;
await fsExtra.writeFile(join(cwd, scenario.file), configContent);
console.log(`Created ${scenario.file} file with ${scenario.name} config`);
},
Expand Down
15 changes: 9 additions & 6 deletions packages/@apphosting/adapter-nextjs/src/bin/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ const originalConfig = await loadConfig(root, opts.projectDirectory);
* load.
*
* If the app does not have a next.config.[js|mjs|ts] file in the first place,
* then can skip config override.
* then one is created with the overrides.
*
* 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 userNextConfigExists = await exists(join(root, originalConfig.configFileName));
await overrideNextConfig(root, originalConfig.configFileName, userNextConfigExists);
await validateNextConfigOverride(
root,
opts.projectDirectory,
originalConfig.configFileName,
userNextConfigExists,
);

await runBuild();

Expand Down
118 changes: 77 additions & 41 deletions packages/@apphosting/adapter-nextjs/src/overrides.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,20 +172,32 @@ describe("next config overrides", () => {
...config,
images: {
...(config.images || {}),
...(config.images?.unoptimized === undefined && config.images?.loader === undefined
? { unoptimized: true }
...(config.images?.unoptimized === undefined && config.images?.loader === undefined
? { unoptimized: true }
: {}),
},
});

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

const defaultNextConfig = `
// @ts-nocheck

/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
unoptimized: true,
}
}

module.exports = nextConfig
`;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-overrides"));
});
Expand All @@ -194,17 +206,17 @@ describe("next config overrides", () => {
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");
await overrideNextConfig(tmpDir, "next.config.js", /* userNextConfigExists = */ true);

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

Expand All @@ -213,7 +225,7 @@ describe("next config overrides", () => {
normalizeWhitespace(`
// @ts-nocheck
const originalConfig = require('./next.config.original.js');

${nextConfigOverrideBody}

module.exports = config;
Expand All @@ -225,19 +237,19 @@ describe("next config overrides", () => {
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");
await overrideNextConfig(tmpDir, "next.config.mjs", /* userNextConfigExists = */ true);

const updatedConfig = fs.readFileSync(path.join(tmpDir, "next.config.mjs"), "utf-8");
assert.equal(
Expand All @@ -257,7 +269,7 @@ describe("next config overrides", () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
// @ts-check

export default (phase, { defaultConfig }) => {
/**
* @type {import('next').NextConfig}
Expand All @@ -270,7 +282,7 @@ describe("next config overrides", () => {
`;

fs.writeFileSync(path.join(tmpDir, "next.config.mjs"), originalConfig);
await overrideNextConfig(tmpDir, "next.config.mjs");
await overrideNextConfig(tmpDir, "next.config.mjs", /* userNextConfigExists = */ true);

const updatedConfig = fs.readFileSync(path.join(tmpDir, "next.config.mjs"), "utf-8");
assert.equal(
Expand All @@ -280,7 +292,7 @@ describe("next config overrides", () => {
import originalConfig from './next.config.original.mjs';

${nextConfigOverrideBody}

export default config;
`),
);
Expand All @@ -290,59 +302,56 @@ describe("next config overrides", () => {
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");
await overrideNextConfig(tmpDir, "next.config.ts", /* userNextConfigExists = */ true);

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

${nextConfigOverrideBody}

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

it("should not do anything if no next.config.* file exists", async () => {
it("should create a default next.config.js file if one does not exist yet", async () => {
const { overrideNextConfig } = await importOverrides;
await overrideNextConfig(tmpDir, "next.config.js");

// Assert that no next.config* files were created
const files = fs.readdirSync(tmpDir);
const nextConfigFiles = files.filter((file) => file.startsWith("next.config"));
assert.strictEqual(nextConfigFiles.length, 0, "No next.config files should exist");
await overrideNextConfig(tmpDir, "next.config.js", /* userNextConfigExists = */ false);
const updatedConfig = fs.readFileSync(path.join(tmpDir, "next.config.js"), "utf-8");
assert.equal(normalizeWhitespace(updatedConfig), normalizeWhitespace(defaultNextConfig));
});
});

describe("validateNextConfigOverride", () => {
let tmpDir: string;
let root: string;
let projectRoot: string;
let originalConfigFileName: string;
let newConfigFileName: string;
let originalConfigPath: string;
let newConfigPath: string;
let configFileName: string;
let configFilePath: string;
let preservedConfigFileName: string;
let preservedConfigFilePath: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-next-config-override"));
root = tmpDir;
projectRoot = tmpDir;
originalConfigFileName = "next.config.js";
newConfigFileName = "next.config.original.js";
originalConfigPath = path.join(root, originalConfigFileName);
newConfigPath = path.join(root, newConfigFileName);
configFileName = "next.config.js";
configFilePath = path.join(root, configFileName);
preservedConfigFileName = "next.config.original.js";
preservedConfigFilePath = path.join(root, preservedConfigFileName);

fs.mkdirSync(root, { recursive: true });
});
Expand All @@ -351,25 +360,52 @@ describe("validateNextConfigOverride", () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it("should throw an error when new config file doesn't exist", async () => {
fs.writeFileSync(originalConfigPath, "module.exports = {}");
it("should throw an error if a next config file was not created because the user did not have one", async () => {
const { validateNextConfigOverride } = await importOverrides;

await assert.rejects(
async () =>
await validateNextConfigOverride(
root,
projectRoot,
configFileName,
/* userNextConfigExists = */ false,
),
/Next.js config file not found/,
);
});

it("should throw an error when main config file doesn't exist", async () => {
fs.writeFileSync(preservedConfigFilePath, "module.exports = {}");

const { validateNextConfigOverride } = await importOverrides;

await assert.rejects(
async () => await validateNextConfigOverride(root, projectRoot, originalConfigFileName),
/New Next.js config file not found/,
async () =>
await validateNextConfigOverride(
root,
projectRoot,
configFileName,
/* userNextConfigExists = */ true,
),
/Next Config Override Failed: Next.js config file not found/,
);
});

it("should throw an error when original config file doesn't exist", async () => {
fs.writeFileSync(newConfigPath, "module.exports = {}");
it("should throw an error when preserveed config file doesn't exist", async () => {
fs.writeFileSync(configFilePath, "module.exports = {}");

const { validateNextConfigOverride } = await importOverrides;

await assert.rejects(
async () => await validateNextConfigOverride(root, projectRoot, originalConfigFileName),
/Original Next.js config file not found/,
async () =>
await validateNextConfigOverride(
root,
projectRoot,
configFileName,
/* userNextConfigExists = */ true,
),
/User's original Next.js config file not preserved/,
);
});
});
Expand Down
Loading