DelSkayn / rquickjs

High level bindings to the quickjs javascript engine
MIT License
431 stars 58 forks source link

How to use a dynamic declared module whih exported function correctly? #322

Open greenhat616 opened 2 weeks ago

greenhat616 commented 2 weeks ago

We want to provide user a script feature, allowing to modify the config by user.

Thanks to ESM, it's easy to add tool modules (with methods for asynchronous and synchronous operations), and users can use them directly with an import statement and expose an asynchronous default method to handle configuration.

Here is the ideal user module type signature:

export default async function main(config: Record<string, any>): Promise<Record<string, any>>
// Or
export default function main(config: Record<string, any>): Record<string, any>

Now, we try to impl this case by the code like below.

use anyhow::{anyhow, Context};
use rquickjs::{
    async_with,
    loader::{BuiltinResolver, ScriptLoader},
    AsyncContext, AsyncRuntime, CatchResultExt, Module,
};
use serde_yaml::Mapping;

pub async fn process(script: &str, input: Mapping) -> Result<Mapping, anyhow::Error> {
    // prepare runtime
    let runtime = AsyncRuntime::new().context("failed to create runtime")?;
    let resolver = (
        BuiltinResolver::default(), // .with_module(path)
                                    // FileResolver::default().with_path(app_path),
    );
    let loader = ScriptLoader::default();
    runtime.set_loader(resolver, loader).await;

    // run script
    let ctx = AsyncContext::full(&runtime)
        .await
        .context("failed to get runtime context")?;
    let config = serde_json::to_string(&input).context("failed to serialize input")?;
    let result = async_with!(ctx => |ctx| {
        let user_module = format!(
            "{script};
            let config = JSON.parse('{config}');
            export let _processed_config = await main(config);"
        );
        println!("user_module: {}", user_module);
        Module::declare(ctx.clone(), "user_script", user_module)
            .catch(&ctx)
            .map_err(|e|
                anyhow!("failed to define user script module: {:?}", e)
            )?;
        let promises = Module::evaluate(
            ctx.clone(),
            "process",
            r#"import { _processed_config } from "user_script";
            globalThis.final_result = JSON.stringify(_processed_config);
            "#
        )
            .catch(&ctx)
            .map_err(|e|
                anyhow!("failed to evaluate user script: {:?}", e)
            )?;
        promises
            .into_future::<()>()
            .await
            .catch(&ctx)
            .map_err(|e|
                anyhow!("failed to wait for user script to finish: {:?}", e)
            )?;
        let final_result = ctx.globals()
            .get::<_, rquickjs::String>("final_result")
            .catch(&ctx)
            .map_err(|e|
                anyhow!("failed to get final result: {:?}", e)
            )?
            .to_string()
            .context("failed to convert final result to string")?;
        let output: Mapping = serde_json::from_str(&final_result)?;
        Ok::<_, anyhow::Error>(output)
    })
    .await?;
    Ok(result)
}

mod test {
    #[test]
    fn test_process() {
        let mapping = serde_yaml::from_str(
            r#"
        rules:
            - 111
            - 222
        tun:
            enable: false
        dns:
            enable: false
        "#,
        )
        .unwrap();
        let script = r#"
        export default async function main(config) {
            if (Array.isArray(config.rules)) {
                config.rules = [...config.rules, "add"];
            }
            config.proxies = ["111"];
            return config;
        }"#;
        tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap()
            .block_on(async move {
                let mapping = crate::process(script, mapping).await.unwrap();
                assert_eq!(
                    mapping["rules"],
                    serde_yaml::Value::Sequence(vec![
                        serde_yaml::Value::String("111".to_string()),
                        serde_yaml::Value::String("222".to_string()),
                        serde_yaml::Value::String("add".to_string()),
                    ])
                );
                assert_eq!(
                    mapping["proxies"],
                    serde_yaml::Value::Sequence(
                        vec![serde_yaml::Value::String("111".to_string()),]
                    )
                );
            });
    }
}

When we passed the function into module declared, we always got this error:

failed to evaluate user script: Exception(Exception { message: Some("Error resolving module 'user_script' from 'process'"), file: None, line: Some(-1), column: Some(-1), stack: Some("") })

I have no idea, so i had to create this issue for help.

This is the reproduce repo: https://github.com/greenhat616/rquickjs-module-test