UNPKG

toloframework

Version:

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

1,335 lines (1,232 loc) 44.1 kB
"use strict"; module.exports = { /** * @param prjDir root directory of the project. It is where we can find `project.tfw.json`. * @return instance of the class `Project`. */ createProject, ERR_WIDGET_TRANSFORMER: 1, ERR_WIDGET_NOT_FOUND: 2, ERR_WIDGET_TOO_DEEP: 3, ERR_FILE_NOT_FOUND: 4 }; /** * @see Project.compile */ const CompilerHTML2 = require( "./compiler-html2" ), CompilerPHP = require( "./compiler-php" ), ConfigurationLoader = require( "./configuration-loader" ), Fatal = require( "./fatal" ), FS = require( "fs" ), Path = require( "path" ), PathUtils = require( "./pathutils" ), Source = require( "./source" ), Template = require( "./template" ), Tree = require( "./htmltree" ), Util = require( "./util" ), WebWorkers = require( "./web-workers" ), WidgetUtil = require( "../ker/wdg/util.js" ); const ONE_KILOBYTES = 1024, TWO_KILOBYTES = 2048, NOT_FOUND_IN_ARRAY = -1; /** * @class project * @param {string} prjDir root directory of the project. It is where we can find `project.tfw.json`. */ function Project( prjDir ) { initializeProjectDirectories( this, prjDir ); initializedPrivateVariables( this ); this.Util = WidgetUtil; const cfg = ConfigurationLoader.parse( prjDir ); this._config = cfg; this.checkProjectOutputFolder(); this.findExtraModules(); this.mkdir( this.srcPath( "mod" ) ); this.mkdir( this.srcPath( "wdg" ) ); this._type = cfg.tfw.compile.type; if ( this._type === 'web' ) { 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" ]; } this._modulesPath.forEach( function forEachModulePath( path ) { console.log( `External lib: ${path}` ); } ); } /** * Modules can be foind in the `./src/mod` folder, in the toloFrameWork standard library * and in all folders defined in the config attribute `tfw.modules` which is an array. * @member Project.findExtraModules * @private * @return {undefined} */ Project.prototype.findExtraModules = function findExtraModules() { const that = this, cfg = this._config; cfg.tfw.modules.forEach( function forEachModule( _item ) { const item = Path.resolve( that._prjDir, _item ); if ( FS.existsSync( item ) ) { that._modulesPath.push( item ); console.log( `Extra modules from: ${item}!` ); } else { that.fatal( `Unable to find module directory:\n${item}` ); } } ); }; /** * @member Project.checkProjectOutputFolder * @private * @return {undefined} */ Project.prototype.checkProjectOutputFolder = function checkProjectOutputFolder() { const cfg = this._config; // 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}` ); }; /** * @param {object} project - Reference to the current Project object. * @param {string} prjDir - Root directory of this project. * @returns {undefined} */ function initializeProjectDirectories( project, prjDir ) { project._prjDir = Path.resolve( prjDir ); project._libDir = Path.resolve( Path.join( __dirname, "../ker" ) ); project._tplDir = Path.resolve( Path.join( __dirname, "../tpl" ) ); project._srcDir = project.mkdir( prjDir, "src" ); project._docDir = project.mkdir( prjDir, "doc" ); project._tmpDir = project.mkdir( prjDir, "tmp" ); project._wwwDir = project.mkdir( prjDir, "www" ); Util.cleanDir( project.wwwPath( 'js' ), true ); Util.cleanDir( project.wwwPath( 'css' ), true ); } /** * 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. * @param {object} project - Project instance. * @param {object} cfg - Config parsed from `package.json`. * @param {object} options - Building options. * @returns {undefined} */ function createModule$IfNeeded( project, cfg, options ) { const file = project.srcPath( "mod/$.js" ); if ( !PathUtils.isNewer( project.prjPath( 'package.json' ), file ) ) { // There is nothing to do until `$.js` is not older than `package.json`. return; } const moduleContent = getModuleContentFor$( cfg ); if ( cfg.tfw.consts ) { if ( cfg.tfw.consts.all ) { Object.keys( cfg.tfw.consts.all ).forEach( ( key ) => { const val = cfg.tfw.consts.all[ key ]; moduleContent.consts[ key ] = val; } ); } if ( options.dev ) { if ( cfg.tfw.consts.debug ) { Object.keys( cfg.tfw.consts.debug ).forEach( ( key ) => { const val = cfg.tfw.consts.debug[ key ]; moduleContent.consts[ key ] = val; } ); } } else if ( cfg.tfw.consts.release ) { Object.keys( cfg.tfw.consts.debug ).forEach( ( key ) => { const val = cfg.tfw.consts.release[ key ]; moduleContent.consts[ key ] = val; } ); } console.log( "Constants: ".yellow + JSON.stringify( moduleContent.consts, null, ' ' ) ); } const contentOfModule$ = FS.readFileSync( Path.join( project._tplDir, "$.js" ) ); PathUtils.file( file, `exports.config=${JSON.stringify( moduleContent )};\n${contentOfModule$}` ); } /** * @param {string} filename - ... * @param {string} content - ... * @param {string} _path - ... * * @return {boolean} `true` if the file needed to be flushed. */ Project.prototype.flushContent = function flushContent( filename, content, _path ) { const path = typeof _path !== "undefined" ? _path : '.', filepath = Path.join( path, filename ); if ( this.hasAlreadyBeenFlushed( filepath ) ) { return false; } if ( filepath.indexOf( '@' ) !== NOT_FOUND_IN_ARRAY ) { const DECIMALS_FOR_SMALL_NUMBER = 3, decimals = content.length < TWO_KILOBYTES ? DECIMALS_FOR_SMALL_NUMBER : 0, fileSize = ( content.length / ONE_KILOBYTES ).toFixed( decimals ); console.log( `>>> ${filepath.cyan} (${fileSize.yellow} kb) ${this.wwwPath(filepath).grey}` ); } const 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}`, Fatal.UNDEFINED, "project.flushContent" ); } } ); return true; }; /** * @param {string} fileFullPath . Full path of the file we want to know if it has already been flushed. * @returns {boolean} Wether this file has already been flushed. */ Project.prototype.hasAlreadyBeenFlushed = function hasAlreadyBeenFlushed( fileFullPath ) { return this._flushedContent.indexOf( fileFullPath ) !== NOT_FOUND_IN_ARRAY; }; /** * Compile every `*.html` file found in _srcDir_. * @param {object} _options - Options for debug, release, ... * @returns {array} CompiledFiles. */ Project.prototype.compile = function ( _options ) { const options = typeof _options !== 'undefined' ? _options : {}, that = this, cfg = this._config; options.config = this._config; this.options = options; createModule$IfNeeded( this, cfg, options ); CompilerHTML2.initialize( this ); // List of modules for doc. this._modulesList = []; const compiledFiles = compileHtmlFiles( this.findHtmlFiles(), options ); // Copying resources. copyResources( this, compiledFiles, cfg ); // WebWorkers WebWorkers.compile( 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 ); } else { that.copyFile( val, that.wwwPath( icon ) ); } } } } )(); } this._compiledFiles = compiledFiles; return compiledFiles; }; /** * @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 {object} project - Current project. * @param {array} sources - Array of compiled Html Files. * @param {object} config - Current configuration from `package.json`. * Array of objects of class `Source` representing each compiled HTML. * @returns {undefined} */ function copyResources( project, sources, config ) { const dependenciesForAllFiles = []; if ( !Util.isEmpty( sources ) ) { sources.forEach( function forEachSource( sourceHTML ) { const output = sourceHTML.tag( "output" ) || {}; if ( !Array.isArray( output.modules ) ) { return; } output.modules.forEach( function forEachModule( module ) { if ( dependenciesForAllFiles.indexOf( module ) < 0 ) { dependenciesForAllFiles.push( module ); } } ); } ); } if ( dependenciesForAllFiles.length > 0 ) { console.log( "Copying resources...".cyan ); dependenciesForAllFiles.forEach( function forEachDependencyFile( module ) { const src = project.srcOrLibPath( module ); if ( src ) { const dst = project.wwwPath( Util.replaceFirstSubFolder( module, 'css/' ) ); project.copyFile( src, dst ); } } ); } // Extra resources defined in `package.json`. const extraResources = config.tfw.resources || []; extraResources.forEach( function forEachExtraResource( res ) { console.log( `Extra resource: ${res.cyan}` ); project.copyFile( project.srcPath( res ), project.wwwPath( res ) ); } ); } /** * @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 {string} 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 ) { if ( typeof path !== 'string' ) return path; const path2 = Util.replaceDotsWithSlashes( path ), pathes = [ this.srcPath( path2 ), this.srcPath( path ) ]; this._modulesPath.forEach( function ( modulePath ) { pathes.push( Path.resolve( modulePath, path2 ), Path.resolve( modulePath, path ) ); } ); pathes.push( this.libPath( path2 ), this.libPath( path ) ); for ( const file of pathes ) { if ( FS.existsSync( file ) ) return file; } 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; } } ); } ); 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 ); 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 + ", " + 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 {string} prjDir root directory of the project. It is where we can find `project.tfw.json`. * @return {object} instance of the class `Project`. */ function createProject( prjDir ) { return new Project( prjDir ); } /** * @param {object} project - Instance of Project. * @returns {undefined} */ function initializedPrivateVariables( project ) { // Will own the list of HTML files as Source objects. project._compiledFiles = []; /* * To prevent double flushing of the same file, this array keeps the * name of the already flushed files. * @see `this.flushContent()` */ project._flushedContent = []; project._modulesPath = []; } /** * @param {string} version - Version in format `major.minor.patch`. * @returns {ojbect} `{major, minor, patch}`. */ function parseVersion( version ) { try { const versionArray = version.split( "." ), MAJOR_INDEX = 0, MINOR_INDEX = 1, PATCH_INDEX = 2; return { major: versionArray[ MAJOR_INDEX ], minor: versionArray[ MINOR_INDEX ], patch: versionArray[ PATCH_INDEX ] }; } catch ( ex ) { throw new Error( `Unable to parse version ${JSON.stringify(version, null, ' ')}` ); } } /** * @param {object} cfg - Configuration. * @returns {object} The minimal object that we must provide as the module `$.js`. */ function getModuleContentFor$( cfg ) { const version = parseVersion( cfg.version ); return { name: JSON.stringify( cfg.name ), description: JSON.stringify( cfg.description || "" ), author: JSON.stringify( cfg.author || "" ), version: JSON.stringify( cfg.version ), major: version.major, minor: version.minor, revision: version.patch, date: new Date(), consts: {} }; } /** * @param {array} htmlFiles - List of all the HTML files we add to compile. * @param {object} options - Options for debug, release, ... * @returns {array} List of actually compiled files. * @see findHtmlFiles */ function compileHtmlFiles( htmlFiles, options ) { const compiledFiles = []; for ( let i = 0; i < htmlFiles.length; i++ ) { const filename = htmlFiles[ i ]; try { const compiledFile = CompilerHTML2.compile( filename, Util.clone( options ) ); compiledFiles.push( compiledFile ); } catch ( ex ) { Fatal.bubble( ex, filename ); } } return compiledFiles; }