diff --git a/packages/babel-preset-env/README.md b/packages/babel-preset-env/README.md index 7cc3fb0e44..f5a77cd684 100644 --- a/packages/babel-preset-env/README.md +++ b/packages/babel-preset-env/README.md @@ -30,6 +30,22 @@ This example only includes the polyfills and code transforms needed for the last } ``` +You may also target browsers supporting ES Modules (https://www.ecma-international.org/ecma-262/6.0/#sec-modules). When specifying this option, the browsers field will be ignored. You can use this approach in combination with `` to conditionally serve smaller scripts to users (https://jakearchibald.com/2017/es-modules-in-browsers/#nomodule-for-backwards-compatibility). + +*Please note*: when specifying the esmodules target, browsers targets will be ignored. + +```json +{ + "presets": [ + ["@babel/preset-env", { + "targets": { + "esmodules": true + } + }] + ] +} +``` + Similarly, if you're targeting Node.js instead of the browser, you can configure @babel/preset-env to only include the polyfills and transforms necessary for a particular version: ```json diff --git a/packages/babel-preset-env/data/built-in-modules.json b/packages/babel-preset-env/data/built-in-modules.json new file mode 100644 index 0000000000..9cab30106f --- /dev/null +++ b/packages/babel-preset-env/data/built-in-modules.json @@ -0,0 +1,8 @@ +{ + "es6.module": { + "edge": "16", + "chrome": "61", + "safari": "10.1", + "ios_saf": "10.3" + } +} diff --git a/packages/babel-preset-env/package.json b/packages/babel-preset-env/package.json index d085c29b55..98d3347049 100644 --- a/packages/babel-preset-env/package.json +++ b/packages/babel-preset-env/package.json @@ -8,7 +8,7 @@ "repository": "https://github.com/babel/babel/tree/master/packages/babel-preset-env", "main": "lib/index.js", "scripts": { - "build-data": "node ./scripts/build-data.js" + "build-data": "node ./scripts/build-data.js; node ./scripts/build-modules-support.js" }, "dependencies": { "@babel/plugin-check-constants": "7.0.0-beta.38", @@ -59,6 +59,7 @@ "@babel/helper-fixtures": "7.0.0-beta.38", "@babel/helper-plugin-test-runner": "7.0.0-beta.38", "compat-table": "kangax/compat-table#3e30cd67a5d3d853caf8424d00ca66d100674d4f", + "request": "^2.83.0", "electron-to-chromium": "^1.3.27" } } diff --git a/packages/babel-preset-env/scripts/build-modules-support.js b/packages/babel-preset-env/scripts/build-modules-support.js new file mode 100644 index 0000000000..55cf110165 --- /dev/null +++ b/packages/babel-preset-env/scripts/build-modules-support.js @@ -0,0 +1,70 @@ +const path = require("path"); +const fs = require("fs"); +const request = require("request"); + +// This mapping represents browsers who have shipped ES Modules Support. +// For more information, checkout the specifications: +// * https://www.ecma-international.org/ecma-262/6.0/#sec-modules +// * https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type +const lastKnown = { + chrome: 61, + firefox: 59, + safari: 10.1, + ios_saf: 10.3, + edge: 16, +}; + +function input() { + return new Promise(function(resolve, reject) { + request( + "https://raw.githubusercontent.com/Fyrd/caniuse/master/features-json/es6-module.json", + function(error, response, body) { + if (error || response.statusCode !== 200) { + return reject( + new Error( + `Error retrieving es6-module.json. ${ + error ? error : `statusCode=${response.statusCode}` + }` + ) + ); + } + + try { + const { stats } = JSON.parse(body); + const allowedBrowsers = {}; + + Object.keys(stats).forEach(browser => { + if (browser !== "and_chr") { + const browserVersions = stats[browser]; + const allowedVersions = Object.keys(browserVersions) + .filter(value => { + return browserVersions[value] === "y"; + }) + .sort((a, b) => a - b); + + if (allowedVersions[0] !== undefined) { + allowedBrowsers[browser] = allowedVersions[0]; + } + } + }); + + resolve(allowedBrowsers); + } catch (error) { + return reject(new Error(`Error parsing es6-module.json.`)); + } + } + ); + }); +} + +function output(minVersions) { + const dataPath = path.join(__dirname, "../data/built-in-modules.json"); + const data = { + "es6.module": minVersions, + }; + fs.writeFileSync(dataPath, `${JSON.stringify(data, null, 2)}\n`); +} + +Promise.resolve(input()) + .then(minVersions => output(minVersions)) + .catch(output(lastKnown)); diff --git a/packages/babel-preset-env/src/index.js b/packages/babel-preset-env/src/index.js index 0bf36d06c4..ae6c4825f1 100644 --- a/packages/babel-preset-env/src/index.js +++ b/packages/babel-preset-env/src/index.js @@ -183,6 +183,17 @@ export default function buildPreset( console.log(""); } + if (optionsTargets && optionsTargets.esmodules && optionsTargets.browsers) { + console.log(""); + console.log( + "@babel/preset-env: esmodules and browsers targets have been specified together.", + ); + console.log( + `\`browsers\` target, \`${optionsTargets.browsers}\` will be ignored.`, + ); + console.log(""); + } + const targets = getTargets(optionsTargets, { ignoreBrowserslistConfig, configPath, diff --git a/packages/babel-preset-env/src/targets-parser.js b/packages/babel-preset-env/src/targets-parser.js index 3eb0a5e9ee..0f3aef0364 100644 --- a/packages/babel-preset-env/src/targets-parser.js +++ b/packages/babel-preset-env/src/targets-parser.js @@ -4,6 +4,7 @@ import browserslist from "browserslist"; import semver from "semver"; import { semverify, isUnreleasedVersion, getLowestUnreleased } from "./utils"; import { objectToBrowserslist } from "./normalize-options"; +import browserModulesData from "../data/built-in-modules.json"; import type { Targets } from "./types"; const browserNameMap = { @@ -109,6 +110,15 @@ type ParsedResult = { }; const getTargets = (targets: Object = {}, options: Object = {}): Targets => { const targetOpts: Targets = {}; + + // `esmodules` as a target indicates the specific set of browsers supporting ES Modules. + // These values OVERRIDE the `browsers` field. + if (targets.esmodules) { + const supportsESModules = browserModulesData["es6.module"]; + targets.browsers = Object.keys(supportsESModules) + .map(browser => `${browser} ${supportsESModules[browser]}`) + .join(", "); + } // Parse browsers target via browserslist; const queryIsValid = isBrowsersQueryValid(targets.browsers); const browsersquery = queryIsValid ? targets.browsers : null; @@ -121,6 +131,7 @@ const getTargets = (targets: Object = {}, options: Object = {}): Targets => { } // Parse remaining targets const parsed = Object.keys(targets) + .filter(value => value !== "esmodules") .sort() .reduce( (results: ParsedResult, target: string): ParsedResult => { diff --git a/packages/babel-preset-env/test/targets-parser.spec.js b/packages/babel-preset-env/test/targets-parser.spec.js index 77c8f38967..d1144ea993 100644 --- a/packages/babel-preset-env/test/targets-parser.spec.js +++ b/packages/babel-preset-env/test/targets-parser.spec.js @@ -78,6 +78,70 @@ describe("getTargets", () => { }); }); + describe("esmodules", () => { + it("returns browsers supporting modules", () => { + assert.deepEqual( + getTargets({ + esmodules: true, + }), + { + chrome: "61.0.0", + safari: "10.1.0", + ios: "10.3.0", + edge: "16.0.0", + }, + ); + }); + + it("returns browsers supporting modules, ignoring browsers key", () => { + assert.deepEqual( + getTargets({ + esmodules: true, + browsers: "ie 8", + }), + { + chrome: "61.0.0", + safari: "10.1.0", + ios: "10.3.0", + edge: "16.0.0", + }, + ); + }); + + it("returns browser supporting modules and keyed browser overrides", () => { + assert.deepEqual( + getTargets({ + esmodules: true, + ie: 11, + }), + { + chrome: "61.0.0", + safari: "10.1.0", + ios: "10.3.0", + ie: "11.0.0", + edge: "16.0.0", + }, + ); + }); + + it("returns browser supporting modules and keyed browser overrides, ignoring browsers field", () => { + assert.deepEqual( + getTargets({ + esmodules: true, + browsers: "ie 10", + ie: 11, + }), + { + chrome: "61.0.0", + safari: "10.1.0", + ios: "10.3.0", + ie: "11.0.0", + edge: "16.0.0", + }, + ); + }); + }); + describe("node", () => { it("should return the current node version with option 'current'", () => { assert.deepEqual(