nodo 1.5.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: 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: []