#!/usr/bin/env objj require("narwhal").ensureEngine("rhino"); @import @import "../lib/cappuccino/objj-analysis-tools.j" var FILE = require("file"); var OS = require("os"); var UTIL = require("narwhal/util"); var CACHEMANIFEST = require("objective-j/cache-manifest"); var stream = require("narwhal/term").stream; var parser = new (require("narwhal/args").Parser)(); parser.usage("INPUT_PROJECT OUTPUT_PROJECT"); parser.help("Combine a Cappuccino application into a single JavaScript file."); parser.option("-m", "--main", "main") .def("main.j") .set() .help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')"); parser.option("-F", "--framework", "frameworks") .push() .help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])"); parser.option("-P", "--path", "paths") .push() .help("Add a path (relative to the application root) to inline."); parser.option("-f", "--force", "force") .def(false) .set(true) .help("Force overwriting OUTPUT_PROJECT if it exists"); parser.option("--index", "index") .def("index.html") .set() .help("The root HTML file to modify (default: index.html)"); parser.option("-s", "--split", "number", "split") .natural() .def(0) .help("Split into multiple files"); parser.option("-c", "--compressor", "compressor") .def("shrinksafe") .set() .help("Select a compressor to use (closure-compiler, yuicompressor, shrinksafe), or \"none\" (default: shrinksafe)"); parser.option("--manifest", "manifest") .set(true) .help("Generate HTML5 cache manifest."); parser.option("-v", "--verbose", "verbose") .def(false) .set(true) .help("Verbose logging"); parser.helpful(); function main(args) { var options = parser.parse(args); if (options.args.length < 2) { parser.printUsage(options); return; } var rootPath = FILE.path(options.args[0]).join("").absolute(); var outputPath = FILE.path(options.args[1]).join("").absolute(); if (outputPath.exists()) { if (options.force) { // FIXME: why doesn't this work?! //outputPath.rmtree(); OS.system(["rm", "-rf", outputPath]); } else { stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)"); OS.exit(1); } } options.frameworks.push("Frameworks"); var mainPath = String(rootPath.join(options.main)); var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); }); var environment = "Browser"; stream.print("\0yellow("+Array(81).join("=")+"\0)"); stream.print("Application root: \0green(" + rootPath + "\0)"); stream.print("Output directory: \0green(" + outputPath + "\0)"); stream.print("\0yellow("+Array(81).join("=")+"\0)"); stream.print("Main file: \0green(" + mainPath + "\0)"); stream.print("Frameworks: \0green(" + frameworks + "\0)"); stream.print("Environment: \0green(" + environment + "\0)"); var flattener = new ObjectiveJFlattener(rootPath); flattener.options = options; flattener.setIncludePaths(frameworks); flattener.setEnvironments([environment, "ObjJ"]); print("Loading application."); flattener.load(mainPath); print("Loading default theme."); flattener.require("objective-j").objj_eval("("+(function() { var defaultThemeName = [CPApplication defaultThemeName], bundle = nil; if (defaultThemeName === @"Aristo") bundle = [CPBundle bundleForClass:[CPApplication class]]; else bundle = [CPBundle mainBundle]; var blend = [[CPThemeBlend alloc] initWithContentsOfURL:[bundle pathForResource:defaultThemeName + @".blend"]]; [blend loadWithDelegate:nil]; })+")")(); var applicationJSs = flattener.buildApplicationJS(); FILE.copyTree(rootPath, outputPath); applicationJSs.forEach(function(applicationJS, n) { var name = "Application"+(n||"")+".js"; if (options.compressor === "none") { print("skipping compression: " + name); } else { print("compressing: " + name); applicationJS = require("minify/"+options.compressor).compress(applicationJS, { charset : "UTF-8", useServer : true }); } outputPath.join(name).write(applicationJS, { charset : "UTF-8" }); }); rewriteMainHTML(outputPath.join(options.index)); if (options.manifest) { CACHEMANIFEST.generateManifest(outputPath, { index : outputPath.join(options.index), exclude : Object.keys(flattener.filesToCache).map(function(path) { return outputPath.join(path).toString(); }) }); } } // ObjectiveJFlattener inherits from ObjectiveJRuntimeAnalyzer function ObjectiveJFlattener(rootPath) { ObjectiveJRuntimeAnalyzer.apply(this, arguments); this.filesToCache = {}; this.fileCacheBuffer = []; this.functionsBuffer = []; } ObjectiveJFlattener.prototype = Object.create(ObjectiveJRuntimeAnalyzer.prototype); ObjectiveJFlattener.prototype.buildApplicationJS = function() { this.setupFileCache(); this.serializeFunctions(); this.serializeFileCache(); var additions = FILE.read(FILE.join(FILE.dirname(module.path), "..", "..", "cappuccino", "lib", "cappuccino", "objj-flatten-additions.js"), { charset:"UTF-8" }); var applicationJSs = []; if (this.options.split === 0) { var buffer = []; buffer.push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); buffer.push(additions); buffer.push(this.fileCacheBuffer.join("\n")); buffer.push(this.functionsBuffer.join("\n")); buffer.push("ObjectiveJ.bootstrap();"); applicationJSs.push(buffer.join("\n")); } else { var appFilesCount = this.options.split; var buffers = []; for (var i = 0; i <= appFilesCount; i++) buffers.push([]); var chunks = this.fileCacheBuffer.concat(this.functionsBuffer).sort(function(chunkA, chunkB) { return chunkA.length - chunkB.length; }); // try to equally distribute the chunks. could be better but good enough for now. var n = 0; while (chunks.length) { buffers[(n++ % appFilesCount) + 1].push(chunks.pop()); } buffers[0].push("var baseURL = new CFURL(\".\", ObjectiveJ.pageURL);"); buffers[0].push(additions); buffers[0].push("var appFilesCount = " + appFilesCount +";"); buffers[0].push("for (var i = 1; i <= appFilesCount; i++) {"); buffers[0].push(" var script = document.createElement(\"script\");"); buffers[0].push(" script.src = \"Application\"+i+\".js\";"); buffers[0].push(" script.charset = \"UTF-8\";"); buffers[0].push(" script.onload = function() { if (--appFilesCount === 0) ObjectiveJ.bootstrap(); };"); buffers[0].push(" document.getElementsByTagName(\"head\")[0].appendChild(script);"); buffers[0].push("}"); buffers.forEach(function(buffer) { applicationJSs.push(buffer.join("\n")); }); } return applicationJSs; } ObjectiveJFlattener.prototype.serializeFunctions = function() { var inlineFunctions = true;//this.options.inlineFunctions; var outputFiles = {}; var _cachedExecutableFunctions = {}; this.require("objective-j").FileExecutable.allFileExecutables().forEach(function(executable) { var path = executable.path(); if (inlineFunctions) { // stringify the function, replacing arguments var functionString = executable._function.toString().replace(", require, exports, module, system, print, window", ""); // HACK var relative = this.rootPath.relative(path).toString(); this.functionsBuffer.push("ObjectiveJ.FileExecutable._cacheFunction(new CFURL("+JSON.stringify(relative)+", baseURL),\n"+functionString+");"); } var bundle = this.context.global.CFBundle.bundleContainingURL(path); if (bundle && bundle.infoDictionary()) { var executablePath = bundle.executablePath(), relativeToBundle = FILE.relative(FILE.join(bundle.path(), ""), path); if (executablePath) { if (inlineFunctions) { // remove the code since we're inlining the functions executable._code = "alert("+JSON.stringify(relativeToBundle)+");"; } if (!outputFiles[executablePath]) { outputFiles[executablePath] = []; outputFiles[executablePath].push("@STATIC;1.0;"); } var fileContents = executable.toMarkedString(); outputFiles[executablePath].push("p;" + relativeToBundle.length + ";" + relativeToBundle); outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents); // stream.print("Adding \0green(" + this.rootPath.relative(path) + "\0) to \0cyan(" + this.rootPath.relative(executablePath) + "\0)"); } } else CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path)); }, this); for (var executablePath in outputFiles) { var relative = this.rootPath.relative(executablePath).toString(); var contents = outputFiles[executablePath].join(""); this.filesToCache[relative] = contents; } } ObjectiveJFlattener.prototype.serializeFileCache = function() { for (var relative in this.filesToCache) { var contents = this.filesToCache[relative]; print("caching: " + relative + " => " + (contents == null ? 404 : 200)); if (contents == null) this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 404);"); else this.fileCacheBuffer.push("CFHTTPRequest._cacheRequest(new CFURL("+JSON.stringify(relative)+", baseURL), 200, {}, "+JSON.stringify(contents)+");"); } } ObjectiveJFlattener.prototype.setupFileCache = function() { var paths = {}; UTIL.update(paths, this.requestedURLs); this.options.paths.forEach(function(relativePath) { paths[this.rootPath.join(relativePath)] = true; }, this); Object.keys(paths).forEach(function(absolute) { var relative = this.rootPath.relative(absolute).toString(); if (relative.indexOf("..") === 0) { print("skipping (parent of app root): " + absolute); return; } if (FILE.isFile(absolute)) { // if (this.options.maxCachedSize && FILE.size(absolute) > this.options.maxCachedSize) // { // print("skipping (larger than "+this.options.maxCachedSize+" bytes): " + absolute); // return; // } var contents = FILE.read(absolute, { charset : "UTF-8" }); this.filesToCache[relative] = contents; } else { this.filesToCache[relative] = null; } }, this); } // "$1" is the matching indentation var scriptTagsBefore = '$1'; var scriptTagsAfter = '$1'; // enable CPLog: // scriptTagsAfter = '$1\n' + scriptTagsAfter; function rewriteMainHTML(indexHTMLPath) { if (indexHTMLPath.isFile()) { var indexHTML = indexHTMLPath.read({ charset : "UTF-8" }); // inline the Application.js if it's smallish var applicationJSPath = indexHTMLPath.dirname().join("Application.js"); if (applicationJSPath.size() < 10*1024) { // escape any dollar signs by replacing them with two // then indent by splitting/joining on newlines scriptTagsAfter = '$1'; } // attempt to find Objective-J script tag and add ours var newIndexHTML = indexHTML.replace(/([ \t]+)]+Objective-J\.js[^>]+>(?:\s*<\/script>)?/, scriptTagsBefore+'\n$&\n'+scriptTagsAfter); if (newIndexHTML !== indexHTML) { stream.print("\0green(Modified: "+indexHTMLPath+".\0)"); indexHTMLPath.write(newIndexHTML, { charset : "UTF-8" }); return; } } else { stream.print("\0yellow(Warning: "+indexHTMLPath+" does not exist. Specify an alternate index HTML file with the --index option.\0)"); } stream.print("\0yellow(Warning: Unable to automatically modify "+indexHTMLPath + ".\0)"); stream.print("\nAdd the following before the Objective-J script tag:"); stream.print(scriptTagsBefore.replace(/\$1/g, " ")); stream.print("\nAdd the following after the Objective-J script tag:"); stream.print(scriptTagsAfter.replace(/\$1/g, " ")); }