diff --git a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts index 8fb1177f..bae61c33 100644 --- a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts +++ b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts @@ -164,6 +164,112 @@ outputFiles: async () => await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next")), ); }); + it(".apphosting gitignored correctly in a monorepo setup", async () => { + const { generateBuildOutput } = await importUtils; + const files = { + ".next/standalone/apps/next-app/standalonefile": "", + ".next/static/staticfile": "", + }; + generateTestFiles(tmpDir, files); + await generateBuildOutput( + tmpDir, + "apps/next-app", + { + bundleYamlPath: path.join(tmpDir, ".apphosting", "bundle.yaml"), + outputDirectoryBasePath: path.join(tmpDir, ".apphosting"), + outputDirectoryAppPath: path.join(tmpDir, ".next", "standalone", "apps", "next-app"), + outputPublicDirectoryPath: path.join( + tmpDir, + ".next", + "standalone", + "apps", + "next-app", + "public", + ), + outputStaticDirectoryPath: path.join( + tmpDir, + ".next", + "standalone", + "apps", + "next-app", + ".next", + "static", + ), + serverFilePath: path.join(tmpDir, ".next", "standalone", "apps", "next-app", "server.js"), + }, + path.join(tmpDir, ".next"), + defaultNextVersion, + adapterMetadata, + ); + + const expectedFiles = { + ".gitignore": "/.apphosting/", + }; + const expectedPartialYaml = { + version: "v1", + runConfig: { runCommand: "node .next/standalone/apps/next-app/server.js" }, + }; + validateTestFiles(tmpDir, expectedFiles); + validatePartialYamlContents(tmpDir, ".apphosting/bundle.yaml", expectedPartialYaml); + }); + + it(".apphosting gitignored without existing .gitignore file", async () => { + const { generateBuildOutput, validateOutputDirectory } = await importUtils; + const files = { + // .next/standalone/.next/ must be created beforehand otherwise + // generateBuildOutput will attempt to copy + // .next/ into .next/standalone/.next + ".next/standalone/.next/package.json": "", + ".next/static/staticfile": "", + }; + generateTestFiles(tmpDir, files); + await generateBuildOutput( + tmpDir, + tmpDir, + outputBundleOptions, + path.join(tmpDir, ".next"), + defaultNextVersion, + { + adapterPackageName: "@apphosting/adapter-nextjs", + adapterVersion: "14.0.1", + }, + ); + await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next")); + + const expectedFiles = { + ".gitignore": "/.apphosting/", + }; + validateTestFiles(tmpDir, expectedFiles); + }); + it(".apphosting gitignored in existing .gitignore file", async () => { + const { generateBuildOutput, validateOutputDirectory } = await importUtils; + const files = { + // .next/standalone/.next/ must be created beforehand otherwise + // generateBuildOutput will attempt to copy + // .next/ into .next/standalone/.next + ".next/standalone/.next/package.json": "", + ".next/static/staticfile": "", + ".gitignore": "/.next/", + }; + generateTestFiles(tmpDir, files); + await generateBuildOutput( + tmpDir, + tmpDir, + outputBundleOptions, + path.join(tmpDir, ".next"), + defaultNextVersion, + { + adapterPackageName: "@apphosting/adapter-nextjs", + adapterVersion: "14.0.1", + }, + ); + await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next")); + + const expectedFiles = { + ".gitignore": "/.next/\n/.apphosting/", + }; + validateTestFiles(tmpDir, expectedFiles); + }); it("expects directories and other files to be copied over", async () => { const { generateBuildOutput, validateOutputDirectory } = await importUtils; const files = { diff --git a/packages/@apphosting/adapter-nextjs/src/utils.ts b/packages/@apphosting/adapter-nextjs/src/utils.ts index ed6cbc0a..ed92422b 100644 --- a/packages/@apphosting/adapter-nextjs/src/utils.ts +++ b/packages/@apphosting/adapter-nextjs/src/utils.ts @@ -12,10 +12,10 @@ import { MiddlewareManifest, } from "./interfaces.js"; import { NextConfigComplete } from "next/dist/server/config-shared.js"; -import { OutputBundleConfig } from "@apphosting/common"; +import { OutputBundleConfig, updateOrCreateGitignore } from "@apphosting/common"; // fs-extra is CJS, readJson can't be imported using shorthand -export const { copy, exists, writeFile, readJson, readdir, readFileSync, existsSync, mkdir } = +export const { copy, exists, writeFile, readJson, readdir, readFileSync, existsSync, ensureDir } = fsExtra; // Loads the user's next.config.js file. @@ -181,7 +181,7 @@ async function generateBundleYaml( nextVersion: string, adapterMetadata: AdapterMetadata, ): Promise { - await mkdir(opts.outputDirectoryBasePath); + await ensureDir(opts.outputDirectoryBasePath); const outputBundle: OutputBundleConfig = { version: "v1", runConfig: { @@ -203,6 +203,8 @@ async function generateBundleYaml( } await writeFile(opts.bundleYamlPath, yamlStringify(outputBundle)); + const normalizedBundleDir = normalize(relative(cwd, opts.outputDirectoryBasePath)); + updateOrCreateGitignore(cwd, [`/${normalizedBundleDir}/`]); return; } diff --git a/packages/@apphosting/common/package.json b/packages/@apphosting/common/package.json index 25ae288e..ed144d7b 100644 --- a/packages/@apphosting/common/package.json +++ b/packages/@apphosting/common/package.json @@ -1,6 +1,6 @@ { "name": "@apphosting/common", - "version": "0.0.5", + "version": "0.0.6", "description": "Shared library code for App Hosting framework adapters", "author": { "name": "Firebase", @@ -17,6 +17,7 @@ }, "scripts": { "build": "tsc", + "test": "ts-mocha -p tsconfig.json 'src/**/*.spec.ts' 'src/*.spec.ts'", "localregistry:start": "npx verdaccio --config ../publish-dev/verdaccio-config.yaml", "localregistry:publish": "(npm view --registry=http://localhost:4873 @apphosting/common && npm unpublish --@apphosting:registry=http://localhost:4873 --force); npm publish --@apphosting:registry=http://localhost:4873" }, diff --git a/packages/@apphosting/common/src/index.spec.ts b/packages/@apphosting/common/src/index.spec.ts new file mode 100644 index 00000000..27bdbd57 --- /dev/null +++ b/packages/@apphosting/common/src/index.spec.ts @@ -0,0 +1,30 @@ +import assert from "assert"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { updateOrCreateGitignore } from "./index"; + +describe("update or create .gitignore", () => { + let tmpDir: string; + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-gitignore")); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it(".gitignore file exists and is correctly updated with missing paths", () => { + fs.writeFileSync(path.join(tmpDir, ".gitignore"), "existingpath/"); + + updateOrCreateGitignore(tmpDir, ["existingpath/", "newpath/"]); + + const gitignoreContent = fs.readFileSync(path.join(tmpDir, ".gitignore"), "utf-8"); + assert.equal(`existingpath/\nnewpath/`, gitignoreContent); + }); + it(".gitignore file does not exist and is created", () => { + updateOrCreateGitignore(tmpDir, ["chickenpath/", "newpath/"]); + const gitignoreContent = fs.readFileSync(path.join(tmpDir, ".gitignore"), "utf-8"); + assert.equal(`chickenpath/\nnewpath/`, gitignoreContent); + }); +}); diff --git a/packages/@apphosting/common/src/index.ts b/packages/@apphosting/common/src/index.ts index 6319a0cc..f16d32db 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -1,4 +1,6 @@ import { spawn } from "child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; // Output bundle metadata specifications to be written to bundle.yaml export interface OutputBundleConfig { @@ -139,3 +141,26 @@ export function getBuildOptions(): BuildOptions { projectDirectory: process.cwd(), }; } + +/** + * Updates or creates a .gitignore file with the given entries in the given path + */ +export function updateOrCreateGitignore(dirPath: string, entries: string[]) { + const gitignorePath = path.join(dirPath, ".gitignore"); + + if (!fs.existsSync(gitignorePath)) { + console.log(`creating ${gitignorePath} with entries: ${entries.join("\n")}`); + fs.writeFileSync(gitignorePath, entries.join("\n")); + return; + } + + let content = fs.readFileSync(gitignorePath, "utf-8"); + for (const entry of entries) { + if (!content.split("\n").includes(entry)) { + console.log(`adding ${entry} to ${gitignorePath}`); + content += `\n${entry}`; + } + } + + fs.writeFileSync(gitignorePath, content); +}