Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: subclassing seems broken in older Chrome #14056

Closed
1 task
romainmenke opened this issue Dec 15, 2021 · 35 comments · Fixed by #14072
Closed
1 task

[Bug]: subclassing seems broken in older Chrome #14056

romainmenke opened this issue Dec 15, 2021 · 35 comments · Fixed by #14072
Labels
i: needs triage i: regression outdated A closed issue/PR that is archived due to age. Recommended to make a new issue

Comments

@romainmenke
Copy link

💻

  • Would you like to work on a fix?

How are you using Babel?

babel-loader (webpack)

Input code

(function (cb) {
	class Foo extends HTMLElement {
		connectedCallback() {
			this._baz = 'hello';
		}

		baz() {
			return this._baz;
		}
	}

	class Fooz extends Foo {
		connectedCallback() {
			super.connectedCallback();

			this._baz = this._baz + ' world';
		}
	}

	customElements.define('web-test-fooz', Fooz);
	document.body.appendChild(new Fooz());
	var fooz = document.getElementsByTagName('web-test-fooz')[0];

	cb(fooz.baz() === 'hello world');
})(callback);

Configuration file name

No response

Configuration

https://github.com/mrhenry/web-tests/blob/main/webpack-core-web.config.js#L166-L217

use: {
	loader: 'babel-loader',
	options: {
		comments: false,
		presets: [
			[
				'@babel/preset-env',
				{
					corejs: '^3.6.3',
					bugfixes: true,
					targets: {
						browsers: [
							"IE >= 8",
							"Opera >= 12",
							"Safari >= 5.1",
							"Chrome >= 15",
							"Edge >= 12",
							"Firefox >= 4"
						]
					},
					useBuiltIns: 'usage',
					exclude: [
						"web.dom-collections.iterator",
						"web.dom-collections.for-each"
					]
				}
			]
		]
	}
}

Current and expected behavior

Current :

Uncaught TypeError: Cannot call a class as a function

In function _classCallCheck

Screenshot 2021-12-15 at 14 38 07

Expected subclasses to work.

Environment

Started occurring after upgrading @babel/core from 7.16.0 to 7.16.5

Possible solution

No response

Additional context

Test results page :
https://mrhenry.github.io/web-tests/#53d613d8-773c-43d5-800b-a78aeeab8ef7

Screenshot 2021-12-15 at 14 40 49

Results for chrome <=36 dropped below 1.0 indicating that the test is now failing.
The only change between tests passing and tests failing is this commit : mrhenry/web-tests@16599fd

(build outputs are included in git)

Test source code :
https://github.com/mrhenry/web-tests/blob/main/specifications/whatwg/html/4.13.4.customElements.define.super/test.pure.js

Git blame for the affected build output :

https://github.com/mrhenry/web-tests/blame/main/specifications/whatwg/html/4.13.4.customElements.define.super/test.babel.js

@babel-bot
Copy link
Collaborator

Hey @romainmenke! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant Slack community that typically always has someone willing to help. You can sign-up here for an invite.

@nicolo-ribaudo
Copy link
Member

It might be caused by #12115

@nicolo-ribaudo
Copy link
Member

@romainmenke If you have access to older Chrome versions (otherwise I have to figure out how to install them), could you test if this code works?

"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }

function _get() { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(arguments.length < 3 ? target : receiver); } return desc.value; }; } return _get.apply(this, arguments); }

function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false, value: Constructor.prototype }); return Constructor; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } Object.defineProperty(subClass, "prototype", { value: Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }), writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }

function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return 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() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); }

function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); }

function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }

function _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; }

function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }

function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }

(function (cb) {
  var Foo = /*#__PURE__*/function (_HTMLElement) {
    _inherits(Foo, _HTMLElement);

    var _super = _createSuper(Foo);

    function Foo() {
      _classCallCheck(this, Foo);

      return _super.apply(this, arguments);
    }

    _createClass(Foo, [{
      key: "connectedCallback",
      value: function connectedCallback() {
        this._baz = 'hello';
      }
    }, {
      key: "baz",
      value: function baz() {
        return this._baz;
      }
    }]);

    return Foo;
  }( /*#__PURE__*/_wrapNativeSuper(HTMLElement));

  var Fooz = /*#__PURE__*/function (_Foo) {
    _inherits(Fooz, _Foo);

    var _super2 = _createSuper(Fooz);

    function Fooz() {
      _classCallCheck(this, Fooz);

      return _super2.apply(this, arguments);
    }

    _createClass(Fooz, [{
      key: "connectedCallback",
      value: function connectedCallback() {
        _get(_getPrototypeOf(Fooz.prototype), "connectedCallback", this).call(this);

        this._baz = this._baz + ' world';
      }
    }]);

    return Fooz;
  }(Foo);

  customElements.define('web-test-fooz', Fooz);
  document.body.appendChild(new Fooz());
  var fooz = document.getElementsByTagName('web-test-fooz')[0];
  console.log(fooz.baz() === 'hello world');
})(alert);

I have modified Babel's output for your test replacing

Object.defineProperty(Constructor, "prototype", { writable: false });

with

Object.defineProperty(Constructor, "prototype", { writable: false, value: Constructor.prototype });

in the _createClass function.

@romainmenke
Copy link
Author

Uncaught TypeError: Cannot redefine property: prototype

My change based on your comment :

https://github.com/mrhenry/web-tests/pull/260/files

@yedidyak
Copy link

This error started to happen to several projects we have after 7.16.5 was released. It happens in Jest tests using preset'module:metro-react-native-babel-preset'. I can try and make a reproducing code sample if needed. It always fails on the constructor of an extended class.

@nicolo-ribaudo
Copy link
Member

@yedidyak Does it fail with any exdended class, or just with classes extending native DOM classes (like HTMLElement)? Also, can you reproduce it in modern engines or just in old Chrome versions like in the OP?

(I still have to figure out how to download Chrome 36 😅)

@yedidyak
Copy link

@nicolo-ribaudo This is a React Native project, so it's not in chrome or with HTMLElement type classes. In at least one case it's a plain JS class that's extending another plain JS class.

We're running this in Jest with node 12.22.1.

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Dec 15, 2021

Oh thank you. A minimal example that shows the error in node 12 using plain classes would help a lot!

@yedidyak
Copy link

yedidyak commented Dec 15, 2021

Thanks!

I'll try and get a full example, but a quick extract from one part that fails:

State.js (transpiled from TS)

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MutableState = exports.State = void 0;
class State {
    constructor({params}) { // <---- fails here 
    }
}
exports.State = State;
class MutableState extends State {
}
exports.MutableState = MutableState;

other file:

const {MutableState} = require('./State');
state= new MutableState({});

Fails on that indicated line, from _classCallCheck (node_modules/@babel/runtime/helpers/classCallCheck.js:3:11)

@romainmenke
Copy link
Author

romainmenke commented Dec 20, 2021

@yedidyak Any updates on the example?
Just want to avoid that this issue remains open too long :)

I also just tried recreate in node based on your example and could not reproduce there.


@nicolo-ribaudo Maybe you can request access on a service like browserstack?
They always have free tiers for open source projects.

Otherwise I am happy to run any tweaks to the generated code on my end and post the results.
You could fork this repo and make changes directly based on this :
mrhenry/web-tests#260

@yedidyak
Copy link

@nicolo-ribaudo @romainmenke

Hi! I eventually managed to work out that it's coming in projects of ours that override Object.defineProperty with a custom implementation that failed when trying to redefine prototype, then swallowed the error 😱

I hope that's helpful in working out where it fails for you.

@romainmenke
Copy link
Author

romainmenke commented Dec 20, 2021

Added a minimal test for class inheritance with non-html classes :

https://github.com/mrhenry/web-tests/blob/main/specifications/tc39/ecma262/15.7.Class-Definitions.ClassHeritage/test.pure.js

https://mrhenry.github.io/web-tests/#293ee801-4d53-4261-add3-dca6b9f8015f

Can't copy paste stacktraces from browserstack but recreated manually :

Same error message :

Uncaught TypeError: Cannot call a class as a function


This test only uses babel, no webpack (so no core-js imports), no other polyfills or dependencies.

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false,
                "bugfixes": true,
                "exclude": [
                    "@babel/plugin-transform-regenerator"
                ],
                "targets": {
                    "browsers": [
                        "IE >= 6",
                        "Opera >= 9",
                        "Safari >= 4",
                        "Chrome >= 4",
                        "Edge >= 12",
                        "Firefox >= 2"
                    ]
                },
                "useBuiltIns": false
            }
        ]
    ]
}

@nicolo-ribaudo
Copy link
Member

Thanks! I figured out how to test old chrome, I'll work on this later today.

@romainmenke
Copy link
Author

Note :

Also fails in IE8, but I don't have a history of tests for subclasses for non-html elements. (webcomponents never worked in IE8)

Screenshot 2021-12-20 at 17 48 38

Will report back after a fix has shipped with IE8 status.

@nicolo-ribaudo
Copy link
Member

The problem is that in older chrome versions the prototype of functions cannot be change when making it non-writable at the same time, so Object.defineProperty fails. I testes it in Chrome 33:

function fn() {}
var proto = {};
Object.defineProperty(fn, "prototype", { value: proto, writable: false });

fn.prototype === proto; // false

However, this works:

function fn() {}
var proto = {};
fn.prototype = proto;
Object.defineProperty(fn, "prototype", { writable: false });

fn.prototype === proto; // true

@nicolo-ribaudo
Copy link
Member

@romainmenke Could you test if replacing the _inherits function with this one works?

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, writable: true, configurable: true },
  });
  Object.defineProperty(subClass, "prototype", { writable: false });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

@romainmenke
Copy link
Author

That works!
Do you want me to try a full run of all browsers with this change to make sure?

@nicolo-ribaudo
Copy link
Member

Yes please! I'm preparing a PR while you test it.

@nicolo-ribaudo
Copy link
Member

nicolo-ribaudo commented Dec 21, 2021

This is probably caused by https://bugs.chromium.org/p/v8/issues/detail?id=3334

cc @zloirock / @ljharb You might want to polyfill it in core-js and es5-shim, splitting Object.defineProperty(_0, "prototype", { value: _1, writable: false }) into

_0.prototype = _1;
NativeObjectDefineProperty(_0, "prototype", { writable: false });

if typeof _0 === "function".

@romainmenke
Copy link
Author

Running now with these changes : https://github.com/mrhenry/web-tests/pull/263/files

this will take a while

@romainmenke
Copy link
Author

whatwg html 4.13.4 customElements.define super - chrome/27.0 - core-web - delta : 0.010 - current : 0.020

tc39 ecma262 15.7 Class-Definitions.ClassHeritage - chrome/35.0 - babel - delta : 0.010 - current : 0.040

tc39 ecma262 15.7 Class-Definitions.ClassHeritage - chrome/35.0 - core-web - delta : 0.010 - current : 0.040

whatwg html 4.13.4 customElements.define super - chrome/35.0 - core-web - delta : 0.010 - current : 0.040

tc39 ecma262 15.7 Class-Definitions.ClassHeritage - chrome/16.0 - babel - delta : 0.010 - current : 0.040

tc39 ecma262 15.7 Class-Definitions.ClassHeritage - chrome/16.0 - core-web - delta : 0.010 - current : 0.030

whatwg html 4.13.4 customElements.define super - chrome/16.0 - core-web - delta : 0.010 - current : 0.030

ecma262 15.7 Class-Definitions.ClassHeritage - chrome/34.0 - babel - delta : 0.010 - current : 0.030

tc39 ecma262 15.7 Class-Definitions.ClassHeritage - chrome/34.0 - core-web - delta : 0.010 - current : 0.030

whatwg html 4.13.4 customElements.define super - chrome/34.0 - core-web - delta : 0.010 - current : 0.030

Looks promising, haven't spotted negative delta's (no new failures)

@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

@nicolo-ribaudo es5-shim doesn't do any defineProperty calls that have a key of "prototype" anywhere I can find. Are you suggesting patching Object.defineProperty itself?

@nicolo-ribaudo
Copy link
Member

Yes, since Object.defineProperty is an ES5 feature.

@zloirock
Copy link
Member

@nicolo-ribaudo it's not the only such issue. This is not a proper fix of this since it will break some other cases. I'll check what can be done.

@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

If the fix is only applied when the key is "prototype" and the descriptor contains both writable and value, what cases will it break?

ljharb added a commit to es-shims/es5-shim that referenced this issue Dec 21, 2021
@zloirock
Copy link
Member

@ljharb are you sure that the current descriptor is writable and you will be able properly check it (for example https://bugs.chromium.org/p/v8/issues/detail?id=1570)?

@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

@zloirock so far in chrome 15-36, at least, as well as node 0.4, f.prototype and Object.getOwnPropertyDescriptor(f, 'prototype').value are the same. That bug seems to be apply to an older version of Chrome, which I'm unable to test. (if you can suggest an easy testing mechanism for Chrome older than 15 or node older than 0.4, I'll be happy to check)

@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

@nicolo-ribaudo the fix is in es5-shim v4.6.3.

@zloirock
Copy link
Member

zloirock commented Dec 21, 2021

@ljharb try with this polyfill, for example this:

!function() {
  'use strict';
  var O = { prototype: 1 };
  Object.defineProperty(O, 'prototype', { writable: false, configurable: true });
  Object.defineProperty(O, 'prototype', { value: 42, writable: false });
}()

@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

Are you trying to demonstrate that the problem isn't just about the value, it's that defineProperty won't make it non-writable if there's any other property in the descriptor?

@zloirock
Copy link
Member

zloirock commented Dec 21, 2021

I'm trying to demonstrate that attempting to write non-writable property in the strict mode throws an error. Like

!function() {
  'use strict';
  var O = { prototype: 1 };
  Object.defineProperty(O, 'prototype', { writable: false, configurable: true });
  O.prototype = 42; // !!!
  Object.defineProperty(O, 'prototype', { value: 42, writable: false });
}()

ljharb added a commit to es-shims/es5-shim that referenced this issue Dec 21, 2021
@ljharb
Copy link
Member

ljharb commented Dec 21, 2021

Thanks, I see the problem. Fixed in v4.6.4.

@romainmenke
Copy link
Author

@JakeChampion can you tell if action is needed in the polyfill-library for this?

@romainmenke
Copy link
Author

@nicolo-ribaudo Thank you for getting this fixed and released!
Everything looks good on my end.

If there are features you want to test in browsers you can always add them in web-tests (or via an issue)

@nicolo-ribaudo
Copy link
Member

Awesome, thanks! 😄

@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Apr 2, 2022
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
i: needs triage i: regression outdated A closed issue/PR that is archived due to age. Recommended to make a new issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants