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:
- Angular packages were loaded from esm.sh CDN
- esm.sh re-bundles packages, creating separate copies of
@angular/core @angular/compilerregistered its JIT hooks with its OWN copy of core- 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:
Find a real generated module in the codebase. Look in
sessions/or generate one via Vitto Studio.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()); }
}
- 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:
- Simple approach: Store it as a signal on
BundlerService, read it inPreviewServerService - Better approach: Make
getPrerequisites()accept acompilerNeededflag
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:
- Chained optional:
item?.name?.first→ should becomectx.item?.name?.first - Array access:
items[0]→ should becomectx.items[0] - Method chains:
getList().filter(...)→ should becomectx.getList().filter(...)
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:
- ✅ Template parser handles all 15 test cases in Phase A
- ✅ Ivy codegen produces correct output for all 10 test cases in Phase B
- ✅ Decorator transformer handles all 7 test cases in Phase C
- ✅ Integration test (Phase D) shows identical rendering for JIT and AOT on at least 3 real generated modules
- ✅ Hybrid fallback (Phase E) implemented — partial AOT failure doesn't crash the preview
- ✅ Console shows
[BundlerService] AOT: compiler eliminated from runtimefor typical generated modules - ✅ 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