Skip to content

Commit 05f4ade

Browse files
committed
feat(runtime-vapor): warning with component stack
1 parent aa5d87b commit 05f4ade

File tree

5 files changed

+266
-8
lines changed

5 files changed

+266
-8
lines changed

packages/runtime-vapor/__tests__/renderEffect.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe('renderEffect', () => {
180180
}).rejects.toThrow('error in beforeUpdate')
181181

182182
expect(
183-
'[Vue warn] Unhandled error during execution of beforeUpdate hook',
183+
'[Vue warn]: Unhandled error during execution of beforeUpdate hook',
184184
).toHaveBeenWarned()
185185
})
186186

@@ -210,7 +210,7 @@ describe('renderEffect', () => {
210210
}).rejects.toThrow('error in updated')
211211

212212
expect(
213-
'[Vue warn] Unhandled error during execution of updated',
213+
'[Vue warn]: Unhandled error during execution of updated',
214214
).toHaveBeenWarned()
215215
})
216216

packages/runtime-vapor/src/component.ts

+39-5
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import type { Data } from '@vue/shared'
3131
export type Component = FunctionalComponent | ObjectComponent
3232

3333
export type SetupFn = (props: any, ctx: SetupContext) => Block | Data | void
34-
export type FunctionalComponent = SetupFn & Omit<ObjectComponent, 'setup'>
34+
export type FunctionalComponent = SetupFn &
35+
Omit<ObjectComponent, 'setup'> & {
36+
displayName?: string
37+
}
3538

3639
export type SetupContext<E = EmitsOptions> = E extends any
3740
? {
@@ -96,15 +99,46 @@ export function createSetupContext(
9699
}
97100
}
98101

99-
export interface ObjectComponent {
100-
props?: ComponentPropsOptions
102+
export interface ObjectComponent extends ComponentInternalOptions {
103+
setup?: SetupFn
101104
inheritAttrs?: boolean
105+
props?: ComponentPropsOptions
102106
emits?: EmitsOptions
103-
setup?: SetupFn
104107
render?(ctx: any): Block
108+
109+
name?: string
105110
vapor?: boolean
106111
}
107112

113+
// Note: can't mark this whole interface internal because some public interfaces
114+
// extend it.
115+
export interface ComponentInternalOptions {
116+
/**
117+
* @internal
118+
*/
119+
__scopeId?: string
120+
/**
121+
* @internal
122+
*/
123+
__cssModules?: Data
124+
/**
125+
* @internal
126+
*/
127+
__hmrId?: string
128+
/**
129+
* Compat build only, for bailing out of certain compatibility behavior
130+
*/
131+
__isBuiltIn?: boolean
132+
/**
133+
* This one should be exposed so that devtools can make use of it
134+
*/
135+
__file?: string
136+
/**
137+
* name inferred from filename
138+
*/
139+
__name?: string
140+
}
141+
108142
type LifecycleHook<TFn = Function> = TFn[] | null
109143

110144
export const componentKey = Symbol(__DEV__ ? `componentKey` : ``)
@@ -121,7 +155,7 @@ export interface ComponentInternalInstance {
121155

122156
provides: Data
123157
scope: EffectScope
124-
component: FunctionalComponent | ObjectComponent
158+
component: Component
125159
comps: Set<ComponentInternalInstance>
126160
dirs: Map<Node, DirectiveBinding[]>
127161

packages/runtime-vapor/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export {
4141
getCurrentWatcher,
4242
} from '@vue/reactivity'
4343

44+
import { NOOP } from '@vue/shared'
45+
import { warn as _warn } from './warning'
46+
export const warn = (__DEV__ ? _warn : NOOP) as typeof _warn
47+
4448
export { nextTick } from './scheduler'
4549
export {
4650
getCurrentInstance,

packages/runtime-vapor/src/warning.ts

+198-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,200 @@
1+
import {
2+
type Component,
3+
type ComponentInternalInstance,
4+
currentInstance,
5+
} from './component'
6+
import { isFunction, isString } from '@vue/shared'
7+
import { isRef, pauseTracking, resetTracking, toRaw } from '@vue/reactivity'
8+
import { VaporErrorCodes, callWithErrorHandling } from './errorHandling'
9+
import type { NormalizedRawProps } from './componentProps'
10+
11+
type TraceEntry = {
12+
instance: ComponentInternalInstance
13+
recurseCount: number
14+
}
15+
16+
type ComponentTraceStack = TraceEntry[]
17+
118
export function warn(msg: string, ...args: any[]) {
2-
console.warn(`[Vue warn] ${msg}`, ...args)
19+
// avoid props formatting or warn handler tracking deps that might be mutated
20+
// during patch, leading to infinite recursion.
21+
pauseTracking()
22+
23+
const instance = currentInstance
24+
const appWarnHandler = instance && instance.appContext.config.warnHandler
25+
const trace = getComponentTrace()
26+
27+
if (appWarnHandler) {
28+
callWithErrorHandling(
29+
appWarnHandler,
30+
instance,
31+
VaporErrorCodes.APP_WARN_HANDLER,
32+
[
33+
msg + args.map(a => a.toString?.() ?? JSON.stringify(a)).join(''),
34+
instance,
35+
trace
36+
.map(
37+
({ instance }) =>
38+
`at <${formatComponentName(instance, instance.component)}>`,
39+
)
40+
.join('\n'),
41+
trace,
42+
],
43+
)
44+
} else {
45+
const warnArgs = [`[Vue warn]: ${msg}`, ...args]
46+
/* istanbul ignore if */
47+
if (
48+
trace.length &&
49+
// avoid spamming console during tests
50+
!__TEST__
51+
) {
52+
warnArgs.push(`\n`, ...formatTrace(trace))
53+
}
54+
console.warn(...warnArgs)
55+
}
56+
57+
resetTracking()
58+
}
59+
60+
export function getComponentTrace(): ComponentTraceStack {
61+
let instance = currentInstance
62+
if (!instance) return []
63+
64+
// we can't just use the stack because it will be incomplete during updates
65+
// that did not start from the root. Re-construct the parent chain using
66+
// instance parent pointers.
67+
const stack: ComponentTraceStack = []
68+
69+
while (instance) {
70+
const last = stack[0]
71+
if (last && last.instance === instance) {
72+
last.recurseCount++
73+
} else {
74+
stack.push({
75+
instance,
76+
recurseCount: 0,
77+
})
78+
}
79+
instance = instance.parent
80+
}
81+
82+
return stack
83+
}
84+
85+
function formatTrace(trace: ComponentTraceStack): any[] {
86+
const logs: any[] = []
87+
trace.forEach((entry, i) => {
88+
logs.push(...(i === 0 ? [] : [`\n`]), ...formatTraceEntry(entry))
89+
})
90+
return logs
91+
}
92+
93+
function formatTraceEntry({ instance, recurseCount }: TraceEntry): any[] {
94+
const postfix =
95+
recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
96+
const isRoot = instance ? instance.parent == null : false
97+
const open = ` at <${formatComponentName(
98+
instance,
99+
instance.component,
100+
isRoot,
101+
)}`
102+
const close = `>` + postfix
103+
return instance.rawProps.length
104+
? [open, ...formatProps(instance.rawProps), close]
105+
: [open + close]
106+
}
107+
108+
function formatProps(rawProps: NormalizedRawProps): any[] {
109+
const fullProps: Record<string, any> = {}
110+
for (const props of rawProps) {
111+
if (isFunction(props)) {
112+
const propsObj = props()
113+
for (const key in propsObj) {
114+
fullProps[key] = propsObj[key]
115+
}
116+
} else {
117+
for (const key in props) {
118+
fullProps[key] = props[key]()
119+
}
120+
}
121+
}
122+
123+
const res: any[] = []
124+
Object.keys(fullProps)
125+
.slice(0, 3)
126+
.forEach(key => res.push(...formatProp(key, fullProps[key])))
127+
128+
if (fullProps.length > 3) {
129+
res.push(` ...`)
130+
}
131+
132+
return res
133+
}
134+
135+
function formatProp(key: string, value: unknown, raw?: boolean): any {
136+
if (isString(value)) {
137+
value = JSON.stringify(value)
138+
return raw ? value : [`${key}=${value}`]
139+
} else if (
140+
typeof value === 'number' ||
141+
typeof value === 'boolean' ||
142+
value == null
143+
) {
144+
return raw ? value : [`${key}=${value}`]
145+
} else if (isRef(value)) {
146+
value = formatProp(key, toRaw(value.value), true)
147+
return raw ? value : [`${key}=Ref<`, value, `>`]
148+
} else if (isFunction(value)) {
149+
return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]
150+
} else {
151+
value = toRaw(value)
152+
return raw ? value : [`${key}=`, value]
153+
}
154+
}
155+
156+
export function getComponentName(
157+
Component: Component,
158+
includeInferred = true,
159+
): string | false | undefined {
160+
return isFunction(Component)
161+
? Component.displayName || Component.name
162+
: Component.name || (includeInferred && Component.__name)
163+
}
164+
165+
export function formatComponentName(
166+
instance: ComponentInternalInstance | null,
167+
Component: Component,
168+
isRoot = false,
169+
): string {
170+
let name = getComponentName(Component)
171+
if (!name && Component.__file) {
172+
const match = Component.__file.match(/([^/\\]+)\.\w+$/)
173+
if (match) {
174+
name = match[1]
175+
}
176+
}
177+
178+
// TODO registry
179+
// if (!name && instance && instance.parent) {
180+
// // try to infer the name based on reverse resolution
181+
// const inferFromRegistry = (registry: Record<string, any> | undefined) => {
182+
// for (const key in registry) {
183+
// if (registry[key] === Component) {
184+
// return key
185+
// }
186+
// }
187+
// }
188+
// name =
189+
// inferFromRegistry(
190+
// instance.components ||
191+
// (instance.parent.type as ComponentOptions).components,
192+
// ) || inferFromRegistry(instance.appContext.components)
193+
// }
194+
195+
return name ? classify(name) : isRoot ? `App` : `Anonymous`
3196
}
197+
198+
const classifyRE = /(?:^|[-_])(\w)/g
199+
const classify = (str: string): string =>
200+
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')

playground/src/warning.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createComponent, warn } from 'vue/vapor'
2+
3+
export default {
4+
vapor: true,
5+
setup() {
6+
return createComponent(Comp, [
7+
{
8+
msg: () => 'hello',
9+
onClick: () => () => {},
10+
},
11+
() => ({ foo: 'world', msg: 'msg' }),
12+
])
13+
},
14+
}
15+
16+
const Comp = {
17+
name: 'Comp',
18+
vapor: true,
19+
props: ['msg', 'foo'],
20+
setup() {
21+
warn('hello')
22+
},
23+
}

0 commit comments

Comments
 (0)