tinymist_query/
package.rs1use std::path::PathBuf;
4
5use ecow::eco_format;
6use serde::{Deserialize, Serialize};
8use tinymist_world::package::PackageSpec;
9use typst::World;
10use typst::diag::{EcoString, StrResult};
11use typst::syntax::package::PackageManifest;
12use typst::syntax::{FileId, VirtualPath};
13
14use crate::LocalContext;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct PackageInfo {
19 pub path: PathBuf,
21 pub namespace: EcoString,
23 pub name: EcoString,
25 pub version: String,
27}
28
29impl From<(PathBuf, PackageSpec)> for PackageInfo {
30 fn from((path, spec): (PathBuf, PackageSpec)) -> Self {
31 Self {
32 path,
33 namespace: spec.namespace,
34 name: spec.name,
35 version: spec.version.to_string(),
36 }
37 }
38}
39
40pub fn get_manifest_id(spec: &PackageInfo) -> StrResult<FileId> {
42 Ok(FileId::new(
43 Some(PackageSpec {
44 namespace: spec.namespace.clone(),
45 name: spec.name.clone(),
46 version: spec.version.parse()?,
47 }),
48 VirtualPath::new("typst.toml"),
49 ))
50}
51
52pub fn get_manifest(world: &dyn World, toml_id: FileId) -> StrResult<PackageManifest> {
54 let toml_data = world
55 .file(toml_id)
56 .map_err(|err| eco_format!("failed to read package manifest ({})", err))?;
57
58 let string = std::str::from_utf8(&toml_data)
59 .map_err(|err| eco_format!("package manifest is not valid UTF-8 ({})", err))?;
60
61 toml::from_str(string)
62 .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
63}
64
65pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()> {
67 let toml_id = get_manifest_id(spec)?;
68 let manifest = ctx.get_manifest(toml_id)?;
69
70 let entry_point = toml_id.join(&manifest.package.entrypoint);
71
72 ctx.shared_().preload_package(entry_point);
73 Ok(())
74}
75
76#[cfg(feature = "local-registry")]
77pub fn list_package_by_namespace(
79 registry: &tinymist_world::package::registry::HttpRegistry,
80 ns: EcoString,
81) -> ecow::EcoVec<(PathBuf, PackageSpec)> {
82 use std::collections::HashSet;
83 use std::sync::OnceLock;
84
85 use ecow::eco_vec;
86 use parking_lot::Mutex;
87
88 trait IsDirFollowLinks {
89 fn is_dir_follow_links(&self) -> bool;
90 }
91
92 impl IsDirFollowLinks for PathBuf {
93 fn is_dir_follow_links(&self) -> bool {
94 self.canonicalize()
97 .map(|meta| meta.is_dir())
98 .unwrap_or(false)
99 }
100 }
101
102 fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
103 let err = match result {
104 Ok(value) => return Some(value),
105 Err(err) => err,
106 };
107
108 static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
109 let mut once = ONCE.get_or_init(Default::default).lock();
110 if once.insert(site) {
111 log::error!("failed to perform {site}: {err}");
112 }
113
114 None
115 }
116
117 let mut packages = eco_vec![];
121
122 log::info!(
123 "searching for packages in namespace {ns} in paths {:?}",
124 registry.paths()
125 );
126 for dir in registry.paths() {
127 let local_path = dir.join(ns.as_str());
128 if !local_path.exists() || !local_path.is_dir_follow_links() {
129 continue;
130 }
131 let Some(package_names) = once_log(std::fs::read_dir(local_path), "read local package")
134 else {
135 continue;
136 };
137 for package in package_names {
138 let Some(package) = once_log(package, "read package name") else {
139 continue;
140 };
141 if package.file_name().to_string_lossy().starts_with('.') {
142 continue;
143 }
144
145 let package_path = package.path();
146 if !package_path.is_dir_follow_links() {
147 continue;
148 }
149 let Some(versions) = once_log(std::fs::read_dir(package_path), "read package versions")
151 else {
152 continue;
153 };
154 for version in versions {
155 let Some(version) = once_log(version, "read package version") else {
156 continue;
157 };
158 if version.file_name().to_string_lossy().starts_with('.') {
159 continue;
160 }
161 let package_version_path = version.path();
162 if !package_version_path.is_dir_follow_links() {
163 continue;
164 }
165 let Some(version) = once_log(
166 version.file_name().to_string_lossy().parse(),
167 "parse package version",
168 ) else {
169 continue;
170 };
171 let spec = PackageSpec {
172 namespace: ns.clone(),
173 name: package.file_name().to_string_lossy().into(),
174 version,
175 };
176 packages.push((package_version_path, spec));
177 }
178 }
179 }
180
181 packages
182}