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
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
//! Enhanced transform module using internal Oxc parser and transformer.
//!
//! This module provides transform functionality similar to the bundler's internal transform,
//! but exposed as an API for use outside the bundling context.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use arcstr::ArcStr;
use oxc::allocator::Allocator;
use oxc::diagnostics::{OxcDiagnostic, Severity as OxcSeverity};
use oxc::parser::{ParseOptions, Parser};
use oxc::transformer::{Helper, HelperLoaderOptions};
use oxc::{
  codegen::{Codegen, CodegenOptions, CodegenReturn},
  isolated_declarations::{IsolatedDeclarations, IsolatedDeclarationsOptions},
  semantic::SemanticBuilder,
  span::SourceType,
  transformer::Transformer,
  transformer_plugins::{
    InjectGlobalVariables, InjectGlobalVariablesConfig, InjectImport, ReplaceGlobalDefines,
    ReplaceGlobalDefinesConfig,
  },
};
use oxc_resolver::TsConfig;
use rolldown_ecmascript::semantic_builder_for_transform;
use rolldown_error::{BuildDiagnostic, EventKind, Severity};
use rolldown_sourcemap::{SourceMap, collapse_sourcemaps};
use rustc_hash::FxHashMap;

use crate::inner_bundler_options::types::transform_option::{
  CompilerAssumptions, DecoratorOptions, Either,
  IsolatedDeclarationsOptions as RolldownIsolatedDeclarationsOptions, JsxOptions, PluginsOptions,
  TransformOptions, TypeScriptOptions,
};
use crate::inner_bundler_options::types::tsconfig_merge::merge_transform_options_with_tsconfig;

pub type InjectOptions = Vec<(String, Either<String, Vec<String>>)>;

/// Tsconfig option for enhanced transform.
#[derive(Debug, Clone)]
pub enum TsconfigOption {
  /// Auto-discover tsconfig.json by walking up from the file's directory.
  Auto,
  /// Use the provided tsconfig directly.
  Config(Arc<TsConfig>),
  /// Don't use tsconfig options.
  Disabled,
}

/// Result of an enhanced transform operation.
#[derive(Debug)]
pub struct EnhancedTransformResult {
  /// The transformed code
  pub code: String,
  /// The source map if sourcemap was enabled
  pub map: Option<SourceMap>,
  /// The .d.ts declaration file (if declaration generation was enabled)
  pub declaration: Option<String>,
  /// The declaration source map
  pub declaration_map: Option<SourceMap>,
  /// Helpers used.
  pub helpers_used: FxHashMap<Helper, String>,
  /// Parse and transformation errors
  pub errors: Vec<BuildDiagnostic>,
  /// Parse and transformation warnings
  pub warnings: Vec<BuildDiagnostic>,
  /// Paths to tsconfig files that were loaded during transformation.
  pub tsconfig_file_paths: Vec<PathBuf>,
}

impl EnhancedTransformResult {
  pub fn new_for_error(
    errors: Vec<BuildDiagnostic>,
    warnings: Vec<BuildDiagnostic>,
    tsconfig_file_paths: Vec<PathBuf>,
  ) -> Self {
    Self {
      code: String::new(),
      map: None,
      declaration: None,
      declaration_map: None,
      helpers_used: FxHashMap::default(),
      errors,
      warnings,
      tsconfig_file_paths,
    }
  }
}

/// Options for enhanced transform operations.
/// This is separate from `TransformOptions` to provide a clear API boundary
/// for the enhanced transform use case.
#[derive(Debug, Default, Clone)]
pub struct EnhancedTransformOptions {
  /// Configure how TSX and JSX are transformed.
  pub jsx: Option<Either<String, JsxOptions>>,

  /// Sets the target environment for the generated JavaScript.
  pub target: Option<Either<String, Vec<String>>>,

  /// Set assumptions in order to produce smaller output.
  pub assumptions: Option<CompilerAssumptions>,

  /// Decorator plugin options.
  pub decorator: Option<DecoratorOptions>,

  /// Configure how TypeScript is transformed.
  pub typescript: Option<TypeScriptOptions>,

  /// Third-party plugins to use.
  pub plugins: Option<PluginsOptions>,

  /// Behaviour for runtime helpers.
  pub helpers: Option<HelperLoaderOptions>,

  /// The current working directory. Used to resolve relative paths in other
  /// options.
  pub cwd: Option<String>,

  /// Override the source type inferred from the file extension.
  pub source_type: Option<SourceType>,

  /// Configure tsconfig handling.
  pub tsconfig: Option<TsconfigOption>,

  /// Enable source map generation.
  pub sourcemap: bool,

  /// An input source map to collapse with the output source map.
  /// This is useful when the source has already been transformed by another tool.
  pub input_map: Option<SourceMap>,

  /// Define plugin: replace global identifiers with constant expressions.
  pub define: Option<Vec<(String, String)>>,

  /// Inject plugin: auto-import globals from modules.
  /// Each entry is `(local_name, source)` where source is either:
  /// - `Left(module)`: namespace/default import from the module
  /// - `Right([module, export])`: named import from the module
  pub inject: Option<InjectOptions>,
}

impl EnhancedTransformOptions {
  #[expect(clippy::too_many_arguments)]
  pub fn from_transform_options(
    options: TransformOptions,
    cwd: Option<String>,
    source_type: Option<SourceType>,
    tsconfig: Option<TsconfigOption>,
    sourcemap: bool,
    input_map: Option<SourceMap>,
    define: Option<Vec<(String, String)>>,
    inject: Option<InjectOptions>,
  ) -> Self {
    Self {
      jsx: options.jsx,
      target: options.target,
      assumptions: options.assumptions,
      decorator: options.decorator,
      typescript: options.typescript,
      plugins: options.plugins,
      helpers: options.helpers,
      cwd,
      source_type,
      tsconfig,
      sourcemap,
      input_map,
      define,
      inject,
    }
  }
}

/// Generate isolated declarations from the parsed program.
fn generate_declarations(
  allocator: &oxc::allocator::Allocator,
  program: &oxc::ast::ast::Program,
  filename: &str,
  source: &ArcStr,
  options: &RolldownIsolatedDeclarationsOptions,
  errors: &mut Vec<BuildDiagnostic>,
  warnings: &mut Vec<BuildDiagnostic>,
) -> (Option<String>, Option<SourceMap>) {
  let isolated_decl_options =
    IsolatedDeclarationsOptions { strip_internal: options.strip_internal.unwrap_or(false) };

  let ret = IsolatedDeclarations::new(allocator, isolated_decl_options).build(program);
  if !ret.errors.is_empty() {
    append_oxc_diagnostics(ret.errors, source, filename, warnings, errors);
    if !errors.is_empty() {
      return (None, None);
    }
  }

  let enable_declaration_map = options.sourcemap.unwrap_or(false);
  let codegen_ret = Codegen::new()
    .with_options(CodegenOptions {
      source_map_path: enable_declaration_map.then(|| {
        let mut path = PathBuf::from(filename);
        path.set_extension("d.ts");
        path
      }),
      ..Default::default()
    })
    .build(&ret.program);
  (Some(codegen_ret.code), codegen_ret.map)
}

fn append_oxc_diagnostics(
  diagnostics: Vec<OxcDiagnostic>,
  source: &ArcStr,
  filename: &str,
  warnings: &mut Vec<BuildDiagnostic>,
  errors: &mut Vec<BuildDiagnostic>,
) {
  let (new_errors, new_warnings): (Vec<_>, Vec<_>) =
    diagnostics.into_iter().partition(|d| d.severity == OxcSeverity::Error);
  errors.extend(BuildDiagnostic::from_oxc_diagnostics(
    new_errors,
    &source.clone(),
    filename,
    Severity::Error,
    EventKind::ParseError,
  ));
  warnings.extend(BuildDiagnostic::from_oxc_diagnostics(
    new_warnings,
    &source.clone(),
    filename,
    Severity::Warning,
    EventKind::ParseError,
  ));
}

fn build_inject_config(
  inject: &[(String, Either<String, Vec<String>>)],
) -> InjectGlobalVariablesConfig {
  let inject_imports: Vec<InjectImport> = inject
    .iter()
    .map(|(local, value)| match value {
      Either::Left(module) => InjectImport::namespace_specifier(module, local),
      Either::Right(parts) if parts.len() >= 2 => {
        InjectImport::named_specifier(&parts[0], Some(&parts[1]), local)
      }
      Either::Right(parts) if parts.len() == 1 => {
        InjectImport::namespace_specifier(&parts[0], local)
      }
      Either::Right(_) => InjectImport::namespace_specifier("", local),
    })
    .collect();
  InjectGlobalVariablesConfig::new(inject_imports)
}

/// Transform source code using the internal Oxc parser and transformer.
///
/// # Arguments
/// * `filename` - The filename (used for source type detection and error reporting)
/// * `source_text` - The source code to transform
/// * `transform_options` - Transform options including tsconfig and sourcemap settings
/// * `yarn_pnp` - Whether to enable Yarn PnP support when resolving tsconfig
pub fn enhanced_transform(
  filename: &str,
  source_text: &str,
  transform_options: EnhancedTransformOptions,
  yarn_pnp: bool,
) -> EnhancedTransformResult {
  let mut errors = Vec::new();
  let mut warnings = Vec::new();
  let mut tsconfig_file_paths = Vec::new();

  let source_type = transform_options
    .source_type
    .unwrap_or_else(|| SourceType::from_path(Path::new(filename)).unwrap_or_default());
  let tsconfig: Option<Arc<TsConfig>> = match &transform_options.tsconfig {
    Some(TsconfigOption::Auto) | None => {
      let file_path = PathBuf::from(filename);
      let result = oxc_resolver::Resolver::new(oxc_resolver::ResolveOptions {
        tsconfig: Some(oxc_resolver::TsconfigDiscovery::Auto),
        yarn_pnp,
        ..Default::default()
      })
      .find_tsconfig(file_path);
      let found = match result {
        Ok(found) => found,
        Err(err) => {
          errors.push(BuildDiagnostic::tsconfig_error(filename.to_string(), err));
          return EnhancedTransformResult::new_for_error(errors, warnings, tsconfig_file_paths);
        }
      };
      if let Some(tsconfig) = &found {
        tsconfig_file_paths.push(tsconfig.path.clone());
      }
      found
    }
    Some(TsconfigOption::Config(config)) => Some(Arc::clone(config)),
    Some(TsconfigOption::Disabled) => None,
  };
  let enable_sourcemap = transform_options.sourcemap;
  let input_map = transform_options.input_map.clone();
  let define_options = transform_options.define.clone();
  let inject_options = transform_options.inject.clone();

  let bundler_options: TransformOptions = transform_options.into();
  let merged_options = if let Some(ref tsconfig) = tsconfig {
    let (merged, merge_warnings) =
      merge_transform_options_with_tsconfig(bundler_options, tsconfig, false);
    warnings.extend(merge_warnings);
    merged
  } else {
    bundler_options
  };
  let declaration_options =
    merged_options.typescript.as_ref().and_then(|ts| ts.declaration.clone());

  let oxc_transform_options: oxc::transformer::TransformOptions = match merged_options.try_into() {
    Ok(opts) => opts,
    Err(e) => {
      errors.push(BuildDiagnostic::bundler_initialize_error(e, None));
      return EnhancedTransformResult::new_for_error(errors, warnings, tsconfig_file_paths);
    }
  };

  let source: ArcStr = source_text.into();

  let allocator = Allocator::default();
  let parse_ret = Parser::new(&allocator, &source, source_type)
    .with_options(ParseOptions { allow_return_outside_function: true, ..Default::default() })
    .parse();
  if parse_ret.panicked || !parse_ret.errors.is_empty() {
    append_oxc_diagnostics(parse_ret.errors, &source, filename, &mut warnings, &mut errors);
    return EnhancedTransformResult::new_for_error(errors, warnings, tsconfig_file_paths);
  }

  let mut program = parse_ret.program;

  let semantic_ret = semantic_builder_for_transform().build(&program);
  let mut scoping = Some(semantic_ret.semantic.into_scoping());
  if !semantic_ret.errors.is_empty() {
    append_oxc_diagnostics(semantic_ret.errors, &source, filename, &mut warnings, &mut errors);
    if !errors.is_empty() {
      return EnhancedTransformResult::new_for_error(errors, warnings, tsconfig_file_paths);
    }
  }

  // Generate isolated declarations if enabled (must be done before transform modifies the AST)
  let (declaration, declaration_map) = if let Some(ref decl_options) = declaration_options
    && source_type.is_typescript()
  {
    generate_declarations(
      &allocator,
      &program,
      filename,
      &source,
      decl_options,
      &mut errors,
      &mut warnings,
    )
  } else {
    (None, None)
  };
  if !errors.is_empty() {
    return EnhancedTransformResult::new_for_error(errors, warnings, tsconfig_file_paths);
  }

  if let Some(ref define) = define_options {
    if !define.is_empty() {
      let define_pairs: Vec<(&str, &str)> =
        define.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
      match ReplaceGlobalDefinesConfig::new(&define_pairs) {
        Ok(config) => {
          let ret = ReplaceGlobalDefines::new(&allocator, config)
            .build(scoping.take().unwrap(), &mut program);
          if !ret.changed {
            scoping = Some(ret.scoping);
          }
        }
        Err(errs) => {
          errors.extend(
            errs
              .into_iter()
              .map(|err| BuildDiagnostic::invalid_define_config(err.message.to_string())),
          );
          return EnhancedTransformResult::new_for_error(errors, warnings, tsconfig_file_paths);
        }
      }
    }
  }

  let scoping = scoping
    .take()
    .unwrap_or_else(|| semantic_builder_for_transform().build(&program).semantic.into_scoping());

  let transform_ret = Transformer::new(&allocator, Path::new(filename), &oxc_transform_options)
    .build_with_scoping(scoping, &mut program);
  if !transform_ret.errors.is_empty() {
    append_oxc_diagnostics(transform_ret.errors, &source, filename, &mut warnings, &mut errors);
    if !errors.is_empty() {
      return EnhancedTransformResult::new_for_error(errors, warnings, tsconfig_file_paths);
    }
  }

  if let Some(ref inject) = inject_options
    && !inject.is_empty()
  {
    let config = build_inject_config(inject);
    let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
    let _ = InjectGlobalVariables::new(&allocator, config).build(scoping, &mut program);
  }

  let codegen_ret: CodegenReturn = Codegen::new()
    .with_options(CodegenOptions {
      source_map_path: enable_sourcemap.then(|| std::path::PathBuf::from(filename)),
      ..Default::default()
    })
    .build(&program);

  let output_map = match (input_map, codegen_ret.map) {
    (Some(im), Some(om)) => Some(collapse_sourcemaps(&[&im, &om])),
    (None, map) => map,
    (Some(_), None) => None,
  };

  EnhancedTransformResult {
    code: codegen_ret.code,
    map: output_map,
    declaration,
    declaration_map,
    #[expect(deprecated)]
    helpers_used: transform_ret.helpers_used,
    errors,
    warnings,
    tsconfig_file_paths,
  }
}