@wizhut_tech/wizjs 0.1.5 → 0.2.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.
@@ -0,0 +1,59 @@
1
+ name: Release
2
+
3
+ # Tag-driven: pushing v<x.y.z> runs the suite, publishes @wizhut_tech/wizjs to
4
+ # npm and opens a GitHub release. Cut one with:
5
+ # npm version patch && git push --follow-tags
6
+ on:
7
+ push:
8
+ tags:
9
+ - "v*"
10
+
11
+ permissions:
12
+ contents: write # gh release create
13
+ id-token: write # npm trusted publishing (OIDC)
14
+
15
+ jobs:
16
+ release:
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ with:
22
+ fetch-depth: 0 # --generate-notes diffs against the previous tag
23
+
24
+ # Do not set registry-url here: setup-node would write an .npmrc with an
25
+ # empty _authToken and that short-circuits npm OIDC trusted publishing
26
+ # (the CLI then 404s the PUT as if the package did not exist).
27
+ - uses: actions/setup-node@v7
28
+ with:
29
+ node-version: "24"
30
+
31
+ # `npm version` keeps these in step; a hand-made tag might not.
32
+ - name: Check the tag matches package.json
33
+ run: |
34
+ tag="${GITHUB_REF_NAME#v}"
35
+ pkg="$(node -p "require('./package.json').version")"
36
+ if [ "$tag" != "$pkg" ]; then
37
+ echo "::error::tag $GITHUB_REF_NAME does not match package.json version $pkg"
38
+ exit 1
39
+ fi
40
+
41
+ - run: npm ci
42
+ - run: npm test
43
+
44
+ # Trusted publishing: npmjs.com is configured to trust this repo's
45
+ # release.yml, so there is no NPM_TOKEN — the OIDC id-token is the
46
+ # credential, and npm attaches a provenance attestation. Needs npm
47
+ # >= 11.5.1, hence the upgrade.
48
+ - name: Publish to npm
49
+ run: |
50
+ npm install -g npm@latest
51
+ npm publish --access public
52
+
53
+ - name: Publish the GitHub release
54
+ env:
55
+ GH_TOKEN: ${{ github.token }}
56
+ run: |
57
+ gh release create "$GITHUB_REF_NAME" \
58
+ --title "wizjs $GITHUB_REF_NAME" \
59
+ --generate-notes
@@ -0,0 +1,29 @@
1
+ const t = require('tap');
2
+
3
+ const { BadlyInitializedError } = require('../src/internal/exceptions.js');
4
+ const root = require('@wizhut_tech/wizjs');
5
+
6
+
7
+ t.test('exceptions/BadlyInitializedError behaves like an Error', (t) => {
8
+ const err = new BadlyInitializedError('not ready');
9
+
10
+ t.type(err, Error, 'extends Error');
11
+ t.type(err, BadlyInitializedError);
12
+ t.equal(err.message, 'not ready');
13
+ t.ok(err.stack, 'carries a stack');
14
+ t.end();
15
+ });
16
+
17
+
18
+ t.test('exceptions/BadlyInitializedError ignores trailing arguments', (t) => {
19
+ const err = new BadlyInitializedError('only the message', 'extra', 42);
20
+
21
+ t.equal(err.message, 'only the message');
22
+ t.end();
23
+ });
24
+
25
+
26
+ t.test('exceptions/BadlyInitializedError is the one the root import exposes', (t) => {
27
+ t.equal(root.exceptions.BadlyInitializedError, BadlyInitializedError);
28
+ t.end();
29
+ });
@@ -0,0 +1,41 @@
1
+ const t = require('tap');
2
+ const path = require('path');
3
+
4
+ const { loadFully } = require('../src/io/files.js');
5
+
6
+
7
+ t.test('files/loadFully returns null for a file that is not there', async (t) => {
8
+ const missing = path.join(t.testdir(), 'no-such-file.txt');
9
+
10
+ t.equal(await loadFully(missing), null);
11
+ });
12
+
13
+
14
+ t.test('files/loadFully returns null when the path is a directory', async (t) => {
15
+ const dir = t.testdir({ 'sub': {} });
16
+
17
+ t.equal(await loadFully(path.join(dir, 'sub')), null);
18
+ });
19
+
20
+
21
+ t.test('files/loadFully returns the whole file as text', async (t) => {
22
+ const dir = t.testdir({ 'hello.txt': 'contents\n' });
23
+
24
+ t.equal(await loadFully(path.join(dir, 'hello.txt')), 'contents\n');
25
+ });
26
+
27
+
28
+ t.test('files/loadFully returns an empty string for an empty file', async (t) => {
29
+ const dir = t.testdir({ 'empty.txt': '' });
30
+
31
+ t.equal(await loadFully(path.join(dir, 'empty.txt')), '',
32
+ 'an empty file is distinguishable from a failed read');
33
+ });
34
+
35
+
36
+ t.test('files/loadFully preserves multi-line and unicode content', async (t) => {
37
+ const body = 'first\nsecond\n\u03b1\u03b2\u03b3\n';
38
+ const dir = t.testdir({ 'multi.txt': body });
39
+
40
+ t.equal(await loadFully(path.join(dir, 'multi.txt')), body);
41
+ });
@@ -1,6 +1,6 @@
1
1
  const t = require('tap');
2
2
 
3
- const { compact, accumulate, Operator } = require('../src/lang/arrays.js');
3
+ const { compact, accumulate, Operator, unique, chunk, flatten } = require('../src/lang/arrays.js');
4
4
 
5
5
 
6
6
  t.test('arrays/compact', (t) => {
@@ -22,4 +22,46 @@ t.test('arrays/accumulate', (t) => {
22
22
  t.equal(accumulate(arr, Operator.multiply, 0), 0);
23
23
  t.equal(accumulate(arr, Operator.divide, 0), 0);
24
24
  t.end();
25
- });
25
+ });
26
+
27
+
28
+ t.test('arrays/unique', (t) => {
29
+ t.match(unique([1, 2, 2, 3, 1]), [1, 2, 3]);
30
+ t.match(unique(['a', 'b', 'a']), ['a', 'b']);
31
+ t.match(unique([NaN, NaN, 1]), [NaN, 1]);
32
+ t.match(unique([0, false, 0, false]), [0, false]);
33
+ t.match(unique([]), []);
34
+ t.match(unique(null), []);
35
+ t.match(unique(undefined), []);
36
+
37
+ const a = { id: 1 };
38
+ const b = { id: 1 };
39
+ t.match(unique([a, b, a]), [a, b]);
40
+ t.end();
41
+ });
42
+
43
+
44
+ t.test('arrays/chunk', (t) => {
45
+ t.match(chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]);
46
+ t.match(chunk([1, 2, 3], 3), [[1, 2, 3]]);
47
+ t.match(chunk([1, 2, 3], 5), [[1, 2, 3]]);
48
+ t.match(chunk([], 2), []);
49
+ t.match(chunk(null, 2), []);
50
+ t.match(chunk(undefined, 2), []);
51
+ t.match(chunk([1, 2, 3], 0), []);
52
+ t.match(chunk([1, 2, 3], -1), []);
53
+ t.match(chunk([1, 2, 3], '2'), [[1, 2], [3]]);
54
+ t.end();
55
+ });
56
+
57
+
58
+ t.test('arrays/flatten', (t) => {
59
+ t.match(flatten([1, [2, 3], 4]), [1, 2, 3, 4]);
60
+ t.match(flatten([1, [2, [3]], 4]), [1, 2, [3], 4]);
61
+ t.match(flatten([1, 2, 3]), [1, 2, 3]);
62
+ t.match(flatten([]), []);
63
+ t.match(flatten(null), []);
64
+ t.match(flatten(undefined), []);
65
+ t.match(flatten([[], [1], 2]), [1, 2]);
66
+ t.end();
67
+ });
@@ -1,6 +1,6 @@
1
1
  const t = require('tap');
2
2
 
3
- const { isNil, isArray, isObject, isNumber, isString } = require('../src/lang/checks.js');
3
+ const { isNil, isArray, isObject, isNumber, isString, isBoolean, isFunction, isInteger, isEmpty } = require('../src/lang/checks.js');
4
4
 
5
5
 
6
6
 
@@ -53,4 +53,65 @@ t.test('checks/isString', (t) => {
53
53
  t.equal(isString(1.1), false);
54
54
  t.equal(isString('misc'), true);
55
55
  t.end();
56
- })
56
+ });
57
+
58
+ t.test('checks/isBoolean', (t) => {
59
+ t.equal(isBoolean(true), true);
60
+ t.equal(isBoolean(false), true);
61
+ t.equal(isBoolean(null), false);
62
+ t.equal(isBoolean(undefined), false);
63
+ t.equal(isBoolean(0), false);
64
+ t.equal(isBoolean(1), false);
65
+ t.equal(isBoolean('true'), false);
66
+ t.equal(isBoolean({}), false);
67
+ t.end();
68
+ });
69
+
70
+ t.test('checks/isFunction', (t) => {
71
+ t.equal(isFunction(function () {}), true);
72
+ t.equal(isFunction(() => 1), true);
73
+ t.equal(isFunction(async () => 1), true);
74
+ t.equal(isFunction(function* () {}), true);
75
+ t.equal(isFunction(class Foo {}), true);
76
+ t.equal(isFunction(null), false);
77
+ t.equal(isFunction(undefined), false);
78
+ t.equal(isFunction(1), false);
79
+ t.equal(isFunction('fn'), false);
80
+ t.equal(isFunction({}), false);
81
+ t.end();
82
+ });
83
+
84
+ t.test('checks/isInteger', (t) => {
85
+ t.equal(isInteger(1), true);
86
+ t.equal(isInteger(0), true);
87
+ t.equal(isInteger(-4), true);
88
+ t.equal(isInteger(1.0), true);
89
+ t.equal(isInteger(1.1), false);
90
+ t.equal(isInteger(NaN), false);
91
+ t.equal(isInteger(Infinity), false);
92
+ t.equal(isInteger('1'), false);
93
+ t.equal(isInteger(null), false);
94
+ t.equal(isInteger(undefined), false);
95
+ t.equal(isInteger(true), false);
96
+ t.end();
97
+ });
98
+
99
+ t.test('checks/isEmpty', (t) => {
100
+ t.equal(isEmpty(null), true);
101
+ t.equal(isEmpty(undefined), true);
102
+ t.equal(isEmpty(''), true);
103
+ t.equal(isEmpty([]), true);
104
+ t.equal(isEmpty({}), true);
105
+ t.equal(isEmpty(new Map()), true);
106
+ t.equal(isEmpty(new Set()), true);
107
+
108
+ t.equal(isEmpty(' '), false);
109
+ t.equal(isEmpty([0]), false);
110
+ t.equal(isEmpty({ a: 1 }), false);
111
+ t.equal(isEmpty(new Map([['a', 1]])), false);
112
+ t.equal(isEmpty(new Set([1])), false);
113
+ t.equal(isEmpty(0), false);
114
+ t.equal(isEmpty(false), false);
115
+ t.equal(isEmpty(() => {}), false);
116
+ t.end();
117
+ });
@@ -0,0 +1,90 @@
1
+ const t = require('tap');
2
+
3
+ const { Counter, DefaultDict } = require('../src/lang/collections.js');
4
+
5
+
6
+ t.test('collections/Counter', (t) => {
7
+ const fromString = new Counter('aab');
8
+ t.equal(fromString.get('a'), 2);
9
+ t.equal(fromString.get('b'), 1);
10
+ t.equal(fromString.get('z'), 0);
11
+ t.equal(fromString.total(), 3);
12
+ t.equal(fromString.size, 2);
13
+ t.match([...fromString], ['a', 'b']);
14
+ t.match([...fromString.keys()], ['a', 'b']);
15
+ t.match([...fromString.values()], [2, 1]);
16
+ t.match([...fromString.entries()], [['a', 2], ['b', 1]]);
17
+ t.match([...fromString.elements()], ['a', 'a', 'b']);
18
+
19
+ const fromObj = new Counter({ a: 4, b: 2 });
20
+ t.equal(fromObj.get('a'), 4);
21
+ t.match(fromObj.mostCommon(1), [['a', 4]]);
22
+ t.match(fromObj.mostCommon(), [['a', 4], ['b', 2]]);
23
+ t.match(fromObj.mostCommon(0), []);
24
+
25
+ const fromArr = new Counter([1, 1, 2]);
26
+ t.equal(fromArr.get(1), 2);
27
+ fromArr.add(1);
28
+ t.equal(fromArr.get(1), 3);
29
+ fromArr.add(3, 5);
30
+ t.equal(fromArr.get(3), 5);
31
+ fromArr.set(2, 0);
32
+ t.equal(fromArr.get(2), 0);
33
+ t.ok(fromArr.delete(2));
34
+ t.equal(fromArr.get(2), 0);
35
+
36
+ const zeros = new Counter();
37
+ zeros.set('gone', 0);
38
+ zeros.set('neg', -1);
39
+ zeros.set('keep', 2);
40
+ t.match([...zeros.elements()], ['keep', 'keep']);
41
+
42
+ const fromMap = new Counter(new Map([['x', 3], ['y', 1]]));
43
+ t.equal(fromMap.get('x'), 3);
44
+
45
+ const copy = new Counter(fromString);
46
+ t.equal(copy.get('a'), 2);
47
+ copy.add('a');
48
+ t.equal(fromString.get('a'), 2);
49
+
50
+ const empty = new Counter();
51
+ t.equal(empty.total(), 0);
52
+ t.match([...empty.elements()], []);
53
+
54
+ const nan = new Counter([NaN, NaN]);
55
+ t.equal(nan.get(NaN), 2);
56
+ t.end();
57
+ });
58
+
59
+
60
+ t.test('collections/DefaultDict', (t) => {
61
+ const groups = new DefaultDict(Array);
62
+ groups.get('a').push(1);
63
+ groups.get('a').push(2);
64
+ groups.get('b').push(3);
65
+ t.match(groups.get('a'), [1, 2]);
66
+ t.match(groups.get('b'), [3]);
67
+ t.equal(groups.size, 2);
68
+ t.ok(groups.has('a'));
69
+ t.equal(groups.peek('missing', 'nope'), 'nope');
70
+ t.match(groups.peek('a'), [1, 2]);
71
+ t.equal(groups.has('missing'), false);
72
+
73
+ groups.set('c', [9]);
74
+ t.match(groups.get('c'), [9]);
75
+ t.ok(groups.delete('c'));
76
+ t.equal(groups.has('c'), false);
77
+
78
+ t.match([...groups.keys()], ['a', 'b']);
79
+ t.equal([...groups.values()].length, 2);
80
+ t.equal([...groups.entries()].length, 2);
81
+
82
+ const lists = new DefaultDict(() => []);
83
+ lists.get('k').push('x');
84
+ t.match(lists.get('k'), ['x']);
85
+
86
+ const noFactory = new DefaultDict();
87
+ t.equal(noFactory.get('x'), undefined);
88
+ t.equal(noFactory.has('x'), false);
89
+ t.end();
90
+ });
@@ -0,0 +1,49 @@
1
+ const t = require('tap');
2
+
3
+ const { if_exception } = require('../src/lang/flow.js');
4
+
5
+
6
+ t.test('flow/if_exception runs the success branch', (t) => {
7
+ const calls = [];
8
+
9
+ if_exception(
10
+ () => calls.push('body'),
11
+ () => calls.push('ok'),
12
+ () => calls.push('failed')
13
+ );
14
+
15
+ t.same(calls, ['body', 'ok']);
16
+ t.end();
17
+ });
18
+
19
+
20
+ t.test('flow/if_exception runs the failure branch with the error', (t) => {
21
+ const calls = [];
22
+ const boom = new Error('boom');
23
+ let seen = null;
24
+
25
+ if_exception(
26
+ () => { calls.push('body'); throw boom; },
27
+ () => calls.push('ok'),
28
+ (error) => { calls.push('failed'); seen = error; }
29
+ );
30
+
31
+ t.same(calls, ['body', 'failed'], 'the success callback is skipped');
32
+ t.equal(seen, boom, 'the thrown error is handed to the failure callback');
33
+ t.end();
34
+ });
35
+
36
+
37
+ t.test('flow/if_exception catches a throw from the success callback', (t) => {
38
+ const boom = new Error('from fn_noexc');
39
+ let seen = null;
40
+
41
+ if_exception(
42
+ () => {},
43
+ () => { throw boom; },
44
+ (error) => { seen = error; }
45
+ );
46
+
47
+ t.equal(seen, boom, 'fn_noexc runs inside the same try block');
48
+ t.end();
49
+ });
@@ -1,6 +1,6 @@
1
1
  const t = require('tap');
2
2
 
3
- const { reflexive } = require('../src/lang/functools.js');
3
+ const { reflexive, partial, compose, once, memoize, cache, reduce } = require('../src/lang/functools.js');
4
4
 
5
5
 
6
6
  t.test('functools/reflexive', (t) => {
@@ -9,4 +9,125 @@ t.test('functools/reflexive', (t) => {
9
9
  t.equal(reflexive('abcde'), 'abcde');
10
10
  t.equal(reflexive(2.1), 2.1);
11
11
  t.end();
12
- });
12
+ });
13
+
14
+
15
+ t.test('functools/partial', (t) => {
16
+ const add = (a, b, c) => a + b + c;
17
+ const add10 = partial(add, 10);
18
+ t.equal(add10(1, 2), 13);
19
+
20
+ const add10and20 = partial(add, 10, 20);
21
+ t.equal(add10and20(3), 33);
22
+
23
+ const greet = function (greeting, name) {
24
+ return `${greeting} ${name}, ${this.title}`;
25
+ };
26
+ const hello = partial(greet, 'hello');
27
+ t.equal(hello.call({ title: 'Dr' }, 'Ada'), 'hello Ada, Dr');
28
+ t.end();
29
+ });
30
+
31
+
32
+ t.test('functools/compose', (t) => {
33
+ const inc = (n) => n + 1;
34
+ const double = (n) => n * 2;
35
+ t.equal(compose(double, inc)(3), 8);
36
+ t.equal(compose(inc, double)(3), 7);
37
+ t.equal(compose(inc)(3), 4);
38
+ t.equal(compose()(3), 3);
39
+
40
+ const first = (...args) => args.join('-');
41
+ const wrap = (s) => `[${s}]`;
42
+ t.equal(compose(wrap, first)('a', 'b'), '[a-b]');
43
+ t.end();
44
+ });
45
+
46
+
47
+ t.test('functools/once', (t) => {
48
+ let calls = 0;
49
+ const fn = (x) => {
50
+ calls += 1;
51
+ return x * 2;
52
+ };
53
+ const onceFn = once(fn);
54
+
55
+ t.equal(onceFn(4), 8);
56
+ t.equal(onceFn(9), 8);
57
+ t.equal(calls, 1);
58
+ t.end();
59
+ });
60
+
61
+
62
+ t.test('functools/memoize', (t) => {
63
+ let calls = 0;
64
+ const fn = (a, b) => {
65
+ calls += 1;
66
+ return a + b;
67
+ };
68
+ const mem = memoize(fn);
69
+
70
+ t.equal(mem(1, 2), 3);
71
+ t.equal(mem(1, 2), 3);
72
+ t.equal(calls, 1);
73
+ t.equal(mem(1, 3), 4);
74
+ t.equal(calls, 2);
75
+
76
+ // different arity is a different cache entry
77
+ t.equal(mem(1, 2, undefined), 3);
78
+ t.equal(calls, 3);
79
+
80
+ // NaN is a valid key (SameValueZero)
81
+ let nanCalls = 0;
82
+ const nanFn = memoize((n) => {
83
+ nanCalls += 1;
84
+ return n;
85
+ });
86
+ t.ok(Number.isNaN(nanFn(NaN)));
87
+ t.ok(Number.isNaN(nanFn(NaN)));
88
+ t.equal(nanCalls, 1);
89
+
90
+ // throws are not cached
91
+ let throws = 0;
92
+ const boom = memoize((ok) => {
93
+ throws += 1;
94
+ if (!ok) {
95
+ throw new Error('nope');
96
+ }
97
+ return 'ok';
98
+ });
99
+ t.throws(() => boom(false));
100
+ t.throws(() => boom(false));
101
+ t.equal(throws, 2);
102
+ t.equal(boom(true), 'ok');
103
+ t.equal(boom(true), 'ok');
104
+ t.equal(throws, 3);
105
+
106
+ // custom resolver
107
+ let resolved = 0;
108
+ const byName = memoize((obj) => {
109
+ resolved += 1;
110
+ return obj.v;
111
+ }, (obj) => obj.id);
112
+ t.equal(byName({ id: 1, v: 10 }), 10);
113
+ t.equal(byName({ id: 1, v: 99 }), 10);
114
+ t.equal(resolved, 1);
115
+
116
+ mem.cache.clear();
117
+ t.equal(mem(1, 2), 3);
118
+ t.equal(calls, 4);
119
+ t.equal(cache, memoize);
120
+ t.end();
121
+ });
122
+
123
+
124
+ t.test('functools/reduce', (t) => {
125
+ t.equal(reduce((a, b) => a + b, [1, 2, 3, 4]), 10);
126
+ t.equal(reduce((a, b) => a + b, [1, 2, 3, 4], 10), 20);
127
+ t.equal(reduce((a, b) => a + b, [], 0), 0);
128
+ t.equal(reduce((a, b) => a * b, (function* () { yield 2; yield 3; yield 4; })()), 24);
129
+ t.throws(() => reduce((a, b) => a + b, []));
130
+ t.throws(() => reduce((a, b) => a + b, null));
131
+ t.equal(reduce((a, b) => a + b, null, 5), 5);
132
+ t.end();
133
+ });