Skip to content

Commit

Permalink
Update next.js bundle.yaml with env and metadata sections (#248)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
sjjj986 authored Sep 3, 2024
1 parent b3553de commit 95d529f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 44 deletions.
116 changes: 77 additions & 39 deletions packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const importUtils = import("@apphosting/adapter-nextjs/dist/utils.js");
import assert from "assert";
import fs from "fs";
import yaml from "yaml";
import path from "path";
import os from "os";
import { OutputBundleOptions } from "../interfaces.js";

describe("build commands", () => {
let tmpDir: string;
let outputBundleOptions: OutputBundleOptions;
let defaultNextVersion: string;
beforeEach(() => {
tmpDir = generateTmpDir();
outputBundleOptions = {
Expand All @@ -18,10 +20,11 @@ describe("build commands", () => {
outputStaticDirectoryPath: path.join(tmpDir, ".apphosting/.next/static"),
serverFilePath: path.join(tmpDir, ".apphosting/server.js"),
};
defaultNextVersion = "14.0.3";
});

it("expects all output bundle files to be generated", async () => {
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
const { generateOutputDirectory, validateOutputDirectory, createMetadata } = await importUtils;
const files = {
".next/standalone/server.js": "",
".next/static/staticfile": "",
Expand All @@ -31,8 +34,15 @@ describe("build commands", () => {
"redirects":[]
}`,
};
const packageVersion = createMetadata(defaultNextVersion).adapterVersion;
generateTestFiles(tmpDir, files);
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
await generateOutputDirectory(
tmpDir,
tmpDir,
outputBundleOptions,
path.join(tmpDir, ".next"),
defaultNextVersion,
);
await validateOutputDirectory(outputBundleOptions);

const expectedFiles = {
Expand All @@ -46,6 +56,12 @@ neededDirs:
- .apphosting
staticAssets:
- .apphosting/public
env: []
metadata:
adapterPackageName: "@apphosting/adapter-nextjs"
adapterVersion: ${packageVersion}
framework: nextjs
frameworkVersion: ${defaultNextVersion}
`,
};
validateTestFiles(tmpDir, expectedFiles);
Expand Down Expand Up @@ -76,22 +92,23 @@ staticAssets:
serverFilePath: path.join(tmpDir, ".apphosting/apps/next-app/server.js"),
},
path.join(tmpDir, ".next"),
defaultNextVersion,
);

const expectedFiles = {
".apphosting/apps/next-app/.next/static/staticfile": "",
".apphosting/apps/next-app/standalonefile": "",
".apphosting/bundle.yaml": `headers: []
redirects: []
rewrites: []
runCommand: node .apphosting/apps/next-app/server.js
neededDirs:
- .apphosting
staticAssets:
- .apphosting/apps/next-app/public
`,
};
const expectedPartialYaml = {
headers: [],
rewrites: [],
redirects: [],
runCommand: "node .apphosting/apps/next-app/server.js",
neededDirs: [".apphosting"],
staticAssets: [".apphosting/apps/next-app/public"],
};
validateTestFiles(tmpDir, expectedFiles);
validatePartialYamlContents(tmpDir, ".apphosting/bundle.yaml", expectedPartialYaml);
});

it("expects directories and other files to be copied over", async () => {
Expand All @@ -108,25 +125,31 @@ staticAssets:
}`,
};
generateTestFiles(tmpDir, files);
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
await generateOutputDirectory(
tmpDir,
tmpDir,
outputBundleOptions,
path.join(tmpDir, ".next"),
defaultNextVersion,
);
await validateOutputDirectory(outputBundleOptions);

const expectedFiles = {
".apphosting/.next/static/staticfile": "",
".apphosting/server.js": "",
".apphosting/public/publicfile": "",
".apphosting/extrafile": "",
".apphosting/bundle.yaml": `headers: []
redirects: []
rewrites: []
runCommand: node .apphosting/server.js
neededDirs:
- .apphosting
staticAssets:
- .apphosting/public
`,
};
const expectedPartialYaml = {
headers: [],
rewrites: [],
redirects: [],
runCommand: "node .apphosting/server.js",
neededDirs: [".apphosting"],
staticAssets: [".apphosting/public"],
};
validateTestFiles(tmpDir, expectedFiles);
validatePartialYamlContents(tmpDir, ".apphosting/bundle.yaml", expectedPartialYaml);
});

it("expects bundle.yaml headers/rewrites/redirects to be generated", async () => {
Expand All @@ -141,30 +164,26 @@ staticAssets:
}`,
};
generateTestFiles(tmpDir, files);
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
await generateOutputDirectory(
tmpDir,
tmpDir,
outputBundleOptions,
path.join(tmpDir, ".next"),
defaultNextVersion,
);
await validateOutputDirectory(outputBundleOptions);

const expectedFiles = {
".apphosting/.next/static/staticfile": "",
".apphosting/server.js": "",
".apphosting/bundle.yaml": `headers:
- source: source
headers:
- header1
redirects:
- source: source
destination: destination
rewrites:
- source: source
destination: destination
runCommand: node .apphosting/server.js
neededDirs:
- .apphosting
staticAssets:
- .apphosting/public
`,
};
const expectedPartialYaml = {
headers: [{ source: "source", headers: ["header1"] }],
rewrites: [{ source: "source", destination: "destination" }],
redirects: [{ source: "source", destination: "destination" }],
};
validateTestFiles(tmpDir, expectedFiles);
validatePartialYamlContents(tmpDir, ".apphosting/bundle.yaml", expectedPartialYaml);
});
it("test failed validateOutputDirectory", async () => {
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
Expand All @@ -178,7 +197,13 @@ staticAssets:
}`,
};
generateTestFiles(tmpDir, files);
await generateOutputDirectory(tmpDir, tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
await generateOutputDirectory(
tmpDir,
tmpDir,
outputBundleOptions,
path.join(tmpDir, ".next"),
defaultNextVersion,
);
assert.rejects(async () => await validateOutputDirectory(outputBundleOptions));
});
it("test populate output bundle options", async () => {
Expand Down Expand Up @@ -220,3 +245,16 @@ function validateTestFiles(baseDir: string, expectedFiles: Object): void {
assert.deepEqual(contents, expectedContents);
});
}

function validatePartialYamlContents(
baseDir: string,
yamlFileName: string,
expectedPartialYaml: any,
): void {
const yamlFilePath = path.join(baseDir, yamlFileName);
const yamlContents = fs.readFileSync(yamlFilePath, "utf8");
const parsedYaml = yaml.parse(yamlContents) as { [key: string]: any };
Object.keys(expectedPartialYaml).forEach((key) => {
assert.deepEqual(parsedYaml[key], expectedPartialYaml[key]);
});
}
11 changes: 10 additions & 1 deletion packages/@apphosting/adapter-nextjs/src/bin/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@ const opts = getBuildOptions();
process.env.NEXT_PRIVATE_STANDALONE = "true";
// Opt-out sending telemetry to Vercel
process.env.NEXT_TELEMETRY_DISABLED = "1";
if (!process.env.FRAMEWORK_VERSION) {
throw new Error("Could not find the nextjs version of the application");
}
await runBuild();

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

await generateOutputDirectory(root, opts.projectDirectory, outputBundleOptions, nextBuildDirectory);
await generateOutputDirectory(
root,
opts.projectDirectory,
outputBundleOptions,
nextBuildDirectory,
process.env.FRAMEWORK_VERSION,
);
await validateOutputDirectory(outputBundleOptions);
8 changes: 8 additions & 0 deletions packages/@apphosting/adapter-nextjs/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,11 @@ export interface OutputBundleOptions {
*/
outputStaticDirectoryPath: string;
}

// Metadata schema for bundle.yaml outputted by next.js adapter
export interface Metadata {
adapterPackageName: string;
adapterVersion: string;
framework: string;
frameworkVersion: string;
}
30 changes: 26 additions & 4 deletions packages/@apphosting/adapter-nextjs/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import fsExtra from "fs-extra";
import { createRequire } from "node:module";
import { join, relative, normalize } from "path";
import { join, dirname, relative, normalize } from "path";
import { fileURLToPath } from "url";
import { stringify as yamlStringify } from "yaml";

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

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

// The default fallback command prefix to run a build.
export const DEFAULT_COMMAND = "npm";
Expand Down Expand Up @@ -83,6 +83,7 @@ export async function generateOutputDirectory(
appDir: string,
outputBundleOptions: OutputBundleOptions,
nextBuildDirectory: string,
nextVersion: string,
): Promise<void> {
const standaloneDirectory = join(nextBuildDirectory, "standalone");
await move(standaloneDirectory, outputBundleOptions.outputDirectoryBasePath, { overwrite: true });
Expand All @@ -91,7 +92,7 @@ export async function generateOutputDirectory(
await Promise.all([
move(staticDirectory, outputBundleOptions.outputStaticDirectoryPath, { overwrite: true }),
moveResources(appDir, outputBundleOptions.outputDirectoryAppPath),
generateBundleYaml(outputBundleOptions, nextBuildDirectory, rootDir),
generateBundleYaml(outputBundleOptions, nextBuildDirectory, rootDir, nextVersion),
]);
return;
}
Expand All @@ -112,11 +113,30 @@ async function moveResources(appDir: string, outputBundleAppDir: string): Promis
return;
}

/**
* Create metadata needed for outputting adapter and framework metrics in bundle.yaml.
*/
export function createMetadata(nextVersion: string): Metadata {
const directoryName = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = `${directoryName}/../package.json`;
if (!existsSync(packageJsonPath)) {
throw new Error(`Next.js adapter package.json file does not exist at ${packageJsonPath}`);
}
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return {
adapterPackageName: packageJson.name,
adapterVersion: packageJson.version,
framework: "nextjs",
frameworkVersion: nextVersion,
};
}

// generate bundle.yaml
async function generateBundleYaml(
outputBundleOptions: OutputBundleOptions,
nextBuildDirectory: string,
cwd: string,
nextVersion: string,
): Promise<void> {
const manifest = await readRoutesManifest(nextBuildDirectory);
const headers = manifest.headers.map((it) => ({ ...it, regex: undefined }));
Expand All @@ -136,6 +156,8 @@ async function generateBundleYaml(
runCommand: `node ${normalize(relative(cwd, outputBundleOptions.serverFilePath))}`,
neededDirs: [normalize(relative(cwd, outputBundleOptions.outputDirectoryBasePath))],
staticAssets: [normalize(relative(cwd, outputBundleOptions.outputPublicDirectoryPath))],
env: [],
metadata: createMetadata(nextVersion),
}),
);
return;
Expand Down

0 comments on commit 95d529f

Please sign in to comment.