coffee-toaster
Version:
Minimalist build system for CoffeeScript.
435 lines (332 loc) • 12.8 kB
text/coffeescript
#<< toaster/utils/*
#<< toaster/core/script
class Builder
# requirements
fs = require 'fs'
fsu = require 'fs-util'
path = require 'path'
cs = require "coffee-script"
cp = require "child_process"
uglify = require("uglify-js").uglify
uglify_parser = require("uglify-js").parser
Script = toaster.core.Script
{FnUtil, ArrayUtil, StringUtil} = toaster.utils
watchers: null
_include_tmpl: "document.write('<scri'+'pt src=\"%SRC%\"></scr'+'ipt>')"
constructor:(@toaster, @cli, @config)->
@vendors = @config.vendors
@bare = @config.bare
@packaging = @config.packaging
@expose = @config.expose
@minify = @config.minify
@exclude = [].concat( @config.exclude )
@httpfolder = @config.httpfolder
@release = @config.release
@debug = @config.debug
@init()
@watch() if @cli.argv.w
init:()->
# initializes buffer array to keep all tracked files
@files = @config.files
for folder in @config.src_folders
# search for all *.coffee files inside src folder
result = fsu.find folder.path, /.coffee$/m
# folder path and alias
fpath = folder.path
falias = folder.alias
# collects every files into Script instances
for file in result
include = true
for item in @exclude
include &= !(new RegExp( item ).test file)
if include
s = new Script @, fpath, file, falias, @cli
@files.push s
reset:()->
for watcher in @watchers
watcher.close()
build:( header_code = "", footer_code = "" )=>
# namespaces
namespaces = @build_namespaces()
# prepare vendors
vendors = @merge_vendors()
# prepare release contents
contents = []
contents.push vendors if vendors isnt ""
contents.push namespaces if @packaging
contents.push header_code if header_code isnt ""
contents.push @compile()
contents.push footer_code if header_code isnt ""
contents = contents.join '\n'
# uglifying
if @minify
ast = uglify_parser.parse contents
ast = uglify.ast_mangle ast
ast = uglify.ast_squeeze ast
contents = uglify.gen_code ast
# write release file
fs.writeFileSync @release, contents
# notify user through cli
now = ("#{new Date}".match /[0-9]{2}\:[0-9]{2}\:[0-9]{2}/)[0]
log "[#{now}] #{'Compiled'.bold} #{@release}".green
# compiling for debug
if @cli.argv.d && @debug? and not @cli.argv.a
files = @compile_for_debug()
# saving boot loader
for f, i in files
include = path.normalize "#{@httpfolder}/toaster/#{f}"
include = include.replace /\\/g, "\/" if path.sep == "\\"
tmpl = @_include_tmpl.replace "%SRC%", include
files[i] = tmpl
# prepare boot loader contents
contents = []
contents.push vendors if vendors isnt ""
contents.push namespaces if @packaging
contents.push header_code if header_code isnt ""
contents.push (files.join "\n")
contents.push footer_code if header_code isnt ""
contents = contents.join '\n'
# write boot-loader file
fs.writeFileSync @debug, contents
# notify user through cli
log "[#{now}] #{'Compiled'.bold} #{@debug}".green
# autorun mode
if @cli.argv.a
# getting arguments after the third ( first three are ['node', 'path_to_toaster', '-a'] )
args = []
if process.argv.length > 3
for i in [3...process.argv.length] by 1
args.push process.argv[i]
if @child?
log "Application restarted:".blue
@child.kill('SIGHUP')
else
log "Application started:".blue
# adding debug arguments
if @cli.argv.d
@child = cp.fork @release, args, { execArgv: ['--debug-brk'] }
else
@child = cp.fork @release, args
# Creates a NS holder for all folders
build_namespaces:()->
tree = {}
for folder in @config.src_folders
t = (tree[folder.alias] = {}) if folder.alias?
@build_ns_tree t || tree, folder.path
buffer = ""
for name, scope of tree
buffer += "var #{name} = "
buffer += "#{@expose}.#{name} = " if @expose?
buffer += (JSON.stringify scope, null, '').replace /\"/g, "'"
buffer += ";\n"
return buffer
# Walk some folderpath and lists all its subfolders
build_ns_tree:( tree, folderpath )->
folders = fsu.ls folderpath
for folder in folders
include = true
for item in @exclude
include &= !(new RegExp( item ).test folder)
if include
@build_ns_tree (tree[folder.match /[^\/\\]+$/m] = {}), folder
watch:()->
@watchers = []
# loops through all source folders
for src in @config.src_folders
# and watch them entirely
@watchers.push (watcher = fsu.watch src.path, /.coffee$/m)
watcher.on 'create', (FnUtil.proxy @on_fs_change, src, 'create')
watcher.on 'change', (FnUtil.proxy @on_fs_change, src, 'change')
watcher.on 'delete', (FnUtil.proxy @on_fs_change, src, 'delete')
# watching vendors for changes
for vendor in @vendors
temp = fsu.watch vendor
temp.on 'create', (FnUtil.proxy @on_fs_change, src, 'create')
temp.on 'change', (FnUtil.proxy @on_fs_change, src, 'change')
temp.on 'delete', (FnUtil.proxy @on_fs_change, src, 'delete')
on_fs_change:(src, ev, f)=>
# skip all folder creation
return if f.type == "dir" and ev == "create"
# folder path and alias
fpath = f.location
spath = src.path
if src.alias?
falias = (path.sep + src.alias)
else
falias = ''
# check if it's from some excluded folder
include = true
for item in @exclude
include &= !(new RegExp( item ).test fpath)
return unless include
# Titleize the type for use in the log messages bellow
type = StringUtil.titleize f.type
# relative filepath (with alias, if informed)
relative_path = fpath.replace spath, falias
# date for CLI notifications
now = ("#{new Date}".match /[0-9]{2}\:[0-9]{2}\:[0-9]{2}/)[0]
# switch over created, deleted, updated and watching
switch ev
# when a new file is created
when "create"
# initiate file and adds it to the array
if f.type == "file"
# toaster/core/script
s = new Script @, spath, fpath, falias, @cli
@files.push s
# cli filepath
msg = "#{('New ' + f.type + ' created').bold}"
log "[#{now}] #{msg} #{f.location}".cyan
# when a file is deleted
when "delete"
# removes files from array
file = ArrayUtil.find @files, relative_path, "filepath"
return if file is null
@files.splice file.index, 1
# cli msg
msg = "#{(type + ' deleted, stop watching').bold}"
log "[#{now}] #{msg} #{f.location}".red
# when a file is updated
when "change"
# updates file information
file = ArrayUtil.find @files, relative_path, "filepath"
if file is null
warn "CHANGED FILE IS APPARENTLY NULL..."
else
file.item.getinfo()
# cli msg
msg = "#{(type + ' changed').bold}"
log "[#{now}] #{msg} #{f.location}".cyan
if @toaster.before_build is null or @toaster.before_build()
# rebuilds modules
@build()
compile:()->
# validating syntax
for file in @files
try
cs.compile file.raw
# if there's some error
catch err
# catches and shows it, and abort the compilation
msg = err.message.replace '"', '\\"'
msg = "#{msg.white} at file: " + "#{file.filepath}".bold.red
error msg
return null
# otherwise move ahead, and expands all dependencies wild-cards
file.expand_dependencies() for file in @files
# reordering files
@reorder()
# merging everything
output = (file.raw for file in @files).join "\n"
# compiling
output = cs.compile output, {bare: @bare}
compile_for_debug:()->
release_path = path.dirname @debug
release_path = path.join release_path, "toaster"
# cleaning previous/existent structure
fsu.rm_rf release_path if fs.existsSync release_path
# creating new structre from scratch
fsu.mkdir_p release_path
# initializing empty array of filepaths
files = []
# loop through all ordered files
for file, index in @files
# computing releative filepath (replacing .coffee by .js)
find = "#{@toaster.basepath}#{path.sep}"
relative_path = file.filepath.replace find, ""
relative_path = relative_path.replace ".coffee", ".js"
# computing absolute filepath
absolute_path = path.resolve (path.join release_path, relative_path)
# extracts its folder path
folder_path = path.dirname absolute_path
# create container folder if it doesnt exist yet
fsu.mkdir_p folder_path if !fs.existsSync folder_path
# writing file
try
fs.writeFileSync absolute_path, cs.compile file.raw, {bare:@bare}
catch err
## dont show nothing because the error was alreary shown
## in the compile rotine above
#
# msg = err.message.replace '"', '\\"'
# msg = "#{msg.white} at file: " + "#{file.filepath}".bold.red
# error msg
continue
# adds to the files buffer
files.push relative_path
# returns all filepaths
return files
missing = {}
reorder: (cycling = false) ->
# log "Module.reorder"
# if cycling is true or @missing is null, initializes empty array
# for holding missing dependencies
#
# cycling means the redorder method is being called recursively,
# no other methods call it with cycling = true
@missing = {} if cycling is false
# looping through all files
for file, i in @files
# if theres no dependencies, go to next file
continue if !file.dependencies.length && !file.baseclasses.length
# otherwise loop thourgh all file dependencies
for filepath, index in file.dependencies
# search for dependency
dependency = ArrayUtil.find @files, filepath, "filepath"
dependency_index = dependency.index if dependency?
# continue if the dependency was already initialized
continue if dependency_index < i && dependency?
# if it's found
if dependency?
# if there's some circular dependency loop
if ArrayUtil.has dependency.item.dependencies, file.filepath
# remove it from the dependencies
file.dependencies.splice index, 1
# then prints a warning msg and continue
warn "Circular dependency found between ".yellow +
filepath.grey.bold + " and ".yellow +
file.filepath.grey.bold
continue
# otherwise if no circular dependency is found, reorder
# the specific dependency and run reorder recursively
# until everything is beautiful
else
@files.splice index, 0, dependency.item
@files.splice dependency.index + 1, 1
@reorder true
break
# otherwise if the dependency is not found
else if @missing[filepath] != true
# then add it to the @missing hash (so it will be ignored
# until reordering finishes)
@missing[filepath] = true
# move it to the end of the dependencies array (avoiding
# it from being touched again)
file.dependencies.push filepath
file.dependencies.splice index, 1
# ..and finally prints a warning msg
warn "#{'Dependency'.yellow} #{filepath.bold.grey} " +
"#{'not found for file'.yellow} " +
file.filepath.grey.bold
# validate if all base classes was properly imported
file_index = ArrayUtil.find @files, file.filepath, "filepath"
file_index = file_index.index
for bc in file.baseclasses
found = ArrayUtil.find @files, bc, "classname"
not_found = (found == null) || (found.index > file_index)
if not_found && !@missing[bc]
@missing[bc] = true
warn "Base class ".yellow +
"#{bc} ".bold.grey +
"not found for class ".yellow +
"#{file.classname} ".bold.grey +
"in file ".yellow +
file.filepath.bold.grey
merge_vendors:()=>
buffer = []
for vendor in @vendors
if fs.existsSync vendor
buffer.push fs.readFileSync vendor, 'utf-8'
else
warn "Vendor not found at ".white + vendor.yellow.bold
return buffer.join "\n"