@wizhut_tech/wizjs 0.1.7 → 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.
- package/__tests__/internal_exceptions.test.js +29 -0
- package/__tests__/io_files.test.js +41 -0
- package/__tests__/lang_flow.test.js +49 -0
- package/__tests__/package_exports.test.js +79 -0
- package/package.json +14 -1
- package/readme.md +16 -1
- package/src/io/files.js +1 -1
- package/CLAUDE.md +0 -56
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const t = require('tap');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const root = require('@wizhut_tech/wizjs');
|
|
6
|
+
|
|
7
|
+
const PUBLIC_NAMESPACES = ['io', 'lang', 'math'];
|
|
8
|
+
const SRC = path.join(__dirname, '..', 'src');
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
function areasOf(namespace) {
|
|
12
|
+
return fs.readdirSync(path.join(SRC, namespace))
|
|
13
|
+
.filter((name) => name.endsWith('.js'))
|
|
14
|
+
.map((name) => name.slice(0, -3))
|
|
15
|
+
.sort();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
t.test('exports/every public leaf module has a subpath', (t) => {
|
|
20
|
+
for (const namespace of PUBLIC_NAMESPACES) {
|
|
21
|
+
const areas = areasOf(namespace);
|
|
22
|
+
t.ok(areas.length > 0, `${namespace} has at least one area`);
|
|
23
|
+
|
|
24
|
+
for (const area of areas) {
|
|
25
|
+
const subpath = `@wizhut_tech/wizjs/${namespace}/${area}`;
|
|
26
|
+
|
|
27
|
+
t.equal(
|
|
28
|
+
require(subpath),
|
|
29
|
+
root[namespace][area],
|
|
30
|
+
`${subpath} is the same object as root.${namespace}.${area}`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
t.equal(
|
|
34
|
+
require(`${subpath}.js`),
|
|
35
|
+
root[namespace][area],
|
|
36
|
+
`${subpath}.js resolves to the same object`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
t.end();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
t.test('exports/root entry point still resolves', (t) => {
|
|
46
|
+
t.equal(require('@wizhut_tech/wizjs'), root);
|
|
47
|
+
t.type(root.lang.collections.Counter, 'function');
|
|
48
|
+
t.type(root.exceptions.BadlyInitializedError, 'function');
|
|
49
|
+
t.end();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
t.test('exports/internals stay private', (t) => {
|
|
54
|
+
t.throws(
|
|
55
|
+
() => require('@wizhut_tech/wizjs/internal/exceptions'),
|
|
56
|
+
{ code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' },
|
|
57
|
+
'src/internal is not reachable as a subpath'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
t.throws(
|
|
61
|
+
() => require('@wizhut_tech/wizjs/src/lang/collections.js'),
|
|
62
|
+
{ code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' },
|
|
63
|
+
'the raw src/ layout is not part of the public surface'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
t.throws(
|
|
67
|
+
() => require('@wizhut_tech/wizjs/lang/does-not-exist'),
|
|
68
|
+
{ code: 'MODULE_NOT_FOUND' },
|
|
69
|
+
'an unknown area under a mapped namespace fails to resolve'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
t.end();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
t.test('exports/package.json is reachable', (t) => {
|
|
77
|
+
t.equal(require('@wizhut_tech/wizjs/package.json').name, '@wizhut_tech/wizjs');
|
|
78
|
+
t.end();
|
|
79
|
+
});
|
package/package.json
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
"devDependencies": {
|
|
8
8
|
"tap": "21.7.5"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=14.13.0"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./io/*": "./src/io/*.js",
|
|
16
|
+
"./io/*.js": "./src/io/*.js",
|
|
17
|
+
"./lang/*": "./src/lang/*.js",
|
|
18
|
+
"./lang/*.js": "./src/lang/*.js",
|
|
19
|
+
"./math/*": "./src/math/*.js",
|
|
20
|
+
"./math/*.js": "./src/math/*.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
10
23
|
"homepage": "https://github.com/wizhut/wizjs#readme",
|
|
11
24
|
"keywords": [
|
|
12
25
|
"javascript",
|
|
@@ -24,5 +37,5 @@
|
|
|
24
37
|
"scripts": {
|
|
25
38
|
"test": "tap run"
|
|
26
39
|
},
|
|
27
|
-
"version": "0.
|
|
40
|
+
"version": "0.2.0"
|
|
28
41
|
}
|
package/readme.md
CHANGED
|
@@ -27,10 +27,25 @@ Use by importing:
|
|
|
27
27
|
}
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
You can also import individual functions
|
|
30
|
+
You can also import individual functions from that root object:
|
|
31
31
|
|
|
32
32
|
`const { lang: { checks : { isNil } } } = require('@wizhut_tech/wizjs');`
|
|
33
33
|
|
|
34
|
+
Or require a single namespace area directly, which keeps the destructuring flat
|
|
35
|
+
when you need helpers from more than one area:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const { isNil } = require('@wizhut_tech/wizjs/lang/checks');
|
|
39
|
+
const { Counter } = require('@wizhut_tech/wizjs/lang/collections');
|
|
40
|
+
const { clamp } = require('@wizhut_tech/wizjs/math/numbers');
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every area listed below is reachable as `@wizhut_tech/wizjs/<namespace>/<area>`,
|
|
44
|
+
and resolves to the very same object the root import exposes, so the two styles
|
|
45
|
+
mix freely. The subpath form needs Node 14.13 or newer. Library internals (under
|
|
46
|
+
`src/internal/`) are deliberately not reachable this way — `BadlyInitializedError`
|
|
47
|
+
is re-exported from the root import instead.
|
|
48
|
+
|
|
34
49
|
### I/O
|
|
35
50
|
|
|
36
51
|
* **Files** utility functions ... [[docs](docs/io_files.md)]
|
package/src/io/files.js
CHANGED
package/CLAUDE.md
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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).
|