Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ const bGenerateMarkup = esTemplate`
value: async function* generateMarkup(
tagName,
props,
attrs,
attrs,
parent,
scopeToken,
contextfulParent,
shadowSlottedContent,
lightSlottedContent,
scopedSlottedContent,
parent,
scopeToken,
contextfulParent
) {
tagName = tagName ?? ${/*component tag name*/ is.literal};
attrs = attrs ?? Object.create(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ const bYieldFromChildGenerator = esTemplateWithYield`
yield* generateMarkup(
tagName,
childProps,
childAttrs,
shadowSlottedContent,
lightSlottedContentMap,
scopedSlottedContentMap,
childAttrs,
instance,
scopeToken,
contextfulParent
contextfulParent,
shadowSlottedContent,
lightSlottedContentMap,
scopedSlottedContentMap
);
} else {
yield \`<\${tagName}>\`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ const bYieldFromDynamicComponentConstructorGenerator = esTemplateWithYield`
null,
childProps,
childAttrs,
shadowSlottedContent,
lightSlottedContentMap,
scopedSlottedContentMap,
instance,
scopeToken,
contextfulParent
contextfulParent,
shadowSlottedContent,
lightSlottedContentMap,
scopedSlottedContentMap
);
}
`<EsStatement[]>;
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/ssr-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export { mutationTracker } from './mutation-tracker';
export {
fallbackTmpl,
fallbackTmplNoYield,
GenerateMarkupFn,
GenerateMarkupAsyncYield,
renderAttrs,
renderAttrsNoYield,
serverSideRenderComponent,
Expand Down
3 changes: 3 additions & 0 deletions packages/@lwc/ssr-runtime/src/lightning-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const SYMBOL__DEFAULT_TEMPLATE = Symbol('default-template');
export class LightningElement implements PropsAvailableAtConstruction {
static renderMode?: 'light' | 'shadow';
static stylesheets?: Stylesheets;
static delegatesFocus?: boolean;
static formAssociated?: boolean;
static shadowSupportMode?: 'any' | 'reset' | 'native';

// Using ! because these are defined by descriptors in ./reflection
accessKey!: string;
Expand Down
144 changes: 76 additions & 68 deletions packages/@lwc/ssr-runtime/src/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand All @@ -14,9 +14,64 @@ import {
} from '@lwc/shared';
import { mutationTracker } from './mutation-tracker';
import { SYMBOL__GENERATE_MARKUP } from './lightning-element';
import type { CompilationMode } from '@lwc/shared';
import type { LightningElement, LightningElementConstructor } from './lightning-element';
import type { Attributes, Properties } from './types';

/** Parameters used by all `generateMarkup` variants that don't get transmogrified. */
type BaseGenerateMarkupParams = readonly [
tagName: string,
props: Properties | null,
attrs: Attributes | null,
// Not always null when invoked internally, but should always be
// null when invoked by ssr-runtime
parent: LightningElement | null,
scopeToken: string | null,
contextfulParent: LightningElement | null,
];

/** Text emitter used by transmogrified formats. */
type Emit = (str: string) => void;

/** Slotted content function used by `asyncYield` mode. */
type SlottedContentGenerator = (
instance: LightningElement
) => AsyncGenerator<string, void, unknown>;
/** Slotted content function used by `sync` and `async` modes. */
type SlottedContentEmitter = ($$emit: Emit, instance: LightningElement) => void;

/** Slotted content map used by `asyncYield` mode. */
type SlottedContentGeneratorMap = Record<number | string, SlottedContentGenerator[]>;
/** Slotted content map used by `sync` and `async` modes. */
type SlottedContentEmitterMap = Record<number | string, SlottedContentEmitter[]>;

/** `generateMarkup` parameters used by `asyncYield` mode. */
type GenerateMarkupGeneratorParams = readonly [
...BaseGenerateMarkupParams,
shadowSlottedContent: SlottedContentGenerator | null,
lightSlottedContent: SlottedContentGeneratorMap | null,
scopedSlottedContent: SlottedContentGeneratorMap | null,
];
/** `generateMarkup` parameters used by `sync` and `async` modes. */
type GenerateMarkupEmitterParams = readonly [
emit: Emit,
...BaseGenerateMarkupParams,
shadowSlottedContent: SlottedContentEmitter | null,
lightSlottedContent: SlottedContentEmitterMap | null,
scopedSlottedContent: SlottedContentEmitterMap | null,
];

/** Signature for `asyncYield` compilation mode. */
export type GenerateMarkupAsyncYield = (
...args: GenerateMarkupGeneratorParams
) => AsyncGenerator<string>;
/** Signature for `async` compilation mode. */
export type GenerateMarkupAsync = (...args: GenerateMarkupEmitterParams) => Promise<void>;
/** Signature for `sync` compilation mode. */
export type GenerateMarkupSync = (...args: GenerateMarkupEmitterParams) => void;

type GenerateMarkupVariants = GenerateMarkupAsyncYield | GenerateMarkupAsync | GenerateMarkupSync;

function renderAttrsPrivate(
instance: LightningElement,
attrs: Attributes,
Expand Down Expand Up @@ -100,29 +155,29 @@ export function renderAttrsNoYield(
emit(renderAttrsPrivate(instance, attrs, hostScopeToken, scopeToken));
}

export function* fallbackTmpl(
shadowSlottedContent: AsyncGeneratorFunction,
_lightSlottedContent: unknown,
_scopedSlottedContent: unknown,
export async function* fallbackTmpl(
shadowSlottedContent: SlottedContentGenerator | null,
_lightSlottedContent: SlottedContentGeneratorMap | null,
_scopedSlottedContent: SlottedContentGeneratorMap | null,
Cmp: LightningElementConstructor,
instance: LightningElement
) {
): AsyncGenerator<string> {
if (Cmp.renderMode !== 'light') {
yield `<template shadowrootmode="open"></template>`;
if (shadowSlottedContent) {
yield shadowSlottedContent(instance);
yield* shadowSlottedContent(instance);
}
}
}

export function fallbackTmplNoYield(
emit: (segment: string) => void,
shadowSlottedContent: AsyncGeneratorFunction | null,
_lightSlottedContent: unknown,
_scopedSlottedContent: unknown,
emit: Emit,
shadowSlottedContent: SlottedContentEmitter | null,
_lightSlottedContent: SlottedContentEmitterMap | null,
_scopedSlottedContent: SlottedContentEmitterMap | null,
Cmp: LightningElementConstructor,
instance: LightningElement | null
) {
instance: LightningElement
): void {
if (Cmp.renderMode !== 'light') {
emit(`<template shadowrootmode="open"></template>`);
if (shadowSlottedContent) {
Expand All @@ -131,64 +186,15 @@ export function fallbackTmplNoYield(
}
}

export type GenerateMarkupFn = (
tagName: string,
props: Properties | null,
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
// Not always null when invoked internally, but should always be
// null when invoked by ssr-runtime
parent: LightningElement | null,
scopeToken: string | null,
contextfulParent: LightningElement | null
) => AsyncGenerator<string>;

export type GenerateMarkupFnAsyncNoGen = (
emit: (segment: string) => void,
tagName: string,
props: Properties | null,
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
// Not always null when invoked internally, but should always be
// null when invoked by ssr-runtime
parent: LightningElement | null,
scopeToken: string | null,
contextfulParent: LightningElement | null
) => Promise<void>;

export type GenerateMarkupFnSyncNoGen = (
emit: (segment: string) => void,
tagName: string,
props: Properties | null,
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
// Not always null when invoked internally, but should always be
// null when invoked by ssr-runtime
parent: LightningElement | null,
scopeToken: string | null,
contextfulParent: LightningElement | null
) => void;

type GenerateMarkupFnVariants =
| GenerateMarkupFn
| GenerateMarkupFnAsyncNoGen
| GenerateMarkupFnSyncNoGen;

interface ComponentWithGenerateMarkup extends LightningElementConstructor {
[SYMBOL__GENERATE_MARKUP]?: GenerateMarkupFnVariants;
[SYMBOL__GENERATE_MARKUP]?: GenerateMarkupVariants;
}

export async function serverSideRenderComponent(
tagName: string,
Component: ComponentWithGenerateMarkup,
props: Properties = {},
mode: 'asyncYield' | 'async' | 'sync' = DEFAULT_SSR_MODE
mode: CompilationMode = DEFAULT_SSR_MODE
): Promise<string> {
if (typeof tagName !== 'string') {
throw new Error(`tagName must be a string, found: ${tagName}`);
Expand All @@ -204,13 +210,15 @@ export async function serverSideRenderComponent(
if (!generateMarkup) {
// If a non-component is accidentally provided, render an empty template
emit(`<${tagName}>`);
fallbackTmplNoYield(emit, null, null, null, Component, null);
// Using a false type assertion for the `instance` param is safe because it's only used
// if there's slotted content, which we are not providing
fallbackTmplNoYield(emit, null, null, null, Component, null as any);
emit(`</${tagName}>`);
return markup;
}

if (mode === 'asyncYield') {
for await (const segment of (generateMarkup as GenerateMarkupFn)(
for await (const segment of (generateMarkup as GenerateMarkupAsyncYield)(
tagName,
props,
null,
Expand All @@ -224,7 +232,7 @@ export async function serverSideRenderComponent(
markup += segment;
}
} else if (mode === 'async') {
await (generateMarkup as GenerateMarkupFnAsyncNoGen)(
await (generateMarkup as GenerateMarkupAsync)(
emit,
tagName,
props,
Expand All @@ -237,7 +245,7 @@ export async function serverSideRenderComponent(
null
);
} else if (mode === 'sync') {
(generateMarkup as GenerateMarkupFnSyncNoGen)(
(generateMarkup as GenerateMarkupSync)(
emit,
tagName,
props,
Expand Down