import import "objj-analysis-tools.j" var defaultMain = "main.j", defaultFrameworks = "Frameworks"; var usageMessage = "Usage: press root_directory output_directory [options]\n\ --main path The relative path (from root_directory) to the main file (default: 'main.j')\n\ --frameworks path The relative path (from root_directory) to the frameworks directory (default: 'Frameworks')\n\ --png Run pngcrush on all PNGs (pngcrush must be installed!)\n\ --flatten Flatten all code into a single Application.js file and attempt add script tag to index.html (useful for Adobe AIR and CDN deployment)\n\ --nostrip Don't strip any files"; function main() { var rootDirectory = null, outputDirectory = null, mainFilename = null, frameworksDirectory = null, optimizePNG = false, flatten = false, noStrip = false, verbose = true; var usageError = false; while (args.length && !usageError) { var arg = args.shift(); switch(arg) { case "--main": if (args.length) mainFile = args.shift(); else usageError = true; break; case "--frameworks": if (args.length) frameworksDirectory = args.shift().replace(/\/$/, ""); else usageError = true; break; case "--png": optimizePNG = true; break; case "--flatten": flatten = true; break; case "--nostrip": noStrip = true; break; default: if (rootDirectory == null) rootDirectory = arg.replace(/\/$/, ""); else if (outputDirectory == null) outputDirectory = arg.replace(/\/$/, ""); else usageError = true; } } if (verbose) CPLogRegister(CPLogPrint); else CPLogRegisterRange(CPLogPrint, "fatal", "info"); if (usageError || rootDirectory == null || outputDirectory == null) { print(usageMessage); return; } rootDirectory = absolutePath(rootDirectory); // determine main and frameworks paths var mainPath = rootDirectory + "/" + (mainFilename || defaultMain), frameworksPath = rootDirectory + "/" + (frameworksDirectory || defaultFrameworks); CPLog.info("Application root: " + rootDirectory); CPLog.info("Output directory: " + outputDirectory); CPLog.info("Main file: " + mainPath) CPLog.info("Frameworks: " + frameworksPath); // get a Rhino context var cx = Packages.org.mozilla.javascript.Context.getCurrentContext(), scope = makeObjjScope(cx); // set OBJJ_INCLUDE_PATHS to include the frameworks path scope.OBJJ_INCLUDE_PATHS = [frameworksPath]; // flattening bookkeeping. keep track of the bundles and evaled code (in the correct order!) var bundleArchives = [], evaledFragments = []; scope.objj_search.prototype.didReceiveBundleResponseOriginal = scope.objj_search.prototype.didReceiveBundleResponse; scope.objj_search.prototype.didReceiveBundleResponse = function(aResponse) { //CPLog.trace("RESPONSE: " + aResponse); var __fakeResponse = { success : aResponse.success, filePath : pathRelativeTo(aResponse.filePath, rootDirectory) }; if (aResponse.success) { var xmlOutput = new Packages.java.io.ByteArrayOutputStream(); outputTransformer(xmlOutput, aResponse.xml, "UTF-8"); __fakeResponse.xml = String(xmlOutput.toString()); //CPLog.trace("SERIALIZED: " + __fakeResponse.xml.substring(0,100)); } bundleArchives.push(__fakeResponse); this.didReceiveBundleResponseOriginal.apply(this, arguments); } // phase 1: get global defines CPLog.info("Loading application..."); var globals = findGlobalDefines(cx, scope, mainPath, evaledFragments); // coalesce the results var dependencies = coalesceGlobalDefines(globals); // phase 2: walk the dependency tree (both imports and references) to determine exactly which files need to be included var requiredFiles = {}; if (noStrip) { // all files are required. no need for analysis requiredFiles = scope.objj_files; } else { if (!scope.objj_files[mainPath]) { CPLog.error("Root file not loaded!"); return; } CPLog.info("Analyzing dependencies..."); var context = { scope : scope, dependencies : dependencies, processedFiles : {}, ignoreFrameworkImports : true, importCallback : function(importing, imported) { requiredFiles[imported] = true; }, referenceCallback : function(referencing, referenced) { requiredFiles[referenced] = true; } } requiredFiles[mainPath] = true; traverseDependencies(context, scope.objj_files[mainPath]); var count = 0, total = 0; for (var path in scope.objj_files) { if (requiredFiles[path]) { CPLog.debug("Included: " + path); count++; } else { CPLog.warn("Excluded: " + path); } total++; } CPLog.info("Total required files: " + count + " out of " + total); // FIXME: sprite images for (var i in context.bundleImages) { var images = context.bundleImages[i]; //CPLog.info("Bundle images for " + i); //for (var j in images) // CPLog.debug(j + " = " + images[j]); } } var outputFiles = {}; if (flatten) { // phase 3a: build single Application.js file (and modified index.html) CPLog.info("Flattening..."); var applicationJS = [], indexHTML = readFile(rootDirectory + "/index.html"); // shim for faking bundle stuff. kind of a giant hack. var fakeDidReceiveBundleResponse = function(aResponse) { var bundle = new objj_bundle(); bundle.path = aResponse.filePath; if (aResponse.success) bundle.info = CPPropertyListCreateFromXMLData({ string : aResponse.xml }); else bundle.info = new objj_dictionary(); objj_bundles[aResponse.filePath] = bundle; } // add fake bundle response bookkeeping applicationJS.push("var __fakeDidReceiveBundleResponse = " + String(fakeDidReceiveBundleResponse)); applicationJS.push("var __fakeBundleArchives = " + CPJSObjectCreateJSON(bundleArchives) + ";"); applicationJS.push("for (var i = 0; i < __fakeBundleArchives.length; i++) __fakeDidReceiveBundleResponse(__fakeBundleArchives[i]);") // add each fragment, wrapped in a function, along with OBJJ_CURRENT_BUNDLE bookkeeping for (var i = 0; i < evaledFragments.length; i++) { if (requiredFiles[evaledFragments[i].file.path]) { applicationJS.push("(function() {"); applicationJS.push("var OBJJ_CURRENT_BUNDLE = objj_bundles['"+pathRelativeTo(evaledFragments[i].bundle.path, rootDirectory)+"'];"); applicationJS.push(evaledFragments[i].info); applicationJS.push("})();"); } else { CPLog.warn("Stripping " + evaledFragments[i].file.path); } } // call main once the page has loaded applicationJS.push( "if (window.addEventListener) \ window.addEventListener('load', main, false); \ else if (window.attachEvent) \ window.attachEvent('onload', main);" ); // do a little regex magic to remove the objj_import and put in a script tag for Application.js var matches = indexHTML.match(/]*>(?:.|\n)*?<\/script>/g); for (var i = 0; i < matches.length; i++) { if (/\bobjj_import\s*\(/.test(matches[i])) { CPLog.warn("Replacing objj_import with script tag."); var replacement = '\n' + matches[i].replace("objj_import", "//objj_import"); indexHTML = indexHTML.replace(matches[i], replacement); } } // output Application.js and index.html outputFiles[rootDirectory + "/Application.js"] = applicationJS.join("\n"); outputFiles[rootDirectory + "/index.html"] = indexHTML; } else { // phase 3b: rebuild .sj files with correct imports, copy .j files var bundles = {}; for (var path in requiredFiles) { var file = scope.objj_files[path], filename = basename(path), directory = dirname(path); if (file.path != path) CPLog.warn("Sanity check (file path): " + file.path + " vs. " + path); if (file.bundle) { var bundleDirectory = dirname(file.bundle.path); if (!bundles[file.bundle.path]) bundles[file.bundle.path] = file.bundle; if (bundleDirectory != directory) CPLog.warn("Sanity check (directory path): " + directory + " vs. " + bundleDirectory); // if it's in a .sj var dict = file.bundle.info, replacedFiles = [dict objectForKey:"CPBundleReplacedFiles"]; if (replacedFiles && [replacedFiles containsObject:filename]) { var staticPath = bundleDirectory + "/" + [dict objectForKey:"CPBundleExecutable"]; if (!outputFiles[staticPath]) { outputFiles[staticPath] = []; outputFiles[staticPath].push("@STATIC;1.0;"); } outputFiles[staticPath].push("p;"); outputFiles[staticPath].push(filename.length+";"); outputFiles[staticPath].push(filename); for (var i = 0; i < file.fragments.length; i++) { if (file.fragments[i].type & FRAGMENT_CODE) { outputFiles[staticPath].push("c;"); outputFiles[staticPath].push(file.fragments[i].info.length+";"); outputFiles[staticPath].push(file.fragments[i].info); } else if (file.fragments[i].type & FRAGMENT_FILE) { var ignoreFragment = false; if (file.fragments[i].conditionallyIgnore) { var importPath = findImportInObjjFiles(scope, file.fragments[i]); if (!importPath || !requiredFiles[importPath]) { ignoreFragment = true; } } if (!ignoreFragment) { if (file.fragments[i].type & FRAGMENT_LOCAL) { var relativePath = pathRelativeTo(file.fragments[i].info, directory) outputFiles[staticPath].push("i;"); outputFiles[staticPath].push(relativePath.length+";"); outputFiles[staticPath].push(relativePath); } else { outputFiles[staticPath].push("I;"); outputFiles[staticPath].push(file.fragments[i].info.length+";"); outputFiles[staticPath].push(file.fragments[i].info); } } else CPLog.warn("Ignoring import fragment " + file.fragments[i].info + " in " + path); } else CPLog.error("Unknown fragment type"); } } // always output individual .j files else { outputFiles[path] = file.contents; } } else CPLog.warn("No bundle for " + path) } // phase 3.5: fix bundle plists for (var path in bundles) { var bundle = bundles[path]; CPLog.info("Bundle: " + path); var dict = file.bundle.info, replacedFiles = [dict objectForKey:"CPBundleReplacedFiles"]; if (replacedFiles) { for (var i = 0; i < replacedFiles.length; i++) { if (!requiredFiles[replacedFiles[i]]) { CPLog.info("Removing: " + replacedFiles[i]); replacedFiles.splice(i, 1); } } } outputFiles[path] = CPPropertyListCreateXMLData(bundle.info).string; } } // phase 4: copy everything and write out the new files var rootDirectoryFile = new Packages.java.io.File(rootDirectory), outputDirectoryFile = new Packages.java.io.File(outputDirectory); // FIXME: intelligently copy only what we need (Resources directories?) copyDirectory(rootDirectoryFile, outputDirectoryFile, optimizePNG); for (var path in outputFiles) { var file = new java.io.File(outputDirectoryFile, pathRelativeTo(path, rootDirectory)); var parent = file.getParentFile(); if (!parent.exists()) { CPLog.warn(parent + " doesn't exist, creating directories."); parent.mkdirs(); } CPLog.info("Writing out " + file); var writer = new java.io.BufferedWriter(new java.io.FileWriter(file)); if (typeof outputFiles[path] == "string") writer.write(outputFiles[path]); else writer.write(outputFiles[path].join("")); writer.close(); } } // Helper Utilities // TODO: moved elsewhere? function copyDirectory(src, dst, optimizePNG) { CPLog.trace("Copying directory " + src); dst.mkdirs(); var files = src.listFiles(); for (var i = 0; i < files.length; i++) { if (files[i].isFile()) copyFile(files[i], new Packages.java.io.File(dst, files[i].getName()), optimizePNG); else if (files[i].isDirectory()) copyDirectory(files[i], new Packages.java.io.File(dst, files[i].getName()), optimizePNG); } } function copyFile(src, dst, optimizePNG) { if (optimizePNG && (/.png$/).test(src.getName())) { CPLog.warn("Optimizing .png " + src); exec(["pngcrush", "-rem", "alla", "-reduce", /*"-brute",*/ src.getAbsolutePath(), dst.getAbsolutePath()]); } else { CPLog.trace("Copying file " + src); var input = (new Packages.java.io.FileInputStream(src)).getChannel(), output = (new Packages.java.io.FileOutputStream(dst)).getChannel(); input.transferTo(0, input.size(), output); input.close(); output.close(); } } function dirname(path) { return path.substring(0, path.lastIndexOf("/")); } function basename(path) { return path.substring(path.lastIndexOf("/") + 1); } function absolutePath(path) { return String((new Packages.java.io.File(path)).getCanonicalPath()); } function pathRelativeTo(target, relativeTo) { var components = [], targetParts = target.split("/"), relativeParts = relativeTo ? relativeTo.split("/") : []; var i = 0; while (i < targetParts.length) { if (targetParts[i] != relativeParts[i]) break; i++; } for (var j = i; j < relativeParts.length; j++) components.push(".."); for (var j = i; j < targetParts.length; j++) components.push(targetParts[j]); var result = components.join("/"); return result; } function exec() { var printOutput = false; var runtime = Packages.java.lang.Runtime.getRuntime() var p = runtime.exec.apply(runtime, arguments); var stdout = new Packages.java.io.BufferedReader(new Packages.java.io.InputStreamReader(p.getInputStream())), stdoutString = "", stderr = new Packages.java.io.BufferedReader(new Packages.java.io.InputStreamReader(p.getErrorStream())), stderrString = ""; var done = false; while (!done) { done = true; if (s = stdout.readLine()) { stdoutString += s; if (printOutput) CPLog.info("exec: " + s); done = false; } if (s = stderr.readLine()) { stderrString += s; //if (printOutput) CPLog.warn("exec: " + s); done = false; } } var code = p.waitFor(); return { code : code, stdout : stdoutString, stderr : stderrString }; } function outputTransformer(os, document, encoding, standalone) { var domSource = new Packages.javax.xml.transform.dom.DOMSource(document); var streamResult = new Packages.javax.xml.transform.stream.StreamResult(os); var tf = Packages.javax.xml.transform.TransformerFactory.newInstance(); var serializer = tf.newTransformer(); serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.VERSION, "1.0"); serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.INDENT, "yes"); if (encoding) serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.ENCODING, encoding); if (standalone) serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.STANDALONE, (standalone ? "yes" : "no")); serializer.transform(domSource, streamResult); } main();