use std::{
iter::once,
mem::take,
sync::{Arc, Mutex},
};
use anyhow::Error;
use error_reporter::SwcReportHandler;
use js_sys::Uint8Array;
use miette::{GraphicalTheme, LabeledSpan, ThemeCharacters, ThemeStyles};
use serde::Serialize;
use swc_common::{
errors::{DiagnosticBuilder, DiagnosticId, Emitter, Handler, HANDLER},
sync::Lrc,
SourceMap, Span, GLOBALS,
};
use swc_error_reporters::{convert_span, to_pretty_source_code};
use swc_ts_fast_strip::{Options, TransformOutput};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{future_to_promise, js_sys::Promise};
mod error_reporter;
mod nodejs;
/// TypeScript declarations for the WebAssembly module, emitted into `wasm.d.ts`.
#[wasm_bindgen(typescript_custom_section)]
const INTERFACE_DEFINITIONS: &'static str = r#"
export declare function transform(src: string | Uint8Array, opts?: Options): Promise<TransformOutput>;
export declare function transformSync(src: string | Uint8Array, opts?: Options): TransformOutput;
export declare function transformModuleSyntax(src: string | Uint8Array): ModuleSyntaxTransformOutput;
export declare function getFirstExpression(src: string | Uint8Array, startColumn: number): string;
export declare function isValidSyntax(src: string | Uint8Array): boolean;
export declare function isRecoverableError(src: string | Uint8Array): boolean;
export declare function tokenize(src: string | Uint8Array): Token[];
export declare function findTopLevelAwaits(src: string | Uint8Array): TopLevelAwaitLocation[];
export type { Options, TransformOutput };
export type TokenKind = "keyword" | "identifier" | "number" | "bigint" | "string" | "template" | "regexp" | "comment" | "punctuator";
export interface Token {
kind: TokenKind;
/** Zero-based UTF-16 offset, inclusive. */
start: number;
/** Zero-based UTF-16 offset, exclusive. */
end: number;
}
export interface TopLevelAwaitLocation {
/** One-based line number. */
line: number;
/** Zero-based UTF-16 column. */
column: number;
}
export interface ModuleSyntaxTransformOutput {
code: string;
hadModuleSyntax: boolean;
}
"#;
#[wasm_bindgen(skip_typescript)]
pub fn transform(input: JsValue, options: JsValue) -> Promise {
future_to_promise(async move { transform_sync(input, options) })
}
#[wasm_bindgen(js_name = "transformSync", skip_typescript)]
pub fn transform_sync(input: JsValue, options: JsValue) -> Result<JsValue, JsValue> {
let options: Options = if options.is_falsy() {
Default::default()
} else {
serde_wasm_bindgen::from_value(options)?
};
let input = coerce_input(input)?;
let result = GLOBALS.set(&Default::default(), || operate(input, options));
match result {
Ok(v) => Ok(serde_wasm_bindgen::to_value(&v)?),
Err(errors) => Err(serde_wasm_bindgen::to_value(&errors[0])?),
}
}
#[wasm_bindgen(js_name = "transformModuleSyntax", skip_typescript)]
pub fn transform_module_syntax(input: JsValue) -> Result<JsValue, JsValue> {
let input = coerce_input(input)?;
let output = nodejs::transform_module_syntax(input);
Ok(serde_wasm_bindgen::to_value(&output)?)
}
#[wasm_bindgen(js_name = "getFirstExpression", skip_typescript)]
pub fn get_first_expression(input: JsValue, start_column: u32) -> Result<String, JsValue> {
let input = coerce_input(input)?;
Ok(nodejs::get_first_expression(input, start_column))
}
#[wasm_bindgen(js_name = "isValidSyntax", skip_typescript)]
pub fn is_valid_syntax(input: JsValue) -> Result<bool, JsValue> {
let input = coerce_input(input)?;
Ok(nodejs::is_valid_syntax(input))
}
#[wasm_bindgen(js_name = "isRecoverableError", skip_typescript)]
pub fn is_recoverable_error(input: JsValue) -> Result<bool, JsValue> {
let input = coerce_input(input)?;
Ok(nodejs::is_recoverable_error(input))
}
/// Returns compact tokens for syntax highlighting, including a partial prefix
/// when the input is incomplete or invalid.
#[wasm_bindgen(skip_typescript)]
pub fn tokenize(input: JsValue) -> Result<JsValue, JsValue> {
let input = coerce_input(input)?;
Ok(serde_wasm_bindgen::to_value(&nodejs::tokenize(&input))?)
}
/// Returns module-scope await locations in source order using Acorn
/// coordinates.
#[wasm_bindgen(js_name = "findTopLevelAwaits", skip_typescript)]
pub fn find_top_level_awaits(input: JsValue) -> Result<JsValue, JsValue> {
let input = coerce_input(input)?;
Ok(serde_wasm_bindgen::to_value(
&nodejs::find_top_level_awaits(&input),
)?)
}
fn coerce_input(input: JsValue) -> Result<String, JsValue> {
match input.as_string() {
Some(input) => Ok(input),
None => {
if input.is_instance_of::<Uint8Array>() {
let input = input.unchecked_into::<Uint8Array>();
String::from_utf8(input.to_vec())
.map_err(|_| JsValue::from_str("Input Uint8Array is not valid utf-8"))
} else {
Err(JsValue::from_str("Input is not a string or Uint8Array"))
}
}
}
}
fn operate(input: String, options: Options) -> Result<TransformOutput, Vec<JsonDiagnostic>> {
let cm = Lrc::new(SourceMap::default());
try_with_json_handler(cm.clone(), |handler| {
swc_ts_fast_strip::operate(&cm, handler, input, options).map_err(anyhow::Error::new)
})
}
#[derive(Clone)]
struct JsonErrorWriter {
errors: Arc<Mutex<Vec<JsonDiagnostic>>>,
/// Successful transforms never need the graphical diagnostic reporter.
reporter: Option<SwcReportHandler>,
cm: Lrc<SourceMap>,
}
fn try_with_json_handler<F, Ret>(cm: Lrc<SourceMap>, op: F) -> Result<Ret, Vec<JsonDiagnostic>>
where
F: FnOnce(&Handler) -> Result<Ret, Error>,
{
let wr = JsonErrorWriter {
errors: Default::default(),
reporter: None,
cm,
};
let emitter: Box<dyn Emitter> = Box::new(wr.clone());
let handler = Handler::with_emitter(true, false, emitter);
let ret = HANDLER.set(&handler, || op(&handler));
if handler.has_errors() {
let mut lock = wr.errors.lock().unwrap();
let error = take(&mut *lock);
Err(error)
} else {
Ok(ret.expect("it should not fail without emitting errors to handler"))
}
}
impl Emitter for JsonErrorWriter {
fn emit(&mut self, db: &mut DiagnosticBuilder) {
let d = &**db;
let snippet = d.span.primary_span().and_then(|span| {
let mut snippet = String::new();
let reporter = self.reporter.get_or_insert_with(json_reporter);
match reporter.render_report(
&mut snippet,
&Snippet {
source_code: &to_pretty_source_code(&self.cm, true),
span,
},
) {
Ok(()) => Some(snippet),
Err(_) => None,
}
});
let children = d
.children
.iter()
.map(|d| todo!("json subdiagnostic: {d:?}"))
.collect::<Vec<_>>();
let error_code = match &d.code {
Some(DiagnosticId::Error(s)) => Some(&**s),
Some(DiagnosticId::Lint(s)) => Some(&**s),
None => None,
};
let start = d
.span
.primary_span()
.and_then(|span| self.cm.try_lookup_char_pos(span.lo()).ok());
let end = d
.span
.primary_span()
.and_then(|span| self.cm.try_lookup_char_pos(span.hi()).ok());
let filename = start.as_ref().map(|loc| loc.file.name.to_string());
let error = JsonDiagnostic {
code: error_code.map(|s| s.to_string()),
message: d.message[0].0.to_string(),
snippet,
filename,
start_line: start.as_ref().map(|loc| loc.line),
start_column: start.as_ref().map(|loc| loc.col_display),
end_line: end.as_ref().map(|loc| loc.line),
end_column: end.as_ref().map(|loc| loc.col_display),
children,
};
self.errors.lock().unwrap().push(error);
}
fn take_diagnostics(&mut self) -> Vec<String> {
Default::default()
}
}
fn json_reporter() -> SwcReportHandler {
SwcReportHandler::default().with_theme(GraphicalTheme {
characters: ThemeCharacters {
hbar: ' ',
vbar: ' ',
xbar: ' ',
vbar_break: ' ',
ltop: ' ',
rtop: ' ',
mtop: ' ',
lbot: ' ',
rbot: ' ',
mbot: ' ',
error: "".into(),
warning: "".into(),
advice: "".into(),
..ThemeCharacters::ascii()
},
styles: ThemeStyles::none(),
})
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonDiagnostic {
/// Error code
#[serde(skip_serializing_if = "Option::is_none")]
code: Option<String>,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
snippet: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
start_line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
start_column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
end_line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
end_column: Option<usize>,
#[serde(skip_serializing_if = "Vec::is_empty")]
children: Vec<JsonSubdiagnostic>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonSubdiagnostic {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
snippet: Option<String>,
filename: String,
line: usize,
}
struct Snippet<'a> {
source_code: &'a dyn miette::SourceCode,
span: Span,
}
impl miette::Diagnostic for Snippet<'_> {
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
if self.span.lo().is_dummy() || self.span.hi().is_dummy() {
return None;
}
Some(self.source_code)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
Some(Box::new(once(LabeledSpan::new_with_span(
None,
convert_span(self.span),
))))
}
}
impl std::error::Error for Snippet<'_> {}
impl std::fmt::Display for Snippet<'_> {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
impl std::fmt::Debug for Snippet<'_> {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}