@wizhut_tech/wizjs 0.1.4 → 0.1.7

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
package/CLAUDE.md ADDED
@@ -0,0 +1,56 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ `@wizhut_tech/wizjs` is a small, curated JavaScript utility library — Python idioms and selected stdlib helpers (`itertools`, `functools`, `collections`) for everyday JS, explicitly *not* trying to be lodash. Helpers are grouped into top-level namespaces (`io`, `lang`, `math`) with focused sub-areas under each. It is plain JavaScript (no TypeScript), CommonJS (no `type: "module"`, no build step), and ships its source directly to npm. It pulls in no runtime dependencies; the only third-party use is Node's built-in `fs` in `io/files`, so anything outside that namespace is runtime-agnostic.
8
+
9
+ ## Commands
10
+
11
+ ```sh
12
+ npm install # install devDependencies (only `tap`)
13
+ npm test # full suite — runs `tap run` over __tests__/
14
+ npx tap __tests__/lang_arrays.test.js # run a SINGLE test file
15
+ ```
16
+
17
+ There is **no build, lint, or typecheck step** — `test` is the only npm script. The test runner is [`tap`](https://node-tap.org) (v21). Tests live in `__tests__/`, one file per namespace sub-area, named `<namespace>_<area>.test.js` (e.g. `lang_arrays.test.js`, `math_numbers.test.js`).
18
+
19
+ Publishing goes to the scoped package `@wizhut_tech/wizjs` and is tag-driven, from CI: `npm version patch` then `git push --follow-tags`, and the `v<x.y.z>` tag triggers [`.github/workflows/release.yml`](.github/workflows/release.yml) — tag/`package.json` check, `npm ci && npm test`, `npm publish --access public`, GitHub release. Authentication is npm OIDC trusted publishing, so there is no token anywhere; don't publish from a laptop, as hand-published versions lose the provenance attestation. There is no `files` allowlist or `.npmignore`, so the published tarball includes `index.js` + `src/`. The repo declares the MIT license in `package.json` but currently has **no LICENSE file**.
20
+
21
+ ## Architecture
22
+
23
+ The single entry point is `index.js`. It `require`s each leaf module from `src/` and composes them into one nested object — the entire public API surface:
24
+
25
+ ```
26
+ io.files → src/io/files.js (loadFully)
27
+ lang.arrays → src/lang/arrays.js (compact, accumulate, Operator, unique, chunk, flatten)
28
+ lang.singleton → src/lang/singleton.js (singleton, getInstance)
29
+ lang.flow → src/lang/flow.js (if_exception)
30
+ lang.checks → src/lang/checks.js (isNil, isArray, isObject, isString, isNumber, isBoolean, isFunction, isInteger, isEmpty)
31
+ lang.functools → src/lang/functools.js (reflexive, partial, compose, once, memoize, cache, reduce)
32
+ lang.itertools → src/lang/itertools.js (count, repeat, cycle, chain, take, zip, range, enumerate, islice, takewhile, dropwhile, filterfalse, zip_longest, product, groupby, starmap, batched, pairwise, compress)
33
+ lang.objects → src/lang/objects.js (pick, omit, get)
34
+ lang.collections → src/lang/collections.js (Counter, DefaultDict)
35
+ math.numbers → src/math/numbers.js (toInteger, toNumber, clamp, mod)
36
+ exceptions → src/internal/exceptions.js (BadlyInitializedError)
37
+ ```
38
+
39
+ Each leaf module is a flat `module.exports = { ... }` of functions (plus the occasional class, e.g. `Operator`). The namespace objects in `index.js` are just references to those `module.exports`, so whatever a leaf exports is reachable both as `wizjs.lang.arrays.<name>` and via destructuring (`const { lang: { arrays: { compact } } } = require('@wizhut_tech/wizjs')`).
40
+
41
+ Directory layout mirrors the namespace path exactly: `src/<top-namespace>/<area>.js`. `src/internal/` holds non-public building blocks (currently just `exceptions.js`); its contents are surfaced selectively — `index.js` re-exports `BadlyInitializedError` under the top-level `exceptions` key rather than mounting the whole module.
42
+
43
+ Where a helper belongs:
44
+ - I/O / filesystem / external resources → `src/io/`
45
+ - General language utilities → `src/lang/` (`checks` = type/predicate tests, `arrays` = array ops, `flow` = control-flow wrappers, `functools` = function helpers, `itertools` = lazy generators, `objects` = plain-object pick/omit/get, `collections` = Python `Counter` / `DefaultDict`, `singleton` = the singleton hack)
46
+ - Numeric helpers → `src/math/`
47
+ - Shared internals not meant as public API → `src/internal/`
48
+
49
+ Cross-module reuse is direct relative `require` (e.g. `arrays.js` and `numbers.js` both import `isNil` from `lang/checks.js`; `itertools.js` imports `toInteger` from `math/numbers.js`).
50
+
51
+ ## Conventions
52
+
53
+ - **Module format:** CommonJS only — `require(...)` / `module.exports`. No ESM, no transpilation.
54
+ - **Naming:** functions are camelCase (`isNil`, `loadFully`, `toInteger`); Python stdlib names are kept as in Python (`if_exception`, `takewhile`, `zip_longest`, `groupby`). Classes are PascalCase (`Operator`, `BadlyInitializedError`, `Counter`, `DefaultDict`). Lazy/iterator helpers in `itertools` are generator functions.
55
+ - **Adding a helper to an existing area:** write the function in the matching `src/<ns>/<area>.js`, add it to that file's `module.exports`, and add cases to the corresponding `__tests__/<ns>_<area>.test.js`. It becomes available automatically since `index.js` references the whole `module.exports`.
56
+ - **Adding a new area or namespace:** create `src/<ns>/<area>.js`, then `require` it in `index.js` and mount it under the right namespace object. Add a `docs/<ns>_<area>.md` page and a matching `__tests__/<ns>_<area>.test.js`. Each namespace sub-area has a doc page under `docs/` linked from `readme.md` (lowercase filename).
@@ -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
+ });
@@ -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
+ });