repertoire-assets 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/FAQ +90 -0
- data/INSTALL +35 -0
- data/LICENSE +22 -0
- data/README +234 -0
- data/Rakefile +10 -0
- data/TODO +52 -0
- data/lib/repertoire-assets/exceptions.rb +8 -0
- data/lib/repertoire-assets/manifest.rb +309 -0
- data/lib/repertoire-assets/processor.rb +589 -0
- data/lib/repertoire-assets/provides.rb +96 -0
- data/lib/repertoire-assets/railtie.rb +18 -0
- data/lib/repertoire-assets/tasks.rake +29 -0
- data/lib/repertoire-assets/version.rb +5 -0
- data/lib/repertoire-assets.rb +11 -0
- data/repertoire-assets.gemspec +56 -0
- data/vendor/yuicompressor-2.4.2.jar +0 -0
- metadata +96 -0
@@ -0,0 +1,589 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Repertoire
|
4
|
+
module Assets
|
5
|
+
class Processor
|
6
|
+
|
7
|
+
attr_accessor :manifest, :provided
|
8
|
+
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
:precache => nil,
|
11
|
+
:compress => nil,
|
12
|
+
:disable_rack_assets => nil,
|
13
|
+
|
14
|
+
:path_prefix => '', # prefix to add before all urls
|
15
|
+
:js_source_files => # app javascript files to jumpstart dependency processing
|
16
|
+
[ 'public/javascripts/application.js', 'public/javascripts/*.js' ],
|
17
|
+
|
18
|
+
:gem_asset_roots => [ '../public' ], # location under $LOAD_PATHs to use as root for asset uris
|
19
|
+
:gem_libraries => # location under $LOAD_PATHs to search for javascript libraries
|
20
|
+
[ '../public/javascripts/*.js' ],
|
21
|
+
|
22
|
+
:cache_root => 'public', # app directory to put cache files & digests in, should be webserver visible
|
23
|
+
:digest_basename => 'digest', # file basename for css & js digests
|
24
|
+
:gem_excludes => [ ] # patterns under $LOAD_PATHs to exclude from the manifest
|
25
|
+
}
|
26
|
+
|
27
|
+
|
28
|
+
# Initialize the asset dependency system, configure the rack middleware,
|
29
|
+
# and precache assets (if requested).
|
30
|
+
#
|
31
|
+
# === Parameters
|
32
|
+
# :delegate::
|
33
|
+
# The rack app to serve assets for
|
34
|
+
# :settings::
|
35
|
+
# Hash of configuration options (below)
|
36
|
+
# :logger::
|
37
|
+
# Framework's logger - defaults to STDERR
|
38
|
+
#
|
39
|
+
# === Exceptions
|
40
|
+
# ConfigurationError
|
41
|
+
#
|
42
|
+
# Common settings and defaults
|
43
|
+
#
|
44
|
+
# :precache [false] # copy and bundle assets into host application?
|
45
|
+
# :compress [false] # compress bundled javascript & stylesheets? (implies :precache)
|
46
|
+
# :disable_rack_assets [false] # don't interpolate <script> and <link> tags (implies :precache)
|
47
|
+
# :path_prefix [''] # prefix for all generated urls
|
48
|
+
#
|
49
|
+
# For other very rarely used configuration options, see the source.
|
50
|
+
#
|
51
|
+
# ---
|
52
|
+
def initialize(delegate, settings={}, logger=nil)
|
53
|
+
@options = DEFAULT_OPTIONS.dup.merge(settings)
|
54
|
+
@logger = logger || Logger.new(STDERR)
|
55
|
+
Processor.verify_options(@options)
|
56
|
+
|
57
|
+
# configure rack middleware
|
58
|
+
@app = delegate
|
59
|
+
@manifester = Manifest.new(@app, self, @options, @logger)
|
60
|
+
@provider = Provides.new(@manifester, self, @options, @logger)
|
61
|
+
|
62
|
+
# build manifest from required javascripts
|
63
|
+
reset!
|
64
|
+
|
65
|
+
# if requested, cache assets in app's public directory at startup
|
66
|
+
@provider.precache! && @manifester.precache! if @options[:precache]
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# The core rack call to process an http request. Calls the appropriate middleware
|
71
|
+
# to provide assets or interpolate the asset manifest.
|
72
|
+
#
|
73
|
+
# If asset precaching is turned off, the dependent files are checked to make
|
74
|
+
# sure the asset manifest is still valid before the request is processed.
|
75
|
+
#
|
76
|
+
# ---
|
77
|
+
def call(env)
|
78
|
+
delegate = case
|
79
|
+
when @options[:disable_rack_assets] then @app # ignore all asset middleware
|
80
|
+
when @options[:precache] then @manifester # use manifest middleware only
|
81
|
+
else
|
82
|
+
reset! if stale?
|
83
|
+
@provider # use provider + manifest middleware
|
84
|
+
end
|
85
|
+
|
86
|
+
delegate.call(env)
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# Rebuild the manifest and provides lists from the javascript source files.
|
91
|
+
#
|
92
|
+
# ---
|
93
|
+
def reset!
|
94
|
+
@source_files = nil
|
95
|
+
@libraries = nil
|
96
|
+
@excludes = nil
|
97
|
+
@asset_roots = nil
|
98
|
+
@manifest = nil
|
99
|
+
@manifest_stamp = nil
|
100
|
+
@provided = nil
|
101
|
+
|
102
|
+
source_files.each do |path|
|
103
|
+
requires(path)
|
104
|
+
end
|
105
|
+
|
106
|
+
@manifest_timestamp = mtime
|
107
|
+
|
108
|
+
@logger.info "Assets processed: %i source files, %i libraries available, %i assets provided, %i required files in manifest" %
|
109
|
+
[ source_files.size, libraries.size, @provided.size, @manifest.size ]
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# Compute the initial javascript source files to scan for dependencies.
|
114
|
+
# By default:
|
115
|
+
#
|
116
|
+
# <app_root>/public/javascripts/*.js
|
117
|
+
#
|
118
|
+
# If there is a file 'application.js', it will be processed before all
|
119
|
+
# others. In a complex application, divide your javascript into files
|
120
|
+
# in a directory below and 'require' them in application.js.
|
121
|
+
#
|
122
|
+
# ==== Returns
|
123
|
+
# :Array[Pathname]::
|
124
|
+
# The pathnames, in absolute format
|
125
|
+
#
|
126
|
+
# ---
|
127
|
+
def source_files
|
128
|
+
@source_files ||= Processor.expand_paths(['.'], @options[:js_source_files])
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# Compute the load path for all potential assets that could be provided by
|
133
|
+
# a javascript library. By default:
|
134
|
+
#
|
135
|
+
# <gem_root>/public/
|
136
|
+
#
|
137
|
+
# For security, the middleware will not serve a file until it or an enclosing
|
138
|
+
# directory are explicitly required or provided by a javascript library file.
|
139
|
+
#
|
140
|
+
# ==== Returns
|
141
|
+
# Array[Pathname]:: The pathnames, in absolute format
|
142
|
+
#
|
143
|
+
# ---
|
144
|
+
def asset_roots
|
145
|
+
unless @asset_roots
|
146
|
+
@asset_roots = Processor.expand_paths($LOAD_PATH, @options[:gem_asset_roots])
|
147
|
+
@asset_roots << Processor.realpath(@options[:cache_root])
|
148
|
+
end
|
149
|
+
|
150
|
+
@asset_roots
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Compute the list of all available javascript libraries and their paths.
|
155
|
+
# By default, javascripts in the following paths will be found:
|
156
|
+
#
|
157
|
+
# <gem_root>/public/javascripts/*.js
|
158
|
+
#
|
159
|
+
# ==== Returns
|
160
|
+
# :Hash<String, Pathname>::
|
161
|
+
# The library names and their absolute paths.
|
162
|
+
#
|
163
|
+
# ---
|
164
|
+
def libraries
|
165
|
+
unless @libraries
|
166
|
+
@libraries = {}
|
167
|
+
paths = Processor.expand_paths($LOAD_PATH, @options[:gem_libraries])
|
168
|
+
paths.each do |path|
|
169
|
+
lib = Processor.library_name(path)
|
170
|
+
if @libraries[lib]
|
171
|
+
@logger.warn "Multiple libraries for <#{lib}>, using #{ Processor.pretty_path @libraries[lib] } (other is #{ Processor.pretty_path path })"
|
172
|
+
end
|
173
|
+
@libraries[lib] ||= path
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
@libraries
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
# Compute the basepaths for all excluded javascript libraries.
|
182
|
+
#
|
183
|
+
# ==== Returns
|
184
|
+
# :Array<String>::
|
185
|
+
# The list of base paths
|
186
|
+
#
|
187
|
+
# ---
|
188
|
+
def excludes
|
189
|
+
unless @excludes
|
190
|
+
@excludes = []
|
191
|
+
@options[:gem_excludes].map do |libname|
|
192
|
+
libpath = libraries[libname]
|
193
|
+
@excludes << libpath.dirname + libname if libpath
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
@excludes
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
# Determine if manifest or provided assets lists need to be regenerated
|
202
|
+
# because a required file has changed.
|
203
|
+
#
|
204
|
+
# ==== Parameters
|
205
|
+
# :product_times:: An optional list of times from product files that
|
206
|
+
# depend on the manifest.
|
207
|
+
#
|
208
|
+
# ==== Returns
|
209
|
+
# <boolean>
|
210
|
+
#
|
211
|
+
def stale?(*product_times)
|
212
|
+
product_times << @manifest_timestamp
|
213
|
+
source_time = mtime
|
214
|
+
current = @manifest &&
|
215
|
+
@provided &&
|
216
|
+
product_times.all? do |time|
|
217
|
+
time >= source_time
|
218
|
+
end
|
219
|
+
!current
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
# Calculate the most recent modification date among all javascript files
|
224
|
+
# available to build the manifest.
|
225
|
+
#
|
226
|
+
# ==== Returns
|
227
|
+
#
|
228
|
+
# <Time>:: The most recent time.
|
229
|
+
#
|
230
|
+
# ---
|
231
|
+
def mtime
|
232
|
+
paths = @source_files | @provided.values
|
233
|
+
mtimes = paths.map do |f|
|
234
|
+
File.exists?(f) ? File.mtime(f) : Time.now
|
235
|
+
end
|
236
|
+
|
237
|
+
mtimes.max
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
protected
|
242
|
+
|
243
|
+
# Add the required javascript file - and all that it requires in turn -
|
244
|
+
# to the manifest. Javascripts that have already been sourced are
|
245
|
+
# omitted.
|
246
|
+
#
|
247
|
+
# The javascript file will be provided for client http access at a uri
|
248
|
+
# relative to the gem asset root. e.g.
|
249
|
+
#
|
250
|
+
# <gem>/public/javascripts/my_module/circle.js
|
251
|
+
#
|
252
|
+
# will apppear at a comparable uri beneath the application root:
|
253
|
+
#
|
254
|
+
# http://javascripts/my_module/circle.js
|
255
|
+
#
|
256
|
+
# ==== Parameters
|
257
|
+
# :path::
|
258
|
+
# The path of the javascript file
|
259
|
+
# :indent::
|
260
|
+
# The logging indent level (for pretty-printing)
|
261
|
+
#
|
262
|
+
# ---
|
263
|
+
def requires(path, level=0)
|
264
|
+
@manifest ||= []
|
265
|
+
@provided ||= {}
|
266
|
+
uri = uri(path)
|
267
|
+
|
268
|
+
# only expand each source file once
|
269
|
+
return if @manifest.include?(uri)
|
270
|
+
|
271
|
+
# handle excluded libraries
|
272
|
+
if excludes.any? { |excluded| Processor.parent_path?(excluded, path) }
|
273
|
+
@logger.debug "Excluding #{' '*level + uri} (#{Processor.pretty_path(path)})"
|
274
|
+
return
|
275
|
+
end
|
276
|
+
|
277
|
+
@logger.debug "Requiring #{' '*level + uri} (#{Processor.pretty_path(path)})"
|
278
|
+
|
279
|
+
# preprocess directives in the file recursively
|
280
|
+
preprocess(path, level+1)
|
281
|
+
|
282
|
+
# add file after those it requires and register it as provided for http access
|
283
|
+
@manifest << uri
|
284
|
+
@provided[uri] = path
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
# Provide a given asset for client http access. If a directory is passed
|
289
|
+
# in, all files beneath it are provided.
|
290
|
+
#
|
291
|
+
# The assets will be provided for client http access at uris
|
292
|
+
# relative to the gem asset root. e.g.
|
293
|
+
#
|
294
|
+
# <gem>/public/images/my_module/circle.png
|
295
|
+
#
|
296
|
+
# will apppear at a comparable uri beneath the application root:
|
297
|
+
#
|
298
|
+
# http://images/my_module/circle.png
|
299
|
+
#
|
300
|
+
# ==== Parameters
|
301
|
+
# :path::
|
302
|
+
# The path of the asset or directory to provide
|
303
|
+
# :indent::
|
304
|
+
# The logging indent level (for pretty-printing)
|
305
|
+
#
|
306
|
+
# ---
|
307
|
+
def provides(path, level=0)
|
308
|
+
@provided ||= {}
|
309
|
+
uri = uri(path)
|
310
|
+
|
311
|
+
@logger.debug "Providing #{' '*level + uri} (#{Processor.pretty_path(path)})"
|
312
|
+
|
313
|
+
path.find do |sub|
|
314
|
+
@provided[ uri(sub) ] = sub if sub.file?
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
protected
|
320
|
+
|
321
|
+
|
322
|
+
# Recursively preprocess require and provide directives in a javascript file.
|
323
|
+
#
|
324
|
+
# ==== Parameters
|
325
|
+
# :path<Pathname>:: the path of the javascript file
|
326
|
+
# :level<Integer>:: the recursion level (for pretty-printing & errors)
|
327
|
+
#
|
328
|
+
# ==== Raises
|
329
|
+
# :UnknownAssetError::
|
330
|
+
# No file could be found for the given asset name
|
331
|
+
# :UnknownDirectiveError::
|
332
|
+
# An unknown processing directive was given
|
333
|
+
#
|
334
|
+
# ---
|
335
|
+
def preprocess(path, level=0)
|
336
|
+
line_num = 1
|
337
|
+
path.each_line do |line|
|
338
|
+
begin
|
339
|
+
# process any directives on line
|
340
|
+
directive(path.dirname, line, level)
|
341
|
+
rescue Error => e
|
342
|
+
@logger.error "Could not process '#{line.chomp}' (%s, line %i)" % [path, line_num]
|
343
|
+
error_status
|
344
|
+
raise e.message
|
345
|
+
end
|
346
|
+
line_num += 1
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# Preprocess any directive on a single line. The file spec is progressively
|
351
|
+
# expanded, as follows.
|
352
|
+
#
|
353
|
+
# (1) <library> and <library/sublibrary> are dereferenced
|
354
|
+
# (2) "relative/file" is expanded based on the current working directory
|
355
|
+
# (3) globs are expanded (see Ruby Dir[])
|
356
|
+
# (4) the default extension (.js) is checked
|
357
|
+
# (5) finally, 'requires' or 'provides' are called on any resulting files
|
358
|
+
#
|
359
|
+
# ==== Parameters
|
360
|
+
# :cwd::
|
361
|
+
# The current working directory (for relative paths)
|
362
|
+
# :line::
|
363
|
+
# The line to process
|
364
|
+
# :level::
|
365
|
+
# The recursion level (for pretty-printing)
|
366
|
+
#
|
367
|
+
# ==== Raises
|
368
|
+
# :UnknownAssetError::
|
369
|
+
# The path does not refer to any existing file
|
370
|
+
# :UnknownDirectiveError::
|
371
|
+
# The line specified an unknown preprocessing directive
|
372
|
+
#
|
373
|
+
# ---
|
374
|
+
def directive(cwd, line, level=0)
|
375
|
+
# extract the preprocessing directive
|
376
|
+
return unless line[ %r{^\s*//=\s*(\w+)\s+(".+"|<.+>)\s*$} ]
|
377
|
+
directive, pathspec = $1, $2
|
378
|
+
|
379
|
+
# progressively expand path specification
|
380
|
+
pathlist = pathspec
|
381
|
+
|
382
|
+
# expand library and sublibrary references
|
383
|
+
pathlist.gsub!( %r{^<([^/>]*)/?(.*)>} ) do
|
384
|
+
libname, sublib = $1, $2
|
385
|
+
# determine library path
|
386
|
+
unless libpath = libraries[libname]
|
387
|
+
raise UnknownAssetError, libname
|
388
|
+
end
|
389
|
+
# distinguish library and sublibraries
|
390
|
+
if sublib.empty?
|
391
|
+
libpath.to_s
|
392
|
+
else
|
393
|
+
libpath.dirname + libname + sublib
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# expand relative references
|
398
|
+
pathlist.gsub!( %r{"(.*)"} ) do
|
399
|
+
subpath = $1
|
400
|
+
cwd + subpath
|
401
|
+
end
|
402
|
+
|
403
|
+
# expand globs & default extension, match existing files
|
404
|
+
pathlist = Dir[ pathlist, pathlist + '.js' ]
|
405
|
+
pathlist.reject! { |p| File.directory?(p) }
|
406
|
+
|
407
|
+
# handle missing asset
|
408
|
+
raise UnknownAssetError, pathspec if pathlist.empty?
|
409
|
+
|
410
|
+
# perform directive over matches
|
411
|
+
pathlist.each do |p|
|
412
|
+
p = Pathname.new(p)
|
413
|
+
case directive
|
414
|
+
when 'require' then requires(p, level)
|
415
|
+
when 'provide' then provides(p, level)
|
416
|
+
else
|
417
|
+
raise UnknownDirectiveError, directive
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Locate the enclosing gem asset root and construct a relative uri to path
|
423
|
+
#
|
424
|
+
# ==== Parameters
|
425
|
+
# :path::
|
426
|
+
# The path of the asset
|
427
|
+
#
|
428
|
+
# ---
|
429
|
+
def uri(path)
|
430
|
+
return nil unless path
|
431
|
+
root = asset_roots.detect { |root| Processor.parent_path?(root, path) }
|
432
|
+
'/' + path.relative_path_from(root).to_s
|
433
|
+
end
|
434
|
+
|
435
|
+
|
436
|
+
# Check path references a valid set of files and give sensible error
|
437
|
+
# messages to locate the problem if not. If successful, each path is
|
438
|
+
# yielded in turn.
|
439
|
+
#
|
440
|
+
# ==== Parameters
|
441
|
+
# :path::
|
442
|
+
# The pathname to check
|
443
|
+
# :identifier::
|
444
|
+
# The reference the user supplied to identify the file
|
445
|
+
# :source_file::
|
446
|
+
# The filename the reference occurred in
|
447
|
+
# :line_num::
|
448
|
+
# The line number of the occurrence
|
449
|
+
# :&block::
|
450
|
+
# Block to run on all successful matches
|
451
|
+
#
|
452
|
+
# ==== Raises
|
453
|
+
# UnknownAssetError::
|
454
|
+
# The path does not refer to an existing file
|
455
|
+
#
|
456
|
+
# ---
|
457
|
+
def path_lint(path, identifier, source_file, line_num, &block)
|
458
|
+
matches = if !path
|
459
|
+
[]
|
460
|
+
elsif path.readable?
|
461
|
+
[ path ]
|
462
|
+
else
|
463
|
+
Dir[ path ]
|
464
|
+
end
|
465
|
+
|
466
|
+
if matches.size > 0
|
467
|
+
matches.each { |p| yield p }
|
468
|
+
else
|
469
|
+
@logger.error "Could not resolve #{identifier} #{ '(%s, line %i)' % [source_file, line_num] if source_file && line_num }"
|
470
|
+
error_status
|
471
|
+
raise UnknownAssetError
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# Log the processor's status to error
|
476
|
+
#
|
477
|
+
# ---
|
478
|
+
def error_status
|
479
|
+
lib_patterns = @options[:gem_libraries].map { |p| "$LOAD_PATH/#{p}" }
|
480
|
+
root_patterns = @options[:gem_asset_roots].map { |p| "$LOAD_PATH/#{p}" }
|
481
|
+
|
482
|
+
@logger.error "Known libraries [ %s ]: %s" %
|
483
|
+
[ lib_patterns.join(", "), libraries.keys.sort.join(", ") ]
|
484
|
+
@logger.error "Asset roots [ %s ]:\n%s" %
|
485
|
+
[ root_patterns.join(", "), asset_roots.sort.join("\n") ]
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
class << self
|
490
|
+
|
491
|
+
# Sanity check for configurations
|
492
|
+
#
|
493
|
+
# ==== Parameters
|
494
|
+
# :options<Hash>:: the configuration options
|
495
|
+
#
|
496
|
+
# ---
|
497
|
+
def verify_options(options)
|
498
|
+
# detect cases where rubygems or bundler are misconfigured
|
499
|
+
raise Error, "No load paths are available" unless $LOAD_PATH
|
500
|
+
|
501
|
+
# precaching must be turned on in order to compress
|
502
|
+
if options[:compress]
|
503
|
+
raise Error, "Must select asset precaching for compression" if options[:precache] == false
|
504
|
+
options[:precache] = true
|
505
|
+
end
|
506
|
+
|
507
|
+
# the javascript source files must be located in the public app root to have valid uris
|
508
|
+
#options[:js_source_files].each do |f|
|
509
|
+
# unless parent_path?(options[:cache_root], f)
|
510
|
+
# raise Error, "Invalid configuration: #{f} must be under app asset root"
|
511
|
+
# end
|
512
|
+
#end
|
513
|
+
|
514
|
+
# the javascript libraries in gems must be located underneath gem asset roots to have valid uris
|
515
|
+
options[:gem_libraries].each do |f|
|
516
|
+
unless options[:gem_asset_roots].any? { |r| parent_path?(r, f) }
|
517
|
+
raise Error, "Invalid configuration: #{f} is not under a valid gem asset root"
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
|
523
|
+
# Expand a list of existing files matching a globs from a set of root paths
|
524
|
+
#
|
525
|
+
# ==== Parameters
|
526
|
+
# :base_paths::
|
527
|
+
# The list of root paths
|
528
|
+
# :patterns::
|
529
|
+
# A list of unix glob-style patterns to match
|
530
|
+
#
|
531
|
+
# ==== Returns
|
532
|
+
# A list of absolute paths to existing files
|
533
|
+
#
|
534
|
+
# ---
|
535
|
+
def expand_paths(base_paths, patterns)
|
536
|
+
paths = []
|
537
|
+
|
538
|
+
base_paths.each do |base|
|
539
|
+
patterns.each do |pattern|
|
540
|
+
paths |= Dir[File.join(base, pattern)].map { |f| realpath(f) }
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
paths.compact
|
545
|
+
end
|
546
|
+
|
547
|
+
# Extract a javascript library name from its complete path. As for ruby
|
548
|
+
# require this is the file's basename irrespective of directory and with
|
549
|
+
# the extension left off.
|
550
|
+
#
|
551
|
+
# ==== Returns
|
552
|
+
# A string identifying the library name
|
553
|
+
#
|
554
|
+
# ---
|
555
|
+
def library_name(path)
|
556
|
+
base = path.basename.to_s
|
557
|
+
base.chomp(path.extname)
|
558
|
+
end
|
559
|
+
|
560
|
+
|
561
|
+
# Attempt to give a short name to identify the gem the provided file
|
562
|
+
# is from. If unsuccessful, return the full path again.
|
563
|
+
#
|
564
|
+
# ---
|
565
|
+
def pretty_path(path)
|
566
|
+
# default: standard rubygems repository format
|
567
|
+
pretty = path.to_s[/.*\/(gems|dirs)\/([^\/]+)\//, 2]
|
568
|
+
pretty || path
|
569
|
+
end
|
570
|
+
|
571
|
+
|
572
|
+
# Utility to check if parent path contains child path
|
573
|
+
#
|
574
|
+
# ---
|
575
|
+
def parent_path?(parent, child)
|
576
|
+
child.to_s.index(parent.to_s) == 0
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
# Utility for resolving full file paths
|
581
|
+
#
|
582
|
+
# ---
|
583
|
+
def realpath(f)
|
584
|
+
Pathname.new(f).realpath
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'logger'
|
3
|
+
require 'rack/utils'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Repertoire
|
7
|
+
module Assets
|
8
|
+
|
9
|
+
#
|
10
|
+
# Rack middleware to serve provided files from gem roots
|
11
|
+
#
|
12
|
+
class Provides
|
13
|
+
|
14
|
+
# serve binary data from gems in blocks of this size
|
15
|
+
CHUNK_SIZE = 8192
|
16
|
+
|
17
|
+
# pattern for uris to precache (because they will appear in the manifests)
|
18
|
+
PRECACHE_EXCLUDE = /\.(js|css)$/
|
19
|
+
|
20
|
+
# Initialize the asset provider middleware.
|
21
|
+
#
|
22
|
+
# === Parameters
|
23
|
+
# :delegate::
|
24
|
+
# The next rack app in the filter chain
|
25
|
+
# :processor::
|
26
|
+
# The Repertoire Assets processor singleton
|
27
|
+
# :options::
|
28
|
+
# Hash of configuration options
|
29
|
+
# :logger::
|
30
|
+
# Framework's logger - defaults to STDERR
|
31
|
+
#
|
32
|
+
# ---
|
33
|
+
def initialize(delegate, processor, options, logger=nil)
|
34
|
+
@delegate = delegate
|
35
|
+
@processor = processor
|
36
|
+
@options = options
|
37
|
+
@logger = logger || Logger.new(STDERR)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# The core rack call to process an http request. If the asset has been
|
42
|
+
# required or provided by a javascript file, it is served. Otherwise
|
43
|
+
# the request is forwarded to the next rack app in the chain.
|
44
|
+
#
|
45
|
+
# ---
|
46
|
+
def call(env)
|
47
|
+
dup._call(env) || @delegate.call(env)
|
48
|
+
end
|
49
|
+
|
50
|
+
def _call(env)
|
51
|
+
uri = Rack::Utils.unescape(env["PATH_INFO"])
|
52
|
+
if @path = @processor.provided[uri]
|
53
|
+
@logger.debug "Mirroring #{uri} (#{Processor.pretty_path(@path)})"
|
54
|
+
|
55
|
+
[200, {
|
56
|
+
"Last-Modified" => @path.mtime.httpdate,
|
57
|
+
"Content-Type" => Rack::Mime.mime_type(@path.extname, 'text/plain'),
|
58
|
+
"Content-Length" => @path.size.to_s
|
59
|
+
}, self]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def each
|
64
|
+
@path.open("rb") do |file|
|
65
|
+
while part = file.read(CHUNK_SIZE)
|
66
|
+
yield part
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Copy all provided assets from their current locations in gems to the
|
73
|
+
# public application root. Thereafter, the web server will serve them
|
74
|
+
# directly.
|
75
|
+
#
|
76
|
+
# Javascript and css files are ignored, since they are bundled together
|
77
|
+
# by the manifest middleware.
|
78
|
+
#
|
79
|
+
# ---
|
80
|
+
def precache!
|
81
|
+
root = Pathname.new( @options[:cache_root] ).realpath
|
82
|
+
|
83
|
+
@processor.provided.each do |uri, path|
|
84
|
+
next if uri[PRECACHE_EXCLUDE]
|
85
|
+
cache_path = Pathname.new("#{root}#{uri}")
|
86
|
+
|
87
|
+
if !cache_path.exist? || path.mtime > cache_path.mtime
|
88
|
+
FileUtils.mkdir_p cache_path.dirname if !cache_path.dirname.directory?
|
89
|
+
FileUtils.cp path, cache_path
|
90
|
+
@logger.info "Cached #{uri} (#{Processor.pretty_path(path)})"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module Repertoire
|
4
|
+
module Assets
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
config.repertoire_assets = ActiveSupport::OrderedOptions.new
|
7
|
+
|
8
|
+
initializer "repertoire_assets" do
|
9
|
+
config.app_middleware.use Repertoire::Assets::Processor, config.repertoire_assets, Rails.logger
|
10
|
+
end
|
11
|
+
|
12
|
+
rake_tasks do
|
13
|
+
dir = Pathname(__FILE__).dirname.expand_path
|
14
|
+
load dir + "tasks.rake"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|