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 cd2715b1..caa5c721 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 dc548bf8..380449fa 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 cc72898f..83085221 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 d63b2b25..61710a5c 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 f34031d7..bb1e8efd 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 cce4db0b..5c6a4959 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 5e4c2c05..90d28015 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 9ea672aa..d2b78318 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 24fb23a3..e7fe1814 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 00000000..8ddf56c8 --- /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 42e7f050..b128d74a 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 05cca770..03dda729 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 d886f074..be0a9758 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 f24c0187..bfa590fb 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 65284f53..023e3bd8 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(`,