diff --git a/src/schematics/common.ts b/src/schematics/common.ts index 061603b69..814f59562 100644 --- a/src/schematics/common.ts +++ b/src/schematics/common.ts @@ -1,6 +1,6 @@ import { SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; import * as semver from 'semver'; -import { FirebaseHostingSite, FirebaseRc } from './interfaces'; +import { FirebaseHostingSite } from './interfaces'; export const shortSiteName = (site?: FirebaseHostingSite) => site?.name?.split('/').pop(); @@ -18,44 +18,6 @@ export const overwriteIfExists = ( } }; -function emptyFirebaseRc() { - return { - targets: {} - }; -} - -function generateFirebaseRcTarget(firebaseProject: string, firebaseHostingSite: FirebaseHostingSite|undefined, project: string) { - return { - hosting: { - [project]: [ - shortSiteName(firebaseHostingSite) ?? firebaseProject - ] - } - }; -} - -export function generateFirebaseRc( - tree: Tree, - path: string, - firebaseProject: string, - firebaseHostingSite: FirebaseHostingSite|undefined, - project: string -) { - const firebaseRc: FirebaseRc = tree.exists(path) - ? safeReadJSON(path, tree) - : emptyFirebaseRc(); - - firebaseRc.targets = firebaseRc.targets || {}; - firebaseRc.targets[firebaseProject] = generateFirebaseRcTarget( - firebaseProject, - firebaseHostingSite, - project - ); - firebaseRc.projects = { default: firebaseProject }; - - overwriteIfExists(tree, path, stringifyFormatted(firebaseRc)); -} - export function safeReadJSON(path: string, tree: Tree) { try { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/src/schematics/deploy/actions.ts b/src/schematics/deploy/actions.ts index 6ee2d6038..c57ec293d 100644 --- a/src/schematics/deploy/actions.ts +++ b/src/schematics/deploy/actions.ts @@ -390,6 +390,8 @@ export default async function deploy( options: DeployBuilderOptions, firebaseToken?: string, ) { + const legacyNgDeploy = !options.version || options.version < 2; + if (!firebaseToken && !process.env.GOOGLE_APPLICATION_CREDENTIALS) { await firebaseTools.login(); const user = await firebaseTools.login({ projectRoot: context.workspaceRoot }); @@ -401,6 +403,12 @@ export default async function deploy( console.log(`Using Google Application Credentials.`); } + if (legacyNgDeploy) { + console.error(`Legacy ng-deploy Firebase is deprecated. +Please migrate to Firebase Hosting's integration with Angular https://firebase.google.com/docs/hosting/frameworks/angular +or the new Firebase App Hosting product https://firebase.google.com/docs/app-hosting`); + } + if (prerenderBuildTarget) { const run = await context.scheduleTarget( targetFromTargetString(prerenderBuildTarget.name), @@ -465,7 +473,7 @@ export default async function deploy( firebaseTools.logger.logger.add(logger); - if ((!options.version || options.version < 2) && serverBuildTarget) { + if (legacyNgDeploy && serverBuildTarget) { if (options.ssr === 'cloud-run') { await deployToCloudRun( firebaseTools, diff --git a/src/schematics/interfaces.ts b/src/schematics/interfaces.ts index 8091a83cb..52b209c61 100644 --- a/src/schematics/interfaces.ts +++ b/src/schematics/interfaces.ts @@ -1,7 +1,6 @@ import { RuntimeOptions } from 'firebase-functions'; export const enum FEATURES { - Hosting, Authentication, Analytics, AppCheck, @@ -16,7 +15,6 @@ export const enum FEATURES { } export const featureOptions = [ - { name: 'ng deploy -- Hosting', value: FEATURES.Hosting }, { name: 'Authentication', value: FEATURES.Authentication }, { name: 'Google Analytics', value: FEATURES.Analytics }, { name: 'App Check', value: FEATURES.AppCheck }, @@ -154,15 +152,6 @@ export interface FirebaseJSON { functions?: FirebaseFunctionsConfig; } -export interface FirebaseRcTarget { - hosting: Record; -} - -export interface FirebaseRc { - targets?: Record; - projects?: Record; -} - export interface DeployBuilderSchema { buildTarget?: string; browserTarget?: string; diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index 4cf04bb37..accd18a53 100644 --- a/src/schematics/setup/index.ts +++ b/src/schematics/setup/index.ts @@ -1,26 +1,19 @@ import { writeFileSync } from 'fs'; import { join } from 'path'; import { asWindowsPath, normalize } from '@angular-devkit/core'; -import { SchematicContext, SchematicsException, Tree, chain } from '@angular-devkit/schematics'; +import { SchematicContext, Tree, chain } from '@angular-devkit/schematics'; import { addRootProvider } from '@schematics/angular/utility'; -import { - generateFirebaseRc, - overwriteIfExists, - safeReadJSON, - stringifyFormatted -} from '../common'; import { getFirebaseTools } from '../firebaseTools'; import { DeployOptions, FEATURES, FirebaseApp, FirebaseHostingSite, FirebaseProject, - NgAddNormalizedOptions } from '../interfaces'; -import { FirebaseJSON, Workspace, WorkspaceProject } from '../interfaces'; import { addIgnoreFiles, featureToRules, - getFirebaseProjectNameFromHost, getProject, getWorkspace + getFirebaseProjectNameFromHost, + getProject, } from '../utils'; -import { appPrompt, featuresPrompt, projectPrompt, projectTypePrompt, sitePrompt, userPrompt } from './prompts'; +import { appPrompt, featuresPrompt, projectPrompt, userPrompt } from './prompts'; export interface SetupConfig extends DeployOptions { firebaseProject: FirebaseProject, @@ -39,30 +32,7 @@ export const setupProject = addIgnoreFiles(tree); - if (features.includes(FEATURES.Hosting)) { - const { path: workspacePath, workspace } = getWorkspace(tree); - const { project, projectName } = getProject(config, tree); - setupFirebase({ - workspace, - workspacePath, - options: { - project: projectName, - firebaseProject: config.firebaseProject, - firebaseApp: config.firebaseApp, - firebaseHostingSite: config.firebaseHostingSite, - sdkConfig: config.sdkConfig, - buildTarget: config.buildTarget, - serveTarget: config.serveTarget, - ssrRegion: config.ssrRegion, - }, - tree, - context, - project - }); - } - - const featuresToImport = features.filter(it => it !== FEATURES.Hosting); - if (featuresToImport.length > 0) { + if (features.length) { return chain([ addRootProvider(projectName, ({code, external}) => { external('initializeApp', '@angular/fire/app'); @@ -96,31 +66,18 @@ export const ngAddSetupProject = ( await firebaseTools.login.use(user.email, { projectRoot }); } - const { project: ngProject, projectName: ngProjectName } = getProject(options, host); + const { projectName: ngProjectName } = getProject(options, host); const [ defaultProjectName ] = getFirebaseProjectNameFromHost(host, ngProjectName); const firebaseProject = await projectPrompt(defaultProjectName, { projectRoot, account: user.email }); - - let hosting = { }; - let firebaseHostingSite: FirebaseHostingSite|undefined; - - if (features.includes(FEATURES.Hosting)) { - // TODO read existing settings from angular.json, if available - const results = await projectTypePrompt(ngProject, ngProjectName); - hosting = { ...hosting, ...results }; - firebaseHostingSite = await sitePrompt(firebaseProject, { projectRoot }); - } - - let firebaseApp: FirebaseApp|undefined; let sdkConfig: Record|undefined; - if (features.find(it => it !== FEATURES.Hosting)) { + if (features.length) { - const defaultAppId = firebaseHostingSite?.appId; - firebaseApp = await appPrompt(firebaseProject, defaultAppId, { projectRoot }); + firebaseApp = await appPrompt(firebaseProject, undefined, { projectRoot }); const result = await firebaseTools.apps.sdkconfig('web', firebaseApp.appId, { nonInteractive: true, projectRoot }); sdkConfig = result.sdkConfig; @@ -128,88 +85,8 @@ export const ngAddSetupProject = ( } return setupProject(host, context, features, { - ...options, ...hosting, firebaseProject, firebaseApp, firebaseHostingSite, sdkConfig, + ...options, firebaseProject, firebaseApp, sdkConfig, }); } }; - -export function generateFirebaseJson( - tree: Tree, - path: string, - project: string, - region: string|undefined, -) { - const firebaseJson: FirebaseJSON = tree.exists(path) - ? safeReadJSON(path, tree) - : {}; - - const newConfig = { - target: project, - source: '.', - frameworksBackend: { - region - } - }; - if (firebaseJson.hosting === undefined) { - firebaseJson.hosting = [newConfig]; - } else if (Array.isArray(firebaseJson.hosting)) { - const existingConfigIndex = firebaseJson.hosting.findIndex(config => config.target === newConfig.target); - if (existingConfigIndex > -1) { - firebaseJson.hosting.splice(existingConfigIndex, 1, newConfig); - } else { - firebaseJson.hosting.push(newConfig); - } - } else { - firebaseJson.hosting = [firebaseJson.hosting, newConfig]; - } - - overwriteIfExists(tree, path, stringifyFormatted(firebaseJson)); -} - -export const setupFirebase = (config: { - project: WorkspaceProject; - options: NgAddNormalizedOptions; - workspacePath: string; - workspace: Workspace; - tree: Tree; - context: SchematicContext; -}) => { - const { tree, workspacePath, workspace, options } = config; - const project = workspace.projects[options.project]; - - if (!project.architect) { - throw new SchematicsException(`Angular project "${options.project}" has a malformed angular.json`); - } - - project.architect.deploy = { - builder: '@angular/fire:deploy', - options: { - version: 2, - }, - configurations: { - production: { - buildTarget: options.buildTarget?.[0], - serveTarget: options.serveTarget?.[0], - }, - development: { - buildTarget: options.buildTarget?.[1], - serveTarget: options.serveTarget?.[1], - } - }, - defaultConfiguration: 'production', - }; - - tree.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); - - generateFirebaseJson(tree, 'firebase.json', options.project, options.ssrRegion); - generateFirebaseRc( - tree, - '.firebaserc', - options.firebaseProject.projectId, - options.firebaseHostingSite, - options.project - ); - - return tree; -}; diff --git a/src/schematics/setup/prompts.ts b/src/schematics/setup/prompts.ts index 6b944c25f..1bc8a85ca 100644 --- a/src/schematics/setup/prompts.ts +++ b/src/schematics/setup/prompts.ts @@ -1,16 +1,14 @@ import { spawnSync } from 'child_process'; import * as fuzzy from 'fuzzy'; import * as inquirer from 'inquirer'; -import { shortSiteName } from '../common'; import { getFirebaseTools } from '../firebaseTools'; -import { FEATURES, FirebaseApp, FirebaseHostingSite, FirebaseProject, PROJECT_TYPE, WorkspaceProject, featureOptions } from '../interfaces'; -import { isSSRApp, isUniversalApp, shortAppId } from '../utils'; +import { FEATURES, FirebaseApp, FirebaseProject, featureOptions } from '../interfaces'; +import { shortAppId } from '../utils'; // eslint-disable-next-line @typescript-eslint/no-var-requires inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); const NEW_OPTION = '~~angularfire-new~~'; -const DEFAULT_SITE_TYPE = 'DEFAULT_SITE'; // `fuzzy` passes either the original list of projects or an internal object // which contains the project as a property. @@ -22,10 +20,6 @@ const isApp = (elem: FirebaseApp | fuzzy.FilterResult): elem is Fir return (elem as { original: FirebaseApp }).original === undefined; }; -const isSite = (elem: FirebaseHostingSite | fuzzy.FilterResult): elem is FirebaseHostingSite => { - return (elem as { original: FirebaseHostingSite }).original === undefined; -}; - export const searchProjects = (projects: FirebaseProject[]) => // eslint-disable-next-line @typescript-eslint/require-await async (_: any, input: string) => { @@ -78,33 +72,6 @@ export const searchApps = (apps: FirebaseApp[]) => }); }; -export const searchSites = (sites: FirebaseHostingSite[]) => - // eslint-disable-next-line @typescript-eslint/require-await - async (_: any, input: string) => { - sites.unshift({ - name: NEW_OPTION, - defaultUrl: '[CREATE NEW SITE]', - } as any); - return fuzzy.filter(input, sites, { - extract(el) { - return el.defaultUrl; - } - }).map((result) => { - let original: FirebaseHostingSite; - if (isSite(result)) { - original = result; - } else { - original = result.original; - } - return { - name: original.defaultUrl, - title: original.defaultUrl, - value: shortSiteName(original), - }; - }); - }; - - type Prompt = (questions: { name: K, source: (...args) => Promise<{ value: U }[]>, default?: U | ((o: U[]) => U | Promise), [key: string]: any }) => Promise<{[T in K]: U }>; @@ -206,64 +173,3 @@ export const appPrompt = async ({ projectId: project }: FirebaseProject, default // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return (apps).find(it => shortAppId(it) === appId)!; }; - -export const sitePrompt = async ({ projectId: project }: FirebaseProject, options: any) => { - const firebaseTools = await getFirebaseTools(); - const sites = await firebaseTools.hosting.sites.list({ ...options, project }).then(it => { - if (it.sites.length === 0) { - // newly created projects don't return their default site, stub one - return [{ - name: project, - defaultUrl: `https://${project}.web.app`, - type: DEFAULT_SITE_TYPE, - appId: undefined, - } as FirebaseHostingSite]; - } else { - return it.sites; - } - }); - const { siteName } = await autocomplete({ - type: 'autocomplete', - name: 'siteName', - source: searchSites(sites), - message: 'Please select a hosting site:', - default: _ => shortSiteName(sites.find(site => site.type === DEFAULT_SITE_TYPE)), - }); - if (siteName === NEW_OPTION) { - const { subdomain } = await inquirer.prompt({ - type: 'input', - name: 'subdomain', - message: 'Please provide an unique, URL-friendly id for the site (.web.app):', - }) as { subdomain: string }; - return await firebaseTools.hosting.sites.create(subdomain, { ...options, nonInteractive: true, project }); - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return (sites).find(it => shortSiteName(it) === siteName)!; -}; - -const DEFAULT_REGION = 'us-central1'; -const ALLOWED_SSR_REGIONS = [ - { name: 'us-central1 (Iowa)', value: 'us-central1' }, - { name: 'us-west1 (Oregon)', value: 'us-west1' }, - { name: 'us-east1 (South Carolina)', value: 'us-east1' }, - { name: 'europe-west1 (Belgium)', value: 'europe-west1' }, - { name: 'asia-east1 (Taiwan)', value: 'asia-east1' }, -]; - -export const projectTypePrompt = async (project: WorkspaceProject, name: string) => { - const buildTarget = [`${name}:build:production`, `${name}:build:development`]; - const serveTarget = isUniversalApp(project) ? - [`${name}:serve-ssr:production`, `${name}:serve-ssr:development`] : - [`${name}:serve:production`, `${name}:serve:development`]; - if (isUniversalApp(project) || isSSRApp(project)) { - const { ssrRegion } = await inquirer.prompt({ - type: 'list', - name: 'ssrRegion', - choices: ALLOWED_SSR_REGIONS, - message: 'In which region would you like to host server-side content?', - default: DEFAULT_REGION, - }) as { ssrRegion: string }; - return { projectType: PROJECT_TYPE.WebFrameworks, ssrRegion, buildTarget, serveTarget }; - } - return { projectType: PROJECT_TYPE.WebFrameworks, buildTarget, serveTarget }; -}; diff --git a/src/schematics/utils.ts b/src/schematics/utils.ts index 8b3ba6d1d..eff08361f 100644 --- a/src/schematics/utils.ts +++ b/src/schematics/utils.ts @@ -1,26 +1,9 @@ import { readFileSync } from 'fs'; import { join } from 'path'; import { Rule, SchematicsException, Tree, chain } from '@angular-devkit/schematics'; -import ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { addRootProvider } from '@schematics/angular/utility'; -import { findNode } from '@schematics/angular/utility/ast-utils'; -import { InsertChange, ReplaceChange, applyToUpdateRecorder } from '@schematics/angular/utility/change'; import { overwriteIfExists } from './common'; -import { DeployOptions, FEATURES, FirebaseApp, FirebaseRc, Workspace, WorkspaceProject } from './interfaces'; - -// We consider a project to be a universal project if it has a `server` architect -// target. If it does, it knows how to build the application's server. -export const isUniversalApp = ( - project: WorkspaceProject -) => project.architect?.server; - -export const isSSRApp = ( - project: WorkspaceProject -) => !!project.architect?.build.options?.ssr; - -export const hasPrerenderOption = ( - project: WorkspaceProject -) => project.architect?.prerender; +import { DeployOptions, FEATURES, FirebaseApp, FirebaseRc, Workspace } from './interfaces'; export const shortAppId = (app?: FirebaseApp) => app?.appId?.split('/').pop(); @@ -109,55 +92,6 @@ const projectFromRc = (rc: FirebaseRc, target: string): [string|undefined, strin return [project || defaultProject, site]; }; -/** - * Adds a package to the package.json - */ -export function addEnvironmentEntry( - host: Tree, - filePath: string, - data: string, -): Tree { - const fileExists = host.exists(filePath); - if (fileExists) { - const buffer = host.read(filePath); - if (!buffer) { - throw new SchematicsException(`Cannot read ${filePath}`); - } - const sourceFile = ts.createSourceFile(filePath, buffer.toString('utf-8'), ts.ScriptTarget.Latest, true); - - const envIdentifier = findNode(sourceFile as any, ts.SyntaxKind.Identifier, 'environment'); - if (!envIdentifier?.parent) { - throw new SchematicsException(`Cannot find 'environment' identifier in ${filePath}`); - } - - const envObjectLiteral = envIdentifier.parent.getChildren().find(({ kind }) => kind === ts.SyntaxKind.ObjectLiteralExpression); - if (!envObjectLiteral) { - throw new SchematicsException(`${filePath} is not in the expected format`); - } - const firebaseIdentifier = findNode(envObjectLiteral, ts.SyntaxKind.Identifier, 'firebase'); - - const recorder = host.beginUpdate(filePath); - if (firebaseIdentifier?.parent) { - const change = new ReplaceChange(filePath, firebaseIdentifier.parent.pos, firebaseIdentifier.parent.getFullText(), data); - applyToUpdateRecorder(recorder, [change]); - } else { - const openBracketToken = envObjectLiteral.getChildren().find(({ kind }) => kind === ts.SyntaxKind.OpenBraceToken); - if (openBracketToken) { - const change = new InsertChange(filePath, openBracketToken.end, `${data},`); - applyToUpdateRecorder(recorder, [change]); - } else { - throw new SchematicsException(`${filePath} is not in the expected format`); - } - } - host.commitUpdate(recorder); - } else { - host.create(filePath, `export const environment = {${data}, -};`); - } - - return host; -} - // TODO rewrite using typescript export function addFixesToServer(host: Tree) { const serverPath = `/server.ts`;