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,