@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.
Files changed (2) hide show
  1. package/package.json +26 -0
  2. 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;