|
package com.example.errors |
|
|
|
import com.example.UnionImplicits._ |
|
import org.scalajs.dom |
|
import org.scalajs.dom.ext.Ajax |
|
import typings.sourceMap.anon.Positionbiasnumberundefin |
|
import typings.sourceMap.mod.{NullableMappedPosition, RawSourceMap, SourceMapConsumer, SourceMapConsumerConstructor} |
|
|
|
import java.net.URI |
|
import scala.concurrent.{ExecutionContext, Future} |
|
import scala.scalajs.js |
|
import scala.scalajs.js.Thenable.Implicits.thenable2future |
|
import scala.scalajs.js.annotation.JSImport |
|
import scala.util.Try |
|
|
|
object SourceMap { |
|
|
|
@js.native |
|
@JSImport("source-map", "SourceMapConsumer") |
|
object SourceMapConsumer2 extends js.Object { |
|
def initialize(config: SourceMapConfig): Unit = js.native |
|
} |
|
|
|
trait SourceMapConfig extends js.Object { |
|
var `lib/mappings.wasm`: js.UndefOr[String] = js.undefined |
|
} |
|
|
|
@js.native |
|
@JSImport("source-map/lib/mappings.wasm", JSImport.Default) |
|
object SourceMapMappings extends js.Object |
|
|
|
SourceMapConsumer2.initialize(new SourceMapConfig { |
|
`lib/mappings.wasm` = SourceMapMappings.asInstanceOf[String] |
|
}) |
|
|
|
final case class Position( |
|
url: Option[URI], |
|
file: Option[String], |
|
identifier: Option[String], |
|
line: Option[Int], |
|
column: Option[Int] |
|
) { |
|
|
|
def positionString: Option[String] = |
|
for { |
|
file <- file |
|
line <- line |
|
column <- column |
|
} yield s"$file:$line:$column" |
|
} |
|
|
|
object Position { |
|
|
|
val Unknown: Position = Position(None, None, None, None, None) |
|
|
|
def apply(codeUrl: String, position: NullableMappedPosition): Position = |
|
Position( |
|
resolveUrl(codeUrl, position.source.toOption), |
|
position.source.toOption, |
|
position.name.toOption, |
|
position.line.toOption.map(_.toInt), |
|
position.column.toOption.map(_.toInt) |
|
) |
|
|
|
private def resolveUrl(codeUrl: String, maybeSource: Option[String]): Option[URI] = |
|
for { |
|
url <- Try(new URI(codeUrl)).toOption |
|
source <- maybeSource |
|
} yield url.resolve(source) |
|
} |
|
|
|
// TODO: Use ConcurrentHashMap once upgraded to Scala.js 1.x |
|
@volatile |
|
private var sourceMaps = Map.empty[String, Future[SourceMapConsumer]] |
|
|
|
def sourcePosition( |
|
fileUrl: String, |
|
line: Int, |
|
column: Int |
|
)(implicit |
|
ec: ExecutionContext |
|
): Future[Position] = |
|
consumer(fileUrl).map { consumer => |
|
val position = js.Dynamic.literal(line = line, column = column).asInstanceOf[Positionbiasnumberundefin] |
|
Position(fileUrl, consumer.originalPositionFor(position)) |
|
} |
|
|
|
private def consumer(fileUrl: String)(implicit ec: ExecutionContext): Future[SourceMapConsumer] = |
|
sourceMaps.get(fileUrl) match { |
|
case Some(sourceMapConsumer) => sourceMapConsumer |
|
case None => |
|
sourceMaps.synchronized { |
|
sourceMaps.get(fileUrl) match { |
|
case Some(sourceMapConsumer) => sourceMapConsumer |
|
case None => |
|
val sourceMapConsumer = createConsumer(fileUrl) |
|
sourceMaps += fileUrl -> sourceMapConsumer |
|
sourceMapConsumer |
|
} |
|
} |
|
} |
|
|
|
private def createConsumer(fileUrl: String)(implicit ec: ExecutionContext): Future[SourceMapConsumer] = |
|
Ajax.get(fileUrl).map(processCodeResponse(fileUrl, _)).flatMap { |
|
case Some(sourceMapURL) => |
|
val headers = Map("streaming" -> "true") |
|
Ajax.get(sourceMapURL, headers = headers).flatMap(processSourceMapResponse(fileUrl, _)) |
|
case None => unknownSourceMapConsumer |
|
} |
|
|
|
private def processCodeResponse( |
|
fileUrl: String, |
|
response: dom.XMLHttpRequest |
|
)(implicit |
|
ec: ExecutionContext |
|
): Option[String] = { |
|
require(response.readyState == dom.XMLHttpRequest.DONE) |
|
if (response.status == 200) { |
|
val code = response.responseText |
|
SourceMapURL.getFrom(code).toOption |
|
} else { |
|
dom.console.debug(s"""Failed to retrieve source code for $fileUrl. |
|
|Status ${response.status}: ${response.responseText}""".stripMargin) |
|
None |
|
} |
|
} |
|
|
|
private def processSourceMapResponse( |
|
fileUrl: String, |
|
response: dom.XMLHttpRequest |
|
)(implicit |
|
ec: ExecutionContext |
|
): Future[SourceMapConsumer] = { |
|
require(response.readyState == dom.XMLHttpRequest.DONE) |
|
if (response.status == 200) { |
|
parseSourceMap(response.responseText) |
|
} else { |
|
dom.console.debug(s"""Failed to retrieve source map for $fileUrl. |
|
|Status ${response.status}: ${response.responseText}""".stripMargin) |
|
// We use an unknown source map consumer to avoid a future lookup. |
|
// We might want to have a way of busting this cache in case the source map can be found later on. |
|
unknownSourceMapConsumer |
|
} |
|
} |
|
|
|
private def parseSourceMap(text: String)(implicit ec: ExecutionContext): Future[SourceMapConsumer] = |
|
Future { |
|
js.JSON.parse(text).asInstanceOf[RawSourceMap] |
|
}.flatMap { sourceMap => |
|
(SourceMapConsumer: SourceMapConsumerConstructor).newInstance1(sourceMap) |
|
}.map(_.merge[SourceMapConsumer]) |
|
|
|
// TODO: Perhaps we should cache this. |
|
private def unknownSourceMapConsumer(implicit ec: ExecutionContext): Future[SourceMapConsumer] = { |
|
val sourceMap = js.Dynamic |
|
.literal( |
|
file = "unknown", |
|
mappings = "", |
|
names = js.Array[String](), |
|
sources = js.Array[String](), |
|
version = 3.0 |
|
) |
|
.asInstanceOf[RawSourceMap] |
|
(SourceMapConsumer: SourceMapConsumerConstructor).newInstance1(sourceMap).map(_.merge[SourceMapConsumer]) |
|
} |
|
} |