From 54727f9874abe8d0c99ee153d252269ae519b45d Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 20 Jul 2020 21:51:30 -0400 Subject: [PATCH] feat: provide ability to overwrite feature flags in esm-bundler builds e.g. by replacing `__VUE_OPTIONS_API__` to `false` using webpack's `DefinePlugin`, the final bundle will drop all code supporting the options API. This does not break existing usage, but requires the user to explicitly configure the feature flags via bundlers to properly tree-shake the disabled branches. As a result, users will see a console warning if the flags have not been properly configured. --- .eslintrc.js | 7 +++++ jest.config.js | 2 +- packages/global.d.ts | 3 +- packages/runtime-core/src/apiCreateApp.ts | 28 ++++++++--------- packages/runtime-core/src/component.ts | 8 +++-- packages/runtime-core/src/componentEmits.ts | 2 +- packages/runtime-core/src/componentProps.ts | 2 +- packages/runtime-core/src/componentProxy.ts | 4 +-- packages/runtime-core/src/devtools.ts | 26 ++++++++-------- packages/runtime-core/src/featureFlags.ts | 33 +++++++++++++++++++++ packages/runtime-core/src/renderer.ts | 18 +++++++++-- packages/runtime-dom/src/index.ts | 1 + packages/shared/src/index.ts | 17 +++++++++++ packages/vue/src/dev.ts | 10 +++---- rollup.config.js | 7 ++++- 15 files changed, 123 insertions(+), 45 deletions(-) create mode 100644 packages/runtime-core/src/featureFlags.ts 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(`,