Skip to content

Commit 6defd8d

Browse files
chore(deps): update npm packages to v22 (#339)
* chore(deps): update npm packages to v22 * fix(linker): support v22 ɵɵngDeclareService partial declarations Angular v22 introduced the @service decorator, whose precompiled libraries ship partial `ɵɵngDeclareService` declarations (e.g. @angular/common's NgLocalization). The Angular Linker did not recognize this declaration, so it passed through unlinked and fell back to JIT at runtime: The service 'NgLocalization' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available. This broke any v22 app that pulls in @angular/common (the e2e fixture's app never bootstrapped). Add a `link_service` that mirrors the upstream PartialServiceLinkerVersion1 + compileService: emit `ɵɵdefineService({ token, factory[, autoProvided: false] })`, delegating to the class `ɵfac` when no factory is declared, or wrapping a supplied factory in `() => (factory)()`. Also derive the core namespace from the declaration's `ngImport` property instead of the call's callee. Bundlers (esbuild's dep optimizer) rewrite `i0.ɵɵngDeclare*(...)` into a bare `ɵɵngDeclare*(...)` call while renaming the namespace import (e.g. to `core_exports`); the callee then no longer carries the namespace, but `ngImport` still points at the correct alias. This matches the TS linker, which emits `importExpr(R3.core)` resolved via the file's import manager, and fixes a 'i0 is not defined' runtime error in optimized bundles. Verified end-to-end: the v22 e2e fixture app boots and all 34 Playwright e2e tests pass. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent c760d4d commit 6defd8d

6 files changed

Lines changed: 947 additions & 752 deletions

File tree

crates/oxc_angular_compiler/src/linker/mod.rs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//! |--------------------|-|
1414
//! | `ɵɵngDeclareFactory` | Factory function |
1515
//! | `ɵɵngDeclareInjectable` | `ɵɵdefineInjectable(...)` |
16+
//! | `ɵɵngDeclareService` | `ɵɵdefineService(...)` |
1617
//! | `ɵɵngDeclareInjector` | `ɵɵdefineInjector(...)` |
1718
//! | `ɵɵngDeclareNgModule` | `ɵɵdefineNgModule(...)` |
1819
//! | `ɵɵngDeclarePipe` | `ɵɵdefinePipe(...)` |
@@ -58,6 +59,7 @@ fn quote_key(key: &str) -> String {
5859
/// Partial declaration function names to link.
5960
const DECLARE_FACTORY: &str = "\u{0275}\u{0275}ngDeclareFactory";
6061
const DECLARE_INJECTABLE: &str = "\u{0275}\u{0275}ngDeclareInjectable";
62+
const DECLARE_SERVICE: &str = "\u{0275}\u{0275}ngDeclareService";
6163
const DECLARE_INJECTOR: &str = "\u{0275}\u{0275}ngDeclareInjector";
6264
const DECLARE_NG_MODULE: &str = "\u{0275}\u{0275}ngDeclareNgModule";
6365
const DECLARE_PIPE: &str = "\u{0275}\u{0275}ngDeclarePipe";
@@ -350,6 +352,7 @@ fn get_declare_name<'a>(call: &'a CallExpression<'a>) -> Option<&'a str> {
350352
match name {
351353
DECLARE_FACTORY
352354
| DECLARE_INJECTABLE
355+
| DECLARE_SERVICE
353356
| DECLARE_INJECTOR
354357
| DECLARE_NG_MODULE
355358
| DECLARE_PIPE
@@ -361,8 +364,24 @@ fn get_declare_name<'a>(call: &'a CallExpression<'a>) -> Option<&'a str> {
361364
}
362365
}
363366

364-
/// Get the Angular import namespace (e.g., "i0") from the callee.
367+
/// Get the Angular import namespace (e.g., "i0") used to reference core symbols
368+
/// in the linked output.
369+
///
370+
/// Prefers the declaration's own `ngImport` property, which is what the upstream
371+
/// TS linker uses (it emits `importExpr(R3.core)`, resolved via the file's import
372+
/// manager). This is important for bundles where a tool (e.g. esbuild's dep
373+
/// optimizer) has rewritten the `i0.ɵɵngDeclare*(...)` member call into a bare
374+
/// `ɵɵngDeclare*(...)` call while renaming the namespace import (e.g. to
375+
/// `core_exports`): the callee no longer carries the namespace, but `ngImport`
376+
/// still points at the correct alias. Falls back to the callee's object, then
377+
/// `i0`.
365378
fn get_ng_import_namespace<'a>(call: &'a CallExpression<'a>) -> &'a str {
379+
if let Some(meta) = get_metadata_object(call)
380+
&& let Some(ns) = get_identifier_property(meta, "ngImport")
381+
{
382+
return ns;
383+
}
384+
366385
match &call.callee {
367386
Expression::StaticMemberExpression(member) => {
368387
if let Expression::Identifier(ident) = &member.object {
@@ -374,6 +393,21 @@ fn get_ng_import_namespace<'a>(call: &'a CallExpression<'a>) -> &'a str {
374393
}
375394
}
376395

396+
/// Read an identifier-valued property (e.g. `ngImport: i0`) from an object.
397+
fn get_identifier_property<'a>(obj: &'a ObjectExpression<'a>, name: &str) -> Option<&'a str> {
398+
obj.properties.iter().find_map(|prop| match prop {
399+
ObjectPropertyKind::ObjectProperty(p)
400+
if matches!(&p.key, PropertyKey::StaticIdentifier(ident) if ident.name == name) =>
401+
{
402+
match &p.value {
403+
Expression::Identifier(ident) => Some(ident.name.as_str()),
404+
_ => None,
405+
}
406+
}
407+
_ => None,
408+
})
409+
}
410+
377411
/// Get the metadata object from a ɵɵngDeclare* call's first argument.
378412
fn get_metadata_object<'a>(call: &'a CallExpression<'a>) -> Option<&'a ObjectExpression<'a>> {
379413
call.arguments.first().and_then(|arg| {
@@ -577,6 +611,17 @@ fn is_property_null(obj: &ObjectExpression<'_>, name: &str) -> bool {
577611
})
578612
}
579613

614+
/// Check if a property exists and its value is the boolean literal `false`.
615+
fn is_property_false(obj: &ObjectExpression<'_>, name: &str) -> bool {
616+
obj.properties.iter().any(|prop| {
617+
matches!(prop,
618+
ObjectPropertyKind::ObjectProperty(p)
619+
if matches!(&p.key, PropertyKey::StaticIdentifier(ident) if ident.name == name)
620+
&& matches!(&p.value, Expression::BooleanLiteral(b) if !b.value)
621+
)
622+
})
623+
}
624+
580625
/// Check if a property exists and its value is a specific string literal.
581626
fn is_property_string(obj: &ObjectExpression<'_>, name: &str, value: &str) -> bool {
582627
obj.properties.iter().any(|prop| {
@@ -852,6 +897,7 @@ fn link_declaration(
852897
let replacement = match name {
853898
DECLARE_FACTORY => link_factory(meta, source, ns, type_name),
854899
DECLARE_INJECTABLE => link_injectable(meta, source, ns, type_name),
900+
DECLARE_SERVICE => link_service(meta, source, ns, type_name),
855901
DECLARE_INJECTOR => link_injector(meta, source, ns, type_name),
856902
DECLARE_NG_MODULE => link_ng_module(meta, source, ns, type_name),
857903
DECLARE_PIPE => link_pipe(meta, source, ns, type_name),
@@ -1018,6 +1064,38 @@ fn link_injectable(
10181064
))
10191065
}
10201066

1067+
/// Link ɵɵngDeclareService → ɵɵdefineService.
1068+
///
1069+
/// `@Service` (Angular v22+) ships partial `ɵɵngDeclareService` declarations in
1070+
/// precompiled libraries (e.g. `@angular/common`'s `NgLocalization`). Mirrors the
1071+
/// TS linker's `PartialServiceLinkerVersion1` + `compileService`:
1072+
///
1073+
/// - No `factory` field → delegate to the class factory: `{Type}.ɵfac`.
1074+
/// - `factory` field → wrap in an arrow that calls it: `() => (factory)()`.
1075+
/// - `autoProvided: false` is the only `autoProvided` value ever emitted (the
1076+
/// partial compiler omits it otherwise).
1077+
fn link_service(
1078+
meta: &ObjectExpression<'_>,
1079+
source: &str,
1080+
ns: &str,
1081+
type_name: &str,
1082+
) -> Option<String> {
1083+
let factory = match get_property_source(meta, "factory", source) {
1084+
// `factory: () => (userFactory)()` — wrap the supplied factory.
1085+
Some(user_factory) => format!("() => ({user_factory})()"),
1086+
// No factory supplied — delegate to the class's own ɵfac.
1087+
None => format!("{type_name}.\u{0275}fac"),
1088+
};
1089+
1090+
// Only `autoProvided: false` is ever present in the declaration.
1091+
let auto_provided_suffix =
1092+
if is_property_false(meta, "autoProvided") { ", autoProvided: false" } else { "" };
1093+
1094+
Some(format!(
1095+
"{ns}.\u{0275}\u{0275}defineService({{ token: {type_name}, factory: {factory}{auto_provided_suffix} }})"
1096+
))
1097+
}
1098+
10211099
/// Link ɵɵngDeclareInjector → ɵɵdefineInjector.
10221100
fn link_injector(
10231101
meta: &ObjectExpression<'_>,
@@ -2266,6 +2344,47 @@ MyService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
22662344
assert!(!result.code.contains("ɵɵngDeclareInjectable"));
22672345
}
22682346

2347+
#[test]
2348+
fn test_link_service_delegates_to_fac() {
2349+
let allocator = Allocator::default();
2350+
let code = r#"
2351+
import * as i0 from "@angular/core";
2352+
class MyService {
2353+
}
2354+
MyService.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.0", ngImport: i0, type: MyService });
2355+
"#;
2356+
let result = link(&allocator, code, "test.mjs");
2357+
assert!(result.linked);
2358+
assert!(
2359+
result
2360+
.code
2361+
.contains("i0.ɵɵdefineService({ token: MyService, factory: MyService.ɵfac })")
2362+
);
2363+
assert!(!result.code.contains("ɵɵngDeclareService"));
2364+
}
2365+
2366+
#[test]
2367+
fn test_link_service_custom_factory_and_auto_provided() {
2368+
let allocator = Allocator::default();
2369+
let code = r#"
2370+
import * as i0 from "@angular/core";
2371+
class NgLocalization {
2372+
}
2373+
NgLocalization.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.0", ngImport: i0, type: NgLocalization, autoProvided: false, factory: () => new NgLocaleLocalization(inject(LOCALE_ID)) });
2374+
"#;
2375+
let result = link(&allocator, code, "test.mjs");
2376+
assert!(result.linked);
2377+
// Custom factory is wrapped in an arrow that invokes it.
2378+
assert!(
2379+
result
2380+
.code
2381+
.contains("factory: () => (() => new NgLocaleLocalization(inject(LOCALE_ID)))()")
2382+
);
2383+
// autoProvided: false is preserved.
2384+
assert!(result.code.contains("autoProvided: false"));
2385+
assert!(!result.code.contains("ɵɵngDeclareService"));
2386+
}
2387+
22692388
#[test]
22702389
fn test_link_class_metadata() {
22712390
let allocator = Allocator::default();

napi/angular-compiler/benchmarks/bitwarden/package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
"benchmark:incremental": "oxnode benchmark.ts --incremental"
1414
},
1515
"dependencies": {
16-
"@angular/animations": "21.2.16",
17-
"@angular/cdk": "21.2.14",
18-
"@angular/common": "21.2.16",
19-
"@angular/compiler": "21.2.16",
20-
"@angular/core": "21.2.16",
21-
"@angular/forms": "21.2.16",
22-
"@angular/platform-browser": "21.2.16",
23-
"@angular/platform-browser-dynamic": "21.2.16",
24-
"@angular/router": "21.2.16",
16+
"@angular/animations": "22.0.0",
17+
"@angular/cdk": "22.0.0",
18+
"@angular/common": "22.0.0",
19+
"@angular/compiler": "22.0.0",
20+
"@angular/core": "22.0.0",
21+
"@angular/forms": "22.0.0",
22+
"@angular/platform-browser": "22.0.0",
23+
"@angular/platform-browser-dynamic": "22.0.0",
24+
"@angular/router": "22.0.0",
2525
"core-js": "^3.48.0",
2626
"rxjs": "catalog:",
2727
"tslib": "catalog:",

napi/angular-compiler/benchmarks/typedb-web/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@
1313
"benchmark:incremental": "tsx benchmark.ts --incremental"
1414
},
1515
"dependencies": {
16-
"@angular/animations": "21.2.16",
17-
"@angular/cdk": "21.2.14",
18-
"@angular/common": "21.2.16",
19-
"@angular/compiler": "21.2.16",
20-
"@angular/core": "21.2.16",
21-
"@angular/forms": "21.2.16",
22-
"@angular/material": "21.2.14",
23-
"@angular/platform-browser": "21.2.16",
24-
"@angular/platform-browser-dynamic": "21.2.16",
25-
"@angular/router": "21.2.16",
16+
"@angular/animations": "22.0.0",
17+
"@angular/cdk": "22.0.0",
18+
"@angular/common": "22.0.0",
19+
"@angular/compiler": "22.0.0",
20+
"@angular/core": "22.0.0",
21+
"@angular/forms": "22.0.0",
22+
"@angular/material": "22.0.0",
23+
"@angular/platform-browser": "22.0.0",
24+
"@angular/platform-browser-dynamic": "22.0.0",
25+
"@angular/router": "22.0.0",
2626
"@portabletext/to-html": "5.0.2",
2727
"@sanity/asset-utils": "2.3.0",
2828
"@sanity/image-url": "2.1.1",
@@ -32,15 +32,15 @@
3232
"interactjs": "1.10.27",
3333
"ngx-cookieconsent": "8.0.0",
3434
"pixi.js-legacy": "7.4.3",
35-
"posthog-js": "1.380.1",
35+
"posthog-js": "1.376.4",
3636
"prismjs": "1.30.0",
3737
"rxjs": "7.8.2",
3838
"tslib": "2.8.1",
3939
"zone.js": "0.16.2"
4040
},
4141
"devDependencies": {
4242
"@oxc-angular/vite": "workspace:^",
43-
"@sanity/types": "5.30.0",
43+
"@sanity/types": "5.28.0",
4444
"@types/d3-force": "3.0.10",
4545
"@types/fontfaceobserver": "2.1.3",
4646
"@types/node": "^22.19.3",

napi/angular-compiler/e2e/app/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
"dev": "vite"
88
},
99
"dependencies": {
10-
"@angular/common": "^21.2.2",
11-
"@angular/compiler": "^21.2.2",
12-
"@angular/core": "^21.2.2",
13-
"@angular/platform-browser": "^21.2.2",
10+
"@angular/common": "^22.0.0",
11+
"@angular/compiler": "^22.0.0",
12+
"@angular/core": "^22.0.0",
13+
"@angular/platform-browser": "^22.0.0",
1414
"rxjs": "catalog:",
1515
"tslib": "catalog:"
1616
},
1717
"devDependencies": {
18-
"@angular/compiler-cli": "^21.2.2",
18+
"@angular/compiler-cli": "^22.0.0",
1919
"@oxc-angular/vite": "workspace:^",
2020
"typescript": "catalog:",
2121
"vite": "catalog:"

napi/playground/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12-
"@angular/common": "^21.2.0",
13-
"@angular/compiler": "^21.2.0",
14-
"@angular/core": "^21.1.6",
15-
"@angular/forms": "^21.2.0",
16-
"@angular/platform-browser": "^21.2.0",
17-
"@angular/router": "^21.2.0",
12+
"@angular/common": "^22.0.0",
13+
"@angular/compiler": "^22.0.0",
14+
"@angular/core": "^22.0.0",
15+
"@angular/forms": "^22.0.0",
16+
"@angular/platform-browser": "^22.0.0",
17+
"@angular/router": "^22.0.0",
1818
"rxjs": "catalog:",
1919
"tslib": "catalog:"
2020
},
2121
"devDependencies": {
22-
"@angular/build": "^21.2.0",
23-
"@angular/compiler-cli": "^21.2.0",
22+
"@angular/build": "^22.0.0",
23+
"@angular/compiler-cli": "^22.0.0",
2424
"@oxc-angular/vite": "workspace:^",
2525
"@tailwindcss/vite": "catalog:",
2626
"@types/node": "catalog:",

0 commit comments

Comments
 (0)