Skip to content

Commit 5880bc7

Browse files
authored
Refactor adapter build configuration logic into @apphosting/common module (#204)
* refactor build configuration logic into @apphosting/common pkg * update deps to include shared module * provide runBuild() fn * update github action for publish common pkg
1 parent d134b18 commit 5880bc7

File tree

13 files changed

+2749
-2692
lines changed

13 files changed

+2749
-2692
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,5 +153,6 @@ jobs:
153153
env:
154154
ADAPTER_NEXTJS_NPM_TOKEN: ${{ secrets.ADAPTER_NEXTJS_NPM_TOKEN }}
155155
ADAPTER_ANGULAR_NPM_TOKEN: ${{ secrets.ADAPTER_ANGULAR_NPM_TOKEN }}
156+
ADAPTER_COMMON_NPM_TOKEN: ${{ secrets.ADAPTER_COMMON_NPM_TOKEN }}
156157
# FIREBASE_FRAMEWORKS_NPM_TOKEN: ${{ secrets.FIREBASE_FRAMEWORKS_NPM_TOKEN }}
157158
NODE_AUTH_TOKEN: ${{ secrets.PERSONAL_NPM_TOKEN }}

package-lock.json

Lines changed: 2493 additions & 2539 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"lint:fix": "eslint packages/* scripts/* --fix"
1212
},
1313
"workspaces": [
14-
"packages/**/*"
14+
"packages/@apphosting/*",
15+
"packages/create-next-on-firebase/*",
16+
"packages/firebase-frameworks/*"
1517
],
1618
"devDependencies": {
1719
"@angular-devkit/build-angular": "^17.1.2",

packages/@apphosting/adapter-angular/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
"sideEffects": false,
2323
"scripts": {
2424
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*",
25-
"test": "ts-mocha -p tsconfig.json src/**/*.spec.ts"
25+
"test": "ts-mocha -p tsconfig.json src/**/*.spec.ts",
26+
"localregistry:start": "npx verdaccio --config ../publish-dev/verdaccio-config.yaml",
27+
"localregistry:publish": "(npm view --registry=http://localhost:4873 @apphosting/adapter-angular && npm unpublish --@apphosting:registry=http://localhost:4873 --force); npm publish --@apphosting:registry=http://localhost:4873"
2628
},
2729
"exports": {
2830
".": {
@@ -39,6 +41,7 @@
3941
],
4042
"license": "Apache-2.0",
4143
"dependencies": {
44+
"@apphosting/common": "^1.0.0",
4245
"firebase-functions": "^4.3.1",
4346
"fs-extra": "^11.1.1",
4447
"strip-ansi": "^7.1.0",
@@ -70,6 +73,7 @@
7073
"tmp": "*",
7174
"ts-mocha": "*",
7275
"ts-node": "*",
73-
"typescript": "*"
76+
"typescript": "*",
77+
"verdaccio": "^5.30.3"
7478
}
7579
}
Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,25 @@
11
#! /usr/bin/env node
22
import {
3-
build,
43
generateOutputDirectory,
5-
DEFAULT_COMMAND,
6-
checkStandaloneBuildConditions,
7-
checkMonorepoBuildConditions,
4+
checkBuildConditions,
85
validateOutputDirectory,
6+
parseOutputBundleOptions,
97
} from "../utils.js";
10-
import { join } from "path";
8+
import { getBuildOptions, runBuild } from "@apphosting/common";
119

1210
const root = process.cwd();
13-
14-
// TODO(blidd-google): Refactor monorepo logic into separate module
15-
16-
// Determine which project in a monorepo to build. The environment variable will only exist when
17-
// a monorepo has been detected in the parent buildpacks, so it can also be used to determine
18-
// whether the project we are building is in a monorepo setup.
19-
const project = process.env.MONOREPO_PROJECT || "";
20-
21-
// Determine root of project to build.
22-
let projectRoot = root;
23-
// N.B. We don't want to change directories for monorepo builds, so that the build process can
24-
// locate necessary files outside the project directory (e.g. at the root).
25-
if (process.env.FIREBASE_APP_DIRECTORY && !project) {
26-
projectRoot = join(root, process.env.FIREBASE_APP_DIRECTORY);
27-
}
28-
29-
// Determine which command to run the build
30-
const cmd = process.env.MONOREPO_COMMAND || DEFAULT_COMMAND;
31-
32-
// Parse args to pass to the build command
33-
let cmdArgs: string[] = [];
34-
if (process.env.MONOREPO_BUILD_ARGS) {
35-
cmdArgs = process.env.MONOREPO_BUILD_ARGS.split(",");
36-
}
11+
const opts = getBuildOptions();
3712

3813
// Check build conditions, which vary depending on your project structure (standalone or monorepo)
39-
if (project) {
40-
checkMonorepoBuildConditions(cmd, project);
41-
} else {
42-
await checkStandaloneBuildConditions(projectRoot);
43-
}
14+
await checkBuildConditions(opts);
4415

45-
const outputBundleOptions = await build(projectRoot, cmd, ...cmdArgs);
16+
// enable JSON build logs for application builder
17+
process.env.NG_BUILD_LOGS_JSON = "1";
18+
const { stdout: output } = await runBuild();
19+
if (!output) {
20+
throw new Error("No output from Angular build command, expecting a build manifest file.");
21+
}
22+
const outputBundleOptions = parseOutputBundleOptions(output);
4623
await generateOutputDirectory(root, outputBundleOptions);
4724

4825
await validateOutputDirectory(outputBundleOptions);

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

Lines changed: 38 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@ import fsExtra from "fs-extra";
22
import logger from "firebase-functions/logger";
33

44
import { fileURLToPath } from "url";
5-
import { spawn, execSync } from "child_process";
5+
import { execSync } from "child_process";
66
import { resolve, normalize, relative, dirname, join } from "path";
77
import { stringify as yamlStringify } from "yaml";
8-
import {
9-
OutputBundleOptions,
10-
OutputPaths,
11-
buildManifestSchema,
12-
ValidManifest,
13-
} from "./interface.js";
8+
import { OutputBundleOptions, OutputPaths, buildManifestSchema } from "./interface.js";
149
import { createRequire } from "node:module";
1510
import stripAnsi from "strip-ansi";
11+
import { BuildOptions } from "@apphosting/common";
1612

1713
// fs-extra is CJS, readJson can't be imported using shorthand
1814
export const { writeFile, move, readJson, mkdir, copyFile } = fsExtra;
@@ -22,29 +18,42 @@ const __filename = fileURLToPath(import.meta.url);
2218
const __dirname = dirname(__filename);
2319
const SIMPLE_SERVER_FILE_PATH = join(__dirname, "simple-server", "bundled_server.mjs");
2420

25-
export const DEFAULT_COMMAND = "npm";
2621
export const REQUIRED_BUILDER = "@angular-devkit/build-angular:application";
2722

2823
/**
2924
* Check if the following build conditions are satisfied for the workspace:
3025
* - The workspace does not contain multiple angular projects.
3126
* - The angular project must be using the application builder.
3227
*/
33-
export async function checkStandaloneBuildConditions(cwd: string): Promise<void> {
28+
export async function checkBuildConditions(opts: BuildOptions): Promise<void> {
29+
// Nx uses a project.json file in lieu of an angular.json file, so if the app is in an Nx workspace,
30+
// we check if Nx's project.json configures the build to use the Angular application builder.
31+
if (opts.buildCommand === "nx") {
32+
const output = execSync(`npx nx show project ${opts.projectName}`);
33+
const projectJson = JSON.parse(output.toString());
34+
const builder = projectJson.targets.build.executor;
35+
if (builder !== REQUIRED_BUILDER) {
36+
throw new Error(
37+
"Only the Angular application builder is supported. Please refer to https://angular.dev/tools/cli/build-system-migration#for-existing-applications guide to upgrade your builder to the Angular application builder. ",
38+
);
39+
}
40+
return;
41+
}
42+
3443
// dynamically load Angular so this can be used in an NPX context
35-
const angularCorePath = require.resolve("@angular/core", { paths: [cwd] });
44+
const angularCorePath = require.resolve("@angular/core", { paths: [process.cwd()] });
3645
const { NodeJsAsyncHost }: typeof import("@angular-devkit/core/node") = await import(
3746
require.resolve("@angular-devkit/core/node", {
38-
paths: [cwd, angularCorePath],
47+
paths: [process.cwd(), angularCorePath],
3948
})
4049
);
4150
const { workspaces }: typeof import("@angular-devkit/core") = await import(
4251
require.resolve("@angular-devkit/core", {
43-
paths: [cwd, angularCorePath],
52+
paths: [process.cwd(), angularCorePath],
4453
})
4554
);
4655
const host = workspaces.createWorkspaceHost(new NodeJsAsyncHost());
47-
const { workspace } = await workspaces.readWorkspace(cwd, host);
56+
const { workspace } = await workspaces.readWorkspace(opts.projectDirectory, host);
4857

4958
const apps: string[] = [];
5059
workspace.projects.forEach((value, key) => {
@@ -67,23 +76,6 @@ export async function checkStandaloneBuildConditions(cwd: string): Promise<void>
6776
}
6877
}
6978

70-
/**
71-
* Check if the monorepo build system is using the Angular application builder.
72-
*/
73-
export function checkMonorepoBuildConditions(cmd: string, target: string) {
74-
let builder;
75-
if (cmd === "nx") {
76-
const output = execSync(`npx nx show project ${target}`);
77-
const projectJson = JSON.parse(output.toString());
78-
builder = projectJson.targets.build.executor;
79-
}
80-
if (builder !== REQUIRED_BUILDER) {
81-
throw new Error(
82-
"Only the Angular application builder is supported. Please refer to https://angular.dev/tools/cli/build-system-migration#for-existing-applications guide to upgrade your builder to the Angular application builder. ",
83-
);
84-
}
85-
}
86-
8779
// Populate file or directory paths we need for generating output directory
8880
export function populateOutputBundleOptions(outputPaths: OutputPaths): OutputBundleOptions {
8981
const outputBundleDir = resolve(".apphosting");
@@ -108,57 +100,24 @@ export function populateOutputBundleOptions(outputPaths: OutputPaths): OutputBun
108100
};
109101
}
110102

111-
// Run build command
112-
export const build = (
113-
projectRoot = process.cwd(),
114-
cmd = DEFAULT_COMMAND,
115-
...argv: string[]
116-
): Promise<OutputBundleOptions> =>
117-
new Promise((resolve, reject) => {
118-
// enable JSON build logs for application builder
119-
process.env.NG_BUILD_LOGS_JSON = "1";
120-
const childProcess = spawn(cmd, ["run", "build", ...argv], {
121-
cwd: projectRoot,
122-
shell: true,
123-
stdio: ["inherit", "pipe", "pipe"],
124-
});
125-
let buildOutput = "";
126-
let manifest = {} as ValidManifest;
127-
128-
childProcess.stdout.on("data", (data: Buffer) => {
129-
buildOutput += data.toString();
103+
export function parseOutputBundleOptions(buildOutput: string): OutputBundleOptions {
104+
const strippedManifest = extractManifestOutput(buildOutput);
105+
const parsedManifest = JSON.parse(strippedManifest) as string;
106+
const manifest = buildManifestSchema.parse(parsedManifest);
107+
if (manifest["errors"].length > 0) {
108+
// errors when extracting manifest
109+
manifest.errors.forEach((error) => {
110+
logger.error(error);
130111
});
131-
132-
childProcess.on("exit", (code) => {
133-
if (code !== 0) {
134-
reject(new Error(`Process exited with error code ${code}. Output: ${buildOutput}`));
135-
}
136-
if (!buildOutput) {
137-
reject(new Error("Unable to locate build manifest with output paths."));
138-
}
139-
try {
140-
const strippedManifest = extractManifestOutput(buildOutput);
141-
const parsedManifest = JSON.parse(strippedManifest) as string;
142-
// validate if the manifest is of the expected form
143-
manifest = buildManifestSchema.parse(parsedManifest);
144-
if (manifest["errors"].length > 0) {
145-
// errors when extracting manifest
146-
manifest.errors.forEach((error) => {
147-
logger.error(error);
148-
});
149-
}
150-
if (manifest["warnings"].length > 0) {
151-
// warnings when extracting manifest
152-
manifest.warnings.forEach((warning) => {
153-
logger.info(warning);
154-
});
155-
}
156-
resolve(populateOutputBundleOptions(manifest["outputPaths"]));
157-
} catch (error) {
158-
reject(new Error("Build manifest is not of expected structure: " + error));
159-
}
112+
}
113+
if (manifest["warnings"].length > 0) {
114+
// warnings when extracting manifest
115+
manifest.warnings.forEach((warning) => {
116+
logger.info(warning);
160117
});
161-
});
118+
}
119+
return populateOutputBundleOptions(manifest["outputPaths"]);
120+
}
162121

163122
/**
164123
* Extracts the build manifest from the build command's console output.

packages/@apphosting/adapter-nextjs/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
"sideEffects": false,
2323
"scripts": {
2424
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*",
25-
"test": "ts-mocha -p tsconfig.json src/**/*.spec.ts"
25+
"test": "ts-mocha -p tsconfig.json src/**/*.spec.ts",
26+
"localregistry:start": "npx verdaccio --config ../publish-dev/verdaccio-config.yaml",
27+
"localregistry:publish": "(npm view --registry=http://localhost:4873 @apphosting/adapter-nextjs && npm unpublish --@apphosting:registry=http://localhost:4873 --force); npm publish --@apphosting:registry=http://localhost:4873"
2628
},
2729
"exports": {
2830
".": {
@@ -39,6 +41,7 @@
3941
],
4042
"license": "Apache-2.0",
4143
"dependencies": {
44+
"@apphosting/common": "^1.0.0",
4245
"fs-extra": "^11.1.1",
4346
"yaml": "^2.3.4"
4447
},
@@ -56,6 +59,7 @@
5659
"next": "~14.0.0",
5760
"semver": "*",
5861
"tmp": "*",
59-
"ts-node": "*"
62+
"ts-node": "*",
63+
"verdaccio": "^5.30.3"
6064
}
6165
}
Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,25 @@
11
#! /usr/bin/env node
22
import {
33
loadConfig,
4-
build,
54
populateOutputBundleOptions,
65
generateOutputDirectory,
7-
DEFAULT_COMMAND,
86
validateOutputDirectory,
97
} from "../utils.js";
108
import { join } from "path";
9+
import { getBuildOptions, runBuild } from "@apphosting/common";
1110

1211
const root = process.cwd();
12+
const opts = getBuildOptions();
1313

14-
let projectRoot = root;
15-
if (process.env.FIREBASE_APP_DIRECTORY) {
16-
projectRoot = join(root, process.env.FIREBASE_APP_DIRECTORY);
17-
}
14+
// Set standalone mode
15+
process.env.NEXT_PRIVATE_STANDALONE = "true";
16+
// Opt-out sending telemetry to Vercel
17+
process.env.NEXT_TELEMETRY_DISABLED = "1";
18+
await runBuild();
1819

19-
// Parse args to pass to the build command
20-
let cmdArgs: string[] = [];
21-
if (process.env.MONOREPO_BUILD_ARGS) {
22-
cmdArgs = process.env.MONOREPO_BUILD_ARGS.split(",");
23-
}
20+
const outputBundleOptions = populateOutputBundleOptions(root, opts.projectDirectory);
21+
const { distDir } = await loadConfig(root, opts.projectDirectory);
22+
const nextBuildDirectory = join(opts.projectDirectory, distDir);
2423

25-
// Determine which command to run the build
26-
const cmd = process.env.MONOREPO_COMMAND || DEFAULT_COMMAND;
27-
28-
// Run build command from the subdirectory if specified.
29-
// N.B. We run the build command from the root for monorepo builds, so that the build process can
30-
// locate necessary files outside the project directory.
31-
build(process.env.MONOREPO_CMD ? root : projectRoot, cmd, ...cmdArgs);
32-
33-
const outputBundleOptions = populateOutputBundleOptions(root, projectRoot);
34-
const { distDir } = await loadConfig(root, projectRoot);
35-
const nextBuildDirectory = join(projectRoot, distDir);
36-
37-
await generateOutputDirectory(root, projectRoot, outputBundleOptions, nextBuildDirectory);
24+
await generateOutputDirectory(root, opts.projectDirectory, outputBundleOptions, nextBuildDirectory);
3825
await validateOutputDirectory(outputBundleOptions);

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { spawnSync } from "child_process";
21
import fsExtra from "fs-extra";
32
import { createRequire } from "node:module";
43
import { join, relative, normalize } from "path";
@@ -70,15 +69,6 @@ export function populateOutputBundleOptions(rootDir: string, appDir: string): Ou
7069
};
7170
}
7271

73-
// Run build command
74-
export function build(cwd: string, cmd = DEFAULT_COMMAND, ...argv: string[]): void {
75-
// Set standalone mode
76-
process.env.NEXT_PRIVATE_STANDALONE = "true";
77-
// Opt-out sending telemetry to Vercel
78-
process.env.NEXT_TELEMETRY_DISABLED = "1";
79-
spawnSync(cmd, ["run", "build", ...argv], { cwd, shell: true, stdio: "inherit" });
80-
}
81-
8272
/**
8373
* Moves the standalone directory, the static directory and copies over all of the apps resources
8474
* to the apphosting output directory.

0 commit comments

Comments
 (0)