From: Evan You Date: Tue, 21 Jul 2020 01:51:30 +0000 (-0400) Subject: feat: provide ability to overwrite feature flags in esm-bundler builds X-Git-Tag: v3.0.0-rc.3~14 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=54727f9874abe8d0c99ee153d252269ae519b45d;p=thirdparty%2Fvuejs%2Fcore.git 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. --- diff --git a/.eslintrc.js b/.eslintrc.js index cd2715b19a..caa5c7213c 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 dc548bf8a5..380449fa8f 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 cc72898f2f..830852217e 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 d63b2b25a1..61710a5c89 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 f34031d713..bb1e8efdb4 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 cce4db0bad..5c6a4959f9 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 5e4c2c0541..90d28015b3 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 9ea672aa2b..d2b78318ef 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 24fb23a31c..e7fe1814bf 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 0000000000..8ddf56c83f --- /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 42e7f05087..b128d74a7f 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 05cca7707e..03dda729c5 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 d886f07434..be0a9758a1 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 f24c01878d..bfa590fb97 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 65284f5355..023e3bd8bb 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(`,