Skip to content

Commit 23a5efc

Browse files
committed
add unit tests and e2e tests to test route overrides
1 parent f0d7595 commit 23a5efc

File tree

8 files changed

+315
-6
lines changed

8 files changed

+315
-6
lines changed

packages/@apphosting/adapter-nextjs/e2e/app.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import * as assert from "assert";
22
import { posix } from "path";
3+
import fsExtra from "fs-extra";
34

45
export const host = process.env.HOST;
56

7+
const { readJson } = fsExtra;
8+
let adapterVersion: string;
9+
10+
before(async () => {
11+
const packageJson = await readJson("package.json");
12+
adapterVersion = packageJson.version;
13+
if (!adapterVersion) throw new Error("couldn't parse package.json version");
14+
});
15+
616
if (!host) {
717
throw new Error("HOST environment variable expected");
818
}
@@ -114,4 +124,25 @@ describe("app", () => {
114124
"private, no-cache, no-store, max-age=0, must-revalidate",
115125
);
116126
});
127+
128+
it("should have x-fah-adapter header on all routes", async () => {
129+
const routes = [
130+
"/",
131+
"/ssg",
132+
"/ssr",
133+
"/ssr/streaming",
134+
"/isr/time",
135+
"/isr/demand",
136+
"/nonexistent-route",
137+
];
138+
139+
for (const route of routes) {
140+
const response = await fetch(posix.join(host, route));
141+
assert.equal(
142+
response.headers.get("x-fah-adapter"),
143+
`nextjs-${adapterVersion}`,
144+
`Route ${route} missing x-fah-adapter header`,
145+
);
146+
}
147+
});
117148
});

packages/@apphosting/adapter-nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"scripts": {
2424
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*",
2525
"test": "npm run test:unit && npm run test:functional",
26-
"test:unit": "ts-mocha -p tsconfig.json src/**/*.spec.ts",
26+
"test:unit": "ts-mocha -p tsconfig.json 'src/**/*.spec.ts' 'src/*.spec.ts'",
2727
"test:functional": "node --loader ts-node/esm ./e2e/run-local.ts",
2828
"localregistry:start": "npx verdaccio --config ../publish-dev/verdaccio-config.yaml",
2929
"localregistry:publish": "(npm view --registry=http://localhost:4873 @apphosting/adapter-nextjs && npm unpublish --@apphosting:registry=http://localhost:4873 --force); npm publish --@apphosting:registry=http://localhost:4873"

packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ metadata:
193193
expectedOutputBundleOptions,
194194
);
195195
});
196+
196197
afterEach(() => {
197198
fs.rmSync(tmpDir, { recursive: true, force: true });
198199
});

packages/@apphosting/adapter-nextjs/src/interfaces.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ export interface RoutesManifest {
7171
localeDetection?: false;
7272
};
7373
}
74-
7574
// The output bundle options are specified here
7675
export interface OutputBundleOptions {
7776
/**
@@ -112,3 +111,36 @@ export interface Metadata extends AdapterMetadata {
112111
framework: string;
113112
frameworkVersion: string;
114113
}
114+
115+
export interface AssetBinding {
116+
filePath: string;
117+
name: string;
118+
}
119+
120+
export interface MiddlewareMatcher {
121+
regexp: string;
122+
locale?: false;
123+
has?: RouteHas[];
124+
missing?: RouteHas[];
125+
originalSource: string;
126+
}
127+
128+
export interface EdgeFunctionDefinition {
129+
files: string[];
130+
name: string;
131+
page: string;
132+
matchers: MiddlewareMatcher[];
133+
wasm?: AssetBinding[];
134+
assets?: AssetBinding[];
135+
regions?: string[] | string;
136+
}
137+
export interface MiddlewareManifest {
138+
version: number;
139+
sortedMiddleware: string[];
140+
middleware: {
141+
[page: string]: EdgeFunctionDefinition;
142+
};
143+
functions: {
144+
[page: string]: EdgeFunctionDefinition;
145+
};
146+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import assert from "assert";
2+
import fs from "fs";
3+
import path from "path";
4+
import os from "os";
5+
import { RoutesManifest, MiddlewareManifest } from "./interfaces.js";
6+
const importOverrides = import("@apphosting/adapter-nextjs/dist/overrides.js");
7+
8+
describe("route overrides", () => {
9+
let tmpDir: string;
10+
let routesManifestPath: string;
11+
let middlewareManifestPath: string;
12+
13+
beforeEach(() => {
14+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-manifests-"));
15+
routesManifestPath = path.join(tmpDir, ".next", "routes-manifest.json");
16+
middlewareManifestPath = path.join(tmpDir, ".next", "server", "middleware-manifest.json");
17+
18+
fs.mkdirSync(path.dirname(routesManifestPath), { recursive: true });
19+
fs.mkdirSync(path.dirname(middlewareManifestPath), { recursive: true });
20+
});
21+
22+
it("should add default fah headers to routes manifest", async () => {
23+
const { addRouteOverrides } = await importOverrides;
24+
const initialManifest: RoutesManifest = {
25+
version: 3,
26+
basePath: "",
27+
pages404: true,
28+
staticRoutes: [],
29+
dynamicRoutes: [],
30+
dataRoutes: [],
31+
headers: [
32+
{
33+
source: "/existing",
34+
headers: [{ key: "X-Custom", value: "test" }],
35+
regex: "^/existing$",
36+
},
37+
],
38+
rewrites: [],
39+
redirects: [],
40+
};
41+
42+
fs.writeFileSync(routesManifestPath, JSON.stringify(initialManifest));
43+
fs.writeFileSync(
44+
middlewareManifestPath,
45+
JSON.stringify({ version: 1, sortedMiddleware: [], middleware: {}, functions: {} }),
46+
);
47+
48+
await addRouteOverrides(tmpDir, ".next", {
49+
adapterPackageName: "@apphosting/adapter-nextjs",
50+
adapterVersion: "1.0.0",
51+
});
52+
53+
const updatedManifest = JSON.parse(
54+
fs.readFileSync(routesManifestPath, "utf-8"),
55+
) as RoutesManifest;
56+
57+
assert.strictEqual(updatedManifest.headers.length, 2);
58+
assert.deepStrictEqual(updatedManifest.headers[0], initialManifest.headers[0]);
59+
60+
const firebaseHeaders = updatedManifest.headers[1];
61+
assert.strictEqual(firebaseHeaders.source, "/:path*");
62+
assert.strictEqual(firebaseHeaders.headers.length, 1);
63+
assert.strictEqual(firebaseHeaders.headers[0].key, "x-fah-adapter");
64+
assert.strictEqual(firebaseHeaders.headers[0].value, "nextjs-1.0.0");
65+
});
66+
67+
it("should add middleware header when middleware exists", async () => {
68+
const { addRouteOverrides } = await importOverrides;
69+
const initialManifest: RoutesManifest = {
70+
version: 3,
71+
basePath: "",
72+
pages404: true,
73+
staticRoutes: [],
74+
dynamicRoutes: [],
75+
dataRoutes: [],
76+
headers: [],
77+
rewrites: [],
78+
redirects: [],
79+
};
80+
81+
const middlewareManifest: MiddlewareManifest = {
82+
version: 1,
83+
sortedMiddleware: ["/"],
84+
middleware: {
85+
"/": {
86+
files: ["middleware.ts"],
87+
name: "middleware",
88+
page: "/",
89+
matchers: [
90+
{
91+
regexp: "^/.*$",
92+
originalSource: "/:path*",
93+
},
94+
],
95+
},
96+
},
97+
functions: {},
98+
};
99+
100+
fs.writeFileSync(routesManifestPath, JSON.stringify(initialManifest));
101+
fs.writeFileSync(middlewareManifestPath, JSON.stringify(middlewareManifest));
102+
103+
await addRouteOverrides(tmpDir, ".next", {
104+
adapterPackageName: "@apphosting/adapter-nextjs",
105+
adapterVersion: "1.0.0",
106+
});
107+
108+
const updatedManifest = JSON.parse(
109+
fs.readFileSync(routesManifestPath, "utf-8"),
110+
) as RoutesManifest;
111+
112+
assert.strictEqual(updatedManifest.headers.length, 1);
113+
114+
const headers = updatedManifest.headers[0];
115+
assert.strictEqual(headers.source, "/:path*");
116+
assert.strictEqual(headers.headers.length, 2);
117+
assert.strictEqual(headers.headers[0].key, "x-fah-adapter");
118+
assert.strictEqual(headers.headers[0].value, "nextjs-1.0.0");
119+
assert.strictEqual(headers.headers[1].key, "x-fah-middleware");
120+
assert.strictEqual(headers.headers[1].value, "true");
121+
});
122+
123+
afterEach(() => {
124+
fs.rmSync(tmpDir, { recursive: true, force: true });
125+
});
126+
});

packages/@apphosting/adapter-nextjs/src/overrides.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { MiddlewareManifest } from "next/dist/build/webpack/plugins/middleware-plugin.js";
2-
import { AdapterMetadata } from "./interfaces.js";
1+
import { AdapterMetadata, MiddlewareManifest } from "./interfaces.js";
32
import { loadRouteManifest, writeRouteManifest, loadMiddlewareManifest } from "./utils.js";
43

54
export async function addRouteOverrides(
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const importUtils = import("@apphosting/adapter-nextjs/dist/utils.js");
2+
import { describe, it, beforeEach, afterEach } from "mocha";
3+
import assert from "assert";
4+
import fs from "fs";
5+
import path from "path";
6+
import os from "os";
7+
import { RoutesManifest, MiddlewareManifest } from "../src/interfaces.js";
8+
9+
describe("manifest utils", () => {
10+
let tmpDir: string;
11+
let distDir: string;
12+
13+
beforeEach(() => {
14+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-manifests-"));
15+
distDir = ".next";
16+
});
17+
18+
it("should load routes manifest", async () => {
19+
const mockRoutesManifest: RoutesManifest = {
20+
version: 3,
21+
basePath: "",
22+
pages404: true,
23+
staticRoutes: [],
24+
dynamicRoutes: [],
25+
dataRoutes: [],
26+
headers: [],
27+
rewrites: [],
28+
redirects: [],
29+
};
30+
31+
const manifestPath = path.join(tmpDir, distDir, "routes-manifest.json");
32+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
33+
fs.writeFileSync(manifestPath, JSON.stringify(mockRoutesManifest));
34+
35+
const { loadRouteManifest } = await importUtils;
36+
const result = loadRouteManifest(tmpDir, distDir);
37+
38+
assert.deepStrictEqual(result, mockRoutesManifest);
39+
});
40+
41+
it("should load middleware manifest", async () => {
42+
const mockMiddleware: MiddlewareManifest = {
43+
version: 1,
44+
sortedMiddleware: ["/"],
45+
functions: {},
46+
middleware: {
47+
"/": {
48+
files: ["middleware.js"],
49+
name: "middleware",
50+
page: "/",
51+
matchers: [
52+
{
53+
regexp: "^(?:\\/(_next\\/data\\/[^/]{1,}))?\\/api\\/([^/.]+)(?:\\/(.*))?",
54+
originalSource: "/api/*",
55+
},
56+
],
57+
},
58+
},
59+
};
60+
61+
const manifestPath = path.join(tmpDir, distDir, "server/middleware-manifest.json");
62+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
63+
fs.writeFileSync(manifestPath, JSON.stringify(mockMiddleware));
64+
65+
const { loadMiddlewareManifest } = await importUtils;
66+
const result = loadMiddlewareManifest(tmpDir, distDir);
67+
68+
assert.deepStrictEqual(result, mockMiddleware);
69+
});
70+
71+
it("should write route manifest", async () => {
72+
const mockManifest: RoutesManifest = {
73+
version: 3,
74+
basePath: "",
75+
pages404: true,
76+
staticRoutes: [],
77+
dynamicRoutes: [],
78+
dataRoutes: [],
79+
headers: [
80+
{
81+
source: "/api/*",
82+
headers: [{ key: "X-Custom", value: "value" }],
83+
regex: "^(?:\\/(_next\\/data\\/[^/]{1,}))?\\/api\\/([^/.]+)(?:\\/(.*))?",
84+
},
85+
],
86+
rewrites: [],
87+
redirects: [],
88+
};
89+
90+
const manifestDir = path.join(tmpDir, distDir);
91+
fs.mkdirSync(manifestDir, { recursive: true });
92+
93+
const { writeRouteManifest } = await importUtils;
94+
await writeRouteManifest(tmpDir, distDir, mockManifest);
95+
96+
const manifestPath = path.join(tmpDir, distDir, "routes-manifest.json");
97+
const written = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
98+
99+
assert.deepStrictEqual(written, mockManifest);
100+
});
101+
102+
it("should throw when loading non-existent route manifest", async () => {
103+
const { loadRouteManifest } = await importUtils;
104+
105+
assert.throws(() => {
106+
loadRouteManifest(tmpDir, distDir);
107+
});
108+
});
109+
110+
it("should throw when loading non-existent middleware manifest", async () => {
111+
const { loadMiddlewareManifest } = await importUtils;
112+
113+
assert.throws(() => {
114+
loadMiddlewareManifest(tmpDir, distDir);
115+
});
116+
});
117+
118+
afterEach(() => {
119+
fs.rmSync(tmpDir, { recursive: true, force: true });
120+
});
121+
});

packages/@apphosting/adapter-nextjs/src/utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { PHASE_PRODUCTION_BUILD, ROUTES_MANIFEST } from "./constants.js";
88
import { OutputBundleOptions, RoutesManifest } from "./interfaces.js";
99
import { NextConfigComplete } from "next/dist/server/config-shared.js";
1010
import { OutputBundleConfig } from "@apphosting/common";
11-
import { AdapterMetadata } from "./interfaces.js";
12-
import { MiddlewareManifest } from "next/dist/build/webpack/plugins/middleware-plugin.js";
11+
import { AdapterMetadata, MiddlewareManifest } from "./interfaces.js";
1312
import { MIDDLEWARE_MANIFEST } from "next/constants.js";
1413

1514
// fs-extra is CJS, readJson can't be imported using shorthand

0 commit comments

Comments
 (0)