Babel transform imports to destructure default - javascript

I'm trying to create a build pipeline which doesn't bundle the files together, but instead uses <script type="module">. This will let me just recompile files as they change without rebundling, greatly improving build times during development.
Our project uses ES6, so this is generally easy.
There is however one snag: third-party modules that only have CommonJS builds (such as react).
There are a few ways around this. For now, I have a transform that changes the import name from react to /node_modules/react and my server is smart enough to then go find the appropriate dist file from node_modules and serve it up. This all works fine.
The problem is that it gets confused when I try to do something like:
import { Component } from 'react';
That won't work how it currently is (because it gets confused by there not being a default). However, this will work:
import * as React from 'react';
const { Component } = React;
I could manually do this for all files and packages, but a) that would make it unnecessarily ugly (with Redux and other things, there are half a dozen different packages in many files we'd have to do this to and b) there are lots of files, I don't want to manually change them all.
Is there a Babel transform plugin that can automatically make this kind of conversion? It seems that this isn't a completely novel approach, so I'm hoping there is a plugin that'll do it for me that my Google-fu failed to find.

I managed to get this working. I ended up having to write my own Babel plugin which makes the changes that I was looking for. Basically, for the different versions of the import statement, it changes it around in order to work better with UMD and CJS modules.
For example, something like this:
import A, { a, b, c } from 'a';
was transformed into something like this:
import * from __babel_A from 'a';
const A = __babel_A.default && (__babel_A.default.default || __babel_A.default) || __babel_A;
const { a, b, c } = __babel_A.default || __babel_A;
This format came about based on the way most things export. Many (like React) would put everything into an object with it's name on either module.exports (if I provided a fake one) or this or window. Others ended up not grouping things together (like ReactRTE) and had its own "default", which is where the default.default bit came from.
In addition to this transform, when my server serves up the dist versions of the third-party files, it'd wrap them up in a way that would then let me do an export default. That looked like this:
const module = {};
const keys = Object.keys(this || window);
const toExport = (function __auto_wrapper() {
${fileCode}
return module.exports || Object.keys(this).reduce((r, k) => keys.includes(k) ? r : Object.assign(r, { [k]: this[k].default || this[k] }), {});
}).call(this || window);
export default Object.keys(toExport).length === 1 ? Object.entries(toExport)[0][1] : toExport;
Again, the different ways are based on what the different projects output. Some will use module.exports when given it, others won't consider module.exports a real thing since I (purposely) didn't initialize export as an object (if I did, it'd try to use require() which I didn't want). In my case, ReactRTE had just a CJS module instead of an UMD, so I also had to do replace on its code to replace require('react') and require('react-dom') with references to the objects on window instead.
This all works as I wanted (completely unbundled code loading in the browser). The only slight side-effect is that React and friends are all available on window (which they normally wouldn't be if bundled properly), but that's pretty minor.

Related

Export a Monkey Patch library and Import it into a separate file in Javascript

I'm working in WebDriverIO w/Mocha, and there's some monkey patches I've written to make my code easier to read. Things like:
Object.prototype.findEntries(...arr) for getting a subset of Object.entries(),
String.prototype.padding(int), adding spacing to the right of a string,
Array.prototype.sortByKey(key) to sort an array of similar objects by the value of a common key, and
Array.prototype.last(), the classic.
Since I have many spec files with even more tests, I'd like to have a MonkeyPatchLib.js in my project that I can import into my spec files, so I can call these custom functions when needed:
myTest.js:
import { all } from './MonkeyPatchLib.js' // <- line in question
describe('My First Spec File', async() => {
it('First Test', async() => {
let myArr=[1,2,3]
console.log(myArr.last())
})
})
I've screwed around with module.exports=[func1(){...},func2(){...}] to no avail. I'm aware that I can simply create custom classes (a la the solution to this question) and export those, but that would require an annoying refactor of my codebase. How should I format the library, and how would one import said library?

How can I reload an ES6 module at runtime?

Prior to ES6 modules, it was (I'm told by other Stack answers) easy to force a JS script to be reloaded, by deleting its require cache:
delete require.cache[require.resolve('./mymodule.js')]
However, I can't find an equivalent for ES6 modules loaded via import.
That might be enough to make this question clear, but just in case, here's a simplified version of the code. What I have is a node server running something like:
-- look.mjs --
var look = function(user) { console.log(user + " looks arond.") }
export { look };
-- parser.mjs --
import { look } from './look.mjs';
function parse(user, str) {
if (str == "look") return look(user);
}
What I want is to be able to manually change the look.mjs file (e.g. to fix a misspelled word), trigger a function that causes look.mjs to be reimported during runtime, such that parse() returns the new value without having to restart the node server.
I tried changing to dynamic import, like this:
-- parser.mjs --
function parse(user, str) {
if (str == "look") {
import('./look.mjs').then(m => m.look(user))
}
}
This doesn't work either. (I mean, it does, but it doesn't reload look.mjs each time it's called, just on the first time) And I'd prefer to keep using static imports if possible.
Also, in case this is not clear, this is all server side. I'm not trying to pass a new module to the client, just get one node module to reload another node module.
I don't know what the reason behind doing this,
I think this is not safe to change the context of modules at runtime and cause unexpected behaviors and this is one of the reasons that Deno came to.
If you want to run some code evaluation at runtime you can use something like this using vm:
https://nodejs.org/dist/latest-v16.x/docs/api/vm.html
You could try using nodemon to dynamically refresh when you make code changes
https://www.npmjs.com/package/nodemon
I agree with #tarek-salem that it's better to use vm library. But there is another way to solve your problem.
There is no way to clear the dynamic import cache which you use in question (btw there is a way to clear the common import cache because require and common import has the same cache and the dynamic import has its own cache). But you can use require instead of dynamic import. To do it first create require in parser.mjs
import Module from "module";
const require = Module.createRequire(import.meta.url);
Then you have 2 options:
Easier: convert look.mjs into commonjs format (rename it look.cjs and use module.exports).
If want to make it possible to either import AND require look.mjs you should create the npm package with package.json
{
"main": "./look.cjs",
"type": "commonjs"
}
In this case in parser.mjs you will be able to use require('look') and in other files import('look') or import * as look from 'look'.

When using dynamic import in a function, how can I specify type info in global variable?

My simplified server code looks like below.
server.ts
import google from "googleapis";
const androidPublisher = google.androidpublisher("v3");
app.use('something', function(req, res, n){
...
})
...(only one of the dozens of other methods use androidPublisher)
I am importing googleapis library in order to setup androidpublisher variable. However, this googleapis library is big and it takes 400ms~700ms to fully import file, when it takes 10ms~30ms to import other library files.
Because my environment is serverless architecture (firebase functions), AND because approximately 1 out of 100 requests actually need androidPublisher, I want to take advantage of dynamic import to import googleapis when it is necessary. Otherwise, above setup actually adds 400ms/700ms latency to every request that spins up new serverless instance, even when androidPublisher is not needed.
So I have made changes like below.
server.ts
let androidPublisherInstance:any;
async function getAndroidPublisher() {
const googleapis = await import("googleapis");
if (androidPublisherInstance === undefined) {
const ap = googleapis.google.androidpublisher("v3");
androidPublisherInstance = ap;
}
return androidPublisherInstance;
}
...(one of methods use getAndroidPublisher() to get androidPublisher instance)
with above setup where I am using global variable & helper function to initialize androidPublisher only when needed. This works as intended and 400ms~700ms latency gets added when androidPublisher is needed for the first time. However, I ended up with type of androidPublisherInstance to be any. I couldn't correctly define the type because type definition is available inside of googleapis and that is residing inside of getAndroidPublisher function.
Thus, I lose all benefit of using typescript and have to play guess games while using methods/properties when I use androidPublisherInstance.
And I think I must use global variable, because I do not want to initialize androidPublisher multiple times (googleapis.google.androidpublisher("v3")) whenever a function calls getAndroidPublisher()
Am I missing something? Is there a way to use dynamic import & let client to be initialized only once without needing to use global variable?
You can just import the type. As long as you use it only in type definitions, not in value expressions, the compiled JavaScript will never load the module:
import { androidpublisher_v3 } from "googleapis";
let androidpublisher: androidpublisher_v3 | undefined;
Alternatively, to make sure you don't accidentally reference it in the wrong place, use only import types:
let androidpublisher: import("googleapis").androidpublisher_v3 | undefined;

How to make Webpack recognize dynamic exports

I'm seeing the following warning when building with Webpack v4 (using babel-loader for the JS files):
Warning in ./src/components/Foo
"export 'ADDENDUM' was not found in '../../types'
...
The import in ./src/components/Foo is:
import { ADDENDUM } from '../../types';
../../types:
import { each } from 'lodash';
export const typesDict = {
ADDENDUM: 'addendum',
};
each(typesDict, (type, typeConstant) => {
exports[typeConstant] = type;
});
This isn't causing a build error, just a warning. The warning is wrong though, since I am exporting ADDENDUM (though dynamically), and everything works as it should.
Is there a way for Webpack to handle these dynamic imports, or to at least turn off the warning? I'm upgrading from Webpack v1 right now, and v1 does not have this problem (or if it does, it's being hidden somehow).
Also please note: I do NOT want to silence all Webpack warnings, such as via the devServer config. I just want to silence this one type of warning.
Based on your ../../types file i assume your approach was to skip writing again the components in the exports object.
Instead of silencing the warning, try something simpler to fix the issue. Since you don't want to write twice the same names, take a look at my example.
No lodash is required, no loops are used and exported constants are written once.
../../types:
export const ADDENDUM = 'addendum';
export const ADDENDUM2 = 'addendum2';
export const ADDENDUM3 = 'addendum3';
That is all, no more dynamic imports, no more warnings.
UPDATE:
Your code is indeed valid, but when you use dynamic exports/imports, compilers/bundlers loose trace of your exports(in your case) since they don't check the contents of your exports object, thus the warning you receive, because the compiler(babel) didn't find exports.ADDENDUM in your code, only you know that it's there, therefore the compiler thinks you're using an unexisting component.
As of imports, it's the same story, the same type of warning was emitted by webpack when something like require('/path/to/' + someVar + '/some.file.js'), because webpack wanted to make a chunk out of it, but that wasn't a full path for webpack and it couldn't find the file because it was a concatenated string (dynamic import). (i don't know if this changed over the years, but i'm sure you understand/knew this perfectly well too)

How to import as a new copy of object?

In my react application, I want to use a library (money.js) in 2 components which have different settings.
This library:
http://openexchangerates.github.io/money.js/
http://openexchangerates.github.io/money.js/money.js
I checked that javascript is using reference so the 2 components are actually referencing the same thing.
Does import create a new copy of imported library?
ES6 variable import by reference or copy
Is that possible without changing the source code (ideally), I can do something like the following
// a.js
import fx from 'money'
fx.rates={USD:1, EUR: 2.001}
// b.js
import fx from 'money'
fx.rates={USD:1, EUR: 2.002}
In my situation, I noticed that changing the rates in b.js, it also affect a.js.
No, there's no general one-size-fits-all way to clone a module or load separate copies of it. You may be able to do it, but it will be dependent on the specific way the module is coded.
It's unfortunate that the library you're using doesn't have the concept of an instance rather than making everything global to the library. You might consider adding that concept to the library and sending the original repo a pull request (if they decline it, you can always create a fork).
You can do it simply by adding query-param to module filepath:
// a.js
import fx from 'money?first'
fx.rates={USD:1, EUR: 2.001}
// b.js
import fx from 'money?second'
fx.rates={USD:1, EUR: 2.002}
you can clone that rates object in each file :-
var localRatesVariable = Object.assign({}, fx.rates);
Modules are evaluated once, that's one of their main properties. It's possible to re-evaluate a module in some environment that supports it (Node.js) but not in client-side application.
In this case the library is hard-coded to use fx.rates object. Even if it's copied, it will continue to use it.
A proper way is to modify library source code to support multiple instances.
An alternative is to create a wrapper that hacks the library to behave like expected. Considering that it's convert method that uses fx.rates and it's synchronous, it can be patched to swap rates property during a call:
import fx from 'money'
export default function fxFactory(rates) {
return Object.assign({}, fx, {
convert(...args) {
let convertResult;
const ratesOriginal = fx.rates;
try {
fx.rates = rates;
convertResult = fx.convert(...args);
} finally {
fx.rates = ratesOriginal;
}
return convertResult;
}
});
}
// a.js
import fxFactory from './fx'
const rates={USD:1, EUR: 2.001};
const fx1 = fxFactory(rates);
You can deep copy object:
let localRatesVariable = JSON.parse(JSON.stringify(fx.rates));

Categories