Skip to content

fix(jit): lower signal initializer APIs to synthesized propDecorators#319

Merged
brandonroberts merged 3 commits into
mainfrom
fix/312-jit-signal-api-lowering
May 29, 2026
Merged

fix(jit): lower signal initializer APIs to synthesized propDecorators#319
brandonroberts merged 3 commits into
mainfrom
fix/312-jit-signal-api-lowering

Conversation

@brandonroberts
Copy link
Copy Markdown
Collaborator

Closes #312.

Summary

  • Detect property initializers calling input(), output(), outputFromObservable(), model(), viewChild()/viewChildren(), contentChild()/contentChildren() (and .required() variants) on classes carrying @Component/@Directive
  • Synthesize the matching @Input/@Output/@ViewChild/etc. entries into static propDecorators so the runtime JIT facade can discover inputs, outputs, and queries
  • Mirrors packages/compiler-cli/src/ngtsc/transform/jit/src/initializer_api_transforms/
  • Skip synthesis when an explicit matching decorator already coexists on the field (decorator wins, matches upstream signal*Transform early-return)

Behavior changes

For a JIT-compiled class:

@Component({ selector: 'c', template: '' })
export class C {
  readonly value = input(0);
  readonly clicked = output<void>();
  readonly el = viewChild<ElementRef>('ref');
}

Before:

static propDecorators = {};
// runtime sees no inputs/outputs/queries — setInput fails (NG0315)

After:

static propDecorators = {
  value: [{ type: Input, args: [{ isSignal: true, alias: "value", required: false, transform: undefined }] }],
  clicked: [{ type: Output, args: ["clicked"] }],
  el: [{ type: ViewChild, args: ["ref", { isSignal: true }] }],
};

Notes on the upstream port

  • Three call shapes recognized: bare identifier (input(...)), .required member (input.required(...)), namespaced (core.input(...))
  • as/satisfies/parenthesized wrappers unwrapped, matching ngc's tryParseInitializerApi
  • Argument positions match the runtime APIs:
    • input(default, options?) / model(default, options?) — options in args[1]
    • input.required(options?) / model.required(options?) — options in args[0]
    • output(options?) — options in args[0]
    • outputFromObservable(observable, options?) — options in args[1]
    • Queries — locator in args[0], options in args[1] (spread with isSignal: true folded)
  • model() emits two decorators per field: @Input({isSignal, alias, required, transform: undefined}) and @Output("<alias>Change")

Test plan

  • 14 new focused tests in crates/oxc_angular_compiler/tests/jit_signal_initializer_test.rs cover each API, alias-from-options, .required variants, explicit-decorator-wins precedence, and the outputFromObservable args[1] regression
  • Full cargo test -p oxc_angular_compiler passes — no regressions across the existing ~1200 tests
  • Reviewer to spot-check the synthesized text shape against ngc compliance fixtures if desired (e.g. packages/compiler-cli/test/compliance/test_cases/signal_inputs/)

🤖 Generated with Claude Code

brandonroberts and others added 2 commits May 28, 2026 14:53
When `jit: true`, OXC's downlevel transform now detects property
initializers calling `input()`, `output()`, `outputFromObservable()`,
`model()`, `viewChild*()`, and `contentChild*()` and synthesizes the
matching `@Input`/`@Output`/`@ViewChild`/etc. propDecorators entries
the runtime JIT facade (`compileComponent`/`compileDirective` in
`@angular/compiler`) needs to discover them.

Without this, JIT-compiled signal components had no inputs, no outputs,
and no queries at runtime — `setInput`, router bindings, and any
`@Component`-level bindings into the class silently failed (NG0315/
NG0950). Mirrors `packages/compiler-cli/src/ngtsc/transform/jit/src/
initializer_api_transforms/`.

Detection mirrors the AOT signal-API detector at
`directive/property_decorators.rs`: bare identifier (`input()`), member
access (`input.required()`), and namespaced (`core.input()`) forms are
recognized, with `as`/`satisfies`/parenthesized wrappers unwrapped.

Skipping rules match upstream `signal*Transform` early-return behavior:
- `input()` skipped when `@Input` already decorates the field
- `output()`/`outputFromObservable()` skipped when `@Output` decorates
- `model()` skipped when either `@Input` or `@Output` decorates
- query APIs skipped when any of `@ViewChild`/`@ViewChildren`/
  `@ContentChild`/`@ContentChildren` decorates

Argument positions mirror the runtime APIs: `output()` options live in
`args[0]`, but `outputFromObservable()` options live in `args[1]` (the
observable is `args[0]`); `input()` and `model()` options live in
`args[1]` for the base form but `args[0]` for the `.required()` variant.
For queries, the positional locator (`args[0]`) carries over verbatim
and any options object (`args[1]`) is spread with `isSignal: true`
folded in.

Adds 14 focused tests at `tests/jit_signal_initializer_test.rs`
covering each API, the alias-from-options path, `.required` variants,
explicit-decorator-wins precedence, and the
`outputFromObservable`-args[1] regression case.

Closes #312.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Brooooooklyn
Copy link
Copy Markdown
Member

@codex review

Copy link
Copy Markdown

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

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: 56120e5964

ℹ️ 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 crates/oxc_angular_compiler/src/component/transform.rs Outdated
Codex P1 review on #319: synthesized `static propDecorators` referenced
bare decorator names (`Input`, `Output`, `ViewChild`, …) that a typical
signal component never imports — it imports only the lowercase
`input`/`output`/etc. functions. Evaluating the class therefore threw
`ReferenceError: Input is not defined` before Angular's JIT facade ran.
Tests passed because they only string-matched the emitted code without
executing it.

Match ngc's `createSyntheticAngularCoreDecoratorAccess`: synthesized
decorators are now namespace-prefixed (`i0.Input`, `i0.Output`,
`i0.ViewChild`, …) and a single `import * as i0 from "@angular/core"`
is injected when any synthesis occurred. The import is skipped when no
signal API lowering applied, so files without initializer-API usage are
unchanged.

Explicit field decorators continue to use the user's bare imports —
only synthesized decorators are namespace-prefixed.

Adds two regression tests:
- `synthesized_decorators_resolve_against_angular_core_namespace_import`
  pins the import + prefix shape so the runtime ReferenceError can't
  silently reappear.
- `namespace_import_skipped_when_no_synthesis_needed` guards against
  the opposite regression of unconditionally polluting every JIT file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@brandonroberts brandonroberts removed the request for review from Brooooooklyn May 29, 2026 16:32
@brandonroberts brandonroberts enabled auto-merge (squash) May 29, 2026 16:32
@brandonroberts brandonroberts merged commit bff6a42 into main May 29, 2026
9 checks passed
@brandonroberts brandonroberts deleted the fix/312-jit-signal-api-lowering branch May 29, 2026 16:43
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.

feat: JIT downlevel parity with packages/compiler-cli for signal-API components

2 participants