From bd8aca004c242d44dbea70c53c81b5bf02c56d67 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 12 Sep 2025 13:47:23 -0600 Subject: [PATCH 01/11] feat: added ability to toggle experimentalComplexExpressions in ssr fixtures --- .../__tests__/fixtures/text-interpolation-complex/.only | 0 .../fixtures/text-interpolation-complex/config.json | 7 +++++++ .../fixtures/text-interpolation-complex/error.txt | 0 .../fixtures/text-interpolation-complex/expected.html | 6 ++++++ .../modules/x/text-interpolation/text-interpolation.html | 3 +++ .../modules/x/text-interpolation/text-interpolation.js | 8 ++++++++ packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts | 7 ++++++- 7 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/.only create mode 100755 packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error.txt create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html create mode 100755 packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html create mode 100755 packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/.only b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/.only new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json new file mode 100755 index 0000000000..a2dc92585b --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json @@ -0,0 +1,7 @@ +{ + "entry": "x/text-interpolation", + "props": { + "publicProp": "public-prop" + }, + "experimentalComplexExpressions": true +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html new file mode 100644 index 0000000000..36620af52d --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html new file mode 100755 index 0000000000..852481087d --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js new file mode 100755 index 0000000000..0e67491f42 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; + +export default class TextInterpolation extends LightningElement { + privateProp = 1; + functionCall() { + this.privateProp++; + } +} diff --git a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts index 8c39d7ba7c..54d4b5277b 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts @@ -33,6 +33,9 @@ interface FixtureConfig { /** The string used to uniquely identify one set of dedupe IDs with multiple SSR islands */ styleDedupe?: string; + + /* TODO [#3370]: remove experimental template expression flag */ + experimentalComplexExpressions?: boolean; } vi.mock('@lwc/ssr-runtime', async () => { @@ -51,7 +54,7 @@ vi.mock('@lwc/ssr-runtime', async () => { const SSR_MODE: CompilationMode = DEFAULT_SSR_MODE; -async function compileFixture({ entry, dirname }: { entry: string; dirname: string }) { +async function compileFixture({ entry, dirname, experimentalComplexExpressions }: { entry: string; dirname: string, experimentalComplexExpressions: boolean }) { const modulesDir = path.resolve(dirname, './modules'); const outputFile = path.resolve(dirname, './dist/compiled-experimental-ssr.js'); const input = 'virtual/fixture/test.js'; @@ -73,6 +76,7 @@ async function compileFixture({ entry, dirname }: { entry: string; dirname: stri loader: path.join(__dirname, './utils/custom-loader.js'), strictSpecifier: false, }, + experimentalComplexExpressions }), ], onwarn({ message, code }) { @@ -109,6 +113,7 @@ describe.concurrent('fixtures', () => { compiledFixturePath = await compileFixture({ entry: config!.entry, dirname, + experimentalComplexExpressions: config!.experimentalComplexExpressions, }); } catch (err: any) { return { From 9035b036b81bd878f30b670f0f9c25baad180985 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Tue, 16 Sep 2025 11:30:21 -0600 Subject: [PATCH 02/11] chore: disable --- .../valid/object-expr/.only | 0 .../valid/object-expr/actual.html | 2 +- .../valid/object-expr/ast.json | 143 ++++-------------- .../valid/object-expr/expected.js | 10 +- 4 files changed, 30 insertions(+), 125 deletions(-) create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/.only diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/.only b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/.only new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/actual.html index a594536bea..3e2488f2ce 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/actual.html +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/actual.html @@ -1,5 +1,5 @@ diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json index 79af6ab260..b33b77bcc7 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json @@ -7,7 +7,7 @@ "endLine": 5, "endColumn": 12, "start": 0, - "end": 139, + "end": 84, "startTag": { "startLine": 1, "startColumn": 1, @@ -21,8 +21,8 @@ "startColumn": 1, "endLine": 5, "endColumn": 12, - "start": 128, - "end": 139 + "start": 73, + "end": 84 } }, "directives": [], @@ -37,7 +37,7 @@ "endLine": 4, "endColumn": 15, "start": 15, - "end": 127, + "end": 72, "startTag": { "startLine": 2, "startColumn": 5, @@ -51,8 +51,8 @@ "startColumn": 5, "endLine": 4, "endColumn": 15, - "start": 117, - "end": 127 + "start": 62, + "end": 72 } }, "attributes": [], @@ -68,149 +68,62 @@ "startLine": 3, "startColumn": 9, "endLine": 3, - "endColumn": 88, + "endColumn": 33, "start": 33, - "end": 112, + "end": 57, "startTag": { "startLine": 3, "startColumn": 9, "endLine": 3, - "endColumn": 47, + "endColumn": 17, "start": 33, - "end": 71 + "end": 41 }, "endTag": { "startLine": 3, - "startColumn": 79, + "startColumn": 24, "endLine": 3, - "endColumn": 88, - "start": 103, - "end": 112 + "endColumn": 33, + "start": 48, + "end": 57 } }, "attributes": [], - "properties": [ - { - "type": "Property", - "name": "attr", - "attributeName": "attr", - "value": { - "type": "ObjectExpression", - "start": 2, - "end": 20, - "properties": [ - { - "type": "Property", - "start": 4, - "end": 18, - "method": false, - "shorthand": false, - "computed": false, - "key": { - "type": "Identifier", - "start": 4, - "end": 7, - "name": "obj" - }, - "value": { - "type": "Literal", - "start": 9, - "end": 18, - "value": "literal", - "raw": "'literal'" - }, - "kind": "init" - } - ], - "location": { - "startLine": 3, - "startColumn": 17, - "endLine": 3, - "endColumn": 46, - "start": 41, - "end": 70 - } - }, - "location": { - "startLine": 3, - "startColumn": 17, - "endLine": 3, - "endColumn": 46, - "start": 41, - "end": 70 - } - } - ], + "properties": [], "directives": [], "listeners": [], "children": [ { "type": "Text", - "raw": "{({ obj: 'literal'}).toString()}", + "raw": "{foo()}", "value": { "type": "CallExpression", "start": 1, - "end": 31, + "end": 6, "callee": { - "type": "MemberExpression", + "type": "Identifier", "start": 1, - "end": 29, - "object": { - "type": "ObjectExpression", - "start": 2, - "end": 19, - "properties": [ - { - "type": "Property", - "start": 4, - "end": 18, - "method": false, - "shorthand": false, - "computed": false, - "key": { - "type": "Identifier", - "start": 4, - "end": 7, - "name": "obj" - }, - "value": { - "type": "Literal", - "start": 9, - "end": 18, - "value": "literal", - "raw": "'literal'" - }, - "kind": "init" - } - ] - }, - "property": { - "type": "Identifier", - "start": 21, - "end": 29, - "name": "toString" - }, - "computed": false, - "optional": false + "end": 4, + "name": "foo" }, "arguments": [], "optional": false, "location": { "startLine": 3, - "startColumn": 47, + "startColumn": 17, "endLine": 3, - "endColumn": 79, - "start": 71, - "end": 103 + "endColumn": 24, + "start": 41, + "end": 48 } }, "location": { "startLine": 3, - "startColumn": 47, + "startColumn": 17, "endLine": 3, - "endColumn": 79, - "start": 71, - "end": 103 + "endColumn": 24, + "start": 41, + "end": 48 } } ] diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js index 322fe089a7..43dab3dd70 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js @@ -6,16 +6,8 @@ const stc0 = { key: 0, }; const stc1 = { - props: { - attr: { - obj: "literal", - }, - }, key: 1, }; -const stc2 = { - obj: "literal", -}; function tmpl($api, $cmp, $slotset, $ctx) { const { d: api_dynamic_text, @@ -26,7 +18,7 @@ function tmpl($api, $cmp, $slotset, $ctx) { return [ api_element("section", stc0, [ api_custom_element("x-pert", _xPert, stc1, [ - api_text(api_dynamic_text(stc2.toString())), + api_text(api_dynamic_text($cmp.foo())), ]), ]), ]; From eb7a327d57a0e9daba90bd24a933d86f0e2562a8 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Wed, 24 Sep 2025 12:33:23 -0600 Subject: [PATCH 03/11] feat: ssr cte support --- .../fixtures/text-interpolation-complex/.only | 0 .../text-interpolation-complex/config.json | 6 +- .../text-interpolation-complex/error.txt | 1 + .../text-interpolation-complex/expected.html | 6 -- .../text-interpolation.html | 2 +- .../text-interpolation/text-interpolation.js | 8 +- .../src/__tests__/fixtures.spec.ts | 12 ++- .../src/compile-template/expression.ts | 82 ++++++++++++------- .../src/compile-template/shared.ts | 55 ------------- .../compile-template/transformers/element.ts | 9 +- .../compile-template/transformers/for-each.ts | 7 +- .../src/compile-template/transformers/slot.ts | 11 +-- .../valid/object-expr/.only | 0 .../template-compiler/src/codegen/codegen.ts | 54 ++---------- .../src/codegen/expression.ts | 74 +++++++++++++++-- packages/@lwc/template-compiler/src/index.ts | 1 + 16 files changed, 163 insertions(+), 165 deletions(-) delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/.only delete mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/.only diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/.only b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/.only deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json index a2dc92585b..154db9aa40 100755 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json @@ -3,5 +3,9 @@ "props": { "publicProp": "public-prop" }, - "experimentalComplexExpressions": true + "experimentalComplexExpressions": true, + "ssrFiles": { + "expected": "expected-ssr.txt", + "error": "error-ssr.txt" + } } diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error.txt index e69de29bb2..35dac9946c 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error.txt +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error.txt @@ -0,0 +1 @@ +LWC1060: Template expression doesn't allow BinaryExpression \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html index 36620af52d..e69de29bb2 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected.html @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html index 852481087d..2398b50a3d 100755 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.html @@ -1,3 +1,3 @@ \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js index 0e67491f42..5959b04818 100755 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js @@ -1,8 +1,10 @@ import { LightningElement } from 'lwc'; export default class TextInterpolation extends LightningElement { - privateProp = 1; - functionCall() { - this.privateProp++; + foo() { + return 'bar'; + } + bar() { + return 'foo'; } } diff --git a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts index 54d4b5277b..0af9fa2537 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts @@ -54,7 +54,15 @@ vi.mock('@lwc/ssr-runtime', async () => { const SSR_MODE: CompilationMode = DEFAULT_SSR_MODE; -async function compileFixture({ entry, dirname, experimentalComplexExpressions }: { entry: string; dirname: string, experimentalComplexExpressions: boolean }) { +async function compileFixture({ + entry, + dirname, + experimentalComplexExpressions, +}: { + entry: string; + dirname: string; + experimentalComplexExpressions: boolean; +}) { const modulesDir = path.resolve(dirname, './modules'); const outputFile = path.resolve(dirname, './dist/compiled-experimental-ssr.js'); const input = 'virtual/fixture/test.js'; @@ -76,7 +84,7 @@ async function compileFixture({ entry, dirname, experimentalComplexExpressions } loader: path.join(__dirname, './utils/custom-loader.js'), strictSpecifier: false, }, - experimentalComplexExpressions + experimentalComplexExpressions, }), ], onwarn({ message, code }) { diff --git a/packages/@lwc/ssr-compiler/src/compile-template/expression.ts b/packages/@lwc/ssr-compiler/src/compile-template/expression.ts index 63a3516a05..f554571f12 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/expression.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/expression.ts @@ -4,45 +4,71 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ - -import { builders as b } from 'estree-toolkit'; - +import { bindExpression } from '@lwc/template-compiler'; import type { ComplexExpression as IrComplexExpression, Expression as IrExpression, Identifier as IrIdentifier, MemberExpression as IrMemberExpression, } from '@lwc/template-compiler'; -import type { - Identifier as EsIdentifier, - Expression as EsExpression, - MemberExpression as EsMemberExpression, -} from 'estree'; +import type { Identifier as EsIdentifier, Expression as EsExpression } from 'estree'; import type { TransformerContext } from './types'; -function getRootMemberExpression(node: IrMemberExpression): IrMemberExpression { - return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node; -} - export function expressionIrToEs( node: IrExpression | IrComplexExpression, cxt: TransformerContext ): EsExpression { - if (node.type === 'Identifier') { - const isLocalVar = cxt.isLocalVar((node as IrIdentifier).name); - return isLocalVar - ? (node as EsIdentifier) - : b.memberExpression(b.identifier('instance'), node as EsIdentifier); - } else if (node.type === 'MemberExpression') { - const nodeClone = structuredClone(node); - const rootMemberExpr = getRootMemberExpression(nodeClone as IrMemberExpression); - if (!cxt.isLocalVar((rootMemberExpr.object as IrIdentifier).name)) { - rootMemberExpr.object = b.memberExpression( - b.identifier('instance'), - rootMemberExpr.object as EsIdentifier - ) as unknown as IrMemberExpression; - } - return nodeClone as unknown as EsMemberExpression; + return bindExpression( + node as IrComplexExpression, + (n: EsIdentifier) => cxt.isLocalVar((n as EsIdentifier).name), + 'instance', + cxt.templateOptions.experimentalComplexExpressions + ); +} + +/** + * Given an expression in a context, return an expression that may be scoped to that context. + * For example, for the expression `foo`, it will typically be `instance.foo`, but if we're + * inside a `for:each` block then the `foo` variable may refer to the scoped `foo`, + * e.g. ` diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json index 52c9d119bb..79af6ab260 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/ast.json @@ -4,10 +4,10 @@ "location": { "startLine": 1, "startColumn": 1, - "endLine": 7, + "endLine": 5, "endColumn": 12, "start": 0, - "end": 183, + "end": 139, "startTag": { "startLine": 1, "startColumn": 1, @@ -17,42 +17,42 @@ "end": 10 }, "endTag": { - "startLine": 7, + "startLine": 5, "startColumn": 1, - "endLine": 7, + "endLine": 5, "endColumn": 12, - "start": 172, - "end": 183 + "start": 128, + "end": 139 } }, "directives": [], "children": [ { "type": "Element", - "name": "ul", + "name": "section", "namespace": "http://www.w3.org/1999/xhtml", "location": { "startLine": 2, "startColumn": 5, - "endLine": 6, - "endColumn": 10, + "endLine": 4, + "endColumn": 15, "start": 15, - "end": 171, + "end": 127, "startTag": { "startLine": 2, "startColumn": 5, "endLine": 2, - "endColumn": 9, + "endColumn": 14, "start": 15, - "end": 19 + "end": 24 }, "endTag": { - "startLine": 6, + "startLine": 4, "startColumn": 5, - "endLine": 6, - "endColumn": 10, - "start": 166, - "end": 171 + "endLine": 4, + "endColumn": 15, + "start": 117, + "end": 127 } }, "attributes": [], @@ -61,198 +61,157 @@ "listeners": [], "children": [ { - "type": "ForEach", - "expression": { - "type": "CallExpression", - "start": 1, - "end": 7, - "callee": { - "type": "Identifier", - "start": 1, - "end": 5, - "name": "list" - }, - "arguments": [], - "optional": false, - "location": { - "startLine": 3, - "startColumn": 19, - "endLine": 3, - "endColumn": 38, - "start": 38, - "end": 57 - } - }, - "item": { - "type": "Identifier", - "name": "item", - "location": { + "type": "Component", + "name": "x-pert", + "namespace": "http://www.w3.org/1999/xhtml", + "location": { + "startLine": 3, + "startColumn": 9, + "endLine": 3, + "endColumn": 88, + "start": 33, + "end": 112, + "startTag": { "startLine": 3, - "startColumn": 39, + "startColumn": 9, "endLine": 3, - "endColumn": 54, - "start": 58, - "end": 73 - } - }, - "index": { - "type": "Identifier", - "name": "index", - "location": { + "endColumn": 47, + "start": 33, + "end": 71 + }, + "endTag": { "startLine": 3, - "startColumn": 55, + "startColumn": 79, "endLine": 3, - "endColumn": 72, - "start": 74, - "end": 91 + "endColumn": 88, + "start": 103, + "end": 112 } }, - "location": { - "startLine": 3, - "startColumn": 9, - "endLine": 5, - "endColumn": 20, - "start": 28, - "end": 161 - }, - "directiveLocation": { - "startLine": 3, - "startColumn": 19, - "endLine": 3, - "endColumn": 38, - "start": 38, - "end": 57 - }, - "children": [ + "attributes": [], + "properties": [ { - "type": "Element", - "name": "li", - "namespace": "http://www.w3.org/1999/xhtml", - "location": { - "startLine": 4, - "startColumn": 13, - "endLine": 4, - "endColumn": 49, - "start": 105, - "end": 141, - "startTag": { - "startLine": 4, - "startColumn": 13, - "endLine": 4, - "endColumn": 28, - "start": 105, - "end": 120 - }, - "endTag": { - "startLine": 4, - "startColumn": 44, - "endLine": 4, - "endColumn": 49, - "start": 136, - "end": 141 + "type": "Property", + "name": "attr", + "attributeName": "attr", + "value": { + "type": "ObjectExpression", + "start": 2, + "end": 20, + "properties": [ + { + "type": "Property", + "start": 4, + "end": 18, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 4, + "end": 7, + "name": "obj" + }, + "value": { + "type": "Literal", + "start": 9, + "end": 18, + "value": "literal", + "raw": "'literal'" + }, + "kind": "init" + } + ], + "location": { + "startLine": 3, + "startColumn": 17, + "endLine": 3, + "endColumn": 46, + "start": 41, + "end": 70 } }, - "attributes": [], - "properties": [], - "directives": [ - { - "type": "Directive", - "name": "Key", - "value": { - "type": "Identifier", - "start": 1, - "end": 5, - "name": "item", - "location": { - "startLine": 4, - "startColumn": 17, - "endLine": 4, - "endColumn": 27, - "start": 109, - "end": 119 - } + "location": { + "startLine": 3, + "startColumn": 17, + "endLine": 3, + "endColumn": 46, + "start": 41, + "end": 70 + } + } + ], + "directives": [], + "listeners": [], + "children": [ + { + "type": "Text", + "raw": "{({ obj: 'literal'}).toString()}", + "value": { + "type": "CallExpression", + "start": 1, + "end": 31, + "callee": { + "type": "MemberExpression", + "start": 1, + "end": 29, + "object": { + "type": "ObjectExpression", + "start": 2, + "end": 19, + "properties": [ + { + "type": "Property", + "start": 4, + "end": 18, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 4, + "end": 7, + "name": "obj" + }, + "value": { + "type": "Literal", + "start": 9, + "end": 18, + "value": "literal", + "raw": "'literal'" + }, + "kind": "init" + } + ] }, - "location": { - "startLine": 4, - "startColumn": 17, - "endLine": 4, - "endColumn": 27, - "start": 109, - "end": 119 - } - } - ], - "listeners": [], - "children": [ - { - "type": "Text", - "raw": "{index}", - "value": { + "property": { "type": "Identifier", - "start": 1, - "end": 6, - "name": "index", - "location": { - "startLine": 4, - "startColumn": 28, - "endLine": 4, - "endColumn": 44, - "start": 120, - "end": 136 - } + "start": 21, + "end": 29, + "name": "toString" }, - "location": { - "startLine": 4, - "startColumn": 28, - "endLine": 4, - "endColumn": 44, - "start": 120, - "end": 136 - } + "computed": false, + "optional": false }, - { - "type": "Text", - "raw": " - ", - "value": { - "type": "Literal", - "value": " - " - }, - "location": { - "startLine": 4, - "startColumn": 28, - "endLine": 4, - "endColumn": 44, - "start": 120, - "end": 136 - } - }, - { - "type": "Text", - "raw": "{item}", - "value": { - "type": "Identifier", - "start": 11, - "end": 15, - "name": "item", - "location": { - "startLine": 4, - "startColumn": 28, - "endLine": 4, - "endColumn": 44, - "start": 120, - "end": 136 - } - }, - "location": { - "startLine": 4, - "startColumn": 28, - "endLine": 4, - "endColumn": 44, - "start": 120, - "end": 136 - } + "arguments": [], + "optional": false, + "location": { + "startLine": 3, + "startColumn": 47, + "endLine": 3, + "endColumn": 79, + "start": 71, + "end": 103 } - ] + }, + "location": { + "startLine": 3, + "startColumn": 47, + "endLine": 3, + "endColumn": 79, + "start": 71, + "end": 103 + } } ] } diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js index 79f05a68a1..322fe089a7 100644 --- a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/valid/object-expr/expected.js @@ -1,33 +1,34 @@ import _implicitStylesheets from "./object-expr.css"; import _implicitScopedStylesheets from "./object-expr.scoped.css?scoped=true"; -import { freezeTemplate, parseFragment, registerTemplate } from "lwc"; -const $fragment1 = parseFragment`${"t1"}`; +import _xPert from "x/pert"; +import { freezeTemplate, registerTemplate } from "lwc"; const stc0 = { key: 0, }; +const stc1 = { + props: { + attr: { + obj: "literal", + }, + }, + key: 1, +}; +const stc2 = { + obj: "literal", +}; function tmpl($api, $cmp, $slotset, $ctx) { const { - k: api_key, d: api_dynamic_text, - sp: api_static_part, - st: api_static_fragment, - i: api_iterator, + t: api_text, + c: api_custom_element, h: api_element, } = $api; return [ - api_element( - "ul", - stc0, - api_iterator($cmp.list(), function (item, index) { - return api_static_fragment($fragment1, api_key(2, item), [ - api_static_part( - 1, - null, - api_dynamic_text(index) + " - " + api_dynamic_text(item) - ), - ]); - }) - ), + api_element("section", stc0, [ + api_custom_element("x-pert", _xPert, stc1, [ + api_text(api_dynamic_text(stc2.toString())), + ]), + ]), ]; /*LWC compiler vX.X.X*/ } From a8b5f8810b8421b18175104938fb6633d2f49f71 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 29 Sep 2025 09:10:02 -0600 Subject: [PATCH 08/11] feat: api specific errors, tests --- .../errors/src/compiler/error-info/index.ts | 2 +- .../compiler/error-info/template-transform.ts | 20 ++++- packages/@lwc/shared/src/api-version.ts | 48 ++++++----- .../src/__tests__/fixtures.spec.ts | 2 +- .../src/compile-template/expression.ts | 11 ++- .../src/compile-template/index.ts | 2 + .../src/compile-template/types.ts | 1 + .../actual.html | 5 ++ .../ast.json | 1 + .../config.json | 4 + .../expected.js | 0 .../metadata.json | 15 ++++ .../actual.html | 5 ++ .../ast.json | 1 + .../config.json | 4 + .../expected.js | 0 .../metadata.json | 15 ++++ .../actual.html | 5 ++ .../ast.json | 1 + .../config.json | 4 + .../expected.js | 0 .../metadata.json | 15 ++++ .../actual.html | 3 + .../ast.json | 1 + .../config.json | 4 + .../expected.js | 0 .../metadata.json | 15 ++++ .../template-compiler/src/parser/attribute.ts | 6 +- .../src/parser/expression.ts | 81 +++++++++++++------ .../template-compiler/src/parser/index.ts | 10 +-- 30 files changed, 225 insertions(+), 56 deletions(-) create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/actual.html create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/ast.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/config.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/expected.js create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/metadata.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/actual.html create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/ast.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/config.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/expected.js create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/metadata.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/actual.html create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/ast.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/config.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/expected.js create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/metadata.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/actual.html create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/ast.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/config.json create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/expected.js create mode 100644 packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/metadata.json diff --git a/packages/@lwc/errors/src/compiler/error-info/index.ts b/packages/@lwc/errors/src/compiler/error-info/index.ts index 4129bedf60..ac79220451 100644 --- a/packages/@lwc/errors/src/compiler/error-info/index.ts +++ b/packages/@lwc/errors/src/compiler/error-info/index.ts @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ /** - * Next error code: 1209 + * Next error code: 1211 */ export * from './compiler'; diff --git a/packages/@lwc/errors/src/compiler/error-info/template-transform.ts b/packages/@lwc/errors/src/compiler/error-info/template-transform.ts index 70fa8f14b0..d3a6729647 100644 --- a/packages/@lwc/errors/src/compiler/error-info/template-transform.ts +++ b/packages/@lwc/errors/src/compiler/error-info/template-transform.ts @@ -980,7 +980,7 @@ export const ParserDiagnostics = { url: '', }, - COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_COMPLEX: { + COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_CTE_UNQUOTED: { code: 1207, message: 'Template expression doesn\'t allow computed property access unless the expression is surrounded by quotes: "{0}"', @@ -988,11 +988,27 @@ export const ParserDiagnostics = { url: '', }, - INVALID_NODE_COMPLEX: { + INVALID_NODE_CTE_UNQUOTED: { code: 1208, message: 'Template expression doesn\'t allow {0} unless the expression is surrounded by quotes: "{1}"', level: DiagnosticLevel.Error, url: '', }, + + COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_CTE_API_VERSION: { + code: 1209, + message: + "Template expression doesn't allow computed property access. The current component API version ({1}) is insufficient and must be increased to at least {2} for this type of expression.", + level: DiagnosticLevel.Error, + url: '', + }, + + INVALID_NODE_CTE_API_VERSION: { + code: 1210, + message: + "Template expression doesn't allow {0}. The current component API version ({1}) is insufficient and must be increased to at least {2} for this type of expression.", + level: DiagnosticLevel.Error, + url: '', + }, }; diff --git a/packages/@lwc/shared/src/api-version.ts b/packages/@lwc/shared/src/api-version.ts index 72bc130103..becdd472cc 100644 --- a/packages/@lwc/shared/src/api-version.ts +++ b/packages/@lwc/shared/src/api-version.ts @@ -14,6 +14,9 @@ export const enum APIVersion { V61_250_SUMMER_24 = 61, V62_252_WINTER_25 = 62, V63_254_SPRING_25 = 63, + V64_256_SUMMER_25 = 64, + V65_258_WINTER_26 = 65, + V66_260_SPRING_26 = 66, } // These must be updated when the enum is updated. @@ -27,6 +30,9 @@ const allVersions = [ APIVersion.V61_250_SUMMER_24, APIVersion.V62_252_WINTER_25, APIVersion.V63_254_SPRING_25, + APIVersion.V64_256_SUMMER_25, + APIVersion.V65_258_WINTER_26, + APIVersion.V66_260_SPRING_26, ]; const allVersionsSet = /*@__PURE__@*/ new Set(allVersions); export const LOWEST_API_VERSION = allVersions[0]; @@ -117,6 +123,28 @@ export const enum APIFeature { ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS, } +const minFeatureApiVersions = new Map([ + [APIFeature.LOWERCASE_SCOPE_TOKENS, APIVersion.V59_246_WINTER_24], + [APIFeature.TREAT_ALL_PARSE5_ERRORS_AS_ERRORS, APIVersion.V59_246_WINTER_24], + [APIFeature.DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION, APIVersion.V60_248_SPRING_24], + [APIFeature.SKIP_UNNECESSARY_REGISTER_DECORATORS, APIVersion.V60_248_SPRING_24], + [APIFeature.USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, APIVersion.V60_248_SPRING_24], + [APIFeature.USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, APIVersion.V60_248_SPRING_24], + [APIFeature.ENABLE_ELEMENT_INTERNALS_AND_FACE, APIVersion.V61_250_SUMMER_24], + [APIFeature.USE_LIGHT_DOM_SLOT_FORWARDING, APIVersion.V61_250_SUMMER_24], + [APIFeature.ENABLE_THIS_DOT_HOST_ELEMENT, APIVersion.V62_252_WINTER_25], + [APIFeature.ENABLE_THIS_DOT_STYLE, APIVersion.V62_252_WINTER_25], + [APIFeature.TEMPLATE_CLASS_NAME_OBJECT_BINDING, APIVersion.V62_252_WINTER_25], + [APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS, APIVersion.V66_260_SPRING_26], +]); + +/** + * @param apiVersionFeature + */ +export function minApiVersion(apiVersionFeature: APIFeature): APIVersion { + return minFeatureApiVersions.get(apiVersionFeature) || HIGHEST_API_VERSION; +} + /** * * @param apiVersionFeature @@ -126,23 +154,5 @@ export function isAPIFeatureEnabled( apiVersionFeature: APIFeature, apiVersion: APIVersion ): boolean { - switch (apiVersionFeature) { - case APIFeature.LOWERCASE_SCOPE_TOKENS: - case APIFeature.TREAT_ALL_PARSE5_ERRORS_AS_ERRORS: - return apiVersion >= APIVersion.V59_246_WINTER_24; - case APIFeature.DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION: - case APIFeature.SKIP_UNNECESSARY_REGISTER_DECORATORS: - case APIFeature.USE_COMMENTS_FOR_FRAGMENT_BOOKENDS: - case APIFeature.USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS: - return apiVersion >= APIVersion.V60_248_SPRING_24; - case APIFeature.ENABLE_ELEMENT_INTERNALS_AND_FACE: - case APIFeature.USE_LIGHT_DOM_SLOT_FORWARDING: - return apiVersion >= APIVersion.V61_250_SUMMER_24; - case APIFeature.ENABLE_THIS_DOT_HOST_ELEMENT: - case APIFeature.ENABLE_THIS_DOT_STYLE: - case APIFeature.TEMPLATE_CLASS_NAME_OBJECT_BINDING: - return apiVersion >= APIVersion.V62_252_WINTER_25; - case APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS: - return apiVersion >= APIVersion.V63_254_SPRING_25; - } + return apiVersion >= minApiVersion(apiVersionFeature); } diff --git a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts index 0af9fa2537..559a30ae3d 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts @@ -61,7 +61,7 @@ async function compileFixture({ }: { entry: string; dirname: string; - experimentalComplexExpressions: boolean; + experimentalComplexExpressions: boolean | undefined; }) { const modulesDir = path.resolve(dirname, './modules'); const outputFile = path.resolve(dirname, './dist/compiled-experimental-ssr.js'); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/expression.ts b/packages/@lwc/ssr-compiler/src/compile-template/expression.ts index f554571f12..e2e764ae54 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/expression.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/expression.ts @@ -5,6 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { bindExpression } from '@lwc/template-compiler'; +import { APIFeature, isAPIFeatureEnabled } from '@lwc/shared'; import type { ComplexExpression as IrComplexExpression, Expression as IrExpression, @@ -18,11 +19,17 @@ export function expressionIrToEs( node: IrExpression | IrComplexExpression, cxt: TransformerContext ): EsExpression { + const isComplexTemplateExpressionEnabled = + cxt.templateOptions.experimentalComplexExpressions && + isAPIFeatureEnabled( + APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS, + cxt.templateOptions.apiVersion + ); return bindExpression( node as IrComplexExpression, - (n: EsIdentifier) => cxt.isLocalVar((n as EsIdentifier).name), + (n: EsIdentifier) => cxt.isLocalVar(n.name), 'instance', - cxt.templateOptions.experimentalComplexExpressions + isComplexTemplateExpressionEnabled ); } diff --git a/packages/@lwc/ssr-compiler/src/compile-template/index.ts b/packages/@lwc/ssr-compiler/src/compile-template/index.ts index c07acc51b1..cd9c1784b8 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/index.ts @@ -116,10 +116,12 @@ export default function compileTemplate( (directive) => directive.name === 'PreserveComments' )?.value?.value; const experimentalComplexExpressions = Boolean(options.experimentalComplexExpressions); + const apiVersion = Number(options.apiVersion); const { addImport, getImports, statements, cxt } = templateIrToEsTree(root, { preserveComments, experimentalComplexExpressions, + apiVersion, }); addImport(['renderStylesheets', 'hasScopedStaticStylesheets']); for (const [imports, source] of getStylesheetImports(filename)) { diff --git a/packages/@lwc/ssr-compiler/src/compile-template/types.ts b/packages/@lwc/ssr-compiler/src/compile-template/types.ts index 73f244a87d..385132bd78 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/types.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/types.ts @@ -48,4 +48,5 @@ export interface TransformerContext { export interface TemplateOpts { preserveComments: boolean; experimentalComplexExpressions: boolean; + apiVersion: number; } diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/actual.html new file mode 100644 index 0000000000..c392a95ca2 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/actual.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/ast.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/ast.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/config.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/config.json new file mode 100644 index 0000000000..00f1c239e3 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/config.json @@ -0,0 +1,4 @@ +{ + "experimentalComplexExpressions": true, + "apiVersion": 59 +} diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/expected.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/metadata.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/metadata.json new file mode 100644 index 0000000000..ba8bf0fe67 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-attribute/metadata.json @@ -0,0 +1,15 @@ +{ + "warnings": [ + { + "code": 1210, + "message": "Invalid expression {foo()} - LWC1210: Template expression doesn't allow CallExpression. The current component API version (59) is insufficient and must be increased to at least 66 for this type of expression.", + "level": 1, + "location": { + "line": 3, + "column": 18, + "start": 42, + "length": 13 + } + } + ] +} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/actual.html new file mode 100644 index 0000000000..e05f5f409c --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/actual.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/ast.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/ast.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/config.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/config.json new file mode 100644 index 0000000000..00f1c239e3 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/config.json @@ -0,0 +1,4 @@ +{ + "experimentalComplexExpressions": true, + "apiVersion": 59 +} diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/expected.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/metadata.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/metadata.json new file mode 100644 index 0000000000..d09517a49b --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-call-expr-text-node/metadata.json @@ -0,0 +1,15 @@ +{ + "warnings": [ + { + "code": 1210, + "message": "Invalid expression {foo()} - LWC1210: Template expression doesn't allow CallExpression. The current component API version (59) is insufficient and must be increased to at least 66 for this type of expression.", + "level": 1, + "location": { + "line": 3, + "column": 18, + "start": 42, + "length": 7 + } + } + ] +} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/actual.html new file mode 100644 index 0000000000..2afc794893 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/actual.html @@ -0,0 +1,5 @@ + diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/ast.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/ast.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/config.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/config.json new file mode 100644 index 0000000000..00f1c239e3 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/config.json @@ -0,0 +1,4 @@ +{ + "experimentalComplexExpressions": true, + "apiVersion": 59 +} diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/expected.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/metadata.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/metadata.json new file mode 100644 index 0000000000..086ed12b46 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-attribute/metadata.json @@ -0,0 +1,15 @@ +{ + "warnings": [ + { + "code": 1209, + "message": "Invalid expression {classNames[0]} - LWC1209: Template expression doesn't allow computed property access. The current component API version (59) is insufficient and must be increased to at least 66 for this type of expression.", + "level": 1, + "location": { + "line": 3, + "column": 12, + "start": 36, + "length": 23 + } + } + ] +} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/actual.html b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/actual.html new file mode 100644 index 0000000000..38c09182be --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/actual.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/ast.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/ast.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/ast.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/config.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/config.json new file mode 100644 index 0000000000..00f1c239e3 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/config.json @@ -0,0 +1,4 @@ +{ + "experimentalComplexExpressions": true, + "apiVersion": 59 +} diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/expected.js b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/expected.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/metadata.json b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/metadata.json new file mode 100644 index 0000000000..d8d1f798f3 --- /dev/null +++ b/packages/@lwc/template-compiler/src/__tests__/fixtures/expression-complex/invalid/invalid-api-version-computed-prop-node/metadata.json @@ -0,0 +1,15 @@ +{ + "warnings": [ + { + "code": 1209, + "message": "Invalid expression {val[state.foo]} - LWC1209: Template expression doesn't allow computed property access. The current component API version (59) is insufficient and must be increased to at least 66 for this type of expression.", + "level": 1, + "location": { + "line": 1, + "column": 11, + "start": 10, + "length": 22 + } + } + ] +} \ No newline at end of file diff --git a/packages/@lwc/template-compiler/src/parser/attribute.ts b/packages/@lwc/template-compiler/src/parser/attribute.ts index a71e261b76..6736a20406 100644 --- a/packages/@lwc/template-compiler/src/parser/attribute.ts +++ b/packages/@lwc/template-compiler/src/parser/attribute.ts @@ -24,7 +24,7 @@ import { isExpression, isPotentialExpression, } from './expression'; -import { isComplexTemplateExpressionEnabled } from './expression-complex'; + import { ATTR_NAME, DATA_RE, @@ -111,7 +111,9 @@ export function normalizeAttributeValue( const isQuoted = isQuotedAttribute(rawAttrVal); const isEscaped = isEscapedAttribute(rawAttrVal); if (!isEscaped && isExpression(value)) { - if (isQuoted && !isComplexTemplateExpressionEnabled(ctx)) { + // Don't test for the API version here, just check if CTE is enabled. + // We can provide more specific errors WRT API versions after the expression has been parsed and we know what it is. + if (isQuoted && !ctx.config.experimentalComplexExpressions) { // // -> ambiguity if the attribute value is a template identifier or a string literal. diff --git a/packages/@lwc/template-compiler/src/parser/expression.ts b/packages/@lwc/template-compiler/src/parser/expression.ts index 9d68abe7be..9313965f3c 100644 --- a/packages/@lwc/template-compiler/src/parser/expression.ts +++ b/packages/@lwc/template-compiler/src/parser/expression.ts @@ -7,12 +7,13 @@ import { parseExpressionAt, isIdentifierStart, isIdentifierChar } from 'acorn'; import { ParserDiagnostics, invariant } from '@lwc/errors'; +import { APIFeature, minApiVersion } from '@lwc/shared'; import * as t from '../shared/estree'; import { isReservedES6Keyword } from './utils/javascript'; +import { isComplexTemplateExpressionEnabled } from './expression-complex'; import type { Expression, Identifier, SourceLocation } from '../shared/types'; import type ParserCtx from './parser'; -import type { NormalizedConfig } from '../config'; import type { Node } from 'acorn'; export const EXPRESSION_SYMBOL_START = '{'; @@ -30,40 +31,71 @@ export function isPotentialExpression(source: string): boolean { return !!source.match(POTENTIAL_EXPRESSION_RE); } +const minCteApiVersion = minApiVersion(APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS); + function validateExpression( source: string, node: t.BaseNode, - config: NormalizedConfig + ctx: ParserCtx, + unquotedAttributeExpression: boolean ): asserts node is Expression { - const isValidNode = t.isIdentifier(node) || t.isMemberExpression(node); - // INVALID_XYZ_COMPLEX provides additional context to the user if CTE is enabled. - // The author may not have delimited the CTE with quotes, resulting in it being parsed - // as a legacy expression. - invariant( - isValidNode, - config.experimentalComplexExpressions - ? ParserDiagnostics.INVALID_NODE_COMPLEX - : ParserDiagnostics.INVALID_NODE, - [node.type, source] - ); - - if (t.isMemberExpression(node)) { + const cteOnlyNode = !t.isIdentifier(node) && !t.isMemberExpression(node); + + // If this node is not an identifier or a member expression (the only two nodes allowed if complexTemplateExpressions are disabled), + // then we throw if the following invariants do not hold true. + if (cteOnlyNode) { + // complexTemplateExpressions must be enabled if this is a cteOnlyNode. + invariant(ctx.config.experimentalComplexExpressions, ParserDiagnostics.INVALID_NODE, [ + node.type, + ]); + // complexTemplateExpressions must be enabled and the component API version must be sufficient. invariant( - config.experimentalComputedMemberExpression || !node.computed, - config.experimentalComplexExpressions - ? ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_COMPLEX - : ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED, - [source] + isComplexTemplateExpressionEnabled(ctx), + ParserDiagnostics.INVALID_NODE_CTE_API_VERSION, + [node.type, ctx.apiVersion, minCteApiVersion] ); + // complexTemplateExpressions must be enabled, the component API version must be sufficient and the expression should not be + // an unquoted attribute expression. + invariant( + isComplexTemplateExpressionEnabled(ctx) && !unquotedAttributeExpression, + ParserDiagnostics.INVALID_NODE_CTE_UNQUOTED, + [node.type, source] + ); + } + + if (t.isMemberExpression(node)) { + // If this is a computed node and experimentalComputedMemberExpressions is not enabled, + // then we throw if the following invariants do not hold true. + if (!ctx.config.experimentalComputedMemberExpression && node.computed) { + // complexTemplateExpressions must be enabled. + invariant( + ctx.config.experimentalComplexExpressions, + ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED, + [source] + ); + // complexTemplateExpressions must be enabled and the component API version must be sufficient. + invariant( + isComplexTemplateExpressionEnabled(ctx), + ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_CTE_API_VERSION, + [source, ctx.apiVersion, minCteApiVersion] + ); + // complexTemplateExpressions must be enabled, the component API version must be sufficient and the expression + // should not be an unquoted attribute expression. + invariant( + isComplexTemplateExpressionEnabled(ctx) && !unquotedAttributeExpression, + ParserDiagnostics.COMPUTED_PROPERTY_ACCESS_NOT_ALLOWED_CTE_UNQUOTED, + [source] + ); + } const { object, property } = node; if (!t.isIdentifier(object)) { - validateExpression(source, object, config); + validateExpression(source, object, ctx, unquotedAttributeExpression); } if (!t.isIdentifier(property)) { - validateExpression(source, property, config); + validateExpression(source, property, ctx, unquotedAttributeExpression); } } } @@ -109,7 +141,8 @@ export function validateSourceIsParsedExpression(source: string, parsedExpressio export function parseExpression( ctx: ParserCtx, source: string, - location: SourceLocation + location: SourceLocation, + unquotedAttributeExpression: boolean ): Expression { const { ecmaVersion } = ctx; return ctx.withErrorWrapping( @@ -122,7 +155,7 @@ export function parseExpression( }); validateSourceIsParsedExpression(source, parsed); - validateExpression(source, parsed, ctx.config); + validateExpression(source, parsed, ctx, unquotedAttributeExpression); return { ...parsed, location }; }, diff --git a/packages/@lwc/template-compiler/src/parser/index.ts b/packages/@lwc/template-compiler/src/parser/index.ts index 8779a79586..eaa268eed4 100644 --- a/packages/@lwc/template-compiler/src/parser/index.ts +++ b/packages/@lwc/template-compiler/src/parser/index.ts @@ -61,7 +61,7 @@ import { SUPPORTED_SVG_TAGS, VALID_IF_MODIFIER, } from './constants'; -import { parseComplexExpression } from './expression-complex'; +import { isComplexTemplateExpressionEnabled, parseComplexExpression } from './expression-complex'; import type { TemplateParseResult, Attribute, @@ -480,7 +480,7 @@ function parseText( let value: Expression | Literal; if (isExpression(token)) { - value = parseExpression(ctx, token, sourceLocation); + value = parseExpression(ctx, token, sourceLocation, false); } else { value = ast.literal(decodeTextContent(token)); } @@ -562,7 +562,7 @@ function parseTextNode(ctx: ParserCtx, parse5Text: parse5Tools.TextNode): Text[] const sourceLocation = ast.sourceLocation(location); - return ctx.config.experimentalComplexExpressions + return isComplexTemplateExpressionEnabled(ctx) ? parseTextComplex(ctx, rawText, sourceLocation, location) : parseText(ctx, rawText, sourceLocation, location); } @@ -1927,12 +1927,12 @@ function getTemplateAttribute( */ const isPotentialComplexExpression = quotedExpression && !escapedExpression && value.startsWith(EXPRESSION_SYMBOL_START); - if (ctx.config.experimentalComplexExpressions && isPotentialComplexExpression) { + if (isComplexTemplateExpressionEnabled(ctx) && isPotentialComplexExpression) { const attributeNameOffset = attribute.name.length + 2; // The +2 accounts for the '="' in the attribute: attr="... const templateSource = ctx.getSource(attributeLocation.startOffset + attributeNameOffset); attrValue = parseComplexExpression(ctx, value, templateSource, location).expression; } else if (isExpression(value) && !escapedExpression) { - attrValue = parseExpression(ctx, value, location); + attrValue = parseExpression(ctx, value, location, !quotedExpression); } else if (isBooleanAttribute) { attrValue = ast.literal(true); } else { From 411b599b59cb9349443a6a40f701694c94fd64c7 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 29 Sep 2025 09:31:42 -0600 Subject: [PATCH 09/11] fix: bundlesize --- scripts/bundlesize/bundlesize.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bundlesize/bundlesize.config.json b/scripts/bundlesize/bundlesize.config.json index 57c8efea07..085eddc8d4 100644 --- a/scripts/bundlesize/bundlesize.config.json +++ b/scripts/bundlesize/bundlesize.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "packages/@lwc/engine-dom/dist/index.js", - "maxSize": "24.72KB" + "maxSize": "24.79KB" }, { "path": "packages/@lwc/synthetic-shadow/dist/index.js", From 70e89713ec0ce8d91d674912831b076941f5a4da Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 29 Sep 2025 10:48:27 -0600 Subject: [PATCH 10/11] fix: retain original switch logic --- packages/@lwc/shared/src/api-version.ts | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/@lwc/shared/src/api-version.ts b/packages/@lwc/shared/src/api-version.ts index becdd472cc..bb5ef0b881 100644 --- a/packages/@lwc/shared/src/api-version.ts +++ b/packages/@lwc/shared/src/api-version.ts @@ -123,26 +123,29 @@ export const enum APIFeature { ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS, } -const minFeatureApiVersions = new Map([ - [APIFeature.LOWERCASE_SCOPE_TOKENS, APIVersion.V59_246_WINTER_24], - [APIFeature.TREAT_ALL_PARSE5_ERRORS_AS_ERRORS, APIVersion.V59_246_WINTER_24], - [APIFeature.DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION, APIVersion.V60_248_SPRING_24], - [APIFeature.SKIP_UNNECESSARY_REGISTER_DECORATORS, APIVersion.V60_248_SPRING_24], - [APIFeature.USE_COMMENTS_FOR_FRAGMENT_BOOKENDS, APIVersion.V60_248_SPRING_24], - [APIFeature.USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, APIVersion.V60_248_SPRING_24], - [APIFeature.ENABLE_ELEMENT_INTERNALS_AND_FACE, APIVersion.V61_250_SUMMER_24], - [APIFeature.USE_LIGHT_DOM_SLOT_FORWARDING, APIVersion.V61_250_SUMMER_24], - [APIFeature.ENABLE_THIS_DOT_HOST_ELEMENT, APIVersion.V62_252_WINTER_25], - [APIFeature.ENABLE_THIS_DOT_STYLE, APIVersion.V62_252_WINTER_25], - [APIFeature.TEMPLATE_CLASS_NAME_OBJECT_BINDING, APIVersion.V62_252_WINTER_25], - [APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS, APIVersion.V66_260_SPRING_26], -]); - /** * @param apiVersionFeature */ export function minApiVersion(apiVersionFeature: APIFeature): APIVersion { - return minFeatureApiVersions.get(apiVersionFeature) || HIGHEST_API_VERSION; + switch (apiVersionFeature) { + case APIFeature.LOWERCASE_SCOPE_TOKENS: + case APIFeature.TREAT_ALL_PARSE5_ERRORS_AS_ERRORS: + return APIVersion.V59_246_WINTER_24; + case APIFeature.DISABLE_OBJECT_REST_SPREAD_TRANSFORMATION: + case APIFeature.SKIP_UNNECESSARY_REGISTER_DECORATORS: + case APIFeature.USE_COMMENTS_FOR_FRAGMENT_BOOKENDS: + case APIFeature.USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS: + return APIVersion.V60_248_SPRING_24; + case APIFeature.ENABLE_ELEMENT_INTERNALS_AND_FACE: + case APIFeature.USE_LIGHT_DOM_SLOT_FORWARDING: + return APIVersion.V61_250_SUMMER_24; + case APIFeature.ENABLE_THIS_DOT_HOST_ELEMENT: + case APIFeature.ENABLE_THIS_DOT_STYLE: + case APIFeature.TEMPLATE_CLASS_NAME_OBJECT_BINDING: + return APIVersion.V62_252_WINTER_25; + case APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS: + return APIVersion.V66_260_SPRING_26; + } } /** From c0cb49d708ba23855187890665910e00ed65c4fb Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Mon, 29 Sep 2025 13:00:36 -0600 Subject: [PATCH 11/11] fix: review comments --- packages/@lwc/template-compiler/src/parser/attribute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@lwc/template-compiler/src/parser/attribute.ts b/packages/@lwc/template-compiler/src/parser/attribute.ts index 6736a20406..ea94e97e05 100644 --- a/packages/@lwc/template-compiler/src/parser/attribute.ts +++ b/packages/@lwc/template-compiler/src/parser/attribute.ts @@ -112,7 +112,7 @@ export function normalizeAttributeValue( const isEscaped = isEscapedAttribute(rawAttrVal); if (!isEscaped && isExpression(value)) { // Don't test for the API version here, just check if CTE is enabled. - // We can provide more specific errors WRT API versions after the expression has been parsed and we know what it is. + // We can provide more specific errors w.r.t API versions after the expression has been parsed and we know what it is. if (isQuoted && !ctx.config.experimentalComplexExpressions) { // // -> ambiguity if the attribute value is a template identifier or a string literal.