mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
build(dart): Move the broccoli TS2Dart stuff from angular and use it.
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
interface AngularBuilderOptions {
|
||||
outputPath: string;
|
||||
dartSDK?: any;
|
||||
logs?: any;
|
||||
}
|
||||
@@ -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[]) {}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
Vendored
+14
@@ -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;
|
||||
@@ -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'");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+8
@@ -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;
|
||||
}
|
||||
Vendored
+160
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Vendored
+195
@@ -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;
|
||||
}
|
||||
Vendored
+517
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+2077
File diff suppressed because it is too large
Load Diff
Vendored
+4
@@ -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" />
|
||||
Reference in New Issue
Block a user