@xnoxs/flux-lang 3.1.1

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 (56) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1089 -0
  3. package/bin/flux.js +1397 -0
  4. package/dist/flux.cjs.js +6664 -0
  5. package/dist/flux.esm.js +6674 -0
  6. package/dist/flux.min.js +263 -0
  7. package/index.d.ts +202 -0
  8. package/index.js +26 -0
  9. package/package.json +77 -0
  10. package/scripts/build.js +76 -0
  11. package/src/bundler.js +216 -0
  12. package/src/checker.js +322 -0
  13. package/src/codegen.js +785 -0
  14. package/src/css-preprocessor.js +399 -0
  15. package/src/formatter.js +140 -0
  16. package/src/jsx.js +480 -0
  17. package/src/lexer.js +518 -0
  18. package/src/linter.js +758 -0
  19. package/src/mangler.js +280 -0
  20. package/src/parser.js +1671 -0
  21. package/src/self/bundler.flux +167 -0
  22. package/src/self/bundler.js +187 -0
  23. package/src/self/checker.flux +249 -0
  24. package/src/self/checker.js +338 -0
  25. package/src/self/codegen.flux +555 -0
  26. package/src/self/codegen.js +784 -0
  27. package/src/self/css-preprocessor.flux +373 -0
  28. package/src/self/css-preprocessor.js +387 -0
  29. package/src/self/formatter.flux +93 -0
  30. package/src/self/formatter.js +114 -0
  31. package/src/self/jsx.flux +430 -0
  32. package/src/self/jsx.js +396 -0
  33. package/src/self/lexer.flux +529 -0
  34. package/src/self/lexer.js +709 -0
  35. package/src/self/lexer.stage2.js +700 -0
  36. package/src/self/linter.flux +515 -0
  37. package/src/self/linter.js +804 -0
  38. package/src/self/mangler.flux +253 -0
  39. package/src/self/mangler.js +348 -0
  40. package/src/self/parser.flux +1146 -0
  41. package/src/self/parser.js +1571 -0
  42. package/src/self/sourcemap.flux +66 -0
  43. package/src/self/sourcemap.js +72 -0
  44. package/src/self/stdlib.flux +356 -0
  45. package/src/self/stdlib.js +396 -0
  46. package/src/self/test-runner.flux +201 -0
  47. package/src/self/test-runner.js +132 -0
  48. package/src/self/transpiler.flux +123 -0
  49. package/src/self/transpiler.js +83 -0
  50. package/src/self/type-checker.flux +821 -0
  51. package/src/self/type-checker.js +1106 -0
  52. package/src/sourcemap.js +82 -0
  53. package/src/stdlib.js +436 -0
  54. package/src/test-runner.js +239 -0
  55. package/src/transpiler.js +172 -0
  56. package/src/type-checker.js +1206 -0
@@ -0,0 +1,396 @@
1
+ // ── Flux stdlib ──
2
+
3
+ function map(arr, fn) { return arr.map(fn); }
4
+
5
+ function filter(arr, fn) { return arr.filter(fn); }
6
+
7
+ function reduce(arr, fn, init) { return arguments.length >= 3 ? arr.reduce(fn, init) : arr.reduce(fn); }
8
+
9
+ function forEach(arr, fn) { arr.forEach(fn); return arr; }
10
+
11
+ function find(arr, fn) { return arr.find(fn); }
12
+
13
+ function findIndex(arr, fn) { return arr.findIndex(fn); }
14
+
15
+ function some(arr, fn) { return arr.some(fn); }
16
+
17
+ function every(arr, fn) { return arr.every(fn); }
18
+
19
+ function join(arr, sep) { return arr.join(sep != null ? sep : ','); }
20
+
21
+ function sort(arr, fn) { return arr.slice().sort(fn); }
22
+
23
+ function flat(arr, depth) { return arr.flat(depth != null ? depth : 1); }
24
+
25
+ function flatMap(arr, fn) { return arr.flatMap(fn); }
26
+
27
+ function includes(arr, val) { return arr.includes(val); }
28
+
29
+ function range(start, end, step) {
30
+ if (arguments.length === 1) { end = start; start = 0; }
31
+ step = step || (end >= start ? 1 : -1);
32
+ const out = [];
33
+ if (step > 0) { for (let i = start; i < end; i += step) out.push(i); }
34
+ else { for (let i = start; i > end; i += step) out.push(i); }
35
+ return out;
36
+ }
37
+
38
+ function zip() {
39
+ const arrays = Array.from(arguments);
40
+ const len = Math.min.apply(null, arrays.map(function(a) { return a.length; }));
41
+ const out = [];
42
+ for (let i = 0; i < len; i++) out.push(arrays.map(function(a) { return a[i]; }));
43
+ return out;
44
+ }
45
+
46
+ function enumerate(arr, start) {
47
+ start = start || 0;
48
+ return arr.map(function(v, i) { return [i + start, v]; });
49
+ }
50
+
51
+ function flatten(arr, depth) {
52
+ return depth == null ? arr.flat(Infinity) : arr.flat(depth);
53
+ }
54
+
55
+ function chunk(arr, size) {
56
+ const out = [];
57
+ for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
58
+ return out;
59
+ }
60
+
61
+ function unique(arr) {
62
+ return Array.from(new Set(arr));
63
+ }
64
+
65
+ function groupBy(arr, fn) {
66
+ return arr.reduce(function(acc, v) {
67
+ var k = typeof fn === 'function' ? fn(v) : v[fn];
68
+ (acc[k] = acc[k] || []).push(v);
69
+ return acc;
70
+ }, {});
71
+ }
72
+
73
+ function sortBy(arr, fn) {
74
+ return arr.slice().sort(function(a, b) {
75
+ var ka = typeof fn === 'function' ? fn(a) : a[fn];
76
+ var kb = typeof fn === 'function' ? fn(b) : b[fn];
77
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
78
+ });
79
+ }
80
+
81
+ function clamp(val, min, max) {
82
+ return Math.min(Math.max(val, min), max);
83
+ }
84
+
85
+ function sum(arr) {
86
+ return arr.reduce(function(a, b) { return a + b; }, 0);
87
+ }
88
+
89
+ function product(arr) {
90
+ return arr.reduce(function(a, b) { return a * b; }, 1);
91
+ }
92
+
93
+ function randInt(min, max) {
94
+ return Math.floor(Math.random() * (max - min + 1)) + min;
95
+ }
96
+
97
+ function sample(arr) {
98
+ return arr[Math.floor(Math.random() * arr.length)];
99
+ }
100
+
101
+ function shuffle(arr) {
102
+ var a = arr.slice();
103
+ for (var i = a.length - 1; i > 0; i--) {
104
+ var j = Math.floor(Math.random() * (i + 1));
105
+ var tmp = a[i]; a[i] = a[j]; a[j] = tmp;
106
+ }
107
+ return a;
108
+ }
109
+
110
+ function pick(obj, keys) {
111
+ var out = {};
112
+ keys.forEach(function(k) { if (Object.prototype.hasOwnProperty.call(obj, k)) out[k] = obj[k]; });
113
+ return out;
114
+ }
115
+
116
+ function omit(obj, keys) {
117
+ var ks = new Set(keys);
118
+ var out = {};
119
+ Object.keys(obj).forEach(function(k) { if (!ks.has(k)) out[k] = obj[k]; });
120
+ return out;
121
+ }
122
+
123
+ function mapValues(obj, fn) {
124
+ var out = {};
125
+ Object.keys(obj).forEach(function(k) { out[k] = fn(obj[k], k); });
126
+ return out;
127
+ }
128
+
129
+ function filterValues(obj, fn) {
130
+ var out = {};
131
+ Object.keys(obj).forEach(function(k) { if (fn(obj[k], k)) out[k] = obj[k]; });
132
+ return out;
133
+ }
134
+
135
+ function fromEntries(entries) {
136
+ return Object.fromEntries(entries);
137
+ }
138
+
139
+ function deepEqual(a, b) {
140
+ return JSON.stringify(a) === JSON.stringify(b);
141
+ }
142
+
143
+ function deepClone(v) {
144
+ return JSON.parse(JSON.stringify(v));
145
+ }
146
+
147
+ function sleep(ms) {
148
+ return new Promise(function(resolve) { setTimeout(resolve, ms); });
149
+ }
150
+
151
+ async function retry(fn, attempts, delay) {
152
+ attempts = attempts || 3;
153
+ delay = delay || 300;
154
+ for (var i = 0; i < attempts; i++) {
155
+ try { return await fn(); } catch(e) {
156
+ if (i === attempts - 1) throw e;
157
+ await sleep(delay * Math.pow(2, i));
158
+ }
159
+ }
160
+ }
161
+
162
+ function memoize(fn) {
163
+ var cache = new Map();
164
+ return function() {
165
+ var key = JSON.stringify(arguments);
166
+ if (cache.has(key)) return cache.get(key);
167
+ var result = fn.apply(this, arguments);
168
+ cache.set(key, result);
169
+ return result;
170
+ };
171
+ }
172
+
173
+ function pipe() {
174
+ var fns = Array.from(arguments);
175
+ return function(x) { return fns.reduce(function(v, f) { return f(v); }, x); };
176
+ }
177
+
178
+ function compose() {
179
+ var fns = Array.from(arguments);
180
+ return function(x) { return fns.reduceRight(function(v, f) { return f(v); }, x); };
181
+ }
182
+
183
+ function partial(fn) {
184
+ var args = Array.from(arguments).slice(1);
185
+ return function() { return fn.apply(this, args.concat(Array.from(arguments))); };
186
+ }
187
+
188
+ function curry(fn) {
189
+ return function curried() {
190
+ var args = Array.from(arguments);
191
+ if (args.length >= fn.length) return fn.apply(this, args);
192
+ return function() { return curried.apply(this, args.concat(Array.from(arguments))); };
193
+ };
194
+ }
195
+
196
+ function capitalize(s) {
197
+ return s ? s[0].toUpperCase() + s.slice(1) : s;
198
+ }
199
+
200
+ function camelCase(s) {
201
+ return s.replace(/[-_\s]+(.)/g, function(_, c) { return c.toUpperCase(); }).replace(/^./, function(c) { return c.toLowerCase(); });
202
+ }
203
+
204
+ function snakeCase(s) {
205
+ return s.replace(/([A-Z])/g, function(c) { return '_' + c.toLowerCase(); }).replace(/[-\s]+/g, '_').replace(/^_/, '');
206
+ }
207
+
208
+ function kebabCase(s) {
209
+ return s.replace(/([A-Z])/g, function(c) { return '-' + c.toLowerCase(); }).replace(/[_\s]+/g, '-').replace(/^-/, '');
210
+ }
211
+
212
+ function truncate(s, len, suffix) {
213
+ suffix = suffix != null ? suffix : '...';
214
+ if (s.length <= len) return s;
215
+ // FIX: guard against suffix longer than len to avoid negative slice index
216
+ var cut = len - suffix.length;
217
+ if (cut <= 0) return suffix.slice(0, len);
218
+ return s.slice(0, cut) + suffix;
219
+ }
220
+
221
+ function pad(s, len, char) {
222
+ char = char || ' ';
223
+ s = String(s);
224
+ var total = len - s.length;
225
+ if (total <= 0) return s;
226
+ var half = Math.floor(total / 2);
227
+ return char.repeat(half) + s + char.repeat(total - half);
228
+ }
229
+ // ── end stdlib ──
230
+
231
+ // Generated by Flux Transpiler v3.1.0
232
+ "use strict";
233
+
234
+ const STDLIB_SYMBOLS = ["range", "zip", "enumerate", "clamp", "sum", "product", "flatten", "chunk", "unique", "groupBy", "sortBy", "pick", "omit", "deepEqual", "deepClone", "sleep", "retry", "memoize", "pipe", "compose", "partial", "curry", "capitalize", "camelCase", "snakeCase", "kebabCase", "truncate", "pad", "randInt", "sample", "shuffle", "mapValues", "filterValues", "fromEntries", "map", "filter", "reduce", "forEach", "find", "findIndex", "some", "every", "join", "sort", "flat", "flatMap", "includes"];
235
+ module.exports.STDLIB_SYMBOLS = STDLIB_SYMBOLS;
236
+ function makeSymbolRe(sym) {
237
+ return new RegExp((("\\b" + sym) + "\\s*\\("), "g");
238
+ }
239
+ function detectUsedSymbols(jsCode) {
240
+ return STDLIB_SYMBOLS.filter((sym) => makeSymbolRe(sym).test(jsCode));
241
+ }
242
+ module.exports.detectUsedSymbols = detectUsedSymbols;
243
+ function buildStdlib(symbols) {
244
+ let needed = new Set(STDLIB_SYMBOLS);
245
+ if (symbols) {
246
+ needed = new Set(symbols.filter((s) => STDLIB_SYMBOLS.includes(s)));
247
+ }
248
+ if ((needed.size == 0)) {
249
+ return "";
250
+ }
251
+ const parts = ["// ── Flux stdlib ──"];
252
+ if (needed.has("map")) {
253
+ parts.push("function map(arr, fn) { return arr.map(fn); }");
254
+ }
255
+ if (needed.has("filter")) {
256
+ parts.push("function filter(arr, fn) { return arr.filter(fn); }");
257
+ }
258
+ if (needed.has("reduce")) {
259
+ parts.push("function reduce(arr, fn, init) { return arguments.length >= 3 ? arr.reduce(fn, init) : arr.reduce(fn); }");
260
+ }
261
+ if (needed.has("forEach")) {
262
+ parts.push("function forEach(arr, fn) { arr.forEach(fn); return arr; }");
263
+ }
264
+ if (needed.has("find")) {
265
+ parts.push("function find(arr, fn) { return arr.find(fn); }");
266
+ }
267
+ if (needed.has("findIndex")) {
268
+ parts.push("function findIndex(arr, fn) { return arr.findIndex(fn); }");
269
+ }
270
+ if (needed.has("some")) {
271
+ parts.push("function some(arr, fn) { return arr.some(fn); }");
272
+ }
273
+ if (needed.has("every")) {
274
+ parts.push("function every(arr, fn) { return arr.every(fn); }");
275
+ }
276
+ if (needed.has("join")) {
277
+ parts.push("function join(arr, sep) { return arr.join(sep != null ? sep : ','); }");
278
+ }
279
+ if (needed.has("sort")) {
280
+ parts.push("function sort(arr, fn) { return arr.slice().sort(fn); }");
281
+ }
282
+ if (needed.has("flat")) {
283
+ parts.push("function flat(arr, depth) { return arr.flat(depth != null ? depth : 1); }");
284
+ }
285
+ if (needed.has("flatMap")) {
286
+ parts.push("function flatMap(arr, fn) { return arr.flatMap(fn); }");
287
+ }
288
+ if (needed.has("includes")) {
289
+ parts.push("function includes(arr, val) { return arr.includes(val); }");
290
+ }
291
+ if (needed.has("range")) {
292
+ parts.push("function range(start, end, step) {\n if (arguments.length === 1) { end = start; start = 0; }\n step = step || (end >= start ? 1 : -1);\n const out = [];\n if (step > 0) { for (let i = start; i < end; i += step) out.push(i); }\n else { for (let i = start; i > end; i += step) out.push(i); }\n return out;\n}");
293
+ }
294
+ if (needed.has("zip")) {
295
+ parts.push("function zip() {\n const arrays = Array.from(arguments);\n const len = Math.min.apply(null, arrays.map(function(a) { return a.length; }));\n const out = [];\n for (let i = 0; i < len; i++) out.push(arrays.map(function(a) { return a[i]; }));\n return out;\n}");
296
+ }
297
+ if (needed.has("enumerate")) {
298
+ parts.push("function enumerate(arr, start) {\n start = start || 0;\n return arr.map(function(v, i) { return [i + start, v]; });\n}");
299
+ }
300
+ if (needed.has("flatten")) {
301
+ parts.push("function flatten(arr, depth) {\n return depth == null ? arr.flat(Infinity) : arr.flat(depth);\n}");
302
+ }
303
+ if (needed.has("chunk")) {
304
+ parts.push("function chunk(arr, size) {\n const out = [];\n for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));\n return out;\n}");
305
+ }
306
+ if (needed.has("unique")) {
307
+ parts.push("function unique(arr) {\n return Array.from(new Set(arr));\n}");
308
+ }
309
+ if (needed.has("groupBy")) {
310
+ parts.push("function groupBy(arr, fn) {\n return arr.reduce(function(acc, v) {\n var k = typeof fn === 'function' ? fn(v) : v[fn];\n (acc[k] = acc[k] || []).push(v);\n return acc;\n }, {});\n}");
311
+ }
312
+ if (needed.has("sortBy")) {
313
+ parts.push("function sortBy(arr, fn) {\n return arr.slice().sort(function(a, b) {\n var ka = typeof fn === 'function' ? fn(a) : a[fn];\n var kb = typeof fn === 'function' ? fn(b) : b[fn];\n return ka < kb ? -1 : ka > kb ? 1 : 0;\n });\n}");
314
+ }
315
+ if (needed.has("clamp")) {
316
+ parts.push("function clamp(val, min, max) {\n return Math.min(Math.max(val, min), max);\n}");
317
+ }
318
+ if (needed.has("sum")) {
319
+ parts.push("function sum(arr) {\n return arr.reduce(function(a, b) { return a + b; }, 0);\n}");
320
+ }
321
+ if (needed.has("product")) {
322
+ parts.push("function product(arr) {\n return arr.reduce(function(a, b) { return a * b; }, 1);\n}");
323
+ }
324
+ if (needed.has("randInt")) {
325
+ parts.push("function randInt(min, max) {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}");
326
+ }
327
+ if (needed.has("sample")) {
328
+ parts.push("function sample(arr) {\n return arr[Math.floor(Math.random() * arr.length)];\n}");
329
+ }
330
+ if (needed.has("shuffle")) {
331
+ parts.push("function shuffle(arr) {\n var a = arr.slice();\n for (var i = a.length - 1; i > 0; i--) {\n var j = Math.floor(Math.random() * (i + 1));\n var tmp = a[i]; a[i] = a[j]; a[j] = tmp;\n }\n return a;\n}");
332
+ }
333
+ if (needed.has("pick")) {
334
+ parts.push("function pick(obj, keys) {\n var out = {};\n keys.forEach(function(k) { if (Object.prototype.hasOwnProperty.call(obj, k)) out[k] = obj[k]; });\n return out;\n}");
335
+ }
336
+ if (needed.has("omit")) {
337
+ parts.push("function omit(obj, keys) {\n var ks = new Set(keys);\n var out = {};\n Object.keys(obj).forEach(function(k) { if (!ks.has(k)) out[k] = obj[k]; });\n return out;\n}");
338
+ }
339
+ if (needed.has("mapValues")) {
340
+ parts.push("function mapValues(obj, fn) {\n var out = {};\n Object.keys(obj).forEach(function(k) { out[k] = fn(obj[k], k); });\n return out;\n}");
341
+ }
342
+ if (needed.has("filterValues")) {
343
+ parts.push("function filterValues(obj, fn) {\n var out = {};\n Object.keys(obj).forEach(function(k) { if (fn(obj[k], k)) out[k] = obj[k]; });\n return out;\n}");
344
+ }
345
+ if (needed.has("fromEntries")) {
346
+ parts.push("function fromEntries(entries) {\n return Object.fromEntries(entries);\n}");
347
+ }
348
+ if (needed.has("deepEqual")) {
349
+ parts.push("function deepEqual(a, b) {\n return JSON.stringify(a) === JSON.stringify(b);\n}");
350
+ }
351
+ if (needed.has("deepClone")) {
352
+ parts.push("function deepClone(v) {\n return JSON.parse(JSON.stringify(v));\n}");
353
+ }
354
+ if (needed.has("sleep")) {
355
+ parts.push("function sleep(ms) {\n return new Promise(function(resolve) { setTimeout(resolve, ms); });\n}");
356
+ }
357
+ if (needed.has("retry")) {
358
+ parts.push("async function retry(fn, attempts, delay) {\n attempts = attempts || 3;\n delay = delay || 300;\n for (var i = 0; i < attempts; i++) {\n try { return await fn(); } catch(e) {\n if (i === attempts - 1) throw e;\n await sleep(delay * Math.pow(2, i));\n }\n }\n}");
359
+ }
360
+ if (needed.has("memoize")) {
361
+ parts.push("function memoize(fn) {\n var cache = new Map();\n return function() {\n var key = JSON.stringify(arguments);\n if (cache.has(key)) return cache.get(key);\n var result = fn.apply(this, arguments);\n cache.set(key, result);\n return result;\n };\n}");
362
+ }
363
+ if (needed.has("pipe")) {
364
+ parts.push("function pipe() {\n var fns = Array.from(arguments);\n return function(x) { return fns.reduce(function(v, f) { return f(v); }, x); };\n}");
365
+ }
366
+ if (needed.has("compose")) {
367
+ parts.push("function compose() {\n var fns = Array.from(arguments);\n return function(x) { return fns.reduceRight(function(v, f) { return f(v); }, x); };\n}");
368
+ }
369
+ if (needed.has("partial")) {
370
+ parts.push("function partial(fn) {\n var args = Array.from(arguments).slice(1);\n return function() { return fn.apply(this, args.concat(Array.from(arguments))); };\n}");
371
+ }
372
+ if (needed.has("curry")) {
373
+ parts.push("function curry(fn) {\n return function curried() {\n var args = Array.from(arguments);\n if (args.length >= fn.length) return fn.apply(this, args);\n return function() { return curried.apply(this, args.concat(Array.from(arguments))); };\n };\n}");
374
+ }
375
+ if (needed.has("capitalize")) {
376
+ parts.push("function capitalize(s) {\n return s ? s[0].toUpperCase() + s.slice(1) : s;\n}");
377
+ }
378
+ if (needed.has("camelCase")) {
379
+ parts.push("function camelCase(s) {\n return s.replace(/[-_\\s]+(.)/g, function(_, c) { return c.toUpperCase(); }).replace(/^./, function(c) { return c.toLowerCase(); });\n}");
380
+ }
381
+ if (needed.has("snakeCase")) {
382
+ parts.push("function snakeCase(s) {\n return s.replace(/([A-Z])/g, function(c) { return '_' + c.toLowerCase(); }).replace(/[-\\s]+/g, '_').replace(/^_/, '');\n}");
383
+ }
384
+ if (needed.has("kebabCase")) {
385
+ parts.push("function kebabCase(s) {\n return s.replace(/([A-Z])/g, function(c) { return '-' + c.toLowerCase(); }).replace(/[_\\s]+/g, '-').replace(/^-/, '');\n}");
386
+ }
387
+ if (needed.has("truncate")) {
388
+ parts.push("function truncate(s, len, suffix) {\n suffix = suffix != null ? suffix : '...';\n if (s.length <= len) return s;\n var cut = len - suffix.length;\n if (cut <= 0) return suffix.slice(0, len);\n return s.slice(0, cut) + suffix;\n}");
389
+ }
390
+ if (needed.has("pad")) {
391
+ parts.push("function pad(s, len, char) {\n char = char || ' ';\n s = String(s);\n var total = len - s.length;\n if (total <= 0) return s;\n var half = Math.floor(total / 2);\n return char.repeat(half) + s + char.repeat(total - half);\n}");
392
+ }
393
+ parts.push("// ── end stdlib ──\n");
394
+ return parts.join("\n");
395
+ }
396
+ module.exports.buildStdlib = buildStdlib;
@@ -0,0 +1,201 @@
1
+ // ============================================================
2
+ // Flux Self-Hosted Test Runner
3
+ // src/self/test-runner.flux — written in Flux, compiled by stage-0
4
+ //
5
+ // Discovers and runs Flux test files (*.test.flux)
6
+ // Test functions must be named test_* (e.g. fn test_arithmetic():)
7
+ // Globals injected: assert, assertEqual, assertNotEqual,
8
+ // assertThrows, assertClose
9
+ // ============================================================
10
+
11
+ import Fs from "fs"
12
+ import Path from "path"
13
+ import Os from "os"
14
+
15
+ val C = {
16
+ reset: '\x1b[0m',
17
+ bold: '\x1b[1m',
18
+ red: '\x1b[31m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ cyan: '\x1b[36m',
22
+ gray: '\x1b[90m',
23
+ dim: '\x1b[2m',
24
+ }
25
+
26
+ fn clr(c, s):
27
+ return process.env.NO_COLOR ? s : c + s + C.reset
28
+
29
+ val TEST_HELPERS = `"use strict";
30
+
31
+ const __results = [];
32
+ let __current = null;
33
+
34
+ function assert(condition, message) {
35
+ if (!condition) {
36
+ throw new Error(message || 'Assertion failed: expected truthy');
37
+ }
38
+ }
39
+
40
+ function assertEqual(actual, expected, message) {
41
+ const ok = JSON.stringify(actual) === JSON.stringify(expected);
42
+ if (!ok) {
43
+ throw new Error(
44
+ message ||
45
+ 'Expected ' + JSON.stringify(expected) + ' but got ' + JSON.stringify(actual)
46
+ );
47
+ }
48
+ }
49
+
50
+ function assertNotEqual(actual, expected, message) {
51
+ const ok = JSON.stringify(actual) !== JSON.stringify(expected);
52
+ if (!ok) {
53
+ throw new Error(message || 'Expected values to be different but both were ' + JSON.stringify(actual));
54
+ }
55
+ }
56
+
57
+ function assertThrows(fn, message) {
58
+ try { fn(); }
59
+ catch (_) { return; }
60
+ throw new Error(message || 'Expected function to throw, but it did not');
61
+ }
62
+
63
+ function assertClose(actual, expected, delta, message) {
64
+ delta = delta || 1e-9;
65
+ if (Math.abs(actual - expected) > delta) {
66
+ throw new Error(message || ('Expected ' + actual + ' to be close to ' + expected + ' (delta=' + delta + ')'));
67
+ }
68
+ }
69
+
70
+ function __runTest(name, fn) {
71
+ try {
72
+ fn();
73
+ __results.push({ name: name, ok: true });
74
+ } catch(e) {
75
+ __results.push({ name: name, ok: false, error: e.message });
76
+ }
77
+ }
78
+ `
79
+
80
+ export fn discoverTestFiles(target):
81
+ val abs = Path.resolve(target)
82
+ if not Fs.existsSync(abs):
83
+ throw new Error("Not found: " + abs)
84
+ val stat = Fs.statSync(abs)
85
+ if stat.isDirectory():
86
+ return Fs.readdirSync(abs)
87
+ .filter(f -> f.endsWith('.test.flux'))
88
+ .map(f -> Path.join(abs, f))
89
+ .sort()
90
+ if abs.endsWith('.flux'): return [abs]
91
+ throw new Error("Expected a .flux file or directory, got: " + abs)
92
+
93
+ export fn runTestFile(filePath, transpile):
94
+ val source = Fs.readFileSync(filePath, 'utf8')
95
+ val result = transpile(source)
96
+
97
+ if not result.success:
98
+ return {
99
+ file: Path.basename(filePath),
100
+ errors: result.errors,
101
+ tests: [],
102
+ compile: false,
103
+ }
104
+
105
+ val fnNames = []
106
+ val fnRe = /^function (test_[a-zA-Z0-9_]+)\s*\(/mg
107
+ var m = fnRe.exec(result.output)
108
+ while m:
109
+ fnNames.push(m[1])
110
+ m = fnRe.exec(result.output)
111
+
112
+ val runnerParts = [
113
+ TEST_HELPERS,
114
+ result.output,
115
+ '',
116
+ '// ── auto-generated test runner ──',
117
+ ]
118
+ for n in fnNames:
119
+ runnerParts.push('__runTest(' + JSON.stringify(n.replace(/^test_/, '').replace(/_/g, ' ')) + ', ' + n + ');')
120
+ runnerParts.push('module.exports = { __results };')
121
+
122
+ val runner = runnerParts.join('\n')
123
+ val tmpPath = Path.join(Os.tmpdir(), '_flux_test_' + Date.now() + '.js')
124
+ try:
125
+ Fs.writeFileSync(tmpPath, runner, 'utf8')
126
+ val mod = require(tmpPath)
127
+ val tests = (mod and mod.__results) ? mod.__results : []
128
+ return {
129
+ file: Path.basename(filePath),
130
+ tests,
131
+ compile: true,
132
+ errors: [],
133
+ }
134
+ catch(e):
135
+ return {
136
+ file: Path.basename(filePath),
137
+ tests: [],
138
+ compile: false,
139
+ errors: [{ message: 'Runtime error: ' + e.message }],
140
+ }
141
+ finally:
142
+ try: Fs.unlinkSync(tmpPath)
143
+ catch(ignored): null
144
+
145
+ export fn runTests(target, transpile):
146
+ var files = []
147
+ try:
148
+ files = discoverTestFiles(target)
149
+ catch(e):
150
+ console.error(clr(C.red, '✗ ' + e.message))
151
+ process.exit(1)
152
+
153
+ if files.length == 0:
154
+ console.log(clr(C.yellow, 'No *.test.flux files found in: ' + target))
155
+ return
156
+
157
+ var totalPass = 0
158
+ var totalFail = 0
159
+ var totalFiles = 0
160
+ val t0 = Date.now()
161
+
162
+ for file in files:
163
+ totalFiles = totalFiles + 1
164
+ console.log(clr(C.cyan, '\n◈ ' + Path.basename(file)))
165
+
166
+ val result = runTestFile(file, transpile)
167
+
168
+ if not result.compile:
169
+ console.log(clr(C.red, ' ✗ Compile error:'))
170
+ for e in result.errors:
171
+ console.log(clr(C.red, ' ' + e.message))
172
+ totalFail = totalFail + 1
173
+ continue
174
+
175
+ if result.tests.length == 0:
176
+ console.log(clr(C.yellow, ' (no test_ functions found)'))
177
+ continue
178
+
179
+ for t in result.tests:
180
+ if t.ok:
181
+ totalPass = totalPass + 1
182
+ console.log(clr(C.green, ' ✓') + clr(C.gray, ' ' + t.name))
183
+ else:
184
+ totalFail = totalFail + 1
185
+ console.log(clr(C.red, ' ✗ ' + t.name))
186
+ console.log(clr(C.dim, ' ' + t.error))
187
+
188
+ val elapsed = Date.now() - t0
189
+ val total = totalPass + totalFail
190
+
191
+ console.log()
192
+ console.log(clr(C.bold, '─'.repeat(50)))
193
+ console.log(
194
+ clr(C.bold, 'Results: ') +
195
+ clr(C.green, totalPass + ' passed') + clr(C.gray, ', ') +
196
+ (totalFail > 0 ? clr(C.red, totalFail + ' failed') : clr(C.gray, totalFail + ' failed')) +
197
+ clr(C.gray, ' (' + total + ' total) in ' + elapsed + 'ms')
198
+ )
199
+ console.log()
200
+
201
+ if totalFail > 0: process.exitCode = 1