ES6 swap module at runtime - javascript

I have api.js which loads either mock.js or server.js and exports it. The result is that based on server environment variables I can swap between backends.
Problem is, I want to do this at Runtime without affecting all the code that is already using the "api" of the mock.js and server.js modules.
I want my app to use either mock.js or server.js depending on connection status. The following code will work during initialization only, not runtime.
import server from './server'
import mock from './mock'
let backend = null
if (process.env.NODE_ENV === 'development' && Math.random() > 0.5 ) {
backend = mock
} else {
backend = server
}
export default backend

You can take advantage of ES6 live binding if you want, e.g.
import server from './server';
import mock from './mock';
export { backend as default };
let backend = server;
setInterval(() => {
backend = Math.random() > 0.5 ? mock : server;
}, 1000);
which will change the default export randomly every second.
In ES6, imported variables are live references to the variable in the module that exports the value, so you can just
import backend from "./api";
and the backend value will change over time.
Specifically in your case, changing
export default backend;
to
export { backend as default};
is critical, as the first doesn't work because it is short for
const uniqueTempVal = backend;
export { uniqueTempVal as default };
which as you can see means reassigning backend later does not affect the exported value.

Put the object you want to export in a property of the exported object:
import server from './server'
import mock from './mock'
let backend = { api: null };
if (process.env.NODE_ENV === 'development' && process.env.NODE_MOCK === true) {
backend.api = mock;
} else {
backend.api = server;
}
export default backend;
Now just use the api property in the importing code.

Related

Exporting a boolean variable that depends on an if statement (React JS & Firebase)?

I'm trying to declare a boolean from my firebase.js configuration file that depends on an if-else statement that checks whether a user is logged in or not.
I want to store the log in status of a user in a boolean variable and export that variable across different components in my React app.
So if user is logged in I want my variable isLoggedIn to be true and export that. But I want the isLoggedIn boolean to be updated depending on the log-in status before it gets exported.
Here is my firebase.js file
import { initializeApp } from 'firebase/app';
import { getAuth } from "firebase/auth";
const firebaseConfig = {
//private keys
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
//Problem
let isLoggedIn = false;
function checkLoggedInStatus() {
if (auth.currentUser !== null) { //i.e. user is signed in
isLoggedIn = true;
}
return isLoggedIn;
}
checkLoggedInStatus();
export isLoggedIn;
There are (at least) two problems here.
What immediately follows export may be, among other things:
A declaration followed by a variable name (eg export let foo = 5;) - this initializes the variable and exports it from the module in a single line
function or class - similar to the above
default, if you want to indicate what you're exporting is the default export
Brackets, if you're exporting identifiers that have already been declared - eg export { foo }
But doing export isLoggedIn; is invalid syntax - it falls into none of those categories (nor any of the other permitted constructs).
The other issue is that the .currentUser property may not exist yet if the auth isn't done initializing. Make sure you wait for it to be done first. Use the observer version instead.
Since that is asynchronous, you have a few options for getting that information to the rest of your script. The first one, that I'd recommend, is to have this auth module call the rest of your script (in other words, make this module your entry point, or close to it). For example:
import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged } from "firebase/auth";
import { renderApp } from './renderApp';
const firebaseConfig = {
//private keys
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
onAuthStateChanged(auth, (user) => {
if (user) {
renderApp(user);
} else {
// No user
renderApp();
}
});
where renderApp is the starting point for the rest of your code that depends on the user. You can't easily export isLoggedIn directly from the firebase module because it's retrieved asynchronously - calling renderApp with it and then passing the value around is the best way to do it.
You can also export a Promise from this firebase module script, one that resolves to whether the user is logged in or not, but that'll require all consumers of the module to have to call .then on the value before being able to use it, which could be quite messy. Better to use your firebase module script as an entry point and only call the rest of your script once the auth object has finished loading.

How to initialize a shared javascript module default export

I want to share an api instance across multiple modules and be able to initialize it with external configuration. My code uses Webpack and Babel to transform those nice ES6 modules into something usable by browsers. I'm trying to achieve this:
// api.js
let api = null;
export default api;
export function initApi(config) {
// use config to configure the shared api instance (e.g. with api base url)
api = ...
}
// ======================
// entry.js
import { initApi } from './api';
import App from './App';
// Initialize the single shared instance before anyone has the chance to use it
const apiConfig = ...
initApi(apiConfig);
// Create the app and run it
// ======================
// App.js
// RootComponent has an import dependency chain that eventually imports DeeplyNestedComponent.js
import RootComponent from './RootComponent';
// Actual App code not important
// ======================
// DeeplyNestedComponent.js
// PROBLEM! This "assignment" to the api var happens before initApi is run!
import api from '../../../../api';
api.getUser(123); // Fails because "api" stays null forever even after the initApi() call
The "problem" occurs because ES6 modules are imported statically and import statements are hoisted. In other words, simply moving the import App from './App' line below initApi(apiConfig) doesn't make the import happen after initApi is called.
One way to solve this is to export an object from api.js (or in another globals.js file if I have multiple such shared objects with the same pattern) instead of a single variable like this:
// api.js
const api = {
api: null,
};
export default api;
export function initApi(config) {
// use config to configure the shared api instance (e.g. with api base url)
api.api = ... // <-- Notice the "api." notation
}
// ======================
// DeeplyNestedComponent.js
// api is now the object with an empty "api" property that will be created when initApi() is called
import api from '../../../../api';
api.api.getUser(123); // <-- Ugh :(
Is there a way to achieve initialization of a shared service instance elegantly when using ES6 modules?
In my case, DeeplyNestedComponent.js must still import the api instance somehow. In other words, there is unfortunately no context object passed from App all the way down to DeeplyNestedComponent.js that could give access the api instance.
The problem with your code is that
let api = null;
export default api;
does export the value null in the implicitly generated binding for the default export. However, you can also export arbitrary bindings under the name default by using the syntax
let api = null;
export { api as default };
This will work as expected. But you still need to make sure that no module accesses this export before you called initApi.

Axios custom instance not working properly with next JS

So firstly i create custom axios instance with baseurl and export it like this:
import axios from 'axios';
const instance = axios.create({
baseURL: process.env.BACKEND_URL,
});
instance.defaults.headers.common['Authorization'] = 'AUTH TOKEN';
instance.defaults.headers.post['Content-Type'] = 'application/json';
export default instance;
The problem is in my saga or in any component in general (ONLY client side) when importing this custom axios instance. I use next-redux-wrapper, and when i prefetch data (using getStaticProps) for my component everything works fine and the axios.defaults.baseURL property works just fine.
However the problem is on client-side, whenever i import the same axios instance in any component or in saga but i call it from lets say componentDidMount, the same axios.default.baseURL is undefined, so if i want to make get request i have to type in the full backend + queries URL. What could the problem be? EXAMPLE:
export function* fetchTPsSaga() {
try {
console.log(axios.defaults.baseURL);
const url = `/training-programs`;
const res = yield axios.get(url);
const tPs = res.data.data;
yield put(fetchTrainingProgramsSuccess(tPs));
} catch (err) {
yield put(fetchTrainingProgramsFail(err));
}
}
// The first time it renders (on server side), it's the valid baseURL property, however if i call the same saga from client-side (when component is rendered) it's UNDEFINED, so i have to type the full url
process.env only work on server-side. You can use publicRuntimeConfig to access environment variables both on client and server-side.
next.config.js
module.exports = {
publicRuntimeConfig: {
// Will be available on both server and client
backendUrl: process.env.BACKEND_URL,
},
}
axios instance file
import axios from 'axios';
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
const instance = axios.create({
baseURL: publicRuntimeConfig.backendUrl,
});
By the way, if you are using Next.js versions 9.4 and up, the Environment Variables provide another way.
Loading Environment Variables Rules
In order to expose a variable to the browser you have to prefix the variable with
NEXT_PUBLIC_
. For example:
NEXT_PUBLIC_BACKEND_URL='http://localhost:3000'
Then you can access this env variable in Axios as its client-side rendering
import axios from 'axios';
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
});
*Note: You have to use process.env.NEXT_PUBLIC_BACKEND_URL instead of process.env.BACKEND_URL

Jest Unit Class with Dependencies

I'm trying to unit test this class that has a dependency of AppDB and createStudy that I need to mock. To get started I'm attempting to unit test the simple method startLoadingData which happens to be a MobX action
import { observable, action } from 'mobx'
import { Intent } from '#blueprintjs/core'
import { createStudy } from '../database/DatabaseInit'
import { AppDB } from '../database/Database'
export default class UIStore {
// ui state
// booleans indicating open/close state of modals
#observable createDialogue
#observable importDialogue
#observable revisionsDialogue
#observable runDialogue
// boolean indicating loading or waiting for async action
#observable loadingData
// array indicating navigation
#observable breadcrumbs
#observable processingMessages
constructor(rootStore) {
this.rootStore = rootStore
this.breadcrumbs = []
this.importDialogue = false
this.createDialogue = false
this.revisionsDialogue = false
this.runDialogue = false
// boolean to display loading blur on table that displays data
this.loadingData = false
// processing messages for import and other async loads
this.processingMessages = []
}
#action startLoadingData() {
this.loadingData = true
}
}
My test file below is getting nowhere because there's an error being thrown related to a separate dependency of sqlite3 in the AppDB and createStudy imports. My understanding is that if I mock those two dependencies that I can avoid the error because they'll be mocked and not real implementations trying to use sqlite3.
// UIStore domain store unit test
// import * as Database from '../../app/database/Database'
// import * as DatabaseInit from '../../app/database/DatabaseInit'
import UIStore from '../../app/stores/UIStore'
describe('UIStore', () => {
beforeEach(() => {
// jest.spyOn(Database, 'AppDB').andReturn('mockAppDB')
// jest.spyOn(DatabaseInit, 'createStudy').andReturn('createStudy')
jest.mock('../../app/database/Database')
// jest.mock('DatabaseInit')
})
it('starts loading data', () => {
const testUIStore = new UIStore(this)
testUIStore.startLoadingData()
expect(testUIStore.loadingData).toBe(true)
})
})
As you can see, trying a bunch of things, but I don't seem to be getting anywhere. I've read about manual mocks, and thought that might be the case so I made a manual mock of Database but not even sure if I'm doing that correctly.
const Database = jest.genMockFromModule('../Database.js')
module.exports = Database
I dont think this matters, but it might be worth noting that AppDB is a ES6 class and createStudy is a method.
Jest should auto mock modules from node_modules if you create a __mocks__ folder in your root project folder and create in that folder mocks for the modules you want auto mocked. By auto mock I mean that when writing a test and Jest detects that folder it will automatically load the mock instead of the original module. This also applies to dependencies of dependencies.
So in your case I would try to create a sqlite3 like so:
/project
|
-> __mocks__
| |
| -> sqlite3/index.js <- export mocked functions
|
-> node_modules
At least this is how I deal with libraries in my Jest tests.
Hope this helps.

Re-including a module on-the-fly

I am currently working on technical debt identified by SonarQube for a Node.js application. My application allows on-the-fly switching between a live and mock datasource. To achieve this I destroy the previous "require" from cache and re-require it. When running SonarQube it does not like "require" statements. It does suggest "import" statements. However that may not be suitable in this case.
Simplified version of existing code:
var config = require('../config');
var polService = require(config.polService);
var root = require('../root');
function doingStuff(liveOrMock) {
setEnvironment(liveOrMock);
delete require.cache[require.resolve(root.path + ‘/config’)];
config = require('../config');
polService = require(config.polService);
}
The setEnvironment function sets process.env.NODE_ENV = liveOrMock, which is used in config.js. We export the config module using module.exports = localOptions[process.env.NODE_ENV]; This code picks a single key-pair from a JSON. The value that comes back is used to choose which module is being used for a restService.
Being able to change what module is being used is for polService is the purpose of the code.
Change your config module to export a function, and then call this function whenever you need to change environment.
In order to make polService a dynamic module, you can use dynamic import(). import() is not supported natively, but you can use this Babel plugin (it works with webpack) to transpile it.
config.js:
export default () => {
// ...
return localOptions[process.env.NODE_ENV];
}
Main module:
import getConfig from '../config';
let config = getConfig();
function doingStuff(liveOrMock) {
setEnvironment(liveOrMock);
config = getConfig();
return import(config.polService).then(result => {
polService = result;
});
}
Keep in mind that now the doingStuff function is asynchronous (i.e. returns a promise), so you can't just call it and access polService immediately. You have to wait for it by either using the then() method, or using await in an async function.
If you have a limited number of polService modules, it might be a better option to import all of them beforehand, and in the doingStuff function just switch which one the polService variable refers to.
import getConfig from '../config';
import polService1 from '../polService1';
import polService2 from '../polService2';
import polService3 from '../polService3';
const polServices = { polService1, polService2, polService3 };
let config = getConfig();
let polService = polService1;
function doingStuff(liveOrMock) {
setEnvironment(liveOrMock);
config = getConfig();
polService = polServices[config.polService];
}

Categories