this string has no description
0
deno-doc-npm.py
1#!/usr/bin/env python3
2"""
3deno-doc-npm: Browse TypeScript API docs for any npm package.
4
5Requires: deno, fzf (for interactive mode)
6
7Usage:
8 deno-doc-npm.py <package>[@version] # full deno doc output
9 deno-doc-npm.py -i <package>[@version] # interactive fuzzy search with fzf
10 deno-doc-npm.py -i react
11 deno-doc-npm.py -i ufo@1.5.0
12 deno-doc-npm.py --preview <json_path> <kind>\\t<name> # (internal: used by fzf)
13"""
14
15import argparse
16import json
17import os
18import subprocess
19import sys
20import tempfile
21import urllib.request
22
23
24# ---------------------------------------------------------------------------
25# Type renderer
26# ---------------------------------------------------------------------------
27
28
29def _render_fn_param(p: dict) -> str:
30 """Render a single function parameter, handling rest/identifier/assign shapes."""
31 pk = p.get("kind", "")
32 if pk == "rest":
33 inner = p.get("arg", {})
34 name = inner.get("name", "")
35 ts = f": {render_type(p.get('tsType'))}" if p.get("tsType") else ""
36 return f"...{name}{ts}"
37 if pk == "assign":
38 inner = p.get("left", {})
39 name = inner.get("name", p.get("name", ""))
40 opt = "?"
41 ts = f": {render_type(p.get('tsType'))}" if p.get("tsType") else ""
42 return f"{name}{opt}{ts}"
43 # kind == "identifier" or anything else with a top-level name
44 name = p.get("name", "")
45 opt = "?" if p.get("optional") else ""
46 ts = f": {render_type(p.get('tsType'))}" if p.get("tsType") else ""
47 return f"{name}{opt}{ts}"
48
49
50def render_type(t: dict | None) -> str:
51 if t is None:
52 return "unknown"
53
54 kind = t.get("kind", "")
55
56 if kind == "typeRef":
57 ref = t["typeRef"]
58 name = ref["typeName"]
59 params = ref.get("typeParams")
60 if params:
61 return f"{name}<{', '.join(render_type(p) for p in params)}>"
62 return name
63
64 if kind == "keyword":
65 return t["keyword"]
66
67 if kind == "union":
68 return " | ".join(render_type(u) for u in t["union"])
69
70 if kind == "intersection":
71 return " & ".join(render_type(u) for u in t["intersection"])
72
73 if kind == "array":
74 inner = render_type(t["array"])
75 if " " in inner:
76 return f"({inner})[]"
77 return f"{inner}[]"
78
79 if kind == "parenthesized":
80 return f"({render_type(t['parenthesized'])})"
81
82 if kind == "fnOrConstructor":
83 fn = t["fnOrConstructor"]
84 params = ", ".join(
85 _render_fn_param(p) for p in fn.get("params", [])
86 )
87 ret = render_type(fn.get("tsType"))
88 return f"({params}) => {ret}"
89
90 if kind == "tuple":
91 return "[" + ", ".join(render_type(u) for u in t["tuple"]) + "]"
92
93 if kind == "indexed_access":
94 ia = t["indexedAccess"]
95 return f"{render_type(ia['objType'])}[{render_type(ia['indexType'])}]"
96
97 if kind == "mapped":
98 m = t["mappedType"]
99 param = m["typeParam"]
100 return (
101 f"{{ [{param['name']} in {render_type(param.get('constraint'))}]: "
102 f"{render_type(m.get('tsType'))} }}"
103 )
104
105 if kind == "typeLiteral":
106 props = t["typeLiteral"].get("properties", [])
107 parts = []
108 for p in props:
109 opt = "?" if p.get("optional") else ""
110 parts.append(f"{p['name']}{opt}: {render_type(p.get('tsType'))}")
111 return "{ " + "; ".join(parts) + " }"
112
113 if kind == "conditional":
114 c = t["conditionalType"]
115 return (
116 f"{render_type(c['checkType'])} extends {render_type(c['extendsType'])} "
117 f"? {render_type(c['trueType'])} : {render_type(c['falseType'])}"
118 )
119
120 if kind == "literal":
121 lit = t["literal"]
122 lk = lit.get("kind", "")
123 if lk == "string":
124 return f'"{lit["string"]}"'
125 if lk == "number":
126 return str(lit["number"])
127 if lk == "boolean":
128 return str(lit["boolean"]).lower()
129 return t.get("repr", kind)
130
131 if kind == "rest":
132 return f"...{render_type(t['rest'])}"
133
134 if kind == "optional":
135 return f"{render_type(t['optional'])}?"
136
137 if kind == "typeOperator":
138 op = t["typeOperator"]
139 return f"{op['operator']} {render_type(op['tsType'])}"
140
141 if kind == "typePredicate":
142 tp = t["typePredicate"]
143 prefix = "asserts " if tp.get("asserts") else ""
144 suffix = f" is {render_type(tp['type'])}" if tp.get("type") else ""
145 return f"{prefix}{tp['param']['name']}{suffix}"
146
147 if kind == "importType":
148 return t.get("repr", "import(...)")
149
150 if kind == "infer":
151 return f"infer {t['infer']['typeParam']['name']}"
152
153 if t.get("repr"):
154 return t["repr"]
155
156 return kind
157
158
159def _render_type_params(type_params: list[dict]) -> str:
160 if not type_params:
161 return ""
162 parts = []
163 for tp in type_params:
164 s = tp["name"]
165 if tp.get("constraint"):
166 s += f" extends {render_type(tp['constraint'])}"
167 if tp.get("default"):
168 s += f" = {render_type(tp['default'])}"
169 parts.append(s)
170 return "<" + ", ".join(parts) + ">"
171
172
173def _render_params(params: list[dict]) -> str:
174 return ", ".join(_render_fn_param(p) for p in params)
175
176
177# ---------------------------------------------------------------------------
178# Node renderer
179# ---------------------------------------------------------------------------
180
181
182def render_node(node: dict) -> str:
183 kind = node.get("kind", "")
184 name = node.get("name", "")
185 lines: list[str] = []
186
187 if kind == "function":
188 fd = node["functionDef"]
189 tp = _render_type_params(fd.get("typeParams", []))
190 params = _render_params(fd.get("params", []))
191 ret = f": {render_type(fd['returnType'])}" if fd.get("returnType") else ""
192 lines.append(f"function {name}{tp}({params}){ret}")
193
194 elif kind == "interface":
195 idef = node["interfaceDef"]
196 tp = _render_type_params(idef.get("typeParams", []))
197 extends = ""
198 if idef.get("extends"):
199 extends = " extends " + ", ".join(e["repr"] for e in idef["extends"])
200 lines.append(f"interface {name}{tp}{extends} {{")
201 for p in idef.get("properties", []):
202 opt = "?" if p.get("optional") else ""
203 ts = f": {render_type(p['tsType'])}" if p.get("tsType") else ""
204 lines.append(f" {p['name']}{opt}{ts}")
205 for m in idef.get("methods", []):
206 params = _render_params(m.get("params", []))
207 ret = f": {render_type(m['returnType'])}" if m.get("returnType") else ""
208 lines.append(f" {m['name']}({params}){ret}")
209 for cs in idef.get("callSignatures", []):
210 params = _render_params(cs.get("params", []))
211 ret = f": {render_type(cs['tsType'])}" if cs.get("tsType") else ""
212 lines.append(f" ({params}){ret}")
213 for ix in idef.get("indexSignatures", []):
214 params = ", ".join(
215 f"{p['name']}: {render_type(p['tsType'])}" for p in ix.get("params", [])
216 )
217 lines.append(f" [{params}]: {render_type(ix.get('tsType'))}")
218 lines.append("}")
219
220 elif kind == "typeAlias":
221 td = node["typeAliasDef"]
222 tp = _render_type_params(td.get("typeParams", []))
223 lines.append(f"type {name}{tp} = {render_type(td['tsType'])}")
224
225 elif kind == "variable":
226 vd = node["variableDef"]
227 ts = f": {render_type(vd['tsType'])}" if vd.get("tsType") else ""
228 lines.append(f"const {name}{ts}")
229
230 elif kind == "class":
231 cd = node["classDef"]
232 tp = _render_type_params(cd.get("typeParams", []))
233 extends = f" extends {cd['superClass']}" if cd.get("superClass") else ""
234 implements = ""
235 if cd.get("implements"):
236 implements = " implements " + ", ".join(i["repr"] for i in cd["implements"])
237 lines.append(f"class {name}{tp}{extends}{implements} {{")
238 for p in cd.get("properties", []):
239 mods = ""
240 if p.get("isStatic"):
241 mods += "static "
242 if p.get("accessibility"):
243 mods += p["accessibility"] + " "
244 if p.get("readonly"):
245 mods += "readonly "
246 opt = "?" if p.get("optional") else ""
247 ts = f": {render_type(p['tsType'])}" if p.get("tsType") else ""
248 lines.append(f" {mods}{p['name']}{opt}{ts}")
249 for m in cd.get("methods", []):
250 static = "static " if m.get("isStatic") else ""
251 fd = m.get("functionDef", {})
252 params = _render_params(fd.get("params", []))
253 ret = f": {render_type(fd['returnType'])}" if fd.get("returnType") else ""
254 lines.append(f" {static}{m['name']}({params}){ret}")
255 lines.append("}")
256
257 elif kind == "enum":
258 lines.append(f"enum {name} {{")
259 for member in node.get("enumDef", {}).get("members", []):
260 init = f" = {render_type(member['init'])}" if member.get("init") else ""
261 lines.append(f" {member['name']}{init},")
262 lines.append("}")
263
264 elif kind == "namespace":
265 lines.append(f"namespace {name}")
266
267 else:
268 lines.append(f"{kind} {name}")
269
270 doc = node.get("jsDoc", {}).get("doc")
271 if doc:
272 lines.append("")
273 lines.append(doc)
274
275 return "\n".join(lines)
276
277
278# ---------------------------------------------------------------------------
279# Symbol collection
280# ---------------------------------------------------------------------------
281
282
283def collect_symbols(data: dict) -> list[tuple[str, str]]:
284 """Return sorted list of (kind, name) for all symbols."""
285 symbols: set[tuple[str, str]] = set()
286 for node in data.get("nodes", []):
287 kind = node.get("kind", "")
288 if kind == "import":
289 continue
290 if kind == "namespace":
291 for elem in node.get("namespaceDef", {}).get("elements", []):
292 symbols.add((elem["kind"], elem["name"]))
293 else:
294 symbols.add((kind, node["name"]))
295 return sorted(symbols, key=lambda x: x[1])
296
297
298def find_nodes(data: dict, kind: str, name: str) -> list[dict]:
299 """Find all nodes matching kind and name (including inside namespaces)."""
300 results = []
301 for node in data.get("nodes", []):
302 nk = node.get("kind", "")
303 if nk == "namespace":
304 for elem in node.get("namespaceDef", {}).get("elements", []):
305 if elem.get("kind") == kind and elem.get("name") == name:
306 results.append(elem)
307 elif nk == kind and node.get("name") == name:
308 results.append(node)
309 return results
310
311
312def render_for_preview(json_path: str, selection: str):
313 """Render a symbol from the JSON file (used by fzf preview)."""
314 with open(json_path) as f:
315 data = json.load(f)
316 parts = selection.split("\t", 1)
317 if len(parts) == 2:
318 kind, name = parts
319 for node in find_nodes(data, kind, name):
320 print(render_node(node))
321 print()
322
323
324# ---------------------------------------------------------------------------
325# Main
326# ---------------------------------------------------------------------------
327
328
329def get_types_url(package: str) -> str | None:
330 """Get the TypeScript types URL from esm.sh's x-typescript-types header."""
331 url = f"https://esm.sh/{package}"
332 req = urllib.request.Request(url, method="HEAD")
333 try:
334 with urllib.request.urlopen(req) as resp:
335 return resp.headers.get("x-typescript-types")
336 except Exception as e:
337 print(f"Error fetching {url}: {e}", file=sys.stderr)
338 return None
339
340
341def main():
342 parser = argparse.ArgumentParser(
343 description="Browse TypeScript API docs for any npm package.",
344 )
345 parser.add_argument("package", nargs="?", help="npm package name[@version]")
346 parser.add_argument(
347 "-i", "--interactive", action="store_true",
348 help="interactive mode (fuzzy search symbols with fzf)",
349 )
350 parser.add_argument(
351 "--preview", nargs=2, metavar=("JSON_PATH", "SELECTION"),
352 help=argparse.SUPPRESS, # internal: used by fzf preview
353 )
354 args = parser.parse_args()
355
356 # Internal preview mode for fzf
357 if args.preview:
358 render_for_preview(args.preview[0], args.preview[1])
359 return
360
361 if not args.package:
362 parser.print_help()
363 sys.exit(1)
364
365 types_url = get_types_url(args.package)
366 if not types_url:
367 print(f"No TypeScript types found for {args.package}", file=sys.stderr)
368 sys.exit(1)
369
370 if not args.interactive:
371 subprocess.run(["deno", "doc", types_url])
372 return
373
374 # Interactive mode: get JSON, pick with fzf
375 result = subprocess.run(
376 ["deno", "doc", "--json", types_url],
377 capture_output=True, text=True,
378 )
379 if result.returncode != 0:
380 print(f"deno doc failed: {result.stderr}", file=sys.stderr)
381 sys.exit(1)
382
383 data = json.loads(result.stdout)
384 symbols = collect_symbols(data)
385
386 if not symbols:
387 print("No symbols found.", file=sys.stderr)
388 sys.exit(1)
389
390 # Write JSON to temp file for fzf preview to read
391 json_fd, json_path = tempfile.mkstemp(suffix=".json")
392 try:
393 with os.fdopen(json_fd, "w") as f:
394 f.write(result.stdout)
395
396 script_path = os.path.abspath(__file__)
397 fzf_input = "\n".join(f"{kind}\t{name}" for kind, name in symbols)
398
399 fzf = subprocess.run(
400 [
401 "fzf",
402 f"--prompt={args.package}> ",
403 "--delimiter=\t",
404 "--with-nth=1..",
405 f"--preview=python3 {script_path} --preview {json_path} {{}}",
406 "--preview-window=wrap",
407 ],
408 input=fzf_input, capture_output=True, text=True,
409 )
410
411 if fzf.returncode != 0 or not fzf.stdout.strip():
412 sys.exit(0)
413
414 selection = fzf.stdout.strip()
415 kind, name = selection.split("\t", 1)
416
417 for node in find_nodes(data, kind, name):
418 print(render_node(node))
419 print()
420
421 finally:
422 os.unlink(json_path)
423
424
425if __name__ == "__main__":
426 main()