Skip to content

Commit

Permalink
feat: provide ability to overwrite feature flags in esm-bundler builds
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yyx990803 committed Jul 21, 2020
1 parent dabdc5e commit 54727f9
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 45 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}/**'],
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion packages/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 13 additions & 15 deletions packages/runtime-core/src/apiCreateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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
Expand All @@ -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>
Expand All @@ -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
Expand All @@ -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: {},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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__) {
Expand Down Expand Up @@ -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__) {
Expand All @@ -247,8 +248,7 @@ export function createAppAPI<HostElement>(
unmount() {
if (isMounted) {
render(null, app._container)

__DEV__ && appUnmounted(app)
devtoolsUnmountApp(app)

This comment has been minimized.

Copy link
@Akryum

Akryum Aug 19, 2020

Member

Missing if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {? @yyx990803

} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
Expand All @@ -267,9 +267,7 @@ export function createAppAPI<HostElement>(

return app
}
}

context.__app = app
})

return app
}
Expand Down
8 changes: 5 additions & 3 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/componentEmits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/componentProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 14 additions & 12 deletions packages/runtime-core/src/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
)
Expand Down
33 changes: 33 additions & 0 deletions packages/runtime-core/src/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -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
)
}
}
18 changes: 15 additions & 3 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -2046,7 +2056,9 @@ function baseCreateRenderer(
}
}

__DEV__ && componentRemoved(instance)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentRemoved(instance)
}
}

const unmountChildren: UnmountChildrenFn = (
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
17 changes: 17 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
: {})
)
}
10 changes: 5 additions & 5 deletions packages/vue/src/dev.ts
Original file line number Diff line number Diff line change
@@ -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.`
)
Expand Down
Loading

0 comments on commit 54727f9

Please sign in to comment.