UNPKG

firebase-tools

Version:
566 lines (565 loc) 22 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.findIntelligentPathForAndroid = exports.findIntelligentPathForIOS = exports.deleteAppAndroidSha = exports.createAppAndroidSha = exports.listAppAndroidSha = exports.getAppConfig = exports.writeConfigToFile = exports.getAppConfigFile = exports.listFirebaseApps = exports.createWebApp = exports.createAndroidApp = exports.createIosApp = exports.getAppPlatform = exports.ShaCertificateType = exports.AppPlatform = exports.getSdkConfig = exports.checkForApps = exports.getSdkOutputPath = exports.sdkInit = exports.getPlatform = exports.APP_LIST_PAGE_SIZE = void 0; const fs = require("fs-extra"); const ora = require("ora"); const path = require("path"); const apiv2_1 = require("../apiv2"); const api_1 = require("../api"); const error_1 = require("../error"); const logger_1 = require("../logger"); const operation_poller_1 = require("../operation-poller"); const types_1 = require("../dataconnect/types"); const projectUtils_1 = require("../projectUtils"); const prompt = require("../prompt"); const projects_1 = require("./projects"); const fileUtils_1 = require("../dataconnect/fileUtils"); const utils_1 = require("../utils"); const TIMEOUT_MILLIS = 30000; exports.APP_LIST_PAGE_SIZE = 100; const CREATE_APP_API_REQUEST_TIMEOUT_MILLIS = 15000; async function getDisplayName() { return await prompt.input("What would you like to call your app?"); } async function getPlatform(appDir, config) { let targetPlatform = await (0, fileUtils_1.getPlatformFromFolder)(appDir); if (targetPlatform === types_1.Platform.NONE) { appDir = await (0, utils_1.promptForDirectory)({ config, relativeTo: appDir, message: "We couldn't determine what kind of app you're using. Where is your app directory?", }); targetPlatform = await (0, fileUtils_1.getPlatformFromFolder)(appDir); } if (targetPlatform === types_1.Platform.NONE || targetPlatform === types_1.Platform.MULTIPLE) { if (targetPlatform === types_1.Platform.NONE) { (0, utils_1.logBullet)(`Couldn't automatically detect app your in directory ${appDir}.`); } else { (0, utils_1.logSuccess)(`Detected multiple app platforms in directory ${appDir}`); } const platforms = [ { name: "iOS (Swift)", value: types_1.Platform.IOS }, { name: "Web (JavaScript)", value: types_1.Platform.WEB }, { name: "Android (Kotlin)", value: types_1.Platform.ANDROID }, ]; targetPlatform = await prompt.select({ message: "Which platform do you want to set up an SDK for? Note: We currently do not support automatically setting up C++ or Unity projects.", choices: platforms, }); } else if (targetPlatform === types_1.Platform.FLUTTER) { (0, utils_1.logWarning)(`Detected ${targetPlatform} app in directory ${appDir}`); throw new error_1.FirebaseError(`Flutter is not supported by apps:configure. Please follow the link below to set up firebase for your Flutter app: https://firebase.google.com/docs/flutter/setup `); } else { (0, utils_1.logSuccess)(`Detected ${targetPlatform} app in directory ${appDir}`); } return targetPlatform === types_1.Platform.MULTIPLE ? AppPlatform.PLATFORM_UNSPECIFIED : targetPlatform; } exports.getPlatform = getPlatform; async function initiateIosAppCreation(options) { if (!options.nonInteractive) { options.displayName = options.displayName || (await getDisplayName()); options.bundleId = options.bundleId || (await prompt.input("Please specify your iOS app bundle ID:")); options.appStoreId = options.appStoreId || (await prompt.input("Please specify your iOS app App Store ID:")); } if (!options.bundleId) { throw new error_1.FirebaseError("Bundle ID for iOS app cannot be empty"); } const spinner = ora("Creating your iOS app").start(); try { const appData = await createIosApp(options.project, { displayName: options.displayName, bundleId: options.bundleId, appStoreId: options.appStoreId, }); spinner.succeed(); return appData; } catch (err) { spinner.fail(); throw err; } } async function initiateAndroidAppCreation(options) { if (!options.nonInteractive) { options.displayName = options.displayName || (await getDisplayName()); options.packageName = options.packageName || (await prompt.input("Please specify your Android app package name:")); } if (!options.packageName) { throw new error_1.FirebaseError("Package name for Android app cannot be empty"); } const spinner = ora("Creating your Android app").start(); try { const appData = await createAndroidApp(options.project, { displayName: options.displayName, packageName: options.packageName, }); spinner.succeed(); return appData; } catch (err) { spinner.fail(); throw err; } } async function initiateWebAppCreation(options) { if (!options.nonInteractive) { options.displayName = options.displayName || (await getDisplayName()); } if (!options.displayName) { throw new error_1.FirebaseError("Display name for Web app cannot be empty"); } const spinner = ora("Creating your Web app").start(); try { const appData = await createWebApp(options.project, { displayName: options.displayName }); spinner.succeed(); return appData; } catch (err) { spinner.fail(); throw err; } } async function sdkInit(appPlatform, options) { let appData; switch (appPlatform) { case AppPlatform.IOS: appData = await initiateIosAppCreation(options); break; case AppPlatform.ANDROID: appData = await initiateAndroidAppCreation(options); break; case AppPlatform.WEB: appData = await initiateWebAppCreation(options); break; default: throw new error_1.FirebaseError("Unexpected error. This should not happen"); } return appData; } exports.sdkInit = sdkInit; async function getSdkOutputPath(appDir, platform, config) { switch (platform) { case AppPlatform.ANDROID: const androidPath = await findIntelligentPathForAndroid(appDir, config); return path.join(androidPath, "google-services.json"); case AppPlatform.WEB: return path.join(appDir, "firebase-js-config.json"); case AppPlatform.IOS: const iosPath = await findIntelligentPathForIOS(appDir, config); return path.join(iosPath, "GoogleService-Info.plist"); } throw new error_1.FirebaseError("Platform " + platform.toString() + " is not supported yet."); } exports.getSdkOutputPath = getSdkOutputPath; function checkForApps(apps, appPlatform) { if (!apps.length) { throw new error_1.FirebaseError(`There are no ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps ` + "associated with this Firebase project"); } } exports.checkForApps = checkForApps; async function selectAppInteractively(apps, appPlatform) { checkForApps(apps, appPlatform); const choices = apps.map((app) => { return { name: `${app.displayName || app.bundleId || app.packageName}` + ` - ${app.appId} (${app.platform})`, value: app, }; }); return await prompt.select({ message: `Select the ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}` + "app to get the configuration data:", choices, }); } async function getSdkConfig(options, appPlatform, appId) { if (!appId) { let projectId = (0, projectUtils_1.needProjectId)(options); if (options.nonInteractive && !projectId) { throw new error_1.FirebaseError("Must supply app and project ids in non-interactive mode."); } else if (!projectId) { const result = await (0, projects_1.getOrPromptProject)(options); projectId = result.projectId; } const apps = await listFirebaseApps(projectId, appPlatform); checkForApps(apps, appPlatform); if (apps.length === 1) { appId = apps[0].appId; appPlatform = apps[0].platform; } else if (options.nonInteractive) { throw new error_1.FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`); } else { const appMetadata = await selectAppInteractively(apps, appPlatform); appId = appMetadata.appId; appPlatform = appMetadata.platform; } } let configData; const spinner = ora(`Downloading configuration data for your Firebase ${appPlatform} app`).start(); try { configData = await getAppConfig(appId, appPlatform); } catch (err) { spinner.fail(); throw err; } spinner.succeed(); return configData; } exports.getSdkConfig = getSdkConfig; var AppPlatform; (function (AppPlatform) { AppPlatform["PLATFORM_UNSPECIFIED"] = "PLATFORM_UNSPECIFIED"; AppPlatform["IOS"] = "IOS"; AppPlatform["ANDROID"] = "ANDROID"; AppPlatform["WEB"] = "WEB"; AppPlatform["ANY"] = "ANY"; })(AppPlatform = exports.AppPlatform || (exports.AppPlatform = {})); var ShaCertificateType; (function (ShaCertificateType) { ShaCertificateType["SHA_CERTIFICATE_TYPE_UNSPECIFIED"] = "SHA_CERTIFICATE_TYPE_UNSPECIFIED"; ShaCertificateType["SHA_1"] = "SHA_1"; ShaCertificateType["SHA_256"] = "SHA_256"; })(ShaCertificateType = exports.ShaCertificateType || (exports.ShaCertificateType = {})); function getAppPlatform(platform) { switch (platform.toUpperCase()) { case "IOS": return AppPlatform.IOS; case "ANDROID": return AppPlatform.ANDROID; case "WEB": return AppPlatform.WEB; case "": return AppPlatform.ANY; default: throw new error_1.FirebaseError("Unexpected platform. Only iOS, Android, and Web apps are supported"); } } exports.getAppPlatform = getAppPlatform; const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.firebaseApiOrigin)(), apiVersion: "v1beta1" }); async function createIosApp(projectId, options) { try { const response = await apiClient.request({ method: "POST", path: `/projects/${projectId}/iosApps`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, body: options, }); const appData = await (0, operation_poller_1.pollOperation)({ pollerName: "Create iOS app Poller", apiOrigin: (0, api_1.firebaseApiOrigin)(), apiVersion: "v1beta1", operationResourceName: response.body.name, }); return appData; } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to create iOS app for project ${projectId}. See firebase-debug.log for more info.`, { exit: 2, original: err }); } } exports.createIosApp = createIosApp; async function createAndroidApp(projectId, options) { try { const response = await apiClient.request({ method: "POST", path: `/projects/${projectId}/androidApps`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, body: options, }); const appData = await (0, operation_poller_1.pollOperation)({ pollerName: "Create Android app Poller", apiOrigin: (0, api_1.firebaseApiOrigin)(), apiVersion: "v1beta1", operationResourceName: response.body.name, }); return appData; } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to create Android app for project ${projectId}. See firebase-debug.log for more info.`, { exit: 2, original: err, }); } } exports.createAndroidApp = createAndroidApp; async function createWebApp(projectId, options) { try { const response = await apiClient.request({ method: "POST", path: `/projects/${projectId}/webApps`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, body: options, }); const appData = await (0, operation_poller_1.pollOperation)({ pollerName: "Create Web app Poller", apiOrigin: (0, api_1.firebaseApiOrigin)(), apiVersion: "v1beta1", operationResourceName: response.body.name, }); return appData; } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to create Web app for project ${projectId}. See firebase-debug.log for more info.`, { exit: 2, original: err }); } } exports.createWebApp = createWebApp; function getListAppsResourceString(projectId, platform) { let resourceSuffix; switch (platform) { case AppPlatform.IOS: resourceSuffix = "/iosApps"; break; case AppPlatform.ANDROID: resourceSuffix = "/androidApps"; break; case AppPlatform.WEB: resourceSuffix = "/webApps"; break; case AppPlatform.ANY: resourceSuffix = ":searchApps"; break; default: throw new error_1.FirebaseError("Unexpected platform. Only support iOS, Android and Web apps"); } return `/projects/${projectId}${resourceSuffix}`; } async function listFirebaseApps(projectId, platform, pageSize = exports.APP_LIST_PAGE_SIZE) { const apps = []; try { let nextPageToken; do { const queryParams = { pageSize }; if (nextPageToken) { queryParams.pageToken = nextPageToken; } const response = await apiClient.request({ method: "GET", path: getListAppsResourceString(projectId, platform), queryParams, timeout: TIMEOUT_MILLIS, }); if (response.body.apps) { const appsOnPage = response.body.apps.map((app) => (app.platform ? app : Object.assign(Object.assign({}, app), { platform }))); apps.push(...appsOnPage); } nextPageToken = response.body.nextPageToken; } while (nextPageToken); return apps; } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to list Firebase ${platform === AppPlatform.ANY ? "" : platform + " "}` + "apps. See firebase-debug.log for more info.", { exit: 2, original: err, }); } } exports.listFirebaseApps = listFirebaseApps; function getAppConfigResourceString(appId, platform) { let platformResource; switch (platform) { case AppPlatform.IOS: platformResource = "iosApps"; break; case AppPlatform.ANDROID: platformResource = "androidApps"; break; case AppPlatform.WEB: platformResource = "webApps"; break; default: throw new error_1.FirebaseError("Unexpected app platform"); } return `/projects/-/${platformResource}/${appId}/config`; } function parseConfigFromResponse(responseBody, platform) { if (platform === AppPlatform.WEB) { return { fileName: "firebase-js-config.json", fileContents: JSON.stringify(responseBody, null, 2), }; } else if ("configFilename" in responseBody) { return { fileName: responseBody.configFilename, fileContents: Buffer.from(responseBody.configFileContents, "base64").toString("utf8"), }; } throw new error_1.FirebaseError("Unexpected app platform"); } function getAppConfigFile(config, platform) { return parseConfigFromResponse(config, platform); } exports.getAppConfigFile = getAppConfigFile; async function writeConfigToFile(filename, nonInteractive, fileContents) { if (fs.existsSync(filename)) { if (nonInteractive) { throw new error_1.FirebaseError(`${filename} already exists`); } const overwrite = await prompt.confirm(`${filename} already exists. Do you want to overwrite?`); if (!overwrite) { return false; } } await fs.writeFile(filename, fileContents); return true; } exports.writeConfigToFile = writeConfigToFile; async function getAppConfig(appId, platform) { try { const response = await apiClient.request({ method: "GET", path: getAppConfigResourceString(appId, platform), timeout: TIMEOUT_MILLIS, }); return response.body; } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to get ${platform} app configuration. See firebase-debug.log for more info.`, { exit: 2, original: err, }); } } exports.getAppConfig = getAppConfig; async function listAppAndroidSha(projectId, appId) { const shaCertificates = []; try { const response = await apiClient.request({ method: "GET", path: `/projects/${projectId}/androidApps/${appId}/sha`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, }); if (response.body.certificates) { shaCertificates.push(...response.body.certificates); } return shaCertificates; } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to list SHA certificate hashes for Android app ${appId}.` + " See firebase-debug.log for more info.", { exit: 2, original: err, }); } } exports.listAppAndroidSha = listAppAndroidSha; async function createAppAndroidSha(projectId, appId, options) { try { const response = await apiClient.request({ method: "POST", path: `/projects/${projectId}/androidApps/${appId}/sha`, body: options, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, }); const shaCertificate = response.body; return shaCertificate; } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to create SHA certificate hash for Android app ${appId}. See firebase-debug.log for more info.`, { exit: 2, original: err, }); } } exports.createAppAndroidSha = createAppAndroidSha; async function deleteAppAndroidSha(projectId, appId, shaId) { try { await apiClient.request({ method: "DELETE", path: `/projects/${projectId}/androidApps/${appId}/sha/${shaId}`, timeout: CREATE_APP_API_REQUEST_TIMEOUT_MILLIS, }); } catch (err) { logger_1.logger.debug(err.message); throw new error_1.FirebaseError(`Failed to delete SHA certificate hash for Android app ${appId}. See firebase-debug.log for more info.`, { exit: 2, original: err, }); } } exports.deleteAppAndroidSha = deleteAppAndroidSha; async function findIntelligentPathForIOS(appDir, options) { const currentFiles = await fs.readdir(appDir, { withFileTypes: true }); for (let i = 0; i < currentFiles.length; i++) { const dirent = currentFiles[i]; const xcodeStr = ".xcodeproj"; const file = dirent.name; if (file.endsWith(xcodeStr)) { return path.join(appDir, file.substring(0, file.length - xcodeStr.length)); } else if (file === "Info.plist" || file === "Assets.xcassets" || (dirent.isDirectory() && file === "Preview Content")) { return appDir; } } let outputPath = null; if (!options.nonInteractive) { outputPath = await (0, utils_1.promptForDirectory)({ config: options.config, message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`, relativeTo: appDir, }); } if (!outputPath) { throw new Error("We weren't able to automatically determine the output directory."); } return outputPath; } exports.findIntelligentPathForIOS = findIntelligentPathForIOS; async function findIntelligentPathForAndroid(appDir, options) { const paths = appDir.split("/"); if (paths[0] === "app") { return appDir; } else { const currentFiles = await fs.readdir(appDir, { withFileTypes: true }); const dirs = []; for (const fileOrDir of currentFiles) { if (fileOrDir.isDirectory()) { if (fileOrDir.name !== "gradle") { dirs.push(fileOrDir.name); } if (fileOrDir.name === "src") { return appDir; } } } let module = path.join(appDir, "app"); if (dirs.length === 1 && dirs[0] === "app") { return module; } if (!options.nonInteractive) { module = await (0, utils_1.promptForDirectory)({ config: options.config, message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`, }); } return module; } } exports.findIntelligentPathForAndroid = findIntelligentPathForAndroid;