UNPKG

8.98 kBJavaScriptView Raw
1// external
2import * as ansi from '@bevry/ansi';
3import figures from '@bevry/figures';
4import versionClean from 'version-clean';
5// local
6import { runCommand, runVersion, runInstall, uniq, trim, loadVersion, lastLine, } from './util.js';
7function getTime() {
8 return Date.now();
9}
10/** Version */
11export class Version {
12 /** The precise version number, or at least the WIP version number/alias until it is resolved further. */
13 version;
14 /** The list of listeners we will call when updates happen. */
15 listeners = [];
16 /** An array of aliases for this version if any were used. */
17 aliases = [];
18 /** The current status of this version, initially it is `pending`. */
19 status = 'pending';
20 /**
21 * The version resolution that was successfully loaded.
22 * For instance, if a nvm alias is used such as "current" which resolves to 18.18.2 which is the system Node.js version, but is not installed via nvm itself, then trying to resolve "18.18.2" will fail with [version "v18.18.2" is not yet installed] but the original "current" resolution will work.
23 */
24 loadedVersion = null;
25 /** Whether or not this version has been successful. */
26 success = null;
27 /** Any error that occurred against this version. */
28 error = null;
29 /** The last stdout value that occurred against this version. */
30 stdout = null;
31 /** The last stderr value that occurred against this version. */
32 stderr = null;
33 /** The time the run started. */
34 started = null;
35 /** The time the run finished. */
36 finished = null;
37 /** Cache of the message. */
38 messageCache = null;
39 /** Create our Version instance */
40 constructor(version, listeners = []) {
41 this.listeners.push(...listeners);
42 this.version = String(version);
43 // If it fails to pass, then it is an alias, not a version
44 if (!versionClean(this.version)) {
45 // this uses a setter to add to this.aliases
46 this.alias = this.version;
47 }
48 }
49 /** The alias for this version if any were provided. E.g. `system` or `current` */
50 get alias() {
51 return this.aliases[0];
52 }
53 set alias(alias) {
54 if (alias) {
55 const aliases = this.aliases.concat(alias);
56 this.aliases = uniq(aliases);
57 }
58 }
59 /** Reset the version state. */
60 reset() {
61 this.success = null;
62 this.error = null;
63 this.stdout = null;
64 this.stderr = null;
65 this.started = null;
66 this.finished = null;
67 this.messageCache = null;
68 return this;
69 }
70 /** Notify that an update has occurred.
71 * @param {string?} status
72 * @returns {this}
73 * @private
74 */
75 async update(status) {
76 if (status)
77 this.status = status;
78 await Promise.all(this.listeners.map((listener) => listener(this)));
79 return this;
80 }
81 /** Load the version, which resolves the precise version number and determines if it is available or not. */
82 async load() {
83 this.status = 'loading';
84 this.reset();
85 await this.update();
86 const result = await loadVersion(this.version);
87 if (result.error) {
88 if ((result.error || '').toString().includes('not yet installed')) {
89 this.status = 'missing';
90 this.success = false;
91 }
92 else {
93 this.status = 'failed';
94 this.success = false;
95 }
96 this.error = result.error;
97 this.stdout = (result.stdout || '').toString();
98 this.stderr = (result.stderr || '').toString();
99 }
100 else {
101 const result = await runVersion(this.version);
102 if (result.error) {
103 this.status = 'failed';
104 this.success = false;
105 this.error = result.error;
106 this.stdout = (result.stdout || '').toString();
107 this.stderr = (result.stderr || '').toString();
108 }
109 else {
110 this.loadedVersion = this.loadedVersion || this.version;
111 this.version = lastLine(result.stdout); // resolve the version
112 this.status = 'loaded';
113 }
114 }
115 await this.update();
116 return this;
117 }
118 /**
119 * Install the version if it was missing.
120 * Requires the current state to be `missing`.
121 */
122 async install() {
123 if (this.status !== 'missing')
124 return this;
125 this.status = 'installing';
126 this.reset();
127 await this.update();
128 const result = await runInstall(this.version);
129 if (result.error) {
130 this.error = result.error;
131 this.status = 'missing';
132 this.success = false;
133 this.stdout = (result.stdout || '').toString();
134 this.stderr = (result.stderr || '').toString();
135 }
136 else {
137 await this.update('installed');
138 await this.load();
139 }
140 return this;
141 }
142 /**
143 * Run the command against the version.
144 * Requires the current state to be `loaded`.
145 */
146 async test(command) {
147 if (!command) {
148 throw new Error('no command provided to the testen version runner');
149 }
150 if (this.status !== 'loaded')
151 return this;
152 this.status = 'running';
153 this.reset();
154 await this.update();
155 this.started = getTime();
156 const result = await runCommand(this.loadedVersion || this.version, command);
157 this.finished = getTime();
158 this.error = result.error;
159 this.stdout = (result.stdout || '').toString();
160 this.stderr = (result.stderr || '').toString();
161 this.success = Boolean(result.error) === false;
162 await this.update(this.success ? 'passed' : 'failed');
163 return this;
164 }
165 /**
166 * Converts the version properties into an array for use of displaying in a neat table.
167 * Doesn't cache as we want to refresh timers.
168 */
169 get row() {
170 const indicator = this.success === null
171 ? ansi.dim(figures.circle)
172 : this.success
173 ? ansi.green(figures.tick)
174 : ansi.red(figures.cross);
175 const result = this.success === null
176 ? ansi.dim(this.status)
177 : this.success
178 ? ansi.green(this.status)
179 : ansi.red(this.status);
180 // note that caching prevents realtime updates of duration time
181 const ms = this.started ? (this.finished || getTime()) - this.started : 0;
182 const duration = this.started
183 ? ansi.dim(ms > 1000 ? `${Math.round(ms / 1000)}s` : `${ms}ms`)
184 : '';
185 const aliases = this.aliases.length
186 ? ansi.dim(` [${this.aliases.join('|')}]`)
187 : '';
188 const row = [
189 ' ' + indicator,
190 this.version + aliases,
191 result,
192 duration,
193 ];
194 return row;
195 }
196 /**
197 * Converts the version properties a detailed message of what has occurred with this version.
198 * Caches for each status change.
199 * @property {string} message
200 * @public
201 */
202 get message() {
203 // Cache
204 if (this.messageCache && this.messageCache[0] === this.status) {
205 return this.messageCache[1];
206 }
207 // Prepare
208 const parts = [];
209 // fetch heading
210 const heading = `Node version ${ansi.underline(this.version)} ${this.status}`;
211 if (this.status === 'missing') {
212 parts.push(ansi.bold(ansi.red(heading)));
213 }
214 else if (this.success === true) {
215 parts.push(ansi.bold(ansi.green(heading)));
216 }
217 else if (this.success === false) {
218 parts.push(ansi.bold(ansi.red(heading)));
219 }
220 else {
221 // running, loading, etc - shown in verbose mode
222 parts.push(ansi.bold(ansi.dim(heading)));
223 }
224 // Output the command that was run
225 if (this.error) {
226 parts.push(ansi.red(this.error.message.split('\n')[0]));
227 }
228 // Output stdout and stderr
229 if (this.status === 'missing') {
230 parts.push(ansi.red(`You need to run: nvm install ${this.version}`));
231 }
232 else {
233 const stdout = trim(this.stdout || '');
234 const stderr = trim(this.stderr || '');
235 if (!stdout && !stderr) {
236 parts.push(ansi.dim('no output'));
237 }
238 else {
239 if (stdout) {
240 parts.push(stdout);
241 }
242 if (stderr) {
243 parts.push(ansi.red(stderr));
244 }
245 }
246 }
247 // Join it all together
248 const message = parts.join('\n');
249 // Cache
250 this.messageCache = [this.status, message];
251 return message;
252 }
253}
254export default Version;