surplus_cli/
wrapper.rs

1//! High level CLI-as-a-library for the Surplus compiler.
2//!
3//! This is directly called to by the `surplus` CLI binary.
4//! It's not intended to be used directly by end users; it's a thin
5//! wrapper primarily for use by the WASM wrapper crate.
6use std::sync::Arc;
7
8#[cfg(feature = "clap")]
9use clap::Parser;
10use oxc::{
11	allocator::Allocator,
12	codegen::{Codegen, CodegenOptions},
13	diagnostics::Severity,
14	mangler::{MangleOptions, MangleOptionsKeepNames},
15	semantic::SemanticBuilder,
16	span::SourceType,
17};
18
19/// The Surplus JSX compiler.
20#[cfg_attr(feature = "clap", derive(Debug, Parser))]
21#[expect(clippy::struct_excessive_bools)]
22pub struct Args {
23	/// Where to output the bundle. Defaults to
24	/// stdout; intermediate folders must exist
25	#[cfg_attr(feature = "clap", arg(short = 'o', long = "output"))]
26	#[cfg(feature = "clap")]
27	pub output: Option<String>,
28	/// The name of the `S.js` package to import from
29	/// if transformation occurs
30	#[cfg_attr(
31		feature = "clap",
32		arg(short = 'S', long = "import", default_value = "@surplus/s")
33	)]
34	pub import_sjs: String,
35	/// Treat warnings as errors
36	#[cfg_attr(feature = "clap", arg(short = 'W'))]
37	pub warnings_as_errors: bool,
38	/// Don't minify the output
39	#[cfg_attr(feature = "clap", arg(short = 'M', long = "no-minify"))]
40	pub no_minify: bool,
41	/// When set, enables sourcemaps (embedded in the output).
42	#[cfg_attr(feature = "clap", arg(short = 'm', long = "map"))]
43	pub generate_sourcemaps: bool,
44	/// The entry point of the Surplus bundle
45	/// (defaults to stdin)
46	pub entry_point: Option<String>,
47	/// Allow typescript syntax in the input
48	#[cfg_attr(feature = "clap", arg(short = 'T', long = "typescript"))]
49	pub typescript: bool,
50}
51
52/// The `Ok` result type for the [`run`] function.
53pub struct Compilation {
54	/// The generated code.
55	pub code: String,
56	/// Any warnings
57	pub warnings: Vec<String>,
58	/// Any errors; if non-empty, `code` will be empty.
59	pub errors: Vec<String>,
60}
61
62/// Runs the Surplus compiler with the given arguments.
63///
64/// This is identical to running the surplus CLI (except for parsing the arguments).
65///
66/// `Err` results indicate fatal errors that prevent compilation from completing.
67/// This does **not** include warnings or syntax errors, which are included in the [`Compilation`]
68/// result.
69pub fn run(source: String, args: &Args) -> Result<Compilation, Box<dyn std::error::Error>> {
70	let mut result = Compilation {
71		code: String::new(),
72		warnings: Vec::new(),
73		errors: Vec::new(),
74	};
75
76	let source = Arc::new(source);
77	let mut errors = 0;
78
79	let allocator = Allocator::default();
80	let parse_result = oxc::parser::Parser::new(
81		&allocator,
82		&source,
83		if args.typescript {
84			SourceType::tsx()
85		} else {
86			SourceType::jsx()
87		},
88	)
89	.parse();
90
91	if parse_result.panicked || !parse_result.errors.is_empty() {
92		if parse_result.errors.is_empty() {
93			return Err("parser panicked, but no errors were reported".into());
94		}
95
96		for mut error in parse_result.errors {
97			if args.warnings_as_errors {
98				error = error.with_severity(Severity::Error);
99			}
100
101			if error.severity == Severity::Error {
102				errors += 1;
103			}
104
105			result
106				.errors
107				.push(format!("{:?}", error.with_source_code(Arc::clone(&source))));
108		}
109
110		if errors > 0 {
111			return Ok(result);
112		}
113	}
114
115	let mut program = parse_result.program;
116
117	let semantic = SemanticBuilder::new()
118		.with_check_syntax_error(true)
119		.build(&program);
120
121	if !semantic.errors.is_empty() {
122		errors = 0;
123		for mut error in semantic.errors {
124			if args.warnings_as_errors {
125				error = error.with_severity(Severity::Error);
126			}
127
128			if error.severity == Severity::Error {
129				errors += 1;
130			}
131
132			result
133				.errors
134				.push(format!("{:?}", error.with_source_code(Arc::clone(&source))));
135		}
136
137		if errors > 0 {
138			return Ok(result);
139		}
140	}
141
142	let scoping = semantic.semantic.into_scoping();
143	let surplus_result = surplus::transform(&allocator, &mut program, scoping, &args.import_sjs);
144
145	if !surplus_result.errors.is_empty() {
146		errors = 0;
147		for mut error in surplus_result.errors {
148			if args.warnings_as_errors {
149				error = error.with_severity(Severity::Error);
150			}
151
152			if error.severity == Severity::Error {
153				errors += 1;
154			}
155
156			result
157				.errors
158				.push(format!("{:?}", error.with_source_code(Arc::clone(&source))));
159		}
160
161		if errors > 0 {
162			return Ok(result);
163		}
164	}
165
166	let codegen_options = CodegenOptions {
167		minify: !args.no_minify,
168		comments: args.no_minify,
169		source_map_path: if args.generate_sourcemaps {
170			if let Some(ref entry) = args.entry_point {
171				Some(entry.into())
172			} else {
173				Some("surplus.js.map".into())
174			}
175		} else {
176			None
177		},
178		..CodegenOptions::default()
179	};
180
181	let scoping = if args.no_minify {
182		surplus_result.scoping
183	} else {
184		let semantic = SemanticBuilder::new()
185			.with_check_syntax_error(false)
186			.with_scope_tree_child_ids(true)
187			.build(&program);
188
189		debug_assert!(semantic.errors.is_empty());
190
191		let options = MangleOptions {
192			keep_names: MangleOptionsKeepNames::all_false(),
193			top_level: true,
194			..MangleOptions::default()
195		};
196
197		oxc::mangler::Mangler::new()
198			.with_options(options)
199			.build_with_semantic(semantic.semantic, &program)
200	};
201
202	let generated = Codegen::new()
203		.with_options(codegen_options)
204		.with_scoping(Some(scoping))
205		.build(&program);
206
207	let sourcemap_string = if args.generate_sourcemaps {
208		if let Some(ref sourcemap) = generated.map {
209			Some(sourcemap.to_data_url())
210		} else {
211			result
212				.warnings
213				.push("sourcemap generation requested, but no sourcemap was generated".into());
214			None
215		}
216	} else {
217		None
218	};
219
220	result.code = generated.code;
221	if let Some(ref sm) = sourcemap_string {
222		result.code.push_str("\n//# sourceMappingURL=");
223		result.code.push_str(sm);
224	}
225
226	Ok(result)
227}