Skip to content

Commit

Permalink
Class features loose should have precedence over preset-env (#11634)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
nicolo-ribaudo and JLHwung committed May 29, 2020
1 parent 5b24d79 commit e6d873e
Show file tree
Hide file tree
Showing 29 changed files with 379 additions and 34 deletions.
112 changes: 82 additions & 30 deletions packages/babel-helper-create-class-features-plugin/src/features.js
Expand Up @@ -8,6 +8,15 @@ export const FEATURES = Object.freeze({
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
// this package isn't deduped by npm. e.g.
// - node_modules/
Expand All @@ -18,49 +27,81 @@ export const FEATURES = Object.freeze({
const featuresKey = "@babel/plugin-class-features/featuresKey";
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) {
// We can't blindly enable the feature because, if it was already set,
// "loose" can't be changed, so that
// @babel/plugin-class-properties { loose: true }
// @babel/plugin-class-properties { loose: false }
// is transformed in loose mode.
// 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);
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 (
hasFeature(file, FEATURES.fields) &&
hasFeature(file, FEATURES.privateMethods) &&
isLoose(file, FEATURES.fields) !== isLoose(file, FEATURES.privateMethods)
) {
throw new Error(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " +
"and @babel/plugin-proposal-private-methods",
);
}
let resolvedLoose: void | true | false;
let higherPriorityPluginName: void | string;

if (
hasFeature(file, FEATURES.fields) &&
hasFeature(file, FEATURES.privateIn) &&
isLoose(file, FEATURES.fields) !== isLoose(file, FEATURES.privateIn)
) {
throw new Error(
"'loose' mode configuration must be the same for both @babel/plugin-proposal-class-properties " +
"and @babel/plugin-proposal-private-property-in-object",
);
for (const [mask, name] of featuresSameLoose) {
if (!hasFeature(file, mask)) continue;

const loose = isLoose(file, mask);

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 (
hasFeature(file, FEATURES.privateMethods) &&
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",
);
if (resolvedLoose !== undefined) {
for (const [mask, name] of featuresSameLoose) {
if (hasFeature(file, mask) && isLoose(file, mask) !== resolvedLoose) {
setLoose(file, mask, resolvedLoose);
console.warn(
`Though the "loose" option was set to "${!resolvedLoose}" in your @babel/preset-env ` +
`config, it will not be used for ${name} since the "loose" mode option was set to ` +
`"${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` +
`\t["${name}", { "loose": ${resolvedLoose} }]\n` +
`to the "plugins" section of your Babel config.`,
);
}
}
}
}

Expand All @@ -72,6 +113,17 @@ export function isLoose(file, 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) {
if (hasOwnDecorators(path.node)) {
if (!hasFeature(file, FEATURES.decorators)) {
Expand Down
26 changes: 22 additions & 4 deletions packages/babel-preset-env/src/index.js
Expand Up @@ -301,10 +301,28 @@ export default declare((api, opts) => {

const pluginUseBuiltIns = useBuiltIns !== false;
const plugins = Array.from(pluginNames)
.map(pluginName => [
getPlugin(pluginName),
{ spec, loose, useBuiltIns: pluginUseBuiltIns },
])
.map(pluginName => {
if (
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);

if (debug) {
Expand Down
@@ -0,0 +1,5 @@
class A {
x = 2;

#foo() {}
}
@@ -0,0 +1,12 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true
}]
],
"plugins": [
["proposal-private-methods", { "loose": true }]
]
}
@@ -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() {};
@@ -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.
@@ -0,0 +1,5 @@
class A {
x = 2;

#foo() {}
}
@@ -0,0 +1,10 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true,
"loose": true
}]
]
}
@@ -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() {};
@@ -0,0 +1,5 @@
class A {
x = 2;

#foo() {}
}
@@ -0,0 +1,9 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true
}]
]
}
@@ -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() {};
@@ -0,0 +1,5 @@
class A {
x = 2;

#foo() {}
}
@@ -0,0 +1,13 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true
}]
],
"plugins": [
["proposal-class-properties", { "loose": true }],
["proposal-private-methods", { "loose": true }]
]
}
@@ -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() {};
@@ -0,0 +1,5 @@
class A {
x = 2;

#foo() {}
}
@@ -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 }]
]
}
@@ -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() {};
@@ -0,0 +1,5 @@
class A {
x = 2;

#foo() {}
}
@@ -0,0 +1,13 @@
{
"validateLogs": true,
"presets": [
["env", {
"targets": { "node": 10 },
"shippedProposals": true,
"loose": true
}]
],
"plugins": [
["proposal-class-properties", { "loose": true }]
]
}
@@ -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() {};

0 comments on commit e6d873e

Please sign in to comment.