eszip 0.111.0

A utility that can download JavaScript and TypeScript module graphs and store them locally in a special zip file
Documentation
// Copyright 2018-2026 the Deno authors. MIT license.

#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![deny(clippy::unused_async)]

mod error;
pub mod v1;
pub mod v2;

use std::sync::Arc;

pub use deno_ast;
pub use deno_graph;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use futures::future::BoxFuture;
use futures::future::LocalBoxFuture;
use futures::io::AsyncBufReadExt;
use futures::io::AsyncReadExt;
use serde::Deserialize;
use serde::Serialize;
use v2::EszipV2Modules;
use v2::EszipVersion;

pub use crate::error::ParseError;
pub use crate::v1::EszipV1;
pub use crate::v2::EszipRelativeFileBaseUrl;
pub use crate::v2::EszipV2;
pub use crate::v2::FromGraphOptions;

pub enum Eszip {
  V1(EszipV1),
  V2(EszipV2),
}

type EszipParserOutput<R> = Result<futures::io::BufReader<R>, ParseError>;

/// This future needs to polled to parse the eszip file.
type EszipParserFuture<R> = BoxFuture<'static, EszipParserOutput<R>>;
/// This future needs to polled to parse the eszip file.
type EszipParserLocalFuture<R> = LocalBoxFuture<'static, EszipParserOutput<R>>;

impl Eszip {
  /// Parse a byte stream into an Eszip. This function completes when the header
  /// is fully received. This does not mean that the entire file is fully
  /// received or parsed yet. To finish parsing, the future returned by this
  /// function in the second tuple slot needs to be polled.
  pub async fn parse<R: futures::io::AsyncRead + Unpin + Send + 'static>(
    reader: R,
  ) -> Result<(Eszip, EszipParserFuture<R>), ParseError> {
    let mut reader = futures::io::BufReader::new(reader);
    let mut magic = [0; 8];
    reader.read_exact(&mut magic).await?;
    if let Some(version) = EszipVersion::from_magic(&magic) {
      let (eszip, fut) = EszipV2::parse_with_version(version, reader).await?;
      Ok((Eszip::V2(eszip), Box::pin(fut)))
    } else {
      let mut buffer = Vec::new();
      let mut reader_w_magic = magic.chain(&mut reader);
      reader_w_magic.read_to_end(&mut buffer).await?;
      let eszip = EszipV1::parse(&buffer)?;
      let fut = async move { Ok::<_, ParseError>(reader) };
      Ok((Eszip::V1(eszip), Box::pin(fut)))
    }
  }

  /// Parse a byte stream into an Eszip. This function completes when the header
  /// is fully received. This does not mean that the entire file is fully
  /// received or parsed yet. To finish parsing, the future returned by this
  /// function in the second tuple slot needs to be polled.
  ///
  /// As opposed to [`Eszip::parse`], this method accepts `!Send` reader. The
  /// returned future does not implement `Send` either.
  pub async fn parse_local<R: futures::io::AsyncRead + Unpin + 'static>(
    reader: R,
  ) -> Result<(Eszip, EszipParserLocalFuture<R>), ParseError> {
    let mut reader = futures::io::BufReader::new(reader);
    reader.fill_buf().await?;
    let buffer = reader.buffer();
    if EszipV2::has_magic(buffer) {
      let (eszip, fut) = EszipV2::parse(reader).await?;
      Ok((Eszip::V2(eszip), Box::pin(fut)))
    } else {
      let mut buffer = Vec::new();
      reader.read_to_end(&mut buffer).await?;
      let eszip = EszipV1::parse(&buffer)?;
      let fut = async move { Ok::<_, ParseError>(reader) };
      Ok((Eszip::V1(eszip), Box::pin(fut)))
    }
  }

  /// Get the module metadata for a given module specifier. This function will
  /// follow redirects. The returned module has functions that can be used to
  /// obtain the module source and source map. The module returned from this
  /// function is guaranteed to be a valid module, which can be loaded into v8.
  ///
  /// Note that this function should be used to obtain a module; if you wish to
  /// get an import map, use [`get_import_map`](Self::get_import_map) instead.
  pub fn get_module(&self, specifier: &str) -> Option<Module> {
    match self {
      Eszip::V1(eszip) => eszip.get_module(specifier),
      Eszip::V2(eszip) => eszip.get_module(specifier),
    }
  }

  /// Get the import map for a given specifier.
  ///
  /// Note that this function should be used to obtain an import map; the returned
  /// "Module" is not necessarily a valid module that can be loaded into v8 (in
  /// other words, JSONC may be returned). If you wish to get a valid module,
  /// use [`get_module`](Self::get_module) instead.
  pub fn get_import_map(&self, specifier: &str) -> Option<Module> {
    match self {
      Eszip::V1(eszip) => eszip.get_import_map(specifier),
      Eszip::V2(eszip) => eszip.get_import_map(specifier),
    }
  }

  /// Takes the npm snapshot out of the eszip.
  pub fn take_npm_snapshot(
    &mut self,
  ) -> Option<ValidSerializedNpmResolutionSnapshot> {
    match self {
      Eszip::V1(_) => None,
      Eszip::V2(eszip) => eszip.take_npm_snapshot(),
    }
  }
}

/// Get an iterator over all the modules (including an import map, if any) in
/// this eszip archive.
///
/// Note that the iterator will iterate over the specifiers' "snapshot" of the
/// archive. If a new module is added to the archive after the iterator is
/// created via `into_iter()`, that module will not be iterated over.
impl IntoIterator for Eszip {
  type Item = (String, Module);
  type IntoIter = std::vec::IntoIter<Self::Item>;

  fn into_iter(self) -> Self::IntoIter {
    match self {
      Eszip::V1(eszip) => eszip.into_iter(),
      Eszip::V2(eszip) => eszip.into_iter(),
    }
  }
}

pub struct Module {
  pub specifier: String,
  pub kind: ModuleKind,
  inner: ModuleInner,
}

pub enum ModuleInner {
  V1(EszipV1),
  V2(EszipV2Modules),
}

impl Module {
  /// Get source code of the module.
  pub async fn source(&self) -> Option<Arc<[u8]>> {
    match &self.inner {
      ModuleInner::V1(eszip_v1) => eszip_v1.get_module_source(&self.specifier),
      ModuleInner::V2(eszip_v2) => {
        eszip_v2.get_module_source(&self.specifier).await
      }
    }
  }

  /// Take source code of the module. This will remove the source code from memory and
  /// the subsequent calls to `take_source()` will return `None`.
  /// For V1, this will take the entire module and returns the source code. We don't need
  /// to preserve module metadata for V1.
  pub async fn take_source(&self) -> Option<Arc<[u8]>> {
    match &self.inner {
      ModuleInner::V1(eszip_v1) => eszip_v1.take(&self.specifier),
      ModuleInner::V2(eszip_v2) => {
        eszip_v2.take_module_source(&self.specifier).await
      }
    }
  }

  /// Get source map of the module.
  pub async fn source_map(&self) -> Option<Arc<[u8]>> {
    match &self.inner {
      ModuleInner::V1(_) => None,
      ModuleInner::V2(eszip) => {
        eszip.get_module_source_map(&self.specifier).await
      }
    }
  }

  /// Take source map of the module. This will remove the source map from memory and
  /// the subsequent calls to `take_source_map()` will return `None`.
  pub async fn take_source_map(&self) -> Option<Arc<[u8]>> {
    match &self.inner {
      ModuleInner::V1(_) => None,
      ModuleInner::V2(eszip) => {
        eszip.take_module_source_map(&self.specifier).await
      }
    }
  }
}

/// This is the kind of module that is being stored. This is the same enum as is
/// present in [deno_core::ModuleType] except that this has additional variant
/// `Jsonc` which is used when an import map is embedded in Deno's config file
/// that can be JSONC.
/// Note that a module of type `Jsonc` can be used only as an import map, not as
/// a normal module.
#[repr(u8)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ModuleKind {
  JavaScript = 0,
  Json = 1,
  Jsonc = 2,
  OpaqueData = 3,
  Wasm = 4,
}

#[cfg(test)]
mod tests {
  use futures::StreamExt;
  use futures::TryStreamExt;
  use futures::io::AllowStdIo;
  use futures::stream;

  use super::*;

  #[tokio::test]
  async fn parse_v1() {
    let file = std::fs::File::open("./testdata/basic.json").unwrap();
    let (eszip, fut) = Eszip::parse(AllowStdIo::new(file)).await.unwrap();
    fut.await.unwrap();
    assert!(matches!(eszip, Eszip::V1(_)));
    eszip.get_module("https://gist.githubusercontent.com/lucacasonato/f3e21405322259ca4ed155722390fda2/raw/e25acb49b681e8e1da5a2a33744b7a36d538712d/hello.js").unwrap();
  }

  #[cfg(feature = "sha256")]
  #[tokio::test]
  async fn parse_v2() {
    let file = std::fs::File::open("./testdata/redirect.eszip2").unwrap();
    let (eszip, fut) = Eszip::parse(AllowStdIo::new(file)).await.unwrap();
    fut.await.unwrap();
    assert!(matches!(eszip, Eszip::V2(_)));
    eszip.get_module("file:///main.ts").unwrap();
  }

  #[tokio::test]
  async fn take_source_v1() {
    let file = std::fs::File::open("./testdata/basic.json").unwrap();
    let (eszip, fut) = Eszip::parse(AllowStdIo::new(file)).await.unwrap();
    fut.await.unwrap();
    assert!(matches!(eszip, Eszip::V1(_)));
    let specifier = "https://gist.githubusercontent.com/lucacasonato/f3e21405322259ca4ed155722390fda2/raw/e25acb49b681e8e1da5a2a33744b7a36d538712d/hello.js";
    let module = eszip.get_module(specifier).unwrap();
    assert_eq!(module.specifier, specifier);
    // We're taking the source from memory.
    let source = module.take_source().await.unwrap();
    assert!(!source.is_empty());
    // Source maps are not supported in v1 and should always return None.
    assert!(module.source_map().await.is_none());
    // Module shouldn't be available anymore.
    assert!(eszip.get_module(specifier).is_none());
  }

  #[cfg(feature = "sha256")]
  #[tokio::test]
  async fn take_source_v2() {
    let file = std::fs::File::open("./testdata/redirect.eszip2").unwrap();
    let (eszip, fut) = Eszip::parse(AllowStdIo::new(file)).await.unwrap();
    fut.await.unwrap();
    assert!(matches!(eszip, Eszip::V2(_)));
    let specifier = "file:///main.ts";
    let module = eszip.get_module(specifier).unwrap();
    // We're taking the source from memory.
    let source = module.take_source().await.unwrap();
    assert!(!source.is_empty());
    let module = eszip.get_module(specifier).unwrap();
    assert_eq!(module.specifier, specifier);
    // Source shouldn't be available anymore.
    assert!(module.source().await.is_none());
    // We didn't take the source map, so it should still be available.
    assert!(module.source_map().await.is_some());
    // Now we're taking the source map.
    let source_map = module.take_source_map().await.unwrap();
    assert!(!source_map.is_empty());
    // Source map shouldn't be available anymore.
    assert!(module.source_map().await.is_none());
  }

  #[tokio::test]
  async fn test_eszip_v1_iterator() {
    let file = std::fs::File::open("./testdata/basic.json").unwrap();
    let (eszip, fut) = Eszip::parse(AllowStdIo::new(file)).await.unwrap();
    tokio::spawn(fut);
    assert!(matches!(eszip, Eszip::V1(_)));

    struct Expected {
      specifier: String,
      source: &'static str,
      kind: ModuleKind,
    }

    let expected = vec![
      Expected {
        specifier: "https://gist.githubusercontent.com/lucacasonato/f3e21405322259ca4ed155722390fda2/raw/e25acb49b681e8e1da5a2a33744b7a36d538712d/hello.js".to_string(),
        source: "addEventListener(\"fetch\", (event)=>{\n    event.respondWith(new Response(\"Hello World\", {\n        headers: {\n            \"content-type\": \"text/plain\"\n        }\n    }));\n});\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjxodHRwczovL2dpc3QuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2x1Y2FjYXNvbmF0by9mM2UyMTQwNTMyMjI1OWNhNGVkMTU1NzIyMzkwZmRhMi9yYXcvZTI1YWNiNDliNjgxZThlMWRhNWEyYTMzNzQ0YjdhMzZkNTM4NzEyZC9oZWxsby5qcz4iXSwic291cmNlc0NvbnRlbnQiOlsiYWRkRXZlbnRMaXN0ZW5lcihcImZldGNoXCIsIChldmVudCkgPT4ge1xuICBldmVudC5yZXNwb25kV2l0aChuZXcgUmVzcG9uc2UoXCJIZWxsbyBXb3JsZFwiLCB7XG4gICAgaGVhZGVyczogeyBcImNvbnRlbnQtdHlwZVwiOiBcInRleHQvcGxhaW5cIiB9LFxuICB9KSk7XG59KTsiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsZ0JBQUEsRUFBQSxLQUFBLElBQUEsS0FBQTtBQUNBLFNBQUEsQ0FBQSxXQUFBLEtBQUEsUUFBQSxFQUFBLFdBQUE7QUFDQSxlQUFBO2FBQUEsWUFBQSxJQUFBLFVBQUEifQ==",
        kind: ModuleKind::JavaScript,
      },
    ];

    for (got, expected) in eszip.into_iter().zip(expected) {
      let (got_specifier, got_module) = got;

      assert_eq!(got_specifier, expected.specifier);
      assert_eq!(got_module.kind, expected.kind);
      assert_eq!(
        String::from_utf8_lossy(&got_module.source().await.unwrap()),
        expected.source
      );
    }
  }

  #[cfg(feature = "sha256")]
  #[tokio::test]
  async fn test_eszip_v2_iterator() {
    let file = std::fs::File::open("./testdata/redirect.eszip2").unwrap();
    let (eszip, fut) = Eszip::parse(AllowStdIo::new(file)).await.unwrap();
    tokio::spawn(fut);
    assert!(matches!(eszip, Eszip::V2(_)));

    struct Expected {
      specifier: String,
      source: &'static str,
      kind: ModuleKind,
    }

    let expected = vec![
      Expected {
        specifier: "file:///main.ts".to_string(),
        source: "export * as a from \"./a.ts\";\n",
        kind: ModuleKind::JavaScript,
      },
      Expected {
        specifier: "file:///b.ts".to_string(),
        source: "export const b = \"b\";\n",
        kind: ModuleKind::JavaScript,
      },
      Expected {
        specifier: "file:///a.ts".to_string(),
        source: "export const b = \"b\";\n",
        kind: ModuleKind::JavaScript,
      },
    ];

    for (got, expected) in eszip.into_iter().zip(expected) {
      let (got_specifier, got_module) = got;

      assert_eq!(got_specifier, expected.specifier);
      assert_eq!(got_module.kind, expected.kind);
      assert_eq!(
        String::from_utf8_lossy(&got_module.source().await.unwrap()),
        expected.source
      );
    }
  }

  #[tokio::test]
  async fn parse_small_chunks_reader() {
    let bytes = std::fs::read("./testdata/redirect.eszip2")
      .unwrap()
      .chunks(2)
      .map(|chunk| chunk.to_vec())
      .collect::<Vec<_>>();
    let reader = stream::iter(bytes)
      .map(std::io::Result::Ok)
      .into_async_read();

    let (eszip, fut) = Eszip::parse(reader).await.unwrap();
    fut.await.unwrap();
    assert!(matches!(eszip, Eszip::V2(_)));
  }
}