From 96e0bbfd4c1ad7f6f80eb327697cb9b0de7bfc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Thu, 15 Oct 2020 20:41:22 +0200 Subject: [PATCH] Add `targets` and browserslist* options to @babel/core --- packages/babel-core/package.json | 3 + .../babel-core/src/config/config-chain.js | 15 +- packages/babel-core/src/config/partial.js | 56 ++++--- .../src/config/resolve-targets-browser.js | 22 +++ .../babel-core/src/config/resolve-targets.js | 35 +++++ packages/babel-core/src/config/util.js | 4 +- .../config/validation/option-assertions.js | 55 +++++++ .../src/config/validation/options.js | 28 ++++ packages/babel-core/test/config-chain.js | 2 + .../test/fixtures/targets/.browserslistrc | 4 + .../fixtures/targets/.browserslistrc-firefox | 1 + .../fixtures/targets/nested/.browserslistrc | 1 + packages/babel-core/test/targets.js | 141 ++++++++++++++++++ .../src/index.js | 13 +- .../src/types.js | 2 +- yarn.lock | 1 + 16 files changed, 350 insertions(+), 33 deletions(-) create mode 100644 packages/babel-core/src/config/resolve-targets-browser.js create mode 100644 packages/babel-core/src/config/resolve-targets.js create mode 100644 packages/babel-core/test/fixtures/targets/.browserslistrc create mode 100644 packages/babel-core/test/fixtures/targets/.browserslistrc-firefox create mode 100644 packages/babel-core/test/fixtures/targets/nested/.browserslistrc create mode 100644 packages/babel-core/test/targets.js diff --git a/packages/babel-core/package.json b/packages/babel-core/package.json index 7b3b14770db5..014590e76f6c 100644 --- a/packages/babel-core/package.json +++ b/packages/babel-core/package.json @@ -38,13 +38,16 @@ }, "browser": { "./lib/config/files/index.js": "./lib/config/files/index-browser.js", + "./lib/config/resolve-targets.js": "./lib/config/resolve-targets-browser.js", "./lib/transform-file.js": "./lib/transform-file-browser.js", "./src/config/files/index.js": "./src/config/files/index-browser.js", + "./src/config/resolve-targets.js": "./src/config/resolve-targets-browser.js", "./src/transform-file.js": "./src/transform-file-browser.js" }, "dependencies": { "@babel/code-frame": "workspace:^7.10.4", "@babel/generator": "workspace:^7.12.0", + "@babel/helper-compilation-targets": "workspace:^7.12.0", "@babel/helper-module-transforms": "workspace:^7.12.0", "@babel/helpers": "workspace:^7.10.4", "@babel/parser": "workspace:^7.12.0", diff --git a/packages/babel-core/src/config/config-chain.js b/packages/babel-core/src/config/config-chain.js index 350111a1a41b..a06479e1824d 100644 --- a/packages/babel-core/src/config/config-chain.js +++ b/packages/babel-core/src/config/config-chain.js @@ -3,6 +3,7 @@ import path from "path"; import buildDebug from "debug"; import type { Handler } from "gensync"; +import getTargets from "@babel/helper-compilation-targets"; import { validate, type ValidatedOptions, @@ -678,7 +679,7 @@ function emptyChain(): ConfigChain { } function normalizeOptions(opts: ValidatedOptions): ValidatedOptions { - const options = { + const options: ValidatedOptions = { ...opts, }; delete options.extends; @@ -699,7 +700,17 @@ function normalizeOptions(opts: ValidatedOptions): ValidatedOptions { options.sourceMaps = options.sourceMap; delete options.sourceMap; } - return options; + + const { targets } = options; + if (typeof targets === "string") { + options.targets = getTargets({ browsers: [targets] }); + } else if (Array.isArray(targets)) { + options.targets = getTargets({ browsers: targets }); + } else if (targets) { + options.targets = getTargets((targets: any)); + } + + return (options: any); } function dedupDescriptors( diff --git a/packages/babel-core/src/config/partial.js b/packages/babel-core/src/config/partial.js index e69c9cd4c453..8aff9be46ead 100644 --- a/packages/babel-core/src/config/partial.js +++ b/packages/babel-core/src/config/partial.js @@ -14,6 +14,7 @@ import { getEnv } from "./helpers/environment"; import { validate, type ValidatedOptions, + type NormalizedOptions, type RootMode, } from "./validation/options"; @@ -24,6 +25,7 @@ import { type ConfigFile, type IgnoreFile, } from "./files"; +import { resolveTargets } from "./resolve-targets"; function* resolveRootMode( rootDir: string, @@ -61,7 +63,7 @@ function* resolveRootMode( } type PrivPartialConfig = { - options: ValidatedOptions, + options: NormalizedOptions, context: ConfigContext, fileHandling: FileHandling, ignore: IgnoreFile | void, @@ -115,30 +117,36 @@ export default function* loadPrivatePartialConfig( const configChain = yield* buildRootChain(args, context); if (!configChain) return null; - const options = {}; + const merged: ValidatedOptions = {}; configChain.options.forEach(opts => { - mergeOptions(options, opts); + mergeOptions((merged: any), opts); }); - // Tack the passes onto the object itself so that, if this object is - // passed back to Babel a second time, it will be in the right structure - // to not change behavior. - options.cloneInputAst = cloneInputAst; - options.babelrc = false; - options.configFile = false; - options.passPerPreset = false; - options.envName = context.envName; - options.cwd = context.cwd; - options.root = context.root; - options.filename = - typeof context.filename === "string" ? context.filename : undefined; - - options.plugins = configChain.plugins.map(descriptor => - createItemFromDescriptor(descriptor), - ); - options.presets = configChain.presets.map(descriptor => - createItemFromDescriptor(descriptor), - ); + const options: NormalizedOptions = { + ...merged, + targets: resolveTargets(merged, absoluteRootDir, filename), + + // Tack the passes onto the object itself so that, if this object is + // passed back to Babel a second time, it will be in the right structure + // to not change behavior. + cloneInputAst, + babelrc: false, + configFile: false, + browserslistConfigFile: false, + passPerPreset: false, + envName: context.envName, + cwd: context.cwd, + root: context.root, + filename: + typeof context.filename === "string" ? context.filename : undefined, + + plugins: configChain.plugins.map(descriptor => + createItemFromDescriptor(descriptor), + ), + presets: configChain.presets.map(descriptor => + createItemFromDescriptor(descriptor), + ), + }; return { options, @@ -195,7 +203,7 @@ class PartialConfig { * These properties are public, so any changes to them should be considered * a breaking change to Babel's API. */ - options: ValidatedOptions; + options: NormalizedOptions; babelrc: string | void; babelignore: string | void; config: string | void; @@ -203,7 +211,7 @@ class PartialConfig { files: Set; constructor( - options: ValidatedOptions, + options: NormalizedOptions, babelrc: string | void, ignore: string | void, config: string | void, diff --git a/packages/babel-core/src/config/resolve-targets-browser.js b/packages/babel-core/src/config/resolve-targets-browser.js new file mode 100644 index 000000000000..4196ce4e9911 --- /dev/null +++ b/packages/babel-core/src/config/resolve-targets-browser.js @@ -0,0 +1,22 @@ +// @flow + +import type { ValidatedOptions } from "./validation/options"; +import getTargets, { type Targets } from "@babel/helper-compilation-targets"; + +export function resolveTargets( + options: ValidatedOptions, + // eslint-disable-next-line no-unused-vars + root: string, + // eslint-disable-next-line no-unused-vars + filename: string | void, +): Targets { + let { targets } = options; + if (typeof targets === "string" || Array.isArray(targets)) { + targets = { browsers: targets }; + } + + return getTargets((targets: any), { + ignoreBrowserslistConfig: true, + env: options.browserslistEnv, + }); +} diff --git a/packages/babel-core/src/config/resolve-targets.js b/packages/babel-core/src/config/resolve-targets.js new file mode 100644 index 000000000000..272fc5e58255 --- /dev/null +++ b/packages/babel-core/src/config/resolve-targets.js @@ -0,0 +1,35 @@ +// @flow + +import typeof * as browserType from "./resolve-targets-browser"; +import typeof * as nodeType from "./resolve-targets"; + +// Kind of gross, but essentially asserting that the exports of this module are the same as the +// exports of index-browser, since this file may be replaced at bundle time with index-browser. +((({}: any): $Exact): $Exact); + +import type { ValidatedOptions } from "./validation/options"; +import path from "path"; +import getTargets, { type Targets } from "@babel/helper-compilation-targets"; + +export function resolveTargets( + options: ValidatedOptions, + root: string, + filename: string | void, +): Targets { + let { targets } = options; + if (typeof targets === "string" || Array.isArray(targets)) { + targets = { browsers: targets }; + } + + let config; + if (typeof options.browserslistConfigFile === "string") { + config = path.resolve(root, options.browserslistConfigFile); + } + + return getTargets((targets: any), { + ignoreBrowserslistConfig: options.browserslistConfigFile === false, + config, + path: filename ?? root, + env: options.browserslistEnv, + }); +} diff --git a/packages/babel-core/src/config/util.js b/packages/babel-core/src/config/util.js index b9d3af143dd3..491a2ea950fa 100644 --- a/packages/babel-core/src/config/util.js +++ b/packages/babel-core/src/config/util.js @@ -1,10 +1,10 @@ // @flow -import type { ValidatedOptions } from "./validation/options"; +import type { ValidatedOptions, NormalizedOptions } from "./validation/options"; export function mergeOptions( target: ValidatedOptions, - source: ValidatedOptions, + source: ValidatedOptions | NormalizedOptions, ): void { for (const k of Object.keys(source)) { if (k === "parserOpts" && source.parserOpts) { diff --git a/packages/babel-core/src/config/validation/option-assertions.js b/packages/babel-core/src/config/validation/option-assertions.js index f3962f18945f..b4c83adff531 100644 --- a/packages/babel-core/src/config/validation/option-assertions.js +++ b/packages/babel-core/src/config/validation/option-assertions.js @@ -1,5 +1,10 @@ // @flow +import { + isBrowsersQueryValid, + TargetNames, +} from "@babel/helper-compilation-targets"; + import type { ConfigFileSearch, BabelrcSearch, @@ -16,6 +21,7 @@ import type { NestingPath, CallerMetadata, RootMode, + TargetsListOrObject, } from "./options"; export type { RootPath } from "./options"; @@ -373,3 +379,52 @@ function assertPluginTarget(loc: GeneralPath, value: mixed): PluginTarget { } return value; } + +export function assertTargets( + loc: GeneralPath, + value: mixed, +): TargetsListOrObject { + if (isBrowsersQueryValid(value)) return (value: any); + + if (typeof value !== "object" || !value || Array.isArray(value)) { + throw new Error( + `${msg(loc)} must be a string, an array of strings or an object`, + ); + } + + assertBrowsersList(access(loc, "browsers"), value.browsers); + assertBoolean(access(loc, "esmodules"), value.esmodules); + + for (const key of Object.keys(value)) { + const val = value[key]; + const subLoc = access(loc, key); + + if (key === "esmodules") assertBoolean(subLoc, val); + else if (key === "browsers") assertBrowsersList(subLoc, val); + else if (!Object.hasOwnProperty.call(TargetNames, key)) { + const validTargets = Object.keys(TargetNames).join(", "); + throw new Error( + `${msg( + subLoc, + )} is not a valid target. Supported targets are ${validTargets}`, + ); + } else assertBrowserVersion(subLoc, val); + } + + return (value: any); +} + +function assertBrowsersList(loc: GeneralPath, value: mixed) { + if (value !== undefined && !isBrowsersQueryValid(value)) { + throw new Error( + `${msg(loc)} must be undefined, a string or an array of strings`, + ); + } +} + +function assertBrowserVersion(loc: GeneralPath, value: mixed) { + if (typeof value === "number" && Math.round(value) === value) return; + if (typeof value === "string") return; + + throw new Error(`${msg(loc)} must be a string or an integer number`); +} diff --git a/packages/babel-core/src/config/validation/options.js b/packages/babel-core/src/config/validation/options.js index 27caf3bcc1e2..56b7655583e4 100644 --- a/packages/babel-core/src/config/validation/options.js +++ b/packages/babel-core/src/config/validation/options.js @@ -1,5 +1,7 @@ // @flow +import type { InputTargets, Targets } from "@babel/helper-compilation-targets"; + import type { ConfigItem } from "../item"; import Plugin from "../plugin"; @@ -23,6 +25,7 @@ import { assertSourceMaps, assertCompact, assertSourceType, + assertTargets, type ValidatorSet, type Validator, type OptionPath, @@ -77,6 +80,16 @@ const NONPRESET_VALIDATORS: ValidatorSet = { $PropertyType, >), only: (assertIgnoreList: Validator<$PropertyType>), + + targets: (assertTargets: Validator< + $PropertyType, + >), + browserslistConfigFile: (assertConfigFileSearch: Validator< + $PropertyType, + >), + browserslistEnv: (assertString: Validator< + $PropertyType, + >), }; const COMMON_VALIDATORS: ValidatorSet = { @@ -208,6 +221,11 @@ export type ValidatedOptions = { plugins?: PluginList, passPerPreset?: boolean, + // browserslists-related options + targets?: TargetsListOrObject, + browserslistConfigFile?: ConfigFileSearch, + browserslistEnv?: string, + // Options for @babel/generator retainLines?: boolean, comments?: boolean, @@ -241,6 +259,11 @@ export type ValidatedOptions = { generatorOpts?: {}, }; +export type NormalizedOptions = { + ...$Diff, + +targets: Targets, +}; + export type CallerMetadata = { // If 'caller' is specified, require that the name is given for debugging // messages. @@ -273,6 +296,11 @@ export type CompactOption = boolean | "auto"; export type RootInputSourceMapOption = {} | boolean; export type RootMode = "root" | "upward" | "upward-optional"; +export type TargetsListOrObject = + | Targets + | InputTargets + | $PropertyType; + export type OptionsSource = | "arguments" | "configfile" diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index 911e813734fd..80f655739d44 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -1004,6 +1004,7 @@ describe("buildConfigChain", function () { const getDefaults = () => ({ babelrc: false, configFile: false, + browserslistConfigFile: false, cwd: process.cwd(), root: process.cwd(), envName: "development", @@ -1011,6 +1012,7 @@ describe("buildConfigChain", function () { plugins: [], presets: [], cloneInputAst: true, + targets: {}, }); const realEnv = process.env.NODE_ENV; const realBabelEnv = process.env.BABEL_ENV; diff --git a/packages/babel-core/test/fixtures/targets/.browserslistrc b/packages/babel-core/test/fixtures/targets/.browserslistrc new file mode 100644 index 000000000000..6b3ad521fb07 --- /dev/null +++ b/packages/babel-core/test/fixtures/targets/.browserslistrc @@ -0,0 +1,4 @@ +chrome 80 + +[browserslist-loading-test] +chrome 70 diff --git a/packages/babel-core/test/fixtures/targets/.browserslistrc-firefox b/packages/babel-core/test/fixtures/targets/.browserslistrc-firefox new file mode 100644 index 000000000000..c2f0c40728a6 --- /dev/null +++ b/packages/babel-core/test/fixtures/targets/.browserslistrc-firefox @@ -0,0 +1 @@ +firefox 74 diff --git a/packages/babel-core/test/fixtures/targets/nested/.browserslistrc b/packages/babel-core/test/fixtures/targets/nested/.browserslistrc new file mode 100644 index 000000000000..124541b16eb8 --- /dev/null +++ b/packages/babel-core/test/fixtures/targets/nested/.browserslistrc @@ -0,0 +1 @@ +edge 14 diff --git a/packages/babel-core/test/targets.js b/packages/babel-core/test/targets.js new file mode 100644 index 000000000000..80418d10597c --- /dev/null +++ b/packages/babel-core/test/targets.js @@ -0,0 +1,141 @@ +import { loadOptions as loadOptionsOrig } from "../lib"; +import { join } from "path"; + +function loadOptions(opts) { + return loadOptionsOrig({ cwd: __dirname, ...opts }); +} + +function withTargets(targets) { + return loadOptions({ targets }); +} + +describe("targets", () => { + it("throws if invalid type", () => { + expect(() => withTargets(2)).toThrow( + ".targets must be a string, an array of strings or an object", + ); + + expect(() => withTargets([2])).toThrow( + ".targets must be a string, an array of strings or an object", + ); + + expect(() => withTargets([{}])).toThrow( + ".targets must be a string, an array of strings or an object", + ); + + expect(() => withTargets([])).not.toThrow(); + expect(() => withTargets({})).not.toThrow(); + }); + + it("throws if invalid target", () => { + expect(() => withTargets({ uglify: "2.3" })).toThrow( + /\.targets\["uglify"\] is not a valid target/, + ); + + expect(() => withTargets({ foo: "bar" })).toThrow( + /\.targets\["foo"\] is not a valid target/, + ); + + expect(() => withTargets({ firefox: 71 })).not.toThrow(); + }); + + it("throws if invalid version", () => { + expect(() => withTargets({ node: 10.1 /* or 10.10? */ })).toThrow( + `.targets["node"] must be a string or an integer number`, + ); + + expect(() => withTargets({ node: true })).toThrow( + `.targets["node"] must be a string or an integer number`, + ); + + expect(() => withTargets({ node: "10.1" })).not.toThrow(); + + expect(() => withTargets({ node: "current" })).not.toThrow(); + }); + + it("esmodules", () => { + expect(() => withTargets({ esmodules: "7" })).toThrow( + `.targets["esmodules"] must be a boolean, or undefined`, + ); + + expect(() => withTargets({ esmodules: false })).not.toThrow(); + expect(() => withTargets({ esmodules: true })).not.toThrow(); + }); + + it("browsers", () => { + expect(() => withTargets({ browsers: 2 })).toThrow( + `.targets["browsers"] must be undefined, a string or an array of strings`, + ); + + expect(() => withTargets({ browsers: [2] })).toThrow( + `.targets["browsers"] must be undefined, a string or an array of strings`, + ); + + expect(() => withTargets({ browsers: {} })).toThrow( + `.targets["browsers"] must be undefined, a string or an array of strings`, + ); + + expect(() => withTargets({ browsers: [] })).not.toThrow(); + }); +}); + +describe("browserslist", () => { + it("loads .browserslistrc by default", () => { + expect( + loadOptions({ + cwd: join(__dirname, "fixtures", "targets"), + }).targets, + ).toEqual({ chrome: "80.0.0" }); + }); + + it("loads .browserslistrc relative to the input file", () => { + expect( + loadOptions({ + cwd: join(__dirname, "fixtures", "targets"), + filename: "./nested/test.js", + }).targets, + ).toEqual({ edge: "14.0.0" }); + }); + + describe("browserslistConfigFile", () => { + it("can disable config loading", () => { + expect( + loadOptions({ + cwd: join(__dirname, "fixtures", "targets"), + browserslistConfigFile: false, + }).targets, + ).toEqual({}); + }); + + it("can specify a custom file", () => { + expect( + loadOptions({ + cwd: join(__dirname, "fixtures", "targets"), + browserslistConfigFile: "./.browserslistrc-firefox", + }).targets, + ).toEqual({ firefox: "74.0.0" }); + }); + + it("is relative to the project root", () => { + expect( + loadOptions({ + cwd: join(__dirname, "fixtures", "targets"), + root: "..", + filename: "./nested/test.js", + browserslistConfigFile: "./targets/.browserslistrc-firefox", + }).targets, + ).toEqual({ firefox: "74.0.0" }); + }); + }); + + describe("browserslistEnv", () => { + it("is forwarded to browserslist", () => { + expect( + loadOptions({ + cwd: join(__dirname, "fixtures", "targets"), + browserslistEnv: "browserslist-loading-test", + }).targets, + ).toEqual({ chrome: "70.0.0" }); + }); + }); +}); diff --git a/packages/babel-helper-compilation-targets/src/index.js b/packages/babel-helper-compilation-targets/src/index.js index 0241bcb90f26..a7948f3c961b 100644 --- a/packages/babel-helper-compilation-targets/src/index.js +++ b/packages/babel-helper-compilation-targets/src/index.js @@ -22,6 +22,7 @@ export { prettifyTargets } from "./pretty"; export { getInclusionReasons } from "./debug"; export { default as filterItems, isRequired } from "./filter-items"; export { unreleasedLabels } from "./targets"; +export { TargetNames }; const v = new OptionValidator(packageName); const browserslistDefaults = browserslist.defaults; @@ -55,8 +56,11 @@ function validateTargetNames(targets: Targets): TargetsTuple { return (targets: any); } -export function isBrowsersQueryValid(browsers: Browsers | Targets): boolean { - return typeof browsers === "string" || Array.isArray(browsers); +export function isBrowsersQueryValid(browsers: mixed): boolean %checks { + return ( + typeof browsers === "string" || + (Array.isArray(browsers) && browsers.every(b => typeof b === "string")) + ); } function validateBrowsers(browsers: Browsers | void) { @@ -207,9 +211,10 @@ export default function getTargets( } const browsers = browserslist(browsersquery, { - path: options.configPath, + config: options.config, + path: options.path ?? options.configPath, mobileToDesktop: true, - env: options.browserslistEnv, + env: options.env ?? options.browserslistEnv, }); const queryBrowsers = getLowestVersions(browsers); diff --git a/packages/babel-helper-compilation-targets/src/types.js b/packages/babel-helper-compilation-targets/src/types.js index 9e0bc3a06f5e..321e704671c3 100644 --- a/packages/babel-helper-compilation-targets/src/types.js +++ b/packages/babel-helper-compilation-targets/src/types.js @@ -22,7 +22,7 @@ export type TargetsTuple = {| [target: Target]: string, |}; -export type Browsers = string | Array; +export type Browsers = string | $ReadOnlyArray; export type InputTargets = { ...Targets, diff --git a/yarn.lock b/yarn.lock index 8b254c43002f..5515b48aaa05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -129,6 +129,7 @@ __metadata: dependencies: "@babel/code-frame": "workspace:^7.10.4" "@babel/generator": "workspace:^7.12.0" + "@babel/helper-compilation-targets": "workspace:^7.12.0" "@babel/helper-module-transforms": "workspace:^7.12.0" "@babel/helper-transform-fixture-test-runner": "workspace:^7.12.0" "@babel/helpers": "workspace:^7.10.4"