]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: provide ability to overwrite feature flags in esm-bundler builds
authorEvan You <yyx990803@gmail.com>
Tue, 21 Jul 2020 01:51:30 +0000 (21:51 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 21 Jul 2020 01:51:30 +0000 (21:51 -0400)
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.

15 files changed:
.eslintrc.js
jest.config.js
packages/global.d.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/devtools.ts
packages/runtime-core/src/featureFlags.ts [new file with mode: 0644]
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/index.ts
packages/shared/src/index.ts
packages/vue/src/dev.ts
rollup.config.js

index cd2715b19af4498f56a22d0e68b9e9a3a5466fbe..caa5c7213ca95ba6f64651e48da1f64d43463988 100644 (file)
@@ -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}/**'],
index dc548bf8a504923de94086b5d092e973cdc9b923..380449fa8ffc858ed59851259c924e82ae49cdaa 100644 (file)
@@ -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',
index cc72898f2f472c2ae20c71d9664f6f974e4c8eda..830852217e6762899513641e6dc252fb3b39b748 100644 (file)
@@ -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
index d63b2b25a10f925ff2a25b4414a95b0f3b586c5e..61710a5c89598afba80647976ce5b94d619a08ef 100644 (file)
@@ -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<HostElement = any> {
@@ -32,7 +32,7 @@ export interface App<HostElement = any> {
   unmount(rootContainer: HostElement | string): void
   provide<T>(key: InjectionKey<T> | 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<string, OptionMergeFunction>
   globalProperties: Record<string, any>
@@ -68,15 +67,13 @@ export interface AppConfig {
 }
 
 export interface AppContext {
+  app: App // for devtools
   config: AppConfig
   mixins: ComponentOptions[]
   components: Record<string, PublicAPIComponent>
   directives: Record<string, Directive>
   provides: Record<string | symbol, any>
   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<HostElement>(
 
     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<HostElement>(
       },
 
       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<HostElement>(
           }
           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<HostElement>(
       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<HostElement>(
 
         return app
       }
-    }
-
-    context.__app = app
+    })
 
     return app
   }
index f34031d713bfcea4d228b0023ebac641eeebabdf..bb1e8efdb4adc4adb8c48980b9dc5d4f1d1e127b 100644 (file)
@@ -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<string, unknown>
 
@@ -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
index cce4db0badd908bc841504be6ee93f1d9e185b9b..5c6a4959f9f67a049013bccb0883fd2cb49a8729 100644 (file)
@@ -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))
index 5e4c2c054113d5b5b7bc2e964dc9413707117bab..90d28015b3a6931130b5fbb9eb28e8b8b5d3a37a 100644 (file)
@@ -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)
index 9ea672aa2b5725d5a7d1356ab9a3890cb6053ad8..d2b78318ef7b39b7994da4be2c924613254c88cd 100644 (file)
@@ -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 {
index 24fb23a31c9234689d6bf4300be233ce7fcbb42d..e7fe1814bf83b0819e7321097c3ef97ea87b2c1f 100644 (file)
@@ -9,7 +9,7 @@ export interface AppRecord {
   types: Record<string, string | Symbol>
 }
 
-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 (file)
index 0000000..8ddf56c
--- /dev/null
@@ -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
+    )
+  }
+}
index 42e7f050871b363068b5502b45da84bb7c3bb443..b128d74a7f57ec27d9b658821707a79fba0a0884 100644 (file)
@@ -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<HostElement = RendererElement> {
   render: RootRenderFunction<HostElement>
@@ -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 = (
index 05cca7707e65e780daec8cc9ded040e9b2d46665..03dda729c50e52bf836969876c4f8b29eb04af87 100644 (file)
@@ -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
   }
 
index d886f0743472ff983467c8ba9bb7c8a6589e26fb..be0a9758a13ec5c1bd49ee6e335624868d94bb19 100644 (file)
@@ -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
+              : {})
+  )
+}
index f24c01878da970a4346ba09d1e2146fb7b1d3323..bfa590fb9745d9d7e88d0d4cf0fb10171f785a41 100644 (file)
@@ -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.`
     )
index 65284f535583af486e2f79d3276c8ded803cc558..023e3bd8bb465074f73b7d4a4418884aac5f8857 100644 (file)
@@ -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(`,