-
Notifications
You must be signed in to change notification settings - Fork 168
Support adapter builds and add tests. #386
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
Changes from 3 commits
607cc5a
a5ec7a0
266fed1
57ce6bc
5068147
b317096
20cd8bc
5ce52d6
f5fd285
7430804
d21f2ca
68015cb
40d8ddf
0a0c8aa
7134f4b
c325449
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
runs |
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
|
||
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, | ||
); | ||
}); | ||
}); |
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"; | ||
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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
interface Scenario { | ||
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"], | ||
}, | ||
], | ||
]); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. planning to add |
||
"test:functional": "node --loader ts-node/esm ./e2e/run-local-build.ts" | ||
}, | ||
"exports": { | ||
".": { | ||
|
@@ -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": "*" | ||
} | ||
} |
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", | ||
}); | ||
} |
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(); |
Uh oh!
There was an error while loading. Please reload this page.