tinymist_query/
package.rs1use std::path::PathBuf;
4
5use ecow::eco_format;
6#[cfg(feature = "local-registry")]
7use ecow::{EcoVec, eco_vec};
8use 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#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PackageInfo {
22 pub path: PathBuf,
24 pub namespace: EcoString,
26 pub name: EcoString,
28 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
44pub 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
56pub 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
69pub 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#[cfg(feature = "local-registry")]
81pub fn list_package(
83 world: &tinymist_project::LspWorld,
84 ns: Option<EcoString>,
85) -> EcoVec<PackageIndexEntry> {
86 trait IsDirFollowLinks {
87 fn is_dir_follow_links(&self) -> bool;
88 }
89
90 impl IsDirFollowLinks for PathBuf {
91 fn is_dir_follow_links(&self) -> bool {
92 self.canonicalize()
95 .map(|meta| meta.is_dir())
96 .unwrap_or(false)
97 }
98 }
99
100 let registry = &world.registry;
101
102 let mut packages = eco_vec![];
106
107 log::info!(
108 "searching for packages in namespace {ns:?} in paths {:?}",
109 registry.paths()
110 );
111
112 let mut search_in_dir = |local_path: PathBuf, ns: EcoString| {
113 if !local_path.exists() || !local_path.is_dir_follow_links() {
114 return;
115 }
116 let Some(package_names) = once_log(std::fs::read_dir(local_path), "read local package")
119 else {
120 return;
121 };
122 for package in package_names {
123 let Some(package) = once_log(package, "read package name") else {
124 continue;
125 };
126 let package_name = EcoString::from(package.file_name().to_string_lossy());
127 if package_name.starts_with('.') {
128 continue;
129 }
130
131 let package_path = package.path();
132 if !package_path.is_dir_follow_links() {
133 continue;
134 }
135 let Some(versions) = once_log(std::fs::read_dir(package_path), "read package versions")
137 else {
138 continue;
139 };
140 for version in versions {
141 let Some(version_entry) = once_log(version, "read package version") else {
142 continue;
143 };
144 if version_entry.file_name().to_string_lossy().starts_with('.') {
145 continue;
146 }
147 let package_version_path = version_entry.path();
148 if !package_version_path.is_dir_follow_links() {
149 continue;
150 }
151 let Some(version) = once_log(
152 version_entry.file_name().to_string_lossy().parse(),
153 "parse package version",
154 ) else {
155 continue;
156 };
157 let spec = PackageSpec {
158 namespace: ns.clone(),
159 name: package_name.clone(),
160 version,
161 };
162 let manifest_id = typst::syntax::FileId::new(
163 Some(spec.clone()),
164 typst::syntax::VirtualPath::new("typst.toml"),
165 );
166 let Some(manifest) =
167 once_log(get_manifest(world, manifest_id), "read package manifest")
168 else {
169 continue;
170 };
171 packages.push(PackageIndexEntry {
172 namespace: ns.clone(),
173 package: manifest.package,
174 template: manifest.template,
175 updated_at: None,
176 path: Some(package_version_path),
177 });
178 }
179 }
180 };
181
182 for dir in registry.paths() {
183 if let Some(ns) = &ns {
184 let local_path = dir.join(ns.as_str());
185 search_in_dir(local_path, ns.clone());
186 } else {
187 let Some(namespaces) = once_log(std::fs::read_dir(dir), "read package directory")
188 else {
189 continue;
190 };
191 for dir in namespaces {
192 let Some(dir) = once_log(dir, "read ns directory") else {
193 continue;
194 };
195 let local_path = dir.path();
196 search_in_dir(local_path, dir.file_name().to_string_lossy().into());
197 }
198 }
199 }
200
201 packages
202}
203
204#[cfg(feature = "local-registry")]
205fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
206 use std::collections::HashSet;
207 use std::sync::OnceLock;
208
209 use parking_lot::Mutex;
210
211 let err = match result {
212 Ok(value) => return Some(value),
213 Err(err) => err,
214 };
215
216 static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
217 let mut once = ONCE.get_or_init(Default::default).lock();
218 if once.insert(site) {
219 log::error!("failed to perform {site}: {err}");
220 }
221
222 None
223}