bwip-js icon indicating copy to clipboard operation
bwip-js copied to clipboard

ESM + better treeshaking

Open joewestcott opened this issue 2 years ago • 8 comments

I'd like to include this library in a front-end application, ideally without bundling the whole library. I don't think this is possible at the moment due to how it's structured. Have a look at the following example:

// in.js
import { ean13 } from "bwip-js";

ean13()

When bundling with ESBuild: esbuild --bundle ./in.js --minify --outfile=out.js

The size of out.js is 823.6kb, which is the entire library, even though I only chose one symbology. It would also be nice if the symbol-specific exports didn't assume I require an output to a canvas.

@metafloor Is it possible for others to contribute to this library? As far as I can tell, the code that does the postscript to js conversion hasn't been provided, it's only possible to see the output.

joewestcott avatar Jul 28 '23 14:07 joewestcott

This appears to be bugs (plural) in esbuild. The bwip-js ES6 modules are already properly structured for tree shaking and are verified to work with webpack.

The first bug I see is that esbuild is preferring the browser entry in package.json over the exports map. Take a look at this module's package.json:

  ...
  "main": "./dist/bwip-js-node.js",
  "browser": "./dist/bwip-js.js",
  "exports": {
      "browser": {
          "import":  "./dist/bwip-js.mjs",
          "require": "./dist/bwip-js.js",
          "script":  "./dist/bwip-js-min.js"
      },
      ...

When you look at the generated code, you will see esbuild pulled in the ./dist/bwip-js.js code.

So I tried to trick esbuild into using the ES6 module by updating package.json with "browser" : "./dist/bwip-js.mjs". That almost worked. It dead-code eliminated all of the symbol-specific export stubs except the ean13 export. Unfortunately, it kept the default export which prevented any substantive dead-code elimination. This is what I found in the generated code:

  var bwip_js_default = {
    // The public interface
    toCanvas: ToCanvas,
    render: Render,
    raw: ToRaw,
    fixupOptions: FixupOptions,
    loadFont: FontLib.loadFont,
    BWIPJS_VERSION: "3.4.4 (2023-07-27)",
    BWIPP_VERSION,
    // Internals
    BWIPJS,
    STBTT,
    FontLib,
    DrawingBuiltin,
    DrawingCanvas
  };

Because the default export is still present, the tree shaking can't happen.

metafloor avatar Jul 28 '23 16:07 metafloor

Thanks for your insight. Are we hitting into this issue? https://github.com/evanw/esbuild/issues/1420

joewestcott avatar Jul 31 '23 08:07 joewestcott

I don't believe that particular issue is the source of what we are seeing. We are not importing, then exporting namespaces. Similarly, we are not re-exporting the lower-level imports directly - our exports are new functions that reference the imports. Don't know if that is enough to trigger the behavior described.

What confuses me is why the default export is bundled. It is not referenced by the top-level code and should be eliminated.

Since esbuild seems to be an interesting bundler, I will play around with it some more to see if I can understand why it is including the default export.

But we are still facing the bug in esbuild where it is not prioritizing the exports map in package.json over the obsolete browser field.

metafloor avatar Jul 31 '23 14:07 metafloor

The default exports bug is now understood. If any of the exported values are unknown/untraceable, then esbuild gives up on the tree-shaking. Why an untraceable value in one module stops tree-shaking in another is beyond me - it should have no effect.

This is the default export in dist/bwip-js.mjs:

export default {
    // The public interface
    toCanvas : ToCanvas, render : Render, raw : ToRaw,
    fixupOptions : FixupOptions,
    loadFont : FontLib.loadFont,
    BWIPJS_VERSION : '3.4.4 (2023-07-27)',
    BWIPP_VERSION : BWIPP_VERSION,
    // Internals
    BWIPJS, STBTT, FontLib, DrawingBuiltin, DrawingCanvas,
};

If the loadFont : FontLib.loadFont, is commented out, the tree-shaking culls the result to approximately 150kb, a huge improvement over the previous 1.5mb.

So that bug is understood and can be worked around. But we need esbuild to properly resolve modules using the package.json exports-map before it is worthwhile to implement any change in bwip-js. Using --log-level=verbose gives a glimpse of what esbuild does to resolve the import statements, and it doesn't even look at the exports-map.

metafloor avatar Jul 31 '23 21:07 metafloor

While looking over the esbuild source code, specifically internal/resolve.go, I saw how to structure package.json so it is compatible. Simply moving the main and browser fields below the exports map makes the resolver logic happy. And this change should be 100% backward compatible with other package bundlers.

Release v3.4.5 also contains a wrapper around FontLib.loadFont() for use in the default export. This makes esbuild's tree shaking happy as well. The end result:

$ cat esbuild.js

import { ean13 } from 'bwip-js';
console.log(ean13());

$ npx esbuild --bundle --outfile=bundle.js esbuild.js

  bundle.js  164.9kb

⚡ Done in 335ms

metafloor avatar Aug 01 '23 17:08 metafloor

Hi @metafloor, thanks for looking into this.

I'm quite surprised to see ESBuild working this way. Is this behaviour documented anywhere?

Happy to mark this issue as solved, although I would appreciate an answer regarding my PS to JS code question above. :)

joewestcott avatar Aug 04 '23 14:08 joewestcott

Regarding the PS to JS, there is a develop branch that I use to host the development tools. It is not well documented but basically the build process is:

$ ./psc
$ ./mkdist
$ node chkdist.js
$ ./runtests
$ ./imgtests

The psc script builds src/bwipp.js from barcode.ps.

metafloor avatar Aug 04 '23 14:08 metafloor

Here is the Vite work around.

export default defineConfig({
    ...
    plugins: [react()],
    resolve: {
        alias: [
            {
                find: 'bwip-js',
                replacement: resolve(
                    __dirname,
                    'node_modules/bwip-js/dist/bwip-js.mjs'
                )
            }
        ]
    },
    ...

Enjoy!

visualjeff avatar Oct 14 '23 02:10 visualjeff