phrender 0.0.1
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 +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +120 -0
- data/Rakefile +5 -0
- data/lib/phrender/ember_driver.rb +4 -0
- data/lib/phrender/logger.rb +55 -0
- data/lib/phrender/phantom_js_engine.rb +82 -0
- data/lib/phrender/phantom_js_session.rb +29 -0
- data/lib/phrender/rack_base.rb +50 -0
- data/lib/phrender/rack_middleware.rb +55 -0
- data/lib/phrender/rack_static.rb +45 -0
- data/lib/phrender/support/ember_driver.js +16 -0
- data/lib/phrender/support/phantom_bridge.js +108 -0
- data/lib/phrender/version.rb +3 -0
- data/lib/phrender.rb +10 -0
- data/lib/string/strip.rb +27 -0
- data/phrender.gemspec +31 -0
- data/spec/phrender/phantom_js_engine/app.js +15 -0
- data/spec/phrender/phantom_js_engine/index.html +6 -0
- data/spec/phrender/phantom_js_engine_spec.rb +41 -0
- data/spec/phrender/rack_middleware/assets/app.js +19 -0
- data/spec/phrender/rack_middleware/assets/phrender.html.erb +10 -0
- data/spec/phrender/rack_middleware_spec.rb +26 -0
- data/spec/phrender/rack_static/app.js +19 -0
- data/spec/phrender/rack_static/files/static.txt +1 -0
- data/spec/phrender/rack_static/phrender.html +0 -0
- data/spec/phrender/rack_static_spec.rb +24 -0
- data/spec/spec_helper.rb +14 -0
- metadata +210 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 092133a20cb9d29d3b2a8867224a38851c3dd891
|
4
|
+
data.tar.gz: 0a2f321d07614c3e4e744419c1df287d93588772
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d4d79647ef74036f80da6c17ea500fea90893da54989e90146947180a3f2a40231a0c38e4b75c6160f8841ae38d9ee714cc8feea1f138b3ac1c89225e5b7154e
|
7
|
+
data.tar.gz: 33e1fa532034a483a3a7e3342c41a66be0b12008be3be5a7fba43e0e4a8fb7002d0072f99b664812bb1b16f81e9e949db4f83ddef99744a1de5341c24419025d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 theScore Inc.
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# Phrender
|
2
|
+
|
3
|
+
A rack app and api to render javascript based websites using PhantomJS.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'phrender'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install phrender
|
18
|
+
|
19
|
+
Then install PhantomJS:
|
20
|
+
|
21
|
+
$ brew install phantomjs
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
*NOTE*: Ember users, there is a driver for ember. See the [Ember Driver](#Ember
|
26
|
+
Driver) section.
|
27
|
+
|
28
|
+
### For completely static javascript sites:
|
29
|
+
|
30
|
+
In your rackup file:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
require 'phrender'
|
34
|
+
|
35
|
+
# Available options are timeout, which gets passed on to PhantomJS. The asset
|
36
|
+
# root should reflect where your app is stored, as well as all images, fonts,
|
37
|
+
# css, and other assets.
|
38
|
+
phrender = Phrender::RackStatic.new("asset_root", options)
|
39
|
+
|
40
|
+
# The index file should link to no javascript on its own, or include any script
|
41
|
+
# tags.
|
42
|
+
phrender.index_file 'phrender.html'
|
43
|
+
|
44
|
+
# This is the main application. It may or may not also start up the app.
|
45
|
+
phrender.add_javascript_file '/assets/application.js'
|
46
|
+
|
47
|
+
# This is just raw javascript. Typically the app boot code.
|
48
|
+
phrender.add_javascript "MyApp.boot()"
|
49
|
+
|
50
|
+
# Rackup!
|
51
|
+
run phrender.rack_app
|
52
|
+
```
|
53
|
+
|
54
|
+
### For partially dynamic javascript sites:
|
55
|
+
|
56
|
+
Use this method if some other rack-based application is serving up your assets,
|
57
|
+
or your index.html file. This can be useful for development, and even in some
|
58
|
+
production cases.
|
59
|
+
|
60
|
+
In your rackup file:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require 'phrender'
|
64
|
+
require 'my_rack_app'
|
65
|
+
|
66
|
+
# This is what you provide
|
67
|
+
app = MyRackApp.new
|
68
|
+
|
69
|
+
# Available options are timeout and ssl, if, for some reason, your rack app is
|
70
|
+
# serving up SSL encrypted pages. Run `phantomjs --help` to see available
|
71
|
+
# options, or use something falsey to disable. Both options are passed to
|
72
|
+
# phantom.
|
73
|
+
phrender = Phrender::RackMiddleware.new(app, options)
|
74
|
+
|
75
|
+
# These options are the same as above, but instead of referencing actual files,
|
76
|
+
# the paths are the request paths to send to the upstream application server.
|
77
|
+
phrender.index_file 'phrender.html'
|
78
|
+
phrender.add_javascript_file '/assets/application.js'
|
79
|
+
phrender.add_javascript "MyApp.boot()"
|
80
|
+
|
81
|
+
# Rackup!
|
82
|
+
run phrender.rack_app
|
83
|
+
```
|
84
|
+
|
85
|
+
## Signalling Render Completion
|
86
|
+
|
87
|
+
Phrender will, after `timeout` milliseconds serve up whatever has been rendered.
|
88
|
+
However, performance is better if you signal to phrender that rendering is
|
89
|
+
complete, and content should be served up. To do this, your application needs to
|
90
|
+
log to the console:
|
91
|
+
|
92
|
+
```
|
93
|
+
console.log('-- PHRENDER COMPLETE --');
|
94
|
+
```
|
95
|
+
|
96
|
+
If you are using ember, there is already a mechanism to do this, using the ember
|
97
|
+
driver.
|
98
|
+
|
99
|
+
## Ember Driver
|
100
|
+
|
101
|
+
There is a driver provided for ember users that overrides `Ember.Route` to log
|
102
|
+
out to phrender, without harming your application. The driver is stored in
|
103
|
+
`lib/phrender/support/ember_driver.js`. This is how you include it:
|
104
|
+
|
105
|
+
```
|
106
|
+
phrender.add_javascript_file '/assets/application.js'
|
107
|
+
phrender.add_javascript_file :ember_driver
|
108
|
+
phrender.add_javascript "MyApp.boot()"
|
109
|
+
```
|
110
|
+
|
111
|
+
It's important to include the driver *after* ember and *before* you boot your
|
112
|
+
application.
|
113
|
+
|
114
|
+
## Contributing
|
115
|
+
|
116
|
+
1. Fork it
|
117
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
118
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
119
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
120
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
class Phrender::Logger
|
4
|
+
@print_color = !ENV.has_key?('DISABLE_COLOR')
|
5
|
+
|
6
|
+
class << self
|
7
|
+
MESSAGE_FORMAT = "%s: %s"
|
8
|
+
|
9
|
+
def log_json(json)
|
10
|
+
%w(console info error trace critical).each do |type|
|
11
|
+
if json.has_key? type
|
12
|
+
send type.to_sym, json[type]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def console(message)
|
18
|
+
log MESSAGE_FORMAT % [ apply_color("CONSOLE", :magenta), message ]
|
19
|
+
end
|
20
|
+
|
21
|
+
def info(message)
|
22
|
+
log MESSAGE_FORMAT % [ apply_color("INFO"), message ]
|
23
|
+
end
|
24
|
+
|
25
|
+
def error(message)
|
26
|
+
log MESSAGE_FORMAT % [ apply_color("ERROR", :red), message ]
|
27
|
+
end
|
28
|
+
|
29
|
+
def trace(message)
|
30
|
+
log MESSAGE_FORMAT % [ apply_color("TRACE", :cyan), message ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def critical(message)
|
34
|
+
log MESSAGE_FORMAT % [ apply_color("CRITICAL", :on_red), message ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def log(msg, color = nil)
|
38
|
+
message = "[%s] - %s" % [Time.now, msg]
|
39
|
+
$stdout.puts message
|
40
|
+
$stdout.flush
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def apply_color(message, color = nil)
|
46
|
+
if !color.nil? && @print_color
|
47
|
+
message.send(color.to_sym)
|
48
|
+
else
|
49
|
+
message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'phrender/logger'
|
2
|
+
require 'phrender/phantom_js_session'
|
3
|
+
|
4
|
+
require 'open3'
|
5
|
+
require 'multi_json'
|
6
|
+
|
7
|
+
class Phrender::PhantomJSEngine
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
# Apply defaults
|
11
|
+
opts = { :timeout => 10000, :ssl => false }.merge opts
|
12
|
+
|
13
|
+
@poll_interval = 0.1
|
14
|
+
@timeout = opts[:timeout] / 1000.0
|
15
|
+
@ssl_protocol = opts.delete :ssl
|
16
|
+
@logger = Phrender::Logger
|
17
|
+
|
18
|
+
phantom_program = File.expand_path '../support/phantom_bridge.js', __FILE__
|
19
|
+
|
20
|
+
MultiJson.use :json_gem
|
21
|
+
|
22
|
+
@boot_cmd = [
|
23
|
+
'phantomjs',
|
24
|
+
phantom_program,
|
25
|
+
"--ignore-ssl-errors=true"
|
26
|
+
]
|
27
|
+
@boot_cmd.push "--ssl-protocol=%s" % [ @ssl_protocol ] if @ssl_protocol
|
28
|
+
end
|
29
|
+
|
30
|
+
def render(html, javascript, url = nil)
|
31
|
+
command = app_cmd(html, javascript, url)
|
32
|
+
session = Phrender::PhantomJSSession.new command, @timeout
|
33
|
+
|
34
|
+
begin
|
35
|
+
sleep @poll_interval
|
36
|
+
parse_output(session)
|
37
|
+
end while !session.expired? && !session.rendered
|
38
|
+
|
39
|
+
# Clean up phantom
|
40
|
+
session.shutdown
|
41
|
+
|
42
|
+
# Feed something out the chain
|
43
|
+
if session.rendered
|
44
|
+
session.page
|
45
|
+
elsif session.expired?
|
46
|
+
@logger.critical "PhantomJS timed out. Likely a javascript execution error."
|
47
|
+
''
|
48
|
+
else
|
49
|
+
@logger.critical "Phantom terminated without expiring or returning anything. This is bad."
|
50
|
+
''
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def app_cmd(html, javascript, url)
|
55
|
+
program_options = { :html => html,
|
56
|
+
:javascript => javascript,
|
57
|
+
:url => url,
|
58
|
+
:timeout => @timeout }
|
59
|
+
encoded_options = MultiJson.dump(MultiJson.dump(program_options))
|
60
|
+
"%s %s" % [ @boot_cmd.join(' '), encoded_options ]
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def parse_output(session)
|
66
|
+
output = session.stdout.gets
|
67
|
+
begin
|
68
|
+
data = JSON.parse output
|
69
|
+
if [ 'error', 'trace', 'console' ].any? { |key| data.has_key? key }
|
70
|
+
@logger.log_json data
|
71
|
+
elsif data.has_key? 'page'
|
72
|
+
session.rendered = true
|
73
|
+
session.page = data['page']
|
74
|
+
end
|
75
|
+
session.rendered
|
76
|
+
rescue
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Phrender::PhantomJSSession
|
2
|
+
attr_accessor :stdin
|
3
|
+
attr_accessor :stdout
|
4
|
+
attr_accessor :stderr
|
5
|
+
attr_accessor :wait_thr
|
6
|
+
attr_accessor :rendered
|
7
|
+
attr_accessor :page
|
8
|
+
|
9
|
+
def initialize(cmd, timeout)
|
10
|
+
@stdin, @stdout, @stderr, @wait_thr = Open3.popen3(cmd)
|
11
|
+
@start_time = Time.now
|
12
|
+
@rendered = false
|
13
|
+
@timeout = timeout
|
14
|
+
end
|
15
|
+
|
16
|
+
def expired?
|
17
|
+
(Time.now - @start_time) >= @timeout
|
18
|
+
end
|
19
|
+
|
20
|
+
def shutdown
|
21
|
+
@stdin.close
|
22
|
+
@stdout.close
|
23
|
+
@stderr.close
|
24
|
+
begin
|
25
|
+
Process.kill("TERM", @wait_thr.pid)
|
26
|
+
rescue Errno::ESRCH
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Phrender::RackBase
|
2
|
+
class Proxy
|
3
|
+
class << self
|
4
|
+
attr_accessor :host
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
self.class.host.call(env, @app)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :index_file
|
17
|
+
|
18
|
+
def initialize(*args)
|
19
|
+
@javascript_paths = []
|
20
|
+
@raw_javascript = ''
|
21
|
+
Proxy.host = self
|
22
|
+
end
|
23
|
+
|
24
|
+
def rack_app
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def render(path, app)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env, app)
|
33
|
+
status, headers, body = app.call(env)
|
34
|
+
if status == 404
|
35
|
+
body = render(env['PATH_INFO'], app)
|
36
|
+
[ 200, { 'Content-Type' => 'text/html' }, body ]
|
37
|
+
else
|
38
|
+
[ status, headers, body ]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_javascript_file(path)
|
43
|
+
@javascript_paths.push path
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_javascript(code)
|
47
|
+
@raw_javascript << ';' + code
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'phrender/phantom_js_engine'
|
2
|
+
require 'phrender/rack_base'
|
3
|
+
|
4
|
+
require 'rack'
|
5
|
+
|
6
|
+
class Phrender::RackMiddleware < Phrender::RackBase
|
7
|
+
def initialize(backend, opts = {})
|
8
|
+
@phantom = Phrender::PhantomJSEngine.new(opts)
|
9
|
+
@backend = backend
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def rack_app
|
14
|
+
backend = @backend
|
15
|
+
@app ||= Rack::Builder.new do
|
16
|
+
use Proxy
|
17
|
+
run backend
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def render(path, app)
|
22
|
+
program = load_js(app)
|
23
|
+
html = load_html(app)
|
24
|
+
@phantom.render(html, program)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def load_html(app)
|
30
|
+
req = Rack::MockRequest.env_for('',
|
31
|
+
'PATH_INFO' => @index_file,
|
32
|
+
'REQUEST_METHOD' => 'GET'
|
33
|
+
)
|
34
|
+
status, headers, body = app.call(req)
|
35
|
+
body
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_js(app)
|
39
|
+
js_from_files = @javascript_paths.map do |path|
|
40
|
+
if path == :ember_driver
|
41
|
+
Phrender::EMBER_DRIVER
|
42
|
+
else
|
43
|
+
req = Rack::MockRequest.env_for('',
|
44
|
+
'PATH_INFO' => path,
|
45
|
+
'REQUEST_METHOD' => 'GET'
|
46
|
+
)
|
47
|
+
status, headers, body = app.call(req)
|
48
|
+
body
|
49
|
+
end
|
50
|
+
end.join(';')
|
51
|
+
program = js_from_files + @raw_javascript
|
52
|
+
program
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'phrender/phantom_js_engine'
|
2
|
+
require 'phrender/rack_base'
|
3
|
+
|
4
|
+
require 'rack'
|
5
|
+
|
6
|
+
class Phrender::RackStatic < Phrender::RackBase
|
7
|
+
def initialize(root_directory, opts = {})
|
8
|
+
@phantom = Phrender::PhantomJSEngine.new(opts)
|
9
|
+
@root_directory = root_directory
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def rack_app
|
14
|
+
static_directory = @root_directory
|
15
|
+
@app ||= Rack::Builder.new do
|
16
|
+
use Proxy
|
17
|
+
run Rack::File.new(static_directory)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def render(path, app)
|
22
|
+
program = load_js(app)
|
23
|
+
html = load_html(app)
|
24
|
+
@phantom.render(html, program)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def load_html(app)
|
30
|
+
File.read File.join(@root_directory, @index_file)
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_js(app)
|
34
|
+
js_from_files = @javascript_paths.map do |path|
|
35
|
+
if path == :ember_driver
|
36
|
+
Phrender::EMBER_DRIVER
|
37
|
+
else
|
38
|
+
File.read File.join(@root_directory, path)
|
39
|
+
end
|
40
|
+
end.join(';')
|
41
|
+
program = js_from_files + @raw_javascript
|
42
|
+
program
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
window.__PHRENDER = true;
|
2
|
+
window.__firstRender = true;
|
3
|
+
window.__afterRender = function() {
|
4
|
+
console.log('-- PHRENDER COMPLETE --');
|
5
|
+
};
|
6
|
+
Ember.Route = Ember.Route.extend({
|
7
|
+
render: function(){
|
8
|
+
if (window.__firstRender) {
|
9
|
+
window.__firstRender = false;
|
10
|
+
} else {
|
11
|
+
Ember.run.scheduleOnce('afterRender', null, window.__afterRender);
|
12
|
+
}
|
13
|
+
this._super.apply(this, arguments);
|
14
|
+
}
|
15
|
+
});
|
16
|
+
|
@@ -0,0 +1,108 @@
|
|
1
|
+
var system = require('system'),
|
2
|
+
fs = require('fs'),
|
3
|
+
webpage = require('webpage');
|
4
|
+
|
5
|
+
// streams
|
6
|
+
var stdout = system.stdout;
|
7
|
+
|
8
|
+
// global opts
|
9
|
+
var options = JSON.parse(system.args[system.args.length - 1]),
|
10
|
+
globals = {
|
11
|
+
"page": null,
|
12
|
+
"timer": null,
|
13
|
+
"expired": false,
|
14
|
+
"rendered": false,
|
15
|
+
"html": options.html
|
16
|
+
};
|
17
|
+
|
18
|
+
// functions
|
19
|
+
var printJson,
|
20
|
+
logMessage,
|
21
|
+
logTrace,
|
22
|
+
logError,
|
23
|
+
printPage,
|
24
|
+
waitForRender,
|
25
|
+
writeHtml,
|
26
|
+
run;
|
27
|
+
|
28
|
+
printJson = function(messageType, message) {
|
29
|
+
var payload = {};
|
30
|
+
payload[messageType] = message;
|
31
|
+
stdout.writeLine(JSON.stringify(payload));
|
32
|
+
stdout.flush();
|
33
|
+
};
|
34
|
+
|
35
|
+
logMessage = function(message) {
|
36
|
+
printJson('console', message);
|
37
|
+
};
|
38
|
+
|
39
|
+
logTrace = function(trace) {
|
40
|
+
if (trace && trace.length) {
|
41
|
+
var traceStack = [];
|
42
|
+
trace.forEach(function(t) {
|
43
|
+
traceStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function + '")' : ''));
|
44
|
+
});
|
45
|
+
|
46
|
+
printJson('trace', traceStack.join('\n'));
|
47
|
+
}
|
48
|
+
};
|
49
|
+
|
50
|
+
logError = function(message, trace) {
|
51
|
+
printJson('error', message);
|
52
|
+
|
53
|
+
logTrace(trace);
|
54
|
+
};
|
55
|
+
|
56
|
+
printPage = function(url) {
|
57
|
+
globals.page = webpage.create();
|
58
|
+
|
59
|
+
globals.page.setContent(globals.html, url);
|
60
|
+
|
61
|
+
// Log javascript console messages
|
62
|
+
globals.page.onConsoleMessage = function(msg) {
|
63
|
+
if (msg.trim() === "-- PHRENDER COMPLETE --"){
|
64
|
+
globals.rendered = true;
|
65
|
+
} else {
|
66
|
+
logMessage(msg);
|
67
|
+
}
|
68
|
+
};
|
69
|
+
|
70
|
+
// capture errors
|
71
|
+
globals.page.onError = logError;
|
72
|
+
|
73
|
+
globals.page.evaluate(function(code) {
|
74
|
+
eval(code);
|
75
|
+
}, options.javascript);
|
76
|
+
|
77
|
+
// Catch something
|
78
|
+
globals.timer = setTimeout(writeHtml, options.timeout);
|
79
|
+
|
80
|
+
// Wait for the flag to switch
|
81
|
+
waitForRender();
|
82
|
+
};
|
83
|
+
|
84
|
+
waitForRender = function() {
|
85
|
+
if (!globals.expired) {
|
86
|
+
if (!globals.rendered) {
|
87
|
+
setTimeout(waitForRender, 100);
|
88
|
+
} else {
|
89
|
+
clearTimeout(globals.timer);
|
90
|
+
writeHtml();
|
91
|
+
}
|
92
|
+
}
|
93
|
+
};
|
94
|
+
|
95
|
+
writeHtml = function() {
|
96
|
+
globals.expired = true;
|
97
|
+
var html = globals.page.evaluate(function() {
|
98
|
+
return document.documentElement.outerHTML;
|
99
|
+
});
|
100
|
+
printJson("page", html);
|
101
|
+
phantom.exit();
|
102
|
+
};
|
103
|
+
|
104
|
+
run = function() {
|
105
|
+
printPage(options.url);
|
106
|
+
};
|
107
|
+
|
108
|
+
run();
|
data/lib/phrender.rb
ADDED
data/lib/string/strip.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Borrowed from activesupport, but without the :try method
|
2
|
+
|
3
|
+
class String
|
4
|
+
# Strips indentation in heredocs.
|
5
|
+
#
|
6
|
+
# For example in
|
7
|
+
#
|
8
|
+
# if options[:usage]
|
9
|
+
# puts <<-USAGE.strip_heredoc
|
10
|
+
# This command does such and such.
|
11
|
+
#
|
12
|
+
# Supported options are:
|
13
|
+
# -h This message
|
14
|
+
# ...
|
15
|
+
# USAGE
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# the user would see the usage message aligned against the left margin.
|
19
|
+
#
|
20
|
+
# Technically, it looks for the least indented line in the whole string, and removes
|
21
|
+
# that amount of leading whitespace.
|
22
|
+
def strip_heredoc
|
23
|
+
matches = scan(/^[ \t]*(?=\S)/).min
|
24
|
+
indent = (matches.respond_to? :size) ? matches.size : 0
|
25
|
+
gsub(/^[ \t]{#{indent}}/, '')
|
26
|
+
end
|
27
|
+
end
|
data/phrender.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'phrender/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "phrender"
|
8
|
+
spec.version = Phrender::VERSION
|
9
|
+
spec.authors = ["M Smart, theScore Inc."]
|
10
|
+
spec.email = ["matthew.smart@thescore.com"]
|
11
|
+
spec.description = %q{Rack server for rendering javascript apps for bots}
|
12
|
+
spec.summary = %q{Rack server for rendering javascript apps for bots}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency "rack", "~> 1.5.2"
|
22
|
+
spec.add_runtime_dependency "colorize"
|
23
|
+
spec.add_runtime_dependency "multi_json"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0.0.beta1'
|
28
|
+
spec.add_development_dependency 'rack-test'
|
29
|
+
spec.add_development_dependency 'sprockets'
|
30
|
+
spec.add_development_dependency 'pry', '0.9.12.2'
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
(function() {
|
2
|
+
"use strict";
|
3
|
+
|
4
|
+
var head = document.getElementsByTagName('head')[0],
|
5
|
+
title = document.createElement('title'),
|
6
|
+
paragraph = document.createElement('p');
|
7
|
+
|
8
|
+
title.appendChild(document.createTextNode("Phrender The Prerenderer"));
|
9
|
+
head.appendChild(title);
|
10
|
+
|
11
|
+
paragraph.appendChild(document.createTextNode("Hello!"));
|
12
|
+
document.body.appendChild(paragraph);
|
13
|
+
|
14
|
+
setTimeout(function(){window.console.log('-- PHRENDER COMPLETE --');},1000);
|
15
|
+
})();
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'phrender/phantom_js_engine'
|
3
|
+
|
4
|
+
describe 'Phrender::PhantomJSEngine' do
|
5
|
+
let(:phantom) {
|
6
|
+
Phrender::PhantomJSEngine.new(:timeout => 10000, :ssl => false)
|
7
|
+
}
|
8
|
+
|
9
|
+
let(:index) {
|
10
|
+
File.read(File.expand_path('../phantom_js_engine/index.html', __FILE__))
|
11
|
+
}
|
12
|
+
|
13
|
+
let(:app) {
|
14
|
+
File.read(File.expand_path('../phantom_js_engine/app.js', __FILE__))
|
15
|
+
}
|
16
|
+
|
17
|
+
it 'generates a startup command with escaped json' do
|
18
|
+
command = phantom.app_cmd(index, app, 'http://localhost')
|
19
|
+
expect(command).to match(
|
20
|
+
/phantomjs (.+?)phrender\/lib\/phrender\/support\/phantom_bridge.js/
|
21
|
+
)
|
22
|
+
expect(command).to include("--ignore-ssl-errors=true")
|
23
|
+
expect(command).to include("<html>")
|
24
|
+
expect(command).to include("use strict")
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'renders a simple page' do
|
28
|
+
whitespace_regex = /(\n|^ +)/
|
29
|
+
html = <<-HTML.strip_heredoc.gsub(whitespace_regex, '')
|
30
|
+
<html>
|
31
|
+
<head>
|
32
|
+
<title>Phrender The Prerenderer</title>
|
33
|
+
</head>
|
34
|
+
<body>
|
35
|
+
<p>Hello!</p>
|
36
|
+
</body>
|
37
|
+
</html>
|
38
|
+
HTML
|
39
|
+
expect(phantom.render(index, app).gsub(whitespace_regex, '')).to eq(html)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
(function() {
|
2
|
+
"use strict";
|
3
|
+
|
4
|
+
var head = document.getElementsByTagName('head')[0],
|
5
|
+
title = document.createElement('title'),
|
6
|
+
paragraph = document.createElement('p');
|
7
|
+
|
8
|
+
title.appendChild(document.createTextNode("Phrender The Prerenderer"));
|
9
|
+
head.appendChild(title);
|
10
|
+
|
11
|
+
paragraph.appendChild(document.createTextNode("Hello!"));
|
12
|
+
document.body.appendChild(paragraph);
|
13
|
+
|
14
|
+
window.App = {
|
15
|
+
run: function() {
|
16
|
+
window.console.log('-- PHRENDER COMPLETE --');
|
17
|
+
}
|
18
|
+
};
|
19
|
+
})();
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'phrender/rack_middleware'
|
3
|
+
require 'sprockets'
|
4
|
+
|
5
|
+
describe 'Phrender::RackMiddleware' do
|
6
|
+
let(:root) { File.expand_path '../rack_middleware', __FILE__ }
|
7
|
+
let(:backend) {
|
8
|
+
b = Sprockets::Environment.new(root)
|
9
|
+
b.append_path 'assets'
|
10
|
+
b
|
11
|
+
}
|
12
|
+
let(:app) {
|
13
|
+
p = Phrender::RackMiddleware.new(backend)
|
14
|
+
p.index_file = 'phrender.html'
|
15
|
+
p.add_javascript_file 'app.js'
|
16
|
+
p.add_javascript 'App.run'
|
17
|
+
p.rack_app
|
18
|
+
}
|
19
|
+
|
20
|
+
it 'runs the app contained in the referenced assets' do
|
21
|
+
get('/')
|
22
|
+
whitespace_regex = /(\n|^ +)/
|
23
|
+
html = '<html><head><title>Phrender The Prerenderer</title></head><body><h1>What a page!</h1><p>Hello!</p></body></html>'
|
24
|
+
expect(last_response.body.gsub(whitespace_regex, '')).to eq(html)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
(function() {
|
2
|
+
"use strict";
|
3
|
+
|
4
|
+
var head = document.getElementsByTagName('head')[0],
|
5
|
+
title = document.createElement('title'),
|
6
|
+
paragraph = document.createElement('p');
|
7
|
+
|
8
|
+
title.appendChild(document.createTextNode("Phrender The Prerenderer"));
|
9
|
+
head.appendChild(title);
|
10
|
+
|
11
|
+
paragraph.appendChild(document.createTextNode("Hello!"));
|
12
|
+
document.body.appendChild(paragraph);
|
13
|
+
|
14
|
+
window.App = {
|
15
|
+
run: function() {
|
16
|
+
window.console.log('-- PHRENDER COMPLETE --');
|
17
|
+
}
|
18
|
+
};
|
19
|
+
})();
|
@@ -0,0 +1 @@
|
|
1
|
+
The body of a static file.
|
File without changes
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'phrender/rack_static'
|
3
|
+
|
4
|
+
describe 'Phrender::RackStatic' do
|
5
|
+
let(:root) { File.expand_path '../rack_static', __FILE__ }
|
6
|
+
let(:app) {
|
7
|
+
p = Phrender::RackStatic.new(root)
|
8
|
+
p.index_file = 'phrender.html'
|
9
|
+
p.add_javascript_file 'app.js'
|
10
|
+
p.add_javascript 'App.run'
|
11
|
+
p.rack_app
|
12
|
+
}
|
13
|
+
|
14
|
+
it 'runs the app contained in the referenced assets' do
|
15
|
+
get('/')
|
16
|
+
expect(last_response.body).to eq('<html><head><title>Phrender The Prerenderer</title></head><body><p>Hello!</p></body></html>')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'resolves static assets' do
|
20
|
+
get('/files/static.txt')
|
21
|
+
expect(last_response.body.strip).to eq('The body of a static file.')
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
require 'string/strip'
|
3
|
+
|
4
|
+
ENV['RACK_ENV'] = 'test'
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
# Use color in STDOUT
|
8
|
+
config.color = true
|
9
|
+
|
10
|
+
# Use the specified formatter
|
11
|
+
config.formatter = :documentation # :progress, :html, :textmate
|
12
|
+
|
13
|
+
config.include Rack::Test::Methods
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: phrender
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- M Smart, theScore Inc.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.5.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.5.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: colorize
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
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: multi_json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
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: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.0.0.beta1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.0.0.beta1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rack-test
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sprockets
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.9.12.2
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - '='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.9.12.2
|
139
|
+
description: Rack server for rendering javascript apps for bots
|
140
|
+
email:
|
141
|
+
- matthew.smart@thescore.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- Gemfile
|
148
|
+
- LICENSE
|
149
|
+
- README.md
|
150
|
+
- Rakefile
|
151
|
+
- lib/phrender.rb
|
152
|
+
- lib/phrender/ember_driver.rb
|
153
|
+
- lib/phrender/logger.rb
|
154
|
+
- lib/phrender/phantom_js_engine.rb
|
155
|
+
- lib/phrender/phantom_js_session.rb
|
156
|
+
- lib/phrender/rack_base.rb
|
157
|
+
- lib/phrender/rack_middleware.rb
|
158
|
+
- lib/phrender/rack_static.rb
|
159
|
+
- lib/phrender/support/ember_driver.js
|
160
|
+
- lib/phrender/support/phantom_bridge.js
|
161
|
+
- lib/phrender/version.rb
|
162
|
+
- lib/string/strip.rb
|
163
|
+
- phrender.gemspec
|
164
|
+
- spec/phrender/phantom_js_engine/app.js
|
165
|
+
- spec/phrender/phantom_js_engine/index.html
|
166
|
+
- spec/phrender/phantom_js_engine_spec.rb
|
167
|
+
- spec/phrender/rack_middleware/assets/app.js
|
168
|
+
- spec/phrender/rack_middleware/assets/phrender.html.erb
|
169
|
+
- spec/phrender/rack_middleware_spec.rb
|
170
|
+
- spec/phrender/rack_static/app.js
|
171
|
+
- spec/phrender/rack_static/files/static.txt
|
172
|
+
- spec/phrender/rack_static/phrender.html
|
173
|
+
- spec/phrender/rack_static_spec.rb
|
174
|
+
- spec/spec_helper.rb
|
175
|
+
homepage: ''
|
176
|
+
licenses:
|
177
|
+
- MIT
|
178
|
+
metadata: {}
|
179
|
+
post_install_message:
|
180
|
+
rdoc_options: []
|
181
|
+
require_paths:
|
182
|
+
- lib
|
183
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - ">="
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
193
|
+
requirements: []
|
194
|
+
rubyforge_project:
|
195
|
+
rubygems_version: 2.2.2
|
196
|
+
signing_key:
|
197
|
+
specification_version: 4
|
198
|
+
summary: Rack server for rendering javascript apps for bots
|
199
|
+
test_files:
|
200
|
+
- spec/phrender/phantom_js_engine/app.js
|
201
|
+
- spec/phrender/phantom_js_engine/index.html
|
202
|
+
- spec/phrender/phantom_js_engine_spec.rb
|
203
|
+
- spec/phrender/rack_middleware/assets/app.js
|
204
|
+
- spec/phrender/rack_middleware/assets/phrender.html.erb
|
205
|
+
- spec/phrender/rack_middleware_spec.rb
|
206
|
+
- spec/phrender/rack_static/app.js
|
207
|
+
- spec/phrender/rack_static/files/static.txt
|
208
|
+
- spec/phrender/rack_static/phrender.html
|
209
|
+
- spec/phrender/rack_static_spec.rb
|
210
|
+
- spec/spec_helper.rb
|