weld 0.0.1.dev.20100402 → 0.0.1.dev.20100404

Sign up to get free protection for your applications and to get access to all the features.
data/bin/weld CHANGED
@@ -1,63 +1,40 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rubygems'
4
- require 'trollop'
5
4
  require 'weld'
6
5
 
7
- options = Trollop.options do
8
- version "#{Weld::APP_NAME} #{Weld::APP_VERSION}\n" << Weld::APP_COPYRIGHT
9
- banner <<-EOS
10
- Weld combines and minifies CSS and JavaScript files at runtime and build time.
6
+ cli = Weld::CLI.new
11
7
 
12
- Usage:
13
- weld [options] <component> [<component> ...]
8
+ begin
9
+ cli.components.each do |name|
10
+ cli.info "Welding #{name}"
14
11
 
15
- Options:
16
- EOS
12
+ component = cli.weld.component(name)
13
+ suffix = Time.now.strftime('-%Y%m%d%H%M')
17
14
 
18
- opt :config, "Use the specified config file.", :short => '-c', :default => './weld.yaml'
19
- opt :no_minify, "Don't perform any minification.", :short => :none
20
- opt :output_dir, "Write welded files to the specified directory.", :short => '-o', :default => './'
21
- opt :type, "Only weld components of the specified type ('css' or 'js').", :short => '-t', :type => :string
22
- end
23
-
24
- if options[:type_given] && !['css', 'js'].include?(options[:type])
25
- abort "Error: Unsupported component type: #{options[:type]}\n" <<
26
- "Try --help for help."
27
- end
28
-
29
- weld = Weld.new(options[:config])
30
-
31
- components = ARGV.length > 0 ? ARGV : @weld.config['components'] || []
32
- type = options[:type].to_sym if options[:type_given]
33
-
34
- components.each do |name|
35
- puts "Welding #{name}"
36
-
37
- component = weld.component(name)
38
- suffix = Time.now.strftime('-%Y%m%d%H%M')
39
-
40
- if type
41
- next if component.method(type).call.empty?
42
-
43
- filename = "#{File.join(options[:output_dir], name)}#{suffix}.#{type}"
44
-
45
- File.open(filename, 'w') do |file|
46
- puts "--> #{filename}"
47
- file.write(options[:no_minify] ? component.merge(type) : component.compress(type))
48
- end
49
- else
50
- [:css, :js].each do |type|
15
+ cli.types.each do |type|
51
16
  next if component.method(type).call.empty?
52
17
 
53
- filename = "#{File.join(options[:output_dir], name)}#{suffix}.#{type}"
18
+ filename = "#{name}#{suffix}.#{type}"
19
+ content = cli.options[:no_minify] ? component.merge(type) : component.compress(type)
20
+
21
+ if cli.options[:push]
22
+ cdn_url = cli.cdn_push(filename, type, content)
23
+ cli.info "--> #{cdn_url}"
24
+ else
25
+ full_path = File.join(cli.options[:output_dir], filename)
54
26
 
55
- File.open(filename, 'w') do |file|
56
- puts "--> #{filename}"
57
- file.write(options[:no_minify] ? component.merge(type) : component.compress(type))
27
+ File.open(full_path, 'w') do |file|
28
+ cli.info "--> #{full_path}"
29
+ file.write(content)
30
+ end
58
31
  end
59
32
  end
60
33
  end
61
34
 
62
- puts
35
+ # rescue => ex
36
+ # cli.error ex.message
37
+ # exit(1)
63
38
  end
39
+
40
+ puts
data/lib/weld.rb CHANGED
@@ -1,21 +1,36 @@
1
- require 'open-uri'
2
- require 'pathname'
1
+ require 'fileutils'
3
2
 
4
- require 'weld/compressor'
3
+ class Weld
4
+ HOME_DIR = ENV['WELD_HOME'] || File.join(File.expand_path('~/'), '.weld')
5
+ end
6
+
7
+ require 'weld/cache'
8
+ require 'weld/component'
5
9
  require 'weld/version'
6
10
 
7
11
  class Weld
8
- autoload :Server, 'weld/server'
12
+ autoload :CDN, 'weld/cdn'
13
+ autoload :CLI, 'weld/cli'
14
+ autoload :Compressor, 'weld/compressor'
15
+ autoload :Server, 'weld/server'
9
16
 
10
- attr_reader :config, :config_file
17
+ attr_reader :cache, :config, :config_file
11
18
 
12
19
  def initialize(config_file)
13
20
  # TODO: handle exceptions
14
21
  @config = YAML::load_file(config_file)
15
22
  @config_file = config_file
23
+
24
+ # Create home dir if it doesn't exist.
25
+ FileUtils.mkdir_p(HOME_DIR)
26
+
27
+ @cache = Cache.new
28
+ @component_cache = {}
16
29
  end
17
30
 
18
31
  def component(name)
32
+ return @component_cache[name.to_sym] if @component_cache.has_key?(name.to_sym)
33
+
19
34
  unless definition = @config['components'][name]
20
35
  raise ComponentNotFoundError, "Component not found: #{name}"
21
36
  end
@@ -36,83 +51,17 @@ class Weld
36
51
  css += definition['css'] || []
37
52
  js += definition['js'] || []
38
53
 
39
- Component.new(self, name, :css => css, :js => js, :requires => requires)
40
- end
41
-
42
- class Component
43
- attr_reader :name, :requires
44
- attr_accessor :css, :js
45
-
46
- def initialize(weld, name, config = {})
47
- raise ArgumentError, "weld must be a Weld instance" unless weld.is_a?(Weld)
48
-
49
- @weld = weld
50
- @name = name
51
- @css = config[:css] || []
52
- @js = config[:js] || []
53
- @requires = config[:requires] || []
54
-
55
- @file_cache = {}
56
- end
57
-
58
- def compress(type)
59
- compressors = @weld.config['compressors']
60
-
61
- unless compressors && compressors[type.to_s] &&
62
- compressor_name = compressors[type.to_s]['name'].capitalize.to_sym
63
- raise CompressorError, "No compressor configured"
64
- end
65
-
66
- options = compressors[type.to_s]['options'] || {}
67
- compressor = Compressor.const_get(compressor_name).new(type, options)
68
-
69
- compressor.compress(merge(type))
70
- end
71
-
72
- def merge(type)
73
- raise UnsupportedFileTypeError, "Unsupported file type: #{type}" unless [:css, :js].include?(type.to_sym)
74
-
75
- content = ''
76
- method(type.to_sym).call.each {|f| content << read_file(f) + "\n" }
77
- content
78
- end
79
-
80
- private
81
-
82
- def read_file(filename)
83
- filename = resolve_file(filename)
84
-
85
- if filename.is_a?(URI)
86
- open(filename, 'User-Agent' => "#{APP_NAME}/#{APP_VERSION}").read
87
- else
88
- File.read(filename)
89
- end
90
- end
91
-
92
- def resolve_file(filename)
93
- return @file_cache[filename] if @file_cache.has_key?(filename)
94
-
95
- if filename =~ /^(?:https?|ftp):\/\//
96
- @file_cache[filename] = URI.parse(filename)
97
- else
98
- (@weld.config['sourcePaths'] || []).each do |source_path|
99
- full_path = if Pathname.new(source_path).relative?
100
- File.join(File.dirname(@weld.config_file), source_path, filename)
101
- else
102
- File.join(source_path, filename)
103
- end
104
-
105
- return @file_cache[filename] = File.expand_path(full_path) if File.exist?(full_path)
106
- end
107
-
108
- raise FileNotFoundError, "File not found: #{filename}"
109
- end
110
- end
54
+ @component_cache[name.to_sym] = Component.new(self, name,
55
+ :css => css,
56
+ :js => js,
57
+ :requires => requires
58
+ )
111
59
  end
112
60
 
113
61
  class Error < StandardError; end
114
62
  class ComponentNotFoundError < Error; end
115
63
  class CompressorError < Error; end
64
+ class ConfigError < Error; end
116
65
  class FileNotFoundError < Error; end
117
66
  class UnsupportedFileTypeError < Error; end
118
67
  end
data/lib/weld/cache.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'digest/sha1'
2
+ require 'time'
3
+
4
+ class Weld::Cache
5
+ CACHE_DIR = File.join(Weld::HOME_DIR, 'cache')
6
+
7
+ def initialize
8
+ # Create the cache directory if it doesn't exist.
9
+ FileUtils.mkdir_p(CACHE_DIR)
10
+
11
+ purge_expired
12
+ end
13
+
14
+ def fetch(key)
15
+ now = Time.now
16
+
17
+ Dir["#{CACHE_DIR}/weld.*.#{key_hash(key)}"].each do |filename|
18
+ next unless meta = parse_filename(filename)
19
+
20
+ if meta[:expires] <= now
21
+ File.delete(filename)
22
+ else
23
+ return File.read(filename)
24
+ end
25
+ end
26
+
27
+ nil
28
+ end
29
+
30
+ alias [] fetch
31
+
32
+ def store(key, value, expires = Time.now + 1800)
33
+ File.open(File.join(CACHE_DIR, "weld.#{expires.to_i}.#{key_hash(key)}"), 'w') do |f|
34
+ f.write(value)
35
+ end
36
+
37
+ value
38
+ end
39
+
40
+ alias []= store
41
+
42
+ private
43
+
44
+ def key_hash(key)
45
+ Digest::SHA1.hexdigest(key.to_s)
46
+ end
47
+
48
+ def parse_filename(filename)
49
+ if File.basename(filename) =~ /^weld\.([0-9]+)\.([0-9a-f]{40})$/
50
+ {:expires => Time.at($1.to_i), :hash => $2}
51
+ else
52
+ nil
53
+ end
54
+ end
55
+
56
+ # Deletes expired files from the cache.
57
+ def purge_expired
58
+ now = Time.now
59
+
60
+ Dir["#{CACHE_DIR}/weld.*"].each do |filename|
61
+ next unless meta = parse_filename(filename)
62
+ File.delete(filename) if meta[:expires] <= now
63
+ end
64
+ end
65
+ end
data/lib/weld/cdn.rb ADDED
@@ -0,0 +1,12 @@
1
+ class Weld::CDN
2
+ autoload :S3, 'weld/cdn/s3'
3
+
4
+ attr_reader :options
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ end
9
+
10
+ def push(filename, type, content)
11
+ end
12
+ end
@@ -0,0 +1,57 @@
1
+ require 'aws/s3'
2
+ require 'stringio'
3
+ require 'zlib'
4
+
5
+ class Weld::CDN::S3 < Weld::CDN
6
+ def initialize(options = {})
7
+ super(options)
8
+
9
+ raise Weld::ConfigError, "S3 bucket not specified" unless @options['bucket']
10
+
11
+ @options['access_key_id'] ||= ENV['AMAZON_ACCESS_KEY_ID']
12
+ @options['secret_access_key'] ||= ENV['AMAZON_SECRET_ACCESS_KEY']
13
+
14
+ AWS::S3::Base.establish_connection!(
15
+ :access_key_id => @options['access_key_id'],
16
+ :secret_access_key => @options['secret_access_key']
17
+ )
18
+
19
+ @bucket = if AWS::S3::Bucket.list.include?(@options['bucket'])
20
+ AWS::S3::Bucket.find(@options['bucket'])
21
+ else
22
+ AWS::S3::Bucket.create(@options['bucket'], :access => :public_read)
23
+ AWS::S3::Bucket.find(@options['bucket'])
24
+ end
25
+ end
26
+
27
+ def push(filename, type, content)
28
+ content_type = type == :css ? 'text/css' : 'application/javascript'
29
+ prefix = (@options['prefix'] || {})[type.to_s] || ''
30
+ url_base = @options['url_base'] || "http://#{@options['bucket']}.s3.amazonaws.com/"
31
+
32
+ object_options = {
33
+ :access => :public_read,
34
+ 'Cache-Control' => 'public,max-age=315360000',
35
+ 'Content-Type' => "#{content_type};charset=utf-8",
36
+ 'Expires' => (Time.now + 315360000).httpdate # 10 years from now
37
+ }
38
+
39
+ if @options['gzip']
40
+ content_gzip = StringIO.new
41
+
42
+ gzip = Zlib::GzipWriter.new(content_gzip, 9)
43
+ gzip.write(content)
44
+ gzip.close
45
+
46
+ object_options['Content-Encoding'] = 'gzip'
47
+ end
48
+
49
+ object = @bucket.new_object
50
+ object.key = "#{prefix}#{filename}"
51
+ object.value = @options['gzip'] ? content_gzip.string : content
52
+
53
+ object.store(object_options)
54
+
55
+ File.join(url_base, object.key)
56
+ end
57
+ end
data/lib/weld/cli.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'trollop'
2
+
3
+ class Weld::CLI
4
+ attr_reader :components, :options, :types, :weld
5
+
6
+ def initialize
7
+ parse_args
8
+
9
+ @weld = Weld.new(@options[:config])
10
+ @components ||= @weld.config['components']
11
+ end
12
+
13
+ def cdn_push(filename, type, content)
14
+ init_cdn
15
+ @cdn.push(filename, type, content)
16
+ end
17
+
18
+ def error(message)
19
+ STDERR.puts "Error: #{message}"
20
+ end
21
+
22
+ def info(message)
23
+ puts message
24
+ end
25
+
26
+ def warn(message)
27
+ STDERR.puts "Warning: #{message}"
28
+ end
29
+
30
+ private
31
+
32
+ def init_cdn
33
+ return if @cdn
34
+
35
+ cdn_config = @weld.config['cdn']
36
+
37
+ unless cdn_config && cdn_type = cdn_config['type']
38
+ raise Weld::ConfigError, "No CDN configured"
39
+ end
40
+
41
+ @cdn = Weld::CDN.const_get(cdn_type.capitalize.to_sym).new(cdn_config['options'] || {})
42
+ end
43
+
44
+ def parse_args
45
+ @options = Trollop.options do
46
+ version "#{Weld::APP_NAME} #{Weld::APP_VERSION}\n" << Weld::APP_COPYRIGHT
47
+ banner <<-EOS
48
+ #{Weld::APP_NAME} combines and minifies CSS and JavaScript files at runtime and build time.
49
+
50
+ Usage:
51
+ weld [options] <component> [<component> ...]
52
+
53
+ Options:
54
+ EOS
55
+
56
+ opt :config, "Use the specified config file.", :short => '-c', :default => './weld.yaml'
57
+ opt :no_minify, "Don't perform any minification.", :short => :none
58
+ opt :output_dir, "Write welded files to the specified directory.", :short => '-o', :default => './'
59
+ opt :push, "Push welded files to the configured CDN instead of saving them locally.", :short => '-p'
60
+ opt :type, "Only weld components of the specified type ('css' or 'js').", :short => '-t', :type => :string
61
+ end
62
+
63
+ if @options[:type_given] && !['css', 'js'].include?(@options[:type])
64
+ abort "Error: Unsupported component type: #{@options[:type]}\n" <<
65
+ "Try --help for help."
66
+ end
67
+
68
+ @components = ARGV.length > 0 ? ARGV.dup : nil
69
+ @types = @options[:type_given] ? [@options[:type].to_sym] : [:css, :js]
70
+
71
+ @options
72
+ end
73
+ end
@@ -0,0 +1,89 @@
1
+ require 'open-uri'
2
+ require 'pathname'
3
+
4
+ class Weld::Component
5
+ attr_reader :name, :requires
6
+ attr_accessor :css, :js
7
+
8
+ def initialize(weld, name, config = {})
9
+ raise ArgumentError, "weld must be a Weld instance" unless weld.is_a?(Weld)
10
+
11
+ @weld = weld
12
+ @name = name
13
+ @css = config[:css] || []
14
+ @js = config[:js] || []
15
+ @requires = config[:requires] || []
16
+
17
+ @filename_cache = {}
18
+ end
19
+
20
+ def compress(type)
21
+ compressors = @weld.config['compressors']
22
+
23
+ unless compressors && compressors[type.to_s] &&
24
+ compressor_name = compressors[type.to_s]['name'].capitalize.to_sym
25
+ raise Weld::CompressorError, "No compressor configured"
26
+ end
27
+
28
+ options = compressors[type.to_s]['options'] || {}
29
+ compressor = Weld::Compressor.const_get(compressor_name).new(type, options)
30
+
31
+ compressor.compress(merge(type))
32
+ end
33
+
34
+ def merge(type)
35
+ raise Weld::UnsupportedFileTypeError, "Unsupported file type: #{type}" unless [:css, :js].include?(type.to_sym)
36
+
37
+ content = ''
38
+ method(type.to_sym).call.each {|f| content << read_file(f) + "\n" }
39
+ content
40
+ end
41
+
42
+ private
43
+
44
+ def read_file(filename)
45
+ filename = resolve_filename(filename)
46
+
47
+ if filename.is_a?(URI)
48
+ if cached = @weld.cache[filename]
49
+ return cached
50
+ end
51
+
52
+ open(filename, 'User-Agent' => "#{Weld::APP_NAME}/#{Weld::APP_VERSION}") do |response|
53
+ unless response.status[0] == '200'
54
+ raise Weld::FileNotFoundError, "URL returned HTTP status code #{response.status[0]}: #{filename}"
55
+ end
56
+
57
+ expires = response.meta['expires'] ? Time.parse(response.meta['expires']) : nil
58
+
59
+ if expires
60
+ @weld.cache.store(filename, response.read, expires)
61
+ else
62
+ response.read
63
+ end
64
+ end
65
+ else
66
+ File.read(filename)
67
+ end
68
+ end
69
+
70
+ def resolve_filename(filename)
71
+ return @filename_cache[filename] if @filename_cache.has_key?(filename)
72
+
73
+ if filename =~ /^(?:https?|ftp):\/\//
74
+ @filename_cache[filename] = URI.parse(filename)
75
+ else
76
+ (@weld.config['sourcePaths'] || []).each do |source_path|
77
+ full_path = if Pathname.new(source_path).relative?
78
+ File.join(File.dirname(@weld.config_file), source_path, filename)
79
+ else
80
+ File.join(source_path, filename)
81
+ end
82
+
83
+ return @filename_cache[filename] = File.expand_path(full_path) if File.exist?(full_path)
84
+ end
85
+
86
+ raise Weld::FileNotFoundError, "File not found: #{filename}"
87
+ end
88
+ end
89
+ end
@@ -1,6 +1,6 @@
1
1
  require 'open3'
2
2
 
3
- class Weld; class Compressor
3
+ class Weld::Compressor
4
4
  autoload :Yui, 'weld/compressor/yui'
5
5
 
6
6
  attr_reader :options, :type
@@ -12,5 +12,4 @@ class Weld; class Compressor
12
12
 
13
13
  def compress(input)
14
14
  end
15
-
16
- end; end
15
+ end
data/lib/weld/server.rb CHANGED
@@ -2,12 +2,8 @@ require 'sinatra/base'
2
2
 
3
3
  class Weld::Server < Sinatra::Base
4
4
 
5
- get %r{^/([^/\.]+)\.(css|js)$} do |name, type|
6
- begin
7
- @weld ||= Weld.new(settings.config_file)
8
- rescue => ex
9
- halt 500, ex.to_s
10
- end
5
+ get %r{^/([^/]+)\.(css|js)$} do |name, type|
6
+ @weld ||= Weld.new(settings.config_file)
11
7
 
12
8
  type = type.to_sym
13
9
  no_minify = params.has_key?('no-minify') || params.has_key?('nominify')
@@ -15,22 +11,10 @@ class Weld::Server < Sinatra::Base
15
11
  content_type(type == :css ? 'text/css' : 'application/javascript',
16
12
  :charset => 'utf-8')
17
13
 
18
- begin
19
- if no_minify
20
- @weld.component(name).merge(type)
21
- else
22
- @weld.component(name).compress(type)
23
- end
24
-
25
- rescue Weld::ComponentNotFoundError,
26
- Weld::FileNotFoundError => ex
27
- halt 404, ex.to_s
28
-
29
- rescue Weld::UnsupportedFileTypeError => ex
30
- halt 400, ex.to_s
31
-
32
- rescue => ex
33
- halt 500, ex.to_s
14
+ if no_minify
15
+ @weld.component(name).merge(type)
16
+ else
17
+ @weld.component(name).compress(type)
34
18
  end
35
19
  end
36
20
 
data/lib/weld/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  class Weld
2
2
  APP_NAME = 'Weld'
3
- APP_VERSION = '0.0.1.dev.20100402'
3
+ APP_VERSION = '0.0.1.dev.20100404'
4
4
  APP_AUTHOR = 'Ryan Grove'
5
5
  APP_EMAIL = 'ryan@wonko.com'
6
6
  APP_URL = 'http://wonko.com/'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: weld
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.dev.20100402
4
+ version: 0.0.1.dev.20100404
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove
@@ -9,9 +9,19 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-04-02 00:00:00 -07:00
12
+ date: 2010-04-04 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: aws-s3
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: "0.6"
24
+ version:
15
25
  - !ruby/object:Gem::Dependency
16
26
  name: sinatra
17
27
  type: :runtime
@@ -45,6 +55,11 @@ files:
45
55
  - README.rdoc
46
56
  - bin/weld
47
57
  - examples/simple-config.yaml
58
+ - lib/weld/cache.rb
59
+ - lib/weld/cdn/s3.rb
60
+ - lib/weld/cdn.rb
61
+ - lib/weld/cli.rb
62
+ - lib/weld/component.rb
48
63
  - lib/weld/compressor/yui.rb
49
64
  - lib/weld/compressor.rb
50
65
  - lib/weld/server.rb