Skip to content

Separate adapter builds into helper function. #384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
31 changes: 26 additions & 5 deletions package-lock.json

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

21 changes: 0 additions & 21 deletions packages/@apphosting/adapter-angular/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,6 @@ export interface OutputBundleOptions {
needsServerGenerated: boolean;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

github is the worst tool I have ever encountered.

// Environment variable schema for bundle.yaml outputted by angular adapter
export interface EnvironmentVariable {
variable: string;
value: string;
availability: Availability.Runtime; // currently support RUNTIME only
}

// defines whether the environment variable is buildtime, runtime or both
export enum Availability {
Buildtime = "BUILD",
Runtime = "RUNTIME",
}

// Metadata schema for bundle.yaml outputted by angular adapter
export interface Metadata {
adapterPackageName: string;
adapterVersion: string;
framework: string;
frameworkVersion: string;
}

// valid manifest schema
export interface ValidManifest {
errors: string[];
Expand Down
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;
const { OutputBundleConfig } = pkg;

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 () => {
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,
);
});
});
104 changes: 104 additions & 0 deletions packages/@apphosting/build/e2e/run-local-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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";
import { spawn } from "child_process";
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 });

// Run scenario-specific setup if provided
if (scenario.setup) {
console.log(`[${runId}] Running setup for ${scenarioName}`);
await scenario.setup(cwd);
}

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 {
await promiseSpawn("node", [buildScript, ...scenario.inputs], {
cwd,
stdioString: true,
stdio: "pipe",
shell: true,
env: {
...process.env,
FRAMEWORK_VERSION: frameworkVersion,
},
}).then((result) => {
// 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);
}
}
45 changes: 45 additions & 0 deletions packages/@apphosting/build/e2e/scenarios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pkg from "@apphosting/common";
const { OutputBundleConfig } = pkg;

interface Scenario {
name: string; // Name of the scenario
inputs: string[];
expectedBundleYaml: OutputBundleConfig;
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"],
},
],
]);
4 changes: 3 additions & 1 deletion packages/@apphosting/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"test:functional": "node --loader ts-node/esm ./e2e/run-local-build.ts"
},
"exports": {
".": {
Expand Down
39 changes: 39 additions & 0 deletions packages/@apphosting/build/src/adapter-builds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { spawn } from "child_process";
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}`,
);
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");

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",
});

child.on("error", reject);

child.on("exit", (code) => {
if (code !== 0) {
reject(new Error(`framework adapter build failed with error code ${code}.`));
}
resolve();
});
});
}
54 changes: 18 additions & 36 deletions packages/@apphosting/build/src/bin/localbuild.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,28 @@
#! /usr/bin/env node
import { spawn } from "child_process";
import { SupportedFrameworks, ApphostingConfig } from "@apphosting/common";
import { adapterBuild } from "../adapter-builds.js";
import { parse as parseYaml } from "yaml";
import fsExtra from "fs-extra";
import { join } from "path";
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).
export const { readFileSync } = fsExtra;

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.
.option("--framework <framework>")
.action(async (projectRoot, opts) => {
// TODO(#382): support other apphosting.*.yaml files.

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

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.
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();
Loading