diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fa7cb3..814a68f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,5 @@ name: CI -on: [push] +on: [push, pull_request] env: CARGO_TERM_COLOR: always RUST_VERSION: 1.84.1 @@ -14,6 +14,10 @@ jobs: run: rustup toolchain install ${{ env.RUST_VERSION }} --component clippy --component rustfmt && rustup default ${{ env.RUST_VERSION }} - name: Cache uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' - name: Build run: cargo build --all - name: Run tests @@ -22,6 +26,8 @@ jobs: run: cargo clippy --all -- -D warnings - name: Run fmt run: cargo fmt --all -- --check + - name: Build and Test wasm + run: npm install && npm run build && npm test - name: Install license tool run: cargo install dd-rust-license-tool - name: Run license tool diff --git a/.gitignore b/.gitignore index 5253332..56fbe1c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ ^target/ target tests/*/instrumented.* +pkg/ +node_modules/ +package-lock.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a1c7d33..4a4e1a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "serde", "version_check", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.19" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ "shlex", ] @@ -595,13 +595,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -622,6 +624,19 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -977,23 +992,22 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miette" -version = "7.5.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ "cfg-if", "miette-derive", "owo-colors", "textwrap", - "thiserror 1.0.69", "unicode-width 0.1.14", ] [[package]] name = "miette-derive" -version = "7.5.0" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", @@ -1008,18 +1022,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "munge" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0091202c98cf06da46c279fdf50cccb6b1c43b4521abdf6a27b4c7e71d5d9d7" +checksum = "9e22e7961c873e8b305b176d2a4e1d41ce7ba31bc1c52d2a107a89568ec74c55" dependencies = [ "munge_macro", ] [[package]] name = "munge_macro" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734799cf91479720b2f970c61a22850940dd91e27d4f02b1c6fc792778df2459" +checksum = "0ac7d860b767c6398e88fe93db73ce53eb496057aa6895ffa4d60cb02e1d1c6b" dependencies = [ "proc-macro2", "quote", @@ -1040,6 +1054,7 @@ checksum = "54a541e66989ad860689e1994447f22ab670d604068ed11376894ada03890c11" dependencies = [ "bytecount", "miette", + "serde", "thiserror 1.0.69", "winnow", ] @@ -1123,11 +1138,15 @@ name = "orchestrion-js" version = "0.1.0" dependencies = [ "assert_cmd", + "getrandom 0.2.16", "nodejs-semver", + "serde", "swc", "swc_core", "swc_ecma_parser", "swc_ecma_visit", + "tsify", + "wasm-bindgen", ] [[package]] @@ -1150,9 +1169,9 @@ checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" [[package]] name = "par-core" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b506ab63a8bd3cd38858c7bfc2d078a189dc3210c7f8c9be1bbaf50c082a0ae" +checksum = "757892557993c69e82f9de0f9051e87144278aa342f03bf53617bbf044554484" dependencies = [ "once_cell", ] @@ -1338,9 +1357,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" dependencies = [ "cc", ] @@ -1611,6 +1630,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -1622,6 +1652,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.140" @@ -1752,9 +1793,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" dependencies = [ "cc", "cfg-if", @@ -1895,9 +1936,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "8.1.0" +version = "8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d96ac5d021c7c20acb3073940b4ee59b62989a705f855783c4a452e0737a2e6" +checksum = "4f8c8e4348383e4154f8d384cdad7e48f5d6d3daef78af376ac4e5ddbbf60c88" dependencies = [ "anyhow", "ast_node", @@ -1981,9 +2022,9 @@ dependencies = [ [[package]] name = "swc_core" -version = "22.5.0" +version = "22.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "750935e8c0a2c23b69968d8c8f65963750fb20c9803311e3d3ed248340fbac97" +checksum = "39b5f70fe5f8db1d3977aae3f2ecb64bae55d0e4f5ec93d372219f2ecf58e372" dependencies = [ "once_cell", "swc_allocator", @@ -2622,9 +2663,9 @@ dependencies = [ [[package]] name = "swc_ecma_transforms_proposal" -version = "12.0.1" +version = "12.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5265158f5134b7b37dd2d53e7730921b8b5f567f6baddcc52129c2eb55927214" +checksum = "08a2b5e62d2badf805aba6e976518cceb28273a108cb9ab9f339c55483edc92c" dependencies = [ "either", "rustc-hash 2.1.1", @@ -2732,9 +2773,9 @@ dependencies = [ [[package]] name = "swc_ecma_utils" -version = "12.0.0" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d6c8ba7d987dcc254f05ad2c23e7a6ec3f259611af2923a8c1a0602556cd21" +checksum = "a7c499ba586b784be6dfbdd76ebd3cfdbabaf43a5bda162a11fe7dd326670b62" dependencies = [ "indexmap", "num_cpus", @@ -2916,9 +2957,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -3164,6 +3205,32 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "tsify" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ec91b85e6c6592ed28636cb1dd1fac377ecbbeb170ff1d79f97aac5e38926d" +dependencies = [ + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a324606929ad11628a19206d7853807481dcaecd6c08be70a235930b8241955" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + [[package]] name = "typed-arena" version = "2.0.2" @@ -3360,6 +3427,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index b7e4d6f..ca3d343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,18 +6,31 @@ rust-version = "1.84.1" license = "Apache-2.0" [lib] -# crate-type = ["cdylib"] -# This was originally set as above, but commented to run tests in tests folder. +crate-type = ["cdylib", "rlib"] -[profile.release] -lto = true +[features] +serde = ["serde/derive"] +wasm = ['serde', "wasm-bindgen", "tsify", "getrandom"] [dependencies] -nodejs-semver = "4" +nodejs-semver = { version = "4", features = ["serde"] } swc = "21" swc_core = { version = "22", features = ["ecma_plugin_transform","ecma_quote"] } swc_ecma_parser = "11" swc_ecma_visit = { version = "8", features = ["path"] } +# serde feature +serde = { version = "1", features = ["derive"], optional = true } + +# wasm feature +wasm-bindgen = { version = "0.2", optional = true } +tsify = { version='0.5', features = ["js"], optional = true} +# we need this to enable the js feature +getrandom = { version = "*", features = ["js"], optional = true } + [dev-dependencies] assert_cmd = "2" + +[profile.release] +lto = true +opt-level = "s" \ No newline at end of file diff --git a/README.md b/README.md index 5788e0c..f823fbb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # Orchestion-JS Orchestrion is a library for instrumenting Node.js libraries at build or load time. -It provides [`VisitMut`] implementations for SWC's AST nodes, which can be used to insert tracing code into matching functions. -It's entirely configurable via a YAML string, and can be used in SWC plugins, or anything else that mutates JavaScript ASTs using SWC. + +It provides `VisitMut` implementations for SWC's AST nodes, which can be used to insert tracing code into matching functions. +It can be used in SWC plugins, or anything else that mutates JavaScript ASTs using SWC. + +Orchestrion can also be built as a JavaScript module, which can be used from Node.js. + +To build the JavaScript module: +- Ensure you have [Rust installed](https://www.rust-lang.org/tools/install) +- Install the wasm toolchain `rustup target add wasm32-unknown-unknown --toolchain stable` +- Install dependencies and build the module `npm install && npm run build` ## Contributing @@ -11,5 +19,3 @@ See CONTRIBUTING.md ## License See LICENSE - -[`VisitMut`]: https://rustdoc.swc.rs/swc_core/ecma/visit/trait.VisitMut.html diff --git a/package.json b/package.json new file mode 100644 index 0000000..603d28d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "orchestrion-js", + "version": "0.1.0", + "license": "Apache-2.0", + "files": [ + "./pkg/orchestrion_js_bg.wasm", + "./pkg/orchestrion_js.js", + "./pkg/orchestrion_js.d.ts", + "LICENSE", + "NOTICE" + ], + "main": "./pkg/orchestrion_js.js", + "types": "./pkg/orchestrion_js.d.ts", + "scripts": { + "build": "wasm-pack build --target nodejs --release -- --features wasm", + "test": "node ./tests/tests.mjs" + }, + "dependencies": { + "wasm-pack": "^0.13.1" + }, + "volta": { + "node": "22.15.0" + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 771108c..202b090 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,12 @@ use crate::function_query::FunctionQuery; use nodejs_semver::{Range, SemverError, Version}; use std::path::PathBuf; +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] #[derive(Debug, Clone)] pub struct ModuleMatcher { pub name: String, @@ -41,6 +47,16 @@ impl ModuleMatcher { } } +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] #[derive(Debug, Clone)] pub struct InstrumentationConfig { pub channel_name: String, @@ -59,6 +75,11 @@ impl InstrumentationConfig { } } +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "camelCase") +)] #[derive(Debug, Clone)] pub struct Config { pub instrumentations: Vec, diff --git a/src/function_query.rs b/src/function_query.rs index 2f7a28a..99d4d2f 100644 --- a/src/function_query.rs +++ b/src/function_query.rs @@ -11,6 +11,8 @@ pub(crate) enum FunctionType { Method, } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub enum FunctionKind { Sync, @@ -40,31 +42,49 @@ impl FunctionKind { } } +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(untagged, rename_all_fields = "camelCase") +)] #[derive(Debug, Clone)] pub enum FunctionQuery { - ClassConstructor { - class_name: String, - index: usize, - }, + // The order here matters because this enum is untagged, serde will try + // choose the first variant that matches the data. ClassMethod { class_name: String, method_name: String, kind: FunctionKind, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "wasm", tsify(optional))] + index: usize, + }, + ClassConstructor { + class_name: String, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "wasm", tsify(optional))] index: usize, }, ObjectMethod { method_name: String, kind: FunctionKind, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "wasm", tsify(optional))] index: usize, }, FunctionDeclaration { function_name: String, kind: FunctionKind, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "wasm", tsify(optional))] index: usize, }, FunctionExpression { expression_name: String, kind: FunctionKind, + #[cfg_attr(feature = "serde", serde(default))] + #[cfg_attr(feature = "wasm", tsify(optional))] index: usize, }, } diff --git a/src/instrumentation.rs b/src/instrumentation.rs index f2d0247..3f55bbe 100644 --- a/src/instrumentation.rs +++ b/src/instrumentation.rs @@ -27,7 +27,7 @@ macro_rules! ident { /// /// [`Instrumentation`]: Instrumentation /// [`VisitMut`]: https://rustdoc.swc.rs/swc_core/ecma/visit/trait.VisitMut.html -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Instrumentation { config: InstrumentationConfig, count: usize, diff --git a/src/lib.rs b/src/lib.rs index d027f03..e328f95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,17 +18,23 @@ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. * This product includes software developed at Datadog (/). Copyright 2025 Datadog, Inc. **/ -use std::path::PathBuf; +use std::{error::Error, path::PathBuf, sync::Arc}; +use swc::{ + config::{IsModule, SourceMapsConfig}, + try_with_handler, Compiler, HandlerOpts, PrintArgs, +}; use swc_core::{ + common::{comments::Comments, errors::ColorConfig, FileName, FilePathMapping}, ecma::{ ast::{ - AssignExpr, ClassDecl, ClassMethod, Constructor, FnDecl, MethodProp, Module, Script, - Str, VarDecl, + AssignExpr, ClassDecl, ClassMethod, Constructor, EsVersion, FnDecl, MethodProp, Module, + Script, Str, VarDecl, }, visit::{VisitMut, VisitMutWith}, }, quote, }; +use swc_ecma_parser::{EsSyntax, Syntax}; mod error; @@ -41,6 +47,9 @@ pub use instrumentation::*; mod function_query; pub use function_query::*; +#[cfg(feature = "wasm")] +pub mod wasm; + /// This struct is responsible for managing all instrumentations. It's created from a YAML string /// via the [`FromStr`] trait. See tests for examples, but by-and-large this just means you can /// call `.parse()` on a YAML string to get an `Instrumentor` instance, if it's valid. @@ -65,37 +74,102 @@ impl Instrumentor { /// For a given module name, version, and file path within the module, return all /// `Instrumentation` instances that match. - pub fn get_matching_instrumentations<'a>( - &'a mut self, - module_name: &'a str, - version: &'a str, - file_path: &'a PathBuf, - ) -> InstrumentationVisitor<'a> { + #[must_use] + pub fn get_matching_instrumentations( + &self, + module_name: &str, + version: &str, + file_path: &PathBuf, + ) -> InstrumentationVisitor { let instrumentations = self .instrumentations - .iter_mut() + .iter() .filter(|instr| instr.matches(module_name, version, file_path)); - InstrumentationVisitor::new(instrumentations, self.dc_module.as_ref()) + InstrumentationVisitor::new(instrumentations, &self.dc_module) } } #[derive(Debug)] -pub struct InstrumentationVisitor<'a> { - instrumentations: Vec<&'a mut Instrumentation>, - dc_module: &'a str, +pub struct InstrumentationVisitor { + instrumentations: Vec, + dc_module: String, } -impl<'a> InstrumentationVisitor<'a> { - fn new(instrumentations: I, dc_module: &'a str) -> Self +impl InstrumentationVisitor { + fn new<'b, I>(instrumentations: I, dc_module: &str) -> Self where - I: Iterator + 'a, + I: Iterator, { Self { - instrumentations: instrumentations.collect(), - dc_module, + instrumentations: instrumentations.cloned().collect(), + dc_module: dc_module.to_string(), } } + + #[must_use] + pub fn has_instrumentations(&self) -> bool { + !self.instrumentations.is_empty() + } + + /// Transform the given JavaScript code. + /// # Errors + /// Returns an error if the transformation fails. + pub fn transform( + &mut self, + contents: &str, + is_module: IsModule, + ) -> Result> { + let compiler = Compiler::new(Arc::new(swc_core::common::SourceMap::new( + FilePathMapping::empty(), + ))); + + #[allow(clippy::redundant_closure_for_method_calls)] + Ok(try_with_handler( + compiler.cm.clone(), + HandlerOpts { + color: ColorConfig::Never, + skip_filename: false, + }, + |handler| { + let source_file = compiler.cm.new_source_file( + Arc::new(FileName::Real(PathBuf::from("index.mjs"))), + contents.to_string(), + ); + + let program = compiler + .parse_js( + source_file.clone(), + handler, + EsVersion::latest(), + Syntax::Es(EsSyntax { + explicit_resource_management: true, + import_attributes: true, + decorators: true, + ..Default::default() + }), + is_module, + Some(&compiler.comments() as &dyn Comments), + ) + .map(|mut program| { + program.visit_mut_with(self); + program + })?; + let result = compiler.print( + &program, + PrintArgs { + source_file_name: None, + source_map: SourceMapsConfig::Bool(false), + comments: None, + emit_source_map_columns: false, + ..Default::default() + }, + )?; + Ok(result.code) + }, + ) + .map_err(|e| e.to_pretty_error())?) + } } macro_rules! visit_with_all { @@ -119,14 +193,14 @@ macro_rules! visit_with_all_fn { }; } -impl VisitMut for InstrumentationVisitor<'_> { +impl VisitMut for InstrumentationVisitor { fn visit_mut_module(&mut self, item: &mut Module) { let mut line = quote!( "import { tracingChannel as tr_ch_apm_tracingChannel } from 'dc';" as ModuleItem, ); if let Some(module_decl) = line.as_mut_module_decl() { if let Some(import) = module_decl.as_mut_import() { - import.src = Box::new(Str::from(self.dc_module)); + import.src = Box::new(Str::from(self.dc_module.as_ref())); item.body.insert(0, line); } } @@ -139,7 +213,7 @@ impl VisitMut for InstrumentationVisitor<'_> { fn visit_mut_script(&mut self, item: &mut Script) { let import = quote!( "const { tracingChannel: tr_ch_apm_tracingChannel } = require($dc);" as Stmt, - dc: Expr = self.dc_module.into(), + dc: Expr = self.dc_module.clone().into(), ); item.body.insert(get_script_start_index(item), import); visit_with_all!(self, visit_mut_script, item); diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 0000000..9fcb5be --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,48 @@ +use crate::*; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +#[wasm_bindgen] +pub struct InstrumentationMatcher(Instrumentor); + +#[wasm_bindgen] +impl InstrumentationMatcher { + #[wasm_bindgen(js_name = "getTransformer")] + pub fn get_transformer( + &mut self, + module_name: &str, + version: &str, + file_path: &str, + ) -> Option { + let instrumentations = + self.0 + .get_matching_instrumentations(module_name, version, &PathBuf::from(file_path)); + + if instrumentations.has_instrumentations() { + Some(Transformer(instrumentations)) + } else { + None + } + } +} + +#[wasm_bindgen] +pub struct Transformer(InstrumentationVisitor); + +#[wasm_bindgen] +impl Transformer { + #[wasm_bindgen] + pub fn transform(&mut self, contents: &str, is_module: bool) -> Result { + let is_module = IsModule::Bool(is_module); + self.0 + .transform(contents, is_module) + .map_err(|e| JsValue::from_str(&e.to_string())) + } +} + +#[wasm_bindgen] +pub fn create( + configs: Vec, + dc_module: Option, +) -> InstrumentationMatcher { + InstrumentationMatcher(Instrumentor::new(Config::new(configs, dc_module))) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 6554d66..b86ed47 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,91 +7,17 @@ use orchestrion_js::*; use std::io::prelude::*; use std::path::PathBuf; use std::process::Command; -use std::sync::Arc; -use swc::{ - config::{IsModule, SourceMapsConfig}, - try_with_handler, Compiler, HandlerOpts, PrintArgs, -}; -use swc_core::common::{comments::Comments, errors::ColorConfig, FileName, FilePathMapping}; -use swc_core::ecma::ast::EsVersion; -use swc_ecma_parser::{EsSyntax, Syntax}; -use swc_ecma_visit::VisitMutWith; +use swc::config::IsModule; -fn print_result(original: &str, modified: &str) { - println!( - "\n - == === Original === == - \n{}\n\n\n - == === Modified === == - \n{}\n\n", - original, modified - ); -} - -fn transpile( - contents: &str, - is_module: IsModule, - instrumentation: &mut InstrumentationVisitor, -) -> String { - let compiler = Compiler::new(Arc::new(swc_core::common::SourceMap::new( - FilePathMapping::empty(), - ))); - try_with_handler( - compiler.cm.clone(), - HandlerOpts { - color: ColorConfig::Never, - skip_filename: false, - }, - |handler| { - let source_file = compiler.cm.new_source_file( - Arc::new(FileName::Real(PathBuf::from("index.mjs"))), - contents.to_string(), - ); - - let program = compiler - .parse_js( - source_file.to_owned(), - handler, - EsVersion::latest(), - Syntax::Es(EsSyntax { - explicit_resource_management: true, - import_attributes: true, - decorators: true, - ..Default::default() - }), - is_module, - Some(&compiler.comments() as &dyn Comments), - ) - .map(|mut program| { - program.visit_mut_with(instrumentation); - program - }) - .unwrap(); - let result = compiler - .print( - &program, - PrintArgs { - source_file_name: None, - source_map: SourceMapsConfig::Bool(false), - comments: None, - emit_source_map_columns: false, - ..Default::default() - }, - ) - .unwrap(); - - print_result(contents, &result.code); - Ok(result.code) - }, - ) - .unwrap() -} - -static TEST_MODULE_NAME: &'static str = "undici"; -static TEST_MODULE_PATH: &'static str = "index.mjs"; +static TEST_MODULE_NAME: &str = "undici"; +static TEST_MODULE_PATH: &str = "index.mjs"; pub fn transpile_and_test(test_file: &str, mjs: bool, config: Config) { let test_file = PathBuf::from(test_file); let test_dir = test_file.parent().expect("Couldn't find test directory"); let file_path = PathBuf::from("index.mjs"); - let mut instrumentor = Instrumentor::new(config); + let instrumentor = Instrumentor::new(config); let mut instrumentations = instrumentor.get_matching_instrumentations(TEST_MODULE_NAME, "0.0.1", &file_path); @@ -101,7 +27,9 @@ pub fn transpile_and_test(test_file: &str, mjs: bool, config: Config) { let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); - let result = transpile(&contents, IsModule::Bool(mjs), &mut instrumentations); + let result = instrumentations + .transform(&contents, IsModule::Bool(mjs)) + .unwrap(); let instrumented_file = test_dir.join(format!("instrumented.{}", extension)); let mut file = std::fs::File::create(&instrumented_file).unwrap(); diff --git a/tests/tests.mjs b/tests/tests.mjs new file mode 100644 index 0000000..3c6637f --- /dev/null +++ b/tests/tests.mjs @@ -0,0 +1,74 @@ +import { create } from "../pkg/orchestrion_js.js"; +import * as assert from "node:assert"; + +const instrumentor = create([ + { + channelName: "up:constructor", + module: { name: "one", versionRange: ">=1", filePath: "index.js" }, + functionQuery: { className: "Up" }, + }, + { + channelName: "up:fetch", + module: { name: "one", versionRange: ">=1", filePath: "index.js" }, + functionQuery: { + className: "Up", + methodName: "fetch", + kind: "Sync", + }, + }, +]); + +const matchedTransforms = instrumentor.getTransformer( + "one", + "1.0.0", + "index.js", +); + +assert.ok(matchedTransforms); + +const output = matchedTransforms.transform( + "export class Up { constructor() {console.log('constructor')} fetch() {console.log('fetch')} }", + true, +); + +assert.strictEqual(output, `import { tracingChannel as tr_ch_apm_tracingChannel } from "diagnostics_channel"; +const tr_ch_apm$up:fetch = tr_ch_apm_tracingChannel("orchestrion:one:up:fetch"); +const tr_ch_apm$up:constructor = tr_ch_apm_tracingChannel("orchestrion:one:up:constructor"); +export class Up { + constructor(){ + const tr_ch_apm_ctx$up:constructor = { + arguments + }; + try { + if (tr_ch_apm$up:constructor.hasSubscribers) { + tr_ch_apm$up:constructor.start.publish(tr_ch_apm_ctx$up:constructor); + } + console.log('constructor'); + } catch (tr_ch_err) { + if (tr_ch_apm$up:constructor.hasSubscribers) { + tr_ch_apm_ctx$up:constructor.error = tr_ch_err; + try { + tr_ch_apm_ctx$up:constructor.self = this; + } catch (refErr) {} + tr_ch_apm$up:constructor.error.publish(tr_ch_apm_ctx$up:constructor); + } + throw tr_ch_err; + } finally{ + if (tr_ch_apm$up:constructor.hasSubscribers) { + tr_ch_apm_ctx$up:constructor.self = this; + tr_ch_apm$up:constructor.end.publish(tr_ch_apm_ctx$up:constructor); + } + } + } + fetch() { + const traced = ()=>{ + console.log('fetch'); + }; + if (!tr_ch_apm$up:fetch.hasSubscribers) return traced(); + return tr_ch_apm$up:fetch.traceSync(traced, { + arguments, + self: this + }); + } +} +`);