/CommonJS/bin/flatten
http://github.com/cacaodev/cappuccino · #! · 368 lines · 286 code · 82 blank · 0 comment · 0 complexity · e525892134b62c88284ca574ebb25905 MD5 · raw file
- #!/usr/bin/env objj
- require("narwhal").ensureEngine("rhino");
- @import <Foundation/Foundation.j>
- @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" || defaultThemeName === @"Aristo2")
- 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<script type = "text/javascript">\n'+
- '$1 OBJJ_AUTO_BOOTSTRAP = false;\n'+
- '$1</script>';
- var scriptTagsAfter =
- '$1<script type="text/javascript" src="Application.js" charset="UTF-8"></script>';
- // enable CPLog:
- // scriptTagsAfter = '$1<script type="text/javascript">\n$1 CPLogRegister(CPLogConsole);\n$1</script>\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<script type="text/javascript">\n'+
- '$1 ' + applicationJSPath.read({ charset : "UTF-8" }).replace(/\$/g, "$$$$").split("\n").join("\n$1 ")+'\n'+
- '$1</script>';
- }
- // attempt to find Objective-J script tag and add ours
- var newIndexHTML = indexHTML.replace(/([ \t]+)<script[^>]+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, " "));
- }