From 95d529f36b31e51149c158d91a7e184919d4ec8e Mon Sep 17 00:00:00 2001 From: sijinli <148275505+sjjj986@users.noreply.github.com> Date: Tue, 3 Sep 2024 01:00:48 -0400 Subject: [PATCH] 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 --- .../adapter-nextjs/src/bin/build.spec.ts | 116 ++++++++++++------ .../adapter-nextjs/src/bin/build.ts | 11 +- .../adapter-nextjs/src/interfaces.ts | 8 ++ .../@apphosting/adapter-nextjs/src/utils.ts | 30 ++++- 4 files changed, 121 insertions(+), 44 deletions(-) diff --git a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts index efb32792..de2bdfd9 100644 --- a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts +++ b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts @@ -1,6 +1,7 @@ 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"; @@ -8,6 +9,7 @@ import { OutputBundleOptions } from "../interfaces.js"; describe("build commands", () => { let tmpDir: string; let outputBundleOptions: OutputBundleOptions; + let defaultNextVersion: string; beforeEach(() => { tmpDir = generateTmpDir(); outputBundleOptions = { @@ -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": "", @@ -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 = { @@ -46,6 +56,12 @@ neededDirs: - .apphosting staticAssets: - .apphosting/public +env: [] +metadata: + adapterPackageName: "@apphosting/adapter-nextjs" + adapterVersion: ${packageVersion} + framework: nextjs + frameworkVersion: ${defaultNextVersion} `, }; validateTestFiles(tmpDir, expectedFiles); @@ -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 () => { @@ -108,7 +125,13 @@ 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 = { @@ -116,17 +139,17 @@ staticAssets: ".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 () => { @@ -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; @@ -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 () => { @@ -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]); + }); +} diff --git a/packages/@apphosting/adapter-nextjs/src/bin/build.ts b/packages/@apphosting/adapter-nextjs/src/bin/build.ts index f5f1feaa..b3c7c6a2 100644 --- a/packages/@apphosting/adapter-nextjs/src/bin/build.ts +++ b/packages/@apphosting/adapter-nextjs/src/bin/build.ts @@ -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); diff --git a/packages/@apphosting/adapter-nextjs/src/interfaces.ts b/packages/@apphosting/adapter-nextjs/src/interfaces.ts index ded083d4..62eaf6cc 100644 --- a/packages/@apphosting/adapter-nextjs/src/interfaces.ts +++ b/packages/@apphosting/adapter-nextjs/src/interfaces.ts @@ -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; +} diff --git a/packages/@apphosting/adapter-nextjs/src/utils.ts b/packages/@apphosting/adapter-nextjs/src/utils.ts index 2a958762..6521dfec 100644 --- a/packages/@apphosting/adapter-nextjs/src/utils.ts +++ b/packages/@apphosting/adapter-nextjs/src/utils.ts @@ -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"; @@ -83,6 +83,7 @@ export async function generateOutputDirectory( appDir: string, outputBundleOptions: OutputBundleOptions, nextBuildDirectory: string, + nextVersion: string, ): Promise { const standaloneDirectory = join(nextBuildDirectory, "standalone"); await move(standaloneDirectory, outputBundleOptions.outputDirectoryBasePath, { overwrite: true }); @@ -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; } @@ -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 { const manifest = await readRoutesManifest(nextBuildDirectory); const headers = manifest.headers.map((it) => ({ ...it, regex: undefined })); @@ -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;