diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/config.json new file mode 100644 index 0000000000..3af06c8613 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/config.json @@ -0,0 +1,8 @@ +{ + "entry": "x/attribute-dynamic-complex", + "experimentalComplexExpressions": true, + "ssrFiles": { + "expected": "expected-ssr.html", + "error": "error-ssr.txt" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/error-ssr.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/error.txt new file mode 100644 index 0000000000..b7603fab66 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/error.txt @@ -0,0 +1 @@ +LWC1034: Ambiguous attribute value class="{bar() + foo()}". If you want to make it a valid identifier you should remove the surrounding quotes class={bar() + foo()}. If you want to make it a string you should escape it class="\{bar() + foo()}". \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/expected-ssr.html new file mode 100644 index 0000000000..259866046b --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/expected-ssr.html @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/modules/x/attribute-dynamic-complex/attribute-dynamic-complex.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/modules/x/attribute-dynamic-complex/attribute-dynamic-complex.html new file mode 100755 index 0000000000..4f9c66db53 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/modules/x/attribute-dynamic-complex/attribute-dynamic-complex.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/modules/x/attribute-dynamic-complex/attribute-dynamic-complex.js b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/modules/x/attribute-dynamic-complex/attribute-dynamic-complex.js new file mode 100755 index 0000000000..602273dffa --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-dynamic-complex/modules/x/attribute-dynamic-complex/attribute-dynamic-complex.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; + +export default class AttributeDynamicComplex extends LightningElement { + bar() { + return 'foo'; + } + foo() { + return 'bar'; + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/config.json new file mode 100755 index 0000000000..b302e68ad6 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/config.json @@ -0,0 +1,8 @@ +{ + "entry": "x/for-each-block", + "experimentalComplexExpressions": true, + "ssrFiles": { + "expected": "expected-ssr.html", + "error": "error-ssr.txt" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/error-ssr.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/error.txt new file mode 100644 index 0000000000..f13176a7a6 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/error.txt @@ -0,0 +1 @@ +LWC1034: Ambiguous attribute value for:each="{list()}". If you want to make it a valid identifier you should remove the surrounding quotes for:each={list()}. If you want to make it a string you should escape it for:each="\{list()}". \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/expected-ssr.html new file mode 100644 index 0000000000..3b62ecf650 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/expected-ssr.html @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/modules/x/for-each-block/for-each-block.html b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/modules/x/for-each-block/for-each-block.html new file mode 100755 index 0000000000..664b217367 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/modules/x/for-each-block/for-each-block.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/modules/x/for-each-block/for-each-block.js b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/modules/x/for-each-block/for-each-block.js new file mode 100755 index 0000000000..4a98edc205 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/for-each-block/complex/modules/x/for-each-block/for-each-block.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; + +export default class Component extends LightningElement { + list() { + return ['paris', 'london', 'tokyo']; + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/config.json new file mode 100644 index 0000000000..74eaaf0c4f --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/config.json @@ -0,0 +1,8 @@ +{ + "entry": "x/component", + "experimentalComplexExpressions": true, + "ssrFiles": { + "expected": "expected-ssr.html", + "error": "error-ssr.txt" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/error-ssr.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/error.txt new file mode 100644 index 0000000000..1f6547bdc5 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/error.txt @@ -0,0 +1 @@ +LWC1034: Ambiguous attribute value lwc:if="{isTrue()}". If you want to make it a valid identifier you should remove the surrounding quotes lwc:if={isTrue()}. If you want to make it a string you should escape it lwc:if="\{isTrue()}". \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/expected-ssr.html new file mode 100644 index 0000000000..3985fdcdf6 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/expected-ssr.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/modules/x/component/component.html b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/modules/x/component/component.html new file mode 100755 index 0000000000..83717eb733 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/modules/x/component/component.html @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/modules/x/component/component.js b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/modules/x/component/component.js new file mode 100755 index 0000000000..ee524af201 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/lwc-if-else-complex/modules/x/component/component.js @@ -0,0 +1,11 @@ +import { LightningElement } from 'lwc'; + +export default class IfBlock extends LightningElement { + isTrue() { + return true; + } + + isFalse() { + return this.false; + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/config.json new file mode 100644 index 0000000000..045b0db31b --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/config.json @@ -0,0 +1,8 @@ +{ + "entry": "x/complexParent", + "experimentalComplexExpressions": true, + "ssrFiles": { + "expected": "expected-ssr.html", + "error": "error-ssr.txt" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/error-ssr.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/error.txt new file mode 100644 index 0000000000..a4bc6c6950 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/error.txt @@ -0,0 +1 @@ +LWC1060: Template expression doesn't allow CallExpression \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/expected-ssr.html new file mode 100644 index 0000000000..63dd5a68eb --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/expected-ssr.html @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexChild/complexChild.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexChild/complexChild.html new file mode 100644 index 0000000000..c624f74879 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexChild/complexChild.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexChild/complexChild.js b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexChild/complexChild.js new file mode 100644 index 0000000000..c5970cfa44 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexChild/complexChild.js @@ -0,0 +1,8 @@ +import { LightningElement } from 'lwc'; + +export default class Child extends LightningElement { + static renderMode = 'light'; + itemFn() { + return () => ({ id: 99, name: 'ssr' }); + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexParent/complexParent.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexParent/complexParent.html new file mode 100644 index 0000000000..fda7fb7d7d --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexParent/complexParent.html @@ -0,0 +1,7 @@ + diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexParent/complexParent.js b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexParent/complexParent.js new file mode 100644 index 0000000000..a5746a015a --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/complex/modules/x/complexParent/complexParent.js @@ -0,0 +1,3 @@ +import { LightningElement } from 'lwc'; + +export default class Parent extends LightningElement {} 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..7f133355aa --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json @@ -0,0 +1,11 @@ +{ + "entry": "x/text-interpolation", + "props": { + "publicProp": "public-prop" + }, + "experimentalComplexExpressions": true, + "ssrFiles": { + "expected": "expected-ssr.html", + "error": "error-ssr.txt" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/error-ssr.txt new file mode 100644 index 0000000000..e69de29bb2 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..35dac9946c --- /dev/null +++ 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-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected-ssr.html new file mode 100644 index 0000000000..68f5d530f9 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/expected-ssr.html @@ -0,0 +1,7 @@ + + + \ 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 new file mode 100644 index 0000000000..e69de29bb2 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..2398b50a3d --- /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..5959b04818 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/modules/x/text-interpolation/text-interpolation.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; + +export default class TextInterpolation extends LightningElement { + 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 8c39d7ba7c..0af9fa2537 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,15 @@ 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 +84,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 +121,7 @@ describe.concurrent('fixtures', () => { compiledFixturePath = await compileFixture({ entry: config!.entry, dirname, + experimentalComplexExpressions: config!.experimentalComplexExpressions, }); } catch (err: any) { return { 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. `