|
| 1 | +defmodule NPM.ScriptInstall do |
| 2 | + @moduledoc false |
| 3 | + |
| 4 | + @state_key :npm_script_installed |
| 5 | + |
| 6 | + @spec install(map(), keyword()) :: :ok |
| 7 | + def install(deps, opts) when is_map(deps) do |
| 8 | + Application.ensure_all_started(:req) |
| 9 | + |
| 10 | + unless Keyword.get(opts, :__skip_project_check__, false) do |
| 11 | + if Mix.Project.get() do |
| 12 | + Mix.raise("NPM.install/2 cannot be used inside a Mix project. Use mix npm.install instead.") |
| 13 | + end |
| 14 | + end |
| 15 | + |
| 16 | + id = cache_id(deps) |
| 17 | + force? = Keyword.get(opts, :force, false) |
| 18 | + |
| 19 | + case :persistent_term.get(@state_key, nil) do |
| 20 | + nil -> |
| 21 | + do_install(deps, id, force?) |
| 22 | + |
| 23 | + {^id, _dir} when not force? -> |
| 24 | + :ok |
| 25 | + |
| 26 | + {^id, _dir} -> |
| 27 | + do_install(deps, id, true) |
| 28 | + |
| 29 | + _ -> |
| 30 | + Mix.raise("NPM.install/2 can only be called with the same dependencies in the given VM") |
| 31 | + end |
| 32 | + end |
| 33 | + |
| 34 | + defp do_install(deps, id, force?) do |
| 35 | + dir = install_dir(id) |
| 36 | + |
| 37 | + if force?, do: File.rm_rf!(dir) |
| 38 | + |
| 39 | + nm_dir = Path.join(dir, "node_modules") |
| 40 | + lockfile_path = Path.join(dir, "npm.lock") |
| 41 | + |
| 42 | + if not force? and File.exists?(lockfile_path) and node_modules_intact?(lockfile_path, nm_dir) do |
| 43 | + :persistent_term.put(@state_key, {id, dir}) |
| 44 | + :ok |
| 45 | + else |
| 46 | + File.mkdir_p!(dir) |
| 47 | + resolve_and_link(deps, dir, nm_dir, lockfile_path) |
| 48 | + :persistent_term.put(@state_key, {id, dir}) |
| 49 | + :ok |
| 50 | + end |
| 51 | + end |
| 52 | + |
| 53 | + defp resolve_and_link(deps, _dir, nm_dir, lockfile_path) do |
| 54 | + NPM.Resolver.clear_cache() |
| 55 | + |
| 56 | + case NPM.Resolver.resolve(deps) do |
| 57 | + {:ok, resolved} -> |
| 58 | + {_nested, flat} = Map.pop(resolved, :nested, %{}) |
| 59 | + lockfile = build_lockfile(flat) |
| 60 | + NPM.Lockfile.write(lockfile, lockfile_path) |
| 61 | + NPM.Linker.link(lockfile, nm_dir) |
| 62 | + |
| 63 | + {:error, message} -> |
| 64 | + Mix.raise("NPM.install/2 resolution failed:\n#{message}") |
| 65 | + end |
| 66 | + end |
| 67 | + |
| 68 | + defp build_lockfile(resolved) do |
| 69 | + for {name, version_str} <- resolved, into: %{} do |
| 70 | + {:ok, packument} = NPM.Registry.get_packument(name) |
| 71 | + info = Map.fetch!(packument.versions, version_str) |
| 72 | + |
| 73 | + {name, |
| 74 | + %{ |
| 75 | + version: version_str, |
| 76 | + integrity: info.dist.integrity, |
| 77 | + tarball: info.dist.tarball, |
| 78 | + dependencies: info.dependencies, |
| 79 | + optional_dependencies: info.optional_dependencies |
| 80 | + }} |
| 81 | + end |
| 82 | + end |
| 83 | + |
| 84 | + defp node_modules_intact?(lockfile_path, nm_dir) do |
| 85 | + case NPM.Lockfile.read(lockfile_path) do |
| 86 | + {:ok, lockfile} when lockfile != %{} -> |
| 87 | + Enum.all?(lockfile, fn {name, _} -> |
| 88 | + File.exists?(Path.join([nm_dir, name, "package.json"])) |
| 89 | + end) |
| 90 | + |
| 91 | + _ -> |
| 92 | + false |
| 93 | + end |
| 94 | + end |
| 95 | + |
| 96 | + defp cache_id(deps) do |
| 97 | + deps |
| 98 | + |> :erlang.term_to_binary() |
| 99 | + |> :erlang.md5() |
| 100 | + |> Base.encode16(case: :lower) |
| 101 | + end |
| 102 | + |
| 103 | + defp install_dir(id) do |
| 104 | + root = |
| 105 | + System.get_env("NPM_INSTALL_DIR") || |
| 106 | + Path.join(NPM.Cache.dir(), "installs") |
| 107 | + |
| 108 | + Path.join(root, id) |
| 109 | + end |
| 110 | + |
| 111 | + @spec installed? :: boolean() |
| 112 | + def installed? do |
| 113 | + :persistent_term.get(@state_key, nil) != nil |
| 114 | + end |
| 115 | + |
| 116 | + @spec install_dir! :: String.t() |
| 117 | + def install_dir! do |
| 118 | + case :persistent_term.get(@state_key, nil) do |
| 119 | + {_id, dir} -> dir |
| 120 | + nil -> Mix.raise("NPM.install/2 has not been called") |
| 121 | + end |
| 122 | + end |
| 123 | + |
| 124 | + @spec node_modules_dir! :: String.t() |
| 125 | + def node_modules_dir! do |
| 126 | + Path.join(install_dir!(), "node_modules") |
| 127 | + end |
| 128 | +end |
0 commit comments