Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/dev/js.rs

15.2 KiB, 42 runs

created by r1870400018:979, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A JavaScript and TypeScript module bundler based on the SWC compiler.
2//!
3//! This module provides functionality to bundle JavaScript and TypeScript files into a single
4//! minified output file. It handles module dependencies, import resolution, and code transformations.
5use oxedyne_fe2o3_core::{
6 prelude::*,
7};
8
9use std::{
10 collections::{
11 HashMap,
12 HashSet,
13 },
14 path::{
15 Path,
16 PathBuf,
17 },
18 str::FromStr,
19 sync::Arc,
20};
21
22use swc_bundler::{
23 Bundler,
24 Hook,
25 Load,
26 ModuleData,
27 ModuleRecord,
28 ModuleType,
29 Resolve,
30};
31use swc_common::{
32 FileName,
33 GLOBALS,
34 Span,
35 SourceMap,
36};
37use swc_ecma_ast::{
38 EsVersion,
39 KeyValueProp,
40 PropName,
41 Expr,
42 Lit,
43 Str,
44 IdentName,
45};
46use swc_ecma_codegen::{
47 text_writer::JsWriter,
48 Emitter,
49};
50use swc_ecma_loader::{
51 resolve::Resolution,
52 resolvers::{
53 lru::CachingResolver,
54 node::NodeModulesResolver,
55 },
56 TargetEnv,
57};
58use swc_ecma_parser::{
59 parse_file_as_module,
60 Syntax,
61 TsSyntax,
62};
63
64
65/// Supported file extensions for bundling.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum FileType {
68 JavaScript,
69 JavaScriptModule,
70 TypeScript,
71 TypeScriptModule,
72 TypeScriptReact,
73}
74
75impl FromStr for FileType {
76 type Err = Error<ErrTag>;
77
78 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 match s {
80 "js" => Ok(Self::JavaScript),
81 "mjs" => Ok(Self::JavaScriptModule),
82 "ts" => Ok(Self::TypeScript),
83 "mts" => Ok(Self::TypeScriptModule),
84 "tsx" => Ok(Self::TypeScriptReact),
85 _ => Err(err!(
86 "Unknown file type extension: {}", s;
87 Invalid, Input, String, Conversion))
88 }
89 }
90}
91
92/// Module resolution allowing the use of javascript import aliases.
93#[derive(Debug)]
94pub struct AliasResolver {
95 aliases: Arc<Vec<(String, PathBuf)>>,
96}
97
98impl AliasResolver {
99 pub fn new(
100 aliases: Vec<(String, PathBuf)>,
101 )
102 -> Self
103 {
104 Self {
105 aliases: Arc::new(aliases),
106 }
107 }
108
109 fn try_resolve_with_extensions(&self, path: &Path) -> Option<PathBuf> {
110 // First try exact path.
111 if path.exists() {
112 return Some(path.to_path_buf());
113 }
114
115 // Try file extensions if path doesn't have one
116 if path.extension().is_none() {
117 // Regular file extensions
118 let file_exts = [".mjs", ".js", ".ts"];
119 for ext in file_exts {
120 let with_ext = path.with_extension(ext.trim_start_matches('.'));
121 if with_ext.exists() {
122 return Some(with_ext);
123 }
124 }
125
126 // Check for index files if path is directory
127 if path.is_dir() {
128 for ext in file_exts {
129 let index_path = path.join(fmt!("index{}", ext));
130 if index_path.exists() {
131 return Some(index_path);
132 }
133 }
134 }
135 }
136
137 None
138 }
139}
140
141impl Resolve for AliasResolver {
142
143 fn resolve(
144 &self,
145 base: &FileName,
146 module_specifier: &str,
147 )
148 -> Result<Resolution, anyhow::Error>
149 {
150 // First try to resolve using aliases.
151 for (alias, path) in self.aliases.iter() {
152 if module_specifier.starts_with(alias) {
153 // Remove alias prefix and combine with alias path.
154 if let Some(rel_path) = module_specifier.strip_prefix(alias) {
155 let full_path = path.join(rel_path.trim_start_matches('/'));
156
157 // Try with extensions.
158 if let Some(resolved_path) = self.try_resolve_with_extensions(&full_path) {
159 // Resolved alias paths.
160 return Ok(Resolution {
161 filename: FileName::Real(resolved_path),
162 slug: None,
163 });
164 }
165
166 // If no extension matches but the specified path exists, use it.
167 if full_path.exists() {
168 // Exact alias paths.
169 return Ok(Resolution {
170 filename: FileName::Real(full_path),
171 slug: None,
172 });
173 }
174 }
175 }
176 }
177
178 // If not an alias, fallback to node module resolution.
179 let resolver = NodeModulesResolver::new(
180 TargetEnv::Browser,
181 Default::default(),
182 true,
183 );
184
185 match resolver.resolve(base, module_specifier) {
186 // Successful node resolution.
187 Ok(resolution) => Ok(resolution),
188 // Failed resolution.
189 Err(_) => Ok(Resolution {
190 filename: FileName::Real(PathBuf::new()),
191 slug: None,
192 }),
193 }
194 }
195}
196
197/// Configuration for the bundler.
198#[derive(Debug, Clone)]
199pub struct BundleConfig {
200 /// Primary entry point files (if empty, all found files are entries).
201 pub entry_points: Vec<PathBuf>,
202 /// Supported file types to bundle.
203 pub file_types: HashSet<FileType>,
204 /// Target ECMAScript version.
205 pub target_version: EsVersion,
206 /// Enable TypeScript support.
207 pub typescript: bool,
208 /// Enable minification.
209 pub minify: bool,
210}
211
212impl Default for BundleConfig {
213 fn default() -> Self {
214 let mut file_types = HashSet::new();
215 file_types.insert(FileType::JavaScript);
216 file_types.insert(FileType::JavaScriptModule);
217 file_types.insert(FileType::TypeScript);
218 file_types.insert(FileType::TypeScriptModule);
219 file_types.insert(FileType::TypeScriptReact);
220
221 Self {
222 entry_points: Vec::new(),
223 file_types,
224 target_version: EsVersion::Es2020,
225 typescript: true,
226 minify: true,
227 }
228 }
229}
230
231/// Custom hook for SWC bundler to handle imports.
232struct BundleHook;
233
234impl Hook for BundleHook {
235
236 fn get_import_meta_props(
237 &self,
238 span: Span,
239 record: &ModuleRecord,
240 )
241 -> Result<Vec<KeyValueProp>, anyhow::Error>
242 {
243 info!("Processing imports for module: {}", record.file_name);
244
245 // Convert filesystem path to web path
246 let web_path = match &record.file_name {
247 FileName::Real(path) => {
248 // Find the "www" directory in the path and take everything after it
249 path.to_string_lossy()
250 .split("/www/")
251 .nth(1)
252 .map(|p| fmt!("/{}", p))
253 .unwrap_or_else(|| "/".to_string())
254 },
255 _ => "/".to_string(),
256 };
257
258 let mut props = Vec::new();
259 props.push(KeyValueProp {
260 key: PropName::Ident(IdentName::new("url".into(), span)),
261 value: Box::new(Expr::Lit(Lit::Str(Str {
262 span,
263 value: web_path.into(),
264 raw: None,
265 }))),
266 });
267
268 Ok(props)
269 }
270}
271
272/// Loads JavaScript/TypeScript modules for the bundler.
273struct ModuleLoader {
274 /// Source map for code generation and error reporting.
275 cm: Arc<SourceMap>,
276 /// Configuration for parsing modules.
277 config: BundleConfig,
278}
279
280impl ModuleLoader {
281 fn new(cm: Arc<SourceMap>, config: BundleConfig) -> Self {
282 Self { cm, config }
283 }
284}
285
286impl Load for ModuleLoader {
287
288 fn load(&self, filename: &FileName) -> Result<ModuleData, anyhow::Error> {
289 let module_path = match filename {
290 FileName::Real(path) => path.display().to_string(),
291 _ => "<unknown>".to_string(),
292 };
293 info!("Attempting to load module: {}", module_path);
294
295 // Load the source file.
296 let fm = match filename {
297 FileName::Real(path) => {
298 if !path.exists() {
299 return Err(anyhow::anyhow!(err!(
300 "Module not found: {:?}. Check import paths in requesting module.",
301 path;
302 IO, File, Missing)));
303 }
304 match self.cm.load_file(path) {
305 Ok(fm) => {
306 debug!("Successfully loaded file: {}", module_path);
307 fm
308 },
309 Err(e) => return Err(anyhow::anyhow!(err!(e,
310 "Failed to load file: {:?}", path;
311 IO, File, Read))),
312 }
313 },
314 _ => return Err(anyhow::anyhow!(err!(
315 "Unsupported module type: {:?}", filename;
316 IO, File))),
317 };
318
319 // Determine syntax based on file extension and config
320 let syntax = if self.config.typescript {
321 Syntax::Typescript(TsSyntax {
322 tsx: fm.name.to_string().ends_with(".tsx"),
323 decorators: true,
324 dts: false,
325 no_early_errors: false,
326 disallow_ambiguous_jsx_like: false,
327 })
328 } else {
329 Syntax::Es(Default::default())
330 };
331
332 debug!("Parsing module with syntax: {:?}", syntax);
333 let module = match parse_file_as_module(
334 &fm,
335 syntax,
336 self.config.target_version,
337 None,
338 &mut vec![],
339 ) {
340 Ok(m) => {
341 debug!("Successfully parsed module");
342 m
343 },
344 Err(e) => return Err(anyhow::anyhow!(err!(
345 "Failed to parse module {}: {:?}", fm.name, e;
346 IO, Format))),
347 };
348
349 Ok(ModuleData {
350 fm,
351 module,
352 helpers: Default::default(),
353 })
354 }
355}
356
357/// Handles bundling of JavaScript and TypeScript files.
358pub struct JsBundle {
359 /// Bundle path map.
360 bundles_map: Vec<(PathBuf, PathBuf)>,
361 /// Path aliases for module resolution.
362 path_aliases: Vec<(String, PathBuf)>,
363 /// Bundling configuration.
364 config: BundleConfig,
365}
366
367impl JsBundle {
368
369 /// Entry points must be specified to ensure correct bundling regardless of project structure.
370 /// The bundler will follow imports from these entry points to build the dependency graph.
371 pub fn new(
372 bundles_map: Vec<(PathBuf, PathBuf)>,
373 path_aliases: Vec<(String, PathBuf)>,
374 )
375 -> Self
376 {
377 let entry_points = bundles_map
378 .clone()
379 .into_iter()
380 .map(|jsbm| jsbm.0)
381 .collect();
382 Self {
383 bundles_map,
384 path_aliases,
385 config: BundleConfig {
386 entry_points,
387 ..Default::default()
388 },
389 }
390 }
391
392 pub fn bundle_entries(
393 &self,
394 src_dir: &Path,
395 )
396 -> Outcome<()>
397 {
398 info!("Starting JS bundling from directory: {:?}", src_dir);
399
400 for js_bundle_map in &self.bundles_map {
401 info!("Bundling entry point: {:?} -> {:?}",
402 js_bundle_map.0, js_bundle_map.1);
403
404 // Verify entry file exists
405 if !js_bundle_map.0.exists() {
406 return Err(err!(
407 "Entry point file not found: {:?}", js_bundle_map.0;
408 IO, File, Missing));
409 }
410
411 let mut entries = HashMap::new();
412 entries.insert(
413 fmt!("entry"),
414 FileName::Real(js_bundle_map.0.clone()),
415 );
416
417 // Create output directory if needed
418 if let Some(parent) = js_bundle_map.1.parent() {
419 if !parent.exists() {
420 res!(std::fs::create_dir_all(parent));
421 }
422 }
423
424 let result = GLOBALS.set(&swc_common::Globals::new(), || {
425 let cm = Arc::new(SourceMap::default());
426 let globals = GLOBALS.set(&swc_common::Globals::new(), || {
427 swc_common::Globals::new()
428 });
429
430 // Create custom resolver with aliases
431 let resolver = CachingResolver::new(
432 4096,
433 AliasResolver::new(
434 self.path_aliases.clone(),
435 ),
436 );
437
438 let loader = ModuleLoader::new(cm.clone(), self.config.clone());
439
440 let mut bundler = Bundler::new(
441 &globals,
442 cm.clone(),
443 loader,
444 resolver,
445 swc_bundler::Config {
446 require: false,
447 disable_inliner: false,
448 external_modules: vec![],
449 module: ModuleType::Es,
450 disable_fixer: false,
451 disable_hygiene: false,
452 disable_dce: false,
453 },
454 Box::new(BundleHook),
455 );
456
457 let bundles = catch_other!(bundler.bundle(entries), IO, Format);
458 info!("Bundle generation complete, processing {} modules", bundles.len());
459
460 let mut combined = String::new();
461 for (idx, bundle) in bundles.iter().enumerate() {
462 info!("Processing bundle module {}/{}", idx + 1, bundles.len());
463 let mut buf = vec![];
464 {
465 let writer = JsWriter::new(
466 cm.clone(),
467 "\n",
468 &mut buf,
469 None,
470 );
471 let mut emitter = Emitter {
472 cfg: swc_ecma_codegen::Config::default(),
473 comments: None,
474 cm: cm.clone(),
475 wr: Box::new(writer),
476 };
477
478 if let Err(e) = emitter.emit_module(&bundle.module) {
479 return Err(err!(e,
480 "Failed to emit bundle module {} for entry point {:?}",
481 idx, js_bundle_map.0;
482 IO, Format));
483 }
484 }
485
486 match String::from_utf8(buf) {
487 Ok(s) => combined.push_str(&s),
488 Err(e) => return Err(err!(e,
489 "Generated invalid UTF-8 in bundle module {}", idx;
490 IO, Format)),
491 };
492 combined.push('\n');
493 }
494
495 // Write bundle with error context
496 if let Err(e) = std::fs::write(&js_bundle_map.1, combined) {
497 return Err(err!(e,
498 "Failed to write bundle to {:?}", js_bundle_map.1;
499 IO, File, Write));
500 }
501
502 info!("Successfully bundled {:?}", js_bundle_map.0);
503 Ok(())
504 });
505
506 res!(result);
507 }
508
509 info!("JavaScript bundling completed successfully");
510 Ok(())
511 }
512
513}