lljz66 commited on
Commit
b89c33e
·
verified ·
1 Parent(s): 0b20c9c

npm: use /latest, the abbreviated doc strips repository and metadata

Browse files
Files changed (1) hide show
  1. src/devnet/sources/npm.py +36 -42
src/devnet/sources/npm.py CHANGED
@@ -5,17 +5,18 @@ from urllib.parse import quote
5
 
6
  from ..core import Resource, UpstreamError, get
7
 
8
- API = "https://registry.npmjs.org/{name}"
 
 
 
 
 
 
 
9
 
10
- # The abbreviated "corgi" document: every version, but only the fields an
11
- # installer needs. The full document is megabytes for popular packages and
12
- # would trip the response size cap.
13
- _ABBREVIATED = {"Accept": "application/vnd.npm.install-v1+json"}
14
-
15
- # ponytail: repo_slug is parsed from the `repository` field, which npm lets
16
- # authors write in at least six shapes. Ceiling: a monorepo `directory` field is
17
- # ignored, so the slug points at the repo root rather than the sub-package.
18
- # Upgrade: cross-check package.json `exports` when a wrong root shows up.
19
 
20
 
21
  def _text(value) -> str | None:
@@ -23,9 +24,9 @@ def _text(value) -> str | None:
23
  if isinstance(value, str):
24
  return value.strip() or None
25
  if isinstance(value, dict):
26
- for key in ("type", "name", "url"):
27
- if isinstance(value.get(key), str) and value[key].strip():
28
- return value[key].strip()
29
  return None
30
 
31
 
@@ -47,22 +48,20 @@ def _repo_slug(repository) -> str | None:
47
 
48
 
49
  def package(name: str) -> dict:
50
- """Current dist-tag, publish date, dependencies and release history."""
51
  raw_name = str(name or "").strip().lower()
52
  if not raw_name or len(raw_name) > 214:
53
  raise UpstreamError("invalid npm package name")
54
 
55
- payload = get(
56
- API.format(name=quote(raw_name, safe="")),
57
- headers=_ABBREVIATED,
58
- ttl=900,
59
- )
60
- tags = payload.get("dist-tags") or {}
61
- latest = tags.get("latest") or ""
62
- versions = payload.get("versions") or {}
63
- times = payload.get("time") or {}
64
- manifest = versions.get(latest) or {}
65
  maintainers = payload.get("maintainers") or []
 
66
  canonical = payload.get("name") or raw_name
67
 
68
  return Resource(
@@ -73,34 +72,29 @@ def package(name: str) -> dict:
73
  url=f"https://www.npmjs.com/package/{canonical}",
74
  description=payload.get("description"),
75
  author={"name": _text(payload.get("author"))},
76
- timestamps={
77
- "published_at": times.get(latest) or None,
78
- "updated_at": times.get("modified") or None,
 
 
79
  },
80
- metrics={"versions": len(versions), "maintainers": len(maintainers)},
81
  tags=[k for k in (payload.get("keywords") or []) if isinstance(k, str)][:12],
82
  metadata={
83
  "license": _text(payload.get("license")),
84
- "repo_slug": _repo_slug(payload.get("repository")),
85
- "repository": _text(payload.get("repository")) or (
86
- payload.get("repository").get("url")
87
- if isinstance(payload.get("repository"), dict)
88
- else None
89
- ),
90
- "dist_tags": tags,
91
  "homepage": _text(payload.get("homepage")),
 
92
  },
93
  data={
94
- "latest_version": latest,
95
- "published_at": times.get(latest) or None,
96
- "created_at": times.get("created") or None,
97
- "versions": sorted(versions)[-30:],
98
- "dependencies": manifest.get("dependencies") or {},
99
- "peer_dependencies": manifest.get("peerDependencies") or {},
100
- "engines": manifest.get("engines") or {},
101
  "maintainers": [
102
  m.get("name") for m in maintainers if isinstance(m, dict) and m.get("name")
103
  ][:10],
 
104
  },
105
  provenance={"provider": "npm_registry", "url": API.format(name=raw_name)},
106
  ).dict()
 
5
 
6
  from ..core import Resource, UpstreamError, get
7
 
8
+ # The /latest document carries full metadata for one version. The full package
9
+ # document adds version history and publish dates, but trips the response size
10
+ # cap for popular packages - @types/node has thousands of versions.
11
+ #
12
+ # ponytail: no version list and no publish dates. Ceiling: npm alone cannot answer
13
+ # "is this package abandoned?" - use the repo's last push or its RSS feed instead.
14
+ # Upgrade: a second call to the abbreviated document when a caller needs history.
15
+ API = "https://registry.npmjs.org/{name}/latest"
16
 
17
+ # ponytail: repo_slug is parsed from `repository`, which npm lets authors write in
18
+ # at least six shapes. Ceiling: a monorepo `directory` field is ignored, so the
19
+ # slug points at the repo root rather than the sub-package.
 
 
 
 
 
 
20
 
21
 
22
  def _text(value) -> str | None:
 
24
  if isinstance(value, str):
25
  return value.strip() or None
26
  if isinstance(value, dict):
27
+ for field in ("type", "name", "url"):
28
+ if isinstance(value.get(field), str) and value[field].strip():
29
+ return value[field].strip()
30
  return None
31
 
32
 
 
48
 
49
 
50
  def package(name: str) -> dict:
51
+ """Current published version, dependencies, license and source repository."""
52
  raw_name = str(name or "").strip().lower()
53
  if not raw_name or len(raw_name) > 214:
54
  raise UpstreamError("invalid npm package name")
55
 
56
+ payload = get(API.format(name=quote(raw_name, safe="")), ttl=900)
57
+ if not isinstance(payload, dict):
58
+ raise UpstreamError("unexpected npm response")
59
+
60
+ version = payload.get("version") or ""
61
+ dependencies = payload.get("dependencies") or {}
62
+ peer = payload.get("peerDependencies") or {}
 
 
 
63
  maintainers = payload.get("maintainers") or []
64
+ repository = payload.get("repository")
65
  canonical = payload.get("name") or raw_name
66
 
67
  return Resource(
 
72
  url=f"https://www.npmjs.com/package/{canonical}",
73
  description=payload.get("description"),
74
  author={"name": _text(payload.get("author"))},
75
+ timestamps={},
76
+ metrics={
77
+ "dependencies": len(dependencies),
78
+ "peer_dependencies": len(peer),
79
+ "maintainers": len(maintainers),
80
  },
 
81
  tags=[k for k in (payload.get("keywords") or []) if isinstance(k, str)][:12],
82
  metadata={
83
  "license": _text(payload.get("license")),
84
+ "repo_slug": _repo_slug(repository),
85
+ "repository": repository if isinstance(repository, str) else (repository or {}).get("url"),
 
 
 
 
 
86
  "homepage": _text(payload.get("homepage")),
87
+ "deprecated": _text(payload.get("deprecated")),
88
  },
89
  data={
90
+ "latest_version": version,
91
+ "dependencies": dependencies,
92
+ "peer_dependencies": peer,
93
+ "engines": payload.get("engines") or {},
 
 
 
94
  "maintainers": [
95
  m.get("name") for m in maintainers if isinstance(m, dict) and m.get("name")
96
  ][:10],
97
+ "tarball": (payload.get("dist") or {}).get("tarball"),
98
  },
99
  provenance={"provider": "npm_registry", "url": API.format(name=raw_name)},
100
  ).dict()