From 8eec50f9a5aa5c0bf252d485630e66a02e71af8d Mon Sep 17 00:00:00 2001
From: Will Harney <62956339+wjhsf@users.noreply.github.com>
Date: Fri, 31 Jan 2025 14:59:20 -0500
Subject: [PATCH] fix(ssr): handle `export { Cmp as default }` @W-17655297
 (#5176)

* fix: allow export { Cmp as default }

* test(ssr): update component-as-default to check light and shadow

* fix: use fallback if no generateMarkup

* fix(ssr): support `export { Cmp as default }`

* fix: support slotted content for unimplemented templates

* fix: use fallbackTmpl for all cases

* Update packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts

* Update packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts

---------

Co-authored-by: John Hefferman <jhefferman@salesforce.com>
---
 .../component-as-default/expected.html        | 19 +++++++++--
 .../exports/component-as-default/index.js     |  6 ++--
 .../component-as-default/modules/x/cmp/cmp.js |  4 ---
 .../modules/x/light/light.html                |  3 ++
 .../modules/x/light/light.js                  |  7 ++++
 .../modules/x/parent/parent.html              |  6 ++++
 .../modules/x/parent/parent.js                |  3 ++
 .../x/{cmp/cmp.html => shadow/shadow.html}    |  0
 .../modules/x/shadow/shadow.js                |  5 +++
 .../src/__tests__/utils/expected-failures.ts  |  1 -
 .../transformers/component/component.ts       | 34 ++++++++++++-------
 packages/@lwc/ssr-runtime/src/render.ts       | 16 ++++++---
 12 files changed, 77 insertions(+), 27 deletions(-)
 delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/cmp/cmp.js
 create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.html
 create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.js
 create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.html
 create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.js
 rename packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/{cmp/cmp.html => shadow/shadow.html} (100%)
 create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/shadow/shadow.js

diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/expected.html
index ce996e78e1..1dd9a11014 100644
--- a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/expected.html
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/expected.html
@@ -1,4 +1,19 @@
-<x-cmp>
+<x-parent>
   <template shadowrootmode="open">
+    <x-shadow>
+      <template shadowrootmode="open">
+      </template>
+    </x-shadow>
+    <x-shadow>
+      <template shadowrootmode="open">
+      </template>
+      <h1>
+        slotted content
+      </h1>
+    </x-shadow>
+    <x-light>
+    </x-light>
+    <x-light>
+    </x-light>
   </template>
-</x-cmp>
\ No newline at end of file
+</x-parent>
\ No newline at end of file
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/index.js b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/index.js
index 55d4302e11..d5a55ceefa 100644
--- a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/index.js
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/index.js
@@ -1,3 +1,3 @@
-export const tagName = 'x-cmp';
-export { default } from 'x/cmp';
-export * from 'x/cmp';
+export const tagName = 'x-parent';
+export { default } from 'x/parent';
+export * from 'x/parent';
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/cmp/cmp.js b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/cmp/cmp.js
deleted file mode 100644
index e0542c7a5d..0000000000
--- a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/cmp/cmp.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { LightningElement } from 'lwc';
-
-class Component extends LightningElement {}
-export { Component as default };
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.html b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.html
new file mode 100644
index 0000000000..ef261dbb01
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.html
@@ -0,0 +1,3 @@
+<template lwc:render-mode="light">
+  This template isn't actually used because `export {Component as default}` isn't recognized as an LWC component.
+</template>
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.js b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.js
new file mode 100644
index 0000000000..65a040de68
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/light/light.js
@@ -0,0 +1,7 @@
+import { LightningElement } from 'lwc';
+
+class Light extends LightningElement {
+    static renderMode = 'light';
+}
+
+export { Light as default };
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.html b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.html
new file mode 100644
index 0000000000..7ace378547
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.html
@@ -0,0 +1,6 @@
+<template>
+  <x-shadow></x-shadow>
+  <x-shadow><h1>slotted content</h1></x-shadow>
+  <x-light></x-light>
+  <x-light><h1>slotted content</h1></x-light>
+</template>
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.js b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.js
new file mode 100644
index 0000000000..a5746a015a
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/parent/parent.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/exports/component-as-default/modules/x/cmp/cmp.html b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/shadow/shadow.html
similarity index 100%
rename from packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/cmp/cmp.html
rename to packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/shadow/shadow.html
diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/shadow/shadow.js b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/shadow/shadow.js
new file mode 100644
index 0000000000..098f79650f
--- /dev/null
+++ b/packages/@lwc/engine-server/src/__tests__/fixtures/exports/component-as-default/modules/x/shadow/shadow.js
@@ -0,0 +1,5 @@
+import { LightningElement } from 'lwc';
+
+class Shadow extends LightningElement {}
+
+export { Shadow as default };
diff --git a/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts b/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts
index 004300f11a..235ff7ebd0 100644
--- a/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts
+++ b/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts
@@ -10,7 +10,6 @@
 export const expectedFailures = new Set([
     'attribute-global-html/as-component-prop/undeclared/index.js',
     'attribute-global-html/as-component-prop/without-@api/index.js',
-    'exports/component-as-default/index.js',
     'known-boolean-attributes/default-def-html-attributes/static-on-component/index.js',
     'wire/errors/throws-when-colliding-prop-then-method/index.js',
 ]);
diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts
index c02ffe8bf8..25540e1c51 100644
--- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts
+++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts
@@ -31,25 +31,32 @@ const bYieldFromChildGenerator = esTemplateWithYield`
                 Slotted content is inserted here.
                 Note that the slotted content will be stored in variables named 
                 `shadowSlottedContent`/`lightSlottedContentMap / scopedSlottedContentMap` which are used below 
-                when the child's generateMarkup function is invoked.
+            when the child's generateMarkup function is invoked.
             */
             is.statement
         }
 
         const scopeToken = hasScopedStylesheets ? stylesheetScopeToken : undefined;
-        const Ctor = ${/* Component */ is.identifier};
+        const generateMarkup = ${/* Component */ is.identifier}[__SYMBOL__GENERATE_MARKUP];
+        const tagName = ${/* tag name */ is.literal};
 
-        yield* Ctor[__SYMBOL__GENERATE_MARKUP](
-            ${/* tag name */ is.literal}, 
-            childProps, 
-            childAttrs, 
-            shadowSlottedContent,
-            lightSlottedContentMap,
-            scopedSlottedContentMap,
-            instance,
-            scopeToken,
-            contextfulParent
-        );
+        if (generateMarkup) {
+            yield* generateMarkup(
+                tagName, 
+                childProps, 
+                childAttrs, 
+                shadowSlottedContent,
+                lightSlottedContentMap,
+                scopedSlottedContentMap,
+                instance,
+                scopeToken,
+                contextfulParent
+            );
+        } else {
+            yield \`<\${tagName}>\`;
+            yield* __fallbackTmpl(shadowSlottedContent, lightSlottedContentMap, scopedSlottedContentMap, ${/* Component */ 3}, instance)
+            yield \`</\${tagName}>\`;
+        }
     }
 `<EsBlockStatement>;
 
@@ -60,6 +67,7 @@ export const Component: Transformer<IrComponent> = function Component(node, cxt)
     cxt.import({ default: childComponentLocalName }, importPath);
     cxt.import({
         SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
+        fallbackTmpl: '__fallbackTmpl',
     });
     const childTagName = node.name;
 
diff --git a/packages/@lwc/ssr-runtime/src/render.ts b/packages/@lwc/ssr-runtime/src/render.ts
index 4a635df8e2..98e15d4f25 100644
--- a/packages/@lwc/ssr-runtime/src/render.ts
+++ b/packages/@lwc/ssr-runtime/src/render.ts
@@ -105,7 +105,7 @@ export function* fallbackTmpl(
     _lightSlottedContent: unknown,
     _scopedSlottedContent: unknown,
     Cmp: LightningElementConstructor,
-    instance: unknown
+    instance: LightningElement
 ) {
     if (Cmp.renderMode !== 'light') {
         yield `<template shadowrootmode="open"></template>`;
@@ -117,11 +117,11 @@ export function* fallbackTmpl(
 
 export function fallbackTmplNoYield(
     emit: (segment: string) => void,
-    shadowSlottedContent: AsyncGeneratorFunction,
+    shadowSlottedContent: AsyncGeneratorFunction | null,
     _lightSlottedContent: unknown,
     _scopedSlottedContent: unknown,
     Cmp: LightningElementConstructor,
-    instance: unknown
+    instance: LightningElement | null
 ) {
     if (Cmp.renderMode !== 'light') {
         emit(`<template shadowrootmode="open"></template>`);
@@ -180,7 +180,7 @@ type GenerateMarkupFnVariants =
     | GenerateMarkupFnAsyncNoGen
     | GenerateMarkupFnSyncNoGen;
 
-interface ComponentWithGenerateMarkup {
+interface ComponentWithGenerateMarkup extends LightningElementConstructor {
     [SYMBOL__GENERATE_MARKUP]: GenerateMarkupFnVariants;
 }
 
@@ -201,6 +201,14 @@ export async function serverSideRenderComponent(
         markup += segment;
     };
 
+    if (!generateMarkup) {
+        // If a non-component is accidentally provided, render an empty template
+        emit(`<${tagName}>`);
+        fallbackTmplNoYield(emit, null, null, null, Component, null);
+        emit(`</${tagName}>`);
+        return markup;
+    }
+
     if (mode === 'asyncYield') {
         for await (const segment of (generateMarkup as GenerateMarkupFn)(
             tagName,