sourcemap.nim 11 KB

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