developit / microbundle

📦 Zero-configuration bundler for tiny modules.
https://npm.im/microbundle
MIT License
8.03k stars 362 forks source link

Babel targets configuration for browser and node builds #982

Open make-github-pseudonymous-again opened 2 years ago

make-github-pseudonymous-again commented 2 years ago

For a while I have been using the following babel preset for production bundles:

        ...
        "presets": [
          [
            "@babel/preset-env",
            {
              "targets": [
                "defaults",
                "maintained node versions"
              ]
            }
          ]
        ],
        ...

This configuration seems to be incompatible with microbundle as it generates errors such as unknown Statement of type "ForOfStatement" when the source contains for (... of ...) statements. Using "presets": ["@babel/preset-env"] without specifying targets works. However, I am concerned about the compatibility achieved by such "default" builds.

What is the proper way to use microbundle to build for both the browser and for node? What's the correct babel configuration to use? I am fine with having one build for the browser and another for Node, but how should one define package.json exports in that case?

I do realize microbundle probably does the correct thing by default, but I am a bit skeptical given the existence of the --target CLI flag, for which little documentation exists.

multivoltage commented 2 years ago

Mayne check my previus question

make-github-pseudonymous-again commented 2 years ago

Thanks @multivoltage. I understand that microbundle's goal is to produce bundles that are as small as possible. I am interested in small bundles, but I am even more interested in compatibility with the most popular execution environments for ESM: reasonably popular web browsers versions and maintained node versions. I see two approaches to solve this problem:

  1. Produce a single batch of bundles that are compatible with all targets. The package.json conditional exports are then trivial to implement (.module.js output for module key, .cjs for main and exports.require, and .modern.js for exports.default).
  2. Produce two batches of bundles, one with --target web (default) and one with --target node. But then it is not clear how to map the different keys.

I am trying to do 1, but I hit some sort of babel misconfiguration. I have no idea on how to do 2 so that it works reliably. Sure modern node and webpack (and also rollup?) support require and import. But the condition node does not seem to have a way to distinguish between CommonJS and ESM. It looks like for it to work we would need conditions node:require, node:import, browser:require, and browser:import. Maybe someone knows better?

PS: Do I understand correctly from the webpack docs that to compose conditionals one can cascade them?

make-github-pseudonymous-again commented 2 years ago

It seems the solution is to give up on custom configurations and let microbundle decide. microbundle bears the responsibility of setting the correct configuration to obtain correct builds for contemporary targets. If you want a build compatible with both contemporary Node and Web, use --target web (the default). If you really want an unmangled build for Node, build twice, once with --target web, and once with --target node. Then use the following configuration in package.json:

  ...
  "type": "module",
  "source": "src/foo.js",             // your source code
  "exports": {
    "node": {
      "require": "./dist/node/foo.cjs",
      "default": "./dist/node/foo.modern.js",
    },
    "default": "./dist/web/foo.modern.js"
  },
  "main": "./dist/node/foo.cjs",           // where to generate the CommonJS bundle
  "module": "./dist/web/foo.js",   // where to generate the ESM bundle
  ...

The reason my example usage crashes is that microbundle esm and cjs formats force the transpilation of generator functions and for ... of syntax but when combined with the @babel/preset-env configuration above, somehow only the generator functions are transpiled, but not the for ... of syntax. Related issues:

Ideally, I think there should be a tool similar to microbundle but that forces explicit babel configuration without creating these conflicts. It would not be that much more of a hassle to setup if microbundle defined their default babel configuration for the combinations of builds and targets as independent babel presets. Would microbundle still add value compared to direct use of rollup in this case? I think so: handles TypeScript by default, produces tiny code, automatic build/target detection (via main, module, and exports). Arguably these need less custom configuration.

make-github-pseudonymous-again commented 2 years ago

Another solution seems to be to explicitly rely on @babel/plugin-transform-for-of, for instance:

...
"presets": [
  [
    "@babel/preset-env",
    {
      "targets": [
        "defaults",
        "maintained node versions"
      ]
    }
  ]
],
"plugins": [
  "@babel/plugin-transform-for-of"
]
...

PS: And if you use destructuring, @babel/plugin-transform-destructuring.