Conditional import of peerDependency in es6 - javascript

I am working on a JavaScript i18n library which localize dates (among other types and objects).
It is currently relying on moment.js, which is defined as a peerDependency (localization is one of the features but not the only one, it might not be used)
// package.json
{
"name": "my-i18n-library",
// ...
"scripts": {
// ...
"clean": "rimraf build",
"build": "babel src -d build",
"prepare": "npm run clean && npm run build"
},
"peerDependency": {
"moment": "~2.20.1",
"date-fns": "~1.29.0"
},
// ...
}
// .babelrc
{
"presets": ["env", "stage-1", "react"]
}
Basically something like (a bit more error-proof but I simplified the logic) :
import Moment from 'moment.js'
import 'moment/min/locales'
class Localizer {
localizeDate(value, locale, displayFormat = "L", parseFormat = "LTS") {
return Moment(date, parseFormat, locale).format(displayFormat);
}
}
Problem is, if moment.js is a nice work, it is like a backpack of stones, you would not bring it on a 50 miles trail, especially if you only need to localize one date in a whole application. Bandwidth-wise, it's not worth it IMO (actually in a lot of people opinions as well).
So I am considering switching to lighter libraries such as date-fns, but I figured out an option I think is even better :
what if we could let the other choose which library suits him the most?
I was thinking to define different implementations of library-related localizers, and conditionally import them depending of which peerDependency is installed :
// /Date/DateFnsLocalizer.js
import { parse } from 'date-fns/parse'
import { format } from 'date-fns/format'
class DateFnsLocalizer {
localizeDate(value, locale, displayFormat = "L") {
return format(parse(date), displayFormat, { locale })
}
}
Is that even possible in JavaScript ?
// /Localizer.js
if (isModuleDefined('moment.js')) {
import BaseLocalizer from './Date/MomentLocalizer'
} else if (isModuleDefined('date-fns')) {
import BaseLocalizer './Date/DateFnsLocalizer'
} else if (isModuleDefined('some-other-lib')) {
import BaseLocalizer './Date/SomeOtherLibLocalizer'
} else {
throw new Error('No date library defined! Please install at least one of ["moment.js", "date-fns", "some-other-lib"]')
}
export default class Localizer extends BaseLocalizer
I think "import" statements have to be made as first statements in the file. Maybe using require instead... (as suggested in ES6: Conditional & Dynamic Import Statements) ? And is it possible to test a module existence without importing it (basically how to code that isModuleDefined() method?)
I have seen those ones as well :
ES6 variable import name in node.js?
How can I conditionally import an ES6 module?
But as we are currently using babel to transpile this library, if this architecture is possible, could it cause compilation troubles in other building tools such as webpack, gulp, grunt etc.?

Related

Upgrading to Angular 10 - Fix CommonJS or AMD dependencies can cause optimization bailouts

I am trying to upgrade my Angular 9 app to Angular 10 version, but I am getting the below warning after the upgrade
rxjs\BehaviorSubject.js depends on rxjs-compat/BehaviorSubject
How can I fix this?
When you use a dependency that is packaged with CommonJS, it can result in larger slower applications
Starting with version 10, Angular now warns you when your build pulls in one of these bundles. If you’ve started seeing these warnings for your dependencies, let your dependency know that you’d prefer an ECMAScript module (ESM) bundle.
Here is an official documentation - Configuring CommonJS dependencies
In your angular.json file look for the build object and add
allowedCommonJsDependencies
as shown below -
"build": {
"builder": "#angular-devkit/build-angular:browser",
"options": {
"allowedCommonJsDependencies": [
"rxjs-compat",
... few more commonjs dependencies ...
]
...
}
...
},
Try replacing the rxjs imports rxjs/internal/operators with rxjs/operators.
Example:
import { catchError, retry } from 'rxjs/internal/operators';
with
import { catchError, retry } from 'rxjs/operators';
It is recommended that you avoid depending on CommonJS modules in your Angular applications. Depending on the CommonJS modules, they can prevent bundlers and minifiers from optimizing your application, which results in larger bundle sizes. Instead, it is recommended that you use ECMAScript modules in your entire application.
Still, if you don't care about your bundling size, to disable these warnings, you can add the CommonJS module name to allowedCommonJsDependencies option in the build options located in the angular.json file.
"build": {
"builder": "#angular-devkit/build-angular:browser",
"options": {
"allowedCommonJsDependencies": [
"rxjs-compat"
]
...
}
...
},
Source
For the RXJS library you can do the following changes.
For imports, such as 'rxjs/internal/<anything>' and 'rxjs/index', replace it with just 'rxjs'.
For imports such as 'rxjs/internal/operators', replace it with 'rxjs/operators'.
Or replace just rxjs.
Just change the import:
from:
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
To:
import { BehaviorSubject } from 'rxjs';
Another case of this is the problem being warned about during the build with the use of BehaviorSubject from rxjs when using the following style of imports:
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
It results in the following error:
Warning: my.service.ts depends on 'rxjs/BehaviorSubject'. CommonJS or
AMD dependencies can cause optimization bailouts.
By importing from the root module instead, the warning is no longer present during the build:
import { BehaviorSubject } from 'rxjs';
In my case (after update to TypeScript version 3.9.7) flatMap is deprecated (from rxjs/operators).
This is alias for mergeMap, so just I replaced:
import { flatMap } from 'rxjs/internal/operators';
to
import { mergeMap } from 'rxjs/operators';
I had a similar issue (app.module.ts depends on 'ngx-google-places-autocomplete'), but many answers did not help me.
So if you have x depends on y, just add y in the angular.json file in "allowedCommonJsDependencies".
to fix this issue on terminal
in angular.json put this line in :
"architect": {
"build": {
"builder": "#angular-devkit/build-angular:browser",
"options": {
"allowedCommonJsDependencies": [
"rxjs",
],
I have a very big project with deprecated imports 'rxjs' and create this script for upgrading all deprecated imports:
python3.6 replace_imports.py PATH_TO_SRC_DIR
This script upgrades the import like "rxjs\/(internal|Observable|Subject|ReplaySubject|Subscription|BehaviorSubject)"
to
import { * } from rxjs
Also try to upgrade rxjs-compat.

Create React App: Transpile JSX of external package in node_modules

I'm trying to use the react-scratchblocks package on my react project.
I've created my project using the create-app-react command.
After importing the package I got the following error:
Failed to compile.
./node_modules/react-scratchblocks/src/Scratchblocks.js
SyntaxError: /Users/jorge/Documents/React/elimu-analyzer-frontend/node_modules/react-scratchblocks/src/Scratchblocks.js: Unexpected token (45:6)
43 | const scripts = this.parseScripts(this.props.code);
44 | return (
> 45 | <div className={this.classNames()}>
| ^
46 | {scripts.map((script,i) => (
47 | <Scratchblock key={i} className="script" script={script}/>
48 | ))}
I know that the jsx it's not been recognized, but what should I do to make this package work? Remember: i've used the create-rect-app to create my React project.
Thanks.
UPDATE 1:
module.exports = function () {
return {
overrides: [{
test: ["./node_modules/react-scratchblocks"],
presets: ["#babel/preset-react"]
}],
};
}
UPDATE 2:
Component that where I import the react-scratchblocks.
import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';
import './styles.css';
import Fire from '../../config/Fire';
import Realtime from '../Realtime';
import Scratchblocks from 'react-scratchblocks'
function Content(props) {
const [menuOption, setMenuOption] = useState(1);
async function logout() {
await Fire.logout();
props.history.push('/');
console.log('oi');
}
if (menuOption === 0) {
return (
<div class='content'>
<Realtime />
</div>
);
}
else if (menuOption === 1) {
return (
<div class="content">
<button onClick={logout}> OUTRA OPÇÃO </button>
</div>
);
}
}
export default withRouter(Content);
Create React App (CRA) only transpiles standard JavaScript syntax inside node_modules.
This does not include JSX compilation. Package react-scratchblocks errors due to untranspiled JSX:
SyntaxError: .../Scratchblocks.js: Unexpected token (45:6)
Statement from maintainers (link):
We only compile valid JavaScript syntax in node_modules. JSX is not valid JavaScript. You are using JSX there.
The reason we've taken this stance is because compiling non-standard syntax tightly couples libraries to build tools.
It's also hard to draw a line once you allow experimental things. Most people will want to use not just JSX, but also experimental transforms like class properties. Or decorators. Now we have to argue with every library maintainer about which transforms we want to support, and which we don't.
Hence package authors would have needed to transpile JSX them selves before distribution.
To transpile JSX manually1 you can apply the Babel React preset to react-scratchblocks inside node_modules:
babel node_modules/react-scratchblocks \
-d node_modules/react-scratchblocks/dist \
--presets=#babel/preset-react
The build step might be outsourced into its own config file (transpile.js):
module.exports = {
...
overrides: [
{
test: ["./node_modules/react-scratchblocks"],
presets: ["#babel/preset-react"]
}
]
};
babel node_modules/react-scratchblocks \
-d node_modules/react-scratchblocks/dist \
--config-file ./transpile.js
Then adjust main entry inside node_modules/react-scratchblocks/package.json to point to the previously transpiled version in dist:
"main": "dist/index.js",
patch-package can further automate the process of fixing broken packages.
Remember this is only a workaround - the duty is on package developers to distribute an npm package with standard JavaScript features.
1 A different alternative would be to adjust Webpack config (only possible with ejected CRA).
I personally suggest you to use craco (see #craco/craco)
Craco is a powerful tool that allows you to edit built-in create-react-app configuration without forcing you to eject the project.
How to install it
run npm install #craco/craco --save-dev
run npm install craco-babel-loader --save-dev
create craco.config.js in the root folder of the project
update the scripts in package.json:
react-scripts start -> craco start
react-scripts build -> craco build
...
this is the content of the configuration craco file
const path = require('path')
const fs = require('fs')
const cracoBabelLoader = require('craco-babel-loader')
// manage relative paths to packages
const appDirectory = fs.realpathSync(process.cwd())
const resolvePackage = relativePath => path.resolve(appDirectory, relativePath)
module.exports = {
plugins: [
{
plugin: cracoBabelLoader,
options: {
includes: [
resolvePackage('node_modules/package-to-transpile'),
resolvePackage('node_modules/another-package-to-transpile'),
],
},
},
],
}
You can change a lot of other configurations, and I suggest you to give a look at craco npm package page
Credtis: https://stackoverflow.com/a/58603207/4277948

"TypeError not a constructor" when upgrading project from Babel v5 to v6+

I've been bashing my head against this brick wall for a day now so it's time to turn to SO for help I think! I'm trying to update a project off babel v5, I'm starting off with this in package.json:
"devDependencies": {
"babel": "^5.8.21",
"test": "mocha --require babel/register",
I've tried to upgrade to babel both v6 and v7 with no success either time. In both cases I end up with TypeError: _application.ApplicationClient is not a constructor when I attempt to run tests that worked fine with v5:
import { ApplicationClient } from '../src/wiotp/sdk/application';
...
let client = new ApplicationClient();
Using v6 as an example, I end up with the following in package.json:
"devDependencies": {
"babel-cli": "^6.0.0",
"babel-core": "^6.0.0",
"babel-preset-env": "^1.7.0",
"mocha": "6.1.4",
"test": "mocha --require babel-core/register --timeout 5000",
and created a .babelrc file (none existed previously):
{
"presets": ["env"]
}
I've read a dozen or more articles trying to understand what is going on here, I've done and undone numerous suggested "fixes" based on Google searches for similar problems, but I'm starting from zero and finding it incredibly hard to get simple plain-English explanation of what the actual problem is for a start, and how babel is supposed to be configured here to resolve it. I'm guessing something that happened by default in v5 needs to be configured somehow in .babelrc now, but /shrugs/ no idea where to go from here now.
babel5 to babel6 branch compare
babel5 to babel7 branch compare
Any pointers for someone struggling to wrap his head around babel, let alone the differences between v5, 6, & 7?
Edit:
src/wiotp/sdk/application/index.js has this:
import { default as ApplicationClient } from './ApplicationClient';
export default {
ApplicationClient
}
Where src/wiotp/sdk/application/ApplicationClient.js has a single class exported (I'm just trying to create an instance of that class in the test code):
export default class ApplicationClient extends BaseClient {
constructor(config) {
src/wiotp/sdk/application/index.js has this:
import { default as ApplicationClient } from './ApplicationClient';
export default {
ApplicationClient
}
This is horrible, and causing your problem. That module does default-export an object literal, instead of just using named exports. It might be a bug and they meant to write either
import { default as ApplicationClient } from './ApplicationClient';
export { ApplicationClient }
or
export { default as ApplicationClient } from './ApplicationClient';
It would recommend to report an issue and provide a patch.
If this is not considered a bug but was done on purpose, you will need to change your code to
import application from '../src/wiotp/sdk/application';
…
let client = new application.ApplicationClient();
// ^^^^^^^^^^^^
or just import it directly from the original module:
import ApplicationClient from '../src/wiotp/sdk/application/ApplicationClient.js';
// ^^^^^^^^^^^^^^^^^^^^^
…
let client = new ApplicationClient();

Test two different npm package versions at the same time

When I create an npm package, sometimes it would face the need to backward old dependency package version.
If the new version has new api, I may write the code in this pattern:
import pkg from 'some-pkg';
const isNewVersion = pkg.newVersionApi !== 'undefined';
if (isNewversion) {
pkg.newVersionApi();
} else {
pkg.oldVersionApi(); // backward compatible api
}
And with this pattern, when I want to write the test, I only can test the installed version code. The other version's code can't be tested.
For real example, in React v15 and v16, React v16 has new API Portal. Before Portal release, v15 has unstable_renderSubtreeIntoContainer api to realize similar feature.
So the code for React would be like:
import ReactDOM from 'react-dom';
const isV16 = ReactDOM.createPortal !== 'undefined';
if (isV16) {
ReactDOM.createPortal(...);
} else {
ReactDOM.unstable_renderSubtreeIntoContainer(...);
}
So I want to ask is there any method to test with different dependency version?
Currently, one method I think of is to install the other version again and test it. But it only can do on local. It can't work on ci and it can't count in coverage together.
I think that is not only for react test. It may face on node.js test. Any suggestion can be discussed.
Updated
This question maybe is related to install two versions dependency in npm. But I know currently installing two versions dependency is not workable.
Here is a might be solution, not sure it will work as you expect. But, you will have a direction to move forward.
package.json
{
"name": "express-demo",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.3",
"debug": "~2.6.3",
"express": "~4.15.2",
"jade": "~1.11.0",
"morgan": "~1.8.1",
"serve-favicon": "~2.4.2",
"webpack": "^3.8.1",
"webpack-dev-middleware": "^1.12.0",
"webpack-hot-middleware": "^2.20.0"
},
"customDependecies": {
"body-parser": [
"",
"1.18.1",
"1.18.0"
]
}
}
Note in above package.json file, I have added a new key customDependecies which I will use for installing multiple dependencies. Here I am using body-parser package for demo. Next you need file, that can read this key and install the deps.
install-deps.js
const {spawnSync} = require('child_process');
const fs = require('fs');
const customDependencies = require('./package.json').customDependecies;
spawnSync('mkdir', ['./node_modules/.tmp']);
for (var dependency in customDependencies) {
customDependencies[dependency].forEach((version) => {
console.log(`Installing ${dependency}#${version}`);
if (version) {
spawnSync('npm', ['install', `${dependency}#${version}`]);
spawnSync('mv', [`./node_modules/${dependency}`, `./node_modules/.tmp/${dependency}#${version}`]);
} else {
spawnSync('npm', ['install', `${dependency}`]);
spawnSync('mv', [`./node_modules/${dependency}`, `./node_modules/.tmp/${dependency}`]);
}
});
customDependencies[dependency].forEach((version) => {
console.log(`Moving ${dependency}#${version}`);
if (version) {
spawnSync('mv', [`./node_modules/.tmp/${dependency}#${version}`, `./node_modules/${dependency}#${version}`]);
} else {
spawnSync('mv', [`./node_modules/.tmp/${dependency}`, `./node_modules/${dependency}`]);
}
});
}
spawnSync('rm', ['-rf', './node_modules/.tmp']);
console.log(`Installing Deps finished.`);
Here, I am installing deps one by one in tmp folder and once installed, I am moving them to ./node_modules folder.
Once, everything is installed, you can check the versions like below
index.js
var bodyParser = require('body-parser/package.json');
var bodyParser1181 = require('body-parser#1.18.1/package.json');
var bodyParser1182 = require('body-parser#1.18.0/package.json');
console.log(bodyParser.version);
console.log(bodyParser1181.version);
console.log(bodyParser1182.version);
Hope, this will serve your purpose.
Create 3 separate projects (folders with package.json) and a shared folder:
A shared folder containing the test module (my-test). Export a function to run the test;
A client project importing my-test and dependency v1. Export a function that calls the test function in my-test.
A client project importing my-test and dependency v2. Export a function that calls the test function in my-test.
A master project that imports both client projects. Run each exported function.
You're going to have to run them separately. Create a separate project folder for each dependency version. Ex. React10, React11, React12. Each will have its own package.json, specified for the correct version. When you run the integration and/or versioned tests, you'll run your standard unit tests across each version, but it may also be advisable to add any version specific unit tests to that folder.
Creating a make file would make your life easier when running your full testing suite. If you do this you can easily integrate this into CI.
In addition to the other suggestions, you can try the approach described at Testing multiple versions of a module dependency.
Here's an example of using this approach to test against multiple webpack versions:
npm install --save-dev module-alias
npm install --save-dev webpack-v4#npm:webpack#4.46.0
npm install --save-dev webpack-v5#npm:webpack#5.45.1
The module-alias package handles the magic of switching between package versions while still supporting normal require('webpack') (or whatever your module is) calls.
The other installs will create two versions of your dependency, each with a distinct directory name within your local node_modules/.
Then, within your test code, you can set up the dependency alias via:
const path = require('path');
require('module-alias').addAlias(
'webpack',
path.resolve('node_modules', 'webpack-v4'),
);
// require('webpack') will now pull in webpack-v4
You'd do the same thing for 'webpack-v5' in a different test harness.
If any of your sub-dependencies have a hardcoded require('webpack') anywhere in their own code, this will ensure that they also pull in the correct webpack version.

BabelJS class ordering when using inheritance [duplicate]

This question already has an answer here:
Bundling ES6 classes with Webpack. Is there a way to hoist extended classes?
(1 answer)
Closed 5 years ago.
I am trying to find a way to make babel load files in a particular order so that superclasses are loaded before childclasses.
An example given the following files:
src/fruit.js:
export class Fruit{
constructor(color){
this._color = color;
}
}
src/apple.js:
export class Apple extends Fruit{
constructor(){
super("green");
}
}
src/xecute.js:
var theApple = new Apple();
package.json
{
"name": "fruit",
"version": "1.0.0",
"description": "Fruit JS",
"scripts": {
"build": "babel src -o out/fruit-bundle.js"
},
"author": "Toby Nilsen",
"license": "MIT",
"devDependencies": {
"babel-cli": "^6.22.2",
"babel-preset-es2015": "^6.5.0"
}
}
.babelrc:
{
"presets": ["es2015"]
}
When I compile my files the following command
npm run build
And run my out/fruit-bundle.js with:
node out\fruit-bundle.js
I get the follwing error:
TypeError: Super expression must either be null or a function, not undefined
This is because babel parses apple.js before fruit.js. I can work around the problem by renaming my files to 0_fruit.js and 1_apple.js, but I would like to know if there is any way for babel to resolve the dependencies and order the output so that superclasses are loaded first?
Babel is just a transpiler. It just transpiles the syntax, but it does not do bundling for you. You'll need a bundler to resolve dependencies in the correct order. Consider checking out Rollup or Webpack. Going with Rollup, the simplest way to do this without the caching and other build optimizations is either to:
Run Rollup to bundle everything to one file then run Babel on Rollup's output.
Run Babel on all files, then use Rollup to bundle them all.
Also, so that the bundler knows the right order, import Fruit from Apple.
import Fruit from 'fruit';
export class Apple extends Fruit{
constructor(){
super("green");
}
}

Categories

Resources