#!/usr/bin/env python3 """Walk every .js in a package and check that each require()/import target resolves, following Node's resolution rules against the vendored node_modules tree.""" import json import os import re import sys ROOT = os.path.abspath(sys.argv[1]) BUILTIN = set("""assert async_hooks buffer child_process cluster console constants crypto dgram diagnostics_channel dns domain events fs http http2 https inspector module net os path perf_hooks process punycode querystring readline repl stream string_decoder sys timers tls trace_events tty url util v8 vm wasi worker_threads zlib""".split()) SPEC = re.compile(r"""(?:require\(|(?:^|[\s;{}])(?:import|export)[^;\n]*?\bfrom\s*|import\()\s*['"]([^'"]+)['"]""", re.M) DYNAMIC = re.compile(r"require\(\s*[^'\")\s]") COMMENTS = re.compile(r"/\*.*?\*/|^\s*//.*$", re.S | re.M) # Optional at runtime: loaded only under a config or platform this module never uses. OPTIONAL = {"re2", "expo-random", "tinybench", "uri-js", "neostandard"} SKIP_DIRS = ("test", "benchmark", "benchmarks", "example", "examples", ".github") def load_json(p): try: with open(p) as fh: return json.load(fh) except Exception: return {} def resolve_file(base): for cand in (base, base + ".js", base + ".json", base + ".cjs", base + ".mjs", base + ".node"): if os.path.isfile(cand): return cand if os.path.isdir(base): pj = load_json(os.path.join(base, "package.json")) for key in ("main", "module"): if isinstance(pj.get(key), str): hit = resolve_file(os.path.join(base, pj[key])) if hit: return hit if pj.get("exports"): return base # exports map: trust it, too complex to model here for idx in ("index.js", "index.json", "index.cjs", "index.node"): if os.path.isfile(os.path.join(base, idx)): return os.path.join(base, idx) return None def resolve_bare(spec, from_dir): d = from_dir while True: hit = resolve_file(os.path.join(d, "node_modules", *spec.split("/"))) if hit: return hit # scoped or deep import into a package that exists parts = spec.split("/") pkg = "/".join(parts[:2]) if spec.startswith("@") else parts[0] pkg_dir = os.path.join(d, "node_modules", *pkg.split("/")) if os.path.isdir(pkg_dir) and len(parts) > (2 if spec.startswith("@") else 1): sub = resolve_file(os.path.join(pkg_dir, *parts[(2 if spec.startswith("@") else 1):])) if sub: return sub if load_json(os.path.join(pkg_dir, "package.json")).get("exports"): return pkg_dir if os.path.dirname(d) == d or len(d) <= len(ROOT) - 1: return None d = os.path.dirname(d) missing, dynamic, files, optional_hits = [], [], 0, set() for dirpath, dirnames, filenames in os.walk(ROOT): dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS] if dirpath.endswith((os.sep + "browser", os.sep + "types")): continue for fn in filenames: if not fn.endswith((".js", ".cjs", ".mjs")): continue if fn.endswith((".native.js", ".browser.js")) or fn.startswith("eslint.config"): continue path = os.path.join(dirpath, fn) files += 1 try: src = COMMENTS.sub("", open(path, encoding="utf-8", errors="replace").read()) except OSError: continue if DYNAMIC.search(src): dynamic.append(os.path.relpath(path, ROOT)) for spec in set(SPEC.findall(src)): if spec.startswith("node:") or spec.split("/")[0] in BUILTIN: continue if spec in OPTIONAL: optional_hits.add((os.path.relpath(path, ROOT), spec)) continue if spec.startswith("."): hit = resolve_file(os.path.normpath(os.path.join(dirpath, spec))) else: hit = resolve_bare(spec, dirpath) if not hit: missing.append((os.path.relpath(path, ROOT), spec)) print("scanned %d js files" % files) if dynamic: print("\n%d file(s) use a computed require() (not statically checkable):" % len(dynamic)) for d in sorted(dynamic)[:12]: print(" ", d) if optional_hits: print("\n%d optional dep(s), not loaded on this platform/config:" % len(optional_hits)) for f, spec in sorted(optional_hits): print(" %-64s -> %s" % (f, spec)) if missing: print("\nUNRESOLVED (%d):" % len(missing)) for f, s in sorted(set(missing)): print(" %-70s -> %s" % (f, s)) sys.exit(1) print("\nevery static require/import resolves")