dumpling 0.1.0

A fast JavaScript runtime and bundler in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::fs;

use crate::builtins;
use crate::error::{Result, DumplingError};
use crate::typescript::{TypeScriptTranspiler, TypeScriptConfig, is_typescript_file};
use crate::import_map::ImportMapResolver;

/// Prefix for Node.js built-in virtual modules
pub const BUILTIN_PREFIX: &str = "node:";

#[derive(Debug, Clone)]
pub struct Module {
    pub id: String,
    pub path: PathBuf,
    pub source: String,
    pub dependencies: Vec<String>,
    pub is_entry: bool,
    /// Module type: "js", "json", "css", or "ts"
    pub module_type: String,
}

impl Module {
    pub fn is_css(&self) -> bool {
        self.module_type == "css"
    }

    pub fn new(id: String, path: PathBuf, source: String, module_type: &str) -> Self {
        Self {
            id,
            path,
            source,
            dependencies: Vec::new(),
            is_entry: false,
            module_type: module_type.to_string(),
        }
    }
}

pub struct ModuleResolver {
    root: PathBuf,
    cache: HashMap<String, Module>,
    node_modules: PathBuf,
    ts_transpiler: TypeScriptTranspiler,
    import_map_resolver: ImportMapResolver,
}

impl ModuleResolver {
    pub fn new(root: PathBuf) -> Self {
        let node_modules = root.join("node_modules");
        let ts_config = TypeScriptConfig::default();
        let ts_transpiler = TypeScriptTranspiler::new(ts_config);
        let import_map_resolver = ImportMapResolver::new(&root);
        
        Self {
            root,
            cache: HashMap::new(),
            node_modules,
            ts_transpiler,
            import_map_resolver,
        }
    }
    
    pub async fn resolve(&mut self, specifier: &str, parent: &Path) -> Result<PathBuf> {
        // Check import map first
        if let Some(resolved_specifier) = self.import_map_resolver.resolve(specifier) {
            // For URL-based imports, we might need to handle them differently
            if resolved_specifier.starts_with("http://") || resolved_specifier.starts_with("https://") {
                // For now, we don't support direct URL imports
                return Err(DumplingError::ModuleResolution(
                    format!("URL imports not yet supported: {}", resolved_specifier)
                ));
            }
            // Treat the resolved specifier as a file path
            return self.resolve_file_path(Path::new(&resolved_specifier)).await;
        }

        // Handle Node.js built-in modules first
        if builtins::is_builtin(specifier) {
            let name = specifier.split('/').next().unwrap_or(specifier);
            return Ok(PathBuf::from(format!("{}{}", BUILTIN_PREFIX, name)));
        }

        // Handle relative imports
        if specifier.starts_with("./") || specifier.starts_with("../") {
            let path = parent.join(specifier);
            return self.resolve_file_path(&path).await;
        }

        // Handle absolute imports
        if specifier.starts_with("/") {
            return self.resolve_file_path(Path::new(specifier)).await;
        }

        // Handle node_modules imports
        self.resolve_node_modules(specifier).await
    }
    
    async fn resolve_file_path(&self, path: &Path) -> Result<PathBuf> {
        // Try direct file
        if path.is_file() {
            return Ok(path.to_path_buf());
        }
        
        // Try with .js extension
        let js_path = path.with_extension("js");
        if js_path.is_file() {
            return Ok(js_path);
        }
        
        // Try with .mjs extension
        let mjs_path = path.with_extension("mjs");
        if mjs_path.is_file() {
            return Ok(mjs_path);
        }

        // Try with .json extension (import assertions support)
        let json_path = path.with_extension("json");
        if json_path.is_file() {
            return Ok(json_path);
        }

        // Try with .ts extension
        let ts_path = path.with_extension("ts");
        if ts_path.is_file() {
            return Ok(ts_path);
        }

        // Try with .tsx extension
        let tsx_path = path.with_extension("tsx");
        if tsx_path.is_file() {
            return Ok(tsx_path);
        }

        // Try with .css extension
        let css_path = path.with_extension("css");
        if css_path.is_file() {
            return Ok(css_path);
        }

        // Try package.json main field
        if path.is_dir() {
            let package_json = path.join("package.json");
            if package_json.is_file() {
                let content = fs::read_to_string(&package_json)?;
                if let Ok(package) = serde_json::from_str::<serde_json::Value>(&content) {
                    if let Some(main) = package.get("main").and_then(|v| v.as_str()) {
                        let main_path = path.join(main);
                        if main_path.is_file() {
                            return Ok(main_path);
                        }
                    }
                }
            }
            
            // Try index.js
            let index_js = path.join("index.js");
            if index_js.is_file() {
                return Ok(index_js);
            }
            
            // Try index.mjs
            let index_mjs = path.join("index.mjs");
            if index_mjs.is_file() {
                return Ok(index_mjs);
            }

            // Try index.ts
            let index_ts = path.join("index.ts");
            if index_ts.is_file() {
                return Ok(index_ts);
            }

            // Try index.tsx
            let index_tsx = path.join("index.tsx");
            if index_tsx.is_file() {
                return Ok(index_tsx);
            }
        }
        
        Err(DumplingError::ModuleResolution(format!("Cannot resolve module: {}", path.display())))
    }

    async fn resolve_package_entry(
        &self,
        package_path: &Path,
        package: &str,
        subpath: Option<&str>,
    ) -> Result<PathBuf> {
        let package_json = package_path.join("package.json");
        if !package_json.is_file() {
            return self.fallback_package_resolution(package_path, package).await;
        }

        let content = fs::read_to_string(&package_json)?;
        let pkg_json: serde_json::Value = serde_json::from_str(&content)
            .map_err(|_| DumplingError::ModuleResolution("Invalid package.json".to_string()))?;

        // Resolve via exports field (full support: subpaths, conditional exports)
        let export_key = match subpath {
            Some(sp) => format!("./{}", sp),
            None => ".".to_string(),
        };

        if let Some(exports) = pkg_json.get("exports") {
            if let Some(resolved) = self.resolve_exports_field(exports, &export_key, package_path)? {
                return Ok(resolved);
            }
        }

        // Fallback: main, module (only for root)
        if subpath.is_none() {
            if let Some(main) = pkg_json.get("main").and_then(|v| v.as_str()) {
                let main_path = package_path.join(main);
                if main_path.is_file() {
                    return Ok(main_path);
                }
            }
            if let Some(module) = pkg_json.get("module").and_then(|v| v.as_str()) {
                let module_path = package_path.join(module);
                if module_path.is_file() {
                    return Ok(module_path);
                }
            }
        }

        // Fallback: try subpath as direct file path (e.g. lodash/fp -> ./fp or ./fp.js)
        if let Some(sp) = subpath {
            let subpath_path = package_path.join(sp);
            if let Ok(resolved) = self.resolve_file_path(&subpath_path).await {
                return Ok(resolved);
            }
        }

        self.fallback_package_resolution(package_path, package).await
    }

    fn resolve_exports_field(
        &self,
        exports: &serde_json::Value,
        key: &str,
        package_path: &Path,
    ) -> Result<Option<PathBuf>> {
        // Try exact match first
        if let Some(entry) = exports.get(key) {
            if let Some(path) = self.resolve_export_entry(entry, package_path)? {
                return Ok(Some(path));
            }
        }

        // Try wildcard: "./foo/*" matches "./foo/bar"
        if key.contains('/') {
            let parts: Vec<&str> = key.splitn(2, '/').collect();
            if parts.len() == 2 {
                let prefix = parts[0];
                let suffix = parts[1];
                let wildcard_key = format!("{}/*", prefix);
                if let Some(entry) = exports.get(&wildcard_key) {
                    if let Some(template) = entry.as_str() {
                        let resolved = template.replace("*", suffix);
                        let path = package_path.join(resolved.trim_start_matches("./"));
                        if path.is_file() {
                            return Ok(Some(path));
                        }
                    }
                }
            }
        }

        Ok(None)
    }

    fn resolve_export_entry(
        &self,
        entry: &serde_json::Value,
        package_path: &Path,
    ) -> Result<Option<PathBuf>> {
        // Simple string: "./dist/index.js"
        if let Some(s) = entry.as_str() {
            let path = package_path.join(s.trim_start_matches("./"));
            return Ok(if path.is_file() { Some(path) } else { None });
        }

        // Conditional object: { "require": "./cjs.js", "import": "./esm.js", "default": "./index.js", "browser": "./browser.js" }
        if let Some(obj) = entry.as_object() {
            // Enhanced condition order based on environment
            let conditions = if self.is_browser_environment() {
                vec!["browser", "import", "require", "node", "default"]
            } else {
                vec!["require", "node", "import", "default"]
            };
            
            for condition in conditions {
                if let Some(val) = obj.get(condition) {
                    if let Some(path) = self.resolve_export_entry(val, package_path)? {
                        return Ok(Some(path));
                    }
                }
            }
        }

        Ok(None)
    }

    fn is_browser_environment(&self) -> bool {
        // Check if we're in a browser-like environment
        // For now, we'll assume a browser environment if certain browser-related modules are requested
        // In a real implementation, this would be determined by configuration or runtime detection
        true
    }

    async fn fallback_package_resolution(
        &self,
        package_path: &Path,
        package: &str,
    ) -> Result<PathBuf> {
        let index_js = package_path.join("index.js");
        if index_js.is_file() {
            return Ok(index_js);
        }
        let index_mjs = package_path.join("index.mjs");
        if index_mjs.is_file() {
            return Ok(index_mjs);
        }
        Err(DumplingError::ModuleResolution(format!(
            "Cannot resolve entry point for package '{}'",
            package
        )))
    }

    async fn resolve_node_modules(&self, specifier: &str) -> Result<PathBuf> {
        // Split specifier into package and subpath
        let (package, subpath) = if let Some((pkg, sub)) = specifier.split_once('/') {
            (pkg, Some(sub))
        } else {
            (specifier, None)
        };
        
        // Look for package in node_modules
        let package_path = self.node_modules.join(package);
        if !package_path.exists() {
            return Err(DumplingError::ModuleResolution(format!("Package '{}' not found", package)));
        }
        
        // Resolve package entry (handles both main and subpath exports)
        self.resolve_package_entry(&package_path, package, subpath).await
    }
    
    pub async fn load_module(&mut self, path: PathBuf) -> Result<Module> {
        let path_str = path.display().to_string();

        // Check cache first
        if let Some(module) = self.cache.get(&path_str) {
            return Ok(module.clone());
        }

        // Handle Node.js built-in modules
        let (source, module_type) = if path_str.starts_with(BUILTIN_PREFIX) {
            let name = path_str.trim_start_matches(BUILTIN_PREFIX);
            (builtins::get_builtin_stub(name).to_string(), "js")
        } else if path_str.ends_with(".json") {
            // JSON import: wrap as module.exports
            let content = fs::read_to_string(&path)?;
            let parsed: serde_json::Value = serde_json::from_str(&content)?;
            (
                format!("module.exports = {};", serde_json::to_string(&parsed).unwrap_or_else(|_| "{}".to_string())),
                "json",
            )
        } else if path_str.ends_with(".css") {
            // CSS import: raw content for injection
            let content = fs::read_to_string(&path)?;
            (content, "css")
        } else if is_typescript_file(&path) {
            // TypeScript file: transpile to JavaScript
            let ts_content = fs::read_to_string(&path)?;
            let js_content = self.ts_transpiler.transpile(&path, &ts_content)?;
            (js_content, "js")
        } else {
            (fs::read_to_string(&path)?, "js")
        };

        // Create module
        let mut module = Module::new(path_str.clone(), path.clone(), source, module_type);

        // Extract dependencies (skip for builtins and CSS - they have none)
        if !path_str.starts_with(BUILTIN_PREFIX) && module_type != "css" {
            module.dependencies = self.extract_dependencies(&module.source);
        }

        // Cache and return
        self.cache.insert(path_str, module.clone());
        Ok(module)
    }
    
    fn extract_dependencies(&self, source: &str) -> Vec<String> {
        let mut dependencies = Vec::new();

        // Remove single-line comments to avoid extracting from commented code
        let source_no_comments: String = source
            .lines()
            .filter(|line| !line.trim_start().starts_with("//"))
            .collect::<Vec<_>>()
            .join("\n");

        // Enhanced regex-based extraction with import assertions support
        let import_regex = regex::Regex::new(r#"import.*from\s+['"]([^'"]+)['"]"#).unwrap();
        let require_regex = regex::Regex::new(r#"require\s*\(\s*['"]([^'"]+)['"]\s*\)"#).unwrap();
        let dynamic_import_regex = regex::Regex::new(r#"import\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*\{\s*[^}]*\}\s*\)?"#).unwrap();
        let import_assertion_regex = regex::Regex::new(r#"import\s+(['"])([^'"]+)\1\s+(?:assert\s+)?\{\s*type:\s*['"]([^'"]+)['"][^}]*\}"#).unwrap();

        for cap in import_regex.captures_iter(&source_no_comments) {
            dependencies.push(cap[1].to_string());
        }

        for cap in require_regex.captures_iter(&source_no_comments) {
            dependencies.push(cap[1].to_string());
        }

        for cap in dynamic_import_regex.captures_iter(&source_no_comments) {
            dependencies.push(cap[2].to_string());
        }

        for cap in import_assertion_regex.captures_iter(&source_no_comments) {
            dependencies.push(cap[2].to_string());
        }

        dependencies
    }
}