1: <?php
2: declare(strict_types=1);
3: /**
4: * +------------------------------------------------------------+
5: * | apnscp |
6: * +------------------------------------------------------------+
7: * | Copyright (c) Apis Networks |
8: * +------------------------------------------------------------+
9: * | Licensed under Artistic License 2.0 |
10: * +------------------------------------------------------------+
11: * | Author: Matt Saladna (msaladna@apisnetworks.com) |
12: * +------------------------------------------------------------+
13: */
14:
15: use Opcenter\Versioning;
16:
17: /**
18: * Manage and install Node versions
19: *
20: * @package core
21: */
22: class Node_Module extends \Module\Support\Anyversion
23: {
24: const NVM_LOCATION = FILESYSTEM_SHARED . '/node/nvm/nvm-exec';
25:
26: /**
27: * Execute Node within the scope of a version
28: *
29: * @param null|string $version Node version, full or shorthand (v16, v16.4.1)
30: * @param null|string $pwd working directory
31: * @param string $command
32: * @param array $args optional command arguments
33: * @return array process output from pman_run
34: */
35: public function do(...$args): array
36: {
37: $argc = count($args);
38: if ($argc === 2) {
39: // old format
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: * nvm wrapper
67: *
68: * @param null|string $name
69: * @param null|string $command
70: * @param array $args optional args
71: * @return array
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:
77: return parent::exec($pwd, $command, $args, $env);
78: }
79:
80: /**
81: * Remove an installed Node
82: *
83: * @param string $version
84: * @return bool
85: */
86: public function uninstall(string $version): bool
87: {
88: if ($version === 'lts') {
89: $version = '--lts';
90: }
91: $ret = $this->exec(null, 'uninstall %s', [$version]);
92: if (!$ret['success']) {
93: return error('failed to uninstall Node %s: %s',
94: $version,
95: coalesce($ret['stderr'], $ret['stdout'])
96: );
97: }
98:
99: return true;
100: }
101:
102: /**
103: * Assign Node version to directory
104: *
105: * Resolves symbolic names to version number
106: *
107: * @param string $version
108: * @param string $path
109: * @return bool
110: */
111: public function make_default(string $version, string $path = '~'): bool
112: {
113: $path .= '/.nvmrc';
114: if ($version === 'lts') {
115: $version = 'lts/*';
116: }
117:
118: return $this->file_put_file_contents($path, $this->resolveVersion($version), true);
119: }
120:
121: /**
122: * Get Node version for a path
123: *
124: * @param string $path
125: * @return string|null
126: */
127: public function version_from_path(string $path): string
128: {
129: $path .= '/.nvmrc';
130: if (!$this->file_exists($path)) {
131: return 'system';
132: }
133:
134: return trim($this->file_get_file_contents($path));
135: }
136:
137: public function get_default(string $path): ?string
138: {
139: deprecated_func('Use version_from_path');
140: return $this->version_from_path($path);
141: }
142:
143: /**
144: * Resolve Node alias to version number
145: *
146: * @param string $version
147: * @return null|string
148: */
149: protected function resolveVersion(string $version): ?string
150: {
151: if ($version === 'lts') {
152: $version = 'lts/*';
153: }
154: $ret = $this->exec(null, 'version %s', [$version]);
155: if ($ret['success']) {
156: $resolvedVersion = trim(preg_replace('/^\bv(?=\d)|\s+\*$/', '', $ret['output']));
157: // allow setting "12" if 12.15.1 exists
158: if (0 === strpos($resolvedVersion, $version)) {
159: return $version;
160: }
161:
162: return $resolvedVersion;
163: }
164:
165: return null;
166: }
167:
168: /**
169: * Get installed LTS version for account
170: *
171: * @param string $alias Node release alias (argon, boron, carbon, dubnium, etc)
172: * @return null|string
173: */
174: public function lts_version(string $alias = '*'): ?string
175: {
176: return $this->resolveVersion('lts/' . $alias);
177: }
178:
179: /**
180: * Install Node
181: *
182: * @param string $version
183: * @return null|string null on error, specific version installed
184: */
185: public function install(string $version): ?string
186: {
187: if ($version === 'lts') {
188: $version = '--lts';
189: }
190: $ret = $this->exec(null, 'install %s', [$version]);
191: if (!$ret['success']) {
192: return nerror('failed to install Node %s, error: %s',
193: $version,
194: coalesce($ret['stderr'], $ret['stdout'])
195: );
196: }
197:
198: $resolved = $this->exec(null, 'version %s', [$version]);
199:
200: return rtrim($resolved['stdout'][0] === 'v' ? substr($resolved['stdout'], 1) : $resolved['stdout']);
201: }
202:
203: /**
204: * Node version is installed
205: *
206: * @param string $version
207: * @return string|null version satisfied
208: */
209: public function installed(string $version, string $comparator = '='): ?string
210: {
211: if ($version === 'lts') {
212: return $this->lts_installed() ? $this->lts_version() : null;
213: }
214: $nodes = array_reverse($this->list());
215:
216: foreach ($nodes as $alias => $localVersion) {
217: if (false !== ($p1 = strpos($localVersion, '.')) &&
218: strrpos($localVersion, '.') !== $p1 &&
219: Versioning::compare($localVersion, $version, $comparator))
220: {
221: return $localVersion;
222: }
223: }
224: return null;
225: }
226:
227: /**
228: * Latest LTS is installed
229: *
230: * @return bool
231: */
232: public function lts_installed(): bool
233: {
234: $versions = $this->list();
235: $lts = $versions['lts/*'] ?? null;
236:
237: return array_has($versions, $lts);
238: }
239:
240: /**
241: * List installed Nodes
242: *
243: * @return array
244: */
245: public function list(): array
246: {
247: // 3 = no nodes installed
248: $ret = $this->exec(null, 'ls --no-colors');
249: if (!$ret['success']) {
250: if ($ret['return'] !== 3) {
251: error('failed to query nodes - is nvm installed? error: %s', $ret['error']);
252: }
253:
254: return [];
255: }
256: if (preg_match_all(\Regex::NVM_NODES, $ret['output'], $versions, PREG_SET_ORDER)) {
257: $nodes = array_combine(array_column($versions, 'alias'), array_column($versions, 'version'));
258: $active = $this->exec(null, 'version current --no-color')['stdout'];
259: if ($active !== 'none') {
260: $nodes['active'] = $active;
261: }
262: return $nodes;
263: }
264:
265: return [];
266: }
267:
268: public function get_available(): array
269: {
270: $cache = \Cache_Super_Global::spawn();
271: $key = 'node.rem';
272: if (false !== ($res = $cache->get($key))) {
273: return $res;
274: }
275: $ret = $this->exec(null, 'ls-remote');
276: if (!$ret['success']) {
277: error('failed to query remote Node versions: %s', coalesce($ret['stderr'], $ret['stdout']));
278:
279: return [];
280: }
281:
282: if (!preg_match_all('/\s*v(?<version>\S*)\s*(?:\((?:Latest )?LTS: (\S*)\))?/', $ret['stdout'], $versions,
283: PREG_SET_ORDER)) {
284: warn('failed to discover any Nodes');
285:
286: return [];
287: }
288: $versions = array_column($versions, 'version');
289: $cache->set($key, $versions);
290:
291: return $versions;
292: }
293: }