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): binding ast parser #1498
Conversation
ca5010d
to
ba279ee
Compare
ba279ee
to
4350da8
Compare
@ekashida couple notes: let's create a test to make sure:
Outputs the same as
Also let's remove all nodes that are not components |
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 shaping up really nicely Eugene. I do propose 3 changes to consider:
- slim down the attribute object shape by eliminating 'object' / 'property' properties in lieu of collapsing them into a single type/value object. See details inline
- decouple binding parser into its own function for cleaner api that can be easily moved into its own package if needed
- debate to move the binding parser functionality into lwc-platform for initial internal consumption
prettier.format(expectedCode, { parser: 'babel' }) | ||
); | ||
} | ||
if (category === 'parser') { |
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.
should this be else 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.
I guess there's no difference since category can't be both 'compiler' and 'parser'
packages/@lwc/template-compiler/src/__tests__/fixtures/parser/for-each-template/expected.json
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/__tests__/fixtures/parser/if-inline/expected.json
Outdated
Show resolved
Hide resolved
export { IRElement } from './shared/types'; | ||
export { Config } from './config'; | ||
|
||
export function parse(source: string, config?: Config): TemplateParseResult { | ||
export function parse(source: string, config?: Config): BindingParseResult | TemplateParseResult { |
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 prefer exposing binding parser as a separate function to keep things more decoupled and not intermingle return types.
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.
Now that i'm thinking about it, would it make more sense to move bindingTransform API into lwc-platform to keep it internal, at least until we hash out all the details? I can see the value of providing data dependencies to external devs, just unsure if now is the time. Thoughts @caridy @diervo @ekashida ?
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 think it's fine since we have it behind a flag called experimentalDataBindingAST
type: 'BindingASTAttribute', | ||
name: attr.name, | ||
// Force BabelTemplateExpression because IRTemplateExpression.value | ||
// can also be a literal...is that a bug? |
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 this specific to literal attribute value?
prop="literal value"
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.
Yeah, you can't have a literal value in an expression so not sure about it
6301f19
to
87bb528
Compare
<foo-bar for:each={items} for:item="item" microwave={oven}></foo-bar> | ||
</template> | ||
`); | ||
expect(inlineRoot).toEqual(templateRoot); |
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.
Looks good, but i would like to revisit the binding attribute object shape. |
Just realized that the commend i made was hidden under resolved conversations. Re-posting here: I'm still not sure about the shape of the BindingASTAttribute object, specifically that it has an 'expression' and an 'object' and properties, i feel like it makes the shape inflexible. What would the shape look like if you had a deeply nested expression? eg: data={item.data.first.second.third.level} I think folding the type and value into a single object is more intuitive and easy to parse, for example: "attributes": [
{
"name": "data",
"value": {
value: "item.data.first.second.third",
type: "expression"
}
}
} or attributes = [{
"data": {
value: "item.data.first.second.third",
type: "expression"
},
}] |
@apapko What do you mean by "intuitive"? ASTs are generally not meant for humans to look at. Parsing an expression such as |
interface BindingASTComponentNode { | ||
attributes: BindingASTAttribute[]; | ||
children: Array<BindingASTNode | BindingASTSlotNode | BindingASTComponentNode>; | ||
component: true; |
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.
component: true
is probably is not needed
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.
Changes:
- Add stringLiteral attributes
- Change the type for directives and remove the directive member (ex. ["if"])
- remove
component:true
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.
Changes:
- Add stringLiteral attributes
- Change the type for directives and remove the directive member (ex. ["if"])
- remove
component:true
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.
Changes:
- Add stringLiteral attributes
- Change the type for directives and remove the directive member (ex. ["if"])
- remove
component:true
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.
Changes:
- Add stringLiteral attributes
- Change the type for directives and remove the directive member (ex. ["if"])
- remove
component:true
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.
Changes:
- Add stringLiteral attributes
- Change the type for directives and remove the directive member (ex. ["if"])
- remove
component:true
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.
Changes:
- Add stringLiteral attributes
- Change the type for directives and remove the directive member (ex. ["if"])
- remove
component:true
"children": [ | ||
{ | ||
"type": "BindingASTComponentNode", | ||
"tag": "foo-header", |
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 object is missing the slot
information, specifically the slot
name to which it is assigned. That will be relevant as the owner of that named slot
could wrap that named slot
with its own conditional.
<div class="foo-list-container" tabindex={tabIndex}> | ||
<foo-list> | ||
<template for:each={items} for:item="item" for:index="index"> | ||
<foo-list-item data={item.data} key={item.id}></foo-list-item> |
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.
Can you add a test for passing index
to one of foo-list-item
's props?
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.
Done!
const object = expression.object as babelTypes.MemberExpression; | ||
return { | ||
type: 'BindingASTMemberExpression', | ||
object: pruneMemberExpression(object), |
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 assume this covers patterns like {grandfather.father.son}
. Can you include a test that exercises this?
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.
@@ -35,7 +35,7 @@ interface BindingASTAttribute { | |||
} | |||
|
|||
interface BindingASTNode { | |||
children: Array<BindingASTNode | BindingASTComponentNode>; | |||
children: Array<BindingASTNode | BindingASTSlotNode | BindingASTComponentNode>; |
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 like the introduction of types!
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.
one thought: would it make sense to have the slot and the component nodes be a subclass of an BindingASTNode? This way we can use BindingASTNode type as a super type for all interfaces/functions and potentially reuse any common functionality if needed. For ex:
interface BindingASTSlotNode {
children: Array<BindingASTNode>;
}
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 believe this was addressed in the refactor
children: [], | ||
}; | ||
} | ||
throw new Error(`Expected element ${element.tag} to be a slot.`); |
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.
should we say Expected a <slot> element, instead received ${element.tag}
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 to Expected <slot> element but received <${element.tag}> element.
// node between the parent and component node. | ||
const directiveNode = transformToASTNode(currentElement); | ||
// node between the parent and child node. | ||
const directiveNode = transformToDirectiveNode(currentElement); | ||
parentASTNode.children.push(directiveNode); |
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 see that we have tests for slots with directives. Would it be worth to add a test for a custom component with a directive as well?
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.
See for-each-inline
5ac2ae7
to
a42cd01
Compare
(cherry picked from commit 1d77e5e)
(cherry picked from commit 1d77e5e)
(cherry picked from commit 1d77e5e)
(cherry picked from commit 1d77e5e)
Details
Template parser that outputs an AST for component data dependencies.
Does this PR introduce breaking changes?
No, it does not introduce breaking changes.