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

🚀♻️ Custom Elements V1 Polyfill #17205

Merged
merged 28 commits into from
Aug 3, 2018

Conversation

jridgewell
Copy link
Contributor

@jridgewell jridgewell commented Jul 30, 2018

Uses a custom baked Custom Elements V1 polyfill, supporting just the parts we need (connectedCallback, disconnectedCallback, whenDefined, define) using just the features available (MutaitonObserver) in the browsers we target (recent Chrome, Safari 7+, recent Firefox, IE11+).

Preliminary size is 85% smaller than the (much more complete)document.registerElement polyfill (1.5kb vs 9.5kb).

This supports only "autonomous custom elements" from Custom Elements V1 (not v0). This does not polyfill extending native built-in elements (we don't do this in AMP). This limited V1 use case has been implemented since Chrome 54 and Safari 10.3.

This supports using the native Custom Elements V1, if available. Both ES5 style classes and ES6 native classes are supported (but not at the same time, it's one or the other). If ES6 native classes are used (as in the upcoming AMP module build), nothing is patched. If ES5 classes are used (as in regular AMP), only HTMLElement is patched.

If native Custom Elements V1 is not supported, this does a full patch. This includes document.createElement and friends, and uses a MutationObserver to detect when nodes become connected/disconnected. In this full-patch mode, Custom Elements are not supported in the shadowy part of ShadowRoots (they are supported in the slots of a shadow root, aka the regular DOM).

Oh, and there are no issues installing into child windows. So, fixes #16052.

I think this was a pretty good Fixit.

@kristoferbaxter
Copy link
Contributor

Incredibly excited to review this. Thank you for the hard work!

@@ -295,6 +295,8 @@ function compile(entryModuleFilenames, outputDir,
'!build/fake-module/src/polyfills/**/*.js',
'!build/fake-polyfills/src/polyfills.js',
'!src/polyfills/*.js',
// TODO(prateekbh): I don't understand how to add
// src/polyfills/custom-elements.js to the _needed_ polyfills.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to exclude src/polyfills/custom-elements.js for the module build?

cuz last time we checked with Malte, the lifecycle without custom elements polyfills is skewed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we need to include it in all builds. I don’t know how to do that for the module build.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default it will be added in all.
Extra work is for exclusion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? The previous line ('!src/polyfills/*.js') excludes everything from polyfills directory.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! with your PR we wont need to shadow it anywhere else, gotcha.

best way would be to remove the above line and add a code below this

polyfillsShadowList.forEach(polyfillFile => {
  srcs.push(`!src/polyfills/${polyfillFile}.js`);
})

this will exclude all but your polyfill

* resolve: function(),
* }}
*/
let DeferredDef;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently has no tests, does it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None yet. I've manually tested, and was going to build this into a fully fledged node module with tests there.

Note that the our regular tests are going to run with this polyfill instead of document.registerElement. If that's not enough, I can begin the test suite in this repo and extract all of it out later?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you actually tried running the tests with the experiment on?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is missing most integration tests that use a fully built AMP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it so it always runs in tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But not yet in integration tests, right?

Copy link
Contributor Author

@jridgewell jridgewell Aug 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, all test run with the new polyfill. At the moment, this includes:

  • SL_Chrome_67
  • SL_Firefox_61
  • SL_Safari_11

We can enable more saucelabs browsers, but I don't think that's absolutely necessary until we're ready to take this out of opt-in experiment?

}

/**
* Was HTMLElement already patched this window?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: missing for

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

*/
function isPatched(win) {
const tag = win.HTMLElement.toString();
return tag.indexOf('[native code]') === -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a good check? [native code] appears in bound fn.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not full proof, but I don't think it's likely someone will obscure the toString of the HTMLElement constructor. I wanted to avoid doing a creation test using a subclass of the constructor, because it would require polluting the custom elements registry.

I can try thinking of a better solution if you're not comfortable with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic property on polyfilled class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to allow PWA publishers to include a fully featured Custom Elements for their own code.

If they do, it's likely they'll patch HTMLElement as part of the polyfill. In that case, wrapping the HTMLElement constructor is unnecessary. But I can't do a full feature test to see if HTMLElement will work in our case unless I register my own CE and try document.createElement to see if it'll throw. This seemed like a good enough approach.

const lifecycleCallbacks = {
'connectedCallback': null,
'disconnectedCallback': null,
// 'adoptedCallback': null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't actually used in AMP, so I started comment them out. I can remove these and leave TODOs instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we do use attribute change callback, right? And you implement it below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so, but Bind created its own mutatedAttributesCallback and there we no matches for attributeChangedCallback when grepping.

*/
this.current_ = null;

const observer = new win.MutationObserver(records => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this everywhere we care about?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. IE11, Chrome 26, FF 14, Safari 7, even Samsung's "Internet".

/**
* Polyfills Custom Elements v1 API
* @param {!Window} win
* @param {!Function} ctor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This badly needs docs

* resolve: function(),
* }}
*/
let DeferredDef;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you actually tried running the tests with the experiment on?

Copy link
Contributor Author

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added copious comments.

* resolve: function(),
* }}
*/
let DeferredDef;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it so it always runs in tests.

src/polyfills.js Outdated
'document-register-element/build/document-register-element.patched';

installCustomElements(self, 'auto');
if (isExperimentOn(self, 'custom-elements-v1') || getMode().test) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want at least one test that ensures the old still works. That is unless you are super optimistic about moving over.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more worried about bugs with using native custom elements than I am with this polyfill not matching the current polyfill. Worst case we move to this one, and disable the native CE/Reflect.construct support.

@@ -22,7 +22,7 @@ const log = require('fancy-log');
const {getStdout} = require('../exec');

const runtimeFile = './dist/v0.js';
const maxSize = '79.28KB';
const maxSize = '80.60KB';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did the maxSize increase because we are shipping both polyfills in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup.

* This intentionally ignores "valid" higher Unicode Code Points.
* https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
*/
const VALID_NAME = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
Copy link
Contributor

@kristoferbaxter kristoferbaxter Aug 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if this should be different for AMP's specific usecase. Could we only allow names matching amp '-' [a-z] (PCENChar)*?

Anything else wouldn't be a valid AMP component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would eliminate the need for an invalid names list of strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather leave this generic since the plan is to extract this into a node module. We can add amp- prefix requirements at AMP's registerElement if we want.

@cramforce
Copy link
Member

cramforce commented Aug 1, 2018 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error: "Cannot read property 'create' of undefined"
7 participants