diff --git a/Makefile.dryice.js b/Makefile.dryice.js new file mode 100644 index 00000000..bde697d0 --- /dev/null +++ b/Makefile.dryice.js @@ -0,0 +1,1087 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org Services B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +//var copy = require('dryice').copy; +var copy = internalRequire('dryice').copy; + +var aceHome = __dirname; + +var buildStep = copy.createDataObject(); + +// The Javascript files that don't need unwrapping +copy({ + source: [ 'demo/mini_require.js' ], + dest: buildStep +}); + +// JS files that we tweak the define lines on +copy({ + source: [ + { + root: aceHome + '/support/cockpit/support/pilot/lib', + include: /.*\.js$/, + exclude: /tests?\// + }, + { + root: aceHome + '/support/cockpit/lib', + include: /.*\.js$/, + exclude: /tests?\// + }, + { + root: aceHome + '/lib', + include: /.*\.js$/, + exclude: /tests?\// + } + ], + filter: [ copy.filter.moduleDefines ], + dest: buildStep +}); + +// The startup file +copy({ + source: { base: aceHome + '/demo/', path: 'demo_startup.js' }, + filter: [ copy.filter.moduleDefines ], + dest: buildStep +}); + +// The CSS files +copy({ + source: [ + { + root: aceHome + '/support/cockpit/support/pilot/lib', + include: /.*\.css$/, + exclude: /tests?\// + }, + { + root: aceHome + '/support/cockpit/lib', + include: /.*\.css$/, + exclude: /tests?\// + }, + { + root: aceHome + '/lib', + include: /.*\.css$/, + exclude: /tests?\// + } + ], + filter: [ copy.filter.addDefines ], + dest: buildStep +}); + +/* +// The graphics files +copy({ + source: sources.map(function(source) { + return { root: aceHome, include: source + '/.*\\.png$' }; + }, this), + filter: [ copy.filter.base64, copy.filter.addDefines ], + dest: buildStep +}); +*/ + +// The Javascript files that don't need unwrapping +copy({ + source: [ 'demo/boot.js' ], + dest: buildStep +}); + +// Create the compressed and uncompressed output files +/* +copy({ + source: buildStep, + filter: copy.filter.uglifyjs, + dest: 'build/ace.js' +}); +*/ +copy({ + source: buildStep, + dest: 'build/ace-uncompressed.js' +}); + + + +function internalRequire(ignore) { +var exports = {}; + + + +var fs = require("fs"); +var ujs = require("uglify-js"); + +/** + * Dryice is ant for javascript + * @param obj An object that contains source, dest and (optionally) filter objects + * source must be one of: + * - string: a file to read (later a directory too) + * - a data object like { value:OUTPUT_DATA } + * - a findObj like { root:DIR, include:RegExp|[RegExp], exclude:RegExp|[RegExp] } + * - a baseObj like { base:BASE, path:PATH } where BASE+PATH = filename + * (this method allows use of commonjs filters) + * - an array containing entries like the 3 above + * - a function which returns one of the 4 above + * dest must be either a string (pointing to a file) or a data object + */ +function copy(obj) { + // Gather a list of all the input sources + addSource(obj, obj.source); + + // Concatenate all the input sources + var value = ''; + obj.sources.forEach(function(source) { + value += source.value; + }, this); + + // Run filters where onRead=false + value = runFilters(value, obj.filter, false); + + // Output + // TODO: for now we're ignoring the concept of directory destinations. + if (typeof obj.dest.value === 'string') { + obj.dest.value += value; + } + else if (typeof obj.dest === 'string') { + fs.writeFileSync(obj.dest, value); + } + else { + throw new Error('Can\'t handle type of dest: ' + typeof obj.dest); + } +} + +function addName(currentName, newName) { + return currentName === null ? currentName : newName; +} + +function addSource(obj, source) { + if (!obj.sources) { + obj.sources = []; + } + + if (typeof source === 'function') { + addSource(obj, source()); + } + else if (Array.isArray(source)) { + source.forEach(function(s) { + addSource(obj, s); + }, this); + } + else if (source.root) { + copy.findFiles(obj, source); + } + else if (source.base) { + addSourceBase(obj, source); + } + else if (typeof source === 'string') { + addSourceFile(obj, source); + } + else if (typeof source.value === 'string') { + if (!source.filtered) { + source.value = runFilters(source.value, obj.filter, true, source.name); + source.filtered = true; + } + obj.sources.push(source); + } + else { + throw new Error('Can\'t handle type of source: ' + typeof source); + } +} + +function addSourceFile(obj, filename) { + var read = fs.readFileSync(filename).toString(); + obj.sources.push({ + name: filename, + value: runFilters(read, obj.filter, true, filename) + }); +} + +function addSourceBase(obj, baseObj) { + var read = fs.readFileSync(baseObj.base + baseObj.path).toString(); + obj.sources.push({ + name: baseObj, + value: runFilters(read, obj.filter, true, baseObj) + }); +} + +function runFilters(value, filter, reading, name) { + if (!filter) { + return value; + } + + if (Array.isArray(filter)) { + filter.forEach(function(f) { + value = runFilters(value, f, reading, name); + }, this); + return value; + } + + if (filter.onRead == reading) { + return filter(value, name); + } + else { + return value; + } +} + +/** + * A holder is an in-memory store of a result of a copy operation. + *
+ * var holder = copy.createDataObject();
+ * copy({ source: 'x.txt', dest: holder });
+ * copy({ source: 'y.txt', dest: holder });
+ * copy({ source: holder, dest: 'z.txt' });
+ * 
+ */ +copy.createDataObject = function() { + return { value: '' }; +}; + +/** + * An object that contains include and exclude object + */ +copy.findFiles = function(obj, findObj) { + if (!findObj.filter) { + findObj.filter = createFilterFromRegex(findObj); + } + if (!findObj.path) { + findObj.path = ''; + } + + if (findObj.root.length > 0 && findObj.root.substr(-1) !== '/') { + findObj.root += '/'; + } + var path = findObj.path; + if (path.length > 0 && path.substr(-1) !== '/') { + path += '/'; + } + + fs.readdirSync(findObj.root + findObj.path).forEach(function(entry) { + var stat = fs.statSync(findObj.root + path + entry); + if (stat.isFile()) { + if (findObj.filter(path + entry)) { + addSourceBase(obj, { + base: findObj.root, + path: path + entry + }); + } + } + else if (stat.isDirectory()) { + findObj.path = path + entry; + copy.findFiles(obj, findObj); + } + }, this); +}; + +function createFilterFromRegex(obj) { + return function(path) { + function noPathMatch(pattern) { + return !pattern.test(path); + } + if (obj.include instanceof RegExp) { + if (noPathMatch(obj.include)) { + return false; + } + } + if (typeof obj.include === 'string') { + if (noPathMatch(new RegExp(obj.include))) { + return false; + } + } + if (Array.isArray(obj.include)) { + if (obj.include.every(noPathMatch)) { + return false; + } + } + + function pathMatch(pattern) { + return pattern.test(path); + } + if (obj.exclude instanceof RegExp) { + if (pathMatch(obj.exclude)) { + return false; + } + } + if (typeof obj.exclude === 'string') { + if (pathMatch(new RegExp(obj.exclude))) { + return false; + } + } + if (Array.isArray(obj.exclude)) { + if (obj.exclude.some(pathMatch)) { + return false; + } + } + + return true; + }; +} + +/** + * File filters + */ +copy.filter = {}; + +/** + * Compress the given input code using UglifyJS. + * + * @param string input + * @return string output + */ +copy.filter.uglifyjs = function(input) { + var opt = copy.filter.uglifyjs.options; + var ast = ujs.parser.parse(input, opt.parse_strict_semicolons); + + if (opt.mangle) { + ast = ujs.uglify.ast_mangle(ast, opt.mangle_toplevel); + } + + if (opt.squeeze) { + ast = ujs.uglify.ast_squeeze(ast, opt.squeeze_options); + if (opt.squeeze_more) { + ast = ujs.uglify.ast_squeeze_more(ast); + } + } + + return ujs.uglify.gen_code(ast, opt.beautify); +}; +copy.filter.uglifyjs.onRead = false; +/** + * UglifyJS filter options. + */ +copy.filter.uglifyjs.options = { + parse_strict_semicolons: false, + + /** + * The beautify argument used for process.gen_code(). See the UglifyJS + * documentation. + */ + beautify: false, + mangle: true, + mangle_toplevel: false, + squeeze: true, + + /** + * The options argument used for process.ast_squeeze(). See the UglifyJS + * documentation. + */ + squeeze_options: {}, + + /** + * Tells if you want to perform potentially unsafe compression. + */ + squeeze_more: false +}; + +/** + * A filter to munge CommonJS headers + */ +copy.filter.addDefines = function(input, source) { + if (!source) { + throw new Error('Missing filename for moduleDefines'); + } + + if (source.base) { + source = source.path; + } + + var module = source.replace(/\.css$/, ''); + + input = input.replace(/"/g, '\\"'); + input = '"' + input.replace(/\n/g, '" +\n "') + '"'; + + return 'define("text!' + source.toString() + '", ' + input + ');\n\n'; +}; +copy.filter.addDefines.onRead = true; + +/** + * + */ +copy.filter.base64 = function() { + +}; +copy.filter.base64.onRead = true; + +/** + * + */ +copy.filter.moduleDefines = function(input, source) { + if (!source) { + throw new Error('Missing filename for moduleDefines'); + } + + if (source.base) { + source = source.path; + } + + var module = source.replace(/\.js$/, ''); + + return input.replace(/\bdefine\(\s*function\(require,\s*exports,\s*module\)\s*\{/, + "define('" + module + "', function(require, exports, module) {"); +}; +copy.filter.moduleDefines.onRead = true; + + +exports.copy = copy; + + + + + + + + + + + + + +/* +// copy a single file +copy({ source: 'foo.txt', dest: 'bar.txt' }); + +// copy a directory +copy({ source: 'foo', dest: 'bar' }); + +// cat a bunch of files together +copy({ source: [ 'file1.js', 'file2.js' ], dest: 'output.js' }); + +// cat files together using a glob pattern +copy({ source: /.js$/, dest: 'built.js' }); + +// cat together a custom set of files +copy({ + source: function() { + var files = [ 'file1.js' ]; + if (baz) files.push('file2.js'); + return files; + }, + dest: 'built.js' +}); + +// some utils are available to custom functions +copy({ + source: function() { + var files = filesMatching(/.js$/); + arrayRemove(files, 'broken.js'); + return files; + }, + dest: 'built.js' +}); + +// maybe one day we could add multiple destinations +copy({ + source: /src/.*\.js$/, + dest: [ /\(*\)\.js$/, '\1.js.bak' ] +}); + +// use a filter while copying +copy({ + source: /src/.*\.js$/, + filter: compressor, + dest: 'built.js' +}); + +// and you can have multiple (custom) filters +copy({ + source: /src/.*\.js$/, + filter: [ + compressor, + function(name, data) { + return 'wibble'; + } + ], + dest: 'built.js' +}); + +// I think it's possible to see a huge array of operations in these terms. +// I'm not suggesting you implement these, just demoing what the API could do +copy({ + source: '**', + filter: [ zip ], + dest: 'scp://example.com/upload/myproject-0.1.zip' +}); + +copy({ + source: /src/.*\.java$/, + filter: javac, + dest: [ /\(*\)\.java$/, '\1.class' ], + comment: 'only joking' +}); +*/ + +return exports; +}; + + + + + +var sys = require("sys"); +var fs = require("fs"); +var path = require("path"); +var util = require("util"); +var Step = require("step"); + + +function copy0(options, callback) { + var source = options.source; + var data = ""; + + if (typeof source == "function") { + source = source(options); + } + + if (typeof source == "string") { + source = [source]; + } else if (typeof options.source == "object") { + data = source.value || ""; + } + + var filters = options.filter || []; + if (typeof filters == "function") { + filters = [filters]; + } + + if (typeof dest == "string") { + var stat = fs.statSync(); + } + + // read the files + if (Array.isArray(source)) { + source.forEach(function(file) { + var stat = fs.statSync(file); + var result = fs.readFileSync(file); + + // execute the filters + filters.forEach(function(filter) { + filter(input, file); + }); + + data += result + "\n"; + }); + } + + // save the output + var dest = options.dest; + if (typeof dest == "string") { + fs.writeFileSync(dest, data); + } else { + dest.value = data; + } +} + + +var Builder = function Builder(appFolder) { + this.appFolder = appFolder; +}; + +Builder.prototype = { + DEBUG: false, + + /** + * Web application folder - the location where scripts, styles and resources + * are fetched from. + */ + appFolder: ".", + + /** + * Target build folder - the location where the packaged web application is + * saved to. + */ + buildFolder: "./build", + + /** + * The default folder and file modes (permissions for chmod). + */ + folderMode: 0755, + fileMode: 0644, + + /** + * Array holding regular expression patterns. When you add entire folders to + * your build you might want to skip certain files. + */ + ignoreFiles: [], + + log: sys.puts, + + debug: function(message) { + if (this.DEBUG) { + this.log(message); + } + }, + + /** + * Sets and creates the target build folder, asynchronously. + * + * @param string folder + * Tells the target folder where you want to save the packaged web + * application. + * + * @param function callback + * The callback you want executed after the folder is created. + * Callback arguments: + * string|Exception|Error error - Holds the error that occurred + * while trying to set the build folder. This is undefined|null + * when the operation was successful. + * + * Builder build - holds a reference to the build instance. + */ + setBuildFolder: function Builder_setBuildFolder(newFolder, callback) + { + var self = this; + + Step( + function checkFolder() { + path.exists(newFolder, this); + }, + + function makeFolder(exists) { + if (!exists) { + fs.mkdir(newFolder, self.folderMode, this); + } else { + return true; + } + }, + + function folderReady(err) { + if (!err) { + self.buildFolder = newFolder; + } + callback(err, self); + } + ); + }, + + /** + * Copy files from the web application folder to the target build folder. + * + * @param object|array|string aFiles + * Holds the list of files you want to copy to the target build + * folder. If this is a string, you copy only one file. If the + * argument is given as an object, keys are considered source files + * and values are considered target files - this allows you to copy + * files to different locations in the build folder, without + * maintaining the same structure as in the original folder. + * @param function callback + * The callback you want executed after the files are copied. The + * callback arguments are the same as for setBuildFolder(). + * @returns void + */ + copyFiles: function Builder_copyFiles(aFiles, callback) + { + var files = {}; + if (typeof aFiles == "string") { + files[aFiles] = aFiles; + } else if (Array.isArray(aFiles)) { + aFiles.forEach(function(file) { + files[file] = file; + }); + } else { + files = aFiles; + } + + var self = this; + + Step( + function copyFiles() { + var group = this.group(); + for (var file in files) { + fs_copyFile(self.appFolder + "/" + file, + self.buildFolder + "/" + files[file], + group()); + } + }, + + function filesReady(err) { + callback(err, self); + } + ); + }, + + /** + * Copy folders from the web application folder to the target build folder. + * + * @param object|array|string aFolders + * Holds the list of folders you want to copy to the target build + * folder. If this is a string, you copy only one folder. If the + * argument is given as an object, keys are considered source folders + * and values are considered target folders - this allows you to copy + * folders to different locations in the build folder, without + * maintaining the same structure as in the original folder. + * @param function callback + * The callback you want executed after the folders are copied. The + * callback arguments are the same as for setBuildFolder(). + * @param boolean [recursive=true] + * (Optional) True if you want to copy subfolders as well, false if + * you want to copy only the files contained in the folders given. + * Default is true. + * @returns void + */ + copyFolders: function Builder_copyFolders(aFolders, callback, recursive) + { + var folders = {}; + if (typeof aFolders == "string") { + folders[aFolders] = aFolders; + } else if (Array.isArray(aFolders)) { + folders.forEach(function(folder) { + folders[folder] = folder; + }); + } else { + folders = aFolders; + } + if (typeof recursive == "undefined") { + recursive = true; + } + + var self = this; + + Step( + function copyFolders() { + var group = this.group(); + for (var folder in folders) { + self.copyFolder(folder, folders[folder], group(), recursive); + } + }, + function foldersReady(err) { + callback(err, self); + } + ); + }, + + /** + * Copy one folder from the web application folder to the target build + * folder. + * + * @param string source + * The folder you want to copy. + * @param string [destination=source] + * The destination folder. By default this is the same as the source, + * but in the buildFolder. You can change the destination, relative + * to the buildFolder. + * @param function callback + * The callback you want executed after the folder is copied. The + * callback arguments are the same as for setBuildFolder(). + * @param boolean [recursive=true] + * (Optional) True if you want to copy the subfolders as well, false + * if you want to copy only the files contained in the given source + * folder. Default is true. + * @returns void + */ + copyFolder: function Builder_copyFolder() + { + var source, destination, recursive, callback; + switch (arguments.length) { + case 2: // source, callback + source = destination = arguments[0]; + callback = arguments[1]; + recursive = true; + break; + case 3: + // source, callback, recursive + // OR source, destination, callback + source = arguments[0]; + if (typeof arguments[1] == "function") { + destination = source; + callback = arguments[1]; + recursive = arguments[2]; + } else { + destination = arguments[1]; + callback = arguments[2]; + recursive = true; + } + break; + case 4: // source, destination, callback, recursive + source = arguments[0]; + destination = arguments[1]; + callback = arguments[2]; + recursive = arguments[3]; + break; + default: + throw new Error("Invalid arguments."); + } + + var self = this; + + // result from fs.readdir(), without . and .. + var readdir_files = []; + + var files = []; + var subfolders = []; + + Step( + function checkDestination() { + path.exists(self.buildFolder + "/" + destination, this); + }, + + function mkdir_destination(exists) { + if (!exists) { + fs.mkdir(self.buildFolder + "/" + destination, this); + } else { + return true; + } + }, + + function readdir_source(err) { + if (err) { + throw err; + } + + // find the files in the given source folder. + fs.readdir(self.appFolder + "/" + source, this); + }, + + function statFiles(err, result) { + if (err) { + throw err; + } + + var group = this.group(); + result.forEach(function(file) { + if (file != "." && file != ".." && + !self.shouldSkipFile("/" + source + "/" + file)) { + fs.stat(file, group()); + readdir_files.push(file); + } + }); + }, + + function copyFiles(err, stats) { + if (err) { + throw err; + } + + // separate files out from folders + stats.forEach(function(stat, index) { + if (stat.isDirectory()) { + subfolders.push(readdir_files[index]); + } else if (stat.isFile()) { + files.push(readdir_files[index]); + } + }); + + // copy the files + var group = this.group(); + files.forEach(function(file) { + fs_copyFile(self.appFolder + "/" + source + "/" + file, + self.buildFolder + "/" + destination + "/" + file, + group()); + }); + }, + + function copySubfolders(err) { + if (err) { + throw err; + } + + if (!recursive) { + return true; + } + + var group = this.group(); + subfolders.forEach(function(folder) { + self.copyFolder(source + "/" + folder, + destination + "/" + folder, + group(), true); + }); + }, + + function copyDone(err) { + callback(err, self); + } + ); + }, + + /** + * Tells if this builder should ignore a file, given the file name. The + * this.ignoreFiles array of regular expression patterns is used. + * + * @param string filename + * @returns boolean + * True if the file should be ignored, or false otherwise. + */ + shouldSkipFile: function Builder_shouldSkipFile(filename) + { + return this.ignoreFiles.some(function(pattern) { + return pattern.test(filename); + }); + } +}; + +Builder.executeManifest = function Builder_executeManifest(manifest, callback) { + return; // stub +}; + +var Script = function Script(build, filename) { + if (!build) { + throw new Error("The first argument must reference a dryice.Builder instance."); + } else if (!filename) { + throw new Error("The second argument must be the script file name."); + } + + this.build = build; + this.filename = filename; +}; + +Script.prototype = { + inputEncoding: "utf8", + outputEncoding: "utf8", + + /** + * List of functions or filter names that process each input file. + */ + inputFilters: [], + + /** + * List of functions or filter names that process the concatenated output + * file. + */ + outputFilters: [], + + /** + * Add files to the compiled script. + * + * @param array|string files + * @param function callback + * @returns void + */ + addFiles: function Builder_addFiles(files, callback) { + if (typeof files == "string") { + files = [files]; + } + + var self = this; + + Step( + function readFiles() { + var group = this.group(); + + files.forEach(function(file) { + fs.readFile(self.build.appFolder + "/" + file, + self.inputEncoding, group()); + }); + }, + + function filterFiles(err, data) { + if (err) { + throw err; + } + + for (var i = 0; i < data.length; i++) { + data[i] = self.executeFilters(self.inputFilters, data[i], + files[i]); + } + + self.output += data.join("\n"); + + return self; + }, + + callback + ); + }, + + run: function Builder_run(buildCallback) { + var self = this; + + Step( + function readFiles() { + var group = this.group(); + + self.input_files.forEach(function(file) { + fs.readFile(self.basedir + "/" + file, + self.input_encoding, group()); + }); + }, + + function filterInput(err, files) { + if (err) { + throw err; + } + + for (var i = 0; i < files.length; i++) { + files[i] = self.run_filters(self.input_filters, files[i], + self.input_files[i]); + } + + return files; + }, + + function postProcessOutput(err, output) { + if (err) { + throw err; + } + + output = output.join("\n"); + output = self.run_filters(self.output_filters, output); + + if (typeof self.output_file == "string") { + fs.writeFile(self.basedir + "/" + self.output_file, + output, self.output_encoding, this); + } else { + self.output_file.write(output); + self.output_file.end(); + return 1; + } + }, + + function buildComplete(err) { + buildCallback(err, self); + } + ); + }, + + /** + * Given an array of filters (functions or filter names), invoke these + * filters on the given input content. + * + * @param array filters + * @param string input + * @param string [filename] + * Optional filename, useful for some filters. + * @return string + * Final filtered output. + */ + executeFilters: function(filters, input, filename) { + filters.forEach(function(filter) { + if (typeof filter == "string") { + input = input_filters[filter].call(this, input, filename); + } else { + input = filter.call(this, input, filename); + } + }, this); + return input; + } +}; + + diff --git a/demo/mini_require.js b/demo/mini_require.js new file mode 100644 index 00000000..150cdf8e --- /dev/null +++ b/demo/mini_require.js @@ -0,0 +1,49 @@ + +function require(module, callback) { + + if (Array.isArray(module)) { + var params = []; + module.forEach(function(m) { + params.push(require.modules[m]); + }, this); + + if (callback) { + callback.apply(null, params); + } + } + + if (typeof module === 'string') { + var payload = require.modules[module]; + if (payload == null) { + console.error('Missing module: ' + module); + } + + if (typeof payload === 'function') { + var exports = {}; + var module = { + id: '', + uri: '' + }; + payload(require, exports, module); + payload = exports; + } + + if (callback) { + callback(); + } + + return payload; + } +} +require.modules = {}; + +function define(module, payload) { + if (typeof module !== 'string') { + console.error('dropping module because define wasn\'t munged.'); + console.trace(); + return; + } + + console.log('defining module: ' + module + ' as a ' + typeof payload); + require.modules[module] = payload; +}