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