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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lwc-monorepo",
"version": "8.19.0",
"version": "8.19.1",
"private": true,
"description": "Lightning Web Components",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/aria-reflection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten."
],
"name": "@lwc/aria-reflection",
"version": "8.19.0",
"version": "8.19.1",
"description": "ARIA element reflection polyfill for strings",
"keywords": [
"aom",
Expand Down
6 changes: 3 additions & 3 deletions packages/@lwc/babel-plugin-component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten."
],
"name": "@lwc/babel-plugin-component",
"version": "8.19.0",
"version": "8.19.1",
"description": "Babel plugin to transform a LWC module",
"keywords": [
"lwc"
Expand Down Expand Up @@ -47,8 +47,8 @@
},
"dependencies": {
"@babel/helper-module-imports": "7.25.9",
"@lwc/errors": "8.19.0",
"@lwc/shared": "8.19.0",
"@lwc/errors": "8.19.1",
"@lwc/shared": "8.19.1",
"line-column": "~1.0.2"
},
"devDependencies": {
Expand Down
14 changes: 7 additions & 7 deletions packages/@lwc/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten."
],
"name": "@lwc/compiler",
"version": "8.19.0",
"version": "8.19.1",
"description": "LWC compiler",
"keywords": [
"lwc"
Expand Down Expand Up @@ -52,11 +52,11 @@
"@babel/plugin-transform-class-properties": "7.25.9",
"@babel/plugin-transform-object-rest-spread": "7.25.9",
"@locker/babel-plugin-transform-unforgeables": "0.22.0",
"@lwc/babel-plugin-component": "8.19.0",
"@lwc/errors": "8.19.0",
"@lwc/shared": "8.19.0",
"@lwc/ssr-compiler": "8.19.0",
"@lwc/style-compiler": "8.19.0",
"@lwc/template-compiler": "8.19.0"
"@lwc/babel-plugin-component": "8.19.1",
"@lwc/errors": "8.19.1",
"@lwc/shared": "8.19.1",
"@lwc/ssr-compiler": "8.19.1",
"@lwc/style-compiler": "8.19.1",
"@lwc/template-compiler": "8.19.1"
}
}
15 changes: 15 additions & 0 deletions packages/@lwc/engine-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,18 @@ This experimental API enables the removal of an object's observable membrane pro
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.
8 changes: 4 additions & 4 deletions packages/@lwc/engine-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten."
],
"name": "@lwc/engine-core",
"version": "8.19.0",
"version": "8.19.1",
"description": "Core LWC engine APIs.",
"keywords": [
"lwc"
Expand Down Expand Up @@ -46,9 +46,9 @@
}
},
"dependencies": {
"@lwc/features": "8.19.0",
"@lwc/shared": "8.19.0",
"@lwc/signals": "8.19.0"
"@lwc/features": "8.19.1",
"@lwc/shared": "8.19.1",
"@lwc/signals": "8.19.1"
},
"devDependencies": {
"observable-membrane": "2.0.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/engine-core/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,5 @@ export { default as wire } from './decorators/wire';
export { readonly } from './readonly';

export { setFeatureFlag, setFeatureFlagForTest } from '@lwc/features';
export { setTrustedSignalSet } from '@lwc/shared';
export { setContextKeys, setTrustedSignalSet, setTrustedContextSet } from '@lwc/shared';
export type { Stylesheet, Stylesheets } from '@lwc/shared';
149 changes: 149 additions & 0 deletions packages/@lwc/engine-core/src/framework/modules/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright (c) 2025, 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 {
isUndefined,
getPrototypeOf,
keys,
getContextKeys,
ArrayFilter,
ContextEventName,
isTrustedContext,
type ContextProvidedCallback,
type ContextBinding as IContextBinding,
} from '@lwc/shared';
import { type VM } from '../vm';
import { logWarnOnce } from '../../shared/logger';
import type { Signal } from '@lwc/signals';
import type { RendererAPI } from '../renderer';
import type { ShouldContinueBubbling } from '../wiring/types';

type ContextVarieties = Map<unknown, Signal<unknown>>;

class ContextBinding<C extends object> implements IContextBinding<C> {
component: C;
#renderer: RendererAPI;
#providedContextVarieties: ContextVarieties;
#elm: HTMLElement;

constructor(vm: VM, component: C, providedContextVarieties: ContextVarieties) {
this.component = component;
this.#renderer = vm.renderer;
this.#elm = vm.elm;
this.#providedContextVarieties = providedContextVarieties;

// Register the component as a context provider.
this.#renderer.registerContextProvider(
this.#elm,
ContextEventName,
(contextConsumer): ShouldContinueBubbling => {
// This callback is invoked when the provided context is consumed somewhere down
// in the component's subtree.
return contextConsumer.setNewContext(this.#providedContextVarieties);
}
);
}

provideContext<V extends object>(
contextVariety: V,
providedContextSignal: Signal<unknown>
): void {
if (this.#providedContextVarieties.has(contextVariety)) {
logWarnOnce(
'Multiple contexts of the same variety were provided. Only the first context will be used.'
);
return;
}
this.#providedContextVarieties.set(contextVariety, providedContextSignal);
}

consumeContext<V extends object>(
contextVariety: V,
contextProvidedCallback: ContextProvidedCallback
): void {
this.#renderer.registerContextConsumer(this.#elm, ContextEventName, {
setNewContext: (providerContextVarieties: ContextVarieties): ShouldContinueBubbling => {
// If the provider has the specified context variety, then it is consumed
// and true is returned to stop bubbling.
if (providerContextVarieties.has(contextVariety)) {
contextProvidedCallback(providerContextVarieties.get(contextVariety));
return true;
}
// Return false as context has not been found/consumed
// and the consumer should continue traversing the context tree
return false;
},
});
}
}

export function connectContext(vm: VM) {
const contextKeys = getContextKeys();

if (isUndefined(contextKeys)) {
return;
}

const { connectContext } = contextKeys;
const { component } = vm;

const enumerableKeys = keys(getPrototypeOf(component));
const contextfulKeys = ArrayFilter.call(enumerableKeys, (enumerableKey) =>
isTrustedContext((component as any)[enumerableKey])
);

if (contextfulKeys.length === 0) {
return;
}

const providedContextVarieties: ContextVarieties = new Map();

try {
for (let i = 0; i < contextfulKeys.length; i++) {
(component as any)[contextfulKeys[i]][connectContext](
new ContextBinding(vm, component, providedContextVarieties)
);
}
} catch (err: any) {
logWarnOnce(
`Attempted to connect to trusted context but received the following error: ${
err.message
}`
);
}
}

export function disconnectContext(vm: VM) {
const contextKeys = getContextKeys();

if (!contextKeys) {
return;
}

const { disconnectContext } = contextKeys;
const { component } = vm;

const enumerableKeys = keys(getPrototypeOf(component));
const contextfulKeys = ArrayFilter.call(enumerableKeys, (enumerableKey) =>
isTrustedContext((component as any)[enumerableKey])
);

if (contextfulKeys.length === 0) {
return;
}

try {
for (let i = 0; i < contextfulKeys.length; i++) {
(component as any)[contextfulKeys[i]][disconnectContext](component);
}
} catch (err: any) {
logWarnOnce(
`Attempted to disconnect from trusted context but received the following error: ${
err.message
}`
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function componentValueObserved(vm: VM, key: PropertyKey, target: any = {
isObject(target) &&
!isNull(target) &&
isTrustedSignal(target) &&
process.env.IS_BROWSER &&
// Only subscribe if a template is being rendered by the engine
tro.isObserving()
) {
Expand Down
7 changes: 6 additions & 1 deletion packages/@lwc/engine-core/src/framework/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import type { WireContextSubscriptionPayload } from './wiring';
import type { WireContextSubscriptionCallback, WireContextSubscriptionPayload } from './wiring';

export type HostNode = any;
export type HostElement = any;
Expand Down Expand Up @@ -76,6 +76,11 @@ export interface RendererAPI {
) => E;
defineCustomElement: (tagName: string, isFormAssociated: boolean) => void;
ownerDocument(elm: E): Document;
registerContextProvider: (
element: E,
adapterContextToken: string,
onContextSubscription: WireContextSubscriptionCallback
) => void;
registerContextConsumer: (
element: E,
adapterContextToken: string,
Expand Down
12 changes: 12 additions & 0 deletions packages/@lwc/engine-core/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { flushMutationLogsForVM, getAndFlushMutationLogs } from './mutation-logg
import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring';
import { VNodeType, isVFragment } from './vnodes';
import { isReportingEnabled, report, ReportingEventId } from './reporting';
import { connectContext, disconnectContext } from './modules/context';
import type { VNodes, VCustomElement, VNode, VBaseElement, VStaticPartElement } from './vnodes';
import type { ReactiveObserver } from './mutation-tracker';
import type {
Expand Down Expand Up @@ -702,6 +703,12 @@ export function runConnectedCallback(vm: VM) {
if (hasWireAdapters(vm)) {
connectWireAdapters(vm);
}

if (lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS) {
// Setup context before connected callback is executed
connectContext(vm);
}

const { connectedCallback } = vm.def;
if (!isUndefined(connectedCallback)) {
logOperationStart(OperationId.ConnectedCallback, vm);
Expand Down Expand Up @@ -751,6 +758,11 @@ function runDisconnectedCallback(vm: VM) {
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(vm.state !== VMState.disconnected, `${vm} must be inserted.`);
}

if (lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS) {
disconnectContext(vm);
}

if (isFalse(vm.isDirty)) {
// this guarantees that if the component is reused/reinserted,
// it will be re-rendered because we are disconnecting the reactivity
Expand Down
8 changes: 7 additions & 1 deletion packages/@lwc/engine-core/src/framework/wiring/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ export function createContextProviderWithRegister(
consumerDisconnectedCallback(consumer);
}
};
setDisconnectedCallback(disconnectCallback);
setDisconnectedCallback?.(disconnectCallback);

consumerConnectedCallback(consumer);
// Return true as the context is always consumed here and the consumer should
// stop bubbling.
return true;
}
);
};
Expand Down Expand Up @@ -91,6 +94,9 @@ export function createContextWatcher(
// eslint-disable-next-line @lwc/lwc-internal/no-invalid-todo
// TODO: dev-mode validation of config based on the adapter.contextSchema
callbackWhenContextIsReady(newContext);
// Return true as the context is always consumed here and the consumer should
// stop bubbling.
return true;
},
setDisconnectedCallback(disconnectCallback: () => void) {
// adds this callback into the disconnect bucket so it gets disconnected from parent
Expand Down
8 changes: 5 additions & 3 deletions packages/@lwc/engine-core/src/framework/wiring/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ export interface WireDebugInfo {
wasDataProvisionedForConfig: boolean;
}

export type ShouldContinueBubbling = boolean;

export type WireContextSubscriptionCallback = (
subscriptionPayload: WireContextSubscriptionPayload
) => void;
) => ShouldContinueBubbling;

export interface WireContextSubscriptionPayload {
setNewContext(newContext: ContextValue): void;
setDisconnectedCallback(disconnectCallback: () => void): void;
setNewContext(newContext: ContextValue): ShouldContinueBubbling;
setDisconnectedCallback?(disconnectCallback: () => void): void;
}

export interface ContextConsumer {
Expand Down
4 changes: 4 additions & 0 deletions packages/@lwc/engine-core/src/shared/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export function logError(message: string, vm?: VM) {
log('error', message, vm, false);
}

export function logErrorOnce(message: string, vm?: VM) {
log('error', message, vm, true);
}

export function logWarn(message: string, vm?: VM) {
log('warn', message, vm, false);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/@lwc/engine-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten."
],
"name": "@lwc/engine-dom",
"version": "8.19.0",
"version": "8.19.1",
"description": "Renders LWC components in a DOM environment.",
"keywords": [
"lwc"
Expand Down Expand Up @@ -46,9 +46,9 @@
}
},
"devDependencies": {
"@lwc/engine-core": "8.19.0",
"@lwc/shared": "8.19.0",
"@lwc/features": "8.19.0"
"@lwc/engine-core": "8.19.1",
"@lwc/shared": "8.19.1",
"@lwc/features": "8.19.1"
},
"lwc": {
"modules": [
Expand Down
Loading
Loading