Skip to content

fix: close remaining ngtsc output gaps from #623 - #670

Merged
Brooooooklyn merged 18 commits into
mainfrom
fix/ngtsc-parity-623
Nov 3, 2026
Merged

Brooooooklyn merged 18 commits into
mainfrom
fix/ngtsc-parity-623

Conversation

@Brooooooklyn

Copy link
Copy Markdown
Member

Part of #623: closes the items in its "Still open" table that oxc can match, checked against ngtsc 22.2.3.

What changed

Item Before Now (as ngtsc) Default output changes?
const enum ctor param in setClassMetadata / partial ctorParameters type: KeyCodes / i1.KeyCodes (a declare const enum threw ReferenceError in JIT/TestBed) type: undefined yes. Imported enums need resolveImportedValues
ɵinj imports (full ɵɵdefineInjector, partial ɵɵngDeclareInjector) keeps directives and pipes drops directives and pipes, plus components in exports. Same-file always; imported with resolveImportedValues yes
Partial ɵɵngDeclareNgModule imports with ModuleWithProviders X.forRoot() → X (an unchecked guess); mwp const kept the NgModule it names (DepModule / i1.DepModule) partial mode only. Full mode is byte-identical
forbidOrphanComponents no option new option → forbidOrphanRendering: true in ɵsetClassDebugInfo no (default false)
enableTemplateSourceLocations phase ran and its output was dropped new option → ɵɵattachSourceLocations(path, [[slot, offset, line, col]]), one call per view, Angular 19.1+, full mode no (default false). The phase no longer runs when the option is off
rootDirs no option; filePath = path given new option. ɵsetClassDebugInfo.filePath is project-relative, and a file outside every root gets none Vite plugin only: defaults to [workspaceRoot], like ngtsc's working-directory fallback

How ɵinj decides, per entry:

forRoot() call / NgModule / unknown / list not fully dropped ──> KEEP
@Directive or @Pipe (same file, or imported + resolveImportedValues) ──> DROP
@Component in imports ──> KEEP (ngtsc's provider walk not ported)
@Component in exports ──> DROP only if declared/imported or in the module's scope
exports entry also in declarations ──> DROP

Any entry oxc can't prove is kept, so providers are never lost.

Still open in #623 (not changed here)

  • ɵinj: dropping standalone components in imports that bring no providers (needs ngtsc's recursive mayExportProviders), and rewriting partly dropped spreads.
  • const enum through a namespace import (k.KeyCodes), or merged with a namespace.
  • MWP: a module class declared after the NgModule (ngtsc wraps the list in () => [...]), ns.X.forRoot(), and spreads.
  • Paths are never lowercased; ngtsc lowercases them on case-insensitive file systems.
  • Intentional: ɵɵgetComponentDepsFactory, and ɵɵsetNgModuleScope without the ngJitMode guard.

Verification

  • Each item was compared with ngtsc 22.2.3 (full and partial) on the cases its tests cover. Partial MWP output was also linked in JIT mode and loaded with @angular/core: ɵmod.imports now lists the module, as with ngtsc.
  • An adversarial review found 4 real issues, all fixed with tests here: a namespace const enum hiding an exported class, a TDZ ReferenceError from an MWP naming a later class, the Vite default root dropping filePath in monorepos, and a test gap.
  • cargo test with and without --all-features, conformance 1264/1264, napi pnpm test 425/425, pnpm check, e2e compare 0 mismatches. One fixture is newly listed as a known difference: ngtsc local mode keeps a declared export in ɵinj, while full mode and oxc drop it.

🤖 Generated with Claude Code

Brooooooklyn and others added 12 commits November 2, 2026 22:24
ngtsc's typeToValue has no value for a const enum outside local
compilation, so full ɵsetClassMetadata and partial
ɵɵngDeclareClassMetadata write `type: undefined` for a constructor
parameter typed by one. oxc wrote the name, which for a
`declare const enum` threw a ReferenceError when JIT or TestBed read it.

- Same file: `const enum`, `declare const enum` and `ns.E` now give
  `undefined`. A name a namespace, interface or `declare global` also
  declares keeps the reference, and so does a class nested in a function
  or block, since one file can't be sure what it means there.
- Imported: with `resolve_imported_values`, a named import the
  cross-file analyzer finds to be a const enum (also through
  `export { X } from` and `export *`) gives `undefined` and no `i1`
  import. The file read becomes a watch dependency. Namespace imports,
  packages, .d.ts-only modules and files with a bare namespace keep the
  reference.
- Factories and DI tokens are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The LocalDecls and local-reference comments still listed const enums as
emitted as written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In full and partial compilation ngtsc builds the ɵinj `imports` from what
each reference resolves to (NgModuleDecoratorHandler.resolve): it drops the
directives and pipes of `imports`, and the directives, components and pipes
of `exports`. Oxc copied ngtsc's local compilation and added both lists as
written. The runtime's walkProviderTree skips those entries, so dropping
them does not change which providers are registered.

An entry is dropped only when Oxc knows what it is:
- from this file: an `exports` entry that `declarations` also lists, a
  class of the file with an Angular @directive or @pipe decorator (aliases
  and namespace imports of the decorator count), and an exported @component
  that `imports` or `declarations` also lists;
- with cross-file analysis (resolveImportedValues): imported classes,
  `const` lists and package .d.ts types, read through the NgModule scope
  code. A list is dropped only when all its classes drop, and an exported
  component only when it is in the module's compilation scope.

Anything that does not resolve, an NgModule, a forRoot() call, and a
standalone component in `imports` stay. Classes nested in a function keep
their lists. The files read become watch dependencies like other
cross-file reads.

Not done: ngtsc also drops a standalone component from `imports` when it
brings in no providers, and rewrites a partly dropped list into the
references it keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rtial declarations

In partial mode, ngtsc lists the NgModule each ModuleWithProviders entry
names in the `ɵɵngDeclareNgModule` imports (`resolveTypeList` with
`createModuleWithProvidersResolver`): `imports: [mwp, Holder.forRoot()]`
becomes `imports: [DepModule, DepModule]`. Oxc listed the class the method
is called on (`Holder`) and kept constants as is. When a JIT linker inlines
that list into `ɵɵdefineNgModule`, the runtime got a class that is no
module and a ModuleWithProviders object, which poisons the module scope.

Same file, always on: a static method of a class in this file is read in
ngtsc's order (a body that is a single `return { ngModule: T }` first,
then a `ModuleWithProviders<T>` return type), and top-level constants of
such calls or of `{ ngModule: T }` are followed. T must be a class declared
before the module, or a binding whose import is kept anyway; otherwise the
entry keeps its old output, since a type-only import is removed and a later
class would be read before it exists.

With cross-file analysis (`resolveImportedValues`), a method or constant of
another file is read through ng_module_scope.rs, and the module is named
through a namespace import (`i1.DepModule`), as ngtsc does.

Only the partial declaration changes: the result goes to a separate
`partial_imports` list, so the injector imports and the full-mode
`ɵɵsetNgModuleScope` keep the entries as written. Full output is the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ngtsc's `forbidOrphanComponents` compiler option adds
`forbidOrphanRendering: true` to the `ɵsetClassDebugInfo` call of every
component, standalone or not. In dev mode with JIT support the runtime then
throws NG0981 when a non-standalone component is rendered before its NgModule
was loaded. oxc had no way to turn this on.

Add `forbid_orphan_components` to the Rust TransformOptions (default false),
`forbidOrphanComponents` to the napi TransformOptions, and a matching vite
plugin option. Full mode only: partial output has no class debug info, as in
ngtsc. The default output does not change.

Checked against ngtsc 22.2.3 on a file with a non-standalone component, a
standalone component, a directive and a pipe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Angular's `enableTemplateSourceLocations` option ends each view's create
block with one `ɵɵattachSourceLocations(path, [[slot, offset, line,
column], ...])` call. oxc had a port of the phase, but it ran whenever
advancedOptimizations was off, made one op per element with 1-based
lines and UTF-8 offsets, and reify threw the ops away.

Now it works like ngtsc full mode:
- off by default; when off the phase does no work and output is the same
- one call per view, at the end of the create block, not chained
- 0-based line and column and UTF-16 offsets, counted like Angular's lexer
  cursor in the raw source: from the .ts file start for an inline literal
  (escapes sit at their backslash), from the .html start for templateUrl,
  and from the template start for any other inline value
- slots come from the element ops' own slots
- only for full compilation (the linker hard-codes it off) and Angular
  19.1.0-next.3 or later (or no version given)

The path is project-relative, like ngtsc's getProjectRelativePath, using
a new `root_dirs` option. With no root dirs nothing is emitted (ngtsc
would fall back to rootDir or the working directory, which oxc does not
know). An inline literal whose raw text cannot be mapped (legacy octal
or `\u` surrogate escapes) also emits nothing.

With root dirs set, `ɵsetClassDebugInfo`'s `filePath` is also made
project-relative, and left out for a file outside every root, as
ngtsc's extractClassDebugInfo does. Without root dirs it stays the path
given. oxc never lowercases paths; ngtsc does on a case-insensitive
file system.

The napi TransformOptions gain the same two options
(`enableTemplateSourceLocations`, `rootDirs`).

Refs #623.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Vite plugin exposes both options. `rootDirs` defaults to Vite's
resolved `root`, so `ɵsetClassDebugInfo` `filePath` is now relative to
it, as in an ngtsc build, instead of the absolute module id.

The e2e compare runner passes the root dirs that match where ngtsc
compiles each fixture, so `filePath` now matches and the global
DEBUG_INFO_PATH known difference is gone (all 1174 fixtures pass).

Refs #623.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The compare oracle runs ngtsc in local mode, which keeps an exported
declaration in ɵinj. A full ngtsc build leaves it out, as Oxc now does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`NS.E` reads an exported member. A const enum one `namespace NS` block
keeps to itself hid an exported `class E` of another block, so the
class's ctor param lost its type (ngtsc keeps `type: NS.E`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Another file's ModuleWithProviders<Later> could name a class declared
after the NgModule, and the partial ɵmod then read it before it was
initialized (TDZ ReferenceError). Apply the same-file check: such an
entry keeps today's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Vite's root, a monorepo library outside the app lost filePath and
lineNumber in ɵsetClassDebugInfo. ngtsc falls back to the working
directory (the workspace root). Also list the new options in the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 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-11-03T03:26:54.240165Z 663537d Manual request
ℹ️ 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.

@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: 75b9b9d66d

ℹ️ 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".

…working directory

A component file with no directory gave `/./x.html`, a path from the
root, so no source location was emitted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@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: 3b4d52df79

ℹ️ 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".

On Windows the plugin resolves `rootDirs` to `D:\proj\src`, which a
bare `/proj/...` module id never matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 001bfb9f21

ℹ️ 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".

…cross-file lists

A local function named `forwardRef` may return any class, so a `const`
list built with it could be dropped from ɵinj with its providers. An
aliased `forwardRef` from @angular/core is now read too, as ngtsc does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@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: a7d6bdf8f6

ℹ️ 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".

…of a parameter

`return { ngModule: modules.X }` reads the call's argument when
`modules` is a parameter, not a namespace import of the same name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: ca81af7dbd

ℹ️ 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".

Brooooooklyn and others added 2 commits November 3, 2026 10:56
… NgModule scopes

The scope reader read the return type first and took a parameter or a
type parameter for the module. It now uses the reader the partial
imports use. With overloads, both read the first declaration, as ngtsc
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gtsc can't evaluate a ModuleWithProviders body

ngtsc's evaluator looks only through parentheses, `as` and `!`, gives up
on an object literal with a getter or method, and evaluates a returned
`Class.method()` call. When the body gives no value, it reads the
`ModuleWithProviders<T>` return type. Both the partial imports and the
NgModule scope reader now do the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 663537d92e

ℹ️ 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".

@Brooooooklyn
Brooooooklyn merged commit 89dd9c6 into main Nov 3, 2026
10 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/ngtsc-parity-623 branch November 3, 2026 04:56
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