Repository navigation
fix: align animate.*, hostDirectives, NgModule lists, host aria and output text with ngtsc; SSR watch; tsc .d.ts (#701–#707) - #708
Brooooooklyn wants to merge 29 commits into
Conversation
ngtsc's verifyHostBindings reports "Event name is missing in binding" for a `()` key and "Property name is missing in binding" for a `[]` key, then the event-binding errors, and joins all messages with "\n" into one NG5001. Oxc accepted the empty keys and stopped at the first `[on*]` key. Collect the errors in ngtsc's order and join them. `[ ]` / `( )`, the HostBinding / HostListener decorators and the linker stay as they were, which matches ngtsc and Angular's linker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion (#704) Angular has three eras for host `aria` property bindings: - before 20.2.0-next.2: a DOM property (kept as is) - 20.2.0-next.2 to 20.3.2, 21.0.0-next.0 to next.4: when reified, any `aria*` name longer than `aria` is written as `ɵɵattribute(ariaAttrName(name))` in the place of its DOM property - 20.3.3+, 21.0.0-next.5+: an `aria-*` property (also a legacy `[@aria-*]`, whose name has lost its `@`) becomes an attribute binding in binding specialization, so it comes after the `ɵɵdomProperty` calls Add the two version checks to AngularVersion, the attribute case to specialize_bindings_for_host, and the reify case to reify_host. Full builds, partial-then-link and the linker all share this path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain `tsc -p tsconfig.lib.json` run writes declarations outside the
Vite / Rolldown build, so no plugin hook adds the Ivy members and
consumers fail with NG2012.
New vite-free entry `@oxc-angular/vite/dts` exports
`augmentDeclarations`:
- `{ tsconfig }`: loads the user's `typescript` from the tsconfig dir and
runs a declaration emit that writes nothing, to get every file tsc
writes and its exact path. This covers files only reached by imports
(`"files": [entry]`), the common source dir of TS 5.x, `declarationDir`,
`.d.mts`, `mapRoot` (the map file path comes from the emit) and
`emitBOM`. `compilerOptions` takes what was given to tsc on the command
line, with paths from `cwd`.
- `{ declarationDir, sources }`: no TypeScript; pairs files with sources
by map or by the one layout of the dir (`matchDtsSources`), like the
plugin's disk pass. `tsconfig` can be added for `paths` aliases.
Each source is compiled in partial mode, as the plugin does. A source
with compile errors is reported and its file is left alone. CRLF files
keep CRLF. When the file on disk is tsc's text, or tsc's text plus Ivy
members, the members are rebuilt from tsc's fresh text, so an
incremental tsc run that kept an old file still gets fresh members and a
second run changes nothing.
`typescript` is an optional peer. The plugin path is not changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Before v20.2, `animate.*` in a template is plain: a static attribute stays in consts, `[animate.x]` is a property, `(animate.x)` a listener and `[attr.animate.x]` an attribute. Host `(animate.x)` listeners are plain `ɵɵlistener` too. This matches ngtsc 19.2 and 20.1. - On v20.2.0 to v20.3.0 a template `animate.leave` adds `ɵɵAnimationsFeature()` (Angular's `analyzeTemplateForAnimations`), in full compile, the linker and the HMR update module. `[class.animate.leave]` does not count, `[@animate.leave]` does, as in Angular. - Animate ops go right after their element one at a time in update order, so the last binding comes first, as ngtsc emits them. - From v20.2, `[(animate.x)]` and `bindon-animate.x` are animation bindings with an animation listener, as in ngtsc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Oxc linker copied the partial `hostDirectives` array into
`ɵɵHostDirectivesFeature` as written. A `forwardRef` entry then ran
while the class was still being defined and threw
"Cannot access 'L' before initialization".
Now each entry is read like Angular's `toHostDirectivesMetadata` and
`createHostDirectivesFeatureArg`: the forwardRef is unwrapped, an entry
with no mapping becomes the bare class, any forward reference wraps the
list in `function () { return [...]; }`, and an empty list gets no
feature. Mapping arrays are read into an object, as Angular does. A
shape that cannot be read is still copied as written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Partial output wrote each input/output pair as [alias, field]. Both
partial emitters now share the full compile's
`create_host_directive_mappings_array`, which writes [field, alias].
- The .d.ts wrote `{ "alias": "field" }` with no trailing `;`. It now
writes `{ "field": "alias"; }` like ngtsc, from one shared helper.
- A component's imported host directive is referenced by the module's
export name, as the directive path already did: `i1.H` for
`import { H as P }`, `i1.default` for a default import. The partial
component now writes `i1.H` too, and builds the factory first so its
deps take the lower `iN`, as ngtsc does.
- `dep.H` through `import * as dep` was dropped from the JS and the
.d.ts. The evaluator now keeps the namespace's local name on the
reference, and the module comes from the import map entry of that
name (so `resolved_imports` applies). The export name is never looked
up as a local name, so another import called `H` is not picked.
- A string export name (`import { "s-dir" as SDir }`, `dep["s-dir"]`)
is written `i1["s-dir"]`. ngtsc writes `i1.s-dir`, which does not
parse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lists like ngtsc (#703) With cross-file analysis, an NgModule list entry that ngtsc reads statically is now read too, so the partial declaration, the full-mode scope and the .d.ts list the classes instead of dropping the entry: - forwardRef(fn) is read like ngtsc's expandForwardRef: an arrow or function expression (parentheses and `as` around it looked through), with an expression body or one `return`. Its target is read like any entry, so `forwardRef(() => dep.X)`, `forwardRef(() => LIST)` and `forwardRef(() => IMPORTED_LIST)` give their classes. An aliased import of forwardRef from @angular/core counts too. - `dep['X']` is read like `dep.X`. - A top-level `let` or `var` list of this file is read, like a `const`. - A package's `declare const X: [typeof A, typeof B]` is read by its tuple type (also nested, and spread), instead of being taken for a class named X. - A const list from another file is named through the files of its classes, so only those files decide whether the lists stay lazy. A file that only holds the list and loads this one back no longer adds a wrap (ngtsc writes none either). The full-mode setNgModuleScope now lists what was read for each list whose entries were all read, like ngtsc. A list with an entry Oxc can't read stays as written (the runtime flattens it) and takes no namespace. The scope takes its namespace numbers after the factory and before ɵinj, as in ngtsc. One difference from ngtsc stays on purpose: when the file of a listed class loads this one back, the lists stay lazy (`() => [...]`). ngtsc's eager list throws when that file is loaded first, and Oxc runs the scope call with no ngJitMode guard. Without cross-file analysis nothing new is guessed: only lists made of classes of this file change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…703) Compiles small projects in full mode with cross-file analysis and loads them in plain Node with the real @angular/core: - a scope listing what forwardRef(() => ns.X), a spread of an imported list and ns.X.forRoot() give still hands a declared component the directives of those modules; - a list with an entry Oxc can't read stays as written next to a read, wrapped one, and still works; - an import cycle keeps the lists lazy and loads in either order, while the eager list ngtsc writes there throws when the other file loads first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….0-next.7 (#704) Before 21.2.8 and on 22.0.0-next.0 to next.6, ngtsc sorts host keys with HOST_REG_EXP, which needs a name inside the brackets. So `[]` and `()` are plain host attributes there and nothing is reported. Oxc reported them on every Angular version and failed builds that ngtsc accepts. Run the two checks only when the version has the newer key parsing. The metadata read for codegen skips them, as it does not know the version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pp --watch` (#705) In watch mode the client and server environments build at the same time, so the server read the page of the previous client round (or the source page in round 1), and a client-only change never rebuilt the server. Now, in `vite build --app --watch` only: - The server manifest holds two free names at transform time. The server's renderChunk waits for the client round in progress (or the first one, once the client watcher exists) and fills in that round's index.html, like the Angular CLI builds index.server.html from the browser page of the same build. - Only the environment named `client` gives the page and ends a round. A round also ends on buildEnd(error), renderError and the client watcher's ERROR event, so a failed client round never blocks the server. The server then uses the last page and warns. - Each watched server watches a file in the temp dir, made in its buildStart, so it exists before the build ends. The client writes it when its page differs from the one a server embedded and that server is not waiting, so a client-only change rebuilds the server, also when client round 1 failed in a fresh outDir. Builds without watch keep the same output and warnings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arations (#706) - Take the line break from the last one in the file and give it only to the lines the splice adds, so line breaks inside template literal types stay as tsc copied them from the source. - A CRLF file on disk is no longer taken as the same text as tsc's LF emit, so newLine given only on the tsc command line keeps CRLF and plain files are left alone. - When tsc's in-memory emit has no map but the file on disk points at one (declarationMap only on the command line), follow the file's comment and shift that map. - Write a map only when its declaration file changed, and keep the map tsc wrote when the file is tsc's text, so file URL sources under mapRoot keep tsc's spelling. - Tests compare paths with "/" so they pass on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…geDetection default (#707) Strings the compiler creates are now printed the way ngtsc's TypeScript printer prints them: every character outside ASCII becomes a \uXXXX escape, one per UTF-16 unit, with upper-case hex. Strings copied from the source keep their source text, as ngtsc does: providers, setClassMetadata arguments and keys, @Attribute and @Inject string tokens, the NgModule id, template literals, and the inline template in partial output. The .d.ts strings and added import lines are escaped too. The linker follows the Angular linker and its Babel 8 generator instead: it prints raw UTF-8 and escapes only the Unicode spaces (\xA0, \u2028, \u2029, ...). Input and output names and exportAs are rebuilt as new strings, as the Angular linker reads them with getString(); host attribute values keep their source text. A partial @Attribute token is now a string literal ('é'), not a bare name. The linker no longer writes changeDetection: 1 for a component with no changeDetection field when the app uses Angular 19, 20 or 21. Those linkers default the field to Default and leave it out of the output. The v22 linker still writes 1 for declarations made before v22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…707) ngtsc 22.2.4 prints the trigger string as "@\u03BCAnim.start". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… nested package tuples (#703) ngtsc wraps an NgModule's lists in `() => [...]` when a name it writes sits after the class. For an import that name is the one written in a variable read for the list, so a list, alias or `{ ngModule }` written after the NgModule (reached directly or through `forwardRef`) is wrapped. Oxc only did this for a local class declared later. A package's `declare const` tuple type may hold a nested tuple or named members. ngtsc's evaluator reads them, so Oxc reads them too instead of keeping the whole list as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#707) The linker now rebuilds each hostDirectives input and output name as a new string, as the Angular linker reads them with getString(). So "é" from the partial file comes out as "é", like Babel 8 prints it. The rest of hostDirectives keeps its source text. The JIT resource imports (angular:jit:template:file;...) now escape text outside ASCII in the specifier, as the Angular CLI builds them with createStringLiteral and the TypeScript printer escapes them. In JIT mode the whole file is printed again, which writes the text raw, so the lines are fixed after that step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # crates/oxc_angular_compiler/tests/linker_test.rs
# Conflicts: # crates/oxc_angular_compiler/src/linker/mod.rs
) A partial hostDirectives entry written with quoted keys ({ "directive": …, "inputs": [...] }) was copied as written, so a forwardRef entry was not unwrapped. Each entry is now read like Babel's parseObjectLiteral: a plain or quoted key, and the last one wins when a key is written twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The stamp file now lives in a new mkdtemp dir (mode 0700), made once per plugin instance. Its path cannot be guessed, so no other user can plant a link there or make the dir read-only. If it cannot be made, warn once. - A server build that is still running takes the new client page in renderChunk, so the client no longer writes the stamp for it. A shared round with a slow server now builds the server once, not twice. - The dir is removed on a normal process exit. Vite sets no signal handler for `vite build --watch`, so a watch stopped with Ctrl+C leaves the empty dir to the system temp cleanup (no signal handler is added on purpose). - Tests: the "cannot make the watched file" test made its project root after pointing TMPDIR at a missing dir, so the test itself threw ENOENT. It now makes the root first. Every watched stamp dir is removed after the suite, so the tests leave nothing in the temp dir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndows - augmentDeclarations with sources throws when declarationDir is not a directory or holds no declaration files, so a wrong path no longer passes as nothing to do (#706). - The #706 comment and README say partial mode writes never for ngContentSelectors, where ngc lists them. - ssr-manifest tests: skip the 0700 mode check on Windows, which has no mode bits, and point TEMP and TMP at the missing dir too, as os.tmpdir() reads them on Windows (#705). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review |
Codex Review Summary: ✅ CompletedThis 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: 3f145db364
ℹ️ 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".
…lists A module list resolved across files (#703) printed an export named by a string as `i1.h-dir`. The scope, the partial lists and the `.d.ts` now write `i1["h-dir"]`, the way hostDirectives already do. 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". |
The Windows runner's temp dir is a short name (`RUNNER~1`); take the `native` real path so tsc sees one spelling of each path, and show tsc's output when a build fails. 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". |
On Windows, tsc in the temp dir did not find `@angular/core` through the `node_modules` junction (TS2307). The projects now live under `test/.dts-augment-tmp`, so they find the package's `node_modules` the usual way. 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: 9dac19e357
ℹ️ 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".
The linker rebuilds `outputs` since #707 and only quoted keys with `.` or `-`, so `"some event": "changed"` came out as `some event: "changed"`, which does not parse. Quote any key that isn't an identifier 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". |
Fixes the follow-ups filed from #697, each checked against real ngtsc / the Angular linker (22.2.4, and 19.2.15 / 20.1.8 / 20.2.x / 20.3.x where a version gate applies).
Fixes #701
Fixes #702
Fixes #703
Fixes #704
Fixes #705
Fixes #706
Fixes #707
What changed
Decisions
animate.*has no v20.2 gate, no ɵɵAnimationsFeature on 20.2.0–20.3.0, and a different order; host(animate.*)listeners before 20.2 #701: the 20.2.0/20.2.1-only hoisting quirk (fixed in Angular 20.2.2) is not copied..d.tsgetsnever) #703: without cross-file analysis (napi default) the output is unchanged; the Vite plugin always turns it on.[]/()keys accepted, andaria-*host bindings stay DOM properties #704: all three aria eras are matched for host bindings.tscrun are not augmented #706: an exported function only, no CLI bin.Known limits
vite build --app --watchembeds the previous round's client index.html #705: a watch stopped with Ctrl+C leaves one emptyoxc-angular-ssr-XXXXXXdir in the temp dir (Vite sets no signal handler forbuild --watch).tscrun are not augmented #706: like the Vite build, partial mode writesneverforngContentSelectors.changeDetection: 1for v19 / v20.1 #707: the v19–v21 linkers run Babel 7 ("\xE9"); Oxc prints like the v22 linker (Babel 8, raw UTF-8).Tests
vite build --app --watchembeds the previous round's client index.html #705 in realvite build --app --watchrounds; dts: declarations written by a standalonetscrun are not augmented #706 against a realtsc+ngcbuild🤖 Generated with Claude Code