-
Notifications
You must be signed in to change notification settings - Fork 393
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
feat(template-compiler): dynamic components #3337
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.
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.
There was a problem hiding this comment.
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
)
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.
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={}>
)
...llup-plugin/src/__tests__/compilerConfig/fixtures/dynamicImportConfig/dynamicImportConfig.js
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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:
- lwc-platform
- lwc-test
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
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.
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.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
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/errors/src/compiler/error-info/template-transform.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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!
...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.
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
- Slotting where content is not wrapped in vfragment
- Slotting where content is not statically optimized
- Scoped Slots
There was a problem hiding this comment.
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!
...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.
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 LwcTagName
s. I didn't see a test case with this error code either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
) { | ||
const { tagName: tag, namespaceURI } = parse5Elm; | ||
|
||
if (tag === LwcTagName.Component) { |
There was a problem hiding this comment.
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? 😸
There was a problem hiding this comment.
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!
let lwcElementParser; | ||
|
||
switch (parse5Elm.tagName) { | ||
case ctx.config.enableDynamicComponents && LwcTagName.Component: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥇
There was a problem hiding this 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!
There was a problem hiding this 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: |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
@@ -43,6 +45,9 @@ async function compileFixture({ input, dirname }: { input: string; dirname: stri | |||
], | |||
}), | |||
], | |||
onwarn(warning) { | |||
warnings.push(warning); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
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 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