Class: Slinky::Manifest

Inherits:
Object
  • Object
show all
Defined in:
lib/slinky/manifest.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dir, config, options = {}) ⇒ Manifest

Returns a new instance of Manifest.



27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/slinky/manifest.rb', line 27

def initialize dir, config, options = {}
  @dir = dir
  @build_to = if d = options[:build_to]
                File.expand_path(d)
              else
                dir
              end
  @manifest_dir = ManifestDir.new dir, self, @build_to, self
  @devel = (options[:devel].nil?) ? true : options[:devel]
  @config = config
  @no_minify = options[:no_minify] || config.dont_minify
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



25
26
27
# File 'lib/slinky/manifest.rb', line 25

def config
  @config
end

#dirObject

Returns the value of attribute dir.



25
26
27
# File 'lib/slinky/manifest.rb', line 25

def dir
  @dir
end

#manifest_dirObject

Returns the value of attribute manifest_dir.



25
26
27
# File 'lib/slinky/manifest.rb', line 25

def manifest_dir
  @manifest_dir
end

Instance Method Details

#add_all_by_path(paths) ⇒ Object

Adds a file to the manifest, updating the dependency graph



56
57
58
59
60
61
# File 'lib/slinky/manifest.rb', line 56

def add_all_by_path paths
  manifest_update paths do |path|
    md = find_by_path(File.dirname(path)).first
    mf = md.add_file(File.basename(path))
  end
end

#buildObject



278
279
280
281
282
283
284
285
286
287
288
# File 'lib/slinky/manifest.rb', line 278

def build
  @manifest_dir.build
  unless @devel
    @config.produce.keys.each{|product|
      compress_product(product)
    }

    # clean up the files that have been processed
    files_for_all_products.each{|mf| FileUtils.rm(mf.build_to, :force => true)}
  end
end

#compress_product(product) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/slinky/manifest.rb', line 205

def compress_product product
  compressor = compressor_for_product product
  post_processor = post_processor_for_product product

  s = files_for_product(product).map{|mf|
    f = File.open(mf.build_to.to_s, 'rb'){|f| f.read}
    post_processor ? (post_processor.call(mf, f)) : f
  }.join("\n")

  # Make the directory the product is in
  FileUtils.mkdir_p("#{@build_to}/#{Pathname.new(product).dirname}")
  File.open("#{@build_to}/#{product}", "w+"){|f|
    unless @no_minify
      f.write(compressor[s])
    else
      f.write(s)
    end
  }
end

#dependency_graph[ManifestFile, ManifestFile]

Builds the directed graph representing the dependencies of all files in the manifest that contain a slinky_require declaration. The graph is represented as a list of pairs (required, by), each of which describes an edge.

Returns:



261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/slinky/manifest.rb', line 261

def dependency_graph
  return @dependency_graph if @dependency_graph

  graph = []
  files(false).each{|mf|
    mf.dependencies.each{|d|
      graph << [d, mf]
    }
  }

  @dependency_graph = Graph.new(files(false), graph)
end

#dependency_listObject



274
275
276
# File 'lib/slinky/manifest.rb', line 274

def dependency_list
  dependency_graph.dependency_list
end

#files(include_ignores = true) ⇒ ManifestFile

Returns a list of all files contained in this manifest

Returns:



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/slinky/manifest.rb', line 43

def files include_ignores = true
  unless @files
    @files = []
    files_rec @manifest_dir
  end
  if include_ignores
    @files
  else
    @files.reject{|f| @config.ignore.any?{|p| f.in_tree? p}}
  end
end

#files_for_all_productsObject



196
197
198
199
200
201
202
203
# File 'lib/slinky/manifest.rb', line 196

def files_for_all_products
  return @files_for_all_products if @files_for_all_products
  SlinkyError.batch_errors do
    @files_for_all_products = @config.produce.keys.map{|product|
      files_for_product(product)
    }.flatten.uniq
  end
end

#files_for_product(product) ⇒ Object

Finds all the matching manifest files for a particular product. This does not take into account dependencies.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/slinky/manifest.rb', line 141

def files_for_product product
  if !p = @config.produce[product]
    SlinkyError.raise NoSuchProductError,
           "Product '#{product}' has not been configured"
  end

  type = type_for_product product
  if type != ".js" && type != ".css"
    SlinkyError.raise InvalidConfigError, "Only .js and .css products are supported"
  end

  g = dependency_graph.transitive_closure

  # Topological indices for each file
  indices = {}
  dependency_list.each_with_index{|f, i| indices[f] = i}

  # Compute the set of excluded files
  excludes = Set.new((p["exclude"] || []).map{|p|
                       find_by_pattern(p)
                     }.flatten.uniq)

  SlinkyError.batch_errors do
    # First find the list of files that have been explictly
    # included/excluded
    p["include"].map{|f|
      mfs = find_by_pattern(f)
            .map{|mf| [mf] + g[f]}
            .flatten
            .reject{|f| f.output_path.extname != type}
      if mfs.empty?
        SlinkyError.raise FileNotFoundError,
                          "No files matched by include #{f} in product #{product}"
      end
      mfs.flatten
    }.flatten.reject{|f|
      excludes.include?(f)
      # Then add all the files these require
    }.map{|f|
      # Find all of the downstream files
      # check that we're not excluding any required files
      g[f].each{|rf|
        if p["exclude"] && r = p["exclude"].find{|ex| rf.matches_path?(ex, true)}
          SlinkyError.raise DependencyError,
            "File #{f} requires #{rf} which is excluded by exclusion rule #{r}"
        end
      }
      [f] + g[f]
    }.flatten.uniq.sort_by{|f|
      # Sort by topological order
      indices[f]
    }
  end
end

#find_by_path(path, allow_multiple = false) ⇒ Object

Finds the file at the given path in the manifest if one exists, otherwise nil.

Parameters:

  • String

    path the path of the file relative to the manifest

Returns:

  • ManifestFile the manifest file at that path if one exists



84
85
86
# File 'lib/slinky/manifest.rb', line 84

def find_by_path path, allow_multiple = false
  @manifest_dir.find_by_path path, allow_multiple
end

#find_by_pattern(pattern) ⇒ Object

Finds all files that match the given pattern. The match rules are similar to those for .gitignore and given by

  1. If the pattern ends with a slash, it will only match directories; e.g. ‘foo/` would match a directory `foo/` but not a file `foo`. In a file context, matching a directory is equivalent to matching all files under that directory, recursively.

  2. If the pattern does not contain a slash, slinky treats it as a relative pathname which can match files in any directory. For example, the rule ‘test.js` will matching `/test.js` and

`/component/test.js`.
  1. If the pattern begins with a slash, it will be treated as an absolute path starting at the root of the source directory.

  2. If the pattern does not begin with a slash, but does contain one or more slashes, it will be treated as a path relative to any directory. For example, ‘test/*.js` will match `/test/main.js`, and /component/test/component.js`, but not `main.js`.

  3. A single star ‘*` in a pattern will match any number of characters within a single path component. For example, `/test/*.js` will match `/test/main_test.js` but not `/test/component/test.js`.

  4. A double star ‘**` will match any number of characters including path separators. For example `/scripts/**/main.js` will match any file named `main.js` under the `/scripts` directory, including

`/scripts/main.js` and `/scripts/component/main.js`.


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/slinky/manifest.rb', line 112

def find_by_pattern pattern
  # The strategy here is to convert the pattern into an equivalent
  # regex and run that against the pathnames of all the files in
  # the manifest.
  regex_str = Regexp.escape(pattern)
              .gsub('\*\*/', ".*")
              .gsub('\*\*', ".*")
              .gsub('\*', "[^/]*")

  if regex_str[0] != '/'
    regex_str = '.*/' + regex_str
  end

  if regex_str[-1] == '/'
    regex_str += '.*'
  end

  regex_str = "^#{regex_str}$"

  regex = Regexp.new(regex_str)

  files(false).reject{|f|
    !regex.match('/' + f.relative_source_path.to_s) &&
    !regex.match('/' + f.relative_output_path.to_s)
  }
end

#md5Object

Returns a md5 encompassing the current state of the manifest. Any change to the manifest should produce a different hash. This can be used to determine if the manifest has changed.



293
294
295
296
297
298
299
300
# File 'lib/slinky/manifest.rb', line 293

def md5
  if @md5
    @md5
  else
    @md5 = Digest::MD5.hexdigest(files.map{|f| [f.source, f.md5]}
                                  .sort.flatten.join(":"))
  end
end

#product_string(product) ⇒ Object

Produces a string of HTML that includes all of the files for the given product.



245
246
247
248
249
250
251
252
253
# File 'lib/slinky/manifest.rb', line 245

def product_string product
  if @devel
    files_for_product(product).map{|f|
      html_for_path("/#{f.relative_output_path}")
    }.join("\n")
  else
    html_for_path("#{product}?#{rand(999999999)}")
  end
end

#remove_all_by_path(paths) ⇒ Object

Removes a file from the manifest



69
70
71
72
73
74
75
76
# File 'lib/slinky/manifest.rb', line 69

def remove_all_by_path paths
  manifest_update paths do |path|
    mf = find_by_path(path).first()
    if mf
      mf.parent.remove_file(mf)
    end
  end
end

#scripts_stringObject

These are special cases for simplicity and backwards compatability. If no products are defined, we have two default products, one which includes are .js files in the repo and one that includes all .css files. This method produces an HTML include string for all of the .js files.



230
231
232
# File 'lib/slinky/manifest.rb', line 230

def scripts_string
  product_string ConfigReader::DEFAULT_SCRIPT_PRODUCT
end

#styles_stringObject

These are special cases for simplicity and backwards compatability. If no products are defined, we have two default products, one which includes are .js files in the repo and one that includes all .css files. This method produces an HTML include string for all of the .css files.



239
240
241
# File 'lib/slinky/manifest.rb', line 239

def styles_string
  product_string ConfigReader::DEFAULT_STYLE_PRODUCT
end

#update_all_by_path(paths) ⇒ Object

Notifies of an update to a file in the manifest



64
65
66
# File 'lib/slinky/manifest.rb', line 64

def update_all_by_path paths
  manifest_update paths
end