|
8 | 8 | * - Hot Module Replacement (HMR) |
9 | 9 | */ |
10 | 10 |
|
11 | | -import { watch } from 'node:fs' |
| 11 | +import { watch, readFileSync } from 'node:fs' |
12 | 12 | import { readFile } from 'node:fs/promises' |
13 | 13 | import { ServerResponse } from 'node:http' |
14 | 14 | import { dirname, resolve } from 'node:path' |
@@ -194,6 +194,9 @@ export function angular(options: PluginOptions = {}): Plugin[] { |
194 | 194 | // Track component files with pending HMR updates (set by fs.watch, checked by HMR endpoint) |
195 | 195 | const pendingHmrUpdates = new Set<string>() |
196 | 196 |
|
| 197 | + // Cache inline template content per .ts file for detecting template-only changes |
| 198 | + const inlineTemplateCache = new Map<string, string>() |
| 199 | + |
197 | 200 | function getMinifyComponentStyles(context?: { |
198 | 201 | environment?: { config?: { build?: ResolvedConfig['build'] } } |
199 | 202 | }): boolean { |
@@ -622,6 +625,12 @@ export function angular(options: PluginOptions = {}): Plugin[] { |
622 | 625 | componentIds.set(actualId, className) |
623 | 626 | debugHmr('registered: %s -> %s', actualId, className) |
624 | 627 | } |
| 628 | + |
| 629 | + // Cache inline template content for detecting template-only changes in handleHotUpdate |
| 630 | + const inlineTemplate = extractInlineTemplate(code) |
| 631 | + if (inlineTemplate !== null) { |
| 632 | + inlineTemplateCache.set(actualId, inlineTemplate) |
| 633 | + } |
625 | 634 | } |
626 | 635 |
|
627 | 636 | return { |
@@ -669,6 +678,55 @@ export function angular(options: PluginOptions = {}): Plugin[] { |
669 | 678 | return [] |
670 | 679 | } |
671 | 680 |
|
| 681 | + // Check if only the inline template changed — if so, use HMR instead of full reload. |
| 682 | + // For external templates this is handled by fs.watch, but inline templates are part |
| 683 | + // of the .ts file and need explicit diffing. |
| 684 | + const cachedTemplate = inlineTemplateCache.get(ctx.file) |
| 685 | + if (cachedTemplate !== undefined) { |
| 686 | + let newContent: string |
| 687 | + try { |
| 688 | + newContent = readFileSync(ctx.file, 'utf-8') |
| 689 | + } catch { |
| 690 | + newContent = '' |
| 691 | + } |
| 692 | + const newTemplate = extractInlineTemplate(newContent) |
| 693 | + |
| 694 | + if (newTemplate !== null && newTemplate !== cachedTemplate) { |
| 695 | + // Template changed — check if ONLY the template changed |
| 696 | + const TMPL_RE = /template\s*:\s*`([\s\S]*?)`/ |
| 697 | + const newWithout = newContent.replace(TMPL_RE, 'template: ``') |
| 698 | + const oldReconstructed = newContent.replace(newTemplate, cachedTemplate).replace(TMPL_RE, 'template: ``') |
| 699 | + |
| 700 | + if (newWithout === oldReconstructed) { |
| 701 | + debugHmr('inline template-only change detected, using HMR for %s', ctx.file) |
| 702 | + |
| 703 | + // Update cache |
| 704 | + inlineTemplateCache.set(ctx.file, newTemplate) |
| 705 | + |
| 706 | + // Mark as pending so the HMR endpoint serves the update module |
| 707 | + pendingHmrUpdates.add(ctx.file) |
| 708 | + |
| 709 | + // Invalidate Vite's module graph |
| 710 | + const componentModule = ctx.server.moduleGraph.getModuleById(ctx.file) |
| 711 | + if (componentModule) { |
| 712 | + ctx.server.moduleGraph.invalidateModule(componentModule) |
| 713 | + } |
| 714 | + |
| 715 | + // Send HMR event (same as external template changes) |
| 716 | + const className = componentIds.get(ctx.file) |
| 717 | + const componentId = `${ctx.file}@${className}` |
| 718 | + const encodedId = encodeURIComponent(componentId) |
| 719 | + ctx.server.ws.send({ |
| 720 | + type: 'custom', |
| 721 | + event: 'angular:component-update', |
| 722 | + data: { id: encodedId, timestamp: Date.now() }, |
| 723 | + }) |
| 724 | + |
| 725 | + return [] |
| 726 | + } |
| 727 | + } |
| 728 | + } |
| 729 | + |
672 | 730 | debugHmr('triggering full reload for component file change') |
673 | 731 | // Component FILE changes require a full reload because: |
674 | 732 | // - Class definition changes can't be hot-swapped safely |
|
0 commit comments