|
| 1 | +# @file pmpm.py |
| 2 | +# A minimum copy of npm written in pure Python. |
| 3 | +# Currently, this can only install dependencies specified by package-lock.json into node_modules. |
| 4 | +# @author Tom Tang <[email protected]> |
| 5 | +# @date July 2023 |
| 6 | + |
| 7 | +import json |
| 8 | +import io |
| 9 | +import os, shutil |
| 10 | +import tempfile |
| 11 | +import tarfile |
| 12 | +from dataclasses import dataclass |
| 13 | +import urllib.request |
| 14 | +from typing import List, Union |
| 15 | + |
| 16 | +@dataclass |
| 17 | +class PackageItem: |
| 18 | + installation_path: str |
| 19 | + tarball_url: str |
| 20 | + has_install_script: bool |
| 21 | + |
| 22 | +def parse_package_lock_json(json_data: Union[str, bytes]) -> List[PackageItem]: |
| 23 | + # See https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json#packages |
| 24 | + packages: dict = json.loads(json_data)["packages"] |
| 25 | + items: List[PackageItem] = [] |
| 26 | + for key, entry in packages.items(): |
| 27 | + if key == "": |
| 28 | + # Skip the root project (listed with a key of "") |
| 29 | + continue |
| 30 | + items.append( |
| 31 | + PackageItem( |
| 32 | + installation_path=key, # relative path from the root project folder |
| 33 | + # The path is flattened for nested node_modules, e.g., "node_modules/create-ecdh/node_modules/bn.js" |
| 34 | + tarball_url=entry["resolved"], # TODO: handle git dependencies |
| 35 | + has_install_script=entry.get("hasInstallScript", False) # the package has a preinstall, install, or postinstall script |
| 36 | + ) |
| 37 | + ) |
| 38 | + return items |
| 39 | + |
| 40 | +def download_package(tarball_url: str) -> bytes: |
| 41 | + with urllib.request.urlopen(tarball_url) as response: |
| 42 | + tarball_data: bytes = response.read() |
| 43 | + return tarball_data |
| 44 | + |
| 45 | +def unpack_package(work_dir:str, installation_path: str, tarball_data: bytes): |
| 46 | + installation_path = os.path.join(work_dir, installation_path) |
| 47 | + shutil.rmtree(installation_path, ignore_errors=True) |
| 48 | + |
| 49 | + with tempfile.TemporaryDirectory(prefix="pmpm_cache-") as tmpdir: |
| 50 | + with io.BytesIO(tarball_data) as tar_file: |
| 51 | + with tarfile.open(fileobj=tar_file) as tar: |
| 52 | + tar.extractall(tmpdir) |
| 53 | + shutil.move( |
| 54 | + os.path.join(tmpdir, "package"), # Strip the root folder |
| 55 | + installation_path |
| 56 | + ) |
| 57 | + |
| 58 | +def main(work_dir: str): |
| 59 | + with open(os.path.join(work_dir, "package-lock.json"), encoding="utf-8") as f: |
| 60 | + items = parse_package_lock_json(f.read()) |
| 61 | + for i in items: |
| 62 | + print("Installing " + i.installation_path) |
| 63 | + tarball_data = download_package(i.tarball_url) |
| 64 | + unpack_package(work_dir, i.installation_path, tarball_data) |
| 65 | + |
| 66 | +if __name__ == "__main__": |
| 67 | + main(os.getcwd()) |
0 commit comments