repertoire-assets 0.2.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/.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
|