
cljs.source_map.clj Maven / Gradle / Ivy
;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be bound by
;; the terms of this license.
;; You must not remove this notice, or any other, from this software.
(ns cljs.source-map
(:require [clojure.java.io :as io]
[clojure.string :as string]
[clojure.data.json :as json]
[clojure.set :as set]
[cljs.source-map.base64-vlq :as base64-vlq]))
;; =============================================================================
;; All source map code in the file assumes the following in memory
;; representation of source map data.
;;
;; { file-name[String]
;; { line[Integer]
;; { col[Integer]
;; [{ :gline ..., :gcol ..., :name ...}] } }
;;
;; The outer level is a sorted map where the entries are file name and
;; sorted map of line information, the keys are strings. The line
;; information is represented as a sorted map of of column
;; information, the keys are integers. The column information is a
;; sorted map where the keys are integers and values are a vector of
;; maps - these maps have the keys :gline and :gcol for the generated
;; line and column. A :name key may be present if available.
;;
;; This representation simplifies merging ClojureScript source map
;; information with source map information generated by Google Closure
;; Compiler optimization. We can now trivially create the merged map
;; by using :gline and :gcol in the ClojureScript source map data to
;; extract final :gline and :gcol from the Google Closure source map.
;; -----------------------------------------------------------------------------
;; Utilities
(defn indexed-sources
"Take a seq of source file names and return a map from
file number to integer index."
[sources]
(->> sources
(map-indexed (fn [a b] [a b]))
(reduce (fn [m [i v]] (assoc m v i)) {})))
(defn source-compare
"Take a seq of source file names and return a comparator
that can be used to construct a sorted map."
[sources]
(let [sources (indexed-sources sources)]
(fn [a b] (compare (sources a) (sources b)))))
;; -----------------------------------------------------------------------------
;; Decoding
(defn seg->map
"Take a source map segment represented as a vector
and return a map."
[seg source-map]
(let [[gcol source line col name] seg]
{:gcol gcol
:source (nth (:sources source-map) source)
:line line
:col col
:name (when-let [name (-> seg meta :name)]
(nth (:names source-map) name))}))
(defn seg-combine
"Combine a source map segment vector and a relative
source map segment vector and combine them to get
an absolute segment posititon information as a vector."
[seg relseg]
(let [[gcol source line col name] seg
[rgcol rsource rline rcol rname] relseg
nseg [(+ gcol rgcol)
(+ (or source 0) rsource)
(+ (or line 0) rline)
(+ (or col 0) rcol)
(+ (or name 0) rname)]]
(if name
(with-meta nseg {:name (+ name rname)})
nseg)))
(defn update-reverse-result
"Helper for decode-reverse. Take a source map and update it
based on a segment map."
[result segmap gline]
(let [{:keys [gcol source line col name]} segmap
d {:gline gline
:gcol gcol}
d (if name (assoc d :name name) d)]
(update-in result [source]
(fnil (fn [m]
(update-in m [line]
(fnil (fn [m]
(update-in m [col]
(fnil (fn [v] (conj v d))
[])))
(sorted-map))))
(sorted-map)))))
(defn decode-reverse
"Convert a v3 source map JSON object into a nested sorted map
organized as file, line, and column. Note this source map
maps from *original* source location to generated source location."
([source-map]
(decode-reverse (:mappings source-map) source-map))
([mappings source-map]
(let [{:keys [sources]} source-map
relseg-init [0 0 0 0 0]
lines (seq (string/split mappings #";"))]
(loop [gline 0
lines lines
relseg relseg-init
result (sorted-map-by (source-compare sources))]
(if lines
(let [line (first lines)
[result relseg]
(if (string/blank? line)
[result relseg]
(let [segs (seq (string/split line #","))]
(loop [segs segs relseg relseg result result]
(if segs
(let [seg (first segs)
nrelseg (seg-combine (base64-vlq/decode seg) relseg)]
(recur (next segs) nrelseg
(update-reverse-result result (seg->map nrelseg source-map) gline)))
[result relseg]))))]
(recur (inc gline) (next lines) (assoc relseg 0 0) result))
result)))))
(defn update-result
"Helper for decode. Take a source map and update it based on a
segment map."
[result segmap gline]
(let [{:keys [gcol source line col name]} segmap
d {:line line
:col col
:source source}
d (if name (assoc d :name name) d)]
(update-in result [gline]
(fnil (fn [m]
(update-in m [gcol]
(fnil #(conj % d) [])))
(sorted-map)))))
(defn decode
"Convert a v3 source map JSON object into a nested sorted map
organized as file, line, and column. Note this source map
maps from *generated* source location to original source
location."
([source-map]
(decode (:mappings source-map) source-map))
([mappings source-map]
(let [relseg-init [0 0 0 0 0]
lines (seq (string/split mappings #";"))]
(loop [gline 0 lines lines relseg relseg-init result {}]
(if lines
(let [line (first lines)
[result relseg]
(if (string/blank? line)
[result relseg]
(let [segs (seq (string/split line #","))]
(loop [segs segs relseg relseg result result]
(if segs
(let [seg (first segs)
nrelseg (seg-combine (base64-vlq/decode seg) relseg)]
(recur (next segs) nrelseg
(update-result result (seg->map nrelseg source-map) gline)))
[result relseg]))))]
(recur (inc gline) (next lines) (assoc relseg 0 0) result))
result)))))
;; -----------------------------------------------------------------------------
;; Encoding
(defn lines->segs
"Take a nested sorted map encoding line and column information
for a file and return a vector of vectors of encoded segments.
Each vector represents a line, and the internal vectors are segments
representing the contents of the line."
[lines]
(let [relseg (atom [0 0 0 0 0])]
(reduce
(fn [segs cols]
(swap! relseg
(fn [[_ source line col name]]
[0 source line col name]))
(conj segs
(reduce
(fn [cols [gcol sidx line col name :as seg]]
(let [offset (map - seg @relseg)]
(swap! relseg
(fn [[_ _ _ _ lname]]
[gcol sidx line col (or name lname)]))
(conj cols (base64-vlq/encode offset))))
[] cols)))
[] lines)))
(defn relativize-path
"Relativize a path using :source-map-path if provided or the parent directory
otherwise."
[path {:keys [output-dir source-map-path source-map relpaths] :as opts}]
(let [bare-munged-path
(cond
(re-find #"\.jar!/" path)
(str (or source-map-path output-dir)
(second (string/split path #"\.jar!")))
:else
(str (or source-map-path output-dir)
"/" (get relpaths path)))]
(cond
source-map-path bare-munged-path
:else
(let [unrel-uri (-> bare-munged-path io/file .toURI)
sm-parent-uri (-> source-map io/file .getAbsoluteFile .getParentFile .toURI)]
(str (.relativize sm-parent-uri unrel-uri))))))
(defn encode*
"Take an internal source map representation represented as nested
sorted maps of file, line, column and return a v3 representation."
[m opts]
(let [lines (atom [[]])
names->idx (atom {})
name-idx (atom 0)
preamble-lines (take (or (:preamble-line-count opts) 0) (repeat []))
info->segv
(fn [info source-idx line col]
(let [segv [(:gcol info) source-idx line col]]
(if-let [name (:name info)]
(let [idx (if-let [idx (get @names->idx name)]
idx
(let [cidx @name-idx]
(swap! names->idx assoc name cidx)
(swap! name-idx inc)
cidx))]
(conj segv idx))
segv)))
encode-cols
(fn [infos source-idx line col]
(doseq [info infos]
(let [segv (info->segv info source-idx line col)
gline (:gline info)
lc (count @lines)]
(if (> gline (dec lc))
(swap! lines
(fn [lines]
(conj (into lines (repeat (dec (- gline (dec lc))) [])) [segv])))
(swap! lines
(fn [lines]
(update-in lines [gline] conj segv)))))))]
(doseq [[source-idx [_ lines]] (map-indexed (fn [i v] [i v]) m)]
(doseq [[line cols] lines]
(doseq [[col infos] cols]
(encode-cols infos source-idx line col))))
(cond-> {"version" 3
"file" (:file opts)
"sources" (into []
(let [paths (keys m)
f (comp
(if (true? (:source-map-timestamp opts))
(fn [uri]
(if-not (string/index-of uri "?")
(str uri "?rel=" (System/currentTimeMillis))
(str uri "&rel=" (System/currentTimeMillis))))
identity)
(if (or (:output-dir opts)
(:source-map-path opts))
#(relativize-path % opts)
#(last (string/split % #"/"))))]
(map f paths)))
"lineCount" (:lines opts)
"mappings" (->> (lines->segs (concat preamble-lines @lines))
(map #(string/join "," %))
(string/join ";"))
"names" (into []
(map (set/map-invert @names->idx)
(range (count @names->idx))))}
(:sources-content opts)
(assoc "sourcesContent" (:sources-content opts)))))
(defn encode
"Take an internal source map representation represented as nested
sorted maps of file, line, column and return a source map v3 JSON
string."
[m opts]
(let [source-map-file-contents (encode* m opts)]
(if (true? (:source-map-pretty-print opts))
(with-out-str
(json/pprint
source-map-file-contents
:escape-slash false))
(json/write-str source-map-file-contents))))
;; -----------------------------------------------------------------------------
;; Merging
(defn merge-source-maps
"Merge an internal source map representation of a single
ClojureScript file mapping original to generated with a
second source map mapping original JS to generated JS.
The is to support source maps that work through multiple
compilation steps like Google Closure optimization passes."
[cljs-map js-map]
(loop [line-map-seq (seq cljs-map) new-lines (sorted-map)]
(if line-map-seq
(let [[line col-map] (first line-map-seq)
new-cols
(loop [col-map-seq (seq col-map) new-cols (sorted-map)]
(if col-map-seq
(let [[col infos] (first col-map-seq)]
(recur (next col-map-seq)
(assoc new-cols col
(reduce (fn [v {:keys [gline gcol]}]
(into v (get-in js-map [gline gcol])))
[] infos))))
new-cols))]
(recur (next line-map-seq)
(assoc new-lines line new-cols)))
new-lines)))
;; -----------------------------------------------------------------------------
;; Reverse Source Map Inversion
(defn invert-reverse-map
"Given a ClojureScript to JavaScript source map, invert it. Useful when
mapping JavaScript stack traces when environment support is unavailable."
[reverse-map]
(let [inverted (atom (sorted-map))]
(doseq [[line columns] reverse-map]
(doseq [[column column-info] columns]
(doseq [{:keys [gline gcol name]} column-info]
(swap! inverted update-in [gline]
(fnil (fn [columns]
(update-in columns [column] (fnil conj [])
{:line line :col column :name name}))
(sorted-map))))))
@inverted))
(comment
;; INSTRUCTIONS:
;; switch into samples/hello
;; run repl to start clojure
;; build with
(require '[cljs.closure :as cljsc])
(cljsc/build "src"
{:optimizations :simple
:output-to "hello.js"
:source-map "hello.js.map"
:output-dir "out"})
;; load source map
(def raw-source-map
(json/read-str (slurp (io/file "hello.js.map")) :key-fn keyword))
;; test it out
(first (decode-reverse raw-source-map))
;; decoded source map preserves file order
(= (keys (decode-reverse raw-source-map)) (:sources raw-source-map))
)
© 2015 - 2025 Weber Informatics LLC | Privacy Policy