From 5c0d8a9de7c64fc309dbdf666c67f779b5dd439a Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Mon, 4 Nov 2019 10:05:42 -0800 Subject: [PATCH] add legacy decorators support to strict class fields (#10616) This PR allows legacy decorators to work with strict class fields, which are now stage 3 and have shipped in a number of browsers. Allowing this will allow users of the legacy transform (which is currently recommended by the champions of the decorator proposal) to use the proper class field semantics for non-decorated fields, which should help prevent breakage later on. This change is not a breaking change, since users had to explicitly opt into loose mode in class fields before. This just gives them the option to remove that opt-in. --- packages/babel-helpers/src/helpers.js | 4 +- .../local-define-property/input.js | 12 ++++++ .../local-define-property/options.json | 7 ++++ .../local-define-property/output.js | 40 +++++++++++++++++++ .../decorators-legacy-interop/loose/input.js | 9 +++++ .../loose/options.json | 7 ++++ .../decorators-legacy-interop/loose/output.js | 35 ++++++++++++++++ .../decorators-legacy-interop/strict/input.js | 9 +++++ .../strict/options.json | 7 ++++ .../strict/output.js | 37 +++++++++++++++++ .../decorator-interop/output.js | 2 +- .../src/transformer-legacy.js | 20 ++++++++++ .../output.mjs | 2 +- 13 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/input.js create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/options.json create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/output.js create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/input.js create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/options.json create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/output.js create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/input.js create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/options.json create mode 100644 packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/output.js diff --git a/packages/babel-helpers/src/helpers.js b/packages/babel-helpers/src/helpers.js index 23416e0022..8fb04a8953 100644 --- a/packages/babel-helpers/src/helpers.js +++ b/packages/babel-helpers/src/helpers.js @@ -1040,9 +1040,7 @@ helpers.initializerWarningHelper = helper("7.0.0-beta.0")` export default function _initializerWarningHelper(descriptor, context){ throw new Error( 'Decorating class property failed. Please ensure that ' + - 'proposal-class-properties is enabled and set to use loose mode. ' + - 'To use proposal-class-properties in spec mode with decorators, wait for ' + - 'the next major version of decorators in stage 2.' + 'proposal-class-properties is enabled and runs after the decorators transform.' ); } `; diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/input.js new file mode 100644 index 0000000000..f939e63b3b --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/input.js @@ -0,0 +1,12 @@ +function dec() {} + +// Create a local function binding so babel has to change the name of the helper +function _defineProperty() {} + +class A { + @dec a; + + @dec b = 123; + + c = 456; +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/options.json new file mode 100644 index 0000000000..6f03cd7aad --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/options.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + ["proposal-decorators", { "legacy": true }], + ["proposal-class-properties"], + "transform-classes" + ] +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/output.js new file mode 100644 index 0000000000..641307b44d --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/local-define-property/output.js @@ -0,0 +1,40 @@ +var _class, _descriptor, _descriptor2, _temp; + +function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperty2(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; } + +function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } + +function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.'); } + +function dec() {} // Create a local function binding so babel has to change the name of the helper + + +function _defineProperty() {} + +let A = (_class = (_temp = function A() { + "use strict"; + + _classCallCheck(this, A); + + _initializerDefineProperty(this, "a", _descriptor, this); + + _initializerDefineProperty(this, "b", _descriptor2, this); + + _defineProperty2(this, "c", 456); +}, _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, "a", [dec], { + configurable: true, + enumerable: true, + writable: true, + initializer: null +}), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, "b", [dec], { + configurable: true, + enumerable: true, + writable: true, + initializer: function () { + return 123; + } +})), _class); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/input.js new file mode 100644 index 0000000000..92298f2eb4 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/input.js @@ -0,0 +1,9 @@ +function dec() {} + +class A { + @dec a; + + @dec b = 123; + + c = 456; +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/options.json new file mode 100644 index 0000000000..8c83198953 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/options.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + ["proposal-decorators", { "legacy": true }], + ["proposal-class-properties", { "loose": true }], + "transform-classes" + ] +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/output.js new file mode 100644 index 0000000000..0a25e2b102 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/loose/output.js @@ -0,0 +1,35 @@ +var _class, _descriptor, _descriptor2, _temp; + +function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } + +function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.'); } + +function dec() {} + +let A = (_class = (_temp = function A() { + "use strict"; + + _classCallCheck(this, A); + + _initializerDefineProperty(this, "a", _descriptor, this); + + _initializerDefineProperty(this, "b", _descriptor2, this); + + this.c = 456; +}, _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, "a", [dec], { + configurable: true, + enumerable: true, + writable: true, + initializer: null +}), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, "b", [dec], { + configurable: true, + enumerable: true, + writable: true, + initializer: function () { + return 123; + } +})), _class); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/input.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/input.js new file mode 100644 index 0000000000..92298f2eb4 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/input.js @@ -0,0 +1,9 @@ +function dec() {} + +class A { + @dec a; + + @dec b = 123; + + c = 456; +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/options.json b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/options.json new file mode 100644 index 0000000000..6f03cd7aad --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/options.json @@ -0,0 +1,7 @@ +{ + "plugins": [ + ["proposal-decorators", { "legacy": true }], + ["proposal-class-properties"], + "transform-classes" + ] +} diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/output.js new file mode 100644 index 0000000000..11cce4ac92 --- /dev/null +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/decorators-legacy-interop/strict/output.js @@ -0,0 +1,37 @@ +var _class, _descriptor, _descriptor2, _temp; + +function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +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; } + +function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } + +function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.'); } + +function dec() {} + +let A = (_class = (_temp = function A() { + "use strict"; + + _classCallCheck(this, A); + + _initializerDefineProperty(this, "a", _descriptor, this); + + _initializerDefineProperty(this, "b", _descriptor2, this); + + _defineProperty(this, "c", 456); +}, _temp), (_descriptor = _applyDecoratedDescriptor(_class.prototype, "a", [dec], { + configurable: true, + enumerable: true, + writable: true, + initializer: null +}), _descriptor2 = _applyDecoratedDescriptor(_class.prototype, "b", [dec], { + configurable: true, + enumerable: true, + writable: true, + initializer: function () { + return 123; + } +})), _class); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js index 2cd978b722..9a71c17528 100644 --- a/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/static-property-tdz/decorator-interop/output.js @@ -12,7 +12,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } -function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and set to use loose mode. ' + 'To use proposal-class-properties in spec mode with decorators, wait for ' + 'the next major version of decorators in stage 2.'); } +function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.'); } function dec() {} diff --git a/packages/babel-plugin-proposal-decorators/src/transformer-legacy.js b/packages/babel-plugin-proposal-decorators/src/transformer-legacy.js index d6160db14c..ca23bad248 100644 --- a/packages/babel-plugin-proposal-decorators/src/transformer-legacy.js +++ b/packages/babel-plugin-proposal-decorators/src/transformer-legacy.js @@ -290,4 +290,24 @@ export default { ]), ); }, + + CallExpression(path, state) { + if (path.node.arguments.length !== 3) return; + if (!WARNING_CALLS.has(path.node.arguments[2])) return; + + // If the class properties plugin isn't enabled, this line will add an unused helper + // to the code. It's not ideal, but it's ok since the configuration is not valid anyway. + if (path.node.callee.name !== state.addHelper("defineProperty").name) { + return; + } + + path.replaceWith( + t.callExpression(state.addHelper("initializerDefineProperty"), [ + t.cloneNode(path.get("arguments")[0].node), + t.cloneNode(path.get("arguments")[1].node), + t.cloneNode(path.get("arguments.2.arguments")[0].node), + t.cloneNode(path.get("arguments.2.arguments")[1].node), + ]), + ); + }, }; diff --git a/packages/babel-plugin-transform-typescript/test/fixtures/class/abstract-class-decorated-parameter/output.mjs b/packages/babel-plugin-transform-typescript/test/fixtures/class/abstract-class-decorated-parameter/output.mjs index e8982550ef..58aa80d516 100644 --- a/packages/babel-plugin-transform-typescript/test/fixtures/class/abstract-class-decorated-parameter/output.mjs +++ b/packages/babel-plugin-transform-typescript/test/fixtures/class/abstract-class-decorated-parameter/output.mjs @@ -2,7 +2,7 @@ var _class, _descriptor; function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object.keys(descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object.defineProperty(target, property, desc); desc = null; } return desc; } -function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and set to use loose mode. ' + 'To use proposal-class-properties in spec mode with decorators, wait for ' + 'the next major version of decorators in stage 2.'); } +function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.'); } import { observable } from 'mobx'; let Foo = (_class = class Foo {