UNPKG

toloframework

Version:

Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.

1,381 lines (1,282 loc) 41.2 kB
/** * @see Project.compile */ var FS = require( "fs" ); var Path = require( "path" ); var ChildProcess = require( 'child_process' ); var Tree = require( "./htmltree" ); var Util = require( "./util" ); var Fatal = require( "./fatal" ); var Input = require( 'readline-sync' ); var Source = require( "./source" ); var Template = require( "./template" ); var PathUtils = require( "./pathutils" ); var CompilerPHP = require( "./compiler-php" ); var CompilerHTML2 = require( "./compiler-html2" ); /** * * `_modulesPath`: Array of pathes where to look for modules not found in local `src/mod` directory. */ var Project = function ( prjDir ) { // Will own the list of HTML files as Source objects. this._compiledFiles = []; this._prjDir = Path.resolve( prjDir ); this._libDir = Path.resolve( Path.join( __dirname, "../ker" ) ); this._tplDir = Path.resolve( Path.join( __dirname, "../tpl" ) ); this._srcDir = this.mkdir( prjDir, "src" ); this._docDir = this.mkdir( prjDir, "doc" ); this._tmpDir = this.mkdir( prjDir, "tmp" ); this._wwwDir = this.mkdir( prjDir, "www" ); Util.cleanDir( this.wwwPath( 'js' ), true ); Util.cleanDir( this.wwwPath( 'css' ), true ); // To prevent double flushing of the same file, this array keep the // name of the already flushed files. // @see `this.flushContent()` this._flushedContent = []; this._modulesPath = []; this.Util = require( "../ker/wdg/util.js" ); var cfg = this.loadConfigFile(); this._config = cfg; // Is there an output specified in config file? if ( cfg.tfw && cfg.tfw.output ) { this._wwwDir = this.prjPath( cfg.tfw.output ); if ( !FS.existsSync( this._wwwDir ) ) { this.fatal( "Output folder does not exist: `" + this._wwwDir + "`!" ); } } console.log( "Output folder: " + this._wwwDir.yellow ); // Finding extra modules. if ( Array.isArray( cfg.tfw.modules ) ) { cfg.tfw.modules.forEach( function ( item ) { item = Path.resolve( this._prjDir, item ); if ( FS.existsSync( item ) ) { this._modulesPath.push( item ); console.log( "Extra modules from: " + item ); } else { this.fatal( "Unable to find module directory:\n" + item ); } }, this ); } var now = new Date(); this.mkdir( this.srcPath( "mod" ) ); this.mkdir( this.srcPath( "wdg" ) ); this._type = cfg.tfw.compile.type; if ( this._type == 'firefoxos' ) { this._config.reservedModules = []; } else { this._config.reservedModules = [ "fs", "path", "process", "child_process", "cluster", "http", "os", "crypto", "dns", "domain", "events", "https", "net", "readline", "stream", "string_decoder", "tls", "dgram", "util", "vm", "zlib" ]; } console.info(); this._modulesPath.forEach( function ( path ) { console.log( "External lib: " + path.bold ); } ); }; /** * Create the file `mod/$.js` only if `package.json` is newer. * This special module must never be wrapped in a module. * Otherwise, it will require itself and lead to an infinite loop. */ function createIfNeededModule$( cfg, options ) { var file = Path.join( this.srcPath( "mod" ), "$.js" ); if ( PathUtils.isNewer( this.prjPath( 'package.json' ), file ) ) { var version = cfg.version.split( "." ); var now = new Date(); var key, val; var config = { name: JSON.stringify( cfg.name ), description: JSON.stringify( cfg.description || "" ), author: JSON.stringify( cfg.author || "" ), version: JSON.stringify( cfg.version ), major: version[ 0 ], minor: version[ 1 ], revision: version[ 2 ], date: new Date( now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds() ), consts: {} }; if ( cfg.tfw.consts ) { if ( cfg.tfw.consts.all ) { for ( key in cfg.tfw.consts.all ) { val = cfg.tfw.consts.all[ key ]; config.consts[ key ] = val; } } if ( options.dev ) { if ( cfg.tfw.consts.debug ) { for ( key in cfg.tfw.consts.debug ) { val = cfg.tfw.consts.debug[ key ]; config.consts[ key ] = val; } } } else { if ( cfg.tfw.consts.release ) { for ( key in cfg.tfw.consts.release ) { val = cfg.tfw.consts.release[ key ]; config.consts[ key ] = val; } } } console.log( "Constants: ".bold.yellow + JSON.stringify( config.consts, null, ' ' ) ); } PathUtils.file( file, //"require( '$', function(exports, module) {\n" "exports.config=" + JSON.stringify( config ) + ";\n" + FS.readFileSync( Path.join( this._tplDir, "$.js" ) ) ); } } /** * @return void */ Project.prototype.flushContent = function ( filepath, content, path ) { if ( typeof path === 'undefined' ) path = '.'; filepath = Path.join( path, filepath ); if ( this._flushedContent.indexOf( filepath ) > -1 ) return false; if ( filepath.indexOf( '@' ) > -1 ) { console.log( ">>> " + filepath.cyan + " (" + ( content.length / 1024 ).toFixed( content.length < 2048 ? 3 : 0 ).yellow + " kb)" + " " + this.wwwPath( filepath ).grey ); } var wwwFilePath = this.wwwPath( filepath ); this.mkdir( Path.dirname( wwwFilePath ) ); FS.writeFile( wwwFilePath, content, ( err ) => { if ( err ) { console.info( "[project] content=...", content ); Fatal.fire( "Unable to write the file: \"" + wwwFilePath + "\"\n\n!" + err, -1, "project.flushContent" ); } } ); return true; }; /** * Compile every `*.html` file found in _srcDir_. */ Project.prototype.compile = function ( options ) { var that = this; if ( typeof options === 'undefined' ) options = {}; options.config = this._config; this.options = options; var compiledFiles = []; var i, filename, cfg = this._config; createIfNeededModule$.call( this, cfg, options ); // List of modules for doc. this._modulesList = []; this._htmlFiles = this.findHtmlFiles(); CompilerHTML2.initialize( this ); for ( i = 0; i < this._htmlFiles.length; i++ ) { filename = this._htmlFiles[ i ]; try { compiledFiles.push( CompilerHTML2.compile( filename, JSON.parse( JSON.stringify( options ) ) ) ); } catch ( ex ) { Fatal.bubble( ex, filename ); } } // Copying resources. copyResources.call( this, compiledFiles ); ( cfg.tfw.resources || [] ).forEach( function ( res ) { console.log( "Resource: " + res.cyan ); that.copyFile( that.srcPath( res ), that.wwwPath( res ) ); } ); // WebWorkers require( './web-workers' )( this, compiledFiles ); // Look at `manifest.webapp` (FxOS) or `package.json` (NWJS). if ( this._type == 'nodewebkit' ) { ( function () { // For NWJS, we have to copy `package.json`. var content = PathUtils.file( that.srcPath( "../package.json" ) ); var data = JSON.parse( content ); if ( typeof data.main !== 'string' ) { data.main = "index.html"; } PathUtils.file( that.wwwPath( "package.json" ), JSON.stringify( data, null, 2 ) ); } )(); } else { ( function () { var manifest = that.srcPath( "manifest.webapp" ); var content = PathUtils.file( manifest ); var data; try { data = JSON.parse( content ); } catch ( ex ) { data = null; } if ( !data || typeof data !== 'object' ) { data = { launch_path: '/index.html', developer: { name: cfg.author, url: cfg.homepage }, icons: { "128": "/icon-128.png", "512": "/icon-512.png" } }; } data.name = cfg.name; data.version = cfg.version; data.description = cfg.description; PathUtils.file( manifest, JSON.stringify( data, null, 2 ) ); PathUtils.file( that.wwwPath( "manifest.webapp" ), JSON.stringify( data, null, 2 ) ); // Copy the icons. if ( typeof data.icons === 'object' ) { var key, val, icon; for ( key in data.icons ) { icon = data.icons[ key ]; val = that.srcOrLibPath( icon ); if ( !val ) { console.log( ' Warning! '.yellowBG + "Missing icon: " + icon.bold ); } else { that.copyFile( val, that.wwwPath( icon ) ); } } } } )(); } this._compiledFiles = compiledFiles; return compiledFiles; }; /** * If a resource file changes, we have to touch the corresponding module's JS file. */ Project.prototype.cascadingTouch = function ( path ) { var srcPath = this.srcPath( 'mod' ); path = Path.normalize( path ); // We are looking for files in resource folders. if ( PathUtils.isDirectory( path ) ) return; if ( path.length < srcPath.length ) return; if ( path.substr( 0, srcPath.length ) != srcPath ) return; // Path of the corresponding JS module. var modulePath; // Up to the prent dir. path = Path.dirname( path ); while ( path.length > srcPath ) { modulePath = path + ".js"; if ( Path.existsSync( modulePath ) ) { PathUtils.touch( modulePath ); return; } } }; /** * @return void */ Project.prototype.getCompiledFiles = function () { return this._compiledFiles; }; /** * @return void */ Project.prototype.services = function ( options ) { console.log( "Adding services...".cyan ); var tfwPath = this.srcPath( "tfw" ); if ( !FS.existsSync( tfwPath ) ) { Template.files( "tfw", tfwPath ); } this.copyFile( tfwPath, this.wwwPath( 'tfw' ), false ); }; /** * Copy resources in `css/`. * @param sources {array} - array of objects of class `Source` representing each compiled HTML. */ function copyResources( sources ) { var that = this; var dependenciesForAllFiles = []; sources.forEach( function ( sourceHTML ) { var output = sourceHTML.tag( "output" ) || {}; if ( !Array.isArray( output.modules ) ) return; output.modules.forEach( function ( module ) { if ( dependenciesForAllFiles.indexOf( module ) < 0 ) { dependenciesForAllFiles.push( module ); } } ); } ); if ( dependenciesForAllFiles.length > 0 ) { console.log( "Copying resources...".cyan ); dependenciesForAllFiles.forEach( function ( module ) { var src = that.srcOrLibPath( module ); if ( src ) { var dst = that.wwwPath( 'css/' + module.substr( 4 ) ); that.copyFile( src, dst ); } } ); } } /** * We use the "pakage.json" file as configuration file. * If it does not exist, we will create it. * * @return The configuration as an object. */ Project.prototype.loadConfigFile = function () { var answer; var oldConfigFile = this.prjPath( "project.tfw.json" ); var configFile = this.prjPath( "package.json" ); var oldCfg = {}; var cfg = {}; var origin = ""; var remotes; var projectName; var version; var type; var path; var currentConfigAsString = ''; if ( FS.existsSync( oldConfigFile ) ) { // Before tfw 0.19, configuration was stored in "project.tfw.json". // But now, we extend "package.json". try { oldCfg = JSON.parse( PathUtils.file( oldConfigFile ) ); } catch ( ex ) { console.log( "Old configuration file found, but it is not a valid JSON!".red ); console.log( "The file has been backuped.".red ); this.copyFile( oldConfigFile, oldConfigFile + ".backup" ); } } if ( FS.existsSync( configFile ) ) { try { cfg = JSON.parse( PathUtils.file( configFile ) ); currentConfigAsString = JSON.stringify( cfg ); } catch ( ex ) { console.log( "Your \"package,json\" file is not a valid JSON!".red ); console.log( "The file has been backuped.".red ); this.copyFile( configFile, configFile + ".backup" ); } } // Looking for git remote origin. if ( !cfg.repository || !cfg.repository.url ) { remotes = ChildProcess.execSync( "git remote -v" ).toString(); remotes.split( "\n" ).forEach( function ( remote ) { if ( remote.substr( 0, 6 ) == "origin" ) { remote = remote.substr( 6 ); if ( remote.substr( -8 ) == " (fetch)" ) { origin = remote.substr( 0, remote.length - 8 ).trim(); } } } ); cfg.repository = { type: "git", url: origin }; } else { origin = cfg.repository.url; } // Filling config attributes. cfg.homepage = cfg.homepage || origin.length > 4 ? origin.substr( 0, origin.length - 4 ) : ""; cfg.bugs = cfg.bugs || { url: origin.length > 4 ? origin.substr( 0, origin.length - 4 ) + "/issues" : "" }; cfg.scripts = cfg.scripts || { test: "jasmine", "test:dbg": "node --debug-brk node_modules/jasmine/bin/jasmine.js" }; if ( !cfg.name ) { projectName = oldCfg.name || Path.basename( this._prjDir ); answer = Input.question( "Project's name [" + projectName + "]: " ); if ( answer.trim().length > 0 ) { projectName = answer; } cfg.name = projectName; } cfg.version = cfg.version || oldCfg.version || "1"; if ( !cfg.author ) { answer = Input.question( "Author: " ); cfg.author = answer.trim(); } if ( !cfg.description ) { answer = Input.question( "Description: " ); cfg.description = answer.trim(); } if ( !cfg.license ) { cfg.license = "GPL"; } if ( !cfg.tfw ) { cfg.tfw = { modules: [], compile: { type: "html", files: oldCfg[ "html-filter" ] || "\\.html$" } }; } // Application type. type = { "firefoxos": "firefoxos", "firefox os": "firefoxos", "firefox-os": "firefoxos", "fxos": "firefoxos", "nodewebkit": "nodewebkit", "node webkit": "nodewebkit", "node-webkit": "nodewebkit", "nw": "nodewebkit" }[ ( cfg.tfw.compile[ "type" ] || "firefoxos" ).trim().toLowerCase() ]; if ( typeof type === 'undefined' ) type = "firefoxos"; if ( type != 'nodewebkit' ) type = "firefoxos"; if ( type != 'firefoxos' ) type = "nodewebkit"; cfg.tfw.compile[ "type" ] = type; // Save config file if it has changed. if ( currentConfigAsString != JSON.stringify( cfg ) ) { console.log( "`package.json` has been overwritten." ); PathUtils.file( configFile, JSON.stringify( cfg, null, 4 ) ); } // Initial template. path = this.prjPath( "src" ); if ( !FS.existsSync( path ) ) { this.mkdir( path ); this.copyFile( this.tplPath( "src" ), path ); } console.info( ( cfg.name + " v" + cfg.version ).bold + " (" + { firefoxos: "Firefox OS", nodewebkit: "Node-Webkit" }[ cfg.tfw.compile.type ] + ")" ); return cfg; }; /** * Before tfw 0.19, configuration was stored in "project.tfw.json". * But now, we extend "package.json". * * @return void */ Project.prototype.upgradeOldConfigFile = function () { }; /** * @return void */ Project.prototype.isReservedModules = function ( filename ) { var reservedModules = this._config.reservedModules; if ( !Array.isArray( reservedModules ) ) return false; filename = filename.split( "/" ).pop(); if ( filename.substr( filename.length - 3 ) == '.js' ) { // Remove extension. filename = filename.substr( 0, filename.length - 3 ); } if ( reservedModules.indexOf( filename ) > -1 ) return true; return false; }; /** * @return module `Template`. */ Project.prototype.Template = Template; /** * @return Tree module. */ Project.prototype.Tree = Tree; /** * @param {string} path path relative to `lib/` in ToloFrameWork folder. * @return an absolute path. */ Project.prototype.libPath = function ( path ) { if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._libDir, path ) ); }; /** * @param {string} path path relative to `tpl/` in ToloFrameWork folder. * @return an absolute path. */ Project.prototype.tplPath = function ( path ) { if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._tplDir, path ) ); }; /** * @param {string} path path relative to `src/`. * @return an absolute path. */ Project.prototype.srcPath = function ( path ) { if ( typeof path === 'undefined' ) return this._srcDir; if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._srcDir, path ) ); }; /** * @param {string} path path relative to `doc/`. * @return an absolute path. */ Project.prototype.docPath = function ( path ) { if ( typeof path === 'undefined' ) return this._docDir; if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._docDir, path ) ); }; /** * @param {string} path path relative to the current page folder. * @return an absolute path. */ Project.prototype.htmPath = function ( path ) { if ( typeof path === 'undefined' ) return this._htmDir; if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._htmDir, path ) ); }; /** * @param {string} path path relative to `prj/`. * @return an absolute path. */ Project.prototype.prjPath = function ( path ) { if ( typeof path === 'undefined' ) return this._prjDir; if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._prjDir, path ) ); }; /** * @param {string} path path relative to `src/` or extenal modules or `lib/`. * @return an absolute path or null if the file does not exist. */ Project.prototype.srcOrLibPath = function ( path ) { var result = this.srcPath( path ); if ( FS.existsSync( result ) ) return result; for ( var i = 0; i < this._modulesPath.length; i++ ) { result = this._modulesPath[ i ]; result = Path.resolve( result, path ); if ( FS.existsSync( result ) ) return result; } result = this.libPath( path ); if ( FS.existsSync( result ) ) return result; return null; }; /** * @return void */ Project.prototype.getExtraModulesPath = function () { return this._modulesPath.slice(); }; /** * @param {string} path path relative to `tmp/`. * @return an absolute path. */ Project.prototype.tmpPath = function ( path ) { if ( typeof path === 'undefined' ) return this._tmpDir; if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._tmpDir, path ) ); }; /** * @param {string} path path relative to `www/`. * @return an absolute path. */ Project.prototype.wwwPath = function ( path ) { if ( typeof path === 'undefined' ) return this._wwwDir; if ( path.substr( 0, this._srcDir.length ) == this._srcDir ) { path = path.substr( this._srcDir.length ); } return Path.resolve( Path.join( this._wwwDir, path ) ); }; /** * @return Dictionary of available widget compilers. The key is the * widget name, the value is an object: * * __path__: absolute path of the compiler's' directory. * * __name__: widget's name. * * __compiler__: compiler's module owning functions such as `compile`, `precompile`, ... * * __precompilation__: is this widget in mode _precompilation_? In this case, it must be called in the Top-Down walking. */ Project.prototype.getAvailableWidgetCompilers = function () { if ( !this._availableWidgetsCompilers ) { var map = {}; var dirs = [ this._srcDir, this._libDir ]; console.log( "Available widgets:" ); dirs.forEach( // Resolve paths for "wdg/" directories. function ( itm, idx, arr ) { var path = Path.resolve( Path.join( itm, "wdg" ) ); if ( !FS.existsSync( path ) ) { path = null; } arr[ idx ] = path; } ); dirs.forEach( function ( dir, idx ) { if ( typeof dir !== 'string' ) return; var files = FS.readdirSync( dir ); files.forEach( function ( filename ) { var file = Path.join( dir, filename ); var stat = FS.statSync( file ); if ( stat.isFile() ) return; if ( !map[ filename ] ) { map[ filename ] = { path: file, name: filename }; var modulePath = Path.join( file, "compile-" + filename + ".js" ); if ( FS.existsSync( modulePath ) ) { var compiler = require( modulePath ); if ( typeof compiler.precompile === 'function' ) { map[ filename ].precompilation = true; map[ filename ].compiler = compiler; } else if ( typeof compiler.compile === 'function' ) { map[ filename ].compiler = compiler; } } var name = ( filename.substr( 0, 1 ).toUpperCase() + filename.substr( 1 ).toLowerCase() ).cyan; if ( idx == 0 ) { name = name.bold; } /* console.log( " " + ( map[ filename ].precompilation ? "<w:".yellow.bold : "<w:" ) + name + ( map[ filename ].precompilation ? ">".yellow.bold : ">" ) + "\t" + file ); */ } } ); } ); this._availableWidgetsCompilers = map; } return this._availableWidgetsCompilers; }; /** * Throw a fatal exception. */ Project.prototype.fatal = function ( msg, id, src ) { Fatal.fire( msg, id, src ); }; /** * Incrément version. */ Project.prototype.makeVersion = function ( options ) { var cfg = this._config; var version = cfg.version.split( "." ); if ( version.length < 3 ) { while ( version.length < 3 ) version.push( "0" ); } else { version[ version.length - 1 ] = ( parseInt( version[ version.length - 1 ] ) || 0 ) + 1; } cfg.version = version.join( "." ); PathUtils.file( this.prjPath( 'package.json' ), JSON.stringify( cfg, null, ' ' ) ); console.log( "New version: " + cfg.version.cyan ); }; /** * Tests use __Karma__ and __Jasmine__ ans the root folder is `spec`. * All the __tfw__ modules are put in `spec/mod` folder. */ Project.prototype.makeTest = function ( compiledFiles, specDir ) { console.log( "Prepare Karma/Jasmine tests... ".cyan + "(" + specDir + ")" ); // Create missing folders. var specPath = this.prjPath( specDir ); if ( !FS.existsSync( specPath ) ) { FS.mkdir( specPath ); } if ( !FS.existsSync( Path.join( specPath, "mod" ) ) ) { FS.mkdir( Path.join( specPath, "mod" ) ); } // List of needed modules. var allModules = []; compiledFiles.forEach( function ( compiledFile ) { var output = compiledFile.tag( 'output' ); var modules = output.modules; modules.forEach( function ( module ) { if ( allModules.indexOf( allModules ) < 0 ) { allModules.push( module ); } } ); } ); // Copy modules in `spec/mod`. allModules.forEach( function ( module ) { var js = new Source( this, module + ".js" ); PathUtils.file( this.prjPath( specDir + "/" + module + ".js" ), js.tag( 'zip' ) ); }, this ); PathUtils.file( this.prjPath( specDir + "/mod/@require.js" ), Template.file( 'require.js' ).out ); }; /** * Writing documentation. * @return void */ Project.prototype.makeDoc = function () { console.log( "Writing documentation...".cyan ); CompilerPHP.compile( this ); var that = this; var modules = "window.M={"; this._modulesList.sort(); this._modulesList.forEach( function ( moduleName, index ) { var src = new Source( that, "mod/" + moduleName ); if ( index > 0 ) modules += ",\n"; modules += JSON.stringify( moduleName.substr( 0, moduleName.length - 3 ) ) + ":" + JSON.stringify( src.tag( "doc" ) ); } ); modules += "}"; var cfg = this._config; var docPath = this.prjPath( "doc" ); Template.files( "doc", docPath, { project: cfg.name } ); this.mkdir( docPath ); PathUtils.file( Path.join( docPath, "modules.js" ), modules ); }; Project.prototype.makeJSDoc = function () { console.error( "Not implemented yet!" ); }; /** * @return {this} */ Project.prototype.addModuleToList = function ( moduleName ) { if ( moduleName.substr( 0, 4 ) != 'mod/' ) return this; moduleName = moduleName.substr( 4 ); if ( moduleName.charAt( 0 ) == '$' ) return this; if ( this._modulesList.indexOf( moduleName ) < 0 ) { this._modulesList.push( moduleName ); } return this; }; /** * Link every `*.html` file found in _srcDir_. */ Project.prototype.link = function () { console.log( "Cleaning output: " + this.wwwPath( 'js' ) ); Util.cleanDir( this.wwwPath( 'js' ) ); console.log( "Cleaning output: " + this.wwwPath( 'css' ) ); Util.cleanDir( this.wwwPath( 'css' ) ); this.mkdir( this.wwwPath( "DEBUG" ) ); this.mkdir( this.wwwPath( "RELEASE" ) ); this._htmlFiles.forEach( function ( filename ) { filename = filename.split( Path.sep ).join( "/" ); console.log( "Linking " + filename.yellow.bold ); var shiftPath = ""; var subdirCount = filename.split( "/" ).length - 1; for ( var i = 0; i < subdirCount; i++ ) { shiftPath += "../"; } this.linkForDebug( filename, shiftPath ); this.linkForRelease( filename, shiftPath ); }, this ); }; /** * @return void */ Project.prototype.sortCSS = function ( linkJS, linkCSS ) { var input = []; linkCSS.forEach( function ( nameCSS, indexCSS ) { var nameJS = nameCSS.substr( 0, nameCSS.length - 3 ) + "js"; var pos = linkJS.indexOf( nameJS ); if ( pos < 0 ) pos = 1000000 + indexCSS; input.push( [ nameCSS, pos ] ); } ); input.sort( function ( a, b ) { var x = a[ 0 ]; var y = b[ 0 ]; if ( x < y ) return -1; if ( x > y ) return 1; x = a[ 1 ]; y = b[ 1 ]; if ( x < y ) return -1; if ( x > y ) return 1; return 0; } ); return input.map( function ( x ) { return x[ 0 ]; } ); }; Project.prototype.sortJS = function ( srcHTML, linkJS ) { var input = []; linkJS.forEach( function ( nameJS ) { var srcJS = srcHTML.create( nameJS ); var item = { key: nameJS, dep: [] }; srcJS.tag( "needs" ).forEach( function ( name ) { if ( name != nameJS && linkJS.indexOf( name ) > -1 ) { item.dep.push( name ); } } ); input.push( item ); } ); return this.topologicalSort( input ); }; Project.prototype.topologicalSort = function ( input ) { var output = []; while ( output.length < input.length ) { // Looking for the less depending item. var candidate = null; input.forEach( function ( item ) { if ( !item.key ) return; if ( !candidate ) { candidate = item; } else { if ( item.dep.length < candidate.dep.length ) { candidate = item; } } } ); // This candidate is the next item of the output list. var key = candidate.key; output.push( key ); delete candidate.key; // Remove this item in all the dependency lists. input.forEach( function ( item ) { if ( !item.key ) return; item.dep = item.dep.filter( function ( x ) { return x != key; } ); } ); } return output; }; /** * Linking in DEBUG mode. * Starting with an HTML file, we will find all dependent JS and CSS. * * Example: filename = "foo/bar.html" * We will create: * * `DEBUG/js/foo/@bar.js` for inner JS. * * `DEBUG/css/foo/@bar.css` for inner CSS. */ Project.prototype.linkForDebug = function ( filename, shiftPath ) { // Add this to a Javascript link to force webserver to deliver a non cached file. var seed = "?" + Date.now(); // The HTML source file. var srcHTML = new Source( this, filename ); // Array of all needed JS topologically sorted. var linkJS = this.sortJS( srcHTML, srcHTML.tag( "linkJS" ) || [] ); // Array of all needed CSS topologically sorted. var linkCSS = this.sortCSS( linkJS, srcHTML.tag( "linkCSS" ) || [] ); // HTML tree structure. var tree = Tree.clone( srcHTML.tag( "tree" ) ); var manifestFiles = []; var head = Tree.getElementByName( tree, "head" ); if ( !head ) { this.fatal( "Invalid HTML file: missing <head></head>!" + "\n\n" + Tree.toString( tree ) ); } // Needed CSS files. var cssDir = this.mkdir( this.wwwPath( "DEBUG/css" ) ); linkCSS.forEach( function ( item ) { var srcCSS = srcHTML.create( item ); var shortName = Path.basename( srcCSS.getAbsoluteFilePath() ); var output = Path.join( cssDir, shortName ); PathUtils.file( output, srcCSS.tag( "debug" ) ); if ( !head.children ) head.children = []; head.children.push( Tree.tag( "link", { href: shiftPath + "css/" + shortName + seed, rel: "stylesheet", type: "text/css" } ) ); head.children.push( { type: Tree.TEXT, text: "\n" } ); manifestFiles.push( "css/" + shortName ); var resources = srcCSS.listResources(); resources.forEach( function ( resource ) { var shortName = "css/" + resource[ 0 ]; var longName = resource[ 1 ]; manifestFiles.push( shortName ); this.copyFile( longName, Path.join( this.wwwPath( "DEBUG" ), shortName ) ); }, this ); }, this ); // For type "nodewebkit", all JS must lie in "node_modules" and they // don't need to be declared in the HTML file. var jsDirShortName = ( this._type == 'nodewebkit' ? "node_modules" : "js" ); var jsDir = this.mkdir( this.wwwPath( "DEBUG/" + jsDirShortName ) ); linkJS.forEach( function ( item ) { var srcJS = srcHTML.create( item ); var shortName = Path.basename( srcJS.getAbsoluteFilePath() ); var output = Path.join( jsDir, shortName ); var code = srcJS.read(); if ( item.substr( 0, 4 ) == 'mod/' ) { if ( this._type == 'nodewebkit' ) { // Let's add internationalisation snippet. code = ( srcJS.tag( "intl" ) || "" ) + code; } else { // This is a module. We need to wrap it in module's declaration snippet. code = "require('" + shortName.substr( 0, shortName.length - 3 ).toLowerCase() + "', function(exports, module){\n" + ( srcJS.tag( "intl" ) || "" ) + code + "\n});\n"; } } PathUtils.file( output, code ); if ( this._type != 'nodewebkit' ) { // Declaration and manifest only needed for project of // type that is not "nodewebkit". if ( !head.children ) head.children = []; head.children.push( Tree.tag( "script", { src: shiftPath + jsDirShortName + "/" + shortName + seed } ) ); head.children.push( { type: Tree.TEXT, text: "\n" } ); manifestFiles.push( jsDirShortName + "/" + shortName ); } }, this ); srcHTML.tag( "resources" ).forEach( function ( itm, idx, arr ) { var src = itm; var dst = src; if ( Array.isArray( src ) ) { dst = src[ 1 ]; src = src[ 0 ]; } manifestFiles.push( dst ); src = this.srcPath( src ); dst = Path.join( this.wwwPath( "DEBUG" ), dst ); this.copyFile( src, dst ); }, this ); // Adding innerJS and innerCSS. var shortNameJS = PathUtils.addPrefix( filename.substr( 0, filename.length - 5 ), "@" ) + ".js"; head.children.push( Tree.tag( "script", { src: shiftPath + jsDirShortName + "/" + shortNameJS + seed } ) ); manifestFiles.push( jsDirShortName + "/" + shortNameJS ); var wwwInnerJS = Path.join( jsDir, shortNameJS ); PathUtils.file( wwwInnerJS, srcHTML.tag( "innerJS" ) ); if ( true ) { // For now, we decided to put the CSS relative to the inner HTML into the <head>'s tag. head.children.push( Tree.tag( "style", {}, srcHTML.tag( "innerCSS" ) ) ); } else { // If we want to externalise the inner CSS in the future, we can use this piece of code. var shortNameCSS = PathUtils.addPrefix( filename.substr( 0, filename.length - 5 ), "@" ) + ".css"; head.children.push( Tree.tag( "link", { href: shiftPath + "css/" + shortNameCSS + seed, rel: "stylesheet", type: "text/css" } ) ); manifestFiles.push( shiftPath + "css/" + shortNameCSS ); PathUtils.file( Path.join( cssDir, shortNameCSS ), srcHTML.tag( "innerCSS" ) ); } if ( this._type != 'nodewebkit' ) { // Looking for manifest file. var html = Tree.findChild( tree, "html" ); if ( html ) { var manifestFilename = Tree.att( "manifest" ); if ( manifestFilename ) { // Writing manifest file only if needed. PathUtils.file( Path.join( this.wwwPath( "DEBUG" ), filename + ".manifest" ), "CACHE MANIFEST\n" + "# " + ( new Date() ) + " - " + Date.now() + "\n\n" + "CACHE:\n" + manifestFiles.join( "\n" ) + "\n\nNETWORK:\n*\n" ); } } } // Writing HTML file. PathUtils.file( Path.join( this.wwwPath( "DEBUG" ), filename ), "<!-- " + ( new Date() ).toString() + " -->" + "<!DOCTYPE html>" + Tree.toString( tree ) ); // Writing ".htaccess" file. this.writeHtaccess( "DEBUG" ); // Looking for webapp manifest for Firefox OS (also used for nodewebkit but with another name). copyManifestWebapp.call( this, "DEBUG" ); }; /** * @param mode can be "RELEASE" or "DEBUG". * @return void */ Project.prototype.writeHtaccess = function ( mode ) { PathUtils.file( Path.join( this.wwwPath( mode ), ".htaccess" ), "AddType application/x-web-app-manifest+json .webapp\n" + "AddType text/cache-manifest .manifest\n" + "ExpiresByType text/cache-manifest \"access plus 0 seconds\"\n" + "Header set Expires \"Thu, 19 Nov 1981 08:52:00 GM\"\n" + "Header set Cache-Control \"no-store, no-cache, must-revalidate, post-check=0, pre-check=0\"\n" + "Header set Pragma \"no-cache\"\n" ); }; /** * @param mode : "DEBUG" or "RELEASE". */ function copyManifestWebapp( mode ) { var filename = "manifest.webapp"; var out; if ( this._type == 'nodewebkit' ) filename = "package.json"; console.log( "Copying " + filename.cyan + "..." ); // Looking for webapp manifest for Firefox OS. if ( false == FS.existsSync( this.srcPath( filename ) ) ) { out = Template.file( filename, this._config ).out; PathUtils.file( this.srcPath( filename ), out ); } var webappFile = this.srcPath( filename ); if ( webappFile ) { var content = FS.readFileSync( webappFile ).toString(); var json = null; try { json = JSON.parse( content ); } catch ( x ) { this.fatal( "'" + filename + "' must be a valid JSON file!\n" + x ); } json.version = this._config.version; if ( typeof json.window === 'object' ) { json.window.toolbar = ( mode == "DEBUG" ); } PathUtils.file( Path.join( this.wwwPath( mode ), filename ), JSON.stringify( json, null, 4 ) ); var icons = json.icons || {}; var key, val; for ( key in icons ) { val = this.srcOrLibPath( icons[ key ] ); if ( val ) { this.copyFile( val, Path.join( this.wwwPath( mode ), icons[ key ] ) ); } } } } /** * @return array of HTML files found in _srcDir_. */ Project.prototype.findHtmlFiles = function () { var that = this; var filters = this._config.tfw.compile.files; if ( typeof filters === 'undefined' ) filters = "\\.html$"; var files = [], srcDir = this.srcPath(), prefixLength = srcDir.length + 1, filter, i, rxFilters = [], arr; if ( !Array.isArray( filters ) ) { filters = [ filters ]; } for ( i = 0; i < filters.length; i++ ) { filter = filters[ i ]; if ( typeof filter !== 'string' ) { this.fatal( "Invalid atribute \"tfw.compile.files\" in \"package.json\"!\n" + "Must be a string or an array of strings." ); } arr = []; filter.split( "/" ).forEach( function ( item ) { try { item = item.trim(); if ( item == '' || item == '*' ) { // `null`matches anything. arr.push( null ); } else { arr.push( new RegExp( item, "i" ) ); } } catch ( ex ) { this.fatal( "Invalid regular expression for filter: " + JSON.stringify( filter ) + "!" ); } } ); rxFilters.push( arr ); } rxFilters.forEach( function ( f ) { PathUtils.findFiles( srcDir, f ).forEach( function ( item ) { if( item.substr( item.length - 5 ).toLowerCase() !== '.html' ) { console.log("Copying " + item.yellow); that.copyFile( item, that.wwwPath( Path.basename( item ) ) ); return; } files.push( item.substr( prefixLength ) ); } ); } ); if ( files.length == 0 ) { this.fatal( "No HTML file found!\n\nPattern: " + JSON.stringify( filters ) + "\nFolder: " + srcDir ); } return files; }; /** * @param arguments all arguments will be joined to form the path of the directory to create. * @return the name of the created directory. */ Project.prototype.mkdir = function () { var key, arg, items = []; for ( key in arguments ) { arg = arguments[ key ].trim(); items.push( arg ); } var path = Path.resolve( Path.normalize( items.join( "/" ) ) ), item, i, curPath = ""; items = path.replace( /\\/g, '/' ).split( "/" ); for ( i = 0; i < items.length; i++ ) { item = items[ i ]; curPath += item + "/"; if ( FS.existsSync( curPath ) ) { var stat = FS.statSync( curPath ); if ( !stat.isDirectory() ) { break; } } else { try { FS.mkdirSync( curPath ); } catch ( ex ) { throw { fatal: "Unable to create directory \"" + curPath + "\"!\n" + ex }; } } } return path; }; // Used for file copy. var buffer = new Buffer( 64 * 1024 ); /** * Copy a file from `src` to `dst`. * @param src full path of the source file. * @param dst full path of the destination file. */ Project.prototype.copyFile = function ( src, dst, log ) { if( log ) { console.log("copyFile( " + src.cyan.bold + ", " + dst + " )"); } if ( !FS.existsSync( src ) ) { this.fatal( "Unable to copy missing file: " + src + "\ninto: " + dst, -1, src ); } var stat = FS.statSync( src ); if ( stat.isDirectory() ) { // We need to copy a whole directory. if ( FS.existsSync( dst ) ) { // Check if the destination is a directory. stat = FS.statSync( dst ); if ( !stat.isDirectory() ) { this.fatal( "Destination is not a directory: \"" + dst + "\"!\nSource is \"" + src + "\".", -1, "project.copyFile" ); } } else { // Make destination directory. this.mkdir( dst ); } var files = FS.readdirSync( src ); files.forEach( function ( filename ) { this.copyFile( Path.join( src, filename ), Path.join( dst, filename ), log ); }, this ); return; } var bytesRead, pos, rfd, wfd; this.mkdir( Path.dirname( dst ) ); try { rfd = FS.openSync( src, "r" ); } catch ( ex ) { this.fatal( "Unable to open file \"" + src + "\" for reading!\n" + ex, -1, "project.copyFile" ); } try { wfd = FS.openSync( dst, "w" ); } catch ( ex ) { this.fatal( "Unable to open file \"" + dst + "\" for writing!\n" + ex, -1, "project.copyFile" ); } bytesRead = 1; pos = 0; while ( bytesRead > 0 ) { try { bytesRead = FS.readSync( rfd, buffer, 0, 64 * 1024, pos ); } catch ( ex ) { this.fatal( "Unable to read file \"" + src + "\"!\n" + ex, -1, "project.copyFile" ); } FS.writeSync( wfd, buffer, 0, bytesRead ); pos += bytesRead; } FS.closeSync( rfd ); return FS.closeSync( wfd ); }; /** * @param prjDir root directory of the project. It is where we can find `project.tfw.json`. * @return instance of the class `Project`. */ exports.createProject = function ( prjDir ) { return new Project( prjDir ); }; exports.ERR_WIDGET_TRANSFORMER = 1; exports.ERR_WIDGET_NOT_FOUND = 2; exports.ERR_WIDGET_TOO_DEEP = 3; exports.ERR_FILE_NOT_FOUND = 4;