Skip to content

Commit 67cc1cc

Browse files
committed
Add some gross hacks to stop bundle.yaml from being wiped empty
1 parent c573c6a commit 67cc1cc

2 files changed

Lines changed: 118 additions & 21 deletions

File tree

src/apphosting/localbuilds.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { loadSecret } from "./secrets";
88
import { confirm } from "../prompt";
99
import { FirebaseError } from "../error";
1010
import * as experiments from "../experiments";
11+
import { logger } from "../logger";
1112

1213
interface UniversalMakerOutput {
1314
command: string;
@@ -27,6 +28,28 @@ export function runUniversalMaker(projectRoot: string, framework?: string): AppH
2728
);
2829
}
2930

31+
const bundleYamlPath = path.join(projectRoot, ".apphosting", "bundle.yaml");
32+
let cachedBundleContent: string | null = null;
33+
34+
if (!fs.existsSync(path.dirname(bundleYamlPath))) {
35+
fs.mkdirSync(path.dirname(bundleYamlPath), { recursive: true });
36+
}
37+
38+
// Watch for the file being created by the Next.js adapter
39+
const watcher = fs.watch(path.dirname(bundleYamlPath), (eventType, filename) => {
40+
if (filename === "bundle.yaml") {
41+
try {
42+
if (fs.existsSync(bundleYamlPath)) {
43+
const currentText = fs.readFileSync(bundleYamlPath, "utf-8");
44+
if (currentText.trim().length > 0) {
45+
cachedBundleContent = currentText;
46+
}
47+
}
48+
} catch (e) {
49+
}
50+
}
51+
});
52+
3053
try {
3154
childProcess.spawnSync(
3255
process.env.UNIVERSAL_MAKER_BINARY,
@@ -41,6 +64,17 @@ export function runUniversalMaker(projectRoot: string, framework?: string): AppH
4164
stdio: "inherit",
4265
},
4366
);
67+
68+
// Close the background watcher safely
69+
watcher.close();
70+
71+
// Restore safely if wiped out in the final seconds
72+
if (cachedBundleContent && fs.existsSync(bundleYamlPath)) {
73+
const lastText = fs.readFileSync(bundleYamlPath, "utf-8");
74+
if (lastText.trim().length === 0) {
75+
fs.writeFileSync(bundleYamlPath, cachedBundleContent, "utf-8");
76+
}
77+
}
4478
} catch (e) {
4579
if (e && typeof e === "object" && "code" in e && e.code === "EACCES") {
4680
throw new FirebaseError(
@@ -65,14 +99,28 @@ export function runUniversalMaker(projectRoot: string, framework?: string): AppH
6599
throw new FirebaseError(`Failed to parse build_output.json: ${(e as Error).message}`);
66100
}
67101

102+
let finalRunCommand = `${umOutput.command} ${umOutput.args.join(" ")}`;
103+
if (fs.existsSync(bundleYamlPath)) {
104+
try {
105+
const bundleRaw = fs.readFileSync(bundleYamlPath, "utf-8");
106+
// Safely parse the YAML string
107+
const bundleData = require("yaml").parse(bundleRaw);
108+
if (bundleData?.runConfig?.runCommand) {
109+
finalRunCommand = bundleData.runConfig.runCommand;
110+
}
111+
} catch (e) {
112+
// Fall back gracefully if parser fails
113+
}
114+
}
115+
68116
return {
69117
metadata: {
70118
language: umOutput.language,
71119
runtime: umOutput.runtime,
72120
framework: framework || "nextjs",
73121
},
74122
runConfig: {
75-
runCommand: `${umOutput.command} ${umOutput.args.join(" ")}`,
123+
runCommand: finalRunCommand,
76124
environmentVariables: Object.entries(umOutput.envVars || {}).map(([k, v]) => ({
77125
variable: k,
78126
value: String(v),
@@ -167,6 +215,9 @@ export async function localBuild(
167215
try {
168216
if (experiments.isEnabled("universalMaker")) {
169217
apphostingBuildOutput = runUniversalMaker(projectRoot, framework);
218+
logger.debug(
219+
`[apphosting] Universal Maker build outputFiles include: ${JSON.stringify(apphostingBuildOutput.outputFiles?.serverApp?.include ?? [])}`,
220+
);
170221
} else {
171222
apphostingBuildOutput = (await localAppHostingBuild(
172223
projectRoot,

src/deploy/apphosting/util.ts

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,47 +27,93 @@ export async function createLocalBuildTarArchive(
2727
): Promise<string> {
2828
const tmpFile = tmp.fileSync({ prefix: `${config.backendId}-`, postfix: ".tar.gz" }).name;
2929

30-
const targetDir = targetSubDir ? path.join(rootDir, targetSubDir) : rootDir;
30+
const isAppHostingDir =
31+
targetSubDir === ".apphosting" ||
32+
(!!targetSubDir && path.basename(targetSubDir) === ".apphosting");
33+
const targetDir = targetSubDir
34+
? path.isAbsolute(targetSubDir)
35+
? targetSubDir
36+
: path.join(rootDir, targetSubDir)
37+
: rootDir;
3138
const ignore = ["firebase-debug.log", "firebase-debug.*.log", ".git"];
32-
const rdrFiles = await fsAsync.readdirRecursive({
33-
path: targetDir,
34-
ignore: ignore,
35-
isGitIgnore: true,
36-
});
37-
const allFiles: string[] = rdrFiles.map((rdrf) => path.relative(rootDir, rdrf.name));
3839

39-
if (targetSubDir) {
40-
const defaultFiles = fs.readdirSync(rootDir).filter((file) => {
41-
return APPHOSTING_YAML_FILE_REGEX.test(file);
40+
let archiveCwd = rootDir;
41+
let pathsToPack: string[];
42+
43+
if (isAppHostingDir) {
44+
// create temporary directory to bundle things flattened
45+
const tempDir = tmp.dirSync({ unsafeCleanup: true }).name;
46+
fs.cpSync(targetDir, tempDir, { recursive: true });
47+
48+
const rootPackageJson = path.join(rootDir, "package.json");
49+
if (fs.existsSync(rootPackageJson)) {
50+
fs.copyFileSync(rootPackageJson, path.join(tempDir, "package.json"));
51+
}
52+
const rootFiles = fs.readdirSync(rootDir);
53+
for (const file of rootFiles) {
54+
if (APPHOSTING_YAML_FILE_REGEX.test(file)) {
55+
fs.copyFileSync(path.join(rootDir, file), path.join(tempDir, file));
56+
}
57+
}
58+
const rootNext = path.join(rootDir, ".next");
59+
if (fs.existsSync(rootNext)) {
60+
fs.cpSync(rootNext, path.join(tempDir, ".next"), { recursive: true });
61+
}
62+
const rootNodeModules = path.join(rootDir, "node_modules");
63+
if (fs.existsSync(rootNodeModules)) {
64+
fs.cpSync(rootNodeModules, path.join(tempDir, "node_modules"), { recursive: true });
65+
}
66+
67+
const rdrFiles = await fsAsync.readdirRecursive({
68+
path: tempDir,
69+
ignore: ignore,
70+
isGitIgnore: false,
71+
});
72+
pathsToPack = rdrFiles.map((rdrf) => path.relative(tempDir, rdrf.name));
73+
archiveCwd = tempDir;
74+
} else {
75+
const rdrFiles = await fsAsync.readdirRecursive({
76+
path: targetDir,
77+
ignore: ignore,
78+
isGitIgnore: !targetSubDir, // Disable gitignore if we are anchored to a build output subdirectory
4279
});
43-
for (const file of defaultFiles) {
44-
if (!allFiles.includes(file)) {
45-
allFiles.push(file);
80+
pathsToPack = rdrFiles.map((rdrf) => path.relative(rootDir, rdrf.name));
81+
82+
if (targetSubDir) {
83+
const defaultFiles = fs.readdirSync(rootDir).filter((file) => {
84+
return APPHOSTING_YAML_FILE_REGEX.test(file);
85+
});
86+
for (const file of defaultFiles) {
87+
const relativePath = path.relative(rootDir, path.join(rootDir, file));
88+
if (!pathsToPack.includes(relativePath)) {
89+
pathsToPack.push(relativePath);
90+
}
4691
}
4792
}
4893
}
4994

50-
// `tar` returns a `TypeError` if `allFiles` is empty. Let's check a feww things.
5195
try {
52-
fs.statSync(rootDir);
96+
fs.statSync(archiveCwd);
5397
} catch (err: unknown) {
5498
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
55-
throw new FirebaseError(`Could not read directory "${rootDir}"`);
99+
throw new FirebaseError(`Could not read directory "${archiveCwd}"`);
56100
}
57101
throw err;
58102
}
59-
if (!allFiles.length) {
60-
throw new FirebaseError(`Cannot create a tar archive with 0 files from directory "${rootDir}"`);
103+
if (!pathsToPack.length) {
104+
throw new FirebaseError(
105+
`Cannot create a tar archive with 0 files from directory "${archiveCwd}"`,
106+
);
61107
}
62108

63109
await tar.create(
64110
{
65111
gzip: true,
66112
file: tmpFile,
67-
cwd: rootDir,
113+
cwd: archiveCwd,
68114
portable: true,
69115
},
70-
allFiles,
116+
pathsToPack,
71117
);
72118
return tmpFile;
73119
}

0 commit comments

Comments
 (0)