diff --git a/.github/workflows/ci_main.yml b/.github/workflows/ci_main.yml new file mode 100644 index 0000000..e1b11fa --- /dev/null +++ b/.github/workflows/ci_main.yml @@ -0,0 +1,49 @@ +name: CI + +on: + pull_request: + types: ['opened', 'reopened', 'synchronize'] + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + name: Run test + + steps: + - uses: actions/checkout@v2 + - uses: buildjet/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: actions/setup-node@v2 + with: + node-version: "16" + cache: "npm" + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + components: llvm-tools-preview + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: install + run: | + npm install -g npm@latest + npm ci + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + rustup target add wasm32-wasi + rustup target add wasm32-unknown-unknown + - name: test + run: | + npm run build + - name: build + run: | + cargo check + cargo check -p garam \ No newline at end of file diff --git a/.gitignore b/.gitignore index c94f110..7a2868a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /node_modules /test/parser/**/_actual.json *.log -/test/**/_actual.* \ No newline at end of file +/test/**/_actual.* +/test.js \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 1929246..d838225 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,19 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -swc_core = { version = "0.79.28", features = ["ecma_visit", "ecma_visit_path", "ecma_ast_serde"] } +swc_core = { version = "0.79.28", features = [ + "common", + "ecma_visit", + "ecma_visit_path", + "ecma_ast_serde", +] } +swc_estree_compat = { version = "0.187.26" } +swc_estree_ast = { version = "0.21.18" } -wasm-bindgen = {version = "0.2.87", features = [] } -serde = {version = "1.0.175", features = ["derive"]} -serde-wasm-bindgen = {version = "0.5.0"} -js-sys = {version = "0.3.64" } -paste = {version = "1.0.14" } +wasm-bindgen = { version = "0.2.87" } +serde = { version = "1.0.175", features = ["derive"] } +serde-wasm-bindgen = { version = "0.5.0" } +js-sys = { version = "0.3.64" } +paste = { version = "1.0.14" } -getrandom = {version = "0.2.10", features = ["js"] } \ No newline at end of file +getrandom = { version = "0.2.10", features = ["js"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..27f596e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 OJ Kwon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..16393bf --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +### Woodpile + +Woodpile is a utility library to traverse [SWC](https://github.com/swc-project/swc) ASTs in Javascript. It is a thin interop layer to the SWC's rust implementation of its visitor macro, attempt to provide consistent, synchronized mechanism to traverse ASTs in Javascript as well. This package could be useful to duck typing, prototyping SWC plugin in Javascript as its interface aims to provide similar experience as SWC's visitor macro. + +For those reason, this package aims correctness over performance. There are inevitable costs to exchange data between Javascript to Rust, and vice versa. If you want to achieve peak performace, you should use SWC's visitor macro directly. + + +### Usage + +`visit` is a main interface to traverse AST. + +Currently, `visit` accepts an object with `visit` property have corresponding callbacks to traverse. + +```ts +const { visit } = require('woodpile'); +const { parseSync } = require('@swc/core'); + +const ast = parseSync('console.log("Hello, World!")'); + +visit(ast, { + visit: { + // Callbacks with visit${NodeName} will be called recursively for the node + visitProgram: (node, self) => { + console.log('visitProgram', node); + }, + visitExpr: (node) => { + console.log('visitExpr', node); + } + }, +}); +``` + +It is possible to return node in each callback which attempts to replace given node. + +```ts + visitProgram: (node) => { + node.Span = ...; + return node + } +``` + +However, it doesn't check if the returned node is valid or not but will hard fail if the returned node is not valid. Callback also passes `self` as a second parameter. This is a context to the visitor object itself. + +There are also another utility function `compat`, attempts to provide conversion to the estree-compatble AST from SWC. Note this is the _closest attempt_ to generate compatible AST, likely will have some differences. + +```ts +const { compat } = require('woodpile'); +const { parseSync } = require('@swc/core'); + +const ast = parseSync('console.log("Hello, World!")'); + +const compat_ast = compat(ast, { + source: "" // optional, original source code to the input ast + flavor: "babel" | "acorn" // optional, default to babel +}) +``` \ No newline at end of file diff --git a/package.json b/package.json index 269559c..0314eef 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,34 @@ { "name": "woodpile", "version": "0.0.1", - "description": "", - "main": "test.js", + "description": "SWC AST walker for Javascript", + "main": "pkg/woodpile.js", + "files": [ + "pkg/*.wasm", + "pkg/*.js", + "pkg/*.d.ts", + "README.md", + "LICENSE" + ], "scripts": { - "build": "wasm-pack build --dev --target nodejs", + "prepublishOnly": "npm run build", + "build": "wasm-pack build --target nodejs", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/kwonoj/woodpile.git" }, - "keywords": [], - "author": "", + "keywords": [ + "SWC", + "AST", + "estree", + "Babel", + "walker", + "visitor", + "swc_ecma_visit" + ], + "author": "OJ Kwon ", "license": "MIT", "bugs": { "url": "https://github.com/kwonoj/woodpile/issues" diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..cf53f1c --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly-2023-07-03 \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 4d0308b..27bb55c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,41 @@ use js_sys::Reflect; use paste::paste; -use swc_core::ecma::{ - ast::{Module, Program}, - visit::{noop_visit_mut_type, VisitMut, VisitMutAstPath, VisitMutWith, VisitMutWithPath}, +use serde_wasm_bindgen::Serializer; +use swc_core::{ + common::{sync::Lrc, FileName, FilePathMapping, SourceMap}, + ecma::{ + ast::{ + ArrayLit, ArrayPat, ArrowExpr, AssignPat, AssignPatProp, AssignProp, AwaitExpr, + BinExpr, BindingIdent, BlockStmt, BreakStmt, CallExpr, Callee, CatchClause, Class, + ClassDecl, ClassExpr, ClassMember, ClassMethod, ClassProp, ComputedPropName, CondExpr, + Constructor, ContinueStmt, DebuggerStmt, Decl, Decorator, DefaultDecl, DoWhileStmt, + EmptyStmt, ExportAll, ExportDecl, ExportDefaultDecl, ExportDefaultExpr, + ExportDefaultSpecifier, ExportNamedSpecifier, ExportNamespaceSpecifier, + ExportSpecifier, Expr, ExprOrSpread, ExprStmt, FnDecl, FnExpr, ForInStmt, ForOfStmt, + ForStmt, Function, GetterProp, Ident, IfStmt, ImportDecl, ImportSpecifier, JSXAttr, + JSXAttrName, JSXAttrOrSpread, JSXAttrValue, JSXClosingElement, JSXClosingFragment, + JSXElement, JSXElementChild, JSXElementName, JSXEmptyExpr, JSXExprContainer, + JSXFragment, JSXMemberExpr, JSXNamespacedName, JSXObject, JSXOpeningElement, + JSXOpeningFragment, JSXSpreadChild, JSXText, KeyValuePatProp, KeyValueProp, + LabeledStmt, Lit, MemberExpr, MetaPropExpr, MethodProp, Module, ModuleDecl, + ModuleExportName, ModuleItem, NamedExport, NewExpr, ObjectLit, ObjectPat, + ObjectPatProp, OptChainExpr, Param, ParenExpr, Pat, PatOrExpr, PrivateMethod, + PrivateName, PrivateProp, Program, Prop, PropName, RestPat, ReturnStmt, Script, + SeqExpr, SetterProp, SpreadElement, StaticBlock, Stmt, Str, SuperPropExpr, SwitchCase, + SwitchStmt, TaggedTpl, ThisExpr, ThrowStmt, Tpl, TryStmt, UnaryExpr, UpdateExpr, + VarDecl, VarDeclarator, WhileStmt, WithStmt, YieldExpr, + }, + visit::{ + noop_visit_mut_type, AstKindPath, VisitMut, VisitMutAstPath, VisitMutWith, + VisitMutWithPath, + }, + }, }; +use swc_estree_ast::flavor::Flavor; use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; pub struct BaseVisitor { + visitor_context: JsValue, visitor: Option, visitor_with_path: Option, } @@ -27,12 +56,18 @@ impl BaseVisitor { }; Self { + visitor_context: visitor, visitor: visitor_value, visitor_with_path, } } - fn call_visitor_reflected_fn(&self, property: &str, arg: &JsValue) { + fn call_visitor_reflected_fn( + &self, + property: &str, + arg: &JsValue, + path_arg: Option<&JsValue>, + ) -> Option { if let Some(visitor) = &self.visitor { let fn_value = Reflect::has(visitor, &JsValue::from_str(property)) .map(|has| { @@ -47,39 +82,242 @@ impl BaseVisitor { if let Some(fn_value) = fn_value { let fn_value = fn_value.dyn_into::(); if let Ok(fn_value) = fn_value { - let _ = fn_value.call1(&JsValue::null(), &arg); + let result = if let Some(path_arg) = path_arg { + fn_value.call3(&self.visitor_context, &arg, path_arg, &self.visitor_context) + } else { + fn_value.call2(&self.visitor_context, &arg, &self.visitor_context) + }; + if let Ok(result) = result { + if result.is_object() { + return Some(result); + } + } } } } + + return None; } } macro_rules! write_visit_mut { + ($capital: ident, $ty: ident) => { + paste! { + fn [](&mut self, n: &mut [<$capital:upper $ty>]) { + let path_jsvalue = serde_wasm_bindgen::to_value(n).expect(format!("Should be able to serialize path {}", stringify!([<$capital:upper $ty>])).as_str()); + + let ret = self.call_visitor_reflected_fn( + &format!("visit{}", stringify!([<$capital$ty:camel>])), + &path_jsvalue, + None + ); + + if let Some(ret) = ret { + let ret: [<$capital:upper $ty>] = serde_wasm_bindgen::from_value(ret).expect(format!("Should be able to deserialize {}", stringify!([<$capital:upper $ty>])).as_str()); + *n = ret; + } + + n.visit_mut_children_with(self); + } + } + }; ($ty:ident) => { paste! { - fn [](&mut self, n: &mut $ty) { + fn [](&mut self, n: &mut $ty) { let path_jsvalue = serde_wasm_bindgen::to_value(n).expect(format!("Should be able to serialize path {}", stringify!($ty)).as_str()); - self.call_visitor_reflected_fn( - &stringify!([<$ty:lower>]), - &path_jsvalue + let ret = self.call_visitor_reflected_fn( + &format!("visit{}", stringify!([<$ty:camel>])), + &path_jsvalue, + None ); + if let Some(ret) = ret { + let ret: $ty = serde_wasm_bindgen::from_value(ret).expect(format!("Should be able to deserialize {}", stringify!($ty)).as_str()); + *n = ret; + } + n.visit_mut_children_with(self); } } }; } +macro_rules! some { + ( $var:expr ) => { + const a: &'static str = stringify!($var); + }; +} + +macro_rules! write_visit_mut_plural { + ($ty:ident) => { + paste! { + fn [](&mut self, n: &mut Vec<$ty>) { + /*r#" + interface Visitor { + []?: ($ty:lower :Array) => Array | void; + }"#;*/ + + + let path_jsvalue = serde_wasm_bindgen::to_value(n).expect(format!("Should be able to serialize path {}", stringify!([<$ty s>])).as_str()); + + let ret = self.call_visitor_reflected_fn( + &format!("visit{}s", stringify!([<$ty:camel>])), + &path_jsvalue, + None + ); + + if let Some(ret) = ret { + let ret: Vec<$ty> = serde_wasm_bindgen::from_value(ret).expect(format!("Should be able to deserialize {}", stringify!([<$ty s>])).as_str()); + *n = ret; + } + + n.visit_mut_children_with(self); + } + } + }; +} + +impl VisitMutAstPath for BaseVisitor { + // [TODO]: serde-serialize support for AstKindPath? +} + impl VisitMut for BaseVisitor { noop_visit_mut_type!(); write_visit_mut!(Program); write_visit_mut!(Module); + write_visit_mut!(Script); + write_visit_mut!(ModuleItem); + write_visit_mut_plural!(ModuleItem); + write_visit_mut!(ModuleDecl); + write_visit_mut!(ExportAll); + write_visit_mut!(ExportDefaultDecl); + write_visit_mut!(ExportDefaultExpr); + write_visit_mut!(ExportSpecifier); + write_visit_mut_plural!(ExportSpecifier); + write_visit_mut!(ExportNamedSpecifier); + write_visit_mut!(NamedExport); + write_visit_mut!(ModuleExportName); + write_visit_mut!(ExportNamespaceSpecifier); + write_visit_mut!(ExportDefaultSpecifier); + write_visit_mut!(Str); + write_visit_mut!(DefaultDecl); + write_visit_mut!(FnExpr); + write_visit_mut!(ExportDecl); + write_visit_mut!(ArrayLit); + write_visit_mut!(ExprOrSpread); + write_visit_mut!(SpreadElement); + write_visit_mut!(Expr); + write_visit_mut!(ArrowExpr); + write_visit_mut!(BlockStmt); + write_visit_mut!(Stmt); + write_visit_mut_plural!(Stmt); + write_visit_mut!(SwitchStmt); + write_visit_mut!(SwitchCase); + write_visit_mut_plural!(SwitchCase); + write_visit_mut!(IfStmt); + write_visit_mut!(ObjectPat); + write_visit_mut!(ObjectPatProp); + write_visit_mut_plural!(ObjectPatProp); + write_visit_mut!(ArrayPat); + write_visit_mut!(Pat); + write_visit_mut!(ImportDecl); + write_visit_mut!(ImportSpecifier); + write_visit_mut!(BreakStmt); + write_visit_mut!(WhileStmt); + write_visit_mut!(TryStmt); + write_visit_mut!(CatchClause); + write_visit_mut!(ThrowStmt); + write_visit_mut!(ReturnStmt); + write_visit_mut!(LabeledStmt); + write_visit_mut!(ForStmt); + write_visit_mut!(ForOfStmt); + write_visit_mut!(ForInStmt); + write_visit_mut!(EmptyStmt); + write_visit_mut!(DoWhileStmt); + write_visit_mut!(DebuggerStmt); + write_visit_mut!(WithStmt); + write_visit_mut!(Decl); + write_visit_mut!(VarDecl); + write_visit_mut!(VarDeclarator); + write_visit_mut_plural!(VarDeclarator); + write_visit_mut!(FnDecl); + write_visit_mut!(Class); + write_visit_mut!(ClassDecl); + write_visit_mut!(ClassExpr); + write_visit_mut!(ClassProp); + write_visit_mut!(ClassMethod); + write_visit_mut!(ClassMember); + write_visit_mut_plural!(ClassMember); + write_visit_mut!(PrivateProp); + write_visit_mut!(PrivateMethod); + write_visit_mut!(PrivateName); + write_visit_mut!(Constructor); + write_visit_mut!(StaticBlock); + write_visit_mut!(PropName); + write_visit_mut!(ComputedPropName); + write_visit_mut!(Function); + write_visit_mut!(Decorator); + write_visit_mut_plural!(Decorator); + write_visit_mut!(ExprStmt); + write_visit_mut!(ContinueStmt); + write_visit_mut!(OptChainExpr); + write_visit_mut!(PatOrExpr); + write_visit_mut!(YieldExpr); + write_visit_mut!(UpdateExpr); + write_visit_mut!(UnaryExpr); + write_visit_mut!(ThisExpr); + write_visit_mut!(Tpl); + write_visit_mut!(TaggedTpl); + write_visit_mut!(Param); + write_visit_mut_plural!(Param); + write_visit_mut!(SeqExpr); + write_visit_mut!(Lit); + write_visit_mut!(ParenExpr); + write_visit_mut!(ObjectLit); + write_visit_mut!(Prop); + write_visit_mut!(SetterProp); + write_visit_mut!(MethodProp); + write_visit_mut!(KeyValueProp); + write_visit_mut!(GetterProp); + write_visit_mut!(AssignProp); + write_visit_mut!(NewExpr); + write_visit_mut!(MetaPropExpr); + write_visit_mut!(MemberExpr); + write_visit_mut!(SuperPropExpr); + write_visit_mut!(Callee); + write_visit_mut!(JSX, Text); + write_visit_mut!(JSX, NamespacedName); + write_visit_mut!(JSX, MemberExpr); + write_visit_mut!(JSX, Object); + write_visit_mut!(JSX, Fragment); + write_visit_mut!(JSX, ClosingFragment); + write_visit_mut!(JSX, ElementChild); + write_visit_mut!(JSX, ExprContainer); + write_visit_mut!(JSX, SpreadChild); + write_visit_mut!(JSX, OpeningFragment); + write_visit_mut!(JSX, EmptyExpr); + write_visit_mut!(JSX, Element); + write_visit_mut!(JSX, ClosingElement); + write_visit_mut!(JSX, ElementName); + write_visit_mut!(JSX, OpeningElement); + write_visit_mut!(JSX, Attr); + write_visit_mut!(JSX, AttrOrSpread); + write_visit_mut!(JSX, AttrValue); + write_visit_mut!(JSX, AttrName); + write_visit_mut!(CondExpr); + write_visit_mut!(CallExpr); + write_visit_mut!(BinExpr); + write_visit_mut!(AwaitExpr); + write_visit_mut!(BindingIdent); + write_visit_mut!(Ident); + write_visit_mut!(RestPat); + write_visit_mut!(AssignPatProp); + write_visit_mut!(AssignPat); + write_visit_mut!(KeyValuePatProp); } -impl VisitMutAstPath for BaseVisitor {} - #[wasm_bindgen] pub fn visit(p: JsValue, visitor: JsValue) { let mut p: Program = serde_wasm_bindgen::from_value(p).unwrap(); @@ -87,3 +325,58 @@ pub fn visit(p: JsValue, visitor: JsValue) { let mut visitor = BaseVisitor::new(visitor); p.visit_mut_with(&mut visitor); } + +#[wasm_bindgen(getter_with_clone)] +pub struct CompatOptions { + pub source: Option, + pub flavor: Option, +} + +#[wasm_bindgen(skip_typescript)] +pub fn compat(p: JsValue, opts: Option) -> JsValue { + let p: Program = serde_wasm_bindgen::from_value(p).unwrap(); + + let src_input = opts + .as_ref() + .and_then(|opts| opts.source.as_ref()) + .map(|x| Lrc::new(x.to_string())); + + let cm = std::sync::Arc::new(SourceMap::new(FilePathMapping::empty())); + let fm = cm.new_source_file_from(FileName::Anon, src_input.unwrap_or_default()); + + let context = swc_estree_compat::babelify::Context { + cm, + fm, + comments: Default::default(), + }; + + let p: swc_estree_ast::File = + swc_estree_compat::babelify::Babelify::babelify(p, &context).into(); + + let flavor = if let Some(opts) = opts { + if let Some(flavor) = opts.flavor { + match flavor.as_str() { + "acorn" => Flavor::Acorn { + extra_comments: false, + }, + _ => Flavor::Babel, + } + } else { + Flavor::Babel + } + } else { + Flavor::Babel + }; + + let serializer = Serializer::json_compatible() + .serialize_missing_as_null(false) + //https://github.com/serde-rs/serde/issues/1346 + .serialize_maps_as_objects(true); + + // [TODO]: Error handling + let result = flavor + .with(|| serde::Serialize::serialize(&p, &serializer)) + .unwrap(); + + return result; +}