Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@justinfagnani
Copy link
Collaborator

Builds on #4261

Adds a parseLitTemplate() function to lib/lit-html/template.ts. This returns an extension of a parse5 DocumentFragment that contains parts and strings, and where some nodes like comments and attributes are annotated with Lit parts. The parts have references to their expressions to allow easy traversal into nested expressions and templates.

I didn't yet port over the similar code in SSR or the compiler. These might have some different requirements (like parallel traversal or re-writing comment markers). Other future extensions could be detecting common invalid binding locations for linting.

@changeset-bot
Copy link

changeset-bot bot commented Oct 6, 2023

🦋 Changeset detected

Latest commit: 2ab8669

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@lit-labs/analyzer Minor
@lit-labs/cli Patch
@lit-labs/compiler Patch
eslint-plugin-lit Patch
@lit-labs/gen-manifest Patch
@lit-labs/gen-utils Patch
@lit-labs/gen-wrapper-angular Patch
@lit-labs/gen-wrapper-react Patch
@lit-labs/gen-wrapper-vue Patch

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

@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2023

📊 Tachometer Benchmark Results

Summary

nop-update

  • this-change, tip-of-tree, previous-release: unsure 🔍 -6% - +3% (-0.70ms - +0.40ms)
    this-change vs tip-of-tree

render

  • this-change: 50.51ms - 68.35ms
  • this-change, tip-of-tree, previous-release: unsure 🔍 -4% - +5% (-0.79ms - +1.02ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: unsure 🔍 -4% - +0% (-1.38ms - +0.18ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: faster ✔ 14% - 48% (7.53ms - 35.05ms)
    this-change vs tip-of-tree

update

  • this-change: 472.17ms - 478.28ms
  • this-change, tip-of-tree, previous-release: unsure 🔍 -8% - +3% (-2.98ms - +1.23ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: unsure 🔍 -3% - +1% (-2.33ms - +0.57ms)
    this-change vs tip-of-tree
  • this-change, tip-of-tree, previous-release: slower ❌ 0% - 3% (1.03ms - 12.99ms)
    this-change vs tip-of-tree

update-reflect

  • this-change: 466.34ms - 470.46ms
  • this-change, tip-of-tree, previous-release: unsure 🔍 -0% - +2% (-0.17ms - +8.65ms)
    this-change vs tip-of-tree

Results

this-change

render

VersionAvg timevs
50.51ms - 68.35ms-

update

VersionAvg timevs
472.17ms - 478.28ms-

update-reflect

VersionAvg timevs
466.34ms - 470.46ms-
this-change, tip-of-tree, previous-release

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
19.51ms - 20.55ms-unsure 🔍
-4% - +5%
-0.79ms - +1.02ms
unsure 🔍
-4% - +6%
-0.74ms - +1.26ms
tip-of-tree
tip-of-tree
19.17ms - 20.65msunsure 🔍
-5% - +4%
-1.02ms - +0.79ms
-unsure 🔍
-5% - +6%
-0.98ms - +1.27ms
previous-release
previous-release
18.92ms - 20.62msunsure 🔍
-6% - +4%
-1.26ms - +0.74ms
unsure 🔍
-6% - +5%
-1.27ms - +0.98ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
35.46ms - 38.43ms-unsure 🔍
-8% - +3%
-2.98ms - +1.23ms
unsure 🔍
-11% - +0%
-4.40ms - +0.24ms
tip-of-tree
tip-of-tree
36.33ms - 39.31msunsure 🔍
-3% - +8%
-1.23ms - +2.98ms
-unsure 🔍
-9% - +3%
-3.53ms - +1.13ms
previous-release
previous-release
37.23ms - 40.81msunsure 🔍
-1% - +12%
-0.24ms - +4.40ms
unsure 🔍
-3% - +9%
-1.13ms - +3.53ms
-

nop-update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
10.96ms - 11.70ms-unsure 🔍
-6% - +3%
-0.70ms - +0.40ms
unsure 🔍
-10% - +1%
-1.16ms - +0.08ms
tip-of-tree
tip-of-tree
11.07ms - 11.88msunsure 🔍
-4% - +6%
-0.40ms - +0.70ms
-unsure 🔍
-9% - +2%
-1.04ms - +0.25ms
previous-release
previous-release
11.37ms - 12.37msunsure 🔍
-1% - +10%
-0.08ms - +1.16ms
unsure 🔍
-2% - +9%
-0.25ms - +1.04ms
-
this-change, tip-of-tree, previous-release

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
34.89ms - 35.91ms-unsure 🔍
-4% - +0%
-1.38ms - +0.18ms
faster ✔
1% - 5%
0.37ms - 1.83ms
tip-of-tree
tip-of-tree
35.40ms - 36.59msunsure 🔍
-1% - +4%
-0.18ms - +1.38ms
-unsure 🔍
-4% - +1%
-1.29ms - +0.28ms
previous-release
previous-release
35.98ms - 37.02msslower ❌
1% - 5%
0.37ms - 1.83ms
unsure 🔍
-1% - +4%
-0.28ms - +1.29ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
75.18ms - 77.25ms-unsure 🔍
-3% - +1%
-2.33ms - +0.57ms
unsure 🔍
-4% - +0%
-2.87ms - +0.15ms
tip-of-tree
tip-of-tree
76.07ms - 78.11msunsure 🔍
-1% - +3%
-0.57ms - +2.33ms
-unsure 🔍
-3% - +1%
-1.98ms - +1.02ms
previous-release
previous-release
76.47ms - 78.67msunsure 🔍
-0% - +4%
-0.15ms - +2.87ms
unsure 🔍
-1% - +3%
-1.02ms - +1.98ms
-
this-change, tip-of-tree, previous-release

render

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
38.46ms - 55.98ms-faster ✔
14% - 48%
7.53ms - 35.05ms
unsure 🔍
-42% - -2%
-26.93ms - +0.23ms
tip-of-tree
tip-of-tree
57.90ms - 79.12msslower ❌
10% - 80%
7.53ms - 35.05ms
-unsure 🔍
-13% - +39%
-6.90ms - +22.78ms
previous-release
previous-release
50.19ms - 70.95msunsure 🔍
-4% - +61%
-0.23ms - +26.93ms
unsure 🔍
-32% - +9%
-22.78ms - +6.90ms
-

update

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
488.82ms - 497.01ms-slower ❌
0% - 3%
1.03ms - 12.99ms
unsure 🔍
-0% - +2%
-2.31ms - +10.30ms
tip-of-tree
tip-of-tree
481.55ms - 490.26msfaster ✔
0% - 3%
1.03ms - 12.99ms
-unsure 🔍
-2% - +1%
-9.50ms - +3.46ms
previous-release
previous-release
484.12ms - 493.72msunsure 🔍
-2% - +0%
-10.30ms - +2.31ms
unsure 🔍
-1% - +2%
-3.46ms - +9.50ms
-

update-reflect

VersionAvg timevs this-change
vs tip-of-tree
tip-of-tree
vs previous-release
previous-release
this-change
494.15ms - 501.16ms-unsure 🔍
-0% - +2%
-0.17ms - +8.65ms
slower ❌
0% - 2%
0.20ms - 8.66ms
tip-of-tree
tip-of-tree
490.74ms - 496.08msunsure 🔍
-2% - +0%
-8.65ms - +0.17ms
-unsure 🔍
-1% - +1%
-3.38ms - +3.76ms
previous-release
previous-release
490.86ms - 495.59msfaster ✔
0% - 2%
0.20ms - 8.66ms
unsure 🔍
-1% - +1%
-3.76ms - +3.38ms
-

tachometer-reporter-action v2 for Benchmarks

@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2023

The size of lit-html.js and lit-core.min.js are as expected.

Copy link
Contributor

@AndrewJakubowicz AndrewJakubowicz left a comment

Choose a reason for hiding this comment

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

Very quick initial look.
Is it worth finding ways to reduce the number of type assertions?

Also love the idea of having one great analyzer that we can share.

export const parseLitTemplate = (
node: ts.TaggedTemplateExpression,
ts: TypeScript,
_checker: ts.TypeChecker
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth removing as not used currently?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have it here for consistency with other functions that need the checker, because it's kind of unusual that this function doesn't need the checker. In case we need to use the checker in here in the future, it won't be a breaking change to public consumers.

import type ts from 'typescript';
import {_$LH} from 'lit-html/private-ssr-support.js';
import {parseFragment} from 'parse5';
// import type {Attribute} from 'parse5/dist/common/token.js';
Copy link
Contributor

Choose a reason for hiding this comment

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

Intended to leave in?

const values = ts.isNoSubstitutionTemplateLiteral(node.template)
? []
: node.template.templateSpans.map((s) => s.expression);
const parts: Array<PartInfo> = [];
Copy link
Member

@kevinpschaaf kevinpschaaf Oct 6, 2023

Choose a reason for hiding this comment

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

It might be good to clarify the use cases for the API's here. i.e. whether we intend the DOM AST that comes out to be mutable or not, since the parts list will only be the "as-parsed" values.

This could either be made a getter that reifies the parts list each access (slower), or could add an "invalidateParts" API?

And are you planning on adding something to go from a LitTemplate back to a ts.TaggedTemplateExpression?

Copy link
Member

Choose a reason for hiding this comment

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

It's obv fine for some use cases, where you just want to statically analyze only, but for others the parts sort of seems not that useful.

Copy link
Collaborator

Choose a reason for hiding this comment

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

fwiw in the lint side of things, we don't need to mutate nodes but we do need a way to translate between nodes or positions. i.e. we need a way to go for a parse5 node to an estree node (whether by reference or just by knowing the position)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Adding more in the comment. I personally view this utility right now as being similar to asking a parser to parse a file - it'll generally give you a new AST each time you call it.

I think this can be a primitive used by a cache-backed function with a different name. Like getLitTemplate(node) that assumes the TS node is immutable and will return the same Lit template instance on repeated calls.

@justinfagnani justinfagnani changed the title Add template parser to analyzer [labs/analyzer] Add template parser to analyzer Feb 1, 2024
Base automatically changed from analyzer-templates to main February 1, 2024 17:34
@justinfagnani justinfagnani force-pushed the analyzer-template-2 branch 3 times, most recently from a853ce1 to ec459b9 Compare February 2, 2024 00:00
Comment on lines +18 to +19
* Returns true if the decorator site is a simple called decorator factory of
* the form `@decoratorName()`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Returns true if the decorator site is a simple called decorator factory of
* the form `@decoratorName()`.
* Given a decorator name, returns true if the decorator site is a simple called
* decorator factory of the form `@decoratorName()` where `decoratorName`
* matches the given name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was a file move that Git or GitHub didn't pick up. I'd rather keep this PR limited to the template parser.

Comment on lines +60 to +66
/**
* A narrower type for ts.Decorator that represents the shape of an analyzable
* `@customElement('x')` callsite.
*/
interface PropertyDecorator extends ts.Decorator {
readonly expression: ts.CallExpression;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could also remove this and rename CustomElementDecorator to something like SimpleDecorator. Also, jsdoc on this should be updated to say @property()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same as the other comment about this being a file move.

if (!ts.isTaggedTemplateExpression(node)) {
return false;
}
if (ts.isIdentifier(node.tag)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can also maybe cover a lot of edge stuff with supporting comments with something maybe like:

node.getFullText().slice(node.getFullStart(), node.getStart()).includes('@litHtmlTemplate')

but more idomatic comment gathering

: node.template.templateSpans;

const parts: Array<PartInfo> = [];
const [html, boundAttributeNames] = getTemplateHtml(strings, 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

OMG this is a clever and wild approach! I was incredibly confused as to why there were litPart comment nodes in a lit tagged template literal and now this entire file makes sense now!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks! This was the plan from a long time ago :) I hope it hold up actually.

// in the prepared and parsed HTML.

// TODO (justinfagnani): implement line and column adjustments
let lineAdjust = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really even need lines? The TS AST nor many ESTree-compatible ASTs use a start and end offset. The exception seems to be babel. I'm in the camp that we can either wipe them or just ignore them for the sake of simplicity and performance

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

parse5 includes lines, so I didn't want to leave them unadjusted and incorrect.

sourceCodeLocationInfo: true,
});

traverse(ast, {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not do some pre-processing of the template strings before traversing. e.g.

  • Create a map of original-positions to processed-positions and vice versa
  • iterate through the strings char by char
  • store index of EVERY char in both maps
  • When we get to the end of the string and to an expression replace the expression with a comment
    • original position is += the full text length of the expression
    • processed position is the length of your marker text

Now, if you have a map of all positions mapped from one template to another, when you traverse the parse5 tree and visit a node, it doesn't matter much what the node is, as long as it has a sourceCodeLocation. The only times you'd have to care is if it's an element. In this case you'd have to also update el.sourceCodeLocation.{attrs,startTag,endTag}

The negatives here are that you are making a map that is memory size of O(n) where n is the max length of the template literal but it should be a map of Map<number,number>, so how bad can it be???

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't fully follow this approach. Can you explain it to me on a call?

type TypeScript = typeof ts;

// Why, oh why are parse5 types so weird? We re-export them to make them easier
// to use.
Copy link
Collaborator

Choose a reason for hiding this comment

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

you should be able to pull these from @parse5/tools itself now

i think it re-exports the defaults

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't see where to get all of these types from.

'@lit-labs/analyzer': minor
---

Add a template parser
Copy link
Collaborator

Choose a reason for hiding this comment

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

Duplicate changeset

"test:server:uvu": {
"#comment": "The quotes around the file regex must be double quotes on windows!",
"command": "uvu test/server \"_test\\.js$\" -i \"lit-html/\"",
"command": "uvu test/server \"_test\\.js$\" -i \"lit/template_test\"",
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's with these changes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just getting the uvu tests run by uvu and the node tests to be run run by node. I want to convert everything to node soon.

@justinfagnani justinfagnani merged commit 5a7ae16 into main Aug 22, 2025
10 checks passed
@justinfagnani justinfagnani deleted the analyzer-template-2 branch August 22, 2025 03:46
@lit-robot lit-robot mentioned this pull request Dec 18, 2025
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.

6 participants