S264 Aot Testing Handoff

Documentation for Sessions

S264: AOT Compilation Testing Handoff

For: Co-work agent (testing + hardening) From: S264.1-264.2 (architecture + implementation) Goal: Test the AOT compilation pipeline, fix gaps, enable the switch from JIT to AOT Branch: session-261-266 Key commits: 93ea222e7 (Phase 1-3 implementation), c1b4d1b0a (wiring)


What Was Built and Why

The Problem We Solved

The Live Preview in Vitto Studio renders generated Angular apps in a blob URL iframe. It was broken because:

  1. Angular packages were loaded from esm.sh CDN
  2. esm.sh re-bundles packages, creating separate copies of @angular/core
  3. @angular/compiler registered its JIT hooks with its OWN copy of core
  4. The app used a DIFFERENT copy of core → compiler registration was invisible → crash

What We Built

Three layers, each building on the previous:

Layer 1: Self-Hosted Angular Runtime (ACTIVE — fixes the bug)
   ↓ Angular packages at /angular-runtime/*.mjs, shared singletons

Layer 2: Compilation Strategy Pattern (WIRED — pluggable JIT/AOT)
   ↓ COMPILATION_STRATEGY injection token, JIT default

Layer 3: Lightweight Template Compiler (WIRED — needs testing)
   ↓ Parses templates → generates Ivy instructions → no runtime compiler needed

Layer 1 is production-ready and running. Layer 2 is wired and working (JIT strategy active). Layer 3 is the part YOU need to test.


How the AOT Pipeline Works

Normal Flow (JIT — current default)

1. LLM generates Angular code with @Component decorators
2. BundlerFilesystemService enriches files (SCSS compile, inline templates, etc.)
3. rspack compiles TypeScript → JavaScript (decorators preserved)
4. PreviewServerService injects prerequisite script that loads:
   - zone.js
   - @angular/compiler (registers JIT hooks)
   - @angular/platform-browser-dynamic
5. Browser parses main.js → encounters @Component decorators →
   JIT compiler processes them into Ivy instructions at runtime
6. Angular bootstraps and renders

AOT Flow (what we're testing)

1. LLM generates Angular code with @Component decorators
2. BundlerFilesystemService enriches files (same as JIT)
3. *** NEW: AotCompilationStrategy.transformSources() runs ***
   - DecoratorTransformer finds @Component decorators
   - Extracts template string from decorator metadata
   - TemplateParser parses the template HTML → AST
   - IvyCodegen converts AST → Ivy instruction function
   - Replaces @Component decorator with static ɵcmp + ɵfac fields
4. rspack compiles TypeScript → JavaScript (decorators ALREADY compiled)
5. PreviewServerService injects prerequisite script that loads:
   - zone.js ONLY (no compiler needed!)
6. Browser parses main.js → Ivy instructions already present →
   Angular bootstraps and renders WITHOUT the compiler (~1MB saved)

The Key Difference

In JIT, the BROWSER compiles @Component({ template: '<div>{{name}}</div>' }) at runtime. In AOT, OUR CODE compiles it BEFORE the browser sees it. The browser gets pre-compiled Ivy instructions.


Key Files You'll Work With

The Template Compiler (libs/libs-structural/platform-preview/src/lib/compiler/)

File What It Does Lines
models/template-ast.ts TypeScript types for the AST nodes (ElementNode, TextNode, IfBlockNode, etc.) ~175
template-parser.ts Parses Angular template HTML string → array of TemplateNode AST nodes ~370
ivy-codegen.ts Takes AST nodes → generates Ivy instruction code (CREATE + UPDATE blocks) ~580
decorator-transformer.ts Takes a .ts file string → finds @Component → calls parser+codegen → replaces decorator with Ivy fields ~260
index.ts Barrel export ~35

The Strategy (libs/libs-structural/platform-preview/src/lib/strategies/)

File What It Does
compilation-strategy.ts Interface definition
jit-compilation.strategy.ts JIT: loads compiler at runtime (current default)
aot-compilation.strategy.ts AOT: calls template compiler, no runtime compiler

Integration Points (services that USE the strategy)

File How It Uses Strategy
bundler.service.ts strategy.getExternals() for rspack config, strategy.transformSources() for AOT pre-compilation
preview-server.service.ts strategy.getPrerequisites() for which modules to load before main.js
bundler-filesystem.service.ts strategy.getMainTsInjection() for what imports to put in synthesized main.ts

How to Switch to AOT (for testing)

In bundler.service.ts, the injection token has a factory default:

export const COMPILATION_STRATEGY = new InjectionToken<CompilationStrategy>(
  'CompilationStrategy',
  { factory: () => new JitCompilationStrategy() },  // ← change this to test AOT
);

To test AOT: Change the factory to () => new AotCompilationStrategy(). To revert: Change it back to JitCompilationStrategy.


Testing Plan

Phase A: Unit Test the Template Parser

Goal: Verify the parser correctly converts template HTML → AST nodes.

Location for tests: Create libs/libs-structural/platform-preview/src/lib/compiler/template-parser.spec.ts

How to run: npx jest --config libs/libs-structural/platform-preview/jest.config.ts template-parser (or check the project's test setup)

Test cases (implement each as a separate it() block):

A1. Simple element
    Input:  <div>hello</div>
    Expect: 1 ElementNode, tag='div', 1 child TextNode with value='hello'

A2. Self-closing element
    Input:  <input type="text" />
    Expect: 1 ElementNode, tag='input', selfClosing=true, 1 AttrNode name='type' value='text'

A3. Property binding
    Input:  <div [hidden]="isHidden">text</div>
    Expect: 1 ElementNode with 1 BindingNode kind='property' name='hidden' expression='isHidden'

A4. Event binding
    Input:  <button (click)="onClick()">Go</button>
    Expect: 1 ElementNode with 1 EventNode name='click' handler='onClick()'

A5. Class binding
    Input:  <div [class.active]="isActive">x</div>
    Expect: 1 BindingNode kind='class' name='active' expression='isActive'

A6. Style binding
    Input:  <div [style.color]="textColor">x</div>
    Expect: 1 BindingNode kind='style' name='color' expression='textColor'

A7. Text interpolation
    Input:  <span>Hello {{name}}, you have {{count}} items</span>
    Expect: 1 ElementNode, child InterpolationNode with 5 parts:
            static('Hello '), expression('name'), static(', you have '),
            expression('count'), static(' items')

A8. @if block
    Input:  @if (isVisible) { <div>shown</div> } @else { <span>hidden</span> }
    Expect: 1 IfBlockNode, condition='isVisible',
            children=[ElementNode tag='div'],
            elseChildren=[ElementNode tag='span']

A9. @for block
    Input:  @for (item of items; track item.id) { <li>{{item.name}}</li> } @empty { <p>No items</p> }
    Expect: 1 ForBlockNode, itemVar='item', collection='items', trackExpr='item.id',
            children=[ElementNode tag='li' with InterpolationNode],
            emptyChildren=[ElementNode tag='p']

A10. @switch block
     Input:  @switch (status) { @case ('active') { <span>Active</span> } @default { <span>Unknown</span> } }
     Expect: 1 SwitchBlockNode, expression='status', 2 cases

A11. Nested elements
     Input:  <div class="wrapper"><h1>Title</h1><p>Body</p></div>
     Expect: 1 ElementNode with 2 children (h1, p), each with text children

A12. Two-way binding
     Input:  <input [(ngModel)]="name" />
     Expect: 1 BindingNode (property 'ngModel') + 1 EventNode ('ngModelChange')

A13. Multiple attributes and bindings
     Input:  <div class="box" id="main" [style.width]="w" (mouseenter)="onHover()">x</div>
     Expect: 2 AttrNodes + 1 BindingNode + 1 EventNode

A14. ng-container
     Input:  <ng-container><span>inside</span></ng-container>
     Expect: 1 ContainerNode with 1 child ElementNode

A15. Empty template
     Input:  (empty string)
     Expect: empty array []

Phase B: Unit Test the Ivy Codegen

Goal: Verify the codegen produces correct Ivy instruction strings from AST.

Location: Create libs/libs-structural/platform-preview/src/lib/compiler/ivy-codegen.spec.ts

Test cases:

B1. Simple text element
    AST:    ElementNode('div', children=[TextNode('hello')])
    Expect: CREATE block has ɵɵelementStart(0, "div") + ɵɵtext(1, "hello") + ɵɵelementEnd()
            decls=2, vars=0

B2. Interpolation
    AST:    ElementNode('span', children=[InterpolationNode([static('Hi '), expr('name')])])
    Expect: CREATE has ɵɵelementStart(0, "span") + ɵɵtext(1) + ɵɵelementEnd()
            UPDATE has ɵɵtextInterpolate1("Hi ", ctx.name, "")
            decls=2, vars=1

B3. Property binding
    AST:    ElementNode('div', bindings=[BindingNode(property, 'hidden', 'isHidden')])
    Expect: UPDATE has ɵɵproperty("hidden", ctx.isHidden)
            vars=1

B4. Event listener
    AST:    ElementNode('button', events=[EventNode('click', 'onClick()')])
    Expect: CREATE has ɵɵlistener("click", function($event) { return ctx.onClick(); })

B5. Class binding
    AST:    ElementNode('div', bindings=[BindingNode(class, 'active', 'isActive')])
    Expect: UPDATE has ɵɵclassProp("active", ctx.isActive)

B6. Self-closing element
    AST:    ElementNode('input', selfClosing=true)
    Expect: CREATE has ɵɵelement(0, "input") (NOT ɵɵelementStart/End)

B7. Static attributes
    AST:    ElementNode('div', attrs=[AttrNode('id', 'main')])
    Expect: consts array has ["id", "main"], elementStart references consts index

B8. Multiple expressions in interpolation
    AST:    InterpolationNode([static(''), expr('a'), static(' + '), expr('b'), static(' = '), expr('c'), static('')])
    Expect: Uses ɵɵtextInterpolate3 (3 expressions)

B9. Nested structure produces correct node indices
    AST:    ElementNode('div', children=[
              ElementNode('h1', children=[TextNode('Title')]),
              ElementNode('p', children=[InterpolationNode([expr('body')])])
            ])
    Expect: Nodes indexed 0(div), 1(h1), 2(Title text), 3(p), 4(body text)
            decls=5

B10. advance() emitted correctly
     AST:    ElementNode('div', bindings=[property('x','a')],
               children=[ElementNode('span', bindings=[property('y','b')])])
     Expect: UPDATE: ɵɵadvance for div, then ɵɵadvance for span (correct delta)

Phase C: Unit Test the Decorator Transformer

Goal: Verify the full pipeline: .ts source → find @Component → parse template → replace with Ivy.

Location: Create libs/libs-structural/platform-preview/src/lib/compiler/decorator-transformer.spec.ts

Test cases:

C1. Simple component transformation
    Input source:
      import { Component } from '@angular/core';
      @Component({
        selector: 'app-hello',
        standalone: true,
        template: `<div>{{message}}</div>`,
      })
      export class HelloComponent {
        message = 'Hello World';
      }
    Expect:
      - @Component decorator REMOVED
      - Static ɵfac field added to class
      - Static ɵcmp field added with template function containing ɵɵelementStart, ɵɵtext, etc.
      - @angular/compiler import REMOVED (if present)
      - Ivy symbol imports added from @angular/core

C2. Component with styles
    Input: @Component({ selector: 'x', template: `<p>hi</p>`, styles: [`p { color: red; }`] })
    Expect: ɵcmp definition includes styles: ["p { color: red; }"]

C3. Component with bindings and events
    Input: @Component({ selector: 'x', standalone: true, template: `<button [disabled]="d" (click)="go()">{{label}}</button>` })
    Expect: Template function has ɵɵproperty, ɵɵlistener, ɵɵtextInterpolate

C4. File with NO @Component passes through unchanged
    Input: export class MyService { getData() { return []; } }
    Expect: Output === Input (no transformation)

C5. File with @Component but NO template returns original (graceful failure)
    Input: @Component({ selector: 'x' }) export class X {}
    Expect: Output === Input (missing template → skip, don't crash)

C6. transformAngularSources() processes multiple files
    Input: { '/a.ts': componentCode, '/b.ts': serviceCode, '/c.ts': anotherComponent }
    Expect: a.ts and c.ts transformed, b.ts unchanged
    Expect: compilerEliminated = true (all components transformed)

C7. transformAngularSources() partial failure
    Input: { '/a.ts': validComponent, '/b.ts': componentWithWeirdTemplate }
    Expect: a.ts transformed, b.ts returned as-is
    Expect: compilerEliminated = false (not all components transformed)

Phase D: Integration Test — JIT vs AOT Comparison

Goal: Run the SAME generated code through both JIT and AOT, verify identical rendering.

How to do this:

  1. Find a real generated module in the codebase. Look in sessions/ or generate one via Vitto Studio.

  2. Or create a test fixture — a minimal but realistic generated component:

// test-fixture.component.ts
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-test-fixture',
  standalone: true,
  imports: [CommonModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="container">
      <h1>{{title}}</h1>
      <p [class.highlight]="isHighlighted">{{description}}</p>

      @if (items().length > 0) {
        <ul>
          @for (item of items(); track item.id) {
            <li [class.selected]="item.id === selectedId()" (click)="select(item.id)">
              {{item.name}} - {{item.status}}
            </li>
          }
        </ul>
      } @else {
        <p class="empty">No items available</p>
      }

      <button [disabled]="!canSubmit()" (click)="onSubmit()">Submit</button>
    </div>
  `,
  styles: [`
    .container { padding: 16px; }
    .highlight { color: var(--ux-primary, #6366f1); }
    .selected { font-weight: bold; }
    .empty { color: var(--ux-text-secondary, #888); }
  `],
})
export class TestFixtureComponent {
  title = 'Test Fixture';
  description = 'Testing AOT compilation';
  isHighlighted = true;
  selectedId = signal(1);
  items = signal([
    { id: 1, name: 'Alpha', status: 'active' },
    { id: 2, name: 'Beta', status: 'pending' },
    { id: 3, name: 'Gamma', status: 'active' },
  ]);
  canSubmit = computed(() => this.selectedId() > 0);

  select(id: number) { this.selectedId.set(id); }
  onSubmit() { console.log('Submitted:', this.selectedId()); }
}
  1. Test procedure:
Step 1: Run with JIT (current default)
   - Open Vitto Studio, load a universe with generated modules
   - Open Live Preview tab
   - Open browser DevTools Console
   - Note: "Angular is running in development mode" should appear
   - Note: The preview should render (data loads, UI shows)
   - Screenshot the rendered output

Step 2: Switch to AOT
   - Edit: libs/libs-structural/platform-preview/src/lib/services/bundler.service.ts
   - Change line ~17: { factory: () => new JitCompilationStrategy() }
                    → { factory: () => new AotCompilationStrategy() }
   - Save, wait for dev server to recompile

Step 3: Run with AOT
   - Reload the Live Preview
   - Open browser DevTools Console
   - Check for:
     ✅ "Angular is running in development mode" (bootstrap succeeded)
     ✅ No red error messages
     ✅ Console shows "[BundlerService] AOT: compiler eliminated from runtime"
     ❌ If you see "Preview Load Error" → AOT transform failed, check console for details
   - Screenshot the rendered output

Step 4: Compare
   - Do both screenshots show the same UI?
   - Do click handlers work? (click items in the list)
   - Does data load? (check for [useCrudList] messages)
   - Are there any console errors that weren't present in JIT?

Step 5: Revert
   - Change the factory back to JitCompilationStrategy
   - Save

Phase E: Hybrid Fallback Implementation

Goal: If AOT fails for some components, automatically load the compiler so JIT can handle them.

What to build:

The AotCompilationStrategy.transformSources() returns { files, compilerEliminated: boolean }. If compilerEliminated is false, the AOT strategy should tell the prerequisites to ALSO load the compiler.

Currently in bundler.service.ts:

if (this.strategy.mode === 'aot') {
  const transformResult = this.strategy.transformSources(enrichedFiles);
  filesToCompile = transformResult.files;
  if (transformResult.compilerEliminated) {
    console.log(`${LOG_PREFIX} AOT: compiler eliminated from runtime`);
  }
}

What's needed: Pass compilerEliminated to PreviewServerService so it can conditionally load the compiler. Options:


Known Gaps to Fix During Testing

Gap 1: Pipe Expressions

The Ivy codegen has basic pipe support but it's simplified. Real Ivy uses ɵɵpipe() + ɵɵpipeBind1() etc.

Test: <span>{{item.date | date:'short'}}</span> Expected behavior: Either correctly generates ɵɵpipe instructions, or falls back gracefully.

Gap 2: @Injectable Not Handled

The decorator transformer only handles @Component. Services with @Injectable aren't transformed.

Impact: Low — services don't have templates, so their @Injectable decorator is handled by Angular's partial compilation format + JIT fallback. But for full AOT, services need ɵɵdefineInjectable generation.

Gap 3: Expression Contextualization Edge Cases

The contextualizeExpr() function prepends ctx. to property accesses. This can break for:

Test: Create a component with these patterns and verify AOT output.

Gap 4: Multi-Component Files

The decorator transformer uses source.match(/@Component\s*\(\s*\{/) which finds only the FIRST @Component in a file. If a file has multiple components, only the first is transformed.

Impact: Low — our LLM generates one component per file. But verify this assumption.


Success Criteria for Flipping the Switch

The AOT switch can happen when ALL of these are true:

  1. ✅ Template parser handles all 15 test cases in Phase A
  2. ✅ Ivy codegen produces correct output for all 10 test cases in Phase B
  3. ✅ Decorator transformer handles all 7 test cases in Phase C
  4. ✅ Integration test (Phase D) shows identical rendering for JIT and AOT on at least 3 real generated modules
  5. ✅ Hybrid fallback (Phase E) implemented — partial AOT failure doesn't crash the preview
  6. ✅ Console shows [BundlerService] AOT: compiler eliminated from runtime for typical generated modules
  7. ✅ No NG0xxx errors appear in AOT mode that don't appear in JIT mode

Quick Reference: Running Things

# Build the angular runtime (needed once, auto-runs via NX dependsOn)
npx tsx tools/scripts/build/bundle-angular-runtime.ts

# Run TypeScript compilation check
npx tsc --noEmit --project libs/libs-structural/platform-preview/tsconfig.lib.json

# Run tests (once test files are created)
npx jest --config libs/libs-structural/platform-preview/jest.config.ts --testPathPattern compiler

# Start dev server (recompiles on save)
yarn serve:space
# Then open http://localhost:6200 → navigate to a universe → Studio → Live Preview

# Switch to AOT for testing
# Edit bundler.service.ts line ~17: JitCompilationStrategy → AotCompilationStrategy