Delivering untranspiled source code via npm

[2017-06-01] dev, javascript, esnext, npm, jsmodules, babel, webpack
(Ad, please don’t block)

This post is part of a series of three:

  1. Current approaches: “Setting up multi-platform npm packages
  2. Motivating a new approach: “Transpiling dependencies with Babel
  3. Implementing the new approach: “Delivering untranspiled source code via npm”

The idea of babel-preset-env is brilliant: write JavaScript with stage 4 features (or earlier stages, if you want to take that risk) and transpile it so that it is an exact fit for your target platform(s).

However, at the moment, preset-env only works for your own app, but not for your dependencies, which are normally already transpiled.

This blog post shows how package authors and package users can use the package.json property esnext to work with untranspiled source code in npm packages. The code is available in the repository esnext-demo on GitHub.

What about module and es2015? I wish there were no need for a new property, but, as I explain elsewhere, module and es2015 do not work for this use case:

  • module is restricted to what is currently supported by Node.js.
  • es2015 is only for untranspiled ES6 code (and not ES2016+ code).

In other words: both properties are close to what we want, but if we want to comply completely with their specifications then we can’t use them.

Package authors  

If your npm package has untranspiled source code, you point to it via esnext:

{
    "esnext": "./index-esnext.js",
    "main": "./index.js"
}

index.js is usually transpiled from index-esnext.js. E.g., via babel-preset-env and targets.node === 'current'.

Package users  

As a package user, you can work with esnext by adapting webpack.config.js in two locations.

First, you need to make sure that webpack finds source code pointed to via esnext:

const PROPKEY_ESNEXT = 'esnext';

module.exports = {
    resolve: {
        // Help webpack find `PROPKEY_ESNEXT` source code
        mainFields: [PROPKEY_ESNEXT, 'browser', 'module', 'main'],
    },
    ···
};

Second, you need to tell Babel that it should transpile all of the app code, but only those .js files in dependencies whose package.json files have the property esnext.

···

const dir_js = path.resolve(__dirname, 'js');
const dir_node_modules = path.resolve(__dirname, 'node_modules');

module.exports = {
    ···
    module: {
        rules: [
            {
                test: /\.m?js$/,

                // Should Babel transpile the file at `filepath`?
                include: (filepath) => {
                    return pathIsInside(filepath, dir_js) ||
                        (pathIsInside(filepath, dir_node_modules) &&
                        hasPkgEsnext(filepath));
                },
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                'env'
                            ],
                        },
                    }
                ]
            }
        ]
    },
};

/**
 * Find package.json for file at `filepath`.
 * Return `true` if it has a property whose key is `PROPKEY_ESNEXT`.
 */
function hasPkgEsnext(filepath) {
    const pkgRoot = findRoot(filepath);
    const packageJsonPath = path.resolve(pkgRoot, 'package.json');
    const packageJsonText = fs.readFileSync(packageJsonPath,
        {encoding: 'utf-8'});
    const packageJson = JSON.parse(packageJsonText);
    return {}.hasOwnProperty.call(packageJson, PROPKEY_ESNEXT); // (A)
}

A preliminary solution  

If you are a consumer of npm packages, you can adopt a preliminary solution until your dependencies support esnext (or another solution becomes popular): transpile module in the same manner. For example, by changing line A in the previous code to:

return {}.hasOwnProperty.call(packageJson, PROPKEY_ESNEXT)
    || {}.hasOwnProperty.call(packageJson, 'module');

Due to module pointing to an ES module, it is safe to transpile. Transpiling CommonJS modules is more tricky: strict mode and possibly other transpilation hazards can break things.

Further reading