Skip to content

Commit e02ca80

Browse files
serpentbladeclaude
andcommitted
fix(22): ANGULAR EXCEPTION — no .d.rozie.ts sidecars next to .rozie sources (ngtsc AOT regression)
Fixes the red main from 053b2ee: Angular 19+21 Matrix (all 17 analogjs e2e × 2 legs) and VR Matrix (all 37 [angular] cells) failed at runtime with "JIT compiler unavailable". ROOT CAUSE: TypeScript module resolution prefers the arbitrary-extension declaration `X.d.rozie.ts` over extension-appending to the disk-cache implementation `X.rozie.ts` when resolving `./X.rozie` — regardless of `allowArbitraryExtensions` (the flag only gates the tsc error, not the resolution). ngtsc (inside @analogjs/vite-plugin-angular) uses that same resolution to validate standalone `imports: [...]` entries; a type-only `declare class` has no ɵcmp metadata, so ngtsc silently skips AOT for every class importing a `.rozie` module → raw decorators in the bundle → JIT fallback at bootstrap. The 22-01 spike validated sidecar/disk-cache coexistence under plain tsc only and missed the ngtsc path; the 22-06 executor then mis-attributed the failing e2e to a stale "known limitation" comment in vite.config.ts (now corrected). Fix (surgical, reverses the Plan-04 LOCKED angular-sidecar decision): - unplugin emitSidecar: target 'angular' never writes a sidecar next to a .rozie source — it deletes any stale one (heal) and bails. renderSidecar keeps angular support for the CLI path (output-tree sidecars, no hazard). - unplugin buildStart: sidecar walk scopes to the Vite resolved root (captured in configResolved) instead of process.cwd(). Fixes the unplugin test suite littering the whole repo with lit/vue-flavored sidecars when run from the repo root — which nondeterministically broke the solid demo typecheck (cross-root ../../../../X.rozie imports resolved to whatever target's litter landed last) and would re-break Angular AOT. - bundlers-smoke.test.ts: cwd isolation (non-Vite adapters keep the process.cwd() convention, so real rollup/esbuild/webpack builds in tests must chdir into their tmp root). - angular-analogjs demo: drop allowArbitraryExtensions, restore the wildcard shim as the fresh-checkout fallback, delete the sidecar-dependent typed-import probe. The disk-cache .rozie.ts class IS the Angular typed import surface (props = typed signal inputs, $expose = typed public methods via @ViewChild). Known limitation: no named XProps export for Angular until an ngtsc-valid sidecar (declaring static ɵcmp: ɵɵComponentDeclaration<...>) exists. - check-sidecar-staleness.mjs: enforces the inverse invariant — any .d.rozie.ts found in an angular-exclusive tree is a hard failure. - docs (install.md + features.md): "The Angular exception" sections. Gates (all cold/forced): turbo build 43/43, typecheck+lint 79/79, test 50/50, unplugin 149/149 (zero litter), angular e2e 17/17, react e2e 22/22, integration AOT proof, staleness gates ×3, VR Docker matrix 287 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 053b2ee commit e02ca80

13 files changed

Lines changed: 293 additions & 110 deletions

File tree

.github/workflows/angular-matrix.yml

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,18 @@ jobs:
6767
# even on Angular-only CI runs.
6868
#
6969
# Phase 22 / REQ-6: `turbo run build` includes `angular-analogjs-demo#build`
70-
# (`vite build`), whose @rozie/unplugin `buildStart` hook emits the
71-
# per-module `<Name>.d.rozie.ts` sidecars (Angular sidecars coexist with the
72-
# disk-cache `.rozie.ts` — Plan-04 LOCKED). Sidecars are gitignored (REQ-7),
73-
# absent on a fresh checkout — this step generates them BEFORE the
74-
# "Typecheck demo" step, closing the cold-checkout race. The demo tsconfig
75-
# sets `allowArbitraryExtensions: true` so tsc honors the sidecar over the
76-
# disk-cache on the direct-import path (SPIKE-FINDINGS).
77-
- name: Build all packages (generates .d.rozie.ts sidecars before typecheck — REQ-6)
70+
# (`vite build`), whose @rozie/unplugin configResolved prebuild writes the
71+
# disk-cache `<Name>.rozie.ts` next to each `.rozie` source (gitignored,
72+
# absent on a fresh checkout) — this step generates them BEFORE the
73+
# "Typecheck demo" step, closing the cold-checkout race.
74+
#
75+
# ANGULAR EXCEPTION (post-Phase-22 regression fix, 2026-06-02): the Angular
76+
# target gets NO `.d.rozie.ts` sidecars — a type-only declaration next to a
77+
# `.rozie` source shadows the disk-cache `.rozie.ts` in ngtsc's resolution
78+
# and silently kills AOT ("JIT compiler unavailable"). The disk-cache class
79+
# IS the Angular typed-import surface. See
80+
# packages/unplugin/src/emitSidecar.ts ANGULAR EXCEPTION.
81+
- name: Build all packages (generates .rozie.ts disk-cache before typecheck — REQ-6)
7882
run: pnpm turbo run build
7983

8084
- name: Run unit tests (core + targets + runtime + unplugin)
@@ -142,12 +146,16 @@ jobs:
142146

143147
# WR-03: a `.rozie` with no sidecar after the build means buildStart
144148
# silently failed to emit it; --require-complete makes that a hard failure.
145-
- name: Verify every demo .rozie has a fresh sidecar (WR-03)
149+
# (Covers the non-Angular demos built by the turbo step above. For Angular
150+
# the gate enforces the INVERSE invariant — no sidecar may exist in any
151+
# tree an Angular build consumes — see ANGULAR EXCEPTION in the script.)
152+
- name: Verify demo sidecars (WR-03 + ANGULAR EXCEPTION)
146153
run: node scripts/check-sidecar-staleness.mjs --require-complete
147154

148-
# REQ-6: the .d.rozie.ts sidecars this consumes were generated by the
149-
# "Build all packages" step above (buildStart hook) — never reorder before it.
150-
- name: Typecheck demo (sidecars generated above — REQ-6)
155+
# REQ-6: the .rozie.ts disk-cache this consumes was generated by the
156+
# "Build all packages" step above (configResolved prebuild) — never reorder
157+
# before it. (No sidecars for Angular — ANGULAR EXCEPTION.)
158+
- name: Typecheck demo (disk-cache generated above — REQ-6)
151159
run: pnpm --filter angular-analogjs-demo run typecheck
152160

153161
- name: Build demo

docs/guide/features.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,17 +417,20 @@ import Dropdown, { type DropdownHandle } from './Dropdown.rozie';
417417

418418
`DropdownHandle` is the synthesized interface for the methods the component exposed (see [Getting the handle from the consumer side](#getting-the-handle-from-the-consumer-side) above).
419419

420-
One tsconfig flag governs whether `tsc` honors the sidecar. **Vue's `vue-tsc` honors it under the `moduleResolution: bundler` default; every other target's `tsc` requires `"allowArbitraryExtensions": true`** explicitly:
420+
One tsconfig flag governs whether `tsc` honors the sidecar. **Vue's `vue-tsc` honors it under the `moduleResolution: bundler` default; the other sidecar targets' `tsc` requires `"allowArbitraryExtensions": true`** explicitly:
421421

422422
| Target | Typecheck tool | `allowArbitraryExtensions` |
423423
| --- | --- | --- |
424424
| Vue | `vue-tsc` | not needed (bundler default) |
425-
| React / Solid / Lit / Angular | `tsc` | **required** |
425+
| React / Solid / Lit | `tsc` | **required** |
426426
| Svelte | `tsc` + `svelte-check` | **required** |
427+
| Angular | `tsc` | **N/A — no sidecars** (the disk-cache `.rozie.ts` class is the typed surface) |
427428

428-
Without the flag (on the five non-Vue targets), `tsc` either emits `TS6263` or silently falls back to a broad `declare module '*.rozie'` wildcard that types every prop as `unknown` — a silent type-lie. The full per-framework setup, the wildcard-shim migration, and the gitignore policy live in [Install → Typed `.rozie` imports](/guide/install#typed-rozie-imports-per-framework-setup).
429+
Without the flag (on the four non-Vue sidecar targets), `tsc` either emits `TS6263` or silently falls back to a broad `declare module '*.rozie'` wildcard that types every prop as `unknown` — a silent type-lie. The full per-framework setup, the wildcard-shim migration, the gitignore policy, and the Angular exception live in [Install → Typed `.rozie` imports](/guide/install#typed-rozie-imports-per-framework-setup).
429430

430-
Each consumer demo in the repo is the byte-tested proof of its framework's setup: `examples/consumers/react-vite` (React, flag set, wildcard deleted), `vue-vite` (Vue, no flag, `@deprecated` cross-root fallback kept), `svelte-vite` / `lit-vanilla-demo` / `angular-analogjs` (flag set, wildcard deleted), and `solid-vite` (flag set, `@deprecated` cross-root fallback kept). Each ships a `typed-import.probe` that asserts a correct prop usage compiles and a wrong-typed prop is a genuine error.
431+
**Angular is the exception**: a `.d.rozie.ts` next to a `.rozie` source would shadow the AOT-compiled `.rozie.ts` disk-cache in ngtsc's module resolution and silently break AOT (runtime `JIT compiler unavailable`), so Rozie never writes one there. Angular imports are typed by the disk-cache class itself — props are typed signal inputs, and `$expose` methods are typed public class methods reachable via `@ViewChild`. See [Install → The Angular exception](/guide/install#the-angular-exception-no-sidecars).
432+
433+
Each consumer demo in the repo is the byte-tested proof of its framework's setup: `examples/consumers/react-vite` (React, flag set, wildcard deleted), `vue-vite` (Vue, no flag, `@deprecated` cross-root fallback kept), `svelte-vite` / `lit-vanilla-demo` (flag set, wildcard deleted), `solid-vite` (flag set, `@deprecated` cross-root fallback kept), and `angular-analogjs` (no sidecars — disk-cache types + wildcard fresh-checkout fallback). The sidecar demos each ship a `typed-import.probe` that asserts a correct prop usage compiles and a wrong-typed prop is a genuine error.
431434

432435
## `$onMount` returning a teardown
433436

docs/guide/install.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ TypeScript only resolves a `.d.<ext>.ts` sidecar for a non-standard extension (l
5656
| Svelte | `tsc` + `svelte-check` | **Required** |
5757
| Solid | `tsc` | **Required** |
5858
| Lit | `tsc` | **Required** |
59-
| Angular | `tsc` | **Required** |
59+
| Angular | `tsc` | **N/A — Angular does not use sidecars** (see [The Angular exception](#the-angular-exception-no-sidecars) below) |
6060

61-
So for every target except Vue, add the flag to your `tsconfig.json`:
61+
So for every target except Vue and Angular, add the flag to your `tsconfig.json`:
6262

6363
```json
6464
{
@@ -69,7 +69,21 @@ So for every target except Vue, add the flag to your `tsconfig.json`:
6969
}
7070
```
7171

72-
Plain `tsc` (and `vue-tsc`) 5.x does **not** auto-enable `allowArbitraryExtensions` under `moduleResolution: bundler` — Vue is the only target whose toolchain honors the sidecar without the flag. Without the flag on the other five, `tsc` reports `TS6263: … but '--allowArbitraryExtensions' is not set` (or silently falls back to a broad `*.rozie` wildcard that types every prop as `unknown` — see migration below).
72+
Plain `tsc` (and `vue-tsc`) 5.x does **not** auto-enable `allowArbitraryExtensions` under `moduleResolution: bundler` — Vue is the only target whose toolchain honors the sidecar without the flag. Without the flag on the other sidecar targets, `tsc` reports `TS6263: … but '--allowArbitraryExtensions' is not set` (or silently falls back to a broad `*.rozie` wildcard that types every prop as `unknown` — see migration below).
73+
74+
### The Angular exception: no sidecars
75+
76+
Angular is the one target where Rozie **never** writes a `.d.rozie.ts` sidecar next to a `.rozie` source — and you must not add one by hand.
77+
78+
Angular's build runs a real TypeScript program *inside* the bundler: ngtsc (via `@analogjs/vite-plugin-angular`) resolves every `.rozie` import to validate it as a standalone-component `imports: [...]` entry. TypeScript's module resolution prefers an arbitrary-extension declaration file (`Counter.d.rozie.ts`) over the compiled implementation (`Counter.rozie.ts`) — regardless of `allowArbitraryExtensions`. A type-only `declare class` carries no `ɵcmp` metadata, so ngtsc silently **skips AOT compilation** for every class that imports a `.rozie` module, and the app throws `JIT compiler unavailable` at runtime.
79+
80+
Instead, the Angular target's typed import surface is the **disk-cache** `<Name>.rozie.ts` that the unplugin writes next to each `.rozie` source (the same file ngtsc AOT-compiles). It is a real, fully-typed standalone component class:
81+
82+
- `import Counter from './Counter.rozie'` resolves to the disk-cache class — props are typed signal inputs/models.
83+
- `$expose` methods are **public class methods**, so an `@ViewChild(Counter) counter!: Counter` handle is fully typed with no named handle-type import needed.
84+
- Keep a `declare module '*.rozie'` wildcard shim as a fresh-checkout fallback (the disk-cache is generated by the first build); once the disk-cache exists, file resolution wins over the wildcard.
85+
86+
The trade-off: Angular consumers have no named `<Name>Props` type export to import (the other five targets get one from the sidecar).
7387

7488
### Named handle imports
7589

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
1-
// Phase 22 (REQ-1/R3/R4/R9): this demo now consumes the per-module
2-
// `<Name>.d.rozie.ts` sidecars emitted by @rozie/unplugin's buildStart hook —
3-
// `import Foo, { type FooProps } from './Foo.rozie'` gets REAL Angular types
4-
// (a typed `declare class Foo` + named `FooProps` + `$expose` handle methods).
1+
// TypeScript shim for `.rozie` imports — informs tsc / Angular's strict
2+
// templates that a `.rozie` import resolves to an Angular standalone
3+
// component class.
54
//
6-
// The former broad `declare module '*.rozie'` wildcard is DEMOTED (deleted for
7-
// this demo, which migrates 100% cleanly — every `.rozie` import here is
8-
// in-`src` and has a sidecar). Per SPIKE-FINDINGS, Angular (`tsc`, typescript
9-
// ~5.9.3) honors the sidecar ONLY with `allowArbitraryExtensions: true` (set in
10-
// tsconfig.json); WITHOUT the flag the wildcard SHADOWS the sidecar (TS2614).
5+
// ANGULAR TYPED-IMPORT POSTURE (post-Phase-22 regression fix, 2026-06-02):
6+
// Angular does NOT use `.d.rozie.ts` sidecars. The typed import surface is the
7+
// disk-cache `<Name>.rozie.ts` that @rozie/unplugin's configResolved prebuild
8+
// writes next to each `.rozie` source — a real, fully-typed standalone
9+
// component class that both tsc AND ngtsc resolve via standard
10+
// extension-appending (`./Counter.rozie` → `Counter.rozie.ts`).
1111
//
12-
// CROSS-ROZIE COMPOSITION (Card → CardHeader, ModalConsumer → Modal/WrapperModal):
13-
// the unplugin's prebuild emits a `<Name>.ts` re-export shim that now points at
14-
// the disk-cache `<Name>.rozie.ts` via its `.rozie.js` specifier — NOT the bare
15-
// `./<Name>.rozie` — so the `.d.rozie.ts` sidecar does NOT shadow the runtime
16-
// VALUE class the `imports: [...]` metadata needs (this closes the Plan-05
17-
// known-red TS2614 entry condition). The sidecar adds the named Props/handle
18-
// type exports the disk-cache lacks; the disk-cache provides the value class.
19-
// Do NOT re-add a broad wildcard — it reintroduces the shadowing.
20-
export {};
12+
// Why no sidecar: TS module resolution prefers an arbitrary-extension
13+
// declaration file (`Counter.d.rozie.ts`) over the appended-extension
14+
// implementation (`Counter.rozie.ts`) — regardless of
15+
// `allowArbitraryExtensions`. ngtsc (inside @analogjs/vite-plugin-angular) uses
16+
// that same resolution to validate standalone `imports: [...]` entries; a
17+
// type-only `declare class` has no ɵcmp metadata, so ngtsc silently skips AOT
18+
// for every class importing a `.rozie` module → runtime "JIT compiler
19+
// unavailable" (the 2026-06-02 Angular + VR matrix regression). See
20+
// packages/unplugin/src/emitSidecar.ts ANGULAR EXCEPTION.
21+
//
22+
// This wildcard is the FRESH-CHECKOUT FALLBACK only: before the demo's first
23+
// build (no disk-cache on disk yet; sidecars are never written for Angular),
24+
// `tsc --noEmit` still needs `.rozie` imports to resolve. Once the disk-cache
25+
// exists, file resolution wins over the ambient wildcard and imports get the
26+
// real class types.
27+
//
28+
// Known limitation (accepted): unlike the other 5 targets, Angular consumers
29+
// have no named `<Name>Props` type export to import. A future ngtsc-valid
30+
// sidecar (declaring `static ɵcmp: ɵɵComponentDeclaration<...>` the way
31+
// compiled Angular libraries do) can restore it.
32+
declare module '*.rozie' {
33+
import type { Type } from '@angular/core';
34+
const component: Type<unknown>;
35+
export default component;
36+
}

examples/consumers/angular-analogjs/src/typed-import.probe.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

examples/consumers/angular-analogjs/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
"esModuleInterop": true,
1111
"skipLibCheck": true,
1212
"lib": ["ES2022", "DOM"],
13-
"types": ["vite/client", "node"],
14-
"allowArbitraryExtensions": true
13+
"types": ["vite/client", "node"]
1514
},
1615
"include": ["src/**/*.ts"]
1716
}

examples/consumers/angular-analogjs/vite.config.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ import Rozie from '@rozie/unplugin/vite';
1515
// Vite 6+ floor: @analogjs/vite-plugin-angular@2.5.0 peerDeps require
1616
// vite ^6 || ^7 || ^8 (RESEARCH.md OQ6 RESOLVED).
1717
//
18-
// Known limitation (Plan 05-04b deviation): AppComponent.ts AOT-compilation
19-
// in this monorepo's pnpm subgraph hits an analogjs/Angular interaction
20-
// where NgCompiler.fromTicket fails with "Cannot read properties of
21-
// undefined (reading 'flags')" during TS Program initialization. The .rozie
22-
// virtual ids ARE successfully AOT-compiled (5 ɵcmp markers in the bundle),
23-
// but the consumer AppComponent.ts is not, leading to "JIT compiler
24-
// unavailable" at runtime. Tracked as deferred — see SUMMARY.md.
18+
// HISTORICAL NOTE (stale "known limitation" removed 2026-06-02): an old
19+
// Plan 05-04b comment here described AppComponent.ts failing AOT with
20+
// "JIT compiler unavailable" as a deferred limitation. That limitation was
21+
// FIXED long ago (the May 2026 dual-TS-version pin, see angular-matrix.yml) —
22+
// the full app AOT-compiles and all 17 e2e tests pass. The stale comment
23+
// misled the Phase 22 executor into dismissing a REAL regression (type-only
24+
// .d.rozie.ts sidecars shadowing the disk-cache in ngtsc resolution — see
25+
// packages/unplugin/src/emitSidecar.ts ANGULAR EXCEPTION). If this demo ever
26+
// throws "JIT compiler unavailable" again, it is a real, new regression:
27+
// check for stray *.d.rozie.ts files next to the .rozie sources first.
2528
export default defineConfig({
2629
plugins: [
2730
Rozie({ target: 'angular' }),

packages/targets/angular/src/emit/emitTypes.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,30 @@
2929
* }
3030
* export default Counter;
3131
*
32-
* ── SPIKE-FINDINGS Angular disk-cache verdict (BINDING for Plan 05) ──────────
32+
* ── SPIKE-FINDINGS Angular verdict — REVISED 2026-06-02 (regression fix) ─────
3333
* Angular is the one target that ALSO writes a compiled `<Name>.rozie.ts`
3434
* disk-cache (unplugin's `emitRozieTsToDisk`). The Wave-0 spike (22-01,
35-
* SPIKE-FINDINGS.md) empirically proved the sidecar `<Name>.d.rozie.ts` and the
36-
* disk-cache `<Name>.rozie.ts` COEXIST under `include: src/**\/*.ts` with ZERO
37-
* duplicate-identifier / module-resolution ambiguity — they are DISTINCT
38-
* modules (the sidecar declares the `./Foo.rozie` specifier; the disk-cache is
39-
* the separate `./Foo.rozie.ts` / `./Foo` module). The sidecar is NOT redundant
40-
* with the disk-cache: the disk-cache exposes only the DEFAULT-export class
41-
* type, while the sidecar adds the named `<Name>Props` / handle exports.
35+
* SPIKE-FINDINGS.md) claimed the sidecar `<Name>.d.rozie.ts` and the disk-cache
36+
* `<Name>.rozie.ts` coexist with "zero module-resolution ambiguity" — that
37+
* verdict was validated under PLAIN tsc only and is WRONG for ngtsc:
4238
*
43-
* ⇒ DECISION (recorded for Plan 05): Wave-3 WRITES the Angular sidecar to disk
44-
* like every other target (the demo additionally needs
45-
* `allowArbitraryExtensions: true` in its tsconfig, per SPIKE-FINDINGS); the
46-
* existing disk-cache `.rozie.ts` is KEPT (complementary, non-conflicting).
39+
* TS module resolution prefers the arbitrary-extension declaration
40+
* `<Name>.d.rozie.ts` over extension-appending to `<Name>.rozie.ts` when
41+
* resolving `./<Name>.rozie` (regardless of `allowArbitraryExtensions`). ngtsc
42+
* (inside @analogjs/vite-plugin-angular) uses that same resolution to validate
43+
* standalone `imports: [...]` entries; a type-only `declare class` carries no
44+
* ɵcmp metadata, so ngtsc silently skips AOT for every class importing a
45+
* `.rozie` module → runtime "JIT compiler unavailable" (the 2026-06-02 Angular
46+
* matrix + VR matrix regression).
47+
*
48+
* ⇒ REVISED DECISION: no `.d.rozie.ts` is ever placed next to a `.rozie` source
49+
* for the Angular target (the unplugin heals/deletes them — see
50+
* packages/unplugin/src/emitSidecar.ts ANGULAR EXCEPTION). The disk-cache
51+
* `.rozie.ts` class IS the Angular typed-import surface. This renderer is kept
52+
* for the CLI path only (sidecars next to compiled OUTPUT files, where no
53+
* `.rozie` sibling exists). A future ngtsc-valid sidecar must declare
54+
* `static ɵcmp: ɵɵComponentDeclaration<...>` the way compiled Angular libraries
55+
* do.
4756
*
4857
* Slot idiom (Angular): slots are projected via `<ng-template>`; the slot-props
4958
* surface is not part of the typed-class consumer contract, so the shared props

0 commit comments

Comments
 (0)