Skip to content

Commit 581f4ed

Browse files
feat: HMR support for inline template changes (#209)
When a component .ts file changes and only the inline `template: `...`` portion differs, route it through the existing component HMR mechanism instead of triggering a full page reload. This gives inline template components the same fast HMR experience as external .html templates. Implementation: - Cache inline template content during transform - In handleHotUpdate, compare old vs new template content - If only the template changed, add to pendingHmrUpdates and send angular:component-update event (same path as external templates) - The existing HMR middleware endpoint already handles inline templates via extractInlineTemplate(), so no middleware changes needed - Falls back to full reload if non-template code also changed
1 parent 1b1c074 commit 581f4ed

File tree

1 file changed

+61
-1
lines changed
  • napi/angular-compiler/vite-plugin

1 file changed

+61
-1
lines changed

napi/angular-compiler/vite-plugin/index.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* - Hot Module Replacement (HMR)
99
*/
1010

11-
import { watch } from 'node:fs'
11+
import { watch, readFileSync } from 'node:fs'
1212
import { readFile } from 'node:fs/promises'
1313
import { ServerResponse } from 'node:http'
1414
import { dirname, resolve } from 'node:path'
@@ -194,6 +194,9 @@ export function angular(options: PluginOptions = {}): Plugin[] {
194194
// Track component files with pending HMR updates (set by fs.watch, checked by HMR endpoint)
195195
const pendingHmrUpdates = new Set<string>()
196196

197+
// Cache inline template content per .ts file for detecting template-only changes
198+
const inlineTemplateCache = new Map<string, string>()
199+
197200
function getMinifyComponentStyles(context?: {
198201
environment?: { config?: { build?: ResolvedConfig['build'] } }
199202
}): boolean {
@@ -636,6 +639,12 @@ export function angular(options: PluginOptions = {}): Plugin[] {
636639
componentIds.set(actualId, className)
637640
debugHmr('registered: %s -> %s', actualId, className)
638641
}
642+
643+
// Cache inline template content for detecting template-only changes in handleHotUpdate
644+
const inlineTemplate = extractInlineTemplate(code)
645+
if (inlineTemplate !== null) {
646+
inlineTemplateCache.set(actualId, inlineTemplate)
647+
}
639648
}
640649

641650
return {
@@ -694,6 +703,57 @@ export function angular(options: PluginOptions = {}): Plugin[] {
694703
return []
695704
}
696705

706+
// Check if only the inline template changed — if so, use HMR instead of full reload.
707+
// For external templates this is handled by fs.watch, but inline templates are part
708+
// of the .ts file and need explicit diffing.
709+
const cachedTemplate = inlineTemplateCache.get(ctx.file)
710+
if (cachedTemplate !== undefined) {
711+
let newContent: string
712+
try {
713+
newContent = readFileSync(ctx.file, 'utf-8')
714+
} catch {
715+
newContent = ''
716+
}
717+
const newTemplate = extractInlineTemplate(newContent)
718+
719+
if (newTemplate !== null && newTemplate !== cachedTemplate) {
720+
// Template changed — check if ONLY the template changed
721+
const TMPL_RE = /template\s*:\s*`([\s\S]*?)`/
722+
const newWithout = newContent.replace(TMPL_RE, 'template: ``')
723+
const oldReconstructed = newContent
724+
.replace(newTemplate, cachedTemplate)
725+
.replace(TMPL_RE, 'template: ``')
726+
727+
if (newWithout === oldReconstructed) {
728+
debugHmr('inline template-only change detected, using HMR for %s', ctx.file)
729+
730+
// Update cache
731+
inlineTemplateCache.set(ctx.file, newTemplate)
732+
733+
// Mark as pending so the HMR endpoint serves the update module
734+
pendingHmrUpdates.add(ctx.file)
735+
736+
// Invalidate Vite's module graph
737+
const componentModule = ctx.server.moduleGraph.getModuleById(ctx.file)
738+
if (componentModule) {
739+
ctx.server.moduleGraph.invalidateModule(componentModule)
740+
}
741+
742+
// Send HMR event (same as external template changes)
743+
const className = componentIds.get(ctx.file)
744+
const componentId = `${ctx.file}@${className}`
745+
const encodedId = encodeURIComponent(componentId)
746+
ctx.server.ws.send({
747+
type: 'custom',
748+
event: 'angular:component-update',
749+
data: { id: encodedId, timestamp: Date.now() },
750+
})
751+
752+
return []
753+
}
754+
}
755+
}
756+
697757
debugHmr('triggering full reload for component file change')
698758
// Component FILE changes require a full reload because:
699759
// - Class definition changes can't be hot-swapped safely

0 commit comments

Comments
 (0)