metafloor / bwip-js

Barcode Writer in Pure JavaScript
Other
2.06k stars 300 forks source link

ESM + better treeshaking #298

Open joewestcott opened 1 year ago

joewestcott commented 1 year ago

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.

metafloor commented 1 year ago

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.

joewestcott commented 1 year ago

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

metafloor commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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
joewestcott commented 1 year ago

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. :)

metafloor commented 1 year ago

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.

visualjeff commented 10 months ago

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!