Skip to content

Commit 54727f9

Browse files
committed
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.
1 parent dabdc5e commit 54727f9

15 files changed

+123
-45
lines changed

.eslintrc.js

+7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ module.exports = {
3232
'no-restricted-syntax': 'off'
3333
}
3434
},
35+
// shared, may be used in any env
36+
{
37+
files: ['packages/shared/**'],
38+
rules: {
39+
'no-restricted-globals': 'off'
40+
}
41+
},
3542
// Packages targeting DOM
3643
{
3744
files: ['packages/{vue,runtime-dom}/**'],

jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module.exports = {
99
__ESM_BUNDLER__: true,
1010
__ESM_BROWSER__: false,
1111
__NODE_JS__: true,
12-
__FEATURE_OPTIONS__: true,
12+
__FEATURE_OPTIONS_API__: true,
1313
__FEATURE_SUSPENSE__: true
1414
},
1515
coverageDirectory: 'coverage',

packages/global.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ declare var __COMMIT__: string
1010
declare var __VERSION__: string
1111

1212
// Feature flags
13-
declare var __FEATURE_OPTIONS__: boolean
13+
declare var __FEATURE_OPTIONS_API__: boolean
14+
declare var __FEATURE_PROD_DEVTOOLS__: boolean
1415
declare var __FEATURE_SUSPENSE__: boolean

packages/runtime-core/src/apiCreateApp.ts

+13-15
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { isFunction, NO, isObject } from '@vue/shared'
1313
import { warn } from './warning'
1414
import { createVNode, cloneVNode, VNode } from './vnode'
1515
import { RootHydrateFunction } from './hydration'
16-
import { initApp, appUnmounted } from './devtools'
16+
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
1717
import { version } from '.'
1818

1919
export interface App<HostElement = any> {
@@ -32,7 +32,7 @@ export interface App<HostElement = any> {
3232
unmount(rootContainer: HostElement | string): void
3333
provide<T>(key: InjectionKey<T> | string, value: T): this
3434

35-
// internal. We need to expose these for the server-renderer and devtools
35+
// internal, but we need to expose these for the server-renderer and devtools
3636
_component: Component
3737
_props: Data | null
3838
_container: HostElement | null
@@ -50,7 +50,6 @@ export interface AppConfig {
5050
// @private
5151
readonly isNativeTag?: (tag: string) => boolean
5252

53-
devtools: boolean
5453
performance: boolean
5554
optionMergeStrategies: Record<string, OptionMergeFunction>
5655
globalProperties: Record<string, any>
@@ -68,15 +67,13 @@ export interface AppConfig {
6867
}
6968

7069
export interface AppContext {
70+
app: App // for devtools
7171
config: AppConfig
7272
mixins: ComponentOptions[]
7373
components: Record<string, PublicAPIComponent>
7474
directives: Record<string, Directive>
7575
provides: Record<string | symbol, any>
7676
reload?: () => void // HMR only
77-
78-
// internal for devtools
79-
__app?: App
8077
}
8178

8279
type PluginInstallFunction = (app: App, ...options: any[]) => any
@@ -89,9 +86,9 @@ export type Plugin =
8986

9087
export function createAppContext(): AppContext {
9188
return {
89+
app: null as any,
9290
config: {
9391
isNativeTag: NO,
94-
devtools: true,
9592
performance: false,
9693
globalProperties: {},
9794
optionMergeStrategies: {},
@@ -126,7 +123,7 @@ export function createAppAPI<HostElement>(
126123

127124
let isMounted = false
128125

129-
const app: App = {
126+
const app: App = (context.app = {
130127
_component: rootComponent as Component,
131128
_props: rootProps,
132129
_container: null,
@@ -165,7 +162,7 @@ export function createAppAPI<HostElement>(
165162
},
166163

167164
mixin(mixin: ComponentOptions) {
168-
if (__FEATURE_OPTIONS__) {
165+
if (__FEATURE_OPTIONS_API__) {
169166
if (!context.mixins.includes(mixin)) {
170167
context.mixins.push(mixin)
171168
} else if (__DEV__) {
@@ -230,8 +227,12 @@ export function createAppAPI<HostElement>(
230227
}
231228
isMounted = true
232229
app._container = rootContainer
230+
// for devtools and telemetry
231+
;(rootContainer as any).__vue_app__ = app
233232

234-
__DEV__ && initApp(app, version)
233+
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
234+
devtoolsInitApp(app, version)
235+
}
235236

236237
return vnode.component!.proxy
237238
} else if (__DEV__) {
@@ -247,8 +248,7 @@ export function createAppAPI<HostElement>(
247248
unmount() {
248249
if (isMounted) {
249250
render(null, app._container)
250-
251-
__DEV__ && appUnmounted(app)
251+
devtoolsUnmountApp(app)
252252
} else if (__DEV__) {
253253
warn(`Cannot unmount an app that is not mounted.`)
254254
}
@@ -267,9 +267,7 @@ export function createAppAPI<HostElement>(
267267

268268
return app
269269
}
270-
}
271-
272-
context.__app = app
270+
})
273271

274272
return app
275273
}

packages/runtime-core/src/component.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
markAttrsAccessed
5050
} from './componentRenderUtils'
5151
import { startMeasure, endMeasure } from './profiling'
52-
import { componentAdded } from './devtools'
52+
import { devtoolsComponentAdded } from './devtools'
5353

5454
export type Data = Record<string, unknown>
5555

@@ -423,7 +423,9 @@ export function createComponentInstance(
423423
instance.root = parent ? parent.root : instance
424424
instance.emit = emit.bind(null, instance)
425425

426-
__DEV__ && componentAdded(instance)
426+
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
427+
devtoolsComponentAdded(instance)
428+
}
427429

428430
return instance
429431
}
@@ -647,7 +649,7 @@ function finishComponentSetup(
647649
}
648650

649651
// support for 2.x options
650-
if (__FEATURE_OPTIONS__) {
652+
if (__FEATURE_OPTIONS_API__) {
651653
currentInstance = instance
652654
applyOptions(instance, Component)
653655
currentInstance = null

packages/runtime-core/src/componentEmits.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function normalizeEmitsOptions(
105105

106106
// apply mixin/extends props
107107
let hasExtends = false
108-
if (__FEATURE_OPTIONS__ && !isFunction(comp)) {
108+
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
109109
if (comp.extends) {
110110
hasExtends = true
111111
extend(normalized, normalizeEmitsOptions(comp.extends))

packages/runtime-core/src/componentProps.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export function normalizePropsOptions(
322322

323323
// apply mixin/extends props
324324
let hasExtends = false
325-
if (__FEATURE_OPTIONS__ && !isFunction(comp)) {
325+
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
326326
const extendProps = (raw: ComponentOptions) => {
327327
const [props, keys] = normalizePropsOptions(raw)
328328
extend(normalized, props)

packages/runtime-core/src/componentProxy.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,10 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {
179179
$parent: i => i.parent && i.parent.proxy,
180180
$root: i => i.root && i.root.proxy,
181181
$emit: i => i.emit,
182-
$options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type),
182+
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
183183
$forceUpdate: i => () => queueJob(i.update),
184184
$nextTick: () => nextTick,
185-
$watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP
185+
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
186186
} as PublicPropertiesMap)
187187

188188
const enum AccessTypes {

packages/runtime-core/src/devtools.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface AppRecord {
99
types: Record<string, string | Symbol>
1010
}
1111

12-
enum DevtoolsHooks {
12+
const enum DevtoolsHooks {
1313
APP_INIT = 'app:init',
1414
APP_UNMOUNT = 'app:unmount',
1515
COMPONENT_UPDATED = 'component:updated',
@@ -31,38 +31,40 @@ export function setDevtoolsHook(hook: DevtoolsHook) {
3131
devtools = hook
3232
}
3333

34-
export function initApp(app: App, version: string) {
34+
export function devtoolsInitApp(app: App, version: string) {
3535
// TODO queue if devtools is undefined
3636
if (!devtools) return
3737
devtools.emit(DevtoolsHooks.APP_INIT, app, version, {
38-
Fragment: Fragment,
39-
Text: Text,
40-
Comment: Comment,
41-
Static: Static
38+
Fragment,
39+
Text,
40+
Comment,
41+
Static
4242
})
4343
}
4444

45-
export function appUnmounted(app: App) {
45+
export function devtoolsUnmountApp(app: App) {
4646
if (!devtools) return
4747
devtools.emit(DevtoolsHooks.APP_UNMOUNT, app)
4848
}
4949

50-
export const componentAdded = createDevtoolsHook(DevtoolsHooks.COMPONENT_ADDED)
50+
export const devtoolsComponentAdded = /*#__PURE__*/ createDevtoolsHook(
51+
DevtoolsHooks.COMPONENT_ADDED
52+
)
5153

52-
export const componentUpdated = createDevtoolsHook(
54+
export const devtoolsComponentUpdated = /*#__PURE__*/ createDevtoolsHook(
5355
DevtoolsHooks.COMPONENT_UPDATED
5456
)
5557

56-
export const componentRemoved = createDevtoolsHook(
58+
export const devtoolsComponentRemoved = /*#__PURE__*/ createDevtoolsHook(
5759
DevtoolsHooks.COMPONENT_REMOVED
5860
)
5961

6062
function createDevtoolsHook(hook: DevtoolsHooks) {
6163
return (component: ComponentInternalInstance) => {
62-
if (!devtools || !component.appContext.__app) return
64+
if (!devtools) return
6365
devtools.emit(
6466
hook,
65-
component.appContext.__app,
67+
component.appContext.app,
6668
component.uid,
6769
component.parent ? component.parent.uid : undefined
6870
)
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { getGlobalThis } from '@vue/shared'
2+
3+
/**
4+
* This is only called in esm-bundler builds.
5+
* It is called when a renderer is created, in `baseCreateRenderer` so that
6+
* importing runtime-core is side-effects free.
7+
*
8+
* istanbul-ignore-next
9+
*/
10+
export function initFeatureFlags() {
11+
let needWarn = false
12+
13+
if (typeof __FEATURE_OPTIONS_API__ !== 'boolean') {
14+
needWarn = true
15+
getGlobalThis().__VUE_OPTIONS_API__ = true
16+
}
17+
18+
if (typeof __FEATURE_PROD_DEVTOOLS__ !== 'boolean') {
19+
needWarn = true
20+
getGlobalThis().__VUE_PROD_DEVTOOLS__ = false
21+
}
22+
23+
if (__DEV__ && needWarn) {
24+
console.warn(
25+
`You are running the esm-bundler build of Vue. It is recommended to ` +
26+
`configure your bundler to explicitly replace the following global ` +
27+
`variables with boolean literals so that it can remove unnecessary code:\n\n` +
28+
`- __VUE_OPTIONS_API__ (support for Options API, default: true)\n` +
29+
`- __VUE_PROD_DEVTOOLS__ (enable devtools inspection in production, default: false)`
30+
// TODO link to docs
31+
)
32+
}
33+
}

packages/runtime-core/src/renderer.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ import { createHydrationFunctions, RootHydrateFunction } from './hydration'
6464
import { invokeDirectiveHook } from './directives'
6565
import { startMeasure, endMeasure } from './profiling'
6666
import { ComponentPublicInstance } from './componentProxy'
67-
import { componentRemoved, componentUpdated } from './devtools'
67+
import { devtoolsComponentRemoved, devtoolsComponentUpdated } from './devtools'
68+
import { initFeatureFlags } from './featureFlags'
6869

6970
export interface Renderer<HostElement = RendererElement> {
7071
render: RootRenderFunction<HostElement>
@@ -383,6 +384,11 @@ function baseCreateRenderer(
383384
options: RendererOptions,
384385
createHydrationFns?: typeof createHydrationFunctions
385386
): any {
387+
// compile-time feature flags check
388+
if (__ESM_BUNDLER__ && !__TEST__) {
389+
initFeatureFlags()
390+
}
391+
386392
const {
387393
insert: hostInsert,
388394
remove: hostRemove,
@@ -1393,9 +1399,13 @@ function baseCreateRenderer(
13931399
invokeVNodeHook(vnodeHook!, parent, next!, vnode)
13941400
}, parentSuspense)
13951401
}
1402+
1403+
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
1404+
devtoolsComponentUpdated(instance)
1405+
}
1406+
13961407
if (__DEV__) {
13971408
popWarningContext()
1398-
componentUpdated(instance)
13991409
}
14001410
}
14011411
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
@@ -2046,7 +2056,9 @@ function baseCreateRenderer(
20462056
}
20472057
}
20482058

2049-
__DEV__ && componentRemoved(instance)
2059+
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
2060+
devtoolsComponentRemoved(instance)
2061+
}
20502062
}
20512063

20522064
const unmountChildren: UnmountChildrenFn = (

packages/runtime-dom/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const createApp = ((...args) => {
6969
container.innerHTML = ''
7070
const proxy = mount(container)
7171
container.removeAttribute('v-cloak')
72+
container.setAttribute('data-vue-app', '')
7273
return proxy
7374
}
7475

packages/shared/src/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,20 @@ export const toNumber = (val: any): any => {
146146
const n = parseFloat(val)
147147
return isNaN(n) ? val : n
148148
}
149+
150+
let _globalThis: any
151+
export const getGlobalThis = (): any => {
152+
return (
153+
_globalThis ||
154+
(_globalThis =
155+
typeof globalThis !== 'undefined'
156+
? globalThis
157+
: typeof self !== 'undefined'
158+
? self
159+
: typeof window !== 'undefined'
160+
? window
161+
: typeof global !== 'undefined'
162+
? global
163+
: {})
164+
)
165+
}

packages/vue/src/dev.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { version, setDevtoolsHook } from '@vue/runtime-dom'
1+
import { setDevtoolsHook } from '@vue/runtime-dom'
2+
import { getGlobalThis } from '@vue/shared'
23

34
export function initDev() {
4-
const target: any = __BROWSER__ ? window : global
5+
const target = getGlobalThis()
56

6-
target.__VUE__ = version
7+
target.__VUE__ = true
78
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__)
89

910
if (__BROWSER__) {
10-
// @ts-ignore `console.info` cannot be null error
11-
console[console.info ? 'info' : 'log'](
11+
console.info(
1212
`You are running a development build of Vue.\n` +
1313
`Make sure to use the production build (*.prod.js) when deploying for production.`
1414
)

0 commit comments

Comments
 (0)