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

feat(template-compiler): dynamic components #3337

Merged
merged 14 commits into from
Feb 20, 2023

Conversation

jmsjtu
Copy link
Member

@jmsjtu jmsjtu commented Feb 9, 2023

Details

This PR implements the changes to the template compiler from the dynamic components RFC.

This is part 1 of 3 of the dynamic components implementation.

Part 2 (babel plugin / runtime implementation) can be found here.
Part 3 (karma integration tests) will be created after parts 1 and 2 have been merged.

PR structure

This implementation is broken down into 3 parts to make it easier to review and will be merged into the jtu/dynamic-components branch.

The reason for merging into the jtu/dynamic-components branch first is to ensure a single atomic commit for the dynamic components feature and to facilitate a clean coordinated release to lwc-platform, as this is a breaking change for lwc-platform.

There are a large number of file changes in this PR, most of which come from updates to the unit tests.

I've grouped all of the unit tests under the following two commits, if you are reviewing this PR through the GitHub UI you can exclude these two commits:

5b27c96
d7b1c63

This should reduce the number of files changed significantly.

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-12190514

Comment on lines +194 to +211
export interface BaseLwcElement<T extends `${LwcTagName}`> extends AbstractBaseElement {
type: 'Lwc';
name: T;
}

/**
* Node representing the lwc:component element
*/
export interface LwcComponent extends BaseLwcElement<'lwc:component'> {}

/**
* All supported special LWC tags, they should all begin with lwc:*
*/
export enum LwcTagName {
Component = 'lwc:component',
}

export type BaseElement = Element | ExternalComponent | Component | Slot | LwcComponent;
Copy link
Member Author

Choose a reason for hiding this comment

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

I created the AST nodes to look like this because I assumed we would have more lwc:* elements in the future.

I felt that this could help set the basis for future lwc:* components but it may also be premature to create the nodes structure ahead of time.

I can change this to a single AST node if the team prefers.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's plan for more lwc:* nodes. I think it will come in the next release. (lwc:host)

Comment on lines +568 to +592
function dc(
Ctor: LightningElementConstructor | null | undefined,
data: VElementData,
children: VNodes = EmptyArray
): VCustomElement | null {
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(isObject(data), `dc() 2nd argument data must be an object.`);
assert.isTrue(
arguments.length === 3 || isArray(children),
`dc() 3rd argument data must be an array.`
);
}
// null or undefined values should produce a null value in the VNodes
if (isNull(Ctor) || isUndefined(Ctor)) {
return null;
}

if (!isComponentConstructor(Ctor)) {
throw new Error(
`Invalid constructor ${toString(Ctor)} is not a LightningElement constructor.`
);
}

return null;
}
Copy link
Member Author

@jmsjtu jmsjtu Feb 9, 2023

Choose a reason for hiding this comment

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

This is a dummy implementation and is not how the function is designed to work. The implementation can be found in part 2, here.

This is only added here because the template compiler will call the dc function for the new dynamic components syntax (<lwc:component lwc:is={}>)

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.

First pass. Yet to review template-compiler code

@@ -11,7 +11,7 @@ export {
TransformOptions,
StylesheetConfig,
CustomPropertiesResolution,
DynamicComponentConfig,
DynamicImportConfig,
Copy link
Contributor

Choose a reason for hiding this comment

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

This change will need a co-ordinated change in lwc-platform

Copy link
Member Author

Choose a reason for hiding this comment

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

Also needs a coordinated release as experimentalDynamicDirective should now be available as a compiler option.

experimentalDynamicComponent?: DynamicComponentConfig;
dynamicImportConfig?: DynamicImportConfig;
// TODO [#3331]: remove usage of lwc:dynamic in 246
experimentalDynamicDirective?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO, these old types are best left alone for deprecation later.

The cascading effect of changing the existing types will become a overhead to manage. Here are some the repos that depend on this type:

  1. lwc-platform
  2. lwc-test

Copy link
Member Author

Choose a reason for hiding this comment

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

I debated about this initially but I think you're right. Changing these compiler options will lead to unnecessary overhead in managing the release of this feature to downstream dependencies.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've changed dynamicImportConfig back to experimentalDynamicComponents to prevent breaking downstream consumers and to ease the enablement of this feature.

We're planning to remove the lwc:dynamic directive entirely in 246 and I plan to deprecate experimentalDynamicComponent in favor of dynamicImportConfig as part of it.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can remove this from the PR description!
Screenshot 2023-02-10 at 12 27 57 PM

packages/@lwc/compiler/src/options.ts Outdated Show resolved Hide resolved
packages/@lwc/compiler/src/options.ts Show resolved Hide resolved
packages/@lwc/compiler/src/options.ts Show resolved Hide resolved

LWC_COMPONENT_TAG_WITHOUT_IS_DIRECTIVE: {
code: 1183,
message: `<lwc:component> must have an 'lwc:is' attribute.`,

This comment was marked as resolved.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I read this as "an ell" so I think it's fine to say an.


UNSUPPORTED_LWC_TAG_NAME: {
code: 1184,
message: '{0} is not a special LWC tag name and will be treated as an HTML element.',

This comment was marked as resolved.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here. I know some Anglophones say "haitch" for "H", but "aitch" is more common AFAIK.

packages/@lwc/template-compiler/src/parser/index.ts Outdated Show resolved Hide resolved
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.

Awesome work with the test cases!

@@ -0,0 +1,11 @@
<template>
<lwc:component lwc:is={ctor1} lwc:if={visible.if}>
Copy link
Contributor

Choose a reason for hiding this comment

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

This test case also covers slotting. But can you please add test case specifically for slotting

  1. Slotting where content is not wrapped in vfragment
  2. Slotting where content is not statically optimized
  3. Scoped Slots

Copy link
Member Author

Choose a reason for hiding this comment

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

@ravijayaramappa the latest commit contains the updated unit tests, let me know what you think!

);
}
} else {
// Certain tag names that start with lwc:* are signals to the compiler for special behavior.
Copy link
Contributor

Choose a reason for hiding this comment

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

With the current implementation, this block is never reached.
parseLwcElement is only invoked if isLwcElementTag is true. And isLwcElementTag is based on known LwcTagNames. I didn't see a test case with this error code either.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great catch! I've updated the logic on isLwcElementTag to only look at the tag name.

The updated logic should now check if the tag starts with lwc: and then create the appropriate AST node.

I've updated the tests here to include the new warnings.

) {
const { tagName: tag, namespaceURI } = parse5Elm;

if (tag === LwcTagName.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 block is designed to handle all future lwc:* tag names. How about a switch statement here to handle all future lwc:* cases? 😸

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated in the latest commit!

packages/@lwc/template-compiler/src/parser/index.ts Outdated Show resolved Hide resolved
packages/@lwc/template-compiler/src/parser/index.ts Outdated Show resolved Hide resolved
let lwcElementParser;

switch (parse5Elm.tagName) {
case ctx.config.enableDynamicComponents && LwcTagName.Component:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is kind of weird to me. So if ctx.config.enableDynamicComponents is false, then we will just treat lwc:component as an unknown HTML element? I guess it's fine, though, since throwing an error here is kind of pointless since we will just have to get rid of it when the enableDynamicComponents flag is removed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah I see, we are warning below. OK, this makes sense then.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, this switch statement is weird. @jmsjtu IMO, the checking of ctx.config.enableDynamicComponents should be inside parseLwcComponent. parseLwcComponent can validate if a template has lwc:is without enabling enableDynamicComponents and warn. This way all the validation for lwc:is is consolidated in one routine.

Copy link
Member Author

Choose a reason for hiding this comment

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

@nolanlawson @ravijayaramappa after some thought I've removed the LWC_COMPONENT_USED_WITHOUT_OPT warning.

The original intent of the warning was to notify component authors that lwc:component would be treated as an unknown HTML element if the ctx.config.enableDynamicComponents is not enabled.

However, now that I think about it, this can be confusing. I've opted instead to treat lwc:component the same regardless of whether ctx.config.enableDynamicComponents is enabled.

This way parseLwcComponent checks to see that lwc:component must be used with lwc:is and applyLwcIsDirective checks to see that ctx.config.enableDynamicComponents is enabled to use lwc:is.

The latest commit has the updated logic, let me know what you think.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@jmsjtu So what happens if enableDynamicComponents is false but the template contains lwc:component?

Copy link
Member Author

Choose a reason for hiding this comment

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

If enableDynamicComponents is false and a template contains a lwc:component without lwc:is it will throw an error saying you need to use lwc:is with lwc:component.

If enableDynamicComponents is false and have lwc:component + lwc:is you will get an error saying enableDynamicComponents must be enabled.

In either case if enableDynamicComponents is false or true we treat lwc:component the same and only validate the config on lwc:is.

Copy link
Contributor

@ravijayaramappa ravijayaramappa Feb 16, 2023

Choose a reason for hiding this comment

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

IMO, in both case #1 and #2, the error should say that enableDynamicComponents must be enabled to use <lwc:component> element. In other words, enableDynamicComponents flag gates the usage of both lwc:component and/or lwc:is.
My original suggestion was related to the organization of the code.

Copy link
Member Author

@jmsjtu jmsjtu Feb 20, 2023

Choose a reason for hiding this comment

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

@nolanlawson @ravijayaramappa - I decided to move the check for enableDynamicComponents from applyLwcIsDirective to parseLwcComponent.

I think this way the validation is cleaner, here are the new unit tests.

Let me know if you think should be changed. I'm going to merge this PR but it'll just go into the jtu/dynamic-components branch.

Sorry for the back and forth on this!

// Bind LWC directives to element
for (const matchAndApply of LWC_DIRECTIVE_PROCESSORS) {
matchAndApply(ctx, parsedAttr, element);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

🥇

Copy link
Collaborator

@nolanlawson nolanlawson left a comment

Choose a reason for hiding this comment

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

This LGTM, and thank you for breaking up the PRs!

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, minor comments.

let lwcElementParser;

switch (parse5Elm.tagName) {
case ctx.config.enableDynamicComponents && LwcTagName.Component:
Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, this switch statement is weird. @jmsjtu IMO, the checking of ctx.config.enableDynamicComponents should be inside parseLwcComponent. parseLwcComponent can validate if a template has lwc:is without enabling enableDynamicComponents and warn. This way all the validation for lwc:is is consolidated in one routine.

) {
let lwcElementParser;

switch (parse5Elm.tagName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to use a switch here rather than if (parse5Elm.tagName === LwcTagName.Component) { ... } else { ... }? Do you expect there will be additional cases here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, we're planning to add more LWC managed elements (that start with lwc:*) like lwc:host

packages/@lwc/template-compiler/src/shared/ast.ts Outdated Show resolved Hide resolved
@jmsjtu jmsjtu merged commit 093015d into jtu/dynamic-components Feb 20, 2023
@jmsjtu jmsjtu deleted the jtu/dynamic-components-template branch February 20, 2023 00:50
@@ -43,6 +45,9 @@ async function compileFixture({ input, dirname }: { input: string; dirname: stri
],
}),
],
onwarn(warning) {
warnings.push(warning);
Copy link
Member

Choose a reason for hiding this comment

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

I would recommend checking whether or not the warning is related to lwc:dynamic before silencing it. Swallowing all the errors might hide some unexpected behavior in tests.

@@ -30,7 +30,11 @@ export interface RollupLwcOptions {
/** The configuration to pass to the `@lwc/template-compiler`. */
preserveHtmlComments?: boolean;
/** The configuration to pass to `@lwc/compiler`. */
experimentalDynamicComponent?: DynamicComponentConfig;
experimentalDynamicComponent?: DynamicImportConfig;
Copy link
Member

Choose a reason for hiding this comment

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

IMO, it's quite tedious to update the RollupLwcOptions interface every time a new flag is added. It might be interesting to define the RollupLwcOptions interface as a union with the LWC compiler config interface.

@jmsjtu jmsjtu mentioned this pull request Mar 3, 2023
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.

6 participants