package.coffee | |
---|---|
The Package classThis module exports the Package class. A Package is a conceptual container within which source files can reference each other as "imports". Packages can produce bundles, a particular File which content is the result of recursively aggregating all imports from a file, from its imports, and so on. Packages can have one or many Sources, in which it looks for files to aggregate bundles. Packages have a type (javascript or css) which is the type of bundles it produces. Objects of this class are not meant to be initialized directly, but rather produced at the initialization of a Project. | |
SubclassingThe Package class can be subclassed for different types of bundled files by specifying a few class properties and the compression mechanism.
| |
Underscore, brewer.js classes and CLI utilities are loaded up. | _ = require 'underscore'
util = require './util'
{Source} = require './source'
{File} = require './file'
{debug, info, finished} = require './command'
class Package extends (require 'events').EventEmitter |
The Package class needs to maintain a registry of its subclasses, which
maps types to subclasses. | @types: ->
(type for own type of this when type not in ['types', 'extend', 'create'])
@extend: (packages...) ->
for pkg in packages
this[pkg.type] = pkg
for alias in (pkg.aliases ? [])
this[alias] = pkg
@create: (options, sources, vendor) ->
throw "Package type #{options.type} not known" unless (typ = this[options.type])?
new typ options, sources, vendor
|
A Package is not meant to be initialized directly, but rather through a Project object, which knows about vendor libraries (more details about those in project.coffee). The first argument, options, is the result of evaluating a package section of a Brewfile. | constructor: (@options, sources, @vendorlibs) ->
fs = require 'fs'
ctor = @constructor
|
| _.defaults options, {
compress: true
build: './build'
bundles: []
}
{@name, @compress, @build, bundles} = @options
@bundlePaths = if _.isString bundles
JSON.parse fs.readFileSync bundles
else bundles
|
Registries for files and sources. Those will hold all those objects, neatly classified by types and access path. | @files = {}
@sources = {}
|
State tracking variables. Since sources are crawled asynchronously, a package object is not ready upfront. We need those variables to act only when appropriate. When the 'ready' event is fired, we update our state. | @_ready = false
@_pendingSources = 0
@on 'ready', => @_ready = true
|
All specified sources and vendor libraries are initialized and registered. Vendor libraries, by default don't have to be watched. | for src in sources
@registerSource Source.create src, this
for lib in @vendorlibs.libraries ctor.type
_.defaults lib, {watch: false}
@registerSource Source.create lib, this
|
This event is triggered when a file | @on 'newfile', (file) =>
type = @constructor.type |
If the registered file is of the same type as the package and it has the same relative path as a listed bundle, make a corresponding bundled file and register it | if (fpath = file.relpath) in @bundlePaths and file.type is type
bundle = @file fpath, "#{type}-bundle", @bundlePath(file)
bundle.dependOnImports file, _.bind @bundle, @
bundle.impermanent = true
bundle.parent = file
bundle.register()
|
If the package is configured to compress its bundles, make a corresponding compressed file and register it | if @compress
compressed = @file fpath, "#{type}-bundle-minified", @compressedPath(file)
compressed.dependOn bundle, _.bind @compressFile, @
compressed.impermanent = true
compressed.register()
|
This method returns a File object that corresponds to the given
arguments, in terms of access path, type and optionally fullpath and
source. If the specification does not corresponds to an existing file,
it creates the file and binds it to the appropriate objects. This is
used to reference a file that might not exist yet, and that should be
attached later to a file on disk. The | file: (relpath, type, fullpath, src) ->
@files[type] ?= {}
file = (_files = @files[type])[relpath]
unless file?
file = _files[relpath] = new File relpath, type, this
file.attach fullpath, src if fullpath?
return file
|
This method is used to register a Source object. The | registerSource: (src) ->
(@sources[src.constructor.type] ?= []).push src
@_pendingSources++
src.files null, (files) =>
@emit 'ready' if --@_pendingSources == 0
|
This method is used to produce a bundle file by aggregating all the
necessary imported files. It uses | bundle: (imported, bundle, cb) ->
i = 0
imports = bundle.parent.tsortedImports()
ws = bundle.writeStream()
ws.on 'close', ->
finished 'Packaged', bundle.fullpath
cb()
iter = ->
if i is imports.length
ws.end()
return
rs = imports[i++].readStream()
return iter() unless rs?
rs.pipe ws, {end: false}
rs.on 'end', iter
iter()
|
This method returns the full path to a bundle file. | bundlePath: (file) ->
(require 'path').join @build, util.changeext file.relpath, @constructor.ext
|
This method returns the full path to the compressed bundle file. | compressedPath: (file) ->
(require 'path').join @build, if @compress is true
util.changeext file.relpath, @constructor.compressedext
else
_.template(compress) filename: file.relpath
|
This method returns a list of modules required by the package, in its current state, including its contained sources, to function properly. | requiredModules: ->
_.chain(@sources)
.values().flatten().invoke('requiredModules')
.flatten().uniq().value()
|
This method is used to make sure the package is ready before performing an action. It takes a continuation callback, called immediately if the package is already ready, or as soon as it is. | ready: (cb) ->
if @_ready then cb() else @on 'ready', cb
|
This method is used to perform all necessary tranforming and
aggregating actions for all files in the package. This includes
compiling files into their javascript or css equivalents,
aggregating bundles and compressing them. This is done by finding
all files that are not depended by any other files (root nodes),
and calling their | actualize: (cb) ->
@ready =>
allFiles = []
for type, files of @files
for relpath, file of files
allFiles.push file
roots = _.filter allFiles, (file) -> file.liabilities.length == 0
i = 0
iter = ->
return process.nextTick(cb) if i is roots.length
roots[i++].actualize iter
iter()
|
This method is used to first | watch: (reset, ready) ->
@actualize =>
for type, sources of @sources
for src in sources
src.watch reset
for type, files of @files
for relpath, file of files
file.watch reset
ready() if ready?
|
This method is used to remove the FSWatchers from all files and sources. | unwatch: ->
for type, sources of @sources
for src in sources
src.unwatch()
for type, files of @files
file.unwatch() for relpath, file of files
|
This method is used to remove all files that were marked as impermanent (see next method). | clean: ->
@ready =>
for file in @impermanents()
file.unlinkSync()
|
This method returns a list of all impermanent files which includes, for instance, compiled files, bundles and their compressed counterparts. The rationale behind this is that those files are all completely derived from source files, thus could be removed safely without losing information. | impermanents: ->
acc = []
for type, files of @files
for file in _.values(files) when file.impermanent is true
acc.push file
acc
exports.Package = Package
|