nodo 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f997b2c506f2cec9bfd235c68cb0db93fcb103fa7f9b40f03740ca27b59ffb4
4
+ data.tar.gz: 8a1ba6ae691ee2a992b2af5a127c43c70178cc403134db04a08f9597dd8588b3
5
+ SHA512:
6
+ metadata.gz: 77c7a96627569da1b4093eb0fbfb6ab94c0c7dae4e425b019d52e06183f098daac57b88f68924fbe117afc1a0cf8be3aec143fd8e6935c839ebd573dde5afd70
7
+ data.tar.gz: 8057351d0968d8bc357af3926b805f82ce6e05949ee54efb9df40293ba4faf0ce8d337550318de0114c7f9dc2df05958ea481dc35aa2981a0d95501c9cb18c64
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Matthias Grosser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ [![Gem Version](https://badge.fury.io/rb/nodo.svg)](http://badge.fury.io/rb/nodo)
2
+
3
+ # Nōdo – call Node.js from Ruby
4
+
5
+ `Nodo` provides a Ruby environment to interact with JavaScript running inside a Node process.
6
+
7
+ ノード means "node" in Japanese.
8
+
9
+ ## Why Nodo?
10
+
11
+ Nodo will dispatch all JS function calls to a single long-running Node process.
12
+
13
+ JavaScript code is run in a namespaced environment, where you can access your initialized
14
+ JS objects during sequential function calls without having to re-initialize them.
15
+
16
+ IPC is done via unix sockets, greatly improving performance over classic process/eval solutions.
17
+
18
+ ## Installation
19
+
20
+ In your Gemfile:
21
+
22
+ ```ruby
23
+ gem 'nodo'
24
+ ```
25
+
26
+ ### Node.js
27
+
28
+ Nodo requires a working installation of Node.js.
29
+
30
+ If the executable is located in your `PATH`, no configuration is required. Otherwise, the path to to binary can be set using:
31
+
32
+ ```ruby
33
+ Nodo.binary = '/usr/local/bin/node'
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ In Nodo, you define JS functions as you would define Ruby methods:
39
+
40
+ ```ruby
41
+ class Foo < Nodo::Core
42
+
43
+ function :say_hi, <<~JS
44
+ (name) => {
45
+ return `Hello ${name}!`;
46
+ }
47
+ JS
48
+
49
+ end
50
+
51
+ foo = Foo.new
52
+ foo.say_hi('Nodo')
53
+ => "Hello Nodo!"
54
+ ```
55
+
56
+ ### Using npm modules
57
+
58
+ Install your modules to `node_modules`:
59
+
60
+ ```shell
61
+ $ yarn add uuid
62
+ ```
63
+
64
+ Then `require` your dependencies:
65
+
66
+ ```ruby
67
+ class Bar < Nodo::Core
68
+ require :uuid
69
+
70
+ function :v4, <<~JS
71
+ () => {
72
+ return uuid.v4();
73
+ }
74
+ JS
75
+ end
76
+
77
+ bar = Bar.new
78
+ bar.v4 => "b305f5c4-db9a-4504-b0c3-4e097a5ec8b9"
79
+ ```
80
+
81
+ ### Aliasing requires
82
+
83
+ ```ruby
84
+ class FooBar < Nodo::Core
85
+ require commonjs: '@rollup/plugin-commonjs'
86
+ end
87
+ ```
88
+
89
+ ### Setting NODE_PATH
90
+
91
+ By default, `./node_modules` is used as the `NODE_PATH`.
92
+
93
+ To set a custom path:
94
+ ```ruby
95
+ Nodo.modules_root = 'path/to/node_modules'
96
+ ```
97
+
98
+ For Rails applications, it will be set to `vendor/node_modules`.
99
+ To use the Rails 6 default of putting `node_modules` to `RAILS_ROOT`:
100
+
101
+ ```ruby
102
+ # config/initializers/nodo.rb
103
+ Nodo.modules_root = Rails.root.join('node_modules')
104
+ ```
105
+
106
+ ### Defining JS constants
107
+
108
+ ```ruby
109
+ class BarFoo < Nodo::Core
110
+ const :HELLO, "World"
111
+ end
112
+ ```
113
+
114
+ ### Execute some custom JS during initialization
115
+
116
+ ```ruby
117
+ class BarFoo < Nodo::Core
118
+
119
+ script <<~JS
120
+ // some custom JS
121
+ // to be executed during initialization
122
+ JS
123
+ end
124
+ ```
125
+
126
+ ### Inheritance
127
+
128
+ Subclasses will inherit functions, constants, dependencies and scripts from their superclasses, while only functions can be overwritten.
@@ -0,0 +1,38 @@
1
+ require 'net/http'
2
+
3
+ module Nodo
4
+ class Client < Net::HTTP
5
+ UNIX_REGEXP = /\Aunix:\/\//i
6
+
7
+ def initialize(address, port = nil)
8
+ super(address, port)
9
+ case address
10
+ when UNIX_REGEXP
11
+ @socket_type = 'unix'
12
+ @socket_path = address.sub(UNIX_REGEXP, '')
13
+ # Host header is required for HTTP/1.1
14
+ @address = 'localhost'
15
+ @port = 80
16
+ else
17
+ @socket_type = 'inet'
18
+ end
19
+ end
20
+
21
+ def connect
22
+ if @socket_type == 'unix'
23
+ connect_unix
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def connect_unix
30
+ s = Timeout.timeout(@open_timeout) { UNIXSocket.open(@socket_path) }
31
+ @socket = Net::BufferedIO.new(s)
32
+ @socket.read_timeout = @read_timeout
33
+ @socket.continue_timeout = @continue_timeout
34
+ @socket.debug_output = @debug_output
35
+ on_connect
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ module Nodo
2
+ class Constant
3
+ attr_reader :name, :value
4
+
5
+ def initialize(name, value)
6
+ @name, @value = name, value
7
+ end
8
+
9
+ def to_js
10
+ "const #{name} = #{value.to_json};\n"
11
+ end
12
+ end
13
+ end
data/lib/nodo/core.rb ADDED
@@ -0,0 +1,209 @@
1
+ module Nodo
2
+ class Core
3
+ SOCKET_NAME = 'nodo.sock'
4
+ DEFINE_METHOD = '__nodo_define_class__'
5
+ TIMEOUT = 5
6
+ ARRAY_CLASS_ATTRIBUTES = %i[dependencies constants scripts].freeze
7
+ HASH_CLASS_ATTRIBUTES = %i[functions].freeze
8
+ CLASS_ATTRIBUTES = (ARRAY_CLASS_ATTRIBUTES + HASH_CLASS_ATTRIBUTES).freeze
9
+
10
+ @@node_pid = nil
11
+ @@tmpdir = nil
12
+ @@mutex = Mutex.new
13
+
14
+ class << self
15
+
16
+ attr_accessor :class_defined
17
+
18
+ def inherited(subclass)
19
+ CLASS_ATTRIBUTES.each do |attr|
20
+ subclass.send "#{attr}=", send(attr).dup
21
+ end
22
+ end
23
+
24
+ def instance
25
+ @instance ||= new
26
+ end
27
+
28
+ def class_defined?
29
+ !!class_defined
30
+ end
31
+
32
+ def clsid
33
+ name || "Class:0x#{object_id.to_s(0x10)}"
34
+ end
35
+
36
+ CLASS_ATTRIBUTES.each do |attr|
37
+ define_method "#{attr}=" do |value|
38
+ instance_variable_set :"@#{attr}", value
39
+ end
40
+ end
41
+
42
+ ARRAY_CLASS_ATTRIBUTES.each do |attr|
43
+ define_method "#{attr}" do
44
+ instance_variable_get(:"@#{attr}") || instance_variable_set(:"@#{attr}", [])
45
+ end
46
+ end
47
+
48
+ HASH_CLASS_ATTRIBUTES.each do |attr|
49
+ define_method "#{attr}" do
50
+ instance_variable_get(:"@#{attr}") || instance_variable_set(:"@#{attr}", {})
51
+ end
52
+ end
53
+
54
+ def require(*mods)
55
+ deps = mods.last.is_a?(Hash) ? mods.pop : {}
56
+ mods = mods.map { |m| [m, m] }.to_h
57
+ self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
58
+ end
59
+
60
+ def function(name, code)
61
+ self.functions = functions.merge(name => Function.new(name, code, caller.first))
62
+ define_method(name) { |*args| call_js_method(name, args) }
63
+ end
64
+
65
+ def const(name, value)
66
+ self.constants = constants + [Constant.new(name, value)]
67
+ end
68
+
69
+ def script(code)
70
+ self.scripts = scripts + [Script.new(code)]
71
+ end
72
+
73
+ def generate_core_code
74
+ <<~JS
75
+ global.nodo = require(#{nodo_js});
76
+
77
+ const socket = process.argv[1];
78
+ if (!socket) {
79
+ process.stderr.write('Socket path is required\\n');
80
+ process.exit(1);
81
+ }
82
+
83
+ const shutdown = () => {
84
+ nodo.core.close(() => { process.exit(0) });
85
+ };
86
+
87
+ process.on('SIGINT', shutdown);
88
+ process.on('SIGTERM', shutdown);
89
+
90
+ nodo.core.run(socket);
91
+ JS
92
+ end
93
+
94
+ def generate_class_code
95
+ <<~JS
96
+ (() => {
97
+ const __nodo_log = nodo.log;
98
+ const __nodo_klass__ = {};
99
+ #{dependencies.map(&:to_js).join}
100
+ #{constants.map(&:to_js).join}
101
+ #{functions.values.map(&:to_js).join}
102
+ #{scripts.map(&:to_js).join}
103
+ return __nodo_klass__;
104
+ })()
105
+ JS
106
+ end
107
+
108
+ protected
109
+
110
+ def finalize(pid, tmpdir)
111
+ proc do
112
+ Process.kill(:SIGTERM, pid)
113
+ Process.wait(pid)
114
+ FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def nodo_js
121
+ Pathname.new(__FILE__).dirname.join('nodo.js').to_s.to_json
122
+ end
123
+ end
124
+
125
+ def initialize
126
+ @@mutex.synchronize do
127
+ ensure_process_is_spawned
128
+ wait_for_socket
129
+ ensure_class_is_defined
130
+ end
131
+ end
132
+
133
+ def node_pid
134
+ @@node_pid
135
+ end
136
+
137
+ def tmpdir
138
+ @@tmpdir
139
+ end
140
+
141
+ def socket_path
142
+ tmpdir && tmpdir.join(SOCKET_NAME)
143
+ end
144
+
145
+ def clsid
146
+ self.class.clsid
147
+ end
148
+
149
+ def ensure_process_is_spawned
150
+ return if node_pid
151
+ spawn_process
152
+ end
153
+
154
+ def ensure_class_is_defined
155
+ return if self.class.class_defined?
156
+ call_js_method(DEFINE_METHOD, self.class.generate_class_code)
157
+ self.class.class_defined = true
158
+ # rescue => e
159
+ # raise Error, e.message
160
+ end
161
+
162
+ def spawn_process
163
+ @@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
164
+ env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
165
+ @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s)
166
+ ObjectSpace.define_finalizer(self, self.class.send(:finalize, node_pid, tmpdir))
167
+ end
168
+
169
+ def wait_for_socket
170
+ start = Time.now
171
+ until socket_path.exist?
172
+ raise TimeoutError, "socket #{socket_path} not found" if Time.now - start > TIMEOUT
173
+ sleep(0.2)
174
+ end
175
+ end
176
+
177
+ def call_js_method(method, args)
178
+ raise CallError, 'Node process not ready' unless node_pid
179
+ raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || method == DEFINE_METHOD
180
+ function = self.class.functions[method]
181
+ raise NameError, "undefined function `#{method}' for #{self.class}" unless function || method == DEFINE_METHOD
182
+ request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json')
183
+ request.body = JSON.dump(args)
184
+ client = Client.new("unix://#{socket_path}")
185
+ response = client.request(request)
186
+ if response.is_a?(Net::HTTPOK)
187
+ parse_response(response)
188
+ else
189
+ handle_error(response, function)
190
+ end
191
+ rescue Errno::EPIPE, IOError
192
+ # TODO: restart or something? If this happens the process is completely broken
193
+ raise Error, 'Node process failed'
194
+ end
195
+
196
+ def handle_error(response, function)
197
+ if response.body
198
+ result = parse_response(response)
199
+ raise JavaScriptError.new(result['error'], function) if result.is_a?(Hash) && result.key?('error')
200
+ end
201
+ raise CallError, "Node returned #{response.code}"
202
+ end
203
+
204
+ def parse_response(response)
205
+ JSON.parse(response.body.force_encoding('UTF-8'))
206
+ end
207
+
208
+ end
209
+ end
@@ -0,0 +1,13 @@
1
+ module Nodo
2
+ class Dependency
3
+ attr_reader :name, :package
4
+
5
+ def initialize(name, package)
6
+ @name, @package = name, package
7
+ end
8
+
9
+ def to_js
10
+ "const #{name} = require(#{package.to_json});\n"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ module Nodo
2
+ class Error < StandardError; end
3
+ class TimeoutError < Error; end
4
+ class CallError < Error; end
5
+
6
+ class JavaScriptError < Error
7
+ attr_reader :attributes
8
+
9
+ def initialize(attributes = {}, function = nil)
10
+ @attributes = attributes || {}
11
+ if backtrace = generate_backtrace(attributes['stack'])
12
+ backtrace.unshift function.source_location if function && function.source_location
13
+ set_backtrace backtrace
14
+ end
15
+ @message = generate_message
16
+ end
17
+
18
+ def to_s
19
+ @message
20
+ end
21
+
22
+ private
23
+
24
+ # "filename:lineNo: in `method''' or “filename:lineNo.''
25
+
26
+ def generate_backtrace(stack)
27
+ backtrace = []
28
+ if stack and lines = stack.split("\n")
29
+ lines.shift
30
+ lines.each do |line|
31
+ if match = line.match(/\A *at (?<call>.+) \((?<src>.*):(?<line>\d+):(?<column>\d+)\)/)
32
+ backtrace << "#{match[:src]}:#{match[:line]}:in `#{match[:call]}'"
33
+ end
34
+ end
35
+ end
36
+ backtrace unless backtrace.empty?
37
+ end
38
+
39
+ def generate_message
40
+ message = "#{attributes['message'] || 'Unknown error'}"
41
+ if loc = attributes['loc']
42
+ message << loc.inject(' in') { |s, (key, value)| s << " #{key}: #{value}" }
43
+ end
44
+ message
45
+ end
46
+ end
47
+
48
+ class DependencyError < JavaScriptError; end
49
+ end
@@ -0,0 +1,13 @@
1
+ module Nodo
2
+ class Function
3
+ attr_reader :name, :code, :source_location
4
+
5
+ def initialize(name, code, source_location)
6
+ @name, @code, @source_location = name, code, source_location
7
+ end
8
+
9
+ def to_js
10
+ "const #{name} = __nodo_klass__.#{name} = (#{code});\n"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ require 'rails/railtie'
2
+ require 'active_support'
3
+
4
+ class Nodo::Railtie < Rails::Railtie
5
+ initializer 'nodo' do |app|
6
+ Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module Nodo
2
+ class Script
3
+ attr_reader :code
4
+
5
+ def initialize(code)
6
+ @code = code
7
+ end
8
+
9
+ def to_js
10
+ "#{code}\n"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Nodo
2
+ VERSION = '1.5.0'
3
+ end
data/lib/nodo.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'pathname'
2
+ require 'json'
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ module Nodo
7
+ class << self
8
+ attr_accessor :modules_root, :env, :binary
9
+ end
10
+ self.modules_root = './node_modules'
11
+ self.env = {}
12
+ self.binary = 'node'
13
+ end
14
+
15
+ require_relative 'nodo/version'
16
+ require_relative 'nodo/errors'
17
+ require_relative 'nodo/dependency'
18
+ require_relative 'nodo/function'
19
+ require_relative 'nodo/script'
20
+ require_relative 'nodo/constant'
21
+ require_relative 'nodo/client'
22
+ require_relative 'nodo/core'
23
+
24
+ require_relative 'nodo/railtie' if defined?(Rails)
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nodo
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthias Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Fast Ruby bridge to run JavaScript inside a Node process
70
+ email:
71
+ - mtgrosser@gmx.net
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE
77
+ - README.md
78
+ - lib/nodo.rb
79
+ - lib/nodo/client.rb
80
+ - lib/nodo/constant.rb
81
+ - lib/nodo/core.rb
82
+ - lib/nodo/dependency.rb
83
+ - lib/nodo/errors.rb
84
+ - lib/nodo/function.rb
85
+ - lib/nodo/railtie.rb
86
+ - lib/nodo/script.rb
87
+ - lib/nodo/version.rb
88
+ homepage: https://github.com/mtgrosser/nodo
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 2.3.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.0.3
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Call Node.js from Ruby
111
+ test_files: []