Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Possibility about conditional export ES6 module based on process.env.NODE_ENV?

I wrote a utility library and I want to tree-shaking them when my user publishes their app.

In Webpack v4, you need to make your module ES6 to support tree-shaking, but I also want to split my development build and my production build into different files.

What I want is exactly like react's NPM module:

// index.js
'use strict';

if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

This leads me questions.

If I make my utility modules all commonjs, I will never get tree-shaking, my app gets so huge.

If I make my utility modules all ES6 static export, I will have to include development message in production code.

And publishing two modules (eg: my-utility and my-utility-es) will not helping, because in development, my code looks like this:

import { someFunc } from 'my-utility';

but in production code, I will have to change it to this:

import { someFunc } from 'my-utility-es';

How can I solve this problem?

Update

To be more clear, my development build and production build contains different source code (eg: production build has stripped all error message).

So specify webpack mode isn't satisfying for me.

like image 388
Joseph Wang Avatar asked Mar 19 '19 21:03

Joseph Wang


2 Answers

I've found out the answer by myself, I think the best way to do this is through babel macros:

import { something } from 'myLibrary/macro';

// In webpack development:
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
// import { something } from 'myLibrary/development';

// In webpack production:
// ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
// import { something } from 'myLibrary/production';

My macro implementation:

import { createMacro } from 'babel-plugin-macros';

function macro({ references, state, babel }) {
  state.file.path.node.body.forEach(node => {
    if (node.type === 'ImportDeclaration') {
      if (node.source.value.includes('myLibrary/macro')) {
        if (process.env.NODE_ENV === 'production') {
          node.source.value = 'myLibrary/module/production';
        } else {
          node.source.value = 'myLibrary/module/development';
        }
      }
    }
  });

  return { keepImports: true };
}

export default createMacro(macro);

like image 75
Joseph Wang Avatar answered Oct 21 '22 13:10

Joseph Wang


Here's the best solution I've found, without requiring the user to use Babel macros...

Let's say:

  • Our library is called crazy-components
  • It exports two React components called ComponentA and ComponentB
  • The components use code fencing to do some logging in dev mode only
// src/index.js
import React from 'react';

export function ComponentA(props) {
  if (process.env.NODE_ENV !== 'production') {
    console.log(`Rendering ComponentA with props ${props}`);
  }

  return <div>ComponentA message: {props.msg}</div>;
}

export function ComponentB(props) {
  if (process.env.NODE_ENV !== 'production') {
    console.log(`Rendering ComponentB with props ${props}`);
  }

  return <div>ComponentB message: {props.msg}</div>;
}

We want the the library to:

  1. Be tree-shakable, so if user does import { ComponentA } from 'crazy-components', the code for ComponentB does not end up in their bundle.

  2. The logging code is stripped from production bundles.

Solution

1. Bundle library with Rollup

2. Configure Rollup to create both CJS and ESM builds in both production and dev versions (production versions with logging code stripped etc).

CJS builds are output to /dist/cjs, ESM builds to /dist/esm. Files are called crazy-components.prod.min.js and crazy-components.dev.js.

Only the dev builds contains the logging code (not explaining how to do all this, if you're reading this, you probably already know).

3. Create entry points for CJS and ESM as follows:

// index.js
if (process.env.NODE_ENV === 'production') {
    module.exports = require('./dist/cjs/crazy-components.min.js');
} else {
    module.exports = require('./dist/cjs/crazy-components.js');
}
// es/index.js
import {
    ComponentA as ComponentA_prod,
    ComponentB as ComponentA_prod
} from '../dist/esm/crazy-components.prod.min.js';

import {
    ComponentA as ComponentA_dev,
    ComponentB as ComponentA_dev
} from '../dist/esm/crazy-components.dev.js';

export const ComponentA = process.env.NODE_ENV === 'production' ? ComponentA_prod : ComponentA_dev;
export const ComponentB = process.env.NODE_ENV === 'production' ? ComponentB_prod : ComponentB_dev;

4. Point to both entry points in package.json:

// package.json
{
  "name": "crazy-components",
  "version": "1.0.0",
  "main": "index.js",
  "module": "es/index.js",
  "sideEffects": false
}

5. (optional) Make importable as ESM in NodeJS

Node 12 (with flag) and Node 13+ support ES modules natively.

Add to package.json:

  "exports": {
    ".": {
      "import": "./es/index.js",
      "require": "./index.js"
    },
    "./es": "./es/index.js"
  },

Add extra package.json file in es folder to flag contents of the folder as ESM to NodeJS:

// es/package.json
{
  "type": "module"
}

Use rollup-plugin-copy to get Rollup to also copy this file into dist/esm:

// rollup.config.js
import copy from 'rollup-plugin-copy';
/* ... other imports ... */

export default {
  input: 'src/index.js',
  /* ... other config ... */
  plugins: [
    /* ... other plugins ... */
    copy({targets: [{src: 'es/package.json', dest: 'dist/esm'}]})
  ]
};

Possible improvements

es/index.js is created by hand, so if you later add ComponentC, it also needs to be added to es/index.js. It'd be ideal if there was a Rollup plugin to automate creation of es/index.js, but I haven't found one.

Also, your mileage may vary. I've only just been trying this out today. It seems to work as you'd expect when the library is imported in a create-react-app app, but I've not tested it with hand-coded Webpack configs.

This approach should be generalisable to any library, not just React components, but I've not tried.

Any suggestions for improvements very welcome!

like image 36
Overlook Motel Avatar answered Oct 21 '22 12:10

Overlook Motel