From 1aa905fae90d8b4a52ae959739eac87342deefe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Fri, 11 Dec 2020 20:05:50 +0100 Subject: [PATCH] Add `@babel/core` support for the new `assumptions` option (#12219) --- .../babel-core/src/config/cache-contexts.js | 32 ++++ packages/babel-core/src/config/full.js | 128 ++++++------- .../src/config/helpers/config-api.js | 35 ++-- packages/babel-core/src/config/partial.js | 4 +- packages/babel-core/src/config/util.js | 13 +- .../config/validation/option-assertions.js | 25 +++ .../src/config/validation/options.js | 11 ++ packages/babel-core/test/assumptions.js | 178 ++++++++++++++++++ packages/babel-core/test/config-chain.js | 1 + .../babel-helper-plugin-utils/src/index.js | 3 + .../babel-preset-env/src/normalize-options.js | 2 +- 11 files changed, 348 insertions(+), 84 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 bc15858339cf..1e1855184e10 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,62 +217,72 @@ 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 factory = maybeAsync( - value, - `You appear to be using an async plugin/preset, but Babel has been called synchronously`, - ); +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 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( + 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) { @@ -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, + cache: CacheConfigurator, ): Handler { const pluginObj = validatePluginObject(value); @@ -387,9 +385,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..24dfedc9f817 100644 --- a/packages/babel-core/src/config/helpers/config-api.js +++ b/packages/babel-core/src/config/helpers/config-api.js @@ -13,6 +13,8 @@ import { import type { CallerMetadata } from "../validation/options"; +import * as Context from "../cache-contexts"; + type EnvFunction = { (): string, ((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): ConfigAPI { +export type PluginAPI = {| + ...PresetAPI, + assumption: AssumptionFunction, +|}; + +export function makeConfigAPI( + cache: CacheConfigurator, +): 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( + 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 +91,14 @@ export function makePluginAPI( return { ...makeConfigAPI(cache), targets }; } +export function makePluginAPI( + 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..5d74daa00ec0 100644 --- a/packages/babel-core/src/config/partial.js +++ b/packages/babel-core/src/config/partial.js @@ -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); }); 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 fefda1437759..116c3df90071 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 = { @@ -431,3 +433,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..c5e99846b974 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,11 @@ type EnvPath = $ReadOnly<{ }>; export type NestingPath = RootPath | OverridesPath | EnvPath; +export const assumptionsNames = new Set([ + "mutableTemplateObject", + "setPublicClassFields", +]); + 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..3f6e58005eb1 --- /dev/null +++ b/packages/babel-core/test/assumptions.js @@ -0,0 +1,178 @@ +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: { mutableTemplateObject: true } })], + }).assumptions, + ).toEqual({ + setPublicClassFields: true, + mutableTemplateObject: 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 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); + }); + }); +}); diff --git a/packages/babel-core/test/config-chain.js b/packages/babel-core/test/config-chain.js index 653688e9ada8..f710d347ed9e 100644 --- a/packages/babel-core/test/config-chain.js +++ b/packages/babel-core/test/config-chain.js @@ -985,6 +985,7 @@ describe("buildConfigChain", function () { presets: [], cloneInputAst: true, targets: {}, + assumptions: {}, }); const realEnv = process.env.NODE_ENV; const realBabelEnv = process.env.BABEL_ENV; diff --git a/packages/babel-helper-plugin-utils/src/index.js b/packages/babel-helper-plugin-utils/src/index.js index 55fc7d6761af..5347c2470fa7 100644 --- a/packages/babel-helper-plugin-utils/src/index.js +++ b/packages/babel-helper-plugin-utils/src/index.js @@ -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) { diff --git a/packages/babel-preset-env/src/normalize-options.js b/packages/babel-preset-env/src/normalize-options.js index 417353bc4cf3..0f7d4c160f20 100644 --- a/packages/babel-preset-env/src/normalize-options.js +++ b/packages/babel-preset-env/src/normalize-options.js @@ -245,7 +245,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,