/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v2.8.2
cli/rt/binary.rs
1 127 строк
35 KB
Bartek Iwańczuk
fix(compile): transpile TypeScript imported at runtime (#34616)
01 июн 2026, 16:32
Не верифицирован
01 июн 2026, 16:32
09a2447
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashMap; use std::ffi::OsString; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use deno_core::FastString; use deno_core::ModuleCodeBytes; use deno_core::ModuleSourceCode; use deno_core::ModuleType; use deno_core::anyhow::Context; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::url::Url; use deno_error::JsError; use deno_error::JsErrorBox; use deno_lib::standalone::binary::DenoRtDeserializable; use deno_lib::standalone::binary::MAGIC_BYTES; use deno_lib::standalone::binary::Metadata; use deno_lib::standalone::binary::RemoteModuleEntry; use deno_lib::standalone::binary::SpecifierDataStore; use deno_lib::standalone::binary::SpecifierId; use deno_lib::standalone::virtual_fs::VfsEntry; use deno_lib::standalone::virtual_fs::VirtualDirectory; use deno_lib::standalone::virtual_fs::VirtualDirectoryEntries; use deno_media_type::MediaType; use deno_npm::NpmPackageId; use deno_npm::resolution::SerializedNpmResolutionSnapshot; use deno_npm::resolution::SerializedNpmResolutionSnapshotPackage; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use deno_semver::StackString; use deno_semver::package::PackageReq; use indexmap::IndexMap; use sys_traits::FsCanonicalize; use sys_traits::FsRead; use thiserror::Error; use crate::file_system::FileBackedVfs; use crate::file_system::VfsRoot; pub struct StandaloneData { pub metadata: Metadata, pub modules: Arc<StandaloneModules>, pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>, pub root_path: PathBuf, pub vfs: Arc<FileBackedVfs>, } /// This function will try to run this binary as a standalone binary /// produced by `deno compile`. It determines if this is a standalone /// binary by skipping over the trailer width at the end of the file, /// then checking for the magic trailer string `d3n0l4nd`. If found, /// the bundle is executed. If not, this function exits with `Ok(None)`. pub fn extract_standalone( cli_args: Cow<[OsString]>, ) -> Result<StandaloneData, AnyError> { let data = find_section()?; // read metadata first to determine the root path let (mut metadata, remaining) = read_section_metadata(data)?; // for self-extracting executables, use the extraction directory as root // so that module specifiers resolve to extracted file paths let root_path = if let Some(hash) = &metadata.self_extracting { let dir = choose_and_create_extraction_dir(hash)?; sys_traits::impls::RealSys .fs_canonicalize(&dir) .unwrap_or(dir) } else { let maybe_current_exe = std::env::current_exe().ok(); let current_exe_name = maybe_current_exe .as_ref() .and_then(|p| p.file_name()) .map(|p| p.to_string_lossy()) // should never happen .unwrap_or_else(|| Cow::Borrowed("binary")); std::env::temp_dir().join(format!("deno-compile-{}", current_exe_name)) }; let root_url = deno_path_util::url_from_directory_path(&root_path)?; let DeserializedDataSection { npm_snapshot, modules_store: remote_modules, vfs_root_entries, vfs_files_data, } = deserialize_binary_data_section(&root_url, remaining)?; let cli_args = cli_args.into_owned(); let current_exe = std::env::current_exe().ok(); let mut args_iter = cli_args.into_iter(); args_iter.next(); // skip argv[0] // Node.js apps relaunch with spawn(process.execPath, [process.argv[1], ...args]). // In standalone mode process.argv[1] === execPath (#32990), so the first arg // after argv[0] is a duplicate of the exe path. Strip it. // // NOTE: this means `./myapp ./myapp --foo` would silently lose the first // `./myapp` arg. In practice standalone binaries are never invoked this way, // and this matches how Node.js SEA handles the relaunch pattern. if let Some(first) = args_iter.next() { let is_exe_dup = current_exe .as_ref() .is_some_and(|exe| Path::new(&first) == exe.as_path()); if !is_exe_dup { metadata.argv.push(first.into_string().unwrap()); } } for arg in args_iter { metadata.argv.push(arg.into_string().unwrap()); } let vfs = { let fs_root = VfsRoot { dir: VirtualDirectory { // align the name of the directory with the root dir name: root_path .file_name() .unwrap() .to_string_lossy() .into_owned(), entries: vfs_root_entries, }, root_path: root_path.clone(), start_file_offset: 0, }; Arc::new(FileBackedVfs::new( Cow::Borrowed(vfs_files_data), fs_root, metadata.vfs_case_sensitivity, )) }; Ok(StandaloneData { metadata, modules: Arc::new(StandaloneModules { modules: remote_modules, vfs: vfs.clone(), }), npm_snapshot, root_path, vfs, }) } /// Extracts the embedded file system to disk for a self-extracting /// executable. The extraction_dir is the directory where files will be /// extracted, which should match the VFS root_path. pub fn extract_vfs_to_disk( vfs: &FileBackedVfs, extraction_dir: &Path, ) -> Result<(), AnyError> { // check if already extracted let done_marker = extraction_dir.join(".done"); if done_marker.exists() { log::debug!("Already extracted to {}", extraction_dir.display()); return Ok(()); } log::debug!("Extracting to {}", extraction_dir.display()); let start = std::time::Instant::now(); std::fs::create_dir_all(extraction_dir).with_context(|| { format!( "Failed to create extraction directory: {}", extraction_dir.display() ) })?; extract_vfs_dir( vfs, vfs.root_dir(), &mut extraction_dir.to_path_buf(), extraction_dir, ) .context("Failed to extract embedded files to disk")?; // write the done marker std::fs::File::create(&done_marker) .context("Failed to write extraction done marker")?; log::debug!("Extracted in {}ms", start.elapsed().as_millis()); Ok(()) } fn choose_and_create_extraction_dir( hash_str: &str, ) -> Result<PathBuf, AnyError> { let current_exe = std::env::current_exe() .context("Failed to determine current executable path")?; let exe_name = current_exe .file_stem() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_else(|| "binary".to_string()); // try next to the executable first if let Some(exe_dir) = current_exe.parent() { let dir = exe_dir.join(format!(".{}", exe_name)).join(hash_str); match std::fs::create_dir_all(&dir) { Ok(()) => return Ok(dir), Err(err) => { log::debug!( "Could not create extraction dir next to executable ({}), falling back to data dir: {}", err, dir.display() ); } } } // fall back to platform-specific data directory let data_dir = get_data_local_dir().context( "Failed to determine local data directory for self-extracting executable", )?; let dir = data_dir.join(&exe_name).join(hash_str); std::fs::create_dir_all(&dir).with_context(|| { format!("Failed to create extraction directory: {}", dir.display()) })?; Ok(dir) } fn get_data_local_dir() -> Option<PathBuf> { #[cfg(target_os = "windows")] { std::env::var_os("LOCALAPPDATA").map(PathBuf::from) } #[cfg(target_os = "macos")] { #[allow(clippy::disallowed_types, reason = "setup code")] sys_traits::EnvHomeDir::env_home_dir(&sys_traits::impls::RealSys) .map(|h| h.join("Library").join("Application Support")) } #[cfg(not(any(target_os = "windows", target_os = "macos")))] { std::env::var_os("XDG_DATA_HOME") .map(PathBuf::from) .or_else(|| { #[allow(clippy::disallowed_types, reason = "setup code")] sys_traits::EnvHomeDir::env_home_dir(&sys_traits::impls::RealSys) .map(|h| h.join(".local").join("share")) }) } } fn extract_vfs_dir( vfs: &FileBackedVfs, dir: &VirtualDirectory, disk_path: &mut PathBuf, extraction_root: &Path, ) -> Result<(), AnyError> { for entry in dir.entries.iter() { match entry { VfsEntry::Dir(sub_dir) => { disk_path.push(&sub_dir.name); // parent is guaranteed to exist since we recurse top-down std::fs::create_dir(&*disk_path) .or_else(|e| { if e.kind() == std::io::ErrorKind::AlreadyExists { Ok(()) } else { Err(e) } }) .with_context(|| { format!("Failed to create directory: {}", disk_path.display()) })?; extract_vfs_dir(vfs, sub_dir, disk_path, extraction_root)?; disk_path.pop(); } VfsEntry::File(file) => { disk_path.push(&file.name); let data = vfs .read_file_all(file) .with_context(|| format!("Failed to read VFS file: {}", file.name))?; #[cfg(unix)] { use std::io::Write; use std::os::unix::fs::OpenOptionsExt; let mode = if file.executable { 0o755 } else { 0o644 }; std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .mode(mode) .open(&*disk_path) .and_then(|mut f| f.write_all(&data)) .with_context(|| { format!("Failed to write file: {}", disk_path.display()) })?; } #[cfg(not(unix))] { std::fs::write(&*disk_path, &*data).with_context(|| { format!("Failed to write file: {}", disk_path.display()) })?; } disk_path.pop(); } VfsEntry::Symlink(symlink) => { disk_path.push(&symlink.name); // symlink dest_parts are relative to the VFS root, // so resolve them relative to the extraction root let absolute_target = symlink.resolve_dest_from_root(extraction_root); create_symlink(&absolute_target, disk_path, symlink.dest_is_dir)?; disk_path.pop(); } } } Ok(()) } fn create_symlink( target: &Path, link_path: &Path, dest_is_dir: bool, ) -> Result<(), AnyError> { #[cfg(unix)] { _ = dest_is_dir; // unused on unix std::os::unix::fs::symlink(target, link_path) .or_else(|_| { // may be left over from an interrupted extraction let _ = std::fs::remove_file(link_path); std::os::unix::fs::symlink(target, link_path) }) .with_context(|| { format!( "Failed to create symlink: {} -> {}", link_path.display(), target.display() ) })?; } #[cfg(windows)] { // on Windows, symlink_file and symlink_dir are distinct — using the // wrong type causes PermissionDenied when accessing through the link let create_link = |target: &Path, link: &Path| { if dest_is_dir { std::os::windows::fs::symlink_dir(target, link) } else { std::os::windows::fs::symlink_file(target, link) } }; create_link(target, link_path) .or_else(|_| { // may be left over from an interrupted extraction let _ = std::fs::remove_file(link_path) .or_else(|_| std::fs::remove_dir(link_path)); create_link(target, link_path) }) .or_else(|_| { // symlinks may require elevated privileges on Windows, // fall back to junctions for dirs or copying for files if dest_is_dir { junction::create(target, link_path) } else { std::fs::copy(target, link_path).map(|_| ()) } }) .with_context(|| { format!( "Failed to create symlink: {} -> {}", link_path.display(), target.display() ) })?; } Ok(()) } fn find_section() -> Result<&'static [u8], AnyError> { #[cfg(windows)] if std::env::var_os("DENO_INTERNAL_RT_USE_FILE_FALLBACK").is_some() { return read_from_file_fallback(); } match libsui::find_section("d3n0l4nd") .context("Failed reading standalone binary section.") { Ok(Some(data)) => Ok(data), Ok(None) => bail!("Could not find standalone binary section."), Err(err) => { #[cfg(windows)] if let Ok(data) = read_from_file_fallback() { return Ok(data); } Err(err) } } } /// This is a temporary hacky fallback until we can find /// a fix for https://github.com/denoland/deno/issues/28982 #[cfg(windows)] fn read_from_file_fallback() -> Result<&'static [u8], AnyError> { use std::sync::OnceLock; fn find_in_bytes(bytes: &[u8], needle: &[u8]) -> Option<usize> { bytes.windows(needle.len()).position(|n| n == needle) } static FILE: OnceLock<std::fs::File> = OnceLock::new(); static MMAP_FILE: OnceLock<memmap2::Mmap> = OnceLock::new(); // DENOLAND in utf16 const RESOURCE_SECTION_HEADER_NAME: &[u8] = &[ 0x44, 0x00, 0x33, 0x00, 0x4E, 0x00, 0x30, 0x00, 0x4C, 0x00, 0x34, 0x00, 0x4E, 0x00, 0x44, 0x00, ]; const MAGIC_BYTES: &[u8] = b"d3n0l4nd"; let file_path = std::env::current_exe()?; let file = FILE.get_or_init(|| std::fs::File::open(file_path).unwrap()); let mmap = MMAP_FILE.get_or_init(|| { // SAFETY: memory mapped file creation unsafe { memmap2::Mmap::map(file).unwrap() } }); // the code in this file will cause this to appear twice in the binary, // so skip over the first one let Some(marker_pos) = find_in_bytes(mmap, RESOURCE_SECTION_HEADER_NAME) else { bail!("Failed to find first section name."); }; let next_bytes = &mmap[marker_pos + RESOURCE_SECTION_HEADER_NAME.len()..]; let Some(marker_pos) = find_in_bytes(next_bytes, RESOURCE_SECTION_HEADER_NAME) else { bail!("Failed to find second section name."); }; let next_bytes = &next_bytes[marker_pos + RESOURCE_SECTION_HEADER_NAME.len()..]; let Some(ascii_pos) = find_in_bytes(next_bytes, MAGIC_BYTES) else { bail!("Failed to find first magic bytes."); }; let next_bytes = &next_bytes[ascii_pos..]; let Some(last_pos) = next_bytes .windows(MAGIC_BYTES.len()) .rposition(|w| w == MAGIC_BYTES) else { bail!("Failed to find end magic bytes.") }; Ok(&next_bytes[..last_pos + MAGIC_BYTES.len()]) } pub struct DeserializedDataSection { pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>, pub modules_store: RemoteModulesStore, pub vfs_root_entries: VirtualDirectoryEntries, pub vfs_files_data: &'static [u8], } fn read_magic_bytes(input: &[u8]) -> Result<(&[u8], bool), AnyError> { if input.len() < MAGIC_BYTES.len() { bail!("Unexpected end of data. Could not find magic bytes."); } let (magic_bytes, input) = input.split_at(MAGIC_BYTES.len()); if magic_bytes != MAGIC_BYTES { return Ok((input, false)); } Ok((input, true)) } /// Reads the magic bytes and metadata from the beginning of the data section. /// Returns the metadata and the remaining input after the metadata. fn read_section_metadata( data: &'static [u8], ) -> Result<(Metadata, &'static [u8]), AnyError> { let (input, found) = read_magic_bytes(data)?; if !found { bail!("Did not find magic bytes."); } let (input, metadata_bytes) = read_bytes_with_u64_len(input).context("reading metadata")?; let metadata: Metadata = serde_json::from_slice(metadata_bytes).context("deserializing metadata")?; Ok((metadata, input)) } /// Deserializes the binary data section after the metadata has already been /// parsed by `read_section_metadata`. fn deserialize_binary_data_section( root_dir_url: &Url, input: &'static [u8], ) -> Result<DeserializedDataSection, AnyError> { // 1. Was the metadata above. // 2. Npm snapshot let (input, data) = read_bytes_with_u64_len(input).context("reading npm snapshot")?; let npm_snapshot = if data.is_empty() { None } else { Some(deserialize_npm_snapshot(data).context("deserializing npm snapshot")?) }; // 3. Specifiers let (input, specifiers_store) = SpecifierStore::deserialize(root_dir_url, input) .context("deserializing specifiers")?; // 4. Redirects let (input, redirects_store) = SpecifierDataStore::<SpecifierId>::deserialize(input) .context("deserializing redirects")?; // 5. Remote modules let (input, remote_modules_store) = SpecifierDataStore::<RemoteModuleEntry<'static>>::deserialize(input) .context("deserializing remote modules")?; // 6. VFS let (input, data) = read_bytes_with_u64_len(input).context("vfs")?; let vfs_root_entries: VirtualDirectoryEntries = serde_json::from_slice(data).context("deserializing vfs data")?; let (input, vfs_files_data) = read_bytes_with_u64_len(input).context("reading vfs files data")?; // finally ensure we read the magic bytes at the end let (_input, found) = read_magic_bytes(input)?; if !found { bail!("Could not find magic bytes at end of data."); } let modules_store = RemoteModulesStore::new( specifiers_store, redirects_store, remote_modules_store, ); Ok(DeserializedDataSection { npm_snapshot, modules_store, vfs_root_entries, vfs_files_data, }) } struct SpecifierStore { data: IndexMap<Arc<Url>, SpecifierId>, reverse: IndexMap<SpecifierId, Arc<Url>>, } impl SpecifierStore { pub fn deserialize<'a>( root_dir_url: &Url, input: &'a [u8], ) -> std::io::Result<(&'a [u8], Self)> { let (input, len) = read_u32_as_usize(input)?; let mut data = IndexMap::with_capacity(len); let mut reverse = IndexMap::with_capacity(len); let mut input = input; for _ in 0..len { let (new_input, specifier_str) = read_string_lossy(input)?; let specifier = match Url::parse(&specifier_str) { Ok(url) => url, Err(err) => match root_dir_url.join(&specifier_str) { Ok(url) => url, Err(_) => { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, err, )); } }, }; let (new_input, id) = SpecifierId::deserialize(new_input)?; let specifier = Arc::new(specifier); data.insert(specifier.clone(), id); reverse.insert(id, specifier); input = new_input; } Ok((input, Self { data, reverse })) } pub fn get_id(&self, specifier: &Url) -> Option<SpecifierId> { self.data.get(specifier).cloned() } pub fn get_specifier(&self, specifier_id: SpecifierId) -> Option<&Url> { self.reverse.get(&specifier_id).map(|url| url.as_ref()) } } pub struct StandaloneModules { modules: RemoteModulesStore, vfs: Arc<FileBackedVfs>, } impl StandaloneModules { pub fn resolve_specifier<'a>( &'a self, specifier: &'a Url, ) -> Result<Option<&'a Url>, TooManyRedirectsError> { if specifier.scheme() == "file" { Ok(Some(specifier)) } else { self.modules.resolve_specifier(specifier) } } pub fn path_in_root(&self, path: &Path) -> bool { deno_path_util::normalize_path(Cow::Borrowed(path)) .starts_with(self.vfs.root()) } pub fn read<'a>( &'a self, specifier: &'a Url, ) -> Result<Option<DenoCompileModuleData<'a>>, JsErrorBox> { if specifier.scheme() == "file" { let path = deno_path_util::url_to_file_path(specifier) .map_err(JsErrorBox::from_err)?; let mut transpiled = None; let mut source_map = None; let mut cjs_export_analysis = None; let mut is_valid_utf8 = false; let bytes = match self.vfs.file_entry(&path) { Ok(entry) => { let bytes = self .vfs .read_file_all(entry) .map_err(JsErrorBox::from_err)?; is_valid_utf8 = entry.is_valid_utf8; transpiled = entry .transpiled_offset .and_then(|t| self.vfs.read_file_offset_with_len(t).ok()); source_map = entry .source_map_offset .and_then(|t| self.vfs.read_file_offset_with_len(t).ok()); cjs_export_analysis = entry .cjs_export_analysis_offset .and_then(|t| self.vfs.read_file_offset_with_len(t).ok()); bytes } Err(err) if err.kind() == ErrorKind::NotFound => { #[allow( clippy::disallowed_types, reason = "use real file system because not in vfs" )] let bytes = match sys_traits::impls::RealSys.fs_read(&path) { Ok(bytes) => bytes, Err(err) if err.kind() == ErrorKind::NotFound => { return Ok(None); } Err(err) => return Err(JsErrorBox::from_err(err)), }; // A file read from disk at runtime (e.g. a plugin discovered by a // compiled program) hasn't been transpiled at compile time, so // transpile TypeScript/JSX here, mirroring `deno run`. let media_type = MediaType::from_specifier(specifier); if matches!( media_type, MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Jsx | MediaType::Tsx ) { let text = String::from_utf8_lossy(&bytes).into_owned(); let transpiled_text = transpile_runtime_module(specifier, media_type, text)?; transpiled = Some(Cow::Owned(transpiled_text.into_bytes())); } bytes } Err(err) => return Err(JsErrorBox::from_err(err)), }; Ok(Some(DenoCompileModuleData { media_type: MediaType::from_specifier(specifier), specifier, is_valid_utf8, data: bytes, transpiled, source_map, cjs_export_analysis, })) } else { self.modules.read(specifier).map_err(JsErrorBox::from_err) } } } /// Transpile TypeScript/JSX source to JavaScript for modules that are not /// embedded in the binary, namely `data:`/`blob:` URLs and local files read /// from disk at runtime. Embedded modules are already transpiled at /// `deno compile` time, but a compiled program can still dynamically `import()` /// TypeScript that it builds or discovers while running. `deno_ast` is already /// linked into the binary via `ext/node`, so transpiling here adds no extra /// binary size. pub(crate) fn transpile_runtime_module( specifier: &Url, media_type: MediaType, source: String, ) -> Result<String, JsErrorBox> { match media_type { MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Jsx | MediaType::Tsx => {} // JavaScript, JSON, Wasm, etc. are served verbatim. _ => return Ok(source), } let parsed = deno_ast::parse_module(deno_ast::ParseParams { specifier: specifier.clone(), text: source.into(), media_type, capture_tokens: false, scope_analysis: false, maybe_syntax: None, }) .map_err(JsErrorBox::from_err)?; let transpiled = parsed .transpile( &deno_ast::TranspileOptions { imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove, ..Default::default() }, &deno_ast::TranspileModuleOptions { module_kind: None }, &deno_ast::EmitOptions { source_map: deno_ast::SourceMapOption::Inline, ..Default::default() }, ) .map_err(JsErrorBox::from_err)?; Ok(transpiled.into_source().text) } pub struct DenoCompileModuleData<'a> { pub specifier: &'a Url, pub media_type: MediaType, pub is_valid_utf8: bool, pub data: Cow<'static, [u8]>, pub transpiled: Option<Cow<'static, [u8]>>, pub source_map: Option<Cow<'static, [u8]>>, pub cjs_export_analysis: Option<Cow<'static, [u8]>>, } impl<'a> DenoCompileModuleData<'a> { pub fn into_parts(self) -> (&'a Url, ModuleType, DenoCompileModuleSource) { fn into_string_unsafe( is_valid_utf8: bool, data: Cow<'static, [u8]>, ) -> DenoCompileModuleSource { match data { Cow::Borrowed(d) if is_valid_utf8 => { DenoCompileModuleSource::String( // SAFETY: we know this is a valid utf8 string unsafe { std::str::from_utf8_unchecked(d) }, ) } Cow::Borrowed(_) => DenoCompileModuleSource::Bytes(data), Cow::Owned(d) => DenoCompileModuleSource::Bytes(Cow::Owned(d)), } } let data = self.transpiled.unwrap_or(self.data); let (media_type, source) = match self.media_type { MediaType::JavaScript | MediaType::Jsx | MediaType::Mjs | MediaType::Cjs | MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Tsx => ( ModuleType::JavaScript, into_string_unsafe(self.is_valid_utf8, data), ), MediaType::Json => ( ModuleType::Json, into_string_unsafe(self.is_valid_utf8, data), ), MediaType::Wasm => { (ModuleType::Wasm, DenoCompileModuleSource::Bytes(data)) } // just assume javascript if we made it here MediaType::Css | MediaType::Html | MediaType::Jsonc | MediaType::Json5 | MediaType::Markdown | MediaType::SourceMap | MediaType::Sql | MediaType::Unknown => { (ModuleType::JavaScript, DenoCompileModuleSource::Bytes(data)) } }; (self.specifier, media_type, source) } } #[derive(Debug)] pub enum DenoCompileModuleSource { String(&'static str), Bytes(Cow<'static, [u8]>), } impl DenoCompileModuleSource { pub fn into_for_v8(self) -> ModuleSourceCode { match self { // todo(https://github.com/denoland/deno_core/pull/943): store whether // the string is ascii or not ahead of time so we can avoid the is_ascii() // check in FastString::from_static Self::String(s) => ModuleSourceCode::String(FastString::from_static(s)), Self::Bytes(b) => ModuleSourceCode::Bytes(module_source_into_bytes(b)), } } pub fn into_bytes_for_v8(self) -> ModuleCodeBytes { match self { DenoCompileModuleSource::String(text) => text.as_bytes().into(), DenoCompileModuleSource::Bytes(b) => module_source_into_bytes(b), } } } fn module_source_into_bytes(data: Cow<'static, [u8]>) -> ModuleCodeBytes { match data { Cow::Borrowed(d) => d.into(), Cow::Owned(d) => d.into_boxed_slice().into(), } } #[derive(Debug, Error, JsError)] #[class(generic)] #[error("Too many redirects resolving: {0}")] pub struct TooManyRedirectsError(Url); pub struct RemoteModulesStore { specifiers: SpecifierStore, redirects: SpecifierDataStore<SpecifierId>, remote_modules: SpecifierDataStore<RemoteModuleEntry<'static>>, } impl RemoteModulesStore { fn new( specifiers: SpecifierStore, redirects: SpecifierDataStore<SpecifierId>, remote_modules: SpecifierDataStore<RemoteModuleEntry<'static>>, ) -> Self { Self { specifiers, redirects, remote_modules, } } pub fn resolve_specifier<'a>( &'a self, specifier: &'a Url, ) -> Result<Option<&'a Url>, TooManyRedirectsError> { let Some(mut current) = self.specifiers.get_id(specifier) else { return Ok(None); }; let mut count = 0; loop { if count > 10 { return Err(TooManyRedirectsError(specifier.clone())); } match self.redirects.get(current) { Some(to) => { current = *to; count += 1; } None => { if count == 0 { return Ok(Some(specifier)); } else { return Ok(self.specifiers.get_specifier(current)); } } } } } pub fn read<'a>( &'a self, original_specifier: &'a Url, ) -> Result<Option<DenoCompileModuleData<'a>>, TooManyRedirectsError> { #[allow(clippy::ptr_arg, reason = "Cow on data is relevant here")] fn handle_cow_ref(data: &Cow<'static, [u8]>) -> Cow<'static, [u8]> { match data { Cow::Borrowed(data) => Cow::Borrowed(data), Cow::Owned(data) => { // this variant should never happen because the data // should always be borrowed static in denort debug_assert!(false); Cow::Owned(data.clone()) } } } let mut count = 0; let Some(mut specifier) = self.specifiers.get_id(original_specifier) else { return Ok(None); }; loop { if count > 10 { return Err(TooManyRedirectsError(original_specifier.clone())); } match self.redirects.get(specifier) { Some(to) => { specifier = *to; count += 1; } None => { let Some(entry) = self.remote_modules.get(specifier) else { return Ok(None); }; return Ok(Some(DenoCompileModuleData { specifier: if count == 0 { original_specifier } else { self.specifiers.get_specifier(specifier).unwrap() }, media_type: entry.media_type, is_valid_utf8: entry.is_valid_utf8, data: handle_cow_ref(&entry.data), transpiled: entry.maybe_transpiled.as_ref().map(handle_cow_ref), source_map: entry.maybe_source_map.as_ref().map(handle_cow_ref), cjs_export_analysis: entry .maybe_cjs_export_analysis .as_ref() .map(handle_cow_ref), })); } } } } } fn deserialize_npm_snapshot( input: &[u8], ) -> Result<ValidSerializedNpmResolutionSnapshot, AnyError> { fn parse_id(input: &[u8]) -> Result<(&[u8], NpmPackageId), AnyError> { let (input, id) = read_string_lossy(input)?; let id = NpmPackageId::from_serialized(&id)?; Ok((input, id)) } #[allow(clippy::needless_lifetimes, reason = "clippy bug")] #[allow(clippy::type_complexity, reason = "private code")] fn parse_root_package<'a>( id_to_npm_id: &'a impl Fn(usize) -> Result<NpmPackageId, AnyError>, ) -> impl Fn(&[u8]) -> Result<(&[u8], (PackageReq, NpmPackageId)), AnyError> + 'a { |input| { let (input, req) = read_string_lossy(input)?; let req = PackageReq::from_str(&req)?; let (input, id) = read_u32_as_usize(input)?; Ok((input, (req, id_to_npm_id(id)?))) } } #[allow(clippy::needless_lifetimes, reason = "clippy bug")] #[allow(clippy::type_complexity, reason = "private code")] fn parse_package_dep<'a>( id_to_npm_id: &'a impl Fn(usize) -> Result<NpmPackageId, AnyError>, ) -> impl Fn(&[u8]) -> Result<(&[u8], (StackString, NpmPackageId)), AnyError> + 'a { |input| { let (input, req) = read_string_lossy(input)?; let (input, id) = read_u32_as_usize(input)?; let req = StackString::from_cow(req); Ok((input, (req, id_to_npm_id(id)?))) } } fn parse_package<'a>( input: &'a [u8], id: NpmPackageId, id_to_npm_id: &impl Fn(usize) -> Result<NpmPackageId, AnyError>, ) -> Result<(&'a [u8], SerializedNpmResolutionSnapshotPackage), AnyError> { let (input, deps_len) = read_u32_as_usize(input)?; let (input, dependencies) = parse_hashmap_n_times(input, deps_len, parse_package_dep(id_to_npm_id))?; Ok(( input, SerializedNpmResolutionSnapshotPackage { id, system: Default::default(), dist: Default::default(), dependencies, optional_dependencies: Default::default(), optional_peer_dependencies: Default::default(), has_bin: false, has_scripts: false, is_deprecated: false, extra: Default::default(), }, )) } let (input, packages_len) = read_u32_as_usize(input)?; // get a hashmap of all the npm package ids to their serialized ids let (input, data_ids_to_npm_ids) = parse_vec_n_times(input, packages_len, parse_id) .context("deserializing id")?; let data_id_to_npm_id = |id: usize| { data_ids_to_npm_ids .get(id) .cloned() .ok_or_else(|| deno_core::anyhow::anyhow!("Invalid npm package id")) }; let (input, root_packages_len) = read_u32_as_usize(input)?; let (input, root_packages) = parse_hashmap_n_times( input, root_packages_len, parse_root_package(&data_id_to_npm_id), ) .context("deserializing root package")?; let (input, packages) = parse_vec_n_times_with_index(input, packages_len, |input, index| { parse_package(input, data_id_to_npm_id(index)?, &data_id_to_npm_id) }) .context("deserializing package")?; if !input.is_empty() { bail!("Unexpected data left over"); } Ok( SerializedNpmResolutionSnapshot { packages, root_packages, } // this is ok because we have already verified that all the // identifiers found in the snapshot are valid via the // npm package id -> npm package id mapping .into_valid_unsafe(), ) } fn parse_hashmap_n_times<TKey: std::cmp::Eq + std::hash::Hash, TValue>( mut input: &[u8], times: usize, parse: impl Fn(&[u8]) -> Result<(&[u8], (TKey, TValue)), AnyError>, ) -> Result<(&[u8], HashMap<TKey, TValue>), AnyError> { let mut results = HashMap::with_capacity(times); for _ in 0..times { let result = parse(input); let (new_input, (key, value)) = result?; results.insert(key, value); input = new_input; } Ok((input, results)) } fn parse_vec_n_times<TResult>( input: &[u8], times: usize, parse: impl Fn(&[u8]) -> Result<(&[u8], TResult), AnyError>, ) -> Result<(&[u8], Vec<TResult>), AnyError> { parse_vec_n_times_with_index(input, times, |input, _index| parse(input)) } fn parse_vec_n_times_with_index<TResult>( mut input: &[u8], times: usize, parse: impl Fn(&[u8], usize) -> Result<(&[u8], TResult), AnyError>, ) -> Result<(&[u8], Vec<TResult>), AnyError> { let mut results = Vec::with_capacity(times); for i in 0..times { let result = parse(input, i); let (new_input, result) = result?; results.push(result); input = new_input; } Ok((input, results)) } fn read_bytes_with_u64_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> { let (input, len) = read_u64(input)?; let (input, data) = read_bytes(input, len as usize)?; Ok((input, data)) } fn read_bytes_with_u32_len(input: &[u8]) -> std::io::Result<(&[u8], &[u8])> { let (input, len) = read_u32_as_usize(input)?; let (input, data) = read_bytes(input, len)?; Ok((input, data)) } fn read_bytes(input: &[u8], len: usize) -> std::io::Result<(&[u8], &[u8])> { check_has_len(input, len)?; let (len_bytes, input) = input.split_at(len); Ok((input, len_bytes)) } #[inline(always)] fn check_has_len(input: &[u8], len: usize) -> std::io::Result<()> { if input.len() < len { Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Unexpected end of data", )) } else { Ok(()) } } fn read_string_lossy(input: &[u8]) -> std::io::Result<(&[u8], Cow<'_, str>)> { let (input, data_bytes) = read_bytes_with_u32_len(input)?; Ok((input, String::from_utf8_lossy(data_bytes))) } fn read_u32_as_usize(input: &[u8]) -> std::io::Result<(&[u8], usize)> { let (input, len_bytes) = read_bytes(input, 4)?; let len = u32::from_le_bytes(len_bytes.try_into().unwrap()); Ok((input, len as usize)) } fn read_u64(input: &[u8]) -> std::io::Result<(&[u8], u64)> { let (input, len_bytes) = read_bytes(input, 8)?; let len = u64::from_le_bytes(len_bytes.try_into().unwrap()); Ok((input, len)) }