wunderbar 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/wunderbar/builder.rb +10 -3
- data/lib/wunderbar/react.rb +16 -177
- data/lib/wunderbar/render.rb +200 -0
- data/lib/wunderbar/underscore.rb +2 -1
- data/lib/wunderbar/vendor/vue-server.min.js +1 -0
- data/lib/wunderbar/vendor/vue.min.js +6 -0
- data/lib/wunderbar/version.rb +2 -2
- data/lib/wunderbar/vue.rb +40 -0
- data/wunderbar.gemspec +4 -4
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d0bf66bfd444cace4acde0057ba3bf0ac53f02f
|
4
|
+
data.tar.gz: 223ff899aa06573c0b4d53b6f79a2c23755a64de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6025587bdcf0cf572312da1006565ac289f4a0ec2615ace9f8fdea71c891544c461b13efef0a5ca1f30fb7a1c3d10bc154426beb539e9adfd1f2a66bcfe8df2c
|
7
|
+
data.tar.gz: 0d237270a67cbc952272be4b54b21af56646b447f8a91e1ff415fb5b9be5a26bd30191ffe9056e2f7c973b85b1376bf2b83f48a6cb0f327e54411fae45f3ae34
|
data/lib/wunderbar/builder.rb
CHANGED
@@ -171,15 +171,21 @@ module Wunderbar
|
|
171
171
|
end
|
172
172
|
|
173
173
|
def text! text
|
174
|
-
|
174
|
+
text = TextNode.new(text)
|
175
|
+
@node.children << text
|
176
|
+
text
|
175
177
|
end
|
176
178
|
|
177
179
|
def declare! *args
|
178
|
-
|
180
|
+
doctype = DocTypeNode.new(*args)
|
181
|
+
@node.children << doctype
|
182
|
+
doctype
|
179
183
|
end
|
180
184
|
|
181
185
|
def comment! text
|
182
|
-
|
186
|
+
comment = CommentNode.new(text)
|
187
|
+
@node.children << comment
|
188
|
+
comment
|
183
189
|
end
|
184
190
|
|
185
191
|
def indented_text!(text)
|
@@ -188,6 +194,7 @@ module Wunderbar
|
|
188
194
|
text.extend SpacedNode if @spaced
|
189
195
|
@node.children << text
|
190
196
|
@spaced = false
|
197
|
+
text
|
191
198
|
end
|
192
199
|
|
193
200
|
def target!
|
data/lib/wunderbar/react.rb
CHANGED
@@ -1,196 +1,35 @@
|
|
1
|
-
|
2
|
-
raise RuntimeError.new('Sinatra is a prereq for wunderbar/react')
|
3
|
-
# a parallel solution is possible for Rails, but that work hasn't
|
4
|
-
# been done yet
|
5
|
-
end
|
6
|
-
|
7
|
-
require 'wunderbar/sinatra'
|
8
|
-
require 'wunderbar/script'
|
1
|
+
require 'wunderbar/render'
|
9
2
|
require 'ruby2js/filter/react'
|
10
3
|
require 'execjs'
|
11
|
-
require 'nokogumbo'
|
12
4
|
|
13
5
|
react = File.expand_path('../vendor/react-with-addons.min.js', __FILE__)
|
14
|
-
Wunderbar::Asset.script name: 'react-min.js', file: react,
|
6
|
+
Wunderbar::Asset.script name: 'react-min.js', file: react, render: true
|
15
7
|
|
16
8
|
reactdom = File.expand_path('../vendor/react-dom.min.js', __FILE__)
|
17
|
-
Wunderbar::Asset.script name: 'react-dom.min.js', file: reactdom,
|
9
|
+
Wunderbar::Asset.script name: 'react-dom.min.js', file: reactdom, render: true,
|
18
10
|
server: File.expand_path('../vendor/react-dom-server.min.js', __FILE__)
|
19
11
|
|
20
|
-
class Wunderbar::
|
21
|
-
|
22
|
-
def self.convert(file)
|
23
|
-
cached = @@cached_scripts[file]
|
24
|
-
return cached if cached and cached.uptodate?
|
25
|
-
return nil unless File.exist? file
|
26
|
-
@@cached_scripts[file] = Ruby2JS.convert(File.read(file), file: file)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
class Wunderbar::ClientScriptNode < Wunderbar::ScriptNode
|
31
|
-
end
|
32
|
-
|
33
|
-
class Wunderbar::XmlMarkup
|
34
|
-
def render(container, &block)
|
35
|
-
csspath = Wunderbar::Node.parse_css_selector(container)
|
36
|
-
root = @node.root
|
37
|
-
|
38
|
-
# find the scripts and target on the page
|
39
|
-
scripts = root.search('script')
|
40
|
-
target = root.at(container)
|
41
|
-
|
42
|
-
# compute base
|
43
|
-
base = root.at('base')
|
44
|
-
base = base && base.attrs[:href]
|
45
|
-
base ||= @_scope.env['REQUEST_URI'][/.*\//]
|
46
|
-
|
47
|
-
_base = @_scope.env['HTTP_X_WUNDERBAR_BASE']
|
48
|
-
base = base[_base.length..-1] if _base and base.start_with? _base
|
49
|
-
|
50
|
-
if base == '..' or base.end_with? '/..'
|
51
|
-
base = (Pathname.new(@_scope.env['REQUEST_URI']) + '../' + base).to_s
|
52
|
-
end
|
53
|
-
|
54
|
-
script = @_scope.env['SCRIPT_NAME']
|
55
|
-
base = base[script.length..-1] if script and base.start_with? script
|
56
|
-
|
57
|
-
base = base[1..-1] if base.start_with? '/'
|
58
|
-
|
59
|
-
# compute client side container
|
60
|
-
element = "document.querySelector(#{container.inspect})"
|
61
|
-
if csspath.length == 1 and csspath[0].length == 1
|
62
|
-
value = csspath[0].values.first
|
63
|
-
case csspath[0].keys.first
|
64
|
-
when :id
|
65
|
-
element = "document.getElementById(#{value.inspect})"
|
66
|
-
when :class
|
67
|
-
value = value.join(' ')
|
68
|
-
element = "document.getElementsByClassName(#{value.inspect})[0]"
|
69
|
-
when :name
|
70
|
-
element = "document.getElementsByTagName(#{value.inspect})[0]"
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
# build client and server scripts
|
75
|
-
common = Ruby2JS.convert(block, scope: @_scope, react: true)
|
76
|
-
server = "ReactDOMServer.renderToString(#{common})"
|
77
|
-
client = "ReactDOM.render(#{common}, #{element})"
|
12
|
+
class Wunderbar::Render
|
13
|
+
RUBY2JS_OPTIONS = {react: true}
|
78
14
|
|
79
|
-
|
80
|
-
|
81
|
-
result = nil
|
82
|
-
next if Wunderbar::ClientScriptNode === script
|
83
|
-
|
84
|
-
if script.attrs[:src]
|
85
|
-
src = script.attrs[:src]
|
86
|
-
|
87
|
-
src = File.join(base, src) if not base.empty?
|
88
|
-
src.sub!(/\?.*$/, '') # strip queries (typically mtimes)
|
89
|
-
src.untaint
|
90
|
-
|
91
|
-
name = File.expand_path(src, @_scope.settings.public_folder.untaint)
|
92
|
-
name.untaint unless src.tainted?
|
93
|
-
if File.exist? name
|
94
|
-
result = File.read(name)
|
95
|
-
else
|
96
|
-
file = File.expand_path(src+'.rb', @_scope.settings.views.untaint)
|
97
|
-
result = Wunderbar::Asset.convert(file)
|
98
|
-
end
|
99
|
-
else
|
100
|
-
result = Ruby2JS.convert(script.block, binding: script.binding)
|
101
|
-
end
|
102
|
-
|
103
|
-
result
|
104
|
-
end
|
105
|
-
|
106
|
-
builder = Wunderbar::HtmlMarkup.new({})
|
107
|
-
begin
|
108
|
-
setup = []
|
109
|
-
Wunderbar::Asset.scripts.each do |script|
|
110
|
-
next unless script.options[:react]
|
111
|
-
setup += script.options[:react] if Array === script.options[:react]
|
112
|
-
|
113
|
-
if script.contents
|
114
|
-
scripts.unshift script.contents
|
115
|
-
elsif script.path
|
116
|
-
if script.path.start_with? '/'
|
117
|
-
path = (ENV['DOCUMENT_ROOT'] + script.path).untaint
|
118
|
-
else
|
119
|
-
path = File.expand_path(script.path, Wunderbar::Asset.root)
|
120
|
-
end
|
121
|
-
setup << File.read(script.options[:server] || path)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
# concatenate and execute scripts on server
|
126
|
-
scripts.unshift *setup.uniq
|
127
|
-
context = ExecJS.compile(scripts.compact.join(";\n"))
|
128
|
-
|
129
|
-
# insert results into target
|
130
|
-
nodes = builder._ { context.eval(server) }
|
131
|
-
nodes.each {|node| node.parent = target}
|
132
|
-
target.children += nodes
|
133
|
-
rescue ExecJS::ProgramError => e
|
134
|
-
Wunderbar.error e
|
135
|
-
target.children << builder._pre(e.message).node?
|
136
|
-
end
|
137
|
-
|
138
|
-
# add client side script
|
139
|
-
tag! 'script', Wunderbar::ClientScriptNode, client
|
15
|
+
def self.server(common)
|
16
|
+
"ReactDOMServer.renderToString(#{common})"
|
140
17
|
end
|
141
|
-
end
|
142
18
|
|
143
|
-
|
144
|
-
|
145
|
-
@etag ||= Digest::MD5.hexdigest(to_str)
|
19
|
+
def self.client(common, element, target)
|
20
|
+
"ReactDOM.render(#{common}, #{element})"
|
146
21
|
end
|
147
22
|
|
148
|
-
def
|
149
|
-
|
23
|
+
def self.eval(scripts, server)
|
24
|
+
context = ExecJS.compile(scripts.compact.join(";\n"))
|
25
|
+
context.eval(server)
|
26
|
+
rescue ExecJS::ProgramError => e
|
27
|
+
Wunderbar.error e
|
28
|
+
"<pre>" + e.message.gsub('&', '&').gsub('<', '<').gsub('>', '>') +
|
29
|
+
"</pre>"
|
150
30
|
end
|
151
31
|
end
|
152
32
|
|
153
|
-
get %r{/([-\w]+)\.js} do |script|
|
154
|
-
file = File.join(settings.views, "#{script}.js.rb")
|
155
|
-
begin
|
156
|
-
js = Wunderbar::Asset.convert(file)
|
157
|
-
pass unless js
|
158
|
-
rescue Exception => e
|
159
|
-
Wunderbar.error e.to_s
|
160
|
-
return [500, {'Content-type' => 'text/plain'}, "*** ERROR ***\n\n#{e}"]
|
161
|
-
end
|
162
|
-
|
163
|
-
response.headers['SourceMap'] = "#{script}.js.map"
|
164
|
-
|
165
|
-
etag js.etag
|
166
|
-
|
167
|
-
content_type 'application/javascript; charset=utf-8'
|
168
|
-
|
169
|
-
js.to_s
|
170
|
-
end
|
171
|
-
|
172
|
-
get %r{/((?:\w+\/)*[-\w]+)\.js.rb} do |script|
|
173
|
-
file = File.join(settings.views, "#{script}.js.rb")
|
174
|
-
pass unless File.exist? file
|
175
|
-
send_file file
|
176
|
-
end
|
177
|
-
|
178
|
-
get %r{/([-\w]+)\.js.map} do |script|
|
179
|
-
file = File.join(settings.views, "#{script}.js.rb")
|
180
|
-
js = Wunderbar::Asset.convert(file)
|
181
|
-
pass unless js
|
182
|
-
|
183
|
-
etag js.sourcemap_etag
|
184
|
-
|
185
|
-
sourcemap = js.sourcemap
|
186
|
-
|
187
|
-
content_type 'application/json;charset:utf8'
|
188
|
-
base = settings.views + '/'
|
189
|
-
sourcemap[:file] = sourcemap[:file].sub base, ''
|
190
|
-
sourcemap[:sources].map! {|source| source.sub base, ''}
|
191
|
-
JSON.pretty_generate sourcemap
|
192
|
-
end
|
193
|
-
|
194
33
|
# Monkeypatch to address https://github.com/sstephenson/execjs/pull/180
|
195
34
|
require 'execjs'
|
196
35
|
class ExecJS::ExternalRuntime::Context
|
@@ -0,0 +1,200 @@
|
|
1
|
+
unless defined? Sinatra
|
2
|
+
raise RuntimeError.new('Sinatra is a prereq for wunderbar/render')
|
3
|
+
# a parallel solution is possible for Rails, but that work hasn't
|
4
|
+
# been done yet
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'wunderbar/sinatra'
|
8
|
+
require 'wunderbar/script'
|
9
|
+
require 'nokogumbo'
|
10
|
+
|
11
|
+
class Wunderbar::Asset
|
12
|
+
@@cached_scripts = {}
|
13
|
+
def self.convert(file)
|
14
|
+
cached = @@cached_scripts[file]
|
15
|
+
return cached if cached and cached.uptodate?
|
16
|
+
return nil unless File.exist? file
|
17
|
+
@@cached_scripts[file] = Ruby2JS.convert(File.read(file), file: file)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Wunderbar::ClientScriptNode < Wunderbar::ScriptNode
|
22
|
+
end
|
23
|
+
|
24
|
+
class Wunderbar::XmlMarkup
|
25
|
+
def render(container, &block)
|
26
|
+
csspath = Wunderbar::Node.parse_css_selector(container)
|
27
|
+
root = @node.root
|
28
|
+
|
29
|
+
# find the scripts and target on the page
|
30
|
+
scripts = root.search('script')
|
31
|
+
target = root.at(container)
|
32
|
+
|
33
|
+
# compute base
|
34
|
+
base = root.at('base')
|
35
|
+
base = base && base.attrs[:href]
|
36
|
+
base ||= @_scope.env['REQUEST_URI'][/.*\//]
|
37
|
+
|
38
|
+
_base = @_scope.env['HTTP_X_WUNDERBAR_BASE']
|
39
|
+
base = base[_base.length..-1] if _base and base.start_with? _base
|
40
|
+
|
41
|
+
if base == '..' or base.end_with? '/..'
|
42
|
+
base = (Pathname.new(@_scope.env['REQUEST_URI']) + '../' + base).to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
script = @_scope.env['SCRIPT_NAME']
|
46
|
+
base = base[script.length..-1] if script and base.start_with? script
|
47
|
+
|
48
|
+
base = base[1..-1] if base.start_with? '/'
|
49
|
+
|
50
|
+
# compute client side container
|
51
|
+
element = "document.querySelector(#{container.inspect})"
|
52
|
+
if csspath.length == 1 and csspath[0].length == 1
|
53
|
+
value = csspath[0].values.first
|
54
|
+
case csspath[0].keys.first
|
55
|
+
when :id
|
56
|
+
element = "document.getElementById(#{value.inspect})"
|
57
|
+
when :class
|
58
|
+
value = value.join(' ')
|
59
|
+
element = "document.getElementsByClassName(#{value.inspect})[0]"
|
60
|
+
when :name
|
61
|
+
element = "document.getElementsByTagName(#{value.inspect})[0]"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# build client and server scripts
|
66
|
+
options = Wunderbar::Render::RUBY2JS_OPTIONS.merge(scope: @_scope)
|
67
|
+
common = Ruby2JS.convert(block, options)
|
68
|
+
server = Wunderbar::Render.server(common)
|
69
|
+
client = Wunderbar::Render.client(common, element, target)
|
70
|
+
|
71
|
+
# extract content of scripts
|
72
|
+
scripts.map! do |script|
|
73
|
+
result = nil
|
74
|
+
next if Wunderbar::ClientScriptNode === script
|
75
|
+
|
76
|
+
if script.attrs[:src]
|
77
|
+
src = script.attrs[:src]
|
78
|
+
|
79
|
+
src = File.join(base, src) if not base.empty?
|
80
|
+
src.sub!(/\?.*$/, '') # strip queries (typically mtimes)
|
81
|
+
src.untaint
|
82
|
+
|
83
|
+
name = File.expand_path(src, @_scope.settings.public_folder.untaint)
|
84
|
+
name.untaint unless src.tainted?
|
85
|
+
if File.exist? name
|
86
|
+
result = File.read(name)
|
87
|
+
else
|
88
|
+
file = File.expand_path(src+'.rb', @_scope.settings.views.untaint)
|
89
|
+
result = Wunderbar::Asset.convert(file)
|
90
|
+
end
|
91
|
+
else
|
92
|
+
result = Ruby2JS.convert(script.block, binding: script.binding)
|
93
|
+
end
|
94
|
+
|
95
|
+
result
|
96
|
+
end
|
97
|
+
|
98
|
+
builder = Wunderbar::HtmlMarkup.new({})
|
99
|
+
setup = []
|
100
|
+
requires = {}
|
101
|
+
browserify = false
|
102
|
+
Wunderbar::Asset.scripts.each do |script|
|
103
|
+
next unless script.options[:render]
|
104
|
+
setup += script.options[:render] if Array === script.options[:render]
|
105
|
+
requires.merge! script.options[:require] if script.options[:require]
|
106
|
+
browserify = true if script.options[:browserify]
|
107
|
+
|
108
|
+
if script.contents
|
109
|
+
scripts.unshift script.contents
|
110
|
+
elsif script.path
|
111
|
+
if script.path.start_with? '/'
|
112
|
+
path = (ENV['DOCUMENT_ROOT'] + script.path).untaint
|
113
|
+
else
|
114
|
+
path = File.expand_path(script.path, Wunderbar::Asset.root)
|
115
|
+
end
|
116
|
+
setup << File.read(script.options[:server] || path)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# concatenate and execute scripts on server
|
121
|
+
if browserify
|
122
|
+
setup << requires.map {|key, value| "#{key}=require(#{value.inspect})"}.
|
123
|
+
join(';')
|
124
|
+
end
|
125
|
+
scripts.unshift *setup.uniq
|
126
|
+
File.write('/home/rubys/tmp/scripts', scripts.join(";\n") + ";\n" + server)
|
127
|
+
html = Wunderbar::Render.eval(scripts, server)
|
128
|
+
|
129
|
+
# insert results into target
|
130
|
+
nodes = builder._ { html }
|
131
|
+
begin
|
132
|
+
nodes.each {|node| node.parent = target}
|
133
|
+
target.children += nodes
|
134
|
+
rescue => e
|
135
|
+
div = Wunderbar::Node.new('span',
|
136
|
+
style: 'background-color:#ff0; margin: 1em 0; padding: 1em; ' +
|
137
|
+
'border: 4px solid red; border-radius: 1em')
|
138
|
+
div.children << Wunderbar::Node.new('pre', e.to_s)
|
139
|
+
div.children << Wunderbar::Node.new('pre', e.backtrace)
|
140
|
+
div.children << Wunderbar::Node.new('pre', html)
|
141
|
+
div.children << Wunderbar::Node.new('pre', nodes.inspect)
|
142
|
+
div.children.each {|node| node.parent = div}
|
143
|
+
target.children << div
|
144
|
+
end
|
145
|
+
|
146
|
+
# add client side script
|
147
|
+
tag! 'script', Wunderbar::ClientScriptNode, client
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class Ruby2JS::Serializer
|
152
|
+
def etag
|
153
|
+
@etag ||= Digest::MD5.hexdigest(to_str)
|
154
|
+
end
|
155
|
+
|
156
|
+
def sourcemap_etag
|
157
|
+
@sourcemap_etag ||= Digest::MD5.hexdigest(sourcemap.inspect)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
get %r{/([-\w]+)\.js} do |script|
|
162
|
+
file = File.join(settings.views, "#{script}.js.rb")
|
163
|
+
begin
|
164
|
+
js = Wunderbar::Asset.convert(file)
|
165
|
+
pass unless js
|
166
|
+
rescue Exception => e
|
167
|
+
Wunderbar.error e.to_s
|
168
|
+
return [500, {'Content-type' => 'text/plain'}, "*** ERROR ***\n\n#{e}"]
|
169
|
+
end
|
170
|
+
|
171
|
+
response.headers['SourceMap'] = "#{script}.js.map"
|
172
|
+
|
173
|
+
etag js.etag
|
174
|
+
|
175
|
+
content_type 'application/javascript; charset=utf-8'
|
176
|
+
|
177
|
+
js.to_s
|
178
|
+
end
|
179
|
+
|
180
|
+
get %r{/((?:\w+\/)*[-\w]+)\.js.rb} do |script|
|
181
|
+
file = File.join(settings.views, "#{script}.js.rb")
|
182
|
+
pass unless File.exist? file
|
183
|
+
send_file file
|
184
|
+
end
|
185
|
+
|
186
|
+
get %r{/([-\w]+)\.js.map} do |script|
|
187
|
+
file = File.join(settings.views, "#{script}.js.rb")
|
188
|
+
js = Wunderbar::Asset.convert(file)
|
189
|
+
pass unless js
|
190
|
+
|
191
|
+
etag js.sourcemap_etag
|
192
|
+
|
193
|
+
sourcemap = js.sourcemap
|
194
|
+
|
195
|
+
content_type 'application/json;charset:utf8'
|
196
|
+
base = settings.views + '/'
|
197
|
+
sourcemap[:file] = sourcemap[:file].sub base, ''
|
198
|
+
sourcemap[:sources].map! {|source| source.sub base, ''}
|
199
|
+
JSON.pretty_generate sourcemap
|
200
|
+
end
|