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

fix(ssr): global attribute disparities #5247

Merged
merged 17 commits into from
Mar 1, 2025

Conversation

jhefferman-sfdc
Copy link
Collaborator

Details

Does this pull request introduce a breaking change?

  • 😮‍💨 No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • 🤞 No, it does not introduce an observable change.

GUS work item

W-17572462

@jhefferman-sfdc jhefferman-sfdc requested a review from a team as a code owner February 28, 2025 00:42
"entry": "x/component"
"entry": "x/component",
"ssrFiles": {
"error": "error-ssr.txt",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

V1/V2 deviate, where V2 parent overrides the child value (as does CSR). V1 is incorrect.

"entry": "x/component",
"ssrFiles": {
"error": "error-ssr.txt",
"expected": "expected-ssr.html"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

V1/V2 deviate, where V2 matches CSR

@@ -92,13 +87,12 @@ export class LightningElement implements PropsAvailableAtConstruction {
// Avoid setting the following types of properties that should not be set:
// - Properties that are not public.
// - Properties that are not global.
// - Properties that are global but are internally overridden.
Copy link
Collaborator Author

@jhefferman-sfdc jhefferman-sfdc Feb 28, 2025

Choose a reason for hiding this comment

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

Does not seem correct. In CSR, child definitions do not override parent. We had tests for this, added more.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If we had fixture tests for this, and it wasn't caught, we should add some karma tests to get the real browser behavior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah i'm adding some karma for these

@jhefferman-sfdc
Copy link
Collaborator Author

/nucleus ignore --reason "Unrelated __lwc failure in webruntime"

@@ -92,13 +87,12 @@ export class LightningElement implements PropsAvailableAtConstruction {
// Avoid setting the following types of properties that should not be set:
// - Properties that are not public.
// - Properties that are not global.
// - Properties that are global but are internally overridden.
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we had fixture tests for this, and it wasn't caught, we should add some karma tests to get the real browser behavior.

// This follows the historical behavior in api.ts:
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
const shouldNormalize = parseInt(literalValue) > 0;
literalValue = shouldNormalize ? '0' : literalValue;
}
return b.property('init', key, b.literal(literalValue));
} else if (value.type === 'Literal' && typeof value.value === 'boolean') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens if tabindex is set to a boolean value? Can it be?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes in the case of <div tabindex>. I don't know how that would be treated in the browser. Good thing to check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup, per the strange CSR logic, booleans are preserved, so nothing to do for this case here.

Comment on lines 158 to 162
// Global HTML "tabindex" attribute is specially massaged into a stringified number
// This follows the historical behavior in api.ts:
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
const shouldNormalize = parseInt(literalValue) > 0;
literalValue = shouldNormalize ? '0' : literalValue;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Global HTML "tabindex" attribute is specially massaged into a stringified number
// This follows the historical behavior in api.ts:
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
const shouldNormalize = parseInt(literalValue) > 0;
literalValue = shouldNormalize ? '0' : literalValue;
literalValue = normalizeTabIndex(literalValue);

If we've got the util, use it!

* This follows the historical behavior in api.ts:
* https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
*/
export function normalizeTabIndex(value: any): any {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add some unit tests for this!

* This follows the historical behavior in api.ts:
* https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
*/
export function normalizeTabIndex(value: any): any {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export function normalizeTabIndex(value: any): any {
export function normalizeTabIndex(value: number): string | number {

The original function was more broadly typed than necessary, since this is new we can be more restrictive.

Also, it's weird that this returns either the unmodified number or the string '0'. We should either always return a number or always return a string. (It's diverging from the original implementation, but not in a meaningful way.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes this is weird behavior. There's probably an actual reason for it though - I wouldn't necessarily assume that the divergence is unmeaningful.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In the end, the attribute is going to end up a string attribute / numeric prop. This particular function is just an intermediate step in that. This particular function copies the business logic from engine-core, but the rest of the implementation (outside the function) is different, so I don't think it makes much of a difference whether the final cast to string/number happens in or out of the function.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export function normalizeTabIndex(value: any): any {
export function normalizeTabIndex<T>(value: T): T | string {

If you want to allow any type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried generic but it added issues with the comparison. I wanted to keep it exactly as the original to avoid any discrepancies

@divmain divmain changed the title fix global attribute disparities fix(ssr): global attribute disparities Feb 28, 2025
Copy link
Collaborator

@divmain divmain left a comment

Choose a reason for hiding this comment

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

Looks like there's a tiny bit more polish needed, but otherwise looks fantastic.

@@ -0,0 +1,16 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* Copyright (c) 2020, salesforce.com, inc.
* Copyright (c) 2025, salesforce.com, inc.

* This follows the historical behavior in api.ts:
* https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
*/
export function normalizeTabIndex(value: any): any {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes this is weird behavior. There's probably an actual reason for it though - I wouldn't necessarily assume that the divergence is unmeaningful.

@@ -8,8 +8,5 @@
// We should slowly drive down these test failures or at least document where we expect the failures
// TODO [#4815]: enable all SSR v2 tests
export const expectedFailures = new Set([
'attribute-global-html/as-component-prop/undeclared/config.json',
'attribute-global-html/as-component-prop/without-@api/config.json',
'known-boolean-attributes/default-def-html-attributes/static-on-component/config.json',
Copy link
Collaborator

Choose a reason for hiding this comment

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

🎉🎉🎉🎉🎉

// This follows the historical behavior in api.ts:
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
const shouldNormalize = parseInt(literalValue) > 0;
literalValue = shouldNormalize ? '0' : literalValue;
}
return b.property('init', key, b.literal(literalValue));
} else if (value.type === 'Literal' && typeof value.value === 'boolean') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes in the case of <div tabindex>. I don't know how that would be treated in the browser. Good thing to check.

if (name === 'class') {
cxt.import('normalizeClass');
propValue = b.callExpression(b.identifier('normalizeClass'), [propValue]);
} else if (name.toLowerCase() === 'tabindex') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: there is more than one name.toLowerCase() showing up in this function; probably could define nameLower variable.

@jhefferman-sfdc
Copy link
Collaborator Author

/nucleus ignore --reason "Unrelated __lwc failure in webruntime"

@jhefferman-sfdc
Copy link
Collaborator Author

I added hydration coverage for global property precedence. As SSR V1 renders the wrong values, it causes hydration errors. I added a check here and an expectation for those failures. Here is the work item to review this and change the pattern if needed: W-17939029

@jhefferman-sfdc jhefferman-sfdc merged commit b5d54cd into master Mar 1, 2025
11 checks passed
@jhefferman-sfdc jhefferman-sfdc deleted the jhefferman/attribute-props branch March 1, 2025 02:07
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.

3 participants