From efe39db023dcd4bad5692031d2be0d0e6a0c1853 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 12 Dec 2019 12:42:21 -0500 Subject: [PATCH] feat(runtime-core): hot module replacement --- packages/global.d.ts | 1 + packages/runtime-core/src/apiOptions.ts | 4 ++ packages/runtime-core/src/component.ts | 1 + packages/runtime-core/src/hmr.ts | 85 +++++++++++++++++++++++++ packages/runtime-core/src/index.ts | 6 +- packages/runtime-core/src/renderer.ts | 11 ++++ rollup.config.js | 2 + 7 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 packages/runtime-core/src/hmr.ts diff --git a/packages/global.d.ts b/packages/global.d.ts index 5ddded4813..cfe02b96a6 100644 --- a/packages/global.d.ts +++ b/packages/global.d.ts @@ -2,6 +2,7 @@ declare var __DEV__: boolean declare var __TEST__: boolean declare var __BROWSER__: boolean +declare var __BUNDLER__: boolean declare var __RUNTIME_COMPILE__: boolean declare var __COMMIT__: string declare var __VERSION__: string diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index 80e158849f..dd35dbe970 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -66,6 +66,10 @@ export interface ComponentOptionsBase< directives?: Record inheritAttrs?: boolean + // SFC & dev only + __scopeId?: string + __hmrId?: string + // type-only differentiator to separate OptionWithoutProps from a constructor // type returned by createComponent() or FunctionalComponent call?: never diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 6da844d63d..be4ee16859 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -37,6 +37,7 @@ export interface FunctionalComponent

{ props?: ComponentPropsOptions

inheritAttrs?: boolean displayName?: string + __hmrId?: string } export type Component = ComponentOptions | FunctionalComponent diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts new file mode 100644 index 0000000000..10ea134112 --- /dev/null +++ b/packages/runtime-core/src/hmr.ts @@ -0,0 +1,85 @@ +import { + ComponentInternalInstance, + ComponentOptions, + RenderFunction +} from './component' + +// Expose the HMR runtime on the global object +// This makes it entirely tree-shakable without polluting the exports and makes +// it easier to be used in toolings like vue-loader +// Note: for a component to be eligible for HMR it also needs the __hmrId option +// to be set so that its instances can be registered / removed. +if (__BUNDLER__ && __DEV__) { + const globalObject: any = + typeof global !== 'undefined' + ? global + : typeof self !== 'undefined' + ? self + : typeof window !== 'undefined' + ? window + : {} + + globalObject.__VUE_HMR_RUNTIME__ = { + isRecorded: tryWrap(isRecorded), + createRecord: tryWrap(createRecord), + rerender: tryWrap(rerender), + reload: tryWrap(reload) + } +} + +interface HMRRecord { + comp: ComponentOptions + instances: Set +} + +const map: Map = new Map() + +export function registerHMR(instance: ComponentInternalInstance) { + map.get(instance.type.__hmrId!)!.instances.add(instance) +} + +export function unregisterHMR(instance: ComponentInternalInstance) { + map.get(instance.type.__hmrId!)!.instances.delete(instance) +} + +function isRecorded(id: string): boolean { + return map.has(id) +} + +function createRecord(id: string, comp: ComponentOptions) { + if (map.has(id)) { + return + } + map.set(id, { + comp, + instances: new Set() + }) +} + +function rerender(id: string, newRender: RenderFunction) { + map.get(id)!.instances.forEach(instance => { + instance.render = newRender + instance.renderCache = [] + instance.update() + // TODO force scoped slots passed to children to have DYNAMIC_SLOTS flag + }) +} + +function reload(id: string, newComp: ComponentOptions) { + // TODO + console.log('reload', id) +} + +function tryWrap(fn: (id: string, arg: any) => void): Function { + return (id: string, arg: any) => { + try { + fn(id, arg) + } catch (e) { + console.error(e) + console.warn( + `Something went wrong during Vue component hot-reload. ` + + `Full reload required.` + ) + } + } +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 019b081e89..de44fdd013 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -66,7 +66,9 @@ export { TransitionHooks } from './components/BaseTransition' -// Internal, for compiler generated code +// Internal API ---------------------------------------------------------------- + +// For compiler generated code // should sync with '@vue/compiler-core/src/runtimeConstants.ts' export { withDirectives } from './directives' export { @@ -87,7 +89,7 @@ import { capitalize as _capitalize, camelize as _camelize } from '@vue/shared' export const capitalize = _capitalize as (s: string) => string export const camelize = _camelize as (s: string) => string -// Internal, for integration with runtime compiler +// For integration with runtime compiler export { registerRuntimeCompiler } from './component' // Types ----------------------------------------------------------------------- diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 2c3d65f361..b688525ddc 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -53,6 +53,7 @@ import { } from './components/Suspense' import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { KeepAliveSink, isKeepAlive } from './components/KeepAlive' +import { registerHMR, unregisterHMR } from './hmr' export interface RendererOptions { patchProp( @@ -857,6 +858,11 @@ export function createRenderer< parentComponent )) + // HMR + if (__BUNDLER__ && __DEV__ && instance.type.__hmrId != null) { + registerHMR(instance) + } + if (__DEV__) { pushWarningContext(initialVNode) } @@ -1549,6 +1555,11 @@ export function createRenderer< parentSuspense: HostSuspenseBoundary | null, doRemove?: boolean ) { + // HMR + if (__BUNDLER__ && __DEV__ && instance.type.__hmrId != null) { + unregisterHMR(instance) + } + const { bum, effects, update, subTree, um, da, isDeactivated } = instance // beforeUnmount hook if (bum !== null) { diff --git a/rollup.config.js b/rollup.config.js index b629f61d3e..fb6f23d8f0 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -147,6 +147,8 @@ function createReplacePlugin( __TEST__: isBundlerESMBuild ? `(process.env.NODE_ENV === 'test')` : false, // If the build is expected to run directly in the browser (global / esm builds) __BROWSER__: isBrowserBuild, + // is targeting bundlers? + __BUNDLER__: isBundlerESMBuild, // support compile in browser? __RUNTIME_COMPILE__: isRuntimeCompileBuild, // support options? -- 2.47.3