A koa and express middleware for browser side javascript module authoring.


2933.2.62 days ago4 years agoMinified + gzip package size for @cara/porter in KB



NPM Downloads NPM Version Build Status

Porter is a consolidated browser module solution which provides a module system for web browsers that is both CommonJS and ES Modules compatible.

Here are the features that make Porter different from (if not better than) other module solutions:

  1. Both synchronous and asynchronous module loading are supported. import is transformed with either Babel or TypeScript. import() is not fully supported yet but there's an equivalent require.async(specifier, mod => {}) provided.
  2. Implemented with the concept Module (file) and Package (directory with package.json and files) built-in.
  3. Fast enough module resolution and transpilation that makes the watch => bundle loop unnecessary. With Porter the middleware, .css and .js requests are intercepted (and processed if changed) correspondingly.


This document is mainly about Porter the middleware. To learn about Porter CLI, please visit the corresponding folder.

Porter the middleware is compatible with Koa (both major versions) and Express:

const Koa = require('koa')
const Porter = require('@cara/porter')

const app = new Koa()
const porter = new Porter()

// express


With the default setup, browser modules at ./components folder is now accessible with /path/to/file.js or /${pkg.name}/${pkg.version}/path/to/file.js. Take demo-cli for example, the file structure shall resemble that of below:

➜  demo-cli git:(master) tree -L 2
├── components        # browser modules
│   ├── app.css
│   └── app.js
├── node_modules      # dependencies
│   ├── @cara
│   ├── jquery
│   └── prismjs
├── package.json
└── public
    └── index.html    # homepage

In ./public/index.html, we can now add CSS and JavaScript entries:

<!DOCTYPE html>
  <meta charset="utf-8">
  <title>An Porter Demo</title>
  <!-- CSS entry -->
  <link rel="stylesheet" type="text/css" href="/app.css">
  <h1>A Porter Demo</h1>
  <!-- JavaScript entry -->
  <script src="/app.js?main"></script>

The extra ?main querystring might seem a bit confusing at first glance. It tells Porter the middleware to bundle loader when /app.js?main is accessed. The equivalent <script> entry of above is:

<script src="/loader.js" data-main="app.js"></script>

Both <script>s work as the JavaScript entry of current page. In ./components/app.js, there are the good old require and exports:

// es5
'use strict'

const jQuery = require('jquery')  // => ./node_modules/jquery/dist/jquery.js
const React = require('react')    // => ./node_modules/react/index.js
const util = require('./util')    // => ./components/util.js or ./components/util/index.js

and the fancy new import and export:

import jQuery from 'jquery'
import * as React from 'react'
import util from './util'

In CSS entry, there's @import:

@import "prismjs/themes/prism.css";
@import "./base.css";


cache={ dest }

To accelerate responses, Porter caches following things:

  • CSS post-process results (code and source map) after @imports being processed.
  • JS transpile results (code and source map) if Babel or TypeScript is enabled.

By default, these files are stored in the folder specified by dest='public'. At some circumstances, we may need to put cache files into a different folder, therefore here is the extra cache={ dest }:

const porter = new Porter({
  cache: { dest: '.porter-cache' },   // where the cache file goes to
  dest: 'public'                      // where the compiled assets will be at after porter.compileAll()

If cache={ dest } is undefined, cache files are put into dest='public' as well.

It is recommended that the directory that contain cache files shall be served statically, which makes the source maps accessible. Here is an example in Koa:

const serve = require('koa-static')



The directory that contains compile results, and cache files as well if cache={ dest } is undefined.


The directory or directories that contain browser modules. For example, if we need to import modules from both the ./components directory and ./node_modules/@corp/shared-components:

const porter = new Porter({
  paths: [ 'components', 'node_modules/@corp/shared-components']


This option should never be necessary. Options like paths and dest are all resolved against root, which defaults to process.cwd(). If the project root is different than process.cwd(), try set root.

source={ serve, root }

Like cache={}, the source={} option is an object, which contains two properties that are all related to source maps. In development phase, the source maps are generated while Porter processes requests. In those source map files, sourceContents are stripped, and sourceRoot are set to /. Therefore to make source mapping actually take place in devtools, we need to enable source={ serve } during development as well:

const porter = new Porter({
  source: { serve: process.env.NODE_ENV == 'development' }

Regarding source={ root }, it is not used until the project goes into production. When the JavaScript and CSS codes are compiled (with porter.compileAll() or so), source maps get generated with sourceContents stripped, and sourceRoot can not be / in production because source={ serve } should be off in production. Therefore, source={ root } shall be set to the actual origin that is able to serve the source securely.

In our practice, the source={ root } is usually set to http://localhost:3000.

transpile={ only }

By default, Porter checks transpiler configs of the project (that is, the root package) only. When it comes to dependencies in ES6+ (which shouldn't be common in npm land), transpile logic shall be activated for them as well. To specify these packages that need to be transpiled as well, use transpile={ only }:

const porter = new Porter({
  transpile: { only: ['some-es6-module'] }

If the module being loaded is listed in transformOnly, and a .babelrc within the module directory is found, porter will process the module source with babel too, like the way it handles components. Don't forget to install the presets and plugins listed in the module's .babelrc .


It is possible (and also recommended) to disable Porter in production, as long as the assets are compiled with porter.compileAll(). To compile assets of the project, simply call porter.compileAll({ entries }):

const porter = new Porter()

  entries: ['app.js', 'app.css']
  .then(() => console.log('done')
  .catch(err => console.error(err.stack))

Porter will compile entries and their dependencies, bundle them together afterwards. How the modules are bundled is a simple yet complicated question. Here's the default bundling strategy:

  • Entries are bundled separately, e.g. entries: ['app.js', 'app2.js'] are compiled into two different bundles.
  • Dependencies are bundled per package with internal modules put together, e.g. jQuery gets compiled as jquery/3.3.1/dist/jquery.js.
  • Dependencies with multiple entries gets bundled per package as well, e.g. lodash methods will be compiled as lodash/4.17.10/~bundle-36bdcd6d.js.

Assume the root package is:

  "name": "@cara/demo-cli",
  "version": "2.0.0"

and the content of ./components/app.js is:

'use strict'

const $ = require('jquery')
const throttle = require('lodash/throttle')
const camelize = require('lodash/camelize')
const util = require('./util')

// code

After porter.compileAll({ entries: ['app.js'] }), the files in ./public should be:

├── @cara
│   └── demo-app
│       └── 2.0.0-3
|           ├── app.js
|           └── app.js.map
├── jquery
│   └── 3.3.1
│       └── dist
|           ├── jquery.js
|           └── jquery.js.map
└── lodash
    └── 4.17.10
        ├── ~bundle.js
        └── ~bundle.js.map

For different kinds of projects, different strategies shall be employed. We can tell Porter to bundle dependencies at certain scope with porter.compileEntry():

// default
porter.compileEntry('app.js', { package: true })

// bundle everything
porter.compileEntry('app.js', { all: true })

Behind the Scene

Let's start with app.js, which might seem a bit confusing at the first glance. It is added to the page directly:

<script src="/app.js?main"></script>

And suddenly you can write app.js as Node.js Modules or ES Modules right away:

import mobx from 'mobx'
const React = require('react')

How can browser know where to import MobX or require React when executing app.js?


The secret is, entries that has main in the querystring (e.g. app.js?main) will be prepended with two things before the the actual app.js when it's served with Porter:

  1. Loader
  2. Package lock

You can import app.js explicitly if you prefer:

<script src="/loader.js"></script>
<!-- or with shortcut -->
<script src="/loader.js" data-main="app"></script>

Both way works. To make app.js consumable by the Loader, it will be wrapped into Common Module Declaration format on the fly:

define(id, deps, function(require, exports, module) {
  // actual main.js content
  • id is deducted from the file path.
  • dependencies is parsed from the factory code with js-tokens.
  • factory (the anonymouse function) body is left untouched or transformed with babel depending on whether .babelrc exists or not.

If ES Module is preferred, you'll need two things:

  1. Put a .babelrc file under your components directory.
  2. Install the presets or plugins configured in said .babelrc.

Back to the Loader, after the wrapped app.js is fetched, it won't execute right away. The dependencies need to be resolved first. For relative dependencies (e.g. dependencies within the same package), it's easy to just resolve them against module.id. For external dependencies (in this case, react and mobx), node_modules are looked.

The parsed dependencies is in two trees, one for modules (file by file), one for packages (folder by folder). When the entry module (e.g. app.js) is accessed, a package lock is generated and prepended before the module to make sure the correct module path is used.

Take heredoc's (simplified) node_modules for example:

➜  heredoc git:(master) ✗ tree node_modules -I "mocha|standard"
└── should
    ├── index.js
    ├── node_modules
    │   └── should-type
    │       ├── index.js
    │       └── package.json
    └── package.json

It will be flattened into:

  "should": {
    "6.0.3": {
      "main": "./lib/should.js",
      "dependencies": {
        "should-type": "0.0.4"
  "should-type": {
    "0.0.4": {}

Loader Config

Besides package lock, there're several basic loader settings (which are all configurable while new Porter()):

property description
baseUrl root path of the browser modules, e.g. https://staticfile.org/
map module mappings that may interfere module resolution
package metadata of the root package, e.g. { name, version, main, entries }
preload a syntax sugar for quick loading certain files before entry

In development phase, Porter configs the loader with following settings:

  baseUrl: '/',
  package: { /* generated from package.json of the project */ }

Wrap It Up

So here is app.js?main expanded:

// GET /loader.js returns both Loader and Loader Config.
;(function() { /* Loader */ })
Object.assign(porter.lock, /* package lock */)

// The module definition and the import kick off.
define(id, dependencies, function(require, exports, module) { /* app.js */ })

Here's the actual interaction between browser and Porter:


The stylesheets part is much easier since Porter processes CSS @imports at the first place. Take following app.css for example:

@import "cropper/dist/cropper.css";
@import "common.css"

body {
  padding: 50px;

When browser requests app.css:

  1. postcss-import processes all of the @imports;
  2. autoprefixer transforms the bundle;

Porter then responses with the processed CSS (which has all @imports replaced with actual file contents).

If you find any bugs or have a feature request, please open an issue on github!

The npm package download data comes from npm's download counts api and package details come from npms.io.