<div style="text-align:center">
	<img src="https://gitee.com/nnxr/lakutata-component-code-runner/raw/master/assets/lakutata-banner.png" />
</div>

[![NPM Version](https://img.shields.io/npm/v/@lakutata-component/code-runner?color=informational&style=flat-square)](https://npmjs.org/package/@lakutata-component/code-runner)
[![NODE Version](https://img.shields.io/node/v/@lakutata-component/code-runner?color=informational&style=flat-square)](https://npmjs.org/package/@lakutata-component/code-runner)
[![Known Vulnerabilities](https://snyk.io/test/npm/@lakutata-component/code-runner/badge.svg?style=flat-square)](https://snyk.io/test/npm/@lakutata-component/code-runner)
[![NPM Download](https://img.shields.io/npm/dm/@lakutata-component/code-runner?style=flat-square)](https://npmjs.org/package/@lakutata-component/code-runner)

## Features

- Support Typescript code snap
- Compile & run code safely
- Support auto import packages
- Support runtime inject environments
- Support individual fs space for each running script
- Support runtime script access defined interfaces for outside function invoking

## Quickstart

```typescript
import {createApp, Validator} from '@lakutata/core'
import {CodeRunnerComponent, SourceCodeObject} from '@lakutata-component/code-runner'
import path from 'path'
import os from 'os'
import fs from 'fs'

createApp({
    id: 'test.app',
    name: 'tester',
    components: {
        test: {
            class: CodeRunnerComponent,
            //Set temp directory path. default is os.tmpdir()
            tmpdir: os.tmpdir(),
            //Npm registry for module fetching, default is https://registry.npmmirror.com/
            moduleRegistry: 'https://registry.npmmirror.com/',
            //The directory for cache & extract modules fetched from registry
            extractModulesPath: path.resolve(__dirname, '../../tmpModules'),
            //Preset modules' name for script compile
            presetModules: [],
            //Timeout for each script
            timeout: 60000,
            //Global output schema, the finalized value of scripts' returned must be match this schema
            outputSchema: Validator.Object(Validator.Number),
            //Code Runtime Interface, this function return an object that can be invoked by running script
            CRI: (runCodeId: string, app: Application) => {
                console.log('runCodeId:',runCodeId)
                return {
                    random: async (a: number) => {
                        return (a + 1) * 1024 * Math.random()
                    }
                }
            },
            //File system settings (Each script has their own runtime virtual file system)
            //If this setting not set, script will always create new virtual file system while their running
            fileSystem: {
                sizeLimit: '1MB',
                write: async (runCodeId: string, fileSystemBinary: Buffer) => {
                    if (!fs.existsSync(path.resolve(__dirname, '../../tmpFileSystem'))) {
                        fs.mkdirSync(path.resolve(__dirname, '../../tmpFileSystem'), {recursive: true})
                    }
                    fs.writeFileSync(path.resolve(__dirname, `../../tmpFileSystem/${runCodeId}`), fileSystemBinary, {flag: 'w+'})
                },
                read: async (runCodeId: string) => {
                    if (!fs.existsSync(path.resolve(__dirname, '../../tmpFileSystem'))) {
                        fs.mkdirSync(path.resolve(__dirname, '../../tmpFileSystem'), {recursive: true})
                    }
                    if (fs.existsSync(path.resolve(__dirname, `../../tmpFileSystem/${runCodeId}`))) {
                        return fs.readFileSync(path.resolve(__dirname, `../../tmpFileSystem/${runCodeId}`))
                    } else {
                        return undefined
                    }
                }
            },
            //Concurrent settings for script vm
            concurrent: {
                min: os.cpus().length / 2,
                max: os.cpus().length
            },
            //IPC sock file path, usually it can be generated automatically
            ipcSockPath: path.resolve(os.tmpdir(), './ipc.sock')
        }
    }
}).then(async app => {
    const testComponent: CodeRunnerComponent = app.Components.get<CodeRunnerComponent>('test')
    //Declare source code object
    const sourceCodeObject: SourceCodeObject = {
        environments: {
            //Declare environments (JSONSchema)
        },
        //Source code
        source: `
        const rdm = () => {
            return Math.round(Math.random() * 10000)
        };
        const fs = require('fs');
        const path = require('path');
        const loki = require('lokijs@1.5.12');
        let users;
        const db = new loki('/example.db', {
            autoload: true,
            autoloadCallback: () => {
                users = db.getCollection('users') || db.addCollection('users', {indices: ['email']})
                for (let i = 0; i < 100; i++) {
                    users.insert({name: 'odin' + Date.now(), email: 'odin.soap@lokijs.org', age: rdm()})
                    users.insert({name: 'thor' + Date.now(), email: 'thor.soap@lokijs.org', age: rdm()})
                    users.insert({name: 'stan' + Date.now(), email: 'stan.soap@lokijs.org', age: rdm()})
                    users.insert({name: 'oliver' + Date.now(), email: 'oliver.soap@lokijs.org', age: rdm()})
                    users.insert({name: 'hector' + Date.now(), email: 'hector.soap@lokijs.org', age: rdm()})
                    users.insert({name: 'achilles' + Date.now(), email: 'achilles.soap@lokijs.org', age: rdm()})
                    db.save()
                }
                console.log(users.count())
            },
            autosave: true,
            autosaveInterval: 4000
        });
        let counter: number;
        const testFilePath = path.resolve(__dirname, './test.txt');
        if (!fs.existsSync(testFilePath)) {
            fs.writeFileSync(testFilePath, '1', {flag: 'w'});
            counter = 1;
        } else {
            const strCounter = fs.readFileSync(testFilePath).toString();
            counter = parseInt(strCounter) + 1;
            fs.writeFileSync(testFilePath, counter.toString(), {flag: 'w'});
        }
        const nerdamer = require('nerdamer/all.min');
        const res = await this.CRI.random(123);
        return {a: 1, b: res, c: parseFloat(nerdamer('cos(x)').evaluate({x: this.ENV.c}).text()), d: counter};
            `,
        //Declare runtime code output data schema (JSONSchema)
        schema: {
            type: 'object',
            properties: {
                a: {type: 'number'},
                b: {type: 'number'}
            },
            required: ['a', 'b']
        }
    }
    //Compile source code object
    const result = await testComponent.compile(sourceCodeObject)
    console.log('Compile Success')
    //Run compiled code, and set code id
    const runCodeObject = Object.assign(result, {
        id: 'TEST_ID_1234567890'
    })
    const start = Date.now()
    const res = await testComponent.run(runCodeObject, {c: Math.round(Math.random() * 1000)})
    //Output result
    console.log(JSON.stringify(res, null, 2), Date.now() - start)
}).catch(e => {
    console.error(e)
    process.exit(1)
})
```

> @lakutata/core required.

## How to Contribute

Please let us know how can we help. Do check out [issues](https://gitee.com/nnxr/lakutata-component-code-runner/issues) for
bug reports or suggestions first.

## License

[MIT](LICENSE)
