rolldown_common 1.0.0

This crate is mostly for sharing code between rolldwon crates.
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
use crate::{
  AddEntryModuleMsg, FilenameTemplate, ModuleId, ModuleLoaderMsg, Modules,
  NormalizedBundlerOptions, Output, OutputAsset, OutputChunk, PreserveEntrySignatures, StrOrBytes,
  is_path_fragment,
};
use anyhow::Context;
use arcstr::ArcStr;
use dashmap::{DashMap, DashSet, Entry};
use rolldown_error::{BuildDiagnostic, InvalidOptionType};
use rolldown_utils::dashmap::{FxDashMap, FxDashSet};
use rolldown_utils::make_unique_name::make_unique_name;
use rolldown_utils::xxhash::{xxhash_base64_url, xxhash_with_base};
use std::ffi::OsStr;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use sugar_path::SugarPath;

#[derive(Debug, Default)]
pub struct EmittedAsset {
  pub name: Option<String>,
  pub original_file_name: Option<String>,
  pub file_name: Option<ArcStr>,
  pub source: StrOrBytes,
}

impl EmittedAsset {
  pub fn name_for_sanitize(&self) -> &str {
    self.name.as_deref().unwrap_or("asset")
  }

  /// Returns true if the emitted asset has a valid name (not an absolute or relative path).
  /// Similar to Rollup's `hasValidName` function.
  pub fn has_valid_name(&self) -> bool {
    let validated_name = self.file_name.as_deref().or(self.name.as_deref());
    validated_name.is_none_or(|name| !is_path_fragment(name))
  }

  /// Returns the validated name (fileName or name) if present.
  pub fn validated_name(&self) -> Option<&str> {
    self.file_name.as_deref().or(self.name.as_deref())
  }
}

#[derive(Debug, Default)]
pub struct EmittedChunk {
  pub name: Option<ArcStr>,
  pub file_name: Option<ArcStr>,
  pub id: String,
  // pub implicitly_loaded_after_one_of: Option<Vec<String>>,
  pub importer: Option<String>,
  pub preserve_entry_signatures: Option<PreserveEntrySignatures>,
}

pub struct EmittedChunkInfo {
  pub reference_id: ArcStr,
  pub filename: ArcStr,
}

#[derive(Debug, Clone)]
pub struct EmittedPrebuiltChunk {
  pub file_name: ArcStr,
  pub name: Option<ArcStr>,
  pub code: String,
  pub exports: Vec<String>,
  pub map: Option<rolldown_sourcemap::SourceMap>,
  pub sourcemap_filename: Option<String>,
  pub facade_module_id: Option<ArcStr>,
  pub is_entry: bool,
  pub is_dynamic_entry: bool,
}

#[derive(Debug)]
pub struct FileEmitter {
  tx: Arc<Mutex<Option<tokio::sync::mpsc::UnboundedSender<ModuleLoaderMsg>>>>,
  source_hash_to_reference_id: FxDashMap<ArcStr, ArcStr>,
  names: FxDashMap<ArcStr, u32>,
  files: FxDashMap<ArcStr, OutputAsset>,
  chunks: FxDashMap<ArcStr, Arc<EmittedChunk>>,
  prebuilt_chunks: FxDashMap<ArcStr, Arc<EmittedPrebuiltChunk>>,
  base_reference_id: AtomicUsize,
  options: Arc<NormalizedBundlerOptions>,
  /// Mark the files that have been emitted to bundle.
  emitted_files: FxDashSet<ArcStr>,
  emitted_chunks: FxDashMap<ArcStr, ArcStr>,
  emitted_filenames: FxDashSet<ArcStr>,
  /// Maps module IDs to their emitted file reference IDs.
  /// Used by the asset module plugin to associate modules with emitted files
  /// so that the `new URL()` finalizer can look up asset filenames.
  module_to_file_ref: FxDashMap<ArcStr, ArcStr>,
}

impl FileEmitter {
  pub fn new(options: Arc<NormalizedBundlerOptions>) -> Self {
    Self {
      tx: Arc::new(Mutex::new(None)),
      source_hash_to_reference_id: DashMap::default(),
      names: DashMap::default(),
      files: DashMap::default(),
      chunks: DashMap::default(),
      prebuilt_chunks: DashMap::default(),
      emitted_chunks: DashMap::default(),
      base_reference_id: AtomicUsize::new(0),
      options,
      emitted_files: DashSet::default(),
      emitted_filenames: FxDashSet::default(),
      module_to_file_ref: DashMap::default(),
    }
  }

  pub fn set_emitted_chunk_info(&self, emitted_chunk_info: impl Iterator<Item = EmittedChunkInfo>) {
    for info in emitted_chunk_info {
      self.emitted_chunks.insert(info.reference_id, info.filename);
    }
  }

  pub fn emit_chunk(&self, chunk: Arc<EmittedChunk>) -> anyhow::Result<ArcStr> {
    // Must stay synchronous: making this async would force the napi binding back
    // onto `block_on`, pinning the JS thread while `send().await` waits on channel
    // capacity — but the consumer draining the channel itself needs the JS thread
    // to run plugin hooks. That cycle is the emit-chunk deadlock. Keep the channel
    // unbounded (so `send()` never waits) and release the lock before the send.
    let sender = self
      .tx
      .lock()
      .ok()
      .context("Failed to acquire FileEmitter tx lock")?
      .clone()
      .context(
        "The `PluginContext.emitFile` with `type: 'chunk'` only work at `buildStart/resolveId/load/transform/moduleParsed` hooks.",
      )?;
    // Only assign a reference id once we know we have a live sender — keeps
    // `emit_chunk` side-effect-free on the error path.
    let reference_id = self.assign_reference_id(chunk.name.clone());
    sender
      .send(ModuleLoaderMsg::AddEntryModule(Box::new(AddEntryModuleMsg {
        chunk: Arc::clone(&chunk),
        reference_id: reference_id.clone(),
      })))
      .map_err(|e| {
        anyhow::Error::new(e).context(
          "FileEmitter: failed to send AddEntryModule message - module loader shut down during file emission",
        )
      })?;
    self.chunks.insert(reference_id.clone(), chunk);
    Ok(reference_id)
  }

  pub fn emit_prebuilt_chunk(&self, chunk: EmittedPrebuiltChunk) -> ArcStr {
    let reference_id = self.assign_reference_id(Some(chunk.file_name.clone()));
    self.prebuilt_chunks.insert(reference_id.clone(), Arc::new(chunk));
    reference_id
  }

  pub fn emit_file(
    &self,
    mut file: EmittedAsset,
    asset_filename_template: Option<FilenameTemplate>,
    sanitized_file_name: Option<ArcStr>,
  ) -> anyhow::Result<ArcStr> {
    if !file.has_valid_name() {
      return Err(
        BuildDiagnostic::invalid_option(InvalidOptionType::InvalidEmittedFileName(
          file.validated_name().unwrap_or_default().to_string(),
        ))
        .into(),
      );
    }

    let hash: ArcStr =
      xxhash_with_base(file.source.as_bytes(), self.options.hash_characters.base()).into();

    // Deduplicate assets if an explicit fileName is not provided
    let reference_id = if file.file_name.is_none() {
      // Use entry API to atomically check and insert
      match self.source_hash_to_reference_id.entry(hash.clone()) {
        Entry::Occupied(entry) => {
          // File already exists, add metadata and return existing reference_id
          let reference_id = entry.get().clone();
          self.files.entry(reference_id.clone()).and_modify(|output| {
            if let Some(name) = file.name {
              output.names.push(name);
            }
            if let Some(original_file_name) = file.original_file_name {
              output.original_file_names.push(original_file_name);
            }
          });
          return Ok(reference_id);
        }
        Entry::Vacant(entry) => {
          // First time seeing this file, generate reference_id and continue
          let reference_id = self.assign_reference_id(None);
          entry.insert(reference_id.clone());
          reference_id
        }
      }
    } else {
      // File has explicit fileName, no deduplication needed
      self.assign_reference_id(file.file_name.clone())
    };

    // Generate filename and insert into files map
    self.generate_file_name(&mut file, &hash, asset_filename_template, sanitized_file_name)?;
    self.files.insert(
      reference_id.clone(),
      OutputAsset {
        filename: file.file_name.unwrap(),
        source: std::mem::take(&mut file.source),
        names: std::mem::take(&mut file.name).map_or(vec![], |name| vec![name]),
        original_file_names: std::mem::take(&mut file.original_file_name)
          .map_or(vec![], |original_file_name| vec![original_file_name]),
      },
    );
    Ok(reference_id)
  }

  pub fn get_file_name(&self, reference_id: &str) -> anyhow::Result<ArcStr> {
    if let Some(file) = self.files.get(reference_id) {
      return Ok(file.filename.clone());
    }
    if let Some(chunk) = self.chunks.get(reference_id) {
      if let Some(filename) = chunk.file_name.as_ref() {
        return Ok(filename.clone());
      }
      if let Some(filename) = self.emitted_chunks.get(reference_id) {
        return Ok(filename.clone());
      }
      return Err(anyhow::anyhow!(
        "Unable to get file name for emitted chunk: {reference_id}.You can only get file names once chunks have been generated after the 'renderStart' hook."
      ));
    }
    if let Some(prebuilt_chunk) = self.prebuilt_chunks.get(reference_id) {
      return Ok(prebuilt_chunk.file_name.clone());
    }
    Err(anyhow::anyhow!("Unable to get file name for unknown file: {reference_id}"))
  }

  pub fn assign_reference_id(&self, filename: Option<ArcStr>) -> ArcStr {
    xxhash_base64_url(
      filename
        .unwrap_or_else(|| {
          self.base_reference_id.fetch_add(1, Ordering::Relaxed).to_string().into()
        })
        .as_bytes(),
    )
    // The reference id can be used for import.meta.ROLLUP_FILE_URL_referenceId and therefore needs to be a valid identifier.
    .replace('-', "$")
    .into()
  }

  pub fn generate_file_name(
    &self,
    file: &mut EmittedAsset,
    hash: &ArcStr,
    filename_template: Option<FilenameTemplate>,
    sanitized_file_name: Option<ArcStr>,
  ) -> anyhow::Result<()> {
    if file.file_name.is_none() {
      let sanitized_file_name = sanitized_file_name.expect("should has sanitized file name");
      let path = Path::new(sanitized_file_name.as_str());
      // Extract extension from the filename only
      let extension = path.extension().and_then(OsStr::to_str);
      // Extract name including directory path, but without extension
      // e.g., "foo/bar.txt" -> "foo/bar", "bar.txt" -> "bar"
      // Security: normalize path and filter out dangerous components
      let name = path.file_stem().and_then(OsStr::to_str).map(|stem| {
        if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
          // Normalize to resolve ".." and "." where possible, then convert to forward slashes
          parent.join(stem).normalize().to_slash_lossy().into_owned()
        } else {
          stem.to_string()
        }
      });
      let filename_template =
        filename_template.expect("should has filename template without filename");

      let mut filename = filename_template
        .render(
          name.as_deref(),
          None,
          Some(extension.unwrap_or_default()),
          Some(|len: Option<usize>| Ok(&hash[..len.map_or(8, |len| len.clamp(1, 21))])),
        )?
        .into();

      // deconflict file name
      // TODO(underfin): could be using bundle files key as `make_unique_name`
      filename = make_unique_name(&filename, &self.names);

      file.file_name = Some(filename);
    }
    Ok(())
  }

  pub fn add_additional_files(
    &self,
    bundle: &mut Vec<Output>,
    warnings: &mut Vec<BuildDiagnostic>,
  ) {
    let mut additional_assets = Vec::new();
    self.files.iter_mut().for_each(|mut file| {
      let (key, value) = file.pair_mut();
      if self.emitted_files.contains(key) {
        return;
      }
      self.emitted_files.insert(key.clone());

      // Follow rollup using lowercase filename to check conflicts
      let lowercase_filename = value.filename.as_str().to_lowercase().into();
      if !self.emitted_filenames.insert(lowercase_filename) {
        warnings
          .push(BuildDiagnostic::filename_conflict(value.filename.clone()).with_severity_warning());
      }

      let mut names = std::mem::take(&mut value.names);
      sort_names(&mut names);

      let mut original_file_names = std::mem::take(&mut value.original_file_names);
      original_file_names.sort_unstable();
      additional_assets.push(Output::Asset(Arc::new(OutputAsset {
        filename: value.filename.clone(),
        names,
        original_file_names,
        source: std::mem::take(&mut value.source),
      })));
    });
    // Sort to ensure deterministic output order regardless of DashMap iteration order
    additional_assets.sort_unstable_by(|a, b| a.filename().cmp(b.filename()));
    bundle.extend(additional_assets);

    // Add prebuilt chunks to the bundle
    self.prebuilt_chunks.iter().for_each(|prebuilt_chunk| {
      let (key, value) = prebuilt_chunk.pair();
      if self.emitted_files.contains(key) {
        return;
      }
      self.emitted_files.insert(key.clone());

      // Check for filename conflicts
      let lowercase_filename: ArcStr = value.file_name.as_str().to_lowercase().into();
      if !self.emitted_filenames.insert(lowercase_filename) {
        warnings.push(
          BuildDiagnostic::filename_conflict(value.file_name.clone()).with_severity_warning(),
        );
      }

      bundle.push(Output::Chunk(Arc::new(OutputChunk {
        name: value.name.clone().unwrap_or_else(|| value.file_name.clone()),
        is_entry: value.is_entry,
        is_dynamic_entry: value.is_dynamic_entry,
        facade_module_id: value.facade_module_id.clone().map(ModuleId::from),
        module_ids: vec![],
        exports: value.exports.iter().map(|s| s.as_str().into()).collect(),
        filename: value.file_name.clone(),
        modules: Modules { keys: vec![], values: vec![] },
        imports: vec![],
        dynamic_imports: vec![],
        code: value.code.clone(),
        map: value.map.clone(),
        sourcemap_filename: value.sourcemap_filename.clone(),
        preliminary_filename: value.file_name.to_string(),
      })));
    });
  }

  pub fn set_context_load_modules_tx(
    &self,
    tx: Option<tokio::sync::mpsc::UnboundedSender<ModuleLoaderMsg>>,
  ) -> anyhow::Result<()> {
    *self.tx.lock().ok().context("Failed to acquire FileEmitter tx lock")? = tx;
    Ok(())
  }

  /// Associate a module ID with an emitted file reference ID.
  /// This allows the `new URL()` finalizer to look up asset filenames by module ID.
  pub fn associate_module_with_file_ref(&self, module_id: &str, reference_id: &str) {
    self.module_to_file_ref.insert(ArcStr::from(module_id), ArcStr::from(reference_id));
  }

  /// Get the emitted file reference ID for a given module ID.
  pub fn file_ref_for_module(&self, module_id: &str) -> Option<ArcStr> {
    self.module_to_file_ref.get(module_id).map(|v| v.value().clone())
  }

  pub fn clear(&self) {
    self.chunks.clear();
    self.files.clear();
    self.prebuilt_chunks.clear();
    self.names.clear();
    self.source_hash_to_reference_id.clear();
    self.base_reference_id.store(0, Ordering::Relaxed);
    self.emitted_files.clear();
    self.emitted_chunks.clear();
    self.emitted_filenames.clear();
    self.module_to_file_ref.clear();
  }
}

fn sort_names(names: &mut [String]) {
  names.sort_unstable_by(|a, b| {
    let len_ord = a.len().cmp(&b.len());
    if len_ord == std::cmp::Ordering::Equal { a.cmp(b) } else { len_ord }
  });
}

pub type SharedFileEmitter = Arc<FileEmitter>;