@endgame45/nodescript 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -0
- package/src/nodescript.js +327 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@endgame45/nodescript",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "src/nodescript.js",
|
|
5
|
+
"files": [
|
|
6
|
+
"src"
|
|
7
|
+
],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "echo \"No tests specified\" && exit 0",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"prepare": "npm run lint"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"nodescript",
|
|
15
|
+
"node",
|
|
16
|
+
"library"
|
|
17
|
+
],
|
|
18
|
+
"author": "horizonstudios",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"chalk": "^4.1.2"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"eslint": "^7.32.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const util = require('util');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_FONTS = new Set([
|
|
5
|
+
'bold', 'dim', 'italic', 'underline', 'inverse', 'hidden', 'strikethrough'
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function parseColor(styler, color, isBg = false) {
|
|
9
|
+
if (!color) return styler;
|
|
10
|
+
const c = String(color).trim();
|
|
11
|
+
try {
|
|
12
|
+
if (/^#/.test(c)) {
|
|
13
|
+
return isBg ? styler.bgHex(c) : styler.hex(c);
|
|
14
|
+
}
|
|
15
|
+
if (/^rgb\(/i.test(c)) {
|
|
16
|
+
const nums = c
|
|
17
|
+
.replace(/^[^(]*\(/, '')
|
|
18
|
+
.replace(/\).*/, '')
|
|
19
|
+
.split(',')
|
|
20
|
+
.map(n => parseInt(n, 10) || 0);
|
|
21
|
+
return isBg ? styler.bgRgb(nums[0], nums[1], nums[2]) : styler.rgb(nums[0], nums[1], nums[2]);
|
|
22
|
+
}
|
|
23
|
+
// fallback to CSS keyword (named color)
|
|
24
|
+
return isBg ? styler.bgKeyword(c) : styler.keyword(c);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return styler; // if chalk call fails, return base styler
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function applyFonts(styler, font) {
|
|
31
|
+
if (!font) return styler;
|
|
32
|
+
const parts = Array.isArray(font) ? font : String(font).split(/\s+/);
|
|
33
|
+
for (const p of parts) {
|
|
34
|
+
if (SUPPORTED_FONTS.has(p)) {
|
|
35
|
+
// chaining returns a styled chalk instance
|
|
36
|
+
styler = styler[p];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return styler;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildStyler(config) {
|
|
43
|
+
let styler = chalk;
|
|
44
|
+
styler = parseColor(styler, config.textColor, false);
|
|
45
|
+
styler = parseColor(styler, config.bgColor, true);
|
|
46
|
+
styler = applyFonts(styler, config.font);
|
|
47
|
+
return styler;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse duration input into milliseconds.
|
|
52
|
+
* Supported inputs:
|
|
53
|
+
* - number => treated as seconds
|
|
54
|
+
* - string with unit: "30s", "2m", "1.5h", "200ms" (unit optional -> seconds)
|
|
55
|
+
* - object: { hours: 1, minutes: 2, seconds: 30, ms: 100 }
|
|
56
|
+
*/
|
|
57
|
+
function parseDuration(input) {
|
|
58
|
+
if (input == null) return 0;
|
|
59
|
+
// numeric: treat as seconds
|
|
60
|
+
if (typeof input === 'number' && Number.isFinite(input)) {
|
|
61
|
+
return Math.max(0, input) * 1000;
|
|
62
|
+
}
|
|
63
|
+
if (typeof input === 'string') {
|
|
64
|
+
const s = input.trim().toLowerCase();
|
|
65
|
+
const m = s.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
66
|
+
if (!m) throw new TypeError('Invalid duration string: ' + input);
|
|
67
|
+
const value = parseFloat(m[1]);
|
|
68
|
+
const unit = m[2] || 's';
|
|
69
|
+
switch (unit) {
|
|
70
|
+
case 'ms': return Math.max(0, value);
|
|
71
|
+
case 's': return Math.max(0, value) * 1000;
|
|
72
|
+
case 'm': return Math.max(0, value) * 60 * 1000;
|
|
73
|
+
case 'h': return Math.max(0, value) * 60 * 60 * 1000;
|
|
74
|
+
default: return Math.max(0, value) * 1000;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (typeof input === 'object') {
|
|
78
|
+
let ms = 0;
|
|
79
|
+
if (input.ms) ms += Number(input.ms) || 0;
|
|
80
|
+
if (input.seconds) ms += (Number(input.seconds) || 0) * 1000;
|
|
81
|
+
if (input.minutes) ms += (Number(input.minutes) || 0) * 60 * 1000;
|
|
82
|
+
if (input.hours) ms += (Number(input.hours) || 0) * 60 * 60 * 1000;
|
|
83
|
+
// backward-compatible: allow singular keys
|
|
84
|
+
if (input.second && !input.seconds) ms += (Number(input.second) || 0) * 1000;
|
|
85
|
+
if (input.minute && !input.minutes) ms += (Number(input.minute) || 0) * 60 * 1000;
|
|
86
|
+
if (input.hour && !input.hours) ms += (Number(input.hour) || 0) * 60 * 60 * 1000;
|
|
87
|
+
if (ms === 0 && ('value' in input)) {
|
|
88
|
+
// allow {value: 30, unit: 's'}
|
|
89
|
+
const unit = String(input.unit || 's').toLowerCase();
|
|
90
|
+
const v = Number(input.value) || 0;
|
|
91
|
+
switch (unit) {
|
|
92
|
+
case 'ms': ms += v; break;
|
|
93
|
+
case 's': ms += v * 1000; break;
|
|
94
|
+
case 'm': ms += v * 60 * 1000; break;
|
|
95
|
+
case 'h': ms += v * 60 * 60 * 1000; break;
|
|
96
|
+
default: ms += v * 1000; break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Math.max(0, ms);
|
|
100
|
+
}
|
|
101
|
+
throw new TypeError('Unsupported duration type: ' + typeof input);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sleep(ms) {
|
|
105
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class LoopBuilder {
|
|
109
|
+
constructor(durationMs = 1000) {
|
|
110
|
+
this.durationMs = durationMs; // default interval between calls
|
|
111
|
+
this.fn = null;
|
|
112
|
+
this.fnArgs = [];
|
|
113
|
+
this._stopped = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// set duration (accepts same inputs as parseDuration)
|
|
117
|
+
duration(d) {
|
|
118
|
+
this.durationMs = parseDuration(d);
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
every(d) { return this.duration(d); }
|
|
122
|
+
|
|
123
|
+
// set target function to call; fn can be sync or async
|
|
124
|
+
call(fn, ...args) {
|
|
125
|
+
if (typeof fn !== 'function') {
|
|
126
|
+
throw new TypeError('call(fn) expects a function');
|
|
127
|
+
}
|
|
128
|
+
this.fn = fn;
|
|
129
|
+
this.fnArgs = args;
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// stop the running loop
|
|
134
|
+
stop() {
|
|
135
|
+
this._stopped = true;
|
|
136
|
+
if (this._stopResolve) {
|
|
137
|
+
this._stopResolve();
|
|
138
|
+
this._stopResolve = null;
|
|
139
|
+
}
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// run a fixed number of iterations
|
|
144
|
+
async times(n) {
|
|
145
|
+
if (!this.fn) throw new Error('No function set. Use .call(fn) before .times()');
|
|
146
|
+
if (!Number.isFinite(n) || n < 0) throw new TypeError('times(n) expects a non-negative number');
|
|
147
|
+
|
|
148
|
+
this._stopped = false;
|
|
149
|
+
for (let i = 0; i < n; i++) {
|
|
150
|
+
if (this._stopped) break;
|
|
151
|
+
const res = this.fn(...this.fnArgs);
|
|
152
|
+
if (res && typeof res.then === 'function') await res;
|
|
153
|
+
if (i < n - 1 && this.durationMs > 0) await sleep(this.durationMs);
|
|
154
|
+
}
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// run while predicate returns truthy. predicate can be a function or a simple boolean accessor function
|
|
159
|
+
async while(predicate, { checkBefore = true } = {}) {
|
|
160
|
+
if (!this.fn) throw new Error('No function set. Use .call(fn) before .while()');
|
|
161
|
+
if (typeof predicate !== 'function') {
|
|
162
|
+
throw new TypeError('while(predicate) expects a function that returns boolean');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this._stopped = false;
|
|
166
|
+
if (checkBefore) {
|
|
167
|
+
while (!this._stopped && predicate()) {
|
|
168
|
+
const res = this.fn(...this.fnArgs);
|
|
169
|
+
if (res && typeof res.then === 'function') await res;
|
|
170
|
+
if (this.durationMs > 0) {
|
|
171
|
+
// allow stop() to resolve early
|
|
172
|
+
await Promise.race([sleep(this.durationMs), new Promise(r => { this._stopResolve = r; })]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
// run at least once, then check
|
|
177
|
+
do {
|
|
178
|
+
if (this._stopped) break;
|
|
179
|
+
const res = this.fn(...this.fnArgs);
|
|
180
|
+
if (res && typeof res.then === 'function') await res;
|
|
181
|
+
if (this.durationMs > 0) {
|
|
182
|
+
await Promise.race([sleep(this.durationMs), new Promise(r => { this._stopResolve = r; })]);
|
|
183
|
+
}
|
|
184
|
+
} while (!this._stopped && predicate());
|
|
185
|
+
}
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// run until predicate returns truthy
|
|
190
|
+
async until(predicate, opts) {
|
|
191
|
+
if (typeof predicate !== 'function') {
|
|
192
|
+
throw new TypeError('until(predicate) expects a function that returns boolean');
|
|
193
|
+
}
|
|
194
|
+
// run while predicate() is false
|
|
195
|
+
return this.while(() => !predicate(), opts);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// start without waiting for completion; returns controller { stop() } and a promise `done`
|
|
199
|
+
start() {
|
|
200
|
+
if (!this.fn) throw new Error('No function set. Use .call(fn) before .start()');
|
|
201
|
+
this._stopped = false;
|
|
202
|
+
const done = (async () => {
|
|
203
|
+
try {
|
|
204
|
+
// run forever until stopped
|
|
205
|
+
while (!this._stopped) {
|
|
206
|
+
const res = this.fn(...this.fnArgs);
|
|
207
|
+
if (res && typeof res.then === 'function') await res;
|
|
208
|
+
if (this.durationMs > 0) {
|
|
209
|
+
await Promise.race([sleep(this.durationMs), new Promise(r => { this._stopResolve = r; })]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
// noop
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
return {
|
|
217
|
+
stop: () => this.stop(),
|
|
218
|
+
done
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const nsc = {
|
|
224
|
+
// user-configurable properties
|
|
225
|
+
textColor: null,
|
|
226
|
+
bgColor: null,
|
|
227
|
+
font: null,
|
|
228
|
+
|
|
229
|
+
_getStyler() {
|
|
230
|
+
return buildStyler({
|
|
231
|
+
textColor: this.textColor,
|
|
232
|
+
bgColor: this.bgColor,
|
|
233
|
+
font: this.font
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// nsc.log can be used in two ways:
|
|
238
|
+
// 1) nsc.log('a', 'b', 123)
|
|
239
|
+
// 2) nsc.log(() => { console.log('inside will be styled') }) <-- temporary override of console.log
|
|
240
|
+
log(...args) {
|
|
241
|
+
// callback-mode: override console.log while invoking the function
|
|
242
|
+
if (args.length === 1 && typeof args[0] === 'function') {
|
|
243
|
+
const fn = args[0];
|
|
244
|
+
const originalLog = console.log;
|
|
245
|
+
const styler = this._getStyler();
|
|
246
|
+
|
|
247
|
+
console.log = (...innerArgs) => {
|
|
248
|
+
const out = util.format(...innerArgs);
|
|
249
|
+
originalLog(styler(out));
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const result = fn();
|
|
254
|
+
// support async callbacks (promises)
|
|
255
|
+
if (result && typeof result.then === 'function') {
|
|
256
|
+
return result
|
|
257
|
+
.then(res => {
|
|
258
|
+
console.log = originalLog;
|
|
259
|
+
return res;
|
|
260
|
+
})
|
|
261
|
+
.catch(err => {
|
|
262
|
+
console.log = originalLog;
|
|
263
|
+
throw err;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// sync
|
|
267
|
+
console.log = originalLog;
|
|
268
|
+
return result;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.log = originalLog;
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// normal-mode: format args and print styled
|
|
276
|
+
const styler = this._getStyler();
|
|
277
|
+
const out = util.format(...args);
|
|
278
|
+
console.log(styler(out));
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Wait for the given duration and resolve the returned Promise.
|
|
283
|
+
* - numeric values are treated as seconds
|
|
284
|
+
* - accepts strings like "30s", "2m", "1.5h", "200ms"
|
|
285
|
+
* - accepts objects like { seconds: 30 } or { hours: 1, minutes: 2 }
|
|
286
|
+
*
|
|
287
|
+
* Usage:
|
|
288
|
+
* nsc.wait(30).then(() => main());
|
|
289
|
+
* nsc.wait('2m').then(main);
|
|
290
|
+
* await nsc.wait({ minutes: 1 });
|
|
291
|
+
*/
|
|
292
|
+
wait(duration) {
|
|
293
|
+
const ms = parseDuration(duration);
|
|
294
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
// alias for fluent naming: nsc.next(30).then(...)
|
|
298
|
+
next(duration) {
|
|
299
|
+
return this.wait(duration);
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// loop fluent API:
|
|
303
|
+
// Examples:
|
|
304
|
+
// await nsc.loop.duration('2s').call(main).times(30);
|
|
305
|
+
// await nsc.loop.duration('1m').call(main).while(() => conditionIsTrue);
|
|
306
|
+
// const ctl = nsc.loop.duration('5s').call(main).start(); ctl.stop();
|
|
307
|
+
loop: {
|
|
308
|
+
duration(d) { return new LoopBuilder(parseDuration(d)); },
|
|
309
|
+
every(d) { return new LoopBuilder(parseDuration(d)); },
|
|
310
|
+
// start a condition-based loop; usage: nsc.loop.while(() => x===y).call(main).start()
|
|
311
|
+
while(predicate) {
|
|
312
|
+
if (typeof predicate !== 'function') throw new TypeError('loop.while(predicate) expects a function');
|
|
313
|
+
const builder = new LoopBuilder();
|
|
314
|
+
// chainable: immediate return builder with .call and .duration supported
|
|
315
|
+
builder._initialWhilePredicate = predicate;
|
|
316
|
+
// expose helper to run directly: builder.call(fn).while(predicate)
|
|
317
|
+
return {
|
|
318
|
+
call: (fn, ...args) => {
|
|
319
|
+
builder.call(fn, ...args);
|
|
320
|
+
return builder.while(predicate);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
module.exports = nsc;
|