From e6f0e7b00fd88fe672c6b8cc018625f8b9ce5f72 Mon Sep 17 00:00:00 2001 From: ktsn Date: Fri, 1 Dec 2017 01:34:03 +0900 Subject: [PATCH 1/2] fix: avoid to overwrite component options on IE <= 10 --- src/component.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++---- src/util.ts | 7 +++++ test/test.ts | 21 +++++++++++++ 3 files changed, 102 insertions(+), 6 deletions(-) diff --git a/src/component.ts b/src/component.ts index 3188127..12cdb42 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,6 +1,7 @@ import Vue, { ComponentOptions } from 'vue' import { VueClass, DecoratedClass } from './declarations' import { collectDataFromConstructor } from './data' +import { hasProto, isPrimitive, warn } from './util' export const $internalHooks = [ 'data', @@ -68,12 +69,79 @@ export function componentFactory ( : Vue const Extended = Super.extend(options) - Object.getOwnPropertyNames(Component).forEach(key => { - if (key !== 'prototype') { - const descriptor = Object.getOwnPropertyDescriptor(Component, key)! - Object.defineProperty(Extended, key, descriptor) - } - }) + forwardStaticMembers(Extended, Component, Super) return Extended } + +const reservedPropertyNames = [ + // Unique id + 'cid', + + // Super Vue constructor + 'super', + + // Component options that will be used by the component + 'options', + 'superOptions', + 'extendOptions', + 'sealedOptions', + + // Private assets + 'component', + 'directive', + 'filter' +] + +function forwardStaticMembers (Extended: typeof Vue, Original: typeof Vue, Super: typeof Vue): void { + // We have to use getOwnPropertyNames since Babel registers methods as non-enumerable + Object.getOwnPropertyNames(Original).forEach(key => { + // `prototype` should not be overwritten + if (key === 'prototype') { + return + } + + const descriptor = Object.getOwnPropertyDescriptor(Original, key)! + + // If the user agent does not support `__proto__` or its family (IE <= 10), + // the sub class properties may be inherited properties from the super class in TypeScript. + // We need to exclude such properties to prevent to overwrite + // the component options object which stored on the extended constructor (See #192). + // If the value is a referenced value (object or function), + // we can check equality of them and exclude it if they have the same reference. + // If it is a primitive value, it will be forwarded for safety. + if (!hasProto) { + + // Only `cid` is explicitly exluded from property forwarding + // because we cannot detect whether it is a inherited property or not + // on the no `__proto__` environment even though the property is reserved. + if (key === 'cid') { + return + } + + const superDescriptor = Object.getOwnPropertyDescriptor(Super, key) + + if ( + !isPrimitive(descriptor.value) + && superDescriptor + && superDescriptor.value === descriptor.value + ) { + return + } + } + + // Warn if the users manually declare reserved properties + if ( + process.env.NODE_ENV !== 'production' + && reservedPropertyNames.indexOf(key) >= 0 + ) { + warn( + `Static property name '${key}' declared on class '${Original.name}' ` + + 'conflicts with reserved property name of Vue internal. ' + + 'It may cause unexpected behavior of the component. Consider renaming the property.' + ) + } + + Object.defineProperty(Extended, key, descriptor) + }) +} diff --git a/src/util.ts b/src/util.ts index f3afbec..4793454 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,6 +3,8 @@ import { DecoratedClass } from './declarations' export const noop = () => {} +export const hasProto = { __proto__: [] } instanceof Array + export interface VueDecorator { // Class decorator (Ctor: typeof Vue): void @@ -29,6 +31,11 @@ export function createDecorator (factory: (options: ComponentOptions, key: } } +export function isPrimitive (value: any): boolean { + const type = typeof value + return value == null || (type !== "object" && type !== "function") +} + export function warn (message: string): void { if (typeof console !== 'undefined') { console.warn('[vue-class-component] ' + message) diff --git a/test/test.ts b/test/test.ts index 0f1cfa1..cae0c5d 100644 --- a/test/test.ts +++ b/test/test.ts @@ -321,4 +321,25 @@ describe('vue-class-component', () => { expect(MyComp.myValue).to.equal(52) expect(MyComp.myFunc()).to.equal(42) }) + + it('should warn if declared static property uses a reserved name but not prevent forwarding', function () { + const originalWarn = console.warn + console.warn = td.function('warn') as any + + @Component + class MyComp extends Vue { + static options = 'test' + } + + const message = '[vue-class-component] ' + + 'Static property name \'options\' declared on class \'MyComp\' conflicts with ' + + 'reserved property name of Vue internal. It may cause unexpected behavior of the component. Consider renaming the property.' + + expect(MyComp.options).to.equal('test') + try { + td.verify(console.warn(message)) + } finally { + console.warn = originalWarn + } + }) }) From e15b8f87aaa9e82a5f63bee61cff98fbcd353eaf Mon Sep 17 00:00:00 2001 From: ktsn Date: Sun, 3 Dec 2017 00:41:42 +0900 Subject: [PATCH 2/2] fix: prevent reconfiguration if native properties does not allow it --- src/component.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/component.ts b/src/component.ts index 12cdb42..d110df1 100644 --- a/src/component.ts +++ b/src/component.ts @@ -101,6 +101,12 @@ function forwardStaticMembers (Extended: typeof Vue, Original: typeof Vue, Super return } + // Some browsers does not allow reconfigure built-in properties + const extendedDescriptor = Object.getOwnPropertyDescriptor(Extended, key) + if (extendedDescriptor && !extendedDescriptor.configurable) { + return + } + const descriptor = Object.getOwnPropertyDescriptor(Original, key)! // If the user agent does not support `__proto__` or its family (IE <= 10),