diff --git a/packages/runtime-core/__tests__/apiApp.spec.ts b/packages/runtime-core/__tests__/apiApp.spec.ts index 64899eba1..a559f5ed4 100644 --- a/packages/runtime-core/__tests__/apiApp.spec.ts +++ b/packages/runtime-core/__tests__/apiApp.spec.ts @@ -286,4 +286,85 @@ describe('api: createApp', () => { app.mount(Root, nodeOps.createElement('div')) expect(handler).toHaveBeenCalledTimes(1) }) + + describe('config.isNativeTag', () => { + const isNativeTag = jest.fn(tag => tag === 'div') + + test('Component.name', () => { + const app = createApp() + Object.defineProperty(app.config, 'isNativeTag', { + value: isNativeTag, + writable: false + }) + + const Root = { + name: 'div', + setup() { + return { + count: ref(0) + } + }, + render() { + return null + } + } + + app.mount(Root, nodeOps.createElement('div')) + expect( + `Do not use built-in or reserved HTML elements as component id: div` + ).toHaveBeenWarned() + }) + + test('Component.components', () => { + const app = createApp() + Object.defineProperty(app.config, 'isNativeTag', { + value: isNativeTag, + writable: false + }) + + const Root = { + components: { + div: () => 'div' + }, + setup() { + return { + count: ref(0) + } + }, + render() { + return null + } + } + + app.mount(Root, nodeOps.createElement('div')) + expect( + `Do not use built-in or reserved HTML elements as component id: div` + ).toHaveBeenWarned() + }) + + test('register using app.component', () => { + const app = createApp() + Object.defineProperty(app.config, 'isNativeTag', { + value: isNativeTag, + writable: false + }) + + const Root = { + setup() { + return { + count: ref(0) + } + }, + render() { + return null + } + } + + app.component('div', () => 'div') + app.mount(Root, nodeOps.createElement('div')) + expect( + `Do not use built-in or reserved HTML elements as component id: div` + ).toHaveBeenWarned() + }) + }) }) diff --git a/packages/runtime-core/src/apiApp.ts b/packages/runtime-core/src/apiApp.ts index b80c502cf..d7f393bc8 100644 --- a/packages/runtime-core/src/apiApp.ts +++ b/packages/runtime-core/src/apiApp.ts @@ -1,10 +1,10 @@ -import { Component, Data } from './component' +import { Component, Data, validateComponentName } from './component' import { ComponentOptions } from './apiOptions' import { ComponentPublicInstance } from './componentProxy' import { Directive } from './directives' import { RootRenderFunction } from './createRenderer' import { InjectionKey } from './apiInject' -import { isFunction } from '@vue/shared' +import { isFunction, NO } from '@vue/shared' import { warn } from './warning' import { createVNode } from './vnode' @@ -27,6 +27,7 @@ export interface App { export interface AppConfig { devtools: boolean performance: boolean + readonly isNativeTag?: (tag: string) => boolean errorHandler?: ( err: Error, instance: ComponentPublicInstance | null, @@ -60,6 +61,7 @@ export function createAppContext(): AppContext { config: { devtools: true, performance: false, + isNativeTag: NO, errorHandler: undefined, warnHandler: undefined }, @@ -111,6 +113,9 @@ export function createAppAPI( }, component(name: string, component?: Component): any { + if (__DEV__) { + validateComponentName(name, context.config) + } if (!component) { return context.components[name] } else { diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index d9e1e9b96..72c3fdac3 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -12,7 +12,7 @@ import { callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' -import { AppContext, createAppContext } from './apiApp' +import { AppContext, createAppContext, AppConfig } from './apiApp' import { Directive } from './directives' import { applyOptions, ComponentOptions } from './apiOptions' import { @@ -21,7 +21,8 @@ import { capitalize, NOOP, isArray, - isObject + isObject, + NO } from '@vue/shared' import { SuspenseBoundary } from './suspense' import { @@ -223,11 +224,37 @@ export const setCurrentInstance = ( currentInstance = instance } +const BuiltInTagSet = new Set(['slot', 'component']) +const isBuiltInTag = (tag: string) => BuiltInTagSet.has(tag) + +export function validateComponentName(name: string, config: AppConfig) { + const appIsNativeTag = config.isNativeTag || NO + if (isBuiltInTag(name) || appIsNativeTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component id: ' + name + ) + } +} + export function setupStatefulComponent( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null ) { const Component = instance.type as ComponentOptions + + if (__DEV__) { + if (Component.name) { + validateComponentName(Component.name, instance.appContext.config) + } + if (Component.components) { + const names = Object.keys(Component.components) + for (let i = 0; i < names.length; i++) { + const name = names[i] + validateComponentName(name, instance.appContext.config) + } + } + } + // 1. create render proxy instance.renderProxy = new Proxy(instance, PublicInstanceProxyHandlers) // 2. create props proxy diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index ac293a0a3..35167655e 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -1,4 +1,5 @@ import { createRenderer } from '@vue/runtime-core' +import { isHTMLTag, isSVGTag } from '@vue/shared' import { nodeOps } from './nodeOps' import { patchProp } from './patchProp' @@ -7,7 +8,19 @@ const { render, createApp } = createRenderer({ ...nodeOps }) -export { render, createApp } +const wrappedCreateApp = () => { + const app = createApp() + // inject `isNativeTag` dev only + Object.defineProperty(app.config, 'isNativeTag', { + value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag), + writable: false + }) + return app +} + +const exportedCreateApp = __DEV__ ? wrappedCreateApp : createApp + +export { render, exportedCreateApp as createApp } // DOM-only runtime helpers export {