Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
greenhat616 opened this issue Jun 15, 2024 · 0 comments
Open

Comments

@greenhat616
Copy link

greenhat616 commented Jun 15, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant