From Webpack to JSPM (0.17)

JSPM is not as mature as webpack, and its ecosystem is not as rich. But it has some cool features, such as being able to load a module from a remote url, which is not possible with webpack, even webpack 2 — at least yet.

This can be useful when you have scenarios where you need to load an arbitrary module, without bundle it inside your app first, while playing nice with dependencies.

If you come from webpack, it can have some pain points that I hit, so I thought I could share these here.

Installation

The installation instructions can be found here.

Basically :

npm install --save-dev jspm@beta

The docs tells to install it both globally and locally, and then do as if you used a global install. So instead of doing :

jspm init .

I added an npm script :

"scripts": {
"jspm": "jspm"
}

You can then call jspm like this, e.g. to init :

npm run jspm init .

I decided to init a new project and report modules by hand to avoid to much interrogations in the process.

After following the interactive init, a jspm.config.js and a package.json file are created.

Modules and registries

To install a module, you either install a module from jspm registry like this :

npm run jspm install jquery

Or add the registry before the package name if you want to use npm or github :

npm run jspm install npm:react-rte
npm run jspm install github:owner/repo

One cool thing I noticed is that it seems to install peer dependencies automatically.

JSPM uses a “jspm” entry in package.json to list dependencies, options etc. It’s like you would do in package.json itself, plus some specific options.

So if you have in your package.json :

"dependencies": {
"left-pad": "1.1.3"
}

It will become :

"jspm": {
"dependencies": {
"left-pad": "npm:left-pad@^1.1.3"
}
}

Github Private repositories

To be able to use github private repos, you need to fill in your credentials :

npm run jspm registry config github

React and JSX

To install react :

npm run jspm install npm:react npm:react-dom

For JSX, follow the instructions here : http://jspm.io/0.17-beta-guide/installing-the-jsx-babel-plugin.html

Development

The docs tells you how to make a dev bundle, but nothing about running a dev server. After digging, I found https://github.com/geelen/jspm-server, which once installed lets you start a dev server :

npm install --save-dev jspm-server

Update package.json :

"scripts": {
"start": "jspm-server"
...
}

Then :

npm start

Resolving

With webpack, by default you can require like this :

import myModule from ‘./myModule’;

It will resolve to “./myModule.js” or “./myModule/index.js”.

2 things here :

  • You can omit the file extension.
  • Files named “index.js” will be aliased when requiring the containing folder path.

You can omit the .js extension using the “defaultExtension” option in config :

System.config({
defaultExtension: 'js',
...
});

Regarding the “index.js” alias, as far as I know, this is not possible with jspm, or at least not easily.

In the end, I resigned to rewrite all imports by hand.

Project relative modules paths

Another jspm specificity is that it encourages you to create some namespace for your local modules, as you would do using “alias” in webpack.

When you init the project, it asks you for a project name, then it adds an entry for this name as a package :

packages: {
'app': {
'main': 'app.js',
'meta': {
...
}
}
}

You can then require modules in your project from this name instead of using relative paths. So instead of doing :

import myOtherModule from '../../myOtherModule';

You can require them like this :

import myOtherModule from 'app/myOtherModule';

CSS modules

The official (? at least the one accessible through npm) css-modules loader is https://github.com/geelen/jspm-loader-css-modules.

It does not scope the css by default, but is supposed to be customizable.

Unfortunately, it does not work, at least with jspm 0.17. I had issues with autoprefixer, which is used by the module.

After exploring issues, trying some forks, I finally found this loader, which is scoped by default and has the missing fixes : https://github.com/MeoMix/jspm-loader-css

Its doc is up to date and less vague.

In package.json :

{
"jspm": {
"dependencies": {
"jspm-loader-css": "github:MeoMix/jspm-loader-css@master",
...
}
}
}

Then :

npm run jspm install

Then update jspm.config.js :

packages: {
app: {
meta: {
"*.css": {
"loader": "jspm-loader-css"
}
...
}
}
}

Relative images paths in CSS files

Relative images paths in CSS files won’t work, and since JSPM does everything client-side, it is not easy.

jspm-loader-css allows to customize its config by adding a css.js file in the root of your project, like this :

import Plugins from 'jspm-loader-css/src/plugins.js'
import Loader from 'jspm-loader-css/src/loader.js'

const plugins = [
Plugins.values,
Plugins.localByDefault,
Plugins.extractImports,
Plugins.scope
];

const { fetch, bundle } = new Loader(plugins);
export { fetch, bundle };

And changing jspm.config.js :

SystemJS.config({
...
meta: {
"*.css": {
"loader": "css.js"
}
},
...
});

To base64 encode images that have a relative path like this :

.handle {
background: url("./images/handle.svg");
}

I found a postcss plugin, postcss-url, that allows to url encode images… in theory.

Since postcss is most often run in a node environment, this won’t work with jspm out of the box.

So we need to do the same, but on the client side. There is an option that allows giving a function that takes arguments like the file path and should return a string that will replace the original path. The bad news is that this function is synchronous, and guess what ? When using XHR, if you use it synchronously, you can’t set the content-type :

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', false); // 3rd arg means sync
xhr.responseType = 'blob'; // This won't work !

After some digging, I found a way to keep the synchronous XHR while being able to base64 encode images, thanks to this great SO answer. There is probably a better way, but manipulating buffers is not my thing :

import Plugins from 'jspm-loader-css/src/plugins.js';
import Loader from 'jspm-loader-css/src/loader.js';
import autoprefixer from 'autoprefixer';
import url from 'postcss-url';
import mime from 'mime';
import path from 'path';
// Since we are in the browser context, we need to do these to base64 encode images
// http://stackoverflow.com/a/7372816/302731
function getBinary(file){
var xhr = new XMLHttpRequest();
xhr.open("GET", file, false);
xhr.overrideMimeType("text/plain; charset=x-user-defined");
xhr.send(null);
return xhr.responseText;
}
function base64Encode(str) {
var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var out = "", i = 0, len = str.length, c1, c2, c3;
while (i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if (i == len) {
out += CHARS.charAt(c1 >> 2);
out += CHARS.charAt((c1 & 0x3) << 4);
out += "==";
break;
}
c2 = str.charCodeAt(i++);
if (i == len) {
out += CHARS.charAt(c1 >> 2);
out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
out += CHARS.charAt((c2 & 0xF) << 2);
out += "=";
break;
}
c3 = str.charCodeAt(i++);
out += CHARS.charAt(c1 >> 2);
out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
out += CHARS.charAt(c3 & 0x3F);
}
return out;
}
const plugins = [
Plugins.values,
Plugins.localByDefault,
Plugins.extractImports,
Plugins.scope,
autoprefixer(),
url({
url: function(URL, decl, from, dirname) {
const filePath = path.resolve(dirname, URL);
const mimeType = mime.lookup(filePath);
const data = base64Encode(getBinary(filePath));
return "data:" + mimeType + ";base64," + data;
}
})
];
const { fetch, bundle } = new Loader(plugins);
export { fetch, bundle };

I may post a new article as I discover more things and meet more advanced topics such as hot reload, production bundling…

Like what you read? Give Julien De Luca a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.