Add @babel/core support for the new assumptions option (#12219)

- Disallow setting assumptions to `false` in presets (#12498)
This commit is contained in:
Nicolò Ribaudo 2020-12-11 20:05:50 +01:00
parent cb404e4776
commit 7965c15557
11 changed files with 367 additions and 82 deletions

View File

@ -0,0 +1,32 @@
// @flow
import type { Targets } from "@babel/helper-compilation-targets";
import type { ConfigContext } from "./config-chain";
import type { CallerMetadata } from "./validation/options";
export type { ConfigContext as FullConfig };
export type FullPreset = {
...ConfigContext,
targets: Targets,
};
export type FullPlugin = {
...FullPreset,
assumptions: { [name: string]: boolean },
};
// Context not including filename since it is used in places that cannot
// process 'ignore'/'only' and other filename-based logic.
export type SimpleConfig = {
envName: string,
caller: CallerMetadata | void,
};
export type SimplePreset = {
...SimpleConfig,
targets: Targets,
};
export type SimplePlugin = {
...SimplePreset,
assumptions: { [name: string]: boolean },
};

View File

@ -14,7 +14,6 @@ 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,
@ -23,16 +22,17 @@ import {
} from "./caching";
import {
validate,
type CallerMetadata,
checkNoUnwrappedItemOptionPairs,
type PluginItem,
} from "./validation/options";
import { validatePluginObject } from "./validation/plugins";
import { makePluginAPI } from "./helpers/config-api";
import { makePluginAPI, makePresetAPI } from "./helpers/config-api";
import loadPrivatePartialConfig from "./partial";
import type { ValidatedOptions } from "./validation/options";
import * as Context from "./cache-contexts";
type LoadedDescriptor = {
value: {},
options: {},
@ -40,11 +40,6 @@ type LoadedDescriptor = {
alias: string,
};
type PluginContext = {
...ConfigContext,
targets: Targets,
};
export type { InputOptions } from "./validation/options";
export type ResolvedConfig = {
@ -56,14 +51,6 @@ export type { Plugin };
export type PluginPassList = Array<Plugin>;
export type PluginPasses = Array<PluginPassList>;
// Context not including filename since it is used in places that cannot
// process 'ignore'/'only' and other filename-based logic.
type SimpleContext = {
envName: string,
caller: CallerMetadata | void,
targets: Targets,
};
export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
inputOpts: mixed,
): Handler<ResolvedConfig | null> {
@ -85,9 +72,10 @@ export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
throw new Error("Assertion failure - plugins and presets exist");
}
const pluginContext: PluginContext = {
const pluginContext: Context.FullPlugin = {
...context,
targets: options.targets,
assumptions: options.assumptions ?? {},
};
const toDescriptor = (item: PluginItem) => {
@ -229,62 +217,72 @@ function enhanceError<T: Function>(context, fn: T): T {
/**
* Load a generic plugin/preset from the given descriptor loaded from the config object.
*/
const loadDescriptor = makeWeakCache(function* (
{ value, options, dirname, alias }: UnloadedDescriptor,
cache: CacheConfigurator<SimpleContext>,
): Handler<LoadedDescriptor> {
// Disabled presets should already have been filtered out
if (options === false) throw new Error("Assertion failure");
const makeDescriptorLoader = <Context, API>(
apiFactory: (cache: CacheConfigurator<Context>) => API,
): ((d: UnloadedDescriptor, c: Context) => Handler<LoadedDescriptor>) =>
makeWeakCache(function* (
{ value, options, dirname, alias }: UnloadedDescriptor,
cache: CacheConfigurator<Context>,
): Handler<LoadedDescriptor> {
// Disabled presets should already have been filtered out
if (options === false) throw new Error("Assertion failure");
options = options || {};
options = options || {};
let item = value;
if (typeof value === "function") {
const factory = maybeAsync(
value,
`You appear to be using an async plugin/preset, but Babel has been called synchronously`,
);
let item = value;
if (typeof value === "function") {
const factory = maybeAsync(
value,
`You appear to be using an async plugin/preset, but Babel has been called synchronously`,
);
const api = {
...context,
...makePluginAPI(cache),
};
try {
item = yield* factory(api, options, dirname);
} catch (e) {
if (alias) {
e.message += ` (While processing: ${JSON.stringify(alias)})`;
const api = {
...context,
...apiFactory(cache),
};
try {
item = yield* factory(api, options, dirname);
} catch (e) {
if (alias) {
e.message += ` (While processing: ${JSON.stringify(alias)})`;
}
throw e;
}
throw e;
}
}
if (!item || typeof item !== "object") {
throw new Error("Plugin/Preset did not return an object.");
}
if (!item || typeof item !== "object") {
throw new Error("Plugin/Preset did not return an object.");
}
if (isThenable(item)) {
yield* []; // if we want to support async plugins
if (isThenable(item)) {
yield* []; // if we want to support async plugins
throw new Error(
`You appear to be using a promise as a plugin, ` +
`which your current version of Babel does not support. ` +
`If you're using a published plugin, ` +
`you may need to upgrade your @babel/core version. ` +
`As an alternative, you can prefix the promise with "await". ` +
`(While processing: ${JSON.stringify(alias)})`,
);
}
throw new Error(
`You appear to be using a promise as a plugin, ` +
`which your current version of Babel does not support. ` +
`If you're using a published plugin, ` +
`you may need to upgrade your @babel/core version. ` +
`As an alternative, you can prefix the promise with "await". ` +
`(While processing: ${JSON.stringify(alias)})`,
);
}
return { value: item, options, dirname, alias };
});
return { value: item, options, dirname, alias };
});
const pluginDescriptorLoader = makeDescriptorLoader<Context.SimplePlugin, *>(
makePluginAPI,
);
const presetDescriptorLoader = makeDescriptorLoader<Context.SimplePreset, *>(
makePresetAPI,
);
/**
* Instantiate a plugin for the given descriptor, returning the plugin/options pair.
*/
function* loadPluginDescriptor(
descriptor: UnloadedDescriptor,
context: SimpleContext,
context: Context.SimplePlugin,
): Handler<Plugin> {
if (descriptor.value instanceof Plugin) {
if (descriptor.options) {
@ -297,14 +295,14 @@ function* loadPluginDescriptor(
}
return yield* instantiatePlugin(
yield* loadDescriptor(descriptor, context),
yield* pluginDescriptorLoader(descriptor, context),
context,
);
}
const instantiatePlugin = makeWeakCache(function* (
{ value, options, dirname, alias }: LoadedDescriptor,
cache: CacheConfigurator<SimpleContext>,
cache: CacheConfigurator<Context.SimplePlugin>,
): Handler<Plugin> {
const pluginObj = validatePluginObject(value);
@ -387,9 +385,11 @@ const validatePreset = (
*/
function* loadPresetDescriptor(
descriptor: UnloadedDescriptor,
context: PluginContext,
context: Context.FullPreset,
): Handler<ConfigChain | null> {
const preset = instantiatePreset(yield* loadDescriptor(descriptor, context));
const preset = instantiatePreset(
yield* presetDescriptorLoader(descriptor, context),
);
validatePreset(preset, context, descriptor);
return yield* buildPresetChain(preset, context);
}

View File

@ -13,6 +13,8 @@ import {
import type { CallerMetadata } from "../validation/options";
import * as Context from "../cache-contexts";
type EnvFunction = {
(): string,
<T>((string) => T): T,
@ -24,6 +26,8 @@ type CallerFactory = ((CallerMetadata | void) => mixed) => SimpleType;
type TargetsFunction = () => Targets;
type AssumptionFunction = (name: string) => boolean | void;
export type ConfigAPI = {|
version: string,
cache: SimpleCacheConfigurator,
@ -33,14 +37,19 @@ export type ConfigAPI = {|
caller?: CallerFactory,
|};
export type PluginAPI = {|
export type PresetAPI = {|
...ConfigAPI,
targets: TargetsFunction,
|};
export function makeConfigAPI<
SideChannel: { envName: string, caller: CallerMetadata | void },
>(cache: CacheConfigurator<SideChannel>): ConfigAPI {
export type PluginAPI = {|
...PresetAPI,
assumption: AssumptionFunction,
|};
export function makeConfigAPI<SideChannel: Context.SimpleConfig>(
cache: CacheConfigurator<SideChannel>,
): ConfigAPI {
const env: any = value =>
cache.using(data => {
if (typeof value === "undefined") return data.envName;
@ -70,13 +79,9 @@ export function makeConfigAPI<
};
}
export function makePluginAPI(
cache: CacheConfigurator<{
envName: string,
caller: CallerMetadata | void,
targets: Targets,
}>,
): PluginAPI {
export function makePresetAPI<SideChannel: Context.SimplePreset>(
cache: CacheConfigurator<SideChannel>,
): PresetAPI {
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
@ -86,6 +91,14 @@ export function makePluginAPI(
return { ...makeConfigAPI(cache), targets };
}
export function makePluginAPI<SideChannel: Context.SimplePlugin>(
cache: CacheConfigurator<SideChannel>,
): PluginAPI {
const assumption = name => cache.using(data => data.assumptions[name]);
return { ...makePresetAPI(cache), assumption };
}
function assertVersion(range: string | number): void {
if (typeof range === "number") {
if (!Number.isInteger(range)) {

View File

@ -117,7 +117,9 @@ export default function* loadPrivatePartialConfig(
const configChain = yield* buildRootChain(args, context);
if (!configChain) return null;
const merged: ValidatedOptions = {};
const merged: ValidatedOptions = {
assumptions: {},
};
configChain.options.forEach(opts => {
mergeOptions((merged: any), opts);
});

View File

@ -7,14 +7,13 @@ export function mergeOptions(
source: ValidatedOptions | NormalizedOptions,
): void {
for (const k of Object.keys(source)) {
if (k === "parserOpts" && source.parserOpts) {
const parserOpts = source.parserOpts;
const targetObj = (target.parserOpts = target.parserOpts || {});
if (
(k === "parserOpts" || k === "generatorOpts" || k === "assumptions") &&
source[k]
) {
const parserOpts = source[k];
const targetObj = target[k] || (target[k] = {});
mergeDefaultFields(targetObj, parserOpts);
} else if (k === "generatorOpts" && source.generatorOpts) {
const generatorOpts = source.generatorOpts;
const targetObj = (target.generatorOpts = target.generatorOpts || {});
mergeDefaultFields(targetObj, generatorOpts);
} else {
const val = source[k];
if (val !== undefined) target[k] = (val: any);

View File

@ -24,6 +24,8 @@ import type {
TargetsListOrObject,
} from "./options";
import { assumptionsNames } from "./options";
export type { RootPath } from "./options";
export type ValidatorSet = {
@ -431,3 +433,37 @@ function assertBrowserVersion(loc: GeneralPath, value: mixed) {
throw new Error(`${msg(loc)} must be a string or an integer number`);
}
export function assertAssumptions(
loc: GeneralPath,
value: mixed,
): { [name: string]: boolean } | void {
if (value === undefined) return;
if (typeof value !== "object" || value === null) {
throw new Error(`${msg(loc)} must be an object or undefined.`);
}
let root = loc;
do {
root = root.parent;
} while (root.type !== "root");
const inPreset = root.source === "preset";
for (const name of Object.keys(value)) {
const subLoc = access(loc, name);
if (!assumptionsNames.has(name)) {
throw new Error(`${msg(subLoc)} is not a supported assumption.`);
}
if (typeof value[name] !== "boolean") {
throw new Error(`${msg(subLoc)} must be a boolean.`);
}
if (inPreset && value[name] === false) {
throw new Error(
`${msg(subLoc)} cannot be set to 'false' inside presets.`,
);
}
}
return (value: any);
}

View File

@ -29,6 +29,7 @@ import {
type ValidatorSet,
type Validator,
type OptionPath,
assertAssumptions,
} from "./option-assertions";
import type { UnloadedDescriptor } from "../config-descriptors";
@ -108,6 +109,9 @@ const COMMON_VALIDATORS: ValidatorSet = {
passPerPreset: (assertBoolean: Validator<
$PropertyType<ValidatedOptions, "passPerPreset">,
>),
assumptions: (assertAssumptions: Validator<
$PropertyType<ValidatedOptions, "assumptions">,
>),
env: (assertEnvSet: Validator<$PropertyType<ValidatedOptions, "env">>),
overrides: (assertOverridesList: Validator<
@ -221,6 +225,8 @@ export type ValidatedOptions = {
plugins?: PluginList,
passPerPreset?: boolean,
assumptions?: { [name: string]: boolean },
// browserslists-related options
targets?: TargetsListOrObject,
browserslistConfigFile?: ConfigFileSearch,
@ -325,6 +331,11 @@ type EnvPath = $ReadOnly<{
}>;
export type NestingPath = RootPath | OverridesPath | EnvPath;
export const assumptionsNames = new Set<string>([
"mutableTemplateObject",
"setPublicClassFields",
]);
function getSource(loc: NestingPath): OptionsSource {
return loc.type === "root" ? loc.source : getSource(loc.parent);
}

View File

@ -0,0 +1,188 @@
import { loadOptions as loadOptionsOrig, transformSync } from "../lib";
function loadOptions(opts) {
return loadOptionsOrig({ cwd: __dirname, ...opts });
}
function withAssumptions(assumptions) {
return loadOptions({ assumptions });
}
describe("assumptions", () => {
it("throws if invalid name", () => {
expect(() => withAssumptions({ foo: true })).toThrow(
`.assumptions["foo"] is not a supported assumption.`,
);
expect(() => withAssumptions({ setPublicClassFields: true })).not.toThrow();
});
it("throws if not boolean", () => {
expect(() => withAssumptions({ setPublicClassFields: "yes" })).toThrow(
`.assumptions["setPublicClassFields"] must be a boolean.`,
);
expect(() => withAssumptions({ setPublicClassFields: true })).not.toThrow();
expect(() =>
withAssumptions({ setPublicClassFields: false }),
).not.toThrow();
});
it("can be enabled by presets", () => {
expect(
loadOptions({
assumptions: {
setPublicClassFields: true,
},
presets: [() => ({ assumptions: { mutableTemplateObject: true } })],
}).assumptions,
).toEqual({
setPublicClassFields: true,
mutableTemplateObject: true,
});
});
it("cannot be disabled by presets", () => {
expect(() =>
loadOptions({
presets: [() => ({ assumptions: { mutableTemplateObject: false } })],
}),
).toThrow(
` .assumptions["mutableTemplateObject"] cannot be set to 'false' inside presets.`,
);
});
it("can be queried from plugins", () => {
let setPublicClassFields;
let unknownAssumption;
transformSync("", {
configFile: false,
browserslistConfigFile: false,
assumptions: {
setPublicClassFields: true,
},
plugins: [
api => {
setPublicClassFields = api.assumption("setPublicClassFields");
// Unknown assumptions don't throw, so that plugins can keep compat
// with older @babel/core versions when they introduce support for
// a new assumption.
unknownAssumption = api.assumption("unknownAssumption");
return {};
},
],
});
expect(setPublicClassFields).toBe(true);
expect(unknownAssumption).toBe(undefined);
});
it("cannot be queried from presets", () => {
let assumptionFn;
transformSync("", {
configFile: false,
browserslistConfigFile: false,
presets: [
api => {
assumptionFn = api.assumption;
return {};
},
],
});
expect(assumptionFn).toBeUndefined();
});
describe("plugin cache", () => {
const makePlugin = () =>
jest.fn(api => {
api.assumption("setPublicClassFields");
return {};
});
const run = (plugin, assumptions) =>
transformSync("", {
assumptions,
configFile: false,
browserslistConfigFile: false,
plugins: [plugin],
});
it("is not invalidated when assumptions don't change", () => {
const plugin = makePlugin();
run(plugin, {
setPublicClassFields: true,
mutableTemplateObject: false,
});
run(plugin, {
setPublicClassFields: true,
mutableTemplateObject: false,
});
expect(plugin).toHaveBeenCalledTimes(1);
});
it("is not invalidated when unused assumptions change", () => {
const plugin = makePlugin();
run(plugin, {
setPublicClassFields: true,
mutableTemplateObject: false,
});
run(plugin, {
setPublicClassFields: true,
mutableTemplateObject: true,
});
expect(plugin).toHaveBeenCalledTimes(1);
});
it("is invalidated when used assumptions change", () => {
const plugin = makePlugin();
run(plugin, {
setPublicClassFields: true,
mutableTemplateObject: false,
});
run(plugin, {
setPublicClassFields: false,
mutableTemplateObject: true,
});
expect(plugin).toHaveBeenCalledTimes(2);
});
it("is invalidated when used assumptions are added", () => {
const plugin = makePlugin();
run(plugin, {
mutableTemplateObject: false,
});
run(plugin, {
mutableTemplateObject: false,
setPublicClassFields: true,
});
expect(plugin).toHaveBeenCalledTimes(2);
});
it("is invalidated when used assumptions are removed", () => {
const plugin = makePlugin();
run(plugin, {
setPublicClassFields: true,
mutableTemplateObject: false,
});
run(plugin, {
mutableTemplateObject: true,
});
expect(plugin).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -985,6 +985,7 @@ describe("buildConfigChain", function () {
presets: [],
cloneInputAst: true,
targets: {},
assumptions: {},
});
const realEnv = process.env.NODE_ENV;
const realBabelEnv = process.env.BABEL_ENV;

View File

@ -26,6 +26,9 @@ const apiPolyfills = {
targets: () => () => {
return {};
},
// This is supported starting from Babel 7.13
// TODO(Babel 8): Remove this polyfill
assumption: () => () => {},
};
function copyApiObject(api) {

View File

@ -244,7 +244,7 @@ export default function normalizeOptions(opts: Options) {
opts.ignoreBrowserslistConfig,
false,
),
loose: v.validateBooleanOption(TopLevelOptions.loose, opts.loose, false),
loose: v.validateBooleanOption(TopLevelOptions.loose, opts.loose),
modules: validateModulesOption(opts.modules),
shippedProposals: v.validateBooleanOption(
TopLevelOptions.shippedProposals,