hyper-console 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ require "hyperloop/console/version"
2
+ require 'hyper-store'
3
+ require 'hyper-operation'
4
+ require 'hyperloop/console/hyper_console'
5
+ require 'hyperloop/console/evaluate'
6
+ Hyperloop.import 'hyper-console'
7
+
8
+ if RUBY_ENGINE == 'opal'
9
+ require 'hyperloop/console/object_space'
10
+ require 'securerandom'
11
+ else
12
+ require 'hyperloop/console/engine'
13
+ Opal.append_path File.expand_path('../', __FILE__).untaint
14
+ end
@@ -0,0 +1,244 @@
1
+ class DebugConsole < Hyperloop::Component
2
+ param :application_window_id
3
+ param :context
4
+ param :title
5
+
6
+ state history: [], scope: :shared
7
+ state console_state: :waiting_for_code, scope: :shared
8
+
9
+ class << self
10
+ attr_accessor :application_window_id
11
+ attr_accessor :context
12
+
13
+ def console_id
14
+ @console_id ||=
15
+ (
16
+ `sessionStorage.getItem('Hyperloop::Console::ConsoleId')` ||
17
+ SecureRandom.uuid.tap { |id| `sessionStorage.setItem('Hyperloop::Console::ConsoleId', #{id})` }
18
+ )
19
+ end
20
+
21
+ def ready!(code, compiled_code)
22
+ @code_to_send = code
23
+ mutate.error_message nil
24
+ evaluate compiled_code if @code_to_send =~ /.\n$/
25
+ end
26
+
27
+ def evaluate(compiled_code)
28
+ @code_to_send = @code_to_send[0..-2]
29
+ mutate.history << {eval: @code_to_send}
30
+ mutate.console_state :sending
31
+ Evaluate.run target_id: application_window_id, sender_id: console_id, context: context, string: compiled_code
32
+ end
33
+ end
34
+
35
+ after_mount do
36
+ DebugConsole.application_window_id = params.application_window_id
37
+ DebugConsole.context = params.context
38
+ mutate.history(JSON.parse(`sessionStorage.getItem('Hyperloop::Console.history')` || '[]'))
39
+ `document.title = #{params.title}`
40
+ `window.scrollTo(window.scrollX, 99999)`
41
+
42
+ Response.on_dispatch do |p|
43
+ next unless p.target_id == DebugConsole.console_id
44
+ mutate.console_state :waiting_for_code
45
+ mutate.history << {p.kind => p.message}
46
+ `sessionStorage.setItem('Hyperloop::Console.history', #{state.history.to_json})`
47
+ end
48
+
49
+ end
50
+
51
+ after_update do
52
+ `window.scrollTo(window.scrollX, 99999)`
53
+ end
54
+
55
+ render(DIV, style: { height: '100vh' }) do
56
+ # the outer DIV is needed to set fill the window so mouse clicks anywhere
57
+ # will go to the last line of the editor
58
+ DIV(class: :card) do
59
+ state.history.each do |item|
60
+ CodeHistoryItem(item_type: item.first.first, value: item.first.last || '')
61
+ end
62
+ CodeEditor(history: state.history, context: params.context) unless state.console_state == :sending
63
+ end.on(:mouse_down) do |e|
64
+ CodeEditor.set_focus
65
+ e.prevent_default
66
+ end
67
+ end
68
+ end
69
+
70
+ class CodeMirror < Hyperloop::Component
71
+
72
+ attr_reader :editor
73
+
74
+ after_mount do
75
+ @editor = `CodeMirror(#{dom_node}, {
76
+ value: #{@code.to_s},
77
+ mode: 'text/x-ruby',
78
+ matchBrackets: true,
79
+ lineNumbers: false,
80
+ indentUnit: 2,
81
+ theme: 'github',
82
+ readOnly: #{!!@read_only}
83
+ });`
84
+ end
85
+
86
+ render(DIV)
87
+
88
+ end
89
+
90
+ class CodeHistoryItem < CodeMirror
91
+
92
+ param :item_type
93
+ param :value
94
+
95
+ def format
96
+ case params.item_type
97
+ when :eval
98
+ params.value
99
+ when :exception
100
+ format_lines("!!", params.value)
101
+ when :result
102
+ format_lines(">>", params.value)
103
+ else
104
+ format_lines("#{params.item_type}:", params.value)
105
+ end
106
+ end
107
+
108
+ def format_lines(prefix, value)
109
+ fill = prefix.gsub(/./, ' ')
110
+ lines = value.split("\n")
111
+ (["#{prefix} #{lines.shift}"]+lines.collect do |line|
112
+ "#{fill} #{line}"
113
+ end).join("\n")
114
+ end
115
+
116
+ def mark_response_lines
117
+ return if params.item_type == :eval
118
+ lines = params.value.split("\n")
119
+ padding = [:exception, :result].include?(params.item_type) ? 3 : 2+params.item_type.to_s.length
120
+ last_line = lines[-1] || ''
121
+ `#{@editor}.markText({line: 0, ch: 0}, {line: #{lines.count-1}, ch: #{last_line.length+padding}}, {className: 'response-line'})`
122
+ end
123
+
124
+ before_mount do
125
+ @code = format
126
+ @read_only = true
127
+ end
128
+
129
+ after_mount do
130
+ mark_response_lines
131
+ end
132
+
133
+ render do
134
+ DIV {}.on(:mouse_down) { |e| e.stop_propagation }
135
+ end
136
+
137
+ end
138
+
139
+ class CodeEditor < CodeMirror
140
+ param :history
141
+ param :context
142
+
143
+ class << self
144
+ attr_accessor :editor
145
+
146
+ def set_focus
147
+ editor.set_focus_at_end if editor
148
+ end
149
+ end
150
+
151
+ before_mount do
152
+ @history = params.history.collect { |item| item[:eval] }.compact + ['']
153
+ @history_pos = @history.length-1
154
+ end
155
+
156
+ after_mount do
157
+ `#{@editor}.on('change', #{lambda {on_change} })`
158
+ `#{@editor}.on('keydown', #{lambda { |cm, e| on_key_down(cm, e) } })`
159
+ `#{@editor}.focus()`
160
+ self.class.editor = self
161
+ end
162
+
163
+ before_unmount do
164
+ self.class.editor = nil
165
+ end
166
+
167
+ def move_history(n)
168
+ @history[@history_pos] = `#{@editor}.getValue()`
169
+ @history_pos += n
170
+ text = @history[@history_pos]
171
+ lines = text.split("\n")
172
+ `#{@editor}.setValue(#{text})`
173
+
174
+ after(0) do
175
+ `window.scrollTo(window.scrollX, 99999)`
176
+ `#{@editor}.setCursor({line: #{lines.count}, ch: #{(lines.last || '').length}})`
177
+ end
178
+ end
179
+
180
+ def set_focus_at_end
181
+ `#{@editor}.focus()`
182
+ lines = `#{@editor}.getValue()`.split("\n")
183
+ `#{@editor}.setCursor({line: #{lines.count}, ch: #{(lines.last || '').length}})`
184
+ end
185
+
186
+
187
+ def on_key_down(cm, e)
188
+ if `e.metaKey` || `e.ctrlKey`
189
+ return unless `e.keyCode` == 13
190
+ `#{@editor}.setValue(#{@editor}.getValue()+#{"\n"})`
191
+ #on_change
192
+ elsif `e.key` == 'ArrowUp'
193
+ move_history(-1) if @history_pos > 0
194
+ elsif `e.key` == 'ArrowDown'
195
+ move_history(1) if @history_pos < @history.length-1
196
+ end
197
+ end
198
+
199
+ def mark_error(message)
200
+ from, to, title = parse_error(message)
201
+ @error = `#{@editor}.markText(#{from.to_n}, #{to.to_n}, {className: 'syntax-error', title: title})`
202
+ end
203
+
204
+ def parse_error(message)
205
+ message = message.split("\n")
206
+ position_info = message[3].split(':')
207
+ last_line_number = `#{@editor}.lineCount()`
208
+ last_line = `#{@editor}.getLine(last_line_number-1)`
209
+ [
210
+ {line: 0, ch: 0},
211
+ {line: last_line_number, ch: last_line.length},
212
+ message[2].split(':(file)')[0]
213
+ ]
214
+ end
215
+
216
+ def clear_error
217
+ `#{@error}.clear()` if @error
218
+ @error = nil
219
+ end
220
+
221
+ def on_change
222
+ clear_error
223
+ code = `#{@editor}.getValue()`
224
+ if params.context.empty?
225
+ compiled_code = Opal.compile(code, irb: true)
226
+ else
227
+ compiled_code = Opal.compile("#{params.context}.instance_eval {#{code}}")
228
+ end
229
+ DebugConsole.ready!(code, compiled_code)
230
+ rescue Exception => e
231
+ mark_error(e.message)
232
+ end
233
+
234
+ render do
235
+ DIV {}.on(:mouse_down) { |e| e.stop_propagation }
236
+ end
237
+ end
238
+
239
+ Document.ready? do
240
+ mount_point = Element["[data-react-class='React.TopLevelRailsComponent']"]
241
+ mount_point.render do
242
+ DebugConsole(JSON.parse(mount_point.attr('data-react-props'))[:render_params])
243
+ end
244
+ end
@@ -0,0 +1,11 @@
1
+ module Hyperloop
2
+ module Console
3
+ module Rails
4
+ class Engine < ::Rails::Engine
5
+ initializer "static assets" do |app|
6
+ app.middleware.use ::ActionDispatch::Static, "#{root}/public"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ class Evaluate < Hyperloop::ServerOp
2
+ param acting_user: nil, nils: true
3
+ param :target_id
4
+ param :sender_id
5
+ param :context
6
+ param string: nil
7
+
8
+ dispatch_to { Hyperloop::Application }
9
+ end
10
+
11
+ class Response < Hyperloop::ServerOp
12
+ param acting_user: nil, nils: true
13
+ param :target_id
14
+ param :kind
15
+ param :message
16
+
17
+ dispatch_to { Hyperloop::Application }
18
+ end
@@ -0,0 +1,26 @@
1
+ require 'opal'
2
+ require 'browser/interval'
3
+ require 'browser/delay'
4
+ require 'opal-jquery'
5
+ #require 'reactrb/auto-import'
6
+ module Hyperloop
7
+ class Component
8
+ VERSION = "0.12.4"
9
+ end
10
+ end
11
+ require 'hyper-component'
12
+ require 'hyper-operation'
13
+ require 'hyper-store'
14
+ require 'react/top_level_render'
15
+ require 'opal/compiler'
16
+ require 'active_support'
17
+ require 'securerandom'
18
+ require 'hyperloop/console/evaluate'
19
+ require 'hyperloop/console/sources/codemirror'
20
+ require 'hyperloop/console/sources/jquery-2.1.4'
21
+ require 'hyperloop/console/sources/react'
22
+ require 'hyperloop/console/sources/react-dom'
23
+ require 'hyperloop/console/ruby'
24
+ require 'hyperloop/console/client_components'
25
+ #require 'action_cable'
26
+ require 'hyperloop/pusher'
@@ -0,0 +1,100 @@
1
+ if RUBY_ENGINE=='opal'
2
+ module Kernel
3
+ def console(title: nil, context: nil)
4
+ Hyperloop::Console.console(title: title, context: context)
5
+ end
6
+ end
7
+
8
+ module Hyperloop
9
+ module Console
10
+ def self.console_id
11
+ @console_id ||= (
12
+ `sessionStorage.getItem('Hyperloop::Console::MainWindowId')` ||
13
+ SecureRandom.uuid.tap { |id| `sessionStorage.setItem('Hyperloop::Console::MainWindowId', #{id})` }
14
+ )
15
+ end
16
+
17
+ def self.messenger(target_id, kind, m)
18
+ Response.run(target_id: target_id, kind: kind, message: m.gsub(/\n$/, ''))
19
+ end
20
+
21
+ def self.console(title: nil, context: nil)
22
+ title ||= context || 'Hyperloop Application Console'
23
+ title = `encodeURIComponent(#{title})`
24
+ context = `encodeURIComponent(#{context})`
25
+ location = "/hyperloop/hyperloop-debug-console/#{console_id}?context=#{context}&title=#{title}"
26
+ window_params = 'width=640,height=460,scrollbars=no,location=0'
27
+ `window.open(#{location}, #{"window#{title}"}, #{window_params}).focus()`
28
+ @__console_dispatcher__ ||= Evaluate.on_dispatch do |params|
29
+ if params.target_id == console_id
30
+ cwarn = `console.warn`
31
+ clog = `console.log`
32
+ cerror = `console.error`
33
+ cinfo = `console.info`
34
+ `console.warn = function(m) { #{messenger(params.sender_id, :warn, `m`)} }`
35
+ `console.log = function(m) { #{messenger(params.sender_id, :log, `m`)} }`
36
+ `console.error = function(m) { #{messenger(params.sender_id, :error, `m`)} }`
37
+ `console.info = function(m) { #{messenger(params.sender_id, :info, `m`)} }`
38
+ begin
39
+ message = `eval(#{params.string})`.inspect
40
+ Response.run(target_id: params.sender_id, kind: :result, message: message || '')
41
+ rescue Exception => e
42
+ Response.run(target_id: params.sender_id, kind: :exception, message: e.to_s)
43
+ ensure
44
+ `console.warn = cwarn`
45
+ `console.log = clog`
46
+ `console.error = cerror`
47
+ `console.info = cinfo`
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ self
53
+ end
54
+ `window.hyperconsole = function() { Opal.Opal.$console() }`
55
+ Hyperloop::Application::Boot.on_dispatch do
56
+ @console ||= console if `window.HyperloopConsoleAutoStart`
57
+ end
58
+ end
59
+ end
60
+ else
61
+ module Hyperloop
62
+ define_setting :console_auto_start, true
63
+
64
+ module Console
65
+ class Config
66
+ include React::IsomorphicHelpers
67
+ prerender_footer do |controller|
68
+ "<script type='text/javascript'>\n"\
69
+ "window.HyperloopConsoleAutoStart = #{!!Hyperloop.console_auto_start};\n"\
70
+ "</script>\n"
71
+ end
72
+ end
73
+ end
74
+
75
+ ::Hyperloop::Engine.routes.append do
76
+ Hyperloop.initialize_policies
77
+
78
+ HyperloopController.class_eval do
79
+ def opal_debug_console
80
+ console_params = { application_window_id: params[:application_window_id], context: params[:context], title: params[:title] }
81
+ render inline:
82
+ "<!DOCTYPE html>\n"\
83
+ "<html>\n"\
84
+ " <head>\n"\
85
+ " <title>Hyperloop Console Loading...</title>\n"\
86
+ " <%= csrf_meta_tags %>\n"\
87
+ " <%= stylesheet_link_tag 'hyper-console-client' %>\n"\
88
+ " <%= javascript_include_tag 'hyper-console-client.min.js' %>\n"\
89
+ " <%= javascript_include_tag 'action_cable' %>\n"\
90
+ " </head>\n"\
91
+ " <body>\n"\
92
+ " <%= react_component '::DebugConsole', #{console_params}, { prerender: false } %>\n"\
93
+ " </body>\n"\
94
+ "</html>\n"
95
+ end
96
+ end
97
+ match 'hyperloop-debug-console/:application_window_id', to: 'hyperloop#opal_debug_console', via: :get
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ class Object
2
+ def self.instance
3
+ ObjectSpace.objects(self).tap do |arr|
4
+ arr.define_singleton_method(:console) { |*args| method_missing(:console, *args) }
5
+ klass = self
6
+ arr.define_singleton_method(:method_missing) do |method, *args, &block|
7
+ if length > 1 && self[0].respond_to?(method)
8
+ raise "Ambiguous #{klass}.instance.#{method} application.\n"\
9
+ "There are #{length} instances of #{klass}.\n"\
10
+ "You can either apply #{method} to a specific instance using #{klass}.instance[x].#{method};\n"\
11
+ "or use an enumerator method like each or collect."
12
+ elsif length == 0
13
+ raise "There are no #{klass} instances."
14
+ else
15
+ self[0].send(method, *args, &block)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ module ObjectSpace
23
+ def self.objects(klass)
24
+ objs = []
25
+ matching_objs = []
26
+ %x{
27
+ var walk_the_object = function(js_obj) {
28
+ var keys = Object.keys(js_obj) //get all own property names of the object
29
+
30
+ keys.forEach( function ( key ) {
31
+ if ( key != '$$proto' ) {
32
+ var value = js_obj[ key ]; // get property value
33
+
34
+ // if the property value is an object...
35
+ if ( value && typeof value === 'object' ) {
36
+
37
+ // if we don't have this reference, and its an object of the class we want
38
+ if ( #{objs}.indexOf( value ) < 0 ) {
39
+ #{objs}.push( value ); // store the reference
40
+ if ( Object.keys(value).indexOf('$$id') >= 0 && value.$$class == #{klass} ) {
41
+ #{matching_objs}.push ( value )
42
+ }
43
+ walk_the_object(value) // traverse all its own properties
44
+ }
45
+ }
46
+ }
47
+ })
48
+ }
49
+ walk_the_object(window)
50
+ }
51
+ matching_objs
52
+ end
53
+ end