Skip to content

feat(ssr-compiler): validate api decorator usage#5071

Merged
wjhsf merged 27 commits intosalesforce:masterfrom
cardoso:ssr-validate-api-decorator
Feb 7, 2025
Merged

feat(ssr-compiler): validate api decorator usage#5071
wjhsf merged 27 commits intosalesforce:masterfrom
cardoso:ssr-validate-api-decorator

Conversation

@cardoso
Copy link
Contributor

@cardoso cardoso commented Dec 20, 2024

Details

I'm a bit short on time today to finish polishing this up, so I'm sending this as a draft.

Follow up to #5062 and #5114
Partially addresses #5032

This PR aims to replicate the api decorator validation done in babel-plugin-component and also related tests.

Some of the validation done in babel-plugin-component seems to be done after everything is collected, while here it's currently all done during traversal. Maybe that's just for better error reporting, which I think can be considered in a separate issue, but need to make sure this is not introducing undesirable behavior here.

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

@cardoso cardoso requested a review from a team as a code owner December 20, 2024 17:08
@cardoso cardoso marked this pull request as draft December 20, 2024 17:08
@cardoso cardoso marked this pull request as ready for review January 7, 2025 16:32
@cardoso
Copy link
Contributor Author

cardoso commented Jan 7, 2025

I'm a bit unsure about the scope of this PR, so I'm opening it up for review.

The main remaining TODO is to replicate the valid tests, but on second thought that might already be covered by the engine-server test suite?

Copy link
Contributor Author

@cardoso cardoso left a comment

Choose a reason for hiding this comment

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

The approach taken here is to mirror behavior and tests from babel-plugin-component. I focused on the api decorator errors and didn't introduce location info to keep the PR short.

Comment on lines +197 to +219

/**
* This set is for attributes that have a camel cased property name
* For example, div.tabIndex.
* We do not want users to define `@api` properties with these names
* Because the template will never call them. It'll always call the camel
* cased version.
*/
export const AMBIGUOUS_PROP_SET = /*@__PURE__@*/ new Map([
['bgcolor', 'bgColor'],
['accesskey', 'accessKey'],
['contenteditable', 'contentEditable'],
['tabindex', 'tabIndex'],
['maxlength', 'maxLength'],
['maxvalue', 'maxValue'],
]);

/**
* This set is for attributes that can never be defined
* by users on their components.
* We throw for these.
*/
export const DISALLOWED_PROP_SET = /*@__PURE__@*/ new Set(['is', 'class', 'slot', 'style']);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe @lwc/errors would be a better place for this. Not sure.

Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't specifically related to errors, @lwc/shared is fine.

Comment on lines +23 to +46
function validateName(definition: ApiDefinition) {
if (definition.computed) {
throw generateError(DecoratorErrors.PROPERTY_CANNOT_BE_COMPUTED);
}

const propertyName = definition.key.name;

switch (true) {
case propertyName === 'part':
throw generateError(DecoratorErrors.PROPERTY_NAME_PART_IS_RESERVED, propertyName);
case propertyName.startsWith('on'):
throw generateError(DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_ON, propertyName);
case propertyName.startsWith('data') && propertyName.length > 4:
throw generateError(DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_DATA, propertyName);
case DISALLOWED_PROP_SET.has(propertyName):
throw generateError(DecoratorErrors.PROPERTY_NAME_IS_RESERVED, propertyName);
case AMBIGUOUS_PROP_SET.has(propertyName):
throw generateError(
DecoratorErrors.PROPERTY_NAME_IS_AMBIGUOUS,
propertyName,
AMBIGUOUS_PROP_SET.get(propertyName)!
);
}
}
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 whole file mirrors the structure and logic from babel-plugin-component:

function validatePropertyName(property: NodePath<types.ClassMethod>, state: LwcBabelPluginPass) {
if (property.node.computed) {
throw generateError(
property,
{
errorInfo: DecoratorErrors.PROPERTY_CANNOT_BE_COMPUTED,
},
state
);
}

@cardoso cardoso marked this pull request as draft January 8, 2025 21:09
Copy link
Contributor Author

@cardoso cardoso left a comment

Choose a reason for hiding this comment

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

Moved back to draft as I learned some stuff in #5114 that will make this better 😅

@cardoso cardoso marked this pull request as ready for review January 28, 2025 12:43
Copy link
Contributor

@wjhsf wjhsf left a comment

Choose a reason for hiding this comment

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

I'll need to have a second look, later, to fully parse this, but here's a few minor code quality comments from a quick glance at this.

Comment on lines +197 to +219

/**
* This set is for attributes that have a camel cased property name
* For example, div.tabIndex.
* We do not want users to define `@api` properties with these names
* Because the template will never call them. It'll always call the camel
* cased version.
*/
export const AMBIGUOUS_PROP_SET = /*@__PURE__@*/ new Map([
['bgcolor', 'bgColor'],
['accesskey', 'accessKey'],
['contenteditable', 'contentEditable'],
['tabindex', 'tabIndex'],
['maxlength', 'maxLength'],
['maxvalue', 'maxValue'],
]);

/**
* This set is for attributes that can never be defined
* by users on their components.
* We throw for these.
*/
export const DISALLOWED_PROP_SET = /*@__PURE__@*/ new Set(['is', 'class', 'slot', 'style']);
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't specifically related to errors, @lwc/shared is fine.


const propertyName = definition.key.name;

switch (true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why switch (true) rather than a regular if/else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's just more readable and maintainable. Here's the code it's based on in babel-plugin-component:

if (propertyName === 'part') {
throw generateError(
property,
{
errorInfo: DecoratorErrors.PROPERTY_NAME_PART_IS_RESERVED,
messageArgs: [propertyName],
},
state
);
} else if (propertyName.startsWith('on')) {
throw generateError(
property,
{
errorInfo: DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_ON,
messageArgs: [propertyName],
},
state
);
} else if (propertyName.startsWith('data') && propertyName.length > 4) {
throw generateError(
property,
{
errorInfo: DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_DATA,
messageArgs: [propertyName],
},
state
);
} else if (DISALLOWED_PROP_SET.has(propertyName)) {
throw generateError(
property,
{
errorInfo: DecoratorErrors.PROPERTY_NAME_IS_RESERVED,
messageArgs: [propertyName],
},
state
);
} else if (AMBIGUOUS_PROP_SET.has(propertyName)) {
const camelCased = AMBIGUOUS_PROP_SET.get(propertyName);
throw generateError(
property,
{
errorInfo: DecoratorErrors.PROPERTY_NAME_IS_AMBIGUOUS,
messageArgs: [propertyName, camelCased],
},
state
);
}
}

Copy link
Contributor

@divmain divmain Jan 30, 2025

Choose a reason for hiding this comment

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

I've used this pattern on occasion and I'm generally okay with it. However, if other folks on the team prefer the if/else pattern, that's fine with me too. Sometimes it's better to conform to the norm of the codebase, even if something else would be more easily readable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I don't feel strongly about this. It was just easier for me to write and understand what was going on in the original code.

cardoso and others added 5 commits January 29, 2025 09:54
Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com>
Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com>
Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com>
@cardoso cardoso requested a review from wjhsf January 29, 2025 13:34
Copy link
Contributor

@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.

No objections to this PR, but I'll let Will give the final 👍 once he is satisfied.


const propertyName = definition.key.name;

switch (true) {
Copy link
Contributor

@divmain divmain Jan 30, 2025

Choose a reason for hiding this comment

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

I've used this pattern on occasion and I'm generally okay with it. However, if other folks on the team prefer the if/else pattern, that's fine with me too. Sometimes it's better to conform to the norm of the codebase, even if something else would be more easily readable.

@wjhsf
Copy link
Contributor

wjhsf commented Jan 31, 2025

/nucleus test

@cardoso cardoso requested review from divmain and wjhsf February 6, 2025 21:09
@wjhsf
Copy link
Contributor

wjhsf commented Feb 6, 2025

/nucleus test

@wjhsf wjhsf merged commit 62fecca into salesforce:master Feb 7, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants