diff --git a/crates/oxc_angular_compiler/src/component/metadata.rs b/crates/oxc_angular_compiler/src/component/metadata.rs index 2a6440eb1..d2eb46a01 100644 --- a/crates/oxc_angular_compiler/src/component/metadata.rs +++ b/crates/oxc_angular_compiler/src/component/metadata.rs @@ -10,6 +10,39 @@ use super::dependency::R3DependencyMetadata; use crate::directive::R3InputMetadata; use crate::output::ast::OutputExpression; +/// Angular prerelease tag (`-next.N` / `-rc.N` in the published version). +/// +/// Ordering follows Angular's release cadence: `next.N` builds come off +/// `main` weekly, `rc.N` builds are cut from the release branch later, so +/// `Next` < `Rc` < release. Unrecognized tags map to [`Prerelease::Other`], +/// which sorts below both so an unknown suffix is treated as earlier than +/// any published prerelease of the same `major.minor.patch`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Prerelease { + /// Any prerelease tag that is not `next.N` or `rc.N`. + Other, + /// A `next.N` (main-branch snapshot) prerelease. + Next(u32), + /// An `rc.N` (release candidate) prerelease. + Rc(u32), +} + +impl Prerelease { + /// Parse a prerelease tag like `next.0` or `rc.1`. + /// + /// Unknown tags are kept as [`Prerelease::Other`] rather than dropped so + /// they still sort below the release of the same triple. + pub fn parse(tag: &str) -> Self { + let (channel, ordinal) = tag.split_once('.').unwrap_or((tag, "0")); + let ordinal: u32 = ordinal.parse().unwrap_or(0); + match channel { + "next" => Self::Next(ordinal), + "rc" => Self::Rc(ordinal), + _ => Self::Other, + } + } +} + /// Angular version information for feature detection. /// /// Used to determine version-conditional behavior like the default value @@ -22,12 +55,21 @@ pub struct AngularVersion { pub minor: u32, /// Patch version number (e.g., 0 for Angular 19.0.0). pub patch: u32, + /// Prerelease tag (e.g., `Next(0)` for `22.3.0-next.0`), if any. + pub prerelease: Option, } impl AngularVersion { - /// Create a new AngularVersion. - pub fn new(major: u32, minor: u32, patch: u32) -> Self { - Self { major, minor, patch } + /// Create a new AngularVersion for a stable release. + pub const fn new(major: u32, minor: u32, patch: u32) -> Self { + Self { major, minor, patch, prerelease: None } + } + + /// Set the prerelease tag (e.g., `Prerelease::Next(0)` for `-next.0`). + #[must_use] + pub fn with_prerelease(mut self, prerelease: Prerelease) -> Self { + self.prerelease = Some(prerelease); + self } /// Check if this version supports implicit standalone default (v19.0.0+). @@ -159,24 +201,89 @@ impl AngularVersion { false } + /// Check if this version is `major.minor.patch` or newer. + /// + /// Compares the release triple only: a prerelease of `22.3.0` counts as + /// `>= 22.2.2` even though it predates the `22.3.0` stable. Callers gating + /// on a fix that landed mid-`next` cycle must compare the stored + /// prerelease against the cutoff explicitly (see + /// [`AngularVersion::at_22_2_2_cutoff_or_later`]). + pub fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool { + (self.major, self.minor, self.patch) >= (major, minor, patch) + } + + /// True when the fixes shipped in `22.2.2` / `22.3.0-next.1` apply. + /// + /// Every prerelease of `22.3.0` before `next.1` — `next.0`, plus any + /// unrecognized tag which parses to [`Prerelease::Other`] and sorts below + /// `next`/`rc` — was cut before the fixes landed on `main`, so it must + /// compare strictly below the `next.1` cutoff rather than be excluded by + /// ordinal. [`AngularVersion::Rc`] sorts after `Next`, so `rc` builds + /// take the fixed path. + fn at_22_2_2_cutoff_or_later(&self) -> bool { + if self.major == 22 && self.minor == 3 && self.patch == 0 { + return match self.prerelease { + None => true, + Some(prerelease) => prerelease >= Prerelease::Next(1), + }; + } + self.is_at_least(22, 2, 2) + } + + /// Check if this version drops `img|src` and `video|src` from the URL + /// security context (v22.2.2+ / v22.3.0-next.1+). + /// + /// `angular/angular@439ff1bdf4` ("refactor(compiler): remove img/video src + /// attribute from the URL security context") landed only on the v22 line, + /// so a bound `src` on ``/`