Skip to content

Commit 463790a

Browse files
committed
init
0 parents  commit 463790a

File tree

14 files changed

+3835
-0
lines changed

14 files changed

+3835
-0
lines changed

.gitignore

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
2+
# Created by https://www.gitignore.io/api/node,visualstudiocode
3+
# Edit at https://www.gitignore.io/?templates=node,visualstudiocode
4+
5+
### Node ###
6+
# Logs
7+
logs
8+
*.log
9+
npm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*
12+
lerna-debug.log*
13+
14+
# Diagnostic reports (https://nodejs.org/api/report.html)
15+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16+
17+
# Runtime data
18+
pids
19+
*.pid
20+
*.seed
21+
*.pid.lock
22+
23+
# Directory for instrumented libs generated by jscoverage/JSCover
24+
lib-cov
25+
26+
# Coverage directory used by tools like istanbul
27+
coverage
28+
*.lcov
29+
30+
# nyc test coverage
31+
.nyc_output
32+
33+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34+
.grunt
35+
36+
# Bower dependency directory (https://bower.io/)
37+
bower_components
38+
39+
# node-waf configuration
40+
.lock-wscript
41+
42+
# Compiled binary addons (https://nodejs.org/api/addons.html)
43+
build/Release
44+
45+
# Dependency directories
46+
node_modules/
47+
jspm_packages/
48+
49+
# TypeScript v1 declaration files
50+
typings/
51+
52+
# TypeScript cache
53+
*.tsbuildinfo
54+
55+
# Optional npm cache directory
56+
.npm
57+
58+
# Optional eslint cache
59+
.eslintcache
60+
61+
# Optional REPL history
62+
.node_repl_history
63+
64+
# Output of 'npm pack'
65+
*.tgz
66+
67+
# Yarn Integrity file
68+
.yarn-integrity
69+
70+
# dotenv environment variables file
71+
.env
72+
.env.test
73+
74+
# parcel-bundler cache (https://parceljs.org/)
75+
.cache
76+
77+
# next.js build output
78+
.next
79+
80+
# nuxt.js build output
81+
.nuxt
82+
83+
# vuepress build output
84+
.vuepress/dist
85+
86+
# Serverless directories
87+
.serverless/
88+
89+
# FuseBox cache
90+
.fusebox/
91+
92+
# DynamoDB Local files
93+
.dynamodb/
94+
95+
### VisualStudioCode ###
96+
.vscode/*
97+
!.vscode/settings.json
98+
!.vscode/tasks.json
99+
!.vscode/launch.json
100+
!.vscode/extensions.json
101+
102+
### VisualStudioCode Patch ###
103+
# Ignore all local history of files
104+
.history
105+
106+
# End of https://www.gitignore.io/api/node,visualstudiocode

R/launch.R

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# based on https://github.com/joshkatz/r-script/blob/master/R/launch.R
2+
suppressMessages(library(jsonlite))
3+
4+
run <- function(dataIn) {
5+
6+
# set up environment
7+
input <- unname(dataIn[[1]])
8+
.e <- as.environment(list(
9+
path = dataIn[[2]],
10+
out = modifyList(list(x = NULL, auto_unbox = T),
11+
dataIn[[3]], keep.null = T)
12+
))
13+
lockBinding(".e", environment())
14+
15+
# run source, capture output
16+
captured <- tryCatch(capture.output({
17+
temp <- source(.e$path, local = T)$value
18+
}), error = function(err) err)
19+
unlockBinding(".e", environment())
20+
21+
# process and return
22+
if (inherits(captured, "error")) {
23+
msg <- conditionMessage(captured)
24+
cat("Error in R script", .e$path, "\n", sQuote(msg), file = stderr())
25+
return(invisible(F))
26+
}
27+
.e$out$x <- if (is.null(temp)) {
28+
""
29+
} else {
30+
temp
31+
}
32+
do.call(toJSON, .e$out)
33+
34+
}
35+
36+
suppressWarnings(
37+
run(fromJSON(Sys.getenv("input")))
38+
)

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# r-script
2+
3+
### In development, still a WIP
4+
5+
Module for running R scripts from node
6+
7+
Based on [r-script](https://github.com/joshkatz/r-script) by [joshkatz](https://github.com/joshkatz). Decided to re-write this project because I need similar functionality and that project seems unmaintained.
8+
9+
## Installation
10+
11+
R needs to already be installed, and [jsonlite](https://cran.r-project.org/web/packages/jsonlite/index.html) R library needs to be installed.
12+
13+
`yarn add @fridgerator/r-script` or `npm install @fridgerator/r-script`
14+
15+
## Usage
16+
17+
In an R file:
18+
19+
```R
20+
input[[1]] + input[[2]]
21+
```
22+
23+
In node:
24+
25+
```javascript
26+
const { R } = require('@fridgerator/r-script')
27+
28+
// optionally pass an environment object if Rscript is not in your system PATH
29+
// `process.env` will be used as default
30+
let r = new R('./add.R', {PATH: '/bin:/location/to/R/bin'})
31+
32+
// data is converted to a list variable `input` in the R script
33+
r.data(2, 3)
34+
35+
// call the script async
36+
r.call()
37+
.then(response => response === 5) // true
38+
.catch(e => console.log('error : ', e))
39+
```

dist/R/launch.R

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# based on https://github.com/joshkatz/r-script/blob/master/R/launch.R
2+
suppressMessages(library(jsonlite))
3+
4+
run <- function(dataIn) {
5+
6+
# set up environment
7+
input <- unname(dataIn[[1]])
8+
.e <- as.environment(list(
9+
path = dataIn[[2]],
10+
out = modifyList(list(x = NULL, auto_unbox = T),
11+
dataIn[[3]], keep.null = T)
12+
))
13+
lockBinding(".e", environment())
14+
15+
# run source, capture output
16+
captured <- tryCatch(capture.output({
17+
temp <- source(.e$path, local = T)$value
18+
}), error = function(err) err)
19+
unlockBinding(".e", environment())
20+
21+
# process and return
22+
if (inherits(captured, "error")) {
23+
msg <- conditionMessage(captured)
24+
cat("Error in R script", .e$path, "\n", sQuote(msg), file = stderr())
25+
return(invisible(F))
26+
}
27+
.e$out$x <- if (is.null(temp)) {
28+
""
29+
} else {
30+
temp
31+
}
32+
do.call(toJSON, .e$out)
33+
34+
}
35+
36+
suppressWarnings(
37+
run(fromJSON(Sys.getenv("input")))
38+
)

dist/bundle.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
interface R {
2+
data(...args: any[]): R;
3+
call(_opts?: R.Options): Promise<object>;
4+
callSync(_opts?: R.Options): object;
5+
}
6+
declare namespace R {
7+
interface Options {
8+
[key: string]: any;
9+
}
10+
}
11+
export = R;

package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@fridgerator/r-script",
3+
"version": "0.1.0",
4+
"description": "Module for running R scripts from node",
5+
"main": "./dist/bundle.js",
6+
"repository": "https://github.com/fridgerator/r-script",
7+
"author": "Nick Franken <[email protected]>",
8+
"license": "MIT",
9+
"scripts": {
10+
"build": "webpack --config webpack.config.js",
11+
"test": "mocha"
12+
},
13+
"files": [
14+
"dist",
15+
"src",
16+
"index.d.ts"
17+
],
18+
"devDependencies": {
19+
"@types/node": "^12.6.6",
20+
"chai": "^4.2.0",
21+
"copy-webpack-plugin": "^5.0.3",
22+
"mocha": "^6.1.4",
23+
"ts-loader": "^6.0.4",
24+
"typescript": "^3.5.3",
25+
"webpack": "^4.36.1",
26+
"webpack-cli": "^3.3.6"
27+
},
28+
"types": "index.d.ts"
29+
}

src/index.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { spawn, spawnSync } from 'child_process'
2+
3+
interface Options {
4+
[key: string]: any
5+
}
6+
7+
export class R {
8+
rData: Options
9+
path: string
10+
options: Options
11+
idCounter: number
12+
args: string[]
13+
14+
constructor(path: string, env?: Options) {
15+
let _env = env || process.env
16+
this.rData = {}
17+
this.path = path
18+
this.options = {
19+
env: Object.assign({}, {DIRNAME: __dirname}, _env),
20+
encoding: 'utf8'
21+
}
22+
this.idCounter = 0
23+
this.args = ['--vanilla', __dirname + '/R/launch.R']
24+
}
25+
26+
private setInputEnv (opts: Options): void {
27+
this.options.env.input = JSON.stringify([this.rData, this.path, opts])
28+
}
29+
30+
data (...args: any[]): R {
31+
for (let i = 0; i < args.length; i++)
32+
this.rData[++this.idCounter] = args[i]
33+
34+
return this
35+
}
36+
37+
call (_opts?: Options): Promise<object> {
38+
return new Promise((resolve: any, reject: any) => {
39+
let opts = _opts || {}
40+
this.setInputEnv(opts)
41+
let child = spawn('Rscript', this.args, this.options)
42+
let body = ""
43+
child.stderr.on('data', d => {
44+
let msg = d.toString()
45+
if (msg.includes('Warning message')) return
46+
reject(msg)
47+
})
48+
child.stdout.on('data', d => body += d)
49+
child.on('close', () => resolve(JSON.parse(body)))
50+
})
51+
}
52+
53+
callSync (_opts?: Options): object | undefined {
54+
let opts = _opts || {}
55+
this.setInputEnv(opts)
56+
let child = spawnSync('Rscript', this.args, this.options)
57+
if (child.stderr) {
58+
let msg = child.stderr.toString()
59+
if (!msg.includes('Warning message'))
60+
throw msg
61+
}
62+
if (child.stdout) {
63+
return JSON.parse(child.stdout.toString())
64+
}
65+
return undefined
66+
}
67+
}

test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const { expect } = require('chai')
2+
const { R } = require('./dist/bundle')
3+
4+
describe('R', () => {
5+
describe('#call', () => {
6+
it('should add two numbers', done => {
7+
let r = new R('./test_files/add.R')
8+
r.data(2, 3)
9+
r.call()
10+
.then(result => {
11+
expect(result).to.equal(5)
12+
done()
13+
})
14+
.catch(e => console.log('err : ', e))
15+
})
16+
17+
it('should return what was passed', done => {
18+
let r = new R('./test_files/return_immediate.R')
19+
r.data({stuff: [1, 2, 9999999]}, 'hello', 99.9)
20+
r.call()
21+
.then(result => {
22+
expect(result).to.deep.equal([{stuff: [1, 2, 9999999]}, 'hello', 99.9])
23+
done()
24+
})
25+
.catch(e => console.log('err : ', e))
26+
})
27+
})
28+
29+
describe('#callSync', () => {
30+
it('should add two numbers', () => {
31+
let r = new R('./test_files/add.R')
32+
r.data(2, 3)
33+
let result = r.callSync()
34+
expect(result).to.equal(5)
35+
})
36+
37+
it('should return what was passed', () => {
38+
let r = new R('./test_files/return_immediate.R')
39+
r.data({stuff: [1, 2, 9999999]}, 'hello', 99.9)
40+
let result = r.callSync()
41+
expect(result).to.deep.equal([{stuff: [1, 2, 9999999]}, 'hello', 99.9])
42+
})
43+
})
44+
})

test_files/add.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
input[[1]] + input[[2]]

0 commit comments

Comments
 (0)