From d5fa2419d6de44b3a706c1539512b7b35fca4c4e Mon Sep 17 00:00:00 2001 From: rax-it Date: Tue, 3 Dec 2024 11:05:08 -0800 Subject: [PATCH 01/11] chore: bake context into lwc framework --- .../@lwc/engine-core/src/framework/context.ts | 20 + packages/@lwc/engine-core/src/framework/vm.ts | 96 +++++ .../index.spec.js | 1 + .../x/state/state.js | 391 ++++++++++++++++++ .../x/test/test.html | 4 + .../x/test/test.js | 4 +- .../x/testChild/testChild.html | 3 + .../x/testChild/testChild.js | 10 + .../component/face-callbacks/index.spec.js | 2 +- packages/@lwc/shared/src/context.ts | 3 + packages/@lwc/shared/src/index.ts | 1 + 11 files changed, 533 insertions(+), 2 deletions(-) create mode 100644 packages/@lwc/engine-core/src/framework/context.ts create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.js create mode 100644 packages/@lwc/shared/src/context.ts diff --git a/packages/@lwc/engine-core/src/framework/context.ts b/packages/@lwc/engine-core/src/framework/context.ts new file mode 100644 index 0000000000..a170e257bf --- /dev/null +++ b/packages/@lwc/engine-core/src/framework/context.ts @@ -0,0 +1,20 @@ +import type { Signal } from '@lwc/signals'; + +export const symbolContextKey = Symbol.for('context'); +export type ContextProvidedCallback = (contextSignal: Signal) => void; + +const EVENT_NAME = 'lightning:context-request'; + +export class ContextRequestEvent extends CustomEvent<{ + key: typeof symbolContextKey; + contextVariety: unknown; + callback: ContextProvidedCallback; +}> { + constructor(detail: { contextVariety: unknown; callback: ContextProvidedCallback }) { + super(EVENT_NAME, { + bubbles: true, + composed: true, + detail: { ...detail, key: symbolContextKey }, + }); + } +} \ No newline at end of file diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index a309c71c91..5590593706 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -11,6 +11,7 @@ import { assert, create, defineProperty, + getPrototypeOf, getOwnPropertyNames, isArray, isFalse, @@ -20,6 +21,8 @@ import { isTrue, isUndefined, flattenStylesheets, + // connectContext, + // disconnectContext } from '@lwc/shared'; import { addErrorComponentStack } from '../shared/error'; @@ -49,6 +52,8 @@ import { flushMutationLogsForVM, getAndFlushMutationLogs } from './mutation-logg import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring'; import { VNodeType, isVFragment } from './vnodes'; import { isReportingEnabled, report, ReportingEventId } from './reporting'; +import { type ContextProvidedCallback, ContextRequestEvent } from './context'; + import type { VNodes, VCustomElement, VNode, VBaseElement, VStaticPartElement } from './vnodes'; import type { ReactiveObserver } from './mutation-tracker'; import type { @@ -60,6 +65,11 @@ import type { ComponentDef } from './def'; import type { Template } from './template'; import type { HostNode, HostElement, RendererAPI } from './renderer'; import type { Stylesheet, Stylesheets, APIVersion } from '@lwc/shared'; +import type { Signal } from '@lwc/signals'; + +const connectContext = Symbol.for("connectContext"); +const disconnectContext = Symbol.for("disconnectContext"); +const symbolContextKey = Symbol.for('context'); type ShadowRootMode = 'open' | 'closed'; @@ -216,6 +226,8 @@ export interface VM { * API version associated with this VM */ apiVersion: APIVersion; + + contextfulFieldsOrProps?: string[]; } type VMAssociable = HostNode | LightningElement; @@ -423,9 +435,24 @@ export function createVM( installWireAdapters(vm); } + // gatherContextfulFields(vm); + return vm; } +function gatherContextfulFields(vm: VM) { + const { component } = vm; + try { + vm.contextfulFieldsOrProps = getOwnPropertyNames(getPrototypeOf(component)).filter( + (propName) => (component as any)[propName]?.[connectContext] + ); + } catch(e) { + console.error(e); + debugger; + } + +} + function validateComponentStylesheets(vm: VM, stylesheets: Stylesheets): boolean { let valid = true; @@ -716,6 +743,9 @@ export function runConnectedCallback(vm: VM) { logOperationEnd(OperationId.ConnectedCallback, vm); } + // Setup context after connected callback is executed + gatherContextfulFields(vm) + // setupContext(vm); // This test only makes sense in the browser, with synthetic lifecycle, and when reporting is enabled or // we're in dev mode. This is to detect a particular issue with synthetic lifecycle. if ( @@ -740,6 +770,72 @@ export function runConnectedCallback(vm: VM) { } } +function setupContext(vm: VM) { + const { contextfulFieldsOrProps, component } = vm; + + if (!contextfulFieldsOrProps || contextfulFieldsOrProps.length === 0) { + return; + } + + let isProvidingContext = false; + const providedContextVarieties = new Map>(); + const contextRuntimeAdapter = { + isServerSide: false, + component, + provideContext( + contextVariety: T, + providedContextSignal: Signal, + ): void { + if (!isProvidingContext) { + isProvidingContext = true; + + // todo: fix typing + component.addEventListener('lightning:context-request', (event: any) => { + if ( + event.detail.key === symbolContextKey && + providedContextVarieties.has(event.detail.contextVariety) + ) { + event.stopImmediatePropagation(); + const providedContextSignal = providedContextVarieties.get( + event.detail.contextVariety, + ); + event.detail.callback(providedContextSignal); + } + }); + } + + let multipleContextWarningShown = false; + + if (providedContextVarieties.has(contextVariety)) { + if (!multipleContextWarningShown) { + multipleContextWarningShown = true; + console.error( + 'Multiple contexts of the same variety were provided. Only the first context will be used.', + ); + } + return; + } + + providedContextVarieties.set(contextVariety, providedContextSignal); + }, + consumeContext( + contextVariety: T, + contextProvidedCallback: ContextProvidedCallback, + ): void { + const event = new ContextRequestEvent({ + contextVariety, + callback: contextProvidedCallback, + }); + + component.dispatchEvent(event); + } + } + + for (const contextfulFieldsOrProp of contextfulFieldsOrProps) { + (component as any)[contextfulFieldsOrProp][connectContext](contextRuntimeAdapter); + } +} + function hasWireAdapters(vm: VM): boolean { return getOwnPropertyNames(vm.def.wire).length > 0; } diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js index f684e9cc01..14978ef0ee 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js @@ -4,6 +4,7 @@ import { customElementCallbackReactionErrorListener } from 'test-utils'; import Test from 'x/test'; import ConnectedCallbackThrow from 'x/connectedCallbackThrow'; import XSlottedParent from 'x/slottedParent'; +import TestChild from 'x/testChild'; function testConnectSlot(name, fn) { it(`should invoke the connectedCallback the root element is added in the DOM via ${name}`, () => { diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js new file mode 100644 index 0000000000..4005d6a638 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js @@ -0,0 +1,391 @@ + +/* eslint-disable */ +/** + * This file is generated from repository: git+https://github.com/salesforce/lightning-labs.git + * module: packages/@lwc/state + */ + +// src/core.ts +// import { setTrustedSignalSet as lwcSetTrustedSignalSet } from "lwc"; +// import { connectContext, disconnectContext } from '@lwc/shared'; + +// node_modules/@lwc/signals/dist/index.js +function isFalse$1(value, msg) { + if (value) { + throw new Error(`Assert Violation: ${msg}`); + } +} +var trustedSignals; +function setTrustedSignalSet(signals) { + isFalse$1(trustedSignals, "Trusted Signal Set is already set!"); + trustedSignals = signals; +} +function addTrustedSignal(signal) { + trustedSignals?.add(signal); +} +var SignalBaseClass = class { + constructor() { + this.subscribers = /* @__PURE__ */ new Set(); + addTrustedSignal(this); + } + subscribe(onUpdate) { + this.subscribers.add(onUpdate); + return () => { + this.subscribers.delete(onUpdate); + }; + } + notify() { + for (const subscriber of this.subscribers) { + subscriber(); + } + } +}; + +// src/shared.ts +var connectContext = Symbol.for("connectContext"); +var disconnectContext = Symbol.for("disconnectContext"); + +// src/standalone-context.ts +var ConsumedContextSignal = class extends SignalBaseClass { + constructor(stateDef) { + super(); + this._value = null; + this.unsubscribe = () => { + }; + this.desiredStateDef = stateDef; + } + get value() { + return this._value; + } + [connectContext](runtimeAdapter) { + if (!runtimeAdapter) { + throw new Error( + "Implementation error: runtimeAdapter must be present at the time of connect." + ); + } + runtimeAdapter.consumeContext( + this.desiredStateDef, + (providedContextSignal) => { + this._value = providedContextSignal.value; + this.notify(); + this.unsubscribe = providedContextSignal.subscribe(() => { + this._value = providedContextSignal.value; + this.notify(); + }); + } + ); + } + [disconnectContext](_componentId) { + this.unsubscribe(); + this.unsubscribe = () => { + }; + } +}; + +// src/index.ts +var atomSetter = Symbol("atomSetter"); +var contextID = Symbol("contextID"); +var AtomSignal = class extends SignalBaseClass { + constructor(value) { + super(); + this._value = value; + } + [atomSetter](value) { + this._value = value; + this.notify(); + } + get value() { + return this._value; + } +}; +var ContextAtomSignal = class extends AtomSignal { + constructor() { + super(...arguments); + this._id = contextID; + } +}; +var ComputedSignal = class extends SignalBaseClass { + constructor(inputSignalsObj, computer) { + super(); + this.isStale = true; + this.computer = computer; + this.dependencies = inputSignalsObj; + const onUpdate = () => { + this.isStale = true; + this.notify(); + }; + for (const signal of Object.values(inputSignalsObj)) { + signal.subscribe(onUpdate); + } + } + computeValue() { + const dependencyValues = {}; + for (const [signalName, signal] of Object.entries(this.dependencies)) { + dependencyValues[signalName] = signal.value; + } + this.isStale = false; + this._value = this.computer(dependencyValues); + } + notify() { + this.isStale = true; + super.notify(); + } + get value() { + if (this.isStale) { + this.computeValue(); + } + return this._value; + } +}; +var isUpdater = (signalOrUpdater) => typeof signalOrUpdater === "function"; +var atom = (initialValue) => new AtomSignal(initialValue); +var computed = (inputSignalsObj, computer) => new ComputedSignal(inputSignalsObj, computer); +var update = (signalsToUpdate, userProvidedUpdaterFn) => { + return (...uniqueArgs) => { + const signalValues = {}; + for (const [signalName, signal] of Object.entries(signalsToUpdate)) { + signalValues[signalName] = signal.value; + } + const newValues = userProvidedUpdaterFn(signalValues, ...uniqueArgs); + for (const [atomName, newValue] of Object.entries(newValues)) { + signalsToUpdate[atomName][atomSetter](newValue); + } + }; +}; +var defineState = (defineStateCallback) => { + const stateDefinition = (...args) => { + class StateManagerSignal extends SignalBaseClass { + constructor() { + super(); + this.isStale = true; + this.isNotifyScheduled = false; + // biome-ignore lint/suspicious/noExplicitAny: we actually do want this, thanks + this.contextSignals = /* @__PURE__ */ new Map(); + this.contextConsumptionQueue = []; + this.contextUnsubscribes = /* @__PURE__ */ new WeakMap(); + const fromContext2 = (contextVarietyUniqueId) => { + if (this.contextSignals.has(contextVarietyUniqueId)) { + return this.contextSignals.get(contextVarietyUniqueId); + } + const localContextSignal = new ContextAtomSignal(void 0); + this.contextSignals.set(contextVarietyUniqueId, localContextSignal); + this.contextConsumptionQueue.push((runtimeAdapter) => { + if (!runtimeAdapter) { + throw new Error( + "Implementation error: runtimeAdapter must be present at the time of connect." + ); + } + runtimeAdapter.consumeContext( + contextVarietyUniqueId, + (providedContextSignal) => { + localContextSignal[atomSetter](providedContextSignal.value); + const unsub = providedContextSignal.subscribe(() => { + localContextSignal[atomSetter](providedContextSignal.value); + }); + if (!this.contextUnsubscribes.has(runtimeAdapter.component)) { + this.contextUnsubscribes.set(runtimeAdapter.component, []); + } + this.contextUnsubscribes.get(runtimeAdapter.component).push(unsub); + } + ); + }); + return localContextSignal; + }; + this.internalStateShape = defineStateCallback(atom, computed, update, fromContext2)(...args); + for (const signalOrUpdater of Object.values(this.internalStateShape)) { + if (signalOrUpdater && !isUpdater(signalOrUpdater)) { + signalOrUpdater.subscribe(this.scheduledNotify.bind(this)); + } + } + } + [connectContext](runtimeAdapter) { + runtimeAdapter.provideContext(stateDefinition, this); + for (const connectContext2 of this.contextConsumptionQueue) { + connectContext2(runtimeAdapter); + } + } + [disconnectContext](componentId) { + const unsubArray = this.contextUnsubscribes.get(componentId); + if (!unsubArray) { + return; + } + while (unsubArray.length !== 0) { + unsubArray.pop()(); + } + } + shareableContext() { + const contextAtom = new ContextAtomSignal(void 0); + const updateContextAtom = () => { + const valueWithUpdaters = this.value; + const filteredValue = Object.fromEntries( + Object.entries(valueWithUpdaters).filter( + ([, valueOrUpdater]) => !isUpdater(valueOrUpdater) + ) + ); + contextAtom[atomSetter](Object.freeze(filteredValue)); + }; + updateContextAtom(); + this.subscribe(updateContextAtom); + return contextAtom; + } + computeValue() { + const computedValue = Object.fromEntries( + Object.entries(this.internalStateShape).filter(([, signalOrUpdater]) => signalOrUpdater).map(([key, signalOrUpdater]) => { + if (isUpdater(signalOrUpdater) || signalOrUpdater._id === contextID) { + return [key, signalOrUpdater]; + } + return [key, signalOrUpdater.value]; + }) + ); + this._value = Object.freeze(computedValue); + this.isStale = false; + } + scheduledNotify() { + this.isStale = true; + if (!this.isNotifyScheduled) { + queueMicrotask(() => { + this.isNotifyScheduled = false; + super.notify(); + }); + this.isNotifyScheduled = true; + } + } + get value() { + if (this.isStale) { + this.computeValue(); + } + return this._value; + } + } + return new StateManagerSignal(); + }; + return stateDefinition; +}; + +// src/contextful-lwc.ts +// import { LightningElement } from "lwc"; + +// src/event.ts +var symbolContextKey = Symbol.for("context"); +var EVENT_NAME = "lightning:context-request"; +var ContextRequestEvent = class extends CustomEvent { + constructor(detail) { + super(EVENT_NAME, { + bubbles: true, + composed: true, + detail: { ...detail, key: symbolContextKey } + }); + } +}; + +// src/contextful-lwc.ts +// var ContextfulLightningElement = class extends LightningElement { +// connectedCallback() { +// this.setupContextReactivity(); +// } +// disconnectedCallback() { +// this.cleanupContext(); +// } +// setupContextReactivity() { +// const contextfulFields = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter( +// (propName) => this[propName]?.[connectContext] +// ); +// if (contextfulFields.length === 0) { +// return; +// } +// const el = this; +// let isProvidingContext = false; +// const providedContextVarieties = /* @__PURE__ */ new Map(); +// const contextRuntimeAdapter = { +// isServerSide: false, +// component: this, +// provideContext(contextVariety, providedContextSignal) { +// if (!isProvidingContext) { +// isProvidingContext = true; +// el.addEventListener("lightning:context-request", (event) => { +// if (event.detail.key === symbolContextKey && providedContextVarieties.has(event.detail.contextVariety)) { +// event.stopImmediatePropagation(); +// const providedContextSignal2 = providedContextVarieties.get( +// event.detail.contextVariety +// ); +// event.detail.callback(providedContextSignal2); +// } +// }); +// } +// let multipleContextWarningShown = false; +// if (providedContextVarieties.has(contextVariety)) { +// if (!multipleContextWarningShown) { +// multipleContextWarningShown = true; +// console.error( +// "Multiple contexts of the same variety were provided. Only the first context will be used." +// ); +// } +// return; +// } +// providedContextVarieties.set(contextVariety, providedContextSignal); +// }, +// consumeContext(contextVariety, contextProvidedCallback) { +// const event = new ContextRequestEvent({ +// contextVariety, +// callback: contextProvidedCallback +// }); +// el.dispatchEvent(event); +// } +// }; +// for (const contextfulField of contextfulFields) { +// this[contextfulField][connectContext](contextRuntimeAdapter); +// } +// } +// cleanupContext() { +// const contextfulFields = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter( +// (propName) => this[propName]?.[disconnectContext] +// ); +// if (contextfulFields.length === 0) { +// return; +// } +// for (const contextfulField of contextfulFields) { +// this[contextfulField][disconnectContext](this); +// } +// } +// }; + +// src/core.ts +// var trustedSignalSet = /* @__PURE__ */ new WeakSet(); +// lwcSetTrustedSignalSet(trustedSignalSet); +// setTrustedSignalSet(trustedSignalSet); + +const nameStateFactory = defineState((atom, computed, update, fromContext) => (initialName = 'foo') => { + const name = atom(initialName); + + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); + + return { + name, + updateName, + }; +}); + +const consumeStateFactory = defineState((atom, computed, update, fromContext) => (initialName = 'bar') => { + const name = atom(initialName); + const context = fromContext(nameStateFactory); + + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); + + return { + name, + updateName, + context + }; +}); + + +export { + defineState, + nameStateFactory, + consumeStateFactory +}; +/* @lwc/state v0.4.2 */ \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html new file mode 100644 index 0000000000..d365273098 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js index e865dec115..4f03c3f486 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js @@ -1,7 +1,9 @@ import { LightningElement, api } from 'lwc'; +import { nameStateFactory } from 'x/state'; -export default class Test extends LightningElement { +export default class TestSymbol extends LightningElement { @api connect; + random = nameStateFactory(); connectedCallback() { this.connect(this); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html new file mode 100644 index 0000000000..9bb4d7be75 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.js new file mode 100644 index 0000000000..f806f86f7e --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; +import { consumeStateFactory } from 'x/state'; + +export default class TestChildSymbol extends LightningElement { + randomChild = consumeStateFactory(); + + // connectedCallback() { + // debugger; + // } +} diff --git a/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js b/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js index 4cd8e74080..89aa5f078a 100644 --- a/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js @@ -64,7 +64,7 @@ const faceSanityTest = (tagName, ctor) => { form.appendChild(face); }); - it('cannot access formAssociated outside of a component', () => { + fit('cannot access formAssociated outside of a component', () => { expect(() => face.formAssociated).toLogWarningDev( /formAssociated cannot be accessed outside of a component. Set the value within the component class./ ); diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts new file mode 100644 index 0000000000..0e1e4e8c06 --- /dev/null +++ b/packages/@lwc/shared/src/context.ts @@ -0,0 +1,3 @@ +export const connectContext = Symbol('connectContext'); +export const disconnectContext = Symbol('disconnectContext'); +export const symbolContextKey = Symbol('context'); \ No newline at end of file diff --git a/packages/@lwc/shared/src/index.ts b/packages/@lwc/shared/src/index.ts index 9d2c1209c5..075f150aa4 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'; From 9da9f19d0667531b4a0d562ce339841866f03312 Mon Sep 17 00:00:00 2001 From: rax-it Date: Tue, 3 Dec 2024 16:41:53 -0800 Subject: [PATCH 02/11] chore: write basic test --- packages/@lwc/engine-core/src/framework/vm.ts | 34 +++++++------------ .../index.spec.js | 14 +++++++- .../x/contextChild/contextChild.html | 3 ++ .../contextChild.js} | 6 ++-- .../x/contextParent/contextParent.html | 4 +++ .../x/contextParent/contextParent.js | 6 ++++ .../x/test/test.html | 4 --- .../x/test/test.js | 4 +-- .../x/testChild/testChild.html | 3 -- .../component/face-callbacks/index.spec.js | 2 +- 10 files changed, 44 insertions(+), 36 deletions(-) create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html rename packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/{testChild/testChild.js => contextChild/contextChild.js} (65%) create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.html create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js delete mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html delete mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 5590593706..97a75c4f67 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -11,8 +11,11 @@ import { assert, create, defineProperty, + entries, getPrototypeOf, + getOwnPropertyDescriptors, getOwnPropertyNames, + keys, isArray, isFalse, isFunction, @@ -226,8 +229,6 @@ export interface VM { * API version associated with this VM */ apiVersion: APIVersion; - - contextfulFieldsOrProps?: string[]; } type VMAssociable = HostNode | LightningElement; @@ -435,24 +436,9 @@ export function createVM( installWireAdapters(vm); } - // gatherContextfulFields(vm); - return vm; } -function gatherContextfulFields(vm: VM) { - const { component } = vm; - try { - vm.contextfulFieldsOrProps = getOwnPropertyNames(getPrototypeOf(component)).filter( - (propName) => (component as any)[propName]?.[connectContext] - ); - } catch(e) { - console.error(e); - debugger; - } - -} - function validateComponentStylesheets(vm: VM, stylesheets: Stylesheets): boolean { let valid = true; @@ -744,8 +730,7 @@ export function runConnectedCallback(vm: VM) { logOperationEnd(OperationId.ConnectedCallback, vm); } // Setup context after connected callback is executed - gatherContextfulFields(vm) - // setupContext(vm); + setupContext(vm); // This test only makes sense in the browser, with synthetic lifecycle, and when reporting is enabled or // we're in dev mode. This is to detect a particular issue with synthetic lifecycle. if ( @@ -771,8 +756,16 @@ export function runConnectedCallback(vm: VM) { } function setupContext(vm: VM) { - const { contextfulFieldsOrProps, component } = vm; + const { component } = vm; + let contextfulFieldsOrProps; + try { + const enumerableKeys = keys(getPrototypeOf(component)); + contextfulFieldsOrProps = enumerableKeys.filter((propName) => ((component as any)[propName]?.[connectContext])); + } catch (e) { + // noop + } + if (!contextfulFieldsOrProps || contextfulFieldsOrProps.length === 0) { return; } @@ -789,7 +782,6 @@ function setupContext(vm: VM) { if (!isProvidingContext) { isProvidingContext = true; - // todo: fix typing component.addEventListener('lightning:context-request', (event: any) => { if ( event.detail.key === symbolContextKey && diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js index 14978ef0ee..9f3ad0f705 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js @@ -4,7 +4,8 @@ import { customElementCallbackReactionErrorListener } from 'test-utils'; import Test from 'x/test'; import ConnectedCallbackThrow from 'x/connectedCallbackThrow'; import XSlottedParent from 'x/slottedParent'; -import TestChild from 'x/testChild'; +import ContextParent from 'x/contextParent'; +import ContextChild from 'x/contextChild'; function testConnectSlot(name, fn) { it(`should invoke the connectedCallback the root element is added in the DOM via ${name}`, () => { @@ -64,3 +65,14 @@ describe('addEventListner in `connectedCallback`', () => { }); }); }); + +describe('context', () => { + it('connects contextful fields when running connectedCallback', () => { + const elm = createElement('x-context-parent', { is: ContextParent }); + document.body.appendChild(elm); + const child = elm.shadowRoot.querySelector('x-context-child'); + + expect(child).toBeDefined(); + expect(child.innerText).toContain('foo'); + }); +}); \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html new file mode 100644 index 0000000000..7051f978f8 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.js similarity index 65% rename from packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.js rename to packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.js index f806f86f7e..a3ef5dfb07 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.js @@ -4,7 +4,7 @@ import { consumeStateFactory } from 'x/state'; export default class TestChildSymbol extends LightningElement { randomChild = consumeStateFactory(); - // connectedCallback() { - // debugger; - // } + get name() { + return this.randomChild.value.context?.value?.name ?? 'not available'; + } } diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.html b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.html new file mode 100644 index 0000000000..d4a51fe57a --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js new file mode 100644 index 0000000000..4505103a46 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js @@ -0,0 +1,6 @@ +import { LightningElement, api } from 'lwc'; +import { nameStateFactory } from 'x/state'; + +export default class ContextParent extends LightningElement { + random = nameStateFactory(); +} diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html deleted file mode 100644 index d365273098..0000000000 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.html +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js index 4f03c3f486..e865dec115 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/test/test.js @@ -1,9 +1,7 @@ import { LightningElement, api } from 'lwc'; -import { nameStateFactory } from 'x/state'; -export default class TestSymbol extends LightningElement { +export default class Test extends LightningElement { @api connect; - random = nameStateFactory(); connectedCallback() { this.connect(this); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html deleted file mode 100644 index 9bb4d7be75..0000000000 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/testChild/testChild.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js b/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js index 89aa5f078a..4cd8e74080 100644 --- a/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/face-callbacks/index.spec.js @@ -64,7 +64,7 @@ const faceSanityTest = (tagName, ctor) => { form.appendChild(face); }); - fit('cannot access formAssociated outside of a component', () => { + it('cannot access formAssociated outside of a component', () => { expect(() => face.formAssociated).toLogWarningDev( /formAssociated cannot be accessed outside of a component. Set the value within the component class./ ); From 37a5598e2cab9b40ff871a2ce11c87c21133b596 Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 4 Dec 2024 15:11:50 -0800 Subject: [PATCH 03/11] chore: shared symbol key --- .../@lwc/engine-core/src/framework/context.ts | 11 +-- .../@lwc/engine-core/src/framework/main.ts | 2 +- packages/@lwc/engine-core/src/framework/vm.ts | 22 +++-- packages/@lwc/engine-dom/src/index.ts | 1 + .../scripts/karma-configs/test/base.js | 4 +- .../karma-plugins/transform-framework.js | 3 + .../index.spec.js | 2 +- .../x/state/state.js | 95 +++---------------- packages/@lwc/shared/src/context.ts | 24 ++++- 9 files changed, 57 insertions(+), 107 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/context.ts b/packages/@lwc/engine-core/src/framework/context.ts index a170e257bf..2e09a39713 100644 --- a/packages/@lwc/engine-core/src/framework/context.ts +++ b/packages/@lwc/engine-core/src/framework/context.ts @@ -1,20 +1,17 @@ +import { ContextEventName, getContextKeys } from '@lwc/shared'; import type { Signal } from '@lwc/signals'; -export const symbolContextKey = Symbol.for('context'); export type ContextProvidedCallback = (contextSignal: Signal) => void; - -const EVENT_NAME = 'lightning:context-request'; - export class ContextRequestEvent extends CustomEvent<{ - key: typeof symbolContextKey; + key: symbol; contextVariety: unknown; callback: ContextProvidedCallback; }> { constructor(detail: { contextVariety: unknown; callback: ContextProvidedCallback }) { - super(EVENT_NAME, { + super(ContextEventName, { bubbles: true, composed: true, - detail: { ...detail, key: symbolContextKey }, + detail: { ...detail, key: getContextKeys().contextEventKey }, }); } } \ No newline at end of file diff --git a/packages/@lwc/engine-core/src/framework/main.ts b/packages/@lwc/engine-core/src/framework/main.ts index c7ecba9c99..1d4903758e 100644 --- a/packages/@lwc/engine-core/src/framework/main.ts +++ b/packages/@lwc/engine-core/src/framework/main.ts @@ -72,5 +72,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 } from '@lwc/shared'; export type { Stylesheet, Stylesheets } from '@lwc/shared'; diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 97a75c4f67..ebc5908028 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -11,9 +11,7 @@ import { assert, create, defineProperty, - entries, getPrototypeOf, - getOwnPropertyDescriptors, getOwnPropertyNames, keys, isArray, @@ -24,8 +22,8 @@ import { isTrue, isUndefined, flattenStylesheets, - // connectContext, - // disconnectContext + getContextKeys, + ContextEventName } from '@lwc/shared'; import { addErrorComponentStack } from '../shared/error'; @@ -70,10 +68,6 @@ import type { HostNode, HostElement, RendererAPI } from './renderer'; import type { Stylesheet, Stylesheets, APIVersion } from '@lwc/shared'; import type { Signal } from '@lwc/signals'; -const connectContext = Symbol.for("connectContext"); -const disconnectContext = Symbol.for("disconnectContext"); -const symbolContextKey = Symbol.for('context'); - type ShadowRootMode = 'open' | 'closed'; export interface TemplateCache { @@ -756,6 +750,14 @@ export function runConnectedCallback(vm: VM) { } function setupContext(vm: VM) { + const contextKeys = getContextKeys(); + + if (!contextKeys) { + // noop + return; + } + + const { connectContext, contextEventKey } = contextKeys; const { component } = vm; let contextfulFieldsOrProps; @@ -782,9 +784,9 @@ function setupContext(vm: VM) { if (!isProvidingContext) { isProvidingContext = true; - component.addEventListener('lightning:context-request', (event: any) => { + component.addEventListener(ContextEventName, (event: any) => { if ( - event.detail.key === symbolContextKey && + event.detail.key === contextEventKey && providedContextVarieties.has(event.detail.contextVariety) ) { event.stopImmediatePropagation(); diff --git a/packages/@lwc/engine-dom/src/index.ts b/packages/@lwc/engine-dom/src/index.ts index 1add267202..d3213fc5a6 100644 --- a/packages/@lwc/engine-dom/src/index.ts +++ b/packages/@lwc/engine-dom/src/index.ts @@ -30,6 +30,7 @@ export { isComponentConstructor, parseFragment, parseSVGFragment, + setContextKeys, setTrustedSignalSet, swapComponent, swapStyle, diff --git a/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js b/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js index c42a76fcb9..1159fb202c 100644 --- a/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js +++ b/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js @@ -28,11 +28,12 @@ const SYNTHETIC_SHADOW = require.resolve('@lwc/synthetic-shadow/dist/index.js'); const LWC_ENGINE = require.resolve('@lwc/engine-dom/dist/index.js'); const WIRE_SERVICE = require.resolve('@lwc/wire-service/dist/index.js'); const ARIA_REFLECTION = require.resolve('@lwc/aria-reflection/dist/index.js'); +const LWC_SHARED = require.resolve('@lwc/shared/dist/index.js'); const TEST_UTILS = require.resolve('../../../helpers/test-utils'); const TEST_SETUP = require.resolve('../../../helpers/test-setup'); -const ALL_FRAMEWORK_FILES = [SYNTHETIC_SHADOW, LWC_ENGINE, WIRE_SERVICE, ARIA_REFLECTION]; +const ALL_FRAMEWORK_FILES = [SYNTHETIC_SHADOW, LWC_ENGINE, WIRE_SERVICE, ARIA_REFLECTION, LWC_SHARED]; // Fix Node warning about >10 event listeners ("Possible EventEmitter memory leak detected"). // This is due to the fact that we are running so many simultaneous rollup commands @@ -52,6 +53,7 @@ function getFiles() { frameworkFiles.push(createPattern(LWC_ENGINE)); frameworkFiles.push(createPattern(WIRE_SERVICE)); frameworkFiles.push(createPattern(TEST_SETUP)); + frameworkFiles.push(createPattern(LWC_SHARED)); return [ ...frameworkFiles, diff --git a/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js b/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js index 85b2f15ee9..97e2143a4b 100644 --- a/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js +++ b/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js @@ -26,7 +26,10 @@ function getIifeName(filename) { } else if (filename.includes('aria-reflection')) { // aria reflection global polyfill does not need an IIFE name return undefined; + } else if (filename.includes('@lwc/shared')) { + return 'LWC_SHARED'; } + throw new Error(`Unknown framework filename, not sure which IIFE name to use: ${filename}`); } diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js index 9f3ad0f705..0a288da850 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js @@ -66,7 +66,7 @@ describe('addEventListner in `connectedCallback`', () => { }); }); -describe('context', () => { +fdescribe('context', () => { it('connects contextful fields when running connectedCallback', () => { const elm = createElement('x-context-parent', { is: ContextParent }); document.body.appendChild(elm); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js index 4005d6a638..35558e0d6d 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js @@ -6,8 +6,7 @@ */ // src/core.ts -// import { setTrustedSignalSet as lwcSetTrustedSignalSet } from "lwc"; -// import { connectContext, disconnectContext } from '@lwc/shared'; +import { setContextKeys } from "lwc"; // node_modules/@lwc/signals/dist/index.js function isFalse$1(value, msg) { @@ -42,8 +41,8 @@ var SignalBaseClass = class { }; // src/shared.ts -var connectContext = Symbol.for("connectContext"); -var disconnectContext = Symbol.for("disconnectContext"); +var connectContext = Symbol("connectContext"); +var disconnectContext = Symbol("disconnectContext"); // src/standalone-context.ts var ConsumedContextSignal = class extends SignalBaseClass { @@ -263,97 +262,20 @@ var defineState = (defineStateCallback) => { }; // src/contextful-lwc.ts -// import { LightningElement } from "lwc"; // src/event.ts -var symbolContextKey = Symbol.for("context"); +var contextEventKey = Symbol("context"); var EVENT_NAME = "lightning:context-request"; var ContextRequestEvent = class extends CustomEvent { constructor(detail) { super(EVENT_NAME, { bubbles: true, composed: true, - detail: { ...detail, key: symbolContextKey } + detail: { ...detail, key: contextEventKey } }); } }; -// src/contextful-lwc.ts -// var ContextfulLightningElement = class extends LightningElement { -// connectedCallback() { -// this.setupContextReactivity(); -// } -// disconnectedCallback() { -// this.cleanupContext(); -// } -// setupContextReactivity() { -// const contextfulFields = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter( -// (propName) => this[propName]?.[connectContext] -// ); -// if (contextfulFields.length === 0) { -// return; -// } -// const el = this; -// let isProvidingContext = false; -// const providedContextVarieties = /* @__PURE__ */ new Map(); -// const contextRuntimeAdapter = { -// isServerSide: false, -// component: this, -// provideContext(contextVariety, providedContextSignal) { -// if (!isProvidingContext) { -// isProvidingContext = true; -// el.addEventListener("lightning:context-request", (event) => { -// if (event.detail.key === symbolContextKey && providedContextVarieties.has(event.detail.contextVariety)) { -// event.stopImmediatePropagation(); -// const providedContextSignal2 = providedContextVarieties.get( -// event.detail.contextVariety -// ); -// event.detail.callback(providedContextSignal2); -// } -// }); -// } -// let multipleContextWarningShown = false; -// if (providedContextVarieties.has(contextVariety)) { -// if (!multipleContextWarningShown) { -// multipleContextWarningShown = true; -// console.error( -// "Multiple contexts of the same variety were provided. Only the first context will be used." -// ); -// } -// return; -// } -// providedContextVarieties.set(contextVariety, providedContextSignal); -// }, -// consumeContext(contextVariety, contextProvidedCallback) { -// const event = new ContextRequestEvent({ -// contextVariety, -// callback: contextProvidedCallback -// }); -// el.dispatchEvent(event); -// } -// }; -// for (const contextfulField of contextfulFields) { -// this[contextfulField][connectContext](contextRuntimeAdapter); -// } -// } -// cleanupContext() { -// const contextfulFields = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter( -// (propName) => this[propName]?.[disconnectContext] -// ); -// if (contextfulFields.length === 0) { -// return; -// } -// for (const contextfulField of contextfulFields) { -// this[contextfulField][disconnectContext](this); -// } -// } -// }; - -// src/core.ts -// var trustedSignalSet = /* @__PURE__ */ new WeakSet(); -// lwcSetTrustedSignalSet(trustedSignalSet); -// setTrustedSignalSet(trustedSignalSet); - const nameStateFactory = defineState((atom, computed, update, fromContext) => (initialName = 'foo') => { const name = atom(initialName); @@ -382,7 +304,12 @@ const consumeStateFactory = defineState((atom, computed, update, fromContext) => }; }); - +setContextKeys({ + connectContext, + disconnectContext, + contextEventKey +}); +debugger; export { defineState, nameStateFactory, diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index 0e1e4e8c06..f4fa786406 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -1,3 +1,21 @@ -export const connectContext = Symbol('connectContext'); -export const disconnectContext = Symbol('disconnectContext'); -export const symbolContextKey = Symbol('context'); \ No newline at end of file +import { isFalse } from './assert'; + +export type ContextKeys = { + connectContext: symbol; + disconnectContext: symbol; + contextEventKey: symbol; +} + +let contextKeys: ContextKeys; + +export function setContextKeys(config: ContextKeys) { + isFalse(contextKeys, 'Context keys are already set!'); + + contextKeys = config; +} + +export function getContextKeys() { + return contextKeys; +} + +export const ContextEventName = 'lightning:context-request'; \ No newline at end of file From f45935cac40f183e7e660df73ec5bb001a22aea3 Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 4 Dec 2024 19:52:28 -0800 Subject: [PATCH 04/11] chore: unsub on disconnect --- packages/@lwc/engine-core/src/framework/vm.ts | 59 +- .../integration-karma/helpers/test-utils.js | 13 + .../scripts/karma-configs/test/base.js | 4 +- .../karma-plugins/transform-framework.js | 3 - .../index.spec.js | 5 +- .../x/state/state.js | 518 +++++++++--------- .../index.spec.js | 16 + .../x/contextChild/contextChild.html | 3 + .../x/contextChild/contextChild.js | 10 + .../x/contextParent/contextParent.html | 5 + .../x/contextParent/contextParent.js | 14 + .../x/state/state.js | 314 +++++++++++ 12 files changed, 674 insertions(+), 290 deletions(-) create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.html create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.html create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js create mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index ebc5908028..ebaa50c9ee 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -23,7 +23,7 @@ import { isUndefined, flattenStylesheets, getContextKeys, - ContextEventName + ContextEventName, } from '@lwc/shared'; import { addErrorComponentStack } from '../shared/error'; @@ -753,22 +753,17 @@ function setupContext(vm: VM) { const contextKeys = getContextKeys(); if (!contextKeys) { - // noop return; } const { connectContext, contextEventKey } = contextKeys; const { component } = vm; - let contextfulFieldsOrProps; + const enumerableKeys = keys(getPrototypeOf(component)); + const contextfulFieldsOrProps = enumerableKeys.filter( + (propName) => (component as any)[propName]?.[connectContext] + ); - try { - const enumerableKeys = keys(getPrototypeOf(component)); - contextfulFieldsOrProps = enumerableKeys.filter((propName) => ((component as any)[propName]?.[connectContext])); - } catch (e) { - // noop - } - - if (!contextfulFieldsOrProps || contextfulFieldsOrProps.length === 0) { + if (contextfulFieldsOrProps.length === 0) { return; } @@ -779,7 +774,7 @@ function setupContext(vm: VM) { component, provideContext( contextVariety: T, - providedContextSignal: Signal, + providedContextSignal: Signal ): void { if (!isProvidingContext) { isProvidingContext = true; @@ -788,13 +783,13 @@ function setupContext(vm: VM) { if ( event.detail.key === contextEventKey && providedContextVarieties.has(event.detail.contextVariety) - ) { + ) { event.stopImmediatePropagation(); const providedContextSignal = providedContextVarieties.get( - event.detail.contextVariety, + event.detail.contextVariety ); event.detail.callback(providedContextSignal); - } + } }); } @@ -803,8 +798,8 @@ function setupContext(vm: VM) { if (providedContextVarieties.has(contextVariety)) { if (!multipleContextWarningShown) { multipleContextWarningShown = true; - console.error( - 'Multiple contexts of the same variety were provided. Only the first context will be used.', + logError( + 'Multiple contexts of the same variety were provided. Only the first context will be used.' ); } return; @@ -814,7 +809,7 @@ function setupContext(vm: VM) { }, consumeContext( contextVariety: T, - contextProvidedCallback: ContextProvidedCallback, + contextProvidedCallback: ContextProvidedCallback ): void { const event = new ContextRequestEvent({ contextVariety, @@ -822,8 +817,8 @@ function setupContext(vm: VM) { }); component.dispatchEvent(event); - } - } + }, + }; for (const contextfulFieldsOrProp of contextfulFieldsOrProps) { (component as any)[contextfulFieldsOrProp][connectContext](contextRuntimeAdapter); @@ -834,6 +829,29 @@ function hasWireAdapters(vm: VM): boolean { return getOwnPropertyNames(vm.def.wire).length > 0; } +function cleanupContext(vm: VM) { + const contextKeys = getContextKeys(); + + if (!contextKeys) { + return; + } + + const { disconnectContext } = contextKeys; + const { component } = vm; + const enumerableKeys = keys(getPrototypeOf(component)); + const contextfulFieldsOrProps = enumerableKeys.filter( + (propName) => (component as any)[propName]?.[disconnectContext] + ); + + if (contextfulFieldsOrProps.length === 0) { + return; + } + + for (const contextfulField of contextfulFieldsOrProps) { + (component as any)[contextfulField][disconnectContext](component); + } +} + function runDisconnectedCallback(vm: VM) { if (process.env.NODE_ENV !== 'production') { assert.isTrue(vm.state !== VMState.disconnected, `${vm} must be inserted.`); @@ -857,6 +875,7 @@ function runDisconnectedCallback(vm: VM) { logOperationEnd(OperationId.DisconnectedCallback, vm); } + cleanupContext(vm); } function runChildNodesDisconnectedCallback(vm: VM) { diff --git a/packages/@lwc/integration-karma/helpers/test-utils.js b/packages/@lwc/integration-karma/helpers/test-utils.js index 2e460729c3..da94466a80 100644 --- a/packages/@lwc/integration-karma/helpers/test-utils.js +++ b/packages/@lwc/integration-karma/helpers/test-utils.js @@ -709,6 +709,18 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { signalValidator.add(signal); } + const contextKeys = { + connectContext: Symbol('connectContext'), + disconnectContext: Symbol('disconnectContext'), + contextEventKey: Symbol('contextEventKey'), + }; + + lwc.setContextKeys(contextKeys); + + function getContextKeys() { + return contextKeys; + } + return { clearRegister, extractDataIds, @@ -736,6 +748,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { catchUnhandledRejectionsAndErrors, addTrustedSignal, expectEquivalentDOM, + getContextKeys, ...apiFeatures, }; })(LWC, jasmine, beforeAll); diff --git a/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js b/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js index 1159fb202c..c42a76fcb9 100644 --- a/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js +++ b/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js @@ -28,12 +28,11 @@ const SYNTHETIC_SHADOW = require.resolve('@lwc/synthetic-shadow/dist/index.js'); const LWC_ENGINE = require.resolve('@lwc/engine-dom/dist/index.js'); const WIRE_SERVICE = require.resolve('@lwc/wire-service/dist/index.js'); const ARIA_REFLECTION = require.resolve('@lwc/aria-reflection/dist/index.js'); -const LWC_SHARED = require.resolve('@lwc/shared/dist/index.js'); const TEST_UTILS = require.resolve('../../../helpers/test-utils'); const TEST_SETUP = require.resolve('../../../helpers/test-setup'); -const ALL_FRAMEWORK_FILES = [SYNTHETIC_SHADOW, LWC_ENGINE, WIRE_SERVICE, ARIA_REFLECTION, LWC_SHARED]; +const ALL_FRAMEWORK_FILES = [SYNTHETIC_SHADOW, LWC_ENGINE, WIRE_SERVICE, ARIA_REFLECTION]; // Fix Node warning about >10 event listeners ("Possible EventEmitter memory leak detected"). // This is due to the fact that we are running so many simultaneous rollup commands @@ -53,7 +52,6 @@ function getFiles() { frameworkFiles.push(createPattern(LWC_ENGINE)); frameworkFiles.push(createPattern(WIRE_SERVICE)); frameworkFiles.push(createPattern(TEST_SETUP)); - frameworkFiles.push(createPattern(LWC_SHARED)); return [ ...frameworkFiles, diff --git a/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js b/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js index 97e2143a4b..85b2f15ee9 100644 --- a/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js +++ b/packages/@lwc/integration-karma/scripts/karma-plugins/transform-framework.js @@ -26,10 +26,7 @@ function getIifeName(filename) { } else if (filename.includes('aria-reflection')) { // aria reflection global polyfill does not need an IIFE name return undefined; - } else if (filename.includes('@lwc/shared')) { - return 'LWC_SHARED'; } - throw new Error(`Unknown framework filename, not sure which IIFE name to use: ${filename}`); } diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js index 0a288da850..3ba3ad7bbc 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js @@ -5,7 +5,6 @@ import Test from 'x/test'; import ConnectedCallbackThrow from 'x/connectedCallbackThrow'; import XSlottedParent from 'x/slottedParent'; import ContextParent from 'x/contextParent'; -import ContextChild from 'x/contextChild'; function testConnectSlot(name, fn) { it(`should invoke the connectedCallback the root element is added in the DOM via ${name}`, () => { @@ -66,7 +65,7 @@ describe('addEventListner in `connectedCallback`', () => { }); }); -fdescribe('context', () => { +describe('context', () => { it('connects contextful fields when running connectedCallback', () => { const elm = createElement('x-context-parent', { is: ContextParent }); document.body.appendChild(elm); @@ -75,4 +74,4 @@ fdescribe('context', () => { expect(child).toBeDefined(); expect(child.innerText).toContain('foo'); }); -}); \ No newline at end of file +}); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js index 35558e0d6d..fea8ac1486 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js @@ -1,318 +1,314 @@ - /* eslint-disable */ /** * This file is generated from repository: git+https://github.com/salesforce/lightning-labs.git - * module: packages/@lwc/state + * module: packages/@lwc/state */ -// src/core.ts -import { setContextKeys } from "lwc"; +import { getContextKeys } from 'test-utils'; // node_modules/@lwc/signals/dist/index.js function isFalse$1(value, msg) { - if (value) { - throw new Error(`Assert Violation: ${msg}`); - } + if (value) { + throw new Error(`Assert Violation: ${msg}`); + } } var trustedSignals; function setTrustedSignalSet(signals) { - isFalse$1(trustedSignals, "Trusted Signal Set is already set!"); - trustedSignals = signals; + isFalse$1(trustedSignals, 'Trusted Signal Set is already set!'); + trustedSignals = signals; } function addTrustedSignal(signal) { - trustedSignals?.add(signal); + trustedSignals?.add(signal); } var SignalBaseClass = class { - constructor() { - this.subscribers = /* @__PURE__ */ new Set(); - addTrustedSignal(this); - } - subscribe(onUpdate) { - this.subscribers.add(onUpdate); - return () => { - this.subscribers.delete(onUpdate); - }; - } - notify() { - for (const subscriber of this.subscribers) { - subscriber(); + constructor() { + this.subscribers = /* @__PURE__ */ new Set(); + addTrustedSignal(this); + } + subscribe(onUpdate) { + this.subscribers.add(onUpdate); + return () => { + this.subscribers.delete(onUpdate); + }; + } + notify() { + for (const subscriber of this.subscribers) { + subscriber(); + } } - } }; // src/shared.ts -var connectContext = Symbol("connectContext"); -var disconnectContext = Symbol("disconnectContext"); +var connectContext = getContextKeys().connectContext; +var disconnectContext = getContextKeys().disconnectContext; // src/standalone-context.ts var ConsumedContextSignal = class extends SignalBaseClass { - constructor(stateDef) { - super(); - this._value = null; - this.unsubscribe = () => { - }; - this.desiredStateDef = stateDef; - } - get value() { - return this._value; - } - [connectContext](runtimeAdapter) { - if (!runtimeAdapter) { - throw new Error( - "Implementation error: runtimeAdapter must be present at the time of connect." - ); + constructor(stateDef) { + super(); + this._value = null; + this.unsubscribe = () => {}; + this.desiredStateDef = stateDef; } - runtimeAdapter.consumeContext( - this.desiredStateDef, - (providedContextSignal) => { - this._value = providedContextSignal.value; - this.notify(); - this.unsubscribe = providedContextSignal.subscribe(() => { - this._value = providedContextSignal.value; - this.notify(); + get value() { + return this._value; + } + [connectContext](runtimeAdapter) { + if (!runtimeAdapter) { + throw new Error( + 'Implementation error: runtimeAdapter must be present at the time of connect.' + ); + } + runtimeAdapter.consumeContext(this.desiredStateDef, (providedContextSignal) => { + this._value = providedContextSignal.value; + this.notify(); + this.unsubscribe = providedContextSignal.subscribe(() => { + this._value = providedContextSignal.value; + this.notify(); + }); }); - } - ); - } - [disconnectContext](_componentId) { - this.unsubscribe(); - this.unsubscribe = () => { - }; - } + } + [disconnectContext](_componentId) { + this.unsubscribe(); + this.unsubscribe = () => {}; + } }; // src/index.ts -var atomSetter = Symbol("atomSetter"); -var contextID = Symbol("contextID"); +var atomSetter = Symbol('atomSetter'); +var contextID = Symbol('contextID'); var AtomSignal = class extends SignalBaseClass { - constructor(value) { - super(); - this._value = value; - } - [atomSetter](value) { - this._value = value; - this.notify(); - } - get value() { - return this._value; - } -}; -var ContextAtomSignal = class extends AtomSignal { - constructor() { - super(...arguments); - this._id = contextID; - } -}; -var ComputedSignal = class extends SignalBaseClass { - constructor(inputSignalsObj, computer) { - super(); - this.isStale = true; - this.computer = computer; - this.dependencies = inputSignalsObj; - const onUpdate = () => { - this.isStale = true; - this.notify(); - }; - for (const signal of Object.values(inputSignalsObj)) { - signal.subscribe(onUpdate); + constructor(value) { + super(); + this._value = value; } - } - computeValue() { - const dependencyValues = {}; - for (const [signalName, signal] of Object.entries(this.dependencies)) { - dependencyValues[signalName] = signal.value; + [atomSetter](value) { + this._value = value; + this.notify(); } - this.isStale = false; - this._value = this.computer(dependencyValues); - } - notify() { - this.isStale = true; - super.notify(); - } - get value() { - if (this.isStale) { - this.computeValue(); + get value() { + return this._value; } - return this._value; - } }; -var isUpdater = (signalOrUpdater) => typeof signalOrUpdater === "function"; -var atom = (initialValue) => new AtomSignal(initialValue); -var computed = (inputSignalsObj, computer) => new ComputedSignal(inputSignalsObj, computer); -var update = (signalsToUpdate, userProvidedUpdaterFn) => { - return (...uniqueArgs) => { - const signalValues = {}; - for (const [signalName, signal] of Object.entries(signalsToUpdate)) { - signalValues[signalName] = signal.value; - } - const newValues = userProvidedUpdaterFn(signalValues, ...uniqueArgs); - for (const [atomName, newValue] of Object.entries(newValues)) { - signalsToUpdate[atomName][atomSetter](newValue); +var ContextAtomSignal = class extends AtomSignal { + constructor() { + super(...arguments); + this._id = contextID; } - }; }; -var defineState = (defineStateCallback) => { - const stateDefinition = (...args) => { - class StateManagerSignal extends SignalBaseClass { - constructor() { +var ComputedSignal = class extends SignalBaseClass { + constructor(inputSignalsObj, computer) { super(); this.isStale = true; - this.isNotifyScheduled = false; - // biome-ignore lint/suspicious/noExplicitAny: we actually do want this, thanks - this.contextSignals = /* @__PURE__ */ new Map(); - this.contextConsumptionQueue = []; - this.contextUnsubscribes = /* @__PURE__ */ new WeakMap(); - const fromContext2 = (contextVarietyUniqueId) => { - if (this.contextSignals.has(contextVarietyUniqueId)) { - return this.contextSignals.get(contextVarietyUniqueId); - } - const localContextSignal = new ContextAtomSignal(void 0); - this.contextSignals.set(contextVarietyUniqueId, localContextSignal); - this.contextConsumptionQueue.push((runtimeAdapter) => { - if (!runtimeAdapter) { - throw new Error( - "Implementation error: runtimeAdapter must be present at the time of connect." - ); - } - runtimeAdapter.consumeContext( - contextVarietyUniqueId, - (providedContextSignal) => { - localContextSignal[atomSetter](providedContextSignal.value); - const unsub = providedContextSignal.subscribe(() => { - localContextSignal[atomSetter](providedContextSignal.value); - }); - if (!this.contextUnsubscribes.has(runtimeAdapter.component)) { - this.contextUnsubscribes.set(runtimeAdapter.component, []); - } - this.contextUnsubscribes.get(runtimeAdapter.component).push(unsub); - } - ); - }); - return localContextSignal; + this.computer = computer; + this.dependencies = inputSignalsObj; + const onUpdate = () => { + this.isStale = true; + this.notify(); }; - this.internalStateShape = defineStateCallback(atom, computed, update, fromContext2)(...args); - for (const signalOrUpdater of Object.values(this.internalStateShape)) { - if (signalOrUpdater && !isUpdater(signalOrUpdater)) { - signalOrUpdater.subscribe(this.scheduledNotify.bind(this)); - } - } - } - [connectContext](runtimeAdapter) { - runtimeAdapter.provideContext(stateDefinition, this); - for (const connectContext2 of this.contextConsumptionQueue) { - connectContext2(runtimeAdapter); + for (const signal of Object.values(inputSignalsObj)) { + signal.subscribe(onUpdate); } - } - [disconnectContext](componentId) { - const unsubArray = this.contextUnsubscribes.get(componentId); - if (!unsubArray) { - return; - } - while (unsubArray.length !== 0) { - unsubArray.pop()(); + } + computeValue() { + const dependencyValues = {}; + for (const [signalName, signal] of Object.entries(this.dependencies)) { + dependencyValues[signalName] = signal.value; } - } - shareableContext() { - const contextAtom = new ContextAtomSignal(void 0); - const updateContextAtom = () => { - const valueWithUpdaters = this.value; - const filteredValue = Object.fromEntries( - Object.entries(valueWithUpdaters).filter( - ([, valueOrUpdater]) => !isUpdater(valueOrUpdater) - ) - ); - contextAtom[atomSetter](Object.freeze(filteredValue)); - }; - updateContextAtom(); - this.subscribe(updateContextAtom); - return contextAtom; - } - computeValue() { - const computedValue = Object.fromEntries( - Object.entries(this.internalStateShape).filter(([, signalOrUpdater]) => signalOrUpdater).map(([key, signalOrUpdater]) => { - if (isUpdater(signalOrUpdater) || signalOrUpdater._id === contextID) { - return [key, signalOrUpdater]; - } - return [key, signalOrUpdater.value]; - }) - ); - this._value = Object.freeze(computedValue); this.isStale = false; - } - scheduledNotify() { + this._value = this.computer(dependencyValues); + } + notify() { this.isStale = true; - if (!this.isNotifyScheduled) { - queueMicrotask(() => { - this.isNotifyScheduled = false; - super.notify(); - }); - this.isNotifyScheduled = true; - } - } - get value() { + super.notify(); + } + get value() { if (this.isStale) { - this.computeValue(); + this.computeValue(); } return this._value; - } } - return new StateManagerSignal(); - }; - return stateDefinition; +}; +var isUpdater = (signalOrUpdater) => typeof signalOrUpdater === 'function'; +var atom = (initialValue) => new AtomSignal(initialValue); +var computed = (inputSignalsObj, computer) => new ComputedSignal(inputSignalsObj, computer); +var update = (signalsToUpdate, userProvidedUpdaterFn) => { + return (...uniqueArgs) => { + const signalValues = {}; + for (const [signalName, signal] of Object.entries(signalsToUpdate)) { + signalValues[signalName] = signal.value; + } + const newValues = userProvidedUpdaterFn(signalValues, ...uniqueArgs); + for (const [atomName, newValue] of Object.entries(newValues)) { + signalsToUpdate[atomName][atomSetter](newValue); + } + }; +}; +var defineState = (defineStateCallback) => { + const stateDefinition = (...args) => { + class StateManagerSignal extends SignalBaseClass { + constructor() { + super(); + this.isStale = true; + this.isNotifyScheduled = false; + // biome-ignore lint/suspicious/noExplicitAny: we actually do want this, thanks + this.contextSignals = /* @__PURE__ */ new Map(); + this.contextConsumptionQueue = []; + this.contextUnsubscribes = /* @__PURE__ */ new WeakMap(); + const fromContext2 = (contextVarietyUniqueId) => { + if (this.contextSignals.has(contextVarietyUniqueId)) { + return this.contextSignals.get(contextVarietyUniqueId); + } + const localContextSignal = new ContextAtomSignal(void 0); + this.contextSignals.set(contextVarietyUniqueId, localContextSignal); + this.contextConsumptionQueue.push((runtimeAdapter) => { + if (!runtimeAdapter) { + throw new Error( + 'Implementation error: runtimeAdapter must be present at the time of connect.' + ); + } + runtimeAdapter.consumeContext( + contextVarietyUniqueId, + (providedContextSignal) => { + localContextSignal[atomSetter](providedContextSignal.value); + const unsub = providedContextSignal.subscribe(() => { + localContextSignal[atomSetter](providedContextSignal.value); + }); + if (!this.contextUnsubscribes.has(runtimeAdapter.component)) { + this.contextUnsubscribes.set(runtimeAdapter.component, []); + } + this.contextUnsubscribes.get(runtimeAdapter.component).push(unsub); + } + ); + }); + return localContextSignal; + }; + this.internalStateShape = defineStateCallback( + atom, + computed, + update, + fromContext2 + )(...args); + for (const signalOrUpdater of Object.values(this.internalStateShape)) { + if (signalOrUpdater && !isUpdater(signalOrUpdater)) { + signalOrUpdater.subscribe(this.scheduledNotify.bind(this)); + } + } + } + [connectContext](runtimeAdapter) { + runtimeAdapter.provideContext(stateDefinition, this); + for (const connectContext2 of this.contextConsumptionQueue) { + connectContext2(runtimeAdapter); + } + } + [disconnectContext](componentId) { + const unsubArray = this.contextUnsubscribes.get(componentId); + if (!unsubArray) { + return; + } + while (unsubArray.length !== 0) { + unsubArray.pop()(); + } + } + shareableContext() { + const contextAtom = new ContextAtomSignal(void 0); + const updateContextAtom = () => { + const valueWithUpdaters = this.value; + const filteredValue = Object.fromEntries( + Object.entries(valueWithUpdaters).filter( + ([, valueOrUpdater]) => !isUpdater(valueOrUpdater) + ) + ); + contextAtom[atomSetter](Object.freeze(filteredValue)); + }; + updateContextAtom(); + this.subscribe(updateContextAtom); + return contextAtom; + } + computeValue() { + const computedValue = Object.fromEntries( + Object.entries(this.internalStateShape) + .filter(([, signalOrUpdater]) => signalOrUpdater) + .map(([key, signalOrUpdater]) => { + if (isUpdater(signalOrUpdater) || signalOrUpdater._id === contextID) { + return [key, signalOrUpdater]; + } + return [key, signalOrUpdater.value]; + }) + ); + this._value = Object.freeze(computedValue); + this.isStale = false; + } + scheduledNotify() { + this.isStale = true; + if (!this.isNotifyScheduled) { + queueMicrotask(() => { + this.isNotifyScheduled = false; + super.notify(); + }); + this.isNotifyScheduled = true; + } + } + get value() { + if (this.isStale) { + this.computeValue(); + } + return this._value; + } + } + return new StateManagerSignal(); + }; + return stateDefinition; }; // src/contextful-lwc.ts // src/event.ts -var contextEventKey = Symbol("context"); -var EVENT_NAME = "lightning:context-request"; +var contextEventKey = getContextKeys().contextEventKey; +var EVENT_NAME = 'lightning:context-request'; var ContextRequestEvent = class extends CustomEvent { - constructor(detail) { - super(EVENT_NAME, { - bubbles: true, - composed: true, - detail: { ...detail, key: contextEventKey } - }); - } + constructor(detail) { + super(EVENT_NAME, { + bubbles: true, + composed: true, + detail: { ...detail, key: contextEventKey }, + }); + } }; -const nameStateFactory = defineState((atom, computed, update, fromContext) => (initialName = 'foo') => { - const name = atom(initialName); +const nameStateFactory = defineState( + (atom, computed, update, fromContext) => + (initialName = 'foo') => { + const name = atom(initialName); - const updateName = update({ name }, (_, newName) => ({ - name: newName, - })); + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); - return { - name, - updateName, - }; -}); + return { + name, + updateName, + }; + } +); -const consumeStateFactory = defineState((atom, computed, update, fromContext) => (initialName = 'bar') => { - const name = atom(initialName); - const context = fromContext(nameStateFactory); - - const updateName = update({ name }, (_, newName) => ({ - name: newName, - })); - - return { - name, - updateName, - context - }; -}); +const consumeStateFactory = defineState( + (atom, computed, update, fromContext) => + (initialName = 'bar') => { + const name = atom(initialName); + const context = fromContext(nameStateFactory); -setContextKeys({ - connectContext, - disconnectContext, - contextEventKey -}); -debugger; -export { - defineState, - nameStateFactory, - consumeStateFactory -}; -/* @lwc/state v0.4.2 */ \ No newline at end of file + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); + + return { + name, + updateName, + context, + }; + } +); + +export { defineState, nameStateFactory, consumeStateFactory }; +/* @lwc/state v0.4.2 */ diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js index 765c954e46..66d5fdcef2 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js @@ -6,6 +6,8 @@ import Test from 'x/test'; import DisconnectedCallbackThrow from 'x/disconnectedCallbackThrow'; import DualTemplate from 'x/dualTemplate'; import ExplicitRender from 'x/explicitRender'; +import ContextParent from 'x/contextParent'; +import { nameStateFactory } from 'x/state'; function testDisconnectSlot(name, fn) { it(`should invoke the disconnectedCallback when root element is removed from the DOM via ${name}`, () => { @@ -189,3 +191,17 @@ describe('disconnectedCallback for components with a explicit render()', () => { }); }); }); + +describe('context', () => { + it('removing child unsubscribes from context subscription during disconnect', async () => { + const elm = createElement('x-context-parent', { is: ContextParent }); + const state = nameStateFactory(); + elm.state = state; + document.body.appendChild(elm); + + expect(state.subscribers.size).toBe(1); + elm.hideChild = true; + await new Promise((resolve) => requestAnimationFrame(resolve)); + expect(state.subscribers.size).toBe(0); + }); +}); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.html b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.html new file mode 100644 index 0000000000..7051f978f8 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js new file mode 100644 index 0000000000..a3ef5dfb07 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js @@ -0,0 +1,10 @@ +import { LightningElement } from 'lwc'; +import { consumeStateFactory } from 'x/state'; + +export default class TestChildSymbol extends LightningElement { + randomChild = consumeStateFactory(); + + get name() { + return this.randomChild.value.context?.value?.name ?? 'not available'; + } +} diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.html b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.html new file mode 100644 index 0000000000..f1aaa7ebd3 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js new file mode 100644 index 0000000000..c8eebea520 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js @@ -0,0 +1,14 @@ +import { LightningElement, api } from 'lwc'; +// import { nameStateFactory } from 'x/state'; + +export default class ContextParent extends LightningElement { + @api + state; + + @api + hideChild = false; + + get showChild() { + return !this.hideChild; + } +} diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js new file mode 100644 index 0000000000..fea8ac1486 --- /dev/null +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js @@ -0,0 +1,314 @@ +/* eslint-disable */ +/** + * This file is generated from repository: git+https://github.com/salesforce/lightning-labs.git + * module: packages/@lwc/state + */ + +import { getContextKeys } from 'test-utils'; + +// node_modules/@lwc/signals/dist/index.js +function isFalse$1(value, msg) { + if (value) { + throw new Error(`Assert Violation: ${msg}`); + } +} +var trustedSignals; +function setTrustedSignalSet(signals) { + isFalse$1(trustedSignals, 'Trusted Signal Set is already set!'); + trustedSignals = signals; +} +function addTrustedSignal(signal) { + trustedSignals?.add(signal); +} +var SignalBaseClass = class { + constructor() { + this.subscribers = /* @__PURE__ */ new Set(); + addTrustedSignal(this); + } + subscribe(onUpdate) { + this.subscribers.add(onUpdate); + return () => { + this.subscribers.delete(onUpdate); + }; + } + notify() { + for (const subscriber of this.subscribers) { + subscriber(); + } + } +}; + +// src/shared.ts +var connectContext = getContextKeys().connectContext; +var disconnectContext = getContextKeys().disconnectContext; + +// src/standalone-context.ts +var ConsumedContextSignal = class extends SignalBaseClass { + constructor(stateDef) { + super(); + this._value = null; + this.unsubscribe = () => {}; + this.desiredStateDef = stateDef; + } + get value() { + return this._value; + } + [connectContext](runtimeAdapter) { + if (!runtimeAdapter) { + throw new Error( + 'Implementation error: runtimeAdapter must be present at the time of connect.' + ); + } + runtimeAdapter.consumeContext(this.desiredStateDef, (providedContextSignal) => { + this._value = providedContextSignal.value; + this.notify(); + this.unsubscribe = providedContextSignal.subscribe(() => { + this._value = providedContextSignal.value; + this.notify(); + }); + }); + } + [disconnectContext](_componentId) { + this.unsubscribe(); + this.unsubscribe = () => {}; + } +}; + +// src/index.ts +var atomSetter = Symbol('atomSetter'); +var contextID = Symbol('contextID'); +var AtomSignal = class extends SignalBaseClass { + constructor(value) { + super(); + this._value = value; + } + [atomSetter](value) { + this._value = value; + this.notify(); + } + get value() { + return this._value; + } +}; +var ContextAtomSignal = class extends AtomSignal { + constructor() { + super(...arguments); + this._id = contextID; + } +}; +var ComputedSignal = class extends SignalBaseClass { + constructor(inputSignalsObj, computer) { + super(); + this.isStale = true; + this.computer = computer; + this.dependencies = inputSignalsObj; + const onUpdate = () => { + this.isStale = true; + this.notify(); + }; + for (const signal of Object.values(inputSignalsObj)) { + signal.subscribe(onUpdate); + } + } + computeValue() { + const dependencyValues = {}; + for (const [signalName, signal] of Object.entries(this.dependencies)) { + dependencyValues[signalName] = signal.value; + } + this.isStale = false; + this._value = this.computer(dependencyValues); + } + notify() { + this.isStale = true; + super.notify(); + } + get value() { + if (this.isStale) { + this.computeValue(); + } + return this._value; + } +}; +var isUpdater = (signalOrUpdater) => typeof signalOrUpdater === 'function'; +var atom = (initialValue) => new AtomSignal(initialValue); +var computed = (inputSignalsObj, computer) => new ComputedSignal(inputSignalsObj, computer); +var update = (signalsToUpdate, userProvidedUpdaterFn) => { + return (...uniqueArgs) => { + const signalValues = {}; + for (const [signalName, signal] of Object.entries(signalsToUpdate)) { + signalValues[signalName] = signal.value; + } + const newValues = userProvidedUpdaterFn(signalValues, ...uniqueArgs); + for (const [atomName, newValue] of Object.entries(newValues)) { + signalsToUpdate[atomName][atomSetter](newValue); + } + }; +}; +var defineState = (defineStateCallback) => { + const stateDefinition = (...args) => { + class StateManagerSignal extends SignalBaseClass { + constructor() { + super(); + this.isStale = true; + this.isNotifyScheduled = false; + // biome-ignore lint/suspicious/noExplicitAny: we actually do want this, thanks + this.contextSignals = /* @__PURE__ */ new Map(); + this.contextConsumptionQueue = []; + this.contextUnsubscribes = /* @__PURE__ */ new WeakMap(); + const fromContext2 = (contextVarietyUniqueId) => { + if (this.contextSignals.has(contextVarietyUniqueId)) { + return this.contextSignals.get(contextVarietyUniqueId); + } + const localContextSignal = new ContextAtomSignal(void 0); + this.contextSignals.set(contextVarietyUniqueId, localContextSignal); + this.contextConsumptionQueue.push((runtimeAdapter) => { + if (!runtimeAdapter) { + throw new Error( + 'Implementation error: runtimeAdapter must be present at the time of connect.' + ); + } + runtimeAdapter.consumeContext( + contextVarietyUniqueId, + (providedContextSignal) => { + localContextSignal[atomSetter](providedContextSignal.value); + const unsub = providedContextSignal.subscribe(() => { + localContextSignal[atomSetter](providedContextSignal.value); + }); + if (!this.contextUnsubscribes.has(runtimeAdapter.component)) { + this.contextUnsubscribes.set(runtimeAdapter.component, []); + } + this.contextUnsubscribes.get(runtimeAdapter.component).push(unsub); + } + ); + }); + return localContextSignal; + }; + this.internalStateShape = defineStateCallback( + atom, + computed, + update, + fromContext2 + )(...args); + for (const signalOrUpdater of Object.values(this.internalStateShape)) { + if (signalOrUpdater && !isUpdater(signalOrUpdater)) { + signalOrUpdater.subscribe(this.scheduledNotify.bind(this)); + } + } + } + [connectContext](runtimeAdapter) { + runtimeAdapter.provideContext(stateDefinition, this); + for (const connectContext2 of this.contextConsumptionQueue) { + connectContext2(runtimeAdapter); + } + } + [disconnectContext](componentId) { + const unsubArray = this.contextUnsubscribes.get(componentId); + if (!unsubArray) { + return; + } + while (unsubArray.length !== 0) { + unsubArray.pop()(); + } + } + shareableContext() { + const contextAtom = new ContextAtomSignal(void 0); + const updateContextAtom = () => { + const valueWithUpdaters = this.value; + const filteredValue = Object.fromEntries( + Object.entries(valueWithUpdaters).filter( + ([, valueOrUpdater]) => !isUpdater(valueOrUpdater) + ) + ); + contextAtom[atomSetter](Object.freeze(filteredValue)); + }; + updateContextAtom(); + this.subscribe(updateContextAtom); + return contextAtom; + } + computeValue() { + const computedValue = Object.fromEntries( + Object.entries(this.internalStateShape) + .filter(([, signalOrUpdater]) => signalOrUpdater) + .map(([key, signalOrUpdater]) => { + if (isUpdater(signalOrUpdater) || signalOrUpdater._id === contextID) { + return [key, signalOrUpdater]; + } + return [key, signalOrUpdater.value]; + }) + ); + this._value = Object.freeze(computedValue); + this.isStale = false; + } + scheduledNotify() { + this.isStale = true; + if (!this.isNotifyScheduled) { + queueMicrotask(() => { + this.isNotifyScheduled = false; + super.notify(); + }); + this.isNotifyScheduled = true; + } + } + get value() { + if (this.isStale) { + this.computeValue(); + } + return this._value; + } + } + return new StateManagerSignal(); + }; + return stateDefinition; +}; + +// src/contextful-lwc.ts + +// src/event.ts +var contextEventKey = getContextKeys().contextEventKey; +var EVENT_NAME = 'lightning:context-request'; +var ContextRequestEvent = class extends CustomEvent { + constructor(detail) { + super(EVENT_NAME, { + bubbles: true, + composed: true, + detail: { ...detail, key: contextEventKey }, + }); + } +}; + +const nameStateFactory = defineState( + (atom, computed, update, fromContext) => + (initialName = 'foo') => { + const name = atom(initialName); + + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); + + return { + name, + updateName, + }; + } +); + +const consumeStateFactory = defineState( + (atom, computed, update, fromContext) => + (initialName = 'bar') => { + const name = atom(initialName); + const context = fromContext(nameStateFactory); + + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); + + return { + name, + updateName, + context, + }; + } +); + +export { defineState, nameStateFactory, consumeStateFactory }; +/* @lwc/state v0.4.2 */ From 18a5c2fdca1914cac045483eef96b12be25fe3df Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 4 Dec 2024 19:56:54 -0800 Subject: [PATCH 05/11] chore: move context setup --- packages/@lwc/engine-core/src/framework/vm.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index ebaa50c9ee..16bd2d8d42 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -706,6 +706,9 @@ export function runConnectedCallback(vm: VM) { if (hasWireAdapters(vm)) { connectWireAdapters(vm); } + // Setup context before connected callback is executed + setupContext(vm); + const { connectedCallback } = vm.def; if (!isUndefined(connectedCallback)) { logOperationStart(OperationId.ConnectedCallback, vm); @@ -723,8 +726,6 @@ export function runConnectedCallback(vm: VM) { logOperationEnd(OperationId.ConnectedCallback, vm); } - // Setup context after connected callback is executed - setupContext(vm); // This test only makes sense in the browser, with synthetic lifecycle, and when reporting is enabled or // we're in dev mode. This is to detect a particular issue with synthetic lifecycle. if ( From a51be92c76780c7b90508f7bcd18b6b4c811dfb7 Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 4 Dec 2024 20:02:59 -0800 Subject: [PATCH 06/11] chore: fix lint --- packages/@lwc/engine-core/src/framework/context.ts | 14 ++++++++++---- .../x/contextParent/contextParent.js | 2 +- .../x/contextParent/contextParent.js | 1 - packages/@lwc/shared/src/context.ts | 10 ++++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/@lwc/engine-core/src/framework/context.ts b/packages/@lwc/engine-core/src/framework/context.ts index 2e09a39713..207e1b4f02 100644 --- a/packages/@lwc/engine-core/src/framework/context.ts +++ b/packages/@lwc/engine-core/src/framework/context.ts @@ -1,11 +1,17 @@ +/* + * 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 { ContextEventName, getContextKeys } from '@lwc/shared'; import type { Signal } from '@lwc/signals'; export type ContextProvidedCallback = (contextSignal: Signal) => void; export class ContextRequestEvent extends CustomEvent<{ - key: symbol; - contextVariety: unknown; - callback: ContextProvidedCallback; + key: symbol; + contextVariety: unknown; + callback: ContextProvidedCallback; }> { constructor(detail: { contextVariety: unknown; callback: ContextProvidedCallback }) { super(ContextEventName, { @@ -14,4 +20,4 @@ export class ContextRequestEvent extends CustomEvent<{ detail: { ...detail, key: getContextKeys().contextEventKey }, }); } -} \ No newline at end of file +} diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js index 4505103a46..a09f0d42f3 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js @@ -1,4 +1,4 @@ -import { LightningElement, api } from 'lwc'; +import { LightningElement } from 'lwc'; import { nameStateFactory } from 'x/state'; export default class ContextParent extends LightningElement { diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js index c8eebea520..64a5ebbbc5 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextParent/contextParent.js @@ -1,5 +1,4 @@ import { LightningElement, api } from 'lwc'; -// import { nameStateFactory } from 'x/state'; export default class ContextParent extends LightningElement { @api diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index f4fa786406..d8602f86a0 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -1,10 +1,16 @@ +/* + * 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 type ContextKeys = { connectContext: symbol; disconnectContext: symbol; contextEventKey: symbol; -} +}; let contextKeys: ContextKeys; @@ -18,4 +24,4 @@ export function getContextKeys() { return contextKeys; } -export const ContextEventName = 'lightning:context-request'; \ No newline at end of file +export const ContextEventName = 'lightning:context-request'; From 081a413f959b54717d5828ca327f95ed815bcc5c Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 4 Dec 2024 20:08:21 -0800 Subject: [PATCH 07/11] chore: fix unit tests --- packages/lwc/__tests__/isomorphic-exports.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lwc/__tests__/isomorphic-exports.spec.ts b/packages/lwc/__tests__/isomorphic-exports.spec.ts index 18fcb5e791..e4e3b2dcb9 100644 --- a/packages/lwc/__tests__/isomorphic-exports.spec.ts +++ b/packages/lwc/__tests__/isomorphic-exports.spec.ts @@ -25,6 +25,7 @@ describe('isomorphic package exports', () => { 'hydrateComponent', 'isNodeFromTemplate', 'rendererFactory', + 'setContextKeys', 'setTrustedSignalSet', ]); }); From 3d0a90e89ee981d8f9fae572c5525a847082604a Mon Sep 17 00:00:00 2001 From: rax-it Date: Thu, 5 Dec 2024 10:05:02 -0800 Subject: [PATCH 08/11] chore: fix ci --- .../component/LightningElement.connectedCallback/index.spec.js | 2 +- .../x/contextChild/contextChild.html | 2 +- scripts/bundlesize/bundlesize.config.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js index 3ba3ad7bbc..81fa174438 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/index.spec.js @@ -72,6 +72,6 @@ describe('context', () => { const child = elm.shadowRoot.querySelector('x-context-child'); expect(child).toBeDefined(); - expect(child.innerText).toContain('foo'); + expect(child.shadowRoot.querySelector('p').textContent).toBe('Test Child: foo'); }); }); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html index 7051f978f8..6ed6f4f29b 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.html @@ -1,3 +1,3 @@ \ No newline at end of file diff --git a/scripts/bundlesize/bundlesize.config.json b/scripts/bundlesize/bundlesize.config.json index e01e9f01e2..b457893ec8 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": "24KB" + "maxSize": "25KB" }, { "path": "packages/@lwc/synthetic-shadow/dist/index.js", From 4f95a2b8dd3eb733d29c480d5856eed74845d22a Mon Sep 17 00:00:00 2001 From: rax-it Date: Thu, 5 Dec 2024 10:45:19 -0800 Subject: [PATCH 09/11] chore: make coverage police happy --- .../@lwc/shared/src/__tests__/context.spec.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 packages/@lwc/shared/src/__tests__/context.spec.ts 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..c361ce6e7b --- /dev/null +++ b/packages/@lwc/shared/src/__tests__/context.spec.ts @@ -0,0 +1,67 @@ +/* + * 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 ContextEventName: string; + + beforeEach(async () => { + vi.resetModules(); + const contextModule = await import('../context'); + setContextKeys = contextModule.setContextKeys; + getContextKeys = contextModule.getContextKeys; + ContextEventName = contextModule.ContextEventName; + }); + + it('should set and get context keys', () => { + const mockContextKeys = { + connectContext: Symbol('connect'), + disconnectContext: Symbol('disconnect'), + contextEventKey: Symbol('event'), + }; + + setContextKeys(mockContextKeys); + const retrievedKeys = getContextKeys(); + + expect(retrievedKeys).toBe(mockContextKeys); + expect(retrievedKeys.connectContext).toBe(mockContextKeys.connectContext); + expect(retrievedKeys.disconnectContext).toBe(mockContextKeys.disconnectContext); + expect(retrievedKeys.contextEventKey).toBe(mockContextKeys.contextEventKey); + }); + + it('should throw when attempting to set context keys multiple times', () => { + const mockContextKeys1 = { + connectContext: Symbol('connect1'), + disconnectContext: Symbol('disconnect1'), + contextEventKey: Symbol('event1'), + }; + + const mockContextKeys2 = { + connectContext: Symbol('connect2'), + disconnectContext: Symbol('disconnect2'), + contextEventKey: Symbol('event2'), + }; + + setContextKeys(mockContextKeys1); + + expect(() => { + setContextKeys(mockContextKeys2); + }).toThrow('Context keys are already set!'); + }); + + it('should export ContextEventName as a constant string', () => { + expect(ContextEventName).toBe('lightning:context-request'); + expect(typeof ContextEventName).toBe('string'); + }); + + it('should return undefined when getting context keys before setting them', () => { + const keys = getContextKeys(); + expect(keys).toBeUndefined(); + }); +}); From 1b6fc7bbc813f783ab78fb5c920777f83f7ce34e Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 11 Dec 2024 11:39:32 -0800 Subject: [PATCH 10/11] chore: hoist state manager module --- packages/@lwc/engine-core/src/framework/vm.ts | 12 +- .../integration-karma/helpers/test-state.js | 322 ++++++++++++++++++ .../scripts/karma-configs/hydration/base.js | 2 + .../scripts/karma-configs/test/base.js | 2 + .../scripts/karma-plugins/lwc.js | 3 +- .../x/contextChild/contextChild.js | 2 +- .../x/contextParent/contextParent.js | 2 +- .../x/state/state.js | 314 ----------------- .../index.spec.js | 2 +- .../x/contextChild/contextChild.js | 2 +- .../x/state/state.js | 314 ----------------- .../@lwc/shared/src/__tests__/context.spec.ts | 9 +- packages/@lwc/shared/src/context.ts | 2 +- 13 files changed, 341 insertions(+), 647 deletions(-) create mode 100644 packages/@lwc/integration-karma/helpers/test-state.js delete mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js delete mode 100644 packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js diff --git a/packages/@lwc/engine-core/src/framework/vm.ts b/packages/@lwc/engine-core/src/framework/vm.ts index 16bd2d8d42..50d2e65810 100644 --- a/packages/@lwc/engine-core/src/framework/vm.ts +++ b/packages/@lwc/engine-core/src/framework/vm.ts @@ -5,6 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { + ArrayFilter, ArrayPush, ArraySlice, ArrayUnshift, @@ -753,14 +754,15 @@ export function runConnectedCallback(vm: VM) { function setupContext(vm: VM) { const contextKeys = getContextKeys(); - if (!contextKeys) { + if (isUndefined(contextKeys)) { return; } const { connectContext, contextEventKey } = contextKeys; - const { component } = vm; + const { component, renderer } = vm; const enumerableKeys = keys(getPrototypeOf(component)); - const contextfulFieldsOrProps = enumerableKeys.filter( + const contextfulFieldsOrProps = ArrayFilter.call( + enumerableKeys, (propName) => (component as any)[propName]?.[connectContext] ); @@ -780,7 +782,7 @@ function setupContext(vm: VM) { if (!isProvidingContext) { isProvidingContext = true; - component.addEventListener(ContextEventName, (event: any) => { + renderer.addEventListener(component, ContextEventName, (event: any) => { if ( event.detail.key === contextEventKey && providedContextVarieties.has(event.detail.contextVariety) @@ -817,7 +819,7 @@ function setupContext(vm: VM) { callback: contextProvidedCallback, }); - component.dispatchEvent(event); + renderer.dispatchEvent(component, event); }, }; diff --git a/packages/@lwc/integration-karma/helpers/test-state.js b/packages/@lwc/integration-karma/helpers/test-state.js new file mode 100644 index 0000000000..d56e7e8915 --- /dev/null +++ b/packages/@lwc/integration-karma/helpers/test-state.js @@ -0,0 +1,322 @@ +window.TestState = (function (testUtils) { + /* eslint-disable */ + /** + * This file is generated from repository: git+https://github.com/salesforce/lightning-labs.git + * module: packages/@lwc/state + */ + // node_modules/@lwc/signals/dist/index.js + function isFalse$1(value, msg) { + if (value) { + throw new Error(`Assert Violation: ${msg}`); + } + } + var trustedSignals; + function setTrustedSignalSet(signals) { + isFalse$1(trustedSignals, 'Trusted Signal Set is already set!'); + trustedSignals = signals; + } + function addTrustedSignal(signal) { + trustedSignals?.add(signal); + } + var SignalBaseClass = class { + constructor() { + this.subscribers = /* @__PURE__ */ new Set(); + addTrustedSignal(this); + } + subscribe(onUpdate) { + this.subscribers.add(onUpdate); + return () => { + this.subscribers.delete(onUpdate); + }; + } + notify() { + for (const subscriber of this.subscribers) { + subscriber(); + } + } + }; + + // src/shared.ts + var connectContext = testUtils.getContextKeys().connectContext; + var disconnectContext = testUtils.getContextKeys().disconnectContext; + + // src/standalone-context.ts + var ConsumedContextSignal = class extends SignalBaseClass { + constructor(stateDef) { + super(); + this._value = null; + this.unsubscribe = () => {}; + this.desiredStateDef = stateDef; + } + get value() { + return this._value; + } + [connectContext](runtimeAdapter) { + if (!runtimeAdapter) { + throw new Error( + 'Implementation error: runtimeAdapter must be present at the time of connect.' + ); + } + runtimeAdapter.consumeContext(this.desiredStateDef, (providedContextSignal) => { + this._value = providedContextSignal.value; + this.notify(); + this.unsubscribe = providedContextSignal.subscribe(() => { + this._value = providedContextSignal.value; + this.notify(); + }); + }); + } + [disconnectContext](_componentId) { + this.unsubscribe(); + this.unsubscribe = () => {}; + } + }; + + // src/index.ts + var atomSetter = Symbol('atomSetter'); + var contextID = Symbol('contextID'); + var AtomSignal = class extends SignalBaseClass { + constructor(value) { + super(); + this._value = value; + } + [atomSetter](value) { + this._value = value; + this.notify(); + } + get value() { + return this._value; + } + }; + var ContextAtomSignal = class extends AtomSignal { + constructor() { + super(...arguments); + this._id = contextID; + } + }; + var ComputedSignal = class extends SignalBaseClass { + constructor(inputSignalsObj, computer) { + super(); + this.isStale = true; + this.computer = computer; + this.dependencies = inputSignalsObj; + const onUpdate = () => { + this.isStale = true; + this.notify(); + }; + for (const signal of Object.values(inputSignalsObj)) { + signal.subscribe(onUpdate); + } + } + computeValue() { + const dependencyValues = {}; + for (const [signalName, signal] of Object.entries(this.dependencies)) { + dependencyValues[signalName] = signal.value; + } + this.isStale = false; + this._value = this.computer(dependencyValues); + } + notify() { + this.isStale = true; + super.notify(); + } + get value() { + if (this.isStale) { + this.computeValue(); + } + return this._value; + } + }; + var isUpdater = (signalOrUpdater) => typeof signalOrUpdater === 'function'; + var atom = (initialValue) => new AtomSignal(initialValue); + var computed = (inputSignalsObj, computer) => new ComputedSignal(inputSignalsObj, computer); + var update = (signalsToUpdate, userProvidedUpdaterFn) => { + return (...uniqueArgs) => { + const signalValues = {}; + for (const [signalName, signal] of Object.entries(signalsToUpdate)) { + signalValues[signalName] = signal.value; + } + const newValues = userProvidedUpdaterFn(signalValues, ...uniqueArgs); + for (const [atomName, newValue] of Object.entries(newValues)) { + signalsToUpdate[atomName][atomSetter](newValue); + } + }; + }; + var defineState = (defineStateCallback) => { + const stateDefinition = (...args) => { + class StateManagerSignal extends SignalBaseClass { + constructor() { + super(); + this.isStale = true; + this.isNotifyScheduled = false; + // biome-ignore lint/suspicious/noExplicitAny: we actually do want this, thanks + this.contextSignals = /* @__PURE__ */ new Map(); + this.contextConsumptionQueue = []; + this.contextUnsubscribes = /* @__PURE__ */ new WeakMap(); + const fromContext2 = (contextVarietyUniqueId) => { + if (this.contextSignals.has(contextVarietyUniqueId)) { + return this.contextSignals.get(contextVarietyUniqueId); + } + const localContextSignal = new ContextAtomSignal(void 0); + this.contextSignals.set(contextVarietyUniqueId, localContextSignal); + this.contextConsumptionQueue.push((runtimeAdapter) => { + if (!runtimeAdapter) { + throw new Error( + 'Implementation error: runtimeAdapter must be present at the time of connect.' + ); + } + runtimeAdapter.consumeContext( + contextVarietyUniqueId, + (providedContextSignal) => { + localContextSignal[atomSetter](providedContextSignal.value); + const unsub = providedContextSignal.subscribe(() => { + localContextSignal[atomSetter](providedContextSignal.value); + }); + if (!this.contextUnsubscribes.has(runtimeAdapter.component)) { + this.contextUnsubscribes.set(runtimeAdapter.component, []); + } + this.contextUnsubscribes + .get(runtimeAdapter.component) + .push(unsub); + } + ); + }); + return localContextSignal; + }; + this.internalStateShape = defineStateCallback( + atom, + computed, + update, + fromContext2 + )(...args); + for (const signalOrUpdater of Object.values(this.internalStateShape)) { + if (signalOrUpdater && !isUpdater(signalOrUpdater)) { + signalOrUpdater.subscribe(this.scheduledNotify.bind(this)); + } + } + } + [connectContext](runtimeAdapter) { + runtimeAdapter.provideContext(stateDefinition, this); + for (const connectContext2 of this.contextConsumptionQueue) { + connectContext2(runtimeAdapter); + } + } + [disconnectContext](componentId) { + const unsubArray = this.contextUnsubscribes.get(componentId); + if (!unsubArray) { + return; + } + while (unsubArray.length !== 0) { + unsubArray.pop()(); + } + } + shareableContext() { + const contextAtom = new ContextAtomSignal(void 0); + const updateContextAtom = () => { + const valueWithUpdaters = this.value; + const filteredValue = Object.fromEntries( + Object.entries(valueWithUpdaters).filter( + ([, valueOrUpdater]) => !isUpdater(valueOrUpdater) + ) + ); + contextAtom[atomSetter](Object.freeze(filteredValue)); + }; + updateContextAtom(); + this.subscribe(updateContextAtom); + return contextAtom; + } + computeValue() { + const computedValue = Object.fromEntries( + Object.entries(this.internalStateShape) + .filter(([, signalOrUpdater]) => signalOrUpdater) + .map(([key, signalOrUpdater]) => { + if ( + isUpdater(signalOrUpdater) || + signalOrUpdater._id === contextID + ) { + return [key, signalOrUpdater]; + } + return [key, signalOrUpdater.value]; + }) + ); + this._value = Object.freeze(computedValue); + this.isStale = false; + } + scheduledNotify() { + this.isStale = true; + if (!this.isNotifyScheduled) { + queueMicrotask(() => { + this.isNotifyScheduled = false; + super.notify(); + }); + this.isNotifyScheduled = true; + } + } + get value() { + if (this.isStale) { + this.computeValue(); + } + return this._value; + } + } + return new StateManagerSignal(); + }; + return stateDefinition; + }; + + // src/contextful-lwc.ts + + // src/event.ts + var contextEventKey = testUtils.getContextKeys().contextEventKey; + var EVENT_NAME = 'lightning:context-request'; + var ContextRequestEvent = class extends CustomEvent { + constructor(detail) { + super(EVENT_NAME, { + bubbles: true, + composed: true, + detail: { ...detail, key: contextEventKey }, + }); + } + }; + + const nameStateFactory = defineState( + (atom, computed, update, fromContext) => + (initialName = 'foo') => { + const name = atom(initialName); + + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); + + return { + name, + updateName, + }; + } + ); + + const consumeStateFactory = defineState( + (atom, computed, update, fromContext) => + (initialName = 'bar') => { + const name = atom(initialName); + const context = fromContext(nameStateFactory); + + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); + + return { + name, + updateName, + context, + }; + } + ); + + return { + defineState, + nameStateFactory, + consumeStateFactory, + }; + /* @lwc/state v0.4.2 */ +})(window.TestUtils); diff --git a/packages/@lwc/integration-karma/scripts/karma-configs/hydration/base.js b/packages/@lwc/integration-karma/scripts/karma-configs/hydration/base.js index b4316eb0cc..2aab421524 100644 --- a/packages/@lwc/integration-karma/scripts/karma-configs/hydration/base.js +++ b/packages/@lwc/integration-karma/scripts/karma-configs/hydration/base.js @@ -27,6 +27,7 @@ const LWC_ENGINE = require.resolve('@lwc/engine-dom/dist/index.js'); const TEST_UTILS = require.resolve('../../../helpers/test-utils'); const TEST_SETUP = require.resolve('../../../helpers/test-setup'); const TEST_HYDRATE = require.resolve('../../../helpers/test-hydrate'); +const TEST_STATE = require.resolve('../../../helpers/test-state'); const ALL_FRAMEWORK_FILES = [SYNTHETIC_SHADOW, LWC_ENGINE]; @@ -43,6 +44,7 @@ function getFiles() { createPattern(TEST_SETUP), createPattern(TEST_UTILS), createPattern(TEST_HYDRATE), + createPattern(TEST_STATE), ]; // check if a .only file exists diff --git a/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js b/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js index c42a76fcb9..d7b469b723 100644 --- a/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js +++ b/packages/@lwc/integration-karma/scripts/karma-configs/test/base.js @@ -31,6 +31,7 @@ const ARIA_REFLECTION = require.resolve('@lwc/aria-reflection/dist/index.js'); const TEST_UTILS = require.resolve('../../../helpers/test-utils'); const TEST_SETUP = require.resolve('../../../helpers/test-setup'); +const TEST_STATE = require.resolve('../../../helpers/test-state'); const ALL_FRAMEWORK_FILES = [SYNTHETIC_SHADOW, LWC_ENGINE, WIRE_SERVICE, ARIA_REFLECTION]; @@ -56,6 +57,7 @@ function getFiles() { return [ ...frameworkFiles, createPattern(TEST_UTILS), + createPattern(TEST_STATE), createPattern('**/*.spec.js', { watched: false }), ]; } diff --git a/packages/@lwc/integration-karma/scripts/karma-plugins/lwc.js b/packages/@lwc/integration-karma/scripts/karma-plugins/lwc.js index 59c13b5f71..e06b2b3497 100644 --- a/packages/@lwc/integration-karma/scripts/karma-plugins/lwc.js +++ b/packages/@lwc/integration-karma/scripts/karma-plugins/lwc.js @@ -100,7 +100,7 @@ function createPreprocessor(config, emitter, logger) { // Rollup should not attempt to resolve the engine and the test utils, Karma takes care of injecting it // globally in the page before running the tests. - external: ['lwc', 'wire-service', 'test-utils', '@test/loader'], + external: ['lwc', 'wire-service', 'test-utils', '@test/loader', 'test-state'], onwarn(warning, warn) { // Ignore warnings from our own Rollup plugin @@ -125,6 +125,7 @@ function createPreprocessor(config, emitter, logger) { lwc: 'LWC', 'wire-service': 'WireService', 'test-utils': 'TestUtils', + 'test-state': 'TestState', }, intro, diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.js index a3ef5dfb07..29a7e25f9a 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextChild/contextChild.js @@ -1,5 +1,5 @@ import { LightningElement } from 'lwc'; -import { consumeStateFactory } from 'x/state'; +import { consumeStateFactory } from 'test-state'; export default class TestChildSymbol extends LightningElement { randomChild = consumeStateFactory(); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js index a09f0d42f3..d80c68baac 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/contextParent/contextParent.js @@ -1,5 +1,5 @@ import { LightningElement } from 'lwc'; -import { nameStateFactory } from 'x/state'; +import { nameStateFactory } from 'test-state'; export default class ContextParent extends LightningElement { random = nameStateFactory(); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js b/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js deleted file mode 100644 index fea8ac1486..0000000000 --- a/packages/@lwc/integration-karma/test/component/LightningElement.connectedCallback/x/state/state.js +++ /dev/null @@ -1,314 +0,0 @@ -/* eslint-disable */ -/** - * This file is generated from repository: git+https://github.com/salesforce/lightning-labs.git - * module: packages/@lwc/state - */ - -import { getContextKeys } from 'test-utils'; - -// node_modules/@lwc/signals/dist/index.js -function isFalse$1(value, msg) { - if (value) { - throw new Error(`Assert Violation: ${msg}`); - } -} -var trustedSignals; -function setTrustedSignalSet(signals) { - isFalse$1(trustedSignals, 'Trusted Signal Set is already set!'); - trustedSignals = signals; -} -function addTrustedSignal(signal) { - trustedSignals?.add(signal); -} -var SignalBaseClass = class { - constructor() { - this.subscribers = /* @__PURE__ */ new Set(); - addTrustedSignal(this); - } - subscribe(onUpdate) { - this.subscribers.add(onUpdate); - return () => { - this.subscribers.delete(onUpdate); - }; - } - notify() { - for (const subscriber of this.subscribers) { - subscriber(); - } - } -}; - -// src/shared.ts -var connectContext = getContextKeys().connectContext; -var disconnectContext = getContextKeys().disconnectContext; - -// src/standalone-context.ts -var ConsumedContextSignal = class extends SignalBaseClass { - constructor(stateDef) { - super(); - this._value = null; - this.unsubscribe = () => {}; - this.desiredStateDef = stateDef; - } - get value() { - return this._value; - } - [connectContext](runtimeAdapter) { - if (!runtimeAdapter) { - throw new Error( - 'Implementation error: runtimeAdapter must be present at the time of connect.' - ); - } - runtimeAdapter.consumeContext(this.desiredStateDef, (providedContextSignal) => { - this._value = providedContextSignal.value; - this.notify(); - this.unsubscribe = providedContextSignal.subscribe(() => { - this._value = providedContextSignal.value; - this.notify(); - }); - }); - } - [disconnectContext](_componentId) { - this.unsubscribe(); - this.unsubscribe = () => {}; - } -}; - -// src/index.ts -var atomSetter = Symbol('atomSetter'); -var contextID = Symbol('contextID'); -var AtomSignal = class extends SignalBaseClass { - constructor(value) { - super(); - this._value = value; - } - [atomSetter](value) { - this._value = value; - this.notify(); - } - get value() { - return this._value; - } -}; -var ContextAtomSignal = class extends AtomSignal { - constructor() { - super(...arguments); - this._id = contextID; - } -}; -var ComputedSignal = class extends SignalBaseClass { - constructor(inputSignalsObj, computer) { - super(); - this.isStale = true; - this.computer = computer; - this.dependencies = inputSignalsObj; - const onUpdate = () => { - this.isStale = true; - this.notify(); - }; - for (const signal of Object.values(inputSignalsObj)) { - signal.subscribe(onUpdate); - } - } - computeValue() { - const dependencyValues = {}; - for (const [signalName, signal] of Object.entries(this.dependencies)) { - dependencyValues[signalName] = signal.value; - } - this.isStale = false; - this._value = this.computer(dependencyValues); - } - notify() { - this.isStale = true; - super.notify(); - } - get value() { - if (this.isStale) { - this.computeValue(); - } - return this._value; - } -}; -var isUpdater = (signalOrUpdater) => typeof signalOrUpdater === 'function'; -var atom = (initialValue) => new AtomSignal(initialValue); -var computed = (inputSignalsObj, computer) => new ComputedSignal(inputSignalsObj, computer); -var update = (signalsToUpdate, userProvidedUpdaterFn) => { - return (...uniqueArgs) => { - const signalValues = {}; - for (const [signalName, signal] of Object.entries(signalsToUpdate)) { - signalValues[signalName] = signal.value; - } - const newValues = userProvidedUpdaterFn(signalValues, ...uniqueArgs); - for (const [atomName, newValue] of Object.entries(newValues)) { - signalsToUpdate[atomName][atomSetter](newValue); - } - }; -}; -var defineState = (defineStateCallback) => { - const stateDefinition = (...args) => { - class StateManagerSignal extends SignalBaseClass { - constructor() { - super(); - this.isStale = true; - this.isNotifyScheduled = false; - // biome-ignore lint/suspicious/noExplicitAny: we actually do want this, thanks - this.contextSignals = /* @__PURE__ */ new Map(); - this.contextConsumptionQueue = []; - this.contextUnsubscribes = /* @__PURE__ */ new WeakMap(); - const fromContext2 = (contextVarietyUniqueId) => { - if (this.contextSignals.has(contextVarietyUniqueId)) { - return this.contextSignals.get(contextVarietyUniqueId); - } - const localContextSignal = new ContextAtomSignal(void 0); - this.contextSignals.set(contextVarietyUniqueId, localContextSignal); - this.contextConsumptionQueue.push((runtimeAdapter) => { - if (!runtimeAdapter) { - throw new Error( - 'Implementation error: runtimeAdapter must be present at the time of connect.' - ); - } - runtimeAdapter.consumeContext( - contextVarietyUniqueId, - (providedContextSignal) => { - localContextSignal[atomSetter](providedContextSignal.value); - const unsub = providedContextSignal.subscribe(() => { - localContextSignal[atomSetter](providedContextSignal.value); - }); - if (!this.contextUnsubscribes.has(runtimeAdapter.component)) { - this.contextUnsubscribes.set(runtimeAdapter.component, []); - } - this.contextUnsubscribes.get(runtimeAdapter.component).push(unsub); - } - ); - }); - return localContextSignal; - }; - this.internalStateShape = defineStateCallback( - atom, - computed, - update, - fromContext2 - )(...args); - for (const signalOrUpdater of Object.values(this.internalStateShape)) { - if (signalOrUpdater && !isUpdater(signalOrUpdater)) { - signalOrUpdater.subscribe(this.scheduledNotify.bind(this)); - } - } - } - [connectContext](runtimeAdapter) { - runtimeAdapter.provideContext(stateDefinition, this); - for (const connectContext2 of this.contextConsumptionQueue) { - connectContext2(runtimeAdapter); - } - } - [disconnectContext](componentId) { - const unsubArray = this.contextUnsubscribes.get(componentId); - if (!unsubArray) { - return; - } - while (unsubArray.length !== 0) { - unsubArray.pop()(); - } - } - shareableContext() { - const contextAtom = new ContextAtomSignal(void 0); - const updateContextAtom = () => { - const valueWithUpdaters = this.value; - const filteredValue = Object.fromEntries( - Object.entries(valueWithUpdaters).filter( - ([, valueOrUpdater]) => !isUpdater(valueOrUpdater) - ) - ); - contextAtom[atomSetter](Object.freeze(filteredValue)); - }; - updateContextAtom(); - this.subscribe(updateContextAtom); - return contextAtom; - } - computeValue() { - const computedValue = Object.fromEntries( - Object.entries(this.internalStateShape) - .filter(([, signalOrUpdater]) => signalOrUpdater) - .map(([key, signalOrUpdater]) => { - if (isUpdater(signalOrUpdater) || signalOrUpdater._id === contextID) { - return [key, signalOrUpdater]; - } - return [key, signalOrUpdater.value]; - }) - ); - this._value = Object.freeze(computedValue); - this.isStale = false; - } - scheduledNotify() { - this.isStale = true; - if (!this.isNotifyScheduled) { - queueMicrotask(() => { - this.isNotifyScheduled = false; - super.notify(); - }); - this.isNotifyScheduled = true; - } - } - get value() { - if (this.isStale) { - this.computeValue(); - } - return this._value; - } - } - return new StateManagerSignal(); - }; - return stateDefinition; -}; - -// src/contextful-lwc.ts - -// src/event.ts -var contextEventKey = getContextKeys().contextEventKey; -var EVENT_NAME = 'lightning:context-request'; -var ContextRequestEvent = class extends CustomEvent { - constructor(detail) { - super(EVENT_NAME, { - bubbles: true, - composed: true, - detail: { ...detail, key: contextEventKey }, - }); - } -}; - -const nameStateFactory = defineState( - (atom, computed, update, fromContext) => - (initialName = 'foo') => { - const name = atom(initialName); - - const updateName = update({ name }, (_, newName) => ({ - name: newName, - })); - - return { - name, - updateName, - }; - } -); - -const consumeStateFactory = defineState( - (atom, computed, update, fromContext) => - (initialName = 'bar') => { - const name = atom(initialName); - const context = fromContext(nameStateFactory); - - const updateName = update({ name }, (_, newName) => ({ - name: newName, - })); - - return { - name, - updateName, - context, - }; - } -); - -export { defineState, nameStateFactory, consumeStateFactory }; -/* @lwc/state v0.4.2 */ diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js index 66d5fdcef2..50b933d557 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/index.spec.js @@ -7,7 +7,7 @@ import DisconnectedCallbackThrow from 'x/disconnectedCallbackThrow'; import DualTemplate from 'x/dualTemplate'; import ExplicitRender from 'x/explicitRender'; import ContextParent from 'x/contextParent'; -import { nameStateFactory } from 'x/state'; +import { nameStateFactory } from 'test-state'; function testDisconnectSlot(name, fn) { it(`should invoke the disconnectedCallback when root element is removed from the DOM via ${name}`, () => { diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js index a3ef5dfb07..29a7e25f9a 100644 --- a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js +++ b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/contextChild/contextChild.js @@ -1,5 +1,5 @@ import { LightningElement } from 'lwc'; -import { consumeStateFactory } from 'x/state'; +import { consumeStateFactory } from 'test-state'; export default class TestChildSymbol extends LightningElement { randomChild = consumeStateFactory(); diff --git a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js b/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js deleted file mode 100644 index fea8ac1486..0000000000 --- a/packages/@lwc/integration-karma/test/component/LightningElement.disconnectedCallback/x/state/state.js +++ /dev/null @@ -1,314 +0,0 @@ -/* eslint-disable */ -/** - * This file is generated from repository: git+https://github.com/salesforce/lightning-labs.git - * module: packages/@lwc/state - */ - -import { getContextKeys } from 'test-utils'; - -// node_modules/@lwc/signals/dist/index.js -function isFalse$1(value, msg) { - if (value) { - throw new Error(`Assert Violation: ${msg}`); - } -} -var trustedSignals; -function setTrustedSignalSet(signals) { - isFalse$1(trustedSignals, 'Trusted Signal Set is already set!'); - trustedSignals = signals; -} -function addTrustedSignal(signal) { - trustedSignals?.add(signal); -} -var SignalBaseClass = class { - constructor() { - this.subscribers = /* @__PURE__ */ new Set(); - addTrustedSignal(this); - } - subscribe(onUpdate) { - this.subscribers.add(onUpdate); - return () => { - this.subscribers.delete(onUpdate); - }; - } - notify() { - for (const subscriber of this.subscribers) { - subscriber(); - } - } -}; - -// src/shared.ts -var connectContext = getContextKeys().connectContext; -var disconnectContext = getContextKeys().disconnectContext; - -// src/standalone-context.ts -var ConsumedContextSignal = class extends SignalBaseClass { - constructor(stateDef) { - super(); - this._value = null; - this.unsubscribe = () => {}; - this.desiredStateDef = stateDef; - } - get value() { - return this._value; - } - [connectContext](runtimeAdapter) { - if (!runtimeAdapter) { - throw new Error( - 'Implementation error: runtimeAdapter must be present at the time of connect.' - ); - } - runtimeAdapter.consumeContext(this.desiredStateDef, (providedContextSignal) => { - this._value = providedContextSignal.value; - this.notify(); - this.unsubscribe = providedContextSignal.subscribe(() => { - this._value = providedContextSignal.value; - this.notify(); - }); - }); - } - [disconnectContext](_componentId) { - this.unsubscribe(); - this.unsubscribe = () => {}; - } -}; - -// src/index.ts -var atomSetter = Symbol('atomSetter'); -var contextID = Symbol('contextID'); -var AtomSignal = class extends SignalBaseClass { - constructor(value) { - super(); - this._value = value; - } - [atomSetter](value) { - this._value = value; - this.notify(); - } - get value() { - return this._value; - } -}; -var ContextAtomSignal = class extends AtomSignal { - constructor() { - super(...arguments); - this._id = contextID; - } -}; -var ComputedSignal = class extends SignalBaseClass { - constructor(inputSignalsObj, computer) { - super(); - this.isStale = true; - this.computer = computer; - this.dependencies = inputSignalsObj; - const onUpdate = () => { - this.isStale = true; - this.notify(); - }; - for (const signal of Object.values(inputSignalsObj)) { - signal.subscribe(onUpdate); - } - } - computeValue() { - const dependencyValues = {}; - for (const [signalName, signal] of Object.entries(this.dependencies)) { - dependencyValues[signalName] = signal.value; - } - this.isStale = false; - this._value = this.computer(dependencyValues); - } - notify() { - this.isStale = true; - super.notify(); - } - get value() { - if (this.isStale) { - this.computeValue(); - } - return this._value; - } -}; -var isUpdater = (signalOrUpdater) => typeof signalOrUpdater === 'function'; -var atom = (initialValue) => new AtomSignal(initialValue); -var computed = (inputSignalsObj, computer) => new ComputedSignal(inputSignalsObj, computer); -var update = (signalsToUpdate, userProvidedUpdaterFn) => { - return (...uniqueArgs) => { - const signalValues = {}; - for (const [signalName, signal] of Object.entries(signalsToUpdate)) { - signalValues[signalName] = signal.value; - } - const newValues = userProvidedUpdaterFn(signalValues, ...uniqueArgs); - for (const [atomName, newValue] of Object.entries(newValues)) { - signalsToUpdate[atomName][atomSetter](newValue); - } - }; -}; -var defineState = (defineStateCallback) => { - const stateDefinition = (...args) => { - class StateManagerSignal extends SignalBaseClass { - constructor() { - super(); - this.isStale = true; - this.isNotifyScheduled = false; - // biome-ignore lint/suspicious/noExplicitAny: we actually do want this, thanks - this.contextSignals = /* @__PURE__ */ new Map(); - this.contextConsumptionQueue = []; - this.contextUnsubscribes = /* @__PURE__ */ new WeakMap(); - const fromContext2 = (contextVarietyUniqueId) => { - if (this.contextSignals.has(contextVarietyUniqueId)) { - return this.contextSignals.get(contextVarietyUniqueId); - } - const localContextSignal = new ContextAtomSignal(void 0); - this.contextSignals.set(contextVarietyUniqueId, localContextSignal); - this.contextConsumptionQueue.push((runtimeAdapter) => { - if (!runtimeAdapter) { - throw new Error( - 'Implementation error: runtimeAdapter must be present at the time of connect.' - ); - } - runtimeAdapter.consumeContext( - contextVarietyUniqueId, - (providedContextSignal) => { - localContextSignal[atomSetter](providedContextSignal.value); - const unsub = providedContextSignal.subscribe(() => { - localContextSignal[atomSetter](providedContextSignal.value); - }); - if (!this.contextUnsubscribes.has(runtimeAdapter.component)) { - this.contextUnsubscribes.set(runtimeAdapter.component, []); - } - this.contextUnsubscribes.get(runtimeAdapter.component).push(unsub); - } - ); - }); - return localContextSignal; - }; - this.internalStateShape = defineStateCallback( - atom, - computed, - update, - fromContext2 - )(...args); - for (const signalOrUpdater of Object.values(this.internalStateShape)) { - if (signalOrUpdater && !isUpdater(signalOrUpdater)) { - signalOrUpdater.subscribe(this.scheduledNotify.bind(this)); - } - } - } - [connectContext](runtimeAdapter) { - runtimeAdapter.provideContext(stateDefinition, this); - for (const connectContext2 of this.contextConsumptionQueue) { - connectContext2(runtimeAdapter); - } - } - [disconnectContext](componentId) { - const unsubArray = this.contextUnsubscribes.get(componentId); - if (!unsubArray) { - return; - } - while (unsubArray.length !== 0) { - unsubArray.pop()(); - } - } - shareableContext() { - const contextAtom = new ContextAtomSignal(void 0); - const updateContextAtom = () => { - const valueWithUpdaters = this.value; - const filteredValue = Object.fromEntries( - Object.entries(valueWithUpdaters).filter( - ([, valueOrUpdater]) => !isUpdater(valueOrUpdater) - ) - ); - contextAtom[atomSetter](Object.freeze(filteredValue)); - }; - updateContextAtom(); - this.subscribe(updateContextAtom); - return contextAtom; - } - computeValue() { - const computedValue = Object.fromEntries( - Object.entries(this.internalStateShape) - .filter(([, signalOrUpdater]) => signalOrUpdater) - .map(([key, signalOrUpdater]) => { - if (isUpdater(signalOrUpdater) || signalOrUpdater._id === contextID) { - return [key, signalOrUpdater]; - } - return [key, signalOrUpdater.value]; - }) - ); - this._value = Object.freeze(computedValue); - this.isStale = false; - } - scheduledNotify() { - this.isStale = true; - if (!this.isNotifyScheduled) { - queueMicrotask(() => { - this.isNotifyScheduled = false; - super.notify(); - }); - this.isNotifyScheduled = true; - } - } - get value() { - if (this.isStale) { - this.computeValue(); - } - return this._value; - } - } - return new StateManagerSignal(); - }; - return stateDefinition; -}; - -// src/contextful-lwc.ts - -// src/event.ts -var contextEventKey = getContextKeys().contextEventKey; -var EVENT_NAME = 'lightning:context-request'; -var ContextRequestEvent = class extends CustomEvent { - constructor(detail) { - super(EVENT_NAME, { - bubbles: true, - composed: true, - detail: { ...detail, key: contextEventKey }, - }); - } -}; - -const nameStateFactory = defineState( - (atom, computed, update, fromContext) => - (initialName = 'foo') => { - const name = atom(initialName); - - const updateName = update({ name }, (_, newName) => ({ - name: newName, - })); - - return { - name, - updateName, - }; - } -); - -const consumeStateFactory = defineState( - (atom, computed, update, fromContext) => - (initialName = 'bar') => { - const name = atom(initialName); - const context = fromContext(nameStateFactory); - - const updateName = update({ name }, (_, newName) => ({ - name: newName, - })); - - return { - name, - updateName, - context, - }; - } -); - -export { defineState, nameStateFactory, consumeStateFactory }; -/* @lwc/state v0.4.2 */ diff --git a/packages/@lwc/shared/src/__tests__/context.spec.ts b/packages/@lwc/shared/src/__tests__/context.spec.ts index c361ce6e7b..01b73d5286 100644 --- a/packages/@lwc/shared/src/__tests__/context.spec.ts +++ b/packages/@lwc/shared/src/__tests__/context.spec.ts @@ -9,14 +9,12 @@ import { describe, beforeEach, expect, it, vi } from 'vitest'; describe('context', () => { let setContextKeys: (config: any) => void; let getContextKeys: () => any; - let ContextEventName: string; beforeEach(async () => { vi.resetModules(); const contextModule = await import('../context'); setContextKeys = contextModule.setContextKeys; getContextKeys = contextModule.getContextKeys; - ContextEventName = contextModule.ContextEventName; }); it('should set and get context keys', () => { @@ -52,12 +50,7 @@ describe('context', () => { expect(() => { setContextKeys(mockContextKeys2); - }).toThrow('Context keys are already set!'); - }); - - it('should export ContextEventName as a constant string', () => { - expect(ContextEventName).toBe('lightning:context-request'); - expect(typeof ContextEventName).toBe('string'); + }).toThrow('`setContextKeys` cannot be called more than once'); }); it('should return undefined when getting context keys before setting them', () => { diff --git a/packages/@lwc/shared/src/context.ts b/packages/@lwc/shared/src/context.ts index d8602f86a0..492b0ed46e 100644 --- a/packages/@lwc/shared/src/context.ts +++ b/packages/@lwc/shared/src/context.ts @@ -15,7 +15,7 @@ export type ContextKeys = { let contextKeys: ContextKeys; export function setContextKeys(config: ContextKeys) { - isFalse(contextKeys, 'Context keys are already set!'); + isFalse(contextKeys, '`setContextKeys` cannot be called more than once'); contextKeys = config; } From 89a322c7159e0e4c1cd9313945e0c7e3ef118c77 Mon Sep 17 00:00:00 2001 From: rax-it Date: Wed, 11 Dec 2024 18:45:22 -0800 Subject: [PATCH 11/11] chore: cleanup test util --- .../integration-karma/helpers/test-state.js | 111 ++++-------------- .../integration-karma/helpers/test-utils.js | 13 -- 2 files changed, 25 insertions(+), 99 deletions(-) diff --git a/packages/@lwc/integration-karma/helpers/test-state.js b/packages/@lwc/integration-karma/helpers/test-state.js index d56e7e8915..7ffaa495ff 100644 --- a/packages/@lwc/integration-karma/helpers/test-state.js +++ b/packages/@lwc/integration-karma/helpers/test-state.js @@ -1,27 +1,20 @@ -window.TestState = (function (testUtils) { - /* eslint-disable */ - /** - * This file is generated from repository: git+https://github.com/salesforce/lightning-labs.git - * module: packages/@lwc/state - */ - // node_modules/@lwc/signals/dist/index.js - function isFalse$1(value, msg) { - if (value) { - throw new Error(`Assert Violation: ${msg}`); - } - } - var trustedSignals; - function setTrustedSignalSet(signals) { - isFalse$1(trustedSignals, 'Trusted Signal Set is already set!'); - trustedSignals = signals; - } - function addTrustedSignal(signal) { - trustedSignals?.add(signal); - } +window.TestState = (function (lwc, testUtils) { + const connectContext = Symbol('connectContext'); + const disconnectContext = Symbol('disconnectContext'); + const contextEventKey = Symbol('contextEventKey'); + + const contextKeys = { + connectContext, + disconnectContext, + contextEventKey, + }; + + lwc.setContextKeys(contextKeys); + var SignalBaseClass = class { constructor() { this.subscribers = /* @__PURE__ */ new Set(); - addTrustedSignal(this); + testUtils.addTrustedSignal(this); } subscribe(onUpdate) { this.subscribers.add(onUpdate); @@ -36,42 +29,6 @@ window.TestState = (function (testUtils) { } }; - // src/shared.ts - var connectContext = testUtils.getContextKeys().connectContext; - var disconnectContext = testUtils.getContextKeys().disconnectContext; - - // src/standalone-context.ts - var ConsumedContextSignal = class extends SignalBaseClass { - constructor(stateDef) { - super(); - this._value = null; - this.unsubscribe = () => {}; - this.desiredStateDef = stateDef; - } - get value() { - return this._value; - } - [connectContext](runtimeAdapter) { - if (!runtimeAdapter) { - throw new Error( - 'Implementation error: runtimeAdapter must be present at the time of connect.' - ); - } - runtimeAdapter.consumeContext(this.desiredStateDef, (providedContextSignal) => { - this._value = providedContextSignal.value; - this.notify(); - this.unsubscribe = providedContextSignal.subscribe(() => { - this._value = providedContextSignal.value; - this.notify(); - }); - }); - } - [disconnectContext](_componentId) { - this.unsubscribe(); - this.unsubscribe = () => {}; - } - }; - // src/index.ts var atomSetter = Symbol('atomSetter'); var contextID = Symbol('contextID'); @@ -264,36 +221,18 @@ window.TestState = (function (testUtils) { return stateDefinition; }; - // src/contextful-lwc.ts + const nameStateFactory = defineState((atom, computed, update) => (initialName = 'foo') => { + const name = atom(initialName); - // src/event.ts - var contextEventKey = testUtils.getContextKeys().contextEventKey; - var EVENT_NAME = 'lightning:context-request'; - var ContextRequestEvent = class extends CustomEvent { - constructor(detail) { - super(EVENT_NAME, { - bubbles: true, - composed: true, - detail: { ...detail, key: contextEventKey }, - }); - } - }; - - const nameStateFactory = defineState( - (atom, computed, update, fromContext) => - (initialName = 'foo') => { - const name = atom(initialName); - - const updateName = update({ name }, (_, newName) => ({ - name: newName, - })); + const updateName = update({ name }, (_, newName) => ({ + name: newName, + })); - return { - name, - updateName, - }; - } - ); + return { + name, + updateName, + }; + }); const consumeStateFactory = defineState( (atom, computed, update, fromContext) => @@ -319,4 +258,4 @@ window.TestState = (function (testUtils) { consumeStateFactory, }; /* @lwc/state v0.4.2 */ -})(window.TestUtils); +})(window.LWC, window.TestUtils); diff --git a/packages/@lwc/integration-karma/helpers/test-utils.js b/packages/@lwc/integration-karma/helpers/test-utils.js index da94466a80..2e460729c3 100644 --- a/packages/@lwc/integration-karma/helpers/test-utils.js +++ b/packages/@lwc/integration-karma/helpers/test-utils.js @@ -709,18 +709,6 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { signalValidator.add(signal); } - const contextKeys = { - connectContext: Symbol('connectContext'), - disconnectContext: Symbol('disconnectContext'), - contextEventKey: Symbol('contextEventKey'), - }; - - lwc.setContextKeys(contextKeys); - - function getContextKeys() { - return contextKeys; - } - return { clearRegister, extractDataIds, @@ -748,7 +736,6 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) { catchUnhandledRejectionsAndErrors, addTrustedSignal, expectEquivalentDOM, - getContextKeys, ...apiFeatures, }; })(LWC, jasmine, beforeAll);