Skip to content

Commit 7f5c63f

Browse files
authored
Support adapter builds and add tests. (#386)
1 parent d44a968 commit 7f5c63f

File tree

11 files changed

+252
-53
lines changed

11 files changed

+252
-53
lines changed

package-lock.json

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

packages/@apphosting/build/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
runs
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as assert from "assert";
2+
import { posix } from "path";
3+
import type { OutputBundleConfig } from "../../common/src/index.ts";
4+
import { scenarios } from "./scenarios.ts";
5+
import fsExtra from "fs-extra";
6+
import { parse as parseYaml } from "yaml";
7+
8+
const { readFileSync } = fsExtra;
9+
10+
const scenario = process.env.SCENARIO;
11+
if (!scenario) {
12+
throw new Error("SCENARIO environment variable expected");
13+
}
14+
15+
const runId = process.env.RUN_ID;
16+
if (!runId) {
17+
throw new Error("RUN_ID environment variable expected");
18+
}
19+
20+
const bundleYaml = posix.join(process.cwd(), "e2e", "runs", runId, ".apphosting", "bundle.yaml");
21+
describe("supported framework apps", () => {
22+
it("apps have bundle.yaml correctly generated", () => {
23+
const bundle: OutputBundleConfig = parseYaml(readFileSync(bundleYaml, "utf8"));
24+
25+
assert.deepStrictEqual(scenarios.get(scenario).expectedBundleYaml.runConfig, bundle.runConfig);
26+
assert.deepStrictEqual(
27+
scenarios.get(scenario).expectedBundleYaml.metadata.adapterPackageName,
28+
bundle.metadata.adapterPackageName,
29+
);
30+
});
31+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { cp } from "fs/promises";
2+
import promiseSpawn from "@npmcli/promise-spawn";
3+
import { dirname, join, relative } from "path";
4+
import { fileURLToPath } from "url";
5+
import fsExtra from "fs-extra";
6+
import { scenarios } from "./scenarios.js";
7+
8+
const { readFileSync, mkdirp, rmdir } = fsExtra;
9+
10+
const __dirname = dirname(fileURLToPath(import.meta.url));
11+
12+
const errors: any[] = [];
13+
14+
await rmdir(join(__dirname, "runs"), { recursive: true }).catch(() => undefined);
15+
16+
// Run each scenario
17+
for (const [scenarioName, scenario] of scenarios) {
18+
console.log(
19+
`\n\n${"=".repeat(80)}\n${" ".repeat(
20+
5,
21+
)}RUNNING SCENARIO: ${scenarioName.toUpperCase()}${" ".repeat(5)}\n${"=".repeat(80)}`,
22+
);
23+
24+
const runId = `${scenarioName}-${Math.random().toString().split(".")[1]}`;
25+
const cwd = join(__dirname, "runs", runId);
26+
await mkdirp(cwd);
27+
28+
const starterTemplateDir = scenarioName.includes("nextjs")
29+
? "../../../starters/nextjs/basic"
30+
: "../../../starters/angular/basic";
31+
console.log(`[${runId}] Copying ${starterTemplateDir} to working directory`);
32+
await cp(starterTemplateDir, cwd, { recursive: true });
33+
34+
console.log(`[${runId}] > npm ci --silent --no-progress`);
35+
await promiseSpawn("npm", ["ci", "--silent", "--no-progress"], {
36+
cwd,
37+
stdio: "inherit",
38+
shell: true,
39+
});
40+
41+
const buildScript = relative(cwd, join(__dirname, "../dist/bin/localbuild.js"));
42+
const buildLogPath = join(cwd, "build.log");
43+
console.log(`[${runId}] > node ${buildScript} (output written to ${buildLogPath})`);
44+
45+
const packageJson = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
46+
const frameworkVersion = scenarioName.includes("nextjs")
47+
? packageJson.dependencies.next.replace("^", "")
48+
: JSON.parse(
49+
readFileSync(join(cwd, "node_modules", "@angular", "core", "package.json"), "utf-8"),
50+
).version;
51+
52+
try {
53+
const result = await promiseSpawn("node", [buildScript, ...scenario.inputs], {
54+
cwd,
55+
stdioString: true,
56+
stdio: "pipe",
57+
shell: true,
58+
env: {
59+
...process.env,
60+
FRAMEWORK_VERSION: frameworkVersion,
61+
},
62+
});
63+
// Write stdout and stderr to the log file
64+
fsExtra.writeFileSync(buildLogPath, result.stdout + result.stderr);
65+
66+
try {
67+
// Determine which test files to run
68+
const testPattern = scenario.tests
69+
? scenario.tests.map((test) => `e2e/${test}`).join(" ")
70+
: "e2e/*.spec.ts";
71+
72+
console.log(`> SCENARIO=${scenarioName} ts-mocha -p tsconfig.json ${testPattern}`);
73+
74+
await promiseSpawn("ts-mocha", ["-p", "tsconfig.json", ...testPattern.split(" ")], {
75+
shell: true,
76+
stdio: "inherit",
77+
env: {
78+
...process.env,
79+
SCENARIO: scenarioName,
80+
RUN_ID: runId,
81+
},
82+
});
83+
} catch (e) {
84+
errors.push(e);
85+
}
86+
} catch (e) {
87+
console.error(`Error in scenario ${scenarioName}:`, e);
88+
errors.push(e);
89+
}
90+
}
91+
if (errors.length) {
92+
console.error(errors);
93+
process.exit(1);
94+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
interface Scenario {
2+
inputs: string[];
3+
expectedBundleYaml: {};
4+
tests?: string[]; // List of test files to run
5+
}
6+
7+
export const scenarios: Map<string, Scenario> = new Map([
8+
[
9+
"nextjs-app",
10+
{
11+
inputs: ["./", "--framework", "nextjs"],
12+
expectedBundleYaml: {
13+
version: "v1",
14+
runConfig: {
15+
runCommand: "node .next/standalone/server.js",
16+
},
17+
metadata: {
18+
adapterPackageName: "@apphosting/adapter-nextjs",
19+
},
20+
},
21+
tests: ["adapter-builds.spec.ts"],
22+
},
23+
],
24+
[
25+
"angular-app",
26+
{
27+
inputs: ["./", "--framework", "angular"],
28+
expectedBundleYaml: {
29+
version: "v1",
30+
runConfig: {
31+
runCommand: "node dist/firebase-app-hosting-angular/server/server.mjs",
32+
environmentVariables: [],
33+
},
34+
metadata: {
35+
adapterPackageName: "@apphosting/adapter-angular",
36+
},
37+
},
38+
tests: ["adapter-builds.spec.ts"],
39+
},
40+
],
41+
]);

packages/@apphosting/build/package.json

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"url": "git+https://github.com/FirebaseExtended/firebase-framework-tools.git"
99
},
1010
"bin": {
11-
"build": "dist/bin/build.js",
12-
"apphosting-local-build": "dist/bin/localbuild.js"
11+
"build": "dist/bin/build.js",
12+
"apphosting-local-build": "dist/bin/localbuild.js"
1313
},
1414
"author": {
1515
"name": "Firebase",
@@ -21,12 +21,18 @@
2121
"type": "module",
2222
"sideEffects": false,
2323
"scripts": {
24-
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*"
24+
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*",
25+
"test": "npm run test:functional",
26+
"test:functional": "node --loader ts-node/esm ./e2e/run-local-build.ts"
2527
},
2628
"exports": {
2729
".": {
2830
"node": "./dist/index.js",
2931
"default": null
32+
},
33+
"./dist/*": {
34+
"node": "./dist/*",
35+
"default": null
3036
}
3137
},
3238
"files": [
@@ -41,6 +47,9 @@
4147
"ts-node": "^10.9.1"
4248
},
4349
"devDependencies": {
44-
"@types/commander": "*"
50+
"@types/commander": "*",
51+
"ts-mocha": "*",
52+
"ts-node": "*",
53+
"typescript": "*"
4554
}
4655
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import promiseSpawn from "@npmcli/promise-spawn";
2+
import { yellow, bgRed, bold } from "colorette";
3+
4+
export async function adapterBuild(projectRoot: string, framework: string) {
5+
// TODO(#382): We are using the latest framework adapter versions, but in the future
6+
// we should parse the framework version and use the matching adapter version.
7+
const adapterName = `@apphosting/adapter-${framework}`;
8+
const packumentResponse = await fetch(`https://registry.npmjs.org/${adapterName}`);
9+
if (!packumentResponse.ok)
10+
throw new Error(
11+
`Failed to fetch ${adapterName}: ${packumentResponse.status} ${packumentResponse.statusText}`,
12+
);
13+
let packument;
14+
try {
15+
packument = await packumentResponse.json();
16+
} catch (e) {
17+
throw new Error(`Failed to parse response from NPM registry for ${adapterName}.`);
18+
}
19+
const adapterVersion = packument?.["dist-tags"]?.["latest"];
20+
if (!adapterVersion) {
21+
throw new Error(`Could not find 'latest' dist-tag for ${adapterName}`);
22+
}
23+
// TODO(#382): should check for existence of adapter in app's package.json and use that version instead.
24+
25+
console.log(" 🔥", bgRed(` ${adapterName}@${yellow(bold(adapterVersion))} `), "\n");
26+
27+
const buildCommand = `apphosting-adapter-${framework}-build`;
28+
await promiseSpawn("npx", ["-y", "-p", `${adapterName}@${adapterVersion}`, buildCommand], {
29+
cwd: projectRoot,
30+
shell: true,
31+
stdio: "inherit",
32+
});
33+
}
Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,23 @@
11
#! /usr/bin/env node
2-
import { spawn } from "child_process";
2+
import { SupportedFrameworks } from "@apphosting/common";
3+
import { adapterBuild } from "../adapter-builds.js";
34
import { program } from "commander";
4-
import { yellow, bgRed, bold } from "colorette";
55

6-
// TODO(#382): add framework option later or incorporate micro-discovery.
7-
// TODO(#382): parse apphosting.yaml for environment variables / secrets.
8-
// TODO(#382): parse apphosting.yaml for runConfig and include in buildSchema
9-
// TODO(#382): Support custom build and run commands (parse and pass run command to build schema).
106
program
117
.argument("<projectRoot>", "path to the project's root directory")
12-
.action(async (projectRoot: string) => {
13-
const framework = "nextjs";
14-
// TODO(#382): We are using the latest framework adapter versions, but in the future
15-
// we should parse the framework version and use the matching adapter version.
16-
const adapterName = `@apphosting/adapter-nextjs`;
17-
const packumentResponse = await fetch(`https://registry.npmjs.org/${adapterName}`);
18-
if (!packumentResponse.ok) throw new Error(`Something went wrong fetching ${adapterName}`);
19-
const packument = await packumentResponse.json();
20-
const adapterVersion = packument?.["dist-tags"]?.["latest"];
21-
if (!adapterVersion) {
22-
throw new Error(`Could not find 'latest' dist-tag for ${adapterName}`);
23-
}
24-
// TODO(#382): should check for existence of adapter in app's package.json and use that version instead.
25-
26-
console.log(" 🔥", bgRed(` ${adapterName}@${yellow(bold(adapterVersion))} `), "\n");
8+
.option("--framework <framework>")
9+
.action(async (projectRoot, opts) => {
10+
// TODO(#382): support other apphosting.*.yaml files.
2711

28-
const buildCommand = `apphosting-adapter-${framework}-build`;
29-
await new Promise<void>((resolve, reject) => {
30-
const child = spawn("npx", ["-y", "-p", `${adapterName}@${adapterVersion}`, buildCommand], {
31-
cwd: projectRoot,
32-
shell: true,
33-
stdio: "inherit",
34-
});
12+
// TODO(#382): parse apphosting.yaml for environment variables / secrets needed during build time.
13+
if (opts.framework && SupportedFrameworks.includes(opts.framework)) {
14+
// TODO(#382): Skip this if there's a custom build command in apphosting.yaml.
15+
await adapterBuild(projectRoot, opts.framework);
16+
}
3517

36-
child.on("exit", (code) => {
37-
if (code !== 0) {
38-
reject(new Error(`framework adapter build failed with error code ${code}.`));
39-
}
40-
resolve();
41-
});
42-
});
43-
// TODO(#382): parse bundle.yaml and apphosting.yaml and output a buildschema somewhere.
18+
// TODO(#382): Parse apphosting.yaml to set custom run command in bundle.yaml
19+
// TODO(#382): parse apphosting.yaml for environment variables / secrets needed during runtime to include in the bunde.yaml
20+
// TODO(#382): parse apphosting.yaml for runConfig to include in bundle.yaml
4421
});
4522

4623
program.parse();

packages/@apphosting/build/tsconfig.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
"noEmit": false,
55
"outDir": "dist",
66
"rootDir": "src"
7-
7+
},
8+
"ts-node": {
9+
"esm": true,
10+
"logError": true,
11+
"pretty": true
812
},
913
"include": [
1014
"src/index.ts",
1115
"src/bin/*.ts",
1216
],
1317
"exclude": [
14-
"src/*.spec.ts"
18+
"src/*.spec.ts",
19+
"src/bin/build.ts"
1520
]
1621
}

0 commit comments

Comments
 (0)