Skip to content

fix(compiler): recognise Angular decorators and signal APIs by their @angular/core import, like ngtsc - #504

Open
Brooooooklyn wants to merge 12 commits into
feat/dts-input-transform-typesfrom
fix/signal-api-import-identity
Open

Brooooooklyn wants to merge 12 commits into
feat/dts-input-transform-typesfrom
fix/signal-api-import-identity

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Stack (5/5): #493 inputs/outputs → #494 transform validation → #495 queries: → #496 .d.ts transform types → #504 signal API import identity. This is #504.

Recognises signal members by the @angular/core import of the function they call, like ngtsc, and emits a transform aliased through a variable the way ngtsc does.

The bug

Signal members were compiled by the name of the function they call:

import { input } from './other';   // or a local `function input()`, or no import at all
@Directive({ selector: '[d]', inputs: ['value'] })
export class D { value = input(0); }

compiled to inputs: { value: [1, "value"] } (a signal input). ngtsc sees a plain property here, so it emits inputs: { value: "value" }. The same happened for local.input(), a foreign output() / model(), and a foreign viewChild() (which added a signal query). An aliased import { input as inp } was missed. #493 and #495 already checked the import for their new collision errors, but compilation didn't.

Separately, const transform = booleanAttribute with inputs: [{ name: 'x', transform }] emitted transform, where ngtsc emits booleanAttribute.

The fix

  • Signal APIs. One check (initializer_api, reusing the fix(directive): compile inputs:/outputs: declared in decorator metadata #493/fix(queries): compile decorator queries: and match ngtsc's query options #495 import check) matches ngtsc's tryParseInitializerApi. It accepts f() / f.required() with f imported by name from the API's module (any alias), and ns.f() / ns.f.required() through a namespace import. The module is @angular/core, or @angular/core/rxjs-interop for outputFromObservable. Every place that reads signal members uses it:

    • inputs, outputs and models
    • signal queries
    • the .d.ts
    • the synthetic setClassMetadata and JIT propDecorators
    • NAPI extractComponentMetadataSync and class metadata

    The public extract_* and build_prop_decorators_metadata functions don't get the file's imports, so they still match by name. ng.input(0, { alias }) and ng.model(0, { alias }) now read their alias.

  • Transforms. A transform that resolves to an imported function or a global is emitted by its reference identity, like ngtsc's getIdentityIn. That is the name it was first reached by in the file:

    • booleanAttribute for const t = booleanAttribute + transform: t or { transform }
    • ba for import { booleanAttribute as ba }
    • booleanAttribute for o.t in a helper called with { t: booleanAttribute }, which was an error before

    Only an imported function named directly (or passed through unchanged, like 0 || fn) is assumed to be a function; a transform computed from an import (!FLAG, make(), Helpers.fn, FLAG && a, fn || a) gets the "imported from another module" error in inputs: and @Input. A shorthand naming an import ({ transform } with import { f as transform }) can't be evaluated, as in ngtsc, so it now gets "Input transform must be a function", and an imported shorthand alias / required is ignored, as ngtsc does.

Member decorators (@Input … @ContentChildren) also count as Angular's only when imported from @angular/core (any alias, or a namespace import), in compilation, decorator stripping, setClassMetadata and JIT propDecorators. A foreign or undeclared one is left on the class, like ngtsc. JIT synthesis reuses the file's own @angular/core namespace import, or names the new one i0_1, i0_2, … when i0 is already taken.

Member @Input(...) / @Output(...) arguments are evaluated like ngtsc: a same-file const, template literal or concatenation gives the alias, and a const or spread object gives alias / required / transform. Before, only literals were read and the alias was dropped silently. Bad arguments get ngtsc's "must resolve to a string (or an object literal)" and "can have at most one argument" errors, and an imported @Output(NAME) reports the "imported from another module" error, like @Input(OPTS).

@Input on input() / model() and @Output on output() / outputFromObservable() / model() report ngtsc's "Using @input with a signal input is not allowed." (and its other variants) on the decorator, in ngtsc's order. Only Angular's API counts, matched by its import. A global transform declared outside the file (const t = atob) is emitted by its name, like parseInt.

Angular's class decorators (@Component, @Directive, @Pipe, @Injectable, @NgModule) count only when imported from @angular/core, by name under any alias or through a namespace import, like ngtsc's isAngularCore. This is the same check member decorators use, and it applies to AOT, JIT, setClassMetadata, hoisting, NAPI metadata and the Vite plugin. A same-named decorator from another library, a local one or an undeclared one is left on the class, which isn't compiled. Like ngtsc, a decorator imported through a module that re-exports Angular's (import {Component} from './shared') isn't recognised either, so import Angular's decorators from @angular/core directly.

Constructor parameter decorators (@Inject, @Optional, @Self, @SkipSelf, @Host, @Attribute) also count only when imported from @angular/core, under any alias or through a namespace import. This applies to the factory, setClassMetadata, JIT ctorParameters and decorator removal; another module's is left on the parameter (__param in JIT), like ngtsc. The Vite plugin's HMR template/styles diff finds each component's decorator by the spelling the compiler reports (componentDecorators), so @Cmp(...) / @ng.Component(...) components hot-reload.

In JIT mode, a member decorator named like an Angular one (@Inject, @Component, @Optional, …) counts only when imported from @angular/core; another module's stays a __decorate() call instead of being dropped. An aliased @Service hoists the declarations it references, like @Service.

Tests

  • ngtsc snapshot: 80 new probes, 78 of them compared (generated with compiler-cli 22.1.7):
    • signal-*: named, aliased, namespace, rxjs-interop, local, undeclared, other-module and default imports
    • shorthandRef-*: const, let, typed, destructured, chained, as, ternary, spread and @Input(OPTS) forms, aliased imports, helpers, shorthand imports, and queries
  • Skipped (2):
    • a shorthand to a static method: ngtsc emits the bare, out-of-scope c
    • a transform typed from another file
  • Spans not compared (3 namespace-import cases): ngtsc reports them in the other file.
  • New regression tests cover the setClassMetadata and JIT propDecorators synthesis, and the NAPI extractComponentMetadataSync. All of them failed before this fix.

No test source needed new imports.

🤖 Generated with Claude Code

@Brooooooklyn
Brooooooklyn added this pull request to stack #503 September 30, 2026 14:50
@Brooooooklyn Brooooooklyn changed the title fix(directive): recognise signal APIs by their @angular/core import, like ngtsc fix(directive): recognise signal APIs by their @angular/core import, like ngtsc Sep 30, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T07:14:08.816079Z 12e4fb4 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Brooooooklyn
Brooooooklyn force-pushed the fix/signal-api-import-identity branch from 8f170d7 to b84d218 Compare September 30, 2026 15:58
@Brooooooklyn Brooooooklyn changed the title fix(directive): recognise signal APIs by their @angular/core import, like ngtsc fix(directive): recognise signal APIs and member decorators by their @angular/core import, like ngtsc Sep 30, 2026
@Brooooooklyn
Brooooooklyn force-pushed the fix/signal-api-import-identity branch 2 times, most recently from df18eb1 to 940c32d Compare September 30, 2026 18:32
@Brooooooklyn
Brooooooklyn force-pushed the fix/signal-api-import-identity branch from 940c32d to dbf5681 Compare September 30, 2026 19:07
@Brooooooklyn
Brooooooklyn force-pushed the fix/signal-api-import-identity branch from dbf5681 to 0d8951f Compare September 30, 2026 19:38
@Brooooooklyn
Brooooooklyn force-pushed the fix/signal-api-import-identity branch from 0d8951f to 8606c3e Compare October 1, 2026 02:43
Brooooooklyn added a commit that referenced this pull request Oct 1, 2026
…ore import, like ngtsc

`@Component`, `@Directive`, `@Pipe`, `@Injectable` and `@NgModule` were
matched by name. Like ngtsc's `findAngularDecorator`, a class decorator
is now Angular's only when it's imported from `@angular/core`: by name
under any alias (`import {Component as Cmp}`), or through a namespace
import (`@ng.Component()`). This is the rule #504 uses for member
decorators, and the same check (`angular_core_decorator`).

Checked against @angular/compiler-cli 22.1.7 (44 probes in
tests/fixtures/class_decorators_ngtsc.json, 40 compared):

- Another library's, a local, an undeclared or a default-imported
  `@Component` (and the other four) is left on the class, which isn't
  compiled, and its `inputs:` aren't checked.
- One re-exported through another module (`import {Component} from
  './shared'`) is left alone too: ngtsc follows the re-export, oxc can't.
- An aliased one (`@Cmp`, `@D`, `@P`, `@M`, `@Inj`) and a namespaced
  `@ng.Injectable` are now compiled; `setClassMetadata` lists them as
  written (`type: Cmp`). An aliased `@Component` keeps `templateUrl` in
  `setClassMetadata`, as ngtsc does.
- `import {Injectable as Component}` compiles as an `@Injectable`.

This holds for AOT compilation and decorator removal, setClassMetadata,
the `@Service` collision check, JIT, the hoisting of declarations a
decorator references, NAPI `extractComponentMetadataSync` /
`extractComponentUrls` / the pipe and class-metadata APIs, and the Vite
plugin: its quick decorator check lets `@Cmp(...)` / `@ng.Component(...)`
files through, and the HMR template/styles diff only blanks the
components the compiler compiled. The public `extract_pipe_metadata`,
`extract_injectable_metadata`, `extract_ng_module_metadata` and
`find_*_decorator_span`, which don't get the file's imports, still match
by name (`extract_pipe_metadata_in` takes them).

Not matched to ngtsc, on purpose: an aliased `@Injectable` gets a
factory (ngtsc's `needsFactory` compares the written name with
'Injectable', so its `ɵprov` points at a missing `ɵfac`).

117 unit-test sources that used `@Component` / `@Directive` without
importing it now import it from `@angular/core`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn Brooooooklyn changed the title fix(directive): recognise signal APIs and member decorators by their @angular/core import, like ngtsc fix(compiler): recognise Angular decorators and signal APIs by their @angular/core import, like ngtsc Oct 1, 2026

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

Copy link
Copy Markdown

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: fa0db68028

ℹ️ 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 napi/angular-compiler/vite-plugin/index.ts Outdated
Brooooooklyn and others added 7 commits October 1, 2026 11:30
…like ngtsc

Checked against @angular/compiler-cli 22.1.7 (probes are in the fixture:
`signal-*`, `shorthandRef-*`):

- Signal members were compiled by the name of the function they call. So
  `value = input(0)` with a local `input`, one imported from './other', an
  undeclared one, or `local.input(0)` became a signal input
  (`value: [1, "value"]`), a foreign `viewChild()` a signal query, and so
  on, where ngtsc sees a plain property; an aliased
  `import {input as inp}` was missed. Like ngtsc's
  `tryParseInitializerApi`, `input` / `model` / `output` / the query
  functions (`outputFromObservable` from `@angular/core/rxjs-interop`)
  now count only when imported from their module by name, under any
  alias, or through a namespace import (`ng.input()`), plus the
  `.required` forms. This holds for the `ɵdir`/`ɵcmp` definition, the
  `.d.ts`, the synthetic `setClassMetadata` / JIT `propDecorators`, and
  NAPI's `extractComponentMetadataSync` / class metadata. The public
  `extract_*` / `build_prop_decorators_metadata` functions, which aren't
  given the file's imports, still match by name. `ng.input(0, {alias})`
  and `ng.model(0, {alias})` now read their alias too.
- A transform that resolves to an imported function or a global is
  emitted by the name ngtsc gives it (the reference's identity in the
  file): `const transform = booleanAttribute` with `{transform}` or
  `transform: t` now emits `booleanAttribute`, not `transform` / `t`;
  `import {booleanAttribute as ba}` emits `ba`; and `o.t` in a helper
  called with `{t: booleanAttribute}` compiles to `booleanAttribute`
  instead of being reported. A value computed from an import (a call, a
  member, a condition on one) is still kept as written.
- A shorthand property naming an import (`{transform}` with
  `import {toNum as transform}`, `{name: 'x', alias}` with an imported
  `alias`) can't be evaluated by ngtsc (TypeScript returns the alias
  symbol, which has no value declaration). oxc now treats it the same
  way: "Input transform must be a function" for a transform, and an
  imported alias or `required` is ignored, as ngtsc does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…port, and keep JIT's i0 free

Member decorators (`@Input`, `@Output`, `@HostBinding`, `@HostListener`,
`@ViewChild(ren)`, `@ContentChild(ren)`) were matched by name. Like ngtsc,
the compiler now only treats one imported from `@angular/core` as Angular's:
by name under any alias (`import { Input as In }`), or through a namespace
import (`@core.Input()`). A same-named decorator from another module, a local
function or an undeclared name is left alone:

- not compiled into the definition, and its options aren't checked
  (`@Input({ transform: 5 })` from `./foreign` no longer reports "Input
  transform must be a function")
- not removed from the class, and not listed in `setClassMetadata`
- in JIT, applied with `__decorate` instead of moved to `propDecorators`

An aliased or namespaced one is compiled, removed from the class (a
namespaced `@core.Input()` was compiled but left on the class) and, in JIT,
listed in `propDecorators` as written (`type: In`, `type: core.Input`).
The public `extract_*` functions, which don't get the file's imports, still
match by name.

JIT synthesis of signal-member decorators referenced `@angular/core` as a
new `import * as i0`, which redeclared any `i0` the file already had (a
SyntaxError; e.g. `import { input as i0 }`). Like ngtsc's `ImportManager`, it
now reuses the file's own `import * as x from '@angular/core'`, or names the
new import `i0_1`, `i0_2`, ... when `i0` is taken.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uator, like ngtsc

A same-file value passed to a member decorator lost its alias with no
error: `const NAME = 'y'` with `@Input(NAME)` compiled to `x: "x"` (ngtsc:
`x: [0, "y", "x"]`), `@Input(OPTS)` with `const OPTS = {alias: 'y',
required: true}` dropped the alias and `required`, and `@Output(NAME)`
compiled to `e: "e"` (ngtsc: `e: "y"`). Only literals were read.

Like ngtsc's `tryParseInputFieldMapping` / `tryParseDecoratorOutput`, the
argument is now evaluated (checked against @angular/compiler-cli 22.1.7):

- `@Input`: a string (a const, `let`, template literal, `'a' + 'b'`,
  `as const`, a ternary, a same-file function call, ...) is the alias; an
  object (a const, a spread, `{alias: NAME}`) gives `alias`, `required`
  and `transform`. This holds for the `ɵdir`/`ɵcmp` inputs, the `.d.ts`
  and NAPI's `extractComponentMetadataSync`, which now also gets the
  transform ngtsc emits.
- `@Output`: a string is the alias.
- ngtsc's errors: "@input can have at most one argument, got N
  argument(s)" (named as written: `@In` for `import {Input as In}`),
  "@input decorator argument must resolve to a string or an object
  literal" (numbers, booleans, `undefined`, arrays, enum members,
  `declare`d values; `null` is allowed), and the same two for `@Output`
  ("must resolve to a string", which also rejects `null` and objects).
- An imported `@Output(NAME)` (`ns.NAME`, `` `${NAME}` ``, a local copy)
  gets the "imported from another module" error, like `@Input(OPTS)`.

The public `extract_*` functions, which don't get the file's imports,
still read literals only.

Left as before: ngtsc's "Input/Output 'y' is bound to both ..." error,
and `@Output()` on a setter (ngtsc compiles it; oxc doesn't read outputs
from methods).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…its name

A global like the DOM's `atob` is now assumed to be a transform function (see
the change below in the stack). Reached through a variable, a helper, an
object or a condition (`const t = atob` and `transform: t`), it was emitted as
written (`t`), where ngtsc emits `atob`. It now gets the same reference
identity as an ES lib global like `parseInt`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and outputs like ngtsc

`@Input() value = input(0)` compiled as a plain input with no error; ngtsc
reports "Using @input with a signal input is not allowed." oxc only looked at
the initializer of a member also listed in `inputs:` (for a different error),
and had no such check. The same went for `@Input` on `model()`, and `@Output`
on `output()`, `outputFromObservable()` or `model()`.

Each is now reported on the decorator with ngtsc's message, in its order: an
input's check comes before its `@Input` arguments are read, an output's after
its `@Output` arguments. The call counts only when it's Angular's API by its
import (any alias, a namespace, `.required`), so a local or foreign `input()`
is left alone. The compiled output is unchanged, as for other errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and helper-returned transforms

`@Input(LOCAL || NAME)`, `@Input({alias: LOCAL || NAME, required: true || FLAG})`,
`@Output(LOCAL || NAME)` and `transform: 0 || booleanAttribute` now compile like
ngtsc (the evaluator fix at the bottom of the stack). Also pins
`@Input(opts({t: booleanAttribute}))`, which emits `booleanAttribute` through
the reference identity this PR adds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rom namespaces with ngtsc

Arguments of member `@Input(...)` / `@Output(...)` read through
`typeof NS.X` or `import S = NS.X` (a name, or options with `alias` and
`required`) now evaluate like ngtsc, `NS.X` in a value position stays
dynamic, and a namespace is a reference to it. Add those cases to the ngtsc
snapshot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brooooooklyn and others added 2 commits October 1, 2026 11:30
…ore import, like ngtsc

`@Component`, `@Directive`, `@Pipe`, `@Injectable` and `@NgModule` were
matched by name. Like ngtsc's `findAngularDecorator`, a class decorator
is now Angular's only when it's imported from `@angular/core`: by name
under any alias (`import {Component as Cmp}`), or through a namespace
import (`@ng.Component()`). This is the rule #504 uses for member
decorators, and the same check (`angular_core_decorator`).

Checked against @angular/compiler-cli 22.1.7 (44 probes in
tests/fixtures/class_decorators_ngtsc.json, 40 compared):

- Another library's, a local, an undeclared or a default-imported
  `@Component` (and the other four) is left on the class, which isn't
  compiled, and its `inputs:` aren't checked.
- One re-exported through another module (`import {Component} from
  './shared'`) is left alone too: ngtsc follows the re-export, oxc can't.
- An aliased one (`@Cmp`, `@D`, `@P`, `@M`, `@Inj`) and a namespaced
  `@ng.Injectable` are now compiled; `setClassMetadata` lists them as
  written (`type: Cmp`). An aliased `@Component` keeps `templateUrl` in
  `setClassMetadata`, as ngtsc does.
- `import {Injectable as Component}` compiles as an `@Injectable`.

This holds for AOT compilation and decorator removal, setClassMetadata,
the `@Service` collision check, JIT, the hoisting of declarations a
decorator references, NAPI `extractComponentMetadataSync` /
`extractComponentUrls` / the pipe and class-metadata APIs, and the Vite
plugin: its quick decorator check lets `@Cmp(...)` / `@ng.Component(...)`
files through, and the HMR template/styles diff only blanks the
components the compiler compiled. The public `extract_pipe_metadata`,
`extract_injectable_metadata`, `extract_ng_module_metadata` and
`find_*_decorator_span`, which don't get the file's imports, still match
by name (`extract_pipe_metadata_in` takes them).

Not matched to ngtsc, on purpose: an aliased `@Injectable` gets a
factory (ngtsc's `needsFactory` compares the written name with
'Injectable', so its `ɵprov` points at a missing `ɵfac`).

117 unit-test sources that used `@Component` / `@Directive` without
importing it now import it from `@angular/core`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… assuming it's a function

Only an imported function named directly (`transform: fn`, or one an
expression passes through unchanged, like `0 || fn`) is assumed to be a
function. A value computed from an import (`!FLAG`, `make()`, `Helpers.fn`,
`FLAG ? a : b`, `fn || a`, `FLAG && a`) now gets the "imported from another
module" error in both `inputs:` and `@Input`, where it used to be emitted as
written with no diagnostic (`!FLAG` is a boolean: a TypeError at runtime).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn
Brooooooklyn force-pushed the fix/signal-api-import-identity branch from fa0db68 to 3e0b07f Compare October 1, 2026 03:38

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

Copy link
Copy Markdown

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: 3e0b07ff2c

ℹ️ 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
Brooooooklyn and others added 2 commits October 1, 2026 13:42
…e the compiler reports

The HMR check empties the inline `template:` / `styles:` of each compiled
component and compares the rest of the file. It found the decorator by
looking for the literal `@Component(`, so a component compiled through
`import { Component as Cmp }` (`@Cmp(...)`) or `import * as ng`
(`@ng.Component(...)`) was never emptied, and an inline template or
styles edit to it reloaded the page instead of hot-replacing it.

The compiler now reports, with HMR on, each compiled component's
decorator as written (`componentDecorators`: component ID → `Component`,
`Cmp`, `ng.Component`, ...). The plugin looks for exactly that
spelling on that class, so the text scan never decides on its own which
decorators are Angular's. Only the decorator the compiler took is
emptied: another library's `@Component`, even on the same class, is left
whole and an edit to it still reloads. A spelling the scan can't match
(`@Cmp<T>(`, `@ Cmp(`, a comment before `(`) is never emptied, so edits
to it reload, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ngular/core import, like ngtsc

`@Inject`, `@Optional`, `@Self`, `@SkipSelf`, `@Host` and `@Attribute`
on constructor parameters were matched by their written name. So with
`import { Inject as Inj } from '@angular/core'`, `@Inj(TOKEN)` was
ignored and then removed: the factory injected the parameter's type
instead of `TOKEN`, and `setClassMetadata` / JIT `ctorParameters`
dropped it. `@ng.Inject(TOKEN)` was ignored by the factory the same way.
And another module's `@Inject` / `@Optional` was taken as Angular's.

Like ngtsc (`isAngularCore` in `getConstructorDependencies`, and the JIT
`downlevel_decorators_transform`), a parameter decorator is now Angular's
only when it's imported from `@angular/core`: by name under any alias,
or through a namespace import. It's the same check as the class and
member decorators (`angular_core_decorator`), used for:

- the factory (`ɵfac`) of components, directives, pipes, injectables and
  NgModules
- `setClassMetadata`'s `ctorParameters`, which lists Angular's
  decorators as written (`type: Inj`), and gives a parameter whose
  decorators are all another module's `decorators: []`, like ngtsc
- JIT `ctorParameters`
- removing the decorators, and dropping the imports only they use

Another module's or a local parameter decorator is left on the parameter
(AOT), or lowered as `__param(index, decorator)` in the class's
`__decorate` (JIT), as TypeScript does for ngtsc. Its import stays.

Checked against @angular/compiler-cli 22.1.7 and its JIT transform (33
probes in tests/fixtures/ctor_param_decorators_ngtsc.json, 29 compared):
named, aliased, namespaced, another module's, local, another module's
namespace, mixed and swapped (`import {Optional as Self}`) imports, on
`@Component`, `@Directive`, `@Injectable`, `@Pipe` and `@NgModule`.
Skipped: an aliased `@Injectable` (ngtsc gives it no factory), and
`@NgModule` with `@Inject(TOKEN)`, whose factory ignores `@Inject` on
main too.

The public `extract_*_metadata` and `build_ctor_params_metadata`, which
don't get the file's imports, still match by name
(`build_ctor_params_metadata_in` takes them).

Nine unit-test sources that used `@Inject` / `@Optional` / ... without
importing them now import them from `@angular/core`.

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

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

Copy link
Copy Markdown

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: c2d3731798

ℹ️ 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/hoist.rs Outdated
…le's `@Inject` on JIT members

The hoist that moves a declaration an Angular decorator references above
the class still required `@Service` to be written as `Service`, so
`import { Service as NgService }` + `@NgService({ factory: FACTORY })` with
a later `const FACTORY` was compiled but not hoisted, and the class's
`setClassMetadata` read `FACTORY` in its TDZ. It now takes the import's
exported name, like the compile path.

JIT member decorators named like Angular's (`Inject`, `Component`,
`Optional`, ...) were dropped whatever module they came from. Like ngtsc's
JIT transform, only `@angular/core`'s are now; another module's stays a
`__decorate()` call, and `@angular/core`'s `Inject` under an alias is
treated like `@Inject`.

Co-Authored-By: Claude Opus 5.5 (1M context) <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.

1 participant