Skip to content

Commit b7105ae

Browse files
committed
refactor: improve option type check warnings
1 parent adff008 commit b7105ae

File tree

12 files changed

+115
-79
lines changed

12 files changed

+115
-79
lines changed

src/core/instance/state.js

-13
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,6 @@ export function initState (vm: Component) {
6161
}
6262
}
6363

64-
function checkOptionType (vm: Component, name: string) {
65-
const option = vm.$options[name]
66-
if (!isPlainObject(option)) {
67-
warn(
68-
`component option "${name}" should be an object.`,
69-
vm
70-
)
71-
}
72-
}
73-
7464
function initProps (vm: Component, propsOptions: Object) {
7565
const propsData = vm.$options.propsData || {}
7666
const props = vm._props = {}
@@ -171,7 +161,6 @@ function getData (data: Function, vm: Component): any {
171161
const computedWatcherOptions = { lazy: true }
172162

173163
function initComputed (vm: Component, computed: Object) {
174-
process.env.NODE_ENV !== 'production' && checkOptionType(vm, 'computed')
175164
const watchers = vm._computedWatchers = Object.create(null)
176165
// computed properties are just getters during SSR
177166
const isSSR = isServerRendering()
@@ -260,7 +249,6 @@ function createComputedGetter (key) {
260249
}
261250

262251
function initMethods (vm: Component, methods: Object) {
263-
process.env.NODE_ENV !== 'production' && checkOptionType(vm, 'methods')
264252
const props = vm.$options.props
265253
for (const key in methods) {
266254
if (process.env.NODE_ENV !== 'production') {
@@ -289,7 +277,6 @@ function initMethods (vm: Component, methods: Object) {
289277
}
290278

291279
function initWatch (vm: Component, watch: Object) {
292-
process.env.NODE_ENV !== 'production' && checkOptionType(vm, 'watch')
293280
for (const key in watch) {
294281
const handler = watch[key]
295282
if (Array.isArray(handler)) {

src/core/util/debug.js

+7-9
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,13 @@ if (process.env.NODE_ENV !== 'production') {
3737
if (vm.$root === vm) {
3838
return '<Root>'
3939
}
40-
let name = typeof vm === 'string'
41-
? vm
42-
: typeof vm === 'function' && vm.options
43-
? vm.options.name
44-
: vm._isVue
45-
? vm.$options.name || vm.$options._componentTag
46-
: vm.name
47-
48-
const file = vm._isVue && vm.$options.__file
40+
const options = typeof vm === 'function' && vm.cid != null
41+
? vm.options
42+
: vm._isVue
43+
? vm.$options || vm.constructor.options
44+
: vm || {}
45+
let name = options.name || options._componentTag
46+
const file = options.__file
4947
if (!name && file) {
5048
const match = file.match(/([^/\\]+)\.vue$/)
5149
name = match && match[1]

src/core/util/options.js

+57-10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
extend,
1515
hasOwn,
1616
camelize,
17+
toRawType,
1718
capitalize,
1819
isBuiltInTag,
1920
isPlainObject
@@ -155,11 +156,19 @@ LIFECYCLE_HOOKS.forEach(hook => {
155156
* a three-way merge between constructor options, instance
156157
* options and parent options.
157158
*/
158-
function mergeAssets (parentVal: ?Object, childVal: ?Object): Object {
159+
function mergeAssets (
160+
parentVal: ?Object,
161+
childVal: ?Object,
162+
vm?: Component,
163+
key: string
164+
): Object {
159165
const res = Object.create(parentVal || null)
160-
return childVal
161-
? extend(res, childVal)
162-
: res
166+
if (childVal) {
167+
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
168+
return extend(res, childVal)
169+
} else {
170+
return res
171+
}
163172
}
164173

165174
ASSET_TYPES.forEach(function (type) {
@@ -172,12 +181,20 @@ ASSET_TYPES.forEach(function (type) {
172181
* Watchers hashes should not overwrite one
173182
* another, so we merge them as arrays.
174183
*/
175-
strats.watch = function (parentVal: ?Object, childVal: ?Object): ?Object {
184+
strats.watch = function (
185+
parentVal: ?Object,
186+
childVal: ?Object,
187+
vm?: Component,
188+
key: string
189+
): ?Object {
176190
// work around Firefox's Object.prototype.watch...
177191
if (parentVal === nativeWatch) parentVal = undefined
178192
if (childVal === nativeWatch) childVal = undefined
179193
/* istanbul ignore if */
180194
if (!childVal) return Object.create(parentVal || null)
195+
if (process.env.NODE_ENV !== 'production') {
196+
assertObjectType(key, childVal, vm)
197+
}
181198
if (!parentVal) return childVal
182199
const ret = {}
183200
extend(ret, parentVal)
@@ -200,7 +217,15 @@ strats.watch = function (parentVal: ?Object, childVal: ?Object): ?Object {
200217
strats.props =
201218
strats.methods =
202219
strats.inject =
203-
strats.computed = function (parentVal: ?Object, childVal: ?Object): ?Object {
220+
strats.computed = function (
221+
parentVal: ?Object,
222+
childVal: ?Object,
223+
vm?: Component,
224+
key: string
225+
): ?Object {
226+
if (childVal && process.env.NODE_ENV !== 'production') {
227+
assertObjectType(key, childVal, vm)
228+
}
204229
if (!parentVal) return childVal
205230
const ret = Object.create(null)
206231
extend(ret, parentVal)
@@ -237,7 +262,7 @@ function checkComponents (options: Object) {
237262
* Ensure all props option syntax are normalized into the
238263
* Object-based format.
239264
*/
240-
function normalizeProps (options: Object) {
265+
function normalizeProps (options: Object, vm: ?Component) {
241266
const props = options.props
242267
if (!props) return
243268
const res = {}
@@ -261,14 +286,20 @@ function normalizeProps (options: Object) {
261286
? val
262287
: { type: val }
263288
}
289+
} else if (process.env.NODE_ENV !== 'production' && props) {
290+
warn(
291+
`Invalid value for option "props": expected an Array or an Object, ` +
292+
`but got ${toRawType(props)}.`,
293+
vm
294+
)
264295
}
265296
options.props = res
266297
}
267298

268299
/**
269300
* Normalize all injections into Object-based format
270301
*/
271-
function normalizeInject (options: Object) {
302+
function normalizeInject (options: Object, vm: ?Component) {
272303
const inject = options.inject
273304
const normalized = options.inject = {}
274305
if (Array.isArray(inject)) {
@@ -282,6 +313,12 @@ function normalizeInject (options: Object) {
282313
? extend({ from: key }, val)
283314
: { from: val }
284315
}
316+
} else if (process.env.NODE_ENV !== 'production' && inject) {
317+
warn(
318+
`Invalid value for option "inject": expected an Array or an Object, ` +
319+
`but got ${toRawType(inject)}.`,
320+
vm
321+
)
285322
}
286323
}
287324

@@ -300,6 +337,16 @@ function normalizeDirectives (options: Object) {
300337
}
301338
}
302339

340+
function assertObjectType (name: string, value: any, vm: ?Component) {
341+
if (!isPlainObject(value)) {
342+
warn(
343+
`Invalid value for option "${name}": expected an Object, ` +
344+
`but got ${toRawType(value)}.`,
345+
vm
346+
)
347+
}
348+
}
349+
303350
/**
304351
* Merge two option objects into a new one.
305352
* Core utility used in both instantiation and inheritance.
@@ -317,8 +364,8 @@ export function mergeOptions (
317364
child = child.options
318365
}
319366

320-
normalizeProps(child)
321-
normalizeInject(child)
367+
normalizeProps(child, vm)
368+
normalizeInject(child, vm)
322369
normalizeDirectives(child)
323370
const extendsFrom = child.extends
324371
if (extendsFrom) {

src/core/util/props.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { observe, observerState } from '../observer/index'
55
import {
66
hasOwn,
77
isObject,
8+
toRawType,
89
hyphenate,
910
capitalize,
1011
isPlainObject
@@ -118,9 +119,9 @@ function assertProp (
118119
}
119120
if (!valid) {
120121
warn(
121-
'Invalid prop: type check failed for prop "' + name + '".' +
122-
' Expected ' + expectedTypes.map(capitalize).join(', ') +
123-
', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.',
122+
`Invalid prop: type check failed for prop "${name}".` +
123+
` Expected ${expectedTypes.map(capitalize).join(', ')}` +
124+
`, got ${toRawType(value)}.`,
124125
vm
125126
)
126127
return

src/shared/util.js

+7
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,15 @@ export function isObject (obj: mixed): boolean %checks {
3838
return obj !== null && typeof obj === 'object'
3939
}
4040

41+
/**
42+
* Get the raw type string of a value e.g. [object Object]
43+
*/
4144
const _toString = Object.prototype.toString
4245

46+
export function toRawType (value: any): string {
47+
return _toString.call(value).slice(8, -1)
48+
}
49+
4350
/**
4451
* Strict object type check. Only returns true
4552
* for plain JavaScript objects.

test/helpers/test-object-option.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import Vue from 'vue'
22

33
export default function testObjectOption (name) {
4-
it('should warn non object value', () => {
4+
it(`Options ${name}: should warn non object value`, () => {
55
const options = {}
66
options[name] = () => {}
77
new Vue(options)
8-
expect(`component option "${name}" should be an object`).toHaveBeenWarned()
8+
expect(`Invalid value for option "${name}"`).toHaveBeenWarned()
99
})
1010

11-
it('should not warn valid object value', () => {
11+
it(`Options ${name}: should not warn valid object value`, () => {
1212
const options = {}
1313
options[name] = {}
1414
new Vue(options)
15-
expect(`component option "${name}" should be an object`).not.toHaveBeenWarned()
15+
expect(`Invalid value for option "${name}"`).not.toHaveBeenWarned()
1616
})
1717
}

test/unit/features/options/components.spec.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import Vue from 'vue'
22
import { UA } from 'core/util/env'
3+
import testObjectOption from '../../../helpers/test-object-option'
34

45
describe('Options components', () => {
6+
testObjectOption('components')
7+
58
it('should accept plain object', () => {
69
const vm = new Vue({
710
template: '<test></test>',

test/unit/features/options/computed.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Vue from 'vue'
22
import testObjectOption from '../../../helpers/test-object-option'
33

44
describe('Options computed', () => {
5+
testObjectOption('computed')
6+
57
it('basic usage', done => {
68
const vm = new Vue({
79
template: '<div>{{ b }}</div>',
@@ -49,8 +51,6 @@ describe('Options computed', () => {
4951
}).then(done)
5052
})
5153

52-
testObjectOption('computed')
53-
5454
it('warn with setter and no getter', () => {
5555
const vm = new Vue({
5656
template: `

test/unit/features/options/extends.spec.js

+23-36
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Vue from 'vue'
2+
import { nativeWatch } from 'core/util/env'
23

34
describe('Options extends', () => {
45
it('should work on objects', () => {
@@ -47,42 +48,28 @@ describe('Options extends', () => {
4748
expect(vm.c).toBe(3)
4849
})
4950

50-
it('should work with global mixins + Object.prototype.watch', done => {
51-
let fakeWatch = false
52-
if (!Object.prototype.watch) {
53-
fakeWatch = true
54-
// eslint-disable-next-line no-extend-native
55-
Object.defineProperty(Object.prototype, 'watch', {
56-
writable: true,
57-
configurable: true,
58-
enumerable: false,
59-
value: () => {}
60-
})
61-
}
62-
63-
Vue.mixin({})
51+
if (nativeWatch) {
52+
it('should work with global mixins + Object.prototype.watch', done => {
53+
Vue.mixin({})
6454

65-
const spy = jasmine.createSpy('watch')
66-
const A = Vue.extend({
67-
data: function () {
68-
return { a: 1 }
69-
},
70-
watch: {
71-
a: spy
72-
},
73-
created: function () {
74-
this.a = 2
75-
}
76-
})
77-
new Vue({
78-
extends: A
55+
const spy = jasmine.createSpy('watch')
56+
const A = Vue.extend({
57+
data: function () {
58+
return { a: 1 }
59+
},
60+
watch: {
61+
a: spy
62+
},
63+
created: function () {
64+
this.a = 2
65+
}
66+
})
67+
new Vue({
68+
extends: A
69+
})
70+
waitForUpdate(() => {
71+
expect(spy).toHaveBeenCalledWith(2, 1)
72+
}).then(done)
7973
})
80-
waitForUpdate(() => {
81-
expect(spy).toHaveBeenCalledWith(2, 1)
82-
83-
if (fakeWatch) {
84-
delete Object.prototype.watch
85-
}
86-
}).then(done)
87-
})
74+
}
8875
})

test/unit/features/options/inject.spec.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import Vue from 'vue'
22
import { Observer } from 'core/observer/index'
33
import { isNative, isObject, hasOwn } from 'core/util/index'
4+
import testObjectOption from '../../../helpers/test-object-option'
45

56
describe('Options provide/inject', () => {
7+
testObjectOption('inject')
8+
69
let injected
710
const injectedComp = {
811
inject: ['foo', 'bar'],

test/unit/features/options/methods.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Vue from 'vue'
22
import testObjectOption from '../../../helpers/test-object-option'
33

44
describe('Options methods', () => {
5+
testObjectOption('methods')
6+
57
it('should have correct context', () => {
68
const vm = new Vue({
79
data: {
@@ -17,8 +19,6 @@ describe('Options methods', () => {
1719
expect(vm.a).toBe(2)
1820
})
1921

20-
testObjectOption('methods')
21-
2222
it('should warn undefined methods', () => {
2323
new Vue({
2424
methods: {

0 commit comments

Comments
 (0)