Skip to content

Commit

Permalink
Create bugfix plugin for classes in computed keys in Firefox (#16390)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Mar 31, 2024
1 parent 680d769 commit 48a3e19
Show file tree
Hide file tree
Showing 66 changed files with 583 additions and 7 deletions.
6 changes: 4 additions & 2 deletions packages/babel-compat-data/data/overlapping-plugins.json
Expand Up @@ -23,9 +23,11 @@
"bugfix/transform-v8-spread-parameters-in-optional-chaining"
],
"transform-class-properties": [
"bugfix/transform-v8-static-class-fields-redefine-readonly"
"bugfix/transform-v8-static-class-fields-redefine-readonly",
"bugfix/transform-firefox-class-in-computed-class-key"
],
"proposal-class-properties": [
"bugfix/transform-v8-static-class-fields-redefine-readonly"
"bugfix/transform-v8-static-class-fields-redefine-readonly",
"bugfix/transform-firefox-class-in-computed-class-key"
]
}
12 changes: 12 additions & 0 deletions packages/babel-compat-data/data/plugin-bugfixes.json
Expand Up @@ -107,6 +107,18 @@
"opera_mobile": "64",
"electron": "13.0"
},
"bugfix/transform-firefox-class-in-computed-class-key": {
"chrome": "74",
"opera": "62",
"edge": "79",
"safari": "14.1",
"node": "12",
"deno": "1",
"ios": "14.5",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"transform-optional-chaining": {
"chrome": "80",
"opera": "67",
Expand Down
12 changes: 12 additions & 0 deletions packages/babel-compat-data/data/plugins.json
Expand Up @@ -23,6 +23,18 @@
"opera_mobile": "52",
"electron": "17.0"
},
"bugfix/transform-firefox-class-in-computed-class-key": {
"chrome": "74",
"opera": "62",
"edge": "79",
"safari": "14.1",
"node": "12",
"deno": "1",
"ios": "14.5",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"transform-class-static-block": {
"chrome": "94",
"opera": "80",
Expand Down
7 changes: 7 additions & 0 deletions packages/babel-compat-data/scripts/data/plugin-features.js
Expand Up @@ -162,6 +162,13 @@ const es2022 = {
features: ["static class fields / static class fields use [[Define]]"],
replaces: "transform-class-properties",
},
"bugfix/transform-firefox-class-in-computed-class-key": {
replaces: "transform-class-properties",
overwrite: {
// TODO: Once Firefox releases the fix, write the correct version here.
firefox: undefined,
},
},
"transform-class-static-block": "Class static initialization blocks",
"transform-private-property-in-object":
"Ergonomic brand checks for private fields",
Expand Down
30 changes: 25 additions & 5 deletions packages/babel-compat-data/scripts/utils-build-data.js
Expand Up @@ -103,6 +103,14 @@ exports.generateData = (environments, features) => {

const normalized = {};
for (const [key, options] of Object.entries(features)) {
if (options.overwrite) {
if (!options.replaces || options.features) {
throw new Error(
`.overwrite is only supported when using .replace and not defining .features (${key})`
);
}
options.features = features[options.replaces].features;
}
if (!options.features) {
normalized[key] = {
features: expandFeatures([options]),
Expand All @@ -118,14 +126,24 @@ exports.generateData = (environments, features) => {
const overlapping = {};

// Apply bugfixes
for (const [key, { features, replaces }] of Object.entries(normalized)) {
for (const [key, { features, replaces, overwrite }] of Object.entries(
normalized
)) {
if (replaces) {
if (normalized[replaces].replaces) {
throw new Error("Transitive replacement is not supported");
throw new Error(`Transitive replacement is not supported (${key})`);
}

if (overwrite) {
normalized[key] = {
features: normalized[replaces].features,
overwrite,
};
} else {
normalized[replaces].features = normalized[replaces].features.filter(
feat => !features.includes(feat)
);
}
normalized[replaces].features = normalized[replaces].features.filter(
feat => !features.includes(feat)
);

if (!overlapping[replaces]) overlapping[replaces] = [];
overlapping[replaces].push(key);
Expand All @@ -142,6 +160,8 @@ exports.generateData = (environments, features) => {
});
addElectronSupportFromChromium(plugin);

if (options.overwrite) Object.assign(plugin, options.overwrite);

data[key] = plugin;
}

Expand Down
@@ -0,0 +1,3 @@
src
test
*.log
@@ -0,0 +1,19 @@
# @babel/plugin-bugfix-firefox-class-in-computed-class-key

> Wraps classes defined in computed keys of other classes affected by https://bugzilla.mozilla.org/show_bug.cgi?id=1887677
See our website [@babel/plugin-bugfix-firefox-class-in-computed-class-key](https://babeljs.io/docs/babel-plugin-bugfix-firefox-class-in-computed-class-key) for more information.

## Install

Using npm:

```sh
npm install --save-dev @babel/plugin-bugfix-firefox-class-in-computed-class-key
```

or using yarn:

```sh
yarn add @babel/plugin-bugfix-firefox-class-in-computed-class-key --dev
```
@@ -0,0 +1,57 @@
{
"name": "@babel/plugin-bugfix-firefox-class-in-computed-class-key",
"version": "7.24.1",
"description": "Wraps classes defined in computed keys of other classes affected by https://bugzilla.mozilla.org/show_bug.cgi?id=1887677",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-plugin-bugfix-firefox-class-in-computed-class-key"
},
"homepage": "https://babel.dev/docs/en/next/babel-plugin-bugfix-firefox-class-in-computed-class-key",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
},
"keywords": [
"babel-plugin",
"bugfix"
],
"dependencies": {
"@babel/helper-environment-visitor": "workspace:^",
"@babel/helper-plugin-utils": "workspace:^"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
},
"devDependencies": {
"@babel/core": "workspace:^",
"@babel/helper-plugin-test-runner": "workspace:^",
"@babel/traverse": "workspace:^"
},
"engines": {
"node": ">=6.9.0"
},
"author": "The Babel Team (https://babel.dev/team)",
"conditions": {
"USE_ESM": [
{
"type": "module"
},
null
],
"BABEL_8_BREAKING": [
{
"engines": {
"node": "^16.20.0 || ^18.16.0 || >=20.0.0"
}
},
{}
]
},
"type": "commonjs"
}
@@ -0,0 +1,113 @@
import type { NodePath, Visitor } from "@babel/traverse";
import type { types as t } from "@babel/core";
import { declare } from "@babel/helper-plugin-utils";
import environmentVisitor from "@babel/helper-environment-visitor";

export default declare(({ types: t, traverse, assertVersion }) => {
assertVersion(REQUIRED_VERSION(7));

const containsClassExpressionVisitor: Visitor<{ found: boolean }> = {
ClassExpression(path, state) {
state.found = true;
path.stop();
},
Function(path) {
path.skip();
},
};

const containsYieldOrAwaitVisitor = traverse.visitors.merge([
{
YieldExpression(path, state) {
state.yield = true;
if (state.await) path.stop();
},
AwaitExpression(path, state) {
state.await = true;
if (state.yield) path.stop();
},
} satisfies Visitor<{ yield: boolean; await: boolean }>,
environmentVisitor,
]);

function containsClassExpression(path: NodePath<t.Node>) {
if (t.isClassExpression(path.node)) return true;
if (t.isFunction(path.node)) return false;
const state = { found: false };
path.traverse(containsClassExpressionVisitor, state);
return state.found;
}

function wrap(path: NodePath<t.Expression>) {
const context = {
yield: t.isYieldExpression(path.node),
await: t.isAwaitExpression(path.node),
};
path.traverse(containsYieldOrAwaitVisitor, context);

let replacement;

if (context.yield) {
const fn = t.functionExpression(
null,
[],
t.blockStatement([t.returnStatement(path.node)]),
/* generator */ true,
/* async */ context.await,
);

replacement = t.yieldExpression(
t.callExpression(t.memberExpression(fn, t.identifier("call")), [
t.thisExpression(),
// NOTE: In some context arguments is invalid (it might not be defined
// in the top-level scope, or it's a syntax error in static class blocks).
// However, `yield` is also invalid in those contexts, so we can safely
// inject a reference to arguments.
t.identifier("arguments"),
]),
true,
);
} else {
const fn = t.arrowFunctionExpression([], path.node, context.await);

if (context.await) {
replacement = t.awaitExpression(t.callExpression(fn, []));
} else {
replacement = t.callExpression(
// We need to use fn.call() instead of just fn() because
// terser transforms (() => class {})() to class {}, effectively
// undoing the wrapping introduced by this plugin.
// https://github.com/terser/terser/issues/1514
// TODO(Babel 8): Remove .call if Terser stops inlining this case.
t.memberExpression(fn, t.identifier("call")),
[],
);
}
}

path.replaceWith(replacement);
}

return {
name: "bugfix-firefox-class-in-computed-class-key",

visitor: {
Class(path) {
const hasPrivateElement = path.node.body.body.some(node =>
t.isPrivate(node),
);
if (!hasPrivateElement) return;

for (const elem of path.get("body.body")) {
if (
"computed" in elem.node &&
elem.node.computed &&
containsClassExpression(elem.get("key"))
) {
wrap(elem.get("key"));
}
}
},
},
};
});
@@ -0,0 +1,4 @@
class A {
#x = 1;
[useIt([1 + class {}])];
}
@@ -0,0 +1,4 @@
class A {
#x = 1;
[(() => useIt([1 + class {}])).call()];
}
@@ -0,0 +1,3 @@
{
"plugins": ["bugfix-firefox-class-in-computed-class-key"]
}
@@ -0,0 +1,4 @@
class A {
#x = 1;
[class {}];
}
@@ -0,0 +1,4 @@
class A {
#x = 1;
[(() => class {}).call()];
}
@@ -0,0 +1,16 @@
class A {
#x;
[class {}]
[await class {}]
}

async function* f() {
class A {
#x;
[class {}]
[await class {}]
[yield class {}]
[yield* class {}]
[await (yield class {})]
}
}
@@ -0,0 +1,21 @@
class A {
#x;
[(() => class {}).call()];
[await (async () => await class {})()];
}
async function* f() {
class A {
#x;
[(() => class {}).call()];
[await (async () => await class {})()];
[yield* function* () {
return yield class {};
}.call(this, arguments)];
[yield* function* () {
return yield* class {};
}.call(this, arguments)];
[yield* async function* () {
return await (yield class {});
}.call(this, arguments)];
}
}
@@ -0,0 +1,5 @@
@deco
class A {
#x = 1;
static #y = 2;
}
@@ -0,0 +1,6 @@
{
"plugins": [
"bugfix-firefox-class-in-computed-class-key",
["proposal-decorators", { "version": "2023-11" }]
]
}

0 comments on commit 48a3e19

Please sign in to comment.