UNPKG

toloframework

Version:

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

1,337 lines (1,242 loc) 44.9 kB
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(), 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; } } } } 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 ) { 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; 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 ) ); }); // 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')); }; /** * 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) { console.log("Prepare Karma/Jasmine tests...".cyan); // Create missing folders. var specPath = this.prjPath("spec"); 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("spec/" + module + ".js"), js.tag('zip')); }, this); PathUtils.file(this.prjPath("spec/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()); Util.cleanDir(this.wwwPath()); 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 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) { 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) { 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) ); }, 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;