dassets 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/README.md +42 -10
  2. data/lib/dassets.rb +16 -28
  3. data/lib/dassets/asset_file.rb +36 -33
  4. data/lib/dassets/default_cache.rb +29 -0
  5. data/lib/dassets/digest_cmd.rb +36 -0
  6. data/lib/dassets/engine.rb +0 -2
  7. data/lib/dassets/file_store.rb +36 -0
  8. data/lib/dassets/runner.rb +2 -9
  9. data/lib/dassets/server/request.rb +5 -5
  10. data/lib/dassets/source_cache.rb +39 -0
  11. data/lib/dassets/source_file.rb +37 -13
  12. data/lib/dassets/version.rb +1 -1
  13. data/test/support/config/assets.rb +1 -0
  14. data/test/system/digest_cmd_run_tests.rb +37 -39
  15. data/test/system/rack_tests.rb +3 -7
  16. data/test/unit/asset_file_tests.rb +72 -40
  17. data/test/unit/config_tests.rb +3 -17
  18. data/test/unit/dassets_tests.rb +8 -42
  19. data/test/unit/default_cache_tests.rb +27 -0
  20. data/test/unit/{cmds/digest_cmd_tests.rb → digest_cmd_tests.rb} +4 -4
  21. data/test/unit/file_store_tests.rb +30 -0
  22. data/test/unit/server/request_tests.rb +7 -11
  23. data/test/unit/server/response_tests.rb +4 -5
  24. data/test/unit/source_cache_tests.rb +50 -0
  25. data/test/unit/source_file_tests.rb +28 -29
  26. metadata +16 -31
  27. data/lib/dassets/cmds/cache_cmd.rb +0 -33
  28. data/lib/dassets/cmds/digest_cmd.rb +0 -53
  29. data/lib/dassets/digests.rb +0 -61
  30. data/test/support/app/assets/.digests +0 -5
  31. data/test/support/app/assets/public/file1.txt +0 -1
  32. data/test/support/app/assets/public/file2.txt +0 -1
  33. data/test/support/app/assets/public/grumpy_cat.jpg +0 -0
  34. data/test/support/app/assets/public/nested/a-thing.txt.no-use +0 -4
  35. data/test/support/app/assets/public/nested/file3.txt +0 -0
  36. data/test/support/app_public/.gitkeep +0 -0
  37. data/test/support/example.digests +0 -3
  38. data/test/system/cache_cmd_run_tests.rb +0 -27
  39. data/test/unit/cmds/cache_cmd_tests.rb +0 -33
  40. data/test/unit/digests_tests.rb +0 -79
data/README.md CHANGED
@@ -17,33 +17,40 @@ 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
- # tell Dassets where to write the digests
21
- c.digests_path '/path/to/.digests' # default: '{source_path}/.digests'
22
-
23
20
  # tell Dassets where to look for source files and (optionally) how to filter those files
24
21
  c.source_path 'lib/asset_files' # default: '{root_path}/app/assets'
25
22
  c.source_filter proc{ |paths| paths.select{ |p| ... } }
26
23
  # --OR--
27
- c.sources 'lib/asset_files' do |paths|
24
+ c.source 'lib/asset_files' do |paths|
28
25
  # return the filtered source path list
29
26
  paths.select{ |p| ... }
30
27
  end
31
28
 
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'
29
+ # (optional) tell Dassets where to store digested asset files
30
+ # if none given, Dassets will not write any digested output
31
+ # use this to "cache" digested assets to the public dir (for example)
32
+ c.file_store 'public' # default: `NullFileStore.new`
35
33
 
36
34
  end
37
35
  ```
38
36
 
39
37
  ### Digest
40
38
 
39
+ You can use the CLI to digest your source files on demand:
40
+
41
41
  ```
42
- $ dassets digest # rebuild the .digests for all asset files, OR
43
- $ dassets digest /path/to/asset/file # update the digest for just one file
42
+ $ dassets digest # digest all source files, OR
43
+ $ dassets digest /path/to/source/file # digest some specific files
44
44
  ```
45
45
 
46
- Use the CLI to build your digests file. Protip: use guard to auto rebuild digests every time you edit an asset file. TODO: link to some guard tools or docs.
46
+ Or you can programmatically digest files as needed:
47
+
48
+ ```ruby
49
+ Dassets.digest_source_files # digest all source files, OR
50
+ Dassets.digest_source_files ['/path/to/source/file'] # digest just some specific files
51
+ ```
52
+
53
+ Digesting involves combining, compiling, fingerprinting, and outputting each source file. Once a source has been digested, it is available for linking, serving, and/or caching.
47
54
 
48
55
  ### Link To
49
56
 
@@ -70,6 +77,31 @@ In production, use the CLI to cache your digested asset files to the public dir:
70
77
  $ dassets cache /path/to/public/dir
71
78
  ```
72
79
 
80
+ TODO: programmatically cache asset files
81
+
82
+ ## Compiling
83
+
84
+ Dassets can handle compiling your asset source as part of its digest pipeline. It does this via "engines". Engines transform source extensions and content.
85
+
86
+ Engines are "registered" with dassets based on source extensions. Name your source file with registered extensions and those engines will be used to compile your source content.
87
+
88
+ ### Some Dassets Engines
89
+
90
+ Examples are key here, so check out some of the Dasset's engines available:
91
+
92
+ TODO
93
+
94
+ ### Creating your own Engine
95
+
96
+ * create a class that subclasses `Dassets::Engine`
97
+ * override the `ext` method to specify how the input source extension should be handled
98
+ * override the `compile` method to specify how the input content should be transformed
99
+ * register your engine class with Dassets
100
+
101
+ ## Combinations
102
+
103
+ TODO
104
+
73
105
  ## Installation
74
106
 
75
107
  Add this line to your application's Gemfile:
@@ -4,61 +4,51 @@ require 'ns-options'
4
4
 
5
5
  require 'dassets/version'
6
6
  require 'dassets/root_path'
7
- require 'dassets/digests'
7
+ require 'dassets/file_store'
8
+ require 'dassets/default_cache'
8
9
  require 'dassets/engine'
10
+ require 'dassets/asset_file'
9
11
 
10
12
  ENV['DASSETS_ASSETS_FILE'] ||= 'config/assets'
11
13
 
12
14
  module Dassets
13
15
 
14
- def self.config; @config ||= Config.new; end
15
- def self.sources; @sources ||= Set.new; end
16
- def self.digests; @digests ||= NullDigests.new; end
17
-
16
+ def self.config; @config ||= Config.new; end
18
17
  def self.configure(&block)
19
18
  block.call(self.config)
20
19
  end
21
20
 
22
- def self.reset
23
- @sources = @digests = nil
24
- end
25
-
26
21
  def self.init
27
22
  require self.config.assets_file
28
- @sources = SourceList.new(self.config)
29
- @digests = Digests.new(self.config.digests_path)
30
23
  end
31
24
 
32
- def self.[](asset_path)
33
- self.digests.asset_file(asset_path)
25
+ def self.[](digest_path)
26
+ AssetFile.new(digest_path)
34
27
  end
35
28
 
36
29
  # Cmds
37
30
 
38
31
  def self.digest_source_files(paths=nil)
39
- require 'dassets/cmds/digest_cmd'
40
- Cmds::DigestCmd.new(paths).run
32
+ require 'dassets/digest_cmd'
33
+ DigestCmd.new(paths).run
41
34
  end
42
35
 
43
36
  class Config
44
37
  include NsOptions::Proxy
45
38
 
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 }
39
+ option :root_path, Pathname, :required => true
40
+ option :assets_file, Pathname, :default => ENV['DASSETS_ASSETS_FILE']
41
+ option :source_path, RootPath, :default => proc{ "app/assets" }
42
+ option :source_filter, Proc, :default => proc{ |paths| paths }
43
+ option :file_store, FileStore, :default => proc{ NullFileStore.new }
53
44
 
54
45
  attr_reader :engines
46
+ attr_accessor :cache
55
47
 
56
48
  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
- })
49
+ super
61
50
  @engines = Hash.new{ |k,v| Dassets::NullEngine.new }
51
+ @cache = DefaultCache.new
62
52
  end
63
53
 
64
54
  def source(path=nil, &filter)
@@ -76,8 +66,6 @@ module Dassets
76
66
  paths = Set.new
77
67
  paths += Dir.glob(File.join(config.source_path, "**/*"))
78
68
  paths.reject!{ |path| !File.file?(path) }
79
- paths.reject!{ |path| path =~ /^#{config.output_path}/ }
80
- paths.reject!{ |path| path =~ /^#{config.digests_path}/ }
81
69
 
82
70
  config.source_filter.call(paths).sort
83
71
  end
@@ -1,66 +1,69 @@
1
- require 'digest/md5'
2
1
  require 'rack/utils'
3
2
  require 'rack/mime'
3
+ require 'dassets/source_cache'
4
4
 
5
5
  module Dassets; end
6
6
  class Dassets::AssetFile
7
7
 
8
- def self.from_abs_path(abs_path)
9
- rel_path = abs_path.sub("#{Dassets.config.output_path}/", '')
10
- md5 = Digest::MD5.file(abs_path).hexdigest
11
- self.new(rel_path, md5)
8
+ attr_reader :digest_path, :dirname, :extname, :basename, :source_cache
9
+
10
+ def initialize(digest_path)
11
+ @digest_path = digest_path
12
+ @dirname = File.dirname(@digest_path)
13
+ @extname = File.extname(@digest_path)
14
+ @basename = File.basename(@digest_path, @extname)
15
+ @source_cache = Dassets::SourceCache.new(@digest_path, Dassets.config.cache)
12
16
  end
13
17
 
14
- attr_reader :path, :md5, :dirname, :extname, :basename
15
- attr_reader :output_path, :url, :href
18
+ def digest!
19
+ return if !self.exists?
20
+ Dassets.config.file_store.save(self.url){ self.content }
21
+ end
16
22
 
17
- def initialize(rel_path, md5)
18
- @path, @md5 = rel_path, md5
19
- @dirname = File.dirname(@path)
20
- @extname = File.extname(@path)
21
- @basename = File.basename(@path, @extname)
23
+ def url
24
+ @url ||= begin
25
+ url_basename = "#{@basename}-#{self.fingerprint}#{@extname}"
26
+ File.join(@dirname, url_basename).sub(/^\.\//, '').sub(/^\//, '')
27
+ end
28
+ end
22
29
 
23
- @output_path = File.join(Dassets.config.output_path, @path)
30
+ def href
31
+ @href ||= "/#{self.url}"
32
+ end
24
33
 
25
- url_basename = "#{@basename}-#{@md5}#{@extname}"
26
- @url = File.join(@dirname, url_basename).sub(/^\.\//, '').sub(/^\//, '')
27
- @href = "/#{@url}"
34
+ def fingerprint
35
+ return nil if !self.exists?
36
+ @fingerprint ||= @source_cache.fingerprint
28
37
  end
29
38
 
30
39
  def content
31
- @content ||= if File.exists?(@output_path) && File.file?(@output_path)
32
- File.read(@output_path)
33
- end
40
+ return nil if !self.exists?
41
+ @content ||= @source_cache.content
34
42
  end
35
43
 
36
44
  def mtime
37
- @mtime ||= if File.exists?(@output_path) && File.file?(@output_path)
38
- File.mtime(@output_path).httpdate
39
- end
45
+ return nil if !self.exists?
46
+ @mtime ||= @source_cache.mtime
40
47
  end
41
48
 
42
- # We check via File::size? whether this file provides size info via stat,
43
- # otherwise we have to figure it out by reading the whole file into memory.
44
49
  def size
45
- @size ||= if File.exists?(@output_path) && File.file?(@output_path)
46
- File.size?(@output_path) || Rack::Utils.bytesize(self.content)
47
- end
50
+ return nil if !self.exists?
51
+ @size ||= Rack::Utils.bytesize(self.content)
48
52
  end
49
53
 
50
54
  def mime_type
51
- @mime_type ||= if File.exists?(@output_path) && File.file?(@output_path)
52
- Rack::Mime.mime_type(@extname)
53
- end
55
+ return nil if !self.exists?
56
+ @mime_type ||= Rack::Mime.mime_type(@extname)
54
57
  end
55
58
 
56
59
  def exists?
57
- File.exists?(@output_path) && File.file?(@output_path)
60
+ @source_cache.exists?
58
61
  end
59
62
 
60
63
  def ==(other_asset_file)
61
64
  other_asset_file.kind_of?(Dassets::AssetFile) &&
62
- self.path == other_asset_file.path &&
63
- self.md5 == other_asset_file.md5
65
+ self.digest_path == other_asset_file.digest_path &&
66
+ self.fingerprint == other_asset_file.fingerprint
64
67
  end
65
68
 
66
69
  end
@@ -0,0 +1,29 @@
1
+ require 'thread'
2
+
3
+ # this is a thread-safe in-memory cache for use with the `SourceCache` that
4
+ # only caches source fingerprint keys. there are a few reasons for using this
5
+ # as the "default":
6
+ # * source fingerprints are accessed more frequently than contents (ie hrefs,
7
+ # urls, etc) so caching them can have nice affects on performance. Plus it
8
+ # seems silly to have to compile the source file everytime you want to get its
9
+ # href so you can link it in.
10
+ # * fingerprints have a much smaller data size so won't overly bloat memory.
11
+
12
+ class Dassets::DefaultCache
13
+
14
+ def initialize
15
+ @hash = {}
16
+ @write_mutex = ::Mutex.new
17
+ end
18
+
19
+ def keys; @hash.keys; end
20
+ def [](key); @hash[key]; end
21
+
22
+ def []=(key, value)
23
+ @write_mutex.synchronize do
24
+ @hash[key] = value if key =~ /-- fingerprint$/ # only write fingerprint keys
25
+ end
26
+ end
27
+
28
+ end
29
+
@@ -0,0 +1,36 @@
1
+ require 'fileutils'
2
+ require 'dassets'
3
+ require 'dassets/source_file'
4
+
5
+ module Dassets; end
6
+ class Dassets::DigestCmd
7
+
8
+ attr_reader :paths
9
+
10
+ def initialize(abs_paths)
11
+ @paths = abs_paths || []
12
+ end
13
+
14
+ def run(io=nil)
15
+ files = @paths
16
+ if @paths.empty?
17
+ # always get the latest source list
18
+ files = Dassets::SourceList.new(Dassets.config)
19
+ end
20
+
21
+ log io, "digesting #{files.count} source file(s) ..."
22
+ digest_the_files(files)
23
+ end
24
+
25
+ private
26
+
27
+ def digest_the_files(files)
28
+ files.each{ |f| Dassets::SourceFile.new(f).asset_file.digest! }
29
+ end
30
+
31
+ def log(io, msg)
32
+ io.puts msg if io
33
+ end
34
+
35
+ end
36
+
@@ -19,7 +19,6 @@ module Dassets
19
19
  end
20
20
 
21
21
  class NullEngine < Engine
22
-
23
22
  def ext(input_ext)
24
23
  input_ext
25
24
  end
@@ -27,7 +26,6 @@ module Dassets
27
26
  def compile(input)
28
27
  input
29
28
  end
30
-
31
29
  end
32
30
 
33
31
  end
@@ -0,0 +1,36 @@
1
+ require 'thread'
2
+ require 'dassets/root_path'
3
+
4
+ module Dassets; end
5
+ class Dassets::FileStore
6
+ attr_reader :root
7
+
8
+ def initialize(root)
9
+ @root = Dassets::RootPath.new(root)
10
+ @save_mutex = ::Mutex.new
11
+ end
12
+
13
+ def save(url, &block)
14
+ @save_mutex.synchronize do
15
+ store_path(url).tap do |path|
16
+ FileUtils.mkdir_p(File.dirname(path))
17
+ File.open(path, "w"){ |f| f.write(block.call) }
18
+ end
19
+ end
20
+ end
21
+
22
+ def store_path(url)
23
+ File.join(@root, url)
24
+ end
25
+
26
+ end
27
+
28
+ class Dassets::NullFileStore < Dassets::FileStore
29
+ def initialize
30
+ super('')
31
+ end
32
+
33
+ def save(url, &block)
34
+ store_path(url) # no-op, just return the store path like the base does
35
+ end
36
+ end
@@ -20,16 +20,9 @@ class Dassets::Runner
20
20
 
21
21
  case @cmd_name
22
22
  when 'digest'
23
- require 'dassets/cmds/digest_cmd'
23
+ require 'dassets/digest_cmd'
24
24
  abs_paths = @cmd_args.map{ |path| File.expand_path(path, @pwd) }
25
- Dassets::Cmds::DigestCmd.new(abs_paths).run($stdout)
26
- when 'cache'
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)
25
+ Dassets::DigestCmd.new(abs_paths).run($stdout)
33
26
  when 'null'
34
27
  NullCommand.new.run
35
28
  else
@@ -14,10 +14,10 @@ class Dassets::Server
14
14
  # Determine if the request is for an asset file
15
15
  # This will be called on every request so speed is an issue
16
16
  # - first check if the request is a GET or HEAD (fast)
17
- # - then check if for a digest resource (kinda fast)
18
- # - then check if on a path in the digests (slower)
17
+ # - then check if for a digested asset resource (kinda fast)
18
+ # - then check if source exists for the digested asset (slower)
19
19
  def for_asset_file?
20
- !!((get? || head?) && for_digest_file? && Dassets.digests[asset_path])
20
+ !!((get? || head?) && for_digested_asset? && asset_file.source_cache.exists?)
21
21
  end
22
22
 
23
23
  def asset_path
@@ -30,8 +30,8 @@ class Dassets::Server
30
30
 
31
31
  private
32
32
 
33
- def for_digest_file?
34
- !path_digest_match.nil?
33
+ def for_digested_asset?
34
+ !path_digest_match.captures.empty?
35
35
  end
36
36
 
37
37
  def path_digest_match
@@ -0,0 +1,39 @@
1
+ require 'dassets/source_file'
2
+
3
+ module Dassets; end
4
+ class Dassets::SourceCache
5
+
6
+ attr_reader :digest_path, :source_file, :cache
7
+
8
+ def initialize(digest_path, cache=nil)
9
+ @digest_path = digest_path
10
+ @source_file = Dassets::SourceFile.find_by_digest_path(digest_path)
11
+ @cache = cache || NoCache.new
12
+ end
13
+
14
+ def content
15
+ @cache["#{self.key} -- content"] ||= @source_file.compiled
16
+ end
17
+
18
+ def fingerprint
19
+ @cache["#{self.key} -- fingerprint"] ||= @source_file.fingerprint
20
+ end
21
+
22
+ def key
23
+ "#{self.digest_path} -- #{self.mtime}"
24
+ end
25
+
26
+ def mtime
27
+ @source_file.mtime
28
+ end
29
+
30
+ def exists?
31
+ @source_file.exists?
32
+ end
33
+
34
+ class NoCache
35
+ def [](key); end
36
+ def []=(key, value); end
37
+ end
38
+
39
+ end