From b4cd6ec638b9cfcd6fb1fa2dd258d2f395fd1c1c Mon Sep 17 00:00:00 2001 From: Joanna Wang Date: Fri, 1 Aug 2025 21:54:24 +0000 Subject: [PATCH 01/10] Add common function to update .gitignore. --- packages/@apphosting/common/package.json | 1 + packages/@apphosting/common/src/index.spec.ts | 29 +++++++++++++++++++ packages/@apphosting/common/src/index.ts | 25 ++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 packages/@apphosting/common/src/index.spec.ts diff --git a/packages/@apphosting/common/package.json b/packages/@apphosting/common/package.json index 25ae288e..5542e156 100644 --- a/packages/@apphosting/common/package.json +++ b/packages/@apphosting/common/package.json @@ -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..a6c02cc5 --- /dev/null +++ b/packages/@apphosting/common/src/index.spec.ts @@ -0,0 +1,29 @@ +import assert from "assert"; +import fs from "fs"; +import yaml from "yaml"; +import path from "path"; +import os from "os"; +const importIndex = import("@apphosting/common/dist/index.js"); + +describe("update or create .gitignore", () => { + let tmpDir: string; + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "test-gitignore")); + }); + + it(".gitignore file exists and is correctly updated with missing paths", async () => { + const { UpdateOrCreateGitignore } = await importIndex; + 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/\n`, gitignoreContent); + }); + it(".gitignore file does not exist and is created", async () => { + const { UpdateOrCreateGitignore } = await importIndex; + 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..9384233f 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 path from "node:path"; +import * as fs from "fs-extra"; // 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.includes(entry)) { + console.log(`adding ${entry} to ${gitignorePath}`); + content += `\n${entry}\n`; + } + } + + fs.writeFileSync(gitignorePath, content); +} From ffcf43d3ee5ccf06480bc9692881e79c6965f359 Mon Sep 17 00:00:00 2001 From: Joanna Wang Date: Fri, 1 Aug 2025 22:03:12 +0000 Subject: [PATCH 02/10] Address bot comments --- packages/@apphosting/common/src/index.spec.ts | 9 ++++----- packages/@apphosting/common/src/index.ts | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/@apphosting/common/src/index.spec.ts b/packages/@apphosting/common/src/index.spec.ts index a6c02cc5..11407be9 100644 --- a/packages/@apphosting/common/src/index.spec.ts +++ b/packages/@apphosting/common/src/index.spec.ts @@ -1,6 +1,5 @@ import assert from "assert"; import fs from "fs"; -import yaml from "yaml"; import path from "path"; import os from "os"; const importIndex = import("@apphosting/common/dist/index.js"); @@ -12,17 +11,17 @@ describe("update or create .gitignore", () => { }); it(".gitignore file exists and is correctly updated with missing paths", async () => { - const { UpdateOrCreateGitignore } = await importIndex; + const { updateOrCreateGitignore } = await importIndex; fs.writeFileSync(path.join(tmpDir, ".gitignore"), "existingpath/"); - UpdateOrCreateGitignore(tmpDir, ["existingpath/", "newpath/"]); + updateOrCreateGitignore(tmpDir, ["existingpath/", "newpath/"]); const gitignoreContent = fs.readFileSync(path.join(tmpDir, ".gitignore"), "utf-8"); assert.equal(`existingpath/\nnewpath/\n`, gitignoreContent); }); it(".gitignore file does not exist and is created", async () => { - const { UpdateOrCreateGitignore } = await importIndex; - UpdateOrCreateGitignore(tmpDir, ["chickenpath/", "newpath/"]); + const { updateOrCreateGitignore } = await importIndex; + 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 9384233f..af58fa1d 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -145,7 +145,7 @@ export function getBuildOptions(): BuildOptions { /** * Updates or creates a .gitignore file with the given entries in the given path */ -export function UpdateOrCreateGitignore(dirPath: string, entries: string[]) { +export function updateOrCreateGitignore(dirPath: string, entries: string[]) { const gitignorePath = path.join(dirPath, ".gitignore"); if (!fs.existsSync(gitignorePath)) { From ca94ea2df29d81b81ee185681c903dc8c0a2b63d Mon Sep 17 00:00:00 2001 From: annajowang <31288696+annajowang@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:04:33 -0400 Subject: [PATCH 03/10] Update packages/@apphosting/common/src/index.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/@apphosting/common/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@apphosting/common/src/index.ts b/packages/@apphosting/common/src/index.ts index af58fa1d..5f243214 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -158,7 +158,7 @@ export function updateOrCreateGitignore(dirPath: string, entries: string[]) { for (const entry of entries) { if (!content.includes(entry)) { console.log(`adding ${entry} to ${gitignorePath}`); - content += `\n${entry}\n`; +content += `\n${entry}`; } } From a2b73353656bf164e28da8cabfa0c137e4137bba Mon Sep 17 00:00:00 2001 From: annajowang <31288696+annajowang@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:04:54 -0400 Subject: [PATCH 04/10] Update packages/@apphosting/common/src/index.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/@apphosting/common/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@apphosting/common/src/index.ts b/packages/@apphosting/common/src/index.ts index 5f243214..f0e61db1 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -156,7 +156,7 @@ export function updateOrCreateGitignore(dirPath: string, entries: string[]) { let content = fs.readFileSync(gitignorePath, "utf-8"); for (const entry of entries) { - if (!content.includes(entry)) { +if (!content.split('\n').includes(entry)) { console.log(`adding ${entry} to ${gitignorePath}`); content += `\n${entry}`; } From 747a05d4ff7562a43c969f13e50937d93066d74e Mon Sep 17 00:00:00 2001 From: annajowang <31288696+annajowang@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:05:42 -0400 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/@apphosting/common/src/index.spec.ts | 8 ++++---- packages/@apphosting/common/src/index.ts | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/@apphosting/common/src/index.spec.ts b/packages/@apphosting/common/src/index.spec.ts index 11407be9..df6cc98c 100644 --- a/packages/@apphosting/common/src/index.spec.ts +++ b/packages/@apphosting/common/src/index.spec.ts @@ -2,13 +2,13 @@ import assert from "assert"; import fs from "fs"; import path from "path"; import os from "os"; -const importIndex = import("@apphosting/common/dist/index.js"); +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", async () => { const { updateOrCreateGitignore } = await importIndex; diff --git a/packages/@apphosting/common/src/index.ts b/packages/@apphosting/common/src/index.ts index f0e61db1..bc6e956a 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -1,6 +1,5 @@ import { spawn } from "child_process"; -import * as path from "node:path"; -import * as fs from "fs-extra"; +import * as fs from "node:fs"; // Output bundle metadata specifications to be written to bundle.yaml export interface OutputBundleConfig { From 051fbb11b6248873efd584d99779bb3904e9aa3e Mon Sep 17 00:00:00 2001 From: Joanna Wang Date: Fri, 1 Aug 2025 22:13:48 +0000 Subject: [PATCH 06/10] fix stupid mess made by stupid bot --- packages/@apphosting/common/src/index.spec.ts | 16 +++++++++------- packages/@apphosting/common/src/index.ts | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/@apphosting/common/src/index.spec.ts b/packages/@apphosting/common/src/index.spec.ts index df6cc98c..403917ad 100644 --- a/packages/@apphosting/common/src/index.spec.ts +++ b/packages/@apphosting/common/src/index.spec.ts @@ -2,25 +2,27 @@ import assert from "assert"; import fs from "fs"; import path from "path"; import os from "os"; -import { UpdateOrCreateGitignore } from "./index"; +import { updateOrCreateGitignore } from "./index"; describe("update or create .gitignore", () => { let tmpDir: string; -afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); -}); + 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", async () => { - const { updateOrCreateGitignore } = await importIndex; 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/\n`, gitignoreContent); + assert.equal(`existingpath/\nnewpath/`, gitignoreContent); }); it(".gitignore file does not exist and is created", async () => { - const { updateOrCreateGitignore } = await importIndex; 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 bc6e956a..e669f3fd 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -1,5 +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 { From a040af0b642ba623e4a54aa42998f520e8c72153 Mon Sep 17 00:00:00 2001 From: Joanna Wang Date: Fri, 1 Aug 2025 22:15:52 +0000 Subject: [PATCH 07/10] lint bot's bad code --- packages/@apphosting/common/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@apphosting/common/src/index.ts b/packages/@apphosting/common/src/index.ts index e669f3fd..f16d32db 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -156,9 +156,9 @@ export function updateOrCreateGitignore(dirPath: string, entries: string[]) { let content = fs.readFileSync(gitignorePath, "utf-8"); for (const entry of entries) { -if (!content.split('\n').includes(entry)) { + if (!content.split("\n").includes(entry)) { console.log(`adding ${entry} to ${gitignorePath}`); -content += `\n${entry}`; + content += `\n${entry}`; } } From 9c03ce9b0e80e013c026948afdb9285bc59d9888 Mon Sep 17 00:00:00 2001 From: Joanna Wang Date: Fri, 1 Aug 2025 22:25:53 +0000 Subject: [PATCH 08/10] lint --- packages/@apphosting/common/src/index.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@apphosting/common/src/index.spec.ts b/packages/@apphosting/common/src/index.spec.ts index 403917ad..27bdbd57 100644 --- a/packages/@apphosting/common/src/index.spec.ts +++ b/packages/@apphosting/common/src/index.spec.ts @@ -14,7 +14,7 @@ describe("update or create .gitignore", () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it(".gitignore file exists and is correctly updated with missing paths", async () => { + it(".gitignore file exists and is correctly updated with missing paths", () => { fs.writeFileSync(path.join(tmpDir, ".gitignore"), "existingpath/"); updateOrCreateGitignore(tmpDir, ["existingpath/", "newpath/"]); @@ -22,7 +22,7 @@ describe("update or create .gitignore", () => { const gitignoreContent = fs.readFileSync(path.join(tmpDir, ".gitignore"), "utf-8"); assert.equal(`existingpath/\nnewpath/`, gitignoreContent); }); - it(".gitignore file does not exist and is created", async () => { + 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); From 70cf87d21b82ecf089e9136b687df1616dd09c84 Mon Sep 17 00:00:00 2001 From: Joanna Wang Date: Tue, 5 Aug 2025 01:14:07 +0000 Subject: [PATCH 09/10] Update common package version --- packages/@apphosting/common/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@apphosting/common/package.json b/packages/@apphosting/common/package.json index 5542e156..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", From 0390e725828047312cb2778963f852e5d234fbca Mon Sep 17 00:00:00 2001 From: Joanna Wang Date: Tue, 5 Aug 2025 19:35:15 +0000 Subject: [PATCH 10/10] gitignore .apphosting/ --- .../adapter-nextjs/src/bin/build.spec.ts | 106 ++++++++++++++++++ .../@apphosting/adapter-nextjs/src/utils.ts | 8 +- 2 files changed, 111 insertions(+), 3 deletions(-) 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; }