this string has no description
0
deno-doc-npm.py
426 lines 14 kB view raw
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()