dassets 0.2.0 → 0.3.0

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.
Files changed (42) hide show
  1. data/README.md +15 -5
  2. data/lib/dassets.rb +59 -12
  3. data/lib/dassets/asset_file.rb +15 -14
  4. data/lib/dassets/cmds/cache_cmd.rb +33 -0
  5. data/lib/dassets/cmds/digest_cmd.rb +53 -0
  6. data/lib/dassets/{digests_file.rb → digests.rb} +14 -23
  7. data/lib/dassets/engine.rb +33 -0
  8. data/lib/dassets/runner.rb +10 -4
  9. data/lib/dassets/server/response.rb +1 -1
  10. data/lib/dassets/source_file.rb +71 -0
  11. data/lib/dassets/version.rb +1 -1
  12. data/test/support/app/assets/.digests +1 -0
  13. data/test/support/app/assets/file1.txt +1 -0
  14. data/test/support/app/assets/file2.txt +1 -0
  15. data/test/support/app/assets/grumpy_cat.jpg +0 -0
  16. data/test/support/app/assets/nested/a-thing.txt.useless.dumb +1 -0
  17. data/test/support/app/assets/nested/file3.txt +0 -0
  18. data/test/support/app/assets/public/nested/a-thing.txt.no-use +4 -0
  19. data/test/support/config/assets.rb +12 -1
  20. data/test/support/example.digests +3 -3
  21. data/test/support/public/nested/a-thing.txt-7413d18f2eba9c695a880aff67fde135.no-use +4 -0
  22. data/test/support/source_files/_ignored.txt +0 -0
  23. data/test/support/source_files/nested/_nested_ignored.txt +0 -0
  24. data/test/support/source_files/nested/test2.txt +0 -0
  25. data/test/support/source_files/test1.txt +0 -0
  26. data/test/system/cache_cmd_run_tests.rb +27 -0
  27. data/test/system/digest_cmd_run_tests.rb +70 -0
  28. data/test/unit/asset_file_tests.rb +11 -11
  29. data/test/unit/cmds/cache_cmd_tests.rb +33 -0
  30. data/test/unit/cmds/digest_cmd_tests.rb +23 -0
  31. data/test/unit/config_tests.rb +52 -7
  32. data/test/unit/dassets_tests.rb +59 -5
  33. data/test/unit/digests_tests.rb +79 -0
  34. data/test/unit/engine_tests.rb +59 -0
  35. data/test/unit/server/response_tests.rb +5 -5
  36. data/test/unit/source_file_tests.rb +82 -0
  37. metadata +45 -13
  38. data/lib/dassets/runner/cache_command.rb +0 -46
  39. data/lib/dassets/runner/digest_command.rb +0 -65
  40. data/test/unit/digests_file_tests.rb +0 -90
  41. data/test/unit/runner/cache_command_tests.rb +0 -62
  42. data/test/unit/runner/digest_command_tests.rb +0 -83
data/README.md CHANGED
@@ -17,11 +17,21 @@ Dassets.configure do |c|
17
17
  # tell Dassets what the root path of your app is
18
18
  c.root_path '/path/to/app/root'
19
19
 
20
- # it works best to *not* keep the asset files in your public dir
21
- c.files_path '/path/to/not/public' # default: '{root_path}/app/assets/public'
22
-
23
- # you can choose the file to write the digests to, if you want
24
- c.digests_file_path '/path/to/.digests' # default: '{files_path}/app/assets/.digests'
20
+ # tell Dassets where to write the digests
21
+ c.digests_path '/path/to/.digests' # default: '{source_path}/.digests'
22
+
23
+ # tell Dassets where to look for source files and (optionally) how to filter those files
24
+ c.source_path 'lib/asset_files' # default: '{root_path}/app/assets'
25
+ c.source_filter proc{ |paths| paths.select{ |p| ... } }
26
+ # --OR--
27
+ c.sources 'lib/asset_files' do |paths|
28
+ # return the filtered source path list
29
+ paths.select{ |p| ... }
30
+ end
31
+
32
+ # tell Dassets where to write output files to
33
+ # it works best to *not* output to your public dir if using fingerprinting
34
+ c.output_path '/lib/assets_output' # default: '{source_path}/public'
25
35
 
26
36
  end
27
37
  ```
@@ -1,39 +1,86 @@
1
1
  require 'pathname'
2
+ require 'set'
2
3
  require 'ns-options'
3
4
 
4
5
  require 'dassets/version'
5
6
  require 'dassets/root_path'
6
- require 'dassets/digests_file'
7
+ require 'dassets/digests'
8
+ require 'dassets/engine'
7
9
 
8
10
  ENV['DASSETS_ASSETS_FILE'] ||= 'config/assets'
9
11
 
10
12
  module Dassets
11
13
 
12
- def self.config; Config; end
13
- def self.configure(&block); Config.define(&block); end
14
+ def self.config; @config ||= Config.new; end
15
+ def self.sources; @sources ||= Set.new; end
16
+ def self.digests; @digests ||= NullDigests.new; end
14
17
 
15
- def self.init
16
- require self.config.assets_file
17
- @digests_file = DigestsFile.new(self.config.digests_file_path)
18
+ def self.configure(&block)
19
+ block.call(self.config)
18
20
  end
19
21
 
20
22
  def self.reset
21
- @digests_file = nil
23
+ @sources = @digests = nil
24
+ end
25
+
26
+ def self.init
27
+ require self.config.assets_file
28
+ @sources = SourceList.new(self.config)
29
+ @digests = Digests.new(self.config.digests_path)
22
30
  end
23
31
 
24
- def self.digests; @digests_file || NullDigestsFile.new; end
25
32
  def self.[](asset_path)
26
33
  self.digests.asset_file(asset_path)
27
34
  end
28
35
 
36
+ # Cmds
37
+
38
+ def self.digest_source_files(paths=nil)
39
+ require 'dassets/cmds/digest_cmd'
40
+ Cmds::DigestCmd.new(paths).run
41
+ end
42
+
29
43
  class Config
30
44
  include NsOptions::Proxy
31
45
 
32
- option :assets_file, Pathname, :default => ENV['DASSETS_ASSETS_FILE']
33
- option :root_path, Pathname, :required => true
34
- option :files_path, RootPath, :default => proc{ "app/assets/public" }
35
- option :digests_file_path, RootPath, :default => proc{ "app/assets/.digests" }
46
+ option :root_path, Pathname, :required => true
47
+ option :digests_path, Pathname, :required => true
48
+ option :output_path, RootPath, :required => true
49
+
50
+ option :assets_file, Pathname, :default => ENV['DASSETS_ASSETS_FILE']
51
+ option :source_path, RootPath, :default => proc{ "app/assets" }
52
+ option :source_filter, Proc, :default => proc{ |paths| paths }
53
+
54
+ attr_reader :engines
55
+
56
+ def initialize
57
+ super({
58
+ :digests_path => proc{ File.join(self.source_path, '.digests') },
59
+ :output_path => proc{ File.join(self.source_path, 'public') }
60
+ })
61
+ @engines = Hash.new{ |k,v| Dassets::NullEngine.new }
62
+ end
63
+
64
+ def source(path=nil, &filter)
65
+ self.source_path = path if path
66
+ self.source_filter = filter if filter
67
+ end
68
+
69
+ def engine(input_ext, engine_class, opts=nil)
70
+ @engines[input_ext.to_s] = engine_class.new(opts)
71
+ end
72
+ end
73
+
74
+ module SourceList
75
+ def self.new(config)
76
+ paths = Set.new
77
+ paths += Dir.glob(File.join(config.source_path, "**/*"))
78
+ paths.reject!{ |path| !File.file?(path) }
79
+ paths.reject!{ |path| path =~ /^#{config.output_path}/ }
80
+ paths.reject!{ |path| path =~ /^#{config.digests_path}/ }
36
81
 
82
+ config.source_filter.call(paths).sort
83
+ end
37
84
  end
38
85
 
39
86
  end
@@ -6,13 +6,13 @@ module Dassets; end
6
6
  class Dassets::AssetFile
7
7
 
8
8
  def self.from_abs_path(abs_path)
9
- rel_path = abs_path.sub("#{Dassets.config.files_path}/", '')
9
+ rel_path = abs_path.sub("#{Dassets.config.output_path}/", '')
10
10
  md5 = Digest::MD5.file(abs_path).hexdigest
11
11
  self.new(rel_path, md5)
12
12
  end
13
13
 
14
14
  attr_reader :path, :md5, :dirname, :extname, :basename
15
- attr_reader :files_path, :cache_path, :href
15
+ attr_reader :output_path, :url, :href
16
16
 
17
17
  def initialize(rel_path, md5)
18
18
  @path, @md5 = rel_path, md5
@@ -20,40 +20,41 @@ class Dassets::AssetFile
20
20
  @extname = File.extname(@path)
21
21
  @basename = File.basename(@path, @extname)
22
22
 
23
- file_name = "#{@basename}-#{@md5}#{@extname}"
24
- @files_path = File.join(Dassets.config.files_path, @path)
25
- @cache_path = File.join(@dirname, file_name).sub(/^\.\//, '').sub(/^\//, '')
26
- @href = "/#{@cache_path}"
23
+ @output_path = File.join(Dassets.config.output_path, @path)
24
+
25
+ url_basename = "#{@basename}-#{@md5}#{@extname}"
26
+ @url = File.join(@dirname, url_basename).sub(/^\.\//, '').sub(/^\//, '')
27
+ @href = "/#{@url}"
27
28
  end
28
29
 
29
30
  def content
30
- @content ||= if File.exists?(@files_path) && File.file?(@files_path)
31
- File.read(@files_path)
31
+ @content ||= if File.exists?(@output_path) && File.file?(@output_path)
32
+ File.read(@output_path)
32
33
  end
33
34
  end
34
35
 
35
36
  def mtime
36
- @mtime ||= if File.exists?(@files_path) && File.file?(@files_path)
37
- File.mtime(@files_path).httpdate
37
+ @mtime ||= if File.exists?(@output_path) && File.file?(@output_path)
38
+ File.mtime(@output_path).httpdate
38
39
  end
39
40
  end
40
41
 
41
42
  # We check via File::size? whether this file provides size info via stat,
42
43
  # otherwise we have to figure it out by reading the whole file into memory.
43
44
  def size
44
- @size ||= if File.exists?(@files_path) && File.file?(@files_path)
45
- File.size?(@files_path) || Rack::Utils.bytesize(self.content)
45
+ @size ||= if File.exists?(@output_path) && File.file?(@output_path)
46
+ File.size?(@output_path) || Rack::Utils.bytesize(self.content)
46
47
  end
47
48
  end
48
49
 
49
50
  def mime_type
50
- @mime_type ||= if File.exists?(@files_path) && File.file?(@files_path)
51
+ @mime_type ||= if File.exists?(@output_path) && File.file?(@output_path)
51
52
  Rack::Mime.mime_type(@extname)
52
53
  end
53
54
  end
54
55
 
55
56
  def exists?
56
- File.exists?(@files_path) && File.file?(@files_path)
57
+ File.exists?(@output_path) && File.file?(@output_path)
57
58
  end
58
59
 
59
60
  def ==(other_asset_file)
@@ -0,0 +1,33 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+ require 'dassets'
4
+ require 'dassets/digests'
5
+
6
+ module Dassets; end
7
+ class Dassets::Cmds; end
8
+ class Dassets::Cmds::CacheCmd
9
+
10
+ attr_reader :cache_root_path, :digests
11
+
12
+ def initialize(cache_root_path)
13
+ @cache_root_path = Pathname.new(cache_root_path)
14
+ @digests = Dassets::Digests.new(Dassets.config.digests_path)
15
+ end
16
+
17
+ def run(io=nil)
18
+ log io, "caching #{@digests.asset_files.count} asset file(s) to `#{@cache_root_path}` ..."
19
+ @digests.asset_files.each do |file|
20
+ cache_path = @cache_root_path.join(file.url).to_s
21
+
22
+ FileUtils.mkdir_p File.dirname(cache_path)
23
+ FileUtils.cp(file.output_path, cache_path)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def log(io, msg)
30
+ io.puts msg if io
31
+ end
32
+
33
+ end
@@ -0,0 +1,53 @@
1
+ require 'fileutils'
2
+ require 'dassets'
3
+ require 'dassets/source_file'
4
+
5
+ module Dassets; end
6
+ class Dassets::Cmds; end
7
+ class Dassets::Cmds::DigestCmd
8
+
9
+ attr_reader :paths
10
+
11
+ def initialize(abs_paths)
12
+ @paths = abs_paths || []
13
+ end
14
+
15
+ def run(io=nil)
16
+ files = paths
17
+
18
+ if @paths.empty?
19
+ log io, "clearing `#{Dassets.config.output_path}`"
20
+ clear_output_path(Dassets.config.output_path)
21
+
22
+ # always clear the digests in use
23
+ log io, "clearing `#{Dassets.digests.file_path}`"
24
+ clear_digests(Dassets.digests)
25
+
26
+ # always get the latest source list
27
+ files = Dassets::SourceList.new(Dassets.config)
28
+ end
29
+
30
+ log io, "digesting #{files.count} source file(s) ..."
31
+ digest_the_files(files)
32
+ end
33
+
34
+ private
35
+
36
+ def clear_output_path(path)
37
+ Dir.glob(File.join(path, '*')).each{ |p| FileUtils.rm_r(p) } if path
38
+ end
39
+
40
+ def clear_digests(digests)
41
+ digests.clear.save! if digests
42
+ end
43
+
44
+ def digest_the_files(files)
45
+ files.map{ |f| Dassets::SourceFile.new(f).digest }
46
+ end
47
+
48
+ def log(io, msg)
49
+ io.puts msg if io
50
+ end
51
+
52
+ end
53
+
@@ -2,40 +2,33 @@ require 'dassets/asset_file'
2
2
 
3
3
  module Dassets
4
4
 
5
- class DigestsFile
5
+ class Digests
6
6
 
7
- attr_reader :path
7
+ attr_reader :file_path
8
8
 
9
9
  def initialize(file_path)
10
- @path, @hash = file_path, decode(file_path)
10
+ @file_path, @hash = file_path, decode(file_path)
11
11
  end
12
12
 
13
- def [](*args); @hash.send('[]', *args); end
14
- def []=(*args); @hash.send('[]=', *args); end
15
- def delete(*args); @hash.delete(*args); end
13
+ def [](*args); @hash.send('[]', *args); end
14
+ def []=(*args); @hash.send('[]=', *args); end
15
+ def delete(*args); @hash.delete(*args); end
16
+ def clear(*args); @hash.clear(*args); self end
16
17
 
17
- def each(*args, &block); @hash.each(*args, &block); end
18
-
19
- def keys; @hash.keys; end
20
- def values; @hash.values; end
21
- def empty?; @hash.empty?; end
18
+ def paths
19
+ @hash.keys
20
+ end
22
21
 
23
22
  def asset_files
24
- @hash.map{ |path, md5| Dassets::AssetFile.new(path, md5) }
23
+ self.paths.map{ |path| self.asset_file(path) }
25
24
  end
26
25
 
27
26
  def asset_file(path)
28
27
  Dassets::AssetFile.new(path, @hash[path] || '')
29
28
  end
30
29
 
31
- def to_hash
32
- Hash.new.tap do |to_hash|
33
- @hash.each{ |k, v| to_hash[k] = v }
34
- end
35
- end
36
-
37
30
  def save!
38
- encode(@hash, @path)
31
+ encode(@hash, @file_path)
39
32
  end
40
33
 
41
34
  private
@@ -59,12 +52,10 @@ module Dassets
59
52
 
60
53
  end
61
54
 
62
- module NullDigestsFile
63
-
55
+ module NullDigests
64
56
  def self.new
65
- DigestsFile.new('/dev/null')
57
+ Digests.new('/dev/null')
66
58
  end
67
-
68
59
  end
69
60
 
70
61
  end
@@ -0,0 +1,33 @@
1
+ module Dassets
2
+
3
+ class Engine
4
+
5
+ attr_reader :opts
6
+
7
+ def initialize(opts=nil)
8
+ @opts = opts || {}
9
+ end
10
+
11
+ def ext(input_ext)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def compile(input)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ end
20
+
21
+ class NullEngine < Engine
22
+
23
+ def ext(input_ext)
24
+ input_ext
25
+ end
26
+
27
+ def compile(input)
28
+ input
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -12,6 +12,7 @@ class Dassets::Runner
12
12
  @opts = opts
13
13
  @cmd_name = args.shift || ""
14
14
  @cmd_args = args
15
+ @pwd = ENV['PWD']
15
16
  end
16
17
 
17
18
  def run
@@ -19,11 +20,16 @@ class Dassets::Runner
19
20
 
20
21
  case @cmd_name
21
22
  when 'digest'
22
- require 'dassets/runner/digest_command'
23
- DigestCommand.new(@cmd_args).run
23
+ require 'dassets/cmds/digest_cmd'
24
+ abs_paths = @cmd_args.map{ |path| File.expand_path(path, @pwd) }
25
+ Dassets::Cmds::DigestCmd.new(abs_paths).run($stdout)
24
26
  when 'cache'
25
- require 'dassets/runner/cache_command'
26
- CacheCommand.new(@cmd_args.first).run
27
+ require 'dassets/cmds/cache_cmd'
28
+ cache_root_path = File.expand_path(@cmd_args.first, @pwd)
29
+ unless cache_root_path && File.directory?(cache_root_path)
30
+ raise CmdError, "specify an existing cache directory"
31
+ end
32
+ Dassets::Cmds::CacheCmd.new(cache_root_path).run($stdout)
27
33
  when 'null'
28
34
  NullCommand.new.run
29
35
  else
@@ -14,7 +14,7 @@ class Dassets::Server
14
14
  @status, @headers, @body = if env['HTTP_IF_MODIFIED_SINCE'] == mtime
15
15
  [ 304, Rack::Utils::HeaderHash.new, [] ]
16
16
  elsif !@asset_file.exists?
17
- [ 404, Rack::Utils::HeaderHash.new, [] ]
17
+ [ 404, Rack::Utils::HeaderHash.new, ["Not Found"] ]
18
18
  else
19
19
  [ 200,
20
20
  Rack::Utils::HeaderHash.new.tap do |h|
@@ -0,0 +1,71 @@
1
+ require 'digest/md5'
2
+ require 'fileutils'
3
+ require 'dassets'
4
+ require 'dassets/asset_file'
5
+
6
+ module Dassets
7
+
8
+ class SourceFile
9
+
10
+ attr_reader :file_path
11
+
12
+ def initialize(file_path)
13
+ @file_path = file_path
14
+ @ext_list = File.basename(@file_path).split('.').reverse
15
+ end
16
+
17
+ def exists?
18
+ File.file?(@file_path)
19
+ end
20
+
21
+ def digest
22
+ return if !self.exists?
23
+
24
+ Dassets::AssetFile.new(self.digest_path, self.fingerprint).tap do |asset_file|
25
+ FileUtils.mkdir_p(File.dirname(asset_file.output_path))
26
+ File.open(asset_file.output_path, "w"){ |f| f.write(self.compiled) }
27
+ Dassets.digests[self.digest_path] = self.fingerprint
28
+ Dassets.digests.save!
29
+ end
30
+ end
31
+
32
+ def digest_path
33
+ @digest_path ||= begin
34
+ digest_basename = @ext_list.inject([]) do |digest_ext_list, ext|
35
+ digest_ext_list << Dassets.config.engines[ext].ext(ext)
36
+ end.reject{ |e| e.empty? }.reverse.join('.')
37
+
38
+ File.join([
39
+ digest_dirname(@file_path, Dassets.config.source_path),
40
+ digest_basename
41
+ ].reject{ |p| p.empty? })
42
+ end
43
+ end
44
+
45
+ def compiled
46
+ @compiled ||= @ext_list.inject(read_file(@file_path)) do |content, ext|
47
+ Dassets.config.engines[ext].compile(content)
48
+ end
49
+ end
50
+
51
+ def fingerprint
52
+ @fingerprint ||= Digest::MD5.new.hexdigest(self.compiled)
53
+ end
54
+
55
+ private
56
+
57
+ def digest_dirname(file_path, source_path)
58
+ slash_path(File.dirname(file_path)).sub(slash_path(source_path), '')
59
+ end
60
+
61
+ def slash_path(path)
62
+ File.join(path, '')
63
+ end
64
+
65
+ def read_file(path)
66
+ File.send(File.respond_to?(:binread) ? :binread : :read, path.to_s)
67
+ end
68
+
69
+ end
70
+
71
+ end