Skip to content
Merged
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
7 changes: 0 additions & 7 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,13 +407,6 @@ gulp.task('test.unit.js', ['build.js.dev'], function (neverDone) {
);
});

gulp.task('watch.js.dev', ['build.js.dev'], function (neverDone) {
watch('modules/**', [
'!broccoli.js.dev',
'!test.unit.js/karma-run',
]);
});


gulp.task('!test.unit.js/karma-server', function() {
karma.server.start({configFile: __dirname + '/karma-js.conf.js', reporters: 'dots'});
Expand Down
45 changes: 45 additions & 0 deletions tools/broccoli/broccoli-tree-stabilizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/// <reference path="broccoli.d.ts" />
/// <reference path="../typings/node/node.d.ts" />

import fs = require('fs');
let symlinkOrCopy = require('symlink-or-copy');


/**
* Stabilizes the inputPath for the following plugins in the build tree.
*
* All broccoli plugins that inherit from `broccoli-writer` or `broccoli-filter` change their
* outputPath during each rebuild.
*
* This means that all following plugins in the build tree can't rely on their inputPath being
* immutable. This results in breakage of any plugin that is not expecting such behavior.
*
* For example all `DiffingBroccoliPlugin`s expect their inputPath to be stable.
*
* By inserting this plugin into the tree after any misbehaving plugin, we can stabilize the
* inputPath for the following plugin in the tree and correct the surprising behavior.
*/
class TreeStabilizer implements BroccoliTree {
inputPath: string;
outputPath: string;


constructor(public inputTree: BroccoliTree) {}


rebuild() {
fs.rmdirSync(this.outputPath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

rmdirSync will fail if non-empty, which it probably is. use rimraf or fs-extra instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, (using rimraf), if outputPath is a symlink of inputPath, isn't that going to remove everything in the inputPath? That seems like a not good thing to happen

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FYI: Calling rimraf (or fs-extra, which calls rimraf) on a symlink will only remove the symlink, not descend into the target directory.

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.

@caitp unless there is a bug in the system, we can't get into a situation where the underlying directory doesn't exist. output path is created by broccoli builder for each rebuild unless it already exists.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That doesn't address what my comment is saying. "rmdirSync" will fail (read: throw) if the directory is non-empty. We aren't talking about whether it exists or not, we're talking about it being non-empty, which it probably is. This most likely works correctly on unix systems where symlinkOrCopy is not copying, but on windows machines it's likely to break


// TODO: investigate if we can use rename the directory instead to improve performance on
// Windows
symlinkOrCopy.sync(this.inputPath, this.outputPath);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This extra tree is just moving the workaround that's already happening elsewhere, which sort of seems like a non-solution, to be honest. I think we're better off talking to @joliss about making sure the contracts of the inputPath(s) and outputPath/cachePath properties make sense, are well established, and are stable across versions

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.

to be clear, this change is just a workaround for incorrect behavior in some broccoli plugins.

my goal is to replace all of the significant plugins in our build chain with diffing plugins, and for those we can skip this operation.

as @joliss confirmed, eventually this should not be a problem for any plugins as they'll get rewritten to work differently.

}


cleanup() {}
}


export default function stabilizeTree(inputTree) {
return new TreeStabilizer(inputTree);
}
6 changes: 4 additions & 2 deletions tools/broccoli/broccoli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ interface BroccoliTree {
* For plugins that take only one input tree, it might be more convenient to use the `inputPath`
*property instead.
*
* This property is set just before the first rebuild and doesn't change afterwards.
* This property is set just before the first rebuild and doesn't change afterwards, unless
* plugins themselves change it.
*
* If the inputPath is outside of broccoli's temp directory, then it's lifetime is not managed by
*the builder.
Expand All @@ -31,7 +32,8 @@ interface BroccoliTree {
/**
* Contains the fs paths for the output trees.
*
* This property is set just before the first rebuild and doesn't change afterwards.
* This property is set just before the first rebuild and doesn't change afterwards, unless the
* plugins themselves change it.
*
* The underlying directory is also created by the builder just before the first rebuild.
* This directory is destroyed and recreated upon each rebuild.
Expand Down
29 changes: 22 additions & 7 deletions tools/broccoli/diffing-broccoli-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import {TreeDiffer, DiffResult} from './tree-differ';
import stabilizeTree from './broccoli-tree-stabilizer';
let symlinkOrCopy = require('symlink-or-copy');


Expand Down Expand Up @@ -51,9 +52,9 @@ class DiffingPluginWrapper implements BroccoliTree {

constructor(private pluginClass, private wrappedPluginArguments) {
if (Array.isArray(wrappedPluginArguments[0])) {
this.inputTrees = wrappedPluginArguments[0];
this.inputTrees = this.stabilizeTrees(wrappedPluginArguments[0]);
} else {
this.inputTree = wrappedPluginArguments[0];
this.inputTree = this.stabilizeTree(wrappedPluginArguments[0]);
}

this.description = this.pluginClass.name;
Expand Down Expand Up @@ -82,6 +83,13 @@ class DiffingPluginWrapper implements BroccoliTree {
}


cleanup() {
if (this.wrappedPlugin.cleanup) {
this.wrappedPlugin.cleanup();
}
}


private relinkOutputAndCachePaths() {
// just symlink the cache and output tree
fs.rmdirSync(this.outputPath);
Expand All @@ -94,16 +102,23 @@ class DiffingPluginWrapper implements BroccoliTree {
let includeExtensions = this.pluginClass.includeExtensions || [];
let excludeExtensions = this.pluginClass.excludeExtensions || [];
this.initialized = true;
this.treeDiffer = new TreeDiffer(this.inputPath, includeExtensions, excludeExtensions);
this.treeDiffer =
new TreeDiffer(this.description, this.inputPath, includeExtensions, excludeExtensions);
this.wrappedPlugin =
new this.pluginClass(this.inputPath, this.cachePath, this.wrappedPluginArguments[1]);
}
}


cleanup() {
if (this.wrappedPlugin.cleanup) {
this.wrappedPlugin.cleanup();
}
private stabilizeTrees(trees: BroccoliTree[]) {
return trees.map((tree) => this.stabilizeTree(tree));
}


private stabilizeTree(tree: BroccoliTree) {
// Ignore all DiffingPlugins as they are already stable, for others we don't know for sure
// so we need to stabilize them.
// Since it's not safe to use instanceof operator in node, we are checking the constructor.name.
return (tree.constructor['name'] === 'DiffingPluginWrapper') ? tree : stabilizeTree(tree);
}
}
89 changes: 50 additions & 39 deletions tools/broccoli/tree-differ.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('TreeDiffer', () => {
};
mockfs(testDir);

let differ = new TreeDiffer('dir1');
let differ = new TreeDiffer('testLabel', 'dir1');

let diffResult = differ.diffTree();

Expand All @@ -49,7 +49,7 @@ describe('TreeDiffer', () => {
};
mockfs(testDir);

let differ = new TreeDiffer('dir1');
let differ = new TreeDiffer('testLabel', 'dir1');

let diffResult = differ.diffTree();

Expand All @@ -76,7 +76,7 @@ describe('TreeDiffer', () => {
};
mockfs(testDir);

let differ = new TreeDiffer('dir1');
let differ = new TreeDiffer('testLabel', 'dir1');

let diffResult = differ.diffTree();

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

let differ = new TreeDiffer('symlinks');
let differ = new TreeDiffer('testLabel', 'symlinks');

let diffResult = differ.diffTree();

Expand Down Expand Up @@ -177,6 +177,17 @@ describe('TreeDiffer', () => {
});


it("should throw an error if an extension isn't prefixed with doc", () => {
// includeExtensions
expect(() => new TreeDiffer('testLabel', 'dir1', ['js']))
.toThrowError("Extension must begin with '.'. Was: 'js'");

// excludeExtentions
expect(() => new TreeDiffer('testLabel', 'dir1', [], ['js']))
.toThrowError("Extension must begin with '.'. Was: 'js'");
});


it('should ignore files with extensions not listed in includeExtensions', () => {
let testDir = {
'dir1': {
Expand All @@ -190,7 +201,7 @@ describe('TreeDiffer', () => {
};
mockfs(testDir);

let differ = new TreeDiffer('dir1', ['.js', '.coffee']);
let differ = new TreeDiffer('testLabel', 'dir1', ['.js', '.coffee']);

let diffResult = differ.diffTree();

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

let differ = new TreeDiffer('dir1', ['.ts', '.cs'], ['.d.ts', '.d.cs']);
let differ = new TreeDiffer('testLabel', 'dir1', ['.ts', '.cs'], ['.d.ts', '.d.cs']);

let diffResult = differ.diffTree();

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

let differ = new TreeDiffer('dir1');
let differ = new TreeDiffer('testLabel', 'dir1');
differ.diffTree();

testDir['dir1']['file-2.txt'] = 'new file';
Expand All @@ -286,24 +297,24 @@ describe('TreeDiffer', () => {
let diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-2.txt']);
});
});


it('should detect file additions mixed with file changes', () => {
let testDir = {
'dir1': {'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})}
};
mockfs(testDir);
it('should detect file additions mixed with file changes', () => {
let testDir = {
'dir1': {'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})}
};
mockfs(testDir);

let differ = new TreeDiffer('dir1');
differ.diffTree();
let differ = new TreeDiffer('testLabel', 'dir1');
differ.diffTree();

testDir['dir1']['file-1.txt'] = 'new content';
testDir['dir1']['file-2.txt'] = 'new file';
mockfs(testDir);
testDir['dir1']['file-1.txt'] = 'new content';
testDir['dir1']['file-2.txt'] = 'new file';
mockfs(testDir);

let diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-2.txt']);
let diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-2.txt']);
});
});


Expand All @@ -316,7 +327,7 @@ describe('TreeDiffer', () => {
};
mockfs(testDir);

let differ = new TreeDiffer('dir1');
let differ = new TreeDiffer('testLabel', 'dir1');
differ.diffTree();

delete testDir['dir1']['file-1.txt'];
Expand All @@ -326,29 +337,29 @@ describe('TreeDiffer', () => {
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual(['file-1.txt']);
});
});


it('should detect file removals mixed with file changes and additions', () => {
let testDir = {
'dir1': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'file-2.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})
}
};
it('should detect file removals mixed with file changes and additions', () => {
let testDir = {
'dir1': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'file-2.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})
}
};

mockfs(testDir);
mockfs(testDir);

let differ = new TreeDiffer('dir1');
differ.diffTree();
let differ = new TreeDiffer('testLabel', 'dir1');
differ.diffTree();

testDir['dir1']['file-1.txt'] = 'changed content';
delete testDir['dir1']['file-2.txt'];
testDir['dir1']['file-3.txt'] = 'new content';
mockfs(testDir);
testDir['dir1']['file-1.txt'] = 'changed content';
delete testDir['dir1']['file-2.txt'];
testDir['dir1']['file-3.txt'] = 'new content';
mockfs(testDir);

let diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-3.txt']);
expect(diffResult.removedPaths).toEqual(['file-2.txt']);
let diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-1.txt', 'file-3.txt']);
expect(diffResult.removedPaths).toEqual(['file-2.txt']);
});
});
});
16 changes: 9 additions & 7 deletions tools/broccoli/tree-differ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class TreeDiffer {
private include: RegExp = null;
private exclude: RegExp = null;

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

Expand All @@ -31,7 +31,9 @@ export class TreeDiffer {
this.exclude = (excludeExtensions || []).length ? buildRegexp(excludeExtensions) : null;

function combine(prev, curr) {
if (curr.charAt(0) !== ".") throw new TypeError("Extension must begin with '.'");
if (curr.charAt(0) !== ".") {
throw new Error(`Extension must begin with '.'. Was: '${curr}'`);
}
let kSpecialRegexpChars = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;
curr = '(' + curr.replace(kSpecialRegexpChars, '\\$&') + ')';
return prev ? (prev + '|' + curr) : curr;
Expand All @@ -40,7 +42,7 @@ export class TreeDiffer {


public diffTree(): DiffResult {
let result = new DirtyCheckingDiffResult(this.rootDirName);
let result = new DirtyCheckingDiffResult(this.label, this.rootDirName);
this.dirtyCheckPath(this.rootPath, result);
this.detectDeletionsAndUpdateFingerprints(result);
result.endTime = Date.now();
Expand Down Expand Up @@ -127,12 +129,12 @@ class DirtyCheckingDiffResult {
public startTime: number = Date.now();
public endTime: number = null;

constructor(public name: string) {}
constructor(public label:string, public directoryName: string) {}

toString() {
return `${pad(this.name, 40)}, duration: ${pad(this.endTime - this.startTime, 5)}ms, ` +
`${pad(this.changedPaths.length + this.removedPaths.length, 5)} changes detected ` +
`(files: ${pad(this.filesChecked, 5)}, directories: ${pad(this.directoriesChecked, 4)})`;
return `${pad(this.label, 30)}, ${pad(this.endTime - this.startTime, 5)}ms, ` +
`${pad(this.changedPaths.length + this.removedPaths.length, 5)} changes ` +
`(files: ${pad(this.filesChecked, 5)}, dirs: ${pad(this.directoriesChecked, 4)})`;
}

log(verbose) {
Expand Down
10 changes: 0 additions & 10 deletions tools/broccoli/trees/browser_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ module.exports = function makeBrowserTree(options, destinationPath) {

var es6Tree = mergeTrees([traceurTree, typescriptTree]);

// TODO(iminar): tree differ seems to have issues with trees created by mergeTrees, investigate!
// ENOENT error is thrown while doing fs.readdirSync on inputRoot
// in the meantime, we just do noop mv to create a new tree
es6Tree = stew.mv(es6Tree, '');

// Call Traceur again to lower the ES6 build tree to ES5
var es5Tree = transpileWithTraceur(es6Tree, {
destExtension: '.js',
Expand Down Expand Up @@ -152,10 +147,5 @@ module.exports = function makeBrowserTree(options, destinationPath) {

var mergedTree = mergeTrees([stew.mv(es6Tree, '/es6'), stew.mv(es5Tree, '/es5')]);

// TODO(iminar): tree differ seems to have issues with trees created by mergeTrees, investigate!
// ENOENT error is thrown while doing fs.readdirSync on inputRoot
// in the meantime, we just do noop mv to create a new tree
mergedTree = stew.mv(mergedTree, '');

return destCopy(mergedTree, destinationPath);
};
5 changes: 0 additions & 5 deletions tools/broccoli/trees/dart_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,5 @@ module.exports = function makeDartTree(destinationPath) {

var dartTree = mergeTrees([sourceTree, getTemplatedPubspecsTree(), getDocsTree()]);

// TODO(iminar): tree differ seems to have issues with trees created by mergeTrees, investigate!
// ENOENT error is thrown while doing fs.readdirSync on inputRoot
// in the meantime, we just do noop mv to create a new tree
dartTree = stew.mv(dartTree, '');

return destCopy(dartTree, destinationPath);
};
Loading