From b828a7145a90afbfea74967a714d11cca0db2a7c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 16 Sep 2016 12:13:52 +0200 Subject: [PATCH] Implement proactive sourcemap loading - fix #47 --- package.json | 3 +- src/chrome/chromeDebugAdapter.ts | 30 ++- src/sourceMaps/sourceMapFactory.ts | 29 ++- src/sourceMaps/sourceMaps.ts | 1 + src/sourceMaps/uri.ts | 94 +++++++ src/transformers/basePathTransformer.ts | 6 + src/transformers/eagerSourceMapTransformer.ts | 232 ++++-------------- src/transformers/lazySourceMapTransformer.ts | 12 + src/transformers/lineNumberTransformer.ts | 4 + src/utils.ts | 129 ++++++++++ typings.json | 1 + 11 files changed, 354 insertions(+), 187 deletions(-) create mode 100644 src/sourceMaps/uri.ts diff --git a/package.json b/package.json index 8a12b172c..a0cc16089 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "typings": "./lib/src/index", "main": "./out/src/index", "dependencies": { - "globby": "^6.0.0", + "glob": "^7.0.6", + "request-light": "^0.1.0", "source-map": "^0.5.6", "vscode-debugadapter": "^1.12.0", "vscode-debugprotocol": "^1.12.0", diff --git a/src/chrome/chromeDebugAdapter.ts b/src/chrome/chromeDebugAdapter.ts index ac8d68eb8..2a50bca9b 100644 --- a/src/chrome/chromeDebugAdapter.ts +++ b/src/chrome/chromeDebugAdapter.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ import {DebugProtocol} from 'vscode-debugprotocol'; -import {StoppedEvent, InitializedEvent, TerminatedEvent, OutputEvent, Handles, ContinuedEvent} from 'vscode-debugadapter'; +import {StoppedEvent, InitializedEvent, TerminatedEvent, OutputEvent, Handles, ContinuedEvent, BreakpointEvent} from 'vscode-debugadapter'; import {ILaunchRequestArgs, ISetBreakpointsArgs, ISetBreakpointsResponseBody, IStackTraceResponseBody, IAttachRequestArgs, IScopesResponseBody, IVariablesResponseBody, @@ -23,6 +23,7 @@ import {LineNumberTransformer} from '../transformers/lineNumberTransformer'; import {BasePathTransformer} from '../transformers/basePathTransformer'; import {RemotePathTransformer} from '../transformers/remotePathTransformer'; import {LazySourceMapTransformer} from '../transformers/lazySourceMapTransformer'; +import {EagerSourceMapTransformer} from '../transformers/eagerSourceMapTransformer'; import * as path from 'path'; @@ -39,6 +40,7 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { private _clientAttached: boolean; private _variableHandles: Handles; + private _breakpointIdHandles: utils.ReverseHandles; private _currentStack: Chrome.Debugger.CallFrame[]; private _committedBreakpointsByUrl: Map; private _overlayHelper: utils.DebounceHelper; @@ -64,10 +66,12 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { this._chromeConnection = chromeConnection || new ChromeConnection(); this._variableHandles = new Handles(); + this._breakpointIdHandles = new utils.ReverseHandles(); + this._overlayHelper = new utils.DebounceHelper(/*timeoutMs=*/200); this._lineNumberTransformer = lineNumberTransformer || new LineNumberTransformer(/*targetLinesStartAt1=*/false); - this._sourceMapTransformer = sourceMapTransformer || new LazySourceMapTransformer(); + this._sourceMapTransformer = sourceMapTransformer || new EagerSourceMapTransformer(); this._pathTransformer = pathTransformer || new RemotePathTransformer(); this.clearEverything(); @@ -308,6 +312,19 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { const committedBps = this._committedBreakpointsByUrl.get(script.url) || []; committedBps.push(params.breakpointId); this._committedBreakpointsByUrl.set(script.url, committedBps); + + // TODO - Handle bps removed before resolve (can set ID on BP) + // and #38 + const bp = { + id: this._breakpointIdHandles.lookup(params.breakpointId), + verified: true, + line: params.location.lineNumber, + column: params.location.columnNumber + }; + const scriptPath = this._pathTransformer.breakpointResolved(bp, script.url); + this._sourceMapTransformer.breakpointResolved(bp, scriptPath); + this._lineNumberTransformer.breakpointResolved(bp); + this.sendEvent(new BreakpointEvent('new', bp)); } protected onConsoleMessage(params: Chrome.Console.MessageAddedParams): void { @@ -326,6 +343,7 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { } public setBreakpoints(args: ISetBreakpointsArgs, requestSeq: number): Promise { + // const requestedLines = args.lines; this._lineNumberTransformer.setBreakpoints(args); return this._sourceMapTransformer.setBreakpoints(args, requestSeq) .then(() => this._pathTransformer.setBreakpoints(args)) @@ -353,8 +371,8 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { // Swallow errors in the promise queue chain so it doesn't get blocked, but return the failing promise for error handling. this._setBreakpointsRequestQ = setBreakpointsPTimeout.catch(() => undefined); return setBreakpointsPTimeout.then(body => { - this._lineNumberTransformer.setBreakpointsResponse(body); this._sourceMapTransformer.setBreakpointsResponse(body, requestSeq); + this._lineNumberTransformer.setBreakpointsResponse(body); return body; }); } else { @@ -435,17 +453,21 @@ export abstract class ChromeDebugAdapter extends BaseDebugAdapter { // Map committed breakpoints to DebugProtocol response breakpoints return responses .map((response, i) => { + const id = response.result ? this._breakpointIdHandles.create(response.result.breakpointId) : undefined; + // The output list needs to be the same length as the input list, so map errors to // unverified breakpoints. if (response.error || !response.result.actualLocation) { return { + id, verified: false, line: requestLines[i], - column: 0 + column: 0, }; } return { + id, verified: true, line: response.result.actualLocation.lineNumber, column: response.result.actualLocation.columnNumber diff --git a/src/sourceMaps/sourceMapFactory.ts b/src/sourceMaps/sourceMapFactory.ts index e2f141f83..7d5fa4bcc 100644 --- a/src/sourceMaps/sourceMapFactory.ts +++ b/src/sourceMaps/sourceMapFactory.ts @@ -5,6 +5,9 @@ import * as path from 'path'; import * as url from 'url'; import * as fs from 'fs'; +import * as os from 'os'; +import * as crypto from 'crypto'; +const xhr = require('request-light'); import * as sourceMapUtils from './sourceMapUtils'; import * as utils from '../utils'; @@ -119,4 +122,28 @@ function loadSourceMapContents(mapPathOrURL: string): Promise { } return contentsP; -} \ No newline at end of file +} + +function downloadSourceMapContents(sourceMapUri: string): Promise { + // use sha256 to ensure the hash value can be used in filenames + const hash = crypto.createHash('sha256').update(sourceMapUri).digest('hex'); + + const cachePath = path.join(os.tmpdir(), 'com.microsoft.VSCode', 'node-debug2', 'sm-cache'); + const sourceMapPath = path.join(cachePath, hash); + + const exists = fs.existsSync(sourceMapPath); + if (exists) { + return loadSourceMapContents(sourceMapPath); + } + + const options = { + url: sourceMapUri, + followRedirects: 5 + }; + + return xhr.xhr(options) + .then( + response => this._writeFile(path, response.responseText) + .then(() => response.responseText), + error => utils.errP(xhr.getErrorStatusDescription(error.status))); +} diff --git a/src/sourceMaps/sourceMaps.ts b/src/sourceMaps/sourceMaps.ts index 851d598cf..35f3a4dcf 100644 --- a/src/sourceMaps/sourceMaps.ts +++ b/src/sourceMaps/sourceMaps.ts @@ -2,6 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import {URI} from './uri'; import {SourceMap, MappedPosition} from './sourceMap'; import {getMapForGeneratedPath} from './sourceMapFactory'; import {ISourceMapPathOverrides} from '../debugAdapterInterfaces'; diff --git a/src/sourceMaps/uri.ts b/src/sourceMaps/uri.ts new file mode 100644 index 000000000..29a603a76 --- /dev/null +++ b/src/sourceMaps/uri.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as Path from 'path'; +import * as URL from 'url'; + +/** + * Helper code copied from vscode-node-debug that's only relevant for SourceMaps right now, + * but should be merged with other url/path handling code, or removed. + */ + +function makePathAbsolute(absPath: string, relPath: string): string { + return Path.resolve(Path.dirname(absPath), relPath); +} + +export class URI { + private _uri: string; + private _u: URL.Url; + + static file(path: string, base?: string) { + + if (!Path.isAbsolute(path)) { + path = makePathAbsolute(base, path); + } + if (path[0] === '/') { + path = 'file://' + path; + } else { + path = 'file:///' + path; + } + + const u = new URI(); + u._uri = path; + try { + u._u = URL.parse(path); + } + catch (e) { + throw new Error(e); + } + return u; + } + + static parse(uri: string, base?: string) { + + if (uri.indexOf('http:') === 0 || uri.indexOf('https:') === 0 || uri.indexOf('file:') === 0 || uri.indexOf('data:') === 0 ) { + const u = new URI(); + u._uri = uri; + try { + u._u = URL.parse(uri); + } + catch (e) { + throw new Error(e); + } + return u; + } + return URI.file(uri, base); + } + + constructor() { + } + + uri(): string { + return this._uri; + } + + isFile(): boolean { + return this._u.protocol === 'file:'; + } + + filePath(): string { + let path = this._u.path; + if (/^\/[a-zA-Z]\:\//.test(path)) { + path = path.substr(1); // remove additional '/' + } + return path; + } + + isData() { + return this._u.protocol === 'data:' && this._uri.indexOf('application/json') > 0 && this._uri.indexOf('base64') > 0; + } + + data(): string { + const pos = this._uri.lastIndexOf(','); + if (pos > 0) { + return this._uri.substr(pos+1); + } + return null; + } + + isHTTP(): boolean { + return this._u.protocol === 'http:' || this._u.protocol === 'https:'; + } +} diff --git a/src/transformers/basePathTransformer.ts b/src/transformers/basePathTransformer.ts index 71878e2e6..9a11c2fe3 100644 --- a/src/transformers/basePathTransformer.ts +++ b/src/transformers/basePathTransformer.ts @@ -2,6 +2,8 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +import {DebugProtocol} from 'vscode-debugprotocol'; + import {ISetBreakpointsArgs, ILaunchRequestArgs, IAttachRequestArgs, IStackTraceResponseBody} from '../debugAdapterInterfaces'; /** @@ -30,6 +32,10 @@ export class BasePathTransformer { return scriptPath; } + public breakpointResolved(bp: DebugProtocol.Breakpoint, scriptPath: string): string { + return scriptPath; + } + public stackTraceResponse(response: IStackTraceResponseBody): void { // Have a responsibility to clean up the sourceReference here when it's not needed... See #93 response.stackFrames.forEach(frame => { diff --git a/src/transformers/eagerSourceMapTransformer.ts b/src/transformers/eagerSourceMapTransformer.ts index 1268cf6c0..f463839aa 100644 --- a/src/transformers/eagerSourceMapTransformer.ts +++ b/src/transformers/eagerSourceMapTransformer.ts @@ -5,8 +5,6 @@ import * as path from 'path'; import * as fs from 'fs'; -const globby = require('globby'); - import {LazySourceMapTransformer} from './lazySourceMapTransformer'; import {ISetBreakpointsArgs, ILaunchRequestArgs, IAttachRequestArgs, @@ -15,167 +13,65 @@ import {SourceMaps} from '../sourceMaps/sourceMaps'; import * as utils from '../utils'; import * as logger from '../logger'; +/** + * Load SourceMaps on launch. Requires reading the file and parsing out the sourceMappingURL, because + * if you wait until the script is loaded as in LazySMT, you get that info from the runtime. + */ export class EagerSourceMapTransformer extends LazySourceMapTransformer { + private static SOURCE_MAPPING_MATCHER = new RegExp('^//[#@] ?sourceMappingURL=(.+)$'); private _preLoad: Promise; protected init(args: ILaunchRequestArgs | IAttachRequestArgs): void { - const outDirs = args.outDirs ? - args.outDirs : - args.outDir ? - [path.join(args.outDir, '**/*.js')] : - []; - - this._preLoad = globby(outDirs) - .then(paths => Promise.all(paths.map(path => this.findSourceMapForFile(path)))); - } - - private findSourceMapForFile(path: string): Promise { - - } - - private - - /** - * Find the sourcemap and load it, then call super, so it already knows about it. - */ - public setBreakpoints(args: ISetBreakpointsArgs, requestSeq: number): Promise { - return super.setBreakpoints(args, requestSeq); - } - - private discoverAllSourceMaps(): Promise { - // search for all map files in generatedCodeDirectory - if (this._generatedCodeDirectory) { - return this.discoverSourceMapsInDirectory(this._generatedCodeDirectory); - } else { - return Promise.resolve(); - } - } - - private discoverSourceMapsInDirectory(dirPath: string): Promise { - return utils.fsReadDirP(dirPath).then(files => { - const maps = files.filter(f => path.extname(f).toLowerCase() === '.map'); - for (let map_name of maps) { - const map_path = Path.join(this._generatedCodeDirectory, map_name); - const m = this._loadSourceMap(map_path); - if (m && m.doesOriginateFrom(pathToSource)) { - this._log(`_findSourceToGeneratedMapping: found source map for source ${pathToSource} in outDir`); - this._sourceToGeneratedMaps[pathToSourceKey] = m; - return Promise.resolve(m); - } - } - }, - err => { - // Log error and continue - logger.error('Error finding sourcemap files in outDir: ' + err.message); - }); - } - - /** - * Tries to find a SourceMap for the given source. - * This is difficult because the source does not contain any information about where - * the generated code or the source map is located. - * Our strategy is as follows: - * - search in all known source maps whether if refers to this source in the sources array. - * - ... - */ - private findSourceToGeneratedMapping(pathToSource: string): Promise { - if (!pathToSource) { - return Promise.resolve(null); - } - - const pathToSourceKey = pathNormalize(pathToSource); - - // try to find in existing - if (pathToSourceKey in this._sourceToGeneratedMaps) { - return Promise.resolve(this._sourceToGeneratedMaps[pathToSourceKey]); - } - - // a reverse lookup: in all source maps try to find pathToSource in the sources array - for (let key in this._generatedToSourceMaps) { - const m = this._generatedToSourceMaps[key]; - if (m.doesOriginateFrom(pathToSource)) { - this._sourceToGeneratedMaps[pathToSourceKey] = m; - return Promise.resolve(m); + super.init(args); + if (args.sourceMaps) { + const generatedCodeGlobs = args.outDirs ? + args.outDirs : + args.outDir ? + [path.join(args.outDir, '**/*.js')] : + []; + + // try to find all source files upfront asynchronously + if (generatedCodeGlobs.length > 0) { + logger.log('SourceMaps: preloading sourcemaps for scripts in globs: ' + JSON.stringify(generatedCodeGlobs)); + this._preLoad = utils.multiGlob(generatedCodeGlobs) + .then(paths => { + logger.log(`SourceMaps: expanded globs and found ${paths.length} scripts`); + return Promise.all(paths.map(scriptPath => this.discoverSourceMapForGeneratedScript(scriptPath))); + }) + .then(() => { }); + } else { + this._preLoad = Promise.resolve(); } } + } - // no map found - - let pathToGenerated = pathToSource; - const ext = Path.extname(pathToSource); - if (ext !== '.js') { - // use heuristic: change extension to ".js" and find a map for it - const pos = pathToSource.lastIndexOf('.'); - if (pos >= 0) { - pathToGenerated = pathToSource.substr(0, pos) + '.js'; - } - } - - let map: SourceMap = null; - - return Promise.resolve(map).then(map => { - - // first look into the generated code directory - if (this._generatedCodeDirectory) { - const promises = new Array>(); - let rest = PathUtils.makeRelative(this._generatedCodeDirectory, pathToGenerated); - while (rest) { - const path = Path.join(this._generatedCodeDirectory, rest); - promises.push(this._findGeneratedToSourceMapping(path)); - rest = PathUtils.removeFirstSegment(rest); - } - return Promise.all(promises).then(results => { - for (let r of results) { - if (r) { - return r; - } - } - return null; - }); - } - return map; - - }).then(map => { - - // VSCode extension host support: - // we know that the plugin has an "out" directory next to the "src" directory - if (map === null) { - let srcSegment = Path.sep + 'src' + Path.sep; - if (pathToGenerated.indexOf(srcSegment) >= 0) { - const outSegment = Path.sep + 'out' + Path.sep; - return this._findGeneratedToSourceMapping(pathToGenerated.replace(srcSegment, outSegment)); + private discoverSourceMapForGeneratedScript(generatedScriptPath: string): Promise { + return this.findSourceMapUrlInFile(generatedScriptPath) + .then(uri => { + if (uri) { + logger.log(`SourceMaps: sourcemap url parsed from end of generated content: ${uri}`); + return this._sourceMaps.processNewSourceMap(generatedScriptPath, uri); + } else { + logger.log(`SourceMaps: no sourcemap url found in generated script: ${generatedScriptPath}`); } - } - return map; - - }).then(map => { - - if (map === null && pathNormalize(pathToGenerated) !== pathToSourceKey) { - return this._findGeneratedToSourceMapping(pathToGenerated); - } - return map; - - }).then(map => { - - if (map) { - this._sourceToGeneratedMaps[pathToSourceKey] = map; - } - return map; - - }); + }) + .catch(err => { + // If we fail to preload one, ignore and keep going + logger.log(`SourceMaps: could not preload for generated script: ${generatedScriptPath}. Error: ${err.toString()}`); + }); } - private _findSourceMapUrlInFile(pathToGenerated: string, content?: string): Promise { - + /** + * Try to find the 'sourceMappingURL' in content or the file with the given path. + * Returns null if no source map url is found or if an error occured. + */ + private findSourceMapUrlInFile(pathToGenerated: string, content?: string): Promise { if (content) { - return Promise.resolve(this._findSourceMapUrl(content)); + return Promise.resolve(this.findSourceMapUrl(content)); } - return this._readFile(pathToGenerated).then(content => { - return this._findSourceMapUrl(content, pathToGenerated); - }).catch(err => { - return null; - }); + return utils.readFileP(pathToGenerated) + .then(content => this.findSourceMapUrl(content)); } /** @@ -183,42 +79,16 @@ export class EagerSourceMapTransformer extends LazySourceMapTransformer { * Relative file paths are converted into absolute paths. * Returns null if no source map url is found. */ - private _findSourceMapUrl(contents: string, pathToGenerated?: string): string { - + private findSourceMapUrl(contents: string): string { const lines = contents.split('\n'); - for (let l = lines.length-1; l >= Math.max(lines.length-10, 0); l--) { // only search for url in the last 10 lines + for (let l = lines.length - 1; l >= Math.max(lines.length - 10, 0); l--) { // only search for url in the last 10 lines const line = lines[l].trim(); - const matches = SourceMaps.SOURCE_MAPPING_MATCHER.exec(line); + const matches = EagerSourceMapTransformer.SOURCE_MAPPING_MATCHER.exec(line); if (matches && matches.length === 2) { - let uri = matches[1].trim(); - if (pathToGenerated) { - this._log(`_findSourceMapUrl: source map url found at end of generated file '${pathToGenerated}'`); - } else { - this._log(`_findSourceMapUrl: source map url found at end of generated content`); - } - - const u = URL.parse(uri); - if (u.protocol === 'file:' || u.protocol == null) { - - // a local file path - let map_path = decodeURI(u.path); - - if (!map_path) { - throw new Error(`no path or empty path`); - } - - // if path is relative make it absolute - if (!Path.isAbsolute(map_path)) { - if (pathToGenerated) { - uri = PathUtils.makePathAbsolute(pathToGenerated, map_path); - } else { - throw new Error(`relative path but no base given`); - } - } - } - return uri; + return matches[1].trim(); } } + return null; } } diff --git a/src/transformers/lazySourceMapTransformer.ts b/src/transformers/lazySourceMapTransformer.ts index 05f4be707..d9a682514 100644 --- a/src/transformers/lazySourceMapTransformer.ts +++ b/src/transformers/lazySourceMapTransformer.ts @@ -3,6 +3,7 @@ *--------------------------------------------------------*/ import * as path from 'path'; +import {DebugProtocol} from 'vscode-debugprotocol'; import {ISetBreakpointsArgs, ILaunchRequestArgs, IAttachRequestArgs, ISetBreakpointsResponseBody, IStackTraceResponseBody, ISourceMapPathOverrides} from '../debugAdapterInterfaces'; @@ -213,6 +214,17 @@ export class LazySourceMapTransformer { } } + public breakpointResolved(bp: DebugProtocol.Breakpoint, scriptPath: string): void { + if (this._sourceMaps) { + const mapped = this._sourceMaps.mapToAuthored(scriptPath, bp.line, bp.column); + if (mapped) { + // Not sending back the path here, since the bp has an ID + bp.line = mapped.line; + bp.column = mapped.column; + } + } + } + /** * Resolve any pending breakpoints for this script */ diff --git a/src/transformers/lineNumberTransformer.ts b/src/transformers/lineNumberTransformer.ts index 49ce2f687..ac72cf39b 100644 --- a/src/transformers/lineNumberTransformer.ts +++ b/src/transformers/lineNumberTransformer.ts @@ -33,6 +33,10 @@ export class LineNumberTransformer implements IDebugTransformer { response.stackFrames.forEach(frame => frame.line = this.convertTargetLineToClient(frame.line)); } + public breakpointResolved(bp: DebugProtocol.Breakpoint): void { + bp.line = this.convertTargetLineToClient(bp.line); + } + private convertClientLineToTarget(line: number): number { if (this._targetLinesStartAt1) { return this._clientLinesStartAt1 ? line : line + 1; diff --git a/src/utils.ts b/src/utils.ts index 137754179..08bb0756b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,8 @@ import * as os from 'os'; import * as fs from 'fs'; import * as url from 'url'; import * as path from 'path'; +import * as glob from 'glob'; +import {Handles} from 'vscode-debugadapter'; import * as logger from './logger'; @@ -282,3 +284,130 @@ export function fsReadDirP(path: string): Promise { }); }); } + +export function readFileP(path: string, encoding: string = 'utf8'): Promise { + return new Promise((resolve, reject) => { + fs.readFile(path, encoding, (err, fileContents) => { + if (err) { + reject(err); + } else { + resolve(fileContents); + } + }); + }); +} + +export function writeFileP(filePath: string, data: string): Promise { + return new Promise((resolve, reject) => { + mkdirs(path.dirname(filePath)); + fs.writeFile(filePath, data, err => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +/** + * Make sure that all directories of the given path exist (like mkdir -p). + */ +export function mkdirs(dirsPath: string) { + if (!fs.existsSync(dirsPath)) { + mkdirs(path.dirname(dirsPath)); + fs.mkdirSync(dirsPath); + } +} + +//---- globbing support ------------------------------------------------- + +export function extendObject(objectCopy: T, object: T): T { + for (let key in object) { + if (object.hasOwnProperty(key)) { + objectCopy[key] = object[key]; + } + } + + return objectCopy; +} + +function isExclude(pattern: string) { + return pattern[0] === '!'; +} + +interface IGlobTask { + pattern: string; + opts: any; +} + +export function multiGlob(patterns: string[], opts?: any): Promise { + const globTasks = new Array(); + + opts = extendObject({ + cache: Object.create(null), + statCache: Object.create(null), + realpathCache: Object.create(null), + symlinks: Object.create(null), + ignore: [] + }, opts); + + try { + patterns.forEach((pattern, i) => { + if (isExclude(pattern)) { + return; + } + + const ignore = patterns.slice(i).filter(isExclude).map(pattern => { + return pattern.slice(1); + }); + + globTasks.push({ + pattern, + opts: extendObject(extendObject({}, opts), { + ignore: opts.ignore.concat(ignore) + }) + }); + }); + } catch (err) { + return Promise.reject(err); + } + + return Promise.all(globTasks.map(task => { + return new Promise((c, e) => { + glob(task.pattern, task.opts, (err, files: string[]) => { + if (err) { + e(err); + } else { + c(files); + } + }); + }); + })).then(results => { + const set = new Set(); + for (let paths of results) { + for (let p of paths) { + set.add(p); + } + } + + let array = []; + set.forEach(v => array.push(v)); + return array; + }); +} + +export class ReverseHandles extends Handles { + private _reverseMap = new Map(); + + public create(value: T): number { + const handle = super.create(value); + this._reverseMap.set(value, handle); + + return handle; + } + + public lookup(value: T): number { + return this._reverseMap.get(value); + } +} \ No newline at end of file diff --git a/typings.json b/typings.json index 4e36c9123..7e66c2dc4 100644 --- a/typings.json +++ b/typings.json @@ -5,6 +5,7 @@ "globalDependencies": { "es6-collections": "registry:dt/es6-collections#0.5.1+20160316155526", "es6-promise": "registry:dt/es6-promise#0.0.0+20160423074304", + "glob": "registry:dt/glob#5.0.10+20160317120654", "mocha": "registry:dt/mocha#2.2.5+20160317120654", "mockery": "registry:dt/mockery#1.4.0+20160316155526", "node": "registry:dt/node#6.0.0+20160524002506",