diff --git a/package.json b/package.json index 9b95109..a799733 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,9 @@ "bluebird": "^3.4.7", "chalk": "^1.1.3", "chokidar": "^1.6.1", - "fs-extra": "^1.0.0", + "fs-extra": "^2.1.2", "glob-all": "^3.1.0", + "graceful-fs": "^4.1.11", "yargs": "^6.3.0" }, "devDependencies": { diff --git a/src/bin/sync-glob.js b/src/bin/sync-glob.js index 8e115bb..1b966bc 100644 --- a/src/bin/sync-glob.js +++ b/src/bin/sync-glob.js @@ -31,6 +31,10 @@ const argv = yargs.usage('Usage: $0 ') .alias('v', 'verbose') .default('verbose', false) .describe('verbose', 'Moar output') + .boolean('debug') + .alias('b', 'debug') + .default('debug', false) + .describe('debug', 'Log essential information for debugging') .version() .help('help') .showHelpOnFail(false, 'Specify --help for available options') @@ -67,6 +71,7 @@ const close = syncGlob(sources, target, { watch: argv.watch, delete: argv.delete, depth: argv.depth || Infinity, + debug: argv.debug, transform: argv.transform, }, (event, data) => { const priority = notifyPriority[event] || 'low' diff --git a/src/index.js b/src/index.js index 3013ed0..a006116 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,12 @@ /* globals process */ import fs from 'fs' +import gracefulFs from 'graceful-fs' + +gracefulFs.gracefulify(fs) +/* eslint-disable import/first */ import path from 'path' +import util from 'util' import globAll from 'glob-all' import chokidar from 'chokidar' import Promise, { promisify } from 'bluebird' @@ -18,6 +23,7 @@ const defaults = { watch: false, delete: true, depth: Infinity, + debug: false, } /** @@ -29,6 +35,7 @@ const defaults = { * @param {bool} [options.watch=false] - Enable or disable watch mode. * @param {bool} [options.delete=true] - Whether to delete the `target`'s content initially. * @param {bool} [options.depth=Infinity] - Chokidars `depth` (If set, limits how many levels of subdirectories will be traversed). + * @param {bool} [options.debug=false] - Log essential information for debugging. * @param {string} [options.transform=false] - A module path resolved by node's `require`. * @param {NotifyCallback} [notify] - An optional notification callback. * @returns {CloseFunc} - Returns a close function which cancels active promises and watch mode. @@ -41,6 +48,9 @@ const syncGlob = (sources, target, options = {}, notify = () => {}) => { } // eslint-disable-next-line no-param-reassign sources = sources.map(trimQuotes) + const originalTarget = target + // eslint-disable-next-line no-param-reassign + target = path.normalize(target) if (typeof options === 'function') { // eslint-disable-next-line no-param-reassign @@ -58,7 +68,7 @@ const syncGlob = (sources, target, options = {}, notify = () => {}) => { const notifyError = (err) => { notify('error', err) } const bases = sourcesBases(sources) const resolveTargetFromBases = resolveTarget(bases) - const { depth, watch } = options + const { depth, watch, debug } = options let { transform } = options if (typeof depth !== 'number' || isNaN(depth)) { @@ -86,10 +96,19 @@ const syncGlob = (sources, target, options = {}, notify = () => {}) => { } // Initial mirror + const initSources = sources.map(source => (isGlob(source) === -1 + && fs.statSync(path.normalize(source)).isDirectory() ? `${source}${source.slice(-1) === '/' ? '' : '/'}**` : source)) const mirrorInit = [ - promisify(globAll)(sources.map(source => (isGlob(source) === -1 - && fs.statSync(source).isDirectory() ? `${source}/**` : source))) - .then(files => files.map(file => path.normalize(file))), + promisify(globAll)(initSources) + .then(files => files.map(file => path.normalize(file))) + .then((files) => { + if (debug) { + console.log(`sources: ${sources} -> ${initSources}`) + console.log(`target: ${originalTarget} -> ${target}`) + console.log(`globed files: \n\t${files.join('\n\t')}`) + } + return files + }), ] if (options.delete) { @@ -160,17 +179,24 @@ const syncGlob = (sources, target, options = {}, notify = () => {}) => { // Watcher to keep in sync from that if (watch) { watcher = chokidar.watch(sources, { + cwd: process.cwd(), persistent: true, depth, ignoreInitial: true, awaitWriteFinish: true, + usePolling: true, + useFsEvents: true, }) watcher.on('ready', notify.bind(undefined, 'watch', sources)) - .on('all', (event, source) => { + .on('all', (event, source, stats) => { const resolvedTarget = resolveTargetFromBases(source, target) let promise + if (debug) { + console.log(`ALL: ${event} -> ${source} ${stats ? `\t\n${util.inspect(stats)}` : ''}`) + } + switch (event) { case 'add': case 'change': @@ -218,6 +244,12 @@ const syncGlob = (sources, target, options = {}, notify = () => {}) => { }) .on('error', notifyError) + if (debug) { + watcher.on('raw', (event, rpath, details) => { + console.log(`RAW: ${event} -> ${rpath} \t\n${util.inspect(details)}`) + }) + } + process.on('SIGINT', close) process.on('SIGQUIT', close) process.on('SIGTERM', close) diff --git a/test/copy.spec.js b/test/copy.spec.js index 74fcc09..38097ff 100644 --- a/test/copy.spec.js +++ b/test/copy.spec.js @@ -1,5 +1,5 @@ import syncGlob from '../src/index' -import { beforeEachSpec, afterAllSpecs, awaitCount, awaitMatch, compare, compareDir, noop } from './helpers' +import { beforeEachSpec, afterAllSpecs, fs, awaitCount, awaitMatch, compare, compareDir, noop } from './helpers' describe('node-sync-glob copy', () => { beforeEach(beforeEachSpec) @@ -111,4 +111,24 @@ describe('node-sync-glob copy', () => { 'mirror', compareDir(done, 'tmp/mock', 'tmp/copy') )) }) + + it('should copy empty sub directories', (done) => { + fs.ensureDirSync('tmp/mock/bar/empty') + + const close = syncGlob('tmp/mock/**/*', 'tmp/copy', awaitMatch( + 'error', (err) => { + fail(err) + close() + done() + }, + 'mirror', () => { + expect(fs.existsSync('tmp/copy/bar/empty')).toBe(true) + + compareDir(null, 'tmp/mock', 'tmp/copy') + + close() + done() + } + )) + }) }) diff --git a/test/helpers.js b/test/helpers.js index 233f94b..5b79474 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -10,6 +10,7 @@ export const fs = { appendFileSync: (source, ...args) => fsExtra.appendFileSync(path.normalize(source), ...args), existsSync: source => fsExtra.existsSync(path.normalize(source)), readFileSync: source => fsExtra.readFileSync(path.normalize(source)), + ensureDirSync: source => fsExtra.ensureDirSync(path.normalize(source)), } export const beforeEachSpec = () => { @@ -106,6 +107,28 @@ export const awaitMatch = (...args) => { } } +const logDirDiffSet = (source, target, res) => { + if (!res.same) { + const logs = res.diffSet.map((entry) => { + const state = { + equal: '==', + left: '->', + right: '<-', + distinct: '<>', + }[entry.state] + const name1 = entry.name1 ? entry.name1 : '' + const name2 = entry.name2 ? entry.name2 : '' + const path1 = entry.path1 ? entry.path1 : '' + const path2 = entry.path2 ? entry.path2 : '' + const { type1, type2 } = entry + + return `${path1}/${name1} ${type1} ${state} ${path2}/${name2} ${type2}` + }) + + console.log(`${source} -> ${target}\n\t${logs.join('\n\t')}`) + } +} + export const compare = (done, source, target, options) => (event, data) => { if (event) { if (Array.isArray(data) && data.length === 2 @@ -120,6 +143,8 @@ export const compare = (done, source, target, options) => (event, data) => { compareContent: true, }) + logDirDiffSet(source, target, res) + expect(res.differences).toBe(0) expect(res.differencesFiles).toBe(0) expect(res.distinctFiles).toBe(0) @@ -140,6 +165,8 @@ export const compareDir = (done, source, target, options = {}) => (event) => { compareContent: true, }) + logDirDiffSet(source, target, res) + expect(res.differences).toBe(0) expect(res.differencesFiles).toBe(0) expect(res.distinctFiles).toBe(0) diff --git a/test/mock/emptyFile b/test/mock/emptyFile new file mode 100644 index 0000000..e69de29 diff --git a/test/sync.spec.js b/test/sync.spec.js index f056f4f..ec79354 100644 --- a/test/sync.spec.js +++ b/test/sync.spec.js @@ -51,7 +51,7 @@ describe('node-sync-glob watch', () => { }) it('should sync a directory', (done) => { - const close = syncGlob('tmp/mock/foo', 'tmp/sync/', { watch }, awaitMatch( + const close = syncGlob('tmp/mock/foo', 'tmp/sync', { watch }, awaitMatch( 'error', (err) => { fail(err) close() @@ -93,4 +93,54 @@ describe('node-sync-glob watch', () => { } )) }) + + it('should sync empty sub directory deletion', (done) => { + try { + console.log(`EXISTS: tmp/mock/bar -> ${fs.existsSync('tmp/mock/bar')}`) + fs.ensureDirSync('tmp/mock/bar/empty') + + const close = syncGlob('tmp/mock/**/*', 'tmp/sync', { watch, debug: true }, awaitMatch( + 'error', (err) => { + fail(err) + close() + done() + }, + ['mirror', 'watch'], compareDir(() => { + fs.removeSync('tmp/mock/bar/empty') + }, 'tmp/mock', 'tmp/sync'), + 'remove', () => { + expect(fs.existsSync('tmp/sync/foo/b.txt')).toBe(true) + expect(fs.existsSync('tmp/sync/bar/empty')).toBe(false) + + close() + done() + } + )) + } catch (err) { + console.log(err) + + fail(err) + done() + } + }) + + it('should sync empty file deletion', (done) => { + const close = syncGlob('tmp/mock/**/*', 'tmp/sync', { watch, debug: true }, awaitMatch( + 'error', (err) => { + fail(err) + close() + done() + }, + ['mirror', 'watch'], compareDir(() => { + fs.removeSync('tmp/mock/emptyFile') + }, 'tmp/mock', 'tmp/sync'), + 'remove', () => { + expect(fs.existsSync('tmp/sync/foo/b.txt')).toBe(true) + expect(fs.existsSync('tmp/sync/emptyFile')).toBe(false) + + close() + done() + } + )) + }) })