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

@@ -9,7 +9,7 @@ import {
makeWeakCacheSync,
type CacheConfigurator,
} from "../caching";
import makeAPI, { type PluginAPI } from "../helpers/config-api";
import { makeConfigAPI, type ConfigAPI } from "../helpers/config-api";
import { makeStaticFileCache } from "./utils";
import loadCjsOrMjsDefault from "./module-types";
import pathPatternToRegex from "../pattern-to-regex";
@@ -203,7 +203,7 @@ const readConfigJS = makeStrongCache(function* readConfigJS(
let assertCache = false;
if (typeof options === "function") {
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;
}

View File

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

View File

@@ -1,6 +1,8 @@
// @flow
import semver from "semver";
import type { Targets } from "@babel/helper-compilation-targets";
import { version as coreVersion } from "../../";
import {
assertSimpleType,
@@ -20,7 +22,9 @@ type EnvFunction = {
type CallerFactory = ((CallerMetadata | void) => mixed) => SimpleType;
export type PluginAPI = {|
type TargetsFunction = () => Targets;
export type ConfigAPI = {|
version: string,
cache: SimpleCacheConfigurator,
env: EnvFunction,
@@ -29,9 +33,14 @@ export type PluginAPI = {|
caller?: CallerFactory,
|};
export default function makeAPI(
cache: CacheConfigurator<{ envName: string, caller: CallerMetadata | void }>,
): PluginAPI {
export type PluginAPI = {|
...ConfigAPI,
targets: TargetsFunction,
|};
export function makeConfigAPI<
SideChannel: { envName: string, caller: CallerMetadata | void },
>(cache: CacheConfigurator<SideChannel>): ConfigAPI {
const env: any = value =>
cache.using(data => {
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 {
if (typeof range === "number") {
if (!Number.isInteger(range)) {

View File

@@ -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;
const options: NormalizedOptions = {
...merged,
targets: resolveTargets(merged, absoluteRootDir, filename),
options.plugins = configChain.plugins.map(descriptor =>
createItemFromDescriptor(descriptor),
);
options.presets = configChain.presets.map(descriptor =>
createItemFromDescriptor(descriptor),
);
// 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,
@@ -201,7 +209,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;
@@ -209,7 +217,7 @@ class PartialConfig {
files: Set<string>;
constructor(
options: ValidatedOptions,
options: NormalizedOptions,
babelrc: string | void,
ignore: 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
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) {

View File

@@ -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,55 @@ 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`,
);
}
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
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<ValidatedOptions, "ignore">,
>),
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 = {
@@ -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<ValidatedOptions, { targets: any }>,
+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<InputTargets, "browsers">;
export type OptionsSource =
| "arguments"
| "configfile"