sourcemap.nim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import os, strformat, strutils, tables, sets, ropes, json, algorithm
  2. type
  3. SourceNode* = ref object
  4. line*: int
  5. column*: int
  6. source*: string
  7. name*: string
  8. children*: seq[Child]
  9. C = enum cSourceNode, cString
  10. Child* = ref object
  11. case kind*: C:
  12. of cSourceNode:
  13. node*: SourceNode
  14. of cString:
  15. s*: string
  16. SourceMap* = ref object
  17. version*: int
  18. sources*: seq[string]
  19. names*: seq[string]
  20. mappings*: string
  21. file*: string
  22. # sourceRoot*: string
  23. # sourcesContent*: string
  24. SourceMapGenerator = ref object
  25. file: string
  26. sourceRoot: string
  27. skipValidation: bool
  28. sources: seq[string]
  29. names: seq[string]
  30. mappings: seq[Mapping]
  31. Mapping* = ref object
  32. source*: string
  33. original*: tuple[line: int, column: int]
  34. generated*: tuple[line: int, column: int]
  35. name*: string
  36. noSource*: bool
  37. noName*: bool
  38. proc child*(s: string): Child =
  39. Child(kind: cString, s: s)
  40. proc child*(node: SourceNode): Child =
  41. Child(kind: cSourceNode, node: node)
  42. proc newSourceNode(line: int, column: int, path: string, node: SourceNode, name: string = ""): SourceNode =
  43. SourceNode(line: line, column: column, source: path, name: name, children: @[child(node)])
  44. proc newSourceNode(line: int, column: int, path: string, s: string, name: string = ""): SourceNode =
  45. SourceNode(line: line, column: column, source: path, name: name, children: @[child(s)])
  46. proc newSourceNode(line: int, column: int, path: string, children: seq[Child], name: string = ""): SourceNode =
  47. SourceNode(line: line, column: column, source: path, name: name, children: children)
  48. # debugging
  49. proc text*(sourceNode: SourceNode, depth: int): string =
  50. let empty = " "
  51. result = &"{repeat(empty, depth)}SourceNode({sourceNode.source}:{sourceNode.line}:{sourceNode.column}):\n"
  52. for child in sourceNode.children:
  53. if child.kind == cString:
  54. result.add(&"{repeat(empty, depth + 1)}{child.s}\n")
  55. else:
  56. result.add(child.node.text(depth + 1))
  57. proc `$`*(sourceNode: SourceNode): string = text(sourceNode, 0)
  58. # base64_VLQ
  59. let integers = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
  60. proc encode*(i: int): string =
  61. result = ""
  62. var n = i
  63. if n < 0:
  64. n = (-n shl 1) or 1
  65. else:
  66. n = n shl 1
  67. var z = 0
  68. while z == 0 or n > 0:
  69. var e = n and 31
  70. n = n shr 5
  71. if n > 0:
  72. e = e or 32
  73. result.add(integers[e])
  74. z += 1
  75. type TokenState = enum Normal, String, Ident, Mangled
  76. iterator tokenize*(line: string): (bool, string) =
  77. # result = @[]
  78. var state = Normal
  79. var token = ""
  80. var isMangled = false
  81. for z, ch in line:
  82. if ch.isAlphaAscii:
  83. if state == Normal:
  84. state = Ident
  85. if token.len > 0:
  86. yield (isMangled, token)
  87. token = $ch
  88. isMangled = false
  89. else:
  90. token.add(ch)
  91. elif ch == '_':
  92. if state == Ident:
  93. state = Mangled
  94. isMangled = true
  95. token.add($ch)
  96. elif ch != '"' and not ch.isAlphaNumeric:
  97. if state in {Ident, Mangled}:
  98. state = Normal
  99. if token.len > 0:
  100. yield (isMangled, token)
  101. token = $ch
  102. isMangled = false
  103. else:
  104. token.add($ch)
  105. elif ch == '"':
  106. if state != String:
  107. state = String
  108. if token.len > 0:
  109. yield (isMangled, token)
  110. token = $ch
  111. isMangled = false
  112. else:
  113. state = Normal
  114. token.add($ch)
  115. if token.len > 0:
  116. yield (isMangled, token)
  117. isMangled = false
  118. token = ""
  119. else:
  120. token.add($ch)
  121. if token.len > 0:
  122. yield (isMangled, token)
  123. proc parse*(source: string, path: string): SourceNode =
  124. let lines = source.splitLines()
  125. var lastLocation: SourceNode = nil
  126. result = newSourceNode(0, 0, path, @[])
  127. # we just use one single parent and add all nim lines
  128. # as its children, I guess in typical codegen
  129. # that happens recursively on ast level
  130. # we also don't have column info, but I doubt more one nim lines can compile to one js
  131. # maybe in macros?
  132. for i, originalLine in lines:
  133. let line = originalLine.strip
  134. if line.len == 0:
  135. continue
  136. # this shouldn't be a problem:
  137. # jsgen doesn't generate comments
  138. # and if you emit // line you probably know what you're doing
  139. if line.startsWith("// line"):
  140. if result.children.len > 0:
  141. result.children[^1].node.children.add(child(line & "\n"))
  142. let pos = line.find(" ", 8)
  143. let lineNumber = line[8 .. pos - 1].parseInt
  144. let linePath = line[pos + 2 .. ^2] # quotes
  145. lastLocation = newSourceNode(
  146. lineNumber,
  147. 0,
  148. linePath,
  149. @[])
  150. result.children.add(child(lastLocation))
  151. else:
  152. var last: SourceNode
  153. for token in line.tokenize():
  154. var name = ""
  155. if token[0]:
  156. name = token[1].split('_', 1)[0]
  157. if result.children.len > 0:
  158. result.children[^1].node.children.add(
  159. child(
  160. newSourceNode(
  161. result.children[^1].node.line,
  162. 0,
  163. result.children[^1].node.source,
  164. token[1],
  165. name)))
  166. last = result.children[^1].node.children[^1].node
  167. else:
  168. result.children.add(
  169. child(
  170. newSourceNode(i + 1, 0, path, token[1], name)))
  171. last = result.children[^1].node
  172. let nl = "\n"
  173. if not last.isNil:
  174. last.source.add(nl)
  175. proc cmp(a: Mapping, b: Mapping): int =
  176. var c = cmp(a.generated, b.generated)
  177. if c != 0:
  178. return c
  179. c = cmp(a.source, b.source)
  180. if c != 0:
  181. return c
  182. c = cmp(a.original, b.original)
  183. if c != 0:
  184. return c
  185. return cmp(a.name, b.name)
  186. proc index*[T](elements: seq[T], element: T): int =
  187. for z in 0 ..< elements.len:
  188. if elements[z] == element:
  189. return z
  190. return -1
  191. proc serializeMappings(map: SourceMapGenerator, mappings: seq[Mapping]): string =
  192. var previous = Mapping(generated: (line: 1, column: 0), original: (line: 0, column: 0), name: "", source: "")
  193. var previousSourceId = 0
  194. var previousNameId = 0
  195. var next = ""
  196. var nameId = 0
  197. var sourceId = 0
  198. result = ""
  199. for z, mapping in mappings:
  200. next = ""
  201. if mapping.generated.line != previous.generated.line:
  202. previous.generated.column = 0
  203. while mapping.generated.line != previous.generated.line:
  204. next.add(";")
  205. previous.generated.line += 1
  206. else:
  207. if z > 0:
  208. if cmp(mapping, mappings[z - 1]) == 0:
  209. continue
  210. next.add(",")
  211. next.add(encode(mapping.generated.column - previous.generated.column))
  212. previous.generated.column = mapping.generated.column
  213. if not mapping.noSource and mapping.source.len > 0:
  214. sourceId = map.sources.index(mapping.source)
  215. next.add(encode(sourceId - previousSourceId))
  216. previousSourceId = sourceId
  217. next.add(encode(mapping.original.line - 1 - previous.original.line))
  218. previous.original.line = mapping.original.line - 1
  219. next.add(encode(mapping.original.column - previous.original.column))
  220. previous.original.column = mapping.original.column
  221. if not mapping.noName and mapping.name.len > 0:
  222. nameId = map.names.index(mapping.name)
  223. next.add(encode(nameId - previousNameId))
  224. previousNameId = nameId
  225. result.add(next)
  226. proc gen*(map: SourceMapGenerator): SourceMap =
  227. var mappings = map.mappings.sorted do (a: Mapping, b: Mapping) -> int:
  228. cmp(a, b)
  229. result = SourceMap(
  230. file: map.file,
  231. version: 3,
  232. sources: map.sources[0..^1],
  233. names: map.names[0..^1],
  234. mappings: map.serializeMappings(mappings))
  235. proc addMapping*(map: SourceMapGenerator, mapping: Mapping) =
  236. if not mapping.noSource and mapping.source notin map.sources:
  237. map.sources.add(mapping.source)
  238. if not mapping.noName and mapping.name.len > 0 and mapping.name notin map.names:
  239. map.names.add(mapping.name)
  240. # echo "map ", mapping.source, " ", mapping.original, " ", mapping.generated, " ", mapping.name
  241. map.mappings.add(mapping)
  242. proc walk*(node: SourceNode, fn: proc(line: string, original: SourceNode)) =
  243. for child in node.children:
  244. if child.kind == cString and child.s.len > 0:
  245. fn(child.s, node)
  246. else:
  247. child.node.walk(fn)
  248. proc toSourceMap*(node: SourceNode, file: string): SourceMapGenerator =
  249. var map = SourceMapGenerator(file: file, sources: @[], names: @[], mappings: @[])
  250. var generated = (line: 1, column: 0)
  251. var sourceMappingActive = false
  252. var lastOriginal = SourceNode(source: "", line: -1, column: 0, name: "", children: @[])
  253. node.walk do (line: string, original: SourceNode):
  254. if original.source.endsWith(".js"):
  255. # ignore it
  256. discard
  257. else:
  258. if original.line != -1:
  259. if lastOriginal.source != original.source or
  260. lastOriginal.line != original.line or
  261. lastOriginal.column != original.column or
  262. lastOriginal.name != original.name:
  263. map.addMapping(
  264. Mapping(
  265. source: original.source,
  266. original: (line: original.line, column: original.column),
  267. generated: (line: generated.line, column: generated.column),
  268. name: original.name))
  269. lastOriginal = SourceNode(
  270. source: original.source,
  271. line: original.line,
  272. column: original.column,
  273. name: original.name,
  274. children: lastOriginal.children)
  275. sourceMappingActive = true
  276. elif sourceMappingActive:
  277. map.addMapping(
  278. Mapping(
  279. noSource: true,
  280. noName: true,
  281. generated: (line: generated.line, column: generated.column),
  282. original: (line: -1, column: -1)))
  283. lastOriginal.line = -1
  284. sourceMappingActive = false
  285. for z in 0 ..< line.len:
  286. if line[z] in Newlines:
  287. generated.line += 1
  288. generated.column = 0
  289. if z == line.len - 1:
  290. lastOriginal.line = -1
  291. sourceMappingActive = false
  292. elif sourceMappingActive:
  293. map.addMapping(
  294. Mapping(
  295. source: original.source,
  296. original: (line: original.line, column: original.column),
  297. generated: (line: generated.line, column: generated.column),
  298. name: original.name))
  299. else:
  300. generated.column += 1
  301. map
  302. proc genSourceMap*(source: string, outFile: string): (Rope, SourceMap) =
  303. let node = parse(source, outFile)
  304. let map = node.toSourceMap(file = outFile)
  305. ((&"{source}\n//# sourceMappingURL={outFile}.map").rope, map.gen)