Context
I am creating a library with 2 ways of initialization:
Automatic - I download some stuff for you asynchronously, then initialize.
Manual - You already downloaded the stuff before, then I initialize the library immediately (sync).
I have successfully implemented tree-shakable libraries in the past. What we would normally do, is separate the code into two modules, and let the app developer choose which one to import, thus allowing tree-shaking the other part. Similarly to this:
import { LibraryAsyncModule } from 'my-library'; // 🔁 Or LibrarySyncModule
#NgModule({
imports: [LibraryAsyncModule] // 🔁 Or LibrarySyncModule
})
export class AppModule { }
What I want to accomplish ✔
To reduce the learning curve of using my library, I'm trying to design is a single imported module which includes the relevant module and allows tree shaking the other. The following diagram shows the desired structure.
What I want to avoid 🚫
I could create factory providers that will detect the config passed to forRoot() and load the corresponding module at runtime. However, importing the modules at runtime turns initialization to async, and will also prevent angular from bundling the used module with the app.
I need a build time solution. My prototypes show that simply including both sync and async modules in the core module result in both being bundled.
How would that single module look like? Any ideas / suggestions? 🤔
Your approach should work, but you will need to enable it.
Since Tree-shaking works only with es6 modules you need to publish your lib with it & specify module property in your package.json.
// package.json
{
...
"main": "dist/index.js",
"module": "dist/es/index.js", // <- this file should contain es modules not commonjs
...
}
This setup will tell bundlers (webpack in your case) to use the es modules export and allow them to enable tree-shaking feature.
I recommend for package devs to use tsdx cli tool which does this automatically and supports many other features such as TypeScript support etc.
npx tsdx create mylib
Related
I'm developing a module that doesn't have a build that the user imports. Instead, he imports individual components and then bundles them along with his code. However, those components share utilities and I want to import them without going through relative path hell.
I know that's a pretty common question and I did some research. Suppose I have module/components/foo/bar/baz/index.js that wants to import module/utils/helper.js
Option 1
Just use relative paths and do:
import helper from '../../../../utils/helper'
Option 2
Use the module-alias package and have:
import helper from '#utils/helper'
This would work in Node.js because modules are resolved at runtime. However, let's say the module user has Webpack and imports the module:
import component from 'module/components/foo/bar/baz'
Webpack wouldn't be able to resolve #utils unless the user specifies that alias in his own Webpack configuration. That would be pretty annoying.
Option 3
Use Webpack aliases in webpack.config.js:
module.exports = {
resolve: {
alias: {
'#utils': path.join(__dirname, 'utils')
}
}
}
This would work fine if the module was pre-bundled. But as I've previously mentioned, I want the library to be usable with ES6 imports so that users can bundle only the parts they need.
Option 4
I could use the module name in the module's own source code:
import helper from 'module/utils/helper'
This appears to solve the problem, but I think it's a pretty bad solution. For development, you'd have to create a symlink node_modules/module -> module. I'm sure this hides many potential issues and collaborators would have to manually do it as well.
Is there a way to avoid relative paths while allowing the library to be used with ES6 imports?
Is it possible to configure ESLint in WebStorm so functions, variables, etc. are parsed also from files in the same folder? In my build process, I concatenate all files in the same folders into big closures, for example:
src/
main/ ===> "main.js"
api.js
init.js
ui.js
constants.js
.
.
renderer/ ===> "renderer.js"
core.js
events.js
I would like ESLint to treat all those files just like one, so I don't get "undef" errors for things that are defined.
If it can't be done automatically, I wouldn't mind to create a manual configuration specifying all those files if that is possible.
EDIT: Why I don't (can't) use modules? TLDR- legacy code and project requirements.
I need to minify all code. Current closure compiler can transpile ES6 into ES5, but I found some ES6 features very prone to produce broken code. So I am forced to use ES5.
As I need ES5. I would only be able to use require() to use modules. Now that's a problem, as require() is a dynamic include and it impacts performance on my context (big electron app for modest power devices)
So to answer #Avin_Kavish, I agree what I do is "technically non conforming", but at the end of the build process it is, because each folder has been grouped into a file. That file is the module or the script. To group the files I use a Gradle plugin https://github.com/eriwen/gradle-js-plugin, I inject a "closure header" and a "closure footer", and all the files in between in the order I want.
Despite the inconvenience, at the end I get super-compact nodeJS code, with all methods obfuscated, etc.
I ended up using #Patrick suggestion, thanks for that!
EDIT 2
WebPack + Electron-WebPack turned out to be what I was looking for.
BTW- I think the proper way to do this is if EsLint would allow a "folder" sourceType.
You didn't provide code examples in your question, but I assume you do something like this:
api.js
const api = {
fetchData() {
// some code that fetches data
}
};
core.js
const core = {
init() {
api.fetchData();
}
};
The ESLint rule that causes errors when you lint these JavaScript modules is the no-undef rule.
It checks for variables that are used without having been defined. In the code example core.js above, this would be api, because that is defined in another module, which ESLint doesn't know about.
You don't care about these errors, because in your actual JS bundle used in production, the code from api.js and core.js is concatenated in one bundle, so api will be defined.
So actually api in this example is a global variable.
The no-undef rule allows you to define global variables so that they won't cause errors.
There are two ways to do this:
Using Comments
At the beginning of your core.js module, add this line:
/* global api */
Using the ESLint Config
As explained here – add this to your .eslintrc file:
{
"globals": {
"api": "writable"
}
}
Side Note
As some commenters to your question pointed out, it would probably be better to use import and export statements in the modules, together with a module bundling tool like webpack to create one bundle from your JavaScript modules.
A physical JavaScript file with an import/export statement is a module by the standard. A single .js file without import/export is a script by the standard. What you are trying to do is non-conforming to this, there is no specification in ECMAScript that allows splitting a single script or module across several files. I do get where you are coming from, for example: C# has partial classes that allows you to split a class across multiple files. But trying to replicate this without a standard syntax is not wise. Especially, when import/export can and will do the job for you
For example, with the following assumptions, your main.js can be refactored to,
constants.js // <--- constants
ui.js // <--- logic to build UI
api.js // <--- exposing public api
init.js // <--- setup code before use
// main.js
// If you name this index.js you can import it as 'src/main' instead of 'src/main/main.js'
import { A,B } from './constants'
import { api } from './api'
import { displayUi } from './ui'
import { init } from './init'
init(A);
displayUi(B);
export { api } // <-- re-expose public api
I'm trying to include bitcore-lib partially into my webpage using tree-shaking that rollup provides out of the box and rollup-plugin-commonjs to load Node.js module.
To better illustrate the problem I make a demo project that available on the github
You can have a look at bundle.js. If I define a module in the following way:
const useful = "3";
const useless = "4";
export {usefull, useless}
Tree shaking works correctly - the final bundle includes only useful dependency.
But if I define a module in the way it defined in bitcore-lib (node-lib.js) in demo project:
module.exports = {
useful: "1",
useless: "2"
};
In that case, the final bundle includes the whole module.
I've expected that useless: 2 dependency shouldn't be included because of tree-shaking. My index.js is here:
import {usefull as usefull1} from "./my-node-lib"
import {usefull as usefull2} from "./my-es-lib"
console.log(`hi! ${usefull1} ${usefull2}`);
My rollup.config.js is available here
Is it a problem of module definition or rollup config?
Tree shaking works only for ES6 modules. At least it's true for Webpack and I suppose for rollup as well. Your first definition is ES6, second is commonjs.
Therefore if a library is not compiled/transpiled to ES6 modules tree shaking will not work.
Another feature which will not work is module concatenation.
Depending on the library you can try to recompile it.
I'm writing an application that uses Angular2 with Typescript as frontend, and NodeJS as backend. I've written a javascript object I wish to share between the frontend and backend. What's the most elegant way to do this?
My initial idea was to write a .d.ts for the frontend, and add a module.exports in the javascript file, so the backend could require('myobject').
While this works, this causes the browser to throw and exception that shows up in the browser console: 'Uncaught ReferenceError: module is not defined'.
And I'd like to not pollute my console with needless error messages. So is there another, more elegant, way of doing this?
The "cleanest" way I know to do this is to write modular script on both ends and create a library of objects you want to share (so that the shared objects are defined in a single location)
Set-up
Frontend: Typescript with either
target ES6, module: commonjs + Babel (requires Typescript 1.7)
or target ES5, module: commonjs
bundled using webpack or browserify
Backend: ES6 (with --harmony flag on node) or ES5
Library
Create a library, say shared, written in Typescript and create the exported Object class
export default class MyObject{ ... }
Make sure the library is compiled with declaration: true (in tsconfig for instance): tsc will generate the js + the typings (declarations).
In the package.json of the shared library, make sure the entry typings is set to point to the generated MyObject.d.ts file. If there are multiple objects; create an index.ts file that re-exports all the objects and point typings to index.d.ts
Usage
Frontend: since you are now using modular JS/TS, import your object directly from Typescript
import MyObject from 'shared'
The typescript transpiler will automatically find the .d.ts definition from the typings entry of shared's package.json.
Backend: simply require('shared')
Note: if there are multiple shared objects, do not use default exports in shared: they cannot be re-exported.
There are many ways to format JavaScript modules: AMD, CommonJS, UMD, ES6, global script. I've seen projects that structure their source code in whatever way they want and run a build process to generate a dist directory containing code in all the above formats. This has the advantage that the user of the code can just pick whichever format is most applicable to his environment.
This method works fine as long as the module has no dependencies on other modules. In the case where the modules must import other modules, there are implied complications. For example RequireJS uses a config file that looks like:
requirejs.config({
paths: {
'jquery': 'js/lib/jquery',
'ember': 'js/lib/ember',
'handlebars': 'js/lib/handlebars',
'underscore': 'js/lib/underscore'
}
});
Other loaders have equivalent mechanisms for mapping import paths.
If jQuery is a dependency, should the module import it from the path 'jquery'? What if the system in which it is being incorporated stores jQuery at the path 'libs/jquery'? In this case, is it the responsibility of the author of the system incorporating jQuery to provide aliases in the configuration of the import path?
This questioning strongly suggests that a truly reusable module must provide code formatted in all module formats as well as document clearly upon what libraries (and versions thereof) it depends and document what import paths at which those libraries are assumed to exist.
For example I could author a fancy jQuery plugin that I distribute in AMD, CommonJS, ES6, and global variations. I would document that this plugin depends on jQuery version 2.0 imported through the path 'jquery_on_a_path_that_confuses_you'. The would-be user of this plugin must copy the plugin into his project and then configure his module loader or build tool to export jQuery at the path 'jquery_on_a_path_that_confuses_you'.
As far as I can tell:
There is no standard for what to use for import paths.
There is no standard way to express the dependency, version, and import path requirements to the user of a piece of code.
There is no standard remedy to deal with clashing import paths or load multiple versions of a library.
Does there exist any plan to deal with this strange arrangement? To me it seems a little crazy to have module systems that don't know how to name their modules. Am I wrong?
You may want to check jspm.io + SystemJS which is a relatively new package manager and universal module loader which is increasing in popularity.
Please find below some presentations and article on the subject I found useful:
https://www.youtube.com/watch?v=MXzQP38mdnE,
https://vimeo.com/65042246,
https://www.youtube.com/watch?v=szJjsduHBQQ,
http://javascriptplayground.com/blog/2014/11/js-modules-jspm-systemjs/
Late with the answer, but if you're after writing plain JS code (without jQuery or other frameworks), I've found that there's the deploader.js repo, which you can use to wrap any kind of JS into modules and do dependency loading.
May worth checking out.