Skip to content

Commit bb70f00

Browse files
authored
Add output validations (#147)
* Add output validations * Update utils.ts * check for bundle.yaml * check for bundleyaml
1 parent 52f781e commit bb70f00

File tree

7 files changed

+99
-33
lines changed

7 files changed

+99
-33
lines changed

packages/@apphosting/adapter-angular/src/bin/build.spec.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { resolve } from "path";
44
import fs from "fs";
55
import path from "path";
66
import os from "os";
7-
import { OutputPathOptions } from "../interface.js";
7+
import { OutputBundleOptions } from "../interface.js";
88

99
describe("build commands", () => {
1010
let tmpDir: string;
11-
let outputPathOptions: OutputPathOptions;
11+
let outputBundleOptions: OutputBundleOptions;
1212
beforeEach(() => {
1313
tmpDir = generateTmpDir();
14-
outputPathOptions = {
14+
outputBundleOptions = {
1515
baseDirectory: resolve(tmpDir, "dist", "test"),
1616
browserDirectory: resolve(tmpDir, ".apphosting", "dist", "browser"),
1717
bundleYamlPath: resolve(tmpDir, ".apphosting", "bundle.yaml"),
@@ -23,13 +23,14 @@ describe("build commands", () => {
2323
});
2424

2525
it("expects all output bundle files to be generated", async () => {
26-
const { generateOutputDirectory } = await importUtils;
26+
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
2727
const files = {
2828
"dist/test/browser/browserfile": "",
2929
"dist/test/server/server.mjs": "",
3030
};
3131
generateTestFiles(tmpDir, files);
32-
await generateOutputDirectory(tmpDir, outputPathOptions);
32+
await generateOutputDirectory(tmpDir, outputBundleOptions);
33+
await validateOutputDirectory(outputBundleOptions);
3334

3435
const expectedFiles = {
3536
".apphosting/dist/browser/browserfile": "",
@@ -46,6 +47,16 @@ staticAssets:
4647
};
4748
validateTestFiles(tmpDir, expectedFiles);
4849
});
50+
it("test failed validateOutputDirectory", async () => {
51+
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
52+
const files = {
53+
"dist/test/browser/browserfile": "",
54+
"dist/test/server/notserver.mjs": "",
55+
};
56+
generateTestFiles(tmpDir, files);
57+
await generateOutputDirectory(tmpDir, outputBundleOptions);
58+
assert.rejects(async () => await validateOutputDirectory(outputBundleOptions));
59+
});
4960

5061
it("test populate output bundle options", async () => {
5162
const { populateOutputBundleOptions } = await importUtils;
@@ -55,6 +66,7 @@ staticAssets:
5566
bundleYamlPath: resolve(".apphosting", "bundle.yaml"),
5667
outputBaseDirectory: resolve(".apphosting", "dist"),
5768
outputDirectory: resolve("", ".apphosting"),
69+
needsServerGenerated: false,
5870
serverFilePath: resolve(".apphosting", "server", "server.mjs"),
5971
needsServerGenerated: false,
6072
};

packages/@apphosting/adapter-angular/src/bin/build.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DEFAULT_COMMAND,
66
checkStandaloneBuildConditions,
77
checkMonorepoBuildConditions,
8+
validateOutputDirectory,
89
} from "../utils.js";
910

1011
const root = process.cwd();
@@ -18,7 +19,6 @@ if (process.env.MONOREPO_PROJECT && process.env.FIREBASE_APP_DIRECTORY) {
1819
} else {
1920
await checkStandaloneBuildConditions(projectRoot);
2021
}
21-
2222
// Determine which build runner to use
2323
let cmd = DEFAULT_COMMAND;
2424
if (process.env.MONOREPO_COMMAND) {
@@ -27,3 +27,5 @@ if (process.env.MONOREPO_COMMAND) {
2727

2828
const outputBundleOptions = await build(projectRoot, cmd);
2929
await generateOutputDirectory(root, outputBundleOptions);
30+
31+
await validateOutputDirectory(outputBundleOptions);

packages/@apphosting/adapter-angular/src/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { z } from "zod";
22
import { URL } from "node:url";
33

44
// options to help generate output directory
5-
export interface OutputPathOptions {
5+
export interface OutputBundleOptions {
66
bundleYamlPath: string;
77
outputDirectory: string;
88
baseDirectory: string;

packages/@apphosting/adapter-angular/src/utils.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { fileURLToPath } from "url";
55
import { spawn } from "child_process";
66
import { resolve, normalize, relative, dirname, join } from "path";
77
import { stringify as yamlStringify } from "yaml";
8-
import { OutputPathOptions, OutputPaths, buildManifestSchema, ValidManifest } from "./interface.js";
8+
import {
9+
OutputBundleOptions,
10+
OutputPaths,
11+
buildManifestSchema,
12+
ValidManifest,
13+
} from "./interface.js";
914
import stripAnsi from "strip-ansi";
1015

1116
// fs-extra is CJS, readJson can't be imported using shorthand
@@ -64,7 +69,7 @@ export function checkMonorepoBuildConditions(builder: string): void {
6469
}
6570

6671
// Populate file or directory paths we need for generating output directory
67-
export function populateOutputBundleOptions(outputPaths: OutputPaths): OutputPathOptions {
72+
export function populateOutputBundleOptions(outputPaths: OutputPaths): OutputBundleOptions {
6873
const outputBundleDir = resolve(".apphosting");
6974

7075
const baseDirectory = fileURLToPath(outputPaths["root"]);
@@ -91,7 +96,7 @@ export function populateOutputBundleOptions(outputPaths: OutputPaths): OutputPat
9196
export const build = (
9297
projectRoot = process.cwd(),
9398
cmd = DEFAULT_COMMAND,
94-
): Promise<OutputPathOptions> =>
99+
): Promise<OutputBundleOptions> =>
95100
new Promise((resolve, reject) => {
96101
// enable JSON build logs for application builder
97102
process.env.NG_BUILD_LOGS_JSON = "1";
@@ -161,42 +166,56 @@ function extractManifestOutput(output: string): string {
161166
*/
162167
export async function generateOutputDirectory(
163168
cwd: string,
164-
outputPathOptions: OutputPathOptions,
169+
outputBundleOptions: OutputBundleOptions,
165170
): Promise<void> {
166-
await move(outputPathOptions.baseDirectory, outputPathOptions.outputBaseDirectory, {
171+
await move(outputBundleOptions.baseDirectory, outputBundleOptions.outputBaseDirectory, {
167172
overwrite: true,
168173
});
169-
if (outputPathOptions.needsServerGenerated) {
170-
await generateServer(outputPathOptions);
174+
if (outputBundleOptions.needsServerGenerated) {
175+
await generateServer(outputBundleOptions);
171176
}
172-
await generateBundleYaml(outputPathOptions, cwd);
177+
await generateBundleYaml(outputBundleOptions, cwd);
173178
}
174179

175180
// Generate bundle.yaml
176181
async function generateBundleYaml(
177-
outputPathOptions: OutputPathOptions,
182+
outputBundleOptions: OutputBundleOptions,
178183
cwd: string,
179184
): Promise<void> {
180185
await writeFile(
181-
outputPathOptions.bundleYamlPath,
186+
outputBundleOptions.bundleYamlPath,
182187
yamlStringify({
183188
headers: [],
184189
redirects: [],
185190
rewrites: [],
186191
// this fix is needed for Angular version 17.3.2
187192
runCommand: `SSR_PORT=$PORT node ${normalize(
188-
relative(cwd, outputPathOptions.serverFilePath),
193+
relative(cwd, outputBundleOptions.serverFilePath),
189194
)}`,
190-
neededDirs: [normalize(relative(cwd, outputPathOptions.outputDirectory))],
191-
staticAssets: [normalize(relative(cwd, outputPathOptions.browserDirectory))],
195+
neededDirs: [normalize(relative(cwd, outputBundleOptions.outputDirectory))],
196+
staticAssets: [normalize(relative(cwd, outputBundleOptions.browserDirectory))],
192197
}),
193198
);
194199
}
195200

196201
// Generate server file for CSR apps
197-
async function generateServer(outputPathOptions: OutputPathOptions): Promise<void> {
198-
await mkdir(dirname(outputPathOptions.serverFilePath));
199-
await copyFile(SIMPLE_SERVER_FILE_PATH, outputPathOptions.serverFilePath);
202+
async function generateServer(outputBundleOptions: OutputBundleOptions): Promise<void> {
203+
await mkdir(dirname(outputBundleOptions.serverFilePath));
204+
await copyFile(SIMPLE_SERVER_FILE_PATH, outputBundleOptions.serverFilePath);
205+
}
206+
207+
// Validate output directory includes all necessary parts
208+
export async function validateOutputDirectory(
209+
outputBundleOptions: OutputBundleOptions,
210+
): Promise<void> {
211+
if (
212+
!(await fsExtra.exists(outputBundleOptions.outputDirectory)) ||
213+
!(await fsExtra.exists(outputBundleOptions.browserDirectory)) ||
214+
!(await fsExtra.exists(outputBundleOptions.serverFilePath)) ||
215+
!(await fsExtra.exists(outputBundleOptions.bundleYamlPath))
216+
) {
217+
throw new Error("Output directory is not of expected structure");
218+
}
200219
}
201220

202221
export const isMain = (meta: ImportMeta) => {

packages/@apphosting/adapter-nextjs/src/bin/build.spec.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ describe("build commands", () => {
2020
});
2121

2222
it("expects all output bundle files to be generated", async () => {
23-
const { generateOutputDirectory } = await importUtils;
23+
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
2424
const files = {
25-
".next/standalone/standalonefile": "",
25+
".next/standalone/server.js": "",
2626
".next/static/staticfile": "",
2727
".next/routes-manifest.json": `{
2828
"headers":[],
@@ -32,10 +32,11 @@ describe("build commands", () => {
3232
};
3333
generateTestFiles(tmpDir, files);
3434
await generateOutputDirectory(tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
35+
await validateOutputDirectory(outputBundleOptions);
3536

3637
const expectedFiles = {
3738
".apphosting/.next/static/staticfile": "",
38-
".apphosting/standalonefile": "",
39+
".apphosting/server.js": "",
3940
".apphosting/bundle.yaml": `headers: []
4041
redirects: []
4142
rewrites: []
@@ -50,9 +51,9 @@ staticAssets:
5051
});
5152

5253
it("expects public directory to be copied over", async () => {
53-
const { generateOutputDirectory } = await importUtils;
54+
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
5455
const files = {
55-
".next/standalone/standalonefile": "",
56+
".next/standalone/server.js": "",
5657
".next/static/staticfile": "",
5758
"public/publicfile": "",
5859
".next/routes-manifest.json": `{
@@ -63,10 +64,11 @@ staticAssets:
6364
};
6465
generateTestFiles(tmpDir, files);
6566
await generateOutputDirectory(tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
67+
await validateOutputDirectory(outputBundleOptions);
6668

6769
const expectedFiles = {
6870
".apphosting/.next/static/staticfile": "",
69-
".apphosting/standalonefile": "",
71+
".apphosting/server.js": "",
7072
".apphosting/public/publicfile": "",
7173
".apphosting/bundle.yaml": `headers: []
7274
redirects: []
@@ -82,9 +84,9 @@ staticAssets:
8284
});
8385

8486
it("expects bundle.yaml headers/rewrites/redirects to be generated", async () => {
85-
const { generateOutputDirectory } = await importUtils;
87+
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
8688
const files = {
87-
".next/standalone/standalonefile": "",
89+
".next/standalone/server.js": "",
8890
".next/static/staticfile": "",
8991
".next/routes-manifest.json": `{
9092
"headers":[{"source":"source", "headers":["header1"]}],
@@ -94,10 +96,11 @@ staticAssets:
9496
};
9597
generateTestFiles(tmpDir, files);
9698
await generateOutputDirectory(tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
99+
await validateOutputDirectory(outputBundleOptions);
97100

98101
const expectedFiles = {
99102
".apphosting/.next/static/staticfile": "",
100-
".apphosting/standalonefile": "",
103+
".apphosting/server.js": "",
101104
".apphosting/bundle.yaml": `headers:
102105
- source: source
103106
headers:
@@ -117,7 +120,21 @@ staticAssets:
117120
};
118121
validateTestFiles(tmpDir, expectedFiles);
119122
});
120-
123+
it("test failed validateOutputDirectory", async () => {
124+
const { generateOutputDirectory, validateOutputDirectory } = await importUtils;
125+
const files = {
126+
".next/standalone/notserver.js": "",
127+
".next/static/staticfile": "",
128+
".next/routes-manifest.json": `{
129+
"headers":[{"source":"source", "headers":["header1"]}],
130+
"rewrites":[{"source":"source", "destination":"destination"}],
131+
"redirects":[{"source":"source", "destination":"destination"}]
132+
}`,
133+
};
134+
generateTestFiles(tmpDir, files);
135+
await generateOutputDirectory(tmpDir, outputBundleOptions, path.join(tmpDir, ".next"));
136+
assert.rejects(async () => await validateOutputDirectory(outputBundleOptions));
137+
});
121138
it("test populate output bundle options", async () => {
122139
const { populateOutputBundleOptions } = await importUtils;
123140
const expectedOutputBundleOptions = {

packages/@apphosting/adapter-nextjs/src/bin/build.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
build,
55
populateOutputBundleOptions,
66
generateOutputDirectory,
7+
validateOutputDirectory,
78
} from "../utils.js";
89

910
import { join } from "path";
@@ -17,3 +18,5 @@ const { distDir } = await loadConfig(cwd);
1718
const nextBuildDirectory = join(cwd, distDir);
1819

1920
await generateOutputDirectory(cwd, outputBundleOptions, nextBuildDirectory);
21+
22+
await validateOutputDirectory(outputBundleOptions);

packages/@apphosting/adapter-nextjs/src/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,16 @@ async function generateBundleYaml(
109109
);
110110
return;
111111
}
112+
113+
// Validate output directory includes all necessary parts
114+
export async function validateOutputDirectory(
115+
outputBundleOptions: OutputBundleOptions,
116+
): Promise<void> {
117+
if (
118+
!(await fsExtra.exists(outputBundleOptions.outputDirectory)) ||
119+
!(await fsExtra.exists(outputBundleOptions.serverFilePath)) ||
120+
!(await fsExtra.exists(outputBundleOptions.bundleYamlPath))
121+
) {
122+
throw new Error("Output directory is not of expected structure");
123+
}
124+
}

0 commit comments

Comments
 (0)