From 745e16769f01a7f4da3d77f26cb77eb4a5cd4159 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 10 May 2022 16:30:41 -0400 Subject: [PATCH] More firebase-frameworks work (#4463) * Work with emulators:start * Add dev mode flag * Dev flag not actually needed * Move entry into firebase-tools * Cleanup * Bump dep * fix missing import Co-authored-by: Bryan Kendall --- npm-shrinkwrap.json | 108 +++++++++++++++++++-- package.json | 2 +- src/deploy/index.ts | 3 +- src/emulator/controller.ts | 9 ++ src/frameworks/index.ts | 123 ++++++++++++++++++++++++ src/hosting/normalizedHostingConfigs.ts | 8 ++ src/serve/index.ts | 7 +- 7 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 src/frameworks/index.ts diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index edadccaa494..4ada9b81da8 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -30,7 +30,7 @@ "exit-code": "^1.0.2", "express": "^4.16.4", "filesize": "^6.1.0", - "firebase-frameworks": "^0.3.0", + "firebase-frameworks": "^0.4.0", "fs-extra": "^5.0.0", "glob": "^7.1.2", "google-auth-library": "^7.11.0", @@ -6059,18 +6059,67 @@ } }, "node_modules/firebase-frameworks": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/firebase-frameworks/-/firebase-frameworks-0.3.0.tgz", - "integrity": "sha512-Zxtx5WsD8ZZdItIeDjjpM+JgaIWDdwBujmLYLKf2Ou6onyRsd8bNRrnVsqrnq4S3FN9TcNYliXdwMu7AwYdW7Q==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/firebase-frameworks/-/firebase-frameworks-0.4.0.tgz", + "integrity": "sha512-Seu+1dKNo3AacMrOHb1V0F41DfCKiM6gW4Go/34z78WtuBkzKNSUOUI+w8XCH7A96QGZRbNbGwt33BiSXEb2xQ==", "dependencies": { + "fs-extra": "^10.1.0", + "jsonc-parser": "^3.0.0", + "semver": "^7.3.7", "tslib": "^2.3.1" } }, + "node_modules/firebase-frameworks/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/firebase-frameworks/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/firebase-frameworks/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/firebase-frameworks/node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, + "node_modules/firebase-frameworks/node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/firebase-functions": { "version": "3.18.1", "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.18.1.tgz", @@ -7945,6 +7994,11 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" + }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -18400,17 +18454,52 @@ } }, "firebase-frameworks": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/firebase-frameworks/-/firebase-frameworks-0.3.0.tgz", - "integrity": "sha512-Zxtx5WsD8ZZdItIeDjjpM+JgaIWDdwBujmLYLKf2Ou6onyRsd8bNRrnVsqrnq4S3FN9TcNYliXdwMu7AwYdW7Q==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/firebase-frameworks/-/firebase-frameworks-0.4.0.tgz", + "integrity": "sha512-Seu+1dKNo3AacMrOHb1V0F41DfCKiM6gW4Go/34z78WtuBkzKNSUOUI+w8XCH7A96QGZRbNbGwt33BiSXEb2xQ==", "requires": { + "fs-extra": "^10.1.0", + "jsonc-parser": "^3.0.0", + "semver": "^7.3.7", "tslib": "^2.3.1" }, "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "requires": { + "lru-cache": "^6.0.0" + } + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" } } }, @@ -19883,6 +19972,11 @@ "minimist": "^1.2.5" } }, + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==" + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", diff --git a/package.json b/package.json index d52686d01f2..954f0c2373f 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "exit-code": "^1.0.2", "express": "^4.16.4", "filesize": "^6.1.0", - "firebase-frameworks": "^0.3.0", + "firebase-frameworks": "^0.4.0", "fs-extra": "^5.0.0", "glob": "^7.1.2", "google-auth-library": "^7.11.0", diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 24a9ceb969e..fc6a88b821c 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -15,6 +15,7 @@ import * as FunctionsTarget from "./functions"; import * as StorageTarget from "./storage"; import * as RemoteConfigTarget from "./remoteconfig"; import * as ExtensionsTarget from "./extensions"; +import { prepareFrameworks } from "../frameworks"; const TARGETS = { hosting: HostingTarget, @@ -58,7 +59,7 @@ export const deploy = async function ( if (previews.frameworkawareness && targetNames.includes("hosting")) { const config = options.config.get("hosting"); if (Array.isArray(config) ? config.some((it) => it.source) : config.source) { - await require("firebase-frameworks").prepare(targetNames, context, options); + await prepareFrameworks(targetNames, context, options); } } diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index d2f86809599..6b270e4b3be 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -46,6 +46,8 @@ import { ParsedTriggerDefinition } from "./functionsEmulatorShared"; import { ExtensionsEmulator } from "./extensionsEmulator"; import { normalizeAndValidate } from "../functions/projectConfig"; import { requiresJava } from "./downloadableEmulators"; +import { prepareFrameworks } from "../frameworks"; +import { previews } from "../previews"; const START_LOGGING_EMULATOR = utils.envOverride( "START_LOGGING_EMULATOR", @@ -402,6 +404,13 @@ export async function startAll( } } + if (previews.frameworkawareness) { + const config = options.config.get("hosting"); + if (Array.isArray(config) ? config.some((it) => it.source) : config.source) { + await prepareFrameworks(targets, options, options); + } + } + if (shouldStart(options, Emulators.HUB)) { const hubAddr = await getAndCheckAddress(Emulators.HUB, options); const hub = new EmulatorHub({ projectId, ...hubAddr }); diff --git a/src/frameworks/index.ts b/src/frameworks/index.ts new file mode 100644 index 00000000000..814e06708c1 --- /dev/null +++ b/src/frameworks/index.ts @@ -0,0 +1,123 @@ +import { join } from "path"; +import { exit } from "process"; + +import { needProjectId } from "../projectUtils"; +import { normalizedHostingConfigs } from "../hosting/normalizedHostingConfigs"; +import { listSites, Site } from "../hosting/api"; +import { getAppConfig, AppPlatform } from "../management/apps"; +import { promises as fsPromises } from "fs"; +import { promptOnce } from "../prompt"; + +const { writeFile } = fsPromises; + +export const shortSiteName = (site?: Site) => site?.name && site.name.split("/").pop(); + +export const prepareFrameworks = async (targetNames: string[], context: any, options: any) => { + const project = needProjectId(context); + // options.site is not present when emulated. We could call requireHostingSite but IAM permissions haven't + // been booted up (at this point) and we may be offline, so just use projectId. Most of the time + // the default site is named the same as the project & for frameworks this is only used for naming the + // function... unless you're using authenticated server-context TODO explore the implication here. + const configs = normalizedHostingConfigs({ site: project, ...options }, { resolveTargets: true }); + options.normalizedHostingConfigs = configs; + if (configs.length === 0) return; + for (const config of configs) { + const { source, site, public: publicDir } = config; + if (!source) continue; + const dist = join(".firebase", site); + const hostingDist = join(dist, "hosting"); + const functionsDist = join(dist, "functions"); + if (publicDir) + throw new Error(`hosting.public and hosting.source cannot both be set in firebase.json`); + const getProjectPath = (...args: string[]) => join(process.cwd(), source, ...args); + const functionName = `ssr${site.replace(/-/g, "")}`; + const { build } = require("firebase-frameworks/tools"); + const { usingCloudFunctions, rewrites, redirects, headers, usesFirebaseConfig } = await build( + { + dist, + project, + site, + function: { + name: functionName, + region: "us-central1", + }, + }, + getProjectPath + ); + config.public = hostingDist; + if (usingCloudFunctions) { + if (context.hostingChannel) { + // TODO move to prompts + const message = + "Cannot preview changes to the backend, you will only see changes to the static content on this channel."; + if (!options.nonInteractive) { + const continueDeploy = await promptOnce({ + type: "confirm", + default: true, + message: `${message} Would you like to continue with the deploy?`, + }); + if (!continueDeploy) exit(1); + } else { + console.error(message); + } + } else { + const functionConfig = { + source: functionsDist, + codebase: `firebase-frameworks-${site}`, + }; + if (targetNames.includes("functions")) { + const combinedFunctionsConfig = [functionConfig].concat( + options.config.get("functions") || [] + ); + options.config.set("functions", combinedFunctionsConfig); + } else { + targetNames.unshift("functions"); + options.config.set("functions", functionConfig); + } + } + + config.rewrites = [ + ...(config.rewrites || []), + ...rewrites, + { + source: "**", + function: functionName, + }, + ]; + + let firebaseProjectConfig = null; + if (usesFirebaseConfig) { + const sites = await listSites(project); + const selectedSite = sites.find((it) => shortSiteName(it) === site); + if (selectedSite) { + const { appId } = selectedSite; + if (appId) { + firebaseProjectConfig = await getAppConfig(appId, AppPlatform.WEB); + } else { + console.warn( + `No Firebase app associated with site ${site}, unable to provide authenticated server context` + ); + } + } + } + writeFile( + join(functionsDist, ".env"), + `FRAMEWORKS_FIREBASE_PROJECT_CONFIG="${JSON.stringify(firebaseProjectConfig).replace( + /"/g, + '\\"' + )}"` + ); + } else { + config.rewrites = [ + ...(config.rewrites || []), + ...rewrites, + { + source: "**", + destination: "/index.html", + }, + ]; + } + config.redirects = [...(config.redirects || []), ...redirects]; + config.headers = [...(config.headers || []), ...headers]; + } +}; diff --git a/src/hosting/normalizedHostingConfigs.ts b/src/hosting/normalizedHostingConfigs.ts index f2833e06694..59d9a2ee701 100644 --- a/src/hosting/normalizedHostingConfigs.ts +++ b/src/hosting/normalizedHostingConfigs.ts @@ -4,8 +4,13 @@ import { cloneDeep } from "lodash"; import { FirebaseError } from "../error"; interface HostingConfig { + source?: string; + public?: string; site: string; target: string; + rewrites?: any[]; + redirects?: any[]; + headers?: any[]; } function filterOnly(configs: HostingConfig[], onlyString: string): HostingConfig[] { @@ -90,6 +95,9 @@ export function normalizedHostingConfigs( cmdOptions: any, // eslint-disable-line @typescript-eslint/no-explicit-any options: { resolveTargets?: boolean } = {} ): HostingConfig[] { + // First see if there's a momoized copy on the options, from frameworks + const normalizedHostingConfigs = cmdOptions.normalizedHostingConfigs; + if (normalizedHostingConfigs) return normalizedHostingConfigs; let configs = cloneDeep(cmdOptions.config.get("hosting")); if (!configs) { return []; diff --git a/src/serve/index.ts b/src/serve/index.ts index 9c853779735..703be9205ba 100644 --- a/src/serve/index.ts +++ b/src/serve/index.ts @@ -1,6 +1,7 @@ import { EmulatorServer } from "../emulator/emulatorServer"; import * as _ from "lodash"; import { logger } from "../logger"; +import { prepareFrameworks } from "../frameworks"; import { previews } from "../previews"; const { FunctionsServer } = require("./functions"); @@ -26,11 +27,7 @@ export async function serve(options: any): Promise { targetNames.includes("hosting") && [].concat(options.config.get("hosting")).some((it: any) => it.source) ) { - await require("firebase-frameworks").prepare( - targetNames, - { project: options.projectId }, - options - ); + await prepareFrameworks(targetNames, options, options); } await Promise.all( _.map(targetNames, (targetName: string) => {