Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
tests:
- name: with-js-config-object-style
config: |
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-custom-header',
value: 'js-config-value',
},
{
key: 'x-config-type',
value: 'object',
},
],
},
];
},
};

module.exports = nextConfig;
file: next.config.js
- name: with-js-config-function-style
config: |
/** @type {import('next').NextConfig} */
const nextConfig = (phase, { defaultConfig }) => {
return {
reactStrictMode: true,
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-custom-header',
value: 'js-config-value',
},
{
key: 'x-config-type',
value: 'function',
},
],
},
];
}
};
};

module.exports = nextConfig;
file: next.config.js
- name: with-js-async-function
config: |
// @ts-check

module.exports = async (phase, { defaultConfig }) => {
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-custom-header',
value: 'js-config-value',
},
{
key: 'x-config-type',
value: 'function',
},
],
},
];
}
}
return nextConfig
}
file: next.config.js
- name: with-ts-config
config: |
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-custom-header',
value: 'ts-config-value',
}
],
},
];
}
}

export default nextConfig
file: next.config.ts
- name: with-ecmascript-modules
config: |
// @ts-check

/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
/* config options here */
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-custom-header',
value: 'mjs-config-value',
},
],
},
];
}
}

export default nextConfig
file: next.config.mjs
- name: with-empty-config
config: |
// @ts-check

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

module.exports = nextConfig
file: next.config.js
- name: with-images-unoptimized-false
config: |
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
unoptimized: false,
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-custom-header',
value: 'js-config-value',
},
{
key: 'x-config-type',
value: 'object',
},
],
},
];
},
};

module.exports = nextConfig;
file: next.config.js
- name: with-custom-image-loader
config: |
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
loader: "akamai",
path: "",
},
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'x-custom-header',
value: 'js-config-value',
},
{
key: 'x-config-type',
value: 'object',
},
],
},
];
},
};

module.exports = nextConfig;
file: next.config.js
125 changes: 125 additions & 0 deletions packages/@apphosting/adapter-nextjs/e2e/config-override.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as assert from "assert";
import { posix } from "path";
import fsExtra from "fs-extra";

const host = process.env.HOST;
if (!host) {
throw new Error("HOST environment variable expected");
}

const scenario = process.env.SCENARIO;
if (!scenario) {
throw new Error("SCENARIO environment variable expected");
}

const runId = process.env.RUN_ID;
if (!runId) {
throw new Error("RUN_ID environment variable expected");
}

const compiledFilesPath = posix.join(
process.cwd(),
"e2e",
"runs",
runId,
".next",
"standalone",
".next",
);

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

describe("next.config override", () => {
it("should have images optimization disabled", async function () {
if (
scenario.includes("with-empty-config") ||
scenario.includes("with-images-unoptimized-false") ||
scenario.includes("with-custom-image-loader")
) {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
this.skip();
}

const serverFiles = await fsExtra.readJson(requiredServerFilePath);
const config = serverFiles.config;

// Verify that images.unoptimized is set to true
assert.ok(config.images, "Config should have images property");
assert.strictEqual(
config.images.unoptimized,
true,
"Images should have unoptimized set to true",
);
});

it("should preserve other user set next configs", async function () {
if (scenario.includes("with-empty-config")) {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
this.skip();
}

// This test checks if the user's original config settings are preserved
// We'll check for the custom header that was set in the next.config
const response = await fetch(posix.join(host, "/"));

assert.ok(response.ok);

// Check for the custom header that was set in the next.config
const customHeader = response.headers.get("x-custom-header") ?? "";
const validValues = ["js-config-value", "ts-config-value", "mjs-config-value"];
assert.ok(
validValues.includes(customHeader),
`Expected header to be one of ${validValues.join(", ")} but got "${customHeader}"`,
);
});

it("should handle function-style config correctly", async function () {
// Only run this test for scenarios with function-style config
if (!scenario.includes("function-style")) {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
this.skip();
}

// Check for the custom header that indicates function-style config was processed correctly
const response = await fetch(posix.join(host, "/"));
assert.ok(response.ok);
assert.equal(response.headers.get("x-config-type") ?? "", "function");
});

it("should handle object-style config correctly", async function () {
// Only run this test for scenarios with object-style config
if (!scenario.includes("object-style") && !scenario.includes("with-empty-config")) {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
this.skip();
}

// Check for the custom header that indicates object-style config was processed correctly
const response = await fetch(posix.join(host, "/"));
assert.ok(response.ok);

// Empty config doesn't set this header
if (!scenario.includes("with-empty-config")) {
assert.equal(response.headers.get("x-config-type") ?? "", "object");
}
});

it("should not override images.unoptimized if user explicitly defines configs", async function () {
if (
!scenario.includes("with-images-unoptimized-false") &&
!scenario.includes("with-custom-image-loader")
) {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
this.skip();
}

const serverFiles = await fsExtra.readJson(requiredServerFilePath);
const config = serverFiles.config;

assert.ok(config.images, "Config should have images property");
assert.strictEqual(
config.images.unoptimized,
false,
"Images should have unoptimized set to false",
);
});
});
33 changes: 32 additions & 1 deletion packages/@apphosting/adapter-nextjs/e2e/run-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ interface Scenario {
tests?: string[]; // List of test files to run
}

// Load test data for config override
const configOverrideTestScenarios = parseYaml(
readFileSync(join(__dirname, "config-override-test-cases.yaml"), "utf8"),
).tests;

const scenarios: Scenario[] = [
{
name: "basic",
Expand Down Expand Up @@ -47,6 +52,27 @@ const scenarios: Scenario[] = [
},
tests: ["middleware.spec.ts"], // Only run middleware-specific tests
},
...configOverrideTestScenarios.map(
(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."))
.map((file) => join(cwd, file));

for (const file of configFiles) {
await fsExtra.remove(file);
console.log(`Removed existing config file: ${file}`);
}

await fsExtra.writeFile(join(cwd, scenario.file), configContent);
console.log(`Created ${scenario.file} file with ${scenario.name} config`);
},
tests: ["config-override.spec.ts"],
}),
),
];

const errors: any[] = [];
Expand All @@ -55,7 +81,11 @@ await rmdir(join(__dirname, "runs"), { recursive: true }).catch(() => undefined)

// Run each scenario
for (const scenario of scenarios) {
console.log(`\n\nRunning scenario: ${scenario.name}`);
console.log(
`\n\n${"=".repeat(80)}\n${" ".repeat(
5,
)}RUNNING SCENARIO: ${scenario.name.toUpperCase()}${" ".repeat(5)}\n${"=".repeat(80)}`,
);

const runId = `${scenario.name}-${Math.random().toString().split(".")[1]}`;
const cwd = join(__dirname, "runs", runId);
Expand Down Expand Up @@ -170,6 +200,7 @@ for (const scenario of scenarios) {
...process.env,
HOST: host,
SCENARIO: scenario.name,
RUN_ID: runId,
},
}).finally(() => {
run.stdin.end();
Expand Down
Loading