blob: a45d1761a187bb94e4a7256f8a3c5968608bcdcb [file] [log] [blame]
(ns npm-force-resolutions.core
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [cljs.nodejs :as nodejs]
[cljs-http.client :as http]
[cljs.core.async :as async :refer [<! >!]]
[clojure.string :as string]
[cognitect.transit :as t]
[xmlhttprequest :refer [XMLHttpRequest]]
[child_process :refer [execSync]]))
(set! js/XMLHttpRequest XMLHttpRequest)
(defn node-slurp [path]
(let [fs (nodejs/require "fs")]
(.readFileSync fs path "utf8")))
(defn node-spit [path data]
(let [fs (nodejs/require "fs")]
(.writeFileSync fs path data)))
(defn read-json [path]
(let [safe-json (-> (node-slurp path)
(string/replace #"\^" "\\\\^")
(string/replace #"\~" "\\\\~"))]
(t/read (t/reader :json) safe-json)))
(defn build-dependency-from-dist [version dist]
(let [tarball (get dist :tarball)
integrity (get dist :integrity)
shasum (get dist :shasum)]
(if integrity
{"version" version
"resolved" tarball
"integrity" integrity}
(if shasum
{"version" version
"resolved" tarball
"integrity" (str "sha1-" (.toString (js/Buffer.from shasum "hex") "base64"))}
{"version" version}))))
(defn fetch-resolved-resolution [registry-url key version]
(go
(if (re-find #"^\d" (str version)) ; we only query the api if it's an exact version, starting with a number
(let [response (<! (http/get (str registry-url key "/" version)))
dist (get-in response [:body :dist])]
{key (build-dependency-from-dist version dist)})
{key (build-dependency-from-dist version {})})))
(defn wait-all [callbacks]
(go
(let [merged (async/merge callbacks)]
(loop [result {}]
(if (= (count result) (count callbacks))
result
(recur (merge result (<! merged))))))))
(defn get-registry-url []
(.trim (.toString (execSync "npm config get registry"))))
(defn find-resolutions [folder]
(go
(let [package-json (read-json (str folder "/package.json"))
resolutions (get package-json "resolutions")
registry-url (get-registry-url)
callbacks (map
(fn [[k v]] (fetch-resolved-resolution registry-url k v))
(seq resolutions))
[resolved-resolutions timeout_] (async/alts!
[(wait-all callbacks)
(async/timeout 8000)])
]
(if resolved-resolutions
resolved-resolutions
(throw (js/Error. "Timeout trying to fetch resolutions from npm"))))))
; Source: https://stackoverflow.com/questions/1676891/mapping-a-function-on-the-values-of-a-map-in-clojure
(defn map-vals [f m]
(apply merge (map (fn [[k v]] {k (f k v)}) m)))
(defn update-on-requires [resolutions dependency]
(update dependency "requires"
(fn [requires]
(map-vals #((fn [key version]
(if (contains? resolutions key)
(get-in resolutions [key, "version"])
version)) %1 %2) requires))))
(defn add-dependencies [resolutions dependency]
(let [required-dependencies (keys (get dependency "requires"))
new-deps (select-keys resolutions required-dependencies)
dependencies (merge new-deps
(get dependency "dependencies" {}))]
(merge dependency {"dependencies" dependencies})))
(defn fix-existing-dependency [resolutions key dependency]
(if (contains? resolutions key)
(let [fixed-dep (conj
(dissoc dependency "version" "resolved" "integrity" "bundled")
(get resolutions key))]
; sorts dependency map by stringified values, which seems to match to what npm does
; allowing us to have a more stable package-lock, easier to see the diffs
(into
(sorted-map-by #(compare
(str (get fixed-dep %1))
(str (get fixed-dep %2))))
fixed-dep))
dependency))
(defn order-map [target]
(into (sorted-map-by (fn [key1 key2]
(compare [key1]
[key2]))) target))
(defn sort-or-remove-map [key dependency]
(if (map? (get dependency key))
(update dependency key order-map)
(dissoc dependency key)))
(declare patch-all-dependencies)
(defn patch-dependency [resolutions key dependency]
(if (contains? dependency "requires")
(->> dependency
(add-dependencies resolutions)
(update-on-requires resolutions)
(fix-existing-dependency resolutions key)
(patch-all-dependencies resolutions)
(sort-or-remove-map "dependencies")
(sort-or-remove-map "requires"))
(fix-existing-dependency resolutions key dependency)))
(defn remove-node-modules-path [key]
(string/replace key #".*node_modules/" ""))
(defn patch-all-dependencies [resolutions package-lock]
(let [updated-dependencies
(update package-lock "dependencies"
(fn [dependencies]
(map-vals
#(patch-dependency resolutions %1 %2)
dependencies)))]
(if (get updated-dependencies "packages")
(update updated-dependencies "packages"
(fn [dependencies]
(map-vals
#(patch-dependency resolutions (remove-node-modules-path %1) %2)
dependencies)))
updated-dependencies)))
(defn update-package-lock [folder]
(go
(let [package-lock (read-json (str folder "/package-lock.json"))
resolutions (<! (find-resolutions folder))]
(->> (patch-all-dependencies resolutions package-lock)
(sort-or-remove-map "dependencies")))))
(defn indent-json [json]
(let [json-format (nodejs/require "json-format")]
(-> (.parse js/JSON json)
(json-format (js-obj "type" "space"
"size" 2))
(string/replace #"\\\\\^" "^")
(string/replace #"\\\\\~" "~")
(string/replace #" +\n" ""))))
(defn main [& args]
(go
(let [folder (or (first args) ".")
package-lock (<! (update-package-lock folder))
package-lock-json (t/write (t/writer :json-verbose) package-lock)]
(node-spit (str folder "/package-lock.json") (indent-json package-lock-json)))))
(set! *main-cli-fn* main)