Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"entry": "x/attribute-dynamic-complex",
"experimentalComplexExpressions": true,
"ssrFiles": {
"expected": "expected-ssr.html",
"error": "error-ssr.txt"
}
}
Original file line number Diff line number Diff line change
@@ -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()}".
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<fixture-test>
<template shadowrootmode="open">
<div class="foobar">
</div>
</template>
</fixture-test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div class="{bar() + foo()}"></div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { LightningElement } from 'lwc';

export default class AttributeDynamicComplex extends LightningElement {
bar() {
return 'foo';
}
foo() {
return 'bar';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"entry": "x/for-each-block",
"experimentalComplexExpressions": true,
"ssrFiles": {
"expected": "expected-ssr.html",
"error": "error-ssr.txt"
}
}
Original file line number Diff line number Diff line change
@@ -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()}".
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<fixture-test>
<template shadowrootmode="open">
<ul>
<li>
0 - paris
</li>
<li>
1 - london
</li>
<li>
2 - tokyo
</li>
</ul>
</template>
</fixture-test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<ul>
<template for:each="{list()}" for:item="item" for:index="index">
<li key={item}>{index} - {item}</li>
</template>
</ul>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LightningElement } from 'lwc';

export default class Component extends LightningElement {
list() {
return ['paris', 'london', 'tokyo'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"entry": "x/component",
"experimentalComplexExpressions": true,
"ssrFiles": {
"expected": "expected-ssr.html",
"error": "error-ssr.txt"
}
}
Original file line number Diff line number Diff line change
@@ -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()}".
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<fixture-test>
<template shadowrootmode="open">
<!---->
I am true!
<!---->
<!---->
I am not false!
<!---->
</template>
</fixture-test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<template lwc:if="{isTrue()}">
I am true!
</template>
<template lwc:else>
I am not true!
</template>

<template lwc:if="{isFalse()}">
I am false!
</template>
<template lwc:else>
I am not false!
</template>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LightningElement } from 'lwc';

export default class IfBlock extends LightningElement {
isTrue() {
return true;
}

isFalse() {
return this.false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"entry": "x/complexParent",
"experimentalComplexExpressions": true,
"ssrFiles": {
"expected": "expected-ssr.html",
"error": "error-ssr.txt"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LWC1060: Template expression doesn't allow CallExpression
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<fixture-test>
<template shadowrootmode="open">
<x-complex-child>
<div>
<!---->
<!---->
<span>
99 - ssr
</span>
<!---->
<!---->
</div>
</x-complex-child>
</template>
</fixture-test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template lwc:render-mode="light">
<div>
<slot lwc:slot-bind="{itemFn()}">Default slot content</slot>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { LightningElement } from 'lwc';

export default class Child extends LightningElement {
static renderMode = 'light';
itemFn() {
return () => ({ id: 99, name: 'ssr' });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<x-complex-child>
<template lwc:slot-data="itemFn">
<span>{itemFn().id} - {itemFn().name}</span>
</template>
</x-complex-child>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { LightningElement } from 'lwc';

export default class Parent extends LightningElement {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"entry": "x/text-interpolation",
"props": {
"publicProp": "public-prop"
},
"experimentalComplexExpressions": true,
"ssrFiles": {
"expected": "expected-ssr.html",
"error": "error-ssr.txt"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
LWC1060: Template expression doesn't allow BinaryExpression
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<fixture-test>
<template shadowrootmode="open">
<p>
foo bar bar foo
</p>
</template>
</fixture-test>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<p>{bar() + ' ' + foo()} {foo() + ' ' + bar()}</p>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { LightningElement } from 'lwc';

export default class TextInterpolation extends LightningElement {
foo() {
return 'bar';
}
bar() {
return 'foo';
}
}
15 changes: 14 additions & 1 deletion packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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';
Expand All @@ -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 }) {
Expand Down Expand Up @@ -109,6 +121,7 @@ describe.concurrent('fixtures', () => {
compiledFixturePath = await compileFixture({
entry: config!.entry,
dirname,
experimentalComplexExpressions: config!.experimentalComplexExpressions,
});
} catch (err: any) {
return {
Expand Down
82 changes: 54 additions & 28 deletions packages/@lwc/ssr-compiler/src/compile-template/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. `<template for:each={foos} for:item="foo">`
* @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)}\``
);
}
Loading