+ foo bar bar foo +
+ +{bar() + ' ' + foo()} {foo() + ' ' + bar()}
+ \ 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. `` + * @param expression + * @param cxt + */ +export function getScopedExpression( + expression: IrExpression, + cxt: TransformerContext +): EsExpression { + let scopeReferencedId: IrExpression | null = null; + if (expression.type === 'MemberExpression') { + // e.g. `foo.bar` -> scopeReferencedId is `foo` + scopeReferencedId = getRootIdentifier(expression); + } else if (expression.type === 'Identifier') { + // e.g. `foo` -> scopeReferencedId is `foo` + scopeReferencedId = expression; } - throw new Error(`Unimplemented expression: ${node.type}`); + + if (scopeReferencedId === null && !cxt.templateOptions.experimentalComplexExpressions) { + throw new Error( + `Invalid expression, must be a MemberExpression or Identifier, found type="${expression.type}": \`${JSON.stringify(expression)}\`` + ); + } + + return cxt.isLocalVar(scopeReferencedId?.name) + ? (expression as EsExpression) + : expressionIrToEs(expression, cxt); +} + +function getRootMemberExpression(node: IrMemberExpression): IrMemberExpression { + return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node; +} + +function getRootIdentifier(node: IrMemberExpression): IrIdentifier { + const rootMemberExpression = getRootMemberExpression(node); + if (rootMemberExpression.object.type === 'Identifier') { + return rootMemberExpression.object; + } + + throw new Error( + `Invalid expression, must be an Identifier, found type="${rootMemberExpression.type}": \`${JSON.stringify(rootMemberExpression)}\`` + ); } diff --git a/packages/@lwc/ssr-compiler/src/compile-template/shared.ts b/packages/@lwc/ssr-compiler/src/compile-template/shared.ts index 2e2e4590a2..33f37c5a8c 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/shared.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/shared.ts @@ -22,8 +22,6 @@ import type { } from '@lwc/template-compiler'; import type { Expression as EsExpression, - Identifier as EsIdentifier, - MemberExpression as EsMemberExpression, ObjectExpression as EsObjectExpression, Property as EsProperty, Statement as EsStatement, @@ -79,59 +77,6 @@ export function bAttributeValue(node: IrNode, attrName: string): EsExpression { } } -function getRootMemberExpression(node: EsMemberExpression): EsMemberExpression { - return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node; -} - -function getRootIdentifier(node: EsMemberExpression, cxt: TransformerContext): EsIdentifier | null { - const rootMemberExpression = getRootMemberExpression(node); - if (is.identifier(rootMemberExpression.object)) { - return rootMemberExpression.object; - } - if (cxt.templateOptions.experimentalComplexExpressions) { - // TODO [#3370]: Implement complex template expressions - return null; - } - // Should be impossible to hit, at least until we implement complex template expressions - /* v8 ignore next */ - throw new Error( - `Invalid expression, must be an Identifier, found type="${rootMemberExpression.type}": \`${JSON.stringify(rootMemberExpression)}\`` - ); -} - -/** - * 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. `` - * @param expression - * @param cxt - */ -export function getScopedExpression(expression: EsExpression, cxt: TransformerContext) { - let scopeReferencedId: EsExpression | null = null; - if (is.memberExpression(expression)) { - // e.g. `foo.bar` -> scopeReferencedId is `foo` - scopeReferencedId = getRootIdentifier(expression, cxt); - } else if (is.identifier(expression)) { - // e.g. `foo` -> scopeReferencedId is `foo` - scopeReferencedId = expression; - } - if (scopeReferencedId === null) { - if (cxt.templateOptions.experimentalComplexExpressions) { - // TODO [#3370]: Implement complex template expressions - return expression; - } - // Should be impossible to hit, at least until we implement complex template expressions - /* v8 ignore next */ - throw new Error( - `Invalid expression, must be a MemberExpression or Identifier, found type="${expression.type}": \`${JSON.stringify(expression)}\`` - ); - } - return cxt.isLocalVar(scopeReferencedId.name) - ? expression - : b.memberExpression(b.identifier('instance'), expression); -} - export function normalizeClassAttributeValue(value: string) { // @ts-expect-error weird indirection results in wrong overload being picked up return StringReplace.call(StringTrim.call(value), /\s+/g, ' '); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts index 32c673fe7c..a6646d0a36 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts @@ -20,16 +20,15 @@ import { type Property as IrProperty, } from '@lwc/template-compiler'; import { esTemplateWithYield } from '../../estemplate'; -import { expressionIrToEs } from '../expression'; +import { expressionIrToEs, getScopedExpression } from '../expression'; import { irChildrenToEs } from '../ir-to-es'; -import { getScopedExpression, normalizeClassAttributeValue } from '../shared'; +import { normalizeClassAttributeValue } from '../shared'; import type { ExternalComponent as IrExternalComponent, Slot as IrSlot, } from '@lwc/template-compiler'; import type { - BinaryExpression, BlockStatement as EsBlockStatement, Expression as EsExpression, Statement as EsStatement, @@ -165,11 +164,11 @@ function yieldAttrOrPropLiteralValue(name: string, valueNode: IrLiteral): EsStat function yieldAttrOrPropDynamicValue( elementName: string, name: string, - value: IrExpression | BinaryExpression, + value: IrExpression, cxt: TransformerContext ): EsStatement[] { cxt.import('htmlEscape'); - const scopedExpression = getScopedExpression(value as EsExpression, cxt); + const scopedExpression = getScopedExpression(value, cxt); switch (name) { case 'class': cxt.import('normalizeClass'); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/for-each.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/for-each.ts index a4b3d53e2a..be53b0b2d2 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/for-each.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/for-each.ts @@ -8,10 +8,11 @@ import { builders as b, is } from 'estree-toolkit'; import { esTemplate } from '../../estemplate'; import { irChildrenToEs } from '../ir-to-es'; -import { getScopedExpression, optimizeAdjacentYieldStmts } from '../shared'; +import { optimizeAdjacentYieldStmts } from '../shared'; +import { getScopedExpression } from '../expression'; import type { ForEach as IrForEach } from '@lwc/template-compiler'; -import type { Expression as EsExpression, ForOfStatement as EsForOfStatement } from 'estree'; +import type { ForOfStatement as EsForOfStatement } from 'estree'; import type { Transformer } from '../types'; const bForOfYieldFrom = esTemplate` @@ -28,7 +29,7 @@ export const ForEach: Transformer