From cfb5f7d65d22c400516ff4b9553658511cb7d0d2 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Wed, 11 Jun 2025 11:16:33 -0600 Subject: [PATCH 01/10] feat: add context support to v2 --- .../src/framework/modules/context.ts | 3 +- packages/@lwc/shared/src/context.ts | 3 + packages/@lwc/ssr-runtime/src/context.ts | 68 +++++++++++++++++++ .../@lwc/ssr-runtime/src/lightning-element.ts | 10 ++- 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 packages/@lwc/ssr-runtime/src/context.ts diff --git a/packages/@lwc/engine-core/src/framework/modules/context.ts b/packages/@lwc/engine-core/src/framework/modules/context.ts index 4e406f2125..0218fcacc9 100644 --- a/packages/@lwc/engine-core/src/framework/modules/context.ts +++ b/packages/@lwc/engine-core/src/framework/modules/context.ts @@ -14,6 +14,7 @@ import { isTrustedContext, type ContextProvidedCallback, type ContextBinding as IContextBinding, + type ContextVarieties } from '@lwc/shared'; import { type VM } from '../vm'; import { logWarnOnce } from '../../shared/logger'; @@ -21,8 +22,6 @@ import type { Signal } from '@lwc/signals'; import type { RendererAPI } from '../renderer'; import type { ShouldContinueBubbling } from '../wiring/types'; -type ContextVarieties = Map>; - class ContextBinding implements IContextBinding { component: C; #renderer: RendererAPI; diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index a75ac4402c..1eba79ebbe 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -5,6 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isFalse } from './assert'; +import type { Signal } from '@lwc/signals'; export const ContextEventName = 'lightning:context-request'; @@ -17,6 +18,8 @@ export type ContextKeys = { export type ContextProvidedCallback = (contextSignal?: object) => void; +export type ContextVarieties = Map>; + export interface ContextBinding { component: C; diff --git a/packages/@lwc/ssr-runtime/src/context.ts b/packages/@lwc/ssr-runtime/src/context.ts new file mode 100644 index 0000000000..31f04bc0ce --- /dev/null +++ b/packages/@lwc/ssr-runtime/src/context.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, salesforce.com, 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 + */ +import { isTrustedContext, getContextKeys, isUndefined, keys } from "@lwc/shared"; +import { getContextfulStack } from "./wire"; +import { type LightningElement, SYMBOL__CONTEXT_VARIETIES } from "./lightning-element"; +import type { Signal } from "@lwc/signals"; +import type { ContextProvidedCallback, ContextBinding as IContextBinding } from '@lwc/shared'; +import type { Properties } from './types'; + +class ContextBinding implements IContextBinding { + component: C; + + constructor(component: C) { + this.component = component; + } + + provideContext( + contextVariety: V, + providedContextSignal: Signal + ): void { + const contextVarieties = this.component[SYMBOL__CONTEXT_VARIETIES]; + if (contextVarieties.has(contextVariety)) { + if (process.env.NODE_ENV !== 'production') { + throw new Error('Multiple contexts of the same variety were provided. Only the first context will be used.'); + } + return; + } + contextVarieties.set(contextVariety, providedContextSignal); + } + + consumeContext( + contextVariety: V, + contextProvidedCallback: ContextProvidedCallback + ): void { + const contextfulStack = getContextfulStack(this.component); + for (const ancestor of contextfulStack) { + // If the ancestor has the specified context variety, consume it and stop searching + const ancestorContextVarieties = ancestor[SYMBOL__CONTEXT_VARIETIES]; + if (ancestorContextVarieties.has(contextVariety)) { + contextProvidedCallback(ancestorContextVarieties.get(contextVariety)); + break; + } + } + } +} + +export function connectContext(le: LightningElement, props: Properties) { + const contextKeys = getContextKeys(); + if (isUndefined(contextKeys)) { + return; + } + const { connectContext } = contextKeys; + let hasTrustedContext = false; + for (const propName of keys(props)) { + const propValue = props[propName] as any; + if (isTrustedContext(propValue)) { + hasTrustedContext = true; + propValue[connectContext](new ContextBinding(le)); + } + } + if (hasTrustedContext) { + le[SYMBOL__CONTEXT_VARIETIES] = new Map(); + } +} \ No newline at end of file diff --git a/packages/@lwc/ssr-runtime/src/lightning-element.ts b/packages/@lwc/ssr-runtime/src/lightning-element.ts index 715c335632..12abae699f 100644 --- a/packages/@lwc/ssr-runtime/src/lightning-element.ts +++ b/packages/@lwc/ssr-runtime/src/lightning-element.ts @@ -29,8 +29,9 @@ import { ClassList } from './class-list'; import { mutationTracker } from './mutation-tracker'; import { descriptors as reflectionDescriptors } from './reflection'; import { getReadOnlyProxy } from './get-read-only-proxy'; +import { connectContext } from './context'; import type { Attributes, Properties } from './types'; -import type { Stylesheets } from '@lwc/shared'; +import type { Stylesheets, ContextVarieties } from '@lwc/shared'; type EventListenerOrEventListenerObject = unknown; type AddEventListenerOptions = unknown; @@ -46,6 +47,7 @@ interface PropsAvailableAtConstruction { export const SYMBOL__SET_INTERNALS = Symbol('set-internals'); export const SYMBOL__GENERATE_MARKUP = Symbol('generate-markup'); export const SYMBOL__DEFAULT_TEMPLATE = Symbol('default-template'); +export const SYMBOL__CONTEXT_VARIETIES = Symbol('context-varieties'); export class LightningElement implements PropsAvailableAtConstruction { static renderMode?: 'light' | 'shadow'; @@ -73,6 +75,7 @@ export class LightningElement implements PropsAvailableAtConstruction { #props!: Properties; #attrs!: Attributes; #classList: ClassList | null = null; + [SYMBOL__CONTEXT_VARIETIES]: ContextVarieties; constructor(propsAvailableAtConstruction: PropsAvailableAtConstruction & Properties) { assign(this, propsAvailableAtConstruction); @@ -81,6 +84,8 @@ export class LightningElement implements PropsAvailableAtConstruction { [SYMBOL__SET_INTERNALS](props: Properties, attrs: Attributes, publicProperties: Set) { this.#props = props; this.#attrs = attrs; + + connectContext(this, props); // Class should be set explicitly to avoid it being overridden by connectedCallback classList mutation. if (attrs.class) { @@ -96,12 +101,13 @@ export class LightningElement implements PropsAvailableAtConstruction { publicProperties.has(propName) || REFLECTIVE_GLOBAL_PROPERTY_SET.has(propName) || isAriaAttribute(attrName) - ) { + ) { // For props passed from parents to children, they are intended to be read-only // to avoid a child mutating its parent's state (this as any)[propName] = getReadOnlyProxy(props[propName]); } } + } get className() { From 719a6e9eb5ddab58cf90a5dc4eae98993ba29873 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 13 Jun 2025 08:48:39 -0600 Subject: [PATCH 02/10] feat: ssrv2 context --- .../src/framework/modules/context.ts | 3 +- .../src/__tests__/fixtures.spec.ts | 12 ++++ .../scripts/karma-plugins/hydration-tests.js | 6 -- .../test-hydration/context/x/main/main.js | 3 +- .../x/tooMuchContext/tooMuchContext.js | 6 +- packages/@lwc/shared/src/context.ts | 3 - .../src/__tests__/fixtures.spec.ts | 26 ++++++++- packages/@lwc/ssr-runtime/src/context.ts | 56 +++++++++++++------ packages/@lwc/ssr-runtime/src/index.ts | 5 ++ .../@lwc/ssr-runtime/src/lightning-element.ts | 16 ++++-- packages/@lwc/ssr-runtime/src/stubs.ts | 12 ---- 11 files changed, 97 insertions(+), 51 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/modules/context.ts b/packages/@lwc/engine-core/src/framework/modules/context.ts index 0218fcacc9..4e406f2125 100644 --- a/packages/@lwc/engine-core/src/framework/modules/context.ts +++ b/packages/@lwc/engine-core/src/framework/modules/context.ts @@ -14,7 +14,6 @@ import { isTrustedContext, type ContextProvidedCallback, type ContextBinding as IContextBinding, - type ContextVarieties } from '@lwc/shared'; import { type VM } from '../vm'; import { logWarnOnce } from '../../shared/logger'; @@ -22,6 +21,8 @@ import type { Signal } from '@lwc/signals'; import type { RendererAPI } from '../renderer'; import type { ShouldContinueBubbling } from '../wiring/types'; +type ContextVarieties = Map>; + class ContextBinding implements IContextBinding { component: C; #renderer: RendererAPI; diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts b/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts index 14a05b484a..6d996c321c 100755 --- a/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts @@ -10,6 +10,7 @@ import { vi, describe, beforeAll, afterAll } from 'vitest'; import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; import { testFixtureDir, formatHTML, pluginVirtual } from '@lwc/test-utils-lwc-internals'; +import { setContextKeys, setTrustedContextSet } from '@lwc/shared'; import { renderComponent, setFeatureFlagForTest } from '../index'; import type { LightningElementConstructor } from '@lwc/engine-core/dist/framework/base-lightning-element'; import type { RollupLwcOptions } from '@lwc/rollup-plugin'; @@ -179,10 +180,21 @@ describe.concurrent('fixtures', () => { // ENABLE_WIRE_SYNC_EMIT is used because this mimics the behavior for LWR in SSR mode. It's also more reasonable // for how both `engine-server` and `ssr-runtime` behave, which is to use sync rendering. setFeatureFlagForProductionTest('ENABLE_WIRE_SYNC_EMIT', true); + // Defining context keys and trusted context must be done once before related tests are ran. + const connectContext = Symbol('connectContext'); + const disconnectContext = Symbol('disconnectContext'); + const trustedContext = new WeakSet(); + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); + setContextKeys({ connectContext, disconnectContext }); + setTrustedContextSet(trustedContext); + (global as any).trustedContext = trustedContext; + (global as any).connectContext = connectContext; + (global as any).disconnectContext = disconnectContext; }); afterAll(() => { setFeatureFlagForProductionTest('ENABLE_WIRE_SYNC_EMIT', false); + setFeatureFlagForProductionTest('ENABLE_EXPERIMENTAL_SIGNALS', false); }); describe.concurrent('default', () => { diff --git a/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js b/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js index cfa37e6f95..f46fbee3a2 100644 --- a/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js +++ b/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js @@ -279,12 +279,6 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) { const location = path.relative(basePath, filePath); log.error('Error processing ā€œ%sā€\n\n%s\n', location, error.stack || error.message); done(error, null); - } finally { - if (requiredFeatureFlags) { - requiredFeatureFlags.forEach((featureFlag) => { - lwcSsr.setFeatureFlagForTest(featureFlag, false); - }); - } } }; } diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/main/main.js b/packages/@lwc/integration-karma/test-hydration/context/x/main/main.js index 4bfa922c53..0c63cfb133 100644 --- a/packages/@lwc/integration-karma/test-hydration/context/x/main/main.js +++ b/packages/@lwc/integration-karma/test-hydration/context/x/main/main.js @@ -2,7 +2,8 @@ import { LightningElement, api } from 'lwc'; import { defineMalformedContext } from 'x/contextManager'; export default class Root extends LightningElement { @api showTree = false; - malformedContext = defineMalformedContext()(); + // Only test in CSR right now as SSR throws which prevents content from being rendered. There is additional fixtures ssr coverage for this case. + malformedContext = typeof window !== 'undefined' ? defineMalformedContext()() : undefined; connectedCallback() { this.showTree = true; diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js b/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js index ca239597b6..513c0fb64a 100644 --- a/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js +++ b/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js @@ -3,5 +3,9 @@ import { LightningElement } from 'lwc'; export default class TooMuchContext extends LightningElement { context = grandparentContextFactory('grandparent provided value'); - tooMuch = grandparentContextFactory('this world is not big enough for me'); + // Only test in CSR right now as it throws in SSR. There is additional fixtures ssr coverage for this case. + tooMuch = + typeof window !== 'undefined' + ? grandparentContextFactory('this world is not big enough for me') + : undefined; } diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index 1eba79ebbe..a75ac4402c 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -5,7 +5,6 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { isFalse } from './assert'; -import type { Signal } from '@lwc/signals'; export const ContextEventName = 'lightning:context-request'; @@ -18,8 +17,6 @@ export type ContextKeys = { export type ContextProvidedCallback = (contextSignal?: object) => void; -export type ContextVarieties = Map>; - export interface ContextBinding { component: C; diff --git a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts index 8c39d7ba7c..e2edb4e107 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts @@ -6,12 +6,17 @@ */ import path from 'node:path'; -import { vi, describe } from 'vitest'; +import { vi, describe, beforeAll, afterAll } from 'vitest'; import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; import { testFixtureDir, formatHTML, pluginVirtual } from '@lwc/test-utils-lwc-internals'; -import { serverSideRenderComponent } from '@lwc/ssr-runtime'; -import { DEFAULT_SSR_MODE, type CompilationMode } from '@lwc/shared'; +import { serverSideRenderComponent, setFeatureFlagForTest } from '@lwc/ssr-runtime'; +import { + DEFAULT_SSR_MODE, + type CompilationMode, + setContextKeys, + setTrustedContextSet, +} from '@lwc/shared'; import { expectedFailures } from './utils/expected-failures'; import type { LightningElementConstructor } from '@lwc/ssr-runtime'; @@ -92,6 +97,21 @@ async function compileFixture({ entry, dirname }: { entry: string; dirname: stri } describe.concurrent('fixtures', () => { + beforeAll(() => { + // Defining context keys and trusted context must be done once before related tests are ran. + const connectContext = Symbol('connectContext'); + const disconnectContext = Symbol('disconnectContext'); + const trustedContext = new WeakSet(); + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); + setContextKeys({ connectContext, disconnectContext }); + setTrustedContextSet(trustedContext); + (global as any).trustedContext = trustedContext; + (global as any).connectContext = connectContext; + (global as any).disconnectContext = disconnectContext; + }); + afterAll(() => { + setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false); + }); testFixtureDir( { root: path.resolve(__dirname, '../../../engine-server/src/__tests__/fixtures'), diff --git a/packages/@lwc/ssr-runtime/src/context.ts b/packages/@lwc/ssr-runtime/src/context.ts index 31f04bc0ce..ac537aba97 100644 --- a/packages/@lwc/ssr-runtime/src/context.ts +++ b/packages/@lwc/ssr-runtime/src/context.ts @@ -4,12 +4,18 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import { isTrustedContext, getContextKeys, isUndefined, keys } from "@lwc/shared"; -import { getContextfulStack } from "./wire"; -import { type LightningElement, SYMBOL__CONTEXT_VARIETIES } from "./lightning-element"; -import type { Signal } from "@lwc/signals"; -import type { ContextProvidedCallback, ContextBinding as IContextBinding } from '@lwc/shared'; -import type { Properties } from './types'; +import { + type ContextProvidedCallback, + type ContextBinding as IContextBinding, + isTrustedContext, + getContextKeys, + isUndefined, + keys, + ArrayFilter, +} from '@lwc/shared'; +import { getContextfulStack } from './wire'; +import { type LightningElement, SYMBOL__CONTEXT_VARIETIES } from './lightning-element'; +import type { Signal } from '@lwc/signals'; class ContextBinding implements IContextBinding { component: C; @@ -25,7 +31,7 @@ class ContextBinding implements IContextBinding implements IContextBinding + isTrustedContext((le as any)[enumerableKey]) + ); + + if (contextfulKeys.length === 0) { + return; } - if (hasTrustedContext) { - le[SYMBOL__CONTEXT_VARIETIES] = new Map(); + + try { + for (let i = 0; i < contextfulKeys.length; i++) { + (le as any)[contextfulKeys[i]][connectContext](new ContextBinding(le)); + } + } catch (err: any) { + if (process.env.NODE_ENV !== 'production') { + throw new Error( + `Attempted to connect to trusted context but received the following error: ${ + err.message + }` + ); + } } -} \ No newline at end of file +} diff --git a/packages/@lwc/ssr-runtime/src/index.ts b/packages/@lwc/ssr-runtime/src/index.ts index c65c028725..7065b37cc9 100644 --- a/packages/@lwc/ssr-runtime/src/index.ts +++ b/packages/@lwc/ssr-runtime/src/index.ts @@ -14,8 +14,13 @@ export { sanitizeHtmlContent, normalizeClass, normalizeTabIndex, + setContextKeys, + setTrustedSignalSet, + setTrustedContextSet, } from '@lwc/shared'; +export { setFeatureFlag, setFeatureFlagForTest } from '@lwc/features'; + export { ClassList } from './class-list'; export { LightningElement, diff --git a/packages/@lwc/ssr-runtime/src/lightning-element.ts b/packages/@lwc/ssr-runtime/src/lightning-element.ts index 12abae699f..21a98e7208 100644 --- a/packages/@lwc/ssr-runtime/src/lightning-element.ts +++ b/packages/@lwc/ssr-runtime/src/lightning-element.ts @@ -31,12 +31,14 @@ import { descriptors as reflectionDescriptors } from './reflection'; import { getReadOnlyProxy } from './get-read-only-proxy'; import { connectContext } from './context'; import type { Attributes, Properties } from './types'; -import type { Stylesheets, ContextVarieties } from '@lwc/shared'; +import type { Stylesheets } from '@lwc/shared'; +import type { Signal } from '@lwc/signals'; type EventListenerOrEventListenerObject = unknown; type AddEventListenerOptions = unknown; type EventListenerOptions = unknown; type ShadowRoot = unknown; +type ContextVarieties = Map>; export type LightningElementConstructor = typeof LightningElement; @@ -75,7 +77,7 @@ export class LightningElement implements PropsAvailableAtConstruction { #props!: Properties; #attrs!: Attributes; #classList: ClassList | null = null; - [SYMBOL__CONTEXT_VARIETIES]: ContextVarieties; + [SYMBOL__CONTEXT_VARIETIES]: ContextVarieties = new Map(); constructor(propsAvailableAtConstruction: PropsAvailableAtConstruction & Properties) { assign(this, propsAvailableAtConstruction); @@ -84,8 +86,11 @@ export class LightningElement implements PropsAvailableAtConstruction { [SYMBOL__SET_INTERNALS](props: Properties, attrs: Attributes, publicProperties: Set) { this.#props = props; this.#attrs = attrs; - - connectContext(this, props); + + if (lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS) { + // Setup context before connected callback is executed + connectContext(this); + } // Class should be set explicitly to avoid it being overridden by connectedCallback classList mutation. if (attrs.class) { @@ -101,13 +106,12 @@ export class LightningElement implements PropsAvailableAtConstruction { publicProperties.has(propName) || REFLECTIVE_GLOBAL_PROPERTY_SET.has(propName) || isAriaAttribute(attrName) - ) { + ) { // For props passed from parents to children, they are intended to be read-only // to avoid a child mutating its parent's state (this as any)[propName] = getReadOnlyProxy(props[propName]); } } - } get className() { diff --git a/packages/@lwc/ssr-runtime/src/stubs.ts b/packages/@lwc/ssr-runtime/src/stubs.ts index 8bd4f1cc10..bea91f9fc1 100644 --- a/packages/@lwc/ssr-runtime/src/stubs.ts +++ b/packages/@lwc/ssr-runtime/src/stubs.ts @@ -42,12 +42,6 @@ export function registerTemplate(..._: unknown[]): never { export function sanitizeAttribute(..._: unknown[]): never { throw new Error('sanitizeAttribute cannot be used in SSR context.'); } -export function setFeatureFlag(..._: unknown[]): never { - throw new Error('setFeatureFlag cannot be used in SSR context.'); -} -export function setFeatureFlagForTest(..._: unknown[]): never { - throw new Error('setFeatureFlagForTest cannot be used in SSR context.'); -} export function swapComponent(..._: unknown[]): never { throw new Error('swapComponent cannot be used in SSR context.'); } @@ -66,12 +60,6 @@ export function unwrap(..._: unknown[]): never { export function wire(..._: unknown[]): never { throw new Error('@wire cannot be used in SSR context.'); } -export function setContextKeys(..._: unknown[]): never { - throw new Error('@setContextKeys cannot be used in SSR context.'); -} -export function setTrustedContextSet(..._: unknown[]): never { - throw new Error('setTrustedContextSet cannot be used in SSR context.'); -} export const renderer = { isSyntheticShadowDefined: false, From a807713101bcfd0e17439a70f07785cacf883e60 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 13 Jun 2025 08:57:51 -0600 Subject: [PATCH 03/10] feat: test coverage --- .../duplicate-context/config.json | 7 ++++ .../duplicate-context/error-ssr.txt | 1 + .../duplicate-context/error.txt | 0 .../duplicate-context/expected-ssr.html | 0 .../duplicate-context/expected.html | 10 ++++++ .../x/contextManager/contextManager.js | 33 +++++++++++++++++++ .../modules/x/main/main.html | 4 +++ .../duplicate-context/modules/x/main/main.js | 7 ++++ .../malformed-context/config.json | 7 ++++ .../malformed-context/error-ssr.txt | 1 + .../malformed-context/error.txt | 0 .../malformed-context/expected-ssr.html | 0 .../malformed-context/expected.html | 7 ++++ .../x/contextManager/contextManager.js | 10 ++++++ .../modules/x/main/main.html | 3 ++ .../malformed-context/modules/x/main/main.js | 5 +++ 16 files changed, 95 insertions(+) create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error.txt create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error.txt create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html create mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json new file mode 100644 index 0000000000..b420d69741 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json @@ -0,0 +1,7 @@ +{ + "entry": "x/main", + "ssrFiles": { + "error": "error-ssr.txt", + "expected": "expected-ssr.html" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt new file mode 100644 index 0000000000..bf0f1e04ca --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt @@ -0,0 +1 @@ +Attempted to connect to trusted context but received the following error: Multiple contexts of the same variety were provided. \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html new file mode 100644 index 0000000000..14e9e63af7 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js new file mode 100644 index 0000000000..fb7331bfab --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js @@ -0,0 +1,33 @@ +class MockContextSignal { + connectProvidedComponent; + disconnectProvidedComponent; + providedContextSignal; + + constructor(initialValue, contextDefinition, fromContext) { + this.value = initialValue; + this.contextDefinition = contextDefinition; + this.fromContext = fromContext; + trustedContext.add(this); + } + [connectContext](runtimeAdapter) { + this.connectProvidedComponent = runtimeAdapter.component; + + runtimeAdapter.provideContext(this.contextDefinition, this); + + if (this.fromContext) { + runtimeAdapter.consumeContext(this.fromContext, (providedContextSignal) => { + this.providedContextSignal = providedContextSignal; + this.value = providedContextSignal.value; + }); + } + } + [disconnectContext](component) { + this.disconnectProvidedComponent = component; + } +} + +export const defineContext = (fromContext) => { + const contextDefinition = (initialValue) => + new MockContextSignal(initialValue, contextDefinition, fromContext); + return contextDefinition; +}; diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html new file mode 100644 index 0000000000..907273e2df --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js new file mode 100644 index 0000000000..ade18c50c4 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; +import { defineContext } from 'x/contextManager'; +const contextFactory = defineContext(); +export default class Main extends LightningElement { + contextOne = contextFactory('context one'); + contextTwo = contextFactory('context two'); +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json new file mode 100644 index 0000000000..b420d69741 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json @@ -0,0 +1,7 @@ +{ + "entry": "x/main", + "ssrFiles": { + "error": "error-ssr.txt", + "expected": "expected-ssr.html" + } +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt new file mode 100644 index 0000000000..4128b709f4 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt @@ -0,0 +1 @@ +Attempted to connect to trusted context but received the following error: le[contextfulKeys[i]][connectContext2] is not a function \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html new file mode 100644 index 0000000000..ee9bdd1422 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js new file mode 100644 index 0000000000..505d0f351b --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js @@ -0,0 +1,10 @@ +// This is a malformed context signal that does not implement the connectContext or disconnectContext methods +class MockMalformedContextSignal { + constructor() { + trustedContext.add(this); + } +} + +export const defineMalformedContext = () => { + return () => new MockMalformedContextSignal(); +}; diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html new file mode 100644 index 0000000000..53d16c5328 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js new file mode 100644 index 0000000000..c93bc51b75 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; +import { defineMalformedContext } from 'x/contextManager'; +export default class Root extends LightningElement { + malformedContext = defineMalformedContext()(); +} From 53bc835db7d7bd5c0529bfc5b3d6e07392c67475 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 13 Jun 2025 20:42:04 -0600 Subject: [PATCH 04/10] fix: prod test run --- .../duplicate-context/error-ssr.txt | 1 - .../duplicate-context/expected-ssr.html | 10 ++++++++++ .../malformed-context/error-ssr.txt | 1 - .../malformed-context/expected-ssr.html | 7 +++++++ packages/@lwc/ssr-runtime/package.json | 1 + packages/@lwc/ssr-runtime/src/context.ts | 17 ++++++----------- 6 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt index bf0f1e04ca..e69de29bb2 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt @@ -1 +0,0 @@ -Attempted to connect to trusted context but received the following error: Multiple contexts of the same variety were provided. \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html index e69de29bb2..14e9e63af7 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt index 4128b709f4..e69de29bb2 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt @@ -1 +0,0 @@ -Attempted to connect to trusted context but received the following error: le[contextfulKeys[i]][connectContext2] is not a function \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html index e69de29bb2..ee9bdd1422 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/ssr-runtime/package.json b/packages/@lwc/ssr-runtime/package.json index 932ed7aba6..1faf6ac52b 100644 --- a/packages/@lwc/ssr-runtime/package.json +++ b/packages/@lwc/ssr-runtime/package.json @@ -50,6 +50,7 @@ "devDependencies": { "@lwc/shared": "8.20.0", "@lwc/engine-core": "8.20.0", + "@lwc/features": "8.20.0", "observable-membrane": "2.0.0" } } diff --git a/packages/@lwc/ssr-runtime/src/context.ts b/packages/@lwc/ssr-runtime/src/context.ts index ac537aba97..b766890c4d 100644 --- a/packages/@lwc/ssr-runtime/src/context.ts +++ b/packages/@lwc/ssr-runtime/src/context.ts @@ -30,10 +30,7 @@ class ContextBinding implements IContextBinding Date: Fri, 13 Jun 2025 20:42:34 -0600 Subject: [PATCH 05/10] fix: prod test run --- packages/@lwc/ssr-runtime/src/context.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/@lwc/ssr-runtime/src/context.ts b/packages/@lwc/ssr-runtime/src/context.ts index b766890c4d..ac537aba97 100644 --- a/packages/@lwc/ssr-runtime/src/context.ts +++ b/packages/@lwc/ssr-runtime/src/context.ts @@ -30,7 +30,10 @@ class ContextBinding implements IContextBinding Date: Fri, 13 Jun 2025 21:13:46 -0600 Subject: [PATCH 06/10] fix temp removal of fixtures coverage due to production failure --- .../src/__tests__/fixtures.spec.ts | 12 ------- .../duplicate-context/config.json | 7 ---- .../duplicate-context/error-ssr.txt | 0 .../duplicate-context/error.txt | 0 .../duplicate-context/expected-ssr.html | 10 ------ .../duplicate-context/expected.html | 10 ------ .../x/contextManager/contextManager.js | 33 ------------------- .../modules/x/main/main.html | 4 --- .../duplicate-context/modules/x/main/main.js | 7 ---- .../malformed-context/config.json | 7 ---- .../malformed-context/error-ssr.txt | 0 .../malformed-context/error.txt | 0 .../malformed-context/expected-ssr.html | 7 ---- .../malformed-context/expected.html | 7 ---- .../x/contextManager/contextManager.js | 10 ------ .../modules/x/main/main.html | 3 -- .../malformed-context/modules/x/main/main.js | 5 --- .../src/__tests__/fixtures.spec.ts | 21 ++---------- 18 files changed, 2 insertions(+), 141 deletions(-) delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error.txt delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error.txt delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html delete mode 100644 packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts b/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts index 6d996c321c..14a05b484a 100755 --- a/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts @@ -10,7 +10,6 @@ import { vi, describe, beforeAll, afterAll } from 'vitest'; import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; import { testFixtureDir, formatHTML, pluginVirtual } from '@lwc/test-utils-lwc-internals'; -import { setContextKeys, setTrustedContextSet } from '@lwc/shared'; import { renderComponent, setFeatureFlagForTest } from '../index'; import type { LightningElementConstructor } from '@lwc/engine-core/dist/framework/base-lightning-element'; import type { RollupLwcOptions } from '@lwc/rollup-plugin'; @@ -180,21 +179,10 @@ describe.concurrent('fixtures', () => { // ENABLE_WIRE_SYNC_EMIT is used because this mimics the behavior for LWR in SSR mode. It's also more reasonable // for how both `engine-server` and `ssr-runtime` behave, which is to use sync rendering. setFeatureFlagForProductionTest('ENABLE_WIRE_SYNC_EMIT', true); - // Defining context keys and trusted context must be done once before related tests are ran. - const connectContext = Symbol('connectContext'); - const disconnectContext = Symbol('disconnectContext'); - const trustedContext = new WeakSet(); - setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); - setContextKeys({ connectContext, disconnectContext }); - setTrustedContextSet(trustedContext); - (global as any).trustedContext = trustedContext; - (global as any).connectContext = connectContext; - (global as any).disconnectContext = disconnectContext; }); afterAll(() => { setFeatureFlagForProductionTest('ENABLE_WIRE_SYNC_EMIT', false); - setFeatureFlagForProductionTest('ENABLE_EXPERIMENTAL_SIGNALS', false); }); describe.concurrent('default', () => { diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json deleted file mode 100644 index b420d69741..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "entry": "x/main", - "ssrFiles": { - "error": "error-ssr.txt", - "expected": "expected-ssr.html" - } -} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error-ssr.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/error.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html deleted file mode 100644 index 14e9e63af7..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected-ssr.html +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html deleted file mode 100644 index 14e9e63af7..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/expected.html +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js deleted file mode 100644 index fb7331bfab..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/contextManager/contextManager.js +++ /dev/null @@ -1,33 +0,0 @@ -class MockContextSignal { - connectProvidedComponent; - disconnectProvidedComponent; - providedContextSignal; - - constructor(initialValue, contextDefinition, fromContext) { - this.value = initialValue; - this.contextDefinition = contextDefinition; - this.fromContext = fromContext; - trustedContext.add(this); - } - [connectContext](runtimeAdapter) { - this.connectProvidedComponent = runtimeAdapter.component; - - runtimeAdapter.provideContext(this.contextDefinition, this); - - if (this.fromContext) { - runtimeAdapter.consumeContext(this.fromContext, (providedContextSignal) => { - this.providedContextSignal = providedContextSignal; - this.value = providedContextSignal.value; - }); - } - } - [disconnectContext](component) { - this.disconnectProvidedComponent = component; - } -} - -export const defineContext = (fromContext) => { - const contextDefinition = (initialValue) => - new MockContextSignal(initialValue, contextDefinition, fromContext); - return contextDefinition; -}; diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html deleted file mode 100644 index 907273e2df..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.html +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js deleted file mode 100644 index ade18c50c4..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/duplicate-context/modules/x/main/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import { LightningElement } from 'lwc'; -import { defineContext } from 'x/contextManager'; -const contextFactory = defineContext(); -export default class Main extends LightningElement { - contextOne = contextFactory('context one'); - contextTwo = contextFactory('context two'); -} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json deleted file mode 100644 index b420d69741..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "entry": "x/main", - "ssrFiles": { - "error": "error-ssr.txt", - "expected": "expected-ssr.html" - } -} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error-ssr.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/error.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html deleted file mode 100644 index ee9bdd1422..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected-ssr.html +++ /dev/null @@ -1,7 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html deleted file mode 100644 index ee9bdd1422..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/expected.html +++ /dev/null @@ -1,7 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js deleted file mode 100644 index 505d0f351b..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/contextManager/contextManager.js +++ /dev/null @@ -1,10 +0,0 @@ -// This is a malformed context signal that does not implement the connectContext or disconnectContext methods -class MockMalformedContextSignal { - constructor() { - trustedContext.add(this); - } -} - -export const defineMalformedContext = () => { - return () => new MockMalformedContextSignal(); -}; diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html deleted file mode 100644 index 53d16c5328..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js b/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js deleted file mode 100644 index c93bc51b75..0000000000 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/context-binding/malformed-context/modules/x/main/main.js +++ /dev/null @@ -1,5 +0,0 @@ -import { LightningElement } from 'lwc'; -import { defineMalformedContext } from 'x/contextManager'; -export default class Root extends LightningElement { - malformedContext = defineMalformedContext()(); -} diff --git a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts index e2edb4e107..9ebe44d79e 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts @@ -6,16 +6,14 @@ */ import path from 'node:path'; -import { vi, describe, beforeAll, afterAll } from 'vitest'; +import { vi, describe } from 'vitest'; import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; import { testFixtureDir, formatHTML, pluginVirtual } from '@lwc/test-utils-lwc-internals'; -import { serverSideRenderComponent, setFeatureFlagForTest } from '@lwc/ssr-runtime'; +import { serverSideRenderComponent } from '@lwc/ssr-runtime'; import { DEFAULT_SSR_MODE, type CompilationMode, - setContextKeys, - setTrustedContextSet, } from '@lwc/shared'; import { expectedFailures } from './utils/expected-failures'; import type { LightningElementConstructor } from '@lwc/ssr-runtime'; @@ -97,21 +95,6 @@ async function compileFixture({ entry, dirname }: { entry: string; dirname: stri } describe.concurrent('fixtures', () => { - beforeAll(() => { - // Defining context keys and trusted context must be done once before related tests are ran. - const connectContext = Symbol('connectContext'); - const disconnectContext = Symbol('disconnectContext'); - const trustedContext = new WeakSet(); - setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', true); - setContextKeys({ connectContext, disconnectContext }); - setTrustedContextSet(trustedContext); - (global as any).trustedContext = trustedContext; - (global as any).connectContext = connectContext; - (global as any).disconnectContext = disconnectContext; - }); - afterAll(() => { - setFeatureFlagForTest('ENABLE_EXPERIMENTAL_SIGNALS', false); - }); testFixtureDir( { root: path.resolve(__dirname, '../../../engine-server/src/__tests__/fixtures'), From 7385cc20b77bee5ad66f0ca3c30842e9561b929b Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 13 Jun 2025 21:17:38 -0600 Subject: [PATCH 07/10] fix: prettier --- packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts index 9ebe44d79e..8c39d7ba7c 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/fixtures.spec.ts @@ -11,10 +11,7 @@ import { rollup } from 'rollup'; import lwcRollupPlugin from '@lwc/rollup-plugin'; import { testFixtureDir, formatHTML, pluginVirtual } from '@lwc/test-utils-lwc-internals'; import { serverSideRenderComponent } from '@lwc/ssr-runtime'; -import { - DEFAULT_SSR_MODE, - type CompilationMode, -} from '@lwc/shared'; +import { DEFAULT_SSR_MODE, type CompilationMode } from '@lwc/shared'; import { expectedFailures } from './utils/expected-failures'; import type { LightningElementConstructor } from '@lwc/ssr-runtime'; From af1465dcdb4e0178841611bdbb50ff302b4a9853 Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 13 Jun 2025 21:45:28 -0600 Subject: [PATCH 08/10] fix: add vitest coverage --- packages/@lwc/ssr-runtime/src/context.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@lwc/ssr-runtime/src/context.ts b/packages/@lwc/ssr-runtime/src/context.ts index ac537aba97..280b2053ca 100644 --- a/packages/@lwc/ssr-runtime/src/context.ts +++ b/packages/@lwc/ssr-runtime/src/context.ts @@ -54,6 +54,8 @@ class ContextBinding implements IContextBinding Date: Fri, 13 Jun 2025 21:46:09 -0600 Subject: [PATCH 09/10] fix: add vitest coverage --- .../ssr-runtime/src/__tests__/context.spec.ts | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 packages/@lwc/ssr-runtime/src/__tests__/context.spec.ts diff --git a/packages/@lwc/ssr-runtime/src/__tests__/context.spec.ts b/packages/@lwc/ssr-runtime/src/__tests__/context.spec.ts new file mode 100644 index 0000000000..a394d5fd01 --- /dev/null +++ b/packages/@lwc/ssr-runtime/src/__tests__/context.spec.ts @@ -0,0 +1,174 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import * as shared from '@lwc/shared'; +import { connectContext, ContextBinding } from '../context'; +import { LightningElement, SYMBOL__CONTEXT_VARIETIES } from '../lightning-element'; +import { getContextfulStack } from '../wire'; +import type { Signal } from '@lwc/signals'; +import type { ContextProvidedCallback } from '@lwc/shared'; + +// Mock the wire module +vi.mock('../wire', () => ({ + getContextfulStack: vi.fn(), +})); + +// Mock @lwc/shared +vi.mock('@lwc/shared', () => ({ + isTrustedContext: vi.fn(), + getContextKeys: vi.fn(), + isUndefined: vi.fn(), + keys: vi.fn(), + entries: vi.fn().mockReturnValue([]), + AriaAttrNameToPropNameMap: {}, + defineProperties: vi.fn(), + assign: vi.fn(), + ArrayFilter: { + call: vi.fn(), + }, +})); + +describe('context', () => { + let mockContextKeys: { connectContext: string }; + let mockContextfulStack: LightningElement[]; + let mockContextVariety: object; + let mockContextSignal: Signal; + let mockContextProvidedCallback: ContextProvidedCallback; + + beforeEach(() => { + // Reset mocks + vi.clearAllMocks(); + + // Setup mock context keys + mockContextKeys = { + connectContext: 'connectContext', + }; + + // Setup mock contextful stack + mockContextfulStack = [new LightningElement({ tagName: 'div' })]; + (getContextfulStack as any).mockReturnValue(mockContextfulStack); + + // Setup mock context variety and signal + mockContextVariety = {}; + mockContextSignal = { value: undefined } as Signal; + mockContextProvidedCallback = vi.fn(); + + // Mock global context keys + (global as any).__LWC_CONTEXT_KEYS__ = mockContextKeys; + + // Mock @lwc/shared functions + (shared.getContextKeys as any).mockReturnValue(mockContextKeys); + (shared.isUndefined as any).mockImplementation((val: unknown) => val === undefined); + (shared.keys as any).mockReturnValue(['contextful']); + (shared.isTrustedContext as any).mockReturnValue(true); + (shared.ArrayFilter.call as any).mockReturnValue(['contextful']); + }); + + describe('connectContext', () => { + it('should do nothing if context keys are undefined', () => { + (shared.getContextKeys as any).mockReturnValue(undefined); + const component = new LightningElement({ tagName: 'div' }); + connectContext(component); + expect(getContextfulStack).not.toHaveBeenCalled(); + }); + + it('should do nothing if no contextful keys are found', () => { + (shared.ArrayFilter.call as any).mockReturnValue([]); + const component = new LightningElement({ tagName: 'div' }); + connectContext(component); + expect(getContextfulStack).not.toHaveBeenCalled(); + }); + + it('should connect context when contextful keys are found', () => { + const component = new LightningElement({ tagName: 'div' }); + const mockContextful = { + [mockContextKeys.connectContext]: vi.fn(), + }; + Object.defineProperty(component, 'contextful', { + value: mockContextful, + configurable: true, + }); + + connectContext(component); + + expect(mockContextful[mockContextKeys.connectContext]).toHaveBeenCalled(); + }); + + it('should throw error in development when connecting context fails', () => { + const originalNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + + const component = new LightningElement({ tagName: 'div' }); + const mockContextful = { + [mockContextKeys.connectContext]: vi.fn().mockImplementation(() => { + throw new Error('Connection failed'); + }), + }; + Object.defineProperty(component, 'contextful', { + value: mockContextful, + configurable: true, + }); + + expect(() => connectContext(component)).toThrow( + 'Attempted to connect to trusted context' + ); + + process.env.NODE_ENV = originalNodeEnv; + }); + }); + + describe('ContextBinding', () => { + it('should provide context successfully', () => { + const component = new LightningElement({ tagName: 'div' }); + const binding = new ContextBinding(component); + + binding.provideContext(mockContextVariety, mockContextSignal); + + expect(component[SYMBOL__CONTEXT_VARIETIES].has(mockContextVariety)).toBe(true); + expect(component[SYMBOL__CONTEXT_VARIETIES].get(mockContextVariety)).toBe( + mockContextSignal + ); + }); + + it('should not allow multiple contexts of the same variety in development', () => { + const originalNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'development'; + + const component = new LightningElement({ tagName: 'div' }); + const binding = new ContextBinding(component); + + binding.provideContext(mockContextVariety, mockContextSignal); + expect(() => { + binding.provideContext(mockContextVariety, mockContextSignal); + }).toThrow('Multiple contexts of the same variety were provided'); + + process.env.NODE_ENV = originalNodeEnv; + }); + + it('should consume context from ancestor', () => { + const component = new LightningElement({ tagName: 'div' }); + const binding = new ContextBinding(component); + + // Setup ancestor with context + const ancestor = mockContextfulStack[0]; + ancestor[SYMBOL__CONTEXT_VARIETIES] = new Map([ + [mockContextVariety, mockContextSignal], + ]); + + binding.consumeContext(mockContextVariety, mockContextProvidedCallback); + + expect(mockContextProvidedCallback).toHaveBeenCalledWith(mockContextSignal); + }); + + it('should not call callback if context is not found in ancestors', () => { + const component = new LightningElement({ tagName: 'div' }); + const binding = new ContextBinding(component); + + // Setup empty ancestor + const ancestor = mockContextfulStack[0]; + ancestor[SYMBOL__CONTEXT_VARIETIES] = new Map(); + + binding.consumeContext(mockContextVariety, mockContextProvidedCallback); + + expect(mockContextProvidedCallback).not.toHaveBeenCalled(); + }); + }); +}); From 24fba78855914233dc22487a525984cc71c05b4b Mon Sep 17 00:00:00 2001 From: John Hefferman Date: Fri, 13 Jun 2025 21:59:18 -0600 Subject: [PATCH 10/10] fix: update ssr-runtime README with new functions --- packages/lwc/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/lwc/README.md b/packages/lwc/README.md index 2466dc4c1f..ae25056be1 100644 --- a/packages/lwc/README.md +++ b/packages/lwc/README.md @@ -79,3 +79,26 @@ import { renderComponent } from '@lwc/engine-server'; ## Experimental Packages The `@lwc/ssr-compiler` and `@lwc/ssr-runtime` packages are still considered experimental, and may break without notice. + +## Experimental APIs + +### setTrustedSignalSet() + +This experimental API enables the addition of a signal as a trusted signal. If the [ENABLE_EXPERIMENTAL_SIGNALS](https://github.com/salesforce/lwc/blob/master/packages/%40lwc/features/README.md#lwcfeatures) feature is enabled, any signal value change will trigger a re-render. + +If `setTrustedSignalSet` is called more than once, it will throw an error. If it is never called, then no trusted signal validation will be performed. The same `setTrustedSignalSet` API must be called on both `@lwc/engine-dom` and `@lwc/signals`. + +### setContextKeys + +Not intended for external use. Enables another library to establish contextful relationships via the LWC component tree. The `connectContext` and `disconnectContext` symbols that are provided are later used to identify methods that facilitate the establishment and dissolution of these contextful relationships. + +### setTrustedContextSet() + +Not intended for external use. This experimental API enables the addition of context as trusted context. If the [ENABLE_EXPERIMENTAL_SIGNALS](https://github.com/salesforce/lwc/blob/master/packages/%40lwc/features/README.md#lwcfeatures) feature is enabled. + +If `setTrustedContextSet` is called more than once, it will throw an error. If it is never called, then context will not be connected. + +### ContextBinding + +The context object's `connectContext` and `disconnectContext` methods are called with this object when contextful components are connected and disconnected. The ContextBinding exposes `provideContext` and `consumeContext`, +enabling the provision/consumption of a contextful Signal of a specified variety for the associated component.