toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
1,036 lines (944 loc) • 31.2 kB
JavaScript
/*
When compiling the file `main.html`, we gather all the CSS and JS files
needed.
If `no-zip` option has not been set, all the CSS files are combined in
`css/@main.html` and all the JS files are combined in `js/@main.html`.
# Modules
Modules are stored in folder `mod/`.
* `mymodule.js`
* `mymodule.css`
* `mymodule/`
* `mymodule.dep`
*/
var FS = require( "fs" );
var Path = require( "path" );
var CompilerINI = require( "./compiler-ini" );
var CompilerCOM = require( "./compiler-com" );
var ParserHTML = require( "./tlk-htmlparser" );
var SourceMap = require( "./source-map" );
var PathUtils = require( "./pathutils" );
var MinifyJS = require( "./minifyJS" );
var Template = require( "./template" );
var DepFind = require( "./dependencies-finder" );
var Chrono = require( "./chrono" );
var Source = require( "./source" );
var Fatal = require( "./fatal" );
var Util = require( "./util" );
var Tree = require( "./htmltree" );
var Libs = require( "./compiler-com-libs" );
var Tpl = require( "./template" );
var Xjs = require("./boilerplate");
var Rework = require('rework');
var Vars = require('rework-vars');
var Project,
Components,
Scopes;
exports.initialize = function ( prj ) {
Project = prj;
Components = {};
Scopes = [ {} ];
CompilerCOM.loadComponents( prj );
};
exports.terminate = function () {};
/**
* @param {string} file Full path of the HTML file to compile.
*/
exports.compile = function ( file, options ) {
if ( typeof options === 'undefined' ) options = {};
var sourceHTML = new Source( Project, file ),
// Output of the main file.
output,
// Output of a page file.
outputPage,
// In case of multi-pages, the first page has the same name as the
// `file`. So it's output must become the output of the `file`.
outputOfFirstPage,
// Page filename relative to the sourceHTML.
pageFilename,
// Array of the sources we must link.
sourcesToLink = [];
// Check if the file and all its components are uptodate.
if ( !isUptodate( sourceHTML ) ) {
Scopes[ 0 ].$filename = sourceHTML.name();
console.log( "Compile HTML: " + sourceHTML.name().cyan );
var root = ParserHTML.parse( sourceHTML.read() );
output = compileRoot( root, sourceHTML, options );
if ( output ) {
sourceHTML.tag( 'output', output );
while ( typeof root.type === 'undefined' && root.children && root.children.length == 1 ) {
root = root.children[ 0 ];
}
if ( root.type == Tree.PAGES ) {
outputPage = JSON.parse( JSON.stringify( output ) );
root.children.forEach( function ( child, idx ) {
console.log( ( " Page " + ( idx + 1 ) ).cyan );
var src = sourceHTML;
pageFilename = src.name();
if ( idx !== 0 ) {
pageFilename = file.substr( 0, file.length - 4 ) + idx + '.html';
src = new Source( Project, pageFilename );
}
outputPage = compileRoot( child, src, options, JSON.parse( JSON.stringify( output ) ) );
outputPage.filename = pageFilename;
src.tag( "output", outputPage );
src.save();
sourcesToLink.push( pageFilename );
if ( idx === 0 ) {
// This is the first page.
outputOfFirstPage = outputPage;
}
} );
sourceHTML.tag( 'pages', sourcesToLink );
sourceHTML.tag( 'output', outputOfFirstPage );
}
sourceHTML.save();
}
}
// Linking.
console.log( "Link: " + sourceHTML.name().yellow );
sourcesToLink = sourceHTML.tag( 'pages' );
if ( sourcesToLink ) {
// Multi-pages.
sourcesToLink.forEach( function ( pageFilename ) {
var src = new Source( Project, pageFilename );
link( src, options );
} );
} else {
// Single page.
link( sourceHTML, options );
}
output = sourceHTML.tag( 'output' );
return sourceHTML;
};
/**
*
*/
function compileRoot( root, sourceHTML, options, output ) {
// Stuff to create HTML file.
if ( typeof output === 'undefined' ) output = {
// Code CSS.
innerCSS: {},
// CSS files.
outerCSS: {},
// Code Javascript to embed in `js/@index.js` file.
innerJS: {},
// Javascript modules directly required.
require: {},
// All the Javascript modules needed to build this page.
modules: [],
// Javascript code to insert in a `DOMContentLoaded` event.
initJS: {},
// Javascript code to insert in a `DOMContentLoaded` event after all the code of `initJS`.
postInitJS: {},
// Files needed to build this file. If a include change, we must recompile.
include: {},
// A resource is a file to create in the output folder when this HTML is linked.
// the key is the resource name, and the value is an objet depending on the type of resource:
// * {dst: "img/plus.png", src: "../gfx/icon-plus.png"}
// * {dst: "img/face.svg", txt: "<svg xmlns:svg=..."}
resource: {},
// Modules that have no real file in `mod` directory, but
// which are created dynamically in Components.
dynamicModules: {}
};
var libs = Libs( sourceHTML, Components, Scopes, output );
libs.compile( root, options );
Tree.trim( root );
output.root = root;
return output;
}
/**
* A source needs to be rebuild if it is not uptodate.
* Here are the reasons for a source not to be uptodate:
* * Source code more recent than the tags (`Source.isUptodate()`).
* * Includes source codes more recent than this source.
*/
function isUptodate( sourceHTML ) {
if ( !sourceHTML.isUptodate() ) return false;
var output = sourceHTML.tag( 'output' );
if ( !output || !output.include ) return true;
// File name relative to `sourceHTML`.
var includeFilename;
// Source object of the `sourceFilename`.
var includeSource;
// Modification time for the current HTML file.
var currentFileModificationTime = sourceHTML.modificationTime();
// Stats of an include file.
var stats;
for ( includeFilename in output.include ) {
includeSource = new Source(
Project,
sourceHTML.getPathRelativeToSource( includeFilename )
);
stats = FS.statSync( includeSource.getAbsoluteFilePath() );
if ( stats.mtime > currentFileModificationTime ) {
// An included file if more recent. We must recompile.
return false;
}
}
return true;
}
/**
* Take a HTML file `filename.html` and combine all the styles in
* `css/@filename.css` and all the javascripts in `js/@filename.js`.
* For each module, check if there is a folder with the same name. If
* yes, copy that resource in `css` dir. For example, it you have
* `mod/foobar.js` and a folder `mod/foobar/`, copy it to
* `www/css/foobar/`.
*/
function link( src, options ) {
var htmlDir = Path.dirname( src.name() );
var pathWWW = Project.wwwPath( htmlDir );
var pathJS = Path.join( pathWWW, "js" );
var pathCSS = Path.join( pathWWW, "css" );
Project.mkdir( pathJS );
Project.mkdir( pathCSS );
var output;
output = linkForRelease( src, pathJS, pathCSS, options, htmlDir );
PathUtils.file(
Project.wwwPath( src.name() ),
'<!DOCTYPE html>' + Tree.toString( output.root ).trim()
);
// Writing resources if any.
writeResources( output );
}
function linkForRelease( src, pathJS, pathCSS, options, htmlDir ) {
Project.mkdir( Path.join( pathJS, "map" ) );
Project.mkdir( Path.join( pathCSS, "map" ) );
var key, val;
var prj = src.prj();
var nameWithoutExt = src.name().substr( 0, src.name().length - 5 );
// If `nameWithoutExt` is in a subfolder, `backToRoot` must containt
// as many `../` as there are subfolders in `nameWithoutExt`.
var backToRoot = getBackToRoot( nameWithoutExt );
backToRoot = ''; // Finalement les dépendances sont au même niveau que le fichier HTML.
var output = src.tag( "output" ) || {};
var root = output.root;
if ( !root ) {
Fatal.fire(
"The cache seems to be corrupted. Try `tfw clean` to clean it up. " +
"And try building again.",
"Please cleanup the cache!"
);
}
output.filename = src.name();
var head = findHead( root );
addDescriptionToHead( head, options );
var innerJS = Tpl.file( "require.js" ).out + concatDicValues( output.innerJS );
innerJS += getInitJS( output );
var innerCSS = concatDicValues( output.innerCSS );
// If there is a CSS file with the same name as the HTML file, embed it.
if ( FS.existsSync( Project.srcOrLibPath( nameWithoutExt + '.css' ) ) ) {
console.log( "Found: " + ( nameWithoutExt + ".css" ).bold );
innerCSS += PathUtils.file( Project.srcOrLibPath( nameWithoutExt + '.css' ) );
}
function addInnerJS() {
// Adding innerJS.
prj.flushContent( "js/" + addFilePrefix( nameWithoutExt ) + ".js", innerJS, htmlDir );
head.children.push( {
type: Tree.TAG,
name: 'script',
attribs: {
defer: null,
src: backToRoot + "js/" + addFilePrefix( nameWithoutExt ) + ".js"
}
} );
}
var combination = combineRequires( output, options );
var externalDeps = lookForExternalDependencies( combination.js );
externalDeps.js.forEach( function ( code ) {
innerJS += code;
} );
for ( key in externalDeps.res ) {
val = externalDeps.res[ key ];
try {
Project.copyFile( key, val );
} catch ( ex ) {
throw Error( "Unable to copy external dependency `" + key + "` into `" + val + "`!\n" +
JSON.stringify( externalDeps, null, ' ' ) );
}
}
// Used to loop over CSS and JS files.
if ( options.dev ) {
addInnerJS();
// DEBUG. Do not combine.
for ( key in combination.css ) {
val = combination.css[ key ];
if ( key.substr( 0, 4 ) == 'mod/' ) {
key = key.substr( 4 );
}
head.children.push( {
type: Tree.TAG,
name: 'link',
void: true,
attribs: {
rel: "stylesheet",
type: "text/css",
href: backToRoot + "css/" + key + ".css"
}
} );
prj.flushContent( "css/" + key + ".css", val.src, htmlDir );
}
for ( key in combination.js ) {
val = combination.js[ key ];
if ( key.substr( 0, 4 ) == 'mod/' ) {
key = key.substr( 4 );
}
head.children.push( {
type: Tree.TAG,
name: 'script',
attribs: {
defer: null,
src: backToRoot + "js/" + key + ".js"
}
} );
prj.flushContent( "js/" + key + ".js", val.src, htmlDir );
}
} else {
// RELEASE.
for ( key in combination.css ) {
val = combination.css[ key ];
innerCSS += val.zip;
}
for ( key in combination.js ) {
val = combination.js[ key ];
innerJS += val.zip + "\n";
}
addInnerJS();
}
// Adding innerCSS.
prj.flushContent( "css/" + addFilePrefix( nameWithoutExt ) + ".css", innerCSS, htmlDir );
head.children.push( {
type: Tree.TAG,
name: 'link',
void: true,
attribs: {
rel: "stylesheet",
type: "text/css",
href: backToRoot + "css/" + addFilePrefix( nameWithoutExt ) + ".css"
}
} );
return output;
}
function lookForExternalDependencies( jsFiles ) {
var jsFileName, jsFile;
var depFileName, depFile;
var dependencies;
// Module private variables which string's value is read from a text file.
var variables = {};
var dep, srcDep, dstDep;
var javascriptSources = [];
var resources = {};
// JSON string content of a dependency file.
var json;
for ( jsFileName in jsFiles ) {
jsFile = jsFiles[ jsFileName ];
depFileName = jsFileName + ".dep";
if ( Project.srcOrLibPath( depFileName ) ) {
depFile = new Source( Project, depFileName );
try {
json = depFile.read();
dependencies = JSON.parse( json );
// Looking for Javascript dependencies.
if ( dependencies.js ) {
if ( !Array.isArray( dependencies.js ) ) {
dependencies.js = [ "" + dependencies.js ];
}
dependencies.js.forEach( function ( js ) {
var filename = "mod/" + js;
var src = new Source( Project, filename );
var code = src.read();
pushUnique( javascriptSources, code );
} );
}
// Looking for other dependencies.
if ( dependencies.res ) {
for ( dep in dependencies.res ) {
if ( dependencies.res[ dep ] === "" ) {
// `res: { "bob/foo.png": "" }` is equivalent to
// `res: { "bob/foo.png": "bob/foo.png" }`
dependencies.res[ dep ] = dep;
}
srcDep = Project.srcOrLibPath( 'mod/' + dep );
if ( !srcDep ) {
srcDep = Project.srcOrLibPath( dep );
}
if ( !srcDep ) {
Fatal.fire(
"Unable to find dependency file `" + dep + "` nor `mod/" +
dep + "`!",
depFile.getAbsoluteFilePath()
);
}
dstDep = Project.wwwPath( dependencies.res[ dep ] );
resources[ srcDep ] = dstDep;
}
}
} catch ( ex ) {
Fatal.fire( "Unable to parse JSON!\n" + ex, depFile.getAbsoluteFilePath() );
}
}
}
return {
js: javascriptSources,
res: resources
};
}
/**
* Push `item` into `arr` if it is not already in.
*/
function pushUnique( arr, item ) {
if ( arr.indexOf( item ) > -1 ) return false;
arr.push( item );
return true;
}
function writeResources( output ) {
// Name of the resource.
var resourceName;
// Data of the resource.
var resourceData;
// Destination path (in `www`folder).
var dstPath;
// Source path (in `src` folder).
var srcPath;
// Resource content.
var content;
for ( resourceName in output.resource ) {
resourceData = output.resource[ resourceName ];
dstPath = Project.wwwPath( resourceData.dst );
if ( PathUtils.isDirectory( dstPath ) ) {
// We must copy a whole directory.
} else {
// Create folders if needed.
Project.mkdir( Path.dirname( dstPath ) );
if ( resourceData.src ) {
srcPath = Project.srcOrLibPath( resourceData.src );
Project.copyFile( srcPath, dstPath );
} else {
content = resourceData.txt;
PathUtils.file( dstPath, content );
}
}
}
// Copy modules' resources if any.
var moduleName;
// Path of the folder containing the resourses of the module (if any).
var resourcePath;
output.modules.forEach( function ( moduleName ) {
resourcePath = Project.srcOrLibPath( moduleName );
if ( resourcePath ) {
// Ok, this folder exists.
//console.info( "Copy resource: " + ( moduleName + "/" ).cyan );
var dst = Path.join( Path.dirname( output.filename ), moduleName.substr( 4 ) );
dst = dst.replace( /\\/g, '/' );
Project.copyFile( resourcePath, Project.wwwPath( 'css/' + dst ) );
}
} );
}
function concatDicValues( map ) {
if ( !map ) return '';
var key, out = '';
for ( key in map ) {
if ( out != '' ) out += "\n";
out += key;
}
return out;
}
function findHead( root ) {
if ( !root ) return null;
var head = Tree.getElementByName( root, "head" );
if ( !head ) {
// There is no <head> tag. Create it!
var html = Tree.getElementByName( root, "html" );
if ( !html ) {
html = {
type: Tree.TAG,
name: "html",
children: []
};
root.children.push( html );
}
head = {
type: Tree.TAG,
name: "head",
children: []
};
html.children.push( head );
}
return head;
}
function getInitJS( output ) {
var js = '';
var dynamicModule, code;
for ( dynamicModule in output.dynamicModules ) {
code = output.dynamicModules[ dynamicModule ];
js += code;
}
js += concatDicValues( output.initJS ) +
"\n" + concatDicValues( output.postInitJS );
if ( js.length > 0 ) {
return Tpl.file( "init.js", {
INIT_JS: js
} ).out;
}
return js;
}
function writeInnerCSS( innerCSS, pathCSS, nameWithoutExt, head, sourcemap ) {
if ( innerCSS.length > 0 ) {
// Add inner CSS file.
writeCSS( '@' + nameWithoutExt + ".css", innerCSS );
head.children.push( {
type: Tree.TAG,
name: 'link',
void: true,
attribs: {
rel: "stylesheet",
type: "text/css",
href: "css/@" + nameWithoutExt + ".css"
}
} );
}
}
function writeInnerJS( innerJS, pathJS, nameWithoutExt, head, sourcemap ) {
if ( innerJS.length > 0 ) {
// Add inner JS file.
writeJS( '@' + nameWithoutExt + ".js", innerJS );
head.children.push( {
type: Tree.TAG,
name: 'script',
attribs: {
defer: null,
src: "js/@" + nameWithoutExt + ".js"
}
} );
}
}
/**
* @param {object} output - Results of the HTML's compilation.
*
* @return {object} two attributes:
* * __js__: map of Javascript sources.
* * __css__: map of stylesheet sources.
*/
function combineRequires( output, options ) {
// The `cache` is used to prevent dependencies cycling. When a
// module has been processed, we add its name in the `cache`. Next
// time we find a module already in `cache` we will not process it.
var cache = {},
// dictionary of directly needed modules. The key is the module's name, the value is always `1`.
modules = output.require || {},
// List of modules' names we have to process.
fringe = [],
// Name of the current module.
moduleName,
// Style Sheet combined content.
css = '',
// Source file of the JS or CSS for the current module.
src,
// Dependencies of the current module's javascript.
dependencies,
// Map of Javascript sources. No compression.
jsFiles = {},
// Map of Stylesheet sources. No compression.
cssFiles = {},
// Iterator used for comments visual improvements.
i;
if ( !Array.isArray( output.modules ) ) output.modules = [];
// Fill the cache with all dynamic modules.
for ( moduleName in output.dynamicModules ) {
cache[ moduleName ] = 1;
}
// Always include the module `$` which was generated automatically.
modules[ 'mod/$' ] = 1;
// Fill the fringe with `modules`.
for ( moduleName in modules ) {
fringe.push( moduleName );
}
// Process all required modules by popping the next module's name from the `fringe`.
while ( fringe.length > 0 ) {
moduleName = fringe.pop(); // Pop the current module from the `fringe`.
cache[ moduleName ] = 1; // Don't process this module more than once.
if ( moduleName.substr( 0, 4 ) == 'cls/' ) {
// We have to include `tfw3.js` for backward compatibility.
output.innerJS[ Template.file( 'tfw3.js' ).out ] = 1;
} else if ( moduleName.substr( 0, 4 ) == 'mod/' ) {
// Remember all the modules used in this HTML page.
if ( output.modules.indexOf( moduleName ) < 0 ) {
output.modules.push( moduleName );
}
}
//============
// Javascript
//------------
// Compile (if not uptodate) the JS of the current module and
// return the source file.
src = compileJS( moduleName + ".js", options, output );
if ( !jsFiles[ moduleName ] ) {
jsFiles[ moduleName ] = {
src: src.tag( 'src' ),
zip: src.tag( 'zip' )
};
}
// Adding dependencies to the `fringe`.
dependencies = src.tag( "dependencies" );
if ( Array.isArray( dependencies ) ) {
dependencies.forEach( function ( dep ) {
if ( !cache[ dep ] ) {
fringe.push( dep );
}
} );
}
//==============
// Style Sheets
//--------------
src = compileCSS( moduleName + ".css", options );
if ( src ) {
if ( !cssFiles[ moduleName ] ) {
cssFiles[ moduleName ] = {
src: src.tag( 'src' ),
zip: src.tag( 'zip' )
};
}
}
}
return {
js: jsFiles,
css: cssFiles
};
}
function writeJS( name, sourceZip, sourceMap ) {
if ( name.substr( -3 ) == '.js' ) {
name = name.substr( 0, name.length - 3 );
}
var path = Path.join( Project.wwwPath( "js" ), name + ".js" );
FS.writeFileSync( path, sourceZip );
if ( sourceMap ) {
path = Path.join( Project.wwwPath( "js" ), name + ".js.map" );
FS.writeFileSync( path, sourceMap );
}
// Look for resources.
var src = Project.srcOrLibPath( name );
if ( FS.existsSync( src ) ) {
var dst = Path.join( Project.wwwPath( "css" ), name );
Project.copyFile( src, dst );
}
}
function writeCSS( name, content, sourceMap ) {
if ( name.substr( -4 ) == '.css' ) {
name = name.substr( 0, name.length - 4 );
}
var path = Path.join( Project.wwwPath( "css" ), name + ".css" );
FS.writeFileSync( path, content );
if ( sourceMap ) {
path = Path.join( Project.wwwPath( "css" ), name + ".css.map" );
FS.writeFileSync( path, sourceMap );
}
}
function moduleExists( requiredModule ) {
var path = Project.srcOrLibPath( requiredModule + ".js" );
if ( path ) return true;
return false;
}
/**
* @param {string} path Source path relative to the `src` folder.
* @return {Source}
* Tags:
* * __src__: debug content.
* * __zip__: release content.
* * __dependencies__: array of dependent modules.
*/
function compileJS( path, options, output ) {
var src = new Source( Project, path ),
// Tout le code qu'on ajoute en début de module.
// Par exemple les variables privées issues de fichiers.
head = ' ',
// Tout le code qu'on ajoute en fin de module.
foot = ' ',
// Fichier de dépendances. Pour `mod/module.js`, il s'agit de `mod/module.dep`.
depFile,
depFilename,
// JSON parsing of the dependency file.
depJSON,
// Nom d'une la variable globale.
varName,
// Nom du fichier texte définissant le contenu d'une variable globale.
varFilename,
// Fichier texte définissant le contenu d'une variable globale.
srcVar,
// Permet de gérer les virgules qui séparent des items.
firstItem,
code,
moduleName = src.name(),
moduleShortName,
iniName, iniPath,
compilation,
mode,
requiredModule,
dependencies,
minification,
minifiedCode,
sourceMap;
var srcXJS = src.clone("xjs");
if ( !src.exists() ) {
if ( !srcXJS.exists() ) {
// This file does not exist!
Fatal.fire(
'Javascript file not found: "' + Project.srcPath( path ) + '"!',
path
);
} else {
// XJS exists but not JS. That's why we create an minimal one.
src.write("// Code behind.\n\"use strict\";\n");
}
}
if ( !isModuleUptodate(src, srcXJS) ) {
var watch = [];
moduleShortName = moduleName.substr( 4 );
moduleShortName = moduleShortName.substr( 0, moduleShortName.length - 3 );
console.log("Compiling JS " + moduleShortName.yellow);
// Dependencies.
depFilename = 'mod/' + moduleShortName + ".dep";
if ( Project.srcOrLibPath( depFilename ) ) {
depFile = new Source( Project, depFilename );
try {
depJSON = JSON.parse( depFile.read() );
head = '';
if ( typeof depJSON.var !== 'undefined' ) {
head = 'var GLOBAL = {';
firstItem = true;
for ( varName in depJSON.var ) {
varFilename = depJSON.var[ varName ];
srcVar = Project.srcOrLibPath( 'mod/' + varFilename );
if ( !srcVar ) {
srcVar = Project.srcOrLibPath( varFilename );
}
if ( !srcVar ) {
Fatal.fire(
"Unable to find dendency file `" + varFilename + "` nor `mod/" +
varFilename + "`!",
depFile.getAbsoluteFilePath()
);
}
pushUnique( watch, "mod/" + varFilename );
if ( firstItem ) {
firstItem = false;
} else {
head += ',';
}
srcVar = new Source( Project, srcVar );
head += '\n ' + JSON.stringify( varName ) +
": " + JSON.stringify( srcVar.read() );
}
head += "};\n";
}
} catch ( ex ) {
Fatal.fire( "Unable to parse JSON dependency file!\n" + ex, depFile.getAbsoluteFilePath() );
}
// List of files to watch. If one of those files is newer
// that the JS file, we have to recompile.
src.tag( 'watch', watch );
}
// Intl.
if ( path == 'mod/$.js' ) {
// Internationalization for all modules except the special one: '$'.
src.tag( "intl", "" );
} else {
iniName = src.name().substr( 0, src.name().length - 2 ) + "ini";
iniPath = Project.srcOrLibPath( iniName );
if ( iniPath ) {
src.tag( "intl", CompilerINI.parse( iniPath ) );
} else {
src.tag( "intl", "var _=function(){return ''};" );
}
}
var isModule = ( moduleName.substr( 0, 4 ) == 'mod/' );
if ( isModule ) {
code = Tpl.file(
"module.js", {
name: moduleShortName,
code: Xjs.generateCodeFrom(src),
intl: src.tag( 'intl' ),
head: head + " ",
foot: foot + " "
}
).out;
} else {
code = src.read();
}
// Export internationalization.
if ( path != 'mod/$.js' ) {
code += "module.exports._ = _;\n";
}
minification = MinifyJS( {
name: moduleShortName + ".js",
content: code + ( isModule ? "});" : "" )
} );
var info = DepFind( code );
if ( info.requires.length > 0 ) {
//console.log( moduleShortName.cyan.bold + " depends on: " + info.requires.join( ', ' ).bold );
}
dependencies = info.requires.map( function ( itm ) {
return "mod/" + itm;
} );
dependencies.forEach(function (dep) {
var depSrc = Project.srcOrLibPath( dep + ".js" );
if( !depSrc ) {
console.error("[".red + moduleShortName.bold.red + "] Missing dependency: ".red + dep);
}
});
if ( dependencies.length > 0 ) {
code += "/**\n" +
" * @module " + moduleShortName + "\n";
dependencies.forEach( function ( itm ) {
if ( itm.substr( 0, 4 ) == 'mod/' ) {
code += " * @see module:" + itm.substr( 4 ) + "\n";
}
} );
code += "\n */\n";
}
src.tag( 'src', code + ( isModule ? "});" : "" ) );
src.tag( 'zip', minification.zip );
src.tag( 'map', minification.map );
src.tag( 'dependencies', dependencies );
src.save();
}
return src;
}
/**
* A module can be made of both a JS and a XJS file.
*/
function isModuleUptodate(src, srcXJS) {
if( !srcXJS.exists() ) {
// There is only a JS file.
return src.isUptodate();
}
return src.isUptodate() && srcXJS.isUptodate();
}
function minifyCSS( name, code, options ) {
var result = null;
if ( !code ) return null;
if ( code.trim().length == 0 ) {
// Empty CSS content.
console.log( " Warning! ".yellowBG.black + name.bold + " is EMPTY!" );
return null; // {src: "", zip: ""};
}
try {
var css = Util.zipCSS( code );
result = {
src: code,
zip: css.styles,
map: css.sourceMap
};
} catch ( ex ) {
throw Error( "Unable to minify CSS \"" + name + "\":\n" + ex +
"\n\nCSS content was:\n" + code.substr( 0, 256 ) +
( code.length > 256 ? '\n[...]' : '' ) );
}
return result;
}
/**
* @param {string} path Source path relative to the `src` folder.
*/
function compileCSS( path, options ) {
var absPath = Project.srcOrLibPath( path );
if ( !absPath ) return null;
var src = new Source( Project, path );
if ( !src.exists() ) return null;
if ( !src.isUptodate() ) {
console.log("Compiling CSS " + path.yellow);
var cssCode = src.read();
var multiBrowserCssCode = Rework( cssCode ).use( Vars({}) ).toString();
var minify = minifyCSS( src.name(), multiBrowserCssCode, options );
src.tag( 'src', cssCode );
src.tag( 'zip', minify.zip );
src.tag( 'map', minify.map );
src.save();
}
return src;
}
/**
* Add a prefix to a filename. This is not as simple as prepending the
* `prefix` to the string `path`, because `path` can contain folders'
* separators. The prefix must be prepended to the real file name and
* not to the whole path.
* Examples with `prefix` == "@":
* * `foobar.html`: `@foobar.html`
* * `myfolder/myfile.js`: `myfolder/@myfile.js`
*/
function addFilePrefix( path, prefix ) {
if ( typeof prefix === 'undefined' ) prefix = '@';
var separatorPosition = path.lastIndexOf( '/' );
if ( separatorPosition < 0 ) {
// Let's try with Windows separators.
separatorPosition = path.lastIndexOf( '\\' );
}
var filenameStart = separatorPosition > -1 ? separatorPosition + 1 : 0;
var result = path.substr( 0, filenameStart ) + prefix + path.substr( filenameStart );
return result.replace( /\\/g, '/' );
}
/**
* The depth of `path` is the number of subfolders it defines. For
* example, `foo.js' defined no subfolder and it is of depth 0. But
* `foo/bar/file.html` has two levels of subfolders hence it is of depth
* 2.
*/
function getBackToRoot( path ) {
// Counter for '/'.
var standardFolderSepCount = 0;
// Counter for '\' (windows folder separator).
var windowsFolderSepCount = 0;
// Loops index used for parsing chars of `path`and to add `../` to the result.
var i;
// Current char read from `path`.
var c;
// Counting folders' separators.
for ( i = 0; i < path.length; i++ ) {
c = path.charAt( i );
if ( c == '/' ) standardFolderSepCount++;
if ( c == '\\' ) windowsFolderSepCount++;
}
var folderSepCount = Math.max( standardFolderSepCount, windowsFolderSepCount );
if ( folderSepCount == 0 ) {
// There is no subfolder.
return '';
}
var result = '';
var folderSep = '/'; // windowsFolderSepCount > standardFolderSepCount ? '\\' : '/';
for ( i = 0; i < folderSepCount; i++ ) {
result += '..' + folderSep;
}
return result;
}
/**
* Add a description in the header if no one was found.
* @param {string} options.config.description - The description to use.
*/
function addDescriptionToHead( head, options ) {
if ( !options || !options.config || typeof options.config.description !== 'string' ) {
return false;
}
if ( !Array.isArray( head.children ) ) {
head.children = [];
}
var i, child;
for ( i = 0; i < head.children.length; i++ ) {
child = head.children[ i ];
if ( child.type !== Tree.ELEMENT ) continue;
if ( child.name.toLowerCase() != 'meta' ) continue;
if ( !child.attribs ) continue;
if ( typeof child.attribs.name !== 'string' ) continue;
if ( child.attribs.name.toLowerCase() == 'description' ) {
// There is already a description. We don't add a new one.
return false;
}
}
head.children.push( {
type: Tree.ELEMENT,
name: 'meta',
attribs: {
name: 'description',
content: options.config.description
}
} );
return true;
}