deno_bundle_runtime/
lib.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use deno_core::OpState;
9use deno_core::error::AnyError;
10use deno_core::op2;
11use deno_error::JsErrorBox;
12
13deno_core::extension!(
14  deno_bundle_runtime,
15  deps = [
16    deno_web
17  ],
18  ops = [
19    op_bundle,
20  ],
21  esm = [
22    "bundle.ts"
23  ],
24  options = {
25    bundle_provider: Option<Arc<dyn BundleProvider>>,
26  },
27  state = |state, options| {
28    if let Some(bundle_provider) = options.bundle_provider {
29      state.put(bundle_provider);
30    } else {
31      state.put::<Arc<dyn BundleProvider>>(Arc::new(()));
32    }
33  },
34);
35
36#[async_trait]
37impl BundleProvider for () {
38  async fn bundle(
39    &self,
40    _options: BundleOptions,
41  ) -> Result<BuildResponse, AnyError> {
42    Err(deno_core::anyhow::anyhow!(
43      "default BundleProvider does not do anything"
44    ))
45  }
46}
47
48#[async_trait]
49pub trait BundleProvider: Send + Sync {
50  async fn bundle(
51    &self,
52    options: BundleOptions,
53  ) -> Result<BuildResponse, AnyError>;
54}
55
56#[derive(Clone, Debug, Eq, PartialEq, Default, serde::Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct BundleOptions {
59  pub entrypoints: Vec<String>,
60  #[serde(default)]
61  pub output_path: Option<String>,
62  #[serde(default)]
63  pub output_dir: Option<String>,
64  #[serde(default)]
65  pub external: Vec<String>,
66  #[serde(default)]
67  pub format: BundleFormat,
68  #[serde(default)]
69  pub minify: bool,
70  #[serde(default)]
71  pub code_splitting: bool,
72  #[serde(default = "tru")]
73  pub inline_imports: bool,
74  #[serde(default)]
75  pub packages: PackageHandling,
76  #[serde(default)]
77  pub sourcemap: Option<SourceMapType>,
78  #[serde(default)]
79  pub platform: BundlePlatform,
80  #[serde(default = "tru")]
81  pub write: bool,
82}
83
84fn tru() -> bool {
85  true
86}
87
88#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub enum BundlePlatform {
91  Browser,
92  #[default]
93  Deno,
94}
95
96#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub enum BundleFormat {
99  #[default]
100  Esm,
101  Cjs,
102  Iife,
103}
104
105#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub enum SourceMapType {
108  #[default]
109  Linked,
110  Inline,
111  External,
112}
113
114impl std::fmt::Display for BundleFormat {
115  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116    match self {
117      BundleFormat::Esm => write!(f, "esm"),
118      BundleFormat::Cjs => write!(f, "cjs"),
119      BundleFormat::Iife => write!(f, "iife"),
120    }
121  }
122}
123
124impl std::fmt::Display for SourceMapType {
125  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126    match self {
127      SourceMapType::Linked => write!(f, "linked"),
128      SourceMapType::Inline => write!(f, "inline"),
129      SourceMapType::External => write!(f, "external"),
130    }
131  }
132}
133
134#[derive(Clone, Debug, Eq, PartialEq, Copy, Default, serde::Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub enum PackageHandling {
137  #[default]
138  Bundle,
139  External,
140}
141
142impl std::fmt::Display for PackageHandling {
143  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144    match self {
145      PackageHandling::Bundle => write!(f, "bundle"),
146      PackageHandling::External => write!(f, "external"),
147    }
148  }
149}
150#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct Message {
153  pub text: String,
154  pub location: Option<Location>,
155  pub notes: Vec<Note>,
156}
157
158#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct PartialMessage {
161  pub id: Option<String>,
162  pub plugin_name: Option<String>,
163  pub text: Option<String>,
164  pub location: Option<Location>,
165  pub notes: Option<Vec<Note>>,
166  pub detail: Option<u32>,
167}
168
169#[derive(Debug, Clone, serde::Serialize)]
170#[serde(rename_all = "camelCase")]
171pub struct BuildOutputFile {
172  pub path: String,
173  pub contents: Option<Vec<u8>>,
174  pub hash: String,
175}
176#[derive(Debug, Clone, serde::Serialize)]
177#[serde(rename_all = "camelCase")]
178pub struct BuildResponse {
179  pub errors: Vec<Message>,
180  pub warnings: Vec<Message>,
181  pub output_files: Option<Vec<BuildOutputFile>>,
182}
183#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct Note {
186  pub text: String,
187  pub location: Option<Location>,
188}
189#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct Location {
192  pub file: String,
193  pub namespace: Option<String>,
194  pub line: u32,
195  pub column: u32,
196  pub length: Option<u32>,
197  pub suggestion: Option<String>,
198}
199
200fn deserialize_regex<'de, D>(deserializer: D) -> Result<regex::Regex, D::Error>
201where
202  D: serde::Deserializer<'de>,
203{
204  use serde::Deserialize;
205  let s = String::deserialize(deserializer)?;
206  regex::Regex::new(&s).map_err(serde::de::Error::custom)
207}
208
209#[derive(Debug, Clone, serde::Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct OnResolveOptions {
212  #[serde(deserialize_with = "deserialize_regex")]
213  pub filter: regex::Regex,
214  pub namespace: Option<String>,
215}
216
217#[derive(Debug, Clone, serde::Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct OnLoadOptions {
220  #[serde(deserialize_with = "deserialize_regex")]
221  pub filter: regex::Regex,
222  pub namespace: Option<String>,
223}
224
225#[op2(async)]
226#[serde]
227pub async fn op_bundle(
228  state: Rc<RefCell<OpState>>,
229  #[serde] options: BundleOptions,
230) -> Result<BuildResponse, JsErrorBox> {
231  // eprintln!("op_bundle: {:?}", options);
232  let provider = {
233    let state = state.borrow();
234    state.borrow::<Arc<dyn BundleProvider>>().clone()
235  };
236
237  provider
238    .bundle(options)
239    .await
240    .map_err(|e| JsErrorBox::generic(e.to_string()))
241}