diff --git a/index.js b/index.js index 97c1803571..91ff493f50 100755 --- a/index.js +++ b/index.js @@ -112,7 +112,7 @@ program logger.info(`Reload for file add: ${filePath}`); Promise.resolve('').then(() => { if (fsUtil.isSourceFile(filePath)) { - return site.buildSourceFiles(); + return site.rebuildAffectedSourceFiles(filePath); } return site.buildAsset(filePath); }).catch((err) => { @@ -124,7 +124,7 @@ program logger.info(`Reload for file change: ${filePath}`); Promise.resolve('').then(() => { if (fsUtil.isSourceFile(filePath)) { - return site.rebuildSourceFiles(filePath); + return site.rebuildAffectedSourceFiles(filePath); } return site.buildAsset(filePath); }).catch((err) => { @@ -136,7 +136,7 @@ program logger.info(`Reload for file deletion: ${filePath}`); Promise.resolve('').then(() => { if (fsUtil.isSourceFile(filePath)) { - return site.rebuildSourceFiles(filePath); + return site.rebuildAffectedSourceFiles(filePath); } return site.removeAsset(filePath); }).catch((err) => { @@ -144,7 +144,7 @@ program }); }; - // server conifg + // server config const serverConfig = { open: options.open, logLevel: 0, @@ -163,7 +163,11 @@ program }) .then(() => { const watcher = chokidar.watch(rootFolder, { - ignored: [outputFolder, /(^|[/\\])\../], + ignored: [ + outputFolder, + /(^|[/\\])\../, + x => x.endsWith('___jb_tmp___'), x => x.endsWith('___jb_old___'), // IDE temp files + ], ignoreInitial: true, }); watcher diff --git a/lib/Page.js b/lib/Page.js index 815e06a844..e178d2c125 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -116,6 +116,7 @@ Page.prototype.generate = function (builtFiles) { this.collectIncludedFiles(markbinder.getDynamicIncludeSrc()); this.collectIncludedFiles(markbinder.getStaticIncludeSrc()); this.collectIncludedFiles(markbinder.getBoilerplateIncludeSrc()); + this.collectIncludedFiles(markbinder.getMissingIncludeSrc()); }) .then(resolve) .catch(reject); @@ -194,6 +195,7 @@ Page.prototype.resolveDependency = function (dependency, builtFiles) { this.collectIncludedFiles(markbinder.getDynamicIncludeSrc()); this.collectIncludedFiles(markbinder.getStaticIncludeSrc()); this.collectIncludedFiles(markbinder.getBoilerplateIncludeSrc()); + this.collectIncludedFiles(markbinder.getMissingIncludeSrc()); }) .then(resolve) .catch(reject); diff --git a/lib/Site.js b/lib/Site.js index 1add398ee9..1ad5450458 100644 --- a/lib/Site.js +++ b/lib/Site.js @@ -1,4 +1,7 @@ +/* eslint-disable no-underscore-dangle */ + const cheerio = require('cheerio'); +const delay = require('./util/delay'); const path = require('path'); const ignore = require('ignore'); const ejs = require('ejs'); @@ -7,6 +10,7 @@ const walkSync = require('walk-sync'); const Promise = require('bluebird'); const ghpages = require('gh-pages'); const logger = require('./util/logger'); +const _ = require('lodash'); const Page = require('./Page'); @@ -259,6 +263,7 @@ Site.prototype.buildSourceFiles = function () { return new Promise((resolve, reject) => { this.generatePages() .then(() => fs.removeAsync(this.tempPath)) + .then(() => logger.info('Pages built')) .then(resolve) .catch((error) => { // if error, remove the site and temp folders @@ -267,13 +272,10 @@ Site.prototype.buildSourceFiles = function () { }); }; -/** - * Rebuild pages that are affected by change in filePath - * @param filePath path of file changed - */ -Site.prototype.rebuildSourceFiles = function (filePath) { +Site.prototype._rebuildAffectedSourceFiles = function (filePaths) { + const uniquePaths = _.uniq(filePaths); return new Promise((resolve, reject) => { - this.regenerateAffectedPages(filePath) + this.regenerateAffectedPages(uniquePaths) .then(() => fs.removeAsync(this.tempPath)) .then(resolve) .catch((error) => { @@ -283,37 +285,48 @@ Site.prototype.rebuildSourceFiles = function (filePath) { }); }; -Site.prototype.buildAsset = function (filePath) { - return new Promise((resolve, reject) => { - // if the file is an ignored file, resolve - // Else, copy it to its destination - const ignoreConfig = this.siteConfig.ignore || []; - const fileRelative = path.relative(this.rootPath, filePath); - const fileIgnore = ignore().add(ignoreConfig); - if (fileIgnore.filter([fileRelative]).length === 0) { - resolve(); - } else { - fs.copyAsync(filePath, path.join(this.outputPath, fileRelative)) - .then(resolve) - .catch((error) => { - rejectHandler(reject, error, []); // assets won't affect deletion - }); - } - }); +/** + * Rebuild pages that are affected by changes in filePaths + * @param filePaths a single path or an array of paths corresponding to the files that have changed + */ +Site.prototype.rebuildAffectedSourceFiles + = delay(Site.prototype._rebuildAffectedSourceFiles, 1000); + +Site.prototype._buildMultipleAssets = function (filePaths) { + const uniquePaths = _.uniq(filePaths); + const ignoreConfig = this.siteConfig.ignore || []; + const fileIgnore = ignore().add(ignoreConfig); + const fileRelativePaths = uniquePaths.map(filePath => path.relative(this.rootPath, filePath)); + const copyAssets = fileIgnore.filter(fileRelativePaths) + .map(asset => fs.copyAsync(path.join(this.rootPath, asset), path.join(this.outputPath, asset))); + return Promise.all(copyAssets) + .then(() => logger.info('Assets built')); }; -Site.prototype.removeAsset = function (filePath) { - return new Promise((resolve, reject) => { - const fileRelative = path.relative(this.rootPath, filePath); - const fileToRemove = path.join(this.outputPath, fileRelative); - fs.removeAsync(fileToRemove) - .then(resolve) - .catch((error) => { - rejectHandler(reject, error, []); // assets won't affect deletion - }); - }); +/** + * Build/copy assets that are specified in filePaths + * @param filePaths a single path or an array of paths corresponding to the assets to build + */ +Site.prototype.buildAsset + = delay(Site.prototype._buildMultipleAssets, 1000); + +Site.prototype._removeMultipleAssets = function (filePaths) { + const uniquePaths = _.uniq(filePaths); + const fileRelativePaths = uniquePaths.map(filePath => path.relative(this.rootPath, filePath)); + const filesToRemove = fileRelativePaths.map( + fileRelativePath => path.join(this.outputPath, fileRelativePath)); + const removeFiles = filesToRemove.map(asset => fs.removeAsync(asset)); + return Promise.all(removeFiles) + .then(() => logger.info('Assets removed')); }; +/** + * Remove assets that are specified in filePaths + * @param filePaths a single path or an array of paths corresponding to the assets to remove + */ +Site.prototype.removeAsset + = delay(Site.prototype._removeMultipleAssets, 1000); + Site.prototype.buildAssets = function () { return new Promise((resolve, reject) => { const ignoreConfig = this.siteConfig.ignore || []; @@ -362,13 +375,13 @@ Site.prototype.generatePages = function () { /** * Re-renders pages that contain the original file path * as the source file or as a static/dynamic included file - * @param filePath path of file changed + * @param filePaths array of paths corresponding to files that have changed */ -Site.prototype.regenerateAffectedPages = function (filePath) { +Site.prototype.regenerateAffectedPages = function (filePaths) { const builtFiles = {}; const processingFiles = []; this.pageModels.forEach((page) => { - if (page.includedFiles[filePath]) { + if (filePaths.some(filePath => page.includedFiles[filePath])) { processingFiles.push(page.generate(builtFiles)); } }); diff --git a/lib/util/delay.js b/lib/util/delay.js new file mode 100644 index 0000000000..4b6c476761 --- /dev/null +++ b/lib/util/delay.js @@ -0,0 +1,38 @@ +const Promise = require('bluebird'); + +/** +* Creates a function that delays invoking `func` until after `wait` milliseconds have elapsed +* and the running `func` has resolved/rejected. +* @param func the promise-returning function to delay, +* func should take in a single array +* @param wait the number of milliseconds to delay +* @returns delayedFunc that takes in a single argument (either an array or a single value) +*/ +module.exports = function delay(func, wait) { + let context; + let pendingArgs = []; + let waitingPromise = null; + let runningPromise = Promise.resolve(); + + return function (arg) { + context = this; + if (Array.isArray(arg)) { + pendingArgs = pendingArgs.concat(arg); + } else { + pendingArgs.push(arg); + } + + if (waitingPromise === null) { + waitingPromise = Promise.all([Promise.delay(wait), runningPromise]) + .finally(() => { + runningPromise = waitingPromise || Promise.resolve(); + waitingPromise = null; + const funcPromise = func.apply(context, [pendingArgs]); + pendingArgs = []; + return funcPromise; + }); + } + + return waitingPromise; + }; +}; diff --git a/package.json b/package.json index 357c58f194..b45d6d48f1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "ignore": "^3.2.0", "js-beautify": "^1.6.12", "live-server": "^1.2.0", + "lodash": "^4.17.5", "markbind": "^1.3.0", "nunjucks": "^3.0.0", "path-is-inside": "^1.0.2",