Esbuild: Support browserslist

Created on 19 May 2020  路  3Comments  路  Source: evanw/esbuild

I think this lib will benefit a lot if It has support of industry standard way of detecting target JavaScript version, the browserslist. If you don't support tools like that I don't think your lib will draw enough attention.

Yes, esbuild can be used as intermediary step, but still I'll have to run Babel.

And you know, if project is small then compile speed is not the issue. So new projects don't have real reasons to prefer esbuild or esbuild-loader over webpack or rollup with Babel.

Compile speed is the issue for large codebases. But large codebases usually have to address wide audiences and need tooling to deliver code that will work everywhere. Their audience often includes people that use older browsers. Browserslist helps to address that a lot.

Most helpful comment

I have an update on this. I just added the ability to add additional targets to the --target flag (not released yet). This should go most of the way toward solving this issue because you can call browserslist yourself and then pass the version numbers on to esbuild. Here's an example of what is now possible: --target=chrome58,firefox57,safari11,edge16.

All 3 comments

It looks like that API combined with perhaps something like mdn-browser-compat-data could be useful. That would let you use browserslist to get a list of browsers and then you could use browser compatibility data to pick the corresponding value for the --target= language flag for esbuild.

I think something like this is better done as another package outside of esbuild core. That way people that don't need this don't have to pay the cost of it (the feature database is rather large).

I was curious about this so I gave it a shot. Here's what I came up with:

| Target | Chrome | Safari | Firefox | Edge |
|--------|--------|--------|---------|------|
| es2015 | 49+ | 10.1+ | 45+ | 14+ |
| es2016 | 52+ | 10.1+ | 52+ | 14+ |
| es2017 | 55+ | 10.1+ | 52+ | 15+ |
| es2018 | 60+ | 11.1+ | 55+ | 79+ |
| es2019 | 66+ | 11.1+ | 58+ | 79+ |
| es2020 | 80+ | 13.1+ | 72+ | 80+ |

Given an esbuild language target (i.e. the esbuild --target=... flag), this table shows the minimum browser version needed to run that code. For example, if I want to support Chrome 54 then I need to pass --target=es2016 to esbuild. The top row of this table is essentially the minimum browser versions that esbuild supports.

Caveats:

  • The MDN data seems high quality but incomplete. I couldn't find data for async generators, for example. I'm not sure where to look to get that data. Perhaps there's some way to replace this with data from https://kangax.github.io/compat-table/?

  • This table doesn't account for browser bugs, which esbuild currently doesn't try to work around. See https://bugs.webkit.org/show_bug.cgi?id=171041 for an example.

  • Since esbuild's syntax transforms are currently incomplete, setting the target correctly doesn't mean esbuild will always be able to generate code that works in those browsers. The good news is attempting to use syntax that isn't yet supported for a given language target will stop the build with an error so you shouldn't be able to accidentally build code that won't work in a browser you support. But you may not be able to use certain syntax with esbuild for that language target.


Here is the code to generate this table (click to expand)

const mdn = require('mdn-browser-compat-data')

const features = {
  // Required features
  class: [mdn.javascript.statements.class.__compat, -Infinity],
  spread_in_arrays: [mdn.javascript.operators.spread.spread_in_arrays.__compat, -Infinity],
  arrow_functions: [mdn.javascript.functions.arrow_functions.__compat, -Infinity],
  default_parameters: [mdn.javascript.functions.default_parameters.__compat, -Infinity],
  method_definitions: [mdn.javascript.functions.method_definitions.__compat, -Infinity],
  rest_parameters: [mdn.javascript.functions.rest_parameters.__compat, -Infinity],
  get: [mdn.javascript.functions.get.__compat, -Infinity],
  set: [mdn.javascript.functions.set.__compat, -Infinity],
  yield: [mdn.javascript.operators.yield.__compat, -Infinity],
  yield_star: [mdn.javascript.operators.yield_star.__compat, -Infinity],
  template_literals: [mdn.javascript.grammar.template_literals.__compat, -Infinity],

  // Features that can be transformed
  optional_catch_binding: [mdn.javascript.statements.try_catch.optional_catch_binding.__compat, 2018],
  public_class_fields: [mdn.javascript.classes.public_class_fields.__compat, 2020],
  static_class_fields: [mdn.javascript.classes.static_class_fields.__compat, 2020],
  exponentiation: [mdn.javascript.operators.exponentiation.__compat, 2015],
  exponentiation_assignment: [mdn.javascript.operators.exponentiation_assignment.__compat, 2015],
  object_spread: [mdn.javascript.operators.spread.spread_in_object_literals.__compat, 2017],
  nullish_coalescing: [mdn.javascript.operators.nullish_coalescing.__compat, 2019],

  // Features that can't be transformed
  async_function: [mdn.javascript.statements.async_function.__compat, 2016],
  private_class_fields: [mdn.javascript.classes.private_class_fields.__compat, 2020],
}

function queryTargetForBrowsers(browsers) {
  let target = 2020 // Always need to transform unreleased Stage 3 features
  for (const feature in features) {
    const [__compat, lowerTarget] = features[feature]
    for (const browser in browsers) {
      const info = __compat.support[browser]
      const versionAdded = (info[0] || info).version_added
      const requestedVersion = browsers[browser]
      if (+requestedVersion < +versionAdded && lowerTarget < target)
        target = lowerTarget
    }
  }
  return target
}

function queryBrowsersForTarget(target) {
  const browsers = {}
  for (const browser of ['chrome', 'safari', 'firefox', 'edge'])
    for (const version of Object.keys(mdn.browsers[browser].releases).sort((a, b) => +a - +b))
      if (queryTargetForBrowsers({ [browser]: version }) >= target) {
        browsers[browser] = version
        break
      }
  return browsers
}

const table = {}
for (let target = 2015; target <= 2020; target++) {
  table[`es${target}`] = queryBrowsersForTarget(target)
}
console.table(table)

I have an update on this. I just added the ability to add additional targets to the --target flag (not released yet). This should go most of the way toward solving this issue because you can call browserslist yourself and then pass the version numbers on to esbuild. Here's an example of what is now possible: --target=chrome58,firefox57,safari11,edge16.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mixtur picture mixtur  路  4Comments

fannheyward picture fannheyward  路  4Comments

tonyhb picture tonyhb  路  3Comments

lolychee picture lolychee  路  4Comments

frandiox picture frandiox  路  3Comments