|
| 1 | +(ns cljs.foreign.node |
| 2 | + (:require [cljs.vendor.clojure.data.json :as json] |
| 3 | + [clojure.java.io :as io] |
| 4 | + [clojure.string :as string])) |
| 5 | + |
| 6 | +(defn package-json-entries |
| 7 | + "Takes options and returns a sequence with the desired order of package.json |
| 8 | + entries for the given :package-json-resolution mode. If no mode is provided, |
| 9 | + defaults to :webpack (if no target is set) and :nodejs (if the target is |
| 10 | + :nodejs)." |
| 11 | + [opts] |
| 12 | + {:pre [(or (= (:package-json-resolution opts) :webpack) |
| 13 | + (= (:package-json-resolution opts) :nodejs) |
| 14 | + (and (sequential? (:package-json-resolution opts)) |
| 15 | + (every? string? (:package-json-resolution opts))) |
| 16 | + (not (contains? opts :package-json-resolution)))]} |
| 17 | + (let [modes {:nodejs ["main"] |
| 18 | + :webpack ["browser" "module" "main"]}] |
| 19 | + (if-let [mode (:package-json-resolution opts)] |
| 20 | + (if (sequential? mode) mode (get modes mode)) |
| 21 | + (case (:target opts) |
| 22 | + :nodejs (:nodejs modes) |
| 23 | + (:webpack modes))))) |
| 24 | + |
| 25 | +(comment |
| 26 | + (= (package-json-entries {}) ["browser" "module" "main"]) |
| 27 | + (= (package-json-entries {:package-json-resolution :nodejs}) ["main"]) |
| 28 | + (= (package-json-entries {:package-json-resolution :webpack}) ["browser" "module" "main"]) |
| 29 | + (= (package-json-entries {:package-json-resolution ["foo" "bar" "baz"]}) ["foo" "bar" "baz"]) |
| 30 | + (= (package-json-entries {:target :nodejs}) ["main"]) |
| 31 | + (= (package-json-entries {:target :nodejs :package-json-resolution :nodejs}) ["main"]) |
| 32 | + (= (package-json-entries {:target :nodejs :package-json-resolution :webpack}) ["browser" "module" "main"]) |
| 33 | + (= (package-json-entries {:target :nodejs :package-json-resolution ["foo" "bar"]}) ["foo" "bar"])) |
| 34 | + |
| 35 | +(defn- package-json? [path] |
| 36 | + (= "package.json" (.getName (io/file path)))) |
| 37 | + |
| 38 | +(defn- top-level-package-json? [path] |
| 39 | + (boolean (re-find #"node_modules[/\\](@[^/\\]+?[/\\])?[^/\\]+?[/\\]package\.json$" path))) |
| 40 | + |
| 41 | +;; the path sans the package.json part |
| 42 | +;; i.e. some_lib/package.json -> some_lib |
| 43 | +(defn- trim-package-json [s] |
| 44 | + (if (string/ends-with? s "package.json") |
| 45 | + (subs s 0 (- (count s) 12)) |
| 46 | + s)) |
| 47 | + |
| 48 | +(defn- trim-relative [path] |
| 49 | + (cond-> path |
| 50 | + (string/starts-with? path "./") |
| 51 | + (subs 2))) |
| 52 | + |
| 53 | +(defn- ->export-pkg-json [path export] |
| 54 | + (io/file |
| 55 | + (trim-package-json path) |
| 56 | + (trim-relative export) |
| 57 | + "package.json")) |
| 58 | + |
| 59 | +(defn- export-subpaths |
| 60 | + "Examine the export subpaths to compute subpackages" |
| 61 | + [pkg-jsons export-subpath export path pkg-name] |
| 62 | + ;; NOTE: ignore "." exports for now |
| 63 | + (if (= "." export-subpath) |
| 64 | + pkg-jsons |
| 65 | + ;; technically the following logic is a bit brittle since `exports` is |
| 66 | + ;; supposed to be used to hide the package structure. |
| 67 | + ;; instead, here we assume the export subpath does match the library structure |
| 68 | + ;; on disk, if we find a package.json we add it to pkg-jsons map |
| 69 | + ;; and we synthesize "name" key based on subpath |
| 70 | + (let [export-pkg-json (->export-pkg-json path export-subpath)] |
| 71 | + ;; note this will ignore export wildcards etc. |
| 72 | + (cond-> pkg-jsons |
| 73 | + (.exists export-pkg-json) |
| 74 | + (-> (assoc |
| 75 | + (.getAbsolutePath export-pkg-json) |
| 76 | + (merge |
| 77 | + (json/read-str (slurp export-pkg-json)) |
| 78 | + ;; add the name field so that path->main-name works later |
| 79 | + (when (and (map? export) |
| 80 | + (contains? export "require")) |
| 81 | + {"name" (str pkg-name (string/replace export-subpath "./" "/"))})))))))) |
| 82 | + |
| 83 | +(defn- add-exports |
| 84 | + "Given a list of pkg-jsons examine them for the `exports` field. `exports` |
| 85 | + is now the preferred way to declare an entrypoint to a Node.js library. However, |
| 86 | + for backwards compatibility it is often combined with `main`. |
| 87 | +
|
| 88 | + `export` can also be a JS object - if so, it can define subpaths. `.` points |
| 89 | + to main and other subpaths can be defined relative to that. |
| 90 | +
|
| 91 | + See https://nodejs.org/api/packages.html#main-entry-point-export for more |
| 92 | + detailed information." |
| 93 | + [pkg-jsons opts] |
| 94 | + (reduce-kv |
| 95 | + (fn [pkg-jsons path {:strs [exports] :as pkg-json}] |
| 96 | + (if (string? exports) |
| 97 | + pkg-jsons |
| 98 | + ;; map case |
| 99 | + (reduce-kv |
| 100 | + (fn [pkg-jsons export-subpath export] |
| 101 | + (export-subpaths pkg-jsons |
| 102 | + export-subpath export path (get pkg-json "name"))) |
| 103 | + pkg-jsons exports))) |
| 104 | + pkg-jsons pkg-jsons)) |
| 105 | + |
| 106 | +(defn path->main-name |
| 107 | + "Determine whether a path is a main entrypoint in the provided package.json. |
| 108 | + If so return the name entry provided in the package.json file." |
| 109 | + [path [pkg-json-path {:as pkg-json :strs [name]}] opts] |
| 110 | + (let [entries (package-json-entries opts) |
| 111 | + entry (first (keep (partial get pkg-json) entries))] |
| 112 | + (when-not (nil? entry) |
| 113 | + ;; should be the only edge case in |
| 114 | + ;; the package.json main field - Antonio |
| 115 | + (let [entry (trim-relative entry) |
| 116 | + entry-path (-> pkg-json-path (string/replace \\ \/) |
| 117 | + trim-package-json (str entry))] |
| 118 | + ;; find a package.json entry point that matches |
| 119 | + ;; the `path` |
| 120 | + (some (fn [candidate] |
| 121 | + (when (= candidate (string/replace path \\ \/)) name)) |
| 122 | + (cond-> [entry-path] |
| 123 | + ;; if we have an entry point that doesn't end in .js or .json |
| 124 | + ;; try to match some alternatives |
| 125 | + (not (or (string/ends-with? entry-path ".js") |
| 126 | + (string/ends-with? entry-path ".json"))) |
| 127 | + (into [(str entry-path ".js") (str entry-path "/index.js") (str entry-path ".json") |
| 128 | + (string/replace entry-path #"\.cjs$" ".js")]))))))) |
| 129 | + |
| 130 | +(defn- path->rel-name [path] |
| 131 | + (-> (subs path (.lastIndexOf path "node_modules")) |
| 132 | + (string/replace \\ \/) |
| 133 | + (string/replace #"node_modules[\\\/]" ""))) |
| 134 | + |
| 135 | +(defn path->provides |
| 136 | + "For a given path in node_modules, determine what namespaces that file would |
| 137 | + provide to ClojureScript. Note it is assumed that we *already* processed all |
| 138 | + package.json files and they are present via pkg-jsons parameter as we need them |
| 139 | + to figure out the provides." |
| 140 | + [path pkg-jsons opts] |
| 141 | + (merge |
| 142 | + {:file path |
| 143 | + :module-type :es6} |
| 144 | + ;; if the file is *not* a package.json, then compute what |
| 145 | + ;; namespaces it :provides to ClojureScript |
| 146 | + (when-not (package-json? path) |
| 147 | + ;; given some path search the package.json to determine whether it is a |
| 148 | + ;; main entry point or not |
| 149 | + (let [pkg-json-main (some #(path->main-name path % opts) pkg-jsons)] |
| 150 | + {:provides (let [module-rel-name (path->rel-name path) |
| 151 | + provides (cond-> [module-rel-name (string/replace module-rel-name #"\.js(on)?$" "")] |
| 152 | + (some? pkg-json-main) (conj pkg-json-main)) |
| 153 | + index-replaced (string/replace module-rel-name #"[\\\/]index\.js(on)?$" "")] |
| 154 | + (cond-> provides |
| 155 | + (and (boolean (re-find #"[\\\/]index\.js(on)?$" module-rel-name)) |
| 156 | + (not (some #{index-replaced} provides))) |
| 157 | + (conj index-replaced)))})))) |
| 158 | + |
| 159 | +(defn get-pkg-jsons |
| 160 | + "Given all a seq of files in node_modules return a map of all package.json |
| 161 | + files indexed by path. Includes any `export` package.json files as well" |
| 162 | + ([module-fseq] |
| 163 | + (get-pkg-jsons module-fseq nil)) |
| 164 | + ([module-fseq opts] |
| 165 | + (add-exports |
| 166 | + (into {} |
| 167 | + (comp (map #(.getAbsolutePath %)) |
| 168 | + (filter top-level-package-json?) |
| 169 | + (map (fn [path] [path (json/read-str (slurp path))]))) |
| 170 | + module-fseq) opts))) |
| 171 | + |
| 172 | +(defn node-file-seq->libs-spec* |
| 173 | + "Given a sequence of non-nested node_module paths where the extension ends in |
| 174 | + `.js/.json`, return lib-spec maps for each path containing at least :file, |
| 175 | + :module-type, and :provides." |
| 176 | + [module-fseq opts] |
| 177 | + (let [;; a map of all the *top-level* package.json paths and their exports |
| 178 | + ;; to the package.json contents as EDN |
| 179 | + pkg-jsons (get-pkg-jsons module-fseq opts)] |
| 180 | + (into [] |
| 181 | + (comp |
| 182 | + (map #(.getAbsolutePath %)) |
| 183 | + ;; for each file, figure out what it will provide to ClojureScript |
| 184 | + (map #(path->provides % pkg-jsons opts))) |
| 185 | + module-fseq))) |
0 commit comments