import * as Path from "path" ; import * as FS from "fs" ; import { SourceMapConsumer , MappedPosition , Position , BasicSourceMapConsumer } from "source-map" ; import * as PathUtils from "./pathUtilities" ; const util = require ( "../node_modules/source-map/lib/util.js" ) ; export interface MappingResult { path : string ; content ? : string ; line : number ; column : number ; } export enum Bias { GREATEST_LOWER_BOUND = 1 , LEAST_UPPER_BOUND = 2 } export interface ISourceMaps { MapPathFromSource ( path , roots ) : SourceMap ; MapFromSource ( path , line , column , roots , bias ? ) : MappingResult ; MapToSource ( path , line , column , roots , bias ? ) : MappingResult ; } export class SourceMaps implements ISourceMaps { private static SOURCE_MAPPING_MATCHER = new RegExp ( "//[#@] ?sourceMappingURL=(.+)$" ) ; private _allSourceMaps : { [ id ] : SourceMap ; } = { } ; private _generatedToSourceMaps : { [ id ] : SourceMap ; } = { } ; private _sourceToGeneratedMaps : { [ id ] : SourceMap ; } = { } ; private _generatedCodeDirectory : string ; public constructor ( generatedCodeDirectory ) { this . _generatedCodeDirectory = generatedCodeDirectory ; } public MapPathFromSource ( pathToSource , roots ) { const map = this . FindSourceToGeneratedMapping ( pathToSource , roots ) ; if ( map ) { return map ; } return null ; } public MapFromSource ( pathToSource , line , column , roots , bias ? ) { const map = this . FindSourceToGeneratedMapping ( pathToSource , roots ) ; if ( map ) { const mr = map . generatedPositionFor ( pathToSource , line , column , bias ) ; if ( mr && typeof mr . line === "number" ) { return { path : map . generatedPath ( ) , line : mr . line , column : mr . column } ; } } return null ; } public MapToSource ( pathToGenerated , line , column , roots , bias ? ) { const map = this . _findGeneratedToSourceMapping ( pathToGenerated , roots ) ; if ( map ) { const mr = map . originalPositionFor ( line , column , bias ) ; if ( mr && mr . source ) { return { path : mr . source , content : ( < any > mr ) . content , line : mr . line , column : mr . column } ; } } return null ; } /**
     * 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.
     * - ...
     */ public FindSourceToGeneratedMapping ( pathToSource , roots ) { if ( ! pathToSource ) { return null ; } if ( pathToSource in this . _sourceToGeneratedMaps ) { return this . _sourceToGeneratedMaps [ pathToSource ] ; } for ( let key in this . _generatedToSourceMaps ) { const m = this . _generatedToSourceMaps [ key ] ; if ( m . doesOriginateFrom ( pathToSource ) ) { this . _sourceToGeneratedMaps [ pathToSource ] = m ; return m ; } } if ( this . _generatedCodeDirectory ) { try { let maps = FS . readdirSync ( this . _generatedCodeDirectory ) . filter ( e => Path . extname ( e . toLowerCase ( ) ) === ".map" ) ; for ( let map_name of maps ) { const map_path = Path . join ( this . _generatedCodeDirectory , map_name ) ; const m = this . _loadSourceMap ( map_path , roots ) ; if ( m && m . doesOriginateFrom ( pathToSource ) ) { this . _sourceToGeneratedMaps [ pathToSource ] = m ; return m ; } } } catch ( e ) { } } let pathToGenerated = pathToSource ; const ext = Path . extname ( pathToSource ) ; if ( ext !== ".js" ) { const pos = pathToSource . lastIndexOf ( "." ) ; if ( pos >= 0 ) { pathToGenerated = pathToSource . substr ( 0 , pos ) + ".js" ; } } let map = null ; if ( this . _generatedCodeDirectory ) { let rest = PathUtils . makeRelative ( this . _generatedCodeDirectory , pathToGenerated ) ; while ( rest ) { const path = Path . join ( this . _generatedCodeDirectory , rest ) ; map = this . _findGeneratedToSourceMapping ( path , roots ) ; if ( map ) { break ; } rest = PathUtils . removeFirstSegment ( rest ) ; } } if ( map === null ) { let srcSegment = Path . sep + "src" + Path . sep ; if ( pathToGenerated . indexOf ( srcSegment ) >= 0 ) { let outSegment = Path . sep + "out" + Path . sep ; pathToGenerated = pathToGenerated . replace ( srcSegment , outSegment ) ; map = this . _findGeneratedToSourceMapping ( pathToGenerated , roots ) ; } } if ( map === null && pathToGenerated !== pathToSource ) { map = this . _findGeneratedToSourceMapping ( pathToGenerated , roots ) ; } if ( map ) { this . _sourceToGeneratedMaps [ pathToSource ] = map ; return map ; } return null ; } /**
     * Tries to find a SourceMap for the given path to a generated file.
     * This is simple if the generated file has the 'sourceMappingURL' at the end.
     * If not, we are using some heuristics...
     */ private _findGeneratedToSourceMapping ( pathToGenerated , roots ) { if ( ! pathToGenerated ) { return null ; } if ( pathToGenerated in this . _generatedToSourceMaps ) { return this . _generatedToSourceMaps [ pathToGenerated ] ; } const mapPath = pathToGenerated + ".map" ; if ( FS . existsSync ( mapPath ) ) { const map = this . _loadSourceMap ( mapPath , roots , pathToGenerated ) ; if ( map ) { return map ; } } const uri = this . _findSourceMapUrlInFile ( pathToGenerated ) ; if ( ! uri ) { return null ; } if ( uri . indexOf ( "data:application/json" ) >= 0 ) { const pos = uri . lastIndexOf ( "," ) ; if ( pos > 0 ) { const data = uri . substr ( pos + 1 ) ; try { const buffer = new Buffer ( data , "base64" ) ; const json = buffer . toString ( ) ; if ( json ) { return this . _registerSourceMap ( new SourceMap ( pathToGenerated , pathToGenerated , json , roots ) ) ; } } catch ( e ) { } } } else { let mapPath = uri ; if ( ! Path . isAbsolute ( mapPath ) ) { mapPath = PathUtils . makePathAbsolute ( pathToGenerated , mapPath ) ; } if ( FS . existsSync ( mapPath ) ) { const map = this . _loadSourceMap ( mapPath , roots , pathToGenerated ) ; if ( map ) { return map ; } } } return null ; } /**
     * try to find the 'sourceMappingURL' in the file with the given path.
     * Returns null in case of errors.
     */ private _findSourceMapUrlInFile ( pathToGenerated ) { try { const contents = FS . readFileSync ( pathToGenerated ) . 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 ( ) ; return uri ; } } } catch ( e ) { } return null ; } /**
     * Loads source map from file system.
     * If no generatedPath is given, the 'file' attribute of the source map is used.
     */ private _loadSourceMap ( map_path , roots , generatedPath ? ) { 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 , roots ) ; this . _allSourceMaps [ map_path ] = map ; this . _registerSourceMap ( map ) ; return map ; } catch ( e ) { } return null ; } private _registerSourceMap ( map ) { const gp = map . generatedPath ( ) ; if ( gp ) { this . _generatedToSourceMaps [ gp ] = map ; } return map ; } } export class SourceMap { public _sourcemapLocation : string ; public _generatedFile : string ; public _sources : string [ ] ; public _sourceRoot : string ; public _smc : SourceMapConsumer ; public _loading : Promise < BasicSourceMapConsumer > ; private _roots : string [ ] ; public constructor ( mapPath , generatedPath , json , roots ) { this . _roots = roots ; this . _sourcemapLocation = this . toUrl ( Path . dirname ( mapPath ) ) ; const sm = JSON . parse ( json ) ; if ( ! generatedPath ) { let file = sm . file ; if ( ! PathUtils . isAbsolutePath ( file ) ) { generatedPath = PathUtils . makePathAbsolute ( mapPath , file ) ; } } this . _generatedFile = generatedPath ; sm . sourceRoot = this . toUrl ( sm . sourceRoot , "" ) ; for ( let i = 0 ; i < sm . sources . length ; i ++ ) { sm . sources [ i ] = this . toUrl ( sm . sources [ i ] ) ; } this . _sourceRoot = sm . sourceRoot ; this . _sources = sm . sources . map ( util . normalize ) . map ( source => { return this . _sourceRoot && util . isAbsolute ( this . _sourceRoot ) && util . isAbsolute ( source ) ? util . relative ( this . _sourceRoot , source ) : source ; } ) ; try { this . _loading = new Promise ( ( resolve , reject ) => { new SourceMapConsumer ( sm ) . then ( smc => { this . _smc = smc ; resolve ( smc ) ; } ) . catch ( e => { reject ( e ) ; } ) ; } ) ; } catch ( e ) { this . _loading = Promise . reject ( e ) ; } } private toUrl ( path , dflt ? ) { if ( path ) { path = path . replace ( /\\/g , "/" ) ; path = path . replace ( /^webpack\:\/\/\// , "" ) ; if ( /^[a-zA-Z]\:\// . test ( path ) ) { path = "file:///" + path ; } if ( /^file\:\/\/\/[A-Z]\:\// . test ( path ) ) { const dl = path [ 8 ] ; path = path . replace ( dl , dl . toLowerCase ( ) ) ; } return path ; } return dflt ; } public generatedPath ( ) { return this . _generatedFile ; } public doesOriginateFrom ( absPath ) { return this . findSource ( absPath ) !== null ; } /**
     * returns the first entry from the sources array that matches the given absPath
     * or null otherwise.
     */ public findSource ( absPath ) { if ( process . platform === "win32" ) { absPath = absPath . replace ( /\\/g , "/" ) ; } for ( let name of this . _sources ) { let url = this . absolutePath ( name ) ; if ( absPath === url ) { return name ; } } return null ; } /**
     * Tries to make the given path absolute by prefixing it with the source maps location.
     * Any url schemes are removed.
     */ private absolutePath ( path ) { if ( ! util . isAbsolute ( path ) ) { let candidatePath = util . join ( this . _sourcemapLocation , path ) ; if ( FS . existsSync ( candidatePath ) ) { path = candidatePath ; } else { for ( let rpath of this . _roots ) { const rootPath = Path . join ( rpath , path ) ; if ( FS . existsSync ( rootPath ) ) { path = rootPath ; break ; } } } } const prefix = "file://" ; if ( path . indexOf ( prefix ) === 0 ) { path = path . substr ( prefix . length ) ; if ( /^\/[a-zA-Z]\:\// . test ( path ) ) { path = path . substr ( 1 ) ; } } return path ; } public originalPositionFor ( line , column , bias ) { if ( ! this . _smc ) { return null ; } const needle = { line : line , column : column , bias : bias || Bias . LEAST_UPPER_BOUND } ; const mp = this . _smc . originalPositionFor ( needle ) ; if ( mp . source ) { const src = this . _smc . sourceContentFor ( mp . source ) ; if ( src ) { ( < any > mp ) . content = src ; } mp . source = this . absolutePath ( mp . source ) ; if ( process . platform === "win32" ) { mp . source = mp . source . replace ( /\//g , "\\" ) ; } } return mp ; } public generatedPositionFor ( absPath , line , column , bias ) { if ( ! this . _smc ) { return null ; } const source = this . findSource ( absPath ) ; if ( source ) { const needle = { source : source , line : line , column : column , bias : bias || Bias . LEAST_UPPER_BOUND } ; return this . _smc . generatedPositionFor ( needle ) ; } return null ; } }