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 @@
-
+
+
+
+
+
+
+
+
+
+ slotted content
+
+
+
+
+
+
-
\ No newline at end of file
+
\ 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 @@
+
+ This template isn't actually used because `export {Component as default}` isn't recognized as an LWC component.
+
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 @@
+
+
+ slotted content
+
+ slotted content
+
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..d773515d14 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
@@ -37,19 +37,22 @@ const bYieldFromChildGenerator = esTemplateWithYield`
}
const scopeToken = hasScopedStylesheets ? stylesheetScopeToken : undefined;
- const Ctor = ${/* Component */ is.identifier};
-
- yield* Ctor[__SYMBOL__GENERATE_MARKUP](
- ${/* tag name */ is.literal},
- childProps,
- childAttrs,
- shadowSlottedContent,
- lightSlottedContentMap,
- scopedSlottedContentMap,
- instance,
- scopeToken,
- contextfulParent
- );
+ const generateMarkup = ${/* Component */ is.identifier}[__SYMBOL__GENERATE_MARKUP];
+ if (!generateMarkup) {
+ yield* __unimplementedTmpl(${/* tag name */ is.literal}, instance, shadowSlottedContent, ${/* Component */ 3});
+ } else {
+ yield* generateMarkup(
+ ${/* tag name */ 4},
+ childProps,
+ childAttrs,
+ shadowSlottedContent,
+ lightSlottedContentMap,
+ scopedSlottedContentMap,
+ instance,
+ scopeToken,
+ contextfulParent
+ );
+ }
}
`;
@@ -60,6 +63,7 @@ export const Component: Transformer = function Component(node, cxt)
cxt.import({ default: childComponentLocalName }, importPath);
cxt.import({
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
+ unimplementedTmpl: '__unimplementedTmpl',
});
const childTagName = node.name;
diff --git a/packages/@lwc/ssr-compiler/src/transmogrify.ts b/packages/@lwc/ssr-compiler/src/transmogrify.ts
index 6a3a89a579..aeb1f58ff5 100644
--- a/packages/@lwc/ssr-compiler/src/transmogrify.ts
+++ b/packages/@lwc/ssr-compiler/src/transmogrify.ts
@@ -114,6 +114,7 @@ const visitors: Visitors = {
//
// - renderAttrs vs renderAttrsNoYield
// - fallbackTmpl vs fallbackTmplNoYield
+ // - unimplementedTmpl vs unimplementedTmplNoYield
//
// If this becomes too burdensome to maintain, we can officially deprecate the generator-based approach
// and switch the @lwc/ssr-runtime implementation wholesale over to the no-generator paradigm.
@@ -136,6 +137,8 @@ const visitors: Visitors = {
node.imported.name = 'fallbackTmplNoYield';
} else if (node.imported.name === 'renderAttrs') {
node.imported.name = 'renderAttrsNoYield';
+ } else if (node.imported.name === 'unimplementedTmpl') {
+ node.imported.name = 'unimplementedTmplNoYield';
}
},
};
diff --git a/packages/@lwc/ssr-runtime/src/index.ts b/packages/@lwc/ssr-runtime/src/index.ts
index cfa9b8dd53..51416bd6af 100644
--- a/packages/@lwc/ssr-runtime/src/index.ts
+++ b/packages/@lwc/ssr-runtime/src/index.ts
@@ -28,6 +28,8 @@ export {
serverSideRenderComponent,
// renderComponent is an alias for serverSideRenderComponent
serverSideRenderComponent as renderComponent,
+ unimplementedTmpl,
+ unimplementedTmplNoYield,
} from './render';
export { normalizeTextContent, renderTextContent } from './render-text-content';
export { hasScopedStaticStylesheets, renderStylesheets } from './styles';
diff --git a/packages/@lwc/ssr-runtime/src/render.ts b/packages/@lwc/ssr-runtime/src/render.ts
index 4a635df8e2..6a8a03675a 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 ``;
@@ -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(``);
@@ -131,6 +131,49 @@ export function fallbackTmplNoYield(
}
}
+/**
+ * If a component is incorrectly implemented, and is missing a `generateMarkup` function,
+ * then use this template as a fallback so the world doesn't explode.
+ * @example export { Cmp as default }
+ */
+export function* unimplementedTmpl(
+ tagName: string,
+ instance: LightningElement,
+ shadowSlottedContent: AsyncGeneratorFunction,
+ Cmp?: LightningElementConstructor
+) {
+ yield `<${tagName}>`;
+ if (Cmp?.renderMode !== 'light') {
+ yield '';
+ if (shadowSlottedContent) {
+ yield shadowSlottedContent(instance);
+ }
+ }
+ yield `${tagName}>`;
+}
+
+/**
+ * If a component is incorrectly implemented, and is missing a `generateMarkup` function,
+ * then use this template as a fallback so the world doesn't explode.
+ * @example export { Cmp as default }
+ */
+export function unimplementedTmplNoYield(
+ emit: (segment: string) => void,
+ tagName: string,
+ instance: LightningElement,
+ shadowSlottedContent: AsyncGeneratorFunction,
+ Cmp?: LightningElementConstructor
+) {
+ emit(`<${tagName}>`);
+ if (Cmp?.renderMode !== 'light') {
+ emit('');
+ if (shadowSlottedContent) {
+ shadowSlottedContent(emit, instance);
+ }
+ }
+ emit(`${tagName}>`);
+}
+
export type GenerateMarkupFn = (
tagName: string,
props: Properties | null,
@@ -180,7 +223,7 @@ type GenerateMarkupFnVariants =
| GenerateMarkupFnAsyncNoGen
| GenerateMarkupFnSyncNoGen;
-interface ComponentWithGenerateMarkup {
+interface ComponentWithGenerateMarkup extends LightningElementConstructor {
[SYMBOL__GENERATE_MARKUP]: GenerateMarkupFnVariants;
}
@@ -201,6 +244,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,