node-runner 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ead50dfa95fa818eb046575996e0047ee234dedf0f0813c5cf6ee4f684677b47
4
+ data.tar.gz: fcd76591a0acd31bb3930e2fa73c743c71290c075046c297417c75382e2efe1b
5
+ SHA512:
6
+ metadata.gz: aa16d705dc422f545ee607c91cc2868d2c2cb5a301a722c2a9964cea5cca80d6051d4b72726af45a04d0c16c6b91e855ff25b01c4b68c84a06740eaa2050ce61
7
+ data.tar.gz: bee406b3703184445d677359429a8d68f75ba382cff78eb94484575d82a0f83d96d99a96d05c57d98becbfff73f1d43a72d3ba32a2735592897f43c720e48121
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ /vendor
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
16
+ Gemfile.lock
17
+ .bundle
18
+ .ruby-version
19
+ test_threads.rb
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rake", "~> 13.0"
4
+ gem "minitest"
5
+
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020-present Jared White and Bridgetown contributors
4
+ Portions copyright (c) 2015-2016 Sam Stephenson
5
+ Portions copyright (c) 2015-2016 Josh Peek
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # NodeRunner for Ruby
2
+
3
+ A simple way to execute Javascript in a Ruby context via Node. (Loosely based on the Node Runtime module from [ExecJS](https://github.com/rails/execjs).)
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/node-runner)](https://badge.fury.io/rb/node-runner)
6
+
7
+ ## Installation
8
+
9
+ Run this command to add this plugin to your project's Gemfile:
10
+
11
+ ```shell
12
+ $ bundle add node-runner
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Simply create a new `NodeRunner` object and pass in the Javascript code you wish to
18
+ execute:
19
+
20
+ ```ruby
21
+ runner = NodeRunner.new(
22
+ <<~NODE
23
+ const hello = (response) => {
24
+ return `Hello? ${response}!`
25
+ }
26
+ NODE
27
+ )
28
+ ```
29
+
30
+ Then call the function as if it were a genuine Ruby method:
31
+
32
+ ```ruby
33
+ runner.hello "Goodbye!"
34
+
35
+ # output: "Hello? Goodbye!"
36
+ ```
37
+
38
+ Under the hood, the data flowing in and out of the Javascript function is translated via JSON, so you'll need to stick to standard JSON-friendly data
39
+ values (strings, integers, arrays, hashes, etc.)
40
+
41
+ You can also use Node require statements in your Javascript:
42
+
43
+ ```ruby
44
+ runner = NodeRunner.new(
45
+ <<~NODE
46
+ const path = require("path")
47
+ const extname = (filename) => {
48
+ return path.extname(filename);
49
+ }
50
+ NODE
51
+ )
52
+
53
+ extname = runner.extname("README.md")
54
+
55
+ extname == ".md"
56
+
57
+ # output: true
58
+ ```
59
+
60
+ Multiple arguments for a function work, as do multiple function calls (aka
61
+ if you define `function_one` and `function_two` in your Javascript, you can call
62
+ `runner.function_one` or `runner.function_two` in Ruby).
63
+
64
+ ## Node Executor Options
65
+
66
+ If you need to customize which `node` binary is executed, or wish to use your
67
+ own wrapper JS to bootstrap the `node` runtime, you can pass a custom instance
68
+ of `NodeRunner::Executor` to `NodeRunner`:
69
+
70
+ ```ruby
71
+ NodeRunner.new "…", executor: NodeRunner::Executor.new(command: "/path/to/custom/node")
72
+ ```
73
+
74
+ `command` can be an array as well, if you want to attempt multiple paths until one is found. Inspect the `node-runner.rb` source code for more information on the available options.
75
+
76
+ ## Testing
77
+
78
+ * Run `bundle exec rake` to run the test suite.
79
+
80
+ ## Contributing
81
+
82
+ 1. Fork it (https://github.com/bridgetownrb/node-runner/fork)
83
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
84
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
85
+ 4. Push to the branch (`git push origin my-new-feature`)
86
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/test_*.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,172 @@
1
+ require "tmpdir"
2
+ require "open3"
3
+ require "json"
4
+
5
+ class NodeRunnerError < StandardError; end
6
+
7
+ class NodeRunner
8
+ def initialize(source = "", options = {})
9
+ @source = encode(source.strip)
10
+ @args = options[:args] || {}
11
+ @executor = options[:executor] || NodeRunner::Executor.new
12
+ @function_name = options[:function_name] || "main"
13
+ end
14
+
15
+ def output
16
+ exec
17
+ end
18
+
19
+ def method_missing(m, *args, &block)
20
+ @function_name = m
21
+ if block
22
+ @source = encode(block.call.strip)
23
+ end
24
+ @args = *args
25
+ exec
26
+ end
27
+
28
+ protected
29
+
30
+ def encode(string)
31
+ string.encode('UTF-8')
32
+ end
33
+
34
+ def exec
35
+ source = @executor.compile_source(@source, @args.to_json, @function_name)
36
+ tmpfile = write_to_tempfile(source)
37
+ filepath = tmpfile.path
38
+
39
+ begin
40
+ extract_result(@executor.exec(filepath), filepath)
41
+ ensure
42
+ File.unlink(tmpfile)
43
+ end
44
+ end
45
+
46
+ def create_tempfile(basename)
47
+ tmpfile = nil
48
+ Dir::Tmpname.create(basename) do |tmpname|
49
+ mode = File::WRONLY | File::CREAT | File::EXCL
50
+ tmpfile = File.open(tmpname, mode, 0600)
51
+ end
52
+ tmpfile
53
+ end
54
+
55
+ def write_to_tempfile(contents)
56
+ tmpfile = create_tempfile(['node_runner', 'js'])
57
+ tmpfile.write(contents)
58
+ tmpfile.close
59
+ tmpfile
60
+ end
61
+
62
+ def extract_result(output, filename)
63
+ status, value, stack = output.empty? ? [] : ::JSON.parse(output, create_additions: false)
64
+ if status == "ok"
65
+ value
66
+ else
67
+ stack ||= ""
68
+ real_filename = File.realpath(filename)
69
+ stack = stack.split("\n").map do |line|
70
+ line.sub(" at ", "")
71
+ .sub(real_filename, "node_runner")
72
+ .sub(filename, "node_runner")
73
+ .strip
74
+ end
75
+ stack.shift # first line is already part of the message (aka value)
76
+ error = NodeRunnerError.new(value)
77
+ error.set_backtrace(stack + caller)
78
+ raise error
79
+ end
80
+ end
81
+ end
82
+
83
+ class NodeRunner::Executor
84
+ attr_reader :name
85
+
86
+ def initialize(options = {})
87
+ @command = options[:command] || ['node']
88
+ @runner_path = options[:runner_path] || File.join(File.expand_path(__dir__), '/node_runner.js')
89
+ @encoding = options[:encoding] || "UTF-8"
90
+ @binary = nil
91
+
92
+ @popen_options = {}
93
+ @popen_options[:external_encoding] = @encoding if @encoding
94
+ @popen_options[:internal_encoding] = ::Encoding.default_internal || 'UTF-8'
95
+
96
+ if @runner_path
97
+ instance_eval generate_compile_method(@runner_path)
98
+ end
99
+ end
100
+
101
+ def exec(filename)
102
+ stdout, stderr, status = Open3.capture3("#{binary} #{filename}")
103
+ if status.success?
104
+ stdout
105
+ else
106
+ raise exec_runtime_error(stderr)
107
+ end
108
+ end
109
+
110
+ protected
111
+
112
+ def binary
113
+ @binary ||= which(@command)
114
+ end
115
+
116
+ def locate_executable(command)
117
+ commands = Array(command)
118
+
119
+ commands.find { |cmd|
120
+ if File.executable? cmd
121
+ cmd
122
+ else
123
+ path = ENV['PATH'].split(File::PATH_SEPARATOR).find { |p|
124
+ full_path = File.join(p, cmd)
125
+ File.executable?(full_path) && File.file?(full_path)
126
+ }
127
+ path && File.expand_path(cmd, path)
128
+ end
129
+ }
130
+ end
131
+
132
+ def generate_compile_method(path)
133
+ <<-RUBY
134
+ def compile_source(source, args, func)
135
+ <<-RUNNER
136
+ #{IO.read(path)}
137
+ RUNNER
138
+ end
139
+ RUBY
140
+ end
141
+
142
+ def encode_source(source)
143
+ encoded_source = encode_unicode_codepoints(source)
144
+ ::JSON.generate("(function(){ #{encoded_source} })()", quirks_mode: true)
145
+ end
146
+
147
+ def encode_unicode_codepoints(str)
148
+ str.gsub(/[\u0080-\uffff]/) do |ch|
149
+ "\\u%04x" % ch.codepoints.to_a
150
+ end
151
+ end
152
+
153
+ def exec_runtime_error(output)
154
+ error = RuntimeError.new(output)
155
+ lines = output.split("\n")
156
+ lineno = lines[0][/:(\d+)$/, 1] if lines[0]
157
+ lineno ||= 1
158
+ error.set_backtrace(["(node_runner):#{lineno}"] + caller)
159
+ error
160
+ end
161
+
162
+ def which(command)
163
+ Array(command).find do |name|
164
+ name, args = name.split(/\s+/, 2)
165
+ path = locate_executable(name)
166
+
167
+ next unless path
168
+
169
+ args ? "#{path} #{args}" : path
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,9 @@
1
+ #{source}
2
+
3
+ try {
4
+ const args = #{args}
5
+ const output = JSON.stringify(['ok', #{func}(...args), []])
6
+ process.stdout.write(output)
7
+ } catch (err) {
8
+ process.stdout.write(JSON.stringify(['err', '' + err, err.stack]))
9
+ }
data/lib/version.rb ADDED
@@ -0,0 +1,4 @@
1
+ class NodeRunner
2
+ VERSION = "1.0.0"
3
+ end
4
+
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "node-runner"
7
+ spec.version = NodeRunner::VERSION
8
+ spec.author = "Bridgetown Team"
9
+ spec.email = "maintainers@bridgetownrb.com"
10
+ spec.summary = "A simple way to execute Javascript in a Ruby context via Node"
11
+ spec.homepage = "https://github.com/bridgetownrb/node-runner"
12
+ spec.license = "MIT"
13
+
14
+ spec.required_ruby_version = ">= 2.5"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r!^(test|script|spec|features)/!) }
17
+ spec.require_paths = ["lib"]
18
+ end
19
+
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: node-runner
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bridgetown Team
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-30 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: maintainers@bridgetownrb.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - ".gitignore"
20
+ - Gemfile
21
+ - LICENSE.txt
22
+ - README.md
23
+ - Rakefile
24
+ - lib/node-runner.rb
25
+ - lib/node_runner.js
26
+ - lib/version.rb
27
+ - node-runner.gemspec
28
+ homepage: https://github.com/bridgetownrb/node-runner
29
+ licenses:
30
+ - MIT
31
+ metadata: {}
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.5'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 3.0.8
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: A simple way to execute Javascript in a Ruby context via Node
51
+ test_files: []