CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/scripts/check_npm_packages.py
Views: 687
1
#!/usr/bin/env python3
2
"""
3
Consistency check for npm packages across node modules
4
5
Hint: to get a "real time" info while working on resolving this, run
6
$ /usr/bin/watch -n1 check_npm_packages.py
7
in the COCALC_ROOT dir in a separate terminal.
8
"""
9
10
import os
11
from os.path import abspath, dirname, basename
12
import json
13
from collections import defaultdict
14
from pprint import pprint
15
from subprocess import run, PIPE
16
from typing import List, Set, Dict, Tuple, Optional
17
from typing_extensions import Final
18
19
T_installs = Dict[str, Dict[str, str]]
20
21
root: Final[str] = os.environ.get('COCALC_ROOT', abspath(os.curdir))
22
23
# these packages are known to be inconsistent on purpose
24
# async and immutable are a little bit more modern in packages/frontend,
25
# while they are behind elsewhere (but at the same vesion)
26
# we don't want to introduce any other inconsistencies...
27
28
# whitelisting typescript is temporary really just for @cocalc/util -- see https://github.com/sagemathinc/cocalc/issues/5888
29
30
whitelist: Final[List[str]] = [
31
'async', # we really want to get rid of using this at all! And we have to use two very different versions across our packages :-(
32
'entities', # it breaks when you upgrade in static to 4.x
33
]
34
35
36
# NOTE: test/puppeteer is long dead...
37
def pkg_dirs() -> List[str]:
38
search = run(['git', 'ls-files', '--', '../**package.json'], stdout=PIPE)
39
data = search.stdout.decode('utf8')
40
packages = [
41
abspath(x) for x in data.splitlines() if 'test/puppeteer/' not in x
42
]
43
return packages
44
45
46
def get_versions(packages) -> Tuple[T_installs, Set[str]]:
47
installs: T_installs = defaultdict(dict)
48
modules: Set[str] = set()
49
50
for pkg in packages:
51
for dep_type in ['dependencies', 'devDependencies']:
52
pkgs = json.load(open(pkg))
53
module = basename(dirname(pkg))
54
modules.add(module)
55
for name, vers in pkgs.get(dep_type, {}).items():
56
assert installs[name].get(module) is None, \
57
f"{name}/{module} already exists as a dependency – don't add it as a devDepedency as well"
58
# don't worry about the patch version
59
installs[name][module] = vers[:vers.rfind('.')]
60
return installs, modules
61
62
63
def print_table(installs: T_installs, modules) -> Tuple[str, int, List[str]]:
64
cnt = 0
65
incon = [] # new, not whitelisted inconsistencies
66
table = ""
67
68
table += f"{'':<30s}"
69
for mod in sorted(modules):
70
table += f"{mod:<15s}"
71
table += "\n"
72
73
for pkg, inst in sorted(installs.items()):
74
if len(set(inst.values())) == 1: continue
75
cnt += 1
76
if pkg not in whitelist and not pkg.startswith('@cocalc'):
77
incon.append(pkg)
78
table += f"{pkg:<30s}"
79
for mod in sorted(modules):
80
vers = inst.get(mod, '')
81
table += f"{vers:<15s}"
82
table += "\n"
83
return table, cnt, incon
84
85
86
def main() -> None:
87
packages: Final = pkg_dirs()
88
89
# We mix up dependencies and devDepdencies into one. Otherwise cross-inconsistencies do not show up.
90
# Also, get_versions fails if there is the same module as a dependencies AND devDependencies in the same package.
91
main_pkgs, main_mods = get_versions(packages)
92
93
table, cnt, incon = print_table(main_pkgs, main_mods)
94
95
if cnt < len(whitelist):
96
msg = f"EVERY whitelisted package *must* have inconsistent versions, or you just badly broke something in one of these packages!: {', '.join(whitelist)}"
97
print(msg)
98
raise RuntimeError(msg)
99
100
elif cnt > len(whitelist):
101
print(table)
102
print(f"\nThere are {cnt} inconsistencies")
103
if len(incon) > 0:
104
print(
105
f"of which these are not whitelisted: {incon} -- they must be fixed"
106
)
107
raise RuntimeError(
108
f"fix new package version inconsistencies of {incon}\n\n\n")
109
else:
110
if cnt > 0:
111
print(f"All are whitelisted and no action is warranted.")
112
113
else:
114
print("SUCCESS: All package.json consistency checks passed.")
115
116
117
if __name__ == '__main__':
118
main()
119
120