diff --git a/.eslintrc.js b/.eslintrc.js index cd2715b19af..caa5c7213ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,6 +32,13 @@ module.exports = { 'no-restricted-syntax': 'off' } }, + // shared, may be used in any env + { + files: ['packages/shared/**'], + rules: { + 'no-restricted-globals': 'off' + } + }, // Packages targeting DOM { files: ['packages/{vue,runtime-dom}/**'], diff --git a/jest.config.js b/jest.config.js index dc548bf8a50..380449fa8ff 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,7 +9,7 @@ module.exports = { __ESM_BUNDLER__: true, __ESM_BROWSER__: false, __NODE_JS__: true, - __FEATURE_OPTIONS__: true, + __FEATURE_OPTIONS_API__: true, __FEATURE_SUSPENSE__: true }, coverageDirectory: 'coverage', diff --git a/packages/global.d.ts b/packages/global.d.ts index cc72898f2f4..830852217e6 100644 --- a/packages/global.d.ts +++ b/packages/global.d.ts @@ -10,5 +10,6 @@ declare var __COMMIT__: string declare var __VERSION__: string // Feature flags -declare var __FEATURE_OPTIONS__: boolean +declare var __FEATURE_OPTIONS_API__: boolean +declare var __FEATURE_PROD_DEVTOOLS__: boolean declare var __FEATURE_SUSPENSE__: boolean diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index d63b2b25a10..61710a5c895 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -13,7 +13,7 @@ import { isFunction, NO, isObject } from '@vue/shared' import { warn } from './warning' import { createVNode, cloneVNode, VNode } from './vnode' import { RootHydrateFunction } from './hydration' -import { initApp, appUnmounted } from './devtools' +import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { version } from '.' export interface App { @@ -32,7 +32,7 @@ export interface App { unmount(rootContainer: HostElement | string): void provide(key: InjectionKey | string, value: T): this - // internal. We need to expose these for the server-renderer and devtools + // internal, but we need to expose these for the server-renderer and devtools _component: Component _props: Data | null _container: HostElement | null @@ -50,7 +50,6 @@ export interface AppConfig { // @private readonly isNativeTag?: (tag: string) => boolean - devtools: boolean performance: boolean optionMergeStrategies: Record globalProperties: Record @@ -68,15 +67,13 @@ export interface AppConfig { } export interface AppContext { + app: App // for devtools config: AppConfig mixins: ComponentOptions[] components: Record directives: Record provides: Record reload?: () => void // HMR only - - // internal for devtools - __app?: App } type PluginInstallFunction = (app: App, ...options: any[]) => any @@ -89,9 +86,9 @@ export type Plugin = export function createAppContext(): AppContext { return { + app: null as any, config: { isNativeTag: NO, - devtools: true, performance: false, globalProperties: {}, optionMergeStrategies: {}, @@ -126,7 +123,7 @@ export function createAppAPI( let isMounted = false - const app: App = { + const app: App = (context.app = { _component: rootComponent as Component, _props: rootProps, _container: null, @@ -165,7 +162,7 @@ export function createAppAPI( }, mixin(mixin: ComponentOptions) { - if (__FEATURE_OPTIONS__) { + if (__FEATURE_OPTIONS_API__) { if (!context.mixins.includes(mixin)) { context.mixins.push(mixin) } else if (__DEV__) { @@ -230,8 +227,12 @@ export function createAppAPI( } isMounted = true app._container = rootContainer + // for devtools and telemetry + ;(rootContainer as any).__vue_app__ = app - __DEV__ && initApp(app, version) + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsInitApp(app, version) + } return vnode.component!.proxy } else if (__DEV__) { @@ -247,8 +248,7 @@ export function createAppAPI( unmount() { if (isMounted) { render(null, app._container) - - __DEV__ && appUnmounted(app) + devtoolsUnmountApp(app) } else if (__DEV__) { warn(`Cannot unmount an app that is not mounted.`) } @@ -267,9 +267,7 @@ export function createAppAPI( return app } - } - - context.__app = app + }) return app } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index f34031d713b..bb1e8efdb4a 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -49,7 +49,7 @@ import { markAttrsAccessed } from './componentRenderUtils' import { startMeasure, endMeasure } from './profiling' -import { componentAdded } from './devtools' +import { devtoolsComponentAdded } from './devtools' export type Data = Record @@ -423,7 +423,9 @@ export function createComponentInstance( instance.root = parent ? parent.root : instance instance.emit = emit.bind(null, instance) - __DEV__ && componentAdded(instance) + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsComponentAdded(instance) + } return instance } @@ -647,7 +649,7 @@ function finishComponentSetup( } // support for 2.x options - if (__FEATURE_OPTIONS__) { + if (__FEATURE_OPTIONS_API__) { currentInstance = instance applyOptions(instance, Component) currentInstance = null diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts index cce4db0badd..5c6a4959f9f 100644 --- a/packages/runtime-core/src/componentEmits.ts +++ b/packages/runtime-core/src/componentEmits.ts @@ -105,7 +105,7 @@ function normalizeEmitsOptions( // apply mixin/extends props let hasExtends = false - if (__FEATURE_OPTIONS__ && !isFunction(comp)) { + if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) { if (comp.extends) { hasExtends = true extend(normalized, normalizeEmitsOptions(comp.extends)) diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 5e4c2c05411..90d28015b3a 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -322,7 +322,7 @@ export function normalizePropsOptions( // apply mixin/extends props let hasExtends = false - if (__FEATURE_OPTIONS__ && !isFunction(comp)) { + if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) { const extendProps = (raw: ComponentOptions) => { const [props, keys] = normalizePropsOptions(raw) extend(normalized, props) diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 9ea672aa2b5..d2b78318ef7 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -179,10 +179,10 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), { $parent: i => i.parent && i.parent.proxy, $root: i => i.root && i.root.proxy, $emit: i => i.emit, - $options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type), + $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), $forceUpdate: i => () => queueJob(i.update), $nextTick: () => nextTick, - $watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP + $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) } as PublicPropertiesMap) const enum AccessTypes { diff --git a/packages/runtime-core/src/devtools.ts b/packages/runtime-core/src/devtools.ts index 24fb23a31c9..e7fe1814bf8 100644 --- a/packages/runtime-core/src/devtools.ts +++ b/packages/runtime-core/src/devtools.ts @@ -9,7 +9,7 @@ export interface AppRecord { types: Record } -enum DevtoolsHooks { +const enum DevtoolsHooks { APP_INIT = 'app:init', APP_UNMOUNT = 'app:unmount', COMPONENT_UPDATED = 'component:updated', @@ -31,38 +31,40 @@ export function setDevtoolsHook(hook: DevtoolsHook) { devtools = hook } -export function initApp(app: App, version: string) { +export function devtoolsInitApp(app: App, version: string) { // TODO queue if devtools is undefined if (!devtools) return devtools.emit(DevtoolsHooks.APP_INIT, app, version, { - Fragment: Fragment, - Text: Text, - Comment: Comment, - Static: Static + Fragment, + Text, + Comment, + Static }) } -export function appUnmounted(app: App) { +export function devtoolsUnmountApp(app: App) { if (!devtools) return devtools.emit(DevtoolsHooks.APP_UNMOUNT, app) } -export const componentAdded = createDevtoolsHook(DevtoolsHooks.COMPONENT_ADDED) +export const devtoolsComponentAdded = /*#__PURE__*/ createDevtoolsHook( + DevtoolsHooks.COMPONENT_ADDED +) -export const componentUpdated = createDevtoolsHook( +export const devtoolsComponentUpdated = /*#__PURE__*/ createDevtoolsHook( DevtoolsHooks.COMPONENT_UPDATED ) -export const componentRemoved = createDevtoolsHook( +export const devtoolsComponentRemoved = /*#__PURE__*/ createDevtoolsHook( DevtoolsHooks.COMPONENT_REMOVED ) function createDevtoolsHook(hook: DevtoolsHooks) { return (component: ComponentInternalInstance) => { - if (!devtools || !component.appContext.__app) return + if (!devtools) return devtools.emit( hook, - component.appContext.__app, + component.appContext.app, component.uid, component.parent ? component.parent.uid : undefined ) diff --git a/packages/runtime-core/src/featureFlags.ts b/packages/runtime-core/src/featureFlags.ts new file mode 100644 index 00000000000..8ddf56c83f9 --- /dev/null +++ b/packages/runtime-core/src/featureFlags.ts @@ -0,0 +1,33 @@ +import { getGlobalThis } from '@vue/shared' + +/** + * This is only called in esm-bundler builds. + * It is called when a renderer is created, in `baseCreateRenderer` so that + * importing runtime-core is side-effects free. + * + * istanbul-ignore-next + */ +export function initFeatureFlags() { + let needWarn = false + + if (typeof __FEATURE_OPTIONS_API__ !== 'boolean') { + needWarn = true + getGlobalThis().__VUE_OPTIONS_API__ = true + } + + if (typeof __FEATURE_PROD_DEVTOOLS__ !== 'boolean') { + needWarn = true + getGlobalThis().__VUE_PROD_DEVTOOLS__ = false + } + + if (__DEV__ && needWarn) { + console.warn( + `You are running the esm-bundler build of Vue. It is recommended to ` + + `configure your bundler to explicitly replace the following global ` + + `variables with boolean literals so that it can remove unnecessary code:\n\n` + + `- __VUE_OPTIONS_API__ (support for Options API, default: true)\n` + + `- __VUE_PROD_DEVTOOLS__ (enable devtools inspection in production, default: false)` + // TODO link to docs + ) + } +} diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 42e7f050871..b128d74a7f5 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -64,7 +64,8 @@ import { createHydrationFunctions, RootHydrateFunction } from './hydration' import { invokeDirectiveHook } from './directives' import { startMeasure, endMeasure } from './profiling' import { ComponentPublicInstance } from './componentProxy' -import { componentRemoved, componentUpdated } from './devtools' +import { devtoolsComponentRemoved, devtoolsComponentUpdated } from './devtools' +import { initFeatureFlags } from './featureFlags' export interface Renderer { render: RootRenderFunction @@ -383,6 +384,11 @@ function baseCreateRenderer( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ): any { + // compile-time feature flags check + if (__ESM_BUNDLER__ && !__TEST__) { + initFeatureFlags() + } + const { insert: hostInsert, remove: hostRemove, @@ -1393,9 +1399,13 @@ function baseCreateRenderer( invokeVNodeHook(vnodeHook!, parent, next!, vnode) }, parentSuspense) } + + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsComponentUpdated(instance) + } + if (__DEV__) { popWarningContext() - componentUpdated(instance) } } }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) @@ -2046,7 +2056,9 @@ function baseCreateRenderer( } } - __DEV__ && componentRemoved(instance) + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsComponentRemoved(instance) + } } const unmountChildren: UnmountChildrenFn = ( diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 05cca7707e6..03dda729c50 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -69,6 +69,7 @@ export const createApp = ((...args) => { container.innerHTML = '' const proxy = mount(container) container.removeAttribute('v-cloak') + container.setAttribute('data-vue-app', '') return proxy } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d886f074347..be0a9758a13 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -146,3 +146,20 @@ export const toNumber = (val: any): any => { const n = parseFloat(val) return isNaN(n) ? val : n } + +let _globalThis: any +export const getGlobalThis = (): any => { + return ( + _globalThis || + (_globalThis = + typeof globalThis !== 'undefined' + ? globalThis + : typeof self !== 'undefined' + ? self + : typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : {}) + ) +} diff --git a/packages/vue/src/dev.ts b/packages/vue/src/dev.ts index f24c01878da..bfa590fb974 100644 --- a/packages/vue/src/dev.ts +++ b/packages/vue/src/dev.ts @@ -1,14 +1,14 @@ -import { version, setDevtoolsHook } from '@vue/runtime-dom' +import { setDevtoolsHook } from '@vue/runtime-dom' +import { getGlobalThis } from '@vue/shared' export function initDev() { - const target: any = __BROWSER__ ? window : global + const target = getGlobalThis() - target.__VUE__ = version + target.__VUE__ = true setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__) if (__BROWSER__) { - // @ts-ignore `console.info` cannot be null error - console[console.info ? 'info' : 'log']( + console.info( `You are running a development build of Vue.\n` + `Make sure to use the production build (*.prod.js) when deploying for production.` ) diff --git a/rollup.config.js b/rollup.config.js index 65284f53558..023e3bd8bb4 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -212,8 +212,13 @@ function createReplacePlugin( __ESM_BROWSER__: isBrowserESMBuild, // is targeting Node (SSR)? __NODE_JS__: isNodeBuild, - __FEATURE_OPTIONS__: true, + + // feature flags __FEATURE_SUSPENSE__: true, + __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true, + __FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild + ? `__VUE_PROD_DEVTOOLS__` + : false, ...(isProduction && isBrowserBuild ? { 'context.onError(': `/*#__PURE__*/ context.onError(`,