mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-22 00:02:38 +00:00
572 lines
20 KiB
Rust
572 lines
20 KiB
Rust
/*
|
|
* Author: Ruben Fiszel
|
|
* Copyright: Windmill Labs, Inc 2022
|
|
* This file and its contents are licensed under the AGPLv3 License.
|
|
* Please see the included NOTICE for copyright information and
|
|
* LICENSE-AGPL for a copy of the license.
|
|
*/
|
|
// use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
|
|
use serde_json::Value;
|
|
use std::collections::HashSet;
|
|
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
|
|
use windmill_parser::{
|
|
json_to_typ, to_snake_case, Arg, MainArgSignature, ObjectProperty, OneOfVariant, Typ,
|
|
};
|
|
|
|
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned};
|
|
use swc_ecma_ast::{
|
|
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident,
|
|
IdentName, Lit, MemberExpr, MemberProp, ModuleDecl, ModuleItem, Number, ObjectLit, ObjectPat,
|
|
Param, Pat, Str, TsArrayType, TsEntityName, TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType,
|
|
TsOptionalType, TsParenthesizedType, TsPropertySignature, TsType, TsTypeAnn, TsTypeElement,
|
|
TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
|
|
};
|
|
use swc_ecma_parser::{lexer::Lexer, EsSyntax, Parser, StringInput, Syntax, TsSyntax};
|
|
|
|
use regex::Regex;
|
|
#[cfg(target_arch = "wasm32")]
|
|
use wasm_bindgen::prelude::*;
|
|
|
|
struct ImportsFinder {
|
|
imports: HashSet<String>,
|
|
}
|
|
|
|
impl Visit for ImportsFinder {
|
|
noop_visit_type!();
|
|
|
|
fn visit_import_decl(&mut self, n: &swc_ecma_ast::ImportDecl) {
|
|
if let Some(ref s) = n.src.raw {
|
|
let s = s.to_string();
|
|
if s.starts_with("'") && s.ends_with("'") {
|
|
self.imports.insert(s[1..s.len() - 1].to_string());
|
|
} else if s.starts_with("\"") && s.ends_with("\"") {
|
|
self.imports.insert(s[1..s.len() - 1].to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
|
|
let cm: Lrc<SourceMap> = Default::default();
|
|
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.into());
|
|
let lexer = Lexer::new(
|
|
Syntax::Typescript(TsSyntax::default()),
|
|
// EsVersion defaults to es5
|
|
Default::default(),
|
|
StringInput::from(&*fm),
|
|
None,
|
|
);
|
|
|
|
let mut parser = Parser::new_from(lexer);
|
|
|
|
let mut err_s = "".to_string();
|
|
for e in parser.take_errors() {
|
|
err_s += &e.into_kind().msg().to_string();
|
|
}
|
|
|
|
let expr = parser.parse_module().map_err(|e| {
|
|
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
|
|
})?;
|
|
|
|
let mut visitor = ImportsFinder { imports: HashSet::new() };
|
|
visitor.visit_module(&expr);
|
|
|
|
let mut imports: Vec<_> = visitor.imports.into_iter().collect();
|
|
imports.sort();
|
|
Ok(imports)
|
|
}
|
|
|
|
struct OutputFinder {
|
|
idents: HashSet<(String, String)>,
|
|
}
|
|
|
|
impl Visit for OutputFinder {
|
|
noop_visit_type!();
|
|
|
|
fn visit_member_expr(&mut self, m: &MemberExpr) {
|
|
m.obj.visit_with(self);
|
|
if let MemberProp::Computed(c) = &m.prop {
|
|
c.visit_with(self);
|
|
}
|
|
match m {
|
|
MemberExpr { obj, prop: MemberProp::Ident(IdentName { sym, .. }), .. } => {
|
|
match *obj.to_owned() {
|
|
Expr::Ident(Ident { sym: sym_i, .. }) => {
|
|
self.idents.insert((sym_i.to_string(), sym.to_string()));
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn parse_expr_for_ids(code: &str) -> anyhow::Result<Vec<(String, String)>> {
|
|
let cm: Lrc<SourceMap> = Default::default();
|
|
let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into());
|
|
let lexer = Lexer::new(
|
|
// We want to parse ecmascript
|
|
Syntax::Es(EsSyntax { jsx: false, ..Default::default() }),
|
|
// EsVersion defaults to es5
|
|
Default::default(),
|
|
StringInput::from(&*fm),
|
|
None,
|
|
);
|
|
|
|
let mut parser = Parser::new_from(lexer);
|
|
|
|
let mut err_s = "".to_string();
|
|
for e in parser.take_errors() {
|
|
err_s += &e.into_kind().msg().to_string();
|
|
}
|
|
|
|
let expr = parser.parse_module().map_err(|e| {
|
|
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
|
|
})?;
|
|
|
|
let mut visitor = OutputFinder { idents: HashSet::new() };
|
|
visitor.visit_module(&expr);
|
|
|
|
Ok(visitor.idents.into_iter().collect())
|
|
}
|
|
|
|
pub fn parse_deno_signature(
|
|
code: &str,
|
|
skip_dflt: bool,
|
|
main_override: Option<String>,
|
|
) -> anyhow::Result<MainArgSignature> {
|
|
let cm: Lrc<SourceMap> = Default::default();
|
|
let fm = cm.new_source_file(FileName::Custom("main.ts".into()).into(), code.into());
|
|
let lexer = Lexer::new(
|
|
// We want to parse ecmascript
|
|
Syntax::Typescript(TsSyntax::default()),
|
|
// EsVersion defaults to es5
|
|
Default::default(),
|
|
StringInput::from(&*fm),
|
|
None,
|
|
);
|
|
|
|
let mut parser = Parser::new_from(lexer);
|
|
|
|
let mut err_s = "".to_string();
|
|
for e in parser.take_errors() {
|
|
err_s += &e.into_kind().msg().to_string();
|
|
}
|
|
|
|
let ast = parser
|
|
.parse_module()
|
|
.map_err(|e| {
|
|
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
|
|
})?
|
|
.body;
|
|
|
|
let has_preprocessor = ast.iter().any(|x| match x {
|
|
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
|
|
decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, .. }),
|
|
..
|
|
})) => &sym.to_string() == "preprocessor",
|
|
_ => false,
|
|
});
|
|
|
|
let main_name = main_override.unwrap_or("main".to_string());
|
|
let params = ast.into_iter().find_map(|x| match x {
|
|
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
|
|
decl: Decl::Fn(FnDecl { ident: Ident { sym, .. }, function, .. }),
|
|
..
|
|
})) if &sym.to_string() == &main_name => Some(function.params),
|
|
_ => None,
|
|
});
|
|
|
|
let mut c: u16 = 0;
|
|
if let Some(params) = params {
|
|
let r = MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: params
|
|
.into_iter()
|
|
.map(|x| parse_param(x, &cm, skip_dflt, &mut c))
|
|
.collect::<anyhow::Result<Vec<Arg>>>()?,
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(has_preprocessor),
|
|
};
|
|
Ok(r)
|
|
} else {
|
|
Ok(MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![],
|
|
no_main_func: Some(true),
|
|
has_preprocessor: Some(has_preprocessor),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse_param(
|
|
x: Param,
|
|
cm: &Lrc<SourceMap>,
|
|
skip_dflt: bool,
|
|
counter: &mut u16,
|
|
) -> anyhow::Result<Arg> {
|
|
let r = match x.pat {
|
|
Pat::Ident(ident) => {
|
|
let (name, typ, nullable) = binding_ident_to_arg(&ident);
|
|
Ok(Arg {
|
|
otyp: None,
|
|
name,
|
|
typ,
|
|
default: None,
|
|
has_default: ident.id.optional || nullable,
|
|
oidx: None,
|
|
})
|
|
}
|
|
// Pat::Object(ObjectPat { ... }) = todo!()
|
|
Pat::Assign(AssignPat { left, right, .. }) => {
|
|
let (name, mut typ, _nullable) = match *left {
|
|
Pat::Ident(ident) => binding_ident_to_arg(&ident),
|
|
Pat::Object(ObjectPat { type_ann, .. }) => {
|
|
let (typ, nullable) = eval_type_ann(&type_ann);
|
|
*counter += 1;
|
|
let name = format!("anon{}", counter);
|
|
(name, typ, nullable)
|
|
}
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"parameter syntax unsupported: `{}`: {:#?}",
|
|
cm.span_to_snippet(left.span())
|
|
.unwrap_or_else(|_| cm.span_to_string(left.span())),
|
|
*left
|
|
))
|
|
}
|
|
};
|
|
|
|
let dflt = if skip_dflt {
|
|
None
|
|
} else {
|
|
match *right {
|
|
Expr::Lit(Lit::Str(Str { value, .. })) => {
|
|
Some(Value::String(value.to_string()))
|
|
}
|
|
Expr::Lit(Lit::Num(Number { value, .. }))
|
|
if (value == (value as u64) as f64) =>
|
|
{
|
|
Some(serde_json::json!(value as u64))
|
|
}
|
|
Expr::Lit(Lit::Num(Number { value, .. })) => Some(serde_json::json!(value)),
|
|
Expr::Lit(Lit::BigInt(BigInt { value, .. })) => Some(serde_json::json!(value)),
|
|
Expr::Lit(Lit::Bool(Bool { value, .. })) => Some(Value::Bool(value)),
|
|
Expr::Object(ObjectLit { span, .. }) => eval_span(span, cm),
|
|
Expr::Array(ArrayLit { span, .. }) => eval_span(span, cm),
|
|
_ => None,
|
|
}
|
|
};
|
|
|
|
if typ == Typ::Unknown && dflt.is_some() {
|
|
typ = json_to_typ(dflt.as_ref().unwrap());
|
|
}
|
|
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
|
|
}
|
|
Pat::Object(ObjectPat { type_ann, .. }) => {
|
|
let (typ, nullable) = eval_type_ann(&type_ann);
|
|
*counter += 1;
|
|
let name = format!("anon{}", counter);
|
|
Ok(Arg { otyp: None, name, typ, default: None, has_default: nullable, oidx: None })
|
|
}
|
|
_ => Err(anyhow::anyhow!(
|
|
"parameter syntax unsupported: `{}`: {:#?}",
|
|
cm.span_to_snippet(x.span())
|
|
.unwrap_or_else(|_| cm.span_to_string(x.span())),
|
|
x.pat
|
|
)),
|
|
};
|
|
r
|
|
}
|
|
|
|
fn eval_span(span: Span, cm: &Lrc<SourceMap>) -> Option<Value> {
|
|
let expr = cm
|
|
.span_to_snippet(span)
|
|
.ok()
|
|
.map(|x| serde_json::from_str(&x).map_err(|_| x));
|
|
|
|
match expr {
|
|
Some(Ok(x)) => Some(x),
|
|
Some(Err(x)) => eval_sync(&x).ok(),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
fn eval_type_ann(type_ann: &Option<Box<TsTypeAnn>>) -> (Typ, bool) {
|
|
return type_ann
|
|
.as_ref()
|
|
.map(|x| tstype_to_typ(&*x.type_ann))
|
|
.unwrap_or((Typ::Unknown, false));
|
|
}
|
|
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
|
|
let (typ, nullable) = eval_type_ann(type_ann);
|
|
(id.sym.to_string(), typ, nullable)
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref IMPORTS_VERSION: Regex = Regex::new(r"^((?:\@[^\/\@]+\/[^\/\@]+)|(?:[^\/\@]+))(?:\@(?:[^\/]+))?(.*)$").unwrap();
|
|
|
|
}
|
|
|
|
pub fn remove_pinned_imports(code: &str) -> anyhow::Result<String> {
|
|
let mut imports = parse_expr_for_imports(code)?;
|
|
imports.sort_by_key(|f| 0 - (f.len() as i32));
|
|
let mut content = code.to_string();
|
|
for import in imports {
|
|
let to_c = IMPORTS_VERSION.captures(&import);
|
|
if let Some(to) = to_c.and_then(|x| {
|
|
x.get(1).map(|y| {
|
|
format!(
|
|
"{}{}",
|
|
y.as_str(),
|
|
x.get(2).map(|z| z.as_str()).unwrap_or("")
|
|
)
|
|
})
|
|
}) {
|
|
content = content.replace(&import, &to);
|
|
}
|
|
}
|
|
Ok(content)
|
|
}
|
|
|
|
fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
|
match ts_type {
|
|
TsType::TsKeywordType(t) => (
|
|
match t.kind {
|
|
TsKeywordTypeKind::TsObjectKeyword => Typ::Object(vec![]),
|
|
TsKeywordTypeKind::TsBooleanKeyword => Typ::Bool,
|
|
TsKeywordTypeKind::TsBigIntKeyword => Typ::Int,
|
|
TsKeywordTypeKind::TsNumberKeyword => Typ::Float,
|
|
TsKeywordTypeKind::TsStringKeyword => Typ::Str(None),
|
|
_ => Typ::Unknown,
|
|
},
|
|
false,
|
|
),
|
|
TsType::TsTypeLit(TsTypeLit { members, .. }) => {
|
|
let properties = members
|
|
.into_iter()
|
|
.filter_map(|x| match x {
|
|
TsTypeElement::TsPropertySignature(TsPropertySignature {
|
|
key,
|
|
type_ann,
|
|
..
|
|
}) => match (*key.to_owned(), type_ann) {
|
|
(Expr::Ident(Ident { sym, .. }), type_ann) => Some(ObjectProperty {
|
|
key: sym.to_string(),
|
|
typ: type_ann
|
|
.as_ref()
|
|
.map(|typ| Box::new(tstype_to_typ(&*typ.type_ann).0))
|
|
.unwrap_or(Box::new(Typ::Unknown)),
|
|
}),
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
(Typ::Object(properties), false)
|
|
}
|
|
TsType::TsParenthesizedType(TsParenthesizedType { type_ann, .. }) => {
|
|
tstype_to_typ(type_ann)
|
|
}
|
|
// TODO: we can do better here and extract the inner type of array
|
|
TsType::TsArrayType(TsArrayType { elem_type, .. }) => {
|
|
(Typ::List(Box::new(tstype_to_typ(&**elem_type).0)), false)
|
|
}
|
|
TsType::TsLitType(TsLitType { lit: TsLit::Str(Str { value, .. }), .. }) => {
|
|
(Typ::Str(Some(vec![value.to_string()])), false)
|
|
}
|
|
TsType::TsOptionalType(TsOptionalType { type_ann, .. }) => {
|
|
(tstype_to_typ(type_ann).0, true)
|
|
}
|
|
TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(
|
|
TsUnionType { types, .. },
|
|
)) => {
|
|
let (is_undefined_option, undefined_position) = if types.len() == 2 {
|
|
(true, find_undefined(types))
|
|
} else if types.into_iter().all(|x| {
|
|
x.as_ts_lit_type().is_some_and(|y| y.lit.as_str().is_some())
|
|
|| x.as_ts_keyword_type().is_some_and(|y| {
|
|
y.kind == TsKeywordTypeKind::TsUndefinedKeyword
|
|
|| y.kind == TsKeywordTypeKind::TsStringKeyword
|
|
|| y.kind == TsKeywordTypeKind::TsNullKeyword
|
|
})
|
|
}) {
|
|
(false, find_undefined(types))
|
|
} else {
|
|
(false, None)
|
|
};
|
|
|
|
if is_undefined_option && undefined_position.is_some() {
|
|
let other_p = if undefined_position.unwrap() == 0 {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
(tstype_to_typ(&types[other_p]).0, true)
|
|
} else {
|
|
if types.len() > 1 {
|
|
let one_of_values: Vec<OneOfVariant> =
|
|
types.into_iter().map_while(parse_one_of_type).collect();
|
|
|
|
if one_of_values.len() == types.len() {
|
|
return (Typ::OneOf(one_of_values), false);
|
|
}
|
|
}
|
|
|
|
let literals = types
|
|
.into_iter()
|
|
.filter(|x| match ***x {
|
|
TsType::TsKeywordType(TsKeywordType { kind, .. }) => {
|
|
kind != TsKeywordTypeKind::TsStringKeyword
|
|
&& kind != TsKeywordTypeKind::TsUndefinedKeyword
|
|
&& kind != TsKeywordTypeKind::TsNullKeyword
|
|
}
|
|
_ => true,
|
|
})
|
|
.map(|x| match &**x {
|
|
TsType::TsLitType(TsLitType {
|
|
lit: TsLit::Str(Str { value, .. }), ..
|
|
}) => Some(value.to_string()),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if literals.iter().find(|x| x.is_none()).is_some() {
|
|
(Typ::Unknown, false)
|
|
} else {
|
|
(
|
|
Typ::Str(Some(literals.into_iter().filter_map(|x| x).collect())),
|
|
undefined_position.is_some(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
TsType::TsTypeRef(TsTypeRef { type_name, type_params, .. }) => {
|
|
let sym = match type_name {
|
|
TsEntityName::Ident(Ident { sym, .. }) => sym,
|
|
TsEntityName::TsQualifiedName(p) => &*p.right.sym,
|
|
};
|
|
match sym.to_string().as_str() {
|
|
"Resource" => (
|
|
Typ::Resource(
|
|
type_params
|
|
.as_ref()
|
|
.and_then(|x| {
|
|
x.params.get(0).and_then(|y| {
|
|
y.as_ts_lit_type().and_then(|z| {
|
|
z.lit.as_str().map(|a| a.to_owned().value.to_string())
|
|
})
|
|
})
|
|
})
|
|
.unwrap_or_else(|| "unknown".to_string()),
|
|
),
|
|
false,
|
|
),
|
|
"Date" => (Typ::Datetime, false),
|
|
"Base64" => (Typ::Bytes, false),
|
|
"Email" => (Typ::Email, false),
|
|
"Sql" => (Typ::Sql, false),
|
|
x @ _ if x.starts_with("DynSelect_") => (
|
|
Typ::DynSelect(x.strip_prefix("DynSelect_").unwrap().to_string()),
|
|
false,
|
|
),
|
|
x @ _ => (Typ::Resource(to_snake_case(x)), false),
|
|
}
|
|
}
|
|
_ => (Typ::Unknown, false),
|
|
}
|
|
}
|
|
|
|
fn parse_one_of_type(x: &Box<TsType>) -> Option<OneOfVariant> {
|
|
match &**x {
|
|
TsType::TsTypeLit(TsTypeLit { members, .. }) => {
|
|
let label = one_of_label(members)?;
|
|
let properties = one_of_properties(members);
|
|
Some(OneOfVariant { label, properties })
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn one_of_label(members: &Vec<TsTypeElement>) -> Option<String> {
|
|
members.iter().find_map(|y| {
|
|
let TsTypeElement::TsPropertySignature(TsPropertySignature { key, type_ann, .. }) = y
|
|
else {
|
|
return None;
|
|
};
|
|
|
|
let Expr::Ident(Ident { sym, .. }) = &**key else {
|
|
return None;
|
|
};
|
|
if sym != "label" {
|
|
return None;
|
|
}
|
|
|
|
let Some(type_ann) = type_ann.as_ref() else {
|
|
return None;
|
|
};
|
|
let TsType::TsLitType(TsLitType { lit: TsLit::Str(Str { value, .. }), .. }) =
|
|
&*type_ann.type_ann
|
|
else {
|
|
return None;
|
|
};
|
|
|
|
Some(value.to_string())
|
|
})
|
|
}
|
|
|
|
fn one_of_properties(members: &Vec<TsTypeElement>) -> Vec<ObjectProperty> {
|
|
members
|
|
.iter()
|
|
.filter_map(|x| {
|
|
let TsTypeElement::TsPropertySignature(TsPropertySignature { key, type_ann, .. }) = x
|
|
else {
|
|
return None;
|
|
};
|
|
|
|
let Expr::Ident(Ident { sym, .. }) = *key.to_owned() else {
|
|
return None;
|
|
};
|
|
let typ = type_ann
|
|
.as_ref()
|
|
.map(|typ| Box::new(tstype_to_typ(&*typ.type_ann).0))
|
|
.unwrap_or(Box::new(Typ::Unknown));
|
|
|
|
Some(ObjectProperty { key: sym.to_string(), typ })
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn find_undefined(types: &Vec<Box<TsType>>) -> Option<usize> {
|
|
types.into_iter().position(|x| match **x {
|
|
TsType::TsKeywordType(TsKeywordType { kind, .. }) => {
|
|
kind == TsKeywordTypeKind::TsUndefinedKeyword
|
|
|| kind == TsKeywordTypeKind::TsNullKeyword
|
|
}
|
|
_ => false,
|
|
})
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
pub fn eval(s: &str) -> JsValue;
|
|
pub fn alert(s: &str);
|
|
#[wasm_bindgen(js_namespace = console)]
|
|
fn log(s: &str);
|
|
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
pub fn eval_sync(code: &str) -> Result<serde_json::Value, String> {
|
|
serde_wasm_bindgen::from_value(eval(format!("let x = {}; x", code).as_str()))
|
|
.map_err(|err| format!("Cannot deserialize value: {:?}", err))
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn eval_sync(_code: &str) -> Result<serde_json::Value, String> {
|
|
panic!("eval_sync is only available in wasm32")
|
|
}
|