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(babel-plugin-component): avoid unnecessary registerDecorators #2741

Closed
wants to merge 4 commits into from

Conversation

nolanlawson
Copy link
Collaborator

@nolanlawson nolanlawson commented Mar 15, 2022

Details

Fixes #2701

This makes it so that we don't unnecessarily call registerDecorators on classes that don't extend anything. (This is an effective test because LWC components must extend something – either LightningElement or a superclass/mixin.)

class NotALightningElement {
  foo: 'foo'
}
-lwc.registerDecorators(NotALightningElement, {
-  fields: ["foo"]
-});

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.

There is maybe some very clever way you could detect this if you really wanted to. But I don't think it actually matters in practice.

GUS work item

W-10736098

@nolanlawson
Copy link
Collaborator Author

/nucleus test

@@ -216,6 +216,12 @@ function decorators({ types: t }) {
return;
}

if (node.superClass === null) {
// Components *must* extend from either LightningElement or some other superclass (e.g. a mixin).
// We can skip classes without a superclass to avoid adding unnecessary registerDecorators() calls
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to confirm, we also don't need to call registerDecorators() or perform any of the subsequent logic on LightningElement, mixins, or other "superclasses" right?

Copy link
Contributor

Choose a reason for hiding this comment

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

(I believe we do have decorators in mixins, but I'm not familiar enough with this area to know whether we need to process the mixin class itself or whether it'll be sufficient to process the class extending/using the mixin)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Both superclasses and mixins may have fields, so we do need to call registerDecorators() on them, yes. For LightningElement no, we don't need to do that.

Luckily both superclasses and mixins extend from another class. I'll add a Karma test for this, though, since I realized we don't seem to have one for mixins.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great! Just a nitpick then, but my impression from the comment was that superclasses/mixins were similar to LightningElement in being top-level classes with no supers. If everything (components, mixins, etc) that has fields must extend from another class, can we make that clearer?

Copy link
Collaborator Author

@nolanlawson nolanlawson Mar 15, 2022

Choose a reason for hiding this comment

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

Ah you're right. Yes we can change this comment.

Copy link
Member

@pmdartus pmdartus left a comment

Choose a reason for hiding this comment

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

One of the side-effects of this change is that decorators applied to classes that aren't extending from a superclass will remain in the generated output. Until now all the decorators were transformed. Instead of failing silently at runtime, rollup will fail to parse the generated output:

Error: Unexpected character '@'

Before merging this PR, I would like to make sure we have considered all the ramifications of this change.

@nolanlawson
Copy link
Collaborator Author

Thanks @pmdartus. This seems like a question we can answer with static analysis:

Do any classes that don't have superclasses contain decorators like @api, @track, or @wire?

@nolanlawson
Copy link
Collaborator Author

W-10858780

@pmdartus
Copy link
Member

We could be running static analysis against database components to make decorators are applied to classes with a superclass. On top of this, I would like to make sure we have the correct linting in place to catch this error early on.

As mentioned in my previous comment, rollup will report a tokenization error if decorators aren't applied correctly. In terms of developer experience, it would be preferable to report a more meaningful error when the compiler detects an invalid usage of a decorator.

@nolanlawson
Copy link
Collaborator Author

In terms of developer experience, it would be preferable to report a more meaningful error when the compiler detects an invalid usage of a decorator.

We could write an ESLint rule. Or were you thinking of just changing the error message in Rollup?

@jye-sf
Copy link
Contributor

jye-sf commented Mar 17, 2022

Will the ESLint rule capture all possible paths, or are there environments where internal/external devs could bypass the ESLint rule? If the latter is at all possible, we may need to have some mechanism to detect the invalid usage (either explicitly detecting the usage during transformation/compilation or implicitly detecting the error message) in addition to the ESLint rule.

@pmdartus
Copy link
Member

In terms of developer experience, it would be preferable to report a more meaningful error when the compiler detects an invalid usage of a decorator.

We could write an ESLint rule. Or were you thinking of just changing the error message in Rollup?

Yes, I was thinking about creating a new ESLint rule. Today we have valid-api, valid-track and valid-wire. What about no-unknown-decorators?

Will the ESLint rule capture all possible paths, or are there environments where internal/external devs could bypass the ESLint rule?

ESLint should be able to capture all the possible usages of JavaScript decorators. As always internal developers could bypass it by using an eslint disablement mechanism. In which case, they are on their own.

@nolanlawson
Copy link
Collaborator Author

Yes, I was thinking about creating a new ESLint rule.

Filed W-10875815 to capture this.

@nolanlawson nolanlawson force-pushed the nolan/no-register-decorators branch from 6a96e20 to 3d7ae32 Compare May 6, 2022 22:13
@nolanlawson
Copy link
Collaborator Author

@pmdartus

One of the side-effects of this change is that decorators applied to classes that aren't extending from a superclass will remain in the generated output. Until now all the decorators were transformed. Instead of failing silently at runtime, rollup will fail to parse the generated output

After looking into this, I'm not sure this is actually a concern. It looks like, today, if you apply @api or @track to a non-LightningElement, it causes a runtime error in the browser (demo):

Uncaught TypeError: undefined is not a VM.
    at assertIsVM (main.js:6366:11)
    at getAssociatedVM (main.js:6378:5)
    at NotAnLwcComponent.set [as foo] (main.js:2762:18)
    at new NotAnLwcComponent (main.js:8132:14)
    at main.js:8145:13

So it seems that users are already prevented from applying decorators to non-LightningElements. This PR would just change a runtime error into a compile-time error. Maybe this is still a problem, but it seems like less of a problem than we previously thought.

@pmdartus
Copy link
Member

I agree with your previous statement. Running the example you shared with this change results in the following error:

src/modules/x/app/app.js
SyntaxError: /Users/p.dartus/code/github/nolanlawson/lwc-barebone/src/modules/x/app/app.js: Decorators are not enabled.
If you are using ["@babel/plugin-proposal-decorators", { "legacy": true }], make sure it comes *before* "@babel/plugin-proposal-class-properties" and enable loose mode, like so:
	["@babel/plugin-proposal-decorators", { "legacy": true }]
	["@babel/plugin-proposal-class-properties", { "loose": true }]
  1 | import { LightningElement, api } from 'lwc';
  2 |
> 3 | class NotAnLwcComponent {
    | ^
  4 |   @api foo = 'foo'
  5 | }
  6 |
    at File.buildCodeFrameError (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/core/lib/transformation/file/file.js:249:12)
    at NodePath.buildCodeFrameError (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/traverse/lib/path/index.js:143:21)
    at shouldTransform (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/helper-create-class-features-plugin/lib/features.js:127:16)
    at PluginPass.Class (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/helper-create-class-features-plugin/lib/index.js:90:44)
    at newFn (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/traverse/lib/visitors.js:177:21)
    at NodePath._call (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/traverse/lib/path/context.js:53:20)
    at NodePath.call (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/traverse/lib/path/context.js:40:17)
    at NodePath.visit (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/traverse/lib/path/context.js:100:31)
    at TraversalContext.visitQueue (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/traverse/lib/context.js:103:16)
    at TraversalContext.visitMultiple (/Users/p.dartus/code/github/nolanlawson/lwc-barebone/node_modules/@babel/traverse/lib/context.js:72:17)

If we can produce a better error than this, I would be more than happy to merge this PR.

@nolanlawson nolanlawson force-pushed the nolan/no-register-decorators branch from 0778132 to 56fe5e0 Compare May 12, 2022 16:48
if (!errored) {
fail('Expected an error to be thrown');
}
});
Copy link
Collaborator Author

@nolanlawson nolanlawson May 12, 2022

Choose a reason for hiding this comment

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

@pmdartus Added a more helpful error message ^

How it looks in the console:

[!] (plugin rollup-plugin-lwc-compiler) Error: SyntaxError: LWC1150: Decorators like @api, @track, and @wire are only supported in LightningElement classes. /Users/nlawson/workspace/lwc-barebone/src/modules/x/app/app.js: Decorators are not enabled.
If you are using ["@babel/plugin-proposal-decorators", { "legacy": true }], make sure it comes *before* "@babel/plugin-proposal-class-properties" and enable loose mode, like so:
        ["@babel/plugin-proposal-decorators", { "legacy": true }]
        ["@babel/plugin-proposal-class-properties", { "loose": true }]
  1 | import { LightningElement, api } from 'lwc';
  2 |
> 3 | class NotAnLwcComponent {
    | ^
  4 |   @api foo = 'foo'
  5 | }
  6 |
src/modules/x/app/app.js
SyntaxError: /Users/nlawson/workspace/lwc-barebone/src/modules/x/app/app.js: Decorators are not enabled.
If you are using ["@babel/plugin-proposal-decorators", { "legacy": true }], make sure it comes *before* "@babel/plugin-proposal-class-properties" and enable loose mode, like so:
        ["@babel/plugin-proposal-decorators", { "legacy": true }]
        ["@babel/plugin-proposal-class-properties", { "loose": true }]
  1 | import { LightningElement, api } from 'lwc';
  2 |
> 3 | class NotAnLwcComponent {
    | ^
  4 |   @api foo = 'foo'
  5 | }
  6 |
    at File.buildCodeFrameError (/Users/nlawson/workspace/lwc/node_modules/@babel/core/lib/transformation/file/file.js:249:12)
    at NodePath.buildCodeFrameError (/Users/nlawson/workspace/lwc/node_modules/@babel/core/node_modules/@babel/traverse/lib/path/index.js:143:21)
    at shouldTransform (/Users/nlawson/workspace/lwc/node_modules/@babel/helper-create-class-features-plugin/lib/features.js:127:16)
    at PluginPass.Class (/Users/nlawson/workspace/lwc/node_modules/@babel/helper-create-class-features-plugin/lib/index.js:92:44)
    at newFn (/Users/nlawson/workspace/lwc/node_modules/@babel/core/node_modules/@babel/traverse/lib/visitors.js:177:21)
    at NodePath._call (/Users/nlawson/workspace/lwc/node_modules/@babel/core/node_modules/@babel/traverse/lib/path/context.js:53:20)
    at NodePath.call (/Users/nlawson/workspace/lwc/node_modules/@babel/core/node_modules/@babel/traverse/lib/path/context.js:40:17)
    at NodePath.visit (/Users/nlawson/workspace/lwc/node_modules/@babel/core/node_modules/@babel/traverse/lib/path/context.js:100:31)
    at TraversalContext.visitQueue (/Users/nlawson/workspace/lwc/node_modules/@babel/core/node_modules/@babel/traverse/lib/context.js:103:16)
    at TraversalContext.visitMultiple (/Users/nlawson/workspace/lwc/node_modules/@babel/core/node_modules/@babel/traverse/lib/context.js:72:17)

@nolanlawson nolanlawson requested a review from pmdartus May 12, 2022 17:17
@nolanlawson
Copy link
Collaborator Author

After static analysis, the risk/reward ratio seems too high to make this worthwhile.

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.

[JS compiler] remove registerDecorators for non-LightningElement classes
4 participants