Current section
Files
Jump to
Current section
Files
lib/npm/workspace.ex
defmodule NPM.Workspace do
@moduledoc """
Workspace management for npm monorepos.
Handles discovery and resolution of workspace packages defined
in the root `package.json` via the `workspaces` field.
"""
@doc """
Discovers workspace packages from the root package.json.
Reads the `workspaces` field and resolves glob patterns to actual
package directories. Returns a list of workspace info maps.
"""
@spec discover(String.t()) :: {:ok, [map()]} | {:error, term()}
def discover(root_dir \\ ".") do
pkg_path = Path.join(root_dir, "package.json")
with {:ok, workspaces} <- NPM.Package.JSON.read_workspaces(pkg_path),
packages <- resolve_workspaces(workspaces, root_dir) do
{:ok, packages}
end
end
@doc """
Returns a list of workspace package names.
"""
@spec names(String.t()) :: {:ok, [String.t()]} | {:error, term()}
def names(root_dir \\ ".") do
case discover(root_dir) do
{:ok, packages} -> {:ok, Enum.map(packages, & &1.name)}
error -> error
end
end
@doc """
Returns a dependency graph of inter-workspace dependencies.
Finds which workspace packages depend on other workspace packages.
"""
@spec dep_graph([map()]) :: %{String.t() => [String.t()]}
def dep_graph(packages) do
ws_names = MapSet.new(Enum.map(packages, & &1.name))
Map.new(packages, fn pkg ->
internal_deps =
pkg.dependencies
|> Map.keys()
|> Enum.filter(&MapSet.member?(ws_names, &1))
|> Enum.sort()
{pkg.name, internal_deps}
end)
end
@doc """
Returns the topological build order for workspace packages.
Packages with no inter-workspace dependencies come first.
"""
@spec build_order([map()]) :: [String.t()]
def build_order(packages) do
graph = dep_graph(packages)
topo_sort(graph)
end
@doc """
Checks if a directory is a workspace root (has workspaces field).
"""
@spec workspace_root?(String.t()) :: boolean()
def workspace_root?(dir \\ ".") do
case NPM.Package.JSON.read_workspaces(Path.join(dir, "package.json")) do
{:ok, ws} when ws != [] -> true
_ -> false
end
end
defp resolve_workspaces(patterns, base_dir) do
patterns
|> NPM.Package.JSON.expand_workspaces(base_dir)
|> Enum.flat_map(&read_workspace_package/1)
end
defp read_workspace_package(ws_dir) do
pkg_path = Path.join(ws_dir, "package.json")
case File.read(pkg_path) do
{:ok, content} ->
data = NPM.JSON.decode!(content)
[
%{
name: data["name"] || Path.basename(ws_dir),
version: data["version"] || "0.0.0",
path: ws_dir,
dependencies: Map.merge(data["dependencies"] || %{}, data["devDependencies"] || %{})
}
]
_ ->
[]
end
end
defp topo_sort(adj) do
g = :digraph.new()
try do
Enum.each(adj, fn {name, _} -> :digraph.add_vertex(g, name) end)
Enum.each(adj, fn {name, deps} ->
Enum.each(deps, fn dep ->
:digraph.add_vertex(g, dep)
:digraph.add_edge(g, dep, name)
end)
end)
case :digraph_utils.topsort(g) do
false -> Map.keys(adj)
sorted -> sorted
end
after
:digraph.delete(g)
end
end
end