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: PATH_SEPARATOR . '~/node_modules/.bin';
77:
78: return parent::exec($pwd, $command, $args, $env);
79: }
80:
81: /**
82: * Remove an installed Node
83: *
84: * @param string $version
85: * @return bool
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: * Assign Node version to directory
105: *
106: * Resolves symbolic names to version number
107: *
108: * @param string $version
109: * @param string $path
110: * @return bool
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: * Get Node version for a path
124: *
125: * @param string $path
126: * @return string|null
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: * Resolve Node alias to version number
146: *
147: * @param string $version
148: * @return null|string
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: // allow setting "12" if 12.15.1 exists
159: if (0 === strpos($resolvedVersion, $version)) {
160: return $version;
161: }
162:
163: return $resolvedVersion;
164: }
165:
166: return null;
167: }
168:
169: /**
170: * Get installed LTS version for account
171: *
172: * @param string $alias Node release alias (argon, boron, carbon, dubnium, etc)
173: * @return null|string
174: */
175: public function lts_version(string $alias = '*'): ?string
176: {
177: return $this->resolveVersion('lts/' . $alias);
178: }
179:
180: /**
181: * Install Node
182: *
183: * @param string $version
184: * @return null|string null on error, specific version installed
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: * Node version is installed
206: *
207: * @param string $version
208: * @return string|null version satisfied
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: * Latest LTS is installed
230: *
231: * @return bool
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: * List installed Nodes
243: *
244: * @return array
245: */
246: public function list(): array
247: {
248: // 3 = no nodes installed
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: }