/**1* Copyright 2013 Facebook, Inc.2*3* Licensed under the Apache License, Version 2.0 (the "License");4* you may not use this file except in compliance with the License.5* You may obtain a copy of the License at6*7* http://www.apache.org/licenses/LICENSE-2.08*9* Unless required by applicable law or agreed to in writing, software10* distributed under the License is distributed on an "AS IS" BASIS,11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12* See the License for the specific language governing permissions and13* limitations under the License.14*/15var EventEmitter = require('events').EventEmitter;16var inherits = require('util').inherits;17var loaders = require('./loaders');18var fs = require('fs');1920var MapUpdateTask = require('./MapUpdateTask');21var ResourceMap = require('./ResourceMap');22var ResourceMapSerializer = require('./ResourceMapSerializer');23var FileFinder = require('./FileFinder');2425var ProjectConfigurationLoader = loaders.ProjectConfigurationLoader;2627/*28*29* ____________________________30* .OOo31* OOOOL32* __________________ .. JOOO?33* .eSSSSSS**'34* gSSSSSSSSSS 6.35* oo SSSSSSSs .G36* ___________ oo SSSSSSSSSgG37* *ISSSSSS **38* SSSSSSS39* .oYSSSSSY.40* OOOOOOOOOOOg41* .OOOOT**TOOOOO.42* ______________________ .OOOO' 'OOOOO.43* .OOOO' OOOO'44* .oOOOO* .OOOO*45* .OOOOO'' .OOOOO46* .JOOOO* .##OOO'47* C##?/ T##I48* Y#| 'V49* Node Haste ________________________ C50*51*/5253/**54* @class Haste. A nice facade to node-haste system55*56* Running a node haste update task is a pretty complicated matter. You have57* to manually create a FileFinder, a MapUpdateTask, a ResourceMapSerializer.58* Haste class automates all of this providing a task oriented API with a59* broad set of configuration options. The only 2 required parameters are60* loaders and scanDirs61*62* @extends {EventEmitter}63*64* @example65* var Haste = require('node-haste/Haste');66* var loaders = require('node-haste/loaders');67*68* var haste = new Haste(69* [70* new loaders.JSLoader({ networkSize: true }),71* new loaders.CSSLoader({ networkSize: true }),72* new ProjectConfigurationLoader(),73* new ResourceLoader()74* ],75* ['html']);76*77* haste.update('.cache', function(map) {78* assert(map instanceof ResourceMap);79* });80*81*82* @param {Array.<Loader>} loaders Preconfigured Loader instances83* @param {Array.<String>} scanDirs84* @param {FileFinder|null} options.finder Custom finder instance85* @param {ResourceMapSerializer|null} options.serializer Custom serializer86* @param {Number|null} options.maxOpenFiles Maximum number of loaders87* MapUpdateTask can use88* @param {Number|null} options.maxProcesses Maximum number of loader forks89* MapUpdateTask can use90* @param {Boolean|null} options.useNativeFind Whether to use native shell91* find command (faster) or node92* implementation (safer)93* @param {function|null} options.ignorePaths Function to reject paths94* @param {String|null} options.version Version of the cache. If95* the version mismatches the96* cached on, cache will be97* ignored98*99*/100function Haste(loaders, scanDirs, options) {101EventEmitter.call(this);102103this.loaders = loaders;104this.scanDirs = scanDirs;105this.options = options || {};106this.finder = this.options.finder || null;107this.serializer = this.options.serializer || null;108}109inherits(Haste, EventEmitter);110111/**112* All in one function:113* 1) load cache if exists114* 2) compare to the existing files115* 3) analyze changes,116* 4) update map,117* 5) write cache back to disk118* 6) return map119*120* @param {String} path121* @param {Function} callback122*/123Haste.prototype.update = function(path, callback, options) {124var map, files;125var me = this;126127var run = function() {128if (!map || !files) {129return;130}131var task = me.createUpdateTask(files, map).on('complete', function(map) {132// only store map if it's changed133var mapChanged = task.changed.length > task.skipped.length;134if (mapChanged) {135me.storeMap(path, map, function() {136me.emit('mapStored');137callback(map, task.messages);138});139} else {140callback(map, task.messages);141}142}).run();143}144145this.getFinder().find(function(f) {146files = f;147me.emit('found', files);148run();149});150151if (options && options.forceRescan) {152map = new ResourceMap();153} else {154this.loadOrCreateMap(path, function(m) {155map = m;156me.emit('mapLoaded');157run();158});159}160};161162/**163* Same as update but will also rerun the update every time something changes164*165* TODO: (voloko) add support for inotify and FSEvent instead of constantly166* running finder167*168* @param {String} path169* @param {Function} callback170* @param {Number} options.timeout How often to rerun finder171* @param {Boolean} options.forceRescan172*/173Haste.prototype.watch = function(path, callback, options) {174var timeout = options && options.timeout || 1000;175var finder = this.getFinder();176var map, files, task;177var me = this;178var firstRun = true;179180function find() {181finder.find(function(f) {182files = f;183if (map) {184update();185}186});187}188189function updated(m) {190map = m;191var mapChanged = task.changed.length > task.skipped.length;192// if changed, store the map and only then callback and schedule next run193if (mapChanged) {194me.storeMap(path, map, function() {195callback(map, task.changed, task.messages);196setTimeout(find, timeout);197});198return;199}200201// callback on the first run even if the map is unchanged202if (firstRun) {203firstRun = false;204callback(map, task.changed, task.messages);205}206setTimeout(find, timeout);207}208209function update() {210task = me.createUpdateTask(files, map).on('complete', updated).run();211}212213if (options && options.forceRescan) {214map = new ResourceMap();215} else {216this.loadOrCreateMap(path, function(m) {217map = m;218if (files) {219update();220}221});222}223224find();225};226227/**228* Updates a map using the configuration options from constructor229* @param {ResourceMap} map230* @param {Function} callback231*/232Haste.prototype.updateMap = function(map, callback) {233this.getFinder().find(function(files) {234this.createUpdateTask(files, map).on('complete', callback).run();235}.bind(this));236};237238/**239* Loads map from a file240* @param {String} path241* @param {Function} callback242*/243Haste.prototype.loadMap = function(path, callback) {244this.getSerializer().loadFromPath(path, callback);245};246247/**248* @param {String} path249* @return {ResourceMap|null}250*/251Haste.prototype.loadMapSync = function(path) {252return this.getSerializer().loadFromPathSync(path);253};254255/**256* Loads map from a file or creates one if cache is not available257* @param {String} path258* @param {Function} callback259*/260Haste.prototype.loadOrCreateMap = function(path, callback) {261this.getSerializer().loadFromPath(path, function(err, map) {262callback(map || new ResourceMap());263});264};265266/**267* @param {String} path268* @return {ResourceMap}269*/270Haste.prototype.loadOrCreateMapSync = function(path) {271return this.loadMapSync(path) || new ResourceMap();272};273274/**275* Stores the map cache276* @param {String} path277* @param {ResourceMap} map278* @param {Function} callback279*/280Haste.prototype.storeMap = function(path, map, callback) {281this.getSerializer().storeToPath(path, map, callback);282};283284285286/**287* @protected288* @param {ResourceMap} map289* @return {MapUpdateTask}290*/291Haste.prototype.createUpdateTask = function(files, map) {292var task = new MapUpdateTask(293files,294this.loaders,295map,296{297maxOpenFiles: this.options.maxOpenFiles,298maxProcesses: this.options.maxProcesses299});300301var events =302['found', 'changed', 'analyzed', 'mapUpdated', 'postProcessed', 'complete'];303var me = this;304events.forEach(function(name) {305task.on(name, function(value) {306me.emit(name, value);307});308});309return task;310};311312/**313* @protected314* @return {FileFinder}315*/316Haste.prototype.getFinder = function() {317if (!this.finder) {318var ext = {};319this.loaders.forEach(function(loader) {320loader.getExtensions().forEach(function(e) {321ext[e] = true;322});323});324this.finder = new FileFinder({325scanDirs: this.scanDirs,326extensions: Object.keys(ext),327useNative: this.options.useNativeFind,328ignore: this.options.ignorePaths329});330}331return this.finder;332};333334/**335* @protected336* @return {ResourceMapSerializer}337*/338Haste.prototype.getSerializer = function() {339return this.serializer || new ResourceMapSerializer(340this.loaders,341{ version: this.options.version });342};343344module.exports = Haste;345346347