Skip to content

Commit d40954f

Browse files
committed
Middleware working again
1 parent 49efdcc commit d40954f

File tree

5 files changed

+130
-42
lines changed

5 files changed

+130
-42
lines changed

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

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ describe("build commands", () => {
5050
tmpDir,
5151
outputBundleOptions,
5252
path.join(tmpDir, ".next"),
53-
defaultNextVersion,
54-
adapterMetadata,
5553
);
5654
await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next"));
5755

@@ -116,8 +114,6 @@ outputFiles:
116114
serverFilePath: path.join(tmpDir, ".next", "standalone", "apps", "next-app", "server.js"),
117115
},
118116
path.join(tmpDir, ".next"),
119-
defaultNextVersion,
120-
adapterMetadata,
121117
);
122118

123119
const expectedFiles = {
@@ -154,11 +150,6 @@ outputFiles:
154150
tmpDir,
155151
outputBundleOptions,
156152
path.join(tmpDir, ".next"),
157-
defaultNextVersion,
158-
{
159-
adapterPackageName: "@apphosting/adapter-nextjs",
160-
adapterVersion: "14.0.1",
161-
},
162153
);
163154
assert.rejects(
164155
async () => await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next")),
@@ -184,8 +175,6 @@ outputFiles:
184175
serverFilePath: path.join(standaloneAppPath, "server.js"),
185176
},
186177
path.join(tmpDir, ".next"),
187-
defaultNextVersion,
188-
adapterMetadata,
189178
);
190179

191180
const expectedFiles = {
@@ -214,8 +203,6 @@ outputFiles:
214203
tmpDir,
215204
outputBundleOptions,
216205
path.join(tmpDir, ".next"),
217-
defaultNextVersion,
218-
adapterMetadata,
219206
);
220207
await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next"));
221208

@@ -240,11 +227,6 @@ outputFiles:
240227
tmpDir,
241228
outputBundleOptions,
242229
path.join(tmpDir, ".next"),
243-
defaultNextVersion,
244-
{
245-
adapterPackageName: "@apphosting/adapter-nextjs",
246-
adapterVersion: "14.0.1",
247-
},
248230
);
249231
await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next"));
250232

@@ -276,8 +258,6 @@ outputFiles:
276258
tmpDir,
277259
outputBundleOptions,
278260
path.join(tmpDir, ".next"),
279-
defaultNextVersion,
280-
adapterMetadata,
281261
);
282262
await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next"));
283263

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
#! /usr/bin/env node
2-
import { runBuild } from "@apphosting/common";
2+
import { getBuildOptions, runBuild } from "@apphosting/common";
3+
import { generateBuildOutput, getAdapterMetadata, loadConfig, populateOutputBundleOptions, validateOutputDirectory } from "../utils.js";
4+
import { join } from "node:path";
35

46
// Opt-out sending telemetry to Vercel
57
process.env.NEXT_TELEMETRY_DISABLED = "1";
68

79
await runBuild();
10+
11+
const opts = getBuildOptions();
12+
const root = process.cwd();
13+
14+
const nextConfig = await loadConfig(root, opts.projectDirectory);
15+
16+
const nextBuildDirectory = join(opts.projectDirectory, nextConfig.distDir);
17+
const outputBundleOptions = populateOutputBundleOptions(
18+
root,
19+
opts.projectDirectory,
20+
nextBuildDirectory,
21+
);
22+
23+
await generateBuildOutput(
24+
root,
25+
opts.projectDirectory,
26+
outputBundleOptions,
27+
nextBuildDirectory,
28+
);
29+
30+
await validateOutputDirectory(outputBundleOptions, nextBuildDirectory);

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

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import { generateBuildOutput, getAdapterMetadata, populateOutputBundleOptions, validateOutputDirectory } from "./utils.js";
2-
import { getBuildOptions } from "@apphosting/common/dist/index.js";
1+
import { generateBundleYaml, getAdapterMetadata, populateOutputBundleOptions } from "./utils.js";
32
import type { NextAdapter } from "next";
3+
import { addRouteOverrides } from "./overrides.js";
4+
import { PHASE_PRODUCTION_BUILD } from "./constants.js";
45

56
const adapter: NextAdapter = {
67
name: '@apphosting/adapter-nextjs',
78
// FEEDBACK: we need to be able to override user-defined config, before defaults injected
89
// it would be nice if this where a separate phase or callback
910
async modifyConfig(config, { phase }) {
10-
if (phase === 'phase-production-build') {
11+
if (phase === PHASE_PRODUCTION_BUILD) {
1112
return {
1213
...config,
1314
images: {
@@ -42,34 +43,28 @@ const adapter: NextAdapter = {
4243
return config;
4344
},
4445
async onBuildComplete(context) {
46+
4547
const nextBuildDirectory = context.distDir;
48+
49+
if (context.outputs.middleware?.config?.matchers) {
50+
await addRouteOverrides(nextBuildDirectory, context.outputs.middleware.config.matchers);
51+
}
52+
53+
// TODO standalone is not bundled yet...
4654
const outputBundleOptions = populateOutputBundleOptions(
4755
context.repoRoot,
4856
context.projectDir,
4957
nextBuildDirectory,
5058
);
5159

52-
if (context.outputs.middleware) {
53-
throw new Error("Next.js middleware is not supported with the experimental App Hosting adapter.");
54-
}
55-
5660
const adapterMetadata = getAdapterMetadata();
5761

5862
const root = process.cwd();
59-
const opts = getBuildOptions();
6063

6164
const nextjsVersion = process.env.FRAMEWORK_VERSION || context.nextVersion || "unspecified";
6265

63-
await generateBuildOutput(
64-
root,
65-
opts.projectDirectory,
66-
outputBundleOptions,
67-
nextBuildDirectory,
68-
nextjsVersion,
69-
adapterMetadata,
70-
);
66+
await generateBundleYaml(outputBundleOptions, root, nextjsVersion, adapterMetadata);
7167

72-
await validateOutputDirectory(outputBundleOptions, nextBuildDirectory);
7368
},
7469
};
7570

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { MiddlewareMatcher } from "next/dist/build/analysis/get-page-static-info.js";
2+
import {
3+
loadRouteManifest,
4+
writeRouteManifest,
5+
} from "./utils.js";
6+
7+
/**
8+
* Modifies the app's route manifest (routes-manifest.json) to add Firebase App Hosting
9+
* specific overrides (i.e headers).
10+
*
11+
* It adds the following headers to all routes:
12+
* - x-fah-adapter: The Firebase App Hosting adapter version used to build the app.
13+
*
14+
* It also adds the following headers to all routes for which middleware is enabled:
15+
* - x-fah-middleware: When middleware is enabled.
16+
* @param appPath The path to the app directory.
17+
* @param distDir The path to the dist directory.
18+
* @param adapterMetadata The adapter metadata.
19+
*/
20+
export async function addRouteOverrides(
21+
distDir: string,
22+
middlewareMatchers: MiddlewareMatcher[],
23+
) {
24+
const routeManifest = loadRouteManifest(distDir);
25+
26+
middlewareMatchers.forEach((matcher) => {
27+
routeManifest.headers.push({
28+
source: matcher.originalSource,
29+
headers: [
30+
{
31+
key: "x-fah-middleware",
32+
value: "true",
33+
},
34+
],
35+
regex: matcher.regexp,
36+
});
37+
});
38+
39+
await writeRouteManifest(distDir, routeManifest);
40+
}

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

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import fsExtra from "fs-extra";
22
import { join, dirname, relative, normalize } from "path";
33
import { stringify as yamlStringify } from "yaml";
4+
import { createRequire } from "node:module";
45

56
import {
67
OutputBundleOptions,
78
AdapterMetadata,
9+
RoutesManifest,
810
} from "./interfaces.js";
911
import { OutputBundleConfig, updateOrCreateGitignore } from "@apphosting/common";
1012
import { fileURLToPath } from "url";
1113

14+
import { PHASE_PRODUCTION_BUILD, ROUTES_MANIFEST } from "./constants.js";
15+
import type { NextConfigComplete } from "next/dist/server/config-shared.js";
16+
1217
// fs-extra is CJS, readJson can't be imported using shorthand
1318
export const { copy, exists, writeFile, readJson, readdir, readFileSync, existsSync, ensureDir } =
1419
fsExtra;
@@ -19,6 +24,27 @@ export const isMain = (meta: ImportMeta): boolean => {
1924
return process.argv[1] === fileURLToPath(meta.url);
2025
};
2126

27+
// Loads the user's next.config.js file.
28+
export async function loadConfig(root: string, projectRoot: string): Promise<NextConfigComplete> {
29+
// createRequire() gives us access to Node's CommonJS implementation of require.resolve()
30+
// (https://nodejs.org/api/module.html#modulecreaterequirefilename).
31+
// We use the require.resolve() resolution algorithm to get the path to the next config module,
32+
// which may reside in the node_modules folder at a higher level in the directory structure
33+
// (e.g. for monorepo projects).
34+
// Note that ESM has an equivalent (https://nodejs.org/api/esm.html#importmetaresolvespecifier),
35+
// but the feature is still experimental.
36+
const require = createRequire(import.meta.url);
37+
const configPath = require.resolve("next/dist/server/config.js", { paths: [projectRoot] });
38+
// dynamically load NextJS so this can be used in an NPX context
39+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
40+
const { default: nextServerConfig }: { default: typeof import("next/dist/server/config.js") } =
41+
await import(configPath);
42+
43+
const loadConfig = nextServerConfig.default;
44+
return await loadConfig(PHASE_PRODUCTION_BUILD, root);
45+
}
46+
47+
2248
/**
2349
* Provides the paths in the output bundle for the built artifacts.
2450
* @param rootDir The root directory of the uploaded source code.
@@ -48,6 +74,33 @@ export function populateOutputBundleOptions(
4874
};
4975
}
5076

77+
/**
78+
* Loads the route manifest from the standalone directory.
79+
* @param standalonePath The path to the standalone directory.
80+
* @param distDir The path to the dist directory.
81+
* @return The route manifest.
82+
*/
83+
export function loadRouteManifest(distDir: string): RoutesManifest {
84+
const manifestPath = join(distDir, ROUTES_MANIFEST);
85+
const json = readFileSync(manifestPath, "utf-8");
86+
return JSON.parse(json) as RoutesManifest;
87+
}
88+
89+
90+
/**
91+
* Writes the route manifest to the standalone directory.
92+
* @param standalonePath The path to the standalone directory.
93+
* @param distDir The path to the dist directory.
94+
* @param customManifest The route manifest to write.
95+
*/
96+
export async function writeRouteManifest(
97+
distDir: string,
98+
customManifest: RoutesManifest,
99+
): Promise<void> {
100+
const manifestPath = join(distDir, ROUTES_MANIFEST);
101+
await writeFile(manifestPath, JSON.stringify(customManifest));
102+
}
103+
51104
/**
52105
* Copy static assets and other resources into the standlone directory, also generates the bundle.yaml
53106
* @param rootDir The root directory of the uploaded source code.
@@ -59,14 +112,11 @@ export async function generateBuildOutput(
59112
appDir: string,
60113
opts: OutputBundleOptions,
61114
nextBuildDirectory: string,
62-
nextVersion: string,
63-
adapterMetadata: AdapterMetadata,
64115
): Promise<void> {
65116
const staticDirectory = join(nextBuildDirectory, "static");
66117
await Promise.all([
67118
copy(staticDirectory, opts.outputStaticDirectoryPath, { overwrite: true }),
68119
copyResources(appDir, opts.outputDirectoryAppPath, opts.bundleYamlPath),
69-
generateBundleYaml(opts, rootDir, nextVersion, adapterMetadata),
70120
]);
71121
// generateBundleYaml creates the output directory (if it does not already exist).
72122
// We need to make sure it is gitignored.
@@ -111,7 +161,7 @@ export function getAdapterMetadata(): AdapterMetadata {
111161
}
112162

113163
// generate bundle.yaml
114-
async function generateBundleYaml(
164+
export async function generateBundleYaml(
115165
opts: OutputBundleOptions,
116166
cwd: string,
117167
nextVersion: string,

0 commit comments

Comments
 (0)