Skip to content

Commit 95d529f

Browse files
authored
Update next.js bundle.yaml with env and metadata sections (#248)
- pass user's nextjs version from framework buildpack - add an empty env section - add a metadata section with adapter & nextjs metrics - refactor bundle.yaml test
1 parent b3553de commit 95d529f

File tree

4 files changed

+121
-44
lines changed

4 files changed

+121
-44
lines changed

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

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
const importUtils = import("@apphosting/adapter-nextjs/dist/utils.js");
22
import assert from "assert";
33
import fs from "fs";
4+
import yaml from "yaml";
45
import path from "path";
56
import os from "os";
67
import { OutputBundleOptions } from "../interfaces.js";
78

89
describe("build commands", () => {
910
let tmpDir: string;
1011
let outputBundleOptions: OutputBundleOptions;
12+
let defaultNextVersion: string;
1113
beforeEach(() => {
1214
tmpDir = generateTmpDir();
1315
outputBundleOptions = {
@@ -18,10 +20,11 @@ describe("build commands", () => {
1820
outputStaticDirectoryPath: path.join(tmpDir, ".apphosting/.next/static"),
1921
serverFilePath: path.join(tmpDir, ".apphosting/server.js"),
2022
};
23+
defaultNextVersion = "14.0.3";
2124
});
2225

2326
it("expects all output bundle files to be generated", async () => {
24-
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
27+
const { generateOutputDirectory, validateOutputDirectory, createMetadata } = await importUtils;
2528
const files = {
2629
".next/standalone/server.js": "",
2730
".next/static/staticfile": "",
@@ -31,8 +34,15 @@ describe("build commands", () => {
3134
"redirects":[]
3235
}`,
3336
};
37+
const packageVersion = createMetadata(defaultNextVersion).adapterVersion;
3438
generateTestFiles(tmpDir, files);
35-
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
39+
await generateOutputDirectory(
40+
tmpDir,
41+
tmpDir,
42+
outputBundleOptions,
43+
path.join(tmpDir, ".next"),
44+
defaultNextVersion,
45+
);
3646
await validateOutputDirectory(outputBundleOptions);
3747

3848
const expectedFiles = {
@@ -46,6 +56,12 @@ neededDirs:
4656
- .apphosting
4757
staticAssets:
4858
- .apphosting/public
59+
env: []
60+
metadata:
61+
adapterPackageName: "@apphosting/adapter-nextjs"
62+
adapterVersion: ${packageVersion}
63+
framework: nextjs
64+
frameworkVersion: ${defaultNextVersion}
4965
`,
5066
};
5167
validateTestFiles(tmpDir, expectedFiles);
@@ -76,22 +92,23 @@ staticAssets:
7692
serverFilePath: path.join(tmpDir, ".apphosting/apps/next-app/server.js"),
7793
},
7894
path.join(tmpDir, ".next"),
95+
defaultNextVersion,
7996
);
8097

8198
const expectedFiles = {
8299
".apphosting/apps/next-app/.next/static/staticfile": "",
83100
".apphosting/apps/next-app/standalonefile": "",
84-
".apphosting/bundle.yaml": `headers: []
85-
redirects: []
86-
rewrites: []
87-
runCommand: node .apphosting/apps/next-app/server.js
88-
neededDirs:
89-
- .apphosting
90-
staticAssets:
91-
- .apphosting/apps/next-app/public
92-
`,
101+
};
102+
const expectedPartialYaml = {
103+
headers: [],
104+
rewrites: [],
105+
redirects: [],
106+
runCommand: "node .apphosting/apps/next-app/server.js",
107+
neededDirs: [".apphosting"],
108+
staticAssets: [".apphosting/apps/next-app/public"],
93109
};
94110
validateTestFiles(tmpDir, expectedFiles);
111+
validatePartialYamlContents(tmpDir, ".apphosting/bundle.yaml", expectedPartialYaml);
95112
});
96113

97114
it("expects directories and other files to be copied over", async () => {
@@ -108,25 +125,31 @@ staticAssets:
108125
}`,
109126
};
110127
generateTestFiles(tmpDir, files);
111-
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
128+
await generateOutputDirectory(
129+
tmpDir,
130+
tmpDir,
131+
outputBundleOptions,
132+
path.join(tmpDir, ".next"),
133+
defaultNextVersion,
134+
);
112135
await validateOutputDirectory(outputBundleOptions);
113136

114137
const expectedFiles = {
115138
".apphosting/.next/static/staticfile": "",
116139
".apphosting/server.js": "",
117140
".apphosting/public/publicfile": "",
118141
".apphosting/extrafile": "",
119-
".apphosting/bundle.yaml": `headers: []
120-
redirects: []
121-
rewrites: []
122-
runCommand: node .apphosting/server.js
123-
neededDirs:
124-
- .apphosting
125-
staticAssets:
126-
- .apphosting/public
127-
`,
142+
};
143+
const expectedPartialYaml = {
144+
headers: [],
145+
rewrites: [],
146+
redirects: [],
147+
runCommand: "node .apphosting/server.js",
148+
neededDirs: [".apphosting"],
149+
staticAssets: [".apphosting/public"],
128150
};
129151
validateTestFiles(tmpDir, expectedFiles);
152+
validatePartialYamlContents(tmpDir, ".apphosting/bundle.yaml", expectedPartialYaml);
130153
});
131154

132155
it("expects bundle.yaml headers/rewrites/redirects to be generated", async () => {
@@ -141,30 +164,26 @@ staticAssets:
141164
}`,
142165
};
143166
generateTestFiles(tmpDir, files);
144-
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
167+
await generateOutputDirectory(
168+
tmpDir,
169+
tmpDir,
170+
outputBundleOptions,
171+
path.join(tmpDir, ".next"),
172+
defaultNextVersion,
173+
);
145174
await validateOutputDirectory(outputBundleOptions);
146175

147176
const expectedFiles = {
148177
".apphosting/.next/static/staticfile": "",
149178
".apphosting/server.js": "",
150-
".apphosting/bundle.yaml": `headers:
151-
- source: source
152-
headers:
153-
- header1
154-
redirects:
155-
- source: source
156-
destination: destination
157-
rewrites:
158-
- source: source
159-
destination: destination
160-
runCommand: node .apphosting/server.js
161-
neededDirs:
162-
- .apphosting
163-
staticAssets:
164-
- .apphosting/public
165-
`,
179+
};
180+
const expectedPartialYaml = {
181+
headers: [{ source: "source", headers: ["header1"] }],
182+
rewrites: [{ source: "source", destination: "destination" }],
183+
redirects: [{ source: "source", destination: "destination" }],
166184
};
167185
validateTestFiles(tmpDir, expectedFiles);
186+
validatePartialYamlContents(tmpDir, ".apphosting/bundle.yaml", expectedPartialYaml);
168187
});
169188
it("test failed validateOutputDirectory", async () => {
170189
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
@@ -178,7 +197,13 @@ staticAssets:
178197
}`,
179198
};
180199
generateTestFiles(tmpDir, files);
181-
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
200+
await generateOutputDirectory(
201+
tmpDir,
202+
tmpDir,
203+
outputBundleOptions,
204+
path.join(tmpDir, ".next"),
205+
defaultNextVersion,
206+
);
182207
assert.rejects(async () => await validateOutputDirectory(outputBundleOptions));
183208
});
184209
it("test populate output bundle options", async () => {
@@ -220,3 +245,16 @@ function validateTestFiles(baseDir: string, expectedFiles: Object): void {
220245
assert.deepEqual(contents, expectedContents);
221246
});
222247
}
248+
249+
function validatePartialYamlContents(
250+
baseDir: string,
251+
yamlFileName: string,
252+
expectedPartialYaml: any,
253+
): void {
254+
const yamlFilePath = path.join(baseDir, yamlFileName);
255+
const yamlContents = fs.readFileSync(yamlFilePath, "utf8");
256+
const parsedYaml = yaml.parse(yamlContents) as { [key: string]: any };
257+
Object.keys(expectedPartialYaml).forEach((key) => {
258+
assert.deepEqual(parsedYaml[key], expectedPartialYaml[key]);
259+
});
260+
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@ const opts = getBuildOptions();
1515
process.env.NEXT_PRIVATE_STANDALONE = "true";
1616
// Opt-out sending telemetry to Vercel
1717
process.env.NEXT_TELEMETRY_DISABLED = "1";
18+
if (!process.env.FRAMEWORK_VERSION) {
19+
throw new Error("Could not find the nextjs version of the application");
20+
}
1821
await runBuild();
1922

2023
const outputBundleOptions = populateOutputBundleOptions(root, opts.projectDirectory);
2124
const { distDir } = await loadConfig(root, opts.projectDirectory);
2225
const nextBuildDirectory = join(opts.projectDirectory, distDir);
2326

24-
await generateOutputDirectory(root, opts.projectDirectory, outputBundleOptions, nextBuildDirectory);
27+
await generateOutputDirectory(
28+
root,
29+
opts.projectDirectory,
30+
outputBundleOptions,
31+
nextBuildDirectory,
32+
process.env.FRAMEWORK_VERSION,
33+
);
2534
await validateOutputDirectory(outputBundleOptions);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,11 @@ export interface OutputBundleOptions {
101101
*/
102102
outputStaticDirectoryPath: string;
103103
}
104+
105+
// Metadata schema for bundle.yaml outputted by next.js adapter
106+
export interface Metadata {
107+
adapterPackageName: string;
108+
adapterVersion: string;
109+
framework: string;
110+
frameworkVersion: string;
111+
}

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import fsExtra from "fs-extra";
22
import { createRequire } from "node:module";
3-
import { join, relative, normalize } from "path";
3+
import { join, dirname, relative, normalize } from "path";
44
import { fileURLToPath } from "url";
55
import { stringify as yamlStringify } from "yaml";
66

77
import { PHASE_PRODUCTION_BUILD } from "./constants.js";
88
import { ROUTES_MANIFEST } from "./constants.js";
9-
import { OutputBundleOptions, RoutesManifest } from "./interfaces.js";
9+
import { Metadata, OutputBundleOptions, RoutesManifest } from "./interfaces.js";
1010
import { NextConfigComplete } from "next/dist/server/config-shared.js";
1111

1212
// fs-extra is CJS, readJson can't be imported using shorthand
13-
export const { move, exists, writeFile, readJson, readdir } = fsExtra;
13+
export const { move, exists, writeFile, readJson, readdir, readFileSync, existsSync } = fsExtra;
1414

1515
// The default fallback command prefix to run a build.
1616
export const DEFAULT_COMMAND = "npm";
@@ -83,6 +83,7 @@ export async function generateOutputDirectory(
8383
appDir: string,
8484
outputBundleOptions: OutputBundleOptions,
8585
nextBuildDirectory: string,
86+
nextVersion: string,
8687
): Promise<void> {
8788
const standaloneDirectory = join(nextBuildDirectory, "standalone");
8889
await move(standaloneDirectory, outputBundleOptions.outputDirectoryBasePath, { overwrite: true });
@@ -91,7 +92,7 @@ export async function generateOutputDirectory(
9192
await Promise.all([
9293
move(staticDirectory, outputBundleOptions.outputStaticDirectoryPath, { overwrite: true }),
9394
moveResources(appDir, outputBundleOptions.outputDirectoryAppPath),
94-
generateBundleYaml(outputBundleOptions, nextBuildDirectory, rootDir),
95+
generateBundleYaml(outputBundleOptions, nextBuildDirectory, rootDir, nextVersion),
9596
]);
9697
return;
9798
}
@@ -112,11 +113,30 @@ async function moveResources(appDir: string, outputBundleAppDir: string): Promis
112113
return;
113114
}
114115

116+
/**
117+
* Create metadata needed for outputting adapter and framework metrics in bundle.yaml.
118+
*/
119+
export function createMetadata(nextVersion: string): Metadata {
120+
const directoryName = dirname(fileURLToPath(import.meta.url));
121+
const packageJsonPath = `${directoryName}/../package.json`;
122+
if (!existsSync(packageJsonPath)) {
123+
throw new Error(`Next.js adapter package.json file does not exist at ${packageJsonPath}`);
124+
}
125+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
126+
return {
127+
adapterPackageName: packageJson.name,
128+
adapterVersion: packageJson.version,
129+
framework: "nextjs",
130+
frameworkVersion: nextVersion,
131+
};
132+
}
133+
115134
// generate bundle.yaml
116135
async function generateBundleYaml(
117136
outputBundleOptions: OutputBundleOptions,
118137
nextBuildDirectory: string,
119138
cwd: string,
139+
nextVersion: string,
120140
): Promise<void> {
121141
const manifest = await readRoutesManifest(nextBuildDirectory);
122142
const headers = manifest.headers.map((it) => ({ ...it, regex: undefined }));
@@ -136,6 +156,8 @@ async function generateBundleYaml(
136156
runCommand: `node ${normalize(relative(cwd, outputBundleOptions.serverFilePath))}`,
137157
neededDirs: [normalize(relative(cwd, outputBundleOptions.outputDirectoryBasePath))],
138158
staticAssets: [normalize(relative(cwd, outputBundleOptions.outputPublicDirectoryPath))],
159+
env: [],
160+
metadata: createMetadata(nextVersion),
139161
}),
140162
);
141163
return;

0 commit comments

Comments
 (0)