Search code examples
npmnode-moduleses6-modulescommonjs

Best practice for NPM package with es6 modules - bundle or not


Writing an NPM package containing es6 modules, is it best practice to keep the source files separate

package.json
esm
 index.js
 Content1
  Content1A.js
  Content1A.js.map
  Content1B.js
  Content1B.js.map
 Content2
  Content2A.js
  Content2A.js.map
  Content2B.js
  Content2B.js.map

with index.js referencing contents in subfolders, or is it better practice to bundle it into one file

package.json
esm
 contents.js
 contents.js.map

Seems the first method has an advantage with CommonJS modules since it gives a consumer possibility to import directly from the source and thus skip unused imports from index.js (since CommonJS modules are not tree-shakeable) but with es6 modules, this argument disappears.


Solution

  • Different bundlers might be capable of different things. The rest of this answer refers to Webpack which, being one of the most common bundlers, should influence decisions in this area.

    The most important factor governing the decision about whether to bundle your library or not should be related to tree-shaking. No other important aspects come to mind for me.

    Parameters affecting tree-shaking in Webpack

    1. sideEffects: false

    Setting in package.json that indicates whether modules in the package have side effects which needs to be executed when the module is imported but not consumed. Setting it to false indicates that no modules have side effects. May also be set to a list of modules which have side effects and other more complex values. Default seem to be true indicating that all modules have side effects.

    This parameters plays a large role when using an entrypoint index in your package, from which all package exports are re-exported. Sparse imports from this index could easily cause your entire package to be bundled if this setting is not correct.

    1. optimization.usedExports: true

    Setting in webpack.config.js indicating to Webpack that all exports that are not used may be excluded. This activates a heuristic used by Terser to remove unused code inside a module. Is set to true by default.

    In toy scenarios, this setting might seem efficient enough and the sideEffects flag might not seem to play a big role. This is not the case in real scenarios with more complex code where it is harder for this heuristic to do a good job.

    1. /*#__PURE__*/

    Annotation to be used before statements (such as functions) to indicate that they can be excluded if not explicitly used. These annotations also play a part in the heuristic used by Terser to remove unused code inside a module.

    Conclusion

    To allow your consumers to benefit the most from tree-shaking, it seems advisable to not bundle your es6 npm package and instead let the separate input modules remain separate so that the sideEffects setting in package.json may result in the consumer bundler to prune as many unused modules as possible. Rely on optimization.usedExports inside modules, evaluate bundle content and add /*#__PURE__*/ annotations where you think it could make a big difference. If everything is bundled in the same file, the sideEffects flag in package.json can't do the main part of the job as everything is in the same module and subsequently we have to rely on a lot of additional /*#__PURE__*/ annotations and heuristics in the consumer bundler to make tree-shaking as efficient as possible, which requires more from you (in terms of annotations) and does not come with any particular advantage. Remember to build your package in production mode as optimizations are not always active otherwise.

    Source