Skip to content

Commit

Permalink
Mostly there already
Browse files Browse the repository at this point in the history
  • Loading branch information
hh10k committed Jul 18, 2016
0 parents commit e7c5fbd
Show file tree
Hide file tree
Showing 5 changed files with 657 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules
152 changes: 152 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
'use strict';

const fs = require('fs');
const path = require('path');
const globby = require('globby');
const gutil = require('gulp-util');
const Transform = require('stream').Transform;

function verify(condition, message) {
if (!condition) {
throw new gutil.PluginError('gulp-prune', message);
}
}

function normalize(file) {
if (process.platform === 'win32') {
file = file.replace(/\\/g, '/');
}
return file;
}

// The mapping function converts source name to one or more destination paths.
function getMapper(options) {
if (options.map !== undefined) {
verify(typeof options.map === 'function', 'options.map must be a function');
verify(options.ext === undefined, 'options.map and options.ext are exclusive');
return options.map;
} else if (typeof options.ext === 'string') {
let mapExt = options.ext;
return (name) => name.replace(/(\.[^.]*)?$/, mapExt);
} else if (options.ext !== undefined) {
verify(options.ext instanceof Array && options.ext.every(e => typeof e === 'string'), 'options.ext must be a string or string[]');
let mapExtList = options.ext.slice();
return (name) => mapExtList.map(e => name.replace(/(\.[^.]*)?$/, e));
} else {
return (name) => name;
}
}

// The delete pattern is a minimatch pattern used to find files in the dest directory.
function getDeletePattern(options) {
if (typeof options.filter === 'string') {
return options.filter;
} else {
verify(options.filter === undefined || typeof options.filter === 'function',
'options.filter must be a string or function');
return '**/*';
}
}

// The delete filter is a function that selects what files to delete. `keep` will be populated later
// as it sees files in the stream.
function getDeleteFilter(options, keep) {
if (typeof options.filter === 'function') {
const filter = options.filter;
return (name) => !keep.hasOwnProperty(name) && filter(name);
} else {
return (name) => !keep.hasOwnProperty(name);
}
}

class PruneTransform extends Transform {

constructor(dest, options) {
super({ objectMode: true });

// Accept prune(dest, [options]), prune(options)
verify(arguments.length <= 2, 'too many arguments');
if (typeof dest === 'string') {
options = options || {};
verify(typeof options === 'object', 'options must be an object');
verify(options.dest === undefined, 'options.dest should not be specified with a dest argument');
} else {
verify(options === undefined, 'dest must be a string');
options = dest;
verify(typeof options === 'object', 'expected dest string or options object');
dest = options.dest;
verify(typeof dest === 'string', 'options.dest or dest argument must be string');
}

this._dest = path.resolve(dest);
this._kept = {};
this._mapper = getMapper(options);
this._pattern = getDeletePattern(options);
this._filter = getDeleteFilter(options, this._kept);

verify(options.verbose === undefined || typeof options.verbose === 'boolean', 'options.verbose must be a boolean');
this._verbose = !!options.verbose;
}

_transform(file, encoding, callback) {
Promise.resolve()
.then(() => {
const name = path.relative(file.base, file.path);
return this._mapper(name);
})
.then(mapped => {
switch (typeof mapped) {
case 'string':
this._kept[normalize(mapped)] = true;
break;
case 'object':
for (let i = 0; i < mapped.length; ++i) {
this._kept[normalize(mapped[i])] = true;
}
break;
default:
check(false, 'options.map function must return a string or string[], or a Promise that resolves to that.');
}
})
.then(() => callback(null, file), callback);
}

_flush(callback) {
globby(this._pattern, { cwd: this._dest, nodir: true })
.then(candidates => {
let deleting = candidates.filter(this._filter);
return Promise.all(deleting.map(f => {
let file = path.join(this._dest, f);
this._remove(file);
}));
})
.then(deleted => callback(), callback);
}

_remove(file) {
return new Promise((resolve, reject) => {
fs.unlink(file, (error) => {
try {
const fileRelative = path.relative('.', file);
if (error) {
if (this._verbose) {
gutil.log('Prune:', gutil.colors.red(`${fileRelative}: ${error.message || error}`));
}
reject(new Error(`${fileRelative}: ${error.message || error}`));
} else {
if (this._verbose) {
gutil.log('Prune:', gutil.colors.yellow(fileRelative));
}
resolve();
}
} catch (e) {
reject(e);
}
});
});
}
}

module.exports = function prune(dest, options) {
return new PruneTransform(dest, options);
}
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "gulp-prune",
"version": "0.1.0",
"description": "Delete extraneous files from target directory on stream flush",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"keywords": [
"gulpplugin",
"prune",
"files"
],
"author": {
"name": "Kurt Blackwell",
"url": "https://github.com/hh10k"
},
"license": "ISC",
"devDependencies": {
"mocha": "^2.5.3",
"mock-fs": "^3.11.0"
},
"dependencies": {
"globby": "^5.0.0",
"gulp-util": "^3.0.7"
}
}
108 changes: 108 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# gulp-prune

A [Gulp](http://gulpjs.com/) plugin to delete files that should not be in the destination directory.

Files that have not been seen will be deleted after the stream is flushed.

## Examples

### Prune with 1:1 mapping

All files in the target directory will be deleted unless they match the source name.

```js
var gulp = require('gulp');
var prune = require('gulp-prune');
var newer = require('gulp-newer');
var babel = require('gulp-babel');

gulp.task('build', () => {
return gulp.src('src/**/*.js')
.pipe(prune('build/'))
.pipe(newer('build/'))
.pipe(babel({ presets: ['es2015'] }))
.pipe(gulp.dest('build/'));
});
```

### Prune with custom mapping

If the source and destination files names are different then the mapping can be customised.

```js
var gulp = require('gulp');
var prune = require('gulp-prune');
var newer = require('gulp-newer');
var sourcemaps = require('gulp-sourcemaps');
var typescript = require('gulp-typescript');

gulp.task('build', () => {
return gulp.src('src/**/*.ts')
.pipe(prune({ dest: 'build/', ext: [ '.js', '.js.map' ] }))
.pipe(newer({ dest: 'build/', ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(typescript())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('build/'));
});
```

### Prune with restrictions

If multiple build tasks all output to the same directory, add a filter so that prune only deletes what that task is expected to output.

```js
var gulp = require('gulp');
var prune = require('gulp-prune');
var newer = require('gulp-newer');
var imagemin = require('gulp-imagemin');
var uglify = require('gulp-uglify');

gulp.task('build-images', () => {
return gulp.src('src/**/*@(.jpg|.png|.gif)')
.pipe(prune('build/', { filter: '**/*@(.jpg|.png|.gif)' }))
.pipe(newer('build/'))
.pipe(imagemin())
.pipe(gulp.dest('build/'));
});

gulp.task('build-sources', () => {
return gulp.src('src/*.js')
.pipe(prune('build/', { filter: '*.js' }))
.pipe(newer('build/'))
.pipe(uglify())
.pipe(gulp.dest('build/'));
});
```

## API

### Export

- `prune(dest)`
- `prune(dest, options)`
- `prune(options)`

### Options

- `options.dest` (or `dest` argument)

The directory to prune files from.

- `options.map`

A function that maps the source file name to what will be output to the `dest` directory. The function may return a string or array of string file names.

- `options.ext`

A convenience version of `options.map` to replace the file extension. May be a single string or an array of strings.

- `options.filter`

If a string, only files that match this [Minimatch](https://www.npmjs.com/package/minimatch) pattern may be pruned.

If a function, will be called for each file to be pruned. Return true to delete it.

- `options.verbose`

Set to true to log all deleted files.
Loading

0 comments on commit e7c5fbd

Please sign in to comment.