diff --git a/adapter/pathTransformer.ts b/adapter/pathTransformer.ts index 55684e0..198814d 100644 --- a/adapter/pathTransformer.ts +++ b/adapter/pathTransformer.ts @@ -4,6 +4,7 @@ import * as utils from '../webkit/utilities'; import {DebugProtocol} from 'vscode-debugprotocol'; +import * as path from 'path'; import {ISetBreakpointsArgs, IDebugTransformer, ILaunchRequestArgs, IAttachRequestArgs, IStackTraceResponseBody} from '../webkit/WebKitAdapterInterfaces'; interface IPendingBreakpoint { @@ -21,10 +22,12 @@ export class PathTransformer implements IDebugTransformer { private _clientPathToWebkitUrl = new Map(); private _webkitUrlToClientPath = new Map(); private _pendingBreakpointsByPath = new Map(); + private inferedDeviceRoot :string = null; public launch(args: ILaunchRequestArgs): void { this._webRoot = utils.getAppRoot(args); this._platform = args.platform; + this.inferedDeviceRoot = (this._platform === 'ios') ? 'file://' : this.inferedDeviceRoot; } public attach(args: IAttachRequestArgs): void { @@ -51,7 +54,28 @@ export class PathTransformer implements IDebugTransformer { args.source.path = this._clientPathToWebkitUrl.get(url); utils.Logger.log(`Paths.setBP: Resolved ${url} to ${args.source.path}`); resolve(); - } else { + } + else if (this.inferedDeviceRoot) { + let inferedUrl = url.replace(this._webRoot, this.inferedDeviceRoot).replace(/\\/g, "/"); + + //change device path if {N} core module or {N} module + if (inferedUrl.indexOf("/node_modules/tns-core-modules/") != -1) + { + inferedUrl = inferedUrl.replace("/node_modules/tns-core-modules/", "/app/tns_modules/"); + } + else if (inferedUrl.indexOf("/node_modules/") != -1) + { + inferedUrl = inferedUrl.replace("/node_modules/", "/app/tns_modules/"); + } + + //change platform specific paths + inferedUrl = inferedUrl.replace(`.${this._platform}.`, '.'); + + args.source.path = inferedUrl; + utils.Logger.log(`Paths.setBP: Resolved (by infering) ${url} to ${args.source.path}`); + resolve(); + } + else { utils.Logger.log(`Paths.setBP: No target url cached for client path: ${url}, waiting for target script to be loaded.`); args.source.path = url; this._pendingBreakpointsByPath.set(args.source.path, { resolve, reject, args }); @@ -70,6 +94,17 @@ export class PathTransformer implements IDebugTransformer { public scriptParsed(event: DebugProtocol.Event): void { const webkitUrl: string = event.body.scriptUrl; + if (!this.inferedDeviceRoot && this._platform === "android") + { + this.inferedDeviceRoot = utils.inferDeviceRoot(this._webRoot, this._platform, webkitUrl); + utils.Logger.log("\n\n\n ***Inferred device root: " + this.inferedDeviceRoot + "\n\n\n"); + + if (this.inferedDeviceRoot.indexOf("/data/user/0/") != -1) + { + this.inferedDeviceRoot = this.inferedDeviceRoot.replace("/data/user/0/", "/data/data/"); + } + } + const clientPath = utils.webkitUrlToClientPath(this._webRoot, this._platform, webkitUrl); if (!clientPath) { diff --git a/adapter/sourceMaps/sourceMapTransformer.ts b/adapter/sourceMaps/sourceMapTransformer.ts index e13081c..55cb021 100644 --- a/adapter/sourceMaps/sourceMapTransformer.ts +++ b/adapter/sourceMaps/sourceMapTransformer.ts @@ -56,7 +56,7 @@ export class SourceMapTransformer implements IDebugTransformer { */ public setBreakpoints(args: ISetBreakpointsArgs, requestSeq: number): Promise { return new Promise((resolve, reject) => { - if (this._sourceMaps && args.source.path) { + if (this._sourceMaps && args.source.path && path.extname(args.source.path) !== ".js") { const argsPath = args.source.path; const mappedPath = this._sourceMaps.MapPathFromSource(argsPath); if (mappedPath) { @@ -189,26 +189,8 @@ export class SourceMapTransformer implements IDebugTransformer { let sourceMapUrlValue = event.body.sourceMapURL; - if (!event.body.sourceMapURL) { - - let fileContents = fs.readFileSync(event.body.scriptUrl, 'utf8'); - - var baseRegex = "\\s*[@#]\\s*sourceMappingURL\\s*=\\s*([^\\s]*)"; - - // Matches /* ... */ comments - var blockCommentRegex = new RegExp("/\\*" + baseRegex + "\\s*\\*/"); - - // Matches // .... comments - var commentRegex = new RegExp("//" + baseRegex + "($|\n|\r\n?)"); - - let match = fileContents.match(commentRegex); - if (!match) { - match = fileContents.match(blockCommentRegex); - } - - if (match) { - sourceMapUrlValue = match[1]; - } + if (!sourceMapUrlValue) { + sourceMapUrlValue = this._sourceMaps.FindSourceMapUrlInFile(event.body.scriptUrl); } if (!sourceMapUrlValue || sourceMapUrlValue === "") { @@ -226,6 +208,36 @@ export class SourceMapTransformer implements IDebugTransformer { } } + // private getSourceMappingFile(filePathOrSourceMapValue: string): string { + + // let result = filePathOrSourceMapValue; + + // if (!fs.existsSync(filePathOrSourceMapValue)) { + // return result; + // } + + // let fileContents = fs.readFileSync(filePathOrSourceMapValue, 'utf8'); + + // var baseRegex = "\\s*[@#]\\s*sourceMappingURL\\s*=\\s*([^\\s]*)"; + + // // Matches /* ... */ comments + // var blockCommentRegex = new RegExp("/\\*" + baseRegex + "\\s*\\*/"); + + // // Matches // .... comments + // var commentRegex = new RegExp("//" + baseRegex + "($|\n|\r\n?)"); + + // let match = fileContents.match(commentRegex); + // if (!match) { + // match = fileContents.match(blockCommentRegex); + // } + + // if (match) { + // result = match[1]; + // } + + // return result; + // } + private resolvePendingBreakpoints(sourcePath: string): void { // If there's a setBreakpoints request waiting on this script, go through setBreakpoints again if (this._pendingBreakpointsByPath.has(sourcePath)) { diff --git a/adapter/sourceMaps/sourceMaps.ts b/adapter/sourceMaps/sourceMaps.ts index 678e272..790cada 100644 --- a/adapter/sourceMaps/sourceMaps.ts +++ b/adapter/sourceMaps/sourceMaps.ts @@ -47,6 +47,8 @@ export interface ISourceMaps { * With a known sourceMapURL for a generated script, process create the SourceMap and cache for later */ ProcessNewSourceMap(path: string, sourceMapURL: string): Promise; + + FindSourceMapUrlInFile(generatedFilePath: string): string; } @@ -70,7 +72,7 @@ export class SourceMaps implements ISourceMaps { var map = this._findSourceToGeneratedMapping(pathToSource); if (map) return map.generatedPath(); - return null;; + return null; } public MapFromSource(pathToSource: string, line: number, column: number): MappingResult { @@ -110,92 +112,280 @@ export class SourceMaps implements ISourceMaps { //---- private ----------------------------------------------------------------------- - private _findSourceToGeneratedMapping(pathToSource: string): SourceMap { + private _findSourceToGeneratedMapping(pathToSource: string): SourceMap { - if (pathToSource) { - - if (pathToSource in this._sourceToGeneratedMaps) { - return this._sourceToGeneratedMaps[pathToSource]; - } + if (!pathToSource) { + return null; + } - for (let key in this._generatedToSourceMaps) { - const m = this._generatedToSourceMaps[key]; - if (m.doesOriginateFrom(pathToSource)) { - this._sourceToGeneratedMaps[pathToSource] = m; - return m; - } - } + if (pathToSource in this._sourceToGeneratedMaps) { + return this._sourceToGeneratedMaps[pathToSource]; + } - // not found in existing maps - } - return null; - } + // 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[pathToSource] = m; + return m; + } + } - /** - * pathToGenerated - an absolute local path or a URL. - * mapPath - a path relative to pathToGenerated. - */ - private _findGeneratedToSourceMapping(pathToGenerated: string, mapPath: string): Promise { - if (!pathToGenerated) { - return Promise.resolve(null); + //try finding a map file next to the source file + let generatedFilePath = null; + const pos = pathToSource.lastIndexOf('.'); + if (pos >= 0) { + generatedFilePath = pathToSource.substr(0, pos) + '.js'; } - if (pathToGenerated in this._generatedToSourceMaps) { - return Promise.resolve(this._generatedToSourceMaps[pathToGenerated]); + if (FS.existsSync(generatedFilePath)) { + let parsedSourceMap = this.findGeneratedToSourceMappingSync(generatedFilePath); + if (parsedSourceMap) { + if (parsedSourceMap.doesOriginateFrom(pathToSource)) { + this._sourceToGeneratedMaps[pathToSource] = parsedSourceMap; + return parsedSourceMap; + } + } } - if (mapPath.indexOf("data:application/json;base64,") >= 0) { - Logger.log(`SourceMaps.findGeneratedToSourceMapping: Using inlined sourcemap in ${pathToGenerated}`); + //try finding all js files in app root and parse their source maps + let files = this.walkPath(this._webRoot); + files.forEach(file => { + let parsedSourceMap = this.findGeneratedToSourceMappingSync(file); + if (parsedSourceMap) { + if (parsedSourceMap.doesOriginateFrom(pathToSource)) { + this._sourceToGeneratedMaps[pathToSource] = parsedSourceMap; + return parsedSourceMap; + } + } + }); - // sourcemap is inlined - const pos = mapPath.indexOf(','); - const data = mapPath.substr(pos+1); - try { - const buffer = new Buffer(data, 'base64'); - const json = buffer.toString(); - if (json) { - const map = new SourceMap(pathToGenerated, json, this._webRoot); - this._generatedToSourceMaps[pathToGenerated] = map; - return Promise.resolve(map); + + // let module_files = this.walkPath(Path.join(this._webRoot, "node_modules")); + // module_files.forEach(file => { + // let parsedSourceMap = this.findGeneratedToSourceMappingSync(file); + // if (parsedSourceMap) + // { + // if (parsedSourceMap.doesOriginateFrom(pathToSource)) + // { + // this._sourceToGeneratedMaps[pathToSource] = parsedSourceMap; + // return parsedSourceMap; + // } + // } + // }); + + return null; + // not found in existing maps + } + + /** + * try to find the 'sourceMappingURL' in the file with the given path. + * Returns null in case of errors. + */ + public FindSourceMapUrlInFile(generatedFilePath: string): string { + + try { + const contents = FS.readFileSync(generatedFilePath).toString(); + const lines = contents.split('\n'); + for (let line of lines) { + const matches = SourceMaps.SOURCE_MAPPING_MATCHER.exec(line); + if (matches && matches.length === 2) { + const uri = matches[1].trim(); + Logger.log(`_findSourceMapUrlInFile: source map url at end of generated file '${generatedFilePath}''`); + return uri; } } - catch (e) { - Logger.log(`SourceMaps.findGeneratedToSourceMapping: exception while processing data url (${e.stack})`); + } catch (e) { + // ignore exception + } + return null; + } + + private walkPath(path: string): string[] { + var results = []; + var list = FS.readdirSync(path); + list.forEach(file => { + file = Path.join(path, file); + var stat = FS.statSync(file); + if (stat && stat.isDirectory()) { + results = results.concat(this.walkPath(file)); + } + else { + results.push(file); } + }); - return null; + return results + } + + // /** + // * Loads source map from file system. + // * If no generatedPath is given, the 'file' attribute of the source map is used. + // */ + // private _loadSourceMap(map_path: string, generatedPath?: string): SourceMap { + + // if (map_path in this._allSourceMaps) { + // return this._allSourceMaps[map_path]; + // } + + // try { + // const mp = Path.join(map_path); + // const contents = FS.readFileSync(mp).toString(); + + // const map = new SourceMap(mp, generatedPath, contents); + // this._allSourceMaps[map_path] = map; + + // this._registerSourceMap(map); + + // Logger.log(`_loadSourceMap: successfully loaded source map '${map_path}'`); + + // return map; + // } + // catch (e) { + // Logger.log(`_loadSourceMap: loading source map '${map_path}' failed with exception: ${e}`); + // } + // return null; + // } + + // private _registerSourceMap(map: SourceMap) { + // const gp = map.generatedPath(); + // if (gp) { + // this._generatedToSourceMaps[gp] = map; + // } + // } + + /** + * pathToGenerated - an absolute local path or a URL. + * mapPath - a path relative to pathToGenerated. + */ + private _findGeneratedToSourceMapping(generatedFilePath: string, mapPath: string): Promise { + if (!generatedFilePath) { + return Promise.resolve(null); + } + + if (generatedFilePath in this._generatedToSourceMaps) { + return Promise.resolve(this._generatedToSourceMaps[generatedFilePath]); } + let parsedSourceMap = this.parseInlineSourceMap(mapPath, generatedFilePath); + if (parsedSourceMap) + { + return Promise.resolve(parsedSourceMap); + } + // if path is relative make it absolute if (!Path.isAbsolute(mapPath)) { - if (Path.isAbsolute(pathToGenerated)) { + if (Path.isAbsolute(generatedFilePath)) { // runtime script is on disk, so map should be too - mapPath = PathUtils.makePathAbsolute(pathToGenerated, mapPath); + mapPath = PathUtils.makePathAbsolute(generatedFilePath, mapPath); } else { // runtime script is not on disk, construct the full url for the source map - const scriptUrl = URL.parse(pathToGenerated); + const scriptUrl = URL.parse(generatedFilePath); mapPath = `${scriptUrl.protocol}//${scriptUrl.host}${Path.dirname(scriptUrl.pathname)}/${mapPath}`; } } - return this._createSourceMap(mapPath, pathToGenerated).then(map => { + return this._createSourceMap(mapPath, generatedFilePath).then(map => { if (!map) { - const mapPathNextToSource = pathToGenerated + ".map"; + const mapPathNextToSource = generatedFilePath + ".map"; if (mapPathNextToSource !== mapPath) { - return this._createSourceMap(mapPathNextToSource, pathToGenerated); + return this._createSourceMap(mapPathNextToSource, generatedFilePath); } } return map; }).then(map => { if (map) { - this._generatedToSourceMaps[pathToGenerated] = map; + this._generatedToSourceMaps[generatedFilePath] = map; } return map || null; }); } + + /** + * generatedFilePath - an absolute local path to the generated file + * returns the SourceMap parsed from inlined value or from a map file available next to the generated file + */ + private findGeneratedToSourceMappingSync(generatedFilePath: string): SourceMap { + if (!generatedFilePath) { + return null; + } + + if (generatedFilePath in this._generatedToSourceMaps) { + return this._generatedToSourceMaps[generatedFilePath]; + } + + let sourceMapUrlValue = this.FindSourceMapUrlInFile(generatedFilePath); + if (!sourceMapUrlValue) + { + return null; + } + + let parsedSourceMap = this.parseInlineSourceMap(sourceMapUrlValue, generatedFilePath); + if (parsedSourceMap) { + return parsedSourceMap; + } + + if (!FS.existsSync(generatedFilePath)) { + Logger.log("findGeneratedToSourceMappingSync: can't find the sourceMapping for file: " + generatedFilePath); + return null; + } + + // if path is relative make it absolute + if (!Path.isAbsolute(sourceMapUrlValue)) { + if (Path.isAbsolute(generatedFilePath)) { + // runtime script is on disk, so map should be too + sourceMapUrlValue = PathUtils.makePathAbsolute(generatedFilePath, sourceMapUrlValue); + } else { + // runtime script is not on disk, construct the full url for the source map + // const scriptUrl = URL.parse(generatedFilePath); + // mapPath = `${scriptUrl.protocol}//${scriptUrl.host}${Path.dirname(scriptUrl.pathname)}/${mapPath}`; + + return null; + } + } + + let map = this._createSourceMapSync(sourceMapUrlValue, generatedFilePath); + if (!map) { + const mapPathNextToSource = generatedFilePath + ".map"; + if (mapPathNextToSource !== sourceMapUrlValue) { + map = this._createSourceMapSync(mapPathNextToSource, generatedFilePath); + } + } + + if (map) { + this._generatedToSourceMaps[generatedFilePath] = map; + return map; + } + + return null; + } + + private parseInlineSourceMap(sourceMapContents: string, generatedFilePath: string) : SourceMap + { + if (sourceMapContents.indexOf("data:application/json;base64,") >= 0) { + // sourcemap is inlined + const pos = sourceMapContents.indexOf(','); + const data = sourceMapContents.substr(pos+1); + try { + const buffer = new Buffer(data, 'base64'); + const json = buffer.toString(); + if (json) { + const map = new SourceMap(generatedFilePath, json, this._webRoot); + this._generatedToSourceMaps[generatedFilePath] = map; + return map; + } + } + catch (e) { + Logger.log(`can't parse inlince sourcemap. exception while processing data url (${e.stack})`); + } + } + + return null; + } + private _createSourceMap(mapPath: string, pathToGenerated: string): Promise { let contentsP: Promise; if (utils.isURL(mapPath)) { @@ -230,6 +420,17 @@ export class SourceMaps implements ISourceMaps { } }); } + + private _createSourceMapSync(mapPath: string, pathToGenerated: string): SourceMap { + let contents = FS.readFileSync(mapPath, 'utf8'); + try { + // Throws for invalid contents JSON + return new SourceMap(pathToGenerated, contents, this._webRoot); + } catch (e) { + Logger.log(`SourceMaps.createSourceMap: exception while processing sourcemap: ${e.stack}`); + return null; + } + } } enum Bias { diff --git a/webkit/utilities.ts b/webkit/utilities.ts index bb9bded..5db388c 100644 --- a/webkit/utilities.ts +++ b/webkit/utilities.ts @@ -240,13 +240,13 @@ export function webkitUrlToClientPath(webRoot: string, additionalFileExtension: return ''; } - aUrl = decodeURI(aUrl); - - // If we don't have the client workingDirectory for some reason, don't try to map the url to a client path + // If we don't have the client workingDirectory for some reason, don't try to map the url to a client path if (!webRoot) { return ''; } + aUrl = decodeURI(aUrl); + // Search the filesystem under the webRoot for the file that best matches the given url let pathName = url.parse(canonicalizeUrl(aUrl)).pathname; if (!pathName || pathName === '/') { @@ -285,6 +285,64 @@ export function webkitUrlToClientPath(webRoot: string, additionalFileExtension: return ''; } +/** + * Infers the device root of a given path. + * The device root is the parent directory of all {N} source files + * This implementation assumes that all files are all under one common root on the device + * Returns all the device parent directories of a source file until the file is found on the client by client path + */ +export function inferDeviceRoot(projectRoot: string, additionalFileExtension: string, aUrl: string): string { + if (!aUrl) { + return null; + } + + // If we don't have the projectRoot for some reason, don't try to map the url to a client path + if (!projectRoot) { + return null; + } + + aUrl = decodeURI(aUrl); + + // Search the filesystem under the webRoot for the file that best matches the given url + let pathName = url.parse(canonicalizeUrl(aUrl)).pathname; + if (!pathName || pathName === '/') { + return null; + } + + // Dealing with the path portion of either a url or an absolute path to remote file. + // Need to force path.sep separator + pathName = pathName.replace(/\//g, path.sep); + + let shiftedParts = []; + let pathParts = pathName.split(path.sep); + while (pathParts.length > 0) { + const clientPath = path.join(projectRoot, pathParts.join(path.sep)); + if (existsSync(clientPath)) { + //return canonicalizeUrl(clientPath); + return shiftedParts.join(path.sep).replace(/\\/g, "/"); + } + + let shifted = pathParts.shift(); + shiftedParts.push(shifted); + } + + //check for {N} android internal files + shiftedParts = []; + pathParts = pathName.split(path.sep); + while (pathParts.length > 0) { + const clientPath = path.join(projectRoot, "platforms/android/src/main/assets", pathParts.join(path.sep)); + if (existsSync(clientPath)) { + //return canonicalizeUrl(clientPath); + return shiftedParts.join(path.sep).replace(/\\/g, "/"); + } + + let shifted = pathParts.shift(); + shiftedParts.push(shifted); + } + + return null; +} + /** * Modify a url either from the client or the webkit target to a common format for comparing. * The client can handle urls in this format too. diff --git a/webkit/webKitDebugAdapter.ts b/webkit/webKitDebugAdapter.ts index 7d94a60..bd5df18 100644 --- a/webkit/webKitDebugAdapter.ts +++ b/webkit/webKitDebugAdapter.ts @@ -411,7 +411,7 @@ export class WebKitDebugAdapter implements IDebugAdapter { // unverified breakpoints. if (response.error || !response.result.locations.length) { return { - verified: false, + verified: !response.error, line: requestLines[i], column: 0 };