Skip to content

Commit

Permalink
Add Env Override Global Variable forceEnvironment (#6901)
Browse files Browse the repository at this point in the history
* Add forceEnvironment and check value in isNode

* Update API reports

* Add tests for environment package

* Update tests and remove console.log

* Add changeset

* Remove old changeset and update it with a new one

* Update jsdoc

* Move getGlobal into separate file to remove circular dep
  • Loading branch information
dwyfrequency authored Dec 22, 2022
1 parent a7622d4 commit 06dc136
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 32 deletions.
6 changes: 6 additions & 0 deletions .changeset/young-hornets-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/util': minor
'firebase': minor
---

Allow users to specify their environment as `node` or `browser` to override Firebase's runtime environment detection and force the SDK to act as if it were in the respective environment.
6 changes: 4 additions & 2 deletions common/api-review/util.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export interface FirebaseDefaults {
config?: Record<string, string>;
// (undocumented)
emulatorHosts?: Record<string, string>;
forceEnvironment?: 'browser' | 'node';
}

// Warning: (ae-missing-release-tag) "FirebaseError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down Expand Up @@ -221,11 +222,12 @@ export const getDefaultEmulatorHost: (productName: string) => string | undefined
// @public
export const getDefaultEmulatorHostnameAndPort: (productName: string) => [hostname: string, port: number] | undefined;

// @public
export const getDefaults: () => FirebaseDefaults | undefined;

// @public
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
export function getGlobal(): typeof globalThis;

Expand Down
1 change: 1 addition & 0 deletions packages/util/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ export * from './src/uuid';
export * from './src/exponential_backoff';
export * from './src/formatters';
export * from './src/compat';
export * from './src/global';
1 change: 1 addition & 0 deletions packages/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ export * from './src/uuid';
export * from './src/exponential_backoff';
export * from './src/formatters';
export * from './src/compat';
export * from './src/global';
10 changes: 8 additions & 2 deletions packages/util/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import { base64Decode } from './crypt';
import { getGlobal } from './environment';
import { getGlobal } from './global';

/**
* Keys for experimental properties on the `FirebaseDefaults` object.
Expand All @@ -39,6 +39,11 @@ export interface FirebaseDefaults {
emulatorHosts?: Record<string, string>;
_authTokenSyncURL?: string;
_authIdTokenMaxAge?: number;
/**
* Override Firebase's runtime environment detection and
* force the SDK to act as if it were in the specified environment.
*/
forceEnvironment?: 'browser' | 'node';
[key: string]: unknown;
}

Expand Down Expand Up @@ -90,8 +95,9 @@ const getDefaultsFromCookie = (): FirebaseDefaults | undefined => {
* (1) if such an object exists as a property of `globalThis`
* (2) if such an object was provided on a shell environment variable
* (3) if such an object exists in a cookie
* @public
*/
const getDefaults = (): FirebaseDefaults | undefined => {
export const getDefaults = (): FirebaseDefaults | undefined => {
try {
return (
getDefaultsFromGlobal() ||
Expand Down
27 changes: 9 additions & 18 deletions packages/util/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { CONSTANTS } from './constants';
import { getDefaults } from './defaults';

/**
* Returns navigator.userAgent string or '' if it's not defined.
Expand Down Expand Up @@ -52,10 +53,17 @@ export function isMobileCordova(): boolean {
/**
* Detect Node.js.
*
* @return true if Node.js environment is detected.
* @return true if Node.js environment is detected or specified.
*/
// Node detection logic from: https://github.com/iliakan/detect-node/
export function isNode(): boolean {
const forceEnvironment = getDefaults()?.forceEnvironment;
if (forceEnvironment === 'node') {
return true;
} else if (forceEnvironment === 'browser') {
return false;
}

try {
return (
Object.prototype.toString.call(global.process) === '[object process]'
Expand Down Expand Up @@ -193,20 +201,3 @@ export function areCookiesEnabled(): boolean {
}
return true;
}

/**
* Polyfill for `globalThis` object.
* @returns the `globalThis` object for the given environment.
*/
export function getGlobal(): typeof globalThis {
if (typeof self !== 'undefined') {
return self;
}
if (typeof window !== 'undefined') {
return window;
}
if (typeof global !== 'undefined') {
return global;
}
throw new Error('Unable to locate global object.');
}
34 changes: 34 additions & 0 deletions packages/util/src/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @license
* Copyright 2022 Google LLC
*
* 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.
*/

/**
* Polyfill for `globalThis` object.
* @returns the `globalThis` object for the given environment.
* @public
*/
export function getGlobal(): typeof globalThis {
if (typeof self !== 'undefined') {
return self;
}
if (typeof window !== 'undefined') {
return window;
}
if (typeof global !== 'undefined') {
return global;
}
throw new Error('Unable to locate global object.');
}
20 changes: 10 additions & 10 deletions packages/util/test/defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ import {
getDefaultEmulatorHost,
getDefaultEmulatorHostnameAndPort
} from '../src/defaults';
import * as environment from '../src/environment';
import * as global from '../src/global';

use(sinonChai);

describe('getDefaultEmulatorHost', () => {
after(() => {
delete environment.getGlobal().__FIREBASE_DEFAULTS__;
delete global.getGlobal().__FIREBASE_DEFAULTS__;
});

context('with no config', () => {
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('getDefaultEmulatorHost', () => {
context('with no config and something unexpected throws', () => {
let consoleInfoStub: SinonStub;
before(() => {
stub(environment, 'getGlobal').throws(new Error('getGlobal threw!'));
stub(global, 'getGlobal').throws(new Error('getGlobal threw!'));
consoleInfoStub = stub(console, 'info');
});
after(() => {
Expand All @@ -83,7 +83,7 @@ describe('getDefaultEmulatorHost', () => {

context('with global config not listing the emulator', () => {
before(() => {
environment.getGlobal().__FIREBASE_DEFAULTS__ = {
global.getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
/* no firestore */
database: '127.0.0.1:8080'
Expand All @@ -98,7 +98,7 @@ describe('getDefaultEmulatorHost', () => {

context('with IPv4 hostname in global config', () => {
before(() => {
environment.getGlobal().__FIREBASE_DEFAULTS__ = {
global.getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '127.0.0.1:8080'
}
Expand All @@ -112,7 +112,7 @@ describe('getDefaultEmulatorHost', () => {

context('with quoted IPv6 hostname in global config', () => {
before(() => {
environment.getGlobal().__FIREBASE_DEFAULTS__ = {
global.getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '[::1]:8080'
}
Expand All @@ -127,7 +127,7 @@ describe('getDefaultEmulatorHost', () => {

describe('getDefaultEmulatorHostnameAndPort', () => {
after(() => {
delete environment.getGlobal().__FIREBASE_DEFAULTS__;
delete global.getGlobal().__FIREBASE_DEFAULTS__;
});

context('with no config', () => {
Expand All @@ -138,7 +138,7 @@ describe('getDefaultEmulatorHostnameAndPort', () => {

context('with global config not listing the emulator', () => {
before(() => {
environment.getGlobal().__FIREBASE_DEFAULTS__ = {
global.getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
/* no firestore */
database: '127.0.0.1:8080'
Expand All @@ -153,7 +153,7 @@ describe('getDefaultEmulatorHostnameAndPort', () => {

context('with IPv4 hostname in global config', () => {
before(() => {
environment.getGlobal().__FIREBASE_DEFAULTS__ = {
global.getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '127.0.0.1:8080'
}
Expand All @@ -170,7 +170,7 @@ describe('getDefaultEmulatorHostnameAndPort', () => {

context('with quoted IPv6 hostname in global config', () => {
before(() => {
environment.getGlobal().__FIREBASE_DEFAULTS__ = {
global.getGlobal().__FIREBASE_DEFAULTS__ = {
emulatorHosts: {
firestore: '[::1]:8080'
}
Expand Down
54 changes: 54 additions & 0 deletions packages/util/test/environments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2022 Google LLC
*
* 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 { expect } from 'chai';
import { isNode } from '../src/environment';
import { SinonStub, stub, restore } from 'sinon';
import * as defaults from '../src/defaults';

const firebaseDefaults: defaults.FirebaseDefaults = {
_authTokenSyncURL: 'string',
_authIdTokenMaxAge: 200,
forceEnvironment: 'node'
};

describe('isNode()', () => {
let getDefaultsFromGlobalStub: SinonStub;

beforeEach(async () => {
getDefaultsFromGlobalStub = stub(defaults, 'getDefaults');
});

afterEach(async () => {
restore();
});

it('returns true if forceEnvironment lists `node`', () => {
getDefaultsFromGlobalStub.returns(firebaseDefaults);

expect(isNode()).to.be.true;
});

it('returns false if forceEnvironment lists `browser`', () => {
getDefaultsFromGlobalStub.returns({
...firebaseDefaults,
forceEnvironment: 'browser'
});

expect(isNode()).to.be.false;
});
});

0 comments on commit 06dc136

Please sign in to comment.