tinymist_query/
package.rs

1//! Package management tools.
2
3use std::path::PathBuf;
4
5use ecow::eco_format;
6#[cfg(feature = "local-registry")]
7use ecow::{EcoVec, eco_vec};
8// use reflexo_typst::typst::prelude::*;
9use serde::{Deserialize, Serialize};
10use tinymist_world::package::PackageSpec;
11use tinymist_world::package::registry::PackageIndexEntry;
12use typst::World;
13use typst::diag::{EcoString, StrResult};
14use typst::syntax::package::PackageManifest;
15use typst::syntax::{FileId, VirtualPath};
16
17use crate::LocalContext;
18
19/// Information about a package.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PackageInfo {
22    /// The path to the package if any.
23    pub path: PathBuf,
24    /// The namespace the package lives in.
25    pub namespace: EcoString,
26    /// The name of the package within its namespace.
27    pub name: EcoString,
28    /// The package's version.
29    pub version: String,
30}
31
32impl From<PackageIndexEntry> for PackageInfo {
33    fn from(entry: PackageIndexEntry) -> Self {
34        let spec = entry.spec();
35        Self {
36            path: entry.path.unwrap_or_default(),
37            namespace: spec.namespace,
38            name: spec.name,
39            version: spec.version.to_string(),
40        }
41    }
42}
43
44/// Parses the manifest of the package located at `package_path`.
45pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
46    Ok(FileId::new(
47        Some(PackageSpec {
48            namespace: spec.namespace.clone(),
49            name: spec.name.clone(),
50            version: spec.version.parse()?,
51        }),
52        VirtualPath::new("typst.toml"),
53    ))
54}
55
56/// Parses the manifest of the package located at `package_path`.
57pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
58    let toml_data = world
59        .file(toml_id)
60        .map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
61
62    let string = std::str::from_utf8(&toml_data)
63        .map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
64
65    toml::from_str(string)
66        .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
67}
68
69/// Check Package.
70pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
71    let toml_id = get_manifest_id(spec)?;
72    let manifest = ctx.get_manifest(toml_id)?;
73
74    let entry_point = toml_id.join(&manifest.package.entrypoint);
75
76    ctx.shared_().preload_package(entry_point);
77    Ok(())
78}
79
80/// A filter for packages.
81#[cfg(feature = "local-registry")]
82pub enum PackageFilter {
83    /// Filter for packages that match the given namespace.
84    For(EcoString),
85    /// Filter for packages that do not match the given namespace.
86    ExceptFor(EcoString),
87    /// Filter that matches all packages.
88    All,
89}
90
91#[cfg(feature = "local-registry")]
92/// Get the packages in namespaces and their descriptions.
93pub fn list_package(
94    world: &tinymist_project::LspWorld,
95    filter: PackageFilter,
96) -> EcoVec<PackageIndexEntry> {
97    trait IsDirFollowLinks {
98        fn is_dir_follow_links(&self) -> bool;
99    }
100
101    impl IsDirFollowLinks for PathBuf {
102        fn is_dir_follow_links(&self) -> bool {
103            // Although `canonicalize` is heavy, we must use it because `symlink_metadata`
104            // is not reliable.
105            self.canonicalize()
106                .map(|meta| meta.is_dir())
107                .unwrap_or(false)
108        }
109    }
110
111    let registry = &world.registry;
112
113    // search packages locally. We only search in the data
114    // directory and not the cache directory, because the latter is not
115    // intended for storage of local packages.
116    let mut packages = eco_vec![];
117
118    let paths = registry.paths();
119    log::info!("searching for packages in paths {paths:?}");
120
121    let mut search_in_dir = |local_path: PathBuf, ns: EcoString| {
122        if !local_path.exists() || !local_path.is_dir_follow_links() {
123            return;
124        }
125        // namespace/package_name/version
126        // 2. package_name
127        let Some(package_names) = once_log(std::fs::read_dir(local_path), "read local package")
128        else {
129            return;
130        };
131        for package in package_names {
132            let Some(package) = once_log(package, "read package name") else {
133                continue;
134            };
135            let package_name = EcoString::from(package.file_name().to_string_lossy());
136            if package_name.starts_with('.') {
137                continue;
138            }
139
140            let package_path = package.path();
141            if !package_path.is_dir_follow_links() {
142                continue;
143            }
144            // 3. version
145            let Some(versions) = once_log(std::fs::read_dir(package_path), "read package versions")
146            else {
147                continue;
148            };
149            for version in versions {
150                let Some(version_entry) = once_log(version, "read package version") else {
151                    continue;
152                };
153                if version_entry.file_name().to_string_lossy().starts_with('.') {
154                    continue;
155                }
156                let package_version_path = version_entry.path();
157                if !package_version_path.is_dir_follow_links() {
158                    continue;
159                }
160                let Some(version) = once_log(
161                    version_entry.file_name().to_string_lossy().parse(),
162                    "parse package version",
163                ) else {
164                    continue;
165                };
166                let spec = PackageSpec {
167                    namespace: ns.clone(),
168                    name: package_name.clone(),
169                    version,
170                };
171                let manifest_id = typst::syntax::FileId::new(
172                    Some(spec.clone()),
173                    typst::syntax::VirtualPath::new("typst.toml"),
174                );
175                let Some(manifest) =
176                    once_log(get_manifest(world, manifest_id), "read package manifest")
177                else {
178                    continue;
179                };
180                packages.push(PackageIndexEntry {
181                    namespace: ns.clone(),
182                    package: manifest.package,
183                    template: manifest.template,
184                    updated_at: None,
185                    path: Some(package_version_path),
186                });
187            }
188        }
189    };
190
191    for dir in paths {
192        let matching_ns = match &filter {
193            PackageFilter::For(ns) => {
194                let local_path = dir.join(ns.as_str());
195                search_in_dir(local_path, ns.clone());
196
197                continue;
198            }
199            PackageFilter::ExceptFor(ns) => Some(ns),
200            PackageFilter::All => None,
201        };
202
203        let Some(namespaces) = once_log(std::fs::read_dir(dir), "read package directory") else {
204            continue;
205        };
206        for dir in namespaces {
207            let Some(dir) = once_log(dir, "read ns directory") else {
208                continue;
209            };
210            let ns = dir.file_name();
211            let ns = ns.to_string_lossy();
212            if let Some(matching_ns) = &matching_ns
213                && matching_ns.as_str() == ns.as_ref()
214            {
215                continue;
216            }
217            let local_path = dir.path();
218            search_in_dir(local_path, ns.into());
219        }
220    }
221
222    packages
223}
224
225#[cfg(feature = "local-registry")]
226fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
227    use std::collections::HashSet;
228    use std::sync::OnceLock;
229
230    use parking_lot::Mutex;
231
232    let err = match result {
233        Ok(value) => return Some(value),
234        Err(err) => err,
235    };
236
237    static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
238    let mut once = ONCE.get_or_init(Default::default).lock();
239    if once.insert(site) {
240        log::error!("failed to perform {site}: {err}");
241    }
242
243    None
244}