#!/usr/bin/env python3"""Create a tar archive of node_modules files belonging to the given packages'transitive dependency trees, written to stdout. If no packages are given,defaults to all entries in package.json dependencies."""import argparseimport jsonimport loggingimport osimport sysimport tarfilefrom collections.abc import Iterator, Mappingfrom pathlib import Pathfrom typing import Anylogger = logging.getLogger(__name__)class PackageDB:def __init__(self) -> None:with Path('package-lock.json').open() as fp:self.packages: Mapping[str, Any] = json.load(fp)['packages']def depends(self) -> Iterator[str]:with Path('package.json').open() as fp:yield from json.load(fp).get('dependencies', {})def resolve(self, name: str, scope: Path) -> tuple[Path, Any]:while True:key = scope / 'node_modules' / nametry:return key, self.packages[str(key)]except KeyError:if scope == Path('.'):raisescope = scope.parent.parent # skip past node_modules/pkgdef files(self, name: str, scope: Path = Path('.')) -> Iterator[Path]:key, pkg = self.resolve(name, scope)for dirpath, dirnames, filenames in os.walk(key):# Don't descend into nested node_modules — those are separate lockfile entries.dirnames[:] = [d for d in dirnames if d != 'node_modules']for fname in filenames:yield Path(dirpath) / fnamefor dep_name in pkg.get('dependencies', {}):yield from self.files(dep_name, scope=key)def main() -> None:parser = argparse.ArgumentParser(description=__doc__)parser.add_argument('-d', '--debug', action='store_true')parser.add_argument('-C', '--directory', type=Path, default=Path('.'))parser.add_argument('--package-lock', action='store_true')parser.add_argument('packages', nargs='*', metavar='PACKAGE')args = parser.parse_args()logging.basicConfig(level=logging.DEBUG if args.debug else logging.WARNING)if sys.stdout.isatty():sys.exit('refusing to write tar to a terminal')os.chdir(args.directory)db = PackageDB()packages = args.packages or db.depends()files: set[Path] = set()if args.package_lock:files.add(Path('node_modules') / '.package-lock.json')for pkg in packages:files.update(db.files(pkg))with tarfile.open(mode='w|', fileobj=sys.stdout.buffer, format=tarfile.USTAR_FORMAT) as tf:for f in sorted(files):with f.open('rb') as fh:info = tf.gettarinfo(arcname=str(f), fileobj=fh)info.uid = info.gid = 0info.uname = info.gname = 'root'tf.addfile(info, fh)if __name__ == '__main__':main()