From 91c2d973eb3467ac56f4c1132a9febe4458c0189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Caridy=20Pati=C3=B1o?= Date: Thu, 29 Aug 2019 15:04:19 -0400 Subject: [PATCH] fix(engine): tests and karma tests fixes --- .../src/framework/base-lightning-element.ts | 27 ++-- .../@lwc/engine/src/framework/component.ts | 11 -- .../engine/src/framework/decorators/api.ts | 15 +- .../src/framework/decorators/register.ts | 149 +++++++++++------- .../engine/src/framework/decorators/track.ts | 13 +- .../engine/src/framework/decorators/wire.ts | 2 +- packages/@lwc/engine/src/framework/def.ts | 98 +++++++++--- packages/@lwc/engine/src/framework/hooks.ts | 4 +- .../engine/src/framework/observed-fields.ts | 16 +- .../src/framework/performance-timing.ts | 1 - packages/@lwc/engine/src/framework/upgrade.ts | 4 +- packages/@lwc/engine/src/framework/vm.ts | 29 ++-- packages/@lwc/engine/src/framework/wc.ts | 18 +-- packages/@lwc/engine/src/framework/wiring.ts | 47 +++--- packages/@lwc/engine/src/shared/language.ts | 6 - .../test/api/getComponentDef/index.spec.js | 18 ++- 16 files changed, 251 insertions(+), 207 deletions(-) diff --git a/packages/@lwc/engine/src/framework/base-lightning-element.ts b/packages/@lwc/engine/src/framework/base-lightning-element.ts index 22c7067014..fbe9c9ae92 100644 --- a/packages/@lwc/engine/src/framework/base-lightning-element.ts +++ b/packages/@lwc/engine/src/framework/base-lightning-element.ts @@ -17,12 +17,10 @@ import assert from '../shared/assert'; import { freeze, create, - getOwnPropertyNames, isFunction, isNull, defineProperties, seal, - ArrayReduce, isObject, isFalse, } from '../shared/language'; @@ -143,7 +141,7 @@ export interface LightningElementConstructor { export declare var LightningElement: LightningElementConstructor; -export interface LightningElement extends EventTarget { +export interface LightningElement { // DOM - The good parts dispatchEvent(event: Event): boolean; addEventListener( @@ -561,22 +559,15 @@ BaseLightningElementConstructor.prototype = { }, }; -// Typescript is inferring the wrong function type for this particular -// overloaded method: https://github.com/Microsoft/TypeScript/issues/27972 -// @ts-ignore type-mismatch -const baseDescriptors: PropertyDescriptorMap = ArrayReduce.call( - getOwnPropertyNames(HTMLElementOriginalDescriptors), - (descriptors: PropertyDescriptorMap, propName: string) => { - descriptors[propName] = createBridgeToElementDescriptor( - propName, - HTMLElementOriginalDescriptors[propName] - ); - return descriptors; - }, - create(null) -); +export const lightningBasedDescriptors: PropertyDescriptorMap = create(null); +for (const propName in HTMLElementOriginalDescriptors) { + lightningBasedDescriptors[propName] = createBridgeToElementDescriptor( + propName, + HTMLElementOriginalDescriptors[propName] + ); +} -defineProperties(BaseLightningElementConstructor.prototype, baseDescriptors); +defineProperties(BaseLightningElementConstructor.prototype, lightningBasedDescriptors); if (process.env.NODE_ENV !== 'production') { patchLightningElementPrototypeWithRestrictions(BaseLightningElementConstructor.prototype); diff --git a/packages/@lwc/engine/src/framework/component.ts b/packages/@lwc/engine/src/framework/component.ts index e15c5133b0..46d3cff83e 100644 --- a/packages/@lwc/engine/src/framework/component.ts +++ b/packages/@lwc/engine/src/framework/component.ts @@ -19,7 +19,6 @@ import { tagNameGetter } from '../env/element'; import { Template } from './template'; import { ReactiveObserver } from '../libs/mutation-tracker'; import { LightningElementConstructor, LightningElement } from './base-lightning-element'; -import { installWireAdapters } from './wiring'; export type ErrorCallback = (error: any, stack: string) => void; export interface ComponentInterface extends LightningElement { @@ -77,16 +76,6 @@ export function createComponent(uninitializedVm: UninitializedVM, Ctor: Componen } } -export function linkComponent(vm: VM) { - if (process.env.NODE_ENV !== 'production') { - assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`); - } - // initializing the wire decorator per instance only when really needed - if (vm.def.wire.length > 0) { - installWireAdapters(vm); - } -} - export function getTemplateReactiveObserver(vm: VM): ReactiveObserver { return new ReactiveObserver(() => { if (process.env.NODE_ENV !== 'production') { diff --git a/packages/@lwc/engine/src/framework/decorators/api.ts b/packages/@lwc/engine/src/framework/decorators/api.ts index 1e84a8d8c8..453570d879 100644 --- a/packages/@lwc/engine/src/framework/decorators/api.ts +++ b/packages/@lwc/engine/src/framework/decorators/api.ts @@ -17,13 +17,12 @@ import { isFunction } from '../../shared/language'; * LWC Components. This function implements the internals of this * decorator. */ -// TODO: how to make api a decoratorFunction type as well? +export default function api(target: any, propertyKey: string, descriptor: PropertyDescriptor); export default function api() { if (process.env.NODE_ENV !== 'production') { - if (arguments.length !== 0) { - assert.fail(`@api decorator can only be used as a decorator function.`); - } + assert.fail(`@api decorator can only be used as a decorator function.`); } + throw new Error(); } export function createPublicPropertyDescriptor(key: string): PropertyDescriptor { @@ -78,7 +77,13 @@ export function createPublicAccessorDescriptor( ): PropertyDescriptor { const { get, set, enumerable, configurable } = descriptor; if (!isFunction(get)) { - throw new TypeError(); + if (process.env.NODE_ENV !== 'production') { + assert.invariant( + isFunction(get), + `Invalid compiler output for public accessor ${toString(key)} decorated with @api` + ); + } + throw new Error(); } return { get(this: ComponentInterface): any { diff --git a/packages/@lwc/engine/src/framework/decorators/register.ts b/packages/@lwc/engine/src/framework/decorators/register.ts index 5587c6b316..c0f1134b6b 100644 --- a/packages/@lwc/engine/src/framework/decorators/register.ts +++ b/packages/@lwc/engine/src/framework/decorators/register.ts @@ -11,7 +11,7 @@ import { defineProperty, getOwnPropertyDescriptor, isFunction, - ArrayPush, + create, toString, isFalse, } from '../../shared/language'; @@ -25,6 +25,8 @@ import { storeWiredFieldMeta, ConfigCallback, } from '../wiring'; +import { EmptyObject } from '../utils'; +import { createObservedFieldPropertyDescriptor } from '../observed-fields'; // data produced by compiler type WireCompilerMeta = Record; @@ -54,36 +56,48 @@ interface RegisterDecoratorMeta { readonly fields?: string[]; } -function validateObservedField(Ctor: ComponentConstructor, fieldName: string) { +function validateObservedField( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { if (process.env.NODE_ENV !== 'production') { - const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); if (!isUndefined(descriptor)) { assert.fail(`Compiler Error: Invalid field ${fieldName} declaration.`); } } } -function validateFieldDecoratedWithTrack(Ctor: ComponentConstructor, fieldName: string) { +function validateFieldDecoratedWithTrack( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { if (process.env.NODE_ENV !== 'production') { - const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); if (!isUndefined(descriptor)) { assert.fail(`Compiler Error: Invalid @track ${fieldName} declaration.`); } } } -function validateFieldDecoratedWithWire(Ctor: ComponentConstructor, fieldName: string) { +function validateFieldDecoratedWithWire( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { if (process.env.NODE_ENV !== 'production') { - const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); if (!isUndefined(descriptor)) { assert.fail(`Compiler Error: Invalid @wire(...) ${fieldName} field declaration.`); } } } -function validateMethodDecoratedWithWire(Ctor: ComponentConstructor, methodName: string) { +function validateMethodDecoratedWithWire( + Ctor: ComponentConstructor, + methodName: string, + descriptor: PropertyDescriptor | undefined +) { if (process.env.NODE_ENV !== 'production') { - const descriptor = getOwnPropertyDescriptor(Ctor.prototype, methodName); if ( isUndefined(descriptor) || !isFunction(descriptor.value) || @@ -94,18 +108,24 @@ function validateMethodDecoratedWithWire(Ctor: ComponentConstructor, methodName: } } -function validateFieldDecoratedWithApi(Ctor: ComponentConstructor, fieldName: string) { +function validateFieldDecoratedWithApi( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { if (process.env.NODE_ENV !== 'production') { - const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); if (!isUndefined(descriptor)) { assert.fail(`Compiler Error: Invalid @api ${fieldName} field declaration.`); } } } -function validateAccessorDecoratedWithApi(Ctor: ComponentConstructor, fieldName: string) { +function validateAccessorDecoratedWithApi( + Ctor: ComponentConstructor, + fieldName: string, + descriptor: PropertyDescriptor | undefined +) { if (process.env.NODE_ENV !== 'production') { - const descriptor = getOwnPropertyDescriptor(Ctor.prototype, fieldName); if (isUndefined(descriptor)) { assert.fail(`Compiler Error: Invalid @api get ${fieldName} accessor declaration.`); } else if (isFunction(descriptor.set)) { @@ -121,9 +141,12 @@ function validateAccessorDecoratedWithApi(Ctor: ComponentConstructor, fieldName: } } -function validateMethodDecoratedWithApi(Ctor: ComponentConstructor, methodName: string) { +function validateMethodDecoratedWithApi( + Ctor: ComponentConstructor, + methodName: string, + descriptor: PropertyDescriptor | undefined +) { if (process.env.NODE_ENV !== 'production') { - const descriptor = getOwnPropertyDescriptor(Ctor.prototype, methodName); if ( isUndefined(descriptor) || !isFunction(descriptor.value) || @@ -144,86 +167,91 @@ export function registerDecorators( ): ComponentConstructor { const proto = Ctor.prototype; const { publicProps, publicMethods, wire, track, fields } = meta; - const apiMethods = []; - const apiFields = []; - const wiredMethods = []; - const wiredFields = []; + const apiMethods: PropertyDescriptorMap = create(null); + const apiFields: PropertyDescriptorMap = create(null); + const wiredMethods: PropertyDescriptorMap = create(null); + const wiredFields: PropertyDescriptorMap = create(null); + const observedFields: PropertyDescriptorMap = create(null); + let descriptor: PropertyDescriptor | undefined; if (!isUndefined(publicProps)) { for (const fieldName in publicProps) { const propConfig = publicProps[fieldName]; - let descriptor: PropertyDescriptor | undefined; + descriptor = getOwnPropertyDescriptor(proto, fieldName); if (propConfig.config > 0) { // accessor declaration if (process.env.NODE_ENV !== 'production') { - validateAccessorDecoratedWithApi(Ctor, fieldName); + validateAccessorDecoratedWithApi(Ctor, fieldName, descriptor); + } + if (isUndefined(descriptor)) { + throw new Error(); } - descriptor = getOwnPropertyDescriptor(proto, fieldName); - descriptor = createPublicAccessorDescriptor( - fieldName, - descriptor as PropertyDescriptor - ); + descriptor = createPublicAccessorDescriptor(fieldName, descriptor); } else { // field declaration if (process.env.NODE_ENV !== 'production') { - validateFieldDecoratedWithApi(Ctor, fieldName); + validateFieldDecoratedWithApi(Ctor, fieldName, descriptor); } descriptor = createPublicPropertyDescriptor(fieldName); } - ArrayPush.call(apiFields, fieldName); + apiFields[fieldName] = descriptor; defineProperty(proto, fieldName, descriptor); } } if (!isUndefined(publicMethods)) { forEach.call(publicMethods, methodName => { + descriptor = getOwnPropertyDescriptor(proto, methodName); if (process.env.NODE_ENV !== 'production') { - validateMethodDecoratedWithApi(Ctor, methodName); + validateMethodDecoratedWithApi(Ctor, methodName, descriptor); + } + if (isUndefined(descriptor)) { + throw new Error(); } - ArrayPush.call(apiMethods, methodName); + apiMethods[methodName] = descriptor; }); } if (!isUndefined(wire)) { for (const fieldOrMethodName in wire) { const { adapter, method } = wire[fieldOrMethodName]; const configCallback = wire[fieldOrMethodName].config; + descriptor = getOwnPropertyDescriptor(proto, fieldOrMethodName); if (method === 1) { if (process.env.NODE_ENV !== 'production') { - validateMethodDecoratedWithWire(Ctor, fieldOrMethodName); + validateMethodDecoratedWithWire(Ctor, fieldOrMethodName, descriptor); } - ArrayPush.call(wiredMethods, fieldOrMethodName); - storeWiredMethodMeta( - Ctor, - fieldOrMethodName, - adapter, - proto[fieldOrMethodName] as (data: any) => void, - configCallback - ); + if (isUndefined(descriptor)) { + throw new Error(); + } + wiredMethods[fieldOrMethodName] = descriptor; + storeWiredMethodMeta(descriptor, adapter, configCallback); } else { if (process.env.NODE_ENV !== 'production') { - validateFieldDecoratedWithWire(Ctor, fieldOrMethodName); + validateFieldDecoratedWithWire(Ctor, fieldOrMethodName, descriptor); } - storeWiredFieldMeta(Ctor, fieldOrMethodName, adapter, configCallback); - ArrayPush.call(wiredFields, fieldOrMethodName); - defineProperty( - proto, - fieldOrMethodName, - internalWireFieldDecorator(fieldOrMethodName) - ); + descriptor = internalWireFieldDecorator(fieldOrMethodName); + wiredFields[fieldOrMethodName] = descriptor; + storeWiredFieldMeta(descriptor, adapter, configCallback); + defineProperty(proto, fieldOrMethodName, descriptor); } } } if (!isUndefined(track)) { for (const fieldName in track) { + descriptor = getOwnPropertyDescriptor(proto, fieldName); if (process.env.NODE_ENV !== 'production') { - validateFieldDecoratedWithTrack(Ctor, fieldName); + validateFieldDecoratedWithTrack(Ctor, fieldName, descriptor); } - defineProperty(proto, fieldName, internalTrackDecorator(fieldName)); + descriptor = internalTrackDecorator(fieldName); + defineProperty(proto, fieldName, descriptor); } } if (!isUndefined(fields)) { for (let i = 0, n = fields.length; i < n; i++) { + const fieldName = fields[i]; + descriptor = getOwnPropertyDescriptor(proto, fieldName); if (process.env.NODE_ENV !== 'production') { - validateObservedField(Ctor, fields[i]); + validateObservedField(Ctor, fieldName, descriptor); } + observedFields[fieldName] = createObservedFieldPropertyDescriptor(fieldName); } } setDecoratorsMeta(Ctor, { @@ -231,7 +259,7 @@ export function registerDecorators( apiFields, wiredMethods, wiredFields, - fields, + observedFields, }); return Ctor; } @@ -239,11 +267,11 @@ export function registerDecorators( const signedDecoratorToMetaMap: Map = new Map(); interface DecoratorMeta { - readonly apiMethods: string[]; - readonly apiFields: string[]; - readonly wiredMethods: string[]; - readonly wiredFields: string[]; - readonly fields?: string[]; + readonly apiMethods: PropertyDescriptorMap; + readonly apiFields: PropertyDescriptorMap; + readonly wiredMethods: PropertyDescriptorMap; + readonly wiredFields: PropertyDescriptorMap; + readonly observedFields: PropertyDescriptorMap; } function setDecoratorsMeta(Ctor: ComponentConstructor, meta: DecoratorMeta) { @@ -251,10 +279,11 @@ function setDecoratorsMeta(Ctor: ComponentConstructor, meta: DecoratorMeta) { } const defaultMeta: DecoratorMeta = { - apiMethods: [], - apiFields: [], - wiredMethods: [], - wiredFields: [], + apiMethods: EmptyObject, + apiFields: EmptyObject, + wiredMethods: EmptyObject, + wiredFields: EmptyObject, + observedFields: EmptyObject, }; export function getDecoratorsMeta(Ctor: ComponentConstructor): DecoratorMeta { diff --git a/packages/@lwc/engine/src/framework/decorators/track.ts b/packages/@lwc/engine/src/framework/decorators/track.ts index 65e7721053..a1a5d55f39 100644 --- a/packages/@lwc/engine/src/framework/decorators/track.ts +++ b/packages/@lwc/engine/src/framework/decorators/track.ts @@ -17,18 +17,17 @@ import { ComponentInterface } from '../component'; * LWC Components. This function can also be invoked directly * with any value to obtain the trackable version of the value. */ -// TODO: how to make track a decoratorFunction type as well? -export default function track(target?: any): any { +export default function track(target: any, propertyKey: string, descriptor: PropertyDescriptor); +export default function track(target: any): any { if (arguments.length === 1) { return reactiveMembrane.getProxy(target); } if (process.env.NODE_ENV !== 'production') { - if (arguments.length !== 3) { - assert.fail( - `@track decorator can only be used with one argument to return a trackable object, or as a decorator function.` - ); - } + assert.fail( + `@track decorator can only be used with one argument to return a trackable object, or as a decorator function.` + ); } + throw new Error(); } export function internalTrackDecorator(key: string): PropertyDescriptor { diff --git a/packages/@lwc/engine/src/framework/decorators/wire.ts b/packages/@lwc/engine/src/framework/decorators/wire.ts index 7a281b29e6..ba1ff48b69 100644 --- a/packages/@lwc/engine/src/framework/decorators/wire.ts +++ b/packages/@lwc/engine/src/framework/decorators/wire.ts @@ -22,7 +22,7 @@ export default function wire( if (process.env.NODE_ENV !== 'production') { assert.fail('@wire(adapter, config?) may only be used as a decorator.'); } - throw new TypeError(); + throw new Error(); } export function internalWireFieldDecorator(key: string): PropertyDescriptor { diff --git a/packages/@lwc/engine/src/framework/def.ts b/packages/@lwc/engine/src/framework/def.ts index 431fcd7126..15f5ba2453 100644 --- a/packages/@lwc/engine/src/framework/def.ts +++ b/packages/@lwc/engine/src/framework/def.ts @@ -16,20 +16,21 @@ import assert from '../shared/assert'; import { freeze, - getOwnPropertyNames, getPrototypeOf, isNull, setPrototypeOf, isUndefined, isFunction, - ArrayConcat, defineProperties, + keys, + create, + assign, } from '../shared/language'; -import { createObservedFieldsDescriptorMap } from './observed-fields'; import { resolveCircularModuleDependency, isCircularModuleDependency, ViewModelReflection, + EmptyObject, } from './utils'; import { ComponentConstructor, @@ -41,8 +42,9 @@ import { Template } from './template'; export interface ComponentDef { name: string; - props: string[]; - wire: string[]; + wire: PropertyDescriptorMap | undefined; + props: PropertyDescriptorMap; + methods: PropertyDescriptorMap; template: Template; ctor: ComponentConstructor; bridge: HTMLElementConstructor; @@ -100,7 +102,7 @@ function createComponentDef( const { name } = meta; let { template } = meta; const decoratorsMeta = getDecoratorsMeta(Ctor); - const { apiFields, apiMethods, wiredFields, wiredMethods, fields } = decoratorsMeta; + const { apiFields, apiMethods, wiredFields, wiredMethods, observedFields } = decoratorsMeta; const proto = Ctor.prototype; let { @@ -113,12 +115,18 @@ function createComponentDef( const superProto = getCtorProto(Ctor, subclassComponentName); const superDef: ComponentDef | null = (superProto as any) !== BaseLightningElement - ? getComponentDef(superProto, subclassComponentName) + ? getComponentInternalDef(superProto, subclassComponentName) : lightingElementDef; const SuperBridge = isNull(superDef) ? BaseBridgeElement : superDef.bridge; - const bridge = HTMLBridgeElementFactory(SuperBridge, apiFields, apiMethods); - const props = ArrayConcat.call(superDef.props, apiFields); - const wire = ArrayConcat.call(superDef.wire, wiredFields, wiredMethods); + const bridge = HTMLBridgeElementFactory(SuperBridge, keys(apiFields), keys(apiMethods)); + const props: PropertyDescriptorMap = assign(create(null), superDef.props, apiFields); + const methods: PropertyDescriptorMap = assign(create(null), superDef.methods, apiMethods); + const wire: PropertyDescriptorMap = assign( + create(null), + superDef.wire, + wiredFields, + wiredMethods + ); connectedCallback = connectedCallback || superDef.connectedCallback; disconnectedCallback = disconnectedCallback || superDef.disconnectedCallback; renderedCallback = renderedCallback || superDef.renderedCallback; @@ -126,15 +134,17 @@ function createComponentDef( render = render || superDef.render; template = template || superDef.template; - if (!isUndefined(fields)) { - defineProperties(proto, createObservedFieldsDescriptorMap(fields)); - } + // installing observed fields into the prototype + // TODO: consider move this to instance level fields to match the original + // semantics of the public fields. + defineProperties(proto, observedFields); const def: ComponentDef = { ctor: Ctor, name, wire, props, + methods, bridge, template, connectedCallback, @@ -190,11 +200,7 @@ export function isComponentConstructor(ctor: any): ctor is ComponentConstructor return false; } -/** - * EXPERIMENTAL: This function allows for the collection of internal - * component metadata. This API is subject to change or being removed. - */ -export function getComponentDef(Ctor: any, subclassComponentName?: string): ComponentDef { +export function getComponentInternalDef(Ctor: any, subclassComponentName?: string): ComponentDef { let def = CtorToDefMap.get(Ctor); if (isUndefined(def)) { @@ -241,8 +247,7 @@ export function setElementProto(elm: HTMLElement, def: ComponentDef) { setPrototypeOf(elm, def.bridge.prototype); } -import { HTMLElementOriginalDescriptors } from './html-properties'; -import { BaseLightningElement } from './base-lightning-element'; +import { BaseLightningElement, lightningBasedDescriptors } from './base-lightning-element'; import { BaseBridgeElement, HTMLBridgeElementFactory, @@ -251,13 +256,62 @@ import { import { getDecoratorsMeta } from './decorators/register'; import { defaultEmptyTemplate } from './secure-template'; import { getHiddenField } from '../shared/fields'; +import { getAttrNameFromPropName } from './attributes'; const lightingElementDef: ComponentDef = { ctor: BaseLightningElement, name: BaseLightningElement.name, - props: getOwnPropertyNames(HTMLElementOriginalDescriptors), - wire: [], + props: lightningBasedDescriptors, + methods: EmptyObject, + wire: EmptyObject, bridge: BaseBridgeElement, template: defaultEmptyTemplate, render: BaseLightningElement.prototype.render, }; + +// TODO: remove experimental public API getComponentDef +interface PropDef { + config: number; + type: string; + attr: string; +} +type PublicMethod = (...args: any[]) => any; +interface PublicComponentDef { + name: string; + props: Record; + methods: Record; + ctor: ComponentConstructor; +} + +/** + * EXPERIMENTAL: This function allows for the collection of internal + * component metadata. This API is subject to change or being removed. + */ +export function getComponentDef(Ctor: any, subclassComponentName?: string): PublicComponentDef { + const def = getComponentInternalDef(Ctor, subclassComponentName); + // From the internal def object, we need to extract the info that is useful + // for some external services, e.g.: Locker Service, usually, all they care + // is about the shape of the constructor, the internals of it are not relevant + // because they don't have a way to mess with that. + const { ctor, name, props, methods } = def; + const publicProps: Record = {}; + for (const key in props) { + // avoid leaking the reference to the public props descriptors + publicProps[key] = { + config: 0, // always a property + type: 'any', // no type inference for public services + attr: getAttrNameFromPropName(key), + }; + } + const publicMethods: Record = {}; + for (const key in methods) { + // avoid leaking the reference to the public method descriptors + publicMethods[key] = methods[key].value as (...args: any[]) => any; + } + return { + ctor, + name, + props: publicProps, + methods: publicMethods, + }; +} diff --git a/packages/@lwc/engine/src/framework/hooks.ts b/packages/@lwc/engine/src/framework/hooks.ts index 26f5e71349..2dfb87f41c 100644 --- a/packages/@lwc/engine/src/framework/hooks.ts +++ b/packages/@lwc/engine/src/framework/hooks.ts @@ -32,7 +32,7 @@ import { unlockDomMutation, lockDomMutation, } from './restrictions'; -import { getComponentDef, setElementProto } from './def'; +import { getComponentInternalDef, setElementProto } from './def'; import { getHiddenField } from '../shared/fields'; const noop = () => void 0; @@ -182,7 +182,7 @@ export function createViewModelHook(vnode: VCustomElement) { return; } const { mode, ctor, owner } = vnode; - const def = getComponentDef(ctor); + const def = getComponentInternalDef(ctor); setElementProto(elm, def); if (isTrue(useSyntheticShadow)) { const { shadowAttribute } = owner.context; diff --git a/packages/@lwc/engine/src/framework/observed-fields.ts b/packages/@lwc/engine/src/framework/observed-fields.ts index c703a465ca..2acd23b721 100644 --- a/packages/@lwc/engine/src/framework/observed-fields.ts +++ b/packages/@lwc/engine/src/framework/observed-fields.ts @@ -9,21 +9,9 @@ import { getComponentVM } from './vm'; import assert from '../shared/assert'; import { valueMutated, valueObserved } from '../libs/mutation-tracker'; import { isRendering, vmBeingRendered } from './invoker'; -import { isFalse, ArrayReduce } from '../shared/language'; +import { isFalse } from '../shared/language'; -export function createObservedFieldsDescriptorMap(fields: PropertyKey[]): PropertyDescriptorMap { - return ArrayReduce.call( - fields, - (acc: PropertyDescriptorMap, field) => { - acc[field] = createObservedFieldPropertyDescriptor(field); - - return acc; - }, - {} - ) as PropertyDescriptorMap; -} - -function createObservedFieldPropertyDescriptor(key: string): PropertyDescriptor { +export function createObservedFieldPropertyDescriptor(key: string): PropertyDescriptor { return { get(this: ComponentInterface): any { const vm = getComponentVM(this); diff --git a/packages/@lwc/engine/src/framework/performance-timing.ts b/packages/@lwc/engine/src/framework/performance-timing.ts index 82e167c9d0..b708931595 100644 --- a/packages/@lwc/engine/src/framework/performance-timing.ts +++ b/packages/@lwc/engine/src/framework/performance-timing.ts @@ -11,7 +11,6 @@ import { StringToLowerCase, isUndefined } from '../shared/language'; type MeasurementPhase = | 'constructor' - | 'wire' | 'render' | 'patch' | 'connectedCallback' diff --git a/packages/@lwc/engine/src/framework/upgrade.ts b/packages/@lwc/engine/src/framework/upgrade.ts index 3297a42375..667ab557f7 100644 --- a/packages/@lwc/engine/src/framework/upgrade.ts +++ b/packages/@lwc/engine/src/framework/upgrade.ts @@ -15,7 +15,7 @@ import { ViewModelReflection, } from './utils'; import { setHiddenField, getHiddenField, createFieldName } from '../shared/fields'; -import { getComponentDef, setElementProto } from './def'; +import { getComponentInternalDef, setElementProto } from './def'; import { patchCustomElementWithRestrictions } from './restrictions'; import { GlobalMeasurementPhase, startGlobalMeasure, endGlobalMeasure } from './performance-timing'; import { appendChild, insertBefore, replaceChild, removeChild } from '../env/node'; @@ -108,7 +108,7 @@ export function createElement(sel: string, options: CreateElementOptions): HTMLE Ctor = resolveCircularModuleDependency(Ctor); } - const def = getComponentDef(Ctor); + const def = getComponentInternalDef(Ctor); setElementProto(element, def); if (process.env.NODE_ENV !== 'production') { diff --git a/packages/@lwc/engine/src/framework/vm.ts b/packages/@lwc/engine/src/framework/vm.ts index e912d8414e..7cf69c46c2 100644 --- a/packages/@lwc/engine/src/framework/vm.ts +++ b/packages/@lwc/engine/src/framework/vm.ts @@ -5,10 +5,9 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import assert from '../shared/assert'; -import { getComponentDef } from './def'; +import { getComponentInternalDef } from './def'; import { createComponent, - linkComponent, renderComponent, ComponentConstructor, markComponentAsDirty, @@ -26,6 +25,7 @@ import { StringToLowerCase, isFalse, isArray, + getOwnPropertyNames, } from '../shared/language'; import { getHiddenField } from '../shared/fields'; import { @@ -56,7 +56,7 @@ import { parentElementGetter, parentNodeGetter } from '../env/node'; import { updateDynamicChildren, updateStaticChildren } from '../3rdparty/snabbdom/snabbdom'; import { hasDynamicChildren } from './hooks'; import { ReactiveObserver } from '../libs/mutation-tracker'; -import { connectWireAdapters, disconnectWireAdapters } from './wiring'; +import { connectWireAdapters, disconnectWireAdapters, installWireAdapters } from './wiring'; export interface SlotSet { [key: string]: VNodes; @@ -213,7 +213,7 @@ export function createVM(elm: HTMLElement, Ctor: ComponentConstructor, options: `VM creation requires a DOM element instead of ${elm}.` ); } - const def = getComponentDef(Ctor); + const def = getComponentInternalDef(Ctor); const { isRoot, mode, owner } = options; idx += 1; const uninitializedVm: UninitializedVM = { @@ -257,7 +257,10 @@ export function createVM(elm: HTMLElement, Ctor: ComponentConstructor, options: // link component to the wire service const initializedVm = uninitializedVm as VM; - linkComponent(initializedVm); + // initializing the wire decorator per instance only when really needed + if (hasWireAdapters(initializedVm)) { + installWireAdapters(initializedVm); + } } function rehydrate(vm: VM) { @@ -389,11 +392,7 @@ function runConnectedCallback(vm: VM) { if (connected) { invokeServiceHook(vm, connected); } - // TODO: eventually this should be done by node-reactions on the wire.ts directly - const { - def: { wire }, - } = vm; - if (wire.length > 0) { + if (hasWireAdapters(vm)) { connectWireAdapters(vm); } const { connectedCallback } = vm.def; @@ -410,6 +409,10 @@ function runConnectedCallback(vm: VM) { } } +function hasWireAdapters(vm: VM): boolean { + return getOwnPropertyNames(vm.def.wire).length > 0; +} + function runDisconnectedCallback(vm: VM) { if (process.env.NODE_ENV !== 'production') { assert.isTrue(vm && 'cmpRoot' in vm, `${vm} is not a vm.`); @@ -428,11 +431,7 @@ function runDisconnectedCallback(vm: VM) { if (disconnected) { invokeServiceHook(vm, disconnected); } - // TODO: eventually this should be done by node-reactions on the wire.ts directly - const { - def: { wire }, - } = vm; - if (wire.length > 0) { + if (hasWireAdapters(vm)) { disconnectWireAdapters(vm); } const { disconnectedCallback } = vm.def; diff --git a/packages/@lwc/engine/src/framework/wc.ts b/packages/@lwc/engine/src/framework/wc.ts index 75de20da97..fe511da8a9 100644 --- a/packages/@lwc/engine/src/framework/wc.ts +++ b/packages/@lwc/engine/src/framework/wc.ts @@ -5,10 +5,10 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import { ComponentConstructor } from './component'; -import { isUndefined, isObject, isNull, ArrayReduce, keys } from '../shared/language'; +import { isUndefined, isObject, isNull, keys } from '../shared/language'; import { createVM, appendRootVM, removeRootVM, getCustomElementVM, CreateVMInit } from './vm'; import { EmptyObject } from './utils'; -import { getComponentDef } from './def'; +import { getComponentInternalDef } from './def'; import { isAttributeLocked, getAttrNameFromPropName } from './attributes'; import { HTMLElementConstructor } from './base-bridge-element'; import { patchCustomElementWithRestrictions } from './restrictions'; @@ -29,17 +29,13 @@ export function buildCustomElementConstructor( Ctor: ComponentConstructor, options?: ShadowRootInit ): HTMLElementConstructor { - const { props, bridge: BaseElement } = getComponentDef(Ctor); + const { props, bridge: BaseElement } = getComponentInternalDef(Ctor); // generating the hash table for attributes to avoid duplicate fields // and facilitate validation and false positives in case of inheritance. - const attributeToPropMap = ArrayReduce.call( - props, - (reducer: Record, propName: string) => { - reducer[getAttrNameFromPropName(propName)] = propName; - return reducer; - }, - {} - ) as Record; + const attributeToPropMap: Record = {}; + for (const propName in props) { + attributeToPropMap[getAttrNameFromPropName(propName)] = propName; + } const normalizedOptions: CreateVMInit = { mode: 'open', isRoot: true, diff --git a/packages/@lwc/engine/src/framework/wiring.ts b/packages/@lwc/engine/src/framework/wiring.ts index fff6b52e02..6cf6948435 100644 --- a/packages/@lwc/engine/src/framework/wiring.ts +++ b/packages/@lwc/engine/src/framework/wiring.ts @@ -5,22 +5,16 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import assert from '../shared/assert'; -import { isUndefined, create, ArrayPush } from '../shared/language'; -import { ComponentConstructor, ComponentInterface } from './component'; +import { isUndefined, ArrayPush, getOwnPropertyNames } from '../shared/language'; +import { ComponentInterface } from './component'; import { valueMutated, ReactiveObserver } from '../libs/mutation-tracker'; import { VM, runWithBoundaryProtection } from './vm'; import { invokeComponentCallback } from './invoker'; import { dispatchEvent } from '../env/dom'; -const WireMetaMap: Map = new Map(); +const WireMetaMap: Map = new Map(); function noop(): void {} -function storeDef(Ctor: ComponentConstructor, key: string, def: WireDef) { - const record: Record = WireMetaMap.get(Ctor) || create(null); - record[key] = def; - WireMetaMap.set(Ctor, record); -} - function createFieldDataCallback(vm: VM, name: string) { const { component, cmpFields } = vm; return (value: any) => { @@ -167,8 +161,6 @@ interface WireAdapter { type WireAdapterSchemaValue = 'optional' | 'required'; -type WireHash = Record; - interface WireDef { method?: (data: any) => void; adapter: WireAdapterConstructor; @@ -202,27 +194,25 @@ export interface WireAdapterConstructor { } export function storeWiredMethodMeta( - Ctor: ComponentConstructor, - methodName: string, + descriptor: PropertyDescriptor, adapter: WireAdapterConstructor, - method: (data: any) => void, configCallback: ConfigCallback ) { // support for callable adapters if ((adapter as any).adapter) { adapter = (adapter as any).adapter; } + const method = descriptor.value; const def: WireMethodDef = { adapter, method, configCallback, }; - storeDef(Ctor, methodName, def); + WireMetaMap.set(descriptor, def); } export function storeWiredFieldMeta( - Ctor: ComponentConstructor, - fieldName: string, + descriptor: PropertyDescriptor, adapter: WireAdapterConstructor, configCallback: ConfigCallback ) { @@ -234,15 +224,14 @@ export function storeWiredFieldMeta( adapter, configCallback, }; - storeDef(Ctor, fieldName, def); + WireMetaMap.set(descriptor, def); } export function installWireAdapters(vm: VM) { const { - def: { ctor }, + def: { wire }, } = vm; - const meta = WireMetaMap.get(ctor); - if (isUndefined(meta)) { + if (getOwnPropertyNames(wire).length === 0) { if (process.env.NODE_ENV !== 'production') { assert.fail( `Internal Error: wire adapters should only be installed in instances with at least one wire declaration.` @@ -251,11 +240,17 @@ export function installWireAdapters(vm: VM) { } else { const connect = (vm.context.wiredConnecting = []); const disconnect = (vm.context.wiredDisconnecting = []); - for (const name in meta) { - const wireDef = meta[name]; - const connector = createConnector(vm, name, wireDef); - ArrayPush.call(connect, () => connector.connect()); - ArrayPush.call(disconnect, () => connector.disconnect()); + for (const fieldNameOrMethod in wire) { + const descriptor = wire[fieldNameOrMethod]; + const wireDef = WireMetaMap.get(descriptor); + if (process.env.NODE_ENV !== 'production') { + assert.invariant(wireDef, `Internal Error: invalid wire definition found.`); + } + if (!isUndefined(wireDef)) { + const adapterInstance = createConnector(vm, fieldNameOrMethod, wireDef); + ArrayPush.call(connect, () => adapterInstance.connect()); + ArrayPush.call(disconnect, () => adapterInstance.disconnect()); + } } } } diff --git a/packages/@lwc/engine/src/shared/language.ts b/packages/@lwc/engine/src/shared/language.ts index e3706c83e0..b4b7130a7c 100644 --- a/packages/@lwc/engine/src/shared/language.ts +++ b/packages/@lwc/engine/src/shared/language.ts @@ -23,14 +23,12 @@ const { slice: ArraySlice, splice: ArraySplice, unshift: ArrayUnshift, - shift: ArrayShift, indexOf: ArrayIndexOf, push: ArrayPush, map: ArrayMap, join: ArrayJoin, concat: ArrayConcat, forEach, - reduce: ArrayReduce, } = Array.prototype; const { @@ -38,7 +36,6 @@ const { toLowerCase: StringToLowerCase, charCodeAt: StringCharCodeAt, slice: StringSlice, - split: StringSplit, } = String.prototype; export { @@ -46,7 +43,6 @@ export { StringReplace, StringCharCodeAt, StringSlice, - StringSplit, freeze, seal, keys, @@ -59,11 +55,9 @@ export { getOwnPropertyDescriptor, getOwnPropertyNames, hasOwnProperty, - ArrayReduce, ArraySlice, ArraySplice, ArrayUnshift, - ArrayShift, ArrayMap, ArrayJoin, ArrayConcat, diff --git a/packages/integration-karma/test/api/getComponentDef/index.spec.js b/packages/integration-karma/test/api/getComponentDef/index.spec.js index cf49bc494b..0c593ec041 100644 --- a/packages/integration-karma/test/api/getComponentDef/index.spec.js +++ b/packages/integration-karma/test/api/getComponentDef/index.spec.js @@ -149,12 +149,12 @@ describe('@api', () => { expect(props).toEqual( jasmine.objectContaining({ getterOnly: { - config: 1, + config: 0, type: 'any', attr: 'getter-only', }, getterAndSetter: { - config: 3, + config: 0, type: 'any', attr: 'getter-and-setter', }, @@ -203,8 +203,14 @@ describe('@api', () => { }); }); +/** + * Skipping the tests for the definitions of the wire when calling getComponentDef + * because that's implementation details of the component, and should not be exposed, + * ideally no one is using that information since it is not accessible from outside + * the component. + */ describe('@wire', () => { - it('should return the wired properties in wire object', () => { + xit('should return the wired properties in wire object', () => { const { wire } = getComponentDef(WireProperties); expect(wire).toEqualWireSettings({ foo: { @@ -238,7 +244,7 @@ describe('@wire', () => { }); }); - it('should return the wired methods in the wire object with a method flag', () => { + xit('should return the wired methods in the wire object with a method flag', () => { const { wire } = getComponentDef(WireMethods); expect(wire).toEqualWireSettings({ foo: { @@ -275,7 +281,7 @@ describe('@wire', () => { }); }); - it('should inherit wire properties from the base class', () => { + xit('should inherit wire properties from the base class', () => { const { wire } = getComponentDef(WirePropertiesInheritance); expect(wire).toEqualWireSettings({ parentProp: { @@ -311,7 +317,7 @@ describe('@wire', () => { }); }); - it('should inherit the wire methods from the case class', () => { + xit('should inherit the wire methods from the case class', () => { const { wire } = getComponentDef(WireMethodsInheritance); expect(wire).toEqualWireSettings({ parentMethod: {