- Repo:
babel/babel
- Start Date: 2020-06-08
- RFC PR: babel/babel#12219
- Related Issues:
- Authors: Nicolò Ribaudo
- Champion: Nicolò Ribaudo
- Implementors: Nicolò Ribaudo
- Released in Babel 7.13.0
Many of our plugins have a loose
option, which enables the compiler to make some assumptions about the code you are writing. It uses these to ignore certain edge cases and generate a smaller or faster output.
The loose
option has been around since 6to5
: at first as a top level option, and then it was split into each plugin as a catch all for anything that we wanted to be less spec compliant.
However, loose
has some problems: namely the term itself is not really descriptive at all and should be named after the part of the language it affects rather than the plugin that's compiling it (see the "Motivation" section for more details). A lot of this behavior is documented with a single example, so in the future we’ll want to be specific with the assumptions themselves and maybe provide a set of tests/codesandboxes explaining the differences.
This RFC proposes introducing a new top-level option, "assumptions"
, which is an object containing different flags that can simplify the code that Babel generates.
These flags have two characteristics:
- They specify something about a specific language feature, and not about a specific Babel plugin.
- Different flags can toggle different unrelated optimizations in the same plugin, instead of toggling both of them with a single generic
"loose"
option.
// babel.config.json
{
"assumptions": {
"noDocumentAll": true,
"pureGetters": true,
"iterableIsArray": true
},
"presets": ["@babel/preset-env"]
}
Input / Output
// input code
for (const el of arr) {
el.logger?.log("Ok!");
}
// output code
for (let i = 0; i < arr.length; i++) {
const el = arr[i];
el.logger == null ? void 0 : el.logger.log("Ok!");
}
// babel.config.json
{
"assumptions": {
"noDocumentAll": false,
"pureGetters": true,
"iterableIsArray": true
},
"presets": ["@babel/preset-env"]
}
Input / Output
// input code
for (const el of arr) {
el.logger?.log("Ok!");
}
// output code
for (let i = 0; i < arr.length; i++) {
const el = arr[i];
el.logger === null || el.logger === void 0 ? void 0 : el.logger.log("Ok!");
}
The loose
options have different problems:
- "What is loose?" Since
loose
is not descriptive, it requires users to check a plugin's docs to see what assumptions it makes (and the docs often only partially mention whatloose
means in a specific context). - "How loose is loose?" Is making
loose
"looser" a breaking change? We have been considering it as such: for this reason, we have been introducing differentloose
-like options in some plugins. For example,transform-for-of
supportsloose
,assumeArray
andallowArrayLike
. - There are cross-dependencies between the
loose
option of different plugins: for example,proposal-class-properties
's loose mode must always matchproposal-private-methods
's (otherwise we throw an error). This has already caused problems: babel/babel#11622. - Some plugins should be aware of other plugins'
loose
option value. For example, we sometimes need to partially transpile optional chaining in the class properties plugin (babel/babel#11248). Should we add a newoptionalChainingLoose
option toproposal-private-methods
andproposal-class-properties
? Or should we use a cross-plugin communication channel to share it? What about when someone has the private methods plugin enabled, but not the optional chaining one?
This RFC also solves another problem, which is not directly related to the points above:
@babel/preset-env
has aloose
option, which is forwarded to all the plugins it enables. When a user wants to setloose
only for a specific feature, they have to manually install the plugin and explicitly add it to their configuration (babel/babel#6978).
This RFC introduces a new top-level option: assumptions
. This is an object containing different flags to mark different assumptions about the input code as safe and thus allow optimizing them.
type Assumptions = {
[assumption: string]: boolean
};
This new option should be allowed in the following locations:
- In programmatic options passed, for example, to
babel.transform()
or tobabel-loader
. - In
babel.config.json
and.babelrc.json
top-level options. - In
overrides
blocks. This is useful, for example, to enable a possiblenoDocumentAll
assumption on a folder containing server-side code and not inside one containing client-side code. - In
env
blocks. This is mostly included for completeness, but I can't think about an use case for it: assumptions are about the input code, which isn't affected by env variables.
Additionally, "assumptions"
should also be allowed inside presets.
There are two main kinds of presets: framework-specific presets, like babel-preset-react-app
, and company-wide presets, used to share the same plugins accross different projects. Since assumptions describe the input code and not the used plugins, this new option fits better in company-wide presets where the person writing the preset and the person writing the input code have less "degrees of separation", but there are also use cases in framework-specific presets: for example, babel-preset-react-app
enables loose: true
for @babel/plugin-proposal-class-properties
.
To avoid conflicts between assumptions set in presets, they will only be able to enable them (i.e. set them to true
). The only way to disable an already enabled assumption is to explicitly do it in a configuration file or in programmatic options. (discussion)
Multiple "assumptions"
objects should be merged using Object.assign
, and not overwritten like other options.
This makes it easy, for example, to enable one additional assumption for a specific folder. Also, it still keeps the ability of disabling an assumption simply by setting it to false
.
They should be merged considering the following precedence, which is the same as what is already used for other options:
- preset <
babel.config.json
<.babelrc.json
(far from the compiled file)<.babelrc.json
(near to the compiled file) < programmatic options - top-level <
env
section <overrides
<overrides.env
Assumptions set inside a preset are not boxed inside the preset but are applied to all the plugins, for two reasons:
- conceptually, the assumptions describe the input file and not a specific plugin enabled inside the preset;
- practically, this makes it possible to have a single "personal" or "company" preset containing assumptions which apply to a user's coding practices, and re-use it in different projects without duplicating the list of assumptions.
In order to expose assumptions defined inside presets to every plugin, we need to first resolve and instantiate all the presets, and then all the plugins with the resolved assumptions. This also means that we cannot provide the defined assumptions list to the presets, because it hasn't been finalized yet when they are instantiated. This is implemented by babel/babel#11689).
The first parameter passed to the plugins (often known as api
) should have a new method: assumptions(name: string): boolean | undefined
, which returns whether or not the assumption has been enabled. If a plugin is asking about an assumption not enabled or not supported by the used @babel/core
version, it will return undefined
.
This is implemented as a function and not as an object whose properties reflect the assumptions because it configures the plugins' caching (they are reinstantiated when an assumption they use changes).
The different loose
or loose
-like options we currently have in our plugins map to these assumptions:
ℹ️ I'm not particularly attatched to these option names: I tried to choose something descriptive for their behavior, but they can all change.
Assumption | Behavior | Current option | Repl | Notes |
---|---|---|---|---|
ignoreToPrimitiveHint |
Transform `a${x}b` to "a" + x + "b" instead of "a".concat(x, "b") |
loose in transform-template-literals |
||
mutableTemplateObject |
Don't use Object.freeze for the template object created for tagged template literals. This effectively means using the taggedTemplateLiteralLoose helper instead of taggedTemplateLiteral |
loose in transform-template-literals |
||
ignoreFunctionLength |
The .length of a function should be cropped at the first default argument. When this option is enabled, ignore this spec requirement and don't rely on the arguments object |
loose in transform-parameters |
🔗 | |
iterableIsArray |
When using an iterable (in array destructuring, for-of or with spreads), assume that the iterable object is an Array |
loose in transform-destructuring and transform-spread , assumeArray in transform-for-of |
🔗 (it doesn't support assumeArray ) |
1 |
arrayLikeIsIterable |
Allow array-like objects to be used where an iterable is expected. This can be useful, for example, to iterate DOM collections in older browsers | allowArrayLike in transform-destructuring , transform-spread and transform-for-of |
1 | |
skipForOfIteratorClosing |
When using for-of with an iterator, it should always be closed with .return() and with .throw() in case of an error. This option allows skipping those methods |
loose in transform-for-of |
||
objectRestNoSymbols |
When using rest in object destructuring, don't copy symbol keys. This effectively means using the objectWithoutPropertiesLoose helper instead of objectWithoutProperties |
loose in transform-destructuring and proposal-object-rest-spread (not documented) |
🔗 | |
setSpreadProperties |
When using object spread, use Object.assign to copy the properties instead of cloning their property descriptors with Object.defineProperty |
loose &useBuiltIns in proposal-object-rest-spread |
||
setComputedProperties |
When using computed object properties, use [[Set]] semantics (i.e. use an assignment) instead of [[Define]] |
loose in computed-properties |
🔗 | |
setClassMethods |
When declaring classes, use [[Set]] semantics (i.e. use an assignment) instead of [[Define]] . This doesn't preserve the correct enumerability. |
loose in transform-classes |
🔗 | |
setPublicClassFields |
When using computed public class fields, use [[Set]] semantics (i.e. use an assignment) instead of [[Define]] |
loose in proposal-class-properties |
🔗 | |
privateFieldsAsProperties |
Instead of storing private fields and methods using a WeakMap or a WeakSet , define them as own non-enumerable properties of the class instance |
loose in proposal-class-properties (not documented), in proposal-private-methods and proposal-private-property-in-object |
🔗 | 2 |
superIsCallableConstructor |
When this option is enabled, super(arg1) will be transpiled to BaseClass.call(this, arg1) . This means that it won't work with native classes or with built-ins, but only with compiled classes or ES5 constructors |
loose in transform-classes |
||
constantSuper |
The super binding in classes can be changed using setPrototypeOf , so it's not possible to statically know it. With this option Babel can assume that the superclass is never changed at runtime. |
loose in transform-classes , proposal-class-properties , proposal-private-methods and proposal-decorators . It should also be added to transform-object-super . |
🔗 | |
noClassCalls |
Assume that classes are always instantiated with new and that the code never tries to call them as a function. This lets Babel skip the this instanceof ThisClass check (_classCallCheck ). |
loose in transform-classes |
||
noDocumentAll |
When compiling the ?? and ?. operators, assume that they are never used with document.all and thus == null is safe |
loose in proposal-optional-chaining and proposal-nullish-coalescing-operator |
🔗 | 3 |
pureGetters |
When an expression that might invoke getters needs to be evaluated multiple times, Babel caches its value. When this option is enabled, the expression can be safely re-evaluated. For example, a.b?.() can be compiled to a.b != null && a.b() without caching a.b |
loose in proposal-optional-chaining and in proposal-object-rest-spread (not documented) |
🔗 | |
enumerableModuleMeta |
When compiling ESM to CJS, Babel defines a non-enumerable, non-witable, non-configurable __esModule property on the exports object. When this option is enabled, that property is set using a simple assignment |
loose in transform-modules-commonjs |
||
constantReexports |
When re-exporting an imported value, assume that it's value doesn't change and set it with a simple assignment | loose in transform-modules-commonjs , transform-modules-umd , transform-modules-amd (not documented) |
||
noNewArrows |
Assume that the code never tries to instantiate arrow function using new , so Babel can avoid injecting checks to prevent it |
spec in transform-arrow-functions |
4 |
iterableIsArray
is not compatible witharrayLikeIsIterable
, even if array-like objects could work withiterableIsArray
in cases where we only rely on indexed access and not on array methods.- Currently, the
loose
option must be the same for these plugins or it will throw an error. We have a workaround in@babel/preset-env
to allow settingloose
there differently from these plugins (babel/babel#11634) ?.
also needs to be compiled by the private fields and methods plugins (babel/babel#11248, but currently there is no way to set it toloose
- The current default behavior is to consider this assumption as valid, and only produce 100% spec-compliant code when the
spec
option is enabled.
- It's not clear if we still need
inheritsLoose
helper injected by thetransform-classes
plugin (#5 (comment by @jridgewell)).
We will only define new assumptions for standard ECMAScript features or for very stable stage 3 proposals (following the same convention we use for @babel/preset-env
's shippedProposals
option).
However, existing assumptions can be used by plugins for proposals in earlier stages of the TC39 process: for example, the #priv in obj
plugin is likely affected by the same assumptions that affect the class private fields plugin.
-
This RFC introduces a very big number of new top-level options (20), and makes it relatively cheap to add new ones. Having a big number of options can add more "tooling fatigue" on the shoulders of our users, and it can make it hard for us to properly document them. However, these new options replace 22 existing plugin options, many of which have the same name (
loose
) but different behaviors. -
(@JLHwung) Some
assumptions
are only effective in one plugin, but users will have to go through their presets/plugins to see if they have opt-in to this plugins.
-
We could decide to add these new descriptive options names directly in plugins options, instead of in
@babel/core
. This solves the problem of "what does loose mean?", but it doesn't solve the problem of having the same assumption in different plugins. -
We could decide to add a simple top-level
loose
option, like we did in Babel 5: this solves the problem of multiple plugins sharing the same assumption. However, it would be even more obscure than the currentloose
options inside the plugins (which at least give some context). Also, having an all-or-nothing toggle means that often you would have to disable every optimization just because you rely on a single edge case.
We can introduce these options without introducing breaking changes, because it's opt-in. There are three possible compatibility strategies between the existing loose
options and assumptions
that we can choose:
-
Allow both,
assumptions
has higher precedence because it's the new recommended option:export default function transformClassProperties(api, options) { const { setPublicClassFields = options.loose } = api.assumptions;
-
Allow both,
loose
has higher precedence because it "nearer" to the plugin:export default function transformClassProperties(api, options) { const setPublicClassFields = options.loose ?? api.assumptions.setPublicClassFields;
-
Disallow using both at the same time. This will make the migration faster but a little harder:
export default function transformClassProperties(api, options) { let { setPublicClassFields } = options.loose; if (options.loose != null) { if (setPublicClassFields != null) throw new Error(); setPublicClassFields = options.loose; }
I prefer the first alternative, since it makes it possible for our users to gradually migrate to the new options and gradually makes the loose
options noops in users' configs while migrating.
This RFC proposed the addition of a new set of options in a way that every option can affect multiple plugins, and a plugin can be affected by multiple options.
Documenting the different loose
and loose
-like options wasn't too hard: every plugin had a bunch of those options, and we could document them in the plugin's page.
A possible structure for the documentation could be a two-entry table, with all the plugins in the rows and the assumptions in the columns, marking which assumption affects which plugins. However, this would lead to an enormous highly sparse table, impossible to fit in a normal web page.
I think that the best way of documenting these options is to put all of them in a single long list, divided in two sections: assumptions that affect standard features, and assumptions that only affect proposals. For each assumption we should write:
- The option name
- It's description
- An example (which could be interactive, using CodeSandbox)
- The affected plugins
In the docs of each affected plugin, we should write the assumptions that affect it and link to the corresponding entry in the assumptions list. We shouldn't duplicate the description of what each assumption does, because duplicating it would likely lead to the two copies become out of sync.
We can also provide a tool to check that the all the assumptions hold. This can't be done with an ESLint plugin because almost all the assumptions are about runtime semantics hard to statically analyze, but we can do it using a Babel plugin which should run while testing, and which injects assertions in the transpiled code:
// input code
foo?.bar;
// output code
(() => {
const _tmp = foo;
if (_tmp == null && _tmp !== null && _tmp !== undefined) {
throw new Error(`
Invalid assumption - noDocumentAll.
"noDocumentAll: true" implies that ?. and ?? are never
used with the special document.all object.
This assumption has been violated, so you should
disable it in your config.
`);
}
return _tmp;
})?.bar
Config
// babel.config.json
{
"assumptions": {
"noDocumentAll": true
},
"env": {
"test": {
"plugins": ["@babel/validate-assumptions"]
}
}
}
- Should we try to pass to the presets at least a partial
assumptions
object? Currently@babel/preset-env
relies onloose
to enable/disable thetypeof-symbol
plugin. ANSWER: No, presets can only produce assumptions and not consume them. - Should we validate the list of
assumptions
in@babel/core
, and disallow unknown ones? This would make it impossible for third-party plugins to introduce their own assumptions, but it also means that it's easier for us to introduce new assumptions without risking ecosystem incompatibilities. ANSWER: Yes, the compatibility problems are easily solved checking Babel's version. - Should assumptions always default to
false
? Currently everything defaults to being spec-compliant, except for thearrow-functions
plugin which has aspec: true
option as an opt-in. ANSWER: Yes, we will makenoNewArrows
default tofalse
in Babel 8. - (@JLHwung) Should we further infer
assumptions
fromtargets
? i.e.{ targets: "node 8" }
can imply{ assumptions: { noDocumentAll: true } }
.