Assign namespaces to src library files - javascript

I have a website made in ReactJS. The website calls functions of a library analyzejs generated by another programming language. I can only call functions of this library; I cannot modify much the content of this library.
So far, I have been using the first version of the library: analyzejs-v1.js. To achieve this, in frontend/public/index.html of the website, I have
<head>
<script src="/lib/analyzejs-v1.js"></script>
<!-- <script src="/lib/analyzejs-v2.js"></script> -->
</head>
<body>
<div id="root"></div>
</body>
I also have a type declaration file frontend/src/defines/analyzejs-v1.d.ts as follows:
declare function f1(): string;
declare function f2(): string;
...
As a result, in the code of the website, I could call directly f1() to use analyzejs-v1.js.
Now, I would like to add another version of the library analyzejs-v2.js. In some part of the website, I want to call f1() of analyzejs-v1.js; in some part of the website, I want to call f1() of analyzejs-v2.js. So I guess I need to add namespaces such as v1 and v2 to these different versions to avoid conflict. Then, I will be able to call v1.f1() and v2.f2().
I tried to modify frontend/src/defines/analyzejs-v1.d.ts as follows:
declare namespace v1 {
function f1(): string;
function f2(): string;
...
}
And in the code of the website, I tried to use v1.f1().
The compilation did not raise any error. However, running the website and using features calling v1.f1() returned me an error on v1.f1(): Unhandled Rejection (ReferenceError): v1 is not defined.
Does anyone know how to add a namespace to such a library?

The best way would be to group the functions inside your javascript files into classes or objects
// test.js
const testNamespace = {
print = () => console.log('print was called from test.js')
}
// test1.js
const anotherNamespace = {
print = () => console.log('print was called from test1.js')
}
now you can call these functions using
anotherNamespace.print();
testNamespace.print();

If you import these files in your code like so:
import analyzejs-v1 from '/lib/analyzejs-v1.js'
import analyzejs-v2 from '/lib/analyzejs-v2.js'
in your code you can call them under different objects:
analyzejs-v1.f();
analyzejs-v2.f();

Problem - you are only using typescript type declarations, which are deleted on compilation, completely not doing anything at runtime. JavaScript actually doesn't support namespaces, only global scope, function scope and block scope.
Solution #1 - using es6 import syntax, which creates a function scope around the imported script (referred to as a module). And even better, React supports dynamic imports, which gives you both a module "namespace", and automatic code splitting, which could help with your heap memory issues:
// Page1.tsx
import("/lib/analyzejs-v1.js").then(module => {
module.f1();
module.f2();
});
// Page2.tsx
import("/lib/analyzejs-v2.js").then(module => {
module.f1();
module.f2();
});
The dynamic import splits the JS bundle so that the dynamically required JS parts are in separated files, and run only when import() is called. This could help with really large files and memory issues.
import() returns a promise, which gets the module as an argument when resolved.
Solution #2 - use the import syntax import * as moduleName from "module/url". This import method contains the functions in the module to an object with the name of your choosing, but everything will be in the main bundle as apposed to dynamic imports:
import * as moduleV1 from "/lib/analyzejs-v1.js";
import * as moduleV2 from "/lib/analyzejs-v2.js";
moduleV1.f1();
moduleV1.f2();
moduleV2.f1();
moduleV2.f2();

So you want to wrap different versions of a vanilla javascript library into a bundled script library and selectively call either in a browser?
This solution uses npm, browserify, require and module.exports.
The strategy is to add a module.exports to the vanilla libraries and then bundle them together as globals v1 and v2.
So setup a folder for building the bundled libraries.
$ mkdir bundle-analyzers
$ cd bundle-analyzers
$ npm init
package name: (bundle-analyzers)
version: (1.0.0)
description: bundle versions of analyzejs with browserify
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
Create a lib folder and copy the analyzejs files into it
+--bundle-analyzer
| +-- lib
| | analyzejs-v1.js
| | analyzejs-v2.js
*
For simplicity, let's assume both analyzejs versions have a common method named version(). Let's expose both
by adding the following to the end of each analyzejs-v*.js file:
module.exports = {
version
}
Create a file analyzejs-bundler.js which will expose both library's exports globally:
var v1 = require("./lib/analyzejs-v1.js");
var v2 = require("./lib/analyzejs-v2.js");
window["v1"] = v1;
window["v2"] = v2;
Install browserify in the root folder
$ npm i browserify
Add an index.html to the project folder which will demo the bundle:
<html>
<head>
<script src="./bundle/analyzejs-bundle.js"></script>
</head>
<body>
<pre id="versions"></pre>
<script>
window.addEventListener('load', () => {
document.getElementById('versions').innerText = [v1.version(), v2.version()].join('\n');
});
</script>
</body>
</html>
Now add a script to package.json to create the bundle:
"scripts": {
"bundler": "browserify ./analyzejs-bundler.js -o ./bundle/analyzejs-bundle.js"
}
Create the bundle
$ npm run bundle
OK so the folder structure should be
+--bundle-analyzer
| +-- bundle
| | analyzejs-bundle.js
| +-- lib
| | analyzejs-v1.js
| | analyzejs-v2.js
| +-- node_modules
| | { node stuff }
| analyzejs-bundler.js
| index.html
| package-lock.json
| package.json
*
Open up index.html from the file system and verify.
Of course if there a zillion exports from the analyzejs library something more sophisticated will be required.
Note it is also possible to create a monolith for versions of analyzejs which may be more suitable:
var v1 = require("./lib/analyzejs-v1.js");
var v2 = require("./lib/analyzejs-v2.js");
window["analyzejs"] = { v1, v2 }
YMMV. The ambient definitions for Typescript depend on the global variables defined and are simply a tedious sideshow to the solution IMO.

Related

How to load a variable form my app into package.json?

I know this sounds as easy as using globals, but I'm not so sure.
Here's my case:
I have multiple files within /src directory of my React app, let's call them src/a.js, src/b.js,
every single of these files exports one object which I then use within my app:
./src/a.js:
export default {
filename: 'a',
foo: 'bar',
};
./src/b.js:
export default {
filename: 'b',
foo: 'bar',
blah: 'hah',
};
Now I have a command to check whether or not structure of objects within these files match (they are being changed by many developers multiple times a day), so when I do npm check in terminal it will return false for above input, because blah does not exist within two files.
My package.json looks like this:
"scripts": {
"check": "node check.js runCheck",
/.../
}
My question is: how the heck do I load these variables to compare them in package.json?
I have a file called:
./check.js:
function check(files) {
// checking files there
};
module.exports.check = check;
Approach #1 - imports
This is a build file, not part of the application itself, so when I try to do:
./check.js:
import a from './src/a';
import b from './src/b';
I'm getting:
SyntaxError: Cannot use import statement outside a module.
Approach #2 - require
This is going to cause trouble, because I'm using imports, not modules within my app, therefore doing:
./check.js:
const a = require('./src/a');
const b = require('./src/b');
Returns:
Error: Cannot find module './src/a'.
Of course I can't do module.exports within the a.js/b.js files, because they're part of my app and they should use exports, I've tried using both export and module.exports, but it does not work as well and looks shitty.
How do I tackle this? Should I load the files using some file loader, parse it as JSON an then compare? Or maybe there's an easier way?
You'll need to use something like esm (https://github.com/standard-things/esm) to run node with module support.
It should be as simple as:
npm install esm
Then update your package script to be:
"check": "node -r esm check.js runCheck",
Edit Btw, a very clear and well structured question.

Dynamically loading a file based on an environment variable in Webpacker (Rails 6)

First time for me on Stack Overflow, please be kind ;) I'll try to do my best!
The context:
I am working on a Rails 6 app with webpacker. This is a program that will be shared by several companies and in order to apply the 'one code, multiple setups' paradigm, we decided to move all the company related configuration files to separate folders, and to put the company name as a variable in our .env file. We need to change some config variables as well as some geofencing data (so our customers can create a new delivery to some address). Basically that's what it looks like:
Project folder
| config
| companies
| a_first_company
| rails_config.rb
| geofencing.js
| a_second_company
| rails_config.rb
| geofencing.js
| ....
In the .env file:
COMPANY=a_first_company
And in the rails configuration (application.rb), we are using a simple:
require_relative "companies/#{ENV['COMPANY']}/rails_config"
But now, here comes the JS part! And I am running into trouble.
The problem:
I would like to include dynamically a JSON object in an existing script. A sample geofencing.js looks like that:
module.exports = {
"countries": ["be"],
"polygon": [
50.8917729, 4.3004608,
...
50.9162381, 4.3450928,
50.8917729, 4.3004608
]
}
And I am trying to import it as a geofencing variable in my existing address autocompletion script:
/app/javascript/plugins/places.js
// I know it doesn't work that way, but basically that what I would like to do:
const geofencing = require(`/config/companies/${process.env.COMPANY}/geofencing`);
...
const initPlaceAutocomplete = () => {
...
var placesAutocomplete = places(
{
// And use the variable here...
insidePolygon: [geofencing.polygon],
type: 'address',
// And there...
countries: geofencing.countries,
templates: {
value: (suggestion) => {
return suggestion.name;
}
},
container: addressInput
}
);
...
}
export { initPlaceAutocomplete };
This file is imported in the view with a <%= javascript_pack_tag 'delivery_new' %>:
/app/javascript/packs/delivery_new.js
import { initPlaceAutocomplete } from '../plugins/places';
initPlaceAutocomplete();
...
The solution (that I haven't found yet):
I have tried several things, like importing the file in the webpack config (/config/webpack/environment.js), just like in the ProvidePlugin documentation:
const {environment} = require('#rails/webpacker')
const path = require('path');
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Popper: ['popper.js', 'default'],
geofencing: path.resolve(path.join(__dirname, '..', '..', 'config', 'companies', process.env['COMPANY'], 'geofencing'))
})
)
module.exports = environment
... But it didn't work.
I also tried various places to import 'geofencing' in several places, with always the same result in the Chrome console: Uncaught ReferenceError: geofencing is not defined.
I noticed, though, that I had access to the process.env variables in the places.js script: writing console.log(process.env['COMPANY']); in the file prompts me the company name in the dev console when I reload the page in Chrome.
Apart from this, I have to say that I am lost. I am basically a newbie to the Webpack 'magic' ;)
Please tell me if you need more info about my setup.
Thanks in advance for your help!
You should be able to require modules using interpolation in the require() expression similar to your example. Try using a relative path, e.g. (assuming parent directories are siblings):
require(`../config/${process.env.COMPANY}/geofencing`)
The above expression will add only the COMPANY geofencing module to the bundle.
If you wish this to be dynamic, webpack can resolve the interpolation at runtime assuming the require paths are scoped to a directory; you're already doing this here with "../config/". As a result, webpack will include all COMPANY geofencing modules in the bundle. As for usage, I might move the require statement inside a function:
function initPlaceAutocomplete(company) {
const geofencing = require(`../config/${company}/geofencing`)
// ...
}
// usage
import { initPlaceAutocomplete } from "../plugins/places"
initPlaceAutocomplete(process.env.COMPANY)
At this point, this could is a good use case for dynamic imports, i.e., webpack supports the TC39 proposal for dynamically loading modules at runtime.
Instead of require("../config..."), you can use the import() function syntax. Unlike require(), the import() function syntax resolves asynchronously.
Now webpack will bundle all the COMPANY geofencing modules, but as separate "chunks" to keep the size of your initial script down. webpack will insert code to resolve these "chunks" asynchronously at runtime. To support this, the import() expression returns a Promise so, I'm using the async/await syntax here as a result.
async function initPlaceAutocomplete(company) {
const geofencing = await import(`../companies/${company}/config`)
// ...
}

ES6 dynamic imports and instanciation of classes

I'm trying to figure out how to perform dynamic import of classes in ES6 one the server side (node.js with Babel).
I would like to have some functionalities similar to what reflection offers in Java. The idea is to import all the classes in a specific folder and instanciate them dynamically.
So for example I could have multiple classes declared in a folder like the one below :
export default class MyClass {
constructor(somevar) {
this._somevar = somevar
}
//...
//some more instance level functions here
}
and then somewhere else in my app's code I could have a function that finds out all the classes in a specific folder and tries to instanciate them :
//somewhere else in my app
instanciationFunction(){
//find all the classes in a specific folder
var classFiles = glob.sync(p + '/path_to_classes/**/*.js', {
nodir: true
});
_.each(classFiles, async function (file) {
console.log(file);
var TheClass = import(file);
var instance = new TheClass();
//and then do whatever I want with that new instance
});
}
I've tried doing it with require but I get errors. Apparently the constructor cant be found.
Any idea would be greatly appreciated.
Thanks
ES module definitions are declarative, and the current direction tools are taking is the path where dependencies are determined during parse (via static analysis), waaay before any of the code is executed. This means dynamic and conditional imports go against the said path. It's not like in Node where imports are determined on execution, upon executing require.
If you want dynamic, runtime imports, consider taking a look at SystemJS. If you're familiar with RequireJS, it takes the same concept, but expands it to multiple module formats, including ES6. It has SystemJS.import which appears to do what you want, plus handles the path resolution that you're currently doing.
Alternatively, if your intention is to shed off excess code, consider using Rollup. It will analyze code for you and only include code that's actually used. That way, you don't need to manually do conditional loading.
You need to preprocess with babel, because they are not yet a part of node (for that matter, neither are static imports - node uses require).
https://github.com/airbnb/babel-plugin-dynamic-import-node
steps:
pre
npm i -D babel-cli or npm i -D babel
1
npm i -D babel-plugin-dynamic-import-node
2
.babelrc
{
"plugins": ["dynamic-import-node"]
}
ready, go!
babel-node test_import.js for babel-cli, or for raw babel:
a
(edit) package.json
"scripts": {
"pretest": "babel test_imports.js -o dist/test_imports.js",
"test": "node dist/test_imports.js"
//...
b
node test
I had the same usecase and i managed to dynamically load and instantiate default exported classes using:
const c = import("theClass.js")
const i = new c.default();
using node v16.4.0

Import existing AMD module into ES6 module

I have an existing application where I have AMD modules defined using RequireJS. I use "text" and "i18n" plugins for requirejs extensively in my project.
I have been experimenting with ES6 modules lately and would like to use them while creating new modules in my application. However, I want to reuse the existing AMD modules and import them while defining my ES6 modules.
Is this even possible? I know Traceur and Babel can create AMD modules from ES6 modules, but that only works for new modules with no dependency on existing AMD modules, but I could not find an example of reusing the existing AMD modules.
Any help will be appreciated. This is a blocker for me right now to start using all ES6 goodies.
Thanks
Yes, it can be done. Create a new application with the following structure:
gulpfile.js
index.html
js/foo.js
js/main.es6
node_modules
Install gulp and gulp-babel. (I prefer to install gulp locally but you may want it globally: that's up to you.)
index.html:
<!DOCTYPE html>
<html>
<head>
<title>Something</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.20/require.js"></script>
<script>
require.config({
baseUrl: "js",
deps: ["main"]
});
</script>
</head>
<body>
</body>
</html>
gulpfile.js:
"use strict";
var gulp = require('gulp');
var babel = require('gulp-babel');
gulp.task("copy", function () {
return gulp.src(["./js/**/*.js", "./index.html"], { base: '.' })
.pipe(gulp.dest("build"));
});
gulp.task("compile-es6", function () {
return gulp.src("js/**/*.es6")
.pipe(babel({"modules": "amd"}))
.pipe(gulp.dest("build/js"));
});
gulp.task("default", ["copy", "compile-es6"]);
js/foo.js:
define(function () {
return {
"foo": "the value of the foo field on module foo."
};
});
js/main.es6:
import foo from "foo";
console.log("in main: ", foo.foo);
After you've run gulp to build the application, open the file build/index.html in your browser. You'll see on the console:
in main: the value of the foo field on module foo.
The ES6 module main was able to load the AMD module foo and use the exported value. It would also be possible to have a native-AMD module load an ES6 module that has been converted to AMD. Once Babel has done its work, they are all AMD modules as far as an AMD loader is concerned.
In addition to #Louis's answer, assuming you already have a bunch of third party libraries specified in require.js configuration, in your new ES6 modules, whenever you are importing a module, be it amd or es6, you'll be fine as long as you keep the imported module name consistent. For example:
Here is the gulpfile:
gulp.task("es6", function () {
return gulp.src("modules/newFolder//es6/*.js")
.pipe(babel({
"presets": ["es2015"],
"plugins": ["transform-es2015-modules-amd"]
// don't forget to install this plugin
}))
.pipe(gulp.dest("modules/newFolder/build"));
});
Here is the es6 file:
import d3 from 'd3';
import myFunc from 'modules/newFolder/es6module'
// ...
This will be compiled to sth like this:
define(['d3', 'modules/newFolder/es6module'], function (_d, _myFunc) {
'use strict';
// ...
});
as long as the module in define(['d3', 'modules/newFolder/es6module'], ... of the compiled file is fine in a original AMD file, it should work with under existing require.js setup, such as compress files etc.
In terms of #coderC's question about require.js loaders, I was using i18n!nls/lang in AMD modules, at first I thought it would be a really tricky thing to find an alternative of AMD plugin loaders in ES6 modules, and I switched to other localization tools such as i18next. But it turned out that it's okay to do this:
import lang from 'i18n!nls/lang';
// import other modules..
because it will be compiled by gulp task to sth like:
define(['d3', 'i18n!nls/lang'], function (_d, _lang) {
// ....
This way, we don't have to worry about the require.js loader.
In a nutshell, in ES6 modules, if you want to use existing AMD plugin/modules, you just need to ensure the compiled file is conformed with the existing setup. Additionally, you can also try the ES6 module bundler Rollup to bundle all the new ES6 files.
Hope this can be helpful for those who are trying to integrate ES6 syntax in project.
A few changes for the latest version of babel:
First, babel({"modules": "amd"}) doesn't work with the latest version of babel. Instead, use babel({"plugins": ["#babel/plugin-transform-modules-amd"]}). (You'll need to install that plugin as a separate module in npm, i.e. with npm install --save-dev #babel/plugin-transform-modules-amd.)
Second, the syntax for gulp.task no longer accepts arrays as its second argument. Instead, use gulp.parallel or gulp.series to create a compound task.
Your gulpfile will end up looking like this:
"use strict";
var gulp = require('gulp');
var babel = require('gulp-babel');
gulp.task("copy", function () {
return gulp.src(["./js/**/*.js", "./index.html"], { base: '.' })
.pipe(gulp.dest("build"));
});
gulp.task("compile-es6", function () {
return gulp.src("js/**/*.es6")
.pipe(babel({"plugins": ["#babel/plugin-transform-modules-amd"]}))
.pipe(gulp.dest("build/js"));
});
gulp.task("default", gulp.parallel("copy", "compile-es6"));

Global require with Browserify v2

I want to use Browserify to bundle my files, but I then need to require one of the modules inside of my Browserify bundled bundle.js on the HTML page itself. This is currently not possible because there is no require function defined on the page.
It appears that the require function defined by browserify in bundle.js is inside of an IIFE, so I can't use that. Is it possible to throw this one out in place of a global require?
<script src="bundle.js"></script>
<script>
// Require the `app` module inside of `bundle.js`
var app = require('app');
app.start();
</script>
I need to do this because my app.start function requires some JSON is passed to it which can only be rendered by the server-side template.
N.B. I am using Browserify v2.
You can use -r to expose a global require() function for the files you specify:
x.js:
module.exports = function (n) { return n * 111 }
Console
$ browserify -r ./x.js > bundle.js
then in your html:
<script src="bundle.js"></script>
<script>
var x = require('./x.js');
console.log(x(3))
</script>
will print 333.
In your case, just do browserify -r app to expose require('app') to the external context.

Categories