Skip to content

Per-class resource locators are text-based, so const-folded templateUrl/styleUrls fall back to file-level resolution #456

Description

@Brooooooklyn

Split out of a Codex review finding on #455. Pre-existing — applies equally to the template locator merged in #449.

Symptom

The @ng/component HMR endpoint resolves a class's own templateUrl and styleUrl(s) by scanning the decorator text. The Rust extractor statically folds same-file constants and interpolated template literals; the text scan cannot.

const DIR = './themes'
@Component({ styleUrls: [`${DIR}/a.css`, SHARED_STYLE] })

Neither entry yields a string literal, so the per-class answer is "unknown" and the endpoint falls back to the file-level styleUrls union. In a single-component file that is harmless. In a multi-component file it reintroduces exactly the cross-class contamination #451 removed.

Current behavior is deliberate, not accidental

#455 makes the fallback explicit and distinguishes three states:

decorator state served
not locatable file-level fallback
locatable, no style field nothing
field present, no string literal file-level fallback

The third row is the const case. Falling back is strictly better than serving empty — a component keeps working — but it is not per-class correct.

Scope

This is not style-specific. extractTemplateUrlFor (from #449) has the same limitation, and falls back to templateUrls[0].

Suggested fix

Expose per-class resolved resources from the existing AST metadata extraction — extract_component_metadata_sync already returns templateUrl/styleUrls per class with constants folded — and have the endpoint consult that instead of re-scanning source text. That removes the text locators from the resolution path entirely rather than teaching them to fold constants.

Worth checking first how the endpoint would get that metadata cheaply: it currently calls extractComponentUrls per request, and componentMetadataCache exists but stores stripped source, not parsed metadata.

Activity

  1. Brooooooklyn commented on Aug 24, 2026

    @Brooooooklyn
    MemberAuthor

    A later review round on #455 suggested an alternative worth recording here, since it is a product decision rather than a bug fix.

    When a per-class answer is unavailable — a const-form styleUrls, a computed metadata key — #455 falls back to the file-level union. In a multi-component file that union contains the siblings' stylesheets, so the class receives its own CSS plus theirs. That is what main does today for every class, so it is not a regression, but it is knowingly contaminated.

    The suggestion: trigger a full reload instead of compiling a union we know is wrong.

    The trade-off is real in both directions. A full reload is always correct but costs the dev-server experience on every const-form component, including single-component files where the union is harmless. The union keeps HMR fast and is only wrong when siblings exist. Resolving this issue as described — exposing per-class resolved resources from the Rust extractor — removes the choice entirely, which is why I would rather do that than pick a lesser evil now.

    Recording it so the option is not lost if the redesign is deferred.

  2. Brooooooklyn commented on Aug 24, 2026

    @Brooooooklyn
    MemberAuthor

    Also applies to inline styles, not just the URL fields.

    const BASE = ".base{}"; styles: [BASE, ".local{}"] — Rust folds the const and compiles both stylesheets. The text scanner reads one literal, marks the field incomplete, and the endpoint omits the styles key, so edits to .local{} do not hot-apply.

    Verified against real @angular/core 22.1.3:

    rust        styles = [".base{}", ".local{}"]
    before HMR  def.styles = [".base[_ngcontent-%COMP%]{}", ".local[_ngcontent-%COMP%]{}"]
    after  HMR  def.styles = [".base[_ngcontent-%COMP%]{}", ".local[_ngcontent-%COMP%]{}"]   (stale)
    

    Same root cause: the locators are text-based and cannot fold same-file constants. Resolving constants would fix all three fields at once.

  3. added a commit that references this issue on Aug 25, 2026
    a98ba07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions