-
Notifications
You must be signed in to change notification settings - Fork 395
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): experimental template expressions #3376
Conversation
6224ace
to
1e27a83
Compare
|
||
// Start parsing immediately after the opening curly brace. | ||
const estreeNode = parseExpressionAt(html, javascriptExprStart, { | ||
ecmaVersion: 2022, |
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.
ES2022 will be parsed, but none of the newly introduced syntax is permitted. This will just make for better error messages; the component author will be notified about their use of disallowed syntax (if they try to use it) rather than seeing an arcane parsing error.
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.
Are we going to need to periodically update this? Can we just use "latest"
? (Which, presumably, will bump when we bump our acorn dep.)
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.
Good question. With the current implementation, the AST is validated using a blocklist for disallowed node types. If we used "latest"
here, we'd probably want to invert that to avoid permitting new JS syntax proposals. However, the validation logic would be much more complex if using an allowlist vs blocklist.
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.
Added TMPL_EXPR_ECMASCRIPT_EDITION
constant to ensure consistency
// TODO [#3370]: If complex template expressions are adopted, `preparsedJsExpressions` | ||
// should be checked. However, to avoid significant complications in the internal types, | ||
// arising from supporting both implementations simultaneously, we will re-parse the | ||
// expression here when `ctx.config.experimentalComplexExpressions` is true. | ||
if (isExpression(value) && !escapedExpression) { | ||
attrValue = parseExpression(ctx, value, location); |
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 isn't ideal. The expression at that location
has already been parsed (during tokenization) when the experimentalComplexExpressions
flag is enabled. Ideally, we should check the preparsedJsExpressions
to find the matching one, and use that for the attrValue
. However, this would require changes to internal types (like that of attrValue
) that cascade into messy changes in multiple places.
To be clear, these are solely type issues. However, to avoid making messy changes across our internal types, I opted to parse the expression a second time, here. This seemed a decent tradeoff for a pilot feature - and can & should be addressed when the flag goes away. I'm open to going the other way if there are strong opinions in favor.
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 is why parseExpressionAt
is called twice?
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.
Yes.
|
||
// Start parsing immediately after the opening curly brace. | ||
const estreeNode = parseExpressionAt(html, javascriptExprStart, { | ||
ecmaVersion: 2022, |
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.
Are we going to need to periodically update this? Can we just use "latest"
? (Which, presumably, will bump when we bump our acorn dep.)
packages/@lwc/template-compiler/src/parser/expression-complex/html.ts
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Outdated
Show resolved
Hide resolved
// TODO [#3370]: If complex template expressions are adopted, `preparsedJsExpressions` | ||
// should be checked. However, to avoid significant complications in the internal types, | ||
// arising from supporting both implementations simultaneously, we will re-parse the | ||
// expression here when `ctx.config.experimentalComplexExpressions` is true. | ||
if (isExpression(value) && !escapedExpression) { | ||
attrValue = parseExpression(ctx, value, location); |
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 is why parseExpressionAt
is called twice?
ef4f825
to
ae5fc8c
Compare
9b9fc3d
to
296d82d
Compare
const POTENTIAL_EXPRESSION_RE = /^.?{.+}.*$/; | ||
const WHITESPACES_RE = /\s/; | ||
|
||
export function isExpression(source: string): boolean { | ||
return !!source.match(VALID_EXPRESSION_RE); | ||
return source[0] === '{' && source.slice(-1) === '}'; |
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 don't believe this should be an observable/breaking change. The only difference between these two conditions is whether source
contains a newline. However, unquoted attribute values (in HTML) cannot contain a new line - so the two are functionally equivalent with the flag disabled.
With the flag enabled, they are not functionally equivalent - the new condition will permit a newline in the attribute value, which is the desired behavior.
Commenting for visibility, in case there is something I'm not considering.
296d82d
to
cde0acc
Compare
@@ -61,6 +62,9 @@ export interface MemberExpression extends BaseNode { | |||
|
|||
export type Expression = Identifier | MemberExpression; | |||
|
|||
// TODO [#3370]: remove experimental template expression flag | |||
export type ComplexExpression = AcornNode & { value?: any }; |
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 AcornNode
type is quite opaque. As far as I understand Acorn return an ESTree-compliant AST. Would it make more sense to have it typed as an ESTree node?
There is also some overlap between the existing Identifier
and Expression
template AST node, would it make sense to merge those type definitions?
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 experimented with this a bit, and any changes to this type resulted in really messy changes required across the codebase. I'm going to leave it for now. However, I've updated the comment to reflect that ComplexExpression
should be replaced with ESTree's Node
if/when the flag is removed.
packages/@lwc/template-compiler/src/parser/expression-complex/html.ts
Outdated
Show resolved
Hide resolved
ParserDiagnostics.INVALID_EXPR_COMMENTS_DISALLOWED | ||
); | ||
invariant( | ||
!STATEMENT_TYPES.has(node.type), |
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.
Instead of blocking statements nodes, I think it is more future-proof to allow list expressions. This way we would have control over which expressions we allow in the template as new expression types are added to the language.
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 originally created an allowlist for node types, but it was messier to translate the decisions in the design doc this into code. I'll probably revisit, but likely in a follow-up PR.
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/types.ts
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/html.ts
Outdated
Show resolved
Hide resolved
7b66bcb
to
d77c47a
Compare
export function isExpression(node: Expression | Literal): node is Expression { | ||
return node.type === 'Identifier' || node.type === 'MemberExpression'; | ||
export function isExpression(node: BaseNode | Literal): node is Expression { | ||
return node.type !== 'Literal'; |
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.
In a previous iteration of this PR, isExpression
had been changed to accept the template expressions flag as its second argument. This led to a deluge of one-line changes where the flag had to be passed in.
To avoid this, I've updated the implementation here. With the flag disabled, the node will have type Identifer
, MemberExpression
, or Literal
. Thus this new condition should be equivalent. The condition also works for the flag-enabled scenario.
d77c47a
to
95b0a46
Compare
95b0a46
to
c6d68b2
Compare
b014017
to
6076891
Compare
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 round of review. Reviewing tests now.
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/parser/expression-complex/validate.ts
Outdated
Show resolved
Hide resolved
packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/await/metadata.json
Outdated
Show resolved
Hide resolved
"warnings": [ | ||
{ | ||
"code": 1052, | ||
"message": "Unexpected compilation error: LWC1052: Error parsing attribute: Unexpected token (3:31)", |
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 error message isn't really useful. The location (3:31)
isn't valid 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.
(3:31)
refers to throw
- that seems correct to me. As for the error message, I'm unsure how to improve it. throw
is not valid in expressions; it is only parsed correctly as a statement. Because we've disallowed statements, I can't test that directly. And because the lexer encountered a reserved keyword in an expression, "Unexpected token" is correct.
I'm unsure of the best way to resolve this. I suppose when "Unexpected tokenis encountered, we could look at the indicated location to see if it is a
throw` string, but that feels somewhat hacky.
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.
About tests:
- Please organize the tests under
@lwc/template-compiler/src/__tests__/fixtures/expression-complex
into logical folders. The flat hierarchy makes it hard to find a specific tests. A suggestion:valid/
,invalid/
,invalid/throws/
,valid/arrow/
etc. - Missing test cases
- Using complex expression without
experimentalComplexExpressions
flag enabled should expect diagnostics - Resolution of variables that are in local scope: identifier declared in
for:item
,lwc:data
- Using complex expression without
6076891
to
a04de93
Compare
To avoid ballooning this PR further, I'm planning a follow-up PR with additional integration tests with other LWC features like |
Details
Note: Because this PR adds a lot of tests, I've crafted commits so that you can easily filter the diff in
Files changed
view to functional changes only (or tests, if that's what interests you).This PR introduces complex template expressions.
Whereas lone identifiers are transformed into corresponding field-access member expressions, parameter variables are not be transformed in this way. For example, the following transformations do not happen:
Instead, the scopes are respected:
{(foo) => foo && $cmp.bar}
. Similar checks occur for local identifiers introduced viafor:each
or similar.This feature is implemented behind a flag, which strongly influenced some some implementation choices.
// TODO [#3370]
comments are used liberally throughout, wherever code needs to be stripped (if template expressions are not adopted) or where changes need to be made (if template expressions are adopted).Some additional notes:
I'm currently tracking the work to be done in pr.todo. This file will be deleted prior to merge.There are no new tests. These will be added in a follow-up PR with theAdded a number of tests for coverage purposes and sanity. Will be adding more tests as a follow-up.experimentalComplexExpressions
flag enabled.Does this pull request introduce a breaking change?
Does this pull request introduce an observable change?
GUS work item
W-12177613