Skip to main content

deps_deno/
types.rs

1//! Deno-specific dependency, version, and metadata types.
2
3use std::any::Any;
4use tower_lsp_server::ls_types::Range;
5
6/// A parsed Deno import, scheme-qualified (D2).
7///
8/// `name` is `"jsr:@std/fs"` or `"npm:react"` — never the bare form — since
9/// [`crate::registry::DenoRegistry`] can only route a lookup to the right registry if the
10/// scheme travels with the name.
11///
12/// # Examples
13///
14/// ```
15/// use deps_deno::types::{DenoDependency, DenoDependencySection};
16/// use tower_lsp_server::ls_types::{Position, Range};
17///
18/// let dep = DenoDependency {
19///     name: "jsr:@std/fs".into(),
20///     name_range: Range::new(Position::new(2, 4), Position::new(2, 15)),
21///     version_req: Some("^1.0".into()),
22///     version_range: Some(Range::new(Position::new(2, 17), Position::new(2, 21))),
23///     section: DenoDependencySection::Imports,
24/// };
25///
26/// assert_eq!(dep.name, "jsr:@std/fs");
27/// ```
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct DenoDependency {
30    /// Scheme-qualified package name (`"jsr:@std/fs"`, `"npm:react"`).
31    pub name: deps_core::PackageName,
32    /// LSP range of `name` within the manifest, including the `scheme:` prefix.
33    pub name_range: Range,
34    /// Version requirement, if the specifier carried one (`@1.2`, `@^18`).
35    pub version_req: Option<deps_core::VersionReq>,
36    /// LSP range of `version_req` within the manifest, if present.
37    pub version_range: Option<Range>,
38    /// Which section of the manifest this import was declared in.
39    pub section: DenoDependencySection,
40}
41
42deps_core::impl_dependency!(DenoDependency {
43    name: name,
44    name_range: name_range,
45    version: version_req,
46    version_range: version_range,
47});
48
49/// Section of a `deno.json` manifest a dependency was declared in.
50///
51/// Single-variant today (D8): only the `imports` map is parsed in the MVP — `scopes` and
52/// `importMap` are out of scope (spec §1). Kept as an enum rather than a unit struct so a
53/// future `Scopes` variant slots in without changing every call site.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum DenoDependencySection {
56    /// The `imports` map.
57    Imports,
58}
59
60/// A JSR package version, from `https://jsr.io/@{scope}/{pkg}/meta.json`.
61///
62/// `published_at` is populated directly from the same response `get_versions` already
63/// fetches (`meta.json`'s per-version `createdAt`) — unlike npm, no extra request is
64/// needed for freshness data (D10).
65///
66/// # Examples
67///
68/// ```
69/// use deps_deno::types::JsrVersion;
70///
71/// let version = JsrVersion {
72///     version: "1.0.24".into(),
73///     yanked: false,
74///     published_at: None,
75/// };
76///
77/// assert!(!version.yanked);
78/// ```
79#[derive(Debug, Clone)]
80pub struct JsrVersion {
81    /// The version string (e.g. `"1.0.24"`).
82    pub version: deps_core::ConcreteVersion,
83    /// Whether JSR marked this specific version as yanked.
84    pub yanked: bool,
85    /// When this version was published, parsed from `meta.json`'s `createdAt`.
86    pub published_at: Option<deps_core::PublishTime>,
87}
88
89// JSR mandates strict semver, so `node_semver` reliably exposes the
90// prerelease identifiers instead of falling back to deps-core's default
91// hyphen-substring heuristic (#322). A parse failure here (treated as
92// not-prerelease) is practically unreachable given that enforcement.
93deps_core::impl_version!(JsrVersion {
94    version: version,
95    status: |v: &JsrVersion| deps_core::RemovalStatus::from_yanked(v.yanked),
96    published_at: published_at,
97    prerelease: |v: &JsrVersion| {
98        node_semver::Version::parse(v.version.as_str()).is_ok_and(|parsed| parsed.is_prerelease())
99    },
100});
101
102/// A JSR package search result, from `https://api.jsr.io/packages?query=`.
103///
104/// `name` is already scheme-qualified (`"jsr:@scope/pkg"`, per D3) so a completion item
105/// built from it inserts a valid Deno specifier rather than a bare JSR package name.
106///
107/// # Examples
108///
109/// ```
110/// use deps_deno::types::JsrPackage;
111///
112/// let pkg = JsrPackage {
113///     name: "jsr:@std/fs".into(),
114///     description: Some("File system utilities".into()),
115///     repository: Some("https://github.com/denoland/std".into()),
116///     documentation: None,
117///     latest_version: "1.0.24".into(),
118/// };
119///
120/// assert_eq!(pkg.name, "jsr:@std/fs");
121/// ```
122#[derive(Debug, Clone)]
123pub struct JsrPackage {
124    /// Scheme-qualified package name (`"jsr:@scope/pkg"`).
125    pub name: deps_core::PackageName,
126    /// Package description, if JSR returned a non-empty one.
127    pub description: Option<String>,
128    /// Repository URL, derived from the search response's `githubRepository` field.
129    pub repository: Option<String>,
130    /// Documentation URL. Always `None` today: JSR's search response carries no separate
131    /// documentation link distinct from the package's own JSR page (`package_url`).
132    pub documentation: Option<String>,
133    /// Latest published version string.
134    pub latest_version: deps_core::ConcreteVersion,
135}
136
137deps_core::impl_metadata!(JsrPackage {
138    name: name,
139    description: description,
140    repository: repository,
141    documentation: documentation,
142    latest_version: latest_version,
143});
144
145/// Wraps a `Box<dyn Metadata>` returned by `deps_npm::NpmRegistry::search`.
146///
147/// Re-prefixes its [`name`](deps_core::Metadata::name) with the `npm:` scheme (D3) so a
148/// completion item built from it inserts a valid Deno specifier rather than a bare npm
149/// package name.
150///
151/// Needed because `Metadata::name` cannot be mutated in place on a `Box<dyn Metadata>` —
152/// this forwards every other method to the wrapped value unchanged.
153pub struct DenoMetadata {
154    name: deps_core::PackageName,
155    inner: Box<dyn deps_core::Metadata>,
156}
157
158impl DenoMetadata {
159    /// Wraps `inner`, overriding its reported name with the already scheme-qualified
160    /// `name`.
161    #[must_use]
162    pub fn new(name: deps_core::PackageName, inner: Box<dyn deps_core::Metadata>) -> Self {
163        Self { name, inner }
164    }
165}
166
167impl deps_core::Metadata for DenoMetadata {
168    fn name(&self) -> &deps_core::PackageName {
169        &self.name
170    }
171
172    fn description(&self) -> Option<&str> {
173        self.inner.description()
174    }
175
176    fn repository(&self) -> Option<&str> {
177        self.inner.repository()
178    }
179
180    fn documentation(&self) -> Option<&str> {
181        self.inner.documentation()
182    }
183
184    fn latest_version(&self) -> &deps_core::ConcreteVersion {
185        self.inner.latest_version()
186    }
187
188    fn as_any(&self) -> &dyn Any {
189        self
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use deps_core::{Metadata, Version};
197    use tower_lsp_server::ls_types::Position;
198
199    #[test]
200    fn test_deno_dependency_creation() {
201        let dep = DenoDependency {
202            name: "jsr:@std/fs".into(),
203            name_range: Range::new(Position::new(0, 0), Position::new(0, 11)),
204            version_req: Some("^1.0".into()),
205            version_range: Some(Range::new(Position::new(0, 13), Position::new(0, 17))),
206            section: DenoDependencySection::Imports,
207        };
208
209        assert_eq!(dep.name, "jsr:@std/fs");
210        assert_eq!(dep.version_req, Some("^1.0".into()));
211    }
212
213    #[test]
214    fn test_jsr_version_trait() {
215        let version = JsrVersion {
216            version: "1.0.24".into(),
217            yanked: true,
218            published_at: None,
219        };
220
221        assert_eq!(version.version_string().as_str(), "1.0.24");
222        assert!(version.removal_status().blocks_resolution());
223    }
224
225    #[test]
226    fn test_jsr_version_is_prerelease() {
227        let stable = JsrVersion {
228            version: "1.0.24".into(),
229            yanked: false,
230            published_at: None,
231        };
232        let prerelease = JsrVersion {
233            version: "1.0.24-rc.1".into(),
234            yanked: false,
235            published_at: None,
236        };
237
238        assert!(!stable.is_prerelease());
239        assert!(stable.is_stable());
240        assert!(prerelease.is_prerelease());
241        assert!(!prerelease.is_stable());
242    }
243
244    #[test]
245    fn test_jsr_package_metadata_trait() {
246        let pkg = JsrPackage {
247            name: "jsr:@std/fs".into(),
248            description: Some("File system utilities".into()),
249            repository: Some("https://github.com/denoland/std".into()),
250            documentation: None,
251            latest_version: "1.0.24".into(),
252        };
253
254        assert_eq!(pkg.name(), "jsr:@std/fs");
255        assert_eq!(pkg.description(), Some("File system utilities"));
256        assert_eq!(pkg.repository(), Some("https://github.com/denoland/std"));
257        assert_eq!(pkg.documentation(), None);
258        assert_eq!(pkg.latest_version().as_str(), "1.0.24");
259    }
260
261    #[test]
262    fn test_deno_metadata_reprefixes_name_and_forwards_everything_else() {
263        struct Inner;
264        impl Metadata for Inner {
265            fn name(&self) -> &deps_core::PackageName {
266                static NAME: std::sync::LazyLock<deps_core::PackageName> =
267                    std::sync::LazyLock::new(|| deps_core::PackageName::new("react"));
268                &NAME
269            }
270            fn description(&self) -> Option<&str> {
271                Some("A JS library")
272            }
273            fn repository(&self) -> Option<&str> {
274                Some("facebook/react")
275            }
276            fn documentation(&self) -> Option<&str> {
277                Some("https://react.dev")
278            }
279            fn latest_version(&self) -> &deps_core::ConcreteVersion {
280                static VERSION: std::sync::LazyLock<deps_core::ConcreteVersion> =
281                    std::sync::LazyLock::new(|| deps_core::ConcreteVersion::new("18.3.1"));
282                &VERSION
283            }
284            fn as_any(&self) -> &dyn Any {
285                self
286            }
287        }
288
289        let wrapped = DenoMetadata::new(
290            deps_core::PackageName::new("npm:react"),
291            Box::new(Inner) as Box<dyn Metadata>,
292        );
293
294        assert_eq!(wrapped.name(), "npm:react");
295        assert_eq!(wrapped.description(), Some("A JS library"));
296        assert_eq!(wrapped.repository(), Some("facebook/react"));
297        assert_eq!(wrapped.documentation(), Some("https://react.dev"));
298        assert_eq!(wrapped.latest_version().as_str(), "18.3.1");
299    }
300}