Skip to content

Commit eb7a327

Browse files
feat: ssr cte support
1 parent 9035b03 commit eb7a327

File tree

16 files changed

+163
-165
lines changed

16 files changed

+163
-165
lines changed

packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/.only

Whitespace-only changes.

packages/@lwc/engine-server/src/__tests__/fixtures/text-interpolation-complex/config.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@
33
"props": {
44
"publicProp": "public-prop"
55
},
6-
"experimentalComplexExpressions": true
6+
"experimentalComplexExpressions": true,
7+
"ssrFiles": {
8+
"expected": "expected-ssr.txt",
9+
"error": "error-ssr.txt"
10+
}
711
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
LWC1060: Template expression doesn't allow BinaryExpression
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +0,0 @@
1-
<fixture-test>
2-
<template shadowrootmode="open">
3-
<p>
4-
</p>
5-
</template>
6-
</fixture-test>
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<template>
2-
<p>function call: {privateProp + 1}</p>
2+
<p>{bar() + ' ' + foo()} {foo() + ' ' + bar()}</p>
33
</template>
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { LightningElement } from 'lwc';
22

33
export default class TextInterpolation extends LightningElement {
4-
privateProp = 1;
5-
functionCall() {
6-
this.privateProp++;
4+
foo() {
5+
return 'bar';
6+
}
7+
bar() {
8+
return 'foo';
79
}
810
}

packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,15 @@ vi.mock('@lwc/ssr-runtime', async () => {
5454

5555
const SSR_MODE: CompilationMode = DEFAULT_SSR_MODE;
5656

57-
async function compileFixture({ entry, dirname, experimentalComplexExpressions }: { entry: string; dirname: string, experimentalComplexExpressions: boolean }) {
57+
async function compileFixture({
58+
entry,
59+
dirname,
60+
experimentalComplexExpressions,
61+
}: {
62+
entry: string;
63+
dirname: string;
64+
experimentalComplexExpressions: boolean;
65+
}) {
5866
const modulesDir = path.resolve(dirname, './modules');
5967
const outputFile = path.resolve(dirname, './dist/compiled-experimental-ssr.js');
6068
const input = 'virtual/fixture/test.js';
@@ -76,7 +84,7 @@ async function compileFixture({ entry, dirname, experimentalComplexExpressions }
7684
loader: path.join(__dirname, './utils/custom-loader.js'),
7785
strictSpecifier: false,
7886
},
79-
experimentalComplexExpressions
87+
experimentalComplexExpressions,
8088
}),
8189
],
8290
onwarn({ message, code }) {

packages/@lwc/ssr-compiler/src/compile-template/expression.ts

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,71 @@
44
* SPDX-License-Identifier: MIT
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
66
*/
7-
8-
import { builders as b } from 'estree-toolkit';
9-
7+
import { bindExpression } from '@lwc/template-compiler';
108
import type {
119
ComplexExpression as IrComplexExpression,
1210
Expression as IrExpression,
1311
Identifier as IrIdentifier,
1412
MemberExpression as IrMemberExpression,
1513
} from '@lwc/template-compiler';
16-
import type {
17-
Identifier as EsIdentifier,
18-
Expression as EsExpression,
19-
MemberExpression as EsMemberExpression,
20-
} from 'estree';
14+
import type { Identifier as EsIdentifier, Expression as EsExpression } from 'estree';
2115
import type { TransformerContext } from './types';
2216

23-
function getRootMemberExpression(node: IrMemberExpression): IrMemberExpression {
24-
return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node;
25-
}
26-
2717
export function expressionIrToEs(
2818
node: IrExpression | IrComplexExpression,
2919
cxt: TransformerContext
3020
): EsExpression {
31-
if (node.type === 'Identifier') {
32-
const isLocalVar = cxt.isLocalVar((node as IrIdentifier).name);
33-
return isLocalVar
34-
? (node as EsIdentifier)
35-
: b.memberExpression(b.identifier('instance'), node as EsIdentifier);
36-
} else if (node.type === 'MemberExpression') {
37-
const nodeClone = structuredClone(node);
38-
const rootMemberExpr = getRootMemberExpression(nodeClone as IrMemberExpression);
39-
if (!cxt.isLocalVar((rootMemberExpr.object as IrIdentifier).name)) {
40-
rootMemberExpr.object = b.memberExpression(
41-
b.identifier('instance'),
42-
rootMemberExpr.object as EsIdentifier
43-
) as unknown as IrMemberExpression;
44-
}
45-
return nodeClone as unknown as EsMemberExpression;
21+
return bindExpression(
22+
node as IrComplexExpression,
23+
(n: EsIdentifier) => cxt.isLocalVar((n as EsIdentifier).name),
24+
'instance',
25+
cxt.templateOptions.experimentalComplexExpressions
26+
);
27+
}
28+
29+
/**
30+
* Given an expression in a context, return an expression that may be scoped to that context.
31+
* For example, for the expression `foo`, it will typically be `instance.foo`, but if we're
32+
* inside a `for:each` block then the `foo` variable may refer to the scoped `foo`,
33+
* e.g. `<template for:each={foos} for:item="foo">`
34+
* @param expression
35+
* @param cxt
36+
*/
37+
export function getScopedExpression(
38+
expression: IrExpression,
39+
cxt: TransformerContext
40+
): EsExpression {
41+
let scopeReferencedId: IrExpression | null = null;
42+
if (expression.type === 'MemberExpression') {
43+
// e.g. `foo.bar` -> scopeReferencedId is `foo`
44+
scopeReferencedId = getRootIdentifier(expression);
45+
} else if (expression.type === 'Identifier') {
46+
// e.g. `foo` -> scopeReferencedId is `foo`
47+
scopeReferencedId = expression;
4648
}
47-
throw new Error(`Unimplemented expression: ${node.type}`);
49+
50+
if (scopeReferencedId === null && !cxt.templateOptions.experimentalComplexExpressions) {
51+
throw new Error(
52+
`Invalid expression, must be a MemberExpression or Identifier, found type="${expression.type}": \`${JSON.stringify(expression)}\``
53+
);
54+
}
55+
56+
return cxt.isLocalVar(scopeReferencedId?.name)
57+
? (expression as EsExpression)
58+
: expressionIrToEs(expression, cxt);
59+
}
60+
61+
function getRootMemberExpression(node: IrMemberExpression): IrMemberExpression {
62+
return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node;
63+
}
64+
65+
function getRootIdentifier(node: IrMemberExpression): IrIdentifier {
66+
const rootMemberExpression = getRootMemberExpression(node);
67+
if (rootMemberExpression.object.type === 'Identifier') {
68+
return rootMemberExpression.object;
69+
}
70+
71+
throw new Error(
72+
`Invalid expression, must be an Identifier, found type="${rootMemberExpression.type}": \`${JSON.stringify(rootMemberExpression)}\``
73+
);
4874
}

packages/@lwc/ssr-compiler/src/compile-template/shared.ts

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import type {
2222
} from '@lwc/template-compiler';
2323
import type {
2424
Expression as EsExpression,
25-
Identifier as EsIdentifier,
26-
MemberExpression as EsMemberExpression,
2725
ObjectExpression as EsObjectExpression,
2826
Property as EsProperty,
2927
Statement as EsStatement,
@@ -79,59 +77,6 @@ export function bAttributeValue(node: IrNode, attrName: string): EsExpression {
7977
}
8078
}
8179

82-
function getRootMemberExpression(node: EsMemberExpression): EsMemberExpression {
83-
return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node;
84-
}
85-
86-
function getRootIdentifier(node: EsMemberExpression, cxt: TransformerContext): EsIdentifier | null {
87-
const rootMemberExpression = getRootMemberExpression(node);
88-
if (is.identifier(rootMemberExpression.object)) {
89-
return rootMemberExpression.object;
90-
}
91-
if (cxt.templateOptions.experimentalComplexExpressions) {
92-
// TODO [#3370]: Implement complex template expressions
93-
return null;
94-
}
95-
// Should be impossible to hit, at least until we implement complex template expressions
96-
/* v8 ignore next */
97-
throw new Error(
98-
`Invalid expression, must be an Identifier, found type="${rootMemberExpression.type}": \`${JSON.stringify(rootMemberExpression)}\``
99-
);
100-
}
101-
102-
/**
103-
* Given an expression in a context, return an expression that may be scoped to that context.
104-
* For example, for the expression `foo`, it will typically be `instance.foo`, but if we're
105-
* inside a `for:each` block then the `foo` variable may refer to the scoped `foo`,
106-
* e.g. `<template for:each={foos} for:item="foo">`
107-
* @param expression
108-
* @param cxt
109-
*/
110-
export function getScopedExpression(expression: EsExpression, cxt: TransformerContext) {
111-
let scopeReferencedId: EsExpression | null = null;
112-
if (is.memberExpression(expression)) {
113-
// e.g. `foo.bar` -> scopeReferencedId is `foo`
114-
scopeReferencedId = getRootIdentifier(expression, cxt);
115-
} else if (is.identifier(expression)) {
116-
// e.g. `foo` -> scopeReferencedId is `foo`
117-
scopeReferencedId = expression;
118-
}
119-
if (scopeReferencedId === null) {
120-
if (cxt.templateOptions.experimentalComplexExpressions) {
121-
// TODO [#3370]: Implement complex template expressions
122-
return expression;
123-
}
124-
// Should be impossible to hit, at least until we implement complex template expressions
125-
/* v8 ignore next */
126-
throw new Error(
127-
`Invalid expression, must be a MemberExpression or Identifier, found type="${expression.type}": \`${JSON.stringify(expression)}\``
128-
);
129-
}
130-
return cxt.isLocalVar(scopeReferencedId.name)
131-
? expression
132-
: b.memberExpression(b.identifier('instance'), expression);
133-
}
134-
13580
export function normalizeClassAttributeValue(value: string) {
13681
// @ts-expect-error weird indirection results in wrong overload being picked up
13782
return StringReplace.call(StringTrim.call(value), /\s+/g, ' ');

packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,15 @@ import {
2020
type Property as IrProperty,
2121
} from '@lwc/template-compiler';
2222
import { esTemplateWithYield } from '../../estemplate';
23-
import { expressionIrToEs } from '../expression';
23+
import { expressionIrToEs, getScopedExpression } from '../expression';
2424
import { irChildrenToEs } from '../ir-to-es';
25-
import { getScopedExpression, normalizeClassAttributeValue } from '../shared';
25+
import { normalizeClassAttributeValue } from '../shared';
2626
import type {
2727
ExternalComponent as IrExternalComponent,
2828
Slot as IrSlot,
2929
} from '@lwc/template-compiler';
3030

3131
import type {
32-
BinaryExpression,
3332
BlockStatement as EsBlockStatement,
3433
Expression as EsExpression,
3534
Statement as EsStatement,
@@ -165,11 +164,11 @@ function yieldAttrOrPropLiteralValue(name: string, valueNode: IrLiteral): EsStat
165164
function yieldAttrOrPropDynamicValue(
166165
elementName: string,
167166
name: string,
168-
value: IrExpression | BinaryExpression,
167+
value: IrExpression,
169168
cxt: TransformerContext
170169
): EsStatement[] {
171170
cxt.import('htmlEscape');
172-
const scopedExpression = getScopedExpression(value as EsExpression, cxt);
171+
const scopedExpression = getScopedExpression(value, cxt);
173172
switch (name) {
174173
case 'class':
175174
cxt.import('normalizeClass');

0 commit comments

Comments
 (0)