From d3a0d90ce78f17043aee240646be7306fb8ca1fd Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Tue, 21 Oct 2008 03:42:24 -0700 Subject: [PATCH] "press" support for flattening to a single JavaScript file, and foundation of intelligent image sprites --- Tools/press/objj-analysis-tools.j | 771 +++++++++++++------------ Tools/press/press.j | 907 ++++++++++++++++++------------ 2 files changed, 956 insertions(+), 722 deletions(-) mode change 100755 => 100644 Tools/press/objj-analysis-tools.j diff --git a/Tools/press/objj-analysis-tools.j b/Tools/press/objj-analysis-tools.j old mode 100755 new mode 100644 index 4dea7978d..ba2f1898c --- a/Tools/press/objj-analysis-tools.j +++ b/Tools/press/objj-analysis-tools.j @@ -1,361 +1,412 @@ -var objjPath = OBJJ_LIB+'/Frameworks/Objective-J/Objective-J.js', - bridgePath = OBJJ_LIB+'/bridge.js', - envPath = "/Users/tlrobinson/280North/git/cappuccino/Tools/press/env.js"; - -/* - param context includes - scope: a global variable containing objj_files hash - processedFiles: hash containing file paths which have already been analyzed - dependencies: hash mapping from paths to an array of global variables defined by that file - [importCallback]: callback function that is called for each imported file (takes importing file path, and imported file path parameters) - [referencedCallback]: callback function that is called for each referenced file (takes referencing file path, referenced file path parameters, and list of tokens) - [importedFiles]: hash that will contain a mapping of file names to a hash of imported files - [referencedFiles]: hash that will contain a mapping of file names to a hash of referenced files (which contains a hash of tokens referenced) - - param file is an objj_file object containing path, fragments, content, bundle, etc -*/ -function traverseDependencies(context, file) -{ - if (context.processedFiles[file.path]) - return; - context.processedFiles[file.path] = true; - - var ignoreImports = false; - if (context.ignoreAllImports) - { - CPLog.warn("Ignoring all import fragments. ("+file.path+")"); - ignoreImports = true; - } - else if (context.ignoreFrameworkImports) - { - var matches = file.path.match(/([^\/]+)\/([^\/]+)\.j$/); // Matches "ZZZ/ZZZ.j" (e.x. AppKit/AppKit.j and Foundation/Foundation.j) - if (matches && matches[1] === matches[2]) - { - CPLog.warn("Framework import file! Ignoring all import fragments. ("+file.path+")"); - ignoreImports = true; - } - } - - if (!file.fragments) - { - if (file.included) - CPLog.warn(file.path + " is included but missing fragments"); - else - CPLog.info("Preprocessing " + file.path); - - file.fragments = objj_preprocess(file.contents, file.bundle, file); - } - - var referencedFiles = {}, - importedFiles = {}; - - CPLog.trace("Processing " + file.path + " fragments ("+file.fragments.length+")"); - for (var i = 0; i < file.fragments.length; i++) - { - var fragment = file.fragments[i]; - - if (fragment.type & FRAGMENT_CODE) - { - var lexer = new objj_lexer(fragment.info, NULL); - - var token; - while (token = lexer.skip_whitespace()) - { - if (context.dependencies[token]) - { - var files = context.dependencies[token] - for (var j = 0; j < files.length; j++) - { - // don't record references to self - if (files[j] != file.path) - { - if (!referencedFiles[files[j]]) - referencedFiles[files[j]] = {}; - - referencedFiles[files[j]][token] = true; - } - } - } - } - } - else if (fragment.type & FRAGMENT_FILE) - { - if (ignoreImports) - { - fragment.conditionallyIgnore = true; - } - else - { - var importedFile = findImportInObjjFiles(context.scope, fragment); - if (importedFile) - { - // should never import self, but just in case? - if (importedFile != file.path) - importedFiles[importedFile] = true; - else - CPLog.error("Ignoring self import (why are you importing yourself!?): " + file.path); - } - else - CPLog.error("Couldn't find file for import " + fragment.info + "("+fragment.type+")"); - } - } - } - - // check each imported file - for (var importedFile in importedFiles) - { - if (importedFile != file.path) - { - if (context.importCallback) - context.importCallback(file.path, importedFile); - - if (context.scope.objj_files[importedFile]) - traverseDependencies(context, context.scope.objj_files[importedFile]); - else - CPLog.error("Missing imported file: " + importedFile); - } - } - - if (context.importedFiles) - context.importedFiles[file.path] = importedFiles; - - // check each referenced file - for (var referencedFile in referencedFiles) - { - if (referencedFile != file.path) - { - if (context.referenceCallback) - context.referenceCallback(file.path, referencedFile, referencedFiles[referencedFile]); - - if (context.scope.objj_files[referencedFile]) - traverseDependencies(context, context.scope.objj_files[referencedFile]); - else - CPLog.error("Missing referenced file: " + referencedFile); - } - } - - if (context.referencedFiles) - context.referencedFiles[file.path] = referencedFiles; -} - -function findImportInObjjFiles(scope, fragment) -{ - var importPath = null; - - if (fragment.type & FRAGMENT_LOCAL) - { - var searchPath = fragment.info; - if (scope.objj_files[searchPath]) - { - importPath = searchPath; - } - } - else - { - var count = scope.OBJJ_INCLUDE_PATHS.length; - while (count--) - { - var searchPath = scope.OBJJ_INCLUDE_PATHS[count].replace(/\/$/, "") + "/" + fragment.info; - if (scope.objj_files[searchPath]) - { - importPath = searchPath; - break; - } - } - } - - return importPath; -} - -// given a fresh scope and the path to a root source file, determine which files define each global variable -function findGlobalDefines(context, scope, rootPath) -{ - addMockBrowserEnvironment(scope); - - var ignore = cloneProperties(scope, true); - ignore['bundle'] = true; - - var dependencies = {}; - - //scope.fragment_evaluate_file_original = scope.fragment_evaluate_file; - //scope.fragment_evaluate_file = function(aFragment) - //{ - // //CPLog.trace("Loading "+aFragment.info); - // - // var result = scope.fragment_evaluate_file_original(aFragment); - // - // return result; - //} - - var needsPatch = true; - - var fragment_evaluate_file_original = scope.fragment_evaluate_file; - scope.fragment_evaluate_file = function(aFragment) - { - // patch Foundation.j (HACK for Rhino bug #374918) - if (needsPatch && aFragment.file && (/Foundation\.j$/).test(aFragment.file.path)) - { - CPLog.error("Patching Foundation.j"); - - var patchFragment = new objj_fragment(); - patchFragment.info = "__RHINO_FIRST_SCOPE.String.prototype.isa=CPString;__RHINO_FIRST_SCOPE.Number.prototype.isa=CPNumber;__RHINO_FIRST_SCOPE.Boolean.prototype.isa=CPNumber;print('PATCHED!');"; - patchFragment.type = FRAGMENT_CODE; - patchFragment.file = aFragment.file; - patchFragment.bundle = aFragment.bundle; - patchFragment.context = aFragment.context; - - for (var i = 0; i < aFragment.context.fragments.length; i++) - { - if ((aFragment.context.fragments[i].type & FRAGMENT_FILE) && (/Foundation\//).test(aFragment.context.fragments[i].info)) - { - CPLog.error("Inserting patch"); - aFragment.context.fragments.splice(i, 0, patchFragment); - break; - } - } - - needsPatch = false; - } - - return fragment_evaluate_file_original(aFragment); - } - - var fragment_evaluate_code_original = scope.fragment_evaluate_code; - scope.fragment_evaluate_code = function(aFragment) - { - CPLog.debug("Evaling "+aFragment.file.path + " / " + aFragment.bundle.path); - - var before = cloneProperties(scope); - - var result = fragment_evaluate_code_original(aFragment); - - var definedGlobals = {}; - diff(before, scope, ignore, definedGlobals, definedGlobals, null); - dependencies[aFragment.file.path] = definedGlobals; - - return result; - } - - runWithScope(context, scope, function(importName) - { - objj_import(importName, true, NULL); - }, [rootPath]); - - return dependencies; -} - -function coalesceGlobalDefines(globals) -{ - var dependencies = {}; - for (var fileName in globals) - { - var fileGlobals = globals[fileName]; - - for (var globalName in fileGlobals) - { - if (!dependencies[globalName]) - dependencies[globalName] = []; - dependencies[globalName].push(fileName); - } - } - return dependencies; -} - -// create a new scope loaded with Objective-J -function makeObjjScope(context, debug) -{ - // init standard js scope objects - var scope = context.initStandardObjects(); - - if (debug) - { - scope.objj_alert = print; - scope.debug = true; - } - - // give the scope "print" - scope.print = function(value) { Packages.java.lang.System.out.println(String(value)); }; - - // HACK for Rhino bug #374918 (fix toll free bridging) - scope.__RHINO_FIRST_SCOPE = this; - CPLog.info("__RHINO_FIRST_SCOPE="+scope.__RHINO_FIRST_SCOPE); - - // load and eval fake browser environment - //var envSource = readFile(envPath); - //if (envSource) - // context.evaluateString(scope, envSource, "env.js", 1, null); - //else - // CPLog.warn("Missing env.js"); - - // load and eval the bridge - var bridgeSource = readFile(bridgePath); - if (bridgeSource) - context.evaluateString(scope, bridgeSource, "bridge.js", 1, null); - else - CPLog.warn("Missing bridge.js"); - - // load and eval obj-j - var objjSource = readFile(objjPath); - if (objjSource) - context.evaluateString(scope, objjSource, "Objective-J.js", 1, null); - else - CPLog.warn("Missing Objective-J.js"); - - return scope; -} - -// run a function within the given scope (func can be a function object if the source of the function is returned by toString() as it is by default) -function runWithScope(context, scope, func, arguments) -{ - scope.__runWithScopeArgs = arguments || []; - - var code = "("+func+").apply(this, this.__runWithScopeArgs); serviceTimeouts();"; - - return context.evaluateString(scope, code, "", 1, null); -} - -// add a mock browser environment to the provided scope -function addMockBrowserEnvironment(scope) -{ - // TODO: complete this. or use env.js? - - scope.Element = function() { - this.style = {} - } - - scope.document = { - createElement : function() { - return new scope.Element(); - } - } -} - -// does a shallow copy of an object. if onlyList is true, it sets each property to "true" instead of the actual value -function cloneProperties(object, onlyList) -{ - var results = {} - for (var memeber in object) - results[memeber] = onlyList ? true : object[memeber]; - return results; -} - -function diff(objectA, objectB, ignore, added, changed, deleted) -{ - for (var i in objectB) - if (added && !ignore[i] && typeof objectA[i] == "undefined") - added[i] = true; - for (var i in objectB) - if (changed && !ignore[i] && typeof objectA[i] != "undefined" && typeof objectB[i] != "undefined" && objectA[i] !== objectB[i]) - changed[i] = true; - for (var i in objectA) - if (deleted && !ignore[i] && typeof objectB[i] == "undefined") - deleted[i] = true; -} - -function allKeys(object) -{ - var result = []; - for (var i in object) - result.push(i) - return result.sort(); +var objjPath = OBJJ_LIB+'/Frameworks-Rhino/Objective-J/Objective-J.js', + bridgePath = OBJJ_LIB+'/bridge.js', + envPath = "/Users/tlrobinson/280North/git/cappuccino/Tools/press/env.js"; + +/* + param context includes + scope: a global variable containing objj_files hash + processedFiles: hash containing file paths which have already been analyzed + dependencies: hash mapping from paths to an array of global variables defined by that file + [importCallback]: callback function that is called for each imported file (takes importing file path, and imported file path parameters) + [referencedCallback]: callback function that is called for each referenced file (takes referencing file path, referenced file path parameters, and list of tokens) + [importedFiles]: hash that will contain a mapping of file names to a hash of imported files + [referencedFiles]: hash that will contain a mapping of file names to a hash of referenced files (which contains a hash of tokens referenced) + + param file is an objj_file object containing path, fragments, content, bundle, etc +*/ +function traverseDependencies(context, file) +{ + if (context.processedFiles[file.path]) + return; + context.processedFiles[file.path] = true; + + var ignoreImports = false; + if (context.ignoreAllImports) + { + CPLog.warn("Ignoring all import fragments. ("+file.path+")"); + ignoreImports = true; + } + else if (context.ignoreFrameworkImports) + { + var matches = file.path.match(/([^\/]+)\/([^\/]+)\.j$/); // Matches "ZZZ/ZZZ.j" (e.x. AppKit/AppKit.j and Foundation/Foundation.j) + if (matches && matches[1] === matches[2]) + { + CPLog.warn("Framework import file! Ignoring all import fragments. ("+file.path+")"); + ignoreImports = true; + } + } + + // if fragments are missing, preprocess the contents + if (!file.fragments) + { + if (file.included) + CPLog.warn(file.path + " is included but missing fragments"); + else + CPLog.warn("Preprocessing " + file.path); + + file.fragments = objj_preprocess(file.contents, file.bundle, file); + } + + // sprite: look for pngs in the Resources directory + if (!context.bundleImages) + context.bundleImages = {}; + + if (!context.bundleImages[file.bundle.path]) + { + var resourcesFile = new java.io.File(dirname(file.bundle.path) + "/Resources"); + if (resourcesFile.exists()) + { + context.bundleImages[file.bundle.path] = {}; + + var pngFiles = find(resourcesFile, (/\.png$/)); + for (var i = 0; i < pngFiles.length; i++) + { + var path = pathRelativeTo(pngFiles[i].getCanonicalPath(), resourcesFile.getCanonicalPath()); + context.bundleImages[file.bundle.path][path] = 1; + } + } + } + var images = context.bundleImages[file.bundle.path]; + + var referencedFiles = {}, + importedFiles = {}; + + CPLog.debug("Processing " + file.path + " fragments ("+file.fragments.length+")"); + for (var i = 0; i < file.fragments.length; i++) + { + var fragment = file.fragments[i]; + + if (fragment.type & FRAGMENT_CODE) + { + var lexer = new objj_lexer(fragment.info, NULL); + + var token; + while (token = lexer.skip_whitespace()) + { + if (context.dependencies[token]) + { + var files = context.dependencies[token] + for (var j = 0; j < files.length; j++) + { + // don't record references to self + if (files[j] != file.path) + { + if (!referencedFiles[files[j]]) + referencedFiles[files[j]] = {}; + + referencedFiles[files[j]][token] = true; + } + } + } + + var matches = token.match(new RegExp("^['\"](.*)['\"]$")); + if (matches && images && images[matches[1]]) + images[matches[1]] = (images[matches[1]] | 2); + } + } + else if (fragment.type & FRAGMENT_FILE) + { + if (ignoreImports) + { + fragment.conditionallyIgnore = true; + } + else + { + var importedFile = findImportInObjjFiles(context.scope, fragment); + if (importedFile) + { + // should never import self, but just in case? + if (importedFile != file.path) + importedFiles[importedFile] = true; + else + CPLog.error("Ignoring self import (why are you importing yourself!?): " + file.path); + } + else + CPLog.error("Couldn't find file for import " + fragment.info + "("+fragment.type+")"); + } + } + } + + // check each imported file + for (var importedFile in importedFiles) + { + if (importedFile != file.path) + { + if (context.importCallback) + context.importCallback(file.path, importedFile); + + if (context.scope.objj_files[importedFile]) + traverseDependencies(context, context.scope.objj_files[importedFile]); + else + CPLog.error("Missing imported file: " + importedFile); + } + } + + if (context.importedFiles) + context.importedFiles[file.path] = importedFiles; + + // check each referenced file + for (var referencedFile in referencedFiles) + { + if (referencedFile != file.path) + { + if (context.referenceCallback) + context.referenceCallback(file.path, referencedFile, referencedFiles[referencedFile]); + + if (context.scope.objj_files[referencedFile]) + traverseDependencies(context, context.scope.objj_files[referencedFile]); + else + CPLog.error("Missing referenced file: " + referencedFile); + } + } + + if (context.referencedFiles) + context.referencedFiles[file.path] = referencedFiles; +} + +function findImportInObjjFiles(scope, fragment) +{ + var importPath = null; + + if (fragment.type & FRAGMENT_LOCAL) + { + var searchPath = fragment.info; + //CPLog.trace("Looking for " + searchPath); + //for (var i in scope.objj_files) CPLog.debug(" " + i); + + if (scope.objj_files[searchPath]) + { + importPath = searchPath; + } + } + else + { + var count = scope.OBJJ_INCLUDE_PATHS.length; + while (count--) + { + var searchPath = scope.OBJJ_INCLUDE_PATHS[count].replace(/\/$/, "") + "/" + fragment.info; + if (scope.objj_files[searchPath]) + { + importPath = searchPath; + break; + } + } + } + + return importPath; +} + +// given a fresh scope and the path to a root source file, determine which files define each global variable +function findGlobalDefines(context, scope, rootPath, evaledFragments) +{ + addMockBrowserEnvironment(scope); + + var ignore = cloneProperties(scope, true); + ignore['bundle'] = true; + + var dependencies = {}; + + //scope.fragment_evaluate_file_original = scope.fragment_evaluate_file; + //scope.fragment_evaluate_file = function(aFragment) + //{ + // //CPLog.trace("Loading "+aFragment.info); + // + // var result = scope.fragment_evaluate_file_original(aFragment); + // + // return result; + //} + + var needsPatch = true, + patchString = "__RHINO_FIRST_SCOPE.String.prototype.isa=CPString;__RHINO_FIRST_SCOPE.Number.prototype.isa=CPNumber;__RHINO_FIRST_SCOPE.Boolean.prototype.isa=CPNumber;print('PATCHED!');"; + + // OVERRIDE fragment_evaluate_file + var fragment_evaluate_file_original = scope.fragment_evaluate_file; + scope.fragment_evaluate_file = function(aFragment) { + + // patch Foundation.j (HACK for Rhino bug #374918) + if (needsPatch && aFragment.file && (/Foundation\.j$/).test(aFragment.file.path)) + { + CPLog.warn("Patching Foundation.j"); + + var patchFragment = new objj_fragment(); + patchFragment.info = patchString; + patchFragment.type = FRAGMENT_CODE; + patchFragment.file = aFragment.file; + patchFragment.bundle = aFragment.bundle; + patchFragment.context = aFragment.context; + + for (var i = 0; i < aFragment.context.fragments.length; i++) + { + if ((aFragment.context.fragments[i].type & FRAGMENT_FILE) && (/Foundation\//).test(aFragment.context.fragments[i].info)) + { + CPLog.warn("Inserting patch"); + aFragment.context.fragments.splice(i, 0, patchFragment); + break; + } + } + + needsPatch = false; + } + + return fragment_evaluate_file_original(aFragment); + } + + // OVERRIDE fragment_evaluate_code + var fragment_evaluate_code_original = scope.fragment_evaluate_code; + scope.fragment_evaluate_code = function(aFragment) { + + CPLog.debug("Evaling "+aFragment.file.path + " / " + aFragment.bundle.path); + + var before = cloneProperties(scope); + + if (evaledFragments) + { + if (aFragment.info != patchString) + evaledFragments.push(aFragment); + } + + var result = fragment_evaluate_code_original(aFragment); + + var definedGlobals = {}; + diff(before, scope, ignore, definedGlobals, definedGlobals, null); + dependencies[aFragment.file.path] = definedGlobals; + + return result; + } + + + runWithScope(context, scope, function(importName) { + objj_import(importName, true, NULL); + }, [rootPath]); + + return dependencies; +} + +function coalesceGlobalDefines(globals) +{ + var dependencies = {}; + for (var fileName in globals) + { + var fileGlobals = globals[fileName]; + + for (var globalName in fileGlobals) + { + if (!dependencies[globalName]) + dependencies[globalName] = []; + dependencies[globalName].push(fileName); + } + } + return dependencies; +} + +// create a new scope loaded with Objective-J +function makeObjjScope(context, debug) +{ + // init standard js scope objects + var scope = context.initStandardObjects(); + + if (debug) + { + scope.objj_alert = print; + scope.debug = true; + } + + // give the scope "print" + scope.print = function(value) { Packages.java.lang.System.out.println(String(value)); }; + + // HACK for Rhino bug #374918 (fix toll free bridging) + scope.__RHINO_FIRST_SCOPE = this; + + // load and eval fake browser environment + //var envSource = readFile(envPath); + //if (envSource) + // context.evaluateString(scope, envSource, "env.js", 1, null); + //else + // CPLog.warn("Missing env.js"); + + // load and eval the bridge + var bridgeSource = readFile(bridgePath); + if (bridgeSource) + context.evaluateString(scope, bridgeSource, "bridge.js", 1, null); + else + CPLog.warn("Missing bridge.js"); + + // load and eval obj-j + var objjSource = readFile(objjPath); + if (objjSource) + context.evaluateString(scope, objjSource, "Objective-J.js", 1, null); + else + CPLog.warn("Missing Objective-J.js"); + + return scope; +} + +// run a function within the given scope (func can be a function object if the source of the function is returned by toString() as it is by default) +function runWithScope(context, scope, func, arguments) +{ + scope.__runWithScopeArgs = arguments || []; + + var code = "("+func+").apply(this, this.__runWithScopeArgs); serviceTimeouts();"; + + return context.evaluateString(scope, code, "", 1, null); +} + +// add a mock browser environment to the provided scope +function addMockBrowserEnvironment(scope) +{ + // TODO: complete this. or use env.js? + + scope.Element = function() { + this.style = {} + } + + scope.document = { + createElement : function() { + return new scope.Element(); + } + } +} + +// does a shallow copy of an object. if onlyList is true, it sets each property to "true" instead of the actual value +function cloneProperties(object, onlyList) +{ + var results = {} + for (var memeber in object) + results[memeber] = onlyList ? true : object[memeber]; + return results; +} + +function diff(objectA, objectB, ignore, added, changed, deleted) +{ + for (var i in objectB) + if (added && !ignore[i] && typeof objectA[i] == "undefined") + added[i] = true; + for (var i in objectB) + if (changed && !ignore[i] && typeof objectA[i] != "undefined" && typeof objectB[i] != "undefined" && objectA[i] !== objectB[i]) + changed[i] = true; + for (var i in objectA) + if (deleted && !ignore[i] && typeof objectB[i] == "undefined") + deleted[i] = true; +} + +function allKeys(object) +{ + var result = []; + for (var i in object) + result.push(i) + return result.sort(); +} + +function find(src, regex) +{ + var results = []; + var files = src.listFiles(); + for (var i = 0; i < files.length; i++) + { + if (files[i].isFile() && regex.test(files[i].getAbsolutePath())) + results.push(files[i]); + else if (files[i].isDirectory()) + results = Array.prototype.concat.apply(results, find(files[i], regex)); + } + return results; } \ No newline at end of file diff --git a/Tools/press/press.j b/Tools/press/press.j index dbf63c359..3db03df71 100644 --- a/Tools/press/press.j +++ b/Tools/press/press.j @@ -1,362 +1,545 @@ -import - -import "objj-analysis-tools.j" - -CPLogRegister(CPLogPrint); - -var defaultMain = "main.j", - defaultFrameworks = "Frameworks"; - -function main() -{ - var rootDirectory = null, - outputDirectory = null, - mainFilename = null, - frameworksDirectory = null, - optimizePNG = false; - - var usageError = false; - while (args.length && !usageError) - { - var arg = args.shift(); - switch(arg) - { - case "--png": - optimizePNG = true; - break; - 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; - default: - if (rootDirectory == null) - rootDirectory = arg.replace(/\/$/, ""); - else if (outputDirectory == null) - outputDirectory = arg.replace(/\/$/, ""); - else - usageError = true; - } - } - - if (rootDirectory == null || outputDirectory == null) - { - print("Usage: press root_directory output_directory [--main override_main.j] [--frameworks override_frameworks] [--png]"); - return; - } - - rootDirectory = absolutePath(rootDirectory); - - // determine main and frameworks paths - var mainPath = rootDirectory + "/" + (mainFilename || defaultMain), - frameworksPath = rootDirectory + "/" + (frameworksDirectory || defaultFrameworks); - - CPLog.info("root=" + rootDirectory); - CPLog.info("output=" + outputDirectory); - CPLog.info("main=" + mainPath) - CPLog.info("frameworks=" + frameworksPath); - - // get Rhino context - var cx = Packages.org.mozilla.javascript.Context.getCurrentContext(), //Packages.org.mozilla.javascript.Context.enter(), - scope = makeObjjScope(cx); - - // set OBJJ_INCLUDE_PATHS to include the frameworks path - scope.OBJJ_INCLUDE_PATHS = [frameworksPath]; - CPLog.info("OBJJ_INCLUDE_PATHS="+scope.OBJJ_INCLUDE_PATHS); - - // phase 1: get global defines - var globals = findGlobalDefines(cx, scope, mainPath); - - // 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 (scope.objj_files[mainPath]) - { - 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.info("Included: " + path); - count++; - } - else - { - CPLog.warn("Excluded: " + path); - } - total++; - } - CPLog.error("Total required files: " + count + " out of " + total); - } - else - { - CPLog.error("Root file not loaded!"); - return; - } - - // phase 3: rebuild .sj files with correct imports, copy .j files - - var outputFiles = {}; - 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 (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 4: copy everything and write out the new files - - var rootDirectoryFile = new Packages.java.io.File(rootDirectory), - outputDirectoryFile = new Packages.java.io.File(outputDirectory); - - 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 }; -} - -main(); +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();