Skip to content

Commit c02e7b3

Browse files
authored
Next Config Override (#309)
1 parent 6190079 commit c02e7b3

File tree

8 files changed

+749
-11
lines changed

8 files changed

+749
-11
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
tests:
2+
- name: with-js-config-object-style
3+
config: |
4+
/** @type {import('next').NextConfig} */
5+
const nextConfig = {
6+
reactStrictMode: true,
7+
async headers() {
8+
return [
9+
{
10+
source: '/:path*',
11+
headers: [
12+
{
13+
key: 'x-custom-header',
14+
value: 'js-config-value',
15+
},
16+
{
17+
key: 'x-config-type',
18+
value: 'object',
19+
},
20+
],
21+
},
22+
];
23+
},
24+
};
25+
26+
module.exports = nextConfig;
27+
file: next.config.js
28+
- name: with-js-config-function-style
29+
config: |
30+
/** @type {import('next').NextConfig} */
31+
const nextConfig = (phase, { defaultConfig }) => {
32+
return {
33+
reactStrictMode: true,
34+
async headers() {
35+
return [
36+
{
37+
source: '/:path*',
38+
headers: [
39+
{
40+
key: 'x-custom-header',
41+
value: 'js-config-value',
42+
},
43+
{
44+
key: 'x-config-type',
45+
value: 'function',
46+
},
47+
],
48+
},
49+
];
50+
}
51+
};
52+
};
53+
54+
module.exports = nextConfig;
55+
file: next.config.js
56+
- name: with-js-async-function
57+
config: |
58+
// @ts-check
59+
60+
module.exports = async (phase, { defaultConfig }) => {
61+
/**
62+
* @type {import('next').NextConfig}
63+
*/
64+
const nextConfig = {
65+
async headers() {
66+
return [
67+
{
68+
source: '/:path*',
69+
headers: [
70+
{
71+
key: 'x-custom-header',
72+
value: 'js-config-value',
73+
},
74+
{
75+
key: 'x-config-type',
76+
value: 'function',
77+
},
78+
],
79+
},
80+
];
81+
}
82+
}
83+
return nextConfig
84+
}
85+
file: next.config.js
86+
- name: with-ts-config
87+
config: |
88+
import type { NextConfig } from 'next'
89+
90+
const nextConfig: NextConfig = {
91+
async headers() {
92+
return [
93+
{
94+
source: '/:path*',
95+
headers: [
96+
{
97+
key: 'x-custom-header',
98+
value: 'ts-config-value',
99+
}
100+
],
101+
},
102+
];
103+
}
104+
}
105+
106+
export default nextConfig
107+
file: next.config.ts
108+
- name: with-ecmascript-modules
109+
config: |
110+
// @ts-check
111+
112+
/**
113+
* @type {import('next').NextConfig}
114+
*/
115+
const nextConfig = {
116+
/* config options here */
117+
async headers() {
118+
return [
119+
{
120+
source: '/:path*',
121+
headers: [
122+
{
123+
key: 'x-custom-header',
124+
value: 'mjs-config-value',
125+
},
126+
],
127+
},
128+
];
129+
}
130+
}
131+
132+
export default nextConfig
133+
file: next.config.mjs
134+
- name: with-empty-config
135+
config: |
136+
// @ts-check
137+
138+
/** @type {import('next').NextConfig} */
139+
const nextConfig = {
140+
/* config options here */
141+
}
142+
143+
module.exports = nextConfig
144+
file: next.config.js
145+
- name: with-images-unoptimized-false
146+
config: |
147+
/** @type {import('next').NextConfig} */
148+
const nextConfig = {
149+
reactStrictMode: true,
150+
images: {
151+
unoptimized: false,
152+
},
153+
async headers() {
154+
return [
155+
{
156+
source: '/:path*',
157+
headers: [
158+
{
159+
key: 'x-custom-header',
160+
value: 'js-config-value',
161+
},
162+
{
163+
key: 'x-config-type',
164+
value: 'object',
165+
},
166+
],
167+
},
168+
];
169+
},
170+
};
171+
172+
module.exports = nextConfig;
173+
file: next.config.js
174+
- name: with-custom-image-loader
175+
config: |
176+
/** @type {import('next').NextConfig} */
177+
const nextConfig = {
178+
images: {
179+
loader: "akamai",
180+
path: "",
181+
},
182+
async headers() {
183+
return [
184+
{
185+
source: '/:path*',
186+
headers: [
187+
{
188+
key: 'x-custom-header',
189+
value: 'js-config-value',
190+
},
191+
{
192+
key: 'x-config-type',
193+
value: 'object',
194+
},
195+
],
196+
},
197+
];
198+
},
199+
};
200+
201+
module.exports = nextConfig;
202+
file: next.config.js
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as assert from "assert";
2+
import { posix } from "path";
3+
import fsExtra from "fs-extra";
4+
5+
const host = process.env.HOST;
6+
if (!host) {
7+
throw new Error("HOST environment variable expected");
8+
}
9+
10+
const scenario = process.env.SCENARIO;
11+
if (!scenario) {
12+
throw new Error("SCENARIO environment variable expected");
13+
}
14+
15+
const runId = process.env.RUN_ID;
16+
if (!runId) {
17+
throw new Error("RUN_ID environment variable expected");
18+
}
19+
20+
const compiledFilesPath = posix.join(
21+
process.cwd(),
22+
"e2e",
23+
"runs",
24+
runId,
25+
".next",
26+
"standalone",
27+
".next",
28+
);
29+
30+
const requiredServerFilePath = posix.join(compiledFilesPath, "required-server-files.json");
31+
32+
describe("next.config override", () => {
33+
it("should have images optimization disabled", async function () {
34+
if (
35+
scenario.includes("with-empty-config") ||
36+
scenario.includes("with-images-unoptimized-false") ||
37+
scenario.includes("with-custom-image-loader")
38+
) {
39+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
40+
this.skip();
41+
}
42+
43+
const serverFiles = await fsExtra.readJson(requiredServerFilePath);
44+
const config = serverFiles.config;
45+
46+
// Verify that images.unoptimized is set to true
47+
assert.ok(config.images, "Config should have images property");
48+
assert.strictEqual(
49+
config.images.unoptimized,
50+
true,
51+
"Images should have unoptimized set to true",
52+
);
53+
});
54+
55+
it("should preserve other user set next configs", async function () {
56+
if (scenario.includes("with-empty-config")) {
57+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
58+
this.skip();
59+
}
60+
61+
// This test checks if the user's original config settings are preserved
62+
// We'll check for the custom header that was set in the next.config
63+
const response = await fetch(posix.join(host, "/"));
64+
65+
assert.ok(response.ok);
66+
67+
// Check for the custom header that was set in the next.config
68+
const customHeader = response.headers.get("x-custom-header") ?? "";
69+
const validValues = ["js-config-value", "ts-config-value", "mjs-config-value"];
70+
assert.ok(
71+
validValues.includes(customHeader),
72+
`Expected header to be one of ${validValues.join(", ")} but got "${customHeader}"`,
73+
);
74+
});
75+
76+
it("should handle function-style config correctly", async function () {
77+
// Only run this test for scenarios with function-style config
78+
if (!scenario.includes("function-style")) {
79+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
80+
this.skip();
81+
}
82+
83+
// Check for the custom header that indicates function-style config was processed correctly
84+
const response = await fetch(posix.join(host, "/"));
85+
assert.ok(response.ok);
86+
assert.equal(response.headers.get("x-config-type") ?? "", "function");
87+
});
88+
89+
it("should handle object-style config correctly", async function () {
90+
// Only run this test for scenarios with object-style config
91+
if (!scenario.includes("object-style") && !scenario.includes("with-empty-config")) {
92+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
93+
this.skip();
94+
}
95+
96+
// Check for the custom header that indicates object-style config was processed correctly
97+
const response = await fetch(posix.join(host, "/"));
98+
assert.ok(response.ok);
99+
100+
// Empty config doesn't set this header
101+
if (!scenario.includes("with-empty-config")) {
102+
assert.equal(response.headers.get("x-config-type") ?? "", "object");
103+
}
104+
});
105+
106+
it("should not override images.unoptimized if user explicitly defines configs", async function () {
107+
if (
108+
!scenario.includes("with-images-unoptimized-false") &&
109+
!scenario.includes("with-custom-image-loader")
110+
) {
111+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
112+
this.skip();
113+
}
114+
115+
const serverFiles = await fsExtra.readJson(requiredServerFilePath);
116+
const config = serverFiles.config;
117+
118+
assert.ok(config.images, "Config should have images property");
119+
assert.strictEqual(
120+
config.images.unoptimized,
121+
false,
122+
"Images should have unoptimized set to false",
123+
);
124+
});
125+
});

packages/@apphosting/adapter-nextjs/e2e/run-local.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ interface Scenario {
1919
tests?: string[]; // List of test files to run
2020
}
2121

22+
// Load test data for config override
23+
const configOverrideTestScenarios = parseYaml(
24+
readFileSync(join(__dirname, "config-override-test-cases.yaml"), "utf8"),
25+
).tests;
26+
2227
const scenarios: Scenario[] = [
2328
{
2429
name: "basic",
@@ -47,6 +52,27 @@ const scenarios: Scenario[] = [
4752
},
4853
tests: ["middleware.spec.ts"], // Only run middleware-specific tests
4954
},
55+
...configOverrideTestScenarios.map(
56+
(scenario: { name: string; config: string; file: string }) => ({
57+
name: scenario.name,
58+
setup: async (cwd: string) => {
59+
const configContent = scenario.config;
60+
const files = await fsExtra.readdir(cwd);
61+
const configFiles = files
62+
.filter((file) => file.startsWith("next.config."))
63+
.map((file) => join(cwd, file));
64+
65+
for (const file of configFiles) {
66+
await fsExtra.remove(file);
67+
console.log(`Removed existing config file: ${file}`);
68+
}
69+
70+
await fsExtra.writeFile(join(cwd, scenario.file), configContent);
71+
console.log(`Created ${scenario.file} file with ${scenario.name} config`);
72+
},
73+
tests: ["config-override.spec.ts"],
74+
}),
75+
),
5076
];
5177

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

5682
// Run each scenario
5783
for (const scenario of scenarios) {
58-
console.log(`\n\nRunning scenario: ${scenario.name}`);
84+
console.log(
85+
`\n\n${"=".repeat(80)}\n${" ".repeat(
86+
5,
87+
)}RUNNING SCENARIO: ${scenario.name.toUpperCase()}${" ".repeat(5)}\n${"=".repeat(80)}`,
88+
);
5989

6090
const runId = `${scenario.name}-${Math.random().toString().split(".")[1]}`;
6191
const cwd = join(__dirname, "runs", runId);
@@ -170,6 +200,7 @@ for (const scenario of scenarios) {
170200
...process.env,
171201
HOST: host,
172202
SCENARIO: scenario.name,
203+
RUN_ID: runId,
173204
},
174205
}).finally(() => {
175206
run.stdin.end();

0 commit comments

Comments
 (0)