diff --git a/packages/app/index.node.ts b/packages/app/index.node.ts index 28128141aa4..560feb223ff 100644 --- a/packages/app/index.node.ts +++ b/packages/app/index.node.ts @@ -17,7 +17,7 @@ import { FirebaseNamespace } from '@firebase/app-types'; import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { createFirebaseNamespace } from './src/firebaseApp'; +import { createFirebaseNamespace } from './src/firebaseNamespace'; import Storage from 'dom-storage'; import { XMLHttpRequest } from 'xmlhttprequest'; diff --git a/packages/app/index.rn.ts b/packages/app/index.rn.ts index 3f0f3e2d0ac..fc8b85ef14b 100644 --- a/packages/app/index.rn.ts +++ b/packages/app/index.rn.ts @@ -17,7 +17,7 @@ import { FirebaseNamespace } from '@firebase/app-types'; import { _FirebaseNamespace } from '@firebase/app-types/private'; -import { createFirebaseNamespace } from './src/firebaseApp'; +import { createFirebaseNamespace } from './src/firebaseNamespace'; /** * To avoid having to include the @types/react-native package, which breaks diff --git a/packages/app/index.ts b/packages/app/index.ts index e00fd6eefed..046059673ac 100644 --- a/packages/app/index.ts +++ b/packages/app/index.ts @@ -16,7 +16,7 @@ */ import { FirebaseNamespace } from '@firebase/app-types'; -import { createFirebaseNamespace } from './src/firebaseApp'; +import { createFirebaseNamespace } from './src/firebaseNamespace'; // Node detection logic from: https://github.com/iliakan/detect-node/ let isNode = false; diff --git a/packages/app/src/errors.ts b/packages/app/src/errors.ts new file mode 100644 index 00000000000..e6e229057d2 --- /dev/null +++ b/packages/app/src/errors.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ErrorFactory } from '@firebase/util'; + +export const enum AppError { + NO_APP = 'no-app', + BAD_APP_NAME = 'bad-app-name', + DUPLICATE_APP = 'duplicate-app', + APP_DELETED = 'app-deleted', + DUPLICATE_SERVICE = 'duplicate-service', + INVALID_APP_ARGUMENT = 'invalid-app-argument' +} + +const errors: { readonly [code in AppError]: string } = { + [AppError.NO_APP]: + "No Firebase App '{$name}' has been created - " + + 'call Firebase App.initializeApp()', + [AppError.BAD_APP_NAME]: "Illegal App name: '{$name}", + [AppError.DUPLICATE_APP]: "Firebase App named '{$name}' already exists", + [AppError.APP_DELETED]: "Firebase App named '{$name}' already deleted", + [AppError.DUPLICATE_SERVICE]: + "Firebase service named '{$name}' already registered", + [AppError.INVALID_APP_ARGUMENT]: + 'firebase.{$name}() takes either no argument or a ' + + 'Firebase App instance.' +}; + +let appErrors = new ErrorFactory('app', 'Firebase', errors); + +export function error(code: AppError, args?: { [name: string]: any }) { + throw appErrors.create(code, args); +} diff --git a/packages/app/src/firebaseApp.ts b/packages/app/src/firebaseApp.ts index 6ff743cd170..968ed1ce7c0 100644 --- a/packages/app/src/firebaseApp.ts +++ b/packages/app/src/firebaseApp.ts @@ -24,27 +24,12 @@ import { import { _FirebaseApp, _FirebaseNamespace, - FirebaseService, - FirebaseServiceFactory, - FirebaseServiceNamespace, - AppHook + FirebaseService } from '@firebase/app-types/private'; -import { - createSubscribe, - deepCopy, - deepExtend, - ErrorFactory, - FirebaseError, - Observer, - patchProperty, - Subscribe -} from '@firebase/util'; - -const contains = function(obj, key) { - return Object.prototype.hasOwnProperty.call(obj, key); -}; +import { deepCopy, deepExtend } from '@firebase/util'; +import { error, AppError } from './errors'; -const DEFAULT_ENTRY_NAME = '[DEFAULT]'; +export const DEFAULT_ENTRY_NAME = '[DEFAULT]'; // An array to capture listeners before the true auth functions // exist @@ -54,7 +39,7 @@ let tokenListeners: any[] = []; * Global context object for a collection of services using * a shared authentication state. */ -class FirebaseAppImpl implements FirebaseApp { +export class FirebaseAppImpl implements FirebaseApp { private options_: FirebaseOptions; private name_: string; private isDeleted_ = false; @@ -212,7 +197,7 @@ class FirebaseAppImpl implements FirebaseApp { */ private checkDestroyed_(): void { if (this.isDeleted_) { - error('app-deleted', { name: this.name_ }); + error(AppError.APP_DELETED, { name: this.name_ }); } } } @@ -222,262 +207,3 @@ class FirebaseAppImpl implements FirebaseApp { (FirebaseAppImpl.prototype.name && FirebaseAppImpl.prototype.options) || FirebaseAppImpl.prototype.delete || console.log('dc'); - -/** - * Return a firebase namespace object. - * - * In production, this will be called exactly once and the result - * assigned to the 'firebase' global. It may be called multiple times - * in unit tests. - */ -export function createFirebaseNamespace(): FirebaseNamespace { - let apps_: { [name: string]: FirebaseApp } = {}; - let factories: { [service: string]: FirebaseServiceFactory } = {}; - let appHooks: { [service: string]: AppHook } = {}; - - // A namespace is a plain JavaScript Object. - let namespace = { - // Hack to prevent Babel from modifying the object returned - // as the firebase namespace. - __esModule: true, - initializeApp: initializeApp, - app: app as any, - apps: null as any, - Promise: Promise, - SDK_VERSION: '${JSCORE_VERSION}', - INTERNAL: { - registerService: registerService, - createFirebaseNamespace: createFirebaseNamespace, - extendNamespace: extendNamespace, - createSubscribe: createSubscribe, - ErrorFactory: ErrorFactory, - removeApp: removeApp, - factories: factories, - useAsService: useAsService, - Promise: Promise, - deepExtend: deepExtend - } - }; - - // Inject a circular default export to allow Babel users who were previously - // using: - // - // import firebase from 'firebase'; - // which becomes: var firebase = require('firebase').default; - // - // instead of - // - // import * as firebase from 'firebase'; - // which becomes: var firebase = require('firebase'); - patchProperty(namespace, 'default', namespace); - - // firebase.apps is a read-only getter. - Object.defineProperty(namespace, 'apps', { - get: getApps - }); - - /** - * Called by App.delete() - but before any services associated with the App - * are deleted. - */ - function removeApp(name: string): void { - let app = apps_[name]; - callAppHooks(app, 'delete'); - delete apps_[name]; - } - - /** - * Get the App object for a given name (or DEFAULT). - */ - function app(name?: string): FirebaseApp { - name = name || DEFAULT_ENTRY_NAME; - if (!contains(apps_, name)) { - error('no-app', { name: name }); - } - return apps_[name]; - } - - patchProperty(app, 'App', FirebaseAppImpl); - - /** - * Create a new App instance (name must be unique). - */ - function initializeApp( - options: FirebaseOptions, - config?: FirebaseAppConfig - ): FirebaseApp; - function initializeApp(options: FirebaseOptions, name?: string): FirebaseApp; - function initializeApp(options: FirebaseOptions, rawConfig = {}) { - if (typeof rawConfig !== 'object' || rawConfig === null) { - const name = rawConfig; - rawConfig = { name }; - } - - const config = rawConfig as FirebaseAppConfig; - - if (config.name === undefined) { - config.name = DEFAULT_ENTRY_NAME; - } - - const { name } = config; - - if (typeof name !== 'string' || !name) { - error('bad-app-name', { name: name + '' }); - } - - if (contains(apps_, name)) { - error('duplicate-app', { name: name }); - } - - let app = new FirebaseAppImpl( - options, - config!, - namespace as FirebaseNamespace - ); - - apps_[name!] = app; - callAppHooks(app, 'create'); - - return app; - } - - /* - * Return an array of all the non-deleted FirebaseApps. - */ - function getApps(): FirebaseApp[] { - // Make a copy so caller cannot mutate the apps list. - return Object.keys(apps_).map(name => apps_[name]); - } - - /* - * Register a Firebase Service. - * - * firebase.INTERNAL.registerService() - * - * TODO: Implement serviceProperties. - */ - function registerService( - name: string, - createService: FirebaseServiceFactory, - serviceProperties?: { [prop: string]: any }, - appHook?: AppHook, - allowMultipleInstances?: boolean - ): FirebaseServiceNamespace { - // Cannot re-register a service that already exists - if (factories[name]) { - error('duplicate-service', { name: name }); - } - - // Capture the service factory for later service instantiation - factories[name] = createService; - - // Capture the appHook, if passed - if (appHook) { - appHooks[name] = appHook; - - // Run the **new** app hook on all existing apps - getApps().forEach(app => { - appHook('create', app); - }); - } - - // The Service namespace is an accessor function ... - const serviceNamespace = (appArg: FirebaseApp = app()) => { - if (typeof (appArg as any)[name] !== 'function') { - // Invalid argument. - // This happens in the following case: firebase.storage('gs:/') - error('invalid-app-argument', { name: name }); - } - - // Forward service instance lookup to the FirebaseApp. - return (appArg as any)[name](); - }; - - // ... and a container for service-level properties. - if (serviceProperties !== undefined) { - deepExtend(serviceNamespace, serviceProperties); - } - - // Monkey-patch the serviceNamespace onto the firebase namespace - (namespace as any)[name] = serviceNamespace; - - // Patch the FirebaseAppImpl prototype - FirebaseAppImpl.prototype[name] = function(...args) { - const serviceFxn = this._getService.bind(this, name); - return serviceFxn.apply(this, allowMultipleInstances ? args : []); - }; - - return serviceNamespace; - } - - /** - * Patch the top-level firebase namespace with additional properties. - * - * firebase.INTERNAL.extendNamespace() - */ - function extendNamespace(props: { [prop: string]: any }): void { - deepExtend(namespace, props); - } - - function callAppHooks(app: FirebaseApp, eventName: string) { - Object.keys(factories).forEach(serviceName => { - // Ignore virtual services - let factoryName = useAsService(app, serviceName); - if (factoryName === null) { - return; - } - - if (appHooks[factoryName]) { - appHooks[factoryName](eventName, app); - } - }); - } - - // Map the requested service to a registered service name - // (used to map auth to serverAuth service when needed). - function useAsService(app: FirebaseApp, name: string): string | null { - if (name === 'serverAuth') { - return null; - } - - let useService = name; - let options = app.options; - - return useService; - } - - return (namespace as any) as FirebaseNamespace; -} - -type AppError = - | 'no-app' - | 'bad-app-name' - | 'duplicate-app' - | 'app-deleted' - | 'duplicate-service' - | 'sa-not-supported' - | 'invalid-app-argument'; - -function error(code: AppError, args?: { [name: string]: any }) { - throw appErrors.create(code, args); -} - -const errors: { readonly [code in AppError]: string } = { - 'no-app': - "No Firebase App '{$name}' has been created - " + - 'call Firebase App.initializeApp()', - 'bad-app-name': "Illegal App name: '{$name}", - 'duplicate-app': "Firebase App named '{$name}' already exists", - 'app-deleted': "Firebase App named '{$name}' already deleted", - 'duplicate-service': "Firebase service named '{$name}' already registered", - 'sa-not-supported': - 'Initializing the Firebase SDK with a service ' + - 'account is only allowed in a Node.js environment. On client ' + - 'devices, you should instead initialize the SDK with an api key and ' + - 'auth domain', - 'invalid-app-argument': - 'firebase.{$name}() takes either no argument or a ' + - 'Firebase App instance.' -}; - -let appErrors = new ErrorFactory('app', 'Firebase', errors); diff --git a/packages/app/src/firebaseNamespace.ts b/packages/app/src/firebaseNamespace.ts new file mode 100644 index 00000000000..06c89ee3cfb --- /dev/null +++ b/packages/app/src/firebaseNamespace.ts @@ -0,0 +1,269 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + FirebaseApp, + FirebaseOptions, + FirebaseNamespace, + FirebaseAppConfig +} from '@firebase/app-types'; +import { + _FirebaseApp, + _FirebaseNamespace, + FirebaseService, + FirebaseServiceFactory, + FirebaseServiceNamespace, + AppHook +} from '@firebase/app-types/private'; +import { + createSubscribe, + deepExtend, + ErrorFactory, + patchProperty +} from '@firebase/util'; +import { FirebaseAppImpl, DEFAULT_ENTRY_NAME } from './firebaseApp'; +import { error, AppError } from './errors'; + +const contains = function(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +}; + +/** + * Return a firebase namespace object. + * + * In production, this will be called exactly once and the result + * assigned to the 'firebase' global. It may be called multiple times + * in unit tests. + */ +export function createFirebaseNamespace(): FirebaseNamespace { + let apps_: { [name: string]: FirebaseApp } = {}; + let factories: { [service: string]: FirebaseServiceFactory } = {}; + let appHooks: { [service: string]: AppHook } = {}; + + // A namespace is a plain JavaScript Object. + let namespace = { + // Hack to prevent Babel from modifying the object returned + // as the firebase namespace. + __esModule: true, + initializeApp: initializeApp, + app: app as any, + apps: null as any, + Promise: Promise, + SDK_VERSION: '${JSCORE_VERSION}', + INTERNAL: { + registerService: registerService, + createFirebaseNamespace: createFirebaseNamespace, + extendNamespace: extendNamespace, + createSubscribe: createSubscribe, + ErrorFactory: ErrorFactory, + removeApp: removeApp, + factories: factories, + useAsService: useAsService, + Promise: Promise, + deepExtend: deepExtend + } + }; + + // Inject a circular default export to allow Babel users who were previously + // using: + // + // import firebase from 'firebase'; + // which becomes: var firebase = require('firebase').default; + // + // instead of + // + // import * as firebase from 'firebase'; + // which becomes: var firebase = require('firebase'); + patchProperty(namespace, 'default', namespace); + + // firebase.apps is a read-only getter. + Object.defineProperty(namespace, 'apps', { + get: getApps + }); + + /** + * Called by App.delete() - but before any services associated with the App + * are deleted. + */ + function removeApp(name: string): void { + let app = apps_[name]; + callAppHooks(app, 'delete'); + delete apps_[name]; + } + + /** + * Get the App object for a given name (or DEFAULT). + */ + function app(name?: string): FirebaseApp { + name = name || DEFAULT_ENTRY_NAME; + if (!contains(apps_, name)) { + error(AppError.NO_APP, { name: name }); + } + return apps_[name]; + } + + patchProperty(app, 'App', FirebaseAppImpl); + + /** + * Create a new App instance (name must be unique). + */ + function initializeApp( + options: FirebaseOptions, + config?: FirebaseAppConfig + ): FirebaseApp; + function initializeApp(options: FirebaseOptions, name?: string): FirebaseApp; + function initializeApp(options: FirebaseOptions, rawConfig = {}) { + if (typeof rawConfig !== 'object' || rawConfig === null) { + const name = rawConfig; + rawConfig = { name }; + } + + const config = rawConfig as FirebaseAppConfig; + + if (config.name === undefined) { + config.name = DEFAULT_ENTRY_NAME; + } + + const { name } = config; + + if (typeof name !== 'string' || !name) { + error(AppError.BAD_APP_NAME, { name: name + '' }); + } + + if (contains(apps_, name)) { + error(AppError.DUPLICATE_APP, { name: name }); + } + + let app = new FirebaseAppImpl( + options, + config!, + namespace as FirebaseNamespace + ); + + apps_[name!] = app; + callAppHooks(app, 'create'); + + return app; + } + + /* + * Return an array of all the non-deleted FirebaseApps. + */ + function getApps(): FirebaseApp[] { + // Make a copy so caller cannot mutate the apps list. + return Object.keys(apps_).map(name => apps_[name]); + } + + /* + * Register a Firebase Service. + * + * firebase.INTERNAL.registerService() + * + * TODO: Implement serviceProperties. + */ + function registerService( + name: string, + createService: FirebaseServiceFactory, + serviceProperties?: { [prop: string]: any }, + appHook?: AppHook, + allowMultipleInstances?: boolean + ): FirebaseServiceNamespace { + // Cannot re-register a service that already exists + if (factories[name]) { + error(AppError.DUPLICATE_SERVICE, { name: name }); + } + + // Capture the service factory for later service instantiation + factories[name] = createService; + + // Capture the appHook, if passed + if (appHook) { + appHooks[name] = appHook; + + // Run the **new** app hook on all existing apps + getApps().forEach(app => { + appHook('create', app); + }); + } + + // The Service namespace is an accessor function ... + const serviceNamespace = (appArg: FirebaseApp = app()) => { + if (typeof (appArg as any)[name] !== 'function') { + // Invalid argument. + // This happens in the following case: firebase.storage('gs:/') + error(AppError.INVALID_APP_ARGUMENT, { name: name }); + } + + // Forward service instance lookup to the FirebaseApp. + return (appArg as any)[name](); + }; + + // ... and a container for service-level properties. + if (serviceProperties !== undefined) { + deepExtend(serviceNamespace, serviceProperties); + } + + // Monkey-patch the serviceNamespace onto the firebase namespace + (namespace as any)[name] = serviceNamespace; + + // Patch the FirebaseAppImpl prototype + FirebaseAppImpl.prototype[name] = function(...args) { + const serviceFxn = this._getService.bind(this, name); + return serviceFxn.apply(this, allowMultipleInstances ? args : []); + }; + + return serviceNamespace; + } + + /** + * Patch the top-level firebase namespace with additional properties. + * + * firebase.INTERNAL.extendNamespace() + */ + function extendNamespace(props: { [prop: string]: any }): void { + deepExtend(namespace, props); + } + + function callAppHooks(app: FirebaseApp, eventName: string) { + Object.keys(factories).forEach(serviceName => { + // Ignore virtual services + let factoryName = useAsService(app, serviceName); + if (factoryName === null) { + return; + } + + if (appHooks[factoryName]) { + appHooks[factoryName](eventName, app); + } + }); + } + + // Map the requested service to a registered service name + // (used to map auth to serverAuth service when needed). + function useAsService(app: FirebaseApp, name: string): string | null { + if (name === 'serverAuth') { + return null; + } + + let useService = name; + let options = app.options; + + return useService; + } + + return (namespace as any) as FirebaseNamespace; +} diff --git a/packages/app/test/firebaseApp.test.ts b/packages/app/test/firebaseApp.test.ts index 9a9d15ce982..8d20221e620 100644 --- a/packages/app/test/firebaseApp.test.ts +++ b/packages/app/test/firebaseApp.test.ts @@ -21,7 +21,7 @@ import { _FirebaseNamespace, FirebaseService } from '@firebase/app-types/private'; -import { createFirebaseNamespace } from '../src/firebaseApp'; +import { createFirebaseNamespace } from '../src/firebaseNamespace'; import { assert } from 'chai'; describe('Firebase App Class', () => {