Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/deploy/apphosting/prepare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import * as apphostingUtils from "../../apphosting/utils";
import { AppHostingYamlConfig, EnvMap } from "../../apphosting/yaml";
import { Options } from "../../options";
import { AppHostingSingle } from "../../firebaseConfig";
import * as fs from "fs";
import * as fsAsync from "../../fsAsync";

const BASE_OPTS = {
cwd: "/",
Expand Down Expand Up @@ -88,6 +90,12 @@ describe("apphosting", () => {
addServiceAccountToRolesStub = sinon
.stub(resourceManager, "addServiceAccountToRoles")
.resolves();

sinon.stub(fs, "existsSync").returns(false);
sinon.stub(fs, "mkdirSync").returns(undefined);
sinon.stub(fs, "rmSync").returns(undefined);
sinon.stub(fs, "copyFileSync").returns(undefined);
sinon.stub(fsAsync, "readdirRecursive").resolves([]);
});

afterEach(() => {
Expand Down
51 changes: 50 additions & 1 deletion src/deploy/apphosting/prepare.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import * as fsAsync from "../../fsAsync";
import { resolveIgnorePatterns } from "./util";
import {
doSetupSourceDeploy,
ensureAppHostingComputeServiceAccount,
Expand Down Expand Up @@ -192,10 +195,15 @@ export default async function (context: Context, options: Options): Promise<void
);
await injectAutoInitEnvVars(cfg, backends, buildEnv, runtimeEnv);

const rootDir = options.projectRoot || process.cwd();
const localBuildDir = path.join(rootDir, "local_build");

try {
await prepareLocalBuildDirectory(rootDir, localBuildDir, cfg);

const { outputFiles, annotations, buildConfig } = await localBuild(
projectId,
options.projectRoot || "./",
localBuildDir,
"nextjs",
buildEnv[cfg.backendId] || {},
{
Expand Down Expand Up @@ -377,3 +385,44 @@ async function ensureAppHostingServiceAgentRoles(
);
}
}

/**
* Prepares the directory for local builds by copying non-ignored files.
*/
async function prepareLocalBuildDirectory(
rootDir: string,
localBuildDir: string,
cfg: AppHostingSingle,
): Promise<void> {
// Resolve ignores for local builds, skipping default node_modules ignore
const ignore = resolveIgnorePatterns(cfg, rootDir, /* skipDefaultNodeModules= */ true);
ignore.push("local_build"); // Always ignore the build directory itself

// Warn if node_modules is explicitly ignored
if (cfg.ignore?.includes("node_modules")) {
logLabeledWarning(
"apphosting",
`You have included 'node_modules' in your ignore list for local builds. This might cause the build to fail if dependencies are missing in the build directory.`,
);
}

// Create local_build dir
if (fs.existsSync(localBuildDir)) {
fs.rmSync(localBuildDir, { recursive: true, force: true });
}
fs.mkdirSync(localBuildDir, { recursive: true });

// Copy files respecting ignores
const filesToCopy = await fsAsync.readdirRecursive({
path: rootDir,
ignore: ignore,
isGitIgnore: true,
});

for (const file of filesToCopy) {
const relativePath = path.relative(rootDir, file.name);
const destPath = path.join(localBuildDir, relativePath);
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.copyFileSync(file.name, destPath);
}
}
86 changes: 86 additions & 0 deletions src/deploy/apphosting/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as path from "path";
import * as tmp from "tmp";
import * as tar from "tar";
import * as util from "./util";
import { AppHostingSingle } from "../../firebaseConfig";

describe("util", () => {
let tmpDir: tmp.DirResult;
Expand Down Expand Up @@ -78,5 +79,90 @@ describe("util", () => {
expect(files).to.include("dist/index.js");
expect(files).to.not.include("apphosting.yaml");
});

it("should respect ignore patterns in config", async () => {
fs.writeFileSync(path.join(distDir, "index.js"), "console.log('hello')");
fs.writeFileSync(path.join(distDir, "ignored.txt"), "ignore me");

const config = {
backendId: "test-backend",
rootDir: "",
ignore: ["**/ignored.txt"],
};

const tarballPath: string = await util.createLocalBuildTarArchive(
config,
rootDir,
path.relative(rootDir, distDir),
);

const files: string[] = [];
tar.list({
file: tarballPath,
sync: true,
onentry: (entry: { path: string }) => files.push(entry.path),
});

expect(files).to.include("dist/index.js");
expect(files).to.not.include("dist/ignored.txt");
});

it("should use default ignores when config.ignore is missing", async () => {
fs.writeFileSync(path.join(distDir, "index.js"), "console.log('hello')");
const nodeModulesDir = path.join(distDir, "node_modules");
fs.mkdirSync(nodeModulesDir);
fs.writeFileSync(path.join(nodeModulesDir, "some-file.js"), "console.log('vendor')");

const configWithoutIgnore: AppHostingSingle = {
backendId: "test-backend",
rootDir: "",
ignore: undefined as unknown as string[],
};

const tarballPath: string = await util.createLocalBuildTarArchive(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
configWithoutIgnore,
rootDir,
path.relative(rootDir, distDir),
);

const files: string[] = [];
tar.list({
file: tarballPath,
sync: true,
onentry: (entry: { path: string }) => files.push(entry.path),
});

expect(files).to.include("dist/index.js");
expect(files).to.not.include("dist/node_modules/some-file.js");
});

it("should respect .gitignore patterns", async () => {
fs.writeFileSync(path.join(distDir, "index.js"), "console.log('hello')");
fs.writeFileSync(path.join(distDir, "gitignored.txt"), "ignore me");
fs.writeFileSync(path.join(distDir, ".gitignore"), "gitignored.txt");

const config = {
backendId: "test-backend",
rootDir: "",
ignore: [],
};

const tarballPath: string = await util.createLocalBuildTarArchive(
config,
rootDir,
path.relative(rootDir, distDir),
);

const files: string[] = [];
tar.list({
file: tarballPath,
sync: true,
onentry: (entry: { path: string }) => files.push(entry.path),
});

expect(files).to.include("dist/index.js");
expect(files).to.not.include("dist/gitignored.txt");
});
});
});
29 changes: 23 additions & 6 deletions src/deploy/apphosting/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function createLocalBuildTarArchive(
const tmpFile = tmp.fileSync({ prefix: `${config.backendId}-`, postfix: ".tar.gz" }).name;

const targetDir = targetSubDir ? path.join(rootDir, targetSubDir) : rootDir;
const ignore = ["firebase-debug.log", "firebase-debug.*.log", ".git"];
const ignore = resolveIgnorePatterns(config, targetDir);
const rdrFiles = await fsAsync.readdirRecursive({
path: targetDir,
ignore: ignore,
Expand Down Expand Up @@ -90,10 +90,7 @@ export async function createSourceDeployArchive(

const targetDir = targetSubDir ? path.join(rootDir, targetSubDir) : rootDir;
// We must ignore firebase-debug.log or weird things happen if you're in the public dir when you deploy.
const ignore = config.ignore || ["node_modules", ".git"];
ignore.push("firebase-debug.log", "firebase-debug.*.log");
const gitIgnorePatterns = parseGitIgnorePatterns(targetDir);
ignore.push(...gitIgnorePatterns);
const ignore = resolveIgnorePatterns(config, targetDir);
try {
const files = await fsAsync.readdirRecursive({
path: targetDir,
Expand All @@ -110,13 +107,33 @@ export async function createSourceDeployArchive(
await pipeAsync(archive, fileStream);
} catch (err: unknown) {
throw new FirebaseError(
`Could not read source directory. Remove links and shortcuts and try again. Original: ${err}`,
`Could not read source directory. Remove links and shortcuts and try again. Original: ${String(err)}`,
{ original: err as Error, exit: 1 },
);
}
return tmpFile;
}

/**
* Resolves the ignore patterns for App Hosting deployments.
* Merges config ignores, defaults, and .gitignore patterns.
*/
export function resolveIgnorePatterns(
config: AppHostingSingle,
targetDir: string,
skipDefaultNodeModules = false,
): string[] {
const ignore = config.ignore
? [...config.ignore]
: skipDefaultNodeModules
? [".git"]
: ["node_modules", ".git"];
ignore.push("firebase-debug.log", "firebase-debug.*.log");
const gitIgnorePatterns = parseGitIgnorePatterns(targetDir);
ignore.push(...gitIgnorePatterns);
return ignore;
}

function parseGitIgnorePatterns(projectRoot: string, gitIgnorePath = ".gitignore"): string[] {
const absoluteFilePath = path.resolve(projectRoot, gitIgnorePath);
if (!fs.existsSync(absoluteFilePath)) {
Expand Down
Loading