Skip to content

Commit faa236c

Browse files
Brooooooklynclaude
andauthored
fix(vite): dispatch template HMR to every component file sharing a templateUrl (#449)
* fix(vite): dispatch template HMR to every component file sharing a templateUrl When two component .ts files declare the same external templateUrl, editing that template dispatched angular:component-update for only the last-transformed owner: resourceToComponent is single-valued, so the last transform wins the slot and handleHotUpdate resolved one owner. Mirror the multi-valued styleComponentOwners pattern for templates: - declare templateComponentOwners: Map<string, Set<string>> - prune it in transform beside the style prune - populate it in the dep loop under TEMPLATE_REGEX - iterate the owner set in handleHotUpdate instead of resourceToComponent.get() resourceToComponent stays single-valued; the same-file case already worked via dispatchAllComponentsInFile. One event per owner is what the client expects: every component listener filters on its own id, and @angular/build does the same via externalTemplateToComponentsMap. Fixes #445 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx * fix(vite): resolve @ng/component templates per class; track owners by role Two review findings on the multi-owner fan-out, both verified: 1. The @ng/component endpoint read templateUrls[0] of the FILE for every requested class. In a multi-component file, a class whose template is not first was served a sibling's markup — the fan-out newly exposed loser owner files to that corruption. Resolve the template per CLASS: the class's own templateUrl (new per-class locator), then its inline template, then templateUrls[0] only as a fallback for decorator shapes the locator cannot parse. 2. The owner-map population gate used TEMPLATE_REGEX, but a decorator templateUrl is not required to end in .html — resolveResources reads any file by decorator ROLE. A .css-named templateUrl lost its HMR dispatch once the resourceToComponent fallback was gone. Track template ownership in a directTemplateUrls role set, mirroring directStyleUrls. The intra-file fan-out design (dispatchAllComponentsInFile) is kept: with per-class serving, sibling dispatch is a no-op again, and the per-class-ownership redesign suggested in review would change behavior main deliberately tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent a6a472f commit faa236c

4 files changed

Lines changed: 503 additions & 19 deletions

File tree

napi/angular-compiler/test/decorator-fields.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
locateComponentDecorators,
66
locateStylesFieldFor,
77
locateTemplateStringFor,
8+
locateTemplateUrlFor,
89
} from '../vite-plugin/utils/decorator-fields.js'
910

1011
describe('decorator-fields utils', () => {
@@ -260,6 +261,47 @@ describe('decorator-fields utils', () => {
260261
const range = locateTemplateStringFor(src, 'Foo')!
261262
expect(src.slice(range[0], range[1] + 1)).toBe(`'<real/>'`)
262263
})
264+
265+
it('does not match a `templateUrl:` field as `template:`', () => {
266+
const src = `@Component({ templateUrl: './foo.html' })\nexport class Foo {}`
267+
expect(locateTemplateStringFor(src, 'Foo')).toBeNull()
268+
})
269+
})
270+
271+
describe('locateTemplateUrlFor', () => {
272+
const multi = `
273+
@Component({ selector: 'a', templateUrl: './first.html' })
274+
export class FirstComponent {}
275+
@Component({ selector: 'b', templateUrl: './second.html' })
276+
export class SecondComponent {}
277+
`
278+
279+
it('returns null when className matches no decorator', () => {
280+
expect(locateTemplateUrlFor(multi, 'Nope')).toBeNull()
281+
})
282+
283+
it('returns null when the named component has no templateUrl field', () => {
284+
const src = `@Component({ template: '<p/>' })\nexport class Foo {}`
285+
expect(locateTemplateUrlFor(src, 'Foo')).toBeNull()
286+
})
287+
288+
it('returns each component its own templateUrl range in a multi-component file', () => {
289+
const first = locateTemplateUrlFor(multi, 'FirstComponent')!
290+
const second = locateTemplateUrlFor(multi, 'SecondComponent')!
291+
expect(multi.slice(first[0], first[1] + 1)).toBe(`'./first.html'`)
292+
expect(multi.slice(second[0], second[1] + 1)).toBe(`'./second.html'`)
293+
})
294+
295+
it('does not match an inline `template:` field as `templateUrl:`', () => {
296+
const src = `@Component({ template: '<p>templateUrl: fake</p>' })\nexport class Foo {}`
297+
expect(locateTemplateUrlFor(src, 'Foo')).toBeNull()
298+
})
299+
300+
it('finds templateUrl when the decorator also has an inline template field', () => {
301+
const src = `@Component({ template: '<p/>', templateUrl: './real.html' })\nexport class Foo {}`
302+
const range = locateTemplateUrlFor(src, 'Foo')!
303+
expect(src.slice(range[0], range[1] + 1)).toBe(`'./real.html'`)
304+
})
263305
})
264306

265307
// -----------------------------------------------------------------

0 commit comments

Comments
 (0)