feat: compile tinymist-query for wasm32 target (#1947)

- #2025 
- #2026

---------

Co-authored-by: Momijiichigo <ichigomomiji436@gmail.com>
This commit is contained in:
Myriad-Dreamin 2025-08-11 09:35:07 +08:00 committed by GitHub
parent b102b42d2c
commit 79f68dc94d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 191 additions and 88 deletions

3
Cargo.lock generated
View file

@ -4250,7 +4250,8 @@ dependencies = [
"console_error_panic_hook",
"js-sys",
"reflexo-typst",
"tinymist-world",
"tinymist-project",
"tinymist-query",
"vergen",
"wasm-bindgen",
]

View file

@ -22,7 +22,7 @@ web = [
"js-sys",
"console_error_panic_hook",
"no-content-hint",
"tinymist-world/web",
"tinymist-project/web",
"reflexo-typst/web",
]
@ -31,7 +31,8 @@ no-content-hint = ["reflexo-typst/no-content-hint"]
[dependencies]
wasm-bindgen = { version = "0.2.100", optional = true }
js-sys = { version = "0.3.77", optional = true }
tinymist-world.workspace = true
tinymist-project.workspace = true
tinymist-query.workspace = true
reflexo-typst.workspace = true
console_error_panic_hook = { version = "0.1.2", optional = true }

View file

@ -40,9 +40,11 @@ notify.workspace = true
fonts = ["typst-assets/fonts"]
no-content-hint = ["tinymist-task/no-content-hint"]
lsp = ["system", "toml"]
lsp = ["toml"]
# "system",
system = ["tinymist-std/system", "tinymist-world/system"]
web = ["tinymist-std/web", "tinymist-world/web"]
[lints]
workspace = true

View file

@ -11,22 +11,20 @@ mod lock;
mod lsp;
#[cfg(feature = "system")]
mod watch;
#[cfg(feature = "system")]
pub mod world;
pub use args::*;
pub use compiler::*;
pub use entry::*;
pub use model::*;
#[cfg(feature = "lsp")]
pub use lock::*;
pub use model::*;
pub use world::*;
#[cfg(feature = "lsp")]
pub use lsp::*;
#[cfg(feature = "system")]
pub use watch::*;
#[cfg(feature = "system")]
pub use world::*;
pub use tinymist_world::{CompileSignal, CompileSnapshot, ProjectInsId};

View file

@ -1,24 +1,20 @@
use std::path::Path;
use std::{borrow::Cow, sync::Arc};
use std::sync::Arc;
use tinymist_std::error::prelude::*;
use tinymist_std::{bail, ImmutPath};
use tinymist_std::ImmutPath;
use tinymist_task::ExportTarget;
use tinymist_world::config::CompileFontOpts;
use tinymist_world::font::system::SystemFontSearcher;
use tinymist_world::package::{registry::HttpRegistry, RegistryPathMapper};
use tinymist_world::vfs::{system::SystemAccessModel, Vfs};
use tinymist_world::package::RegistryPathMapper;
use tinymist_world::vfs::Vfs;
use tinymist_world::{args::*, WorldComputeGraph};
use tinymist_world::{
CompileSnapshot, CompilerFeat, CompilerUniverse, CompilerWorld, EntryOpts, EntryState,
};
use typst::diag::FileResult;
use typst::foundations::{Bytes, Dict, Str, Value};
use typst::foundations::{Bytes, Dict};
use typst::utils::LazyHash;
use typst::Features;
use crate::ProjectInput;
use crate::world::font::FontResolverImpl;
use crate::{CompiledArtifact, Interrupt};
@ -33,7 +29,11 @@ impl CompilerFeat for LspCompilerFeat {
/// It accesses a physical file system.
type AccessModel = DynAccessModel;
/// It performs native HTTP requests for fetching package data.
type Registry = HttpRegistry;
#[cfg(feature = "system")]
type Registry = tinymist_world::package::registry::HttpRegistry;
// todo: registry in browser
#[cfg(not(feature = "system"))]
type Registry = tinymist_world::package::registry::DummyRegistry;
}
/// LSP universe that spawns LSP worlds.
@ -59,6 +59,7 @@ pub trait WorldProvider {
fn resolve(&self) -> Result<LspUniverse>;
}
#[cfg(feature = "system")]
impl WorldProvider for CompileOnceArgs {
fn resolve(&self) -> Result<LspUniverse> {
let entry = self.entry()?.try_into()?;
@ -78,7 +79,7 @@ impl WorldProvider for CompileOnceArgs {
packages,
fonts,
self.creation_timestamp,
DynAccessModel(Arc::new(SystemAccessModel {})),
DynAccessModel(Arc::new(tinymist_world::vfs::system::SystemAccessModel {})),
))
}
@ -129,8 +130,11 @@ impl WorldProvider for CompileOnceArgs {
}
// todo: merge me with the above impl
impl WorldProvider for (ProjectInput, ImmutPath) {
#[cfg(feature = "system")]
impl WorldProvider for (crate::ProjectInput, ImmutPath) {
fn resolve(&self) -> Result<LspUniverse> {
use typst::foundations::{Str, Value};
let (proj, lock_dir) = self;
let entry = self.entry()?.try_into()?;
let inputs = proj
@ -172,7 +176,7 @@ impl WorldProvider for (ProjectInput, ImmutPath) {
packages,
Arc::new(fonts),
None, // creation_timestamp - not available in project file context
DynAccessModel(Arc::new(SystemAccessModel {})),
DynAccessModel(Arc::new(tinymist_world::vfs::system::SystemAccessModel {})),
))
}
@ -207,6 +211,11 @@ impl WorldProvider for (ProjectInput, ImmutPath) {
}
}
#[cfg(not(feature = "system"))]
type LspRegistry = tinymist_world::package::registry::DummyRegistry;
#[cfg(feature = "system")]
type LspRegistry = tinymist_world::package::registry::HttpRegistry;
/// Builder for LSP universe.
pub struct LspUniverseBuilder;
@ -219,7 +228,7 @@ impl LspUniverseBuilder {
export_target: ExportTarget,
features: Features,
inputs: ImmutDict,
package_registry: HttpRegistry,
package_registry: LspRegistry,
font_resolver: Arc<FontResolverImpl>,
creation_timestamp: Option<i64>,
access_model: DynAccessModel,
@ -246,38 +255,54 @@ impl LspUniverseBuilder {
}
/// Resolve fonts from given options.
#[cfg(feature = "system")]
pub fn only_embedded_fonts() -> Result<FontResolverImpl> {
let mut searcher = SystemFontSearcher::new();
searcher.resolve_opts(CompileFontOpts {
let mut searcher = tinymist_world::font::system::SystemFontSearcher::new();
searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
font_paths: vec![],
no_system_fonts: true,
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
with_embedded_fonts: typst_assets::fonts()
.map(std::borrow::Cow::Borrowed)
.collect(),
})?;
Ok(searcher.build())
}
/// Resolve fonts from given options.
#[cfg(feature = "system")]
pub fn resolve_fonts(args: CompileFontArgs) -> Result<FontResolverImpl> {
let mut searcher = SystemFontSearcher::new();
searcher.resolve_opts(CompileFontOpts {
let mut searcher = tinymist_world::font::system::SystemFontSearcher::new();
searcher.resolve_opts(tinymist_world::config::CompileFontOpts {
font_paths: args.font_paths,
no_system_fonts: args.ignore_system_fonts,
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),
with_embedded_fonts: typst_assets::fonts()
.map(std::borrow::Cow::Borrowed)
.collect(),
})?;
Ok(searcher.build())
}
/// Resolve package registry from given options.
/// Resolves package registry from given options.
#[cfg(feature = "system")]
pub fn resolve_package(
cert_path: Option<ImmutPath>,
args: Option<&CompilePackageArgs>,
) -> HttpRegistry {
HttpRegistry::new(
) -> tinymist_world::package::registry::HttpRegistry {
tinymist_world::package::registry::HttpRegistry::new(
cert_path,
args.and_then(|args| Some(args.package_path.clone()?.into())),
args.and_then(|args| Some(args.package_cache_path.clone()?.into())),
)
}
/// Resolves package registry from given options.
#[cfg(not(feature = "system"))]
pub fn resolve_package(
_cert_path: Option<ImmutPath>,
_args: Option<&CompilePackageArgs>,
) -> tinymist_world::package::registry::DummyRegistry {
tinymist_world::package::registry::DummyRegistry
}
}
/// Access model for LSP universe and worlds.

View file

@ -4,8 +4,11 @@ pub use tinymist_world as base;
pub use tinymist_world::args::*;
pub use tinymist_world::config::CompileFontOpts;
pub use tinymist_world::entry::*;
pub use tinymist_world::{diag, font, package, system, vfs};
pub use tinymist_world::{diag, font, package, vfs};
pub use tinymist_world::{
with_main, CompilerUniverse, CompilerWorld, DiagnosticFormat, EntryOpts, EntryState,
RevisingUniverse, SourceWorld, TaskInputs,
};
#[cfg(feature = "system")]
pub use tinymist_world::system;

View file

@ -209,26 +209,28 @@ impl TypeCompletionWorker<'_, '_, '_, '_> {
BuiltinTy::TextSize => return None,
BuiltinTy::TextLang => {
for (&key, desc) in rust_iso639::ALL_MAP.entries() {
let detail = eco_format!("An ISO 639-1/2/3 language code, {}.", desc.name);
let detail =
eco_format!("An ISO 639-1/2/3 language code, {}.", desc.get_name());
self.base.push_completion(Completion {
kind: CompletionKind::Syntax,
label: key.to_lowercase().into(),
apply: Some(eco_format!("\"{}\"", key.to_lowercase())),
detail: Some(detail),
label_details: Some(desc.name.into()),
label_details: Some(desc.get_name()),
..Completion::default()
});
}
}
BuiltinTy::TextRegion => {
for (&key, desc) in rust_iso3166::ALPHA2_MAP.entries() {
let detail = eco_format!("An ISO 3166-1 alpha-2 region code, {}.", desc.name);
let detail =
eco_format!("An ISO 3166-1 alpha-2 region code, {}.", desc.get_name());
self.base.push_completion(Completion {
kind: CompletionKind::Syntax,
label: key.to_lowercase().into(),
apply: Some(eco_format!("\"{}\"", key.to_lowercase())),
detail: Some(detail),
label_details: Some(desc.name.into()),
label_details: Some(desc.get_name()),
..Completion::default()
});
}
@ -346,3 +348,37 @@ impl TypeCompletionWorker<'_, '_, '_, '_> {
Some(())
}
}
// desc.name()
trait GetName {
fn get_name(&self) -> EcoString;
}
#[cfg(not(target_arch = "wasm32"))]
impl GetName for rust_iso639::LanguageCode<'_> {
fn get_name(&self) -> EcoString {
self.name.into()
}
}
#[cfg(not(all(direct_wasm, target_arch = "wasm32")))]
impl GetName for rust_iso3166::CountryCode {
fn get_name(&self) -> EcoString {
self.name.into()
}
}
#[cfg(target_arch = "wasm32")]
impl GetName for rust_iso639::LanguageCode {
fn get_name(&self) -> EcoString {
self.name().into()
}
}
#[cfg(all(direct_wasm, target_arch = "wasm32"))]
impl GetName for rust_iso3166::CountryCode {
fn get_name(&self) -> EcoString {
self.name().into()
}
}

View file

@ -20,9 +20,7 @@ use tinymist_std::typst::TypstDocument;
use tinymist_world::debug_loc::DataSource;
use tinymist_world::vfs::{PathResolution, WorkspaceResolver};
use tinymist_world::{EntryReader, DETACHED_ENTRY};
use typst::diag::{
eco_format, At, FileError, FileResult, SourceDiagnostic, SourceResult, StrResult,
};
use typst::diag::{At, FileError, FileResult, SourceDiagnostic, SourceResult, StrResult};
use typst::foundations::{Bytes, IntoValue, Module, StyleChain, Styles};
use typst::introspection::Introspector;
use typst::layout::Position;
@ -674,6 +672,7 @@ impl SharedContext {
}
/// Get the local packages and their descriptions.
#[cfg(not(target_arch = "wasm32"))]
pub fn local_packages(&self) -> EcoVec<PackageSpec> {
crate::package::list_package_by_namespace(&self.world.registry, eco_format!("local"))
.into_iter()
@ -681,6 +680,12 @@ impl SharedContext {
.collect()
}
/// Get the local packages and their descriptions.
#[cfg(target_arch = "wasm32")]
pub fn local_packages(&self) -> EcoVec<PackageSpec> {
eco_vec![]
}
pub(crate) fn const_eval(rr: ast::Expr<'_>) -> Option<Value> {
Some(match rr {
ast::Expr::None(_) => Value::None,

View file

@ -31,11 +31,7 @@ pub fn path_to_url(path: &Path) -> anyhow::Result<Url> {
return untitled_url(untitled);
}
Url::from_file_path(path).or_else(|never| {
let _: () = never;
anyhow::bail!("could not convert path to URI: path: {path:?}",)
})
url_from_file_path(path)
}
/// Convert a path resolution to a URL.
@ -54,9 +50,7 @@ pub fn url_to_path(uri: Url) -> PathBuf {
return PathBuf::from("/untitled/nEoViM-BuG");
}
return uri
.to_file_path()
.unwrap_or_else(|_| panic!("could not convert URI to path: URI: {uri:?}",));
return url_to_file_path(&uri);
}
if uri.scheme() == "untitled" {
@ -73,7 +67,40 @@ pub fn url_to_path(uri: Url) -> PathBuf {
return Path::new(String::from_utf8_lossy(&bytes).as_ref()).clean();
}
uri.to_file_path().unwrap()
url_to_file_path(&uri)
}
#[cfg(not(target_arch = "wasm32"))]
fn url_from_file_path(path: &Path) -> anyhow::Result<Url> {
Url::from_file_path(path).or_else(|never| {
let _: () = never;
anyhow::bail!("could not convert path to URI: path: {path:?}",)
})
}
#[cfg(target_arch = "wasm32")]
fn url_from_file_path(path: &Path) -> anyhow::Result<Url> {
// In WASM, create a simple file:// URL
let path_str = path.to_string_lossy();
let url_str = if path_str.starts_with('/') {
format!("file://{}", path_str)
} else {
format!("file:///{}", path_str)
};
Url::parse(&url_str).map_err(|e| anyhow::anyhow!("could not convert path to URI: {}", e))
}
#[cfg(not(target_arch = "wasm32"))]
fn url_to_file_path(uri: &Url) -> PathBuf {
uri.to_file_path()
.unwrap_or_else(|_| panic!("could not convert URI to path: URI: {uri:?}",))
}
#[cfg(target_arch = "wasm32")]
fn url_to_file_path(uri: &Url) -> PathBuf {
// In WASM, manually parse the URL path
PathBuf::from(uri.path())
}
#[cfg(test)]

View file

@ -1,14 +1,10 @@
//! Package management tools.
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::OnceLock;
use ecow::{eco_format, eco_vec, EcoVec};
use parking_lot::Mutex;
use ecow::eco_format;
// use reflexo_typst::typst::prelude::*;
use serde::{Deserialize, Serialize};
use tinymist_world::package::registry::HttpRegistry;
use tinymist_world::package::PackageSpec;
use typst::diag::{EcoString, StrResult};
use typst::syntax::package::PackageManifest;
@ -77,11 +73,47 @@ pub fn check_package(ctx: &mut LocalContext, spec: &PackageInfo) -> StrResult<()
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
/// Get the packages in namespaces and their descriptions.
pub fn list_package_by_namespace(
registry: &HttpRegistry,
registry: &tinymist_world::package::registry::HttpRegistry,
ns: EcoString,
) -> EcoVec<(PathBuf, PackageSpec)> {
) -> ecow::EcoVec<(PathBuf, PackageSpec)> {
use std::collections::HashSet;
use std::sync::OnceLock;
use ecow::eco_vec;
use parking_lot::Mutex;
trait IsDirFollowLinks {
fn is_dir_follow_links(&self) -> bool;
}
impl IsDirFollowLinks for PathBuf {
fn is_dir_follow_links(&self) -> bool {
// Although `canonicalize` is heavy, we must use it because `symlink_metadata`
// is not reliable.
self.canonicalize()
.map(|meta| meta.is_dir())
.unwrap_or(false)
}
}
fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
let err = match result {
Ok(value) => return Some(value),
Err(err) => err,
};
static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
let mut once = ONCE.get_or_init(Default::default).lock();
if once.insert(site) {
log::error!("failed to perform {site}: {err}");
}
None
}
// search packages locally. We only search in the data
// directory and not the cache directory, because the latter is not
// intended for storage of local packages.
@ -148,32 +180,3 @@ pub fn list_package_by_namespace(
packages
}
trait IsDirFollowLinks {
fn is_dir_follow_links(&self) -> bool;
}
impl IsDirFollowLinks for PathBuf {
fn is_dir_follow_links(&self) -> bool {
// Although `canonicalize` is heavy, we must use it because `symlink_metadata`
// is not reliable.
self.canonicalize()
.map(|meta| meta.is_dir())
.unwrap_or(false)
}
}
fn once_log<T, E: std::fmt::Display>(result: Result<T, E>, site: &'static str) -> Option<T> {
let err = match result {
Ok(value) => return Some(value),
Err(err) => err,
};
static ONCE: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
let mut once = ONCE.get_or_init(Default::default).lock();
if once.insert(site) {
log::error!("failed to perform {site}: {err}");
}
None
}

View file

@ -814,7 +814,6 @@ impl<F: CompilerFeat> World for CompilerWorld<F> {
if let Some(timestamp) = self.creation_timestamp {
tinymist_std::time::UtcDateTime::from_unix_timestamp(timestamp)
.unwrap_or_else(|_| now().into())
.into()
} else {
now().into()
}

View file

@ -51,10 +51,13 @@ clap = ["dep:clap"]
# Note: this is the feature for typlite as a CLI, not for others.
# `docx` is enabled in CLI mode, but not in library mode.
# `fonts` is enabled in CLI mode.
cli = ["clap", "clap/wrap_help", "docx", "fonts"]
cli = ["clap", "clap/wrap_help", "docx", "fonts", "system"]
no-content-hint = ["tinymist-project/no-content-hint"]
docx = ["docx-rs", "image", "resvg"]
system = ["tinymist-project/system"]
web = ["tinymist-project/web"]
# Embeds Typst's default fonts for
# - text (Linux Libertine),
# - math (New Computer Modern Math), and