diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/error.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/expected.html
new file mode 100644
index 0000000000..0f1a1b5d5b
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/expected.html
@@ -0,0 +1,7 @@
+
+
+
+ yolo
+
+
+
\ No newline at end of file
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/index.js b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/index.js
new file mode 100644
index 0000000000..cd34090d2c
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/index.js
@@ -0,0 +1,3 @@
+export const tagName = 'x-component';
+export { default } from 'x/component';
+export * from 'x/component';
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/modules/x/component/component.html b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/modules/x/component/component.html
new file mode 100755
index 0000000000..efb27c23c0
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/modules/x/component/component.html
@@ -0,0 +1,5 @@
+
+
+ {row.foo.bar.baz}
+
+
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/modules/x/component/component.js b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/modules/x/component/component.js
new file mode 100755
index 0000000000..4c777808d7
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/very-deeply-nested-each/modules/x/component/component.js
@@ -0,0 +1,14 @@
+import { LightningElement } from 'lwc';
+
+export default class Component extends LightningElement {
+ rows = [
+ {
+ id: 1,
+ foo: {
+ bar: {
+ baz: 'yolo'
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/@lwc/rollup-plugin/src/index.ts b/packages/@lwc/rollup-plugin/src/index.ts
index b504085af1..d4cafc8f47 100644
--- a/packages/@lwc/rollup-plugin/src/index.ts
+++ b/packages/@lwc/rollup-plugin/src/index.ts
@@ -161,6 +161,7 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
let { rootDir, modules = [] } = pluginOptions;
const {
targetSSR,
+ ssrMode,
stylesheetConfig,
sourcemap = false,
preserveHtmlComments,
@@ -348,6 +349,7 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
enableStaticContentOptimization: pluginOptions.enableStaticContentOptimization,
}),
targetSSR,
+ ssrMode,
});
if (warnings) {
diff --git a/packages/@lwc/ssr-compiler/src/compile-template/shared.ts b/packages/@lwc/ssr-compiler/src/compile-template/shared.ts
index 2a3165764b..af9eb3c4e2 100644
--- a/packages/@lwc/ssr-compiler/src/compile-template/shared.ts
+++ b/packages/@lwc/ssr-compiler/src/compile-template/shared.ts
@@ -10,14 +10,17 @@ import { reservedKeywords } from '@lwc/shared';
import { Node as IrNode } from '@lwc/template-compiler';
import { esTemplate } from '../estemplate';
+import { TransformerContext } from './types';
import type {
ImportDeclaration as EsImportDeclaration,
Statement as EsStatement,
Expression as EsExpression,
+ MemberExpression as EsMemberExpression,
+ Identifier as EsIdentifier,
} from 'estree';
export const bImportHtmlEscape = esTemplate`
- import { htmlEscape } from '@lwc/shared';
+ import { htmlEscape } from '@lwc/ssr-runtime';
`;
export const importHtmlEscapeKey = 'import:htmlEscape';
@@ -75,3 +78,28 @@ export function bAttributeValue(node: IrNode, attrName: string): EsExpression {
return b.memberExpression(b.literal('instance'), nameAttrValue as EsExpression);
}
}
+
+function getRootMemberExpression(node: EsMemberExpression): EsMemberExpression {
+ return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node;
+}
+
+function getRootIdentifier(node: EsMemberExpression): EsIdentifier | null {
+ const rootMemberExpression = getRootMemberExpression(node);
+ return is.identifier(rootMemberExpression?.object) ? rootMemberExpression.object : null;
+}
+
+/**
+ * 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
+ */
+export function getScopedExpression(expression: EsExpression, cxt: TransformerContext) {
+ const scopeReferencedId = is.memberExpression(expression)
+ ? getRootIdentifier(expression)
+ : null;
+ return cxt.isLocalVar(scopeReferencedId?.name)
+ ? expression
+ : b.memberExpression(b.identifier('instance'), expression);
+}
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 ad70fbb407..c6cc6dbf1f 100644
--- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts
+++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts
@@ -25,7 +25,7 @@ import {
import { esTemplateWithYield } from '../../estemplate';
import { expressionIrToEs } from '../expression';
import { irChildrenToEs } from '../ir-to-es';
-import { bImportHtmlEscape, importHtmlEscapeKey } from '../shared';
+import { bImportHtmlEscape, getScopedExpression, importHtmlEscapeKey } from '../shared';
import type {
BinaryExpression,
@@ -33,18 +33,18 @@ import type {
Expression as EsExpression,
Statement as EsStatement,
} from 'estree';
-import type { Transformer } from '../types';
+import type { Transformer, TransformerContext } from '../types';
const bYield = (expr: EsExpression) => b.expressionStatement(b.yieldExpression(expr));
const bConditionalLiveYield = esTemplateWithYield`
{
const prefix = (${/* isClass */ is.literal} && stylesheetScopeTokenClassPrefix) || '';
- const attrOrPropValue = ${is.expression};
- const valueType = typeof attrOrPropValue;
- if (attrOrPropValue && (valueType === 'string' || valueType === 'boolean')) {
- yield ' ' + ${is.literal};
+ const attrValue = ${/* attribute value expression */ is.expression};
+ const valueType = typeof attrValue;
+ if (attrValue && (valueType === 'string' || valueType === 'boolean')) {
+ yield ' ' + ${/* attribute name */ is.literal};
if (valueType === 'string') {
- yield \`="\${prefix}\${htmlEscape(attrOrPropValue, true)}"\`;
+ yield \`="\${prefix}\${htmlEscape(attrValue, true)}"\`;
}
}
}
@@ -83,10 +83,11 @@ function yieldAttrOrPropLiteralValue(
function yieldAttrOrPropLiveValue(
name: string,
value: IrExpression | BinaryExpression,
- isClass: boolean
+ isClass: boolean,
+ cxt: TransformerContext
): EsStatement[] {
- const instanceMemberRef = b.memberExpression(b.identifier('instance'), value as EsExpression);
- return [bConditionalLiveYield(b.literal(isClass), instanceMemberRef, b.literal(name))];
+ const scopedExpression = getScopedExpression(value as EsExpression, cxt);
+ return [bConditionalLiveYield(b.literal(isClass), scopedExpression, b.literal(name))];
}
function reorderAttributes(
@@ -142,7 +143,7 @@ export const Element: Transformer = fu
if (value.type === 'Literal') {
return yieldAttrOrPropLiteralValue(name, value, isClass);
} else {
- return yieldAttrOrPropLiveValue(name, value, isClass);
+ return yieldAttrOrPropLiveValue(name, value, isClass, cxt);
}
});
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 5c82d152fe..9d496876f9 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,26 +8,12 @@
import { builders as b, is } from 'estree-toolkit';
import { esTemplate } from '../../estemplate';
import { irToEs } from '../ir-to-es';
-import { optimizeAdjacentYieldStmts } from '../shared';
+import { getScopedExpression, optimizeAdjacentYieldStmts } from '../shared';
import type { ForEach as IrForEach } from '@lwc/template-compiler';
-import type {
- Expression as EsExpression,
- ForOfStatement as EsForOfStatement,
- Identifier as EsIdentifier,
- MemberExpression as EsMemberExpression,
-} from 'estree';
+import type { Expression as EsExpression, ForOfStatement as EsForOfStatement } from 'estree';
import type { Transformer } from '../types';
-function getRootMemberExpression(node: EsMemberExpression): EsMemberExpression {
- return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node;
-}
-
-function getRootIdentifier(node: EsMemberExpression): EsIdentifier | null {
- const rootMemberExpression = getRootMemberExpression(node);
- return is.identifier(rootMemberExpression?.object) ? rootMemberExpression.object : null;
-}
-
const bForOfYieldFrom = esTemplate`
for (let [${is.identifier}, ${is.identifier}] of Object.entries(${is.expression} ?? {})) {
${is.statement};
@@ -45,13 +31,7 @@ export const ForEach: Transformer = function ForEach(node, cxt): EsFo
cxt.popLocalVars();
const expression = node.expression as EsExpression;
-
- const scopeReferencedId = is.memberExpression(expression)
- ? getRootIdentifier(expression)
- : null;
- const iterable = cxt.isLocalVar(scopeReferencedId?.name)
- ? (node.expression as EsExpression)
- : b.memberExpression(b.identifier('instance'), node.expression as EsExpression);
+ const iterable = getScopedExpression(expression, cxt);
return [
bForOfYieldFrom(
diff --git a/packages/@lwc/ssr-runtime/package.json b/packages/@lwc/ssr-runtime/package.json
index e644f2dc02..188197fcbb 100644
--- a/packages/@lwc/ssr-runtime/package.json
+++ b/packages/@lwc/ssr-runtime/package.json
@@ -43,7 +43,7 @@
}
}
},
- "dependencies": {
+ "devDependencies": {
"@lwc/shared": "8.3.0"
}
}
diff --git a/packages/@lwc/ssr-runtime/src/index.ts b/packages/@lwc/ssr-runtime/src/index.ts
index 20e9cfedcf..5d29c779ba 100644
--- a/packages/@lwc/ssr-runtime/src/index.ts
+++ b/packages/@lwc/ssr-runtime/src/index.ts
@@ -25,3 +25,4 @@ export {
export * from './stubs';
export { toIteratorDirective } from './to-iterator-directive';
export { validateStyleTextContents } from './validate-style-text-contents';
+export { htmlEscape } from '@lwc/shared';