How to get separate "global" module per instance? - javascript

Is it possible get individual "global" module for each instance?
import MyModule from './index.js';
const first = new MyModule();
const second = new MyModule();
// PLEASE SEE EXAMPLE BELOW FOR LOGS
// expect to log for both:
// 1. "in entry: true"
// 2. "in init: true"
// but second module logs:
// 1. "in entry: false"
// 2. "in init: false"
Issue being here that both share globals and 1. instance changes property to false.
What are my options to have in individual globals module per instance?
PS! I have tens of thousands of lines of legacy code. Would be 100x better if I didn't need to change/remove/refactor globals.js contents nor these imports import globals from './globals.js';. Nevertheless, give all your ideas - I need to fix it, even if I need to change a lot.
Code for example above / minimal reproducible example
index.js
import globals from './globals.js';
import init from './init.js';
export default function MyModule (conf) {
console.log('in entry:', globals.state1);
init();
}
globals.js
export default {
state1: true,
};
init.js
import globals from './globals.js';
export default function init () {
console.log('in init:', globals.state1);
globals.entry1 = false;
}

Since an object is passed by reference you need to create a copy each time you use globals.
A good way is to export a function like:
/* globals.js */
const globals = { // Maybe wrap the object in Object.freeze() to prevent side effects
state1: true,
};
export default () => ({ ...globals });
You can then call the function to create a copy each time you need it.
/* init.js */
import getGlobals from './globals.js';
export default function init () {
const globals = getGlobals();
console.log('in init:', globals.state1);
globals.entry1 = false;
}

Related

JavaScript - How to call a method of a imported JS file

How to define a method 'foo' in a javascript file 'test.js' so that I can import that file into another javascript file and call the method 'foo'?
Before going further you'll need to figure out what kind of modules you're using, there're 2 common types of modules that I know CommonJS and ES
In either type of module you're using, in order for you to import your method, you will need to export it as a module first.
The details of their differences are found in the NodeJS document NodeJS Import
For the ES modules you can export your method 'foo' like this:
// normal function
export function foo1() {}
// arrow function
export const foo2 = () => {}
// or this grouping style that I prefer would look more elegant
function foo1() {}
const foo2 = () => {}
// we can export multiple methods at once
export {
foo1,
foo2
}
On the other hand, using the CommonJS module, you can export your method 'foo' like this:
// a single method
module.exports.foo1 = function() {}
module.exports = function foo2() {}
// multiple methods
module.exports = {
foo1: () => {},
foo2: () => {}
}
Once you've exported your methods as modules, you can now import them like this:
import { foo1, foo2 } from './test.js'
const exec = foo1();
or
import * as test from './test.js'
const exec = test.foo2();
Hope this help

Jest mock window object before importing a dependency

I need to have a value set in the window object before a dependency is imported. Say I have this code
// foo.test.js
import { dependency } from './foo'
describe('...', () => {
it('...', () => {
// use dependency
})
})
But for dependency to be imported I need to have a value defined in window.myValues
// foo.js
export const dependency = {
key: window.myValue.nestedValue
}
That code will give me an error when importing the file because window.myValue.nestedValue is trying to access the property nestedValue of undefined.
How can I get that done?
Edit
Following christianeide's answer below I get the following error
● Test suite failed to run
TypeError: Cannot convert undefined or null to object
2 | delete global.window.myValue
3 | global.window = Object.create(window)
> 4 | global.window.myValue = {
| ^
5 | nestedValue: 'someValue'
6 | }
7 | }
at module.exports (jest.setup.js:4:17)
at node_modules/#jest/core/build/runGlobalHook.js:82:17
at ScriptTransformer.requireAndTranspileModule (node_modules/#jest/transform/build/ScriptTransformer.js:684:24)
at node_modules/#jest/core/build/runGlobalHook.js:72:27
at pEachSeries (node_modules/p-each-series/index.js:8:9)
at async _default (node_modules/#jest/core/build/runGlobalHook.js:58:5)
at async runJest (node_modules/#jest/core/build/runJest.js:345:5)
es6 imports are "hoisted", meaning wherever you write them in the code, they'll get processed before the importing module is executed, so the imported module is always executed before the importing module. In your case, this means foo.js executes before foo.test.js, so even if you properly mock your property of window in your tests, foo.js will not see your mock.
You can get around this by using require in your tests to import foo.js after the property of window has been mocked.
// foo.test.js
window.myValue = { nestedValue: MOCK_NESTED_VALUE };
const { dependency } = require('./foo');
describe('...', () => {
it('...', () => {
// use dependency
})
})
As other answers have pointed out, if myValue is one of existing "sytem" properties of window such as window.location, you might have to delete it first. When deleting, don't forget to back it up so that you can restore it when cleaning up after your test.
Try assigning values to global.window.
Something like this:
delete global.window.myValue;
global.window = Object.create(window);
global.window.myValue = {
nestedValue: 'someValue',
};
This can be done inside jest.setup.js, but you could also probably define the values inside foo.test.js
I was able to append properties on the window object by creating a setup.js:
global.propertyA = () => {};
global.nestedPropertyB = {
propertyC: () => {}
};
And setting the file in setupFilesAfterEnv in my jest.config.js file:
module.exports = {
setupFilesAfterEnv: ['<rootDir>/tests/js/setup.js']
}
I have solved such an issue with jest.mock: as it's hoisted above import statements, it's possible to execute any code before and after importing an actual module.
In my case I needed to test some functionality and set node env to a value, different from 'test', and, as I needed this substitution only for the time of import, I set its value back to 'test' before returning the actual module. Of course, it may vary depending on use cases - it's just an example of executing code before and after importing is done. Also i tried to use a getter for a particular module field, and it also worked.
import ApiBase from "../ApiBase";
jest.mock('../ApiBase', () => {
process.env.NODE_ENV = '';
const actual = jest.requireActual('../ApiBase');
process.env.NODE_ENV = 'test';
return actual;
});

Checking variable status from another JavaScript module using export default

I have two Javascript modules, one for my navigation, and one for route changes.
Inside of nav.js is simple if to check if the menu can be opened
const initNav = () => {
const openMenu = () => {
if (!menuIsOpen && isMobile) {
navItems.classList.add("is-menu-open");
menuIsOpen = true;
} else {
navItems.classList.remove("is-menu-open");
menuIsOpen = false;
}
}
}
initNav();
export default initNav;
Then, at the top of the other module I import
import initNav from './nav.js';
The question is, inside of my other module route.js, I need to be able to check if the menu is opened, and, if it is then close it, so I was going to use:
beforeLeave: function (data) {
if (menuIsOpen) {
navItems.classList.remove("is-menu-open");
menuIsOpen = false;
}
}
The console, however, says menuIsOpen is not defined.
I can't then this way check to see the status of this variable.
Am I able to do this another way, rather than combining the two modules into one very large js module?
The problem is that you're trying to reassign a variable that exists in multiple modules, but imported variables are read-only references - they can only be reassigned in their original module. So you can't just also export the menuIsOpen from nav.js because reassigning it in route.js would not be allowed.
You could have nav.js export a function that reassigns its menuIsOpen while also exporting menuIsOpen, but reassignable exports in general are unintuitive - nowhere else in Javascript does what looks like a reference to a primitive value seemingly reassign itself due to a different module reassigning the binding. Linters often forbid them.
A better alternative is for the main user of menuIsOpen to export setter and getter functions to interface with menuIsOpen. For example, you could do something like the following:
// nav.js
export const getMenuIsOpen = () => menuIsOpen;
export const setMenuIsOpen = (newVal) => menuIsOpen = newVal;
// ...
// route.js
import initNav, { getMenuIsOpen, setMenuIsOpen } from './nav.js';
// ...
beforeLeave: function (data) {
if (getMenuIsOpen()) {
navItems.classList.remove("is-menu-open");
setMenuIsOpen(false);
}
}
For anything more complicated than a single variable that needs to be accessed and changeable in multiple places, you can consider exporting an object instead, eg:
// nav.js
export const navState = {
menuIsOpen: false,
// other properties
};
// ...
// route.js
import initNav, { navState } from './nav.js';
// ...
beforeLeave: function (data) {
if (navState.menuIsOpen) {
navItems.classList.remove("is-menu-open");
navState.menuIsOpen = false;
}
}
Having to handle mutable state across multiple modules makes code a bit ugly. I prefer to avoid it when I can. Given the code in the question, since you already have a reference to navItems in both modules, you might be able to avoid having a persistent menuIsOpen variable at all if you simply check if the navItems class list has is-menu-open:
// nav.js
// ...
const openMenu = () => {
// defining menuIsOpen as a standalone variable isn't necessary,
// but you might find it makes the code more readable
const menuIsOpen = navItems.classList.contains('is-menu-open');
if (!menuIsOpen && isMobile) {
navItems.classList.add("is-menu-open");
} else {
navItems.classList.remove("is-menu-open");
}
}
// ...
// route.js
// ...
beforeLeave: function (data) {
const menuIsOpen = navItems.classList.contains('is-menu-open');
if (menuIsOpen) {
navItems.classList.remove("is-menu-open");
}
}
If you do need a standalone menuIsOpen variable in nav.js, you could have nav.js export a function which can close the menu and set menuIsOpen to false:
// nav.js
// ...
export const closeMenu = () => {
navItems.classList.remove("is-menu-open");
menuIsOpen = false;
};
// ...
// route.js
import initNav, { closeMenu } from './nav.js';
// ...
beforeLeave: closeMenu
(if beforeLeave really isn't doing anything else in your actual code other than checking if the class exists and removing it if so, you don't need to check if the class exists first - you can just remove it from the class list unconditionally - this can apply to all the other snippets in the answer too)
try getter/setter or export the variable menuIsOpen
Try like this
beforeLeave: (data) => {
if (menuIsOpen) {
navItems.classList.remove("is-menu-open");
menuIsOpen = false;
}
}

Jasmine mockability of imported vs required named exports

I'll start with the setup, before I get into the problem. Let's start with the base file whose named exports we'll want to spy on:
// fileA.js
export function bar() {}
export function foo() {}
And then have two variations of classes that import these, one in CommonJS style, and one in ES6 style:
// es6Baz.js
import * as A from 'fileA'
export default class Baz {
callFoo() { A.foo(); }
}
// commonBaz.js
const A = require('fileA');
export default class Baz {
callFoo() { A.foo(); }
}
Then likewise, test files for these two same variants:
// testEs6A.js
import Baz from 'es6Baz';
import * as A from 'fileA';
it('test', () => {
spyOn(A, 'foo');
const b = new Baz();
b.callFoo();
expect(A.foo).toHaveBeenCalled(); // this will fail
});
// testCommonA.js
import Baz from 'es6Baz';
const A = require('fileA');
it('test', () => {
spyOn(A, 'foo');
const b = new Baz();
b.callFoo();
expect(A.foo).toHaveBeenCalled(); // this will pass
});
The main question here is: why does mocking work with the CommonJS approach, but not the ES6 one, assuming we're using Babel to compile?
My understanding was that ES6 live binds, while CommonJS copies, so I was surprised the former failed, and even more surprised the latter succeeded. My best guess is that a * import results in a locally namespaced object that's different in the files this is done in (i.e. A in es6Baz.js is not the same as A in testEst6A.js), but is that actually the case? And why would the require work, when this doesn't?

How to create a definition for 'module.exports' function

I am exporting a function like this which works.
module.exports = function (options: any): RequestHandler {
// Do stuff
}
I am trying to add a definition for the exported function, but I am not sure if this is the right way to do it:
declare global {
export function tsm(options: any): RequestHandler
}
When I try to test it both of the following say that this is valid:
const tsm = require('ts-middleware')
global.tsm() // Gives intellisense
tsm() // Also gives intellisense
It shouldn't give information about global.tsm(), so I think that I created my definition wrong. What is the correct way to create a function definition?
I don't want to use the function like this:
const tsm = require('ts-middleware')
tsm.tsm()
But I do want to use it like this:
const tsm = require('ts-middleware')
tsm()
To define typings for a module, use declare module 'x' {...}. i.e.:
declare module 'ts-middleware' {
export = (option: any): RequestHandler
}
However, you do not really need to do that because you wrote your code in TypeScript. The compiler can generate the typings for you automatically.
You just need to add declaration: true in your tsconfig.json:
// tsconfig.json
{
"compilerOptions": {
"declaration": true
}
}
Also, I would strongly recommend you to write your code using ESM. i.e. instead of module.exports = ...:
// named export, preferred
export function foo(options: any): RequestHandler { ... }
// or default export
export default function foo(options: any): RequestHandler { ... }
// import in TypeScript for named export
import { foo } from 'ts-middleware'
// import in TypeScript for default export
import foo from 'ts-middleware'
// require in JavaScript commonjs
const middleware = require('ts-middleware')
middleware.foo(...)

Categories

Resources