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