Skip to content

Commit d82674d

Browse files
Brooooooklynclaude
andauthored
fix: linker uses version-aware standalone defaulting for pre-v19 libraries (#90)
The linker unconditionally defaulted `standalone` to `true` when `isStandalone` was absent from partial declarations. Angular v12–v18 libraries default to `false`; only v19+ defaults to `true`. This caused NgModule-declared components from older libraries (e.g. PrimeNG v12) to be incorrectly marked standalone, breaking directive resolution (NG0303). Add `get_default_standalone_value()` using the `semver` crate to read the `version` field and return `major >= 19`. The special `0.0.0-PLACEHOLDER` dev sentinel also defaults to `true`, matching the Angular reference linker. - Fix #87 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7208cd7 commit d82674d

File tree

3 files changed

+213
-3
lines changed

3 files changed

+213
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_angular_compiler/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ rustc-hash = { workspace = true }
2828
indexmap = { workspace = true }
2929
oxc_resolver = { version = "11", optional = true }
3030
pathdiff = { version = "0.2", optional = true }
31+
semver = "1.0.27"
3132

3233
[features]
3334
default = []

crates/oxc_angular_compiler/src/linker/mod.rs

Lines changed: 211 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,21 @@ fn get_bool_property(obj: &ObjectExpression<'_>, name: &str) -> Option<bool> {
450450
None
451451
}
452452

453+
/// Determine the default value for `standalone` based on the declaration's `version` field.
454+
/// Angular v19+ defaults to `true`; earlier versions default to `false`.
455+
/// The special placeholder version `"0.0.0-PLACEHOLDER"` (used in dev builds) defaults to `true`.
456+
fn get_default_standalone_value(meta: &ObjectExpression<'_>) -> bool {
457+
if let Some(version_str) = get_string_property(meta, "version") {
458+
if version_str == "0.0.0-PLACEHOLDER" {
459+
return true;
460+
}
461+
if let Ok(version) = semver::Version::parse(version_str) {
462+
return version.major >= 19;
463+
}
464+
}
465+
true // If we can't determine the version, default to true (latest behavior)
466+
}
467+
453468
/// Extract the `deps` array from a factory metadata object and generate inject calls.
454469
fn extract_deps_source(obj: &ObjectExpression<'_>, source: &str, ns: &str) -> String {
455470
for prop in &obj.properties {
@@ -821,7 +836,8 @@ fn link_pipe(
821836
) -> Option<String> {
822837
let pipe_name = get_string_property(meta, "name")?;
823838
let pure = get_property_source(meta, "pure", source).unwrap_or("true");
824-
let standalone = get_property_source(meta, "isStandalone", source).unwrap_or("true");
839+
let standalone = get_property_source(meta, "isStandalone", source)
840+
.unwrap_or_else(|| if get_default_standalone_value(meta) { "true" } else { "false" });
825841

826842
Some(format!(
827843
"{ns}.\u{0275}\u{0275}definePipe({{ name: \"{pipe_name}\", type: {type_name}, pure: {pure}, standalone: {standalone} }})"
@@ -1011,7 +1027,8 @@ fn link_directive(
10111027
if let Some(export_as) = get_property_source(meta, "exportAs", source) {
10121028
parts.push(format!("exportAs: {export_as}"));
10131029
}
1014-
let standalone = get_bool_property(meta, "isStandalone").unwrap_or(true);
1030+
let standalone = get_bool_property(meta, "isStandalone")
1031+
.unwrap_or_else(|| get_default_standalone_value(meta));
10151032
parts.push(format!("standalone: {standalone}"));
10161033

10171034
if get_bool_property(meta, "isSignal") == Some(true) {
@@ -1430,7 +1447,8 @@ fn link_component(
14301447
}
14311448

14321449
// 11. standalone
1433-
let standalone = get_bool_property(meta, "isStandalone").unwrap_or(true);
1450+
let standalone = get_bool_property(meta, "isStandalone")
1451+
.unwrap_or_else(|| get_default_standalone_value(meta));
14341452
parts.push(format!("standalone: {standalone}"));
14351453

14361454
// 11b. signals
@@ -2560,4 +2578,194 @@ MyComp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.
25602578
"InheritDefinitionFeature must come before NgOnChangesFeature"
25612579
);
25622580
}
2581+
2582+
// === Issue #87: Version-aware standalone defaulting ===
2583+
2584+
#[test]
2585+
fn test_link_component_v12_defaults_standalone_false() {
2586+
let allocator = Allocator::default();
2587+
let code = r#"
2588+
import * as i0 from "@angular/core";
2589+
class MyComponent {}
2590+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div>Hello</div>" });
2591+
"#;
2592+
let result = link(&allocator, code, "test.mjs");
2593+
assert!(result.linked);
2594+
assert!(
2595+
result.code.contains("standalone: false"),
2596+
"v12 component without isStandalone should default to false, got:\n{}",
2597+
result.code
2598+
);
2599+
}
2600+
2601+
#[test]
2602+
fn test_link_component_v18_defaults_standalone_false() {
2603+
let allocator = Allocator::default();
2604+
let code = r#"
2605+
import * as i0 from "@angular/core";
2606+
class MyComponent {}
2607+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.0", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div>Hello</div>" });
2608+
"#;
2609+
let result = link(&allocator, code, "test.mjs");
2610+
assert!(result.linked);
2611+
assert!(
2612+
result.code.contains("standalone: false"),
2613+
"v18 component without isStandalone should default to false, got:\n{}",
2614+
result.code
2615+
);
2616+
}
2617+
2618+
#[test]
2619+
fn test_link_component_v19_defaults_standalone_true() {
2620+
let allocator = Allocator::default();
2621+
let code = r#"
2622+
import * as i0 from "@angular/core";
2623+
class MyComponent {}
2624+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.0", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div>Hello</div>" });
2625+
"#;
2626+
let result = link(&allocator, code, "test.mjs");
2627+
assert!(result.linked);
2628+
assert!(
2629+
result.code.contains("standalone: true"),
2630+
"v19 component without isStandalone should default to true, got:\n{}",
2631+
result.code
2632+
);
2633+
}
2634+
2635+
#[test]
2636+
fn test_link_component_v20_defaults_standalone_true() {
2637+
let allocator = Allocator::default();
2638+
let code = r#"
2639+
import * as i0 from "@angular/core";
2640+
class MyComponent {}
2641+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.0", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div>Hello</div>" });
2642+
"#;
2643+
let result = link(&allocator, code, "test.mjs");
2644+
assert!(result.linked);
2645+
assert!(
2646+
result.code.contains("standalone: true"),
2647+
"v20 component without isStandalone should default to true, got:\n{}",
2648+
result.code
2649+
);
2650+
}
2651+
2652+
#[test]
2653+
fn test_link_component_placeholder_defaults_standalone_true() {
2654+
let allocator = Allocator::default();
2655+
let code = r#"
2656+
import * as i0 from "@angular/core";
2657+
class MyComponent {}
2658+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div>Hello</div>" });
2659+
"#;
2660+
let result = link(&allocator, code, "test.mjs");
2661+
assert!(result.linked);
2662+
assert!(
2663+
result.code.contains("standalone: true"),
2664+
"0.0.0-PLACEHOLDER component without isStandalone should default to true, got:\n{}",
2665+
result.code
2666+
);
2667+
}
2668+
2669+
#[test]
2670+
fn test_link_component_explicit_standalone_overrides_version() {
2671+
let allocator = Allocator::default();
2672+
// v12 but explicitly standalone: true
2673+
let code = r#"
2674+
import * as i0 from "@angular/core";
2675+
class MyComponent {}
2676+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: MyComponent, selector: "my-comp", isStandalone: true, template: "<div>Hello</div>" });
2677+
"#;
2678+
let result = link(&allocator, code, "test.mjs");
2679+
assert!(result.linked);
2680+
assert!(
2681+
result.code.contains("standalone: true"),
2682+
"Explicit isStandalone: true should override version default, got:\n{}",
2683+
result.code
2684+
);
2685+
}
2686+
2687+
#[test]
2688+
fn test_link_directive_v12_defaults_standalone_false() {
2689+
let allocator = Allocator::default();
2690+
let code = r#"
2691+
import * as i0 from "@angular/core";
2692+
class NgIf {}
2693+
NgIf.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: NgIf, selector: "[ngIf]" });
2694+
"#;
2695+
let result = link(&allocator, code, "common.mjs");
2696+
assert!(result.linked);
2697+
assert!(
2698+
result.code.contains("standalone: false"),
2699+
"v12 directive without isStandalone should default to false, got:\n{}",
2700+
result.code
2701+
);
2702+
}
2703+
2704+
#[test]
2705+
fn test_link_directive_v19_defaults_standalone_true() {
2706+
let allocator = Allocator::default();
2707+
let code = r#"
2708+
import * as i0 from "@angular/core";
2709+
class MyDir {}
2710+
MyDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", ngImport: i0, type: MyDir, selector: "[myDir]" });
2711+
"#;
2712+
let result = link(&allocator, code, "test.mjs");
2713+
assert!(result.linked);
2714+
assert!(
2715+
result.code.contains("standalone: true"),
2716+
"v19 directive without isStandalone should default to true, got:\n{}",
2717+
result.code
2718+
);
2719+
}
2720+
2721+
#[test]
2722+
fn test_link_pipe_v12_defaults_standalone_false() {
2723+
let allocator = Allocator::default();
2724+
let code = r#"
2725+
import * as i0 from "@angular/core";
2726+
class AsyncPipe {}
2727+
AsyncPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "12.0.0", version: "12.0.5", ngImport: i0, type: AsyncPipe, name: "async" });
2728+
"#;
2729+
let result = link(&allocator, code, "common.mjs");
2730+
assert!(result.linked);
2731+
assert!(
2732+
result.code.contains("standalone: false"),
2733+
"v12 pipe without isStandalone should default to false, got:\n{}",
2734+
result.code
2735+
);
2736+
}
2737+
2738+
#[test]
2739+
fn test_link_pipe_v19_defaults_standalone_true() {
2740+
let allocator = Allocator::default();
2741+
let code = r#"
2742+
import * as i0 from "@angular/core";
2743+
class AsyncPipe {}
2744+
AsyncPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.0.0", ngImport: i0, type: AsyncPipe, name: "async" });
2745+
"#;
2746+
let result = link(&allocator, code, "common.mjs");
2747+
assert!(result.linked);
2748+
assert!(
2749+
result.code.contains("standalone: true"),
2750+
"v19 pipe without isStandalone should default to true, got:\n{}",
2751+
result.code
2752+
);
2753+
}
2754+
2755+
#[test]
2756+
fn test_link_component_v19_prerelease_defaults_standalone_true() {
2757+
let allocator = Allocator::default();
2758+
let code = r#"
2759+
import * as i0 from "@angular/core";
2760+
class MyComponent {}
2761+
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.0-rc.1", ngImport: i0, type: MyComponent, selector: "my-comp", template: "<div>Hello</div>" });
2762+
"#;
2763+
let result = link(&allocator, code, "test.mjs");
2764+
assert!(result.linked);
2765+
assert!(
2766+
result.code.contains("standalone: true"),
2767+
"v19.0.0-rc.1 component without isStandalone should default to true, got:\n{}",
2768+
result.code
2769+
);
2770+
}
25632771
}

0 commit comments

Comments
 (0)