dassets 0.3.0 → 0.4.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 (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