Skip to content

fix: support outputFromObservable() from @angular/core/rxjs-interop#3

Closed
ashley-hunter wants to merge 51 commits intomainfrom
output-from-observable
Closed

fix: support outputFromObservable() from @angular/core/rxjs-interop#3
ashley-hunter wants to merge 51 commits intomainfrom
output-from-observable

Conversation

@ashley-hunter
Copy link
Copy Markdown
Owner

Problem

Properties declared with outputFromObservable() from @angular/core/rxjs-interop were silently dropped from the compiled output metadata. The Angular runtime uses the outputs: {} object in ɵɵdefineComponent/ɵɵdefineDirective to wire up event bindings — missing entries mean parent components can never bind to those outputs.

Example that was broken:

readonly queryChanged = outputFromObservable(
  this.queryEditorService.latestParsedDataprimeQuery$.pipe(
    skip(1),
    debounceTime(300),
  ),
);

Root cause

try_parse_signal_output in property_decorators.rs only matched the identifier "output". outputFromObservable was never recognised, so its properties returned None and were excluded from the outputs metadata.

Fix

Extend try_parse_signal_output to detect both output() and outputFromObservable() (including namespaced forms like core.outputFromObservable()). The key behavioural difference is argument position: output() takes options at index 0, while outputFromObservable(observable, options?) takes them at index 1. The observable expression itself is irrelevant for metadata extraction and is ignored.

Tests

5 new unit tests added via TDD (red → green):

  • Simple argoutputFromObservable(new EventEmitter<string>())
  • Property referenceoutputFromObservable(this.service.obs$)
  • Piped observableoutputFromObservable(this.service.obs$.pipe(skip(1), debounceTime(300))) (the reported real-world case)
  • AliasoutputFromObservable(new EventEmitter(), { alias: 'clicked' })
  • Mixed — class using both output() and outputFromObservable() together

Also adds an e2e compare fixture (inputs-outputs/output-from-observable) covering all four scenarios so the output can be validated against the official Angular compiler.

Coverage

All 8 Angular initializer API functions are now handled by the Rust compiler. outputFromObservable was the only missing one.

Brooooooklyn and others added 30 commits April 2, 2026 18:59
…oidzero-dev#205)

* fix(angular): preserve block-body functions in decorator providers

Block-body arrow functions and function expressions in decorator properties
(e.g., useFactory) were silently having unsupported statements dropped.
Only return and expression statements survived, corrupting the function body
and causing runtime errors.

Add RawSource fallback: when convert_oxc_expression encounters a block-body
arrow with unsupported statement types (const, if, for, try/catch, etc.) or
a function expression, it preserves the complete source text verbatim via
span slicing instead of silently dropping statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(angular): pass source_text through build_decorator_metadata_array

Thread source_text into class metadata builder so decorator arguments
containing block-body arrows/function expressions are preserved via
RawSource fallback instead of being silently dropped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(angular): thread source_text through remaining metadata builders

- build_ctor_params_metadata: pass source_text so @Inject(...) args
  with complex expressions are preserved in ɵsetClassMetadata
- build_prop_decorators_metadata: pass source_text so @input({...})
  with complex transform functions are preserved
- extract_provided_in: propagate source_text from parent
  extract_injectable_metadata into forwardRef extraction

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(angular): strip TS types from RawSource, add expression-body fallback, thread source_text through property decorators

Three reviewer fixes:

1. P1 - RawSource now strips TypeScript type annotations via
   parse-transform-codegen pipeline, preventing invalid JS output
   like `(dep: Dep) => { ... }` in generated code.

2. Medium - Expression-body arrows with unsupported inner expressions
   (e.g., `() => someUnsupportedExpr`) now fall back to RawSource
   instead of returning None.

3. P2 - Thread source_text through property_decorators.rs so
   @input({transform}), @ViewChild, @ContentChild arguments with
   complex expressions are preserved via RawSource fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(angular): use module source type and add fast path for TS stripping

- Use SourceType::ts().with_module(true) instead of script-mode ts()
  so import.meta and ESM-only syntax parse correctly in RawSource
  fallback expressions.

- Add fast path: try parsing as .mjs first. If the expression is
  already valid JavaScript (no type annotations), return it as-is
  without running the heavier semantic→transform→codegen pipeline.
  Only expressions with actual TypeScript syntax pay the full cost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a class has both Angular decorators (e.g. @Injectable) and
non-Angular decorators (e.g. NGXS @State, @selector, @action),
lower all decorators when converting class declarations to class
expressions. Non-Angular member decorators are emitted as __decorate()
calls matching TypeScript's tsc output: null for methods/accessors,
void 0 for properties, instance members before static members.

Co-authored-by: Ashley Hunter <ashh640@users.noreply.github.com>
When a component .ts file changes and only the inline `template: `...``
portion differs, route it through the existing component HMR mechanism
instead of triggering a full page reload.

This gives inline template components the same fast HMR experience as
external .html templates.

Implementation:
- Cache inline template content during transform
- In handleHotUpdate, compare old vs new template content
- If only the template changed, add to pendingHmrUpdates and send
  angular:component-update event (same path as external templates)
- The existing HMR middleware endpoint already handles inline templates
  via extractInlineTemplate(), so no middleware changes needed
- Falls back to full reload if non-template code also changed
…ty (voidzero-dev#208)

* fix: compile animation trigger bindings in host property to ɵɵsyntheticHostProperty

convert_animations_for_host was unconditionally converting all AnimationBindingOp
entries to CreateOp::Animation (ɵɵanimateEnter/ɵɵanimateLeave), which is only
correct for animate.enter/animate.leave bindings. Host [@trigger] bindings
(AnimationBindingKind::Value) need to remain in the update list so they are
reified as ɵɵsyntheticHostProperty in the rf & 2 block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle animation trigger bindings in directive host property

The directive's parse_host_property_name was missing the @ animation check,
causing host: { '[@slidein]': 'state' } on directives to emit ɵɵdomProperty
instead of ɵɵsyntheticHostProperty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
voidzero-dev#211)

fix(vite-plugin): automatically skip Angular linker for packages in optimizeDeps.exclude

The linker plugin now reads `optimizeDeps.exclude` (via `configResolved`) and adds an early return in both places where linking happens:
- Rolldown pre-bundling load hook
- Vite transform hook
- Supports classic `node_modules` dependency structure and Yarn PnP

Co-authored-by: Arnoud Bos <arnoud.bos@crunchr.com>
…ev#216)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…v#218)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…dev#230)

Angular's control-directives pipeline treats [formField] as a normal property binding and then inserts separate controlCreate/control instructions. Our Rust pipeline had drifted from that logic and rewrote [formField] into a custom ControlOp carrying the bound value, which emitted legacy output like ɵɵcontrol(ctx.myField, "formField") without ever writing the directive input via ɵɵproperty("formField", ...). In Angular 21 signal forms this leaves FormField.field unset and can surface as NG0950 at runtime.

This change restores the expected control flow for template bindings:
- keep [formField] as a regular PropertyOp
- emit a separate ControlOp after the property update
- reify ControlOp to zero-arg ɵɵcontrol()
- stop extracting duplicate const metadata from ControlOp itself

The tests now cover both the regression and Angular's mixed-order control fixture behavior:
- [formField] must emit ɵɵproperty("formField", ...) plus ɵɵcontrol()
- legacy ɵɵcontrol(value, "formField") output is rejected
- mixed [formField]/[value] bindings preserve update order
- extracted const metadata preserves per-element binding order

Verified with targeted cargo test runs for the new regression, control binding extraction, mixed property ordering, const ordering, pipe slot propagation, and the existing [field] non-control regression.
…adata (voidzero-dev#232)

The Angular Linker's build_features was ignoring the controlCreate property
on ɵɵngDeclareDirective, so directives like @angular/forms/signals FormField
were linked without ɵɵControlFeature(passThroughInput). This left
DirectiveDef.controlDef unset at runtime, making template-emitted
ɵɵcontrolCreate()/ɵɵcontrol() calls no-ops and breaking [formField] bindings
with NG0950. Mirrors Angular TS compiler.ts:151-155.

Fixes voidzero-dev#229

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…zero-dev#231)

The extract_param_dependency function in pipe/decorator.rs was missing
the "Inject" arm in its decorator match, causing @Inject(TOKEN) to be
silently ignored. The injection token was then extracted from the TypeScript
type annotation instead — which is undefined at runtime when the type is
an interface.

The same fix already exists in directive/decorator.rs and
injectable/decorator.rs. This brings pipe/decorator.rs in line.

Surfaced by Angular 20 which added an assertDefined(token) guard in the
DI runtime that throws immediately on an undefined token, whereas Angular
19 would silently pass through.
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Brooooooklyn and others added 21 commits April 16, 2026 15:31
…ev#236)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…#240)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Bumps [rand](https://github.com/rust-random/rand) from 0.8.5 to 0.8.6.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.8.6/CHANGELOG.md)
- [Commits](rust-random/rand@0.8.5...0.8.6)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.8.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#253)

Pin GitHub Actions to hashes for latest tags

Agent-Logs-Url: https://github.com/voidzero-dev/oxc-angular-compiler/sessions/3826faf2-3d5a-4884-9327-778388ffca6f

Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…#251)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…anics (voidzero-dev#248)

fix: use byte-safe indexing in CSS style encapsulation to prevent UTF-8 panics

Several functions in the style encapsulation module used char indices
to slice UTF-8 strings, causing panics on selectors containing
multibyte characters (e.g. `ü`, `é`, `─`). This fixes
`split_by_combinators`, `find_pseudo_element_start`,
`find_pseudo_class_start`, `find_matching_paren`, and
`try_scope_pseudo_function_with_context` to use either
`char_indices()` or byte-level scanning for ASCII-only delimiters.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
outputFromObservable() was not recognised as an output initializer,
so properties declared with it were silently dropped from the outputs
metadata in the compiled ɵɵdefineComponent/ɵɵdefineDirective call.

Extend try_parse_signal_output to detect both output() and
outputFromObservable(). The key difference: output() takes options as
its first argument while outputFromObservable(observable, options?)
takes them as its second — the observable expression itself is
irrelevant for metadata extraction.

Adds 5 unit tests covering: simple EventEmitter arg, direct property
reference, piped observable chain (the reported real-world case),
alias via second arg, and mixed usage with output(). Also adds an e2e
compare fixture so the output can be verified against the official
Angular compiler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

6 participants