diff --git a/packages/@lwc/engine-core/src/framework/reporting.ts b/packages/@lwc/engine-core/src/framework/reporting.ts index 83099d6a25..bd478073ff 100644 --- a/packages/@lwc/engine-core/src/framework/reporting.ts +++ b/packages/@lwc/engine-core/src/framework/reporting.ts @@ -6,7 +6,7 @@ */ import { noop } from '@lwc/shared'; -import { ShadowMode, ShadowSupportMode } from './vm'; +import { RenderMode, ShadowMode, ShadowSupportMode } from './vm'; export const enum ReportingEventId { CrossRootAriaInSyntheticShadow = 'CrossRootAriaInSyntheticShadow', @@ -17,6 +17,7 @@ export const enum ReportingEventId { ConnectedCallbackWhileDisconnected = 'ConnectedCallbackWhileDisconnected', ShadowModeUsage = 'ShadowModeUsage', ShadowSupportModeUsage = 'ShadowSupportModeUsage', + RenderModeMismatch = 'RenderModeMismatch', } export interface BasePayload { @@ -49,6 +50,10 @@ export interface StylesheetMutationPayload extends BasePayload { // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface ConnectedCallbackWhileDisconnectedPayload extends BasePayload {} +export interface RenderModeMismatchPayload extends BasePayload { + mode: RenderMode; +} + export interface ShadowModeUsagePayload extends BasePayload { mode: ShadowMode; } @@ -68,6 +73,7 @@ export type ReportingPayloadMapping = { [ReportingEventId.ConnectedCallbackWhileDisconnected]: ConnectedCallbackWhileDisconnectedPayload; [ReportingEventId.ShadowModeUsage]: ShadowModeUsagePayload; [ReportingEventId.ShadowSupportModeUsage]: ShadowSupportModeUsagePayload; + [ReportingEventId.RenderModeMismatch]: RenderModeMismatchPayload; }; export type ReportingDispatcher = ( diff --git a/packages/@lwc/engine-core/src/framework/template.ts b/packages/@lwc/engine-core/src/framework/template.ts index 31206ba9ff..cc2350a35c 100644 --- a/packages/@lwc/engine-core/src/framework/template.ts +++ b/packages/@lwc/engine-core/src/framework/template.ts @@ -47,6 +47,7 @@ import { MutableVNodes, VNodes, VStaticPart, VStaticPartElement, VStaticPartText import { RendererAPI } from './renderer'; import { getMapFromClassName } from './modules/computed-class-attr'; import { FragmentCacheKey, getFromFragmentCache, setInFragmentCache } from './fragment-cache'; +import { isReportingEnabled, report, ReportingEventId } from './reporting'; export interface Template { (api: RenderAPI, cmp: object, slotSet: SlotSet, cache: TemplateCache): VNodes; @@ -99,26 +100,31 @@ function validateSlots(vm: VM) { } } -function validateLightDomTemplate(template: Template, vm: VM) { - assertNotProd(); // should never leak to prod mode +function checkHasMatchingRenderMode(template: Template, vm: VM) { + // don't validate in prod environments where reporting is disabled + if (process.env.NODE_ENV === 'production' && !isReportingEnabled()) { + return; + } + // don't validate the default empty template - it is not inherently light or shadow if (template === defaultEmptyTemplate) { return; } - if (vm.renderMode === RenderMode.Light) { - if (template.renderMode !== 'light') { - logError( - `Light DOM components can't render shadow DOM templates. Add an 'lwc:render-mode="light"' directive to the root template tag of ${getComponentTag( - vm - )}.` - ); - } - } else { - if (!isUndefined(template.renderMode)) { - logError( - `Shadow DOM components template can't render light DOM templates. Either remove the 'lwc:render-mode' directive from ${getComponentTag( - vm - )} or set it to 'lwc:render-mode="shadow"` - ); + // TODO [#4663]: `renderMode` mismatch between template and component causes `console.error` but no error + // Note that `undefined` means shadow in this case, because shadow is the default. + const vmIsLight = vm.renderMode === RenderMode.Light; + const templateIsLight = template.renderMode === 'light'; + if (vmIsLight !== templateIsLight) { + report(ReportingEventId.RenderModeMismatch, { + tagName: vm.tagName, + mode: vm.renderMode, + }); + if (process.env.NODE_ENV !== 'production') { + const tagName = getComponentTag(vm); + const message = vmIsLight + ? `Light DOM components can't render shadow DOM templates. Add an 'lwc:render-mode="light"' directive to the root template tag of ${tagName}.` + : `Shadow DOM components template can't render light DOM templates. Either remove the 'lwc:render-mode' directive from ${tagName} or set it to 'lwc:render-mode="shadow"`; + + logError(message); } } } @@ -390,9 +396,7 @@ export function evaluateTemplate(vm: VM, html: Template): VNodes { ); } - if (process.env.NODE_ENV !== 'production') { - validateLightDomTemplate(html, vm); - } + checkHasMatchingRenderMode(html, vm); // Perf opt: do not reset the shadow root during the first rendering (there is // nothing to reset). diff --git a/packages/@lwc/integration-karma/test/template/directive-lwc-render-mode/index.spec.js b/packages/@lwc/integration-karma/test/template/directive-lwc-render-mode/index.spec.js index 4c0995ef09..dd2ccde1f1 100644 --- a/packages/@lwc/integration-karma/test/template/directive-lwc-render-mode/index.spec.js +++ b/packages/@lwc/integration-karma/test/template/directive-lwc-render-mode/index.spec.js @@ -1,9 +1,20 @@ import { createElement } from 'lwc'; - +import { attachReportingControlDispatcher, detachReportingControlDispatcher } from 'test-utils'; import Shadow from 'x/shadow'; import Light from 'x/light'; describe('lwc:render-mode', () => { + let dispatcher; + + beforeEach(() => { + dispatcher = jasmine.createSpy(); + attachReportingControlDispatcher(dispatcher, ['RenderModeMismatch']); + }); + + afterEach(() => { + detachReportingControlDispatcher(); + }); + it('should throw error if shadow template is passed to light component', () => { expect(() => { const root = createElement('x-test', { is: Light }); @@ -11,6 +22,16 @@ describe('lwc:render-mode', () => { }).toLogErrorDev( /Light DOM components can't render shadow DOM templates. Add an 'lwc:render-mode="light"' directive to the root template tag of ./ ); + + expect(dispatcher.calls.allArgs()).toEqual([ + [ + 'RenderModeMismatch', + { + tagName: 'x-test', + mode: 0, // RenderMode.Light + }, + ], + ]); }); it('should throw error if light template is passed to shadow component', () => { expect(() => { @@ -19,5 +40,15 @@ describe('lwc:render-mode', () => { }).toLogErrorDev( /Shadow DOM components template can't render light DOM templates. Either remove the 'lwc:render-mode' directive from or set it to 'lwc:render-mode="shadow"/ ); + + expect(dispatcher.calls.allArgs()).toEqual([ + [ + 'RenderModeMismatch', + { + tagName: 'x-test', + mode: 1, // RenderMode.Shadow + }, + ], + ]); }); });