Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions tools/broccoli/diffing-broccoli-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import {TreeDiffer, DiffResult} from './tree-differ';
import {IncludeFilterFn, TreeDiffer, DiffResult} from './tree-differ';
let symlinkOrCopy = require('symlink-or-copy');


Expand Down Expand Up @@ -91,10 +91,17 @@ class DiffingPluginWrapper implements BroccoliTree {

private init() {
if (!this.initialized) {
let includeExtensions = this.pluginClass.includeExtensions || [];
let excludeExtensions = this.pluginClass.excludeExtensions || [];
let includeExtensions: string[] = this.pluginClass.includeExtensions || [];
let excludeExtensions: string[] = this.pluginClass.excludeExtensions || [];
let includeFilter: IncludeFilterFn = null;
let includeFilterMethod = this.pluginClass.includeFilterMethod;
if (includeFilterMethod) {
includeFilter = (filePath: string) => this.wrappedPlugin[includeFilterMethod](filePath);
}

this.initialized = true;
this.treeDiffer = new TreeDiffer(this.inputPath, includeExtensions, excludeExtensions);
this.treeDiffer =
new TreeDiffer(this.inputPath, {includeExtensions, excludeExtensions, includeFilter});
this.wrappedPlugin =
new this.pluginClass(this.inputPath, this.cachePath, this.wrappedPluginArguments[1]);
}
Expand Down
67 changes: 65 additions & 2 deletions tools/broccoli/tree-differ.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ describe('TreeDiffer', () => {
};
mockfs(testDir);

let differ = new TreeDiffer('dir1', ['.js', '.coffee']);
let differ = new TreeDiffer('dir1', {includeExtensions: ['.js', '.coffee']});

let diffResult = differ.diffTree();

Expand Down Expand Up @@ -236,7 +236,8 @@ describe('TreeDiffer', () => {
};
mockfs(testDir);

let differ = new TreeDiffer('dir1', ['.ts', '.cs'], ['.d.ts', '.d.cs']);
let differ = new TreeDiffer(
'dir1', {includeExtensions: ['.ts', '.cs'], excludeExtensions: ['.d.ts', '.d.cs']});

let diffResult = differ.diffTree();

Expand Down Expand Up @@ -266,6 +267,68 @@ describe('TreeDiffer', () => {
diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual([]);
});


it('should ignore files for which the includeFilter callback returns false', () => {
let testDir = {
'dir1': {
'file-1.ts': mockfs.file({content: 'file-1.ts content', mtime: new Date(1000)}),
'file-1.cs': mockfs.file({content: 'file-1.cs content', mtime: new Date(1000)}),
'file-1d.cs': mockfs.file({content: 'file-1d.cs content', mtime: new Date(1000)}),
'file-1.d.cs': mockfs.file({content: 'file-1.d.cs content', mtime: new Date(1000)}),
'file-2.md': mockfs.file({content: 'file-2.md content', mtime: new Date(1000)}),
'file-3.ts': mockfs.file({content: 'file-3.ts content', mtime: new Date(1000)}),
'file-4.d.ts': mockfs.file({content: 'file-4.d.ts content', mtime: new Date(1000)}),
'subdir-1': {
'file-1.1.cc': mockfs.file({content: 'file-1.1.cc content', mtime: new Date(1000)}),
'file-1.2.ts': mockfs.file({content: 'file-1.2.ts content', mtime: new Date(1000)}),
'file-1.3.cs': mockfs.file({content: 'file-1.3.cs content', mtime: new Date(1000)})
}
}
};
mockfs(testDir);
let log: string[] = [];
let differ = new TreeDiffer('dir1', {
includeExtensions: ['.ts', '.cs'],
excludeExtensions: ['.d.ts', '.d.cs'],
includeFilter(filePath: string) {
log.push(filePath);
return filePath.split('/').indexOf('subdir-1') > -1;
}
});

let diffResult = differ.diffTree();

expect(diffResult.changedPaths).toEqual(['subdir-1/file-1.2.ts', 'subdir-1/file-1.3.cs']);
expect(log).toEqual([
'file-1.cs',
'file-1.ts',
'file-1d.cs',
'file-3.ts',
'subdir-1/file-1.2.ts',
'subdir-1/file-1.3.cs'
]);
log.length = 0;

// Don't re-test includeFilter callback
testDir['dir1']['subdir-1']['file-1.2.ts'] =
mockfs.file({content: 'changed content', mtime: new Date(1200)});
mockfs(testDir);
diffResult = differ.diffTree();

expect(diffResult.changedPaths).toEqual(['subdir-1/file-1.2.ts']);
expect(log).toEqual([]);

// Test newly added files
testDir['dir1']['subdir-1']['subdir-2'] = {
'file-2.1.ts': mockfs.file({content: 'new file', mtime: new Date(1400)})
};
mockfs(testDir);
diffResult = differ.diffTree();

expect(diffResult.changedPaths).toEqual(['subdir-1/subdir-2/file-2.1.ts']);
expect(log).toEqual(['subdir-1/subdir-2/file-2.1.ts']);
});
});

describe('diff of new files', () => {
Expand Down
49 changes: 41 additions & 8 deletions tools/broccoli/tree-differ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,43 @@ function tryStatSync(path) {
}
}

export type IncludeFilterFn = (filePath: string) => boolean

interface TreeDifferOptions {
// Simple operations for determining which files are processed
includeExtensions?: string[];
excludeExtensions?: string[];

// More advanced methods to determine if a file must be processed
includeFilter?: IncludeFilterFn;
}


export class TreeDiffer {
private fingerprints: {[key: string]: string} = Object.create(null);
private nextFingerprints: {[key: string]: string} = Object.create(null);
private includedFileCache: {[key: string]: boolean} = Object.create(null);
private rootDirName: string;
private include: RegExp = null;
private exclude: RegExp = null;
private includeFilter: IncludeFilterFn = null;

constructor(private rootPath: string, includeExtensions?: string[],
excludeExtensions?: string[]) {
constructor(private rootPath: string, options?: TreeDifferOptions) {
this.rootDirName = path.basename(rootPath);

let buildRegexp = (arr) => new RegExp(`(${arr.reduce(combine, "")})$`, "i");

this.include = (includeExtensions || []).length ? buildRegexp(includeExtensions) : null;
this.exclude = (excludeExtensions || []).length ? buildRegexp(excludeExtensions) : null;
if (options) {
if (options.includeExtensions && options.includeExtensions.length) {
this.include = buildRegexp(options.includeExtensions);
}

if (options.excludeExtensions && options.excludeExtensions.length) {
this.exclude = buildRegexp(options.excludeExtensions);
}

this.includeFilter = options.includeFilter || null;
}

function combine(prev, curr) {
if (curr.charAt(0) !== ".") throw new TypeError("Extension must begin with '.'");
Expand Down Expand Up @@ -61,8 +82,7 @@ export class TreeDiffer {
result.directoriesChecked++;
this.dirtyCheckPath(absolutePath, result);
} else {
if (!(this.include && !absolutePath.match(this.include)) &&
!(this.exclude && absolutePath.match(this.exclude))) {
if (this.shouldProcessFile(absolutePath)) {
result.filesChecked++;
if (this.isFileDirty(absolutePath, pathStat)) {
result.changedPaths.push(path.relative(this.rootPath, absolutePath));
Expand Down Expand Up @@ -96,8 +116,7 @@ export class TreeDiffer {

private detectDeletionsAndUpdateFingerprints(result: DiffResult) {
for (let absolutePath in this.fingerprints) {
if (!(this.include && !absolutePath.match(this.include)) &&
!(this.exclude && absolutePath.match(this.exclude))) {
if (this.shouldProcessFile(absolutePath)) {
if (this.fingerprints[absolutePath] !== null) {
let relativePath = path.relative(this.rootPath, absolutePath);
result.removedPaths.push(relativePath);
Expand All @@ -108,6 +127,20 @@ export class TreeDiffer {
this.fingerprints = this.nextFingerprints;
this.nextFingerprints = Object.create(null);
}


private shouldProcessFile(absolutePath: string) {
let cachedResult = this.includedFileCache[absolutePath];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to get #2053 in with some version of the PR I sent you to avoid caching absolute paths here (if you think it's worth caching at all, it doesn't seem unreasonable). This way, there should be roughly one set of cache map transitions on average, and new ones won't need to be created later on

if (cachedResult === undefined) {
cachedResult =
!(this.include && !absolutePath.match(this.include)) &&
!(this.exclude && absolutePath.match(this.exclude)) &&
!(this.includeFilter && !this.includeFilter(
path.relative(this.rootPath, absolutePath)));
this.includedFileCache[absolutePath] = cachedResult;
}
return cachedResult;
}
}


Expand Down