dassets 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +42 -10
- data/lib/dassets.rb +16 -28
- data/lib/dassets/asset_file.rb +36 -33
- data/lib/dassets/default_cache.rb +29 -0
- data/lib/dassets/digest_cmd.rb +36 -0
- data/lib/dassets/engine.rb +0 -2
- data/lib/dassets/file_store.rb +36 -0
- data/lib/dassets/runner.rb +2 -9
- data/lib/dassets/server/request.rb +5 -5
- data/lib/dassets/source_cache.rb +39 -0
- data/lib/dassets/source_file.rb +37 -13
- data/lib/dassets/version.rb +1 -1
- data/test/support/config/assets.rb +1 -0
- data/test/system/digest_cmd_run_tests.rb +37 -39
- data/test/system/rack_tests.rb +3 -7
- data/test/unit/asset_file_tests.rb +72 -40
- data/test/unit/config_tests.rb +3 -17
- data/test/unit/dassets_tests.rb +8 -42
- data/test/unit/default_cache_tests.rb +27 -0
- data/test/unit/{cmds/digest_cmd_tests.rb → digest_cmd_tests.rb} +4 -4
- data/test/unit/file_store_tests.rb +30 -0
- data/test/unit/server/request_tests.rb +7 -11
- data/test/unit/server/response_tests.rb +4 -5
- data/test/unit/source_cache_tests.rb +50 -0
- data/test/unit/source_file_tests.rb +28 -29
- metadata +16 -31
- data/lib/dassets/cmds/cache_cmd.rb +0 -33
- data/lib/dassets/cmds/digest_cmd.rb +0 -53
- data/lib/dassets/digests.rb +0 -61
- data/test/support/app/assets/.digests +0 -5
- data/test/support/app/assets/public/file1.txt +0 -1
- data/test/support/app/assets/public/file2.txt +0 -1
- data/test/support/app/assets/public/grumpy_cat.jpg +0 -0
- data/test/support/app/assets/public/nested/a-thing.txt.no-use +0 -4
- data/test/support/app/assets/public/nested/file3.txt +0 -0
- data/test/support/app_public/.gitkeep +0 -0
- data/test/support/example.digests +0 -3
- data/test/system/cache_cmd_run_tests.rb +0 -27
- data/test/unit/cmds/cache_cmd_tests.rb +0 -33
- 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.
|
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
|
33
|
-
#
|
34
|
-
|
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
|
43
|
-
$ dassets digest /path/to/
|
42
|
+
$ dassets digest # digest all source files, OR
|
43
|
+
$ dassets digest /path/to/source/file # digest some specific files
|
44
44
|
```
|
45
45
|
|
46
|
-
|
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:
|
data/lib/dassets.rb
CHANGED
@@ -4,61 +4,51 @@ require 'ns-options'
|
|
4
4
|
|
5
5
|
require 'dassets/version'
|
6
6
|
require 'dassets/root_path'
|
7
|
-
require 'dassets/
|
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;
|
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.[](
|
33
|
-
|
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/
|
40
|
-
|
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,
|
47
|
-
option :
|
48
|
-
option :
|
49
|
-
|
50
|
-
option :
|
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
|
data/lib/dassets/asset_file.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
15
|
-
|
18
|
+
def digest!
|
19
|
+
return if !self.exists?
|
20
|
+
Dassets.config.file_store.save(self.url){ self.content }
|
21
|
+
end
|
16
22
|
|
17
|
-
def
|
18
|
-
@
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
30
|
+
def href
|
31
|
+
@href ||= "/#{self.url}"
|
32
|
+
end
|
24
33
|
|
25
|
-
|
26
|
-
|
27
|
-
@
|
34
|
+
def fingerprint
|
35
|
+
return nil if !self.exists?
|
36
|
+
@fingerprint ||= @source_cache.fingerprint
|
28
37
|
end
|
29
38
|
|
30
39
|
def content
|
31
|
-
|
32
|
-
|
33
|
-
end
|
40
|
+
return nil if !self.exists?
|
41
|
+
@content ||= @source_cache.content
|
34
42
|
end
|
35
43
|
|
36
44
|
def mtime
|
37
|
-
|
38
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
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.
|
63
|
-
self.
|
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
|
+
|
data/lib/dassets/engine.rb
CHANGED
@@ -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
|
data/lib/dassets/runner.rb
CHANGED
@@ -20,16 +20,9 @@ class Dassets::Runner
|
|
20
20
|
|
21
21
|
case @cmd_name
|
22
22
|
when 'digest'
|
23
|
-
require 'dassets/
|
23
|
+
require 'dassets/digest_cmd'
|
24
24
|
abs_paths = @cmd_args.map{ |path| File.expand_path(path, @pwd) }
|
25
|
-
Dassets::
|
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
|
18
|
-
# - then check if
|
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?) &&
|
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
|
34
|
-
!path_digest_match.
|
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
|