diff --git a/src/commands/logout.ts b/src/commands/logout.ts index 387b98e19c4..cfc126d3399 100644 --- a/src/commands/logout.ts +++ b/src/commands/logout.ts @@ -9,7 +9,8 @@ import { promptOnce } from "../prompt"; module.exports = new Command("logout [email]") .description("log the CLI out of Firebase") .action(async (email: string | undefined, options: any) => { - const globalToken = utils.getInheritedOption(options, "token") as string | undefined; + const globalToken = utils.getInheritedOption(options, "token"); + utils.assertIsStringOrUndefined(globalToken); const allAccounts = auth.getAllAccounts(); if (allAccounts.length === 0 && !globalToken) { diff --git a/src/commands/remoteconfig-get.ts b/src/commands/remoteconfig-get.ts index baff2614054..12f18f50006 100644 --- a/src/commands/remoteconfig-get.ts +++ b/src/commands/remoteconfig-get.ts @@ -6,6 +6,8 @@ import { RemoteConfigTemplate } from "../remoteconfig/interfaces"; import getProjectId = require("../getProjectId"); import { requirePermissions } from "../requirePermissions"; import { parseTemplateForTable } from "../remoteconfig/get"; +import { Options } from "../options"; +import * as utils from "../utils"; import Table = require("cli-table"); import * as fs from "fs"; @@ -32,7 +34,8 @@ module.exports = new Command("remoteconfig:get") ) .before(requireAuth) .before(requirePermissions, ["cloudconfig.configs.get"]) - .action(async (options) => { + .action(async (options: Options) => { + utils.assertIsString(options.versionNumber); const template: RemoteConfigTemplate = await rcGet.getTemplate( getProjectId(options), checkValidNumber(options.versionNumber) @@ -59,9 +62,15 @@ module.exports = new Command("remoteconfig:get") const fileOut = !!options.output; if (fileOut) { const shouldUseDefaultFilename = options.output === true || options.output === ""; - const filename = shouldUseDefaultFilename - ? options.config.get("remoteconfig.template") - : options.output; + + let filename = undefined; + if (shouldUseDefaultFilename) { + filename = options.config.src.remoteconfig!.template; + } else { + utils.assertIsString(options.output); + filename = options.output; + } + const outTemplate = { ...template }; delete outTemplate.version; fs.writeFileSync(filename, JSON.stringify(outTemplate, null, 2)); diff --git a/src/config.ts b/src/config.ts index f9d873d0cf2..6ea5063c9f0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,26 +2,26 @@ import { FirebaseConfig } from "./firebaseConfig"; -const _ = require("lodash"); -const clc = require("cli-color"); +import * as _ from "lodash"; +import * as clc from "cli-color"; +import * as fs from "fs-extra"; +import * as path from "path"; const cjson = require("cjson"); -const fs = require("fs-extra"); -const path = require("path"); -const detectProjectRoot = require("./detectProjectRoot").detectProjectRoot; -const { FirebaseError } = require("./error"); -const fsutils = require("./fsutils"); +import { detectProjectRoot } from "./detectProjectRoot"; +import { FirebaseError } from "./error"; +import * as fsutils from "./fsutils"; +import { promptOnce } from "./prompt"; +import { resolveProjectPath } from "./projectPath"; +import * as utils from "./utils"; const loadCJSON = require("./loadCJSON"); const parseBoltRules = require("./parseBoltRules"); -const { promptOnce } = require("./prompt"); -const { resolveProjectPath } = require("./projectPath"); -const utils = require("./utils"); - -type PlainObject = Record; export class Config { + static DEFAULT_FUNCTIONS_SOURCE = "functions"; + static FILENAME = "firebase.json"; - static MATERIALIZE_TARGETS = [ + static MATERIALIZE_TARGETS: Array = [ "database", "emulators", "firestore", @@ -39,6 +39,10 @@ export class Config { private _src: any; + /** + * @param src incoming firebase.json source, parsed by not validated. + * @param options command-line options. + */ constructor(src: any, options: any) { this.options = options || {}; this.projectDir = options.projectDir || detectProjectRoot(options); @@ -54,52 +58,43 @@ export class Config { ); } + // Move the deprecated top-level "rules" ket into the "database" object if (_.has(this._src, "rules")) { _.set(this._src, "database.rules", this._src.rules); } + // If a top-level key contains a string path pointing to a suported file + // type (JSON or Bolt), we read the file. + // + // TODO: This is janky and confusing behavior, we should remove it ASAP. Config.MATERIALIZE_TARGETS.forEach((target) => { if (_.get(this._src, target)) { - _.set(this.data, target, this._materialize(target)); + _.set(this.data, target, this.materialize(target)); } }); - // auto-detect functions from package.json in directory + // Auto-detect functions from package.json in directory if ( this.projectDir && !this.get("functions.source") && fsutils.fileExistsSync(this.path("functions/package.json")) ) { - this.set("functions.source", "functions"); - } - } - - _hasDeepKey(obj: PlainObject, key: string) { - if (_.has(obj, key)) { - return true; + this.set("functions.source", Config.DEFAULT_FUNCTIONS_SOURCE); } - - for (const k in obj) { - if (obj.hasOwnProperty(k)) { - if (_.isPlainObject(obj[k]) && this._hasDeepKey(obj[k] as PlainObject, key)) { - return true; - } - } - } - return false; } - _materialize(target: string) { + materialize(target: string) { const val = _.get(this._src, target); - if (_.isString(val)) { - let out = this._parseFile(target, val); + if (typeof val === "string") { + let out = this.parseFile(target, val); // if e.g. rules.json has {"rules": {}} use that - const lastSegment = _.last(target.split(".")); - if (_.size(out) === 1 && _.has(out, lastSegment)) { + const segments = target.split("."); + const lastSegment = segments[segments.length - 1]; + if (Object.keys(out).length === 1 && _.has(out, lastSegment)) { out = out[lastSegment]; } return out; - } else if (_.isPlainObject(val) || _.isArray(val)) { + } else if (val !== null && typeof val === "object") { return val; } @@ -108,7 +103,7 @@ export class Config { }); } - _parseFile(target: string, filePath: string) { + parseFile(target: string, filePath: string) { const fullPath = resolveProjectPath(this.options, filePath); const ext = path.extname(filePath); if (!fsutils.fileExistsSync(fullPath)) { @@ -158,6 +153,10 @@ export class Config { } set(key: string, value: any) { + // TODO: We should really remove all instances of config.set() around the + // codebase but until we do we need this to prevent src from going stale. + _.set(this._src, key, value); + return _.set(this.data, key, value); } @@ -167,7 +166,7 @@ export class Config { path(pathName: string) { const outPath = path.normalize(path.join(this.projectDir, pathName)); - if (_.includes(path.relative(this.projectDir, outPath), "..")) { + if (path.relative(this.projectDir, outPath).includes("..")) { throw new FirebaseError(clc.bold(pathName) + " is outside of project directory", { exit: 1 }); } return outPath; diff --git a/src/database/rulesConfig.ts b/src/database/rulesConfig.ts index 17422452378..6299d225895 100644 --- a/src/database/rulesConfig.ts +++ b/src/database/rulesConfig.ts @@ -1,6 +1,8 @@ import { FirebaseError } from "../error"; import { Config } from "../config"; import { logger } from "../logger"; +import { Options } from "../options"; +import * as utils from "../utils"; export interface RulesInstanceConfig { instance: string; @@ -18,9 +20,9 @@ interface DatabaseConfig { */ export function normalizeRulesConfig( rulesConfig: RulesInstanceConfig[], - options: any + options: Options ): RulesInstanceConfig[] { - const config = options.config as Config; + const config = options.config; return rulesConfig.map((rc) => { return { instance: rc.instance, @@ -29,18 +31,15 @@ export function normalizeRulesConfig( }); } -export function getRulesConfig(projectId: string, options: any): RulesInstanceConfig[] { - // TODO(samstern): Config should be typed - const config = options.config as any; - - const dbConfig: { rules?: string } | DatabaseConfig[] | undefined = config.get("database"); - +export function getRulesConfig(projectId: string, options: Options): RulesInstanceConfig[] { + const dbConfig = options.config.src.database; if (dbConfig === undefined) { return []; } if (!Array.isArray(dbConfig)) { if (dbConfig && dbConfig.rules) { + utils.assertIsStringOrUndefined(options.instance); const instance = options.instance || `${options.project}-default-rtdb`; return [{ rules: dbConfig.rules, instance }]; } else { @@ -50,13 +49,14 @@ export function getRulesConfig(projectId: string, options: any): RulesInstanceCo } const results: RulesInstanceConfig[] = []; + const rc = options.rc as any; for (const c of dbConfig) { if (c.target) { // Make sure the target exists (this will throw otherwise) - options.rc.requireTarget(projectId, "database", c.target); + rc.requireTarget(projectId, "database", c.target); // Get a list of db instances the target maps to - const instances: string[] = options.rc.target(projectId, "database", c.target); + const instances: string[] = rc.target(projectId, "database", c.target); for (const i of instances) { results.push({ instance: i, rules: c.rules }); } diff --git a/src/deploy/firestore/prepare.ts b/src/deploy/firestore/prepare.ts index be2bd7b863b..f6e4807b8d9 100644 --- a/src/deploy/firestore/prepare.ts +++ b/src/deploy/firestore/prepare.ts @@ -4,14 +4,15 @@ import * as clc from "cli-color"; import loadCJSON = require("../../loadCJSON"); import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy"; import utils = require("../../utils"); +import { Options } from "../../options"; /** * Prepares Firestore Rules deploys. * @param context The deploy context. * @param options The CLI options object. */ -async function prepareRules(context: any, options: any): Promise { - const rulesFile = options.config.get("firestore.rules"); +async function prepareRules(context: any, options: Options): Promise { + const rulesFile = options.config.src.firestore?.rules; if (context.firestoreRules && rulesFile) { const rulesDeploy = new RulesDeploy(options, RulesetServiceType.CLOUD_FIRESTORE); @@ -25,12 +26,12 @@ async function prepareRules(context: any, options: any): Promise { * @param context The deploy context. * @param options The CLI options object. */ -function prepareIndexes(context: any, options: any): void { - if (!context.firestoreIndexes || !options.config.get("firestore.indexes")) { +function prepareIndexes(context: any, options: Options): void { + if (!context.firestoreIndexes || !options.config.src.firestore?.indexes) { return; } - const indexesFileName = options.config.get("firestore.indexes"); + const indexesFileName = options.config.src.firestore.indexes; const indexesPath = options.config.path(indexesFileName); const parsedSrc = loadCJSON(indexesPath); diff --git a/src/deploy/functions/deploy.ts b/src/deploy/functions/deploy.ts index 0375d788601..8b23d1aaae5 100644 --- a/src/deploy/functions/deploy.ts +++ b/src/deploy/functions/deploy.ts @@ -10,6 +10,8 @@ import * as fs from "fs"; import * as gcs from "../../gcp/storage"; import * as gcf from "../../gcp/cloudfunctions"; import { Options } from "../../options"; +import { Config } from "../../config"; +import * as utils from "../../utils"; const GCP_REGION = functionsUploadRegion; @@ -45,7 +47,7 @@ export async function deploy( options: Options, payload: args.Payload ): Promise { - if (!options.config.get("functions")) { + if (!options.config.src.functions) { return; } @@ -66,10 +68,14 @@ export async function deploy( } await Promise.all(uploads); + utils.assertDefined( + options.config.src.functions.source, + "Error: 'functions.source' is not defined" + ); logSuccess( clc.green.bold("functions:") + " " + - clc.bold(options.config.get("functions.source")) + + clc.bold(options.config.src.functions.source) + " folder uploaded successfully" ); } catch (err) { diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index b9d19e863c0..e416f48c4e9 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -1,6 +1,5 @@ import * as clc from "cli-color"; -import { FirebaseError } from "../../error"; import { Options } from "../../options"; import { ensureCloudBuildEnabled } from "./ensureCloudBuildEnabled"; import { functionMatchesAnyGroup, getFilterGroups } from "./functionsDeployHelper"; @@ -14,6 +13,7 @@ import * as functionsConfig from "../../functionsConfig"; import * as getProjectId from "../../getProjectId"; import * as runtimes from "./runtimes"; import * as validate from "./validate"; +import * as utils from "../../utils"; import { logger } from "../../logger"; export async function prepare( @@ -21,7 +21,7 @@ export async function prepare( options: Options, payload: args.Payload ): Promise { - if (!options.config.has("functions")) { + if (!options.config.src.functions) { return; } @@ -60,10 +60,14 @@ export async function prepare( } // Prepare the functions directory for upload, and set context.triggers. + utils.assertDefined( + options.config.src.functions.source, + "Error: 'functions.source' is not defined" + ); logBullet( clc.cyan.bold("functions:") + " preparing " + - clc.bold(options.config.get("functions.source")) + + clc.bold(options.config.src.functions.source) + " directory for uploading..." ); context.functionsSource = await prepareFunctionsUpload(runtimeConfig, options); diff --git a/src/deploy/functions/prepareFunctionsUpload.ts b/src/deploy/functions/prepareFunctionsUpload.ts index f200ec9a302..02d80e09df2 100644 --- a/src/deploy/functions/prepareFunctionsUpload.ts +++ b/src/deploy/functions/prepareFunctionsUpload.ts @@ -14,6 +14,7 @@ import * as utils from "../../utils"; import * as fsAsync from "../../fsAsync"; import * as args from "./args"; import { Options } from "../../options"; +import { Config } from "../../config"; const CONFIG_DEST_FILE = ".runtimeconfig.json"; @@ -74,7 +75,7 @@ async function packageSource(options: Options, sourceDir: string, configValues: // you're in the public dir when you deploy. // We ignore any CONFIG_DEST_FILE that already exists, and write another one // with current config values into the archive in the "end" handler for reader - const ignore = options.config.get("functions.ignore", ["node_modules", ".git"]) as string[]; + const ignore = options.config.src.functions?.ignore || ["node_modules", ".git"]; ignore.push( "firebase-debug.log", "firebase-debug.*.log", @@ -103,10 +104,16 @@ async function packageSource(options: Options, sourceDir: string, configValues: } ); } + + utils.assertDefined(options.config.src.functions); + utils.assertDefined( + options.config.src.functions.source, + "Error: 'functions.source' is not defined" + ); utils.logBullet( clc.cyan.bold("functions:") + " packaged " + - clc.bold(options.config.get("functions.source")) + + clc.bold(options.config.src.functions.source) + " (" + filesize(archive.pointer()) + ") for uploading" @@ -118,6 +125,12 @@ export async function prepareFunctionsUpload( runtimeConfig: backend.RuntimeConfigValues, options: Options ): Promise { - const sourceDir = options.config.path(options.config.get("functions.source") as string); + utils.assertDefined(options.config.src.functions); + utils.assertDefined( + options.config.src.functions.source, + "Error: 'functions.source' is not defined" + ); + + const sourceDir = options.config.path(options.config.src.functions.source); return packageSource(options, sourceDir, runtimeConfig); } diff --git a/src/deploy/remoteconfig/prepare.ts b/src/deploy/remoteconfig/prepare.ts index dc9503c066f..34ee2a674bc 100644 --- a/src/deploy/remoteconfig/prepare.ts +++ b/src/deploy/remoteconfig/prepare.ts @@ -2,12 +2,13 @@ import { getProjectNumber } from "../../getProjectNumber"; import loadCJSON = require("../../loadCJSON"); import { getEtag } from "./functions"; import { validateInputRemoteConfigTemplate } from "./functions"; +import { Options } from "../../options"; -export default async function (context: any, options: any): Promise { +export default async function (context: any, options: Options): Promise { if (!context) { return; } - const filePath = options.config.get("remoteconfig.template"); + const filePath = options.config.src.remoteconfig?.template; if (!filePath) { return; } diff --git a/src/deploy/storage/prepare.ts b/src/deploy/storage/prepare.ts index f849758c36d..a65c8c5538c 100644 --- a/src/deploy/storage/prepare.ts +++ b/src/deploy/storage/prepare.ts @@ -2,13 +2,14 @@ import * as _ from "lodash"; import gcp = require("../../gcp"); import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy"; +import { Options } from "../../options"; /** * Prepares for a Firebase Storage deployment. * @param context The deploy context. * @param options The CLI options object. */ -export default async function (context: any, options: any): Promise { +export default async function (context: any, options: Options): Promise { let rulesConfig = options.config.get("storage"); if (!rulesConfig) { return; @@ -27,7 +28,7 @@ export default async function (context: any, options: any): Promise { rulesConfig.forEach((ruleConfig: any) => { if (ruleConfig.target) { - options.rc.requireTarget(context.projectId, "storage", ruleConfig.target); + (options.rc as any).requireTarget(context.projectId, "storage", ruleConfig.target); } rulesDeploy.addFile(ruleConfig.rules); }); diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index 4d22831325a..8813aeacd6f 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -115,14 +115,6 @@ export class Constants { return DEFAULT_PORTS[emulator]; } - static getHostKey(emulator: Emulators): string { - return `emulators.${emulator.toString()}.host`; - } - - static getPortKey(emulator: Emulators): string { - return `emulators.${emulator.toString()}.port`; - } - static description(name: Emulators): string { return EMULATOR_DESCRIPTION[name]; } diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index cd768aa1bb5..39f32215879 100644 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -42,12 +42,11 @@ import { fileExistsSync } from "../fsutils"; import { StorageEmulator } from "./storage"; import { getDefaultDatabaseInstance } from "../getDefaultDatabaseInstance"; import { getProjectDefaultAccount } from "../auth"; +import { Options } from "../options"; +import { ParsedTriggerDefinition } from "./functionsEmulatorShared"; -async function getAndCheckAddress(emulator: Emulators, options: any): Promise
{ - let host = Constants.normalizeHost( - options.config.get(Constants.getHostKey(emulator), Constants.getDefaultHost(emulator)) - ); - +async function getAndCheckAddress(emulator: Emulators, options: Options): Promise
{ + let host = options.config.src.emulators?.[emulator]?.host || Constants.getDefaultHost(emulator); if (host === "localhost" && utils.isRunningInWSL()) { // HACK(https://github.com/firebase/firebase-tools-ui/issues/332): Use IPv4 // 127.0.0.1 instead of localhost. This, combined with the hack in @@ -58,7 +57,7 @@ async function getAndCheckAddress(emulator: Emulators, options: any): Promise { +export async function startAll(options: Options, showUI: boolean = true): Promise { // Emulators config is specified in firebase.json as: // "emulators": { // "firestore": { @@ -390,6 +385,7 @@ export async function startAll(options: any, showUI: boolean = true): Promise | undefined), }, - predefinedTriggers: options.extensionTriggers, + predefinedTriggers: options.extensionTriggers as ParsedTriggerDefinition[] | undefined, nodeMajorVersion: parseRuntimeVersion( options.extensionNodeVersion || options.config.get("functions.runtime") ), @@ -469,6 +473,7 @@ export async function startAll(options: any, showUI: boolean = true): Promise { - if (options.port) { +export const actionFunction = async (options: Options) => { + if (typeof options.port === "string") { options.port = parseInt(options.port, 10); } @@ -28,6 +29,7 @@ export const actionFunction = async (options: any) => { debugPort = commandUtils.parseInspectionPort(options); } + utils.assertDefined(options.project); const hubClient = new EmulatorHubClient(options.project); let remoteEmulators: Record = {}; @@ -52,7 +54,7 @@ export const actionFunction = async (options: any) => { } else if (!options.port) { // If the user did not pass in any port and the functions emulator is not already running, we can // use the port defined for the Functions emulator in their firebase.json - options.port = options.config.get(Constants.getPortKey(Emulators.FUNCTIONS), undefined); + options.port = options.config.src.emulators?.functions?.port; } // If the port was not set by the --port flag or determined from 'firebase.json', just scan diff --git a/src/options.ts b/src/options.ts index 1ad4a0c3c48..1ff547c543d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,13 +5,19 @@ import { Config } from "./config"; export interface Options { cwd: string; configPath: string; - // OMITTED: project. Use context.projectId instead only: string; config: Config; filteredTargets: string[]; - nonInteractive: boolean; force: boolean; + // Options which are present on every command + project?: string; + account?: string; + json: boolean; + nonInteractive: boolean; + interactive: boolean; + debug: boolean; + // TODO(samstern): Remove this once options is better typed [key: string]: unknown; } diff --git a/src/serve/functions.ts b/src/serve/functions.ts index cafd1967a50..6fa3bffd42a 100644 --- a/src/serve/functions.ts +++ b/src/serve/functions.ts @@ -4,6 +4,9 @@ import { EmulatorServer } from "../emulator/emulatorServer"; import { parseRuntimeVersion } from "../emulator/functionsEmulatorUtils"; import * as getProjectId from "../getProjectId"; import { getProjectDefaultAccount } from "../auth"; +import { Options } from "../options"; +import { Config } from "../config"; +import * as utils from "../utils"; // TODO(samstern): It would be better to convert this to an EmulatorServer // but we don't have the "options" object until start() is called. @@ -16,12 +19,15 @@ export class FunctionsServer { } } - async start(options: any, partialArgs: Partial): Promise { + async start(options: Options, partialArgs: Partial): Promise { const projectId = getProjectId(options, false); - const functionsDir = path.join( - options.config.projectDir, - options.config.get("functions.source") + utils.assertDefined(options.config.src.functions); + utils.assertDefined( + options.config.src.functions.source, + "Error: 'functions.source' is not defined" ); + + const functionsDir = path.join(options.config.projectDir, options.config.src.functions.source); const account = getProjectDefaultAccount(options.config.projectDir); const nodeMajorVersion = parseRuntimeVersion(options.config.get("functions.runtime")); @@ -37,6 +43,7 @@ export class FunctionsServer { }; if (options.host) { + utils.assertIsStringOrUndefined(options.host); args.host = options.host; } @@ -44,11 +51,14 @@ export class FunctionsServer { // we can use the port argument. Otherwise it goes to hosting and // we use port + 1. if (options.port) { - const hostingRunning = options.targets && options.targets.indexOf("hosting") >= 0; + utils.assertIsNumber(options.port); + const targets = options.targets as string[] | undefined; + const port = options.port; + const hostingRunning = targets && targets.indexOf("hosting") >= 0; if (hostingRunning) { - args.port = options.port + 1; + args.port = port + 1; } else { - args.port = options.port; + args.port = port; } } diff --git a/src/test/config.spec.js b/src/test/config.spec.js index 9febcb7fa34..fc6f3a29210 100644 --- a/src/test/config.spec.js +++ b/src/test/config.spec.js @@ -24,36 +24,36 @@ describe("Config", function () { }); }); - describe("#_parseFile", function () { + describe("#parseFile", function () { it("should load a cjson file", function () { var config = new Config({}, { cwd: _fixtureDir("config-imports") }); - expect(config._parseFile("hosting", "hosting.json").public).to.equal("."); + expect(config.parseFile("hosting", "hosting.json").public).to.equal("."); }); it("should error out for an unknown file", function () { var config = new Config({}, { cwd: _fixtureDir("config-imports") }); expect(function () { - config._parseFile("hosting", "i-dont-exist.json"); + config.parseFile("hosting", "i-dont-exist.json"); }).to.throw("Imported file i-dont-exist.json does not exist"); }); it("should error out for an unrecognized extension", function () { var config = new Config({}, { cwd: _fixtureDir("config-imports") }); expect(function () { - config._parseFile("hosting", "unsupported.txt"); + config.parseFile("hosting", "unsupported.txt"); }).to.throw("unsupported.txt is not of a supported config file type"); }); }); - describe("#_materialize", function () { + describe("#materialize", function () { it("should assign unaltered if an object is found", function () { var config = new Config({ example: { foo: "bar" } }, {}); - expect(config._materialize("example").foo).to.equal("bar"); + expect(config.materialize("example").foo).to.equal("bar"); }); it("should prevent top-level key duplication", function () { var config = new Config({ rules: "rules.json" }, { cwd: _fixtureDir("dup-top-level") }); - expect(config._materialize("rules")).to.deep.equal({ ".read": true }); + expect(config.materialize("rules")).to.deep.equal({ ".read": true }); }); }); }); diff --git a/src/test/deploy/functions/prompts.spec.ts b/src/test/deploy/functions/prompts.spec.ts index c6e36b3f3c7..a3f1613da21 100644 --- a/src/test/deploy/functions/prompts.spec.ts +++ b/src/test/deploy/functions/prompts.spec.ts @@ -35,6 +35,9 @@ const SAMPLE_OPTIONS: Options = { config: {} as any, only: "functions", nonInteractive: false, + json: false, + interactive: false, + debug: false, force: false, filteredTargets: ["functions"], }; diff --git a/src/utils.ts b/src/utils.ts index 9cdb941d189..5bd6b96eec2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,7 @@ import * as process from "process"; import { Readable } from "stream"; import * as winston from "winston"; import { SPLAT } from "triple-beam"; +import { AssertionError } from "assert"; const ansiStrip = require("cli-color/strip") as (input: string) => string; import { configstore } from "./configstore"; @@ -499,3 +500,42 @@ export function isRunningInWSL(): boolean { export function thirtyDaysFromNow(): Date { return new Date(Date.now() + THIRTY_DAYS_IN_MILLISECONDS); } + +/** + * See: + * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions + */ +export function assertDefined(val: T, message?: string): asserts val is NonNullable { + if (val === undefined || val === null) { + throw new AssertionError({ + message: message || `expected value to be defined but got "${val}"`, + }); + } +} + +export function assertIsString(val: any, message?: string): asserts val is string { + if (typeof val !== "string") { + throw new AssertionError({ + message: message || `expected "string" but got "${typeof val}"`, + }); + } +} + +export function assertIsNumber(val: any, message?: string): asserts val is number { + if (typeof val !== "number") { + throw new AssertionError({ + message: message || `expected "number" but got "${typeof val}"`, + }); + } +} + +export function assertIsStringOrUndefined( + val: any, + message?: string +): asserts val is string | undefined { + if (!(val === undefined || typeof val === "string")) { + throw new AssertionError({ + message: message || `expected "string" or "undefined" but got "${typeof val}"`, + }); + } +}