feat(template-compiler): dynamic components#3337
feat(template-compiler): dynamic components#3337jmsjtu merged 14 commits intojtu/dynamic-componentsfrom
Conversation
…omponents-template
…omponents-template
| 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Let's plan for more lwc:* nodes. I think it will come in the next release. (lwc:host)
| 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; | ||
| } |
There was a problem hiding this comment.
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={}>)
...llup-plugin/src/__tests__/compilerConfig/fixtures/dynamicImportConfig/dynamicImportConfig.js
Outdated
Show resolved
Hide resolved
ravijayaramappa
left a comment
There was a problem hiding this comment.
First pass. Yet to review template-compiler code
| StylesheetConfig, | ||
| CustomPropertiesResolution, | ||
| DynamicComponentConfig, | ||
| DynamicImportConfig, |
There was a problem hiding this comment.
This change will need a co-ordinated change in lwc-platform
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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:
- lwc-platform
- lwc-test
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| LWC_COMPONENT_TAG_WITHOUT_IS_DIRECTIVE: { | ||
| code: 1183, | ||
| message: `<lwc:component> must have an 'lwc:is' attribute.`, |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
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.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Same here. I know some Anglophones say "haitch" for "H", but "aitch" is more common AFAIK.
packages/@lwc/errors/src/compiler/error-info/template-transform.ts
Outdated
Show resolved
Hide resolved
ravijayaramappa
left a comment
There was a problem hiding this comment.
Awesome work with the test cases!
...wc/template-compiler/src/__tests__/fixtures/directive-for-each/dynamic-component/actual.html
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/__tests__/fixtures/directive-inner-html/invalid/actual.html
Outdated
Show resolved
Hide resolved
| @@ -0,0 +1,11 @@ | |||
| <template> | |||
| <lwc:component lwc:is={ctor1} lwc:if={visible.if}> | |||
There was a problem hiding this comment.
This test case also covers slotting. But can you please add test case specifically for slotting
- Slotting where content is not wrapped in vfragment
- Slotting where content is not statically optimized
- Scoped Slots
There was a problem hiding this comment.
@ravijayaramappa the latest commit contains the updated unit tests, let me know what you think!
...late-compiler/src/__tests__/fixtures/renderer-hooks/directive-inner-html/invalid/actual.html
Outdated
Show resolved
Hide resolved
...rc/__tests__/fixtures/scoped-slots/invalid/slot-data/invalid-on-non-slot-content/actual.html
Show resolved
Hide resolved
| ); | ||
| } | ||
| } else { | ||
| // Certain tag names that start with lwc:* are signals to the compiler for special behavior. |
There was a problem hiding this comment.
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.
| ) { | ||
| const { tagName: tag, namespaceURI } = parse5Elm; | ||
|
|
||
| if (tag === LwcTagName.Component) { |
There was a problem hiding this comment.
This block is designed to handle all future lwc:* tag names. How about a switch statement here to handle all future lwc:* cases? 😸
| let lwcElementParser; | ||
|
|
||
| switch (parse5Elm.tagName) { | ||
| case ctx.config.enableDynamicComponents && LwcTagName.Component: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ah I see, we are warning below. OK, this makes sense then.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
@jmsjtu So what happens if enableDynamicComponents is false but the template contains lwc:component?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
There was a problem hiding this comment.
@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); | ||
| } |
nolanlawson
left a comment
There was a problem hiding this comment.
This LGTM, and thank you for breaking up the PRs!
ravijayaramappa
left a comment
There was a problem hiding this comment.
LGTM, minor comments.
| let lwcElementParser; | ||
|
|
||
| switch (parse5Elm.tagName) { | ||
| case ctx.config.enableDynamicComponents && LwcTagName.Component: |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Yup, we're planning to add more LWC managed elements (that start with lwc:*) like lwc:host
| }), | ||
| ], | ||
| onwarn(warning) { | ||
| warnings.push(warning); |
There was a problem hiding this comment.
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.
| preserveHtmlComments?: boolean; | ||
| /** The configuration to pass to `@lwc/compiler`. */ | ||
| experimentalDynamicComponent?: DynamicComponentConfig; | ||
| experimentalDynamicComponent?: DynamicImportConfig; |
There was a problem hiding this comment.
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.

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-componentsbranch first is to ensure a single atomic commit for the dynamic components feature and to facilitate a clean coordinated release tolwc-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?
Does this pull request introduce an observable change?
GUS work item
W-12190514