Skip to content

Commit 6e0761d

Browse files
committed
Added emulator status
1 parent 7d2313f commit 6e0761d

File tree

7 files changed

+116
-43
lines changed

7 files changed

+116
-43
lines changed

common/api-review/util.api.md

+15
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ export type EmulatorMockTokenOptions = ({
140140
sub: string;
141141
}) & Partial<FirebaseIdToken>;
142142

143+
// Warning: (ae-missing-release-tag) "EmulatorStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
144+
//
145+
// @public (undocumented)
146+
export interface EmulatorStatus {
147+
// (undocumented)
148+
isRunningEmulator: boolean;
149+
// (undocumented)
150+
name: string;
151+
}
152+
143153
// Warning: (ae-missing-release-tag) "ErrorData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
144154
//
145155
// @public (undocumented)
@@ -481,6 +491,11 @@ export interface Subscribe<T> {
481491
// @public (undocumented)
482492
export type Unsubscribe = () => void;
483493

494+
// Warning: (ae-missing-release-tag) "updateStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
495+
//
496+
// @public (undocumented)
497+
export function updateStatus(emulatorStatus: EmulatorStatus, isCloudWorkstation: boolean): void;
498+
484499
// Warning: (ae-missing-release-tag) "validateArgCount" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
485500
//
486501
// @public

packages/auth/src/api/index.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
FirebaseError,
2020
isCloudflareWorker,
2121
isCloudWorkstation,
22-
querystring
22+
querystring,
23+
updateStatus
2324
} from '@firebase/util';
2425

2526
import { AuthErrorCode, NamedErrorParams } from '../core/errors';
@@ -198,7 +199,16 @@ export async function _performFetchWithErrorHandling<V>(
198199
customErrorMap: Partial<ServerErrorMap<ServerError>>,
199200
fetchFn: () => Promise<Response>
200201
): Promise<V> {
201-
(auth as AuthInternal)._canInitEmulator = false;
202+
const authInternal = auth as AuthInternal;
203+
updateStatus(
204+
{
205+
name: 'Auth',
206+
isRunningEmulator: authInternal.emulatorConfig !== undefined
207+
},
208+
authInternal.emulatorConfig!! &&
209+
isCloudWorkstation(authInternal.emulatorConfig.host)
210+
);
211+
authInternal._canInitEmulator = false;
202212
const errorMap = { ...SERVER_ERROR_MAP, ...customErrorMap };
203213
try {
204214
const networkTimeout = new NetworkTimeout<Response>(auth);

packages/auth/src/core/auth/emulator.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ describe.only('core/auth/emulator', () => {
124124
});
125125
});
126126

127-
it('subsequent calls update the endpoint appropriately', async () => {
127+
it('subsequent calls update the endpoint appropriately', async () => {
128128
connectAuthEmulator(auth, 'http://127.0.0.1:2021');
129129
expect(auth.emulatorConfig).to.eql({
130130
protocol: 'http',

packages/auth/src/core/auth/emulator.ts

+8-38
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Auth } from '../../model/public_types';
1818
import { AuthErrorCode } from '../errors';
1919
import { _assert } from '../util/assert';
2020
import { _castAuth } from './auth_impl';
21-
import { deepEqual } from '@firebase/util';
21+
import { deepEqual, isCloudWorkstation, updateStatus } from '@firebase/util';
2222

2323
/**
2424
* Changes the {@link Auth} instance to communicate with the Firebase Auth Emulator, instead of production
@@ -98,7 +98,13 @@ export function connectAuthEmulator(
9898
authInternal.settings.appVerificationDisabledForTesting = true;
9999

100100
if (!disableWarnings) {
101-
emitEmulatorWarning();
101+
updateStatus(
102+
{
103+
name: 'Auth',
104+
isRunningEmulator: true
105+
},
106+
isCloudWorkstation(emulatorConfig.host)
107+
);
102108
}
103109
}
104110

@@ -137,39 +143,3 @@ function parsePort(portStr: string): number | null {
137143
}
138144
return port;
139145
}
140-
141-
function emitEmulatorWarning(): void {
142-
function attachBanner(): void {
143-
const el = document.createElement('p');
144-
const sty = el.style;
145-
el.innerText =
146-
'Running in emulator mode. Do not use with production credentials.';
147-
sty.position = 'fixed';
148-
sty.width = '100%';
149-
sty.backgroundColor = '#ffffff';
150-
sty.border = '.1em solid #000000';
151-
sty.color = '#b50000';
152-
sty.bottom = '0px';
153-
sty.left = '0px';
154-
sty.margin = '0px';
155-
sty.zIndex = '10000';
156-
sty.textAlign = 'center';
157-
el.classList.add('firebase-emulator-warning');
158-
document.body.appendChild(el);
159-
}
160-
161-
if (typeof console !== 'undefined' && typeof console.info === 'function') {
162-
console.info(
163-
'WARNING: You are using the Auth Emulator,' +
164-
' which is intended for local testing only. Do not use with' +
165-
' production credentials.'
166-
);
167-
}
168-
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
169-
if (document.readyState === 'loading') {
170-
window.addEventListener('DOMContentLoaded', attachBanner);
171-
} else {
172-
attachBanner();
173-
}
174-
}
175-
}

packages/firestore/src/lite-api/database.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import {
2626
createMockUserToken,
2727
deepEqual,
2828
EmulatorMockTokenOptions,
29-
getDefaultEmulatorHostnameAndPort
29+
getDefaultEmulatorHostnameAndPort,
30+
isCloudWorkstation,
31+
updateStatus
3032
} from '@firebase/util';
3133

3234
import {
@@ -140,6 +142,13 @@ export class Firestore implements FirestoreService {
140142

141143
_freezeSettings(): FirestoreSettingsImpl {
142144
this._settingsFrozen = true;
145+
updateStatus(
146+
{
147+
name: 'Firestore',
148+
isRunningEmulator: (this._settings as PrivateSettings).emulator!!
149+
},
150+
isCloudWorkstation(this._settings.host)
151+
);
143152
return this._settings;
144153
}
145154

@@ -297,6 +306,7 @@ export function getFirestore(
297306
if (!db._initialized) {
298307
const emulator = getDefaultEmulatorHostnameAndPort('firestore');
299308
if (emulator) {
309+
console.log('emulator');
300310
connectFirestoreEmulator(db, ...emulator);
301311
}
302312
}
@@ -346,8 +356,10 @@ export function connectFirestoreEmulator(
346356
...settings,
347357
host: newHostSetting,
348358
ssl,
349-
emulatorOptions: options
359+
emulatorOptions: options,
360+
emulator: true
350361
};
362+
console.log(newConfig);
351363
// No-op if the new configuration matches the current configuration. This supports SSR
352364
// enviornments which might call `connectFirestoreEmulator` multiple times as a standard practice.
353365
if (deepEqual(newConfig, existingConfig)) {

packages/firestore/src/lite-api/settings.ts

+4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface PrivateSettings extends FirestoreSettings {
8383
experimentalLongPollingOptions?: ExperimentalLongPollingOptions;
8484
useFetchStreams?: boolean;
8585
emulatorOptions?: { mockUserToken?: EmulatorMockTokenOptions | string };
86+
emulator?: boolean;
8687

8788
localCache?: FirestoreLocalCache;
8889
}
@@ -112,6 +113,8 @@ export class FirestoreSettingsImpl {
112113
readonly useFetchStreams: boolean;
113114
readonly localCache?: FirestoreLocalCache;
114115

116+
readonly emulator?: boolean;
117+
115118
// Can be a google-auth-library or gapi client.
116119
// eslint-disable-next-line @typescript-eslint/no-explicit-any
117120
credentials?: any;
@@ -130,6 +133,7 @@ export class FirestoreSettingsImpl {
130133
this.host = settings.host;
131134
this.ssl = settings.ssl ?? DEFAULT_SSL;
132135
}
136+
this.emulator = settings.emulator;
133137

134138
this.credentials = settings.credentials;
135139
this.ignoreUndefinedProperties = !!settings.ignoreUndefinedProperties;

packages/util/src/emulator.ts

+62
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import { base64urlEncodeWithoutPadding } from './crypt';
19+
import { FirebaseApp } from '@firebase/app';
1920

2021
// Firebase Auth tokens contain snake_case claims following the JWT standard / convention.
2122
/* eslint-disable camelcase */
@@ -140,3 +141,64 @@ export function createMockUserToken(
140141
signature
141142
].join('.');
142143
}
144+
145+
function getOrCreate(id: string): { created: boolean; element: HTMLElement } {
146+
let parentDiv = document.getElementById(id);
147+
let created = false;
148+
if (!parentDiv) {
149+
parentDiv = document.createElement('div');
150+
parentDiv.setAttribute('id', id);
151+
created = true;
152+
}
153+
return { created, element: parentDiv };
154+
}
155+
156+
export interface EmulatorStatus {
157+
name: string;
158+
isRunningEmulator: boolean;
159+
}
160+
export function updateStatus(
161+
emulatorStatus: EmulatorStatus,
162+
isCloudWorkstation: boolean
163+
) {
164+
function setupDom() {
165+
const parentDivId = `__firebase_status`;
166+
167+
let { element: parentDiv, created } = getOrCreate(parentDivId);
168+
169+
if (created) {
170+
parentDiv.style.position = 'fixed';
171+
parentDiv.style.bottom = '0px';
172+
parentDiv.style.border = 'solid 1px';
173+
parentDiv.style.width = '100%';
174+
parentDiv.style.borderRadius = '10px';
175+
parentDiv.style.padding = '.5em';
176+
parentDiv.style.textAlign = 'center';
177+
document.body.appendChild(parentDiv);
178+
}
179+
180+
const { name, isRunningEmulator } = emulatorStatus;
181+
const { element, created: productDivCreated } = getOrCreate(
182+
`${parentDivId}_${name}`
183+
);
184+
// If in prod, and not using a cloud workstation, we should remove the node, as the banner can be distracting.
185+
if (!isRunningEmulator && !isCloudWorkstation) {
186+
element.remove();
187+
return;
188+
}
189+
if (productDivCreated) {
190+
parentDiv.appendChild(element);
191+
}
192+
element.style.color = isRunningEmulator ? 'green' : 'red';
193+
element.innerHTML = `${name} is running in ${
194+
isRunningEmulator ? 'emulator' : 'prod'
195+
} mode`;
196+
}
197+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
198+
if (document.readyState === 'loading') {
199+
window.addEventListener('DOMContentLoaded', setupDom);
200+
} else {
201+
setupDom();
202+
}
203+
}
204+
}

0 commit comments

Comments
 (0)