Skip to content

Commit 5c4b25f

Browse files
fix: gate protection, review comments, malformed context handling
1 parent 094aeb8 commit 5c4b25f

File tree

14 files changed

+133
-40
lines changed

14 files changed

+133
-40
lines changed

packages/@lwc/engine-core/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,20 @@ This experimental API enables the removal of an object's observable membrane pro
121121
This experimental API enables the addition of a signal as a trusted signal. If the [ENABLE_EXPERIMENTAL_SIGNALS](https://github.com/salesforce/lwc/blob/master/packages/%40lwc/features/README.md#lwcfeatures) feature is enabled, any signal value change will trigger a re-render.
122122

123123
If `setTrustedSignalSet` is called more than once, it will throw an error. If it is never called, then no trusted signal validation will be performed. The same `setTrustedSignalSet` API must be called on both `@lwc/engine-dom` and `@lwc/signals`.
124+
125+
### setTrustedContextSet()
126+
127+
This experimental API enables the addition of context as trusted context. If the [ENABLE_EXPERIMENTAL_SIGNALS](https://github.com/salesforce/lwc/blob/master/packages/%40lwc/features/README.md#lwcfeatures) feature is enabled
128+
and context has been added to this set, the context object's connectContext and disconnectContext symbols will be called with a ContextConnector when the associated component is connected and disconnected.
129+
130+
If `setTrustedContextSet` is called more than once, it will throw an error. If it is never called, then context will not be connected.
131+
132+
### ContextConnector
133+
134+
The context manager `connectContext` and `disconnectContext` symbols are called with this object when contextful components are connected and disconnected. The ContextConnector exposes `provideContext` and `consumeContext`,
135+
enabling the provision/consumption of a contextful Signal of a specified variety for the associated component.
136+
137+
### setContextKeys
138+
139+
Enables a state manager context implementation to provide LWC with context Symbols, namely `connectContext` and `disconnectContext`. These symbols would then be defined on any context manager implementation, and will be called
140+
with a ContextConnector object when contextful components are connected and disconnected.

packages/@lwc/engine-core/src/framework/modules/context.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ import {
1212
ArrayFilter,
1313
ContextEventName,
1414
isTrustedContext,
15+
type ContextProvidedCallback,
16+
type ContextConnector as IContextConnector,
1517
} from '@lwc/shared';
1618
import { type VM } from '../vm';
17-
import { logErrorOnce } from '../../shared/logger';
19+
import { logWarnOnce } from '../../shared/logger';
1820
import type { Signal } from '@lwc/signals';
1921
import type { RendererAPI } from '../renderer';
2022
import type { ShouldContinueBubbling } from '../wiring/types';
2123

22-
type ContextProvidedCallback = (contextSignal?: Signal<unknown>) => void;
2324
type ContextVarieties = Map<unknown, Signal<unknown>>;
2425

25-
class ContextConnector<C extends object> {
26+
class ContextConnector<C extends object> implements IContextConnector<C> {
2627
component: C;
2728
#renderer: RendererAPI;
2829
#providedContextVarieties: ContextVarieties;
@@ -54,7 +55,7 @@ class ContextConnector<C extends object> {
5455
}
5556

5657
if (this.#providedContextVarieties.has(contextVariety)) {
57-
logErrorOnce(
58+
logWarnOnce(
5859
'Multiple contexts of the same variety were provided. Only the first context will be used.'
5960
);
6061
return;
@@ -103,9 +104,17 @@ export function connectContext(vm: VM) {
103104

104105
const providedContextVarieties: ContextVarieties = new Map();
105106

106-
for (let i = 0; i < contextfulKeys.length; i++) {
107-
(component as any)[contextfulKeys[i]][connectContext](
108-
new ContextConnector(vm, component, providedContextVarieties)
107+
try {
108+
for (let i = 0; i < contextfulKeys.length; i++) {
109+
(component as any)[contextfulKeys[i]][connectContext](
110+
new ContextConnector(vm, component, providedContextVarieties)
111+
);
112+
}
113+
} catch (err: any) {
114+
logWarnOnce(
115+
`Attempted to connect to trusted context but received the following error: ${
116+
err.message
117+
}`
109118
);
110119
}
111120
}
@@ -129,7 +138,15 @@ export function disconnectContext(vm: VM) {
129138
return;
130139
}
131140

132-
for (let i = 0; i < contextfulKeys.length; i++) {
133-
(component as any)[contextfulKeys[i]][disconnectContext](component);
141+
try {
142+
for (let i = 0; i < contextfulKeys.length; i++) {
143+
(component as any)[contextfulKeys[i]][disconnectContext](component);
144+
}
145+
} catch (err: any) {
146+
logWarnOnce(
147+
`Attempted to disconnect from trusted context but received the following error: ${
148+
err.message
149+
}`
150+
);
134151
}
135152
}

packages/@lwc/engine-core/src/framework/mutation-tracker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function componentValueObserved(vm: VM, key: PropertyKey, target: any = {
4242
isObject(target) &&
4343
!isNull(target) &&
4444
isTrustedSignal(target) &&
45+
process.env.IS_BROWSER &&
4546
// Only subscribe if a template is being rendered by the engine
4647
tro.isObserving()
4748
) {

packages/@lwc/engine-core/src/framework/vm.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -704,8 +704,10 @@ export function runConnectedCallback(vm: VM) {
704704
connectWireAdapters(vm);
705705
}
706706

707-
// Setup context before connected callback is executed
708-
connectContext(vm);
707+
if (lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS) {
708+
// Setup context before connected callback is executed
709+
connectContext(vm);
710+
}
709711

710712
const { connectedCallback } = vm.def;
711713
if (!isUndefined(connectedCallback)) {
@@ -756,7 +758,11 @@ function runDisconnectedCallback(vm: VM) {
756758
if (process.env.NODE_ENV !== 'production') {
757759
assert.isTrue(vm.state !== VMState.disconnected, `${vm} must be inserted.`);
758760
}
759-
disconnectContext(vm);
761+
762+
if (lwcRuntimeFlags.ENABLE_EXPERIMENTAL_SIGNALS) {
763+
disconnectContext(vm);
764+
}
765+
760766
if (isFalse(vm.isDirty)) {
761767
// this guarantees that if the component is reused/reinserted,
762768
// it will be re-rendered because we are disconnecting the reactivity

packages/@lwc/engine-server/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export {
2929
isComponentConstructor,
3030
parseFragment,
3131
parseFragment as parseSVGFragment,
32-
setTrustedSignalSet,
3332
setTrustedContextSet,
3433
setContextKeys,
3534
} from '@lwc/engine-core';

packages/@lwc/integration-karma/helpers/test-hydrate.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,22 @@ window.HydrateTest = (function (lwc, testUtils) {
2222
return div;
2323
}
2424

25+
function setFeatureFlags(requiredFeatureFlags, value) {
26+
if (requiredFeatureFlags) {
27+
requiredFeatureFlags.forEach((featureFlag) => {
28+
lwc.setFeatureFlagForTest(featureFlag, value);
29+
});
30+
}
31+
}
32+
2533
function runTest(ssrRendered, Component, testConfig) {
2634
const container = appendTestTarget(ssrRendered);
2735
const selector = container.firstChild.tagName.toLowerCase();
2836
let target = container.querySelector(selector);
2937

3038
let testResult;
3139
const consoleSpy = testUtils.spyConsole();
40+
setFeatureFlags(testConfig.requiredFeatureFlags, true);
3241

3342
if (testConfig.test) {
3443
const snapshot = testConfig.snapshot ? testConfig.snapshot(target) : {};

packages/@lwc/integration-karma/helpers/test-utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
470470
}
471471
}
472472

473+
function setFeatureFlagForTest(featureFlag, value) {
474+
LWC.setFeatureFlagForTest(featureFlag, value);
475+
}
476+
473477
// This mapping should be kept up-to-date with the mapping in @lwc/shared -> aria.ts
474478
const ariaPropertiesMapping = {
475479
ariaAutoComplete: 'aria-autocomplete',
@@ -747,6 +751,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
747751
catchUnhandledRejectionsAndErrors,
748752
addTrustedSignal,
749753
expectEquivalentDOM,
754+
setFeatureFlagForTest,
750755
...apiFeatures,
751756
};
752757
})(LWC, jasmine, beforeAll);

packages/@lwc/integration-karma/scripts/karma-plugins/hydration-tests.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ const fs = require('node:fs/promises');
1111
const { format } = require('node:util');
1212
const { rollup } = require('rollup');
1313
const lwcRollupPlugin = require('@lwc/rollup-plugin');
14-
const ssr = ENGINE_SERVER ? require('@lwc/engine-server') : require('@lwc/ssr-runtime');
14+
const lwcSsr = ENGINE_SERVER ? require('@lwc/engine-server') : require('@lwc/ssr-runtime');
1515
const { DISABLE_STATIC_CONTENT_OPTIMIZATION } = require('../shared/options');
1616
const Watcher = require('./Watcher');
1717

1818
const context = {
19-
LWC: ssr,
19+
LWC: lwcSsr,
2020
moduleOutput: null,
2121
};
2222

23-
ssr.setHooks({
23+
lwcSsr.setHooks({
2424
sanitizeHtmlContent(content) {
2525
return content;
2626
},
@@ -145,14 +145,7 @@ function throwOnUnexpectedConsoleCalls(runnable, expectedConsoleCalls = {}) {
145145
* So, script runs, generates markup, & we get that markup out and return it to Karma for use
146146
* in client-side tests.
147147
*/
148-
async function getSsrCode(moduleCode, testConfig, filename) {
149-
// Create a temporary module to evaluate the bundled code and extract the expected console calls
150-
const configModule = new vm.Script(testConfig);
151-
const configContext = { config: {} };
152-
vm.createContext(configContext);
153-
configModule.runInContext(configContext);
154-
const { expectedSSRConsoleCalls } = configContext.config;
155-
148+
async function getSsrCode(moduleCode, testConfig, filename, expectedSSRConsoleCalls) {
156149
const script = new vm.Script(
157150
`
158151
${testConfig};
@@ -221,10 +214,22 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) {
221214
// Wrap all the tests into a describe block with the file stricture name
222215
const describeTitle = path.relative(basePath, suiteDir).split(path.sep).join(' ');
223216

224-
try {
225-
const { code: testCode, watchFiles: testWatchFiles } =
226-
await getTestModuleCode(filePath);
217+
const { code: testCode, watchFiles: testWatchFiles } = await getTestModuleCode(filePath);
227218

219+
// Create a temporary module to evaluate the bundled code and extract config properties for test configuration
220+
const configModule = new vm.Script(testCode);
221+
const configContext = { config: {} };
222+
vm.createContext(configContext);
223+
configModule.runInContext(configContext);
224+
const { expectedSSRConsoleCalls, requiredFeatureFlags } = configContext.config;
225+
226+
if (requiredFeatureFlags) {
227+
requiredFeatureFlags.forEach((featureFlag) => {
228+
lwcSsr.setFeatureFlagForTest(featureFlag, true);
229+
});
230+
}
231+
232+
try {
228233
// You can add an `.only` file alongside an `index.spec.js` file to make it `fdescribe()`
229234
const onlyFileExists = await existsUp(suiteDir, '.only');
230235

@@ -241,7 +246,8 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) {
241246
ssrOutput = await getSsrCode(
242247
componentDefCSR,
243248
testCode,
244-
path.join(suiteDir, 'ssr.js')
249+
path.join(suiteDir, 'ssr.js'),
250+
expectedSSRConsoleCalls
245251
);
246252
} else {
247253
// ssr-compiler has it's own def
@@ -254,7 +260,8 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) {
254260
ssrOutput = await getSsrCode(
255261
componentDefSSR.replace(`process.env.NODE_ENV === 'test-karma-lwc'`, 'true'),
256262
testCode,
257-
path.join(suiteDir, 'ssr.js')
263+
path.join(suiteDir, 'ssr.js'),
264+
expectedSSRConsoleCalls
258265
);
259266
}
260267

@@ -271,6 +278,12 @@ function createHCONFIG2JSPreprocessor(config, logger, emitter) {
271278
const location = path.relative(basePath, filePath);
272279
log.error('Error processing “%s”\n\n%s\n', location, error.stack || error.message);
273280
done(error, null);
281+
} finally {
282+
if (requiredFeatureFlags) {
283+
requiredFeatureFlags.forEach((featureFlag) => {
284+
lwcSsr.setFeatureFlagForTest(featureFlag, false);
285+
});
286+
}
274287
}
275288
};
276289
}

packages/@lwc/integration-karma/test-hydration/context/index.spec.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export default {
22
// server is expected to generate the same console error as the client
33
expectedSSRConsoleCalls: {
4-
error: [
4+
error: [],
5+
warn: [
6+
'Attempted to connect to trusted context but received the following error',
57
'Multiple contexts of the same variety were provided. Only the first context will be used.',
68
],
7-
warn: [],
89
},
10+
requiredFeatureFlags: ['ENABLE_EXPERIMENTAL_SIGNALS'],
911
snapshot(target) {
1012
const grandparent = target.shadowRoot.querySelector('x-grandparent');
1113
const detachedChild = target.shadowRoot.querySelector('x-child');
@@ -36,12 +38,14 @@ export default {
3638
assertContextDisconnected(target, snapshot);
3739

3840
// Expect an error as one context was generated twice.
41+
// Expect an error as one context was malformed (did not define connectContext or disconnectContext methods).
3942
// Expect server/client context output parity (no hydration warnings)
4043
TestUtils.expectConsoleCalls(consoleCalls, {
41-
error: [
44+
error: [],
45+
warn: [
46+
'Attempted to connect to trusted context but received the following error',
4247
'Multiple contexts of the same variety were provided. Only the first context will be used.',
4348
],
44-
warn: [],
4549
});
4650
},
4751
};
@@ -52,7 +56,7 @@ function assertCorrectContext(snapshot) {
5256
.withContext(`${component.tagName} should have the correct context`)
5357
.toBe('grandparent provided value, another grandparent provided value');
5458

55-
expect(component.context.connectProvidedComponent.hostElement)
59+
expect(component.context.connectProvidedComponent?.hostElement)
5660
.withContext(
5761
`The context of ${component.tagName} should have been connected with the correct component`
5862
)
@@ -86,7 +90,7 @@ function assertContextDisconnected(target, snapshot) {
8690
Object.values(snapshot.components).forEach(
8791
(component) =>
8892
(component.disconnect = () => {
89-
expect(component.context.disconnectProvidedComponent.hostElement)
93+
expect(component.context.disconnectProvidedComponent?.hostElement)
9094
.withContext(
9195
`The context of ${component.tagName} should have been disconnected with the correct component`
9296
)

packages/@lwc/integration-karma/test-hydration/context/x/contextManager/contextManager.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,19 @@ class MockContextSignal {
3535
}
3636
}
3737

38+
// This is a malformed context signal that does not implement the connectContext or disconnectContext methods
39+
class MockMalformedContextSignal {
40+
constructor() {
41+
trustedContext.add(this);
42+
}
43+
}
44+
3845
export const defineContext = (fromContext) => {
3946
const contextDefinition = (initialValue) =>
4047
new MockContextSignal(initialValue, contextDefinition, fromContext);
4148
return contextDefinition;
4249
};
50+
51+
export const defineMalformedContext = () => {
52+
return () => new MockMalformedContextSignal();
53+
};

0 commit comments

Comments
 (0)