Files
cappuccino/CommonJS/bin/flatten
T
Martin Carlberg 6094d25cfb New: Support to compile deep structured project in the web browser
A Cappuccino project has always needed to have a flat file structure to
be able to compile from source in the web browser.

A new dictionary can be added to the project's Info.plist with the key
'CPFileTranslationDictionary'. It should contain all the path
translations in the project to allow the Browser to find out where in
the file structure the source files are. Check in the AppKit framework
for an example.

Also, a include list can be added in the Info.plist for the key
'CPCompileIncludeFileArray'. The file in the list will be included
by a '#include' statement for each file that is compiled in this project

A last new feature is to add macros in the 'OBJJ_COMPILER_FLAGS' global
variable. It is usually declared in the index.html file. It is done
with the format '-Dmacroname[=macrodefinition]'.

An updated Info.plist file in the AppKit framework will allow the AppKit
to compile from the source in the web browser. The following things is
needed to get this to work:

1. Replace the AppKit folder in your project's Frameworks folder with a
copy of the AppKit folder from the Cappuccino project.
2. Add a copy of the built Aristo2.blend folder to the AppKit's Resource
folder.
3. Add "-DPLATFORM_DOM", "-DPLATFORM_BROWSER" and optionally "-DDEBUG"
to the global variable 'OBJJ_COMPILER_FLAGS' in the project's index.html
file.

The app load time will increase (about 3 seconds on a 2018 MacBook Pro)
as the AppKit framework is compiled from source.

The Foundation framework is not yet tested but it might be working by
adding info about the file structure in the Info.plist file.
2020-12-21 11:14:36 +01:00

369 lines
13 KiB
Plaintext
Executable 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.StaticResource._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, " "));
}