1: | <?php |
2: | declare(strict_types=1); |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: | use Opcenter\Versioning; |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | class Node_Module extends \Module\Support\Anyversion |
23: | { |
24: | const NVM_LOCATION = FILESYSTEM_SHARED . '/node/nvm/nvm-exec'; |
25: | |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | |
33: | |
34: | |
35: | public function do(...$args): array |
36: | { |
37: | $argc = count($args); |
38: | if ($argc === 2) { |
39: | |
40: | warn("API change: node_do uses new signature"); |
41: | [$version, $command] = $args; |
42: | $pwd = null; |
43: | $args = $env = []; |
44: | } else { |
45: | if ($argc < 5) { |
46: | $args += array_fill($argc, 5-$argc, []); |
47: | } |
48: | [$version, $pwd, $command, $args, $env] = $args; |
49: | } |
50: | |
51: | if ($version === 'lts') { |
52: | $version = '--lts'; |
53: | } else if ($version) { |
54: | $version = escapeshellarg($version); |
55: | } |
56: | |
57: | return $this->exec($pwd, "exec --silent {$version} -- {$command}", $args, $env); |
58: | } |
59: | |
60: | protected function environmentRoot(bool $reset = false): ?string |
61: | { |
62: | return FILESYSTEM_SHARED . '/node/nvm'; |
63: | } |
64: | |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | |
72: | |
73: | protected function exec(?string $pwd, string $command, array $args = [], array $env = []): array |
74: | { |
75: | $env['PATH'] = \Util_Process::DEFAULT_PATH . PATH_SEPARATOR . './node_modules/.bin' . |
76: | PATH_SEPARATOR . '~/node_modules/.bin'; |
77: | |
78: | return parent::exec($pwd, $command, $args, $env); |
79: | } |
80: | |
81: | |
82: | |
83: | |
84: | |
85: | |
86: | |
87: | public function uninstall(string $version): bool |
88: | { |
89: | if ($version === 'lts') { |
90: | $version = '--lts'; |
91: | } |
92: | $ret = $this->exec(null, 'uninstall %s', [$version]); |
93: | if (!$ret['success']) { |
94: | return error('failed to uninstall Node %s: %s', |
95: | $version, |
96: | coalesce($ret['stderr'], $ret['stdout']) |
97: | ); |
98: | } |
99: | |
100: | return true; |
101: | } |
102: | |
103: | |
104: | |
105: | |
106: | |
107: | |
108: | |
109: | |
110: | |
111: | |
112: | public function make_default(string $version, string $path = '~'): bool |
113: | { |
114: | $path .= '/.nvmrc'; |
115: | if ($version === 'lts') { |
116: | $version = 'lts/*'; |
117: | } |
118: | |
119: | return $this->file_put_file_contents($path, $this->resolveVersion($version), true); |
120: | } |
121: | |
122: | |
123: | |
124: | |
125: | |
126: | |
127: | |
128: | public function version_from_path(string $path): string |
129: | { |
130: | $path .= '/.nvmrc'; |
131: | if (!$this->file_exists($path)) { |
132: | return 'system'; |
133: | } |
134: | |
135: | return trim($this->file_get_file_contents($path)); |
136: | } |
137: | |
138: | public function get_default(string $path): ?string |
139: | { |
140: | deprecated_func('Use version_from_path'); |
141: | return $this->version_from_path($path); |
142: | } |
143: | |
144: | |
145: | |
146: | |
147: | |
148: | |
149: | |
150: | protected function resolveVersion(string $version): ?string |
151: | { |
152: | if ($version === 'lts') { |
153: | $version = 'lts/*'; |
154: | } |
155: | $ret = $this->exec(null, 'version %s', [$version]); |
156: | if ($ret['success']) { |
157: | $resolvedVersion = trim(preg_replace('/^\bv(?=\d)|\s+\*$/', '', $ret['output'])); |
158: | |
159: | if (0 === strpos($resolvedVersion, $version)) { |
160: | return $version; |
161: | } |
162: | |
163: | return $resolvedVersion; |
164: | } |
165: | |
166: | return null; |
167: | } |
168: | |
169: | |
170: | |
171: | |
172: | |
173: | |
174: | |
175: | public function lts_version(string $alias = '*'): ?string |
176: | { |
177: | return $this->resolveVersion('lts/' . $alias); |
178: | } |
179: | |
180: | |
181: | |
182: | |
183: | |
184: | |
185: | |
186: | public function install(string $version): ?string |
187: | { |
188: | if ($version === 'lts') { |
189: | $version = '--lts'; |
190: | } |
191: | $ret = $this->exec(null, 'install %s', [$version]); |
192: | if (!$ret['success']) { |
193: | return nerror('failed to install Node %s, error: %s', |
194: | $version, |
195: | coalesce($ret['stderr'], $ret['stdout']) |
196: | ); |
197: | } |
198: | |
199: | $resolved = $this->exec(null, 'version %s', [$version]); |
200: | |
201: | return rtrim($resolved['stdout'][0] === 'v' ? substr($resolved['stdout'], 1) : $resolved['stdout']); |
202: | } |
203: | |
204: | |
205: | |
206: | |
207: | |
208: | |
209: | |
210: | public function installed(string $version, string $comparator = '='): ?string |
211: | { |
212: | if ($version === 'lts') { |
213: | return $this->lts_installed() ? $this->lts_version() : null; |
214: | } |
215: | $nodes = array_reverse($this->list()); |
216: | |
217: | foreach ($nodes as $alias => $localVersion) { |
218: | if (false !== ($p1 = strpos($localVersion, '.')) && |
219: | strrpos($localVersion, '.') !== $p1 && |
220: | Versioning::compare($localVersion, $version, $comparator)) |
221: | { |
222: | return $localVersion; |
223: | } |
224: | } |
225: | return null; |
226: | } |
227: | |
228: | |
229: | |
230: | |
231: | |
232: | |
233: | public function lts_installed(): bool |
234: | { |
235: | $versions = $this->list(); |
236: | $lts = $versions['lts/*'] ?? null; |
237: | |
238: | return array_has($versions, $lts); |
239: | } |
240: | |
241: | |
242: | |
243: | |
244: | |
245: | |
246: | public function list(): array |
247: | { |
248: | |
249: | $ret = $this->exec(null, 'ls --no-colors'); |
250: | if (!$ret['success']) { |
251: | if ($ret['return'] !== 3) { |
252: | error('failed to query nodes - is nvm installed? error: %s', $ret['error']); |
253: | } |
254: | |
255: | return []; |
256: | } |
257: | if (preg_match_all(\Regex::NVM_NODES, $ret['output'], $versions, PREG_SET_ORDER)) { |
258: | $nodes = array_combine(array_column($versions, 'alias'), array_column($versions, 'version')); |
259: | $active = $this->exec(null, 'version current --no-color')['stdout']; |
260: | if ($active !== 'none') { |
261: | $nodes['active'] = $active; |
262: | } |
263: | return $nodes; |
264: | } |
265: | |
266: | return []; |
267: | } |
268: | |
269: | public function get_available(): array |
270: | { |
271: | $cache = \Cache_Super_Global::spawn(); |
272: | $key = 'node.rem'; |
273: | if (false !== ($res = $cache->get($key))) { |
274: | return $res; |
275: | } |
276: | $ret = $this->exec(null, 'ls-remote'); |
277: | if (!$ret['success']) { |
278: | error('failed to query remote Node versions: %s', coalesce($ret['stderr'], $ret['stdout'])); |
279: | |
280: | return []; |
281: | } |
282: | |
283: | if (!preg_match_all('/\s*v(?<version>\S*)\s*(?:\((?:Latest )?LTS: (\S*)\))?/', $ret['stdout'], $versions, |
284: | PREG_SET_ORDER)) { |
285: | warn('failed to discover any Nodes'); |
286: | |
287: | return []; |
288: | } |
289: | $versions = array_column($versions, 'version'); |
290: | $cache->set($key, $versions); |
291: | |
292: | return $versions; |
293: | } |
294: | } |