Skip to content

Conversation

@nolanlawson
Copy link
Contributor

Details

Fixes #3073.

Hides the pivot constructor as best we can, when running in ENABLE_SCOPED_CUSTOM_ELEMENT_REGISTRY mode. This better prevents users from "detecting" that the pivot patches are applied.

We can't fully fix this (see details in #3073), so this is a "best effort."

Does this pull request introduce a breaking change?

  • ✅ No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • ⚠️ Yes, it does include an observable change.

With this change, customElements.get/customElement.whenDefined changes its behavior depending on whether or not a pivot (LWC) constructor was defined. But this only occurs if the flag is set, and we haven't enabled the flag anywhere yet.

GUS work item

W-11884701

adoptedCallback?: () => void;
attributeChangedCallback?: (name: string, oldValue: any, newValue: any) => void;
observedAttributes: Set<string>;
}
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 just decided to move this up to have all the TypeScript types/interfaces together.

const globalDefinitionsByTag = new Map<string, Definition>();
const globalDefinitionsByClass = new Map<CustomElementConstructor, Definition>();
const awaitingUpgrade = new Map<string, Set<HTMLElement>>();
const pendingWhenDefinedCallbacks = new Map<string, WhenDefinedCallback[]>();
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 considered keeping track of the Promise as well as the resolve function, so that we didn't need an array of callbacks and could just keep returning the same Promise, but it got really hairy to deal with the Promise constructor callbacks. An array was a lot simpler.

expect(Ctor).not.toBeUndefined();
expect(Ctor).toBe(firstCtor);
});
});
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 tests may seem excessive, but it's to get 100% code coverage.

}
}
// If anyone called customElements.whenDefined() and is still waiting for a promise resolution, resolve now
flushPendingWhenDefinedCallbacks(tagName, constructor);
Copy link
Contributor

Choose a reason for hiding this comment

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

Question:
If the user register's a whenDefined callback for 'x-foo' and it is the engine registers its own constructor for x-foo. The user's callback will still be invoked in this flush operation, correct? Is that the desired operation? This would lead to inconsistency in behavior of the APIs.

// user registers callback for x-foo
// engine defines x-foo with a pivot ctor unrelated to user
// user's callback is flushed
// Now user thin's the element is defined, runs customElements.get('x-foo') and I believe an `undefined` is returned

The meta question is, should the engine's usage of the native registry to perform pivoting be transparent to the user?

Copy link
Contributor

Choose a reason for hiding this comment

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

nvm, this case is handled in L492

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 can add a test for the negative case, to confirm that the promise never resolves.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nevermind, there is a test for that here: https://github.com/salesforce/lwc/pull/3112/files#r997496983

const promise = customElements.whenDefined(tagName).then((ctor) => {
resolvedCtor = ctor;
});
expect(customElements.get(tagName)).toBeUndefined();
Copy link
Contributor

Choose a reason for hiding this comment

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

Between L66 and L71, the customElements registry is untouched except for registering the callback. In which case is it expected that customElements.get(tagName) will behave differently at L71?

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 you're right, this line is unnecessary.

Comment on lines 79 to 83
return Promise.resolve()
.then(() => {
expect(resolvedCtor).toEqual(undefined); // whenDefined does not leak pivot constructor
customElements.define(tagName, MyCustomComponent);
return promise;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return Promise.resolve()
.then(() => {
expect(resolvedCtor).toEqual(undefined); // whenDefined does not leak pivot constructor
customElements.define(tagName, MyCustomComponent);
return promise;
expect(resolvedCtor).toEqual(undefined); // whenDefined does not leak pivot constructor
customElements.define(tagName, MyCustomComponent);
return promise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See https://github.com/salesforce/lwc/pull/3112/files#r997496983.

I'll add a comment about why Promise.resolve() is in there.

expect(Ctor).not.toBeUndefined();
expect(Ctor).toBe(firstCtor);
});
it('whenDefined() should always return the same constructor - defined before whenDefined', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
it('whenDefined() should always return the same constructor - defined before whenDefined', () => {
it('whenDefined() should always return the same constructor - pivot defined before whenDefined', () => {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test actually runs with and without pivots.

});
});

it('whenDefined() should always return the same constructor - defined after whenDefined', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above - additional comments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above; I'll add a comment

Copy link
Contributor

@ravijayaramappa ravijayaramappa left a comment

Choose a reason for hiding this comment

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

LGTM, just some questions and minor comments

class MyCustomComponent extends HTMLElement {}
return Promise.resolve()
.then(() => {
expect(resolvedCtor).toEqual(undefined); // whenDefined does not leak pivot constructor
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This ^ verifies that whenDefined() does not leak a private element created by LWC. (At least insofar as we assume Promise.resolve() would have caught it)

@nolanlawson
Copy link
Contributor Author

/nucleus test

3 similar comments
@nolanlawson
Copy link
Contributor Author

/nucleus test

@nolanlawson
Copy link
Contributor Author

/nucleus test

@nolanlawson
Copy link
Contributor Author

/nucleus test

@nolanlawson nolanlawson merged commit 0635524 into master Oct 19, 2022
@nolanlawson nolanlawson deleted the nolan/node-reactions-pivots-undefined-2 branch October 19, 2022 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Pivots] customElements.get/whenDefined should not return PivotConstructor

3 participants