From 4c025f5f2a766990155f44102e900f298f55699b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Mon, 19 Oct 2020 19:20:58 +0200 Subject: [PATCH] Add @babel/core support for the new "assumptions" option --- .../babel-core/src/config/cache-contexts.js | 32 +++ packages/babel-core/src/config/full.js | 116 +++++------ .../src/config/helpers/config-api.js | 30 ++- packages/babel-core/src/config/partial.js | 7 +- packages/babel-core/src/config/util.js | 13 +- .../config/validation/option-assertions.js | 25 +++ .../src/config/validation/options.js | 28 +++ packages/babel-core/test/assumptions.js | 183 ++++++++++++++++++ packages/babel-core/test/config-chain.js | 1 + 9 files changed, 364 insertions(+), 71 deletions(-) create mode 100644 packages/babel-core/src/config/cache-contexts.js create mode 100644 packages/babel-core/test/assumptions.js diff --git a/packages/babel-core/src/config/cache-contexts.js b/packages/babel-core/src/config/cache-contexts.js new file mode 100644 index 000000000000..969362c0c42c --- /dev/null +++ b/packages/babel-core/src/config/cache-contexts.js @@ -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 }, +}; diff --git a/packages/babel-core/src/config/full.js b/packages/babel-core/src/config/full.js index fff0468f4366..4e175e1d31d1 100644 --- a/packages/babel-core/src/config/full.js +++ b/packages/babel-core/src/config/full.js @@ -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; export type PluginPasses = Array; -// 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 { @@ -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,55 +217,65 @@ function enhanceError(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, -): Handler { - // Disabled presets should already have been filtered out - if (options === false) throw new Error("Assertion failure"); - - options = options || {}; - - let item = value; - if (typeof value === "function") { - const api = { - ...context, - ...makePluginAPI(cache), - }; - try { - item = value(api, options, dirname); - } catch (e) { - if (alias) { - e.message += ` (While processing: ${JSON.stringify(alias)})`; +const makeDescriptorLoader = ( + apiFactory: (cache: CacheConfigurator) => API, +): ((d: UnloadedDescriptor, c: Context) => Handler) => + makeWeakCache(function* ( + { value, options, dirname, alias }: UnloadedDescriptor, + cache: CacheConfigurator, + ): Handler { + // Disabled presets should already have been filtered out + if (options === false) throw new Error("Assertion failure"); + + options = options || {}; + + let item = value; + if (typeof value === "function") { + const api = { + ...context, + ...apiFactory(cache), + }; + try { + item = value(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 (typeof item.then === "function") { - yield* []; // if we want to support async plugins + if (typeof item.then === "function") { + yield* []; // if we want to support async plugins - throw new Error( - `You appear to be using an async 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.`, - ); - } + throw new Error( + `You appear to be using an async 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.`, + ); + } - return { value: item, options, dirname, alias }; -}); + return { value: item, options, dirname, alias }; + }); + +const pluginDescriptorLoader = makeDescriptorLoader( + makePluginAPI, +); +const presetDescriptorLoader = makeDescriptorLoader( + makePresetAPI, +); /** * Instantiate a plugin for the given descriptor, returning the plugin/options pair. */ function* loadPluginDescriptor( descriptor: UnloadedDescriptor, - context: SimpleContext, + context: Context.SimplePlugin, ): Handler { if (descriptor.value instanceof Plugin) { if (descriptor.options) { @@ -290,14 +288,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, + cache: CacheConfigurator, ): Handler { const pluginObj = validatePluginObject(value); @@ -380,9 +378,11 @@ const validatePreset = ( */ function* loadPresetDescriptor( descriptor: UnloadedDescriptor, - context: PluginContext, + context: Context.FullPreset, ): Handler { - const preset = instantiatePreset(yield* loadDescriptor(descriptor, context)); + const preset = instantiatePreset( + yield* presetDescriptorLoader(descriptor, context), + ); validatePreset(preset, context, descriptor); return yield* buildPresetChain(preset, context); } diff --git a/packages/babel-core/src/config/helpers/config-api.js b/packages/babel-core/src/config/helpers/config-api.js index 70d793995d1a..5cff79d9a87e 100644 --- a/packages/babel-core/src/config/helpers/config-api.js +++ b/packages/babel-core/src/config/helpers/config-api.js @@ -24,6 +24,8 @@ type CallerFactory = ((CallerMetadata | void) => mixed) => SimpleType; type TargetsFunction = () => Targets; +type AssumptionFunction = (name: string) => boolean; + export type ConfigAPI = {| version: string, cache: SimpleCacheConfigurator, @@ -33,11 +35,16 @@ export type ConfigAPI = {| caller?: CallerFactory, |}; -export type PluginAPI = {| +export type PresetAPI = {| ...ConfigAPI, targets: TargetsFunction, |}; +export type PluginAPI = {| + ...PresetAPI, + assumption: AssumptionFunction, +|}; + export function makeConfigAPI< SideChannel: { envName: string, caller: CallerMetadata | void }, >(cache: CacheConfigurator): ConfigAPI { @@ -70,13 +77,13 @@ export function makeConfigAPI< }; } -export function makePluginAPI( - cache: CacheConfigurator<{ +export function makePresetAPI< + SideChannel: { envName: string, caller: CallerMetadata | void, targets: Targets, - }>, -): PluginAPI { + }, +>(cache: CacheConfigurator): 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 +93,19 @@ export function makePluginAPI( return { ...makeConfigAPI(cache), targets }; } +export function makePluginAPI< + SideChannel: { + envName: string, + caller: CallerMetadata | void, + targets: Targets, + assumptions: { [name: string]: boolean }, + }, +>(cache: CacheConfigurator): 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)) { diff --git a/packages/babel-core/src/config/partial.js b/packages/babel-core/src/config/partial.js index 4bc40e3249db..ca610dce3587 100644 --- a/packages/babel-core/src/config/partial.js +++ b/packages/babel-core/src/config/partial.js @@ -117,7 +117,12 @@ export default function* loadPrivatePartialConfig( const configChain = yield* buildRootChain(args, context); if (!configChain) return null; - const merged: ValidatedOptions = {}; + const merged: ValidatedOptions = { + // TODO(Babel 8): everything should default to false. Remove this object. + assumptions: { + newableArrowFunctions: true, + }, + }; configChain.options.forEach(opts => { mergeOptions((merged: any), opts); }); diff --git a/packages/babel-core/src/config/util.js b/packages/babel-core/src/config/util.js index 491a2ea950fa..891e09c90975 100644 --- a/packages/babel-core/src/config/util.js +++ b/packages/babel-core/src/config/util.js @@ -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); diff --git a/packages/babel-core/src/config/validation/option-assertions.js b/packages/babel-core/src/config/validation/option-assertions.js index d500149599ed..e945bd713baf 100644 --- a/packages/babel-core/src/config/validation/option-assertions.js +++ b/packages/babel-core/src/config/validation/option-assertions.js @@ -24,6 +24,8 @@ import type { TargetsListOrObject, } from "./options"; +import { assumptionsNames } from "./options"; + export type { RootPath } from "./options"; export type ValidatorSet = { @@ -438,3 +440,26 @@ 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.`); + } + + 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.`); + } + } + + return (value: any); +} diff --git a/packages/babel-core/src/config/validation/options.js b/packages/babel-core/src/config/validation/options.js index 56b7655583e4..69d25f1db6f7 100644 --- a/packages/babel-core/src/config/validation/options.js +++ b/packages/babel-core/src/config/validation/options.js @@ -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, >), + assumptions: (assertAssumptions: Validator< + $PropertyType, + >), env: (assertEnvSet: Validator<$PropertyType>), 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,28 @@ type EnvPath = $ReadOnly<{ }>; export type NestingPath = RootPath | OverridesPath | EnvPath; +export const assumptionsNames = new Set([ + "arrayLikeIsIterable", + "arrayIndexedIteration", + "copyReexports", + "ignoreFunctionLength", + "ignoreToPrimitiveHint", + "inheritsAsObjectCreate", + "iterableIsArray", + "mutableTemplateObject", + "newableArrowFunctions", + "noDocumentAll", + "objectRestNoSymbols", + "privateFieldsAsPublic", + "setClassMethods", + "setComputedProperties", + "setModuleMeta", + "setPublicClassFields", + "setSpreadProperties", + "skipForOfIterationClosing", + "superAsFunctionCall", +]); + function getSource(loc: NestingPath): OptionsSource { return loc.type === "root" ? loc.source : getSource(loc.parent); } diff --git a/packages/babel-core/test/assumptions.js b/packages/babel-core/test/assumptions.js new file mode 100644 index 000000000000..a4e62b481f51 --- /dev/null +++ b/packages/babel-core/test/assumptions.js @@ -0,0 +1,183 @@ +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 set by presets", () => { + expect( + loadOptions({ + assumptions: { + setPublicClassFields: true, + }, + presets: [() => ({ assumptions: { setClassMethods: true } })], + }).assumptions, + ).toEqual({ + setPublicClassFields: true, + setClassMethods: true, + // This is enabled by default + newableArrowFunctions: true, + }); + }); + + 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 default to "false" and 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(false); + }); + + 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"); + api.assumption("mutableTemplateObject"); + 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, + setClassMethods: false, + }); + run(plugin, { + setPublicClassFields: true, + setClassMethods: false, + }); + + expect(plugin).toHaveBeenCalledTimes(1); + }); + + it("is not invalidated when unused assumptions change", () => { + const plugin = makePlugin(); + + run(plugin, { + setPublicClassFields: true, + setClassMethods: false, + }); + run(plugin, { + setPublicClassFields: true, + setClassMethods: true, + }); + + expect(plugin).toHaveBeenCalledTimes(1); + }); + + it("is invalidated when used assumptions change", () => { + const plugin = makePlugin(); + + run(plugin, { + setPublicClassFields: true, + setClassMethods: false, + }); + run(plugin, { + setPublicClassFields: false, + setClassMethods: true, + }); + + expect(plugin).toHaveBeenCalledTimes(2); + }); + + it("is invalidated when used assumptions are added", () => { + const plugin = makePlugin(); + + run(plugin, { + setPublicClassFields: true, + setClassMethods: false, + }); + run(plugin, { + setPublicClassFields: false, + setClassMethods: true, + mutableTemplateObject: true, + }); + + expect(plugin).toHaveBeenCalledTimes(2); + }); + + it("is invalidated when used assumptions are removed", () => { + const plugin = makePlugin(); + + run(plugin, { + setPublicClassFields: true, + setClassMethods: false, + }); + run(plugin, { + setClassMethods: true, + }); + + expect(plugin).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index e64ff3f00f1b..57e9c4ce6b09 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -1013,6 +1013,7 @@ describe("buildConfigChain", function () { presets: [], cloneInputAst: true, targets: {}, + assumptions: { newableArrowFunctions: true }, }); const realEnv = process.env.NODE_ENV; const realBabelEnv = process.env.BABEL_ENV;