jsus 0.3.4 → 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/Gemfile +15 -2
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/autotest/discover.rb +1 -1
- data/bin/jsus +87 -341
- data/features/command-line/compression.feature +24 -1
- data/jsus.gemspec +30 -9
- data/lib/extensions/rgl.rb +37 -0
- data/lib/jsus.rb +5 -4
- data/lib/jsus/cli.rb +206 -0
- data/lib/jsus/compiler.rb +28 -0
- data/lib/jsus/container.rb +24 -17
- data/lib/jsus/middleware.rb +10 -9
- data/lib/jsus/package.rb +1 -1
- data/lib/jsus/util.rb +20 -0
- data/lib/jsus/util/compressor.rb +67 -16
- data/lib/jsus/util/watcher.rb +79 -0
- data/spec/benchmarks/topsort.rb +29 -0
- data/spec/data/ComplexDependencies/Mootools/Source/Core.js +2 -0
- data/spec/extensions/rgl_spec.rb +34 -0
- data/spec/jsus/middleware_spec.rb +23 -1
- data/spec/jsus/util/compressor_spec.rb +44 -0
- data/spec/jsus/util/watcher_spec.rb +122 -0
- data/spec/jsus/util_spec.rb +32 -0
- data/spec/spec_helper.rb +1 -1
- metadata +207 -214
data/lib/jsus/container.rb
CHANGED
@@ -128,26 +128,33 @@ module Jsus
|
|
128
128
|
end
|
129
129
|
end
|
130
130
|
end
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
error_msg << "Please resolve them immediately!"
|
138
|
-
error_msg << "List of circular dependencies:"
|
139
|
-
cycles.each do |cycle|
|
140
|
-
error_msg << "-" * 30
|
141
|
-
error_msg << (cycle + [cycle.first]).map {|sf| sf.filename}.join(" => ")
|
142
|
-
end
|
143
|
-
error_msg = error_msg.join("\n")
|
144
|
-
Jsus.logger.fatal(error_msg)
|
145
|
-
end
|
131
|
+
|
132
|
+
begin
|
133
|
+
graph.topsorted_vertices
|
134
|
+
rescue RGL::TopsortedGraphHasCycles => e
|
135
|
+
output_cycles(graph)
|
136
|
+
raise e # fail fast
|
146
137
|
end
|
147
|
-
graph.topsort_iterator.each { |item| result << item }
|
148
|
-
result
|
149
138
|
end
|
150
139
|
|
140
|
+
# @api private
|
141
|
+
def output_cycles(graph)
|
142
|
+
cycles = graph.cycles
|
143
|
+
error_msg = []
|
144
|
+
unless cycles.empty?
|
145
|
+
error_msg << "Jsus has discovered you have circular dependencies in your code."
|
146
|
+
error_msg << "Please resolve them immediately!"
|
147
|
+
error_msg << "List of circular dependencies:"
|
148
|
+
cycles.each do |cycle|
|
149
|
+
error_msg << "-" * 30
|
150
|
+
error_msg << (cycle + [cycle.first]).map {|sf| sf.filename}.join(" => ")
|
151
|
+
end
|
152
|
+
error_msg << "-" * 30
|
153
|
+
error_msg = error_msg.join("\n")
|
154
|
+
Jsus.logger.fatal(error_msg)
|
155
|
+
end
|
156
|
+
end # output_cycles
|
157
|
+
|
151
158
|
# Cached map of dependencies pointing to source files.
|
152
159
|
# @return [Hash]
|
153
160
|
# @api private
|
data/lib/jsus/middleware.rb
CHANGED
@@ -36,14 +36,15 @@ module Jsus
|
|
36
36
|
class << self
|
37
37
|
# Default settings for Middleware
|
38
38
|
DEFAULT_SETTINGS = {
|
39
|
-
:packages_dir
|
40
|
-
:cache
|
41
|
-
:cache_path
|
42
|
-
:prefix
|
43
|
-
:cache_pool
|
44
|
-
:includes_root
|
45
|
-
:
|
46
|
-
:
|
39
|
+
:packages_dir => ".",
|
40
|
+
:cache => false,
|
41
|
+
:cache_path => nil,
|
42
|
+
:prefix => "jsus",
|
43
|
+
:cache_pool => true,
|
44
|
+
:includes_root => ".",
|
45
|
+
:compression_method => :yui, # :yui, :frontcompiler, :uglifier, :closure
|
46
|
+
:log_method => nil, # [:alert, :html, :console]
|
47
|
+
:postproc => [] # ["mooltie8", "moocompat12"]
|
47
48
|
}.freeze
|
48
49
|
|
49
50
|
# @return [Hash] Middleware current settings
|
@@ -175,7 +176,7 @@ module Jsus
|
|
175
176
|
files = path_string_to_files(path_string)
|
176
177
|
if !files.empty?
|
177
178
|
response = Container.new(*files).map {|f| f.content }.join("\n")
|
178
|
-
response = Jsus::Util::Compressor.
|
179
|
+
response = Jsus::Util::Compressor.compress(response, :method => self.class.settings[:compression_method]) if request_options[:compress]
|
179
180
|
respond_with(response)
|
180
181
|
else
|
181
182
|
not_found!
|
data/lib/jsus/package.rb
CHANGED
@@ -27,7 +27,7 @@ module Jsus
|
|
27
27
|
elsif File.exists?(File.join(directory, 'package.json'))
|
28
28
|
self.header = JSON.load(File.open(File.join(directory, 'package.json'), 'r:utf-8') {|f| f.read })
|
29
29
|
else
|
30
|
-
Jsus
|
30
|
+
Jsus.logger.fatal "Directory #{directory} does not contain a valid package.yml / package.json file!"
|
31
31
|
raise "Directory #{directory} does not contain a valid package.yml / package.json file!"
|
32
32
|
end
|
33
33
|
Dir.chdir(directory) do
|
data/lib/jsus/util.rb
CHANGED
@@ -9,5 +9,25 @@ module Jsus
|
|
9
9
|
autoload :FileCache, 'jsus/util/file_cache'
|
10
10
|
autoload :CodeGenerator, 'jsus/util/code_generator'
|
11
11
|
autoload :Logger, 'jsus/util/logger'
|
12
|
+
autoload :Watcher, 'jsus/util/watcher'
|
13
|
+
|
14
|
+
class <<self
|
15
|
+
# Tries to load given gem.
|
16
|
+
# @param [String] gemname gem name
|
17
|
+
# @param [String, nil] lib path to load. When set to nil, uses gemname
|
18
|
+
# param
|
19
|
+
# @return [Boolean] true if that lib could be required, false otherwise
|
20
|
+
# @api semipublic
|
21
|
+
def try_load(gemname, lib = nil)
|
22
|
+
required_file = lib || gemname
|
23
|
+
begin
|
24
|
+
require(required_file)
|
25
|
+
true
|
26
|
+
rescue LoadError
|
27
|
+
Jsus.logger.error %{ERROR: missing file #{required_file}}
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end # class <<self
|
12
32
|
end # Util
|
13
33
|
end # Jsus
|
data/lib/jsus/util/compressor.rb
CHANGED
@@ -1,21 +1,72 @@
|
|
1
1
|
module Jsus
|
2
2
|
module Util
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module Compressor
|
4
|
+
class <<self
|
5
|
+
# Compresses the javascript source with given compressor and returns
|
6
|
+
# the result.
|
7
|
+
#
|
8
|
+
# @param [String] source javascript source code
|
9
|
+
# @param [Hash] options
|
10
|
+
# @option [Symbol] (:yui) method compressor to use.
|
11
|
+
# Available methods: :uglifier, :frontcompiler, :closure, :yui
|
12
|
+
# @return [String] compressed js code
|
13
|
+
# @api public
|
14
|
+
def compress(source, options = {})
|
15
|
+
method = options.fetch(:method, :yui)
|
16
|
+
@result = case method.to_s
|
17
|
+
when "uglifier" then compress_with_uglifier(source)
|
18
|
+
when "frontcompiler" then compress_with_frontcompiler(source)
|
19
|
+
when "closure" then compress_with_closure(source)
|
20
|
+
when "yui" then compress_with_yui(source)
|
21
|
+
else
|
22
|
+
Jsus.logger.error "tried to use unavailable method #{method.inspect}"
|
23
|
+
source
|
24
|
+
end
|
25
|
+
end # compress
|
8
26
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
17
|
-
|
18
|
-
|
19
|
-
|
27
|
+
private
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
def compress_with_yui(source)
|
31
|
+
try_compress(source, "yuicompressor") do
|
32
|
+
YUICompressor.compress_js(source, :munge => true)
|
33
|
+
end
|
34
|
+
end # compress_with_yui
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
def compress_with_uglifier(source)
|
38
|
+
try_compress(source, "uglifier") do
|
39
|
+
Uglifier.compile(source, :squeeze => true, :copyright => false)
|
40
|
+
end
|
41
|
+
end # compress_with_uglifier
|
42
|
+
|
43
|
+
# @api private
|
44
|
+
def compress_with_frontcompiler(source)
|
45
|
+
try_compress(source, 'front-compiler') do
|
46
|
+
FrontCompiler.new.compact_js(source)
|
47
|
+
end
|
48
|
+
end # compress_with_frontcompiler
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
def compress_with_closure(source)
|
52
|
+
try_compress(source, 'closure-compiler') do
|
53
|
+
Closure::Compiler.new.compile(source)
|
54
|
+
end
|
55
|
+
end # compress_with_closure
|
56
|
+
|
57
|
+
# @api private
|
58
|
+
def try_compress(source, library, libname = nil)
|
59
|
+
if Jsus::Util.try_load(library, libname) then
|
60
|
+
yield
|
61
|
+
else
|
62
|
+
source
|
63
|
+
end
|
64
|
+
rescue Exception => e
|
65
|
+
Jsus.logger.error "#{library} could not compress the file, exception raised #{e}\n" <<
|
66
|
+
"Returning initial source"
|
67
|
+
source
|
68
|
+
end # try_compress
|
69
|
+
end # class <<self
|
70
|
+
end # module Compressor
|
20
71
|
end # module Util
|
21
72
|
end # module Jsus
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Jsus::Util::Watcher
|
2
|
+
class << self
|
3
|
+
# Watches input directories and their subdirectories for changes in
|
4
|
+
# js source files and package metadata files.
|
5
|
+
# @param [String, Array] input_dirs directory or directories to watch
|
6
|
+
# @yield [filename] Callback to trigger on creation / update / removal of
|
7
|
+
# any file in given directories
|
8
|
+
# @yieldparam [String] filename Updated filename full path
|
9
|
+
# @return [FSSM::Monitor] fssm monitor instance
|
10
|
+
# @api public
|
11
|
+
def watch(input_dirs, &callback)
|
12
|
+
new(input_dirs, &callback)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Instantiates a FSSM monitor and starts watching. Consider using class method
|
17
|
+
# Jsus::Util::Watcher.watch instead.
|
18
|
+
# @see .watch
|
19
|
+
# @api semipublic
|
20
|
+
def initialize(input_dirs, &callback)
|
21
|
+
require 'fssm'
|
22
|
+
@callback = callback
|
23
|
+
input_dirs = Array(input_dirs).compact
|
24
|
+
@semaphore = Mutex.new
|
25
|
+
watcher = self
|
26
|
+
FSSM.monitor do
|
27
|
+
input_dirs.each do |dir|
|
28
|
+
dir = File.expand_path(dir)
|
29
|
+
path(dir) do
|
30
|
+
glob ["**/*.js", "**/package.yml", "**/package.json"]
|
31
|
+
create &watcher.method(:watch_callback)
|
32
|
+
update &watcher.method(:watch_callback)
|
33
|
+
delete &watcher.method(:watch_callback)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
rescue LoadError => e
|
39
|
+
Jsus.logger.error "You need to install fssm gem for --watch option."
|
40
|
+
Jsus.logger.error "You may also want to install rb-fsevent for OS X" if RUBY_PLATFORM =~ /darwin/
|
41
|
+
raise e
|
42
|
+
end
|
43
|
+
|
44
|
+
# Default callback for the FSSM watcher.
|
45
|
+
# @note Defers the processing to a separate thread and ignores all the incoming
|
46
|
+
# events received during the processing.
|
47
|
+
# @param [String] base base part of filename
|
48
|
+
# @param [String] match matched part of filename
|
49
|
+
# @api semipublic
|
50
|
+
def watch_callback(base, match)
|
51
|
+
Thread.new do
|
52
|
+
run do
|
53
|
+
full_path = File.join(base, match)
|
54
|
+
@callback.call(full_path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end # watch_callback
|
58
|
+
|
59
|
+
# @api semipublic
|
60
|
+
def run
|
61
|
+
if @semaphore.try_lock
|
62
|
+
begin
|
63
|
+
yield
|
64
|
+
rescue Exception => e
|
65
|
+
Jsus.logger.error "Exception happened during watching: #{e}, #{e.inspect}"
|
66
|
+
Jsus.logger.error "\t#{e.backtrace.join("\n\t")}" if Jsus.verbose?
|
67
|
+
Jsus.logger.error "Compilation FAILED."
|
68
|
+
ensure
|
69
|
+
@semaphore.unlock
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end # run
|
73
|
+
|
74
|
+
# @return [Boolean]
|
75
|
+
# @api public
|
76
|
+
def running?
|
77
|
+
@semaphore.locked?
|
78
|
+
end # running?
|
79
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rgl/adjacency'
|
4
|
+
require 'rgl/topsort'
|
5
|
+
$:.unshift(File.expand_path("../../../lib/", __FILE__))
|
6
|
+
require 'extensions/rgl'
|
7
|
+
|
8
|
+
edges = [[
|
9
|
+
1,3, 1,4,
|
10
|
+
2,3,
|
11
|
+
3,7,
|
12
|
+
4,7,
|
13
|
+
5,6,
|
14
|
+
6,7,
|
15
|
+
7,8
|
16
|
+
]] * 10
|
17
|
+
|
18
|
+
edges.each_with_index do |v_list, i|
|
19
|
+
v_list.each {|v| v += i * 100 }
|
20
|
+
end
|
21
|
+
|
22
|
+
edges.flatten!
|
23
|
+
|
24
|
+
n = 5_000
|
25
|
+
graph = RGL::DirectedAdjacencyGraph[*edges]
|
26
|
+
Benchmark.bmbm do |x|
|
27
|
+
x.report("default") { n.times { graph.topsort_iterator.to_a ; graph.cycles.to_a } }
|
28
|
+
x.report("custom") { n.times { graph.topsorted_vertices } }
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "RGL extensions" do
|
4
|
+
describe "DirectedAdjacencyGraph#topsorted_vertices" do
|
5
|
+
let(:edges) do
|
6
|
+
[
|
7
|
+
1,3, 1,4,
|
8
|
+
2,3,
|
9
|
+
3,7,
|
10
|
+
4,7,
|
11
|
+
5,6,
|
12
|
+
6,7,
|
13
|
+
7,8
|
14
|
+
]
|
15
|
+
end
|
16
|
+
subject { RGL::DirectedAdjacencyGraph[*edges] }
|
17
|
+
it "should return list of topologically sorted vertices" do
|
18
|
+
result = subject.topsorted_vertices
|
19
|
+
result.index(1).should < result.index(3)
|
20
|
+
result.index(2).should < result.index(3)
|
21
|
+
result.index(1).should < result.index(4)
|
22
|
+
result.index(1).should < result.index(7)
|
23
|
+
result.index(5).should < result.index(6)
|
24
|
+
result.index(5).should < result.index(7)
|
25
|
+
result.index(6).should < result.index(7)
|
26
|
+
result.index(7).should < result.index(8)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should throw an exception if graph contains cycles" do
|
30
|
+
subject.add_edge(7,2)
|
31
|
+
lambda { subject.topsorted_vertices }.should raise_error(RGL::TopsortedGraphHasCycles)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -281,7 +281,7 @@ describe Jsus::Middleware do
|
|
281
281
|
describe "post processing" do
|
282
282
|
let(:packages_dir) { File.expand_path("spec/data/ComplexDependencies") }
|
283
283
|
before(:each) { Jsus::Middleware.settings = {:packages_dir => packages_dir} }
|
284
|
-
let(
|
284
|
+
let(:path) { "/javascripts/jsus/require/Package.js" }
|
285
285
|
it "should not do anything if postprocs setting is empty" do
|
286
286
|
Jsus::Middleware.settings = {:postproc => []}
|
287
287
|
get(path).body.should include("//<ltIE8>")
|
@@ -305,6 +305,28 @@ describe Jsus::Middleware do
|
|
305
305
|
end
|
306
306
|
end # describe "post processing"
|
307
307
|
|
308
|
+
describe "compression" do
|
309
|
+
let(:packages_dir) { File.expand_path("spec/data/ComplexDependencies") }
|
310
|
+
before(:each) { described_class.settings = {:packages_dir => packages_dir} }
|
311
|
+
let(:path) { "/javascripts/jsus/compressed/Package.js" }
|
312
|
+
|
313
|
+
it "should be successful" do
|
314
|
+
get(path).should be_successful
|
315
|
+
end
|
316
|
+
|
317
|
+
it "should respond with type text/javascript" do
|
318
|
+
get(path).content_type.should == "text/javascript"
|
319
|
+
end
|
320
|
+
|
321
|
+
it "should respond with generated content" do
|
322
|
+
get(path).body.should =~ /var.*Core/
|
323
|
+
end
|
324
|
+
|
325
|
+
it "should compress the code" do
|
326
|
+
get(path).body.should_not include("/*")
|
327
|
+
end
|
328
|
+
end # describe "compression"
|
329
|
+
|
308
330
|
describe "errors logging" do
|
309
331
|
let(:packages_dir) { File.expand_path("spec/data/MissingDependencies") }
|
310
332
|
before(:each) { Jsus::Middleware.settings = {:packages_dir => packages_dir} }
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Jsus::Util::Compressor do
|
4
|
+
let(:source) { File.read("spec/data/test_source_one.js") }
|
5
|
+
describe ".compress" do
|
6
|
+
it "should take source and compress it with some default params" do
|
7
|
+
result = described_class.compress(source)
|
8
|
+
result.should include("var TestSourceOne")
|
9
|
+
end
|
10
|
+
|
11
|
+
# To test actual compression, we check for removal of comments which is
|
12
|
+
# not the only criterion, but the easiest one to check
|
13
|
+
|
14
|
+
it "should accept :yui for method" do
|
15
|
+
result = described_class.compress(source, :method => :yui)
|
16
|
+
result.should include("var TestSourceOne")
|
17
|
+
result.should_not include("/*")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should accept :uglifier for method" do
|
21
|
+
result = described_class.compress(source, :method => :uglifier)
|
22
|
+
result.should include("var TestSourceOne")
|
23
|
+
result.should_not include("/*")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should accept :frontcompiler for method" do
|
27
|
+
result = described_class.compress(source, :method => :frontcompiler)
|
28
|
+
result.should include("var TestSourceOne")
|
29
|
+
result.should_not include("/*")
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should accept :closure for method" do
|
33
|
+
result = described_class.compress(source, :method => :closure)
|
34
|
+
result.should include("var TestSourceOne")
|
35
|
+
result.should_not include("/*")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should accept strings for methods" do
|
39
|
+
result = described_class.compress(source, :method => "yui")
|
40
|
+
result.should include("var TestSourceOne")
|
41
|
+
result.should_not include("/*")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|