diff --git a/packages/@apphosting/adapter-angular/package.json b/packages/@apphosting/adapter-angular/package.json index 65ed96ba..2cdd1212 100644 --- a/packages/@apphosting/adapter-angular/package.json +++ b/packages/@apphosting/adapter-angular/package.json @@ -1,6 +1,6 @@ { "name": "@apphosting/adapter-angular", - "version": "17.2.15", + "version": "17.2.16", "main": "dist/index.js", "description": "Experimental addon to the Firebase CLI to add web framework support", "repository": { diff --git a/packages/@apphosting/adapter-angular/src/utils.ts b/packages/@apphosting/adapter-angular/src/utils.ts index 52ecc563..e5be7de1 100644 --- a/packages/@apphosting/adapter-angular/src/utils.ts +++ b/packages/@apphosting/adapter-angular/src/utils.ts @@ -175,7 +175,10 @@ function extractManifestOutput(output: string): string { if (start === -1 || end === -1 || start > end) { throw new Error(`Failed to find valid JSON object from build output: ${output}`); } - return stripAnsi(output.substring(start, end + 1)); + // Clean the raw json string by removing the "web:build:" prefixes for a Turbo build + const prefixRegex = /\n?web:build:/g; + const cleanedOutput = output.substring(start, end + 1).replace(prefixRegex, ""); + return stripAnsi(cleanedOutput); } /** diff --git a/packages/@apphosting/adapter-nextjs/package.json b/packages/@apphosting/adapter-nextjs/package.json index 54a6ad53..3490ab47 100644 --- a/packages/@apphosting/adapter-nextjs/package.json +++ b/packages/@apphosting/adapter-nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@apphosting/adapter-nextjs", - "version": "14.0.17", + "version": "14.0.18", "main": "dist/index.js", "description": "Experimental addon to the Firebase CLI to add web framework support", "repository": { diff --git a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts index 8fb1177f..e98d9f52 100644 --- a/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts +++ b/packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts @@ -164,6 +164,95 @@ 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); + const standaloneAppPath = path.join(tmpDir, ".next", "standalone", "apps", "next-app"); + await generateBuildOutput( + tmpDir, + "apps/next-app", + { + bundleYamlPath: path.join(tmpDir, ".apphosting", "bundle.yaml"), + outputDirectoryBasePath: path.join(tmpDir, ".apphosting"), + outputDirectoryAppPath: standaloneAppPath, + outputPublicDirectoryPath: path.join(standaloneAppPath, "public"), + outputStaticDirectoryPath: path.join(standaloneAppPath, ".next", "static"), + serverFilePath: path.join(standaloneAppPath, "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, + adapterMetadata, + ); + 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 = { @@ -188,10 +277,7 @@ outputFiles: outputBundleOptions, path.join(tmpDir, ".next"), defaultNextVersion, - { - adapterPackageName: "@apphosting/adapter-nextjs", - adapterVersion: "14.0.1", - }, + adapterMetadata, ); await validateOutputDirectory(outputBundleOptions, path.join(tmpDir, ".next")); diff --git a/packages/@apphosting/adapter-nextjs/src/utils.ts b/packages/@apphosting/adapter-nextjs/src/utils.ts index ed6cbc0a..3c6ab548 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. @@ -135,6 +135,10 @@ export async function generateBuildOutput( copyResources(appDir, opts.outputDirectoryAppPath, opts.bundleYamlPath), generateBundleYaml(opts, rootDir, nextVersion, adapterMetadata), ]); + // generateBundleYaml creates the output directory (if it does not already exist). + // We need to make sure it is gitignored. + const normalizedBundleDir = normalize(relative(rootDir, opts.outputDirectoryBasePath)); + updateOrCreateGitignore(rootDir, [`/${normalizedBundleDir}/`]); return; } @@ -181,7 +185,7 @@ async function generateBundleYaml( nextVersion: string, adapterMetadata: AdapterMetadata, ): Promise { - await mkdir(opts.outputDirectoryBasePath); + await ensureDir(opts.outputDirectoryBasePath); const outputBundle: OutputBundleConfig = { version: "v1", runConfig: { diff --git a/packages/@apphosting/common/package.json b/packages/@apphosting/common/package.json index ed144d7b..28bfbf6e 100644 --- a/packages/@apphosting/common/package.json +++ b/packages/@apphosting/common/package.json @@ -1,6 +1,6 @@ { "name": "@apphosting/common", - "version": "0.0.6", + "version": "0.0.7", "description": "Shared library code for App Hosting framework adapters", "author": { "name": "Firebase", diff --git a/packages/@apphosting/common/src/index.spec.ts b/packages/@apphosting/common/src/index.spec.ts index 27bdbd57..3993469f 100644 --- a/packages/@apphosting/common/src/index.spec.ts +++ b/packages/@apphosting/common/src/index.spec.ts @@ -2,7 +2,56 @@ import assert from "assert"; import fs from "fs"; import path from "path"; import os from "os"; -import { updateOrCreateGitignore } from "./index"; +import { getBuildOptions, updateOrCreateGitignore } from "./index"; + +const originalCwd = process.cwd.bind(process); + +describe("get a set of build options", () => { + const mockCwd = "/fake/project/"; + beforeEach(() => { + process.cwd = () => mockCwd; + }); + + afterEach(() => { + process.cwd = originalCwd; + delete process.env.MONOREPO_COMMAND; + delete process.env.MONOREPO_BUILD_ARGS; + delete process.env.GOOGLE_BUILDABLE; + delete process.env.MONOREPO_PROJECT; + }); + + it("returns monorepo build options when MONOREPO_COMMAND is set", () => { + process.env.MONOREPO_COMMAND = "turbo"; + process.env.MONOREPO_BUILD_ARGS = "--filter=web,--env-mode=strict"; + process.env.GOOGLE_BUILDABLE = "/workspace/apps/web"; + process.env.MONOREPO_PROJECT = "web"; + + const expectedOptions = { + buildCommand: "turbo", + buildArgs: ["run", "build", "--filter=web", "--env-mode=strict"], + projectDirectory: "/workspace/apps/web", + projectName: "web", + }; + assert.deepStrictEqual( + getBuildOptions(), + expectedOptions, + "Monorepo build options are incorrect", + ); + }); + + it("returns standard build options when MONOREPO_COMMAND is not set", () => { + const expectedOptions = { + buildCommand: "npm", + buildArgs: ["run", "build"], + projectDirectory: process.cwd(), + }; + assert.deepStrictEqual( + getBuildOptions(), + expectedOptions, + "Standard build options are incorrect", + ); + }); +}); describe("update or create .gitignore", () => { let tmpDir: string; diff --git a/packages/@apphosting/common/src/index.ts b/packages/@apphosting/common/src/index.ts index f16d32db..7ad051de 100644 --- a/packages/@apphosting/common/src/index.ts +++ b/packages/@apphosting/common/src/index.ts @@ -130,7 +130,7 @@ export function getBuildOptions(): BuildOptions { if (process.env.MONOREPO_COMMAND) { return { buildCommand: process.env.MONOREPO_COMMAND, - buildArgs: ["run", "build"].concat(process.env.MONOREPO_BUILD_ARGS?.split(".") || []), + buildArgs: ["run", "build"].concat(process.env.MONOREPO_BUILD_ARGS?.split(",") || []), projectDirectory: process.env.GOOGLE_BUILDABLE || "", projectName: process.env.MONOREPO_PROJECT, };