diff --git a/package.json b/package.json index 0c7a19167e..b859d6fd0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lwc-monorepo", - "version": "8.19.0", + "version": "8.19.1", "private": true, "description": "Lightning Web Components", "repository": { diff --git a/packages/@lwc/aria-reflection/package.json b/packages/@lwc/aria-reflection/package.json index 3c24bfc2e7..43cd4d749d 100644 --- a/packages/@lwc/aria-reflection/package.json +++ b/packages/@lwc/aria-reflection/package.json @@ -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", diff --git a/packages/@lwc/babel-plugin-component/package.json b/packages/@lwc/babel-plugin-component/package.json index 79f54dbc9d..4d6a541986 100644 --- a/packages/@lwc/babel-plugin-component/package.json +++ b/packages/@lwc/babel-plugin-component/package.json @@ -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" @@ -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": { diff --git a/packages/@lwc/compiler/package.json b/packages/@lwc/compiler/package.json index 7680145151..ec12c4bf65 100644 --- a/packages/@lwc/compiler/package.json +++ b/packages/@lwc/compiler/package.json @@ -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" @@ -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" } } diff --git a/packages/@lwc/engine-core/README.md b/packages/@lwc/engine-core/README.md index 01c339bd3d..277fbc7439 100644 --- a/packages/@lwc/engine-core/README.md +++ b/packages/@lwc/engine-core/README.md @@ -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. diff --git a/packages/@lwc/engine-core/package.json b/packages/@lwc/engine-core/package.json index a4af3d8cb5..08fc6940ba 100644 --- a/packages/@lwc/engine-core/package.json +++ b/packages/@lwc/engine-core/package.json @@ -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" @@ -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" diff --git a/packages/@lwc/engine-core/src/framework/main.ts b/packages/@lwc/engine-core/src/framework/main.ts index fee61e3f23..3b2ccad6a0 100644 --- a/packages/@lwc/engine-core/src/framework/main.ts +++ b/packages/@lwc/engine-core/src/framework/main.ts @@ -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'; diff --git a/packages/@lwc/engine-core/src/framework/modules/context.ts b/packages/@lwc/engine-core/src/framework/modules/context.ts new file mode 100644 index 0000000000..4e406f2125 --- /dev/null +++ b/packages/@lwc/engine-core/src/framework/modules/context.ts @@ -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>; + +class ContextBinding implements IContextBinding { + 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( + contextVariety: V, + providedContextSignal: Signal + ): 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( + 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 + }` + ); + } +} diff --git a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts index bc00a6b822..3ed830fc64 100644 --- a/packages/@lwc/engine-core/src/framework/mutation-tracker.ts +++ b/packages/@lwc/engine-core/src/framework/mutation-tracker.ts @@ -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() ) { diff --git a/packages/@lwc/engine-core/src/framework/renderer.ts b/packages/@lwc/engine-core/src/framework/renderer.ts index 7bc18f7ac0..0c331e03af 100644 --- a/packages/@lwc/engine-core/src/framework/renderer.ts +++ b/packages/@lwc/engine-core/src/framework/renderer.ts @@ -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; @@ -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, diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 5112f39eb2..54db244bc1 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -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 { @@ -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); @@ -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 diff --git a/packages/@lwc/engine-core/src/framework/wiring/context.ts b/packages/@lwc/engine-core/src/framework/wiring/context.ts index 6e7c6d690a..6c3e50d4b8 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/context.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/context.ts @@ -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; } ); }; @@ -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 diff --git a/packages/@lwc/engine-core/src/framework/wiring/types.ts b/packages/@lwc/engine-core/src/framework/wiring/types.ts index 8eba2060d9..84af12b4e6 100644 --- a/packages/@lwc/engine-core/src/framework/wiring/types.ts +++ b/packages/@lwc/engine-core/src/framework/wiring/types.ts @@ -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 { diff --git a/packages/@lwc/engine-core/src/shared/logger.ts b/packages/@lwc/engine-core/src/shared/logger.ts index 75cbda3f67..738a052c6c 100644 --- a/packages/@lwc/engine-core/src/shared/logger.ts +++ b/packages/@lwc/engine-core/src/shared/logger.ts @@ -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); } diff --git a/packages/@lwc/engine-dom/package.json b/packages/@lwc/engine-dom/package.json index 34fc7695f2..6846fdb1a5 100644 --- a/packages/@lwc/engine-dom/package.json +++ b/packages/@lwc/engine-dom/package.json @@ -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" @@ -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": [ diff --git a/packages/@lwc/engine-dom/src/index.ts b/packages/@lwc/engine-dom/src/index.ts index 1add267202..d7a20625ba 100644 --- a/packages/@lwc/engine-dom/src/index.ts +++ b/packages/@lwc/engine-dom/src/index.ts @@ -30,7 +30,9 @@ export { isComponentConstructor, parseFragment, parseSVGFragment, + setContextKeys, setTrustedSignalSet, + setTrustedContextSet, swapComponent, swapStyle, swapTemplate, diff --git a/packages/@lwc/engine-dom/src/renderer/context.ts b/packages/@lwc/engine-dom/src/renderer/context.ts index 40151635e3..7e013db49f 100644 --- a/packages/@lwc/engine-dom/src/renderer/context.ts +++ b/packages/@lwc/engine-dom/src/renderer/context.ts @@ -16,8 +16,8 @@ import type { export class WireContextSubscriptionEvent extends CustomEvent { // These are initialized on the constructor via defineProperties. - public readonly setNewContext!: (newContext: WireContextValue) => void; - public readonly setDisconnectedCallback!: (disconnectCallback: () => void) => void; + public readonly setNewContext!: (newContext: WireContextValue) => boolean; + public readonly setDisconnectedCallback?: (disconnectCallback: () => void) => void; constructor( adapterToken: string, @@ -56,11 +56,15 @@ export function registerContextProvider( onContextSubscription: WireContextSubscriptionCallback ) { addEventListener(elm, adapterContextToken, ((evt: WireContextSubscriptionEvent) => { - evt.stopImmediatePropagation(); const { setNewContext, setDisconnectedCallback } = evt; - onContextSubscription({ - setNewContext, - setDisconnectedCallback, - }); + // If context subscription is successful, stop event propagation + if ( + onContextSubscription({ + setNewContext, + setDisconnectedCallback, + }) + ) { + evt.stopImmediatePropagation(); + } }) as EventListener); } diff --git a/packages/@lwc/engine-server/package.json b/packages/@lwc/engine-server/package.json index 5615248d2a..ae887aacbc 100644 --- a/packages/@lwc/engine-server/package.json +++ b/packages/@lwc/engine-server/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/engine-server", - "version": "8.19.0", + "version": "8.19.1", "description": "Renders LWC components in a server environment.", "keywords": [ "lwc" @@ -46,10 +46,10 @@ } }, "devDependencies": { - "@lwc/engine-core": "8.19.0", - "@lwc/rollup-plugin": "8.19.0", - "@lwc/shared": "8.19.0", - "@lwc/features": "8.19.0", + "@lwc/engine-core": "8.19.1", + "@lwc/rollup-plugin": "8.19.1", + "@lwc/shared": "8.19.1", + "@lwc/features": "8.19.1", "@rollup/plugin-virtual": "^3.0.2", "parse5": "^7.2.1" } diff --git a/packages/@lwc/engine-server/src/context.ts b/packages/@lwc/engine-server/src/context.ts index ada99f110d..f8bc3e4f02 100644 --- a/packages/@lwc/engine-server/src/context.ts +++ b/packages/@lwc/engine-server/src/context.ts @@ -26,7 +26,7 @@ export function createContextProvider(adapter: WireAdapterConstructor) { return createContextProviderWithRegister(adapter, registerContextProvider); } -function registerContextProvider( +export function registerContextProvider( elm: HostElement | LightningElement, adapterContextToken: string, onContextSubscription: WireContextSubscriptionCallback @@ -57,10 +57,10 @@ export function registerContextConsumer( const subscribeToProvider = currentNode[HostContextProvidersKey].get(adapterContextToken); if (!isUndefined(subscribeToProvider)) { - subscribeToProvider(subscriptionPayload); - // If we find a provider, we shouldn't continue traversing - // looking for another provider. - break; + // If context subscription is successful, stop traversing to locate a provider + if (subscribeToProvider(subscriptionPayload)) { + break; + } } } diff --git a/packages/@lwc/engine-server/src/index.ts b/packages/@lwc/engine-server/src/index.ts index a7fbf7bc4a..4410c98767 100644 --- a/packages/@lwc/engine-server/src/index.ts +++ b/packages/@lwc/engine-server/src/index.ts @@ -29,6 +29,8 @@ export { isComponentConstructor, parseFragment, parseFragment as parseSVGFragment, + setTrustedContextSet, + setContextKeys, } from '@lwc/engine-core'; // Engine-server public APIs ----------------------------------------------------------------------- diff --git a/packages/@lwc/engine-server/src/renderer.ts b/packages/@lwc/engine-server/src/renderer.ts index 39dd4474a0..06d944d844 100644 --- a/packages/@lwc/engine-server/src/renderer.ts +++ b/packages/@lwc/engine-server/src/renderer.ts @@ -35,7 +35,7 @@ import { startTrackingMutations, stopTrackingMutations, } from './utils/mutation-tracking'; -import { registerContextConsumer } from './context'; +import { registerContextConsumer, registerContextProvider } from './context'; import type { HostNode, HostElement, HostAttribute, HostChildNode } from './types'; import type { LifecycleCallback } from '@lwc/engine-core'; @@ -509,6 +509,7 @@ export const renderer = { insertStylesheet, assertInstanceOfHTMLElement, ownerDocument, + registerContextProvider, registerContextConsumer, attachInternals, defineCustomElement: getUpgradableElement, diff --git a/packages/@lwc/errors/package.json b/packages/@lwc/errors/package.json index 536dd2fd07..abbf7dacaf 100644 --- a/packages/@lwc/errors/package.json +++ b/packages/@lwc/errors/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/errors", - "version": "8.19.0", + "version": "8.19.1", "description": "LWC Error Utilities", "keywords": [ "lwc" diff --git a/packages/@lwc/features/package.json b/packages/@lwc/features/package.json index 94972d77df..cebf85d1dd 100644 --- a/packages/@lwc/features/package.json +++ b/packages/@lwc/features/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/features", - "version": "8.19.0", + "version": "8.19.1", "description": "LWC Features Flags", "keywords": [ "lwc" @@ -46,6 +46,6 @@ } }, "dependencies": { - "@lwc/shared": "8.19.0" + "@lwc/shared": "8.19.1" } } diff --git a/packages/@lwc/integration-karma/helpers/test-hydrate.js b/packages/@lwc/integration-karma/helpers/test-hydrate.js index fe3749868e..c9e7bdd492 100644 --- a/packages/@lwc/integration-karma/helpers/test-hydrate.js +++ b/packages/@lwc/integration-karma/helpers/test-hydrate.js @@ -22,6 +22,14 @@ window.HydrateTest = (function (lwc, testUtils) { return div; } + function setFeatureFlags(requiredFeatureFlags, value) { + if (requiredFeatureFlags) { + requiredFeatureFlags.forEach((featureFlag) => { + lwc.setFeatureFlagForTest(featureFlag, value); + }); + } + } + function runTest(ssrRendered, Component, testConfig) { const container = appendTestTarget(ssrRendered); const selector = container.firstChild.tagName.toLowerCase(); @@ -29,6 +37,7 @@ window.HydrateTest = (function (lwc, testUtils) { let testResult; const consoleSpy = testUtils.spyConsole(); + setFeatureFlags(testConfig.requiredFeatureFlags, true); if (testConfig.test) { const snapshot = testConfig.snapshot ? testConfig.snapshot(target) : {}; diff --git a/packages/@lwc/integration-karma/helpers/test-utils.js b/packages/@lwc/integration-karma/helpers/test-utils.js index 9d41487b10..68c2a338c7 100644 --- a/packages/@lwc/integration-karma/helpers/test-utils.js +++ b/packages/@lwc/integration-karma/helpers/test-utils.js @@ -470,6 +470,10 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { } } + function setFeatureFlagForTest(featureFlag, value) { + LWC.setFeatureFlagForTest(featureFlag, value); + } + // This mapping should be kept up-to-date with the mapping in @lwc/shared -> aria.ts const ariaPropertiesMapping = { ariaAutoComplete: 'aria-autocomplete', @@ -747,6 +751,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { catchUnhandledRejectionsAndErrors, addTrustedSignal, expectEquivalentDOM, + setFeatureFlagForTest, ...apiFeatures, }; })(LWC, jasmine, beforeAll); diff --git a/packages/@lwc/integration-karma/package.json b/packages/@lwc/integration-karma/package.json index e8694a093d..98c708a9fa 100644 --- a/packages/@lwc/integration-karma/package.json +++ b/packages/@lwc/integration-karma/package.json @@ -1,7 +1,7 @@ { "name": "@lwc/integration-karma", "private": true, - "version": "8.19.0", + "version": "8.19.1", "scripts": { "start": "KARMA_MODE=watch karma start ./scripts/karma-configs/test/local.js", "test": "karma start ./scripts/karma-configs/test/local.js --single-run", @@ -21,11 +21,11 @@ "karma-sauce-launcher-fix-firefox": "using a fork to work around https://github.com/karma-runner/karma-sauce-launcher/issues/275" }, "devDependencies": { - "@lwc/compiler": "8.19.0", - "@lwc/engine-dom": "8.19.0", - "@lwc/engine-server": "8.19.0", - "@lwc/rollup-plugin": "8.19.0", - "@lwc/synthetic-shadow": "8.19.0", + "@lwc/compiler": "8.19.1", + "@lwc/engine-dom": "8.19.1", + "@lwc/engine-server": "8.19.1", + "@lwc/rollup-plugin": "8.19.1", + "@lwc/synthetic-shadow": "8.19.1", "@types/jasmine": "^5.1.7", "chokidar": "^4.0.3", "istanbul-lib-coverage": "^3.2.2", 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 99cd3d7ff1..e1135eaeb7 100644 --- a/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js +++ b/packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js @@ -11,16 +11,16 @@ const fs = require('node:fs/promises'); const { format } = require('node:util'); const { rollup } = require('rollup'); const lwcRollupPlugin = require('@lwc/rollup-plugin'); -const ssr = ENGINE_SERVER ? require('@lwc/engine-server') : require('@lwc/ssr-runtime'); +const lwcSsr = ENGINE_SERVER ? require('@lwc/engine-server') : require('@lwc/ssr-runtime'); const { DISABLE_STATIC_CONTENT_OPTIMIZATION } = require('../shared/options'); const Watcher = require('./Watcher'); const context = { - LWC: ssr, + LWC: lwcSsr, moduleOutput: null, }; -ssr.setHooks({ +lwcSsr.setHooks({ sanitizeHtmlContent(content) { return content; }, @@ -103,7 +103,7 @@ async function getCompiledModule(dirName, compileForSSR) { return { code, watchFiles }; } -function throwOnUnexpectedConsoleCalls(runnable) { +function throwOnUnexpectedConsoleCalls(runnable, expectedConsoleCalls = {}) { // The console is shared between the VM and the main realm. Here we ensure that known warnings // are ignored and any others cause an explicit error. const methods = ['error', 'warn', 'log', 'info']; @@ -118,6 +118,10 @@ function throwOnUnexpectedConsoleCalls(runnable) { /Cannot set property "(inner|outer)HTML"/.test(error?.message) ) { return; + } else if ( + expectedConsoleCalls[method]?.some((matcher) => error.message.includes(matcher)) + ) { + return; } throw new Error(`Unexpected console.${method} call: ${error}`); @@ -141,26 +145,28 @@ function throwOnUnexpectedConsoleCalls(runnable) { * So, script runs, generates markup, & we get that markup out and return it to Karma for use * in client-side tests. */ -async function getSsrCode(moduleCode, testConfig, filename) { +async function getSsrCode(moduleCode, testConfig, filename, expectedSSRConsoleCalls) { const script = new vm.Script( ` - ${testConfig}; - config = config || {}; - ${moduleCode}; - moduleOutput = LWC.renderComponent( - 'x-${COMPONENT_UNDER_TEST}-${guid++}', - Main, - config.props || {}, - false, - 'sync' - );`, + ${testConfig}; + config = config || {}; + ${moduleCode}; + moduleOutput = LWC.renderComponent( + 'x-${COMPONENT_UNDER_TEST}-${guid++}', + Main, + config.props || {}, + false, + 'sync' + ); + `, { filename } ); throwOnUnexpectedConsoleCalls(() => { vm.createContext(context); script.runInContext(context); - }); + }, expectedSSRConsoleCalls); + return context.moduleOutput; } @@ -208,10 +214,22 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) { // Wrap all the tests into a describe block with the file stricture name const describeTitle = path.relative(basePath, suiteDir).split(path.sep).join(' '); - try { - const { code: testCode, watchFiles: testWatchFiles } = - await getTestModuleCode(filePath); + const { code: testCode, watchFiles: testWatchFiles } = await getTestModuleCode(filePath); + // Create a temporary module to evaluate the bundled code and extract config properties for test configuration + const configModule = new vm.Script(testCode); + const configContext = { config: {} }; + vm.createContext(configContext); + configModule.runInContext(configContext); + const { expectedSSRConsoleCalls, requiredFeatureFlags } = configContext.config; + + if (requiredFeatureFlags) { + requiredFeatureFlags.forEach((featureFlag) => { + lwcSsr.setFeatureFlagForTest(featureFlag, true); + }); + } + + try { // You can add an `.only` file alongside an `index.spec.js` file to make it `fdescribe()` const onlyFileExists = await existsUp(suiteDir, '.only'); @@ -228,7 +246,8 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) { ssrOutput = await getSsrCode( componentDefCSR, testCode, - path.join(suiteDir, 'ssr.js') + path.join(suiteDir, 'ssr.js'), + expectedSSRConsoleCalls ); } else { // ssr-compiler has it's own def @@ -241,7 +260,8 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) { ssrOutput = await getSsrCode( componentDefSSR.replace(`process.env.NODE_ENV === 'test-karma-lwc'`, 'true'), testCode, - path.join(suiteDir, 'ssr.js') + path.join(suiteDir, 'ssr.js'), + expectedSSRConsoleCalls ); } @@ -258,6 +278,12 @@ 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/index.spec.js b/packages/@lwc/integration-karma/test-hydration/context/index.spec.js new file mode 100644 index 0000000000..ed71537fac --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/index.spec.js @@ -0,0 +1,101 @@ +export default { + // server is expected to generate the same console error as the client + expectedSSRConsoleCalls: { + error: [], + warn: [ + 'Attempted to connect to trusted context but received the following error', + 'Multiple contexts of the same variety were provided. Only the first context will be used.', + ], + }, + requiredFeatureFlags: ['ENABLE_EXPERIMENTAL_SIGNALS'], + snapshot(target) { + const grandparent = target.shadowRoot.querySelector('x-grandparent'); + const detachedChild = target.shadowRoot.querySelector('x-child'); + const firstParent = grandparent.shadowRoot.querySelectorAll('x-parent')[0]; + const secondParent = grandparent.shadowRoot.querySelectorAll('x-parent')[1]; + const childOfFirstParent = firstParent.shadowRoot.querySelector('x-child'); + const childOfSecondParent = secondParent.shadowRoot.querySelector('x-child'); + + return { + components: { + grandparent, + firstParent, + secondParent, + childOfFirstParent, + childOfSecondParent, + }, + detachedChild, + }; + }, + test(target, snapshot, consoleCalls) { + // Assert context is provided by the grandparent and consumed correctly by all children + assertCorrectContext(snapshot); + + // Assert context is shadowed when consumed in a chain + assertContextShadowed(snapshot); + + // Assert context is disconnected when components are removed + assertContextDisconnected(target, snapshot); + + // Expect an error as one context was generated twice. + // Expect an error as one context was malformed (did not define connectContext or disconnectContext methods). + // Expect server/client context output parity (no hydration warnings) + TestUtils.expectConsoleCalls(consoleCalls, { + error: [], + warn: [ + 'Attempted to connect to trusted context but received the following error', + 'Multiple contexts of the same variety were provided. Only the first context will be used.', + ], + }); + }, +}; + +function assertCorrectContext(snapshot) { + Object.values(snapshot.components).forEach((component) => { + expect(component.shadowRoot.querySelector('div').textContent) + .withContext(`${component.tagName} should have the correct context`) + .toBe('grandparent provided value, another grandparent provided value'); + + expect(component.context.connectProvidedComponent?.hostElement) + .withContext( + `The context of ${component.tagName} should have been connected with the correct component` + ) + .toBe(component); + }); + expect(snapshot.detachedChild.shadowRoot.querySelector('div').textContent).toBe(', '); +} + +function assertContextShadowed(snapshot) { + const grandparentContext = snapshot.components.grandparent.context; + const firstParentContext = snapshot.components.firstParent.context; + const childOfFirstParentContext = snapshot.components.childOfFirstParent.context; + + expect(childOfFirstParentContext.providedContextSignal) + .withContext( + `Child should have been provided with the parent context and not that of the grandparent (grandparent context was shadowed)` + ) + .toBe(firstParentContext); + + expect(firstParentContext.providedContextSignal) + .withContext(`Parent should have been provided with grandparent context`) + .toBe(grandparentContext); + + // For good measure + expect(grandparentContext) + .withContext(`Grandparent context should not be the same as the parent context`) + .not.toBe(firstParentContext); +} + +function assertContextDisconnected(target, snapshot) { + Object.values(snapshot.components).forEach( + (component) => + (component.disconnect = () => { + expect(component.context.disconnectProvidedComponent?.hostElement) + .withContext( + `The context of ${component.tagName} should have been disconnected with the correct component` + ) + .toBe(component); + }) + ); + target.showTree = false; +} diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/base/base.js b/packages/@lwc/integration-karma/test-hydration/context/x/base/base.js new file mode 100644 index 0000000000..6c2ca30bc5 --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/base/base.js @@ -0,0 +1,11 @@ +import { LightningElement, api } from 'lwc'; + +export default class Base extends LightningElement { + @api disconnect; + + disconnectedCallback() { + if (this.disconnect) { + this.disconnect(); + } + } +} diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/child/child.html b/packages/@lwc/integration-karma/test-hydration/context/x/child/child.html new file mode 100644 index 0000000000..6db837b7e4 --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/child/child.html @@ -0,0 +1,3 @@ + diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/child/child.js b/packages/@lwc/integration-karma/test-hydration/context/x/child/child.js new file mode 100644 index 0000000000..7763fa7fdb --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/child/child.js @@ -0,0 +1,9 @@ +import { api } from 'lwc'; +import Base from 'x/base'; +import { defineContext } from 'x/contextManager'; +import { parentContextFactory, anotherParentContextFactory } from 'x/parentContext'; + +export default class Child extends Base { + @api context = defineContext(parentContextFactory)(); + @api anotherContext = defineContext(anotherParentContextFactory)(); +} diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/contextManager/contextManager.js b/packages/@lwc/integration-karma/test-hydration/context/x/contextManager/contextManager.js new file mode 100644 index 0000000000..5f0c42d791 --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/contextManager/contextManager.js @@ -0,0 +1,53 @@ +import { setContextKeys, setTrustedContextSet } from 'lwc'; + +const connectContext = Symbol('connectContext'); +const disconnectContext = Symbol('disconnectContext'); +const trustedContext = new WeakSet(); + +setTrustedContextSet(trustedContext); +setContextKeys({ connectContext, disconnectContext }); + +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; + } +} + +// This is a malformed context signal that does not implement the connectContext or disconnectContext methods +class MockMalformedContextSignal { + constructor() { + trustedContext.add(this); + } +} + +export const defineContext = (fromContext) => { + const contextDefinition = (initialValue) => + new MockContextSignal(initialValue, contextDefinition, fromContext); + return contextDefinition; +}; + +export const defineMalformedContext = () => { + return () => new MockMalformedContextSignal(); +}; diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/grandparent/grandparent.html b/packages/@lwc/integration-karma/test-hydration/context/x/grandparent/grandparent.html new file mode 100644 index 0000000000..2c178bf46a --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/grandparent/grandparent.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/grandparent/grandparent.js b/packages/@lwc/integration-karma/test-hydration/context/x/grandparent/grandparent.js new file mode 100644 index 0000000000..8bb4b172a5 --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/grandparent/grandparent.js @@ -0,0 +1,8 @@ +import { api } from 'lwc'; +import Base from 'x/base'; +import { grandparentContextFactory, anotherGrandparentContextFactory } from 'x/grandparentContext'; + +export default class Grandparent extends Base { + @api context = grandparentContextFactory('grandparent provided value'); + @api anotherContext = anotherGrandparentContextFactory('another grandparent provided value'); +} diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/grandparentContext/grandparentContext.js b/packages/@lwc/integration-karma/test-hydration/context/x/grandparentContext/grandparentContext.js new file mode 100644 index 0000000000..807ac8874a --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/grandparentContext/grandparentContext.js @@ -0,0 +1,4 @@ +import { defineContext } from 'x/contextManager'; + +export const grandparentContextFactory = defineContext(); +export const anotherGrandparentContextFactory = defineContext(); diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/main/main.html b/packages/@lwc/integration-karma/test-hydration/context/x/main/main.html new file mode 100644 index 0000000000..2241f1adbf --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/main/main.html @@ -0,0 +1,5 @@ + \ No newline at end of file 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 new file mode 100644 index 0000000000..4bfa922c53 --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/main/main.js @@ -0,0 +1,10 @@ +import { LightningElement, api } from 'lwc'; +import { defineMalformedContext } from 'x/contextManager'; +export default class Root extends LightningElement { + @api showTree = false; + malformedContext = defineMalformedContext()(); + + connectedCallback() { + this.showTree = true; + } +} diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/parent/parent.html b/packages/@lwc/integration-karma/test-hydration/context/x/parent/parent.html new file mode 100644 index 0000000000..a8e86f7a9b --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/parent/parent.html @@ -0,0 +1,4 @@ + diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/parent/parent.js b/packages/@lwc/integration-karma/test-hydration/context/x/parent/parent.js new file mode 100644 index 0000000000..3e2b8689cf --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/parent/parent.js @@ -0,0 +1,8 @@ +import { api } from 'lwc'; +import { parentContextFactory, anotherParentContextFactory } from 'x/parentContext'; +import Base from 'x/base'; + +export default class Parent extends Base { + @api context = parentContextFactory(); + @api anotherContext = anotherParentContextFactory(); +} diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/parentContext/parentContext.js b/packages/@lwc/integration-karma/test-hydration/context/x/parentContext/parentContext.js new file mode 100644 index 0000000000..03a078239f --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/parentContext/parentContext.js @@ -0,0 +1,5 @@ +import { defineContext } from 'x/contextManager'; +import { grandparentContextFactory, anotherGrandparentContextFactory } from 'x/grandparentContext'; + +export const parentContextFactory = defineContext(grandparentContextFactory); +export const anotherParentContextFactory = defineContext(anotherGrandparentContextFactory); diff --git a/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.html b/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.html new file mode 100644 index 0000000000..cb3b7ad81c --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.html @@ -0,0 +1,3 @@ + \ No newline at end of file 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 new file mode 100644 index 0000000000..ca239597b6 --- /dev/null +++ b/packages/@lwc/integration-karma/test-hydration/context/x/tooMuchContext/tooMuchContext.js @@ -0,0 +1,7 @@ +import { grandparentContextFactory } from 'x/grandparentContext'; +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'); +} diff --git a/packages/@lwc/integration-tests/package.json b/packages/@lwc/integration-tests/package.json index c8ae9b4523..d2fbf5715b 100644 --- a/packages/@lwc/integration-tests/package.json +++ b/packages/@lwc/integration-tests/package.json @@ -1,7 +1,7 @@ { "name": "@lwc/integration-tests", "private": true, - "version": "8.19.0", + "version": "8.19.1", "scripts": { "build": "node scripts/build.js", "build:dev": "MODE=dev yarn build", @@ -18,7 +18,7 @@ "sauce:prod:ci": "MODE=prod yarn build:prod && MODE=prod ../../../scripts/ci/retry.sh wdio ./scripts/wdio.sauce.conf.js" }, "devDependencies": { - "@lwc/rollup-plugin": "8.19.0", + "@lwc/rollup-plugin": "8.19.1", "@wdio/cli": "^9.9.1", "@wdio/local-runner": "^9.9.1", "@wdio/mocha-framework": "^9.9.0", @@ -27,7 +27,7 @@ "@wdio/static-server-service": "^9.9.0", "deepmerge": "^4.3.1", "dotenv": "^16.5.0", - "lwc": "8.19.0", + "lwc": "8.19.1", "minimist": "^1.2.8", "webdriverio": "^9.0.7" }, diff --git a/packages/@lwc/integration-types/package.json b/packages/@lwc/integration-types/package.json index 336dcbd8f3..11e1ee7a4f 100644 --- a/packages/@lwc/integration-types/package.json +++ b/packages/@lwc/integration-types/package.json @@ -1,7 +1,7 @@ { "name": "@lwc/integration-types", "private": true, - "version": "8.19.0", + "version": "8.19.1", "description": "Type validation for LWC packages", "type": "module", "scripts": { @@ -9,8 +9,8 @@ "playground": "rollup -c src/playground/rollup.config.js --watch" }, "dependencies": { - "@lwc/rollup-plugin": "8.19.0", - "lwc": "8.19.0" + "@lwc/rollup-plugin": "8.19.1", + "lwc": "8.19.1" }, "devDependencies": { "@rollup/plugin-replace": "^6.0.2", diff --git a/packages/@lwc/module-resolver/package.json b/packages/@lwc/module-resolver/package.json index d6a5924358..9f35d9e0c6 100644 --- a/packages/@lwc/module-resolver/package.json +++ b/packages/@lwc/module-resolver/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/module-resolver", - "version": "8.19.0", + "version": "8.19.1", "description": "Resolves paths for LWC components", "keywords": [ "lwc" diff --git a/packages/@lwc/perf-benchmarks-components/package.json b/packages/@lwc/perf-benchmarks-components/package.json index 5151158ab4..68ab5eb234 100644 --- a/packages/@lwc/perf-benchmarks-components/package.json +++ b/packages/@lwc/perf-benchmarks-components/package.json @@ -1,12 +1,12 @@ { "name": "@lwc/perf-benchmarks-components", - "version": "8.19.0", + "version": "8.19.1", "private": true, "scripts": { "build": "rm -fr dist && rollup -c ./rollup.config.mjs" }, "devDependencies": { - "@lwc/rollup-plugin": "8.19.0" + "@lwc/rollup-plugin": "8.19.1" }, "nx": { "targets": { diff --git a/packages/@lwc/perf-benchmarks/package.json b/packages/@lwc/perf-benchmarks/package.json index 8161654bdd..d8c0d8ef64 100644 --- a/packages/@lwc/perf-benchmarks/package.json +++ b/packages/@lwc/perf-benchmarks/package.json @@ -1,6 +1,6 @@ { "name": "@lwc/perf-benchmarks", - "version": "8.19.0", + "version": "8.19.1", "private": true, "scripts": { "build": "rm -fr dist && rollup -c ./rollup.config.mjs && node scripts/build.js && ./scripts/fix-deps.sh", @@ -15,11 +15,11 @@ "Don't forget to add these to fix-deps.sh as well." ], "dependencies": { - "@lwc/engine-dom": "8.19.0", - "@lwc/engine-server": "8.19.0", - "@lwc/perf-benchmarks-components": "8.19.0", - "@lwc/ssr-runtime": "8.19.0", - "@lwc/synthetic-shadow": "8.19.0" + "@lwc/engine-dom": "8.19.1", + "@lwc/engine-server": "8.19.1", + "@lwc/perf-benchmarks-components": "8.19.1", + "@lwc/ssr-runtime": "8.19.1", + "@lwc/synthetic-shadow": "8.19.1" }, "devDependencies": { "@best/cli": "^14.0.0", diff --git a/packages/@lwc/rollup-plugin/package.json b/packages/@lwc/rollup-plugin/package.json index 8db198dd85..cfc6045c7f 100644 --- a/packages/@lwc/rollup-plugin/package.json +++ b/packages/@lwc/rollup-plugin/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/rollup-plugin", - "version": "8.19.0", + "version": "8.19.1", "description": "Rollup plugin to compile LWC", "keywords": [ "lwc" @@ -46,13 +46,13 @@ } }, "dependencies": { - "@lwc/compiler": "8.19.0", - "@lwc/module-resolver": "8.19.0", - "@lwc/shared": "8.19.0", + "@lwc/compiler": "8.19.1", + "@lwc/module-resolver": "8.19.1", + "@lwc/shared": "8.19.1", "@rollup/pluginutils": "~5.1.4" }, "devDependencies": { - "@lwc/errors": "8.19.0" + "@lwc/errors": "8.19.1" }, "peerDependencies": { "rollup": "^1.2.0||^2.0.0||^3.0.0||^4.0.0" diff --git a/packages/@lwc/shared/package.json b/packages/@lwc/shared/package.json index 7501935b60..b8780c0d65 100644 --- a/packages/@lwc/shared/package.json +++ b/packages/@lwc/shared/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/shared", - "version": "8.19.0", + "version": "8.19.1", "description": "Utilities and methods that are shared across packages", "keywords": [ "lwc" diff --git a/packages/@lwc/shared/src/__tests__/context.spec.ts b/packages/@lwc/shared/src/__tests__/context.spec.ts new file mode 100644 index 0000000000..d5fdf67093 --- /dev/null +++ b/packages/@lwc/shared/src/__tests__/context.spec.ts @@ -0,0 +1,103 @@ +/* + * 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 { describe, beforeEach, expect, it, vi } from 'vitest'; + +describe('context', () => { + let setContextKeys: (config: any) => void; + let getContextKeys: () => any; + let setTrustedContextSet: (signals: WeakSet) => void; + let addTrustedContext: (signal: object) => void; + let isTrustedContext: (target: object) => boolean; + + beforeEach(async () => { + vi.resetModules(); + const contextModule = await import('../context'); + setContextKeys = contextModule.setContextKeys; + getContextKeys = contextModule.getContextKeys; + setTrustedContextSet = contextModule.setTrustedContextSet; + addTrustedContext = contextModule.addTrustedContext; + isTrustedContext = contextModule.isTrustedContext; + }); + + it('should set and get context keys', () => { + const mockContextKeys = { + connectContext: Symbol('connect'), + disconnectContext: Symbol('disconnect'), + }; + + setContextKeys(mockContextKeys); + const retrievedKeys = getContextKeys(); + + expect(retrievedKeys).toBe(mockContextKeys); + expect(retrievedKeys.connectContext).toBe(mockContextKeys.connectContext); + expect(retrievedKeys.disconnectContext).toBe(mockContextKeys.disconnectContext); + }); + + it('should throw when attempting to set context keys multiple times', () => { + const mockContextKeys1 = { + connectContext: Symbol('connect1'), + disconnectContext: Symbol('disconnect1'), + }; + + const mockContextKeys2 = { + connectContext: Symbol('connect2'), + disconnectContext: Symbol('disconnect2'), + }; + + setContextKeys(mockContextKeys1); + + expect(() => { + setContextKeys(mockContextKeys2); + }).toThrow('`setContextKeys` cannot be called more than once'); + + expect(getContextKeys()).toBe(mockContextKeys1); + }); + + it('should return undefined when getting context keys before setting them', () => { + const keys = getContextKeys(); + expect(keys).toBeUndefined(); + }); + + describe('setTrustedContextSet', () => { + it('should throw an error if trustedContexts is already set', () => { + setTrustedContextSet(new WeakSet()); + expect(() => setTrustedContextSet(new WeakSet())).toThrow( + 'Trusted Context Set is already set!' + ); + }); + }); + + describe('addTrustedContext', () => { + it('should add a signal to the trustedContexts set', () => { + const mockWeakSet = new WeakSet(); + setTrustedContextSet(mockWeakSet); + const signal = {}; + addTrustedContext(signal); + expect(isTrustedContext(signal)).toBe(true); + }); + }); + + describe('isTrustedContext', () => { + it('should return true for a trusted context', () => { + const mockWeakSet = new WeakSet(); + setTrustedContextSet(mockWeakSet); + const signal = {}; + addTrustedContext(signal); + expect(isTrustedContext(signal)).toBe(true); + }); + + it('should return false for an untrusted context', () => { + const mockWeakSet = new WeakSet(); + setTrustedContextSet(mockWeakSet); + expect(isTrustedContext({})).toBe(false); + }); + + it('should return true for all calls when trustedContexts is not set', () => { + expect(isTrustedContext({})).toBe(true); + }); + }); +}); diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts new file mode 100644 index 0000000000..a75ac4402c --- /dev/null +++ b/packages/@lwc/shared/src/context.ts @@ -0,0 +1,61 @@ +/* + * 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 { isFalse } from './assert'; + +export const ContextEventName = 'lightning:context-request'; + +let trustedContext: WeakSet; + +export type ContextKeys = { + connectContext: symbol; + disconnectContext: symbol; +}; + +export type ContextProvidedCallback = (contextSignal?: object) => void; + +export interface ContextBinding { + component: C; + + provideContext(contextVariety: V, providedContextSignal: object): void; + + consumeContext( + contextVariety: V, + contextProvidedCallback: ContextProvidedCallback + ): void; +} + +let contextKeys: ContextKeys; + +export function setContextKeys(config: ContextKeys) { + isFalse(contextKeys, '`setContextKeys` cannot be called more than once'); + + contextKeys = config; +} + +export function getContextKeys() { + return contextKeys; +} + +export function setTrustedContextSet(context: WeakSet) { + isFalse(trustedContext, 'Trusted Context Set is already set!'); + + trustedContext = context; +} + +export function addTrustedContext(contextParticipant: object) { + // This should be a no-op when the trustedSignals set isn't set by runtime + trustedContext?.add(contextParticipant); +} + +export function isTrustedContext(target: object): boolean { + if (!trustedContext) { + // The runtime didn't set a trustedContext set + // this check should only be performed for runtimes that care about filtering context participants to track + return true; + } + return trustedContext.has(target); +} diff --git a/packages/@lwc/shared/src/index.ts b/packages/@lwc/shared/src/index.ts index feb8164a87..835fa8b3bf 100644 --- a/packages/@lwc/shared/src/index.ts +++ b/packages/@lwc/shared/src/index.ts @@ -8,6 +8,7 @@ import * as assert from './assert'; export * from './api-version'; export * from './aria'; +export * from './context'; export * from './language'; export * from './keys'; export * from './void-elements'; diff --git a/packages/@lwc/signals/package.json b/packages/@lwc/signals/package.json index 4ec33d3cfc..7aa52f9f0f 100644 --- a/packages/@lwc/signals/package.json +++ b/packages/@lwc/signals/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/signals", - "version": "8.19.0", + "version": "8.19.1", "description": "Provides the interface to interact with reactivity from outside the framework", "keywords": [ "lwc" @@ -46,6 +46,6 @@ } }, "devDependencies": { - "@lwc/shared": "8.19.0" + "@lwc/shared": "8.19.1" } } diff --git a/packages/@lwc/ssr-client-utils/package.json b/packages/@lwc/ssr-client-utils/package.json index 15b47cf680..d88c03d529 100644 --- a/packages/@lwc/ssr-client-utils/package.json +++ b/packages/@lwc/ssr-client-utils/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/ssr-client-utils", - "version": "8.19.0", + "version": "8.19.1", "description": "Client-side complement to @lwc/ssr-compiler", "keywords": [ "lwc", diff --git a/packages/@lwc/ssr-compiler/package.json b/packages/@lwc/ssr-compiler/package.json index ce3313d6bd..e333f0cb55 100644 --- a/packages/@lwc/ssr-compiler/package.json +++ b/packages/@lwc/ssr-compiler/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/ssr-compiler", - "version": "8.19.0", + "version": "8.19.1", "description": "Compile component for use during server-side rendering", "keywords": [ "compiler", @@ -49,9 +49,9 @@ }, "dependencies": { "@babel/types": "7.27.0", - "@lwc/errors": "8.19.0", - "@lwc/shared": "8.19.0", - "@lwc/template-compiler": "8.19.0", + "@lwc/errors": "8.19.1", + "@lwc/shared": "8.19.1", + "@lwc/template-compiler": "8.19.1", "acorn": "8.14.1", "astring": "^1.9.0", "estree-toolkit": "^1.7.12", @@ -59,7 +59,7 @@ "meriyah": "^5.0.0" }, "devDependencies": { - "@lwc/babel-plugin-component": "8.19.0", + "@lwc/babel-plugin-component": "8.19.1", "@types/estree": "^1.0.7" } } diff --git a/packages/@lwc/ssr-runtime/package.json b/packages/@lwc/ssr-runtime/package.json index 62621cb48d..f26f360d7c 100644 --- a/packages/@lwc/ssr-runtime/package.json +++ b/packages/@lwc/ssr-runtime/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/ssr-runtime", - "version": "8.19.0", + "version": "8.19.1", "description": "Runtime complement to @lwc/ssr-compiler", "keywords": [ "lwc", @@ -48,8 +48,8 @@ } }, "devDependencies": { - "@lwc/shared": "8.19.0", - "@lwc/engine-core": "8.19.0", + "@lwc/shared": "8.19.1", + "@lwc/engine-core": "8.19.1", "observable-membrane": "2.0.0" } } diff --git a/packages/@lwc/ssr-runtime/src/stubs.ts b/packages/@lwc/ssr-runtime/src/stubs.ts index 52be66439b..8bd4f1cc10 100644 --- a/packages/@lwc/ssr-runtime/src/stubs.ts +++ b/packages/@lwc/ssr-runtime/src/stubs.ts @@ -66,6 +66,12 @@ 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, diff --git a/packages/@lwc/style-compiler/package.json b/packages/@lwc/style-compiler/package.json index 7f7d18b941..3e157fde12 100644 --- a/packages/@lwc/style-compiler/package.json +++ b/packages/@lwc/style-compiler/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/style-compiler", - "version": "8.19.0", + "version": "8.19.1", "description": "Transform style sheet to be consumed by the LWC engine", "keywords": [ "lwc" @@ -46,7 +46,7 @@ } }, "dependencies": { - "@lwc/shared": "8.19.0", + "@lwc/shared": "8.19.1", "postcss": "~8.5.3", "postcss-selector-parser": "~7.1.0", "postcss-value-parser": "~4.2.0" diff --git a/packages/@lwc/synthetic-shadow/package.json b/packages/@lwc/synthetic-shadow/package.json index d192157efa..e995e0ff87 100644 --- a/packages/@lwc/synthetic-shadow/package.json +++ b/packages/@lwc/synthetic-shadow/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/synthetic-shadow", - "version": "8.19.0", + "version": "8.19.1", "description": "Synthetic Shadow Root for LWC", "keywords": [ "lwc" @@ -46,8 +46,8 @@ } }, "devDependencies": { - "@lwc/features": "8.19.0", - "@lwc/shared": "8.19.0" + "@lwc/features": "8.19.1", + "@lwc/shared": "8.19.1" }, "lwc": { "modules": [ diff --git a/packages/@lwc/template-compiler/package.json b/packages/@lwc/template-compiler/package.json index 7d2ba4c799..507923dc1a 100644 --- a/packages/@lwc/template-compiler/package.json +++ b/packages/@lwc/template-compiler/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/template-compiler", - "version": "8.19.0", + "version": "8.19.1", "description": "Template compiler package", "keywords": [ "lwc" @@ -46,8 +46,8 @@ } }, "dependencies": { - "@lwc/errors": "8.19.0", - "@lwc/shared": "8.19.0", + "@lwc/errors": "8.19.1", + "@lwc/shared": "8.19.1", "acorn": "~8.14.1", "astring": "~1.9.0", "he": "~1.2.0" diff --git a/packages/@lwc/types/package.json b/packages/@lwc/types/package.json index 8c3e62aaca..a9c2fa5a72 100644 --- a/packages/@lwc/types/package.json +++ b/packages/@lwc/types/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/types", - "version": "8.19.0", + "version": "8.19.1", "keywords": [ "lwc", "types", @@ -32,6 +32,6 @@ "*.d.ts" ], "dependencies": { - "@lwc/engine-core": "8.19.0" + "@lwc/engine-core": "8.19.1" } } diff --git a/packages/@lwc/wire-service/package.json b/packages/@lwc/wire-service/package.json index a8dfca4226..3f4f4f988f 100644 --- a/packages/@lwc/wire-service/package.json +++ b/packages/@lwc/wire-service/package.json @@ -4,7 +4,7 @@ "You can safely modify dependencies, devDependencies, keywords, etc., but other props will be overwritten." ], "name": "@lwc/wire-service", - "version": "8.19.0", + "version": "8.19.1", "description": "@wire service", "keywords": [ "lwc" @@ -46,8 +46,8 @@ } }, "devDependencies": { - "@lwc/engine-core": "8.19.0", - "@lwc/shared": "8.19.0" + "@lwc/engine-core": "8.19.1", + "@lwc/shared": "8.19.1" }, "lwc": { "modules": [ diff --git a/packages/lwc/package.json b/packages/lwc/package.json index 098ba5acba..648a9775b0 100644 --- a/packages/lwc/package.json +++ b/packages/lwc/package.json @@ -1,6 +1,6 @@ { "name": "lwc", - "version": "8.19.0", + "version": "8.19.1", "description": "Lightning Web Components (LWC)", "homepage": "https://lwc.dev/", "repository": { @@ -21,24 +21,24 @@ "!vitest.config.*" ], "dependencies": { - "@lwc/aria-reflection": "8.19.0", - "@lwc/babel-plugin-component": "8.19.0", - "@lwc/compiler": "8.19.0", - "@lwc/engine-core": "8.19.0", - "@lwc/engine-dom": "8.19.0", - "@lwc/engine-server": "8.19.0", - "@lwc/errors": "8.19.0", - "@lwc/features": "8.19.0", - "@lwc/module-resolver": "8.19.0", - "@lwc/rollup-plugin": "8.19.0", - "@lwc/shared": "8.19.0", - "@lwc/ssr-compiler": "8.19.0", - "@lwc/ssr-runtime": "8.19.0", - "@lwc/style-compiler": "8.19.0", - "@lwc/synthetic-shadow": "8.19.0", - "@lwc/template-compiler": "8.19.0", - "@lwc/types": "8.19.0", - "@lwc/wire-service": "8.19.0" + "@lwc/aria-reflection": "8.19.1", + "@lwc/babel-plugin-component": "8.19.1", + "@lwc/compiler": "8.19.1", + "@lwc/engine-core": "8.19.1", + "@lwc/engine-dom": "8.19.1", + "@lwc/engine-server": "8.19.1", + "@lwc/errors": "8.19.1", + "@lwc/features": "8.19.1", + "@lwc/module-resolver": "8.19.1", + "@lwc/rollup-plugin": "8.19.1", + "@lwc/shared": "8.19.1", + "@lwc/ssr-compiler": "8.19.1", + "@lwc/ssr-runtime": "8.19.1", + "@lwc/style-compiler": "8.19.1", + "@lwc/synthetic-shadow": "8.19.1", + "@lwc/template-compiler": "8.19.1", + "@lwc/types": "8.19.1", + "@lwc/wire-service": "8.19.1" }, "lwc": { "modules": [ diff --git a/playground/package.json b/playground/package.json index dbb2db399d..7fe0c1b6f6 100644 --- a/playground/package.json +++ b/playground/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@lwc/playground", - "version": "8.19.0", + "version": "8.19.1", "type": "module", "description": "Playground project to experiment with LWC.", "scripts": { @@ -9,9 +9,9 @@ "build": "NODE_ENV=production rollup -c" }, "devDependencies": { - "@lwc/rollup-plugin": "8.19.0", + "@lwc/rollup-plugin": "8.19.1", "@rollup/plugin-replace": "^6.0.2", - "lwc": "8.19.0", + "lwc": "8.19.1", "rollup": "^4.40.0", "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-serve": "^3.0.0" diff --git a/scripts/bundlesize/bundlesize.config.json b/scripts/bundlesize/bundlesize.config.json index 7538bb9005..fca9dee345 100644 --- a/scripts/bundlesize/bundlesize.config.json +++ b/scripts/bundlesize/bundlesize.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "packages/@lwc/engine-dom/dist/index.js", - "maxSize": "24.5KB" + "maxSize": "24.59KB" }, { "path": "packages/@lwc/synthetic-shadow/dist/index.js",