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): binding ast parser #1498

Merged
merged 28 commits into from Sep 23, 2019
Merged

Conversation

ekashida
Copy link
Member

@ekashida ekashida commented Sep 9, 2019

Details

Template parser that outputs an AST for component data dependencies.

Does this PR introduce breaking changes?

  • No, it does not introduce breaking changes.

@diervo
Copy link
Contributor

diervo commented Sep 10, 2019

@ekashida couple notes:

let's create a test to make sure:

<template if:true={x}>
  <x-foo></x-foo>
</template>

Outputs the same as

  <x-foo if:true{x}></x-foo>

Also let's remove all nodes that are not components

Copy link
Collaborator

@apapko apapko left a 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:

  1. 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
  2. decouple binding parser into its own function for cleaner api that can be easily moved into its own package if needed
  3. debate to move the binding parser functionality into lwc-platform for initial internal consumption

prettier.format(expectedCode, { parser: 'babel' })
);
}
if (category === 'parser') {
Copy link
Collaborator

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?

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 guess there's no difference since category can't be both 'compiler' and 'parser'

packages/@lwc/template-compiler/src/config.ts Show resolved Hide resolved
packages/@lwc/template-compiler/src/config.ts 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 {
Copy link
Collaborator

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.

Copy link
Collaborator

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 ?

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 think it's fine since we have it behind a flag called experimentalDataBindingAST

packages/@lwc/template-compiler/src/transform/binding.ts Outdated Show resolved Hide resolved
packages/@lwc/template-compiler/src/transform/binding.ts Outdated Show resolved Hide resolved
type: 'BindingASTAttribute',
name: attr.name,
// Force BabelTemplateExpression because IRTemplateExpression.value
// can also be a literal...is that a bug?
Copy link
Collaborator

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"

Copy link
Member Author

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

<foo-bar for:each={items} for:item="item" microwave={oven}></foo-bar>
</template>
`);
expect(inlineRoot).toEqual(templateRoot);
Copy link
Member Author

Choose a reason for hiding this comment

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

@apapko
Copy link
Collaborator

apapko commented Sep 12, 2019

Looks good, but i would like to revisit the binding attribute object shape.

@apapko
Copy link
Collaborator

apapko commented Sep 12, 2019

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"
        },
  }]

@ekashida
Copy link
Member Author

ekashida commented Sep 12, 2019

I think folding the type and value into a single object is more intuitive and easy to parse

@apapko What do you mean by "intuitive"? ASTs are generally not meant for humans to look at. Parsing an expression such as item.data.first.second.third into a recursive object/property structure will make it very easy for programs to analyze the AST.

interface BindingASTComponentNode {
attributes: BindingASTAttribute[];
children: Array<BindingASTNode | BindingASTSlotNode | BindingASTComponentNode>;
component: true;
Copy link
Contributor

@diervo diervo Sep 19, 2019

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

Copy link
Contributor

@diervo diervo left a 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

Copy link
Contributor

@diervo diervo left a 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

Copy link
Contributor

@diervo diervo left a 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

Copy link
Contributor

@diervo diervo left a 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

Copy link
Contributor

@diervo diervo left a 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

Copy link
Contributor

@diervo diervo left a 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",

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>

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?

Copy link
Member Author

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),

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?

Copy link
Member Author

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>;
Copy link
Collaborator

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!

Copy link
Collaborator

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>;
}

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 believe this was addressed in the refactor

children: [],
};
}
throw new Error(`Expected element ${element.tag} to be a slot.`);
Copy link
Collaborator

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}

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 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);
Copy link
Collaborator

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

See for-each-inline

@ekashida ekashida merged commit 1d77e5e into master Sep 23, 2019
@ekashida ekashida deleted the ekashida/binding-ast branch September 23, 2019 20:20
ekashida added a commit that referenced this pull request Sep 24, 2019
ekashida added a commit that referenced this pull request Sep 24, 2019
ekashida added a commit that referenced this pull request Sep 24, 2019
* feat(template-compiler): binding ast parser (#1498)


(cherry picked from commit 1d77e5e)

* chore: locally define isUndefined
ekashida added a commit that referenced this pull request Sep 24, 2019
ekashida added a commit that referenced this pull request Sep 24, 2019
ekashida added a commit that referenced this pull request Sep 27, 2019
ekashida added a commit that referenced this pull request Sep 27, 2019
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.

None yet

4 participants