build(dart): Move the broccoli TS2Dart stuff from angular and use it.

This commit is contained in:
Hans Larsen
2016-01-26 10:13:02 -08:00
parent 10d1e75bba
commit 5b69ae0c65
28 changed files with 5203 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
interface AngularBuilderOptions {
outputPath: string;
dartSDK?: any;
logs?: any;
}
+167
View File
@@ -0,0 +1,167 @@
var broccoli = require('broccoli');
var fs = require('fs');
var makeBrowserTree = require('./trees/browser_tree');
var makeNodeTree = require('./trees/node_tree');
var makeDartTree = require('./trees/dart_tree');
var path = require('path');
var printSlowTrees = require('broccoli-slow-trees');
var Q = require('q');
type ProjectMap = {
[key: string]: boolean
};
type Options = {
projects: ProjectMap;
noTypeChecks: boolean;
generateEs6: boolean;
useBundles: boolean;
}
;
/**
* BroccoliBuilder facade for all of our build pipelines.
*/
export class AngularBuilder {
private nodeBuilder: BroccoliBuilder;
private browserDevBuilder: BroccoliBuilder;
private browserProdBuilder: BroccoliBuilder;
private dartBuilder: BroccoliBuilder;
private outputPath: string;
private firstResult: BuildResult;
constructor(public options: AngularBuilderOptions) { this.outputPath = options.outputPath; }
public rebuildBrowserDevTree(opts: Options): Promise<BuildResult> {
this.browserDevBuilder = this.browserDevBuilder || this.makeBrowserDevBuilder(opts);
return this.rebuild(this.browserDevBuilder, 'js.dev');
}
public rebuildBrowserProdTree(opts: Options): Promise<BuildResult> {
this.browserProdBuilder = this.browserProdBuilder || this.makeBrowserProdBuilder(opts);
return this.rebuild(this.browserProdBuilder, 'js.prod');
}
public rebuildNodeTree(opts: Options): Promise<BuildResult> {
this.nodeBuilder = this.nodeBuilder || this.makeNodeBuilder(opts.projects);
return this.rebuild(this.nodeBuilder, 'js.cjs');
}
public rebuildDartTree(projects: ProjectMap): Promise<BuildResult> {
this.dartBuilder = this.dartBuilder || this.makeDartBuilder(projects);
return this.rebuild(this.dartBuilder, 'dart');
}
cleanup(): Promise<any> {
return Q.all([
this.nodeBuilder && this.nodeBuilder.cleanup(),
this.browserDevBuilder && this.browserDevBuilder.cleanup(),
this.browserProdBuilder && this.browserProdBuilder.cleanup()
]);
}
private makeBrowserDevBuilder(opts: Options): BroccoliBuilder {
let tree = makeBrowserTree(
{
name: 'dev',
typeAssertions: true,
sourceMaps: true,
projects: opts.projects,
noTypeChecks: opts.noTypeChecks,
generateEs6: opts.generateEs6,
useBundles: opts.useBundles
},
path.join(this.outputPath, 'js', 'dev'));
return new broccoli.Builder(tree);
}
private makeBrowserProdBuilder(opts: Options): BroccoliBuilder {
let tree = makeBrowserTree(
{
name: 'prod',
typeAssertions: false,
sourceMaps: false,
projects: opts.projects,
noTypeChecks: opts.noTypeChecks,
generateEs6: opts.generateEs6,
useBundles: opts.useBundles
},
path.join(this.outputPath, 'js', 'prod'));
return new broccoli.Builder(tree);
}
private makeNodeBuilder(projects: ProjectMap): BroccoliBuilder {
let tree = makeNodeTree(projects, path.join(this.outputPath, 'js', 'cjs'));
return new broccoli.Builder(tree);
}
private makeDartBuilder(projects: ProjectMap): BroccoliBuilder {
let options = {
outputPath: path.join(this.outputPath, 'dart'),
dartSDK: this.options.dartSDK,
logs: this.options.logs,
projects: projects
};
let tree = makeDartTree(options);
return new broccoli.Builder(tree);
}
private rebuild(builder, name) {
return builder.build().then(
(result) => {
if (!this.firstResult) {
this.firstResult = result;
}
printSlowTrees(result.graph);
writeBuildLog(result, name);
},
(error) => {
// the build tree is the same during rebuilds, only leaf properties of the nodes change
// so let's traverse it and get updated values for input/cache/output paths
if (this.firstResult) {
writeBuildLog(this.firstResult, name);
}
throw error;
});
}
}
function writeBuildLog(result: BuildResult, name: string) {
let logPath = `tmp/build.${name}.log`;
let prevLogPath = logPath + '.previous';
let formattedLogContent = JSON.stringify(broccoliNodeToBuildNode(result.graph), null, 2);
if (fs.existsSync(prevLogPath)) fs.unlinkSync(prevLogPath);
if (fs.existsSync(logPath)) fs.renameSync(logPath, prevLogPath);
fs.writeFileSync(logPath, formattedLogContent, {encoding: 'utf-8'});
}
function broccoliNodeToBuildNode(broccoliNode) {
let tree = broccoliNode.tree.newStyleTree || broccoliNode.tree;
return new BuildNode(tree.description || tree.constructor.name,
tree.inputPath ? [tree.inputPath] : tree.inputPaths, tree.cachePath,
tree.outputPath, broccoliNode.selfTime / (1000 * 1000 * 1000),
broccoliNode.totalTime / (1000 * 1000 * 1000),
broccoliNode.subtrees.map(broccoliNodeToBuildNode));
}
class BuildNode {
constructor(public pluginName: string, public inputPaths: string[], public cachePath: string,
public outputPath: string, public selfTime: number, public totalTime: number,
public inputNodes: BroccoliNode[]) {}
}
+108
View File
@@ -0,0 +1,108 @@
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
/**
* Checks that modules do not import files that are not supposed to import.
*
* This guarantees that platform-independent modules remain platoform-independent.
*/
class CheckImports implements DiffingBroccoliPlugin {
static IMPORT_DECL_REGEXP = new RegExp(`^import[^;]+;`, "mg");
static IMPORT_PATH_REGEXP = new RegExp(`['"]([^'"]+)+['"]`, "m");
static ALLOWED_IMPORTS = {
"angular2/src/core": ["angular2/src/facade"],
"angular2/src/facade": ["rxjs"],
"angular2/src/common": ["angular2/core", "angular2/src/facade"],
"angular2/src/http": ["angular2/core", "angular2/src/facade", "rxjs"],
"angular2/src/upgrade": ["angular2/core", "angular2/src/facade", "angular2/platform/browser"]
//"angular2/src/render": [
// "angular2/animate",
// "angular2/core",
// "angular2/src/facade",
//]
};
private initRun = true;
constructor(private inputPath, private cachePath, private options) {}
rebuild(treeDiff: DiffResult) {
const errors = this.checkAllPaths(treeDiff);
if (errors.length > 0) {
throw new Error(
`The following imports are not allowed because they break barrel boundaries:\n${errors.join("\n")}`);
}
this.symlinkInputAndCacheIfNeeded();
return treeDiff;
}
private checkAllPaths(treeDiff: DiffResult) {
const changesFiles = treeDiff.addedPaths.concat(treeDiff.changedPaths);
return flatMap(changesFiles, _ => this.checkFilePath(_));
}
private symlinkInputAndCacheIfNeeded() {
if (this.initRun) {
fs.rmdirSync(this.cachePath);
fs.symlinkSync(this.inputPath, this.cachePath);
}
this.initRun = false;
}
private checkFilePath(filePath: string) {
const sourceFilePath = path.join(this.inputPath, filePath);
if (endsWith(sourceFilePath, ".ts") && fs.existsSync(sourceFilePath)) {
const content = fs.readFileSync(sourceFilePath, "UTF-8");
const imports = content.match(CheckImports.IMPORT_DECL_REGEXP);
if (imports) {
return imports.filter(i => !this.isAllowedImport(filePath, i))
.map(i => this.formatError(filePath, i));
} else {
return [];
}
}
return [];
}
private isAllowedImport(sourceFile: string, importDecl: string): boolean {
const res = CheckImports.IMPORT_PATH_REGEXP.exec(importDecl);
if (!res || res.length < 2) return true; // non-es6 import
const importPath = res[1];
if (startsWith(importPath, "./") || startsWith(importPath, "../")) return true;
const c = CheckImports.ALLOWED_IMPORTS;
for (var prop in c) {
if (c.hasOwnProperty(prop) && startsWith(sourceFile, prop)) {
const allowedPaths = c[prop];
return startsWith(importPath, prop) ||
allowedPaths.filter(p => startsWith(importPath, p)).length > 0;
}
}
return true;
}
private formatError(filePath: string, importPath: string): string {
const i = importPath.replace(new RegExp(`\n`, 'g'), "\\n");
return `${filePath}: ${i}`;
}
}
function startsWith(str: string, substring: string): boolean {
return str.substring(0, substring.length) === substring;
}
function endsWith(str: string, substring: string): boolean {
return str.indexOf(substring, str.length - substring.length) !== -1;
}
function flatMap<T, U>(arr: T[], fn: (t: T) => U[]): U[] {
return [].concat(...arr.map(fn));
}
export default wrapDiffingPlugin(CheckImports);
+107
View File
@@ -0,0 +1,107 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/fs-extra/fs-extra.d.ts" />
import fse = require('fs-extra');
import path = require('path');
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
var spawn = require('child_process').spawn;
var exec = require('child_process').exec;
function processToPromise(process) {
return new Promise(function(resolve, reject) {
process.on('close', function(code) {
if (code) {
reject(code);
} else {
resolve();
}
});
});
}
class DartFormatter implements DiffingBroccoliPlugin {
private DARTFMT: string;
private verbose: boolean;
private firstBuild: boolean = true;
constructor(public inputPath: string, public cachePath: string, options) {
if (!options.dartSDK) throw new Error("Missing Dart SDK");
this.DARTFMT = options.dartSDK.DARTFMT;
this.verbose = options.logs.dartfmt;
}
rebuild(treeDiff: DiffResult): Promise<any> {
let args = ['-w'];
let argsLength = 2;
let argPackages = [];
let firstBuild = this.firstBuild;
treeDiff.addedPaths.concat(treeDiff.changedPaths)
.forEach((changedFile) => {
let sourcePath = path.join(this.inputPath, changedFile);
let destPath = path.join(this.cachePath, changedFile);
if (!firstBuild && /\.dart$/.test(changedFile)) {
if ((argsLength + destPath.length + 2) >= 0x2000) {
// Win32 command line arguments length
argPackages.push(args);
args = ['-w'];
argsLength = 2;
}
args.push(destPath);
argsLength += destPath.length + 2;
}
fse.copySync(sourcePath, destPath);
});
treeDiff.removedPaths.forEach((removedFile) => {
let destPath = path.join(this.cachePath, removedFile);
fse.removeSync(destPath);
});
if (!firstBuild && args.length > 1) {
argPackages.push(args);
}
let execute = (args) => {
if (args.length < 2) return Promise.resolve();
return new Promise((resolve, reject) => {
exec(this.DARTFMT + ' ' + args.join(' '), (err, stdout, stderr) => {
if (this.verbose) {
console.log(stdout);
}
if (err) {
console.error(shortenFormatterOutput(stderr));
reject('Formatting failed.');
} else {
resolve();
}
});
});
};
if (firstBuild) {
// On firstBuild, format the entire cachePath
this.firstBuild = false;
return execute(['-w', this.cachePath]);
}
return Promise.all(argPackages.map(execute));
}
}
export default wrapDiffingPlugin(DartFormatter);
var ARROW_LINE = /^(\s+)\^+/;
var BEFORE_CHARS = 15;
var stripAnsi = require('strip-ansi');
function shortenFormatterOutput(formatterOutput) {
var lines = formatterOutput.split('\n');
var match, line;
for (var i = 0; i < lines.length; i += 1) {
line = lines[i];
if (match = stripAnsi(line).match(ARROW_LINE)) {
let leadingWhitespace = match[1].length;
let leadingCodeChars = Math.min(leadingWhitespace, BEFORE_CHARS);
lines[i] = line.substr(leadingWhitespace - leadingCodeChars);
lines[i - 1] = lines[i - 1].substr(leadingWhitespace - leadingCodeChars, 80) + '…';
}
}
return lines.join('\n');
}
+36
View File
@@ -0,0 +1,36 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/fs-extra/fs-extra.d.ts" />
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
/**
* Intercepts each file as it is copied to the destination tempdir,
* and tees a copy to the given path outside the tmp dir.
*/
class DestCopy implements DiffingBroccoliPlugin {
constructor(private inputPath, private cachePath, private outputRoot: string) {}
rebuild(treeDiff: DiffResult) {
treeDiff.addedPaths.concat(treeDiff.changedPaths)
.forEach((changedFilePath) => {
var destFilePath = path.join(this.outputRoot, changedFilePath);
var destDirPath = path.dirname(destFilePath);
fse.mkdirsSync(destDirPath);
fse.copySync(path.join(this.inputPath, changedFilePath), destFilePath);
});
treeDiff.removedPaths.forEach((removedFilePath) => {
var destFilePath = path.join(this.outputRoot, removedFilePath);
// TODO: what about obsolete directories? we are not cleaning those up yet
fs.unlinkSync(destFilePath);
});
}
}
export default wrapDiffingPlugin(DestCopy);
+14
View File
@@ -0,0 +1,14 @@
/// <reference path="../typings/es6-promise/es6-promise.d.ts" />
interface FilterOptions {
extensions?: string[]
}
declare class Filter {
constructor(inputTree: any, options?: FilterOptions);
processString(contents: string, relativePath: string): string;
// NB: This function is probably not intended as part of the public API
processFile(srcDir: string, destDir: string, relativePath: string): Promise<any>;
}
export = Filter;
+76
View File
@@ -0,0 +1,76 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/jasmine/jasmine.d.ts" />
let mockfs = require('mock-fs');
import fs = require('fs');
import path = require('path');
import {TreeDiffer} from './tree-differ';
import {DiffingFlatten} from './broccoli-flatten';
describe('Flatten', () => {
afterEach(() => mockfs.restore());
function flatten(inputPaths) { return new DiffingFlatten(inputPaths, 'output', null); }
function read(path) { return fs.readFileSync(path, {encoding: "utf-8"}); }
function rm(path) { return fs.unlinkSync(path); }
function write(path, content) { fs.writeFileSync(path, content, {encoding: "utf-8"}); }
it('should flatten files and be incremental', () => {
let testDir = {
'input': {
'dir1': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
'subdir-1': {
'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
},
'empty-dir': {}
},
},
'output': {}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'input');
let flattenedTree = flatten('input');
flattenedTree.rebuild(differ.diffTree());
expect(fs.readdirSync('output')).toEqual(['file-1.1.txt', 'file-1.txt', 'file-2.txt']);
// fails due to a mock-fs bug related to reading symlinks?
// expect(read('output/file-1.1.txt')).toBe('file-1.1.txt content');
// delete a file
rm('input/dir1/file-1.txt');
// add a new one
write('input/dir1/file-3.txt', 'file-3.txt content');
flattenedTree.rebuild(differ.diffTree());
expect(fs.readdirSync('output')).toEqual(['file-1.1.txt', 'file-2.txt', 'file-3.txt']);
});
it('should throw an exception if duplicates are found', () => {
let testDir = {
'input': {
'dir1': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'subdir-1':
{'file-1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})},
'empty-dir': {}
},
},
'output': {}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'input');
let flattenedTree = flatten('input');
expect(() => flattenedTree.rebuild(differ.diffTree()))
.toThrowError("Duplicate file 'file-1.txt' found in path 'dir1" + path.sep + "subdir-1" +
path.sep + "file-1.txt'");
});
});
+51
View File
@@ -0,0 +1,51 @@
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
var symlinkOrCopy = require('symlink-or-copy').sync;
var isWindows = process.platform === 'win32';
/**
* Intercepts each changed file and replaces its contents with
* the associated changes.
*/
export class DiffingFlatten implements DiffingBroccoliPlugin {
constructor(private inputPath, private cachePath, private options) {}
rebuild(treeDiff: DiffResult) {
let pathsToUpdate = treeDiff.addedPaths;
// since we need to run on Windows as well we can't rely on symlinks being available,
// which means that we need to respond to both added and changed paths
if (isWindows) {
pathsToUpdate = pathsToUpdate.concat(treeDiff.changedPaths);
}
pathsToUpdate.forEach((changedFilePath) => {
var sourceFilePath = path.join(this.inputPath, changedFilePath);
var destFilePath = path.join(this.cachePath, path.basename(changedFilePath));
var destDirPath = path.dirname(destFilePath);
if (!fs.existsSync(destDirPath)) {
fse.mkdirpSync(destDirPath);
}
if (!fs.existsSync(destFilePath)) {
symlinkOrCopy(sourceFilePath, destFilePath);
} else {
throw new Error(`Duplicate file '${path.basename(changedFilePath)}' ` +
`found in path '${changedFilePath}'`);
}
});
treeDiff.removedPaths.forEach((removedFilePath) => {
var destFilePath = path.join(this.cachePath, path.basename(removedFilePath));
fs.unlinkSync(destFilePath);
});
}
}
export default wrapDiffingPlugin(DiffingFlatten);
+49
View File
@@ -0,0 +1,49 @@
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
var _ = require('lodash');
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
interface LodashRendererOptions {
encoding?: string;
context?: any;
// files option unsupported --- use Funnel on inputTree instead.
files?: string[];
}
const kDefaultOptions: LodashRendererOptions = {
encoding: 'utf-8',
context: {},
files: []
};
/**
* Intercepts each changed file and replaces its contents with
* the associated changes.
*/
export class LodashRenderer implements DiffingBroccoliPlugin {
constructor(private inputPath, private cachePath,
private options: LodashRendererOptions = kDefaultOptions) {}
rebuild(treeDiff: DiffResult) {
let {encoding = 'utf-8', context = {}} = this.options;
let processFile = (relativePath) => {
let sourceFilePath = path.join(this.inputPath, relativePath);
let destFilePath = path.join(this.cachePath, relativePath);
let content = fs.readFileSync(sourceFilePath, {encoding});
let transformedContent = _.template(content)(context);
fse.outputFileSync(destFilePath, transformedContent);
};
let removeFile = (relativePath) => {
let destFilePath = path.join(this.cachePath, relativePath);
fs.unlinkSync(destFilePath);
};
treeDiff.addedPaths.concat(treeDiff.changedPaths).forEach(processFile);
treeDiff.removedPaths.forEach(removeFile);
}
}
export default wrapDiffingPlugin(LodashRenderer);
@@ -0,0 +1,91 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/jasmine/jasmine.d.ts" />
let mockfs = require('mock-fs');
import fs = require('fs');
import {TreeDiffer} from './tree-differ';
import {MergeTrees} from './broccoli-merge-trees';
describe('MergeTrees', () => {
afterEach(() => mockfs.restore());
function mergeTrees(inputPaths, cachePath, options) {
return new MergeTrees(inputPaths, cachePath, options);
}
function MakeTreeDiffers(rootDirs) {
let treeDiffers = rootDirs.map((rootDir) => new TreeDiffer('MergeTrees', rootDir));
treeDiffers.diffTrees = () => { return treeDiffers.map(tree => tree.diffTree()); };
return treeDiffers;
}
function read(path) { return fs.readFileSync(path, "utf-8"); }
it('should copy the file from the right-most inputTree with overwrite=true', () => {
let testDir: any = {
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})},
'tree3': {'foo.js': mockfs.file({content: 'tree3/foo.js content', mtime: new Date(1000)})}
};
mockfs(testDir);
let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {overwrite: true});
treeMerger.rebuild(treeDiffer.diffTrees());
expect(read('dest/foo.js')).toBe('tree3/foo.js content');
delete testDir.tree2['foo.js'];
delete testDir.tree3['foo.js'];
mockfs(testDir);
treeMerger.rebuild(treeDiffer.diffTrees());
expect(read('dest/foo.js')).toBe('tree1/foo.js content');
testDir.tree2['foo.js'] = mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)});
mockfs(testDir);
treeMerger.rebuild(treeDiffer.diffTrees());
expect(read('dest/foo.js')).toBe('tree2/foo.js content');
});
it('should throw if duplicates are found during the initial build', () => {
let testDir: any = {
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
'tree2': {'foo.js': mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)})},
'tree3': {'foo.js': mockfs.file({content: 'tree3/foo.js content', mtime: new Date(1000)})}
};
mockfs(testDir);
let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {});
expect(() => treeMerger.rebuild(treeDiffer.diffTrees()))
.toThrowError(
'Duplicate path found while merging trees. Path: "foo.js".\n' +
'Either remove the duplicate or enable the "overwrite" option for this merge.');
testDir = {
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
'tree2': {},
'tree3': {}
};
mockfs(testDir);
});
it('should throw if duplicates are found during rebuild', () => {
let testDir = {
'tree1': {'foo.js': mockfs.file({content: 'tree1/foo.js content', mtime: new Date(1000)})},
'tree2': {},
'tree3': {}
};
mockfs(testDir);
let treeDiffer = MakeTreeDiffers(['tree1', 'tree2', 'tree3']);
let treeMerger = mergeTrees(['tree1', 'tree2', 'tree3'], 'dest', {});
expect(() => treeMerger.rebuild(treeDiffer.diffTrees())).not.toThrow();
testDir.tree2['foo.js'] = mockfs.file({content: 'tree2/foo.js content', mtime: new Date(1000)});
mockfs(testDir);
expect(() => treeMerger.rebuild(treeDiffer.diffTrees()))
.toThrowError(
'Duplicate path found while merging trees. Path: "foo.js".\n' +
'Either remove the duplicate or enable the "overwrite" option for this merge.');
});
});
+137
View File
@@ -0,0 +1,137 @@
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
var symlinkOrCopySync = require('symlink-or-copy').sync;
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
var isWindows = process.platform === 'win32';
interface MergeTreesOptions {
overwrite?: boolean;
}
function outputFileSync(sourcePath, destPath) {
let dirname = path.dirname(destPath);
fse.mkdirsSync(dirname, {fs: fs});
symlinkOrCopySync(sourcePath, destPath);
}
function pathOverwrittenError(path) {
const msg = 'Either remove the duplicate or enable the "overwrite" option for this merge.';
return new Error(`Duplicate path found while merging trees. Path: "${path}".\n${msg}`);
}
export class MergeTrees implements DiffingBroccoliPlugin {
private pathCache: {[key: string]: number[]} = Object.create(null);
public options: MergeTreesOptions;
private firstBuild: boolean = true;
constructor(public inputPaths: string[], public cachePath: string,
options: MergeTreesOptions = {}) {
this.options = options || {};
}
rebuild(treeDiffs: DiffResult[]) {
let overwrite = this.options.overwrite;
let pathsToEmit: string[] = [];
let pathsToRemove: string[] = [];
let emitted: {[key: string]: boolean} = Object.create(null);
let contains = (cache, val) => {
for (let i = 0, ii = cache.length; i < ii; ++i) {
if (cache[i] === val) return true;
}
return false;
};
let emit = (relativePath) => {
// ASSERT(!emitted[relativePath]);
pathsToEmit.push(relativePath);
emitted[relativePath] = true;
};
if (this.firstBuild) {
this.firstBuild = false;
// Build initial cache
treeDiffs.reverse().forEach((treeDiff: DiffResult, index) => {
index = treeDiffs.length - 1 - index;
treeDiff.addedPaths.forEach((changedPath) => {
let cache = this.pathCache[changedPath];
if (cache === undefined) {
this.pathCache[changedPath] = [index];
pathsToEmit.push(changedPath);
} else if (overwrite) {
// ASSERT(contains(pathsToEmit, changedPath));
cache.unshift(index);
} else {
throw pathOverwrittenError(changedPath);
}
});
});
} else {
// Update cache
treeDiffs.reverse().forEach((treeDiff: DiffResult, index) => {
index = treeDiffs.length - 1 - index;
if (treeDiff.removedPaths) {
treeDiff.removedPaths.forEach((removedPath) => {
let cache = this.pathCache[removedPath];
// ASSERT(cache !== undefined);
// ASSERT(contains(cache, index));
if (cache[cache.length - 1] === index) {
pathsToRemove.push(path.join(this.cachePath, removedPath));
cache.pop();
if (cache.length === 0) {
this.pathCache[removedPath] = undefined;
} else if (!emitted[removedPath]) {
if (cache.length === 1 && !overwrite) {
throw pathOverwrittenError(removedPath);
}
emit(removedPath);
}
}
});
}
let pathsToUpdate = treeDiff.addedPaths;
if (isWindows) {
pathsToUpdate = pathsToUpdate.concat(treeDiff.changedPaths);
}
pathsToUpdate.forEach((changedPath) => {
let cache = this.pathCache[changedPath];
if (cache === undefined) {
// File was added
this.pathCache[changedPath] = [index];
emit(changedPath);
} else if (!contains(cache, index)) {
cache.push(index);
cache.sort((a, b) => a - b);
if (cache.length > 1 && !overwrite) {
throw pathOverwrittenError(changedPath);
}
if (cache[cache.length - 1] === index && !emitted[changedPath]) {
emit(changedPath);
}
}
});
});
}
pathsToRemove.forEach((destPath) => fse.removeSync(destPath));
pathsToEmit.forEach((emittedPath) => {
let cache = this.pathCache[emittedPath];
let destPath = path.join(this.cachePath, emittedPath);
let sourceIndex = cache[cache.length - 1];
let sourceInputPath = this.inputPaths[sourceIndex];
let sourcePath = path.join(sourceInputPath, emittedPath);
if (cache.length > 1) {
fse.removeSync(destPath);
}
outputFileSync(sourcePath, destPath);
});
}
}
export default wrapDiffingPlugin(MergeTrees);
+59
View File
@@ -0,0 +1,59 @@
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
var minimatch = require('minimatch');
var FILE_ENCODING = {encoding: 'utf-8'};
/**
* Intercepts each changed file and replaces its contents with
* the associated changes.
*/
class DiffingReplace implements DiffingBroccoliPlugin {
constructor(private inputPath, private cachePath, private options) {}
rebuild(treeDiff: DiffResult) {
var patterns = this.options.patterns;
var files = this.options.files;
treeDiff.addedPaths.concat(treeDiff.changedPaths)
.forEach((changedFilePath) => {
var sourceFilePath = path.join(this.inputPath, changedFilePath);
var destFilePath = path.join(this.cachePath, changedFilePath);
var destDirPath = path.dirname(destFilePath);
if (!fs.existsSync(destDirPath)) {
fse.mkdirpSync(destDirPath);
}
var fileMatches = files.some((filePath) => minimatch(changedFilePath, filePath));
if (fileMatches) {
var content = fs.readFileSync(sourceFilePath, FILE_ENCODING);
patterns.forEach((pattern) => {
var replacement = pattern.replacement;
if (typeof replacement === 'function') {
replacement = function(content) {
return pattern.replacement(content, changedFilePath);
};
}
content = content.replace(pattern.match, replacement);
});
fs.writeFileSync(destFilePath, content, FILE_ENCODING);
} else if (!fs.existsSync(destFilePath)) {
try {
fs.symlinkSync(sourceFilePath, destFilePath);
} catch (e) {
fs.writeFileSync(destFilePath, fs.readFileSync(sourceFilePath));
}
}
});
treeDiff.removedPaths.forEach((removedFilePath) => {
var destFilePath = path.join(this.cachePath, removedFilePath);
fs.unlinkSync(destFilePath);
});
}
}
export default wrapDiffingPlugin(DiffingReplace);
@@ -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);
// TODO: investigate if we can use rename the directory instead to improve performance on
// Windows
symlinkOrCopy.sync(this.inputPath, this.outputPath);
}
cleanup() {}
}
export default function stabilizeTree(inputTree) {
return new TreeStabilizer(inputTree);
}
+50
View File
@@ -0,0 +1,50 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/fs-extra/fs-extra.d.ts" />
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import * as ts2dart from 'ts2dart';
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
class TSToDartTranspiler implements DiffingBroccoliPlugin {
static includeExtensions = ['.ts'];
private transpiler: ts2dart.Transpiler;
constructor(public inputPath: string, public cachePath: string,
public options: ts2dart.TranspilerOptions) {
options.basePath = inputPath;
this.transpiler = new ts2dart.Transpiler(options);
}
rebuild(treeDiff: DiffResult) {
let toEmit = [path.resolve(this.inputPath, 'angular2/manual_typings/globals.d.ts')];
let getDartFilePath = (path: string) => path.replace(/((\.js)|(\.ts))$/i, '.dart');
treeDiff.addedPaths.concat(treeDiff.changedPaths)
.forEach((changedPath) => {
let inputFilePath = path.resolve(this.inputPath, changedPath);
// Ignore files which don't need to be transpiled to Dart
let dartInputFilePath = getDartFilePath(inputFilePath);
if (fs.existsSync(dartInputFilePath)) return;
// Prepare to rebuild
toEmit.push(path.resolve(this.inputPath, changedPath));
});
treeDiff.removedPaths.forEach((removedPath) => {
let absolutePath = path.resolve(this.inputPath, removedPath);
// Ignore files which don't need to be transpiled to Dart
let dartInputFilePath = getDartFilePath(absolutePath);
if (fs.existsSync(dartInputFilePath)) return;
let dartOutputFilePath = getDartFilePath(removedPath);
fs.unlinkSync(path.join(this.cachePath, dartOutputFilePath));
});
this.transpiler.transpile(toEmit, this.cachePath);
}
}
export default wrapDiffingPlugin(TSToDartTranspiler);
+370
View File
@@ -0,0 +1,370 @@
/// <reference path="../typings/node/node.d.ts" />
import fs = require('fs');
import fse = require('fs-extra');
import path = require('path');
import * as ts from 'typescript';
import {wrapDiffingPlugin, DiffingBroccoliPlugin, DiffResult} from './diffing-broccoli-plugin';
type FileRegistry = ts.Map<{version: number}>;
const FS_OPTS = {
encoding: 'utf-8'
};
// Sub-directory where the @internal typing files (.d.ts) are stored
export const INTERNAL_TYPINGS_PATH: string = 'internal_typings';
// Monkey patch the TS compiler to be able to re-emit files with @internal symbols
let tsEmitInternal: boolean = false;
const originalEmitFiles: Function = (<any>ts).emitFiles;
(<any>ts).emitFiles = function(resolver: any, host: any, targetSourceFile: any): any {
if (tsEmitInternal) {
const orignalgetCompilerOptions = host.getCompilerOptions;
host.getCompilerOptions = () => {
let options = clone(orignalgetCompilerOptions.call(host));
options.stripInternal = false;
options.outDir = `${options.outDir}/${INTERNAL_TYPINGS_PATH}`;
return options;
}
}
return originalEmitFiles(resolver, host, targetSourceFile);
};
/**
* Broccoli plugin that implements incremental Typescript compiler.
*
* It instantiates a typescript compiler instance that keeps all the state about the project and
* can re-emit only the files that actually changed.
*
* Limitations: only files that map directly to the changed source file via naming conventions are
* re-emitted. This primarily affects code that uses `const enum`s, because changing the enum value
* requires global emit, which can affect many files.
*/
class DiffingTSCompiler implements DiffingBroccoliPlugin {
private tsOpts: ts.CompilerOptions;
private fileRegistry: FileRegistry = Object.create(null);
private rootFilePaths: string[];
private tsServiceHost: ts.LanguageServiceHost;
private tsService: ts.LanguageService;
private firstRun: boolean = true;
private previousRunFailed: boolean = false;
// Whether to generate the @internal typing files (they are only generated when `stripInternal` is
// true)
private genInternalTypings: boolean = false;
static includeExtensions = ['.ts'];
static excludeExtensions = ['.d.ts'];
constructor(public inputPath: string, public cachePath: string, public options) {
if (options.rootFilePaths) {
this.rootFilePaths = options.rootFilePaths.splice(0);
delete options.rootFilePaths;
} else {
this.rootFilePaths = [];
}
if (options.internalTypings) {
this.genInternalTypings = true;
delete options.internalTypings;
}
// the conversion is a bit awkward, see https://github.com/Microsoft/TypeScript/issues/5276
// in 1.8 use convertCompilerOptionsFromJson
this.tsOpts =
ts.parseJsonConfigFileContent({compilerOptions: options, files: []}, null, null).options;
if ((<any>this.tsOpts).stripInternal === false) {
// @internal are included in the generated .d.ts, do not generate them separately
this.genInternalTypings = false;
}
// TODO: the above turns rootDir set to './' into an empty string - looks like a tsc bug
// check back when we upgrade to 1.7.x
if (this.tsOpts.rootDir === '') {
this.tsOpts.rootDir = './';
}
this.tsOpts.outDir = this.cachePath;
this.tsServiceHost = new CustomLanguageServiceHost(this.tsOpts, this.rootFilePaths,
this.fileRegistry, this.inputPath);
this.tsService = ts.createLanguageService(this.tsServiceHost, ts.createDocumentRegistry());
}
rebuild(treeDiff: DiffResult) {
let pathsToEmit = [];
let pathsWithErrors = [];
let errorMessages = [];
treeDiff.addedPaths.concat(treeDiff.changedPaths)
.forEach((tsFilePath) => {
if (!this.fileRegistry[tsFilePath]) {
this.fileRegistry[tsFilePath] = {version: 0};
this.rootFilePaths.push(tsFilePath);
} else {
this.fileRegistry[tsFilePath].version++;
}
pathsToEmit.push(tsFilePath);
});
treeDiff.removedPaths.forEach((tsFilePath) => {
console.log('removing outputs for', tsFilePath);
this.rootFilePaths.splice(this.rootFilePaths.indexOf(tsFilePath), 1);
this.fileRegistry[tsFilePath] = null;
this.removeOutputFor(tsFilePath);
});
if (this.firstRun) {
this.firstRun = false;
this.doFullBuild();
} else {
tsEmitInternal = false;
pathsToEmit.forEach((tsFilePath) => {
let output = this.tsService.getEmitOutput(tsFilePath);
if (output.emitSkipped) {
let errorFound = this.collectErrors(tsFilePath);
if (errorFound) {
pathsWithErrors.push(tsFilePath);
errorMessages.push(errorFound);
}
} else {
output.outputFiles.forEach(o => {
let destDirPath = path.dirname(o.name);
fse.mkdirsSync(destDirPath);
fs.writeFileSync(o.name, this.fixSourceMapSources(o.text), FS_OPTS);
});
}
});
if (pathsWithErrors.length) {
this.previousRunFailed = true;
var error =
new Error('Typescript found the following errors:\n' + errorMessages.join('\n'));
error['showStack'] = false;
throw error;
} else if (this.previousRunFailed) {
this.doFullBuild();
} else if (this.genInternalTypings) {
// serialize the .d.ts files containing @internal symbols
tsEmitInternal = true;
pathsToEmit.forEach((tsFilePath) => {
let output = this.tsService.getEmitOutput(tsFilePath);
if (!output.emitSkipped) {
output.outputFiles.forEach(o => {
if (endsWith(o.name, '.d.ts')) {
let destDirPath = path.dirname(o.name);
fse.mkdirsSync(destDirPath);
fs.writeFileSync(o.name, this.fixSourceMapSources(o.text), FS_OPTS);
}
});
}
});
tsEmitInternal = false;
}
}
}
private collectErrors(tsFilePath): String {
let allDiagnostics = this.tsService.getCompilerOptionsDiagnostics()
.concat(this.tsService.getSyntacticDiagnostics(tsFilePath))
.concat(this.tsService.getSemanticDiagnostics(tsFilePath));
let errors = [];
allDiagnostics.forEach(diagnostic => {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
if (diagnostic.file) {
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
errors.push(` ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
} else {
errors.push(` Error: ${message}`);
}
});
if (errors.length) {
return errors.join('\n');
}
}
private doFullBuild() {
let program = this.tsService.getProgram();
tsEmitInternal = false;
let emitResult = program.emit(undefined, (absoluteFilePath, fileContent) => {
fse.mkdirsSync(path.dirname(absoluteFilePath));
fs.writeFileSync(absoluteFilePath, this.fixSourceMapSources(fileContent), FS_OPTS);
});
if (this.genInternalTypings) {
// serialize the .d.ts files containing @internal symbols
tsEmitInternal = true;
program.emit(undefined, (absoluteFilePath, fileContent) => {
if (endsWith(absoluteFilePath, '.d.ts')) {
fse.mkdirsSync(path.dirname(absoluteFilePath));
fs.writeFileSync(absoluteFilePath, fileContent, FS_OPTS);
}
});
tsEmitInternal = false;
}
if (emitResult.emitSkipped) {
let allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
let errorMessages = [];
allDiagnostics.forEach(diagnostic => {
var pos = '';
if (diagnostic.file) {
var {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
pos = `${diagnostic.file.fileName} (${line + 1}, ${character + 1}): `
}
var message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
errorMessages.push(` ${pos}${message}`);
});
if (errorMessages.length) {
this.previousRunFailed = true;
var error =
new Error('Typescript found the following errors:\n' + errorMessages.join('\n'));
error['showStack'] = false;
throw error;
} else {
this.previousRunFailed = false;
}
}
}
/**
* There is a bug in TypeScript 1.6, where the sourceRoot and inlineSourceMap properties
* are exclusive. This means that the sources property always contains relative paths
* (e.g, ../../../../angular2/src/di/injector.ts).
*
* Here, we normalize the sources property and remove the ../../../
*
* This issue is fixed in https://github.com/Microsoft/TypeScript/pull/5620.
* Once we switch to TypeScript 1.8, we can remove this method.
*/
private fixSourceMapSources(content: string): string {
try {
const marker = "//# sourceMappingURL=data:application/json;base64,";
const index = content.indexOf(marker);
if (index == -1) return content;
const base = content.substring(0, index + marker.length);
const sourceMapBit =
new Buffer(content.substring(index + marker.length), 'base64').toString("utf8");
const sourceMaps = JSON.parse(sourceMapBit);
const source = sourceMaps.sources[0];
sourceMaps.sources = [source.substring(source.lastIndexOf("../") + 3)];
return `${base}${new Buffer(JSON.stringify(sourceMaps)).toString('base64')}`;
} catch (e) {
return content;
}
}
private removeOutputFor(tsFilePath: string) {
let absoluteJsFilePath = path.join(this.cachePath, tsFilePath.replace(/\.ts$/, '.js'));
let absoluteMapFilePath = path.join(this.cachePath, tsFilePath.replace(/.ts$/, '.js.map'));
let absoluteDtsFilePath = path.join(this.cachePath, tsFilePath.replace(/\.ts$/, '.d.ts'));
if (fs.existsSync(absoluteJsFilePath)) {
fs.unlinkSync(absoluteJsFilePath);
if (fs.existsSync(absoluteMapFilePath)) {
// source map could be inline or not generated
fs.unlinkSync(absoluteMapFilePath);
}
fs.unlinkSync(absoluteDtsFilePath);
}
}
}
class CustomLanguageServiceHost implements ts.LanguageServiceHost {
private currentDirectory: string;
private defaultLibFilePath: string;
constructor(private compilerOptions: ts.CompilerOptions, private fileNames: string[],
private fileRegistry: FileRegistry, private treeInputPath: string) {
this.currentDirectory = process.cwd();
this.defaultLibFilePath = ts.getDefaultLibFilePath(compilerOptions).replace(/\\/g, '/');
}
getScriptFileNames(): string[] { return this.fileNames; }
getScriptVersion(fileName: string): string {
return this.fileRegistry[fileName] && this.fileRegistry[fileName].version.toString();
}
/**
* This method is called quite a bit to lookup 3 kinds of paths:
* 1/ files in the fileRegistry
* - these are the files in our project that we are watching for changes
* - in the future we could add caching for these files and invalidate the cache when
* the file is changed lazily during lookup
* 2/ .d.ts and library files not in the fileRegistry
* - these are not our files, they come from tsd or typescript itself
* - these files change only rarely but since we need them very rarely, it's not worth the
* cache invalidation hassle to cache them
* 3/ bogus paths that typescript compiler tries to lookup during import resolution
* - these paths are tricky to cache since files come and go and paths that was bogus in the
* past might not be bogus later
*
* In the initial experiments the impact of this caching was insignificant (single digit %) and
* not worth the potential issues with stale cache records.
*/
getScriptSnapshot(tsFilePath: string): ts.IScriptSnapshot {
let absoluteTsFilePath;
if (tsFilePath == this.defaultLibFilePath || path.isAbsolute(tsFilePath)) {
absoluteTsFilePath = tsFilePath;
} else if (this.compilerOptions.moduleResolution === ts.ModuleResolutionKind.NodeJs &&
tsFilePath.match(/^node_modules/)) {
absoluteTsFilePath = path.resolve(tsFilePath);
} else if (tsFilePath.match(/^rxjs/)) {
absoluteTsFilePath = path.resolve('node_modules', tsFilePath);
} else {
absoluteTsFilePath = path.join(this.treeInputPath, tsFilePath);
}
if (!fs.existsSync(absoluteTsFilePath)) {
// TypeScript seems to request lots of bogus paths during import path lookup and resolution,
// so we we just return undefined when the path is not correct.
return undefined;
}
return ts.ScriptSnapshot.fromString(fs.readFileSync(absoluteTsFilePath, FS_OPTS));
}
getCurrentDirectory(): string { return this.currentDirectory; }
getCompilationSettings(): ts.CompilerOptions { return this.compilerOptions; }
getDefaultLibFileName(options: ts.CompilerOptions): string {
// ignore options argument, options should not change during the lifetime of the plugin
return this.defaultLibFilePath;
}
}
export default wrapDiffingPlugin(DiffingTSCompiler);
function clone<T>(object: T): T {
const result: any = {};
for (const id in object) {
result[id] = (<any>object)[id];
}
return <T>result;
}
function endsWith(str: string, substring: string): boolean {
return str.indexOf(substring, str.length - substring.length) !== -1;
}
+8
View File
@@ -0,0 +1,8 @@
/// <reference path="../typings/es6-promise/es6-promise.d.ts" />
declare module "broccoli-writer" {
class Writer {
write(readTree: (tree) => Promise<string>, destDir: string): Promise<any>;
}
export = Writer;
}
+160
View File
@@ -0,0 +1,160 @@
/// <reference path="../typings/es6-promise/es6-promise.d.ts" />
interface BroccoliTree {
/**
* Contains the fs path for the input tree when the plugin takes only one input tree.
*
* For plugins that take multiple trees see the `inputPaths` property.
*
* This property is set just before the first rebuild and doesn't change afterwards.
*/
inputPath: string;
/**
* Contains the array of fs paths for input trees.
*
* 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, 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.
* If the inputPath is within broccoli's temp directory it is an outputPath (and output directory)
*of another plugin.
* This means that while the `outputPath` doesn't change, the underlying directory is frequently
*recreated.
*/
inputPaths?: string[];
/**
* Contains the fs paths for the output trees.
*
* 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.
*/
outputPath?: string;
/**
* Contains the fs paths for a cache directory available to the plugin.
*
* This property is set just before the first rebuild and doesn't change afterwards.
*
* The underlying directory is also created by the builder just before the first rebuild.
* The lifetime of the directory is associated with the lifetime of the plugin.
*/
cachePath?: string;
inputTree?: BroccoliTree;
inputTrees?: BroccoliTree[];
/**
* Trees which implement the rebuild api are wrapped automatically for api compat,
* and `newStyleTree` keeps a reference to the original unwrapped tree.
*/
newStyleTree?: BroccoliTree;
/**
* Description or name of the plugin used for reporting.
*
* If missing `tree.constructor.name` is usually used instead.
*/
description?: string;
rebuild(): (Promise<any>| void);
cleanup(): void;
}
interface OldBroccoliTree {
read?(readTree: (tree: BroccoliTree) => Promise<string>): (Promise<string>| string);
}
interface BroccoliBuilder {
/**
* Triggers a build and returns a promise for the build result
*/
build(): Promise<BuildResult>;
/**
* Cleans up the whole build tree by calling `.cleanup()` method on all trees that are part of the
* pipeline.
*/
cleanup(): Promise<any>;
}
interface BuildResult {
/**
* Directory that contains result of the build.
*
* This directory will contains symlinks, so it is not safe to just use it as is.
*
* Use `copy-dereference` npm package to create a safe-to-use replica of the build artifacts.
*/
directory: string;
/**
* The DAG (graph) of all trees in the build pipeline.
*/
graph: BroccoliNode;
/**
* Total time it took to make the build.
*/
totalTime: number;
}
interface BroccoliNode {
///**
// * Id of the current node
// */
// id: number; //only in master
/**
* Time spent processing the current node during a single rebuild.
*/
selfTime: number;
/**
* Time spent processing the current node and its subtrees during a single rebuild.
*/
totalTime: number;
/**
* Tree associated with the current node.
*/
tree: BroccoliTree;
/**
* Child nodes with references to trees that are input for the tree of the current node.
*/
subtrees: BroccoliNode[];
/**
* Parent nodes with references to trees that are consume the output of processing the current
* tree.
*/
parents: BroccoliNode[];
/**
* Path to the directory containing the output of processing the current tree.
*/
directory: string;
}
+190
View File
@@ -0,0 +1,190 @@
/// <reference path="broccoli.d.ts" />
/// <reference path="../typings/fs-extra/fs-extra.d.ts" />
/// <reference path="../typings/node/node.d.ts" />
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');
export {DiffResult} from './tree-differ';
/**
* Makes writing diffing plugins easy.
*
* Factory method that takes a class that implements the DiffingBroccoliPlugin interface and returns
* an instance of BroccoliTree.
*
* @param pluginClass
* @returns {DiffingBroccoliPlugin}
*/
export function wrapDiffingPlugin(pluginClass): DiffingPluginWrapperFactory {
return function() { return new DiffingPluginWrapper(pluginClass, arguments); };
}
export interface DiffingBroccoliPlugin {
rebuild(diff: (DiffResult | DiffResult[])): (Promise<DiffResult | void>| DiffResult | void);
cleanup ? () : void;
}
type DiffingPluginWrapperFactory = (inputTrees: (BroccoliTree | BroccoliTree[]), options?) =>
BroccoliTree;
class DiffingPluginWrapper implements BroccoliTree {
treeDiffer: TreeDiffer = null;
treeDiffers: TreeDiffer[] = null;
initialized = false;
wrappedPlugin: DiffingBroccoliPlugin = null;
inputTree = null;
inputTrees = null;
description = null;
// props monkey-patched by broccoli builder:
inputPath = null;
inputPaths = null;
cachePath = null;
outputPath = null;
private diffResult: DiffResult = null;
constructor(private pluginClass, private wrappedPluginArguments) {
if (Array.isArray(wrappedPluginArguments[0])) {
this.inputTrees = this.stabilizeTrees(wrappedPluginArguments[0]);
} else {
this.inputTree = this.stabilizeTree(wrappedPluginArguments[0]);
}
this.description = this.pluginClass.name;
}
private getDiffResult(): (DiffResult | DiffResult[]) {
let returnOrCalculateDiffResult = (tree, index) => {
// returnOrCalculateDiffResult will do one of two things:
//
// If `this.diffResult` is null, calculate a DiffResult using TreeDiffer
// for the input tree.
//
// Otherwise, `this.diffResult` was produced from the output of the
// inputTree's rebuild() method, and can be used without being checked.
// Set `this.diffResult` to null and return the previously stored value.
let diffResult = tree.diffResult;
if (diffResult) return diffResult;
let differ = index === false ? this.treeDiffer : this.treeDiffers[index];
return differ.diffTree();
};
if (this.inputTrees) {
return this.inputTrees.map(returnOrCalculateDiffResult);
} else if (this.inputTree) {
return returnOrCalculateDiffResult(this.inputTree, false);
} else {
throw new Error("Missing TreeDiffer");
}
}
private maybeStoreDiffResult(value: (DiffResult | void)) {
if (!(value instanceof DiffResult)) value = null;
this.diffResult = <DiffResult>(value);
}
rebuild(): (Promise<any>| void) {
try {
let firstRun = !this.initialized;
this.init();
let diffResult = this.getDiffResult();
let result = this.wrappedPlugin.rebuild(diffResult);
if (result) {
let resultPromise = <Promise<DiffResult | void>>(result);
if (resultPromise.then) {
// rebuild() -> Promise<>
return resultPromise.then((result: (DiffResult | void)) => {
this.maybeStoreDiffResult(result);
this.relinkOutputAndCachePaths();
});
}
}
this.maybeStoreDiffResult(<(DiffResult | void)>(result));
this.relinkOutputAndCachePaths();
} catch (e) {
e.message = `[${this.description}]: ${e.message}`;
throw e;
}
}
cleanup() {
if (this.wrappedPlugin && this.wrappedPlugin.cleanup) {
this.wrappedPlugin.cleanup();
}
}
private relinkOutputAndCachePaths() {
// just symlink the cache and output tree
fs.rmdirSync(this.outputPath);
symlinkOrCopy.sync(this.cachePath, this.outputPath);
}
private init() {
if (!this.initialized) {
let includeExtensions = this.pluginClass.includeExtensions || [];
let excludeExtensions = this.pluginClass.excludeExtensions || [];
let description = this.description;
this.initialized = true;
if (this.inputPaths) {
this.treeDiffers =
this.inputPaths.map((inputPath) => new TreeDiffer(
description, inputPath, includeExtensions, excludeExtensions));
} else if (this.inputPath) {
this.treeDiffer =
new TreeDiffer(description, this.inputPath, includeExtensions, excludeExtensions);
}
this.wrappedPlugin = new this.pluginClass(this.inputPaths || this.inputPath, this.cachePath,
this.wrappedPluginArguments[1]);
}
}
private stabilizeTrees(trees: BroccoliTree[]) {
// Prevent extensions to prevent array from being mutated from the outside.
// For-loop used to avoid re-allocating a new array.
var stableTrees = [];
for (let i = 0; i < trees.length; ++i) {
// ignore null/undefined input tries in order to support conditional build pipelines
if (trees[i]) {
stableTrees.push(this.stabilizeTree(trees[i]));
}
}
if (stableTrees.length === 0) {
throw new Error('No input trees provided!');
}
return Object.freeze(stableTrees);
}
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.
//
// New-style/rebuild trees should always be stable.
let isNewStyleTree = !!(tree['newStyleTree'] || typeof tree.rebuild === 'function' ||
tree['isReadAPICompatTree'] || tree.constructor['name'] === 'Funnel');
return isNewStyleTree ? tree : stabilizeTree(tree);
}
}
+49
View File
@@ -0,0 +1,49 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/fs-extra/fs-extra.d.ts" />
/// <reference path="./broccoli-writer.d.ts" />
import Writer = require('broccoli-writer');
import fs = require('fs');
import fsx = require('fs-extra');
var minimatch = require('minimatch');
var path = require('path');
var glob = require('glob');
export interface MultiCopyOptions {
/** The path of the file to copy. */
srcPath: string;
/** A list of glob patterns of folders to copy to, matched against the input tree. */
targetPatterns: string[];
/** List of glob patterns to *not* copy to, matched against the matches from `targetPatterns`. */
exclude?: string[];
}
/**
* A writer that copies an input file from an input path into (potentially many) output locations
* given by glob patterns, .
*/
export class MultiCopy extends Writer {
constructor(private inputTree, private options: MultiCopyOptions) { super(); }
write(readTree: (tree) => Promise<string>, destDir: string): Promise<any> {
return readTree(this.inputTree)
.then((inputPath: string) => {
var fileName = path.basename(this.options.srcPath);
var data = fs.readFileSync(path.join(inputPath, this.options.srcPath), 'utf-8');
this.options.targetPatterns.forEach(pattern => {
var paths: string[] = glob.sync(pattern);
paths = paths.filter(p => fs.statSync(p).isDirectory());
if (this.options.exclude) {
paths = paths.filter(p => !this.options.exclude.some((excl) => minimatch(p, excl)));
}
paths.forEach(p => {
var folder = path.join(destDir, p);
fsx.mkdirsSync(folder);
var outputPath = path.join(folder, fileName);
fs.writeFileSync(outputPath, data);
});
});
});
}
}
+378
View File
@@ -0,0 +1,378 @@
/// <reference path="../typings/node/node.d.ts" />
/// <reference path="../typings/jasmine/jasmine.d.ts" />
let mockfs = require('mock-fs');
import fs = require('fs');
import path = require('path');
import {TreeDiffer} from './tree-differ';
describe('TreeDiffer', () => {
afterEach(() => mockfs.restore());
describe('diff of added and changed files', () => {
it('should list all files (but no directories) during the first diff', () => {
let testDir = {
'dir1': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
'subdir-1': {
'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
},
'empty-dir': {}
}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'dir1');
let diffResult = differ.diffTree();
expect(diffResult.addedPaths)
.toEqual(['file-1.txt', 'file-2.txt', 'subdir-1' + path.sep + 'file-1.1.txt']);
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual([]);
});
it('should return empty diff if nothing has changed', () => {
let testDir = {
'dir1': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
'subdir-1': {
'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
},
}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'dir1');
let diffResult = differ.diffTree();
expect(diffResult.addedPaths).not.toEqual([]);
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual([]);
diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual([]);
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual([]);
});
it('should list only changed files during the subsequent diffs', () => {
let testDir = {
'dir1': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
'subdir-1': {
'file-1.1.txt':
mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
}
}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'dir1');
let diffResult = differ.diffTree();
expect(diffResult.addedPaths)
.toEqual(['file-1.txt', 'file-2.txt', 'subdir-1' + path.sep + 'file-1.1.txt']);
// change two files
testDir['dir1']['file-1.txt'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['subdir-1']['file-1.1.txt'] =
mockfs.file({content: 'file-1.1.txt content', mtime: new Date(9999)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.changedPaths)
.toEqual(['file-1.txt', 'subdir-1' + path.sep + 'file-1.1.txt']);
expect(diffResult.removedPaths).toEqual([]);
// change one file
testDir['dir1']['file-1.txt'] = mockfs.file({content: 'super new', mtime: new Date(1000)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-1.txt']);
});
it('should handle changes via symbolic links', () => {
let testDir = {
'orig_path': {
'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)}),
'file-2.txt': mockfs.file({content: 'file-2.txt content', mtime: new Date(1000)}),
'subdir-1': {
'file-1.1.txt': mockfs.file({content: 'file-1.1.txt content', mtime: new Date(1000)})
}
},
'symlinks': {
'file-1.txt': mockfs.symlink({path: '../orig_path/file-1.txt'}),
'file-2.txt': mockfs.symlink({path: '../orig_path/file-2.txt'}),
'subdir-1':
{'file-1.1.txt': mockfs.symlink({path: '../../orig_path/subdir-1/file-1.1.txt'})}
}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'symlinks');
let diffResult = differ.diffTree();
expect(diffResult.addedPaths)
.toEqual(['file-1.txt', 'file-2.txt', 'subdir-1' + path.sep + 'file-1.1.txt']);
// change two files
testDir['orig_path']['file-1.txt'] =
mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['orig_path']['subdir-1']['file-1.1.txt'] =
mockfs.file({content: 'file-1.1.txt content', mtime: new Date(9999)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual([]);
expect(diffResult.changedPaths)
.toEqual(['file-1.txt', 'subdir-1' + path.sep + 'file-1.1.txt']);
expect(diffResult.removedPaths).toEqual([]);
// change one file
testDir['orig_path']['file-1.txt'] =
mockfs.file({content: 'super new', mtime: new Date(1000)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-1.txt']);
// remove a link
delete testDir['orig_path']['file-1.txt'];
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual([]);
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual(['file-1.txt']);
// don't report it as a removal twice
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual([]);
// re-add it.
testDir['orig_path']['file-1.txt'] =
mockfs.file({content: 'super new', mtime: new Date(1000)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual(['file-1.txt']);
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual([]);
});
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': {
'file-1.js': mockfs.file({content: 'file-1.js content', mtime: new Date(1000)}),
'file-2.md': mockfs.file({content: 'file-2.md content', mtime: new Date(1000)}),
'file-3.coffee': mockfs.file({content: 'file-3.coffee content', mtime: new Date(1000)}),
'subdir-1': {
'file-1.1.cc': mockfs.file({content: 'file-1.1.cc content', mtime: new Date(1000)})
}
}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'dir1', ['.js', '.coffee']);
let diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual(['file-1.js', 'file-3.coffee']);
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual([]);
// change two files
testDir['dir1']['file-1.js'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['file-3.coffee'] =
mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['subdir-1']['file-1.1.cc'] =
mockfs.file({content: 'file-1.1.cc content', mtime: new Date(9999)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual([]);
expect(diffResult.changedPaths).toEqual(['file-1.js', 'file-3.coffee']);
expect(diffResult.removedPaths).toEqual([]);
// change one file
testDir['dir1']['file-1.js'] = mockfs.file({content: 'super new', mtime: new Date(1000)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual(['file-1.js']);
});
it('should ignore files with extensions listed in excludeExtensions', () => {
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)})
}
}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'dir1', ['.ts', '.cs'], ['.d.ts', '.d.cs']);
let diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual(['file-1.cs', 'file-1.ts', 'file-1d.cs', 'file-3.ts']);
// change two files
testDir['dir1']['file-1.ts'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['file-1.cs'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['file-1.d.cs'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['file-3.ts'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['file-4.d.ts'] = mockfs.file({content: 'new content', mtime: new Date(1000)});
testDir['dir1']['subdir-1']['file-1.1.cc'] =
mockfs.file({content: 'file-1.1.cc content', mtime: new Date(9999)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual([]);
expect(diffResult.changedPaths).toEqual(['file-1.cs', 'file-1.ts', 'file-3.ts']);
expect(diffResult.removedPaths).toEqual([]);
// change one file
testDir['dir1']['file-4.d.ts'] = mockfs.file({content: 'super new', mtime: new Date(1000)});
mockfs(testDir);
diffResult = differ.diffTree();
expect(diffResult.changedPaths).toEqual([]);
});
});
describe('diff of new files', () => {
it('should detect file additions', () => {
let testDir = {
'dir1':
{'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'dir1');
differ.diffTree();
testDir['dir1']['file-2.txt'] = 'new file';
mockfs(testDir);
let diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual(['file-2.txt']);
expect(diffResult.changedPaths).toEqual([]);
expect(diffResult.removedPaths).toEqual([]);
});
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('testLabel', 'dir1');
differ.diffTree();
testDir['dir1']['file-1.txt'] = 'new content';
testDir['dir1']['file-2.txt'] = 'new file';
mockfs(testDir);
let diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual(['file-2.txt']);
expect(diffResult.changedPaths).toEqual(['file-1.txt']);
});
});
describe('diff of removed files', () => {
it('should detect file removals and report them as removed files', () => {
let testDir = {
'dir1':
{'file-1.txt': mockfs.file({content: 'file-1.txt content', mtime: new Date(1000)})}
};
mockfs(testDir);
let differ = new TreeDiffer('testLabel', 'dir1');
differ.diffTree();
delete testDir['dir1']['file-1.txt'];
mockfs(testDir);
let diffResult = differ.diffTree();
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)})
}
};
mockfs(testDir);
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);
let diffResult = differ.diffTree();
expect(diffResult.addedPaths).toEqual(['file-3.txt']);
expect(diffResult.changedPaths).toEqual(['file-1.txt']);
expect(diffResult.removedPaths).toEqual(['file-2.txt']);
});
});
});
+176
View File
@@ -0,0 +1,176 @@
/// <reference path="../typings/node/node.d.ts" />
import fs = require('fs');
import path = require('path');
function tryStatSync(path) {
try {
return fs.statSync(path);
} catch (e) {
if (e.code === "ENOENT") return null;
throw e;
}
}
export class TreeDiffer {
private fingerprints: {[key: string]: string} = Object.create(null);
private nextFingerprints: {[key: string]: string} = Object.create(null);
private rootDirName: string;
private include: RegExp = null;
private exclude: RegExp = null;
constructor(private label: string, private rootPath: string, includeExtensions?: string[],
excludeExtensions?: string[]) {
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;
function combine(prev, curr) {
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;
}
}
public diffTree(): DiffResult {
let result = new DirtyCheckingDiffResult(this.label, this.rootDirName);
this.dirtyCheckPath(this.rootPath, result);
this.detectDeletionsAndUpdateFingerprints(result);
result.endTime = Date.now();
return result;
}
private dirtyCheckPath(rootDir: string, result: DirtyCheckingDiffResult) {
fs.readdirSync(rootDir).forEach((segment) => {
let absolutePath = path.join(rootDir, segment);
let pathStat = fs.lstatSync(absolutePath);
if (pathStat.isSymbolicLink()) {
pathStat = tryStatSync(absolutePath);
if (pathStat === null) return;
}
if (pathStat.isDirectory()) {
result.directoriesChecked++;
this.dirtyCheckPath(absolutePath, result);
} else {
if (!(this.include && !absolutePath.match(this.include)) &&
!(this.exclude && absolutePath.match(this.exclude))) {
result.filesChecked++;
let relativeFilePath = path.relative(this.rootPath, absolutePath);
switch (this.isFileDirty(absolutePath, pathStat)) {
case FileStatus.Added:
result.addedPaths.push(relativeFilePath);
break;
case FileStatus.Changed:
result.changedPaths.push(relativeFilePath);
}
}
}
});
return result;
}
private isFileDirty(path: string, stat: fs.Stats): FileStatus {
let oldFingerprint = this.fingerprints[path];
let newFingerprint = `${stat.mtime.getTime()} # ${stat.size}`;
this.nextFingerprints[path] = newFingerprint;
if (oldFingerprint) {
this.fingerprints[path] = null;
if (oldFingerprint === newFingerprint) {
// nothing changed
return FileStatus.Unchanged;
}
return FileStatus.Changed;
}
return FileStatus.Added;
}
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.fingerprints[absolutePath] !== null) {
let relativePath = path.relative(this.rootPath, absolutePath);
result.removedPaths.push(relativePath);
}
}
}
this.fingerprints = this.nextFingerprints;
this.nextFingerprints = Object.create(null);
}
}
export class DiffResult {
public addedPaths: string[] = [];
public changedPaths: string[] = [];
public removedPaths: string[] = [];
constructor(public label: string = '') {}
log(verbose: boolean): void {}
toString(): string {
// TODO(@caitp): more meaningful logging
return '';
}
}
class DirtyCheckingDiffResult extends DiffResult {
public filesChecked: number = 0;
public directoriesChecked: number = 0;
public startTime: number = Date.now();
public endTime: number = null;
constructor(label: string, public directoryName: string) { super(label); }
toString() {
return `${pad(this.label, 30)}, ${pad(this.endTime - this.startTime, 5)}ms, ` +
`${pad(this.addedPaths.length + this.changedPaths.length + this.removedPaths.length, 5)} changes ` +
`(files: ${pad(this.filesChecked, 5)}, dirs: ${pad(this.directoriesChecked, 4)})`;
}
log(verbose) {
let prefixedPaths = this.addedPaths.map(p => `+ ${p}`)
.concat(this.changedPaths.map(p => `* ${p}`))
.concat(this.removedPaths.map(p => `- ${p}`));
console.log(`Tree diff: ${this}` + ((verbose && prefixedPaths.length) ?
` [\n ${prefixedPaths.join('\n ')}\n]` :
''));
}
}
function pad(value, length) {
value = '' + value;
let whitespaceLength = (value.length < length) ? length - value.length : 0;
whitespaceLength = whitespaceLength + 1;
return new Array(whitespaceLength).join(' ') + value;
}
enum FileStatus {
Added,
Unchanged,
Changed
}
+71
View File
@@ -0,0 +1,71 @@
// Type definitions for es6-promise
// Project: https://github.com/jakearchibald/ES6-Promise
// Definitions by: François de Campredon <https://github.com/fdecampredon/>, vvakame <https://github.com/vvakame>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
interface Thenable<R> {
then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Thenable<U>;
}
declare class Promise<R> implements Thenable<R> {
/**
* If you call resolve in the body of the callback passed to the constructor,
* your promise is fulfilled with result object passed to resolve.
* If you call reject your promise is rejected with the object passed to resolve.
* For consistency and debugging (eg stack traces), obj should be an instanceof Error.
* Any errors thrown in the constructor callback will be implicitly passed to reject().
*/
constructor(callback: (resolve : (value?: R | Thenable<R>) => void, reject: (error?: any) => void) => void);
/**
* onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects.
* Both are optional, if either/both are omitted the next onFulfilled/onRejected in the chain is called.
* Both callbacks have a single parameter , the fulfillment value or rejection reason.
* "then" returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve.
* If an error is thrown in the callback, the returned promise rejects with that error.
*
* @param onFulfilled called when/if "promise" resolves
* @param onRejected called when/if "promise" rejects
*/
then<U>(onFulfilled?: (value: R) => U | Thenable<U>, onRejected?: (error: any) => U | Thenable<U>): Promise<U>;
/**
* Sugar for promise.then(undefined, onRejected)
*
* @param onRejected called when/if "promise" rejects
*/
catch<U>(onRejected?: (error: any) => U | Thenable<U>): Promise<U>;
}
declare module Promise {
/**
* Make a new promise from the thenable.
* A thenable is promise-like in as far as it has a "then" method.
*/
function resolve<R>(value?: R | Thenable<R>): Promise<R>;
/**
* Make a promise that rejects to obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error
*/
function reject(error: any): Promise<any>;
/**
* Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects.
* the array passed to all can be a mixture of promise-like objects and other objects.
* The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value.
*/
function all<R>(promises: (R | Thenable<R>)[]): Promise<R[]>;
/**
* Make a Promise that fulfills when any item fulfills, and rejects if any item rejects.
*/
function race<R>(promises: (R | Thenable<R>)[]): Promise<R>;
}
declare module 'es6-promise' {
var foo: typeof Promise; // Temp variable to reference Promise in local context
module rsvp {
export var Promise: typeof foo;
}
export = rsvp;
}
+195
View File
@@ -0,0 +1,195 @@
// Type definitions for fs-extra
// Project: https://github.com/jprichardson/node-fs-extra
// Definitions by: midknight41 <https://github.com/midknight41>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
// Imported from: https://github.com/soywiz/typescript-node-definitions/fs-extra.d.ts
///<reference path="../node/node.d.ts"/>
declare module "fs-extra" {
import stream = require("stream");
export interface Stats {
isFile(): boolean;
isDirectory(): boolean;
isBlockDevice(): boolean;
isCharacterDevice(): boolean;
isSymbolicLink(): boolean;
isFIFO(): boolean;
isSocket(): boolean;
dev: number;
ino: number;
mode: number;
nlink: number;
uid: number;
gid: number;
rdev: number;
size: number;
blksize: number;
blocks: number;
atime: Date;
mtime: Date;
ctime: Date;
}
export interface FSWatcher {
close(): void;
}
export class ReadStream extends stream.Readable { }
export class WriteStream extends stream.Writable { }
//extended methods
export function copy(src: string, dest: string, callback?: (err: Error) => void): void;
export function copy(src: string, dest: string, filter: (src: string) => boolean, callback?: (err: Error) => void): void;
export function copySync(src: string, dest: string): void;
export function copySync(src: string, dest: string, filter: (src: string) => boolean): void;
export function createFile(file: string, callback?: (err: Error) => void): void;
export function createFileSync(file: string): void;
export function mkdirs(dir: string, callback?: (err: Error) => void): void;
export function mkdirp(dir: string, callback?: (err: Error) => void): void;
export function mkdirs(dir: string, options?: MkdirOptions, callback?: (err: Error) => void): void;
export function mkdirp(dir: string, options?: MkdirOptions, callback?: (err: Error) => void): void;
export function mkdirsSync(dir: string, options?: MkdirOptions): void;
export function mkdirpSync(dir: string, options?: MkdirOptions): void;
export function outputFile(file: string, data: any, callback?: (err: Error) => void): void;
export function outputFileSync(file: string, data: any): void;
export function outputJson(file: string, data: any, callback?: (err: Error) => void): void;
export function outputJSON(file: string, data: any, callback?: (err: Error) => void): void;
export function outputJsonSync(file: string, data: any): void;
export function outputJSONSync(file: string, data: any): void;
export function readJson(file: string, callback?: (err: Error) => void): void;
export function readJson(file: string, options?: OpenOptions, callback?: (err: Error) => void): void;
export function readJSON(file: string, callback?: (err: Error) => void): void;
export function readJSON(file: string, options?: OpenOptions, callback?: (err: Error) => void): void;
export function readJsonSync(file: string, options?: OpenOptions): void;
export function readJSONSync(file: string, options?: OpenOptions): void;
export function remove(dir: string, callback?: (err: Error) => void): void;
export function removeSync(dir: string): void;
// export function delete(dir: string, callback?: (err: Error) => void): void;
// export function deleteSync(dir: string): void;
export function writeJson(file: string, object: any, callback?: (err: Error) => void): void;
export function writeJson(file: string, object: any, options?: OpenOptions, callback?: (err: Error) => void): void;
export function writeJSON(file: string, object: any, callback?: (err: Error) => void): void;
export function writeJSON(file: string, object: any, options?: OpenOptions, callback?: (err: Error) => void): void;
export function writeJsonSync(file: string, object: any, options?: OpenOptions): void;
export function writeJSONSync(file: string, object: any, options?: OpenOptions): void;
export function rename(oldPath: string, newPath: string, callback?: (err: Error) => void): void;
export function renameSync(oldPath: string, newPath: string): void;
export function truncate(fd: number, len: number, callback?: (err: Error) => void): void;
export function truncateSync(fd: number, len: number): void;
export function chown(path: string, uid: number, gid: number, callback?: (err: Error) => void): void;
export function chownSync(path: string, uid: number, gid: number): void;
export function fchown(fd: number, uid: number, gid: number, callback?: (err: Error) => void): void;
export function fchownSync(fd: number, uid: number, gid: number): void;
export function lchown(path: string, uid: number, gid: number, callback?: (err: Error) => void): void;
export function lchownSync(path: string, uid: number, gid: number): void;
export function chmod(path: string, mode: number, callback?: (err: Error) => void): void;
export function chmod(path: string, mode: string, callback?: (err: Error) => void): void;
export function chmodSync(path: string, mode: number): void;
export function chmodSync(path: string, mode: string): void;
export function fchmod(fd: number, mode: number, callback?: (err: Error) => void): void;
export function fchmod(fd: number, mode: string, callback?: (err: Error) => void): void;
export function fchmodSync(fd: number, mode: number): void;
export function fchmodSync(fd: number, mode: string): void;
export function lchmod(path: string, mode: string, callback?: (err: Error) => void): void;
export function lchmod(path: string, mode: number, callback?: (err: Error) => void): void;
export function lchmodSync(path: string, mode: number): void;
export function lchmodSync(path: string, mode: string): void;
export function stat(path: string, callback?: (err: Error, stats: Stats) => void): void;
export function lstat(path: string, callback?: (err: Error, stats: Stats) => void): void;
export function fstat(fd: number, callback?: (err: Error, stats: Stats) => void): void;
export function statSync(path: string): Stats;
export function lstatSync(path: string): Stats;
export function fstatSync(fd: number): Stats;
export function link(srcpath: string, dstpath: string, callback?: (err: Error) => void): void;
export function linkSync(srcpath: string, dstpath: string): void;
export function symlink(srcpath: string, dstpath: string, type?: string, callback?: (err: Error) => void): void;
export function symlinkSync(srcpath: string, dstpath: string, type?: string): void;
export function readlink(path: string, callback?: (err: Error, linkString: string) => void): void;
export function realpath(path: string, callback?: (err: Error, resolvedPath: string) => void): void;
export function realpath(path: string, cache: string, callback: (err: Error, resolvedPath: string) => void): void;
export function realpathSync(path: string, cache?: boolean): string;
export function unlink(path: string, callback?: (err: Error) => void): void;
export function unlinkSync(path: string): void;
export function rmdir(path: string, callback?: (err: Error) => void): void;
export function rmdirSync(path: string): void;
export function mkdir(path: string, mode?: number, callback?: (err: Error) => void): void;
export function mkdir(path: string, mode?: string, callback?: (err: Error) => void): void;
export function mkdirSync(path: string, mode?: number): void;
export function mkdirSync(path: string, mode?: string): void;
export function readdir(path: string, callback?: (err: Error, files: string[]) => void ): void;
export function readdirSync(path: string): string[];
export function close(fd: number, callback?: (err: Error) => void): void;
export function closeSync(fd: number): void;
export function open(path: string, flags: string, mode?: string, callback?: (err: Error, fs: number) => void): void;
export function openSync(path: string, flags: string, mode?: string): number;
export function utimes(path: string, atime: number, mtime: number, callback?: (err: Error) => void): void;
export function utimesSync(path: string, atime: number, mtime: number): void;
export function futimes(fd: number, atime: number, mtime: number, callback?: (err: Error) => void): void;
export function futimesSync(fd: number, atime: number, mtime: number): void;
export function fsync(fd: number, callback?: (err: Error) => void): void;
export function fsyncSync(fd: number): void;
export function write(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number, callback?: (err: Error, written: number, buffer: NodeBuffer) => void): void;
export function writeSync(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number): number;
export function read(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number, callback?: (err: Error, bytesRead: number, buffer: NodeBuffer) => void ): void;
export function readSync(fd: number, buffer: NodeBuffer, offset: number, length: number, position: number): number;
export function readFile(filename: string, encoding: string, callback: (err: Error, data: string) => void ): void;
export function readFile(filename: string, options: OpenOptions, callback: (err: Error, data: string) => void ): void;
export function readFile(filename: string, callback: (err: Error, data: NodeBuffer) => void ): void;
export function readFileSync(filename: string): NodeBuffer;
export function readFileSync(filename: string, encoding: string): string;
export function readFileSync(filename: string, options: OpenOptions): string;
export function writeFile(filename: string, data: any, encoding?: string, callback?: (err: Error) => void): void;
export function writeFile(filename: string, data: any, options?: OpenOptions, callback?: (err: Error) => void): void;
export function writeFileSync(filename: string, data: any, encoding?: string): void;
export function writeFileSync(filename: string, data: any, option?: OpenOptions): void;
export function appendFile(filename: string, data: any, encoding?: string, callback?: (err: Error) => void): void;
export function appendFile(filename: string, data: any,option?: OpenOptions, callback?: (err: Error) => void): void;
export function appendFileSync(filename: string, data: any, encoding?: string): void;
export function appendFileSync(filename: string, data: any, option?: OpenOptions): void;
export function watchFile(filename: string, listener: { curr: Stats; prev: Stats; }): void;
export function watchFile(filename: string, options: { persistent?: boolean; interval?: number; }, listener: { curr: Stats; prev: Stats; }): void;
export function unwatchFile(filename: string, listener?: Stats): void;
export function watch(filename: string, options?: { persistent?: boolean; }, listener?: (event: string, filename: string) => any): FSWatcher;
export function exists(path: string, callback?: (exists: boolean) => void ): void;
export function existsSync(path: string): boolean;
export function ensureDir(path: string, cb: (err: Error) => void): void;
export interface OpenOptions {
encoding?: string;
flag?: string;
}
export interface MkdirOptions {
fs?: any;
mode?: number;
}
export interface ReadStreamOptions {
flags?: string;
encoding?: string;
fd?: number;
mode?: number;
bufferSize?: number;
}
export interface WriteStreamOptions {
flags?: string;
encoding?: string;
string?: string;
}
export function createReadStream(path: string, options?: ReadStreamOptions): ReadStream;
export function createWriteStream(path: string, options?: WriteStreamOptions): WriteStream;
}
+517
View File
@@ -0,0 +1,517 @@
// Type definitions for Jasmine 2.2
// Project: http://jasmine.github.io/
// Definitions by: Boris Yankov <https://github.com/borisyankov/>, Theodore Brown <https://github.com/theodorejb>, David Pärsson <https://github.com/davidparsson/>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
// For ddescribe / iit use : https://github.com/borisyankov/DefinitelyTyped/blob/master/karma-jasmine/karma-jasmine.d.ts
declare function describe(description: string, specDefinitions: () => void): void;
declare function fdescribe(description: string, specDefinitions: () => void): void;
declare function xdescribe(description: string, specDefinitions: () => void): void;
declare function it(expectation: string, assertion?: () => void): void;
declare function it(expectation: string, assertion?: (done: () => void) => void): void;
declare function fit(expectation: string, assertion?: () => void): void;
declare function fit(expectation: string, assertion?: (done: () => void) => void): void;
declare function xit(expectation: string, assertion?: () => void): void;
declare function xit(expectation: string, assertion?: (done: () => void) => void): void;
/** If you call the function pending anywhere in the spec body, no matter the expectations, the spec will be marked pending. */
declare function pending(reason?: string): void;
declare function beforeEach(action: () => void): void;
declare function beforeEach(action: (done: () => void) => void): void;
declare function afterEach(action: () => void): void;
declare function afterEach(action: (done: () => void) => void): void;
declare function beforeAll(action: () => void): void;
declare function beforeAll(action: (done: () => void) => void): void;
declare function afterAll(action: () => void): void;
declare function afterAll(action: (done: () => void) => void): void;
declare function expect(spy: Function): jasmine.Matchers;
declare function expect(actual: any): jasmine.Matchers;
declare function fail(e?: any): void;
declare function spyOn(object: any, method: string): jasmine.Spy;
declare function runs(asyncMethod: Function): void;
declare function waitsFor(latchMethod: () => boolean, failureMessage?: string, timeout?: number): void;
declare function waits(timeout?: number): void;
declare module jasmine {
var clock: () => Clock;
function any(aclass: any): Any;
function objectContaining(sample: any): ObjectContaining;
export function createSpy(name: string, originalFn?: Function): Spy;
export function createSpyObj(baseName: string, methodNames: any[]): any;
export function createSpyObj<T>(baseName: string, methodNames: any[]): T;
function pp(value: any): string;
function getEnv(): Env;
function addCustomEqualityTester(equalityTester: CustomEqualityTester): void;
function addMatchers(matchers: CustomMatcherFactories): void;
interface Any {
new (expectedClass: any): any;
jasmineMatches(other: any): boolean;
jasmineToString(): string;
}
// taken from TypeScript lib.core.es6.d.ts, applicable to CustomMatchers.contains()
interface ArrayLike<T> {
length: number;
[n: number]: T;
}
interface ObjectContaining {
new (sample: any): any;
jasmineMatches(other: any, mismatchKeys: any[], mismatchValues: any[]): boolean;
jasmineToString(): string;
}
interface Block {
new (env: Env, func: SpecFunction, spec: Spec): any;
execute(onComplete: () => void): void;
}
interface WaitsBlock extends Block {
new (env: Env, timeout: number, spec: Spec): any;
}
interface WaitsForBlock extends Block {
new (env: Env, timeout: number, latchFunction: SpecFunction, message: string, spec: Spec): any;
}
interface Clock {
install(): void;
uninstall(): void;
/** Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.clock().tick function, which takes a number of milliseconds. */
tick(ms: number): void;
}
interface CustomEqualityTester {
(first: any, second: any): boolean;
}
interface CustomMatcher {
compare<T>(actual: T, expected: T): CustomMatcherResult;
compare(actual: any, expected: any): CustomMatcherResult;
}
interface CustomMatcherFactory {
(util: MatchersUtil, customEqualityTesters: Array<CustomEqualityTester>): CustomMatcher;
}
interface CustomMatcherFactories {
[index: string]: CustomMatcherFactory;
}
interface CustomMatcherResult {
pass: boolean;
message: string;
}
interface MatchersUtil {
equals(a: any, b: any, customTesters?: Array<CustomEqualityTester>): boolean;
contains<T>(haystack: ArrayLike<T> | string, needle: any, customTesters?: Array<CustomEqualityTester>): boolean;
buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: Array<any>): string;
}
interface Env {
setTimeout: any;
clearTimeout: void;
setInterval: any;
clearInterval: void;
updateInterval: number;
currentSpec: Spec;
matchersClass: Matchers;
version(): any;
versionString(): string;
nextSpecId(): number;
addReporter(reporter: Reporter): void;
execute(): void;
describe(description: string, specDefinitions: () => void): Suite;
// ddescribe(description: string, specDefinitions: () => void): Suite; Not a part of jasmine. Angular team adds these
beforeEach(beforeEachFunction: () => void): void;
beforeAll(beforeAllFunction: () => void): void;
currentRunner(): Runner;
afterEach(afterEachFunction: () => void): void;
afterAll(afterAllFunction: () => void): void;
xdescribe(desc: string, specDefinitions: () => void): XSuite;
it(description: string, func: () => void): Spec;
// iit(description: string, func: () => void): Spec; Not a part of jasmine. Angular team adds these
xit(desc: string, func: () => void): XSpec;
compareRegExps_(a: RegExp, b: RegExp, mismatchKeys: string[], mismatchValues: string[]): boolean;
compareObjects_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean;
equals_(a: any, b: any, mismatchKeys: string[], mismatchValues: string[]): boolean;
contains_(haystack: any, needle: any): boolean;
addCustomEqualityTester(equalityTester: CustomEqualityTester): void;
addMatchers(matchers: CustomMatcherFactories): void;
specFilter(spec: Spec): boolean;
}
interface FakeTimer {
new (): any;
reset(): void;
tick(millis: number): void;
runFunctionsWithinRange(oldMillis: number, nowMillis: number): void;
scheduleFunction(timeoutKey: any, funcToCall: () => void, millis: number, recurring: boolean): void;
}
interface HtmlReporter {
new (): any;
}
interface HtmlSpecFilter {
new (): any;
}
interface Result {
type: string;
}
interface NestedResults extends Result {
description: string;
totalCount: number;
passedCount: number;
failedCount: number;
skipped: boolean;
rollupCounts(result: NestedResults): void;
log(values: any): void;
getItems(): Result[];
addResult(result: Result): void;
passed(): boolean;
}
interface MessageResult extends Result {
values: any;
trace: Trace;
}
interface ExpectationResult extends Result {
matcherName: string;
passed(): boolean;
expected: any;
actual: any;
message: string;
trace: Trace;
}
interface Trace {
name: string;
message: string;
stack: any;
}
interface PrettyPrinter {
new (): any;
format(value: any): void;
iterateObject(obj: any, fn: (property: string, isGetter: boolean) => void): void;
emitScalar(value: any): void;
emitString(value: string): void;
emitArray(array: any[]): void;
emitObject(obj: any): void;
append(value: any): void;
}
interface StringPrettyPrinter extends PrettyPrinter {
}
interface Queue {
new (env: any): any;
env: Env;
ensured: boolean[];
blocks: Block[];
running: boolean;
index: number;
offset: number;
abort: boolean;
addBefore(block: Block, ensure?: boolean): void;
add(block: any, ensure?: boolean): void;
insertNext(block: any, ensure?: boolean): void;
start(onComplete?: () => void): void;
isRunning(): boolean;
next_(): void;
results(): NestedResults;
}
interface Matchers {
new (env: Env, actual: any, spec: Env, isNot?: boolean): any;
env: Env;
actual: any;
spec: Env;
isNot?: boolean;
message(): any;
(expected: any): boolean;
toEqual(expected: any): boolean;
toMatch(expected: any): boolean;
toBe(expected: any): boolean;
toBeDefined(): boolean;
toBeUndefined(): boolean;
toBeNull(): boolean;
toBeNaN(): boolean;
toBeTruthy(): boolean;
toBeFalsy(): boolean;
toHaveBeenCalled(): boolean;
toHaveBeenCalledWith(...params: any[]): boolean;
toContain(expected: any): boolean;
toBeLessThan(expected: any): boolean;
toBeGreaterThan(expected: any): boolean;
toBeCloseTo(expected: any, precision: any): boolean;
toContainHtml(expected: string): boolean;
toContainText(expected: string): boolean;
toThrow(expected?: any): boolean;
toThrowError(expected?: any): boolean;
not: Matchers;
Any: Any;
}
interface Reporter {
reportRunnerStarting(runner: Runner): void;
reportRunnerResults(runner: Runner): void;
reportSuiteResults(suite: Suite): void;
reportSpecStarting(spec: Spec): void;
reportSpecResults(spec: Spec): void;
log(str: string): void;
}
interface MultiReporter extends Reporter {
addReporter(reporter: Reporter): void;
}
interface Runner {
new (env: Env): any;
execute(): void;
beforeEach(beforeEachFunction: SpecFunction): void;
afterEach(afterEachFunction: SpecFunction): void;
beforeAll(beforeAllFunction: SpecFunction): void;
afterAll(afterAllFunction: SpecFunction): void;
finishCallback(): void;
addSuite(suite: Suite): void;
add(block: Block): void;
specs(): Spec[];
suites(): Suite[];
topLevelSuites(): Suite[];
results(): NestedResults;
}
interface SpecFunction {
(spec?: Spec): void;
}
interface SuiteOrSpec {
id: number;
env: Env;
description: string;
queue: Queue;
}
interface Spec extends SuiteOrSpec {
new (env: Env, suite: Suite, description: string): any;
suite: Suite;
afterCallbacks: SpecFunction[];
spies_: Spy[];
results_: NestedResults;
matchersClass: Matchers;
getFullName(): string;
results(): NestedResults;
log(arguments: any): any;
runs(func: SpecFunction): Spec;
addToQueue(block: Block): void;
addMatcherResult(result: Result): void;
expect(actual: any): any;
waits(timeout: number): Spec;
waitsFor(latchFunction: SpecFunction, timeoutMessage?: string, timeout?: number): Spec;
fail(e?: any): void;
getMatchersClass_(): Matchers;
addMatchers(matchersPrototype: CustomMatcherFactories): void;
finishCallback(): void;
finish(onComplete?: () => void): void;
after(doAfter: SpecFunction): void;
execute(onComplete?: () => void): any;
addBeforesAndAftersToQueue(): void;
explodes(): void;
spyOn(obj: any, methodName: string, ignoreMethodDoesntExist: boolean): Spy;
removeAllSpies(): void;
}
interface XSpec {
id: number;
runs(): void;
}
interface Suite extends SuiteOrSpec {
new (env: Env, description: string, specDefinitions: () => void, parentSuite: Suite): any;
parentSuite: Suite;
getFullName(): string;
finish(onComplete?: () => void): void;
beforeEach(beforeEachFunction: SpecFunction): void;
afterEach(afterEachFunction: SpecFunction): void;
beforeAll(beforeAllFunction: SpecFunction): void;
afterAll(afterAllFunction: SpecFunction): void;
results(): NestedResults;
add(suiteOrSpec: SuiteOrSpec): void;
specs(): Spec[];
suites(): Suite[];
children(): any[];
execute(onComplete?: () => void): void;
}
interface XSuite {
execute(): void;
}
interface Spy {
(...params: any[]): any;
identity: string;
and: SpyAnd;
calls: Calls;
mostRecentCall: { args: any[]; };
argsForCall: any[];
wasCalled: boolean;
callCount: number;
}
interface SpyAnd {
/** By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation. */
callThrough(): Spy;
/** By chaining the spy with and.returnValue, all calls to the function will return a specific value. */
returnValue(val: any): void;
/** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied function. */
callFake(fn: Function): Spy;
/** By chaining the spy with and.throwError, all calls to the spy will throw the specified value. */
throwError(msg: string): void;
/** When a calling strategy is used for a spy, the original stubbing behavior can be returned at any time with and.stub. */
stub(): Spy;
}
interface Calls {
/** By chaining the spy with calls.any(), will return false if the spy has not been called at all, and then true once at least one call happens. **/
any(): boolean;
/** By chaining the spy with calls.count(), will return the number of times the spy was called **/
count(): number;
/** By chaining the spy with calls.argsFor(), will return the arguments passed to call number index **/
argsFor(index: number): any[];
/** By chaining the spy with calls.allArgs(), will return the arguments to all calls **/
allArgs(): any[];
/** By chaining the spy with calls.all(), will return the context (the this) and arguments passed all calls **/
all(): any;
/** By chaining the spy with calls.mostRecent(), will return the context (the this) and arguments for the most recent call **/
mostRecent(): any;
/** By chaining the spy with calls.first(), will return the context (the this) and arguments for the first call **/
first(): any;
/** By chaining the spy with calls.reset(), will clears all tracking for a spy **/
reset(): void;
}
interface Util {
inherit(childClass: Function, parentClass: Function): any;
formatException(e: any): any;
htmlEscape(str: string): string;
argsToArray(args: any): any;
extend(destination: any, source: any): any;
}
interface JsApiReporter extends Reporter {
started: boolean;
finished: boolean;
result: any;
messages: any;
new (): any;
suites(): Suite[];
summarize_(suiteOrSpec: SuiteOrSpec): any;
results(): any;
resultsForSpec(specId: any): any;
log(str: any): any;
resultsForSpecs(specIds: any): any;
summarizeResult_(result: any): any;
}
interface Jasmine {
Spec: Spec;
clock: Clock;
util: Util;
}
export var HtmlReporter: HtmlReporter;
export var HtmlSpecFilter: HtmlSpecFilter;
export var DEFAULT_TIMEOUT_INTERVAL: number;
export interface GlobalPolluter {
describe(description: string, specDefinitions: () => void): void;
fdescribe(description: string, specDefinitions: () => void): void;
xdescribe(description: string, specDefinitions: () => void): void;
it(expectation: string, assertion?: () => void): void;
it(expectation: string, assertion?: (done: () => void) => void): void;
fit(expectation: string, assertion?: () => void): void;
fit(expectation: string, assertion?: (done: () => void) => void): void;
xit(expectation: string, assertion?: () => void): void;
xit(expectation: string, assertion?: (done: () => void) => void): void;
pending(): void;
beforeEach(action: () => void): void;
beforeEach(action: (done: () => void) => void): void;
afterEach(action: () => void): void;
afterEach(action: (done: () => void) => void): void;
beforeAll(action: () => void): void;
beforeAll(action: (done: () => void) => void): void;
afterAll(action: () => void): void;
afterAll(action: (done: () => void) => void): void;
expect(spy: Function): jasmine.Matchers;
expect(actual: any): jasmine.Matchers;
fail(e?: any): void;
spyOn(object: any, method: string): jasmine.Spy;
runs(asyncMethod: Function): void;
waitsFor(latchMethod: () => boolean, failureMessage?: string, timeout?: number): void;
waits(timeout?: number): void;
}
}
+2077
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
/// <reference path="es6-promise/es6-promise.d.ts" />
/// <reference path="fs-extra/fs-extra.d.ts" />
/// <reference path="jasmine/jasmine.d.ts" />
/// <reference path="node/node.d.ts" />