Skip to content

Commit

Permalink
fix(ssr): make context providers work again (#5004)
Browse files Browse the repository at this point in the history
Co-authored-by: John Hefferman <[email protected]>
Co-authored-by: Nolan Lawson <[email protected]>
  • Loading branch information
3 people authored Dec 4, 2024
1 parent 6b40dc6 commit 7701069
Show file tree
Hide file tree
Showing 38 changed files with 234 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<x-root>
<x-provider>
<!---->
<x-consumer>
<div>
some context
</div>
</x-consumer>
<!---->
</x-provider>
</x-root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template lwc:render-mode="light">
<div>{foo}</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LightningElement, wire } from 'lwc';
import { WireAdapter } from '../../../wire-adapter';

export default class SlottedConsumerComponent extends LightningElement {
static renderMode = 'light';
@wire(WireAdapter) foo;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template lwc:render-mode="light">
<slot></slot>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { LightningElement } from 'lwc';
import { contextualizer } from '../../../wire-adapter';

export default class ProviderComponent extends LightningElement {
static renderMode = 'light';
connectedCallback() {
contextualizer(this, {
consumerConnectedCallback(consumer) {
consumer.provide({
value: 'some context',
});
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template lwc:render-mode="light">
<x-provider>
<x-consumer></x-consumer>
</x-provider>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement } from 'lwc';

export default class Rootcomponent extends LightningElement {
static renderMode = 'light';
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<x-root>
<x-provider>
<!---->
<!---->
<x-consumer>
<span>
some context
</span>
</x-consumer>
<span>
id - name
</span>
<!---->
<!---->
</x-provider>
</x-root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const tagName = 'x-root';
export { default } from 'x/root';
export * from 'x/root';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template lwc:render-mode="light">
<span>{foo}</span>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LightningElement, wire } from 'lwc';
import { WireAdapter } from '../../../wire-adapter';

export default class SlottedConsumerComponent extends LightningElement {
static renderMode = 'light';
@wire(WireAdapter) foo;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template lwc:render-mode="light">
<slot lwc:slot-bind={item}></slot>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LightningElement } from 'lwc';
import { contextualizer } from '../../../wire-adapter';

export default class ProviderComponent extends LightningElement {
static renderMode = 'light';
item = {
id: 'id',
name: 'name',
};
connectedCallback() {
contextualizer(this, {
consumerConnectedCallback(consumer) {
consumer.provide({
value: 'some context',
});
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template lwc:render-mode="light">
<x-provider>
<template lwc:slot-data="data">
<x-consumer></x-consumer>
<span>{data.id} - {data.name}</span>
</template>
</x-provider>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LightningElement } from 'lwc';

export default class Rootcomponent extends LightningElement {
static renderMode = 'light';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createContextProvider } from 'lwc';

export class WireAdapter {
contextValue = { value: 'missing' };
static contextSchema = { value: 'required' };

constructor(callback) {
this._callback = callback;
}

connect() {
// noop
}

disconnect() {
// noop
}

update(_config, context) {
if (context) {
if (!context.hasOwnProperty('value')) {
throw new Error(`Invalid context provided`);
}
this.contextValue = context.value;
this._callback(this.contextValue);
}
}
}

export const contextualizer = createContextProvider(WireAdapter);
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const tagName = 'x-root';
export { default } from 'x/root';
export * from 'x/root';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createContextProvider } from 'lwc';

export class WireAdapter {
contextValue = { value: 'missing' };
static contextSchema = { value: 'required' };

constructor(callback) {
this._callback = callback;
}

connect() {
// noop
}

disconnect() {
// noop
}

update(_config, context) {
if (context) {
if (!context.hasOwnProperty('value')) {
throw new Error(`Invalid context provided`);
}
this.contextValue = context.value;
this._callback(this.contextValue);
}
}
}

export const contextualizer = createContextProvider(WireAdapter);
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export const expectedFailures = new Set([
'attribute-namespace/index.js',
'attribute-style/basic/index.js',
'attribute-style/dynamic/index.js',
'context-slotted/index.js',
'exports/component-as-default/index.js',
'known-boolean-attributes/default-def-html-attributes/static-on-component/index.js',
'render-dynamic-value/index.js',
Expand Down
5 changes: 3 additions & 2 deletions packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const bGenerateMarkup = esTemplate`
shadowSlottedContent,
lightSlottedContent,
parent,
scopeToken
scopeToken,
contextfulParent
) {
tagName = tagName ?? ${/*component tag name*/ is.literal};
attrs = attrs ?? Object.create(null);
Expand All @@ -51,7 +52,7 @@ const bGenerateMarkup = esTemplate`
tagName: tagName.toUpperCase(),
});
__establishContextfulRelationship(parent, instance);
__establishContextfulRelationship(contextfulParent, instance);
${/*connect wire*/ is.statement}
instance[__SYMBOL__SET_INTERNALS](props, attrs);
Expand Down
8 changes: 7 additions & 1 deletion packages/@lwc/ssr-compiler/src/compile-template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const bExportTemplate = esTemplate`
let textContentBuffer = '';
let didBufferTextContent = false;
// Establishes a contextual relationship between two components for ContextProviders.
// This variable will typically get overridden (shadowed) within slotted content.
const contextfulParent = instance;
const isLightDom = Cmp.renderMode === 'light';
if (!isLightDom) {
yield \`<template shadowrootmode="open"\${Cmp.delegatesFocus ? ' shadowrootdelegatesfocus' : ''}>\`
Expand All @@ -55,7 +59,9 @@ const bExportTemplate = esTemplate`
if (!isLightDom) {
yield '</template>';
if (shadowSlottedContent) {
yield* shadowSlottedContent();
// instance must be passed in; this is used to establish the contextful relationship
// between context provider (aka parent component) and context consumer (aka slotted content)
yield* shadowSlottedContent(contextfulParent);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const bYieldFromChildGenerator = esTemplateWithYield`
lightSlottedContentMap,
instance,
scopeToken,
contextfulParent
);
}
`<EsBlockStatement>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ const bYieldFromDynamicComponentConstructorGenerator = esTemplateWithYield`
childAttrs,
shadowSlottedContent,
lightSlottedContentMap,
scopeToken
instance,
scopeToken,
contextfulParent
);
}
`<EsStatement[]>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ import type { TransformerContext } from '../../types';

const bGenerateSlottedContent = esTemplateWithYield`
const shadowSlottedContent = ${/* hasShadowSlottedContent */ is.literal}
? async function* generateShadowSlottedContent() {
? async function* generateSlottedContent(contextfulParent) {
// The 'contextfulParent' variable is shadowed here so that a contextful relationship
// is established between components rendered in slotted content & the "parent"
// component that contains the <slot>.
${/* shadow slot content */ is.statement}
}
// Avoid creating the object unnecessarily
Expand Down Expand Up @@ -63,7 +67,7 @@ const bGenerateSlottedContent = esTemplateWithYield`
// it may be repeated multiple times in the same scope, because it's a function _expression_ rather
// than a function _declaration_, so it isn't available to be referenced anywhere.
const bAddLightContent = esTemplate`
addLightContent(${/* slot name */ is.expression} ?? "", async function* generateSlottedContent(${
addLightContent(${/* slot name */ is.expression} ?? "", async function* generateSlottedContent(contextfulParent, ${
/* scoped slot data variable */ isNullableOf(is.identifier)
}) {
// FIXME: make validation work again
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const bConditionalSlot = esTemplateWithYield`
const generators = lightSlottedContent?.[${/* slotName */ is.expression} ?? ""];
if (generators) {
for (const generator of generators) {
yield* generator(${/* scoped slot data */ isNullableOf(is.expression)});
yield* generator(contextfulParent, ${/* scoped slot data */ isNullableOf(is.expression)});
}
} else {
// If we're in this else block, then the generator _must_ have yielded
Expand Down
43 changes: 37 additions & 6 deletions packages/@lwc/ssr-runtime/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,25 +119,40 @@ export type GenerateMarkupFn = (
props: Properties | null,
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null
lightSlottedContent: 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: Record<string, any> | null,
props: Properties | null,
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null
lightSlottedContent: 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: Record<string, any> | null,
props: Properties | null,
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null
lightSlottedContent: 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 =
Expand Down Expand Up @@ -172,6 +187,9 @@ export async function serverSideRenderComponent(
props,
null,
null,
null,
null,
null,
null
)) {
markup += segment;
Expand All @@ -183,10 +201,23 @@ export async function serverSideRenderComponent(
props,
null,
null,
null,
null,
null,
null
);
} else if (mode === 'sync') {
(generateMarkup as GenerateMarkupFnSyncNoGen)(emit, tagName, props, null, null, null);
(generateMarkup as GenerateMarkupFnSyncNoGen)(
emit,
tagName,
props,
null,
null,
null,
null,
null,
null
);
} else {
throw new Error(`Invalid mode: ${mode}`);
}
Expand Down

0 comments on commit 7701069

Please sign in to comment.