-
Notifications
You must be signed in to change notification settings - Fork 1k
[lit-labs/compiler] add element, attribute, boolean, event, property parts #4056
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
Conversation
🦋 Changeset detectedLatest commit: 54a40ce The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Tachometer Benchmark ResultsSummarynop-update
render
update
update-reflect
Resultsthis-change
render
update
update-reflect
this-change, tip-of-tree, previous-release
render
update
nop-update
this-change, tip-of-tree, previous-release
render
update
this-change, tip-of-tree, previous-release
render
update
update-reflect
|
| const partsObjectBinding = partConstructors | ||
| .map((part) => { | ||
| const identifierAlias = partIdentifiers[part]; | ||
| if (!identifierAlias) { |
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 I'm following this, you allow an object to be provided that renames the part constructors that are imported.
Who decides on the mapping, and is this needed? Can't you create a new unique name for the opt-level scope with TS APIs(factory.createUniqueName) and always rename the constructors?
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.
You are following correctly. The prep stage is where we decide which parts to import (because we know which parts have been encountered). Therefore if a file contains no attribute parts, we do not need to generate any imports.
The unique names are to provide defense against naming collisions – in the edge case where a file already contains an AttributePart identifier.
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't you just use factory.createUniqueName instead of taking a mapping?
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.
The mapping is created with factory.createUniqueName. I think we're just ironing out where that happens, and therefore what state needs to be associated with the unique identifier.
Currently the order of operations, taken from CompiledTemplatePass.getTransformer() are:
- Do a traversal of the AST to locate all the templates.
pass.findTemplates - Rewrite the found templates.
pass.rewriteTemplates. If an attribute part requires a constructor, the unique name is created here, and added to the part constructor mapping. - Finally, if any part constructors are used, add the part constructor import to the source file.
The benefit of this ordering is that we only add the import if a compiled template needs the constructors, and we easily can only emit the constructors that are needed.
An alternative order could be:
- Generate all the unique constructor identifiers up front.
- Do steps 1. and 2. above. While rewriting the templates track which constructors are actually used.
- Then pass in a set of which ones we want to actually add to the file with the import.
Book keeping is still required if we don't want to naively add all the imports.
Yet another alternative is to not do any bookkeeping, and add the import and all the Attribute part constructors regardless of usage. Although that may require a downstream transform to tree shake or dead code eliminate them if we want the lowest file size. Maybe it's ok to simplify this PR and emit them all.
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, and going back to your top questions.
Who decides on the mapping, and is this needed?
The rewriteTemplate method only adds a uniqueIdentifier for an attribute part constructor if it is used. It's not necessarily needed. We could do one of the alternatives in my comment above.
Can't you create a new unique name for the opt-level scope with TS APIs(factory.createUniqueName) and always rename the constructors.
What is the opt-level scope? In the current implementation the constructors are always renamed. They're just instantiated with factory.createUniqueName during the rewriteTemplate pass; so we both track the unique name and which ones are used in a CompiledTemplate. Then we only import and pull the constructors off _$LH that are used.
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.
Sorry, I meant "top-level" scope - ie, a name unique in that scope. But I mispoke a bit because I'm not sure that the TS API has scope-aware unique name generation, and the name has to be unique in all scopes that have templates, not just the top-level scope. Anyway, factory.createUniqueName() is fine.
augustjk
left a comment
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 to me overall.
augustjk
left a comment
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.
Thanks for adding the extra tests!
| /** | ||
| * Add Parts constructors import. To avoid identifier collisions, each part | ||
| * constructor is mapped to a unique identifier that must be provided. If no | ||
| * identifier is provided for a part, then the part will not be emitted. |
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 no identifier is provided for a part, then the part will not be emitted.
what does that mean? should we instead throw in that case?
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 means that if a certain part constructor does not have a unique identifier provided, we ignore it and don't pull it off _$LH.
This prevents compiled files containing every part constructor. We only import what is used.
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.
Oh, I see. I initially read it like "in [this case], [this parameter] is ignored", which set off my warning bells, but it's more like "add imports as requested by the parameters". The only things that are ignored are the values which aren't requested, i.e. that aren't in the attributePartConstructorNameMap
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.
Exactly! I'll reword the comment to make that clearer
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.
Reworded in 54a40ce
| /** | ||
| * Add Parts constructors import. To avoid identifier collisions, each part | ||
| * constructor is mapped to a unique identifier that must be provided. If no | ||
| * identifier is provided for a part, then the part will not be emitted. |
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.
Oh, I see. I initially read it like "in [this case], [this parameter] is ignored", which set off my warning bells, but it's more like "add imports as requested by the parameters". The only things that are ignored are the values which aren't requested, i.e. that aren't in the attributePartConstructorNameMap
| /** | ||
| * Add Parts constructors import. To avoid identifier collisions, each part | ||
| * constructor is mapped to a unique identifier that must be provided. If no | ||
| * identifier is provided for a part, then the part will not be emitted. | ||
| * | ||
| * This uses a namespace import for g3 compatibility. |
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.
Consider this rewrite:
| /** | |
| * Add Parts constructors import. To avoid identifier collisions, each part | |
| * constructor is mapped to a unique identifier that must be provided. If no | |
| * identifier is provided for a part, then the part will not be emitted. | |
| * | |
| * This uses a namespace import for g3 compatibility. | |
| /** | |
| * Add an import for the needed part constructors. | |
| * | |
| * By the time this function is called, we've already transformed the templates in the | |
| * file, and discovered which part constructors we need, and those transforms have | |
| * generated unique identifiers for each, so this function's job is to make sure the | |
| * requested constructors are available as those identifiers. | |
| * | |
| * This uses a namespace import for g3 compatibility. |
Issue: #189
RFC: lit/rfcs#21
Full prototype PR: #3984
Why
Prior to this PR the compiler only handled child parts. This PR adds all the attribute parts, as well as element parts.
How
The logic is quite similar to what happens in the
Template constructorcode inlit-html.ts.Because this is using parse5 and not TreeWalker, there are small differences in the API, but otherwise it's the same.
The compiler also needs to add imports for the various attribute parts, tracked in a separate data structure. During preparation if an attribute part is encountered (for the first time), a unique identifier is assigned for the attribute part constructor. This signals that the constructor needs to be imported and reassigned to the unique identifier. The unique identifier is used to avoid naming collisions.
While implementing this change, I also noticed that I messed up the
tsconfigand it wasn't accessing any DOM types – which are needed for parse5 types to work correctly. Hence the modifications totsconfig.Test plan
Tested by adding a golden for each individual part. This ensures that a single part only generates a single constructor import. A golden was added containing every part as well.
Note I also manually tested the kitchen sink compiled golden in the lit playground, and checked that each part is correctly handled.
Missing features
Note: while comparing the preparation code to lit-html, note that the prepare phase is not completed by this change. Still to come: raw text elements handling, bindings in comments.
Thank you!