Skip to content

Set App Hosting overrides for NextJS apps without a Next Config #323

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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