Skip to content

Commit

Permalink
Optional inputFiles comparator
Browse files Browse the repository at this point in the history
  • Loading branch information
codynguyen committed Jun 29, 2018
1 parent 23d7e90 commit 3bbc5f3
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 0 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,48 @@ The structure of `output.js` will be as follows:
// - footer
```

##### Ordering of inputFiles

Several orderings may be appropriate for `inputFiles`. Some are slightly more
expensive then others, and others are specific to certain scenarios.

By default files matched within `inputFiles` will be sorted lexicographically.
This is to ensure that between multiple concats, the output remain stable and
also as files are added and removed, that the output file change in predictable
(diff friendly) ways.

A common alternative is to sort these files based on the matcher precedent.

Given:

```
inputFiles: ['foo/*.global.css', 'foo/*.overrides.css']
```

One may want all .global.css files to precede .overrides.css files, while
within each glob preserving lexicographical order

To accomplish this, broccoli-concat provides an alternative comparator.

```js
var concat = require('broccoli-concat');
var matchComparator = require('broccoli-concat/comparators/match');

concat(input, {
outputFile: 'styles.css',
inputFiles: ['foo/*.global.css', 'foo/*.overrides.css'],
inputFilesComparator: matchComparator
})
```

If an alternative ordering strategy is required, the signature of inputFilesComparator is:

* firstFile: {String} fileName
* secondFile: {String} fileName
* matchers: {Array} matching the inputFiles argument, but where each entry has been converted into a minimatch instance

For an example see: [comparators/match](comparators/match)

#### Debug Usage

*note: this is intended for debugging purposes only, and will most likely negatively affect your build performace is left enabled*
Expand Down
22 changes: 22 additions & 0 deletions comparators/match.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

module.exports = function comparator(x, y, inputFileMatchers) {
if (x === y) { return 0; }
if (inputFileMatchers.length < 2) { return 0; }

for (var i = 0; i < inputFileMatchers.length; i++) {
var matcher = inputFileMatchers[i];

if (matcher.match(x) && matcher.match(y)) {
continue
} else if (matcher.match(x)) {
return -1;
} else if (matcher.match(y)) {
return 1;
} else {
return 1;
}
}

return 0;
}
14 changes: 14 additions & 0 deletions concat.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var omit = require('lodash.omit');
var uniq = require('lodash.uniq');
var walkSync = require('walk-sync');
var ensurePosix = require('ensure-posix-path');
var Minimatch = require('minimatch').Minimatch;

var ensureNoGlob = require('./lib/utils/ensure-no-glob');
var isDirectory = require('./lib/utils/is-directory');
Expand Down Expand Up @@ -47,6 +48,9 @@ class Concat extends Plugin {
this.sourceMapConfig = omit(options.sourceMapConfig || {}, 'enabled');
this.allInputFiles = uniq([].concat(options.headerFiles || [], options.inputFiles || [], options.footerFiles || []));
this.inputFiles = options.inputFiles;
this.inputFilesMatchers = (options.inputFiles || []).map(function(string) {
return new Minimatch(string);
});
this.outputFile = options.outputFile;
this.allowNone = options.allowNone;
this.header = options.header;
Expand All @@ -55,6 +59,7 @@ class Concat extends Plugin {
this.footer = options.footer;
this.footerFiles = options.footerFiles;
this.separator = (options.separator != null) ? options.separator : '\n';
this.inputFilesComparator = options.inputFilesComparator;

ensureNoGlob('headerFiles', this.headerFiles);
ensureNoGlob('footerFiles', this.footerFiles);
Expand Down Expand Up @@ -262,6 +267,15 @@ class Concat extends Plugin {
throw new Error('Concat: nothing matched [' + this.inputFiles + ']');
}

var inputFilesComparator = this.inputFilesComparator;
if (inputFilesComparator) {
var inputFilesMatchers = this.inputFilesMatchers;

files = files.slice().sort(function(x, y) {
return inputFilesComparator(x, y, inputFilesMatchers);
});
}

files.forEach(function(file) {
beginSection();
this.concat.addFile(file.replace(posixInputPath + '/', ''));
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"lodash.merge": "^4.3.0",
"lodash.omit": "^4.1.0",
"lodash.uniq": "^4.2.0",
"minimatch": "3.0.4",
"walk-sync": "^0.3.1"
},
"engines": {
Expand Down
124 changes: 124 additions & 0 deletions test/concat-ordering-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
'use strict';

var concat = require('..');
var fs = require('fs-extra');
var path = require('path');
var broccoli = require('broccoli');
var validateSourcemap = require('sourcemap-validator');
var Minimatch = require('minimatch').Minimatch;
var expectFile = require('./helpers/expect-file');

var chai = require('chai');
chai.config.truncateThreshold = 0;
var chaiAsPromised = require('chai-as-promised');

chai.use(chaiAsPromised);

var expect = chai.expect;

var fixtures = path.join(__dirname, 'fixtures');
var firstFixture = path.join(fixtures, 'first');

describe('sourcemap-concat', function() {
var builder;

afterEach(function() {
if (builder) {
return builder.cleanup();
}
});

describe('inputFiles optional postProcessing', function() {
var matchComparator = require('../comparators/match');

describe('inputFiles matcher matchComparator', function() {

it('scenario 1', function() {
var globs = [
new Minimatch('a*'),
new Minimatch('b*')
];

var files = ['zasdf', 'asdf', 'basdf'].sort(function(a, b) {
return matchComparator(a,b, globs);
});

expect(files).to.eql(['asdf', 'basdf', 'zasdf']);
});

it('scenario 2', function() {
var globs = [
new Minimatch('b*'),
new Minimatch('a*')
];

var files = ['zasdf', 'asdf', 'basdf'].sort(function(a, b) {
return matchComparator(a,b, globs);
});

expect(files).to.eql(['basdf', 'asdf', 'zasdf']);
});
});

it('response order first -> second', function() {
var firstMatcher = new Minimatch('**/first*');
var secondMatcher = new Minimatch('**/second*');

var node = concat(firstFixture, {
inputFiles: ['inner/**/*.js'],
outputFile: 'inner-first-second.js',
inputFilesComparator: function(x, y) {
return matchComparator(x,y, [firstMatcher, secondMatcher]);
}
});

builder = new broccoli.Builder(node);
return builder.build().then(function(result) {
expectFile('inner-first-second.js').in(result);
expectFile('inner-first-second.map').in(result);
expectValidSourcemap('inner-first-second.js').in(result);
});
});

it('response order second -> first', function() {
var firstMatcher = new Minimatch('**/first*');
var secondMatcher = new Minimatch('**/second*');

var node = concat(firstFixture, {
inputFiles: ['inner/**/*.js'],
outputFile: 'inner-second-first.js',
inputFilesComparator: function(x, y) {
return matchComparator(x, y, [secondMatcher, firstMatcher]);
}
});

builder = new broccoli.Builder(node);
return builder.build().then(function(result) {
expectFile('inner-second-first.js').in(result);
expectFile('inner-second-first.map').in(result);
expectValidSourcemap('inner-second-first.js').in(result);
});
});
});
});

function expectValidSourcemap(jsFilename, mapFilename) {
return {
in: function (result, subdir) {
if (!subdir) {
subdir = '.';
}

if (!mapFilename) {
mapFilename = jsFilename.replace(/\.js$/, '.map');
}

expectFile(jsFilename).in(result, subdir);
expectFile(mapFilename).in(result, subdir);

var actualMin = fs.readFileSync(path.join(result.directory, subdir, jsFilename), 'utf-8');
var actualMap = fs.readFileSync(path.join(result.directory, subdir, mapFilename), 'utf-8');
validateSourcemap(actualMin, actualMap, {});
}
};
}
12 changes: 12 additions & 0 deletions test/expected/inner-first-second.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/expected/inner-first-second.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions test/expected/inner-second-first.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/expected/inner-second-first.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3bbc5f3

Please sign in to comment.