Skip to content

fix(vite): resolve @ng/component styles per class - #455

Merged
Brooooooklyn merged 10 commits into
mainfrom
fix/issue-451-per-class-endpoint-styles
Aug 25, 2026
Merged

fix(vite): resolve @ng/component styles per class#455
Brooooooklyn merged 10 commits into
mainfrom
fix/issue-451-per-class-endpoint-styles

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Aug 24, 2026

Copy link
Copy Markdown
Member

Fixes #451. The style-side counterpart to the per-class template repair in #449.

Two defects, one block, one root cause

The @ng/component HMR endpoint built the update module for any requested class from the file-level styleUrls, which extractComponentUrls returns as the union of every component in the file.

  1. Sibling styles served. In a multi-component file, a class received its siblings' stylesheets.
  2. Inline styles shadowed. The file-level list was tried first, so being non-empty it shadowed the inline styles: of a class that declares no external ones. This is exactly the shape fix(vite): dispatch template HMR to every component file sharing a templateUrl #449 fixed on the template side.

The fix

The styles block now uses the same three-step order as the template block directly above it:

class's own styleUrls / styleUrl  →  class's inline styles  →  file-level list (fallback)

The fallback preserves today's behavior for decorator shapes the per-class locator cannot parse. The read/preprocess loop is extracted to a local readStyles(urls) used by both paths, so the per-style try/catch, the resolvedConfig guard around preprocessCSS, and the styleContents.length > 0 check are unchanged.

Both Angular spellings

Harder than the template case, which had one field name and one value shape. This handles styleUrls: ['./a.css', './b.css'] and the singular styleUrl: './a.css', with all four cross-match guards under test — neither url field matches the other, and neither matches inline styles:. isFieldKeyAt's two-sided word boundary is what does it: searching styleUrl inside styleUrls: is rejected because the following s is a word character.

styleUrls wins if a decorator carries both. Angular rejects that combination outright, so this only makes the choice deterministic.

A test-harness trap worth knowing

The first RED run failed with neither marker present, plus Failed to preprocess style: … Cannot read properties of undefined (reading 'client'). The shared setupPluginWithServer in hmr-hot-update.test.ts passes a stub resolved config ({ build: {}, isProduction: false }), so preprocessCSS throws and the existing per-style catch silently drops every external stylesheet.

Left unnoticed, tests 1 and 2 would have gone green after the fix while proving nothing. The new describe block builds a real config via resolveConfig, the way style-deps-hmr.test.ts already does. I did not change the shared helper — around 30 other tests depend on it — but anyone adding endpoint tests that assert on external stylesheet content needs the real-config setup.

Tests

TDD; each behavior test failed on 0b56f50 for the intended served-content reason:

  • serves each component its own styleUrls in a multi-component file — served the sibling's marker.
  • serves the singular styleUrl of the requested class — served the sibling's marker.
  • serves the inline styles of a class whose sibling uses a styleUrl — inline marker absent.

Plus 8 locator unit tests in decorator-fields.test.ts following the locateTemplateUrlFor precedent from #449.

Verification

248 unit tests pass (237 pre-existing + 11 new), oxfmt --check clean. Verified by diff that the template block, pending-slot handling, error path, and transient-empty logic are untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx


Note

Medium Risk
Changes core HMR middleware and a large, heuristic @Component text parser; regressions would show up as wrong or missing styles in dev, not in production builds.

Overview
Fixes incorrect HMR style delivery when multiple components share one file: the @ng/component endpoint no longer builds update modules from the file-level styleUrls union, which leaked siblings’ CSS and could hide a class’s inline styles.

Endpoint behavior mirrors the existing per-class template path: read that class’s inline styles and styleUrl / styleUrls, preprocess external paths, merge inline first then external (compiler order), and only fall back to the file-level URL list when the per-class metadata cannot be read (e.g. constants the text scan does not resolve).

decorator-fields gains substantial parsing: comment/string-aware @Component discovery, locateStyleFieldsFor with absent / unreadable / literal states, readStringLiterals with a complete flag, quoted and escaped keys, and rules aligned with the Rust extractor (spreads dropped, shorthand styleUrl unreadable vs array shorthands absent, partial literals not acted on).

Tests add broad unit coverage in decorator-fields.test.ts and integration cases in hmr-hot-update.test.ts using a real Vite resolveConfig so external CSS preprocessing is exercised; expectDispatched guards Windows path spelling for HMR ids.

Reviewed by Cursor Bugbot for commit 39b4d45. Bugbot is set up for automated code reviews on this repo. Configure here.

The HMR endpoint built the update module for any requested class from
the FILE's styleUrls, which extractComponentUrls returns as the union of
every component in the file. In a multi-component file a class was
served its siblings' stylesheets.

The same block held a second defect of the shape #449 fixed on the
template side: the file-level list was tried FIRST, so being non-empty
it shadowed the inline `styles:` of a class that declares no external
ones. One root cause, both fixed here.

Resolve per class, in the order the template block above already uses:
the class's own styleUrls, then its inline styles, then the file-level
list as a fallback for decorator shapes the locator cannot parse.

Both Angular spellings are handled — `styleUrls: [...]` and the singular
`styleUrl: '...'` — with the cross-match guards under test: neither url
field matches the other, and neither matches inline `styles:`.

Note for future tests in hmr-hot-update.test.ts: the shared
setupPluginWithServer passes a stub resolved config, so preprocessCSS
throws and the per-style catch drops every external stylesheet. The new
endpoint tests build a real config via resolveConfig; without it they
could not observe external styles at all.

Fixes #451

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_9a88e946-d917-4733-9430-7f8ab1053d12)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3261b8cb13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/index.ts Outdated
Comment thread napi/angular-compiler/vite-plugin/index.ts Outdated
Comment thread napi/angular-compiler/vite-plugin/index.ts Outdated
… its own

Three things, all in the endpoint's style resolution.

1. Replace the regex string-walking in extractStyleUrlsFor and its twin in
   extractInlineStyles with a real scanner, readStringLiterals, built on the
   file's existing skipComment and findClosingDelim. The regex was blind to
   comments: `['/* don't */ './a.css']` yielded ["t touch */ "] and dropped
   the real URL. That is a main bug on the inline-styles path, which has
   shipped with the regex. A 16-input differential probe shows the two
   disagree on exactly the 3 comment cases and nowhere else.

2. A class declaring no styles at all fell through to the file-level union
   and was served a sibling's scoped CSS — the isolation defect this change
   exists to remove.

3. A class declaring both `styles` and `styleUrls` lost the inline half; the
   branches were exclusive. The compiler merges them, so HMR diverged from
   compile after any update.

The fix distinguishes three states, which the old null could not express:
the decorator is not locatable (fall back to the file-level list), it is
locatable with no style field (serve nothing — a real empty answer), or it
declares a field holding no string literal (fall back, since the Rust
extractor folds constants this text scan cannot).

Merge order is inline first, resolved external appended, taken from the
implementation rather than assumed: decorator.rs assigns the inline array
into metadata.styles and transform.rs pushes resolved content onto it. The
contract test asserts presence only, so it would not have caught a reversal.
Whitespace-only entries are dropped, matching Angular's trim filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e39b3ac6-f744-487a-bcd8-79fe4b0f7f2b)

@Brooooooklyn

Copy link
Copy Markdown
Member Author

Review round — c853252

Two changes since the first push: the regex was replaced with a real parser at the maintainer's request, and two of three adversarial-review findings are fixed.

The regex was broken, not just ugly

It walked the array body blind to comments. Measured:

array body regex yields
/* don't touch */ './a.css' ["t touch */ "] — real URL lost
// it's here + './a.css' ["s here\n "]

An apostrophe in a comment returns a garbage path and drops the entry. This ships on main todayextractStyleUrlsFor copied the regex from extractInlineStyles, so inline styles: arrays have been served corrupt since well before this branch. The RED output showed it plainly: styles: [ "s fine */\n " ].

readStringLiterals replaces both copies, built on the file's existing skipComment and findClosingDelim rather than a new tokenizer. A 16-input differential probe puts the two implementations side by side: they disagree on exactly the 3 comment cases and agree everywhere else — escaped quotes, mixed quote styles, template literals with ${…}, nested arrays, empty arrays, non-literal entries, and CSS text with an apostrophe inside a double-quoted string. Strict bug fix, no drift.

Findings acted on

# finding verdict action
1 a styleless class still gets a sibling's CSS via the file-level fallback confirmed — a hole in the isolation this PR claims fixed
2 a class declaring both styles and styleUrls loses the inline half confirmed against the repo's own contract test fixed
3 text locators cannot resolve const-folded URLs pre-existing, and applies equally to the template locator from #449 declined → #456

The review's suggested merge order was backwards. It recommended externalStyles.concat(inlineStyles). The compiler does the opposite: decorator.rs assigns the decorator's inline array into metadata.styles, and resolved content is pushed onto it afterwards. So the canonical order is inline first, external appended. The contract test asserts presence only, never order, so it would not have caught the reversal — the order here is taken from the implementation and asserted by index in the new test.

The three-way state

Fixing finding 1 could not simply mean "no styles found → serve nothing", or a const-form styleUrls (finding 3's case) would lose every style instead of falling back. The locator now reports the distinction the old null was destroying:

decorator state served
not locatable file-level fallback (unchanged)
locatable, no style field nothing — a real empty answer
field present, no string literal file-level fallback (unchanged)
parseable inline + external, merged

Test 3 is the guard for row 3, and it passed before the change as well as after — it exists to prove the refactor preserves that path.

270 unit tests pass (248 → 270), oxfmt --check clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c853252c89

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/index.ts
Comment thread napi/angular-compiler/vite-plugin/index.ts Outdated
The three-way classifier inferred completeness from how many literals it
read, which got two cases backwards.

- `styleUrl: STYLE_URL` yields no locator range, because an identifier is
  not a quote opener. That was read as "field absent", i.e. a confident
  "this class has no styles", so the endpoint served none — stripping the
  CSS of a component whose const the Rust extractor folds. Before this
  branch it fell back. Same for a mixed `[STYLE_URL, './b.css']`, which
  silently served only the literal half.
- `styleUrls: []` is valid and means "no styles", but zero literals was
  read as unknown, so the fallback served a sibling's CSS.

Field presence is now detected by the key, independent of the value, and
the value is classified absent / literal / unreadable. An unreadable
element anywhere makes the whole field unknown: acting on the literals
beside it would drop whatever the rest names. An interpolated template
literal counts as unreadable. Verified against the Rust extractor, which
folds both consts and interpolations, so falling back is right.

locateFieldInsideArgs keeps its nullable-range shape as a wrapper, so its
existing callers are untouched — only the style path wants the third
state. readStringLiterals now reports `complete` alongside its literals
rather than gaining a sibling function that discards it.

Also fix a Windows-only test failure caught by NAPI Smoke: one test
passed the raw path to `transform` and a normalized one as `ctx.file`,
which are the same file but different strings on Windows, so
`componentsByFile.has(ctx.file)` missed and nothing was dispatched. Vite
normalizes both in production, so this was a test artifact; the test now
uses one spelling like its long-standing neighbours. An expectDispatched
guard asserts the queued id matches the requested one, so this class of
bug names itself instead of surfacing as an empty response body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_11a567f6-625f-4d65-adbf-8daf04f8bf4f)

@Brooooooklyn

Copy link
Copy Markdown
Member Author

Review round — 9066e43

The re-review of c853252 found two defects in the three-way classifier that commit introduced. Both confirmed by reading the code, both fixed.

# finding verdict
1 a const/identifier style value is read as "field absent" and the class is served NO styles confirmed — a regression from c853252, worse than the fallback it replaced
2 a valid styleUrls: [] / styles: [] is read as unknown and inherits a sibling's CSS confirmed — the contamination this PR exists to remove

The first was mine and the worst kind: silently stripping a component's CSS. locateFieldInsideArgs returns the same null for "no such key" and "key present, value is not a literal", so my absent/unparseable distinction collapsed one level below where I checked it.

Field presence is now detected by the key, independent of the value, and the value classifies as absent / literal / unreadable:

value served
key absent nothing
[] nothing — a real empty answer
['./a.css'], './a.css' those styles
IDENT, [IDENT, './b.css'], `${X}/a.css` file-level fallback

An unreadable element anywhere makes the whole field unknown — acting on the literals beside it would drop whatever the rest names. Verified against the Rust extractor rather than assumed: extractComponentUrls folds both same-file consts and interpolations, so falling back is correct. It does not fold DIR + '/b.css', which is why the classifier only has to be conservative, not clever.

locateFieldInsideArgs keeps its nullable-range shape as a wrapper, so its existing callers are untouched — only the style path wants the third state.

Windows

NAPI Smoke (windows-latest) failed on c853252 while every other job passed. Not a flake: Test runs cargo test, and NAPI Smoke is the job that runs pnpm test, so only one job runs vitest at all.

One test I added passed the raw path to transform and a normalizePath'd one as ctx.file — the same file, different strings on Windows — so componentsByFile.has(ctx.file) missed, Branch 2 was never entered, and the endpoint returned an empty body.

A test artifact, not a plugin bug. Vite normalizes both the watcher's file and the resolver's ids, so production never mixes spellings; and structurally, if these disagreed in production, Branch 2 would be dead on Windows for every user. Reproduced on macOS by substituting // for the separator difference, which produced the identical assertion and the plain .ts: triggering full reload fall-through in the plugin's debug output.

The test now uses one spelling, like its long-standing neighbours. The assertion is unchanged and nothing is skipped. An expectDispatched guard now asserts the queued id matches the requested one, so this class of bug names itself rather than surfacing as an unexplained empty body. Every other .ts-editing test in the block was audited; the rest were already single-spelling and now carry the guard too.

282 unit tests pass, oxfmt --check clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9066e4325b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/utils/decorator-fields.ts Outdated
Comment thread napi/angular-compiler/vite-plugin/index.ts
…ble one

`@Component({ 'styleUrls': [...] })` is valid TS, and the Rust extractor
resolves it, but the text scan saw no field and the classifier reported a
confident "this class declares no styles" — so the endpoint served none
and the component lost its CSS. Before this branch's styleless fix the
case fell back and kept working. Same failure mode as the const case in
9066e43, different trigger.

Quoted keys, single and double, are now matched like bare ones. That
widens template/templateUrl/styles too; the cross-match guards are
unaffected, since only the key's spelling changed, not its identity.

The general form matters more than the trigger: "no styles" is only safe
to conclude after seeing every top-level key. A computed key or an
escaped one hides a field that may exist, so the style fields become
unreadable and the endpoint falls back. A field that IS visible stays
readable — a decorator holding one computed key does not lose the fields
beside it.

A spread is deliberately NOT treated that way. Measured: the Rust
extractor drops `...BASE` entirely and reports no styleUrls, so the
compiled component really has none. Falling back would hand that class
its siblings' stylesheets, which is the contamination this PR removes.
Serving nothing is what matches the compile path, and a test guards it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e0dc0761-3b9d-4751-8075-9730b00c1b40)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90edef81d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/utils/decorator-fields.ts Outdated
Two more ways the per-class scan disagreed with the Rust extractor, both
ending in a component served the wrong CSS or none.

Escapes: the scanner returned raw source text and called the read
complete, so `styleUrls: ['./cmp.css']` produced a path containing
the literal escape, the read failed, the per-style catch swallowed it,
and the component lost its stylesheet. Literals are now cooked with JS
semantics — single-char escapes, \xHH, \uHHHH, \u{…}, line continuations,
and NonEscapeCharacter — matching what Rust resolves. Malformed hex or
unicode, a trailing backslash, and legacy octal report incomplete so the
caller falls back; we are exact where we can be and defer where we
cannot. Literals with no backslash take a fast path and are byte-for-byte
unchanged.

Decoding was chosen over marking escaped literals unknown: falling back
hands a multi-component file the sibling union, which is the very
contamination this change removes.

Phantom decorators: `locateComponentDecorators` enumerated `@Component(`
with a regex, so a commented-out decorator between a real one and its
class captured the class. The endpoint then read the commented metadata —
serving old.css instead of real.css, or nothing at all when the phantom
also hid the template. Enumeration now skips comments and string
literals. Narrower than it first appeared: only a phantom sitting between
a decorator and its class did damage; one appearing first was already
dropped by the second pass, and both orderings are now pinned.

Two tests from earlier in this branch asserted the raw text — the defect
itself — and now assert the cooked value. Verified against Rust:
`./it's.css`, `./cmp.css`, `./aqb.css`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_348b6b10-f906-4d2e-968a-ee7a331f0dbc)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2342b0490

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/utils/decorator-fields.ts Outdated
…y forms

A shorthand property matched the key but had no colon, so the lookup fell
through and the classifier concluded "definitively styleless" — the
endpoint served nothing while Rust resolved the constant:

  const styleUrl = './x.css'
  @component({ template: '<p/>', styleUrl })    rust ./x.css, scan absent

This was the fourth round fixing one form of the same defect, each found
by a reviewer rather than by us. So rather than patch shorthand alone, 26
key and value forms were probed against extractComponentUrls. Exactly one
was broken; 10 of the new guards passed on first run, which is what makes
the audit credible rather than selective.

Calibration matters as much as the fix. Reporting unreadable too eagerly
sends the class to the file-level union, which is the sibling
contamination this work removes. So findFieldInArgs now tracks key versus
value position — without it the fix would have degraded
`selector: styleUrl`, a readable field sitting right there — and only the
SINGULAR styleUrl shorthand is unreadable, because that is the only form
the compiler resolves. Unrelated methods, getters, and deeper nesting are
untouched.

Three divergences are deliberately left alone. Duplicate keys are a
TypeScript error, so the input is invalid either way. For a numeric key
and an `as` cast it is Rust that returns nothing while our scan reads the
literal — matching it would mean dropping styles to mirror a parsing gap
on the other side, which is the wrong direction to converge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6711738f-fa7e-4ae1-920f-06a2eb3c9da5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a43c73ede

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/utils/decorator-fields.ts
`styleUrls /* why */: ['./x.css']` was read as absent at c2342b0, so the
endpoint served nothing while Rust resolved the stylesheet. Round 8's
skipToToken fixed it incidentally while fixing shorthand properties, and
nothing pinned it — an incidental fix can regress silently.

The earlier 26-form audit framed itself around key SHAPES and never asked
where a comment may sit inside a declaration. That axis is covered now:
around the key and colon, before the key, the line-comment form, the
singular form, inside the array before, between and after elements, after
a quoted key's closing quote, two consecutive comments, and comments
carrying decoy syntax or an apostrophe. A comment-only array stays
parsed-empty, which is right — it is still a valid empty array.

The shorthand terminator cases stay unreadable, so the guards pin round
8's behavior rather than quietly widening it.

These are not vacuous: replayed against c2342b0, 7 fail — 5 as absent,
the CSS-stripping outcome, and 2 by falling back instead of reading the
value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_05a083d6-4bdd-4904-bec9-50a9ed2a915b)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21d92d4896

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/index.ts
A decorator may spell a key with a unicode escape. `styleUrls` is
`styleUrls`, and the Rust extractor reads it. The text scanner did not,
so it reported the field absent and the endpoint served the wrong CSS.

`readIdentifierKey` now decodes `\uHHHH` and `\u{...}` in a key position,
the only two escapes JS permits in an identifier. It validates each
decoded character against the identifier start/continue classes for its
position. A malformed or illegal escape marks the key unreadable, not
absent, so the endpoint falls back instead of guessing.

The same decoding now applies to quoted keys, which were unreadable
before. `'styleUrls'` matches the field exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_56761ed3-a2e7-494b-bca0-69ed88fd7d71)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20c13a0c58

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/utils/decorator-fields.ts Outdated
`styleUrl: './a.css' + SUFFIX` opens with a literal the value does not
denote. The scan returned that leading piece and called the field
readable, so the endpoint served a stylesheet the compiled component
never had — and, being a confident answer, skipped the fallback that
exists for values the scan cannot resolve.

A literal now counts as the value only when the property ends at its
closing delimiter. `endsPropertyValue` skips whitespace and comments,
then requires `,`, `}` or `)`. Anything else marks the field unreadable.

Measured against the Rust extractor, which compiles no styles for any of
these: concatenation, a method call, `as`, `as const`, `!`, `satisfies`.
The guard is shared with `template` and `templateUrl`, which moves those
from wrong to correct the same way. `stripComponentMetadata` strips one
less shape, which only downgrades HMR to a full reload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_07768b01-0017-4acb-865f-28fd37662439)

@Brooooooklyn

Copy link
Copy Markdown
Member Author

Decision: closing the scanner-form loop here

Five consecutive rounds each fixed one more form the text scan read differently from the Rust extractor — const values, quoted keys, escaped literals, shorthand, escaped identifier keys, trailing expressions. Every one was found by review rather than by us, and each fix revealed an adjacent form. That is not converging, so I am settling the remaining question rather than running a seventh round.

The governing rule

The scan may report "unreadable" freely, but must never assert a literal it cannot verify. A wrong literal makes HMR apply CSS the compiled app does not have; an "unreadable" only costs a fallback.

This supersedes a call I made in round 8, and I want that on the record rather than buried. Round 8 found three forms where Rust is the side that fails — a numeric key, an as cast, duplicate keys — and I concluded they were better left alone, since converging toward a Rust parsing gap would mean deliberately dropping styles. Round 10 changed direction for the expression forms and I am keeping that, because the rule above is the more defensible one: it is the same principle that made a spread stay absent in round 6, where serving nothing matches what the compiler produces.

Duplicate keys stay untouched. That input is a TypeScript error, so neither side is meaningfully right.

The residual, measured

For a value neither side can resolve, the fallback serves the file-level union. In a multi-component file that union is the siblings' stylesheets:

@Component({ styleUrls: ['./own.css' + S] })   ← E
@Component({ styleUrls: ['./sibling.css'] })   ← Sib

rust union → ["./sibling.css"]        E's own entry is dropped by the compiler too
scan       → unreadable → fallback  → E is served ./sibling.css

So E gets the sibling's CSS. In a single-component file the union is empty and E is correctly served nothing.

This is the known #456 limitation, not new: the fallback cannot distinguish "Rust folded a constant, so the union holds the right answer" from "Rust failed too, so the union holds only other components". Both arrive as unreadable. #456 removes the fallback entirely by exposing per-class resolved resources from the extractor, which is the structural end of this whole class of bug — including every form fixed in the last five rounds.

I am not encoding a guess about which of the two the value is. That distinction belongs in the extractor, not in a text scan.

State

378 unit tests pass. CI was fully green on 21d92d4 across Linux, macOS and Windows including the E2E build; the newer commits are building. All 12 review threads are resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7f483d08d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread napi/angular-compiler/vite-plugin/index.ts
`styleUrls: [...SHARED, './own.css']` marked the array incomplete, so
`extractClassStylesFor` returned null and the endpoint fell back to the
file-level union — handing the class every sibling stylesheet in the
file.

The deciding property is structural. `extract_string_array` asks each
element for `as_expression()`, and `ArrayExpressionElement::is_expression()`
enumerates neither `SpreadElement` nor `Elision`. A spread is therefore
dropped before any value resolution, for every possible program, with the
const table never consulted. An element that IS an expression reaches the
resolver, which folds constants this text scan cannot read — so those
still mark the array unknown, and the guard added in 7f483d0 stands.

The spread is skipped whole via `advanceOneToken`, so a literal nested
inside it is never mistaken for one of this class's styles; an unclosed
delimiter still reports unknown rather than guessing. This also settles a
contradiction inside the file: `hasUnreadableKey` already treats a
decorator-level `...BASE` as droppable, on exactly this reasoning.

Measured against the Rust extractor across 12 array shapes: every answer
the scan now asserts matches, and `[S, './own.css']` still bails because
Rust folds the constant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01665HouXD8imyMphWe1k7Dx
@cursor

cursor Bot commented Aug 25, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_dc69a6d9-ecd8-4bde-9c9e-95fc75aee1ae)

@Brooooooklyn
Brooooooklyn merged commit 088e2ab into main Aug 25, 2026
11 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/issue-451-per-class-endpoint-styles branch August 25, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@ng/component endpoint serves the file-level styleUrls to every class in a multi-component file

1 participant