Add targets and browserslist* options to @babel/core (#12189)

This commit is contained in:
Nicolò Ribaudo 2020-12-10 13:12:21 +01:00
parent 31ca15ef58
commit cb404e4776
38 changed files with 733 additions and 106 deletions

View File

@ -39,13 +39,16 @@
}, },
"browser": { "browser": {
"./lib/config/files/index.js": "./lib/config/files/index-browser.js", "./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", "./lib/transform-file.js": "./lib/transform-file-browser.js",
"./src/config/files/index.js": "./src/config/files/index-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" "./src/transform-file.js": "./src/transform-file-browser.js"
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": "workspace:^7.12.13", "@babel/code-frame": "workspace:^7.12.13",
"@babel/generator": "workspace:^7.12.17", "@babel/generator": "workspace:^7.12.17",
"@babel/helper-compilation-targets": "workspace:^7.12.17",
"@babel/helper-module-transforms": "workspace:^7.12.17", "@babel/helper-module-transforms": "workspace:^7.12.17",
"@babel/helpers": "workspace:^7.12.17", "@babel/helpers": "workspace:^7.12.17",
"@babel/parser": "workspace:^7.12.17", "@babel/parser": "workspace:^7.12.17",

View File

@ -9,7 +9,7 @@ import {
makeWeakCacheSync, makeWeakCacheSync,
type CacheConfigurator, type CacheConfigurator,
} from "../caching"; } from "../caching";
import makeAPI, { type PluginAPI } from "../helpers/config-api"; import { makeConfigAPI, type ConfigAPI } from "../helpers/config-api";
import { makeStaticFileCache } from "./utils"; import { makeStaticFileCache } from "./utils";
import loadCjsOrMjsDefault from "./module-types"; import loadCjsOrMjsDefault from "./module-types";
import pathPatternToRegex from "../pattern-to-regex"; import pathPatternToRegex from "../pattern-to-regex";
@ -203,7 +203,7 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
let assertCache = false; let assertCache = false;
if (typeof options === "function") { if (typeof options === "function") {
yield* []; // if we want to make it possible to use async configs yield* []; // if we want to make it possible to use async configs
options = ((options: any): (api: PluginAPI) => {})(makeAPI(cache)); options = ((options: any): (api: ConfigAPI) => {})(makeConfigAPI(cache));
assertCache = true; assertCache = true;
} }

View File

@ -14,6 +14,7 @@ import {
type PresetInstance, type PresetInstance,
} from "./config-chain"; } from "./config-chain";
import type { UnloadedDescriptor } from "./config-descriptors"; import type { UnloadedDescriptor } from "./config-descriptors";
import type { Targets } from "@babel/helper-compilation-targets";
import traverse from "@babel/traverse"; import traverse from "@babel/traverse";
import { import {
makeWeakCache, makeWeakCache,
@ -27,7 +28,7 @@ import {
type PluginItem, type PluginItem,
} from "./validation/options"; } from "./validation/options";
import { validatePluginObject } from "./validation/plugins"; import { validatePluginObject } from "./validation/plugins";
import makeAPI from "./helpers/config-api"; import { makePluginAPI } from "./helpers/config-api";
import loadPrivatePartialConfig from "./partial"; import loadPrivatePartialConfig from "./partial";
import type { ValidatedOptions } from "./validation/options"; import type { ValidatedOptions } from "./validation/options";
@ -39,6 +40,11 @@ type LoadedDescriptor = {
alias: string, alias: string,
}; };
type PluginContext = {
...ConfigContext,
targets: Targets,
};
export type { InputOptions } from "./validation/options"; export type { InputOptions } from "./validation/options";
export type ResolvedConfig = { export type ResolvedConfig = {
@ -55,6 +61,7 @@ export type PluginPasses = Array<PluginPassList>;
type SimpleContext = { type SimpleContext = {
envName: string, envName: string,
caller: CallerMetadata | void, caller: CallerMetadata | void,
targets: Targets,
}; };
export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig( export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
@ -78,6 +85,11 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
throw new Error("Assertion failure - plugins and presets exist"); throw new Error("Assertion failure - plugins and presets exist");
} }
const pluginContext: PluginContext = {
...context,
targets: options.targets,
};
const toDescriptor = (item: PluginItem) => { const toDescriptor = (item: PluginItem) => {
const desc = getItemDescriptor(item); const desc = getItemDescriptor(item);
if (!desc) { if (!desc) {
@ -112,12 +124,12 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
// in the previous pass. // in the previous pass.
if (descriptor.ownPass) { if (descriptor.ownPass) {
presets.push({ presets.push({
preset: yield* loadPresetDescriptor(descriptor, context), preset: yield* loadPresetDescriptor(descriptor, pluginContext),
pass: [], pass: [],
}); });
} else { } else {
presets.unshift({ presets.unshift({
preset: yield* loadPresetDescriptor(descriptor, context), preset: yield* loadPresetDescriptor(descriptor, pluginContext),
pass: pluginDescriptorsPass, pass: pluginDescriptorsPass,
}); });
} }
@ -172,7 +184,7 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
const descriptor: UnloadedDescriptor = descs[i]; const descriptor: UnloadedDescriptor = descs[i];
if (descriptor.options !== false) { if (descriptor.options !== false) {
try { try {
pass.push(yield* loadPluginDescriptor(descriptor, context)); pass.push(yield* loadPluginDescriptor(descriptor, pluginContext));
} catch (e) { } catch (e) {
if (e.code === "BABEL_UNKNOWN_PLUGIN_PROPERTY") { if (e.code === "BABEL_UNKNOWN_PLUGIN_PROPERTY") {
// print special message for `plugins: ["@babel/foo", { foo: "option" }]` // print special message for `plugins: ["@babel/foo", { foo: "option" }]`
@ -235,7 +247,7 @@ const loadDescriptor = makeWeakCache(function* (
const api = { const api = {
...context, ...context,
...makeAPI(cache), ...makePluginAPI(cache),
}; };
try { try {
item = yield* factory(api, options, dirname); item = yield* factory(api, options, dirname);
@ -375,7 +387,7 @@ const validatePreset = (
*/ */
function* loadPresetDescriptor( function* loadPresetDescriptor(
descriptor: UnloadedDescriptor, descriptor: UnloadedDescriptor,
context: ConfigContext, context: PluginContext,
): Handler<ConfigChain | null> { ): Handler<ConfigChain | null> {
const preset = instantiatePreset(yield* loadDescriptor(descriptor, context)); const preset = instantiatePreset(yield* loadDescriptor(descriptor, context));
validatePreset(preset, context, descriptor); validatePreset(preset, context, descriptor);

View File

@ -1,6 +1,8 @@
// @flow // @flow
import semver from "semver"; import semver from "semver";
import type { Targets } from "@babel/helper-compilation-targets";
import { version as coreVersion } from "../../"; import { version as coreVersion } from "../../";
import { import {
assertSimpleType, assertSimpleType,
@ -20,7 +22,9 @@ type EnvFunction = {
type CallerFactory = ((CallerMetadata | void) => mixed) => SimpleType; type CallerFactory = ((CallerMetadata | void) => mixed) => SimpleType;
export type PluginAPI = {| type TargetsFunction = () => Targets;
export type ConfigAPI = {|
version: string, version: string,
cache: SimpleCacheConfigurator, cache: SimpleCacheConfigurator,
env: EnvFunction, env: EnvFunction,
@ -29,9 +33,14 @@ export type PluginAPI = {|
caller?: CallerFactory, caller?: CallerFactory,
|}; |};
export default function makeAPI( export type PluginAPI = {|
cache: CacheConfigurator<{ envName: string, caller: CallerMetadata | void }>, ...ConfigAPI,
): PluginAPI { targets: TargetsFunction,
|};
export function makeConfigAPI<
SideChannel: { envName: string, caller: CallerMetadata | void },
>(cache: CacheConfigurator<SideChannel>): ConfigAPI {
const env: any = value => const env: any = value =>
cache.using(data => { cache.using(data => {
if (typeof value === "undefined") return data.envName; if (typeof value === "undefined") return data.envName;
@ -61,6 +70,22 @@ export default function makeAPI(
}; };
} }
export function makePluginAPI(
cache: CacheConfigurator<{
envName: string,
caller: CallerMetadata | void,
targets: Targets,
}>,
): PluginAPI {
const targets = () =>
// We are using JSON.parse/JSON.stringify because it's only possible to cache
// primitive values. We can safely stringify the targets object because it
// only contains strings as its properties.
// Please make the Record and Tuple proposal happen!
JSON.parse(cache.using(data => JSON.stringify(data.targets)));
return { ...makeConfigAPI(cache), targets };
}
function assertVersion(range: string | number): void { function assertVersion(range: string | number): void {
if (typeof range === "number") { if (typeof range === "number") {
if (!Number.isInteger(range)) { if (!Number.isInteger(range)) {

View File

@ -14,6 +14,7 @@ import { getEnv } from "./helpers/environment";
import { import {
validate, validate,
type ValidatedOptions, type ValidatedOptions,
type NormalizedOptions,
type RootMode, type RootMode,
} from "./validation/options"; } from "./validation/options";
@ -24,6 +25,7 @@ import {
type ConfigFile, type ConfigFile,
type IgnoreFile, type IgnoreFile,
} from "./files"; } from "./files";
import { resolveTargets } from "./resolve-targets";
function* resolveRootMode( function* resolveRootMode(
rootDir: string, rootDir: string,
@ -61,7 +63,7 @@ function* resolveRootMode(
} }
type PrivPartialConfig = { type PrivPartialConfig = {
options: ValidatedOptions, options: NormalizedOptions,
context: ConfigContext, context: ConfigContext,
fileHandling: FileHandling, fileHandling: FileHandling,
ignore: IgnoreFile | void, ignore: IgnoreFile | void,
@ -115,30 +117,36 @@ export default function* loadPrivatePartialConfig(
const configChain = yield* buildRootChain(args, context); const configChain = yield* buildRootChain(args, context);
if (!configChain) return null; if (!configChain) return null;
const options = {}; const merged: ValidatedOptions = {};
configChain.options.forEach(opts => { configChain.options.forEach(opts => {
mergeOptions(options, opts); mergeOptions((merged: any), opts);
}); });
const options: NormalizedOptions = {
...merged,
targets: resolveTargets(merged, absoluteRootDir, filename),
// Tack the passes onto the object itself so that, if this object is // 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 // passed back to Babel a second time, it will be in the right structure
// to not change behavior. // to not change behavior.
options.cloneInputAst = cloneInputAst; cloneInputAst,
options.babelrc = false; babelrc: false,
options.configFile = false; configFile: false,
options.passPerPreset = false; browserslistConfigFile: false,
options.envName = context.envName; passPerPreset: false,
options.cwd = context.cwd; envName: context.envName,
options.root = context.root; cwd: context.cwd,
options.filename = root: context.root,
typeof context.filename === "string" ? context.filename : undefined; filename:
typeof context.filename === "string" ? context.filename : undefined,
options.plugins = configChain.plugins.map(descriptor => plugins: configChain.plugins.map(descriptor =>
createItemFromDescriptor(descriptor), createItemFromDescriptor(descriptor),
); ),
options.presets = configChain.presets.map(descriptor => presets: configChain.presets.map(descriptor =>
createItemFromDescriptor(descriptor), createItemFromDescriptor(descriptor),
); ),
};
return { return {
options, options,
@ -201,7 +209,7 @@ class PartialConfig {
* These properties are public, so any changes to them should be considered * These properties are public, so any changes to them should be considered
* a breaking change to Babel's API. * a breaking change to Babel's API.
*/ */
options: ValidatedOptions; options: NormalizedOptions;
babelrc: string | void; babelrc: string | void;
babelignore: string | void; babelignore: string | void;
config: string | void; config: string | void;
@ -209,7 +217,7 @@ class PartialConfig {
files: Set<string>; files: Set<string>;
constructor( constructor(
options: ValidatedOptions, options: NormalizedOptions,
babelrc: string | void, babelrc: string | void,
ignore: string | void, ignore: string | void,
config: string | void, config: string | void,

View File

@ -0,0 +1,26 @@
// @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 };
}
// $FlowIgnore it thinks that targets.esmodules doesn't exist.
if (targets && targets.esmodules) {
targets = { ...targets, esmodules: "intersect" };
}
return getTargets((targets: any), {
ignoreBrowserslistConfig: true,
browserslistEnv: options.browserslistEnv,
});
}

View File

@ -0,0 +1,39 @@
// @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<browserType>): $Exact<nodeType>);
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 };
}
// $FlowIgnore it thinks that targets.esmodules doesn't exist.
if (targets && targets.esmodules) {
targets = { ...targets, esmodules: "intersect" };
}
let configFile;
if (typeof options.browserslistConfigFile === "string") {
configFile = path.resolve(root, options.browserslistConfigFile);
}
return getTargets((targets: any), {
ignoreBrowserslistConfig: options.browserslistConfigFile === false,
configFile,
configPath: filename ?? root,
browserslistEnv: options.browserslistEnv,
});
}

View File

@ -1,10 +1,10 @@
// @flow // @flow
import type { ValidatedOptions } from "./validation/options"; import type { ValidatedOptions, NormalizedOptions } from "./validation/options";
export function mergeOptions( export function mergeOptions(
target: ValidatedOptions, target: ValidatedOptions,
source: ValidatedOptions, source: ValidatedOptions | NormalizedOptions,
): void { ): void {
for (const k of Object.keys(source)) { for (const k of Object.keys(source)) {
if (k === "parserOpts" && source.parserOpts) { if (k === "parserOpts" && source.parserOpts) {

View File

@ -1,5 +1,10 @@
// @flow // @flow
import {
isBrowsersQueryValid,
TargetNames,
} from "@babel/helper-compilation-targets";
import type { import type {
ConfigFileSearch, ConfigFileSearch,
BabelrcSearch, BabelrcSearch,
@ -16,6 +21,7 @@ import type {
NestingPath, NestingPath,
CallerMetadata, CallerMetadata,
RootMode, RootMode,
TargetsListOrObject,
} from "./options"; } from "./options";
export type { RootPath } from "./options"; export type { RootPath } from "./options";
@ -373,3 +379,55 @@ function assertPluginTarget(loc: GeneralPath, value: mixed): PluginTarget {
} }
return value; 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`,
);
}
const browsersLoc = access(loc, "browsers");
const esmodulesLoc = access(loc, "esmodules");
assertBrowsersList(browsersLoc, value.browsers);
assertBoolean(esmodulesLoc, 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`);
}

View File

@ -1,5 +1,7 @@
// @flow // @flow
import type { InputTargets, Targets } from "@babel/helper-compilation-targets";
import type { ConfigItem } from "../item"; import type { ConfigItem } from "../item";
import Plugin from "../plugin"; import Plugin from "../plugin";
@ -23,6 +25,7 @@ import {
assertSourceMaps, assertSourceMaps,
assertCompact, assertCompact,
assertSourceType, assertSourceType,
assertTargets,
type ValidatorSet, type ValidatorSet,
type Validator, type Validator,
type OptionPath, type OptionPath,
@ -77,6 +80,16 @@ const NONPRESET_VALIDATORS: ValidatorSet = {
$PropertyType<ValidatedOptions, "ignore">, $PropertyType<ValidatedOptions, "ignore">,
>), >),
only: (assertIgnoreList: Validator<$PropertyType<ValidatedOptions, "only">>), only: (assertIgnoreList: Validator<$PropertyType<ValidatedOptions, "only">>),
targets: (assertTargets: Validator<
$PropertyType<ValidatedOptions, "targets">,
>),
browserslistConfigFile: (assertConfigFileSearch: Validator<
$PropertyType<ValidatedOptions, "browserslistConfigFile">,
>),
browserslistEnv: (assertString: Validator<
$PropertyType<ValidatedOptions, "browserslistEnv">,
>),
}; };
const COMMON_VALIDATORS: ValidatorSet = { const COMMON_VALIDATORS: ValidatorSet = {
@ -208,6 +221,11 @@ export type ValidatedOptions = {
plugins?: PluginList, plugins?: PluginList,
passPerPreset?: boolean, passPerPreset?: boolean,
// browserslists-related options
targets?: TargetsListOrObject,
browserslistConfigFile?: ConfigFileSearch,
browserslistEnv?: string,
// Options for @babel/generator // Options for @babel/generator
retainLines?: boolean, retainLines?: boolean,
comments?: boolean, comments?: boolean,
@ -241,6 +259,11 @@ export type ValidatedOptions = {
generatorOpts?: {}, generatorOpts?: {},
}; };
export type NormalizedOptions = {
...$Diff<ValidatedOptions, { targets: any }>,
+targets: Targets,
};
export type CallerMetadata = { export type CallerMetadata = {
// If 'caller' is specified, require that the name is given for debugging // If 'caller' is specified, require that the name is given for debugging
// messages. // messages.
@ -273,6 +296,11 @@ export type CompactOption = boolean | "auto";
export type RootInputSourceMapOption = {} | boolean; export type RootInputSourceMapOption = {} | boolean;
export type RootMode = "root" | "upward" | "upward-optional"; export type RootMode = "root" | "upward" | "upward-optional";
export type TargetsListOrObject =
| Targets
| InputTargets
| $PropertyType<InputTargets, "browsers">;
export type OptionsSource = export type OptionsSource =
| "arguments" | "arguments"
| "configfile" | "configfile"

View File

@ -976,6 +976,7 @@ describe("buildConfigChain", function () {
const getDefaults = () => ({ const getDefaults = () => ({
babelrc: false, babelrc: false,
configFile: false, configFile: false,
browserslistConfigFile: false,
cwd: process.cwd(), cwd: process.cwd(),
root: process.cwd(), root: process.cwd(),
envName: "development", envName: "development",
@ -983,6 +984,7 @@ describe("buildConfigChain", function () {
plugins: [], plugins: [],
presets: [], presets: [],
cloneInputAst: true, cloneInputAst: true,
targets: {},
}); });
const realEnv = process.env.NODE_ENV; const realEnv = process.env.NODE_ENV;
const realBabelEnv = process.env.BABEL_ENV; const realBabelEnv = process.env.BABEL_ENV;

View File

@ -0,0 +1 @@
;

View File

@ -0,0 +1,4 @@
{
"targets": ["firefox 64", "node 8"],
"plugins": ["./plugin"]
}

View File

@ -0,0 +1,2 @@
;
"plugin: {\"firefox\":\"64.0.0\",\"node\":\"8.17.0\"}"

View File

@ -0,0 +1,14 @@
module.exports = function (api) {
const { types: t } = api;
const targets = api.targets();
return {
visitor: {
Program(path) {
const output = t.stringLiteral(`plugin: ${JSON.stringify(targets)}`);
path.pushContainer("body", output);
},
},
};
};

View File

@ -0,0 +1 @@
;

View File

@ -0,0 +1,4 @@
{
"targets": ["firefox 64", "node 8"],
"presets": ["./preset"]
}

View File

@ -0,0 +1,2 @@
;
"preset: {\"firefox\":\"64.0.0\",\"node\":\"8.17.0\"}"

View File

@ -0,0 +1,18 @@
module.exports = function (api) {
const targets = api.targets();
return {
plugins: [plugin],
};
function plugin({ types: t }) {
return {
visitor: {
Program(path) {
const output = t.stringLiteral(`preset: ${JSON.stringify(targets)}`);
path.pushContainer("body", output);
},
},
};
}
};

View File

@ -0,0 +1,4 @@
chrome 80
[browserslist-loading-test]
chrome 70

View File

@ -0,0 +1 @@
firefox 74

View File

@ -0,0 +1 @@
edge 14

View File

@ -0,0 +1,150 @@
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" });
});
});
it("esmodules and browsers are intersected", () => {
expect(
withTargets({
esmodules: true,
browsers: "chrome >= 80, firefox >= 30",
}).targets,
).toEqual({ chrome: "80.0.0", firefox: "60.0.0" });
});
});

View File

@ -9,6 +9,7 @@ import {
semverMin, semverMin,
isUnreleasedVersion, isUnreleasedVersion,
getLowestUnreleased, getLowestUnreleased,
getHighestUnreleased,
} from "./utils"; } from "./utils";
import { OptionValidator } from "@babel/helper-validator-option"; import { OptionValidator } from "@babel/helper-validator-option";
import { browserNameMap } from "./targets"; import { browserNameMap } from "./targets";
@ -21,9 +22,11 @@ export { prettifyTargets } from "./pretty";
export { getInclusionReasons } from "./debug"; export { getInclusionReasons } from "./debug";
export { default as filterItems, isRequired } from "./filter-items"; export { default as filterItems, isRequired } from "./filter-items";
export { unreleasedLabels } from "./targets"; export { unreleasedLabels } from "./targets";
export { TargetNames };
const ESM_SUPPORT = browserModulesData["es6.module"];
const v = new OptionValidator(PACKAGE_JSON.name); const v = new OptionValidator(PACKAGE_JSON.name);
const browserslistDefaults = browserslist.defaults;
function validateTargetNames(targets: Targets): TargetsTuple { function validateTargetNames(targets: Targets): TargetsTuple {
const validTargets = Object.keys(TargetNames); const validTargets = Object.keys(TargetNames);
@ -39,8 +42,11 @@ function validateTargetNames(targets: Targets): TargetsTuple {
return (targets: any); return (targets: any);
} }
export function isBrowsersQueryValid(browsers: Browsers | Targets): boolean { export function isBrowsersQueryValid(browsers: mixed): boolean %checks {
return typeof browsers === "string" || Array.isArray(browsers); return (
typeof browsers === "string" ||
(Array.isArray(browsers) && browsers.every(b => typeof b === "string"))
);
} }
function validateBrowsers(browsers: Browsers | void) { function validateBrowsers(browsers: Browsers | void) {
@ -149,55 +155,83 @@ function generateTargets(inputTargets: InputTargets): Targets {
return ((input: any): Targets); return ((input: any): Targets);
} }
export default function getTargets( function resolveTargets(queries: Browsers): Targets {
inputTargets: InputTargets = {}, const resolved = browserslist(queries, { mobileToDesktop: true });
options: Object = {}, return getLowestVersions(resolved);
): Targets {
let { browsers } = inputTargets;
// `esmodules` as a target indicates the specific set of browsers supporting ES Modules.
// These values OVERRIDE the `browsers` field.
if (inputTargets.esmodules) {
const supportsESModules = browserModulesData["es6.module"];
browsers = Object.keys(supportsESModules)
.map(browser => `${browser} ${supportsESModules[browser]}`)
.join(", ");
} }
// Parse browsers target via browserslist type GetTargetsOption = {
const browsersquery = validateBrowsers(browsers); // This is not the path of the config file, but the path where start searching it from
configPath?: string,
// The path of the config file
configFile?: string,
// The env to pass to browserslist
browserslistEnv?: string,
// true to disable config loading
ignoreBrowserslistConfig?: boolean,
};
export default function getTargets(
inputTargets: InputTargets = {},
options: GetTargetsOption = {},
): Targets {
let { browsers, esmodules } = inputTargets;
validateBrowsers(browsers);
const input = generateTargets(inputTargets); const input = generateTargets(inputTargets);
let targets: TargetsTuple = validateTargetNames(input); let targets: TargetsTuple = validateTargetNames(input);
const shouldParseBrowsers = !!browsersquery; const shouldParseBrowsers = !!browsers;
const hasTargets = shouldParseBrowsers || Object.keys(targets).length > 0; const hasTargets = shouldParseBrowsers || Object.keys(targets).length > 0;
const shouldSearchForConfig = const shouldSearchForConfig =
!options.ignoreBrowserslistConfig && !hasTargets; !options.ignoreBrowserslistConfig && !hasTargets;
if (shouldParseBrowsers || shouldSearchForConfig) { if (!browsers && shouldSearchForConfig) {
browsers =
browserslist.loadConfig({
config: options.configFile,
path: options.configPath,
env: options.browserslistEnv,
}) ??
// If no targets are passed, we need to overwrite browserslist's defaults // If no targets are passed, we need to overwrite browserslist's defaults
// so that we enable all transforms (acting like the now deprecated // so that we enable all transforms (acting like the now deprecated
// preset-latest). // preset-latest).
// [];
// Note, if browserslist resolves the config (ex. package.json), then usage
// of `defaults` in queries will be different since we don't want to break
// the behavior of "no targets is the same as preset-latest".
if (!hasTargets) {
browserslist.defaults = [];
} }
const browsers = browserslist(browsersquery, { // `esmodules` as a target indicates the specific set of browsers supporting ES Modules.
path: options.configPath, // These values OVERRIDE the `browsers` field.
mobileToDesktop: true, if (esmodules && (esmodules !== "intersect" || !browsers)) {
env: options.browserslistEnv, browsers = Object.keys(ESM_SUPPORT)
}); .map(browser => `${browser} >= ${ESM_SUPPORT[browser]}`)
.join(", ");
esmodules = false;
}
if (browsers) {
const queryBrowsers = resolveTargets(browsers);
if (esmodules === "intersect") {
for (const browser of Object.keys(queryBrowsers)) {
const version = queryBrowsers[browser];
if (ESM_SUPPORT[browser]) {
queryBrowsers[browser] = getHighestUnreleased(
version,
semverify(ESM_SUPPORT[browser]),
browser,
);
} else {
delete queryBrowsers[browser];
}
}
}
const queryBrowsers = getLowestVersions(browsers);
targets = Object.assign(queryBrowsers, targets); targets = Object.assign(queryBrowsers, targets);
// Reset browserslist defaults
browserslist.defaults = browserslistDefaults;
} }
// Parse remaining targets // Parse remaining targets

View File

@ -22,11 +22,17 @@ export type TargetsTuple = {|
[target: Target]: string, [target: Target]: string,
|}; |};
export type Browsers = string | Array<string>; export type Browsers = string | $ReadOnlyArray<string>;
export type InputTargets = { export type InputTargets = {
...Targets, ...Targets,
browsers?: Browsers, browsers?: Browsers,
esmodules?: boolean,
// When `true`, this completely replaces the `browsers` option.
// When `intersect`, this is intersected with the `browsers`
// option (giving the higher browsers as the result).
// TODO(Babel 8): Make `true` behave like `intersect` and
// remove `intersect`.
esmodules?: boolean | "intersect",
}; };

View File

@ -51,6 +51,14 @@ export function getLowestUnreleased(a: string, b: string, env: string): string {
return semverMin(a, b); return semverMin(a, b);
} }
export function getHighestUnreleased(
a: string,
b: string,
env: string,
): string {
return getLowestUnreleased(a, b, env) === a ? b : a;
}
export function getLowestImplementedVersion( export function getLowestImplementedVersion(
plugin: Targets, plugin: Targets,
environment: Target, environment: Target,

View File

@ -1,5 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getTargets esmodules can be intersected with a .browserslistrc file 1`] = `
Object {
"chrome": "70.0.0",
"firefox": "60.0.0",
}
`;
exports[`getTargets esmodules can be intersected with the browsers option 1`] = `
Object {
"chrome": "70.0.0",
"firefox": "60.0.0",
}
`;
exports[`getTargets esmodules explicit browser versions have the precedence over 'esmodules' 1`] = `
Object {
"chrome": "20.0.0",
"firefox": "70.0.0",
}
`;
exports[`getTargets esmodules returns browser supporting modules and keyed browser overrides 1`] = ` exports[`getTargets esmodules returns browser supporting modules and keyed browser overrides 1`] = `
Object { Object {
"android": "61.0.0", "android": "61.0.0",

View File

@ -0,0 +1,2 @@
chrome >= 70
firefox >= 30

View File

@ -1,4 +1,5 @@
import browserslist from "browserslist"; import browserslist from "browserslist";
import { join } from "path";
import getTargets from ".."; import getTargets from "..";
describe("getTargets", () => { describe("getTargets", () => {
@ -233,6 +234,48 @@ describe("getTargets", () => {
}), }),
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it("can be intersected with the browsers option", () => {
expect(
getTargets({
esmodules: "intersect",
browsers: ["chrome >= 70", "firefox >= 30"],
}),
).toMatchSnapshot();
});
it("can be intersected with a .browserslistrc file", () => {
expect(
getTargets(
{
esmodules: "intersect",
},
{ configPath: join(__dirname, "fixtures", "foo.js") },
),
).toMatchSnapshot();
});
it("explicit browser versions have the precedence over 'esmodules'", () => {
expect(
getTargets({
browsers: "chrome 5, firefox 5",
esmodules: "intersect",
chrome: 20,
firefox: 70,
}),
).toMatchSnapshot();
});
it("'intersect' behaves like 'true' if no browsers are specified", () => {
expect(
getTargets(
{ esmodules: "intersect" },
{ ignoreBrowserslistConfig: true },
),
).toEqual(
getTargets({ esmodules: true }, { ignoreBrowserslistConfig: true }),
);
});
}); });
describe("node", () => { describe("node", () => {

View File

@ -1,19 +1,33 @@
export function declare(builder) { export function declare(builder) {
return (api, options, dirname) => { return (api, options, dirname) => {
if (!api.assertVersion) { let clonedApi;
// Inject a custom version of 'assertVersion' for Babel 6 and early
// versions of Babel 7's beta that didn't have it. for (const name of Object.keys(apiPolyfills)) {
api = Object.assign(copyApiObject(api), { if (api[name]) continue;
assertVersion(range) {
throwVersionError(range, api.version); // TODO: Use ??= when flow lets us to do so
}, clonedApi = clonedApi ?? copyApiObject(api);
}); clonedApi[name] = apiPolyfills[name](clonedApi);
} }
return builder(api, options || {}, dirname); return builder(clonedApi ?? api, options || {}, dirname);
}; };
} }
const apiPolyfills = {
// Not supported by Babel 7 and early versions of Babel 7 beta.
// It's important that this is polyfilled for older Babel versions
// since it's needed to report the version mismatch.
assertVersion: api => range => {
throwVersionError(range, api.version);
},
// This is supported starting from Babel 7.13
// TODO(Babel 8): Remove this polyfill
targets: () => () => {
return {};
},
};
function copyApiObject(api) { function copyApiObject(api) {
// Babel >= 7 <= beta.41 passed the API as a new object that had // Babel >= 7 <= beta.41 passed the API as a new object that had
// babel/core as the prototype. While slightly faster, it also // babel/core as the prototype. While slightly faster, it also

View File

@ -206,6 +206,26 @@ export const getPolyfillPlugins = ({
return polyfillPlugins; return polyfillPlugins;
}; };
function getLocalTargets(
optionsTargets,
ignoreBrowserslistConfig,
configPath,
browserslistEnv,
) {
if (optionsTargets?.esmodules && optionsTargets.browsers) {
console.warn(`
@babel/preset-env: esmodules and browsers targets have been specified together.
\`browsers\` target, \`${optionsTargets.browsers.toString()}\` will be ignored.
`);
}
return getTargets(
// $FlowIgnore optionsTargets doesn't have an "uglify" property anymore
(optionsTargets: InputTargets),
{ ignoreBrowserslistConfig, configPath, browserslistEnv },
);
}
function supportsStaticESM(caller) { function supportsStaticESM(caller) {
return !!caller?.supportsStaticESM; return !!caller?.supportsStaticESM;
} }
@ -225,6 +245,8 @@ function supportsTopLevelAwait(caller) {
export default declare((api, opts) => { export default declare((api, opts) => {
api.assertVersion(7); api.assertVersion(7);
const babelTargets = api.targets();
const { const {
bugfixes, bugfixes,
configPath, configPath,
@ -243,6 +265,16 @@ export default declare((api, opts) => {
browserslistEnv, browserslistEnv,
} = normalizeOptions(opts); } = normalizeOptions(opts);
let targets = babelTargets;
if (
// If any browserslist-related option is specified, fallback to the old
// behavior of not using the targets specified in the top-level options.
opts.targets ||
opts.configPath ||
opts.browserslistEnv ||
opts.ignoreBrowserslistConfig
) {
if (!process.env.BABEL_8_BREAKING) { if (!process.env.BABEL_8_BREAKING) {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var hasUglifyTarget = false; var hasUglifyTarget = false;
@ -258,20 +290,13 @@ option \`forceAllTransforms: true\` instead.
} }
} }
if (optionsTargets?.esmodules && optionsTargets.browsers) { targets = getLocalTargets(
console.warn(` optionsTargets,
@babel/preset-env: esmodules and browsers targets have been specified together. ignoreBrowserslistConfig,
\`browsers\` target, \`${optionsTargets.browsers.toString()}\` will be ignored. configPath,
`); browserslistEnv,
}
const targets = getTargets(
// $FlowIgnore optionsTargets doesn't have an "uglify" property anymore
(optionsTargets: InputTargets),
{ ignoreBrowserslistConfig, configPath, browserslistEnv },
); );
const include = transformIncludesAndExcludes(optionsInclude); }
const exclude = transformIncludesAndExcludes(optionsExclude);
const transformTargets = ( const transformTargets = (
process.env.BABEL_8_BREAKING process.env.BABEL_8_BREAKING
@ -281,6 +306,9 @@ option \`forceAllTransforms: true\` instead.
? {} ? {}
: targets; : targets;
const include = transformIncludesAndExcludes(optionsInclude);
const exclude = transformIncludesAndExcludes(optionsExclude);
const compatData = getPluginList(shippedProposals, bugfixes); const compatData = getPluginList(shippedProposals, bugfixes);
const shouldSkipExportNamespaceFrom = const shouldSkipExportNamespaceFrom =
(modules === "auto" && api.caller?.(supportsExportNamespaceFrom)) || (modules === "auto" && api.caller?.(supportsExportNamespaceFrom)) ||

View File

@ -0,0 +1 @@
foo?.bar;

View File

@ -0,0 +1,8 @@
{
"validateLogs": true,
"ignoreOutput": true,
"targets": "chrome 80",
"presets": [
["env", { "debug": true, "targets": "chrome 60" }]
]
}

View File

@ -0,0 +1,26 @@
@babel/preset-env: `DEBUG` option
Using targets:
{
"chrome": "60"
}
Using modules transform: auto
Using plugins:
proposal-numeric-separator { "chrome":"60" }
proposal-logical-assignment-operators { "chrome":"60" }
proposal-nullish-coalescing-operator { "chrome":"60" }
proposal-optional-chaining { "chrome":"60" }
proposal-json-strings { "chrome":"60" }
proposal-optional-catch-binding { "chrome":"60" }
proposal-async-generator-functions { "chrome":"60" }
syntax-object-rest-spread { "chrome":"60" }
transform-dotall-regex { "chrome":"60" }
proposal-unicode-property-regex { "chrome":"60" }
transform-named-capturing-groups-regex { "chrome":"60" }
proposal-export-namespace-from { "chrome":"60" }
transform-modules-commonjs { "chrome":"60" }
proposal-dynamic-import { "chrome":"60" }
Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.

View File

@ -0,0 +1 @@
foo?.bar;

View File

@ -0,0 +1,8 @@
{
"validateLogs": true,
"ignoreOutput": true,
"targets": "chrome 80",
"presets": [
["env", { "debug": true }]
]
}

View File

@ -0,0 +1,23 @@
@babel/preset-env: `DEBUG` option
Using targets:
{
"chrome": "80"
}
Using modules transform: auto
Using plugins:
syntax-numeric-separator { "chrome":"80" }
proposal-logical-assignment-operators { "chrome":"80" }
syntax-nullish-coalescing-operator { "chrome":"80" }
syntax-optional-chaining { "chrome":"80" }
syntax-json-strings { "chrome":"80" }
syntax-optional-catch-binding { "chrome":"80" }
syntax-async-generators { "chrome":"80" }
syntax-object-rest-spread { "chrome":"80" }
transform-modules-commonjs { "chrome":"80" }
proposal-dynamic-import { "chrome":"80" }
proposal-export-namespace-from {}
Using polyfills: No polyfills were added, since the `useBuiltIns` option was not set.

View File

@ -131,6 +131,7 @@ __metadata:
dependencies: dependencies:
"@babel/code-frame": "workspace:^7.12.13" "@babel/code-frame": "workspace:^7.12.13"
"@babel/generator": "workspace:^7.12.17" "@babel/generator": "workspace:^7.12.17"
"@babel/helper-compilation-targets": "workspace:^7.12.17"
"@babel/helper-module-transforms": "workspace:^7.12.17" "@babel/helper-module-transforms": "workspace:^7.12.17"
"@babel/helper-transform-fixture-test-runner": "workspace:*" "@babel/helper-transform-fixture-test-runner": "workspace:*"
"@babel/helpers": "workspace:^7.12.17" "@babel/helpers": "workspace:^7.12.17"