Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ TODOs.md
coverage
.vscode/*
!.vscode/extensions.json

# test scratch dirs
test/.tmp
29 changes: 26 additions & 3 deletions src/hotReload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

export function genHotReloadCode(
id: string,
templateRequest: string | undefined
templateRequest: string | undefined,
renderFnName: 'render' | 'ssrRender' = 'render'
): string {
return `
/* hot reload */
Expand All @@ -13,12 +14,34 @@ if (module.hot) {
if (!api.createRecord('${id}', __exports__)) {
api.reload('${id}', __exports__)
}
${templateRequest ? genTemplateHotReloadCode(id, templateRequest) : ''}
${
templateRequest
? genTemplateHotReloadCode(id, templateRequest, renderFnName)
: ''
}
}
`
}

function genTemplateHotReloadCode(id: string, request: string) {
function genTemplateHotReloadCode(
id: string,
request: string,
renderFnName: 'render' | 'ssrRender'
) {
// A server build imports `ssrRender`, which `api.rerender` cannot handle: it
// assigns whatever it is given to `render` and re-renders the mounted
// instances, and a server build has none (only the DOM renderer registers
// them). Write the new function back to the component definition instead -
// that object is what parent modules hold a reference to, so the next render
// on the server picks it up.
if (renderFnName === 'ssrRender') {
return `
module.hot.accept(${request}, () => {
__exports__.ssrRender = ssrRender
})
`
}

return `
module.hot.accept(${request}, () => {
api.rerender('${id}', render)
Expand Down
12 changes: 10 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export interface VueLoaderOptions {

customElement?: boolean | RegExp

/**
* Whether to generate hot reload code for the component.
*
* Enabled by default for client builds outside production. Set to `true`
* explicitly to opt a server build in as well - useful when a dev server
* renders on the server and wants the server bundle to hot swap modules
* instead of restarting the whole process.
*/
hotReload?: boolean
exposeFilename?: boolean
/**
Expand Down Expand Up @@ -180,7 +188,7 @@ export default function loader(
// feature information
const hasScoped = descriptor.styles.some((s) => s.scoped)
const needsHotReload =
!isServer &&
(!isServer || options.hotReload === true) &&
!isProduction &&
!!(descriptor.script || descriptor.scriptSetup || descriptor.template) &&
options.hotReload !== false
Expand Down Expand Up @@ -420,7 +428,7 @@ export default function loader(
}

if (needsHotReload) {
code += genHotReloadCode(id, templateRequest)
code += genHotReloadCode(id, templateRequest, renderFnName)
}

code += `\n\nexport default __exports__`
Expand Down
4 changes: 2 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as qs from 'querystring'
import type { VueLoaderOptions } from '.'
import type { RuleSetRule, Compiler, RuleSetUse } from '@rspack/core'
import { needHMR } from './util'
import { clientCache, typeDepToSFCMap } from './resolveScript'
import { invalidateScript, typeDepToSFCMap } from './resolveScript'
import { compiler as vueCompiler } from './compiler'
import { descriptorCache } from './descriptorCache'

Expand Down Expand Up @@ -286,7 +286,7 @@ class VueLoaderPlugin {
for (const sfc of affectedSFCs) {
// bust script resolve cache
const desc = descriptorCache.get(sfc)
if (desc) clientCache.delete(desc)
if (desc) invalidateScript(desc)
// force update importing SFC
// @ts-ignore
compiler.fileTimestamps.set(sfc, {
Expand Down
11 changes: 11 additions & 0 deletions src/resolveScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ const serverCache = new WeakMap<SFCDescriptor, SFCScriptBlock | null>()

export const typeDepToSFCMap = new Map<string, Set<string>>()

/**
* Drop the compiled script of a descriptor, so that the next build recompiles
* it. Both caches are cleared because a single descriptor can be compiled for
* a client and a server build, and there is no way to tell here which one the
* stale entry belongs to.
*/
export function invalidateScript(descriptor: SFCDescriptor) {
clientCache.delete(descriptor)
serverCache.delete(descriptor)
}

/**
* inline template mode can only be enabled if:
* - is production (separate compilation needed for HMR during dev)
Expand Down
6 changes: 5 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export function needHMR(
const isProduction =
compilerOptions.mode === 'production' ||
process.env.NODE_ENV === 'production'
return !isServer && !isProduction && vueLoaderOptions.hotReload !== false
return (
(!isServer || vueLoaderOptions.hotReload === true) &&
!isProduction &&
vueLoaderOptions.hotReload !== false
)
}

export function resolveTemplateTSOptions(
Expand Down
Loading