Skip to content

Commit fdd4ab4

Browse files
authored
modular autoinit (#6526)
Add functionality to read config from __FIREBASE_DEFAULTS__ global or env variable. See go/firebase-api-client-autoinit
1 parent ee871fc commit fdd4ab4

File tree

17 files changed

+333
-30
lines changed

17 files changed

+333
-30
lines changed

.changeset/mighty-insects-judge.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@firebase/app': minor
3+
'@firebase/app-types': minor
4+
'@firebase/util': minor
5+
'@firebase/auth': patch
6+
'@firebase/database': patch
7+
'@firebase/firestore': patch
8+
'@firebase/functions': patch
9+
'@firebase/storage': patch
10+
'firebase': minor
11+
---
12+
13+
Add functionality to auto-initialize project config and emulator settings from global defaults provided by framework tooling.

common/api-review/app.api.md

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export function initializeApp(options: FirebaseOptions, name?: string): Firebase
9393
// @public
9494
export function initializeApp(options: FirebaseOptions, config?: FirebaseAppSettings): FirebaseApp;
9595

96+
// @public
97+
export function initializeApp(): FirebaseApp;
98+
9699
// @public
97100
export function onLog(logCallback: LogCallback | null, options?: LogOptions): void;
98101

common/api-review/firestore.api.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,10 @@ export function getDocsFromCache<T>(query: Query<T>): Promise<QuerySnapshot<T>>;
262262
export function getDocsFromServer<T>(query: Query<T>): Promise<QuerySnapshot<T>>;
263263

264264
// @public
265-
export function getFirestore(): Firestore;
265+
export function getFirestore(app: FirebaseApp): Firestore;
266266

267267
// @public
268-
export function getFirestore(app: FirebaseApp): Firestore;
268+
export function getFirestore(): Firestore;
269269

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

common/api-review/util.api.md

+26
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,28 @@ export function errorPrefix(fnName: string, argName: string): string;
173173
// @public (undocumented)
174174
export type Executor<T> = (observer: Observer<T>) => void;
175175

176+
// @public
177+
export type ExperimentalKey = 'authTokenSyncURL' | 'authIdTokenMaxAge';
178+
176179
// Warning: (ae-missing-release-tag) "extractQuerystring" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
177180
//
178181
// @public
179182
export function extractQuerystring(url: string): string;
180183

184+
// @public
185+
export interface FirebaseDefaults {
186+
// (undocumented)
187+
[key: string]: unknown;
188+
// (undocumented)
189+
_authIdTokenMaxAge?: number;
190+
// (undocumented)
191+
_authTokenSyncURL?: string;
192+
// (undocumented)
193+
config?: Record<string, string>;
194+
// (undocumented)
195+
emulatorHosts?: Record<string, string>;
196+
}
197+
181198
// Warning: (ae-missing-release-tag) "FirebaseError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
182199
//
183200
// @public (undocumented)
@@ -195,6 +212,15 @@ export class FirebaseError extends Error {
195212
// @public
196213
export type FirebaseSignInProvider = 'custom' | 'email' | 'password' | 'phone' | 'anonymous' | 'google.com' | 'facebook.com' | 'github.com' | 'twitter.com' | 'microsoft.com' | 'apple.com';
197214

215+
// @public
216+
export const getDefaultAppConfig: () => Record<string, string> | undefined;
217+
218+
// @public
219+
export const getDefaultEmulatorHost: (productName: string) => string | undefined;
220+
221+
// @public
222+
export const getExperimentalSetting: <T extends ExperimentalKey>(name: T) => FirebaseDefaults[`_${T}`];
223+
198224
// Warning: (ae-missing-release-tag) "getGlobal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
199225
//
200226
// @public

packages/app/src/api.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
LogOptions,
4040
setUserLogHandler
4141
} from '@firebase/logger';
42-
import { deepEqual } from '@firebase/util';
42+
import { deepEqual, getDefaultAppConfig } from '@firebase/util';
4343

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

@@ -110,10 +110,18 @@ export function initializeApp(
110110
options: FirebaseOptions,
111111
config?: FirebaseAppSettings
112112
): FirebaseApp;
113+
/**
114+
* Creates and initializes a FirebaseApp instance.
115+
*
116+
* @public
117+
*/
118+
export function initializeApp(): FirebaseApp;
113119
export function initializeApp(
114-
options: FirebaseOptions,
120+
_options?: FirebaseOptions,
115121
rawConfig = {}
116122
): FirebaseApp {
123+
let options = _options;
124+
117125
if (typeof rawConfig !== 'object') {
118126
const name = rawConfig;
119127
rawConfig = { name };
@@ -132,6 +140,12 @@ export function initializeApp(
132140
});
133141
}
134142

143+
options ||= getDefaultAppConfig();
144+
145+
if (!options) {
146+
throw ERROR_FACTORY.create(AppError.NO_OPTIONS);
147+
}
148+
135149
const existingApp = _apps.get(name) as FirebaseAppImpl;
136150
if (existingApp) {
137151
// return the existing app if options and config deep equal the ones in the existing app.
@@ -188,6 +202,9 @@ export function initializeApp(
188202
*/
189203
export function getApp(name: string = DEFAULT_ENTRY_NAME): FirebaseApp {
190204
const app = _apps.get(name);
205+
if (!app && name === DEFAULT_ENTRY_NAME) {
206+
return initializeApp();
207+
}
191208
if (!app) {
192209
throw ERROR_FACTORY.create(AppError.NO_APP, { appName: name });
193210
}

packages/app/src/errors.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const enum AppError {
2222
BAD_APP_NAME = 'bad-app-name',
2323
DUPLICATE_APP = 'duplicate-app',
2424
APP_DELETED = 'app-deleted',
25+
NO_OPTIONS = 'no-options',
2526
INVALID_APP_ARGUMENT = 'invalid-app-argument',
2627
INVALID_LOG_ARGUMENT = 'invalid-log-argument',
2728
IDB_OPEN = 'idb-open',
@@ -38,6 +39,8 @@ const ERRORS: ErrorMap<AppError> = {
3839
[AppError.DUPLICATE_APP]:
3940
"Firebase App named '{$appName}' already exists with different options or config",
4041
[AppError.APP_DELETED]: "Firebase App named '{$appName}' already deleted",
42+
[AppError.NO_OPTIONS]:
43+
'Need to provide options, when not being deployed to hosting via source.',
4144
[AppError.INVALID_APP_ARGUMENT]:
4245
'firebase.{$appName}() takes either no argument or a ' +
4346
'Firebase App instance.',

packages/auth/src/platform_browser/index.ts

+55-3
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,50 @@
1717

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

20-
import { initializeAuth } from '..';
20+
import {
21+
initializeAuth,
22+
beforeAuthStateChanged,
23+
onIdTokenChanged,
24+
connectAuthEmulator
25+
} from '..';
2126
import { registerAuth } from '../core/auth/register';
2227
import { ClientPlatform } from '../core/util/version';
2328
import { browserLocalPersistence } from './persistence/local_storage';
2429
import { browserSessionPersistence } from './persistence/session_storage';
2530
import { indexedDBLocalPersistence } from './persistence/indexed_db';
2631
import { browserPopupRedirectResolver } from './popup_redirect';
27-
import { Auth } from '../model/public_types';
32+
import { Auth, User } from '../model/public_types';
33+
import { getDefaultEmulatorHost, getExperimentalSetting } from '@firebase/util';
34+
35+
const DEFAULT_ID_TOKEN_MAX_AGE = 5 * 60;
36+
const authIdTokenMaxAge =
37+
getExperimentalSetting('authIdTokenMaxAge') || DEFAULT_ID_TOKEN_MAX_AGE;
38+
39+
let lastPostedIdToken: string | undefined | null = null;
40+
41+
const mintCookieFactory = (url: string) => async (user: User | null) => {
42+
const idTokenResult = user && (await user.getIdTokenResult());
43+
const idTokenAge =
44+
idTokenResult &&
45+
(new Date().getTime() - Date.parse(idTokenResult.issuedAtTime)) / 1_000;
46+
if (idTokenAge && idTokenAge > authIdTokenMaxAge) {
47+
return;
48+
}
49+
// Specifically trip null => undefined when logged out, to delete any existing cookie
50+
const idToken = idTokenResult?.token;
51+
if (lastPostedIdToken === idToken) {
52+
return;
53+
}
54+
lastPostedIdToken = idToken;
55+
await fetch(url, {
56+
method: idToken ? 'POST' : 'DELETE',
57+
headers: idToken
58+
? {
59+
'Authorization': `Bearer ${idToken}`
60+
}
61+
: {}
62+
});
63+
};
2864

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

44-
return initializeAuth(app, {
80+
const auth = initializeAuth(app, {
4581
popupRedirectResolver: browserPopupRedirectResolver,
4682
persistence: [
4783
indexedDBLocalPersistence,
4884
browserLocalPersistence,
4985
browserSessionPersistence
5086
]
5187
});
88+
89+
const authTokenSyncUrl = getExperimentalSetting('authTokenSyncURL');
90+
if (authTokenSyncUrl) {
91+
const mintCookie = mintCookieFactory(authTokenSyncUrl);
92+
beforeAuthStateChanged(auth, mintCookie, () =>
93+
mintCookie(auth.currentUser)
94+
);
95+
onIdTokenChanged(auth, user => mintCookie(user));
96+
}
97+
98+
const authEmulatorHost = getDefaultEmulatorHost('auth');
99+
if (authEmulatorHost) {
100+
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
101+
}
102+
103+
return auth;
52104
}
53105

54106
registerAuth(ClientPlatform.BROWSER);

packages/auth/src/platform_node/index.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ import { _createError } from '../core/util/assert';
2121
import { FirebaseApp, getApp, _getProvider } from '@firebase/app';
2222
import { Auth } from '../model/public_types';
2323

24-
import { initializeAuth, inMemoryPersistence } from '..';
24+
import { initializeAuth, inMemoryPersistence, connectAuthEmulator } from '..';
2525
import { registerAuth } from '../core/auth/register';
2626
import { ClientPlatform } from '../core/util/version';
2727
import { AuthImpl } from '../core/auth/auth_impl';
2828

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

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

49-
return initializeAuth(app);
50+
const auth = initializeAuth(app);
51+
52+
const authEmulatorHost = getDefaultEmulatorHost('auth');
53+
if (authEmulatorHost) {
54+
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
55+
}
56+
57+
return auth;
5058
}
5159

5260
registerAuth(ClientPlatform.NODE);

packages/database/src/api/Database.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import { Provider } from '@firebase/component';
2727
import {
2828
getModularInstance,
2929
createMockUserToken,
30-
EmulatorMockTokenOptions
30+
EmulatorMockTokenOptions,
31+
getDefaultEmulatorHost
3132
} from '@firebase/util';
3233

3334
import { AppCheckTokenProvider } from '../core/AppCheckTokenProvider';
@@ -316,9 +317,15 @@ export function getDatabase(
316317
app: FirebaseApp = getApp(),
317318
url?: string
318319
): Database {
319-
return _getProvider(app, 'database').getImmediate({
320+
const db = _getProvider(app, 'database').getImmediate({
320321
identifier: url
321322
}) as Database;
323+
const databaseEmulatorHost = getDefaultEmulatorHost('database');
324+
if (databaseEmulatorHost) {
325+
const [host, port] = databaseEmulatorHost.split(':');
326+
connectDatabaseEmulator(db, host, parseInt(port, 10));
327+
}
328+
return db;
322329
}
323330

324331
/**

packages/firestore/externs.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"packages/logger/dist/src/logger.d.ts",
2727
"packages/webchannel-wrapper/src/index.d.ts",
2828
"packages/util/dist/src/crypt.d.ts",
29+
"packages/util/dist/src/defaults.d.ts",
2930
"packages/util/dist/src/emulator.d.ts",
3031
"packages/util/dist/src/environment.d.ts",
3132
"packages/util/dist/src/compat.d.ts",

packages/firestore/src/api/database.ts

+23-13
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@
1515
* limitations under the License.
1616
*/
1717

18-
// eslint-disable-next-line import/no-extraneous-dependencies
1918
import {
2019
_getProvider,
2120
_removeServiceInstance,
2221
FirebaseApp,
2322
getApp
2423
} from '@firebase/app';
25-
import { deepEqual } from '@firebase/util';
24+
import { deepEqual, getDefaultEmulatorHost } from '@firebase/util';
2625

2726
import { User } from '../auth/user';
2827
import {
@@ -43,7 +42,10 @@ import {
4342
setOnlineComponentProvider
4443
} from '../core/firestore_client';
4544
import { makeDatabaseInfo } from '../lite-api/components';
46-
import { Firestore as LiteFirestore } from '../lite-api/database';
45+
import {
46+
Firestore as LiteFirestore,
47+
connectFirestoreEmulator
48+
} from '../lite-api/database';
4749
import { Query } from '../lite-api/reference';
4850
import {
4951
indexedDbClearPersistence,
@@ -186,14 +188,6 @@ export function initializeFirestore(
186188
});
187189
}
188190

189-
/**
190-
* Returns the existing default {@link Firestore} instance that is associated with the
191-
* default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
192-
* instance with default settings.
193-
*
194-
* @returns The {@link Firestore} instance of the provided app.
195-
*/
196-
export function getFirestore(): Firestore;
197191
/**
198192
* Returns the existing default {@link Firestore} instance that is associated with the
199193
* provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
@@ -215,7 +209,15 @@ export function getFirestore(app: FirebaseApp): Firestore;
215209
*/
216210
export function getFirestore(databaseId: string): Firestore;
217211
/**
218-
* Returns the existing {@link Firestore} instance that is associated with the
212+
* Returns the existing default {@link Firestore} instance that is associated with the
213+
* default {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
214+
* instance with default settings.
215+
*
216+
* @returns The {@link Firestore} instance of the provided app.
217+
*/
218+
export function getFirestore(): Firestore;
219+
/**
220+
* Returns the existing default {@link Firestore} instance that is associated with the
219221
* provided {@link @firebase/app#FirebaseApp}. If no instance exists, initializes a new
220222
* instance with default settings.
221223
*
@@ -236,9 +238,17 @@ export function getFirestore(
236238
typeof appOrDatabaseId === 'string'
237239
? appOrDatabaseId
238240
: optionalDatabaseId || DEFAULT_DATABASE_NAME;
239-
return _getProvider(app, 'firestore').getImmediate({
241+
const db = _getProvider(app, 'firestore').getImmediate({
240242
identifier: databaseId
241243
}) as Firestore;
244+
if (!db._initialized) {
245+
const firestoreEmulatorHost = getDefaultEmulatorHost('firestore');
246+
if (firestoreEmulatorHost) {
247+
const [host, port] = firestoreEmulatorHost.split(':');
248+
connectFirestoreEmulator(db, host, parseInt(port, 10));
249+
}
250+
}
251+
return db;
242252
}
243253

244254
/**

0 commit comments

Comments
 (0)