diff --git a/tools/batch-build-plugin.js b/tools/batch-build-plugin.js index 7e8f8f96a2..c7b69549d9 100644 --- a/tools/batch-build-plugin.js +++ b/tools/batch-build-plugin.js @@ -1,25 +1,132 @@ var archinfo = require('./archinfo.js'); +var colonConverter = require('./colon-converter.js'); var compiler = require('./compiler.js'); var files = require('./files.js'); var _ = require('underscore'); -exports.BatchBuildHandlerFactory = function (options, factory) { +// XXX BBP define Plugin.PHASE_LINKER etc +exports.DEFAULT_PHASE = 200; + +exports.BatchBuildHandlerFactory = function (options, factoryFunction) { var self = this; self.id = options.id; + self.phase = options.phase; self.extensions = options.extensions.slice(); - self.archMatching = !! options.archMatching; + self.archMatching = options.archMatching; self.isTemplate = !! options.isTemplate; + self.factoryFunction = factoryFunction; +}; +_.extend(exports.BatchBuildHandlerFactory.prototype, { + createHandler: function () { + var self = this; + // XXX BBP proper error handling --- this is running user-supplied plugin + // code + var batchHandler = self.factoryFunction(); + return new BatchBuildHandler(self, batchHandler); + } +}); + +var BatchBuildHandler = function (factory, batchHandler) { + var self = this; + // The actual object returned from the user-supplied factory. + self.batchHandler = batchHandler; self.factory = factory; }; +_.extend(BatchBuildHandler.prototype, { + // XXX BBP full docs + // files is an array of {packageSourceBatch, fileIndex} + run: function (sourceSlots) { + var self = this; -exports.DEFAULT_PHASE = 200; + var inputFiles = _.map(sourceSlots, function (sourceSlot) { + // Register this file as being processed, so it gets replaced by its + // output at the end of the phase. + sourceSlot.activate(); + return new InputFile(sourceSlot); + }); + + // XXX BBP proper error handling --- this is running user-supplied plugin + // code + self.batchHandler.processFilesForTarget(inputFiles); + } +}); + +// This is the object presented to the user's plugin code. +// XXX BBP actually design its API +// XXX BBP decide if the API always presents / to the code (it probably +// should because you're not supposed to do your own IO anyway) +var InputFile = function (sourceSlot) { + var self = this; + // We use underscored attributes here because this is user-visible code and we + // don't want users to be accessing anything that we don't document. + self._sourceSlot = sourceSlot; +}; +_.extend(InputFile.prototype, { + // XXX BBP we should have a better API + xxxContentsAsBuffer: function () { + var self = this; + return self._sourceSlot.inputSource.data; + }, + xxxPathInPackage: function () { + var self = this; + return self._sourceSlot.inputSource.path; + }, + xxxBasename: function () { + var self = this; + return files.pathBasename(self.xxxPathInPackage()); + }, + xxxDirname: function () { + var self = this; + return files.pathDirname(self.xxxPathInPackage()); + }, + // XXX is this null for app? + xxxPackageName: function () { + var self = this; + return self._sourceSlot.packageSourceBatch.unibuild.pkg.name; + }, + xxxOutput: function (sourceInfo) { + var self = this; + // XXX BBP validate input!! + self._sourceSlot.outputSource(sourceInfo); + } +}); + +// XXX BBBP doc +var SourceSlot = function (unibuildSourceInfo, packageSourceBatch) { + var self = this; + self.inputSource = unibuildSourceInfo; // XXX BBP prototype? + // When we activate this sourceSlot, this becomes a list. Then at the end of + // the phase, we replace this object with a new SourceSlot for each output + // source, if any. If this SourceSlot is not activated in a given phase, it + // remains to the next phase. + self.outputSources = null; + self.packageSourceBatch = packageSourceBatch; +}; +_.extend(SourceSlot.prototype, { + activate: function () { + var self = this; + if (self.outputSources) + throw Error("activate twice?"); + self.outputSources = []; + }, + outputSource: function (source) { + var self = this; + if (! self.outputSources) + throw Error("outputSource on unactivated SourceSlot?"); + + // XXX BBP prototype? + self.outputSources.push(source); + } +}); // XXX BBP ??? var PackageSourceBatch = function (unibuild, processor) { var self = this; self.processor = processor; // XXX BBP maybe give these objects a prototype - self.orderedSources = _.clone(unibuild.sources); + self.sourceSlots = _.map(unibuild.sources, function (source) { + return new SourceSlot(source, self); + }); // Remember the unibuild too, so that we can pull out legacy resources // (js/css/etc that were placed into an Isopack by an // registerSourceHandler-style build plugin) later. @@ -67,6 +174,16 @@ _.extend(PackageSourceBatch.prototype, { } var phaseHandlers = activeHandlers[phase]; _.each(factories, function (factory) { + // Ignore factories which aren't for this arch (eg, ignore CSS + // handlers on the server). + // XXX BBP actually this is wrong, we should DROP these sources + // (otherwise they last until the end and cause errors) + // (maybe this should just be handled at getSources time) + if (factory.archMatching && + ! archinfo.matches(self.processor.arch, factory.archMatching)) { + return; + } + _.each(factory.extensions, function (ext) { if (_.has(phaseHandlers, ext)) { // XXX BBP use buildmessage @@ -86,7 +203,7 @@ _.extend(PackageSourceBatch.prototype, { }, // XXX BBP doc? rename? - eachFileInPhase: function (phase, f) { + eachSourceSlotWithPhaseFactoryId: function (phase, f) { var self = this; // No handlers in this phase! Done. @@ -95,17 +212,71 @@ _.extend(PackageSourceBatch.prototype, { } // ext -> factoryId var phaseHandlers = self.activeHandlers[phase]; - _.each(self.orderedSources, function (source, fileIndex) { - var parts = files.pathBasename(source.path).split('.'); + _.each(self.sourceSlots, function (sourceSlot) { + var parts = files.pathBasename(sourceSlot.inputSource.path).split('.'); for (var i = 1; i < parts.length; i++) { var extension = parts.slice(i).join('.'); if (_.has(phaseHandlers, extension)) { - f(fileIndex, phaseHandlers[extension]); + f(sourceSlot, phaseHandlers[extension]); break; } } // No handlers at this phase for this file! That's OK. }); + }, + + replaceSourceSlotsWithOutput: function () { + var self = this; + var newSourceSlots = []; + _.each(self.sourceSlots, function (sourceSlot) { + // If this source file wasn't processed in this phase, keep it around + // until the next phase. + if (! sourceSlot.outputSources) { + newSourceSlots.push(sourceSlot); + return; + } + // Otherwise, replace it with SourceSlots for the sources that it output + // (if any). + _.each(sourceSlot.outputSources, function (outputSource) { + newSourceSlots.push(new SourceSlot(outputSource, self)); + }); + }); + + self.sourceSlots = newSourceSlots; + }, + + getResources: function () { + var self = this; + // Start with legacy resources. + var resources = self.unibuild.getResources(self.processor.arch, { + isopackCache: self.processor.isopackCache + }); + // XXX BBP this is wrong (eg totally broken for in app) + var serveRoot; + if (self.unibuild.pkg.name) { + serveRoot = files.pathJoin('/packages/', self.unibuild.pkg.name); + } else { + serveRoot = '/'; + } + // Now add resources from the batch plugins. + _.each(self.sourceSlots, function (sourceSlot) { + var source = sourceSlot.inputSource; + // XXX BBP make less hacky + if (! source.path.match(/\.css$/)) { + throw Error("we only know how to output CSS! " + source.path); + } + resources.push({ + type: "css", + refreshable: true, + data: source.data, + servePath: colonConverter.convert( + files.pathJoin( + serveRoot, + // XXX BBP should we decide in our API that everything is / ? + files.convertToStandardPath(source.path, true))) + }); + }); + return resources; } }); @@ -114,6 +285,7 @@ _.extend(PackageSourceBatch.prototype, { exports.BatchBuildProcessor = function (options) { var self = this; self.unibuilds = options.unibuilds; + self.arch = options.arch; self.isopackCache = options.isopackCache; // id -> { factory, handler, phase } self.handlers = null; @@ -130,12 +302,7 @@ _.extend(exports.BatchBuildProcessor.prototype, { if (_.has(self.handlers, factory.id)) { throw Error("duplicate handler factory ID! " + factory.id); } - self.handlers[factory.id] = { - factory: factory, - phase: phase, - // XXX BBP better error if this throws? - handler: factory.factory() - }; + self.handlers[factory.id] = factory.createHandler(); }); }); }); @@ -147,8 +314,8 @@ _.extend(exports.BatchBuildProcessor.prototype, { throw Error("call _loadPluginsAndCreateHandlers first"); var phaseSet = {}; - _.each(self.handlers, function (handlerInfo) { - phaseSet[handlerInfo.phase] = true; + _.each(self.handlers, function (handler) { + phaseSet[handler.factory.phase] = true; }); return _.map(phaseSet, function (unused, phaseStr) { return +phaseStr; @@ -166,21 +333,37 @@ _.extend(exports.BatchBuildProcessor.prototype, { var phases = self._getSortedPhases(self.unibuilds); _.each(phases, function (phase) { - // id -> [{ packageSourceBatch, fileIndex }] + // For each phase, figure out which files go with which handlers. + // These should be disjoint sets (which don't necessarily include + // every file in the target). + // id -> [SourceSlot] var handlersToRun = {}; _.each(sourceBatches, function (sourceBatch) { - sourceBatch.eachFileInPhase(phase, function (fileIndex, factoryId) { + sourceBatch.eachSourceSlotWithPhaseFactoryId(phase, function (sourceSlot, factoryId) { if (! _.has(handlersToRun, factoryId)) { handlersToRun[factoryId] = []; } - handlersToRun[factoryId].push({ - packageSourceBatch: sourceBatch, - fileIndex: fileIndex - }); + handlersToRun[factoryId].push(sourceSlot); }); }); - console.log(phase, handlersToRun); + + // Now actually run the handlers. + _.each(handlersToRun, function (sourceSlots, factoryId) { + var handler = self.handlers[factoryId]; + if (! handler) + throw Error("handler not created?"); + + handler.run(sourceSlots); + }); + + // Now that we've run all the handlers, replace all source files with + // their output. + _.each(sourceBatches, function (sourceBatch) { + sourceBatch.replaceSourceSlotsWithOutput(); + }); }); + + return sourceBatches; } }); diff --git a/tools/bundler.js b/tools/bundler.js index 1223bd9f6f..ad4ec37575 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -425,6 +425,8 @@ var Target = function (options) { // All of the Unibuilds that are to go into this target, in the order // that they are to be loaded. self.unibuilds = []; + // XXX BBP ??? + self.sourceBatches = null; // JavaScript files. List of File. They will be loaded at startup in // the order given. @@ -664,9 +666,10 @@ _.extend(Target.prototype, { var self = this; var processor = new batchBuildPlugin.BatchBuildProcessor({ unibuilds: self.unibuilds, + arch: self.arch, isopackCache: self.isopackCache }); - processor.runBatchHandlers(); + self.sourceBatches = processor.runBatchHandlers(); }, // Process all of the sorted unibuilds (which includes running the JavaScript @@ -678,7 +681,9 @@ _.extend(Target.prototype, { var isOs = archinfo.matches(self.arch, "os"); // Copy their resources into the bundle in order - _.each(self.unibuilds, function (unibuild) { + _.each(self.sourceBatches, function (sourceBatch) { + var unibuild = sourceBatch.unibuild; + if (self.cordovaDependencies) { _.each(unibuild.pkg.cordovaDependencies, function (version, name) { self._addCordovaDependency( @@ -693,9 +698,7 @@ _.extend(Target.prototype, { var isApp = ! unibuild.pkg.name; // Emit the resources - var resources = unibuild.getResources(self.arch, { - isopackCache: self.isopackCache - }); + var resources = sourceBatch.getResources(); // First, find all the assets, so that we can associate them with each js // resource (for os unibuilds). diff --git a/tools/isopack.js b/tools/isopack.js index f7ea220598..cf54fa96a3 100644 --- a/tools/isopack.js +++ b/tools/isopack.js @@ -671,6 +671,7 @@ _.extend(Isopack.prototype, { phaseHandlers.push( new batchBuildPlugin.BatchBuildHandlerFactory({ id: handlerId, + phase: phase, extensions: options.extensions, archMatching: options.archMatching, isTemplate: options.isTemplate