Add support for extending builtins (#7020)
This commit is contained in:
committed by
Henry Zhu
parent
148fe7d3ff
commit
0c885b3200
@@ -4,9 +4,15 @@
|
||||
|
||||
## Caveats
|
||||
|
||||
Built-in classes such as `Date`, `Array`, `DOM` etc cannot be properly subclassed
|
||||
due to limitations in ES5 (for the [classes](http://babeljs.io/docs/plugins/transform-classes) plugin).
|
||||
You can try to use [@babel/plugin-transform-builtin-extend](https://github.com/loganfsmyth/babel-plugin-transform-builtin-extend) based on `Object.setPrototypeOf` and `Reflect.construct`, but it also has some limitations.
|
||||
When extending a native class (e.g., `class extends Array {}`), the super class
|
||||
needs to be wrapped. This is needed to workaround two problems:
|
||||
- Babel transpiles classes using `SuperClass.apply(/* ... */)`, but native
|
||||
classes aren't callable and thus throw in this case.
|
||||
- Some built-in functions (like `Array`) always return a new object. Instead of
|
||||
returning it, Babel should treat it as the new `this`.
|
||||
|
||||
The wrapper works on IE11 and every other browser with `Object.setPrototypeOf` or `__proto__` as fallback.
|
||||
There is **NO IE <= 10 support**. If you need IE <= 10 it's recommended that you don't extend natives.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"@babel/helper-define-map": "7.0.0-beta.35",
|
||||
"@babel/helper-function-name": "7.0.0-beta.35",
|
||||
"@babel/helper-optimise-call-expression": "7.0.0-beta.35",
|
||||
"@babel/helper-replace-supers": "7.0.0-beta.35"
|
||||
"@babel/helper-replace-supers": "7.0.0-beta.35",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"keywords": [
|
||||
"babel-plugin"
|
||||
|
||||
@@ -3,6 +3,15 @@ import VanillaTransformer from "./vanilla";
|
||||
import annotateAsPure from "@babel/helper-annotate-as-pure";
|
||||
import nameFunction from "@babel/helper-function-name";
|
||||
import { types as t } from "@babel/core";
|
||||
import globals from "globals";
|
||||
|
||||
const getBuiltinClasses = category =>
|
||||
Object.keys(globals[category]).filter(name => /^[A-Z]/.test(name));
|
||||
|
||||
const builtinClasses = new Set([
|
||||
...getBuiltinClasses("builtin"),
|
||||
...getBuiltinClasses("browser"),
|
||||
]);
|
||||
|
||||
export default function(api, options) {
|
||||
const { loose } = options;
|
||||
@@ -54,7 +63,9 @@ export default function(api, options) {
|
||||
|
||||
node[VISITED] = true;
|
||||
|
||||
path.replaceWith(new Constructor(path, state.file).run());
|
||||
path.replaceWith(
|
||||
new Constructor(path, state.file, builtinClasses).run(),
|
||||
);
|
||||
|
||||
if (path.isCallExpression()) {
|
||||
annotateAsPure(path);
|
||||
|
||||
@@ -4,6 +4,8 @@ import optimiseCall from "@babel/helper-optimise-call-expression";
|
||||
import * as defineMap from "@babel/helper-define-map";
|
||||
import { traverse, template, types as t } from "@babel/core";
|
||||
|
||||
type ReadonlySet<T> = Set<T> | { has(val: T): boolean };
|
||||
|
||||
const noMethodVisitor = {
|
||||
"FunctionExpression|FunctionDeclaration"(path) {
|
||||
path.skip();
|
||||
@@ -61,7 +63,7 @@ const findThisesVisitor = traverse.visitors.merge([
|
||||
]);
|
||||
|
||||
export default class ClassTransformer {
|
||||
constructor(path: NodePath, file) {
|
||||
constructor(path: NodePath, file, builtinClasses: ReadonlySet<string>) {
|
||||
this.parent = path.parent;
|
||||
this.scope = path.scope;
|
||||
this.node = path.node;
|
||||
@@ -93,6 +95,12 @@ export default class ClassTransformer {
|
||||
|
||||
this.superName = this.node.superClass || t.identifier("Function");
|
||||
this.isDerived = !!this.node.superClass;
|
||||
|
||||
const { name } = this.superName;
|
||||
this.extendsNative =
|
||||
this.isDerived &&
|
||||
builtinClasses.has(name) &&
|
||||
!this.scope.hasBinding(name, /* noGlobals */ true);
|
||||
}
|
||||
|
||||
run() {
|
||||
@@ -112,7 +120,13 @@ export default class ClassTransformer {
|
||||
|
||||
//
|
||||
if (this.isDerived) {
|
||||
closureArgs.push(superName);
|
||||
if (this.extendsNative) {
|
||||
closureArgs.push(
|
||||
t.callExpression(this.file.addHelper("wrapNativeSuper"), [superName]),
|
||||
);
|
||||
} else {
|
||||
closureArgs.push(superName);
|
||||
}
|
||||
|
||||
superName = this.scope.generateUidIdentifierBasedOnNode(superName);
|
||||
closureParams.push(superName);
|
||||
|
||||
23
packages/babel-plugin-transform-classes/test/fixtures/exec/class-prototype-chain.js
vendored
Normal file
23
packages/babel-plugin-transform-classes/test/fixtures/exec/class-prototype-chain.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
function B() {}
|
||||
B.b = function() {
|
||||
return 'B.b';
|
||||
};
|
||||
|
||||
class C extends B {}
|
||||
|
||||
assert.equal(Object.getPrototypeOf(C), B);
|
||||
assert.equal(Object.getPrototypeOf(C.prototype), B.prototype);
|
||||
|
||||
assert.equal(C.b(), 'B.b');
|
||||
|
||||
class D extends Object {}
|
||||
|
||||
assert.ok(D instanceof Object)
|
||||
assert.ok(D.prototype instanceof Object);
|
||||
assert.equal(D.keys, Object.keys);
|
||||
|
||||
class E {}
|
||||
|
||||
assert.equal(Object.getPrototypeOf(E), Function.prototype);
|
||||
assert.equal(Object.getPrototypeOf(E.prototype), Object.prototype);
|
||||
assert.isFalse('keys' in E);
|
||||
@@ -0,0 +1,4 @@
|
||||
// JSON is wrapped because it starts with an uppercase letter, but it
|
||||
// should not be possible to extend it anyway.
|
||||
|
||||
assert.throws(() => class BetterJSON extends JSON {});
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["transform-classes", "transform-block-scoping"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Imported from
|
||||
// https://github.com/WebReflection/babel-plugin-transform-builtin-classes/blob/85efe1374e1c59a8323c7eddd4326f6c93d9f64f/test/test.js
|
||||
|
||||
class List extends Array {
|
||||
constructor(value) {
|
||||
super().push(value);
|
||||
}
|
||||
push(value) {
|
||||
super.push(value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(new List(1) instanceof List, 'new List is an instanceof List');
|
||||
assert.ok(new List(2) instanceof Array, 'new List is an instanceof Array');
|
||||
|
||||
var l = new List(3);
|
||||
assert.ok(l.length === 1 && l[0] === 3, 'constructor pushes an entry');
|
||||
assert.ok(l.push(4) === l && l.length === 2 && l.join() === '3,4', 'method override works');
|
||||
|
||||
class SecondLevel extends List {
|
||||
method() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(new SecondLevel(1) instanceof SecondLevel, 'new SecondLevel is an instanceof SecondLevel');
|
||||
assert.ok(new SecondLevel(2) instanceof List, 'new SecondLevel is an instanceof List');
|
||||
assert.ok(new SecondLevel(3) instanceof Array, 'new SecondLevel is an instanceof Array');
|
||||
|
||||
var s = new SecondLevel(4);
|
||||
assert.ok(s.length === 1 && s[0] === 4, 'constructor pushes an entry');
|
||||
assert.ok(s.push(5) === s && s.length === 2 && s.join() === '4,5', 'inherited override works');
|
||||
assert.ok(s.method() === s, 'new method works');
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["transform-classes","transform-block-scoping"]
|
||||
}
|
||||
1
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/actual.js
vendored
Normal file
1
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/actual.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
class List extends Array {}
|
||||
4
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/exec.js
vendored
Normal file
4
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/exec.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
class List extends Array {}
|
||||
|
||||
assert.ok(new List instanceof List);
|
||||
assert.ok(new List instanceof Array);
|
||||
23
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/expected.js
vendored
Normal file
23
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/expected.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }
|
||||
|
||||
var _gPO = Object.getPrototypeOf || function _gPO(o) { return o.__proto__; };
|
||||
|
||||
var _sPO = Object.setPrototypeOf || function _sPO(o, p) { o.__proto__ = p; };
|
||||
|
||||
var _construct = typeof Reflect === "object" && Reflect.construct || function _construct(Parent, args, Class) { var Constructor, a = [null]; a.push.apply(a, args); Constructor = Parent.bind.apply(Parent, a); return _sPO(new Constructor(), Class.prototype); };
|
||||
|
||||
var _cache = typeof Map === "function" && new Map();
|
||||
|
||||
function _wrapNativeSuper(Class) { if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() {} Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writeable: true, configurable: true } }); return _sPO(Wrapper, _sPO(function Super() { return _construct(Class, arguments, _gPO(this).constructor); }, Class)); }
|
||||
|
||||
var List =
|
||||
/*#__PURE__*/
|
||||
function (_Array) {
|
||||
_inheritsLoose(List, _Array);
|
||||
|
||||
function List() {
|
||||
return _Array.apply(this, arguments) || this;
|
||||
}
|
||||
|
||||
return List;
|
||||
}(_wrapNativeSuper(Array));
|
||||
6
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/options.json
vendored
Normal file
6
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/loose/options.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"plugins": [
|
||||
["transform-classes", { "loose": true }],
|
||||
"transform-block-scoping"
|
||||
]
|
||||
}
|
||||
3
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/shadowed/actual.js
vendored
Normal file
3
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/shadowed/actual.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
class Array {}
|
||||
|
||||
class List extends Array {}
|
||||
16
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/shadowed/expected.js
vendored
Normal file
16
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/shadowed/expected.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
let Array = function Array() {
|
||||
babelHelpers.classCallCheck(this, Array);
|
||||
};
|
||||
|
||||
let List =
|
||||
/*#__PURE__*/
|
||||
function (_Array) {
|
||||
babelHelpers.inherits(List, _Array);
|
||||
|
||||
function List() {
|
||||
babelHelpers.classCallCheck(this, List);
|
||||
return babelHelpers.possibleConstructorReturn(this, (List.__proto__ || Object.getPrototypeOf(List)).apply(this, arguments));
|
||||
}
|
||||
|
||||
return List;
|
||||
}(Array);
|
||||
3
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/shadowed/options.json
vendored
Normal file
3
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/shadowed/options.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["transform-classes", "external-helpers"]
|
||||
}
|
||||
1
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/actual.js
vendored
Normal file
1
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/actual.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
class List extends Array {}
|
||||
4
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/exec.js
vendored
Normal file
4
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/exec.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
class List extends Array {}
|
||||
|
||||
assert.ok(new List instanceof List);
|
||||
assert.ok(new List instanceof Array);
|
||||
29
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/expected.js
vendored
Normal file
29
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/expected.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
|
||||
|
||||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
|
||||
|
||||
var _gPO = Object.getPrototypeOf || function _gPO(o) { return o.__proto__; };
|
||||
|
||||
var _sPO = Object.setPrototypeOf || function _sPO(o, p) { o.__proto__ = p; };
|
||||
|
||||
var _construct = typeof Reflect === "object" && Reflect.construct || function _construct(Parent, args, Class) { var Constructor, a = [null]; a.push.apply(a, args); Constructor = Parent.bind.apply(Parent, a); return _sPO(new Constructor(), Class.prototype); };
|
||||
|
||||
var _cache = typeof Map === "function" && new Map();
|
||||
|
||||
function _wrapNativeSuper(Class) { if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() {} Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writeable: true, configurable: true } }); return _sPO(Wrapper, _sPO(function Super() { return _construct(Class, arguments, _gPO(this).constructor); }, Class)); }
|
||||
|
||||
var List =
|
||||
/*#__PURE__*/
|
||||
function (_Array) {
|
||||
_inherits(List, _Array);
|
||||
|
||||
function List() {
|
||||
_classCallCheck(this, List);
|
||||
|
||||
return _possibleConstructorReturn(this, (List.__proto__ || Object.getPrototypeOf(List)).apply(this, arguments));
|
||||
}
|
||||
|
||||
return List;
|
||||
}(_wrapNativeSuper(Array));
|
||||
3
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/options.json
vendored
Normal file
3
packages/babel-plugin-transform-classes/test/fixtures/extend-builtins/spec/options.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["transform-classes", "transform-block-scoping"]
|
||||
}
|
||||
Reference in New Issue
Block a user