module Opal # The Bundle class is used to bundle a package, and optionally all of its # dependencies and the base opal boot file into a js file ready for browser # deployment. It takes a package, and a set of options, and simply returns a # combined string of the built packages. # # This does not yet support building resources from the packages. Just the # basic package building works. # # The following options are supported: # # - `:dependencies` - An array of additional dependencies to add that may not # be listed in the .gemspec. This is used, for example, to include opalspec # into test builds so that opalspec does not need to be a dependency in all # build environments. # - `:main` - The file to run in the browser. An example may be # 'vienna/autorun'. Note this is not a bin file, and should be a simple lib # file that will be in the load path. # - `:bin` - Alternatively, a bin file listed within a gem that should be # included, and then run within the browser. An example would be # 'vienna/vienna', where the first part is the gem containing the bin file, # which is named the second part. # - `:standalone` - Exclude dependencies listed in the gemspec. This is false # by default so all listed dependencies will be recursively added to the # resulting file. If set to true, the gemspec dependencies will not be # included. This does not affect the `:dependencies` option, which are # included seperately to these dependencies. # - `:to` - Usually this process will just return a string, but by passing a # `:to` option, the result will be written to the file specified by the given # name. This only works for simple bundling where no resources are needed. class Bundle # Ensure we keep a unique set of packages - we dont want duplications attr_reader :handled_packages # Queue of packages waiting to be built attr_reader :package_queue # The {Gem} instance to build with a set of optional options. # # @param [Gem] package The package to build # @param {Hash} opts Build options def initialize(package, opts = {}) @package = package @options = opts @handled_packages = [] @package_queue = [] end # Actually build the bundle. Returns the result, for now, as a string. # # @return {String} bundled packages def build result = [] pkg = nil @package_queue << @package @handled_packages << @package.name unless @options[:standalone] add_dependency('core', @package) end while pkg = @package_queue.pop puts pkg # if this is our initial package and we are standalone, make sure we # pass that option into build package if pkg == @package && @options[:standalone] puts "standalone #{pkg}" result << build_package(pkg, true) else result << build_package(pkg) end end unless @options[:standalone] result.unshift opal_boot_content end if @options[:to] File.open(@options[:to], 'w+') { |out| out.write result.join("\n") } end result.join "\n" end # Boot content for opal. This bootloader takes all the gems in the browser # context and manages them. Opal self-loads in the bootloader. # # @return [String] def opal_boot_content path = File.expand_path File.join(__FILE__, '..', '..', '..', 'lib', 'opal.js') File.read path end # Builds a single package. This will build the package, discover its # dependencies, and then add them to the queue of packages to build. This # method will then return the built package as a string. # # @param {Package} package The package to build # @param {Boolean} standalone Whether to exclude gem dependencies. # @return {String} Built package def build_package(package, standalone = false) # Work through each dependency and work out if we need it unless standalone package.dependencies.each do |dep| add_dependency dep, package end end package.to_bundle end def add_dependency(dep, package) unless @handled_packages.include? dep @handled_packages << dep loc = package.find_dependency(dep) raise "Cannot find dependency '#{dep}' for #{package}" unless loc pkg = Gem.new loc @package_queue << pkg end end end end