class Slinky::Manifest

Attributes

config[RW]
dir[RW]
manifest_dir[RW]

Public Class Methods

new(dir, config, options = {}) click to toggle source
# File lib/slinky/manifest.rb, line 26
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

Public Instance Methods

add_all_by_path(paths) click to toggle source

Adds a file to the manifest, updating the dependency graph

# File lib/slinky/manifest.rb, line 55
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
build() click to toggle source
# File lib/slinky/manifest.rb, line 279
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) click to toggle source
# File lib/slinky/manifest.rb, line 206
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() click to toggle source

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.

@return [[ManifestFile, ManifestFile]] the graph

# File lib/slinky/manifest.rb, line 262
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_list() click to toggle source
# File lib/slinky/manifest.rb, line 275
def dependency_list
  dependency_graph.dependency_list
end
files(include_ignores = true) click to toggle source

Returns a list of all files contained in this manifest

@return [ManifestFile] a list of manifest files

# File lib/slinky/manifest.rb, line 42
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_products() click to toggle source
# File lib/slinky/manifest.rb, line 197
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) click to toggle source

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

# File lib/slinky/manifest.rb, line 140
def files_for_product product
  p = @config.produce[product]

  if p.nil?
    raise NoSuchProductError.new(
            "Product '#{product}' has not been configured")
  end

  type = type_for_product product
  if type != ".js" && type != ".css"
    raise InvalidConfigError.new("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) click to toggle source

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

@param String path the path of the file relative to the manifest

@return ManifestFile the manifest file at that path if one exists

# File lib/slinky/manifest.rb, line 83
def find_by_path path, allow_multiple = false
  @manifest_dir.find_by_path path, allow_multiple
end
find_by_pattern(pattern) click to toggle source

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`.
# File lib/slinky/manifest.rb, line 111
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
md5() click to toggle source

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.

# File lib/slinky/manifest.rb, line 294
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) click to toggle source

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

# File lib/slinky/manifest.rb, line 246
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) click to toggle source

Removes a file from the manifest

# File lib/slinky/manifest.rb, line 68
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_string() click to toggle source

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.

# File lib/slinky/manifest.rb, line 231
def scripts_string
  product_string ConfigReader::DEFAULT_SCRIPT_PRODUCT
end
styles_string() click to toggle source

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.

# File lib/slinky/manifest.rb, line 240
def styles_string
  product_string ConfigReader::DEFAULT_STYLE_PRODUCT
end
update_all_by_path(paths) click to toggle source

Notifies of an update to a file in the manifest

# File lib/slinky/manifest.rb, line 63
def update_all_by_path paths
  manifest_update paths
end

Private Instance Methods

compressor_for_product(product) click to toggle source
# File lib/slinky/manifest.rb, line 311
def compressor_for_product product
  require 'sassc'
  case type_for_product(product)
  when ".js"
    # Use UglifyJS
    lambda{|s| Uglifier.compile(s.force_encoding("UTF-8"),
                           mangle: false, output: {ascii_only: false})}
  when ".css"
    # Use SASS's compressed output
    lambda{|s| SassC::Engine.new(s, :syntax => :scss, :style => :compressed).render}
  end
end
files_rec(md) click to toggle source
# File lib/slinky/manifest.rb, line 304
def files_rec md
  @files += md.files
  md.children.each do |c|
    files_rec c
  end
end
html_for_path(path) click to toggle source
# File lib/slinky/manifest.rb, line 341
def html_for_path path
  ext = path.split("?").first.split(".").last
  case ext
  when "css"
    %Q|<link rel="stylesheet" href="#{path}" />|
  when "js"
    %Q|<script type="text/javascript" src="#{path}"></script>|
  else
    raise InvalidConfigError.new("Unsupported file extension #{ext}")
  end
end
invalidate_cache() click to toggle source
# File lib/slinky/manifest.rb, line 334
def invalidate_cache
  @files = nil
  @dependency_graph = nil
  @md5 = nil
  @files_for_all_products = nil
end
manifest_update(paths) { |path| ... } click to toggle source
# File lib/slinky/manifest.rb, line 357
def manifest_update paths
  paths.each{|path|
    if path[0] == '/'
      path = Pathname.new(path).relative_path_from(Pathname.new(@dir).expand_path).to_s
    end
    yield path if block_given?
  }
  invalidate_cache
  files.each{|f|
    if f.directives.include?(:slinky_scripts) ||
       f.directives.include?(:slinky_styles) ||
       f.directives.include?(:slinky_product)
      f.invalidate
      f.find_directives
    end
  }
end
post_processor_for_product(product) click to toggle source
# File lib/slinky/manifest.rb, line 324
def post_processor_for_product product
  case type_for_product(product)
  when ".css"
     lambda{|s, css| css.gsub(CSS_URL_MATCHER){|url|
         p = s.relative_output_path.dirname.to_s + "/#{$1}"
         "url('/#{p}')"
       }}
  end
end
type_for_product(product) click to toggle source
# File lib/slinky/manifest.rb, line 353
def type_for_product product
  "." + product.split(".")[-1]
end