Skip to content

Commit

Permalink
chore(ssr): update generateMarkup type defs (#5242)
Browse files Browse the repository at this point in the history
* chore(types): add missing static props SSR LightningElement

* refactor(ssr): move slotted content to last params

makes fancy typing easier

* refactor(types): rekerjigger `generateMarkup` type defs

* feat(ssr): implement light dom slotted content in fallback template

* Update packages/@lwc/ssr-runtime/src/index.ts

* chore: revert light DOM slots in fallback template

not behavior we want
  • Loading branch information
wjhsf authored Feb 27, 2025
1 parent 5a137ce commit 6eee0d1
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 82 deletions.
8 changes: 4 additions & 4 deletions packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts
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

0 comments on commit 6eee0d1

Please sign in to comment.