Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modular autoinit #6526

Merged
merged 16 commits into from Sep 28, 2022
13 changes: 13 additions & 0 deletions .changeset/mighty-insects-judge.md
@@ -0,0 +1,13 @@
---
'@firebase/app': minor
'@firebase/app-types': minor
'@firebase/util': minor
'@firebase/auth': patch
'@firebase/database': patch
'@firebase/firestore': patch
'@firebase/functions': patch
'@firebase/storage': patch
'firebase': minor
---

Add functionality to auto-initialize project config and emulator settings from global defaults provided by framework tooling.
3 changes: 3 additions & 0 deletions common/api-review/app.api.md
Expand Up @@ -93,6 +93,9 @@ export function initializeApp(options: FirebaseOptions, name?: string): Firebase
// @public
export function initializeApp(options: FirebaseOptions, config?: FirebaseAppSettings): FirebaseApp;

// @public
export function initializeApp(): FirebaseApp;

// @public
export function onLog(logCallback: LogCallback | null, options?: LogOptions): void;

Expand Down
3 changes: 3 additions & 0 deletions common/api-review/firestore.api.md
Expand Up @@ -233,6 +233,9 @@ export function getFirestore(): Firestore;
// @public
export function getFirestore(app: FirebaseApp): Firestore;

// @public
export function getFirestore(): Firestore;
hsubox76 marked this conversation as resolved.
Show resolved Hide resolved

// @public
export function increment(n: number): FieldValue;

Expand Down
17 changes: 17 additions & 0 deletions common/api-review/util.api.md
Expand Up @@ -195,6 +195,23 @@ export class FirebaseError extends Error {
// @public
export type FirebaseSignInProvider = 'custom' | 'email' | 'password' | 'phone' | 'anonymous' | 'google.com' | 'facebook.com' | 'github.com' | 'twitter.com' | 'microsoft.com' | 'apple.com';

// Warning: (ae-missing-release-tag) "getDefaultAppConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address these warnings

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added @public flag and added more comments to everything in defaults.ts.

//
// @public (undocumented)
export const getDefaultAppConfig: () => Record<string, string> | undefined;

// Warning: (ae-missing-release-tag) "getDefaultEmulatorHost" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const getDefaultEmulatorHost: (name: string) => string | undefined;

// Warning: (ae-forgotten-export) The symbol "ExperimentalKey" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "FirebaseDefaults" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "getExperimentalSetting" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const getExperimentalSetting: <T extends ExperimentalKey>(name: T) => FirebaseDefaults[`_${T}`];

// Warning: (ae-missing-release-tag) "getGlobal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
Expand Down
21 changes: 19 additions & 2 deletions packages/app/src/api.ts
Expand Up @@ -39,7 +39,7 @@ import {
LogOptions,
setUserLogHandler
} from '@firebase/logger';
import { deepEqual } from '@firebase/util';
import { deepEqual, getDefaultAppConfig } from '@firebase/util';

export { FirebaseError } from '@firebase/util';

Expand Down Expand Up @@ -110,10 +110,18 @@ export function initializeApp(
options: FirebaseOptions,
config?: FirebaseAppSettings
): FirebaseApp;
/**
* Creates and initializes a FirebaseApp instance.
*
* @public
*/
export function initializeApp(): FirebaseApp;
export function initializeApp(
options: FirebaseOptions,
_options?: FirebaseOptions,
rawConfig = {}
): FirebaseApp {
let options = _options;

if (typeof rawConfig !== 'object') {
const name = rawConfig;
rawConfig = { name };
Expand All @@ -132,6 +140,12 @@ export function initializeApp(
});
}

options ||= getDefaultAppConfig();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was James, heh.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Just mind that it overrides all falsy values—false, 0, "", null, undefined, NaN, etc. This can be surprise folk, especially with ints.

You can use the nullish form, ??= if you only want to override null & undefined.


if (!options) {
throw ERROR_FACTORY.create(AppError.NO_OPTIONS);
}

const existingApp = _apps.get(name) as FirebaseAppImpl;
if (existingApp) {
// return the existing app if options and config deep equal the ones in the existing app.
Expand Down Expand Up @@ -188,6 +202,9 @@ export function initializeApp(
*/
export function getApp(name: string = DEFAULT_ENTRY_NAME): FirebaseApp {
const app = _apps.get(name);
if (!app && name === DEFAULT_ENTRY_NAME) {
return initializeApp();
}
if (!app) {
throw ERROR_FACTORY.create(AppError.NO_APP, { appName: name });
}
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/errors.ts
Expand Up @@ -22,6 +22,7 @@ export const enum AppError {
BAD_APP_NAME = 'bad-app-name',
DUPLICATE_APP = 'duplicate-app',
APP_DELETED = 'app-deleted',
NO_OPTIONS = 'no-options',
INVALID_APP_ARGUMENT = 'invalid-app-argument',
INVALID_LOG_ARGUMENT = 'invalid-log-argument',
IDB_OPEN = 'idb-open',
Expand All @@ -38,6 +39,8 @@ const ERRORS: ErrorMap<AppError> = {
[AppError.DUPLICATE_APP]:
"Firebase App named '{$appName}' already exists with different options or config",
[AppError.APP_DELETED]: "Firebase App named '{$appName}' already deleted",
[AppError.NO_OPTIONS]:
'Need to provide options, when not being deployed to hosting via source.',
[AppError.INVALID_APP_ARGUMENT]:
'firebase.{$appName}() takes either no argument or a ' +
'Firebase App instance.',
Expand Down
58 changes: 55 additions & 3 deletions packages/auth/src/platform_browser/index.ts
Expand Up @@ -17,14 +17,50 @@

import { FirebaseApp, getApp, _getProvider } from '@firebase/app';

import { initializeAuth } from '..';
import {
initializeAuth,
beforeAuthStateChanged,
onIdTokenChanged,
connectAuthEmulator
} from '..';
import { registerAuth } from '../core/auth/register';
import { ClientPlatform } from '../core/util/version';
import { browserLocalPersistence } from './persistence/local_storage';
import { browserSessionPersistence } from './persistence/session_storage';
import { indexedDBLocalPersistence } from './persistence/indexed_db';
import { browserPopupRedirectResolver } from './popup_redirect';
import { Auth } from '../model/public_types';
import { Auth, User } from '../model/public_types';
import { getDefaultEmulatorHost, getExperimentalSetting } from '@firebase/util';

const DEFAULT_ID_TOKEN_MAX_AGE = 5 * 60;
const authIdTokenMaxAge =
getExperimentalSetting('authIdTokenMaxAge') || DEFAULT_ID_TOKEN_MAX_AGE;

let lastPostedIdToken: string | undefined | null = null;

const mintCookieFactory = (url: string) => async (user: User | null) => {
const idTokenResult = user && (await user.getIdTokenResult());
const idTokenAge =
idTokenResult &&
(new Date().getTime() - Date.parse(idTokenResult.issuedAtTime)) / 1_000;
if (idTokenAge && idTokenAge > authIdTokenMaxAge) {
return;
}
// Specifically trip null => undefined when logged out, to delete any existing cookie
const idToken = idTokenResult?.token;
if (lastPostedIdToken === idToken) {
return;
}
lastPostedIdToken = idToken;
await fetch(url, {
method: idToken ? 'POST' : 'DELETE',
headers: idToken
? {
'Authorization': `Bearer ${idToken}`
}
: {}
});
};

/**
* Returns the Auth instance associated with the provided {@link @firebase/app#FirebaseApp}.
Expand All @@ -41,14 +77,30 @@ export function getAuth(app: FirebaseApp = getApp()): Auth {
return provider.getImmediate();
}

return initializeAuth(app, {
const auth = initializeAuth(app, {
popupRedirectResolver: browserPopupRedirectResolver,
persistence: [
indexedDBLocalPersistence,
browserLocalPersistence,
browserSessionPersistence
]
});

const authTokenSyncUrl = getExperimentalSetting('authTokenSyncURL');
if (authTokenSyncUrl) {
const mintCookie = mintCookieFactory(authTokenSyncUrl);
beforeAuthStateChanged(auth, mintCookie, () => {
void mintCookie(auth.currentUser);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the Promise ignored? Please add a comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I fixed an eslint error the wrong way. beforeAuthStateChanged does seem to await its callbacks so returned the promise instead.

});
onIdTokenChanged(auth, user => mintCookie(user));
}

const authEmulatorHost = getDefaultEmulatorHost('auth');
if (authEmulatorHost) {
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
}

return auth;
}

registerAuth(ClientPlatform.BROWSER);
12 changes: 10 additions & 2 deletions packages/auth/src/platform_node/index.ts
Expand Up @@ -21,13 +21,14 @@ import { _createError } from '../core/util/assert';
import { FirebaseApp, getApp, _getProvider } from '@firebase/app';
import { Auth } from '../model/public_types';

import { initializeAuth, inMemoryPersistence } from '..';
import { initializeAuth, inMemoryPersistence, connectAuthEmulator } from '..';
import { registerAuth } from '../core/auth/register';
import { ClientPlatform } from '../core/util/version';
import { AuthImpl } from '../core/auth/auth_impl';

import { FetchProvider } from '../core/util/fetch_provider';
import * as fetchImpl from 'node-fetch';
import { getDefaultEmulatorHost } from '@firebase/util';

// Initialize the fetch polyfill, the types are slightly off so just cast and hope for the best
FetchProvider.initialize(
Expand All @@ -46,7 +47,14 @@ export function getAuth(app: FirebaseApp = getApp()): Auth {
return provider.getImmediate();
}

return initializeAuth(app);
const auth = initializeAuth(app);

const authEmulatorHost = getDefaultEmulatorHost('auth');
if (authEmulatorHost) {
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
}

return auth;
}

registerAuth(ClientPlatform.NODE);
Expand Down
11 changes: 9 additions & 2 deletions packages/database/src/api/Database.ts
Expand Up @@ -27,7 +27,8 @@ import { Provider } from '@firebase/component';
import {
getModularInstance,
createMockUserToken,
EmulatorMockTokenOptions
EmulatorMockTokenOptions,
getDefaultEmulatorHost
} from '@firebase/util';

import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider';
Expand Down Expand Up @@ -316,9 +317,15 @@ export function getDatabase(
app: FirebaseApp = getApp(),
url?: string
): Database {
return _getProvider(app, 'database').getImmediate({
const db = _getProvider(app, 'database').getImmediate({
identifier: url
}) as Database;
const databaseEmulatorHost = getDefaultEmulatorHost('database');
if (databaseEmulatorHost) {
const [host, port] = databaseEmulatorHost.split(':');
connectDatabaseEmulator(db, host, parseInt(port, 10));
}
return db;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/firestore/externs.json
Expand Up @@ -26,6 +26,7 @@
"packages/logger/dist/src/logger.d.ts",
"packages/webchannel-wrapper/src/index.d.ts",
"packages/util/dist/src/crypt.d.ts",
"packages/util/dist/src/defaults.d.ts",
"packages/util/dist/src/emulator.d.ts",
"packages/util/dist/src/environment.d.ts",
"packages/util/dist/src/compat.d.ts",
Expand Down
28 changes: 23 additions & 5 deletions packages/firestore/src/api/database.ts
Expand Up @@ -15,14 +15,13 @@
* limitations under the License.
*/

// eslint-disable-next-line import/no-extraneous-dependencies
import {
_getProvider,
_removeServiceInstance,
FirebaseApp,
getApp
} from '@firebase/app';
import { deepEqual } from '@firebase/util';
import { deepEqual, getDefaultEmulatorHost } from '@firebase/util';

import { User } from '../auth/user';
import {
Expand All @@ -43,7 +42,10 @@ import {
setOnlineComponentProvider
} from '../core/firestore_client';
import { makeDatabaseInfo } from '../lite-api/components';
import { Firestore as LiteFirestore } from '../lite-api/database';
import {
Firestore as LiteFirestore,
connectFirestoreEmulator
} from '../lite-api/database';
import { Query } from '../lite-api/reference';
import {
indexedDbClearPersistence,
Expand Down Expand Up @@ -215,7 +217,15 @@ export function getFirestore(app: FirebaseApp): Firestore;
*/
export function getFirestore(databaseId: string): Firestore;
/**
* Returns the existing {@link Firestore} instance that is associated with the
* Returns the existing default {@link Firestore} instance that is associated with the
* default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
* @returns The {@link Firestore} instance of the provided app.
*/
export function getFirestore(): Firestore;
/**
* Returns the existing default {@link Firestore} instance that is associated with the
* provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
* instance with default settings.
*
Expand All @@ -236,9 +246,17 @@ export function getFirestore(
typeof appOrDatabaseId === 'string'
? appOrDatabaseId
: optionalDatabaseId || DEFAULT_DATABASE_NAME;
return _getProvider(app, 'firestore').getImmediate({
const db = _getProvider(app, 'firestore').getImmediate({
identifier: databaseId
}) as Firestore;
if (!db._initialized) {
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
if (firestoreEmulatorHost) {
const [host, port] = firestoreEmulatorHost.split(':');
connectFirestoreEmulator(db, host, parseInt(port, 10));
}
}
return db;
}

/**
Expand Down
19 changes: 16 additions & 3 deletions packages/firestore/src/lite-api/database.ts
Expand Up @@ -22,7 +22,11 @@ import {
FirebaseApp,
getApp
} from '@firebase/app';
import { createMockUserToken, EmulatorMockTokenOptions } from '@firebase/util';
import {
createMockUserToken,
EmulatorMockTokenOptions,
getDefaultEmulatorHost
} from '@firebase/util';

import {
CredentialsProvider,
Expand All @@ -44,6 +48,8 @@ import {
FirestoreSettings
} from './settings';

export { EmulatorMockTokenOptions } from '@firebase/util';

declare module '@firebase/component' {
interface NameServiceMapping {
'firestore/lite': Firestore;
Expand Down Expand Up @@ -260,12 +266,19 @@ export function getFirestore(
typeof appOrDatabaseId === 'string'
? appOrDatabaseId
: optionalDatabaseId || '(default)';
return _getProvider(app, 'firestore/lite').getImmediate({
const db = _getProvider(app, 'firestore/lite').getImmediate({
identifier: databaseId
}) as Firestore;
if (!db._initialized) {
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
if (firestoreEmulatorHost) {
const [host, port] = firestoreEmulatorHost.split(':');
connectFirestoreEmulator(db, host, parseInt(port, 10));
}
}
return db;
}

export { EmulatorMockTokenOptions } from '@firebase/util';
/**
* Modify this instance to communicate with the Cloud Firestore emulator.
*
Expand Down