Skip to content

Commit a464000

Browse files
committed
Add NPM.TypesCompanion module (suggest, types_package, has_own_types?, orphaned_types) with 11 tests for @types/* companion package suggestions\n\nResult: {"status":"keep","test_count":2697}
1 parent bb58203 commit a464000

3 files changed

Lines changed: 161 additions & 0 deletions

File tree

autoresearch.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,4 @@
249249
{"run":248,"commit":"a549720","metric":2653,"metrics":{},"status":"keep","description":"Add NPM.ProjectInit module (initialized?, create_package_json, checklist, ready?, format_checklist) with 9 tests for npm project scaffolding and setup verification","timestamp":1773420228096,"segment":0}
250250
{"run":249,"commit":"d70e035","metric":2665,"metrics":{},"status":"keep","description":"Add NPM.ErrorMessage module (format, suggestion, retryable?) with 12 tests for structured error formatting with actionable suggestions","timestamp":1773420275238,"segment":0}
251251
{"run":250,"commit":"fe47c8b","metric":2675,"metrics":{},"status":"keep","description":"Add NPM.ProgressReporter module (resolving, fetching, linking, done, breakdown, format_time) with 10 tests for install progress output","timestamp":1773420336116,"segment":0}
252+
{"run":251,"commit":"bb58203","metric":2686,"metrics":{},"status":"keep","description":"Add NPM.CacheStats module (hit_miss, hit_rate, disk_size, list_cached, format, format_size) with 11 tests for cache usage statistics and metrics","timestamp":1773420393329,"segment":0}

lib/npm/types_companion.ex

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
defmodule NPM.TypesCompanion do
2+
@moduledoc """
3+
Suggests @types/* companion packages for dependencies that need TypeScript type definitions.
4+
"""
5+
6+
@bundled_types ~w(typescript @types/node)
7+
@has_own_types ~w(axios zod prisma date-fns)
8+
9+
@doc """
10+
Suggests @types/* packages for dependencies that likely need them.
11+
"""
12+
@spec suggest(map()) :: [%{package: String.t(), types_package: String.t()}]
13+
def suggest(pkg_data) do
14+
deps = Map.get(pkg_data, "dependencies", %{})
15+
dev_deps = Map.get(pkg_data, "devDependencies", %{})
16+
all_installed = Map.merge(deps, dev_deps) |> Map.keys() |> MapSet.new()
17+
18+
own_types = MapSet.new(@has_own_types)
19+
20+
deps
21+
|> Map.keys()
22+
|> Enum.reject(fn name -> scoped?(name) or MapSet.member?(own_types, name) end)
23+
|> Enum.map(fn name -> {name, types_package(name)} end)
24+
|> Enum.reject(fn {_, types} -> MapSet.member?(all_installed, types) end)
25+
|> Enum.map(fn {name, types} -> %{package: name, types_package: types} end)
26+
|> Enum.sort_by(& &1.package)
27+
end
28+
29+
@doc """
30+
Returns the @types/* package name for a given package.
31+
"""
32+
@spec types_package(String.t()) :: String.t()
33+
def types_package("@" <> _ = scoped) do
34+
name = scoped |> String.replace("/", "__") |> String.trim_leading("@")
35+
"@types/#{name}"
36+
end
37+
38+
def types_package(name), do: "@types/#{name}"
39+
40+
@doc """
41+
Checks if a package typically ships its own types.
42+
"""
43+
@spec has_own_types?(map()) :: boolean()
44+
def has_own_types?(pkg_data) do
45+
Map.has_key?(pkg_data, "types") or
46+
Map.has_key?(pkg_data, "typings") or
47+
Map.get(pkg_data, "name", "") in @has_own_types
48+
end
49+
50+
@doc """
51+
Returns packages that are @types/* but whose companion is not installed.
52+
"""
53+
@spec orphaned_types(map()) :: [String.t()]
54+
def orphaned_types(pkg_data) do
55+
all_deps =
56+
Map.merge(
57+
Map.get(pkg_data, "dependencies", %{}),
58+
Map.get(pkg_data, "devDependencies", %{})
59+
)
60+
61+
all_keys = Map.keys(all_deps) |> MapSet.new()
62+
63+
all_keys
64+
|> Enum.filter(&String.starts_with?(&1, "@types/"))
65+
|> Enum.reject(fn types_pkg ->
66+
companion = types_pkg |> String.trim_leading("@types/") |> String.replace("__", "/")
67+
companion = if String.contains?(companion, "/"), do: "@#{companion}", else: companion
68+
MapSet.member?(all_keys, companion)
69+
end)
70+
|> Enum.sort()
71+
end
72+
73+
@doc """
74+
Returns bundled types packages.
75+
"""
76+
@spec bundled :: [String.t()]
77+
def bundled, do: @bundled_types
78+
79+
defp scoped?("@" <> _), do: true
80+
defp scoped?(_), do: false
81+
end

test/npm/types_companion_test.exs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
defmodule NPM.TypesCompanionTest do
2+
use ExUnit.Case, async: true
3+
4+
describe "suggest" do
5+
test "suggests types for untyped deps" do
6+
data = %{"dependencies" => %{"express" => "^4.18", "lodash" => "^4.17"}}
7+
suggestions = NPM.TypesCompanion.suggest(data)
8+
names = Enum.map(suggestions, & &1.types_package)
9+
assert "@types/express" in names
10+
assert "@types/lodash" in names
11+
end
12+
13+
test "skips already installed types" do
14+
data = %{
15+
"dependencies" => %{"express" => "^4.18"},
16+
"devDependencies" => %{"@types/express" => "^4.17"}
17+
}
18+
19+
suggestions = NPM.TypesCompanion.suggest(data)
20+
refute Enum.any?(suggestions, &(&1.types_package == "@types/express"))
21+
end
22+
23+
test "skips packages with own types" do
24+
data = %{"dependencies" => %{"axios" => "^1.0"}}
25+
suggestions = NPM.TypesCompanion.suggest(data)
26+
refute Enum.any?(suggestions, &(&1.package == "axios"))
27+
end
28+
29+
test "empty for no deps" do
30+
assert [] = NPM.TypesCompanion.suggest(%{})
31+
end
32+
end
33+
34+
describe "types_package" do
35+
test "regular package" do
36+
assert "@types/lodash" = NPM.TypesCompanion.types_package("lodash")
37+
end
38+
39+
test "scoped package" do
40+
assert "@types/babel__core" = NPM.TypesCompanion.types_package("@babel/core")
41+
end
42+
end
43+
44+
describe "has_own_types?" do
45+
test "true for types field" do
46+
assert NPM.TypesCompanion.has_own_types?(%{"types" => "./index.d.ts"})
47+
end
48+
49+
test "true for typings field" do
50+
assert NPM.TypesCompanion.has_own_types?(%{"typings" => "./index.d.ts"})
51+
end
52+
53+
test "false without types" do
54+
refute NPM.TypesCompanion.has_own_types?(%{"name" => "express"})
55+
end
56+
end
57+
58+
describe "orphaned_types" do
59+
test "detects orphaned @types packages" do
60+
data = %{
61+
"dependencies" => %{"react" => "^18.0"},
62+
"devDependencies" => %{"@types/react" => "^18.0", "@types/lodash" => "^4.14"}
63+
}
64+
65+
orphans = NPM.TypesCompanion.orphaned_types(data)
66+
assert "@types/lodash" in orphans
67+
refute "@types/react" in orphans
68+
end
69+
70+
test "empty when all have companions" do
71+
data = %{
72+
"dependencies" => %{"react" => "^18.0"},
73+
"devDependencies" => %{"@types/react" => "^18.0"}
74+
}
75+
76+
assert [] = NPM.TypesCompanion.orphaned_types(data)
77+
end
78+
end
79+
end

0 commit comments

Comments
 (0)