Repository navigation
fix: close remaining ngtsc output gaps from #623 - #670
Conversation
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>
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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>
|
@codex review |
There was a problem hiding this comment.
💡 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>
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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>
|
@codex review |
There was a problem hiding this comment.
💡 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>
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
… 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>
|
@codex review |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Part of #623: closes the items in its "Still open" table that oxc can match, checked against ngtsc 22.2.3.
What changed
const enumctor param insetClassMetadata/ partialctorParameterstype: KeyCodes/i1.KeyCodes(adeclare const enumthrewReferenceErrorin JIT/TestBed)type: undefinedresolveImportedValuesɵinjimports(fullɵɵdefineInjector, partialɵɵngDeclareInjector)exports. Same-file always; imported withresolveImportedValuesɵɵngDeclareNgModuleimportswithModuleWithProvidersX.forRoot()→X(an unchecked guess);mwpconst keptDepModule/i1.DepModule)forbidOrphanComponentsforbidOrphanRendering: trueinɵsetClassDebugInfofalse)enableTemplateSourceLocationsɵɵattachSourceLocations(path, [[slot, offset, line, col]]), one call per view, Angular 19.1+, full modefalse). The phase no longer runs when the option is offrootDirsfilePath= path givenɵsetClassDebugInfo.filePathis project-relative, and a file outside every root gets none[workspaceRoot], like ngtsc's working-directory fallbackHow
ɵinjdecides, per entry:Any entry oxc can't prove is kept, so providers are never lost.
Still open in #623 (not changed here)
ɵinj: dropping standalone components inimportsthat bring no providers (needs ngtsc's recursivemayExportProviders), and rewriting partly dropped spreads.const enumthrough a namespace import (k.KeyCodes), or merged with a namespace.() => [...]),ns.X.forRoot(), and spreads.ɵɵgetComponentDepsFactory, andɵɵsetNgModuleScopewithout thengJitModeguard.Verification
@angular/core:ɵmod.importsnow lists the module, as with ngtsc.ReferenceErrorfrom an MWP naming a later class, the Vite default root droppingfilePathin monorepos, and a test gap.cargo testwith and without--all-features, conformance 1264/1264, napipnpm test425/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