buildr
Version:
The (Java|Coffee)Script and (CSS|Less) (Builder|Bundler|Packer|Minifier|Merger|Checker)
1,148 lines (900 loc) • 23.5 kB
text/coffeescript
# Requires
fs = require 'fs'
path = require 'path'
util = require 'bal-util'
coffee = require 'coffee-script'
less = require 'less-bal'
pulverizr = require 'pulverizr-bal'
csslint = require('csslint').CSSLint
jshint = require('jshint').JSHINT
uglify = require 'uglify-js'
jsp = uglify.parser
pro = uglify.uglify
cwd = process.cwd()
# =====================================
# Prototypes
# Checks if an array contains a value
Array::has or= (value2) ->
for value1 in @
if value1 is value2
return true
return false
# =====================================
# Buildr
# Define
class Buildr
# Configuration
config: {
# Options
log: true # true or false, log status updates to console?
# Paths
srcPath: false # String
outPath: false # String or false
# Checking
checkScripts: true # Array or true or false
checkStyles: true # Array or true or false
jshintOptions: false # Object or false
csslintOptions: false # Object or false
# Compression (requires outPath)
compressScripts: true # Array or true or false
compressStyles: true # Array or true or false
compressImages: true # Array or true or false
# Order
scriptsOrder: false # Array or false
stylesOrder: false # Array or false
# Bundling (requires outPath and Order)
bundleScriptPath: false # String or false
bundleStylePath: false # String or false
deleteBundledFiles: true # true or false
# Loaders (requires Order)
srcLoaderHeader: false # String or false
srcLoaderPath: false # String or false
}
# Files to clean
filesToClean: []
# Error
errors: []
# Constructor
constructor: (config) ->
# Prepare
config or= {}
# Apply
for own key, value of config
@config[key] = value
# Completed
true
# =====================================
# Actions
# Log
log: (messages...) ->
console.log.apply console, messages
# Process
process: (next) ->
# Check configuration
@checkConfiguration (err) =>
return next err if err
# Check files
@checkFiles (err) =>
return next err if err
# Copy srcPath to outPath
@cpSrcToOut (err) =>
return next err if err
# Generate files
@generateFiles (err) =>
return next err if err
# Clean outPath
@cleanOutPath (err) =>
return next err if err
# Compress outPath
@compressFiles (err) =>
next err
# ---------------------------------
# Check Configuration
# Check Configuration
# next(err)
checkConfiguration: (next) ->
# Check
return next new Error('srcPath is required') unless @config.srcPath
# Prepare
tasks = new util.Group (err) =>
@log 'Checked configuration'
next err
tasks.total = 6
@log 'Checking configuration'
# Ensure
@config.outPath or= @config.srcPath
# Expand srcPath
util.expandPath @config.srcPath, cwd, {}, (err,srcPath) =>
return tasks.exit err if err
@config.srcPath = srcPath
tasks.complete err
# Expand outPath
util.expandPath @config.outPath, cwd, {}, (err,outPath) =>
return tasks.exit err if err
@config.outPath = outPath
tasks.complete err
# Expand bundleScriptPath
if @config.bundleScriptPath
util.expandPath @config.bundleScriptPath, cwd, {}, (err,bundleScriptPath) =>
return tasks.exit err if err
@config.bundleScriptPath = bundleScriptPath
tasks.complete err
else
tasks.complete false
# Expand bundleStylePath
if @config.bundleStylePath
util.expandPath @config.bundleStylePath, cwd, {}, (err,bundleStylePath) =>
return tasks.exit err if err
@config.bundleStylePath = bundleStylePath
tasks.complete err
else
tasks.complete false
# Expand srcLoaderPath
if @config.srcLoaderPath
util.expandPath @config.srcLoaderPath, cwd, {}, (err,srcLoaderPath) =>
return tasks.exit err if err
@config.srcLoaderPath = srcLoaderPath
tasks.complete err
else
tasks.complete false
# Adjust Atomic Options
if @config.srcPath is @config.outPath
@config.deleteBundledFiles = false
if @config.compressScripts
@config.compressScripts =
if @config.bundleScriptPath
[@config.bundleScriptPath]
else
false
if @config.compressStyles
@config.compressStyles =
if @config.bundleStylePath
[@config.bundleStylePath]
else
false
if @config.compressImages
@config.compressImages = false
# Auto find files?
# Not yet implemented
if @config.bundleScripts is true
@config.bundleScripts = false
if @config.bundleStyles is true
@config.bundleStyles = false
# Finish
tasks.complete false
# ---------------------------------
# Check Files
# Check files
# next(err)
checkFiles: (next,config) ->
# Prepare
config or= @config
return next false unless config.checkScripts or config.checkStyles
@log 'Check files'
# Handle
@forFilesInDirectory(
# Directory
config.srcPath
# Callback
(fileFullPath,fileRelativePath,next) =>
# Render
@checkFile fileFullPath, next
# Next
(err) =>
return next err if err
@log 'Checked files'
next err
)
# Completed
true
# ---------------------------------
# Copy srcPath to outPath
# Copy srcPath to outPath
# next(err)
cpSrcToOut: (next,config) ->
# Prepare
config or= @config
return next false if config.outPath is config.srcPath
@log "Copying #{config.srcPath} to #{config.outPath}"
# Remove outPath
util.rmdir config.outPath, (err) =>
return next err if err
# Copy srcPath to outPath
util.cpdir config.srcPath, config.outPath, (err) =>
# Next
@log "Copied #{config.srcPath} to #{config.outPath}"
next err
# Completed
true
# ---------------------------------
# Generate Files
# Generate files
# next(err)
generateFiles: (next) ->
# Prepare
tasks = new util.Group (err) =>
@log 'Generated files'
next err
tasks.total += 3
@log 'Generating files'
# Generate src loader file
@generateSrcLoaderFile tasks.completer()
# Generate bundled script file
@generateBundledScriptFile tasks.completer()
# Generate bundle style file
@generateBundledStyleFile tasks.completer()
# Completed
true
# Generate src loader file
# next(err)
generateSrcLoaderFile: (next,config) ->
# Check
config or= @config
return next false unless config.srcLoaderPath
# Log
@log "Generating #{config.srcLoaderPath}"
# Prepare
templates = {}
srcLoaderData = ''
srcLoaderPath = config.srcLoaderPath
loadedInTemplates = null
# Loaded in Templates
templateTasks = new util.Group (err) =>
# Check
next err if err
# Stringify scripts
srcLoaderData += "scripts = [\n"
for script in config.scriptsOrder
srcLoaderData += "\t'#{script}'\n"
srcLoaderData += "\]\n\n"
# Stringify styles
srcLoaderData += "styles = [\n"
for style in config.stylesOrder
srcLoaderData += "\t'#{style}'\n"
srcLoaderData += "\]\n\n"
# Append Templates
srcLoaderData += templates.srcLoader+"\n\n"+templates.srcLoaderHeader
# Write in coffee first for debugging
fs.writeFile srcLoaderPath, srcLoaderData, (err) =>
# Check
return next err if err
# Compile Script
srcLoaderData = coffee.compile(srcLoaderData)
# Now write in javascript
fs.writeFile srcLoaderPath, srcLoaderData, (err) =>
# Check
return next err if err
# Log
@log "Generated #{config.srcLoaderPath}"
# Good
next false
# Total Template Tasks
templateTasks.total = if config.srcLoaderHeader then 1 else 2
# Load srcLoader Template
fs.readFile __dirname+'/templates/srcLoader.coffee', (err,data) ->
return templateTasks.exit err if err
templates.srcLoader = data.toString()
templateTasks.complete err
# Load srcLoaderHeader Template
if config.srcLoaderHeader
templates.srcLoaderHeader = config.srcLoaderHeader
else
fs.readFile __dirname+'/templates/srcLoaderHeader.coffee', (err,data) ->
return templateTasks.exit err if err
templates.srcLoaderHeader = data.toString()
templateTasks.complete err
# Completed
true
# Generate out style file
# next(err)
generateBundledStyleFile: (next,config) ->
# Check
config or= @config
return next false unless config.bundleStylePath
# Log
@log "Generating #{config.bundleStylePath}"
# Prepare
source = ''
# Cycle
@useOrScan(
# Files
config.stylesOrder
# Directory
@config.outPath
# Callback
(fileFullPath,fileRelativePath,next) =>
# Ensure .less file exists
extension = path.extname(fileRelativePath)
switch extension
# CSS
when '.css'
# Determine less path
_fileRelativePath = fileRelativePath
_fileFullPath = fileFullPath
fileRelativePath = _fileRelativePath.substring(0,_fileRelativePath.length-extension.length)+'.less'
fileFullPath = _fileFullPath.substring(0,_fileFullPath.length-extension.length)+'.less'
# Amend clean files
if config.deleteBundledFiles
@filesToClean.push _fileFullPath
@filesToClean.push fileFullPath
# Check if less path exists
path.exists fileFullPath, (exists) ->
# It does
if exists
# Append source
source += """@import "#{fileRelativePath}";\n"""
next false
# It doesn't
else
# Create it
util.cp _fileFullPath, fileFullPath, (err) ->
return next err if err
# Append source
source += """@import "#{fileRelativePath}";\n"""
next false
# Less
when '.less'
# Amend clean files
if config.deleteBundledFiles
@filesToClean.push fileFullPath
# Append source
source += """@import "#{fileRelativePath}";\n"""
next false
# Something else
else
next false
# Next
(err) =>
return next err if err
# Compile file
@compileStyleData(
# File Path
config.bundleStylePath
# Source
source
# Next
(err,result) =>
return next err if err
# Write
fs.writeFile config.bundleStylePath, result, (err) =>
# Log
@log "Generated #{config.bundleStylePath}"
# Forward
next err, result
)
)
# Completed
true
# Generate out script file
# next(err)
generateBundledScriptFile: (next,config) ->
# Check
config or= @config
return next false unless config.bundleScriptPath
# Log
@log "Generating #{config.bundleScriptPath}"
# Prepare
results = {}
# Cycle
@useOrScan(
# Files
config.scriptsOrder
# Directory
config.outPath
# Callback
(fileFullPath,fileRelativePath,next) =>
# Ensure valid extension
extension = path.extname(fileRelativePath)
switch extension
# Script
when '.js','.coffee'
# Render
@compileScriptFile(
# File path
fileFullPath
# Next
(err,result) =>
return next err if err
results[fileRelativePath] = result
if config.deleteBundledFiles
@filesToClean.push fileFullPath
next err
# Write file
false
)
# Else
else
next false
# Next
(err) =>
return next err if err
# Prepare
result = ''
# Cycle Array
if config.scriptsOrder.has?
for fileRelativePath in config.scriptsOrder
return next new Error("The file #{fileRelativePath} failed to compile") unless results[fileRelativePath]?
result += results[fileRelativePath]
# Write file
fs.writeFile config.bundleScriptPath, result, (err) =>
# Log
@log "Generated #{config.bundleScriptPath}"
# Forward
next err
)
# Completed
true
# ---------------------------------
# Clean outPath
# Clean outPath
# next(err)
cleanOutPath: (next) ->
# Check
return next false unless (@filesToClean||[]).length
# Prepare
tasks = new util.Group (err) =>
@log 'Cleaned outPath'
next err
tasks.total += @filesToClean.length
@log 'Cleaning outPath'
# Delete files to clean
for fileFullPath in @filesToClean
@log "Cleaning #{fileFullPath}"
fs.unlink fileFullPath, tasks.completer()
# Completed
true
# ---------------------------------
# Compress Files
# Compress files
# next(err)
compressFiles: (next,config) ->
# Prepare
config or= @config
return next false unless config.compressScripts or config.compressStyles or config.compressImages
@log 'Compress files'
# Handle
@forFilesInDirectory(
# Directory
config.outPath
# Callback
(fileFullPath,fileRelativePath,next) =>
# Render
@compressFile fileFullPath, next
# Next
(err) =>
return next err if err
@log 'Compressed files'
next err
)
# Completed
true
# =====================================
# Helpers
# For each file in an array
# callback(fileFullPath,fileRelativePath,next)
# next(err)
forFilesInArray: (files,parentPath,callback,next) ->
# Check
return next false unless (files||[]).length
# Prepare
tasks = new util.Group (err) =>
next err
tasks.total += files.length
# Cycle
for fileRelativePath in files
# Expand filePath
((fileRelativePath)=>
util.expandPath fileRelativePath, parentPath, {}, (err,fileFullPath) =>
return tasks.exit err if err
callback fileFullPath, fileRelativePath, tasks.completer()
)(fileRelativePath)
# Completed
true
# For each file in a directory
# callback(fileFullPath,fileRelativePath,next)
# next(err)
forFilesInDirectory: (parentPath,callback,next) ->
# Scan for files
util.scandir(
# Path
parentPath
# File Action
# next(err)
callback
# Dir Action
false
# Next
next
)
# Completed
true
# Use or scan
# callback(fileFullPath,fileRelativePath,next)
# next(err)
useOrScan: (files,parentPath,callback,next) ->
# Handle
if files is true
@forFilesInDir(
# Files
files
# Directory
parentPath
# Callback
callback
# Next
next
)
else if files and files.length
@forFilesInArray(
# Files
files
# Directory
parentPath
# Callback
callback
# Next
next
)
else
next false
# Completed
true
# =====================================
# Files
# Compile the file
# next(err)
compileFile: (fileFullPath,next) ->
# Prepare
extension = path.extname fileFullPath
# Handle
switch extension
when '.coffee'
@compileScriptFile fileFullPath, next
when '.less'
@compileStyleFile fileFullPath, next
else
next false
# Completed
true
# Compress the file
# next(err)
compressFile: (fileFullPath,next,config) ->
# Prepare
config or= @config
extension = path.extname fileFullPath
# Handle
switch extension
# Scripts
when '.js'
if config.compressScripts is true or config.compressScripts.has? and config.compressScripts.has(fileFullPath)
@compressScriptFile fileFullPath, next
else
next false
# Styles
when '.css'
if config.compressStyles is true or config.compressStyles.has? and config.compressStyles.has(fileFullPath)
@compressStyleFile fileFullPath, next
else
next false
# Images
when '.gif','.jpg','.jpeg','.png','.tiff','.bmp'
if config.compressImages is true or config.compressImages.has? and config.compressImages.has(fileFullPath)
@compressImageFile fileFullPath, next
else
next false
# Other
else
next false
# Completed
true
# Check the file
# next(err)
checkFile: (fileFullPath,next,config) ->
# Prepare
config or= @config
extension = path.extname fileFullPath
# Handle
switch extension
when '.js'
if config.checkScripts is true or config.checkScripts.has? and config.checkScripts.has(fileFullPath)
@checkScriptFile fileFullPath, next
else
next false
when '.css'
if config.checkStyles is true or config.checkStyles.has? and config.checkStyles.has(fileFullPath)
@checkStyleFile fileFullPath, next
else
next false
else
next false
# Completed
true
# =====================================
# Image Files
# ---------------------------------
# Compress
# Compress Image File
# next(err)
compressImageFile: (fileFullPath,next) ->
# Log
@log "Compressing #{fileFullPath}"
# Attempt
try
# Compress
pulverizr.compress fileFullPath, quiet: true
# Log
@log "Compressed #{fileFullPath}"
# Forward
next false
# Error
catch err
# Forward
next err
# Complete
true
# =====================================
# Style Files
# ---------------------------------
# Compile
# Compile Style File
# next(err,result)
compileStyleData: (fileFullPath,src,next) ->
# Prepare
result = ''
options =
paths: [path.dirname(fileFullPath)]
optimization: 1
filename: fileFullPath
# Compile
new (less.Parser)(options).parse src, (err, tree) =>
if err
@log err
next new Error('Less compilation failed'), result
else
try
# Compile
result = tree.toCSS compress: 0
# Write
next false, result
catch err
next err, result
# Completed
true
# Compile Style File
# next(err,result)
compileStyleFile: (fileFullPath,next,write=true) ->
# Log
# @log "Compiling #{fileFullPath}"
# Read
fs.readFile fileFullPath, (err,data) =>
return next err if err
# Compile
@compileStyleData fileFullPath, data.toString(), (err,result) =>
return next err, result if err or !write
# Write
fs.writeFile fileFullPath, result, (err) =>
return next err if err
# Log
# @log "Compiled #{fileFullPath}"
# Forward
next err, result
# Completed
true
# ---------------------------------
# Compress
# Compress Style File
# next(err,result)
compressStyleData: (fileFullPath,src,next) ->
# Prepare
result = ''
options =
paths: [path.dirname(fileFullPath)]
optimization: 1
filename: fileFullPath
# Compress
new (less.Parser)(options).parse src, (err, tree) =>
if err
@log err
next new Error('Less compilation failed'), result
else
try
# Compress
result = tree.toCSS compress: 1
# Write
next false, result
catch err
next err, result
# Completed
true
# Compress Style File
# next(err,result)
compressStyleFile: (fileFullPath,next,write=true) ->
# Log
@log "Compressing #{fileFullPath}"
# Read
fs.readFile fileFullPath, (err,data) =>
return next err if err
# Compress
@compressStyleData fileFullPath, data.toString(), (err,result) ->
return next err, result if err or !write
# Write
fs.writeFile fileFullPath, result, (err) ->
return next err if err
# Log
@log "Compressed #{fileFullPath}"
# Forward
next err, result
# Completed
true
# ---------------------------------
# Check
# Check Style Data
# next(err,errord)
checkStyleData: (fileFullPath,src,next,config) ->
# Prepare
config or= @config
errord = false
# Peform checks
result = csslint.verify src, config.csslintOptions||{}
formatId = 'text'
# Check for errors
unless result.messages.length
return next false, false
# Log the errors
for message in result.messages
continue unless message and message.type is 'error'
# Errord
errord = true
# Output
if errord
@log csslint.getFormatter(formatId).formatResults(result, fileFullPath, formatId)
# Forward
next false, errord
# Check Style File
# next(err,errord)
checkStyleFile: (fileFullPath,next) ->
# Log
@log "Checking #{fileFullPath}"
# Read
fs.readFile fileFullPath, (err,data) =>
# Error
return next err, false if err
# Check
@checkStyleData fileFullPath, data.toString(), (err,errord) =>
return next err if err
# Log
@log "Checked #{fileFullPath}"
# Forward
return next err, errord
# Completed
true
# =====================================
# Script Files
# ---------------------------------
# Compile
# Compile Script Data
# next(err,result)
compileScriptData: (extension,src,next) ->
# Prepare
result = false
# Compile
try
switch extension
when '.coffee'
result = coffee.compile src
when '.js'
result = src
else
throw new Error('Unknown script type: '+extension)
catch err
next err
# Forward
next false, result
# Compile Script File
# next(err,result)
compileScriptFile: (fileFullPath,next,write=true) ->
# Log
# @log "Compiling #{fileFullPath}"
# Read
fs.readFile fileFullPath, (err,data) =>
return next err if err
# Compile
@compileScriptData path.extname(fileFullPath), data.toString(), (err,result) =>
return next err, result if err or !write
# Write
fs.writeFile fileFullPath, result, (err) =>
return next err if err
# Log
# @log "Compiled #{fileFullPath}"
# Forward
next err, result
# Completed
true
# ---------------------------------
# Compress
# Compress Script Data
# next(err,result)
compressScriptData: (src,next) ->
# Compress
ast = jsp.parse(src) # parse code and get the initial AST
ast = pro.ast_mangle(ast) # get a new AST with mangled names
ast = pro.ast_squeeze(ast) # get an AST with compression optimizations
out = pro.gen_code(ast) # compressed code here
# Forward
return next false, out
# Compress Script File
# next(err,result)
compressScriptFile: (fileFullPath,next,write=true) ->
# Log
@log "Compressing #{fileFullPath}"
# Read
fs.readFile fileFullPath, (err,data) =>
return next err if err
# Compile
@compressScriptData data.toString(), (err,result) =>
return next err, result if err or !write
# Write
fs.writeFile fileFullPath, result, (err) =>
return next err if err
# Log
@log "Compressed #{fileFullPath}"
# Forward
next err, result
# Completed
true
# ---------------------------------
# Check
# Check Script Data
# next(err,errord)
checkScriptData: (fileFullPath,src,next,config) ->
# Prepare
config or= @config
errord = false
# Peform checks
jshint src, config.jshintOptions||{}
result = jshint.data()
result.errors or= []
# Check for errors
unless result.errors.length
return next false, false
# Log the file
@log "\n#{fileFullPath}:"
# Log the errors
for error in result.errors
continue unless error and error.raw
# Errord
errord = true
# Log
message = error.raw.replace(/\.$/,'').replace /\{([a-z])\}/, (a,b) ->
error[b] or a
evidence =
if error.evidence
"\n\t" + error.evidence.replace(/^\s+/, '')
else
''
@log "\tLine #{error.line}: #{message} #{evidence}\n"
# Forward
next false, errord
# Check Script File
# next(err,errord)
checkScriptFile: (fileFullPath,next) ->
# Log
@log "Checking #{fileFullPath}"
# Read
fs.readFile fileFullPath, (err,data) =>
# Error
return next err, false if err
# Check
@checkScriptData fileFullPath, data.toString(), (err,errord) =>
return next err if err
# Log
console.log "Checked #{fileFullPath}"
# Forward
return next err, errord
# Completed
true
# =====================================
# Export
module.exports =
createInstance: (options) ->
return new Buildr(options)