Skip to main content

rolldown_ecmascript/
ecma_compiler.rs

1use std::path::PathBuf;
2
3use arcstr::ArcStr;
4use oxc::{
5  allocator::Allocator,
6  ast::{
7    ast::{Program, Statement},
8    builder::AstBuilder,
9  },
10  codegen::{Codegen, CodegenOptions, CodegenReturn, CommentOptions, LegalComment},
11  minifier::{ManglePropertyCache, Minifier, MinifierOptions},
12  parser::{ParseOptions, Parser},
13  span::{SPAN, SourceType},
14};
15use oxc_sourcemap::SourceMap;
16use rolldown_error::{BuildDiagnostic, BuildResult, EventKind, Severity};
17
18use crate::ecma_ast::{
19  EcmaAst,
20  program_cell::{ProgramCell, ProgramCellDependent, ProgramCellOwner},
21};
22pub struct EcmaCompiler;
23
24impl EcmaCompiler {
25  pub fn parse(id: &str, source: impl Into<ArcStr>, ty: SourceType) -> BuildResult<EcmaAst> {
26    let source: ArcStr = source.into();
27    let allocator = oxc::allocator::Allocator::default();
28    let inner =
29      ProgramCell::try_new(ProgramCellOwner { source: source.clone(), allocator }, |owner| {
30        let parser = Parser::new(&owner.allocator, &owner.source, ty).with_options(ParseOptions {
31          allow_return_outside_function: true,
32          preserve_parens: false,
33          ..ParseOptions::default()
34        });
35        let ret = parser.parse();
36        if ret.fatal_error || !ret.diagnostics.is_empty() {
37          Err(BuildDiagnostic::from_oxc_diagnostics(
38            ret.diagnostics,
39            &source.clone(),
40            id,
41            Severity::Error,
42            EventKind::ParseError,
43          ))
44        } else {
45          Ok(ProgramCellDependent { program: ret.program })
46        }
47      })?;
48    Ok(EcmaAst { program: inner, source_type: ty })
49  }
50
51  pub fn parse_expr_as_program(
52    id: &str,
53    source: impl Into<ArcStr>,
54    ty: SourceType,
55  ) -> BuildResult<EcmaAst> {
56    let source: ArcStr = source.into();
57    let allocator = oxc::allocator::Allocator::default();
58    let inner =
59      ProgramCell::try_new(ProgramCellOwner { source: source.clone(), allocator }, |owner| {
60        let builder = AstBuilder::new(&owner.allocator);
61        let parser = Parser::new(&owner.allocator, &owner.source, ty)
62          .with_options(ParseOptions { preserve_parens: false, ..ParseOptions::default() });
63        let ret = parser.parse_expression();
64        match ret {
65          Ok(expr) => {
66            let program = Program::new(
67              SPAN,
68              SourceType::default().with_module(true),
69              owner.source.as_str(),
70              [],
71              None,
72              [],
73              [Statement::new_expression_statement(SPAN, expr, &builder)],
74              &builder,
75            );
76            Ok(ProgramCellDependent { program })
77          }
78          Err(errors) => Err(BuildDiagnostic::from_oxc_diagnostics(
79            errors,
80            &source.clone(),
81            id,
82            Severity::Error,
83            EventKind::ParseError,
84          )),
85        }
86      })?;
87    Ok(EcmaAst { program: inner, source_type: ty })
88  }
89
90  pub fn print_with(ast: &EcmaAst, options: PrintOptions) -> CodegenReturn<'_> {
91    let legal = if options.comments.legal { LegalComment::Inline } else { LegalComment::None };
92    Codegen::new()
93      .with_options(CodegenOptions {
94        comments: CommentOptions {
95          normal: false,
96          legal,
97          jsdoc: options.comments.jsdoc,
98          annotation: options.comments.annotation,
99        },
100        initial_indent: options.initial_indent,
101        ascii_only: options.ascii_only,
102        source_map_path: options.sourcemap.then(|| PathBuf::from(options.filename)),
103        ..CodegenOptions::default()
104      })
105      .build(ast.program())
106  }
107
108  /// The returned map borrows `source_text` (its `sourcesContent` and token names are slices
109  /// of it) — callers that keep it beyond the borrow must `into_owned` it; callers that only
110  /// feed it to `collapse_sourcemaps` can use it as-is and skip that copy.
111  /// The property-mangle cache is present when property mangling ran.
112  #[expect(clippy::too_many_arguments)]
113  pub fn dce_or_minify<'a>(
114    allocator: &'a Allocator,
115    source_text: &'a str,
116    source_type: SourceType,
117    enable_sourcemap: bool,
118    filename: &str,
119    compress: bool,
120    minify_options: MinifierOptions,
121    codegen_options: CodegenOptions,
122  ) -> BuildResult<(String, Option<SourceMap<'a>>, Option<ManglePropertyCache>)> {
123    let ret = Parser::new(allocator, source_text, source_type)
124      .with_options(ParseOptions { preserve_parens: false, ..ParseOptions::default() })
125      .parse();
126    if ret.fatal_error || !ret.diagnostics.is_empty() {
127      return Err(
128        BuildDiagnostic::from_oxc_diagnostics(
129          ret.diagnostics,
130          &ArcStr::from(source_text),
131          filename,
132          Severity::Error,
133          EventKind::ParseError,
134        )
135        .into(),
136      );
137    }
138    let mut program = ret.program;
139    let minifier = Minifier::new(minify_options);
140    let ret = if compress {
141      minifier.minify(allocator, &mut program)
142    } else {
143      minifier.dce(allocator, &mut program)
144    };
145    let property_mangle_cache = ret.property_mangle_cache;
146    let ret = Codegen::new()
147      .with_options(CodegenOptions {
148        source_map_path: enable_sourcemap.then(|| PathBuf::from(filename)),
149        ..codegen_options
150      })
151      .with_scoping(ret.scoping)
152      .with_private_member_mappings(ret.class_private_mappings)
153      .build(&program);
154    Ok((ret.code, ret.map, property_mangle_cache))
155  }
156}
157
158#[test]
159fn basic_test() {
160  let ast = EcmaCompiler::parse("", "const a = 1;".to_string(), SourceType::default()).unwrap();
161  let code = EcmaCompiler::print_with(&ast, PrintOptions::default()).code;
162  assert_eq!(code, "const a = 1;\n");
163}
164#[derive(Debug, Default)]
165
166pub struct PrintOptions {
167  pub comments: PrintCommentsOptions,
168  pub filename: String,
169  pub sourcemap: bool,
170  pub initial_indent: u32,
171  pub ascii_only: bool,
172}
173
174#[derive(Debug, Default, Clone, Copy)]
175pub struct PrintCommentsOptions {
176  pub legal: bool,
177  pub annotation: bool,
178  pub jsdoc: bool,
179}