Class features loose should have precedence over preset-env (#11634)

* Class features loose should have precedence over preset-env

* Comment

* Update packages/babel-helper-create-class-features-plugin/src/features.js

[skip ci]

Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>

* Future proof

* Add warning when loose mode changes automatically

* Better message

Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
This commit is contained in:
Nicolò Ribaudo 2020-05-29 23:56:32 +02:00 committed by GitHub
parent 5b24d79875
commit e6d873e061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 380 additions and 35 deletions

View File

@ -8,6 +8,15 @@ export const FEATURES = Object.freeze({
privateIn: 1 << 4, privateIn: 1 << 4,
}); });
const featuresSameLoose = new Map([
[FEATURES.fields, "@babel/plugin-proposal-class-properties"],
[FEATURES.privateMethods, "@babel/plugin-proposal-private-methods"],
[
FEATURES.privateIn,
"@babel/plugin-proposal-private-private-property-in-object",
],
]);
// We can't use a symbol because this needs to always be the same, even if // We can't use a symbol because this needs to always be the same, even if
// this package isn't deduped by npm. e.g. // this package isn't deduped by npm. e.g.
// - node_modules/ // - node_modules/
@ -18,6 +27,17 @@ export const FEATURES = Object.freeze({
const featuresKey = "@babel/plugin-class-features/featuresKey"; const featuresKey = "@babel/plugin-class-features/featuresKey";
const looseKey = "@babel/plugin-class-features/looseKey"; const looseKey = "@babel/plugin-class-features/looseKey";
// See https://github.com/babel/babel/issues/11622.
// Since preset-env sets loose for the fields and private methods plugins, it can
// cause conflicts with the loose mode set by an explicit plugin in the config.
// To solve this problem, we ignore preset-env's loose mode if another plugin
// explicitly sets it
// The code to handle this logic doesn't check that "low priority loose" is always
// the same. However, it is only set by the preset and not directly by users:
// unless someone _wants_ to break it, it shouldn't be a problem.
const looseLowPriorityKey =
"@babel/plugin-class-features/looseLowPriorityKey/#__internal__@babel/preset-env__please-overwrite-loose-instead-of-throwing";
export function enableFeature(file, feature, loose) { export function enableFeature(file, feature, loose) {
// We can't blindly enable the feature because, if it was already set, // We can't blindly enable the feature because, if it was already set,
// "loose" can't be changed, so that // "loose" can't be changed, so that
@ -25,42 +45,63 @@ export function enableFeature(file, feature, loose) {
// @babel/plugin-class-properties { loose: false } // @babel/plugin-class-properties { loose: false }
// is transformed in loose mode. // is transformed in loose mode.
// We only enabled the feature if it was previously disabled. // We only enabled the feature if it was previously disabled.
if (!hasFeature(file, feature)) { if (!hasFeature(file, feature) || canIgnoreLoose(file, feature)) {
file.set(featuresKey, file.get(featuresKey) | feature); file.set(featuresKey, file.get(featuresKey) | feature);
if (loose) file.set(looseKey, file.get(looseKey) | feature); if (
loose ===
"#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error"
) {
setLoose(file, feature, true);
file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature);
} else if (
loose ===
"#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error"
) {
setLoose(file, feature, false);
file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature);
} else {
setLoose(file, feature, loose);
}
} }
if ( let resolvedLoose: void | true | false;
hasFeature(file, FEATURES.fields) && let higherPriorityPluginName: void | string;
hasFeature(file, FEATURES.privateMethods) &&
isLoose(file, FEATURES.fields) !== isLoose(file, FEATURES.privateMethods) for (const [mask, name] of featuresSameLoose) {
) { if (!hasFeature(file, mask)) continue;
throw new Error(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " + const loose = isLoose(file, mask);
"and @babel/plugin-proposal-private-methods",
); if (canIgnoreLoose(file, mask)) {
continue;
} else if (resolvedLoose === !loose) {
throw new Error(
"'loose' mode configuration must be the same for @babel/plugin-proposal-class-properties, " +
"@babel/plugin-proposal-private-methods and " +
"@babel/plugin-proposal-private-property-in-object (when they are enabled).",
);
} else {
resolvedLoose = loose;
higherPriorityPluginName = name;
}
} }
if ( if (resolvedLoose !== undefined) {
hasFeature(file, FEATURES.fields) && for (const [mask, name] of featuresSameLoose) {
hasFeature(file, FEATURES.privateIn) && if (hasFeature(file, mask) && isLoose(file, mask) !== resolvedLoose) {
isLoose(file, FEATURES.fields) !== isLoose(file, FEATURES.privateIn) setLoose(file, mask, resolvedLoose);
) { console.warn(
throw new Error( `Though the "loose" option was set to "${!resolvedLoose}" in your @babel/preset-env ` +
"'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " + `config, it will not be used for ${name} since the "loose" mode option was set to ` +
"and @babel/plugin-proposal-private-property-in-object", `"${resolvedLoose}" for ${higherPriorityPluginName}.\nThe "loose" option must be the ` +
); `same for @babel/plugin-proposal-class-properties, @babel/plugin-proposal-private-methods ` +
} `and @babel/plugin-proposal-private-property-in-object (when they are enabled): you can ` +
`silence this warning by explicitly adding\n` +
if ( `\t["${name}", { "loose": ${resolvedLoose} }]\n` +
hasFeature(file, FEATURES.privateMethods) && `to the "plugins" section of your Babel config.`,
hasFeature(file, FEATURES.privateIn) && );
isLoose(file, FEATURES.privateMethods) !== isLoose(file, FEATURES.privateIn) }
) { }
throw new Error(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-private-methods " +
"and @babel/plugin-proposal-private-property-in-object",
);
} }
} }
@ -72,6 +113,17 @@ export function isLoose(file, feature) {
return !!(file.get(looseKey) & feature); return !!(file.get(looseKey) & feature);
} }
function setLoose(file, feature, loose) {
if (loose) file.set(looseKey, file.get(looseKey) | feature);
else file.set(looseKey, file.get(looseKey) & ~feature);
file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) & ~feature);
}
function canIgnoreLoose(file, feature) {
return !!(file.get(looseLowPriorityKey) & feature);
}
export function verifyUsedFeatures(path, file) { export function verifyUsedFeatures(path, file) {
if (hasOwnDecorators(path.node)) { if (hasOwnDecorators(path.node)) {
if (!hasFeature(file, FEATURES.decorators)) { if (!hasFeature(file, FEATURES.decorators)) {

View File

@ -301,10 +301,28 @@ export default declare((api, opts) => {
const pluginUseBuiltIns = useBuiltIns !== false; const pluginUseBuiltIns = useBuiltIns !== false;
const plugins = Array.from(pluginNames) const plugins = Array.from(pluginNames)
.map(pluginName => [ .map(pluginName => {
getPlugin(pluginName), if (
{ spec, loose, useBuiltIns: pluginUseBuiltIns }, pluginName === "proposal-class-properties" ||
]) pluginName === "proposal-private-methods" ||
// This is not included in preset-env yet, but let's keep it here so we
// don't forget about it in the future.
pluginName === "proposal-private-property-in-object"
) {
return [
getPlugin(pluginName),
{
loose: loose
? "#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error"
: "#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error",
},
];
}
return [
getPlugin(pluginName),
{ spec, loose, useBuiltIns: pluginUseBuiltIns },
];
})
.concat(polyfillPlugins); .concat(polyfillPlugins);
if (debug) { if (debug) {

View File

@ -0,0 +1,5 @@
class A {
x = 2;
#foo() {}
}

View File

@ -0,0 +1,12 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true
}]
],
"plugins": [
["proposal-private-methods", { "loose": true }]
]
}

View File

@ -0,0 +1,17 @@
var id = 0;
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }
class A {
constructor() {
Object.defineProperty(this, _foo, {
value: _foo2
});
this.x = 2;
}
}
var _foo = _classPrivateFieldLooseKey("foo");
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,4 @@
Though the "loose" option was set to "false" in your @babel/preset-env config, it will not be used for @babel/plugin-proposal-class-properties since the "loose" mode option was set to "true" for @babel/plugin-proposal-private-methods.
The "loose" option must be the same for @babel/plugin-proposal-class-properties, @babel/plugin-proposal-private-methods and @babel/plugin-proposal-private-property-in-object (when they are enabled): you can silence this warning by explicitly adding
["@babel/plugin-proposal-class-properties", { "loose": true }]
to the "plugins" section of your Babel config.

View File

@ -0,0 +1,5 @@
class A {
x = 2;
#foo() {}
}

View File

@ -0,0 +1,10 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true,
"loose": true
}]
]
}

View File

@ -0,0 +1,17 @@
var id = 0;
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }
class A {
constructor() {
Object.defineProperty(this, _foo, {
value: _foo2
});
this.x = 2;
}
}
var _foo = _classPrivateFieldLooseKey("foo");
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,5 @@
class A {
x = 2;
#foo() {}
}

View File

@ -0,0 +1,9 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true
}]
]
}

View File

@ -0,0 +1,14 @@
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class A {
constructor() {
_foo.add(this);
_defineProperty(this, "x", 2);
}
}
var _foo = new WeakSet();
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,13 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true
}]
],
"plugins": [
["proposal-class-properties", { "loose": true }],
["proposal-private-methods", { "loose": true }]
]
}

View File

@ -0,0 +1,17 @@
var id = 0;
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }
class A {
constructor() {
Object.defineProperty(this, _foo, {
value: _foo2
});
this.x = 2;
}
}
var _foo = _classPrivateFieldLooseKey("foo");
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,14 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true,
"loose": true
}]
],
"plugins": [
["proposal-class-properties", { "loose": false }],
["proposal-private-methods", { "loose": false }]
]
}

View File

@ -0,0 +1,14 @@
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class A {
constructor() {
_foo.add(this);
_defineProperty(this, "x", 2);
}
}
var _foo = new WeakSet();
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,5 @@
class A {
x = 2;
#foo() {}
}

View File

@ -0,0 +1,13 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true,
"loose": true
}]
],
"plugins": [
["proposal-class-properties", { "loose": true }]
]
}

View File

@ -0,0 +1,17 @@
var id = 0;
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }
class A {
constructor() {
Object.defineProperty(this, _foo, {
value: _foo2
});
this.x = 2;
}
}
var _foo = _classPrivateFieldLooseKey("foo");
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,5 @@
class A {
x = 2;
#foo() {}
}

View File

@ -0,0 +1,12 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true
}]
],
"plugins": [
["proposal-class-properties", { "loose": true }]
]
}

View File

@ -0,0 +1,17 @@
var id = 0;
function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }
class A {
constructor() {
Object.defineProperty(this, _foo, {
value: _foo2
});
this.x = 2;
}
}
var _foo = _classPrivateFieldLooseKey("foo");
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,4 @@
Though the "loose" option was set to "false" in your @babel/preset-env config, it will not be used for @babel/plugin-proposal-private-methods since the "loose" mode option was set to "true" for @babel/plugin-proposal-class-properties.
The "loose" option must be the same for @babel/plugin-proposal-class-properties, @babel/plugin-proposal-private-methods and @babel/plugin-proposal-private-property-in-object (when they are enabled): you can silence this warning by explicitly adding
["@babel/plugin-proposal-private-methods", { "loose": true }]
to the "plugins" section of your Babel config.

View File

@ -0,0 +1,5 @@
class A {
x = 2;
#foo() {}
}

View File

@ -0,0 +1,13 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true,
"loose": true
}]
],
"plugins": [
["proposal-class-properties", { "loose": false }]
]
}

View File

@ -0,0 +1,14 @@
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class A {
constructor() {
_foo.add(this);
_defineProperty(this, "x", 2);
}
}
var _foo = new WeakSet();
var _foo2 = function _foo2() {};

View File

@ -0,0 +1,4 @@
Though the "loose" option was set to "true" in your @babel/preset-env config, it will not be used for @babel/plugin-proposal-private-methods since the "loose" mode option was set to "false" for @babel/plugin-proposal-class-properties.
The "loose" option must be the same for @babel/plugin-proposal-class-properties, @babel/plugin-proposal-private-methods and @babel/plugin-proposal-private-property-in-object (when they are enabled): you can silence this warning by explicitly adding
["@babel/plugin-proposal-private-methods", { "loose": false }]
to the "plugins" section of your Babel config.