KotlinJsSourceMap.kt
src/jvmMain/kotlin/afield/lang/remotejs/KotlinJsSourceMap.kt · 123 lines
package afield.lang.remotejs
import org.json.JSONObject
import java.util.concurrent.ConcurrentHashMap
The source map of a Kotlin→JS box's compiled module, remembered per box so that output arriving after the run can still be given a line.
The map is a by-product of the compile, and the output that needs it usually arrives once the run
has finished: an error from a timer, a console.log from an event handler, each carrying a line of
generated JavaScript and nothing else. RemoteJS.kt builds a map the moment KotlinToJsCompiler
returns one and remembers it under the box id; KotlinJsHeadProfiles.kt asks for it when a reply
needs a line; the next compile of that box replaces it.
None of this reports a failure to the box. A compile that produced no map, or a map whose
mappings the decoder rejects, leaves the box with none, and its output keeps the generated line
it came with. A position that maps somewhere other than the box's own Kotlin is a null from
editorLine, and editorLineForStack goes on to the next frame of the stack.
class KotlinJsSourceMap(json: String, private val userSourceSuffix: String, private val toEditorLine: (Int) -> Int) {
Maps a line in the JavaScript a Kotlin→JS box compiled to back to the line in the box's editor.
The head reports runtime errors and routed console.log calls at the generated JavaScript line
(line 802 for an error on line 3 of the box). The compiler's source map (v3) takes a generated
line and column to a line of the prepared Kotlin source, and KotlinToJsCompiler.PreparedSource
takes that to the editor line. Frames in the Kotlin stdlib map to another source and are skipped,
so an exception thrown inside reduce lands on the user line that called it.
class KotlinJsSourceMap(json: String, private val userSourceSuffix: String, private val toEditorLine: (Int) -> Int) {
private class Segment(val column: Int, val source: Int, val line: Int)
private val sources: List<String>
private val lines: List<List<Segment>>
init
init {
val o = JSONObject(json)
val s = o.optJSONArray("sources")
sources = (0 until (s?.length() ?: 0)).map { s!!.getString(it) }
lines = decode(o.optString("mappings", ""))
}
fun editorLine(line: Int, column: Int = 1): Int?
The one-based editor line for generated (one-based) line / column, or null when that
position is not in the box's own Kotlin.
fun editorLine(line: Int, column: Int = 1): Int? {
val segments = lines.getOrNull(line - 1)?.takeIf { it.isNotEmpty() } ?: return null
val col0 = column - 1
val hit = segments.lastOrNull { it.column <= col0 } ?: segments.first()
val source = sources.getOrNull(hit.source) ?: return null
if (!source.endsWith(userSourceSuffix)) return null
return toEditorLine(hit.line + 1).takeIf { it > 0 }
}
fun editorLineForStack(stack: String, codeName: String): Int?
The editor line of the innermost frame of stack in codeName that is the box's own code.
fun editorLineForStack(stack: String, codeName: String): Int? =
STACK_LOCATION.findAll(stack)
.filter { it.groupValues[1] == codeName }
.firstNotNullOfOrNull { editorLine(it.groupValues[2].toInt(), it.groupValues[3].toInt()) }
companion object
companion object {
Same frame shape as the head's errors.js; the code name is box_<name>|<uid>.
private val STACK_LOCATION = Regex("""(?:\(|\s|^)((?:box_|field-box:|https?:|file:)[^\s()]*?):(\d+):(\d+)(?:\)|\s|$)""")
private val byBox = ConcurrentHashMap<String, KotlinJsSourceMap>()
fun remember(boxId: String, map: KotlinJsSourceMap?)
Remember the map of the module a box last sent, for output that arrives later.
fun remember(boxId: String, map: KotlinJsSourceMap?) {
if (map == null) byBox.remove(boxId) else byBox[boxId] = map
}
fun forBox(boxId: String): KotlinJsSourceMap? = byBox[boxId]
The map of the module box boxId last compiled, or null when it has none to map through.
source — 47 lines, from line 76
private const val B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
private fun decode(mappings: String): List<List<Segment>> {
val out = ArrayList<List<Segment>>()
var source = 0
var srcLine = 0
var srcCol = 0
var name = 0
for (lineText in mappings.split(';')) {
var genCol = 0
val segs = ArrayList<Segment>()
for (seg in lineText.split(',')) {
if (seg.isEmpty()) continue
val v = vlq(seg)
genCol += v[0]
if (v.size >= 4) {
source += v[1]; srcLine += v[2]; srcCol += v[3]
if (v.size >= 5) name += v[4]
segs += Segment(genCol, source, srcLine)
}
}
out += segs
}
return out
}
private fun vlq(s: String): IntArray {
val values = ArrayList<Int>()
var value = 0
var shift = 0
for (c in s) {
val digit = B64.indexOf(c)
require(digit >= 0) { "bad source map character '$c'" }
value += (digit and 31) shl shift
if (digit and 32 != 0) {
shift += 5
} else {
val negative = value and 1 == 1
val v = value ushr 1
values += if (negative) -v else v
value = 0; shift = 0
}
}
return values.toIntArray()
}
}
}