Skip to content

Add Env Override Global Variable forceEnvironment #6901

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

Merged
merged 9 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -87,8 +92,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;
});
});