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 cd2715b19..caa5c7213 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 dc548bf8a..380449fa8 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 cc72898f2..830852217 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 d63b2b25a..61710a5c8 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 f34031d71..bb1e8efdb 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 cce4db0ba..5c6a4959f 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 5e4c2c054..90d28015b 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 9ea672aa2..d2b78318e 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 24fb23a31..e7fe1814b 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 000000000..8ddf56c83 --- /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 42e7f0508..b128d74a7 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 05cca7707..03dda729c 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 d886f0743..be0a9758a 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 f24c01878..bfa590fb9 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 65284f535..023e3bd8b 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(`,