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.
- 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
|