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.
This commit is contained in:
parent
dabdc5e115
commit
54727f9874
@ -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}/**'],
|
||||
|
@ -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',
|
||||
|
3
packages/global.d.ts
vendored
3
packages/global.d.ts
vendored
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
)
|
||||
|
33
packages/runtime-core/src/featureFlags.ts
Normal file
33
packages/runtime-core/src/featureFlags.ts
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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 = (
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
: {})
|
||||
)
|
||||
}
|
||||
|
@ -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.`
|
||||
)
|
||||
|
@ -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(`,
|
||||
|
Loading…
Reference in New Issue
Block a user