diff --git a/packages/global.d.ts b/packages/global.d.ts index 5ddded48..cfe02b96 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 80e15884..dd35dbe9 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 6da844d6..be4ee168 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 00000000..10ea1341 --- /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 019b081e..de44fdd0 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 2c3d65f3..b688525d 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 b629f61d..fb6f23d8 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?