Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
38 changes: 27 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/@apphosting/build/.gitignore

This file was deleted.

1 change: 1 addition & 0 deletions packages/@apphosting/build/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
runs
32 changes: 32 additions & 0 deletions packages/@apphosting/build/e2e/adapter-builds.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as assert from "assert";
import { posix } from "path";
import pkg from "@apphosting/common";
import { scenarios } from "./scenarios.ts";
import fsExtra from "fs-extra";
import { parse as parseYaml } from "yaml";

const { readFileSync, mkdirp, rmdir, readJSONSync } = fsExtra;

Check failure on line 8 in packages/@apphosting/build/e2e/adapter-builds.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

'readJSONSync' is assigned a value but never used

Check failure on line 8 in packages/@apphosting/build/e2e/adapter-builds.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

'rmdir' is assigned a value but never used

Check failure on line 8 in packages/@apphosting/build/e2e/adapter-builds.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

'mkdirp' is assigned a value but never used
const { OutputBundleConfig } = pkg;

Check failure on line 9 in packages/@apphosting/build/e2e/adapter-builds.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

'OutputBundleConfig' is assigned a value but never used

const scenario = process.env.SCENARIO;
if (!scenario) {
throw new Error("SCENARIO environment variable expected");
}

const runId = process.env.RUN_ID;
if (!runId) {
throw new Error("RUN_ID environment variable expected");
}

const bundleYaml = posix.join(process.cwd(), "e2e", "runs", runId, ".apphosting", "bundle.yaml");
describe("supported framework apps", () => {
it("apps have bundle.yaml correctly generated", async () => {

Check failure on line 23 in packages/@apphosting/build/e2e/adapter-builds.spec.ts

View workflow job for this annotation

GitHub Actions / Lint

Async arrow function has no 'await' expression
const bundle = parseYaml(readFileSync(bundleYaml, "utf8")) as OutputBundleConfig;

assert.deepStrictEqual(scenarios.get(scenario).expectedBundleYaml.runConfig, bundle.runConfig);
assert.deepStrictEqual(
scenarios.get(scenario).expectedBundleYaml.metadata.adapterPackageName,
bundle.metadata.adapterPackageName,
);
});
});
95 changes: 95 additions & 0 deletions packages/@apphosting/build/e2e/run-local-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { cp } from "fs/promises";
import promiseSpawn from "@npmcli/promise-spawn";
import { dirname, join, relative } from "path";
import { fileURLToPath } from "url";
import { parse as parseYaml } from "yaml";

Check failure on line 5 in packages/@apphosting/build/e2e/run-local-build.ts

View workflow job for this annotation

GitHub Actions / Lint

'parseYaml' is defined but never used
import fsExtra from "fs-extra";
import { scenarios } from "./scenarios.ts";

const { readFileSync, mkdirp, rmdir } = fsExtra;

const __dirname = dirname(fileURLToPath(import.meta.url));

const errors: any[] = [];

await rmdir(join(__dirname, "runs"), { recursive: true }).catch(() => undefined);

// Run each scenario
for (const [scenarioName, scenario] of scenarios) {
console.log(
`\n\n${"=".repeat(80)}\n${" ".repeat(
5,
)}RUNNING SCENARIO: ${scenarioName.toUpperCase()}${" ".repeat(5)}\n${"=".repeat(80)}`,
);

const runId = `${scenarioName}-${Math.random().toString().split(".")[1]}`;
const cwd = join(__dirname, "runs", runId);
await mkdirp(cwd);

const starterTemplateDir = scenarioName.includes("nextjs")
? "../../../starters/nextjs/basic"
: "../../../starters/angular/basic";
console.log(`[${runId}] Copying ${starterTemplateDir} to working directory`);
await cp(starterTemplateDir, cwd, { recursive: true });

console.log(`[${runId}] > npm ci --silent --no-progress`);
await promiseSpawn("npm", ["ci", "--silent", "--no-progress"], {
cwd,
stdio: "inherit",
shell: true,
});

const buildScript = relative(cwd, join(__dirname, "../dist/bin/localbuild.js"));
const buildLogPath = join(cwd, "build.log");
console.log(`[${runId}] > node ${buildScript} (output written to ${buildLogPath})`);

const packageJson = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
const frameworkVersion = scenarioName.includes("nextjs")
? packageJson.dependencies.next.replace("^", "")
: JSON.parse(
readFileSync(join(cwd, "node_modules", "@angular", "core", "package.json"), "utf-8"),
).version;

try {
const result = await promiseSpawn("node", [buildScript, ...scenario.inputs], {
cwd,
stdioString: true,
stdio: "pipe",
shell: true,
env: {
...process.env,
FRAMEWORK_VERSION: frameworkVersion,
},
});
// Write stdout and stderr to the log file
fsExtra.writeFileSync(buildLogPath, result.stdout + result.stderr);

try {
// Determine which test files to run
const testPattern = scenario.tests
? scenario.tests.map((test) => `e2e/${test}`).join(" ")
: "e2e/*.spec.ts";

console.log(`> SCENARIO=${scenarioName} ts-mocha -p tsconfig.json ${testPattern}`);

await promiseSpawn("ts-mocha", ["-p", "tsconfig.json", ...testPattern.split(" ")], {
shell: true,
stdio: "inherit",
env: {
...process.env,
SCENARIO: scenarioName,
RUN_ID: runId,
},
});
} catch (e) {
errors.push(e);
}
} catch (e) {
console.error(`Error in scenario ${scenarioName}:`, e);
errors.push(e);
}
}
if (errors.length) {
console.error(errors);
process.exit(1);
}
41 changes: 41 additions & 0 deletions packages/@apphosting/build/e2e/scenarios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
interface Scenario {

Check failure on line 1 in packages/@apphosting/build/e2e/scenarios.ts

View workflow job for this annotation

GitHub Actions / Lint

'Scenario' is defined but never used
inputs: string[];
expectedBundleYaml: {};
tests?: string[]; // List of test files to run
}

export const scenarios = new Map([
[
"nextjs-app",
{
inputs: ["./", "--framework", "nextjs"],
expectedBundleYaml: {
version: "v1",
runConfig: {
runCommand: "node .next/standalone/server.js",
},
metadata: {
adapterPackageName: "@apphosting/adapter-nextjs",
},
},
tests: ["adapter-builds.spec.ts"],
},
],
[
"angular-app",
{
inputs: ["./", "--framework", "angular"],
expectedBundleYaml: {
version: "v1",
runConfig: {
runCommand: "node dist/firebase-app-hosting-angular/server/server.mjs",
environmentVariables: [],
},
metadata: {
adapterPackageName: "@apphosting/adapter-angular",
},
},
tests: ["adapter-builds.spec.ts"],
},
],
]);
16 changes: 11 additions & 5 deletions packages/@apphosting/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"url": "git+https://github.com/FirebaseExtended/firebase-framework-tools.git"
},
"bin": {
"build": "dist/bin/build.js",
"apphosting-local-build": "dist/bin/localbuild.js"
"build": "dist/bin/build.js",
"apphosting-local-build": "dist/bin/localbuild.js"
},
"author": {
"name": "Firebase",
Expand All @@ -21,7 +21,9 @@
"type": "module",
"sideEffects": false,
"scripts": {
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*"
"build": "rm -rf dist && tsc && chmod +x ./dist/bin/*",
"test": "npm run test:functional",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

planning to add test:unit in followup CLs

"test:functional": "node --loader ts-node/esm ./e2e/run-local-build.ts"
},
"exports": {
".": {
Expand All @@ -38,9 +40,13 @@
"colorette": "^2.0.20",
"commander": "^11.1.0",
"npm-pick-manifest": "^9.0.0",
"ts-node": "^10.9.1"
"ts-node": "^10.9.1",
"tsc": "^2.0.4"
},
"devDependencies": {
"@types/commander": "*"
"@types/commander": "*",
"ts-mocha": "*",
"ts-node": "*",
"typescript": "*"
}
}
33 changes: 33 additions & 0 deletions packages/@apphosting/build/src/adapter-builds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import promiseSpawn from "@npmcli/promise-spawn";
import { yellow, bgRed, bold } from "colorette";

export async function adapterBuild(projectRoot: string, framework: string) {
// TODO(#382): We are using the latest framework adapter versions, but in the future
// we should parse the framework version and use the matching adapter version.
const adapterName = `@apphosting/adapter-${framework}`;
const packumentResponse = await fetch(`https://registry.npmjs.org/${adapterName}`);
if (!packumentResponse.ok)
throw new Error(
`Failed to fetch ${adapterName}: ${packumentResponse.status} ${packumentResponse.statusText}`,
);
let packument;
try {
packument = await packumentResponse.json();
} catch (e) {
throw new Error(`Failed to parse response from NPM registry for ${adapterName}.`);
}
const adapterVersion = packument?.["dist-tags"]?.["latest"];
if (!adapterVersion) {
throw new Error(`Could not find 'latest' dist-tag for ${adapterName}`);
}
// TODO(#382): should check for existence of adapter in app's package.json and use that version instead.

console.log(" 🔥", bgRed(` ${adapterName}@${yellow(bold(adapterVersion))} `), "\n");

const buildCommand = `apphosting-adapter-${framework}-build`;
await promiseSpawn("npx", ["-y", "-p", `${adapterName}@${adapterVersion}`, buildCommand], {
cwd: projectRoot,
shell: true,
stdio: "inherit",
});
}
49 changes: 13 additions & 36 deletions packages/@apphosting/build/src/bin/localbuild.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,23 @@
#! /usr/bin/env node
import { spawn } from "child_process";
import { SupportedFrameworks } from "@apphosting/common";
import { adapterBuild } from "../adapter-builds.js";
import { program } from "commander";
import { yellow, bgRed, bold } from "colorette";

// TODO(#382): add framework option later or incorporate micro-discovery.
// TODO(#382): parse apphosting.yaml for environment variables / secrets.
// TODO(#382): parse apphosting.yaml for runConfig and include in buildSchema
// TODO(#382): Support custom build and run commands (parse and pass run command to build schema).
program
.argument("<projectRoot>", "path to the project's root directory")
.action(async (projectRoot: string) => {
const framework = "nextjs";
// TODO(#382): We are using the latest framework adapter versions, but in the future
// we should parse the framework version and use the matching adapter version.
const adapterName = `@apphosting/adapter-nextjs`;
const packumentResponse = await fetch(`https://registry.npmjs.org/${adapterName}`);
if (!packumentResponse.ok) throw new Error(`Something went wrong fetching ${adapterName}`);
const packument = await packumentResponse.json();
const adapterVersion = packument?.["dist-tags"]?.["latest"];
if (!adapterVersion) {
throw new Error(`Could not find 'latest' dist-tag for ${adapterName}`);
}
// TODO(#382): should check for existence of adapter in app's package.json and use that version instead.

console.log(" 🔥", bgRed(` ${adapterName}@${yellow(bold(adapterVersion))} `), "\n");
.option("--framework <framework>")
.action(async (projectRoot, opts) => {
// TODO(#382): support other apphosting.*.yaml files.

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

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

program.parse();
9 changes: 7 additions & 2 deletions packages/@apphosting/build/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
"compilerOptions": {
"noEmit": false,
"outDir": "dist",
"rootDir": "src"

"rootDir": "src",
"allowImportingTsExtensions": true
},
"ts-node": {
"esm": true,
"logError": true,
"pretty": true
},
"include": [
"src/index.ts",
Expand Down
Loading
Loading