1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 19:04:46 +00:00

298 Commits

Author SHA1 Message Date
renovate[bot]
5674e67929 chore(deps): update node.js to v24 2025-10-28 20:14:39 +00:00
renovate[bot]
3e43e4db3d chore(deps): update all non-major dependencies (#103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-25 01:44:03 +00:00
b8308e383c Merge pull request #99 from robonen/renovate/jsdom-27.x
chore(deps): update pnpm.catalog.default jsdom to v27
2025-10-23 09:58:09 +03:00
renovate[bot]
93c878cc35 chore(deps): update pnpm.catalog.default jsdom to v27 2025-10-23 06:56:36 +00:00
7653975fa4 Merge pull request #102 from robonen/renovate/actions-setup-node-6.x
chore(deps): update actions/setup-node action to v6
2025-10-23 09:52:09 +03:00
renovate[bot]
e2cb3f5a75 chore(deps): update actions/setup-node action to v6 2025-10-14 05:25:23 +00:00
renovate[bot]
67fbad8930 chore(deps): update all non-major dependencies (#101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 01:41:58 +00:00
renovate[bot]
e49c49e320 chore(deps): update all non-major dependencies (#100)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 01:45:40 +00:00
renovate[bot]
43cdc3b5e6 chore(deps): update all non-major dependencies (#98)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 01:40:15 +00:00
a9a6c04176 Merge pull request #97 from robonen/renovate/actions-setup-node-5.x
chore(deps): update actions/setup-node action to v5
2025-09-06 15:37:33 +03:00
a6d3e8971f Merge branch 'master' into renovate/actions-setup-node-5.x 2025-09-06 15:36:20 +03:00
40dfdabd08 Merge pull request #96 from robonen/chore/deps
chore: update deps
2025-09-06 15:36:02 +03:00
renovate[bot]
876a815fd3 chore(deps): update actions/setup-node action to v5 2025-09-06 12:35:00 +00:00
b1b9889ad2 chore: update deps 2025-09-06 19:34:30 +07:00
renovate[bot]
9d2a393372 chore(deps): update all non-major dependencies (#95)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-22 01:07:02 +00:00
renovate[bot]
4071e49ad6 chore(deps): update all non-major dependencies (#92)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 01:13:12 +00:00
88bd87f9b0 Merge pull request #93 from robonen/renovate/actions-checkout-5.x
chore(deps): update actions/checkout action to v5
2025-08-15 00:45:05 +03:00
renovate[bot]
ac265c05a8 chore(deps): update actions/checkout action to v5 2025-08-14 21:44:00 +00:00
69e5ebc085 Merge pull request #94 from robonen/feat/vue/unrefElement
feat(web/vue): unrefElement
2025-08-15 00:41:59 +03:00
48a85dbae2 build(web/vue): bump version to 0.0.11 2025-08-15 04:39:37 +07:00
0cfdce7456 docs(web/vue): update examples in unrefElement documentation to include type parameters 2025-08-15 04:38:46 +07:00
e035d1abca docs(web/vue): update documentation for unrefElement function 2025-08-15 04:37:58 +07:00
1851d5c80c feat(web/vue): add unrefElement function and tests for element handling 2025-08-15 04:33:47 +07:00
48626a9fe5 Merge pull request #91 from robonen/refactor/web/vue
refactor(web/vue): reuse injection context composable in injection store
2025-08-07 05:28:14 +07:00
04aa9e4721 build(web/vue): bump version to 0.0.10 2025-08-07 05:26:02 +07:00
d55e3989f3 refactor(web/vue): reuse inject context composable in context state 2025-08-07 05:25:04 +07:00
renovate[bot]
acee7e4167 chore(deps): update all non-major dependencies (#90)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-02 01:50:33 +00:00
renovate[bot]
a633bd8da0 chore(deps): update all non-major dependencies (#88)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-27 01:09:23 +00:00
e194ba3883 Merge pull request #81 from robonen/renovate/renovate-41.x
chore(deps): update devdependency renovate to v41
2025-07-19 02:13:10 +07:00
renovate[bot]
d7c978bf9e chore(deps): update devdependency renovate to v41 2025-07-18 07:53:11 +00:00
renovate[bot]
5674095073 chore(deps): update all non-major dependencies (#84)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-18 01:10:46 +00:00
77bab6055c Merge pull request #87 from robonen/fix/web/vue/missing-export
fix(web/vue): missing useAsyncState export
2025-07-14 02:19:49 +07:00
7fcafae467 chore(web/vue): bump version to 0.0.9 2025-07-14 02:17:46 +07:00
52a5add405 fix(web/vue): add missing export useAsyncState from composables 2025-07-14 02:17:31 +07:00
bd5fdab6a0 Merge pull request #86 from robonen/chore/web/vue/0.0.8
chore(web/vue): bump version to 0.0.8
2025-07-14 01:01:39 +07:00
e8d7cccfe0 chore(web/vue): bump version to 0.0.8 2025-07-14 01:00:25 +07:00
be13ec7079 Merge pull request #85 from robonen/web/vue/useAsyncState
feat(web/vue): add useAsyncState
2025-07-14 00:51:53 +07:00
55438b63f9 test(web/vue): update useAsyncState to allow optional delay parameter and add tests 2025-07-14 00:42:50 +07:00
1e9859da83 feat(web/vue): enhance async state management for useAsyncState with improved error handling and loading states 2025-07-10 05:34:47 +07:00
renovate[bot]
aa8a0f00f3 chore(deps): update all non-major dependencies (#83)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-29 01:29:06 +00:00
renovate[bot]
e1e879ebbb chore(deps): update all non-major dependencies (#82)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 01:09:26 +00:00
renovate[bot]
6339b21f56 chore(deps): update all non-major dependencies (#80)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 01:11:50 +00:00
renovate[bot]
1d4f5c5512 chore(deps): update all non-major dependencies (#78)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 01:00:27 +00:00
2cc0efd556 Merge pull request #79 from robonen/feat/tsconfig-ts-imports
feat(configs/tsconfig): enable importing TypeScript extensions in tsc…
2025-05-27 16:10:51 +07:00
bef0aea14c build(configs/tsconfig): bump version to 0.0.2 2025-05-27 16:09:56 +07:00
40d1d6962b feat(configs/tsconfig): enable importing TypeScript extensions in tsconfig 2025-05-27 16:07:59 +07:00
renovate[bot]
eb8514fe89 chore(deps): update all non-major dependencies (#73)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-24 01:49:10 +00:00
b6200aa7a3 Merge pull request #77 from robonen/refactor/template
Refactor template
2025-05-20 19:42:49 +07:00
a67322ca66 test: temporary disable type checking in test configuration 2025-05-20 19:26:58 +07:00
8297e47086 Merge branch 'master' into refactor/template 2025-05-20 19:25:22 +07:00
95e1bcd0c4 feat: update vitest configuration and dependencies to version 3.2.0-beta.2 2025-05-20 19:20:38 +07:00
6d68246d16 refactor(core/stdlib): update test descriptions and improve placeholder handling 2025-05-20 19:20:26 +07:00
049b5b351a feat(core/stdlib): implement get function and remove getByPath 2025-05-20 19:20:03 +07:00
890d984aad feat(core/stdlib): add type definitions and tests for collections and union types 2025-05-20 19:19:41 +07:00
9d01b12160 Merge pull request #76 from robonen/fix/renovate
fix(infra/renovate): update renovate path
2025-05-20 14:34:08 +07:00
6f2311afeb fix(infra/renovate): update renovate configuration to extend correct default settings 2025-05-20 14:32:28 +07:00
f7312b1060 Merge pull request #74 from robonen/refactor/dir-struct
Refactor directory structure
2025-05-19 18:08:01 +07:00
3d15f7b3b2 chore: remove CHANGELOG.md file 2025-05-19 18:04:25 +07:00
32bf20899f chore: remove shebang from cli.ts 2025-05-19 17:52:26 +07:00
8355477e0e chore: remove unused cover image 2025-05-19 17:51:39 +07:00
968cf26fd0 refactor: simplify version check logic in publish workflow 2025-05-19 17:49:57 +07:00
78fb4da82a refactor: change separate tools by category 2025-05-19 17:43:42 +07:00
d55737df2f chore: dedupe deps 2025-05-19 04:46:08 +07:00
39ce28a5ef Merge pull request #70 from robonen/renovate/all-minor-patch
chore(deps): update all non-major dependencies
2025-05-19 04:44:01 +07:00
renovate[bot]
3d813d22b9 chore(deps): update all non-major dependencies 2025-05-18 21:43:01 +00:00
4f558270ce Merge pull request #72 from robonen/chore/config
Chore/vitest-config
2025-05-19 04:40:53 +07:00
4d6922e06a fix: update CI and publish workflows to use correct build and test commands 2025-05-19 04:36:39 +07:00
fa726eecc4 chore: add workspace vitest configuration for testing with jsdom and coverage 2025-05-19 04:34:13 +07:00
c5f34efe05 chore: remove obsolete documentation and configuration files 2025-05-19 03:34:50 +07:00
ead9c019cd Merge pull request #71 from robonen/refactor/update-cli
feat(cli): auto resolve latest packages for cli
2025-05-18 00:04:23 +07:00
8ee6970674 chore: update package manager version and remove unused dependencies 2025-05-18 00:03:19 +07:00
27c80d24ef feat(cli): update CLI tool for project creation with package.json and config generation 2025-05-17 23:55:52 +07:00
c596e8aa29 build: bump stdlib 0.0.7 2025-05-11 15:41:48 +07:00
f8b37cacd3 fix(packages/stdlib): fix pubsub types 2025-05-11 15:36:26 +07:00
40d8194134 ci: add private packages checking in publish action 2025-05-11 15:10:46 +07:00
renovate[bot]
11d1ac232e chore(deps): update all non-major dependencies (#69)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 01:32:48 +00:00
7c1d801c8e build: fix publish script, bump stdlib 0.0.6 and vue 0.0.7 2025-05-09 22:31:02 +07:00
de391fa80d build: bump stdlib 0.0.5 and vue 0.0.6 2025-05-09 14:22:13 +07:00
8ab58078ba build: revert stdlib and vue versions 2025-05-09 14:11:18 +07:00
88f6cec9b2 ci: add registry-url 2025-05-09 13:38:29 +07:00
09e72d904c build: bump stdlib 0.0.5 and vue 0.0.6 2025-05-09 13:32:45 +07:00
695647470b ci: fix branch for publish workflow 2025-05-09 13:22:32 +07:00
b2beb6a5fc Merge pull request #63 from robonen/feat/sync-mutex
fix(packages/stdlib): add SyncMutex primitive
2025-05-09 13:19:14 +07:00
c7048be9fb chore(packages/vue): rename startTime to renderStartTime in useRenderInfo 2025-05-09 13:18:12 +07:00
4ead7fb18c chore: add npm-publish gh action 2025-05-09 13:12:37 +07:00
3994f349f4 feat(packages/stdlib): add execute method for SyncMutex 2025-05-09 13:12:07 +07:00
8d6f08c332 fix(packages/vue): set render duration ref only after mounted and updated 2025-05-09 13:11:20 +07:00
3a2837c1a1 fix(packages/vue): revert to old version useRenderCount 2025-05-09 13:09:03 +07:00
renovate[bot]
82a0c0f746 chore(deps): update all non-major dependencies (#68)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-08 01:55:42 +00:00
renovate[bot]
e8667d6a0a chore(deps): update devdependency renovate to ^40.1.3 (#67)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-04 01:52:46 +00:00
6ed7d39a11 Merge pull request #65 from robonen/renovate/all-minor-patch
chore(deps): update all non-major dependencies
2025-05-01 07:58:37 +07:00
renovate[bot]
74c170e853 chore(deps): update all non-major dependencies 2025-05-01 00:57:28 +00:00
fa96b9ddee Merge pull request #57 from robonen/renovate/pnpm-10.x
chore(deps): update pnpm to v10
2025-05-01 07:56:47 +07:00
ff4a88b896 Merge pull request #66 from robonen/renovate/renovate-40.x
chore(deps): update devdependency renovate to v40
2025-05-01 07:56:31 +07:00
renovate[bot]
871e0cfad2 chore(deps): update devdependency renovate to v40 2025-04-30 23:32:34 +00:00
renovate[bot]
849d444172 chore(deps): update pnpm to v10 2025-04-28 02:03:30 +00:00
renovate[bot]
cea221ed57 chore(deps): update all non-major dependencies (#64)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 01:26:48 +00:00
renovate[bot]
49dacf071f chore(deps): update all non-major dependencies (#61)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-23 01:13:25 +00:00
a07ac35db9 fix(packages/vue): add mutex for useRenderCount to avoid infinite rerender 2025-02-23 00:44:58 +07:00
8c5252986e feat(packages/stdlib): add SyncMutex 2025-02-23 00:44:14 +07:00
fad1284cd3 Merge pull request #62 from robonen/feat/injection-store
feat(packages/vue): useInjectionStore
2025-02-22 23:40:54 +07:00
ca0a63ea38 build(packages/vue): bump v0.0.5 2025-02-22 23:39:50 +07:00
7bfbb8e52a chore: update deps 2025-02-22 23:39:31 +07:00
30b72fb2f0 refactor(packages/vue): use another way to provide state at app level in useContextFactory 2025-02-22 23:35:20 +07:00
5594cef31e feat(packages/vue): add useInjectionStore 2025-02-22 23:30:45 +07:00
caa7c4221a Merge pull request #60 from robonen/chore/drop-apps
chore(apps): drop apps workspace completely
2025-02-09 05:20:32 +07:00
6ae3c939d8 Merge branch 'master' into chore/drop-apps
# Conflicts:
#	apps/vhs/package.json
#	pnpm-workspace.yaml
2025-02-09 05:19:10 +07:00
1bada217e9 chore(apps): drop apps workspace completely 2025-02-09 05:13:18 +07:00
renovate[bot]
c813bd174c chore(deps): update all non-major dependencies (#58)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-07 01:13:10 +00:00
renovate[bot]
a2f49b6286 chore(deps): update pnpm.catalog.default vitest to v3.0.5 [security] (#59)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-05 01:04:57 +00:00
renovate[bot]
987b8d4abd chore(deps): update all non-major dependencies (#56)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-02 01:51:20 +00:00
c68436a36a feat(packages/stdlib): bump 0.0.4 2025-01-23 04:03:46 +07:00
renovate[bot]
cca2e2e798 chore(deps): update node.js to >=22.13.0 (#55)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 23:58:49 +07:00
2936c5a8d6 chore(deps): update all deps, bump vitest major version 2025-01-17 23:57:05 +07:00
renovate[bot]
d6bc42d568 chore(deps): update all non-major dependencies (#54)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 01:18:51 +00:00
renovate[bot]
552b6afc54 chore(deps): update all non-major dependencies (#52)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-31 01:46:19 +00:00
renovate[bot]
2eb4665f4f chore(deps): update devdependency renovate to v39 (#47)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-27 17:12:35 +07:00
renovate[bot]
4d5c05538a chore(deps): update pnpm to v9.15.1 (#51)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-25 01:30:15 +00:00
renovate[bot]
45bec99eb6 chore(deps): update all non-major dependencies (#50)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 01:47:40 +00:00
90dbdf4c88 feat(packages/stdlib): add promise types 2024-11-30 04:24:49 +07:00
41e3d90e41 refactor(packages/stdlib): fix new lines for types 2024-11-30 04:24:27 +07:00
e93b1ccb68 refactor(packages/stdlib): add test case for BitVector 2024-11-29 05:42:50 +07:00
f88a466262 build(packages/vue): bump 0.0.4 2024-11-26 16:14:50 +07:00
46ea487222 refactor(packages/vue): return object instead of tuple for useContextFactory 2024-11-26 16:10:59 +07:00
1823771b4a refactor(packages/vue): add app.provide() support for useContextFactory 2024-11-26 15:56:25 +07:00
3e2b88d871 Merge remote-tracking branch 'origin/master' 2024-11-26 15:07:17 +07:00
renovate[bot]
bb90892af9 chore(deps): update all non-major dependencies (#49)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 01:36:03 +00:00
4b91a425f7 build(packages/vue): bump 0.0.3 2024-11-23 13:36:01 +07:00
f49e85286c build(packages/vue): bump 0.0.2 2024-11-23 13:30:57 +07:00
1b1a34c63b feat(packages/ui): export useFocusGuard 2024-11-23 13:24:19 +07:00
a89723126e build(packages/vue): enable deps inlining 2024-11-23 13:21:09 +07:00
046ebbd172 chore(packages/vue): move workspace deps in dev deps 2024-11-23 13:16:59 +07:00
e4905ef87e feat(packages/vue): add focusGuard composable 2024-11-22 16:49:59 +07:00
50257463b7 feat(packages/platform): add focusGuard brwoser util 2024-11-22 16:49:24 +07:00
979fd6e6df build(packages/platform): setup test suit, add build config 2024-11-22 16:48:47 +07:00
bdc8fab071 ci(repo): update ci pipeline (#48)
* ci(repo): update ci pipeline

* ci(repo): remove pnpm version from ci

* ci(repo): make build and test single command

* ci(repo): remove matrix tests
2024-11-05 02:18:27 +07:00
0c87de4573 chore(packages/renovate): cs 2024-11-03 00:56:46 +07:00
f87ee85c0b fix(packages/renovate): fix php matchUpdateTypes syntax 2024-11-03 00:47:05 +07:00
9f2df92371 feat(pacakges/renovate): add php path versions ignoring 2024-11-03 00:39:35 +07:00
renovate[bot]
f385d497f4 chore(deps): update all non-major dependencies 2024-11-02 01:16:45 +00:00
276ba9736f build(release): bump version to 0.0.3 2024-10-31 00:54:43 +07:00
9ccde9b040 Merge pull request #46 from robonen/object-utils
Object utils
2024-10-31 00:51:43 +07:00
dd0f481e19 chore(packages/stdlib): fix code style, stack pop and peek jsdoc 2024-10-31 00:50:08 +07:00
6408c1b328 feat(packages/stdlib): add object tools exports 2024-10-31 00:48:55 +07:00
23541a5476 feat(packages/stdlib): add pick object tool 2024-10-31 00:48:24 +07:00
3daa47dc83 feat(packages/stdlib): add omit object tool 2024-10-31 00:48:01 +07:00
a1747ea535 Merge pull request #45 from robonen/renovate/node-22.x
chore(deps): update node.js to v22
2024-10-30 23:25:14 +07:00
renovate[bot]
cbffd80555 chore(deps): update node.js to v22 2024-10-30 16:23:51 +00:00
58cb287f93 chore(deps): update all deps 2024-10-30 23:22:36 +07:00
renovate[bot]
17a7cbb936 chore(deps): update all non-major dependencies 2024-10-26 01:35:31 +00:00
96749c8510 Merge pull request #43 from robonen/bits
Add bit vector and refactor stack struct
2024-10-26 06:49:48 +07:00
3cc500f22b chore(packages/stdlib): remove unused import 2024-10-26 06:47:59 +07:00
fc774bc1af refactor(packages/stdlib): add small margin to the expected value 2024-10-26 06:45:53 +07:00
c96213137e refactor(packages/stdlib): stack return undefined on peek if it empty 2024-10-26 06:37:27 +07:00
8080e7eafe feat(packages/stdlib): add bit vector tool 2024-10-26 06:22:42 +07:00
53c969370a refactor(packages/stdlib): separate flags and helpers in bit tools 2024-10-26 06:20:43 +07:00
22fc55ce01 refactor(packages/stdlib): replace type by any function helper 2024-10-24 07:42:30 +07:00
9b5eef04c7 feat(packages/stdlib): add sleep async util 2024-10-24 07:30:41 +07:00
5bc3dd5ee0 feat(packages/stdlib): add tryIt async util 2024-10-24 07:29:55 +07:00
29d8aa086c refactor(packages/stdlib): update dir names and imports 2024-10-24 07:29:37 +07:00
85eb28a5dc feat(packages/stdlib): add sum arrays util 2024-10-24 07:28:24 +07:00
759d418d88 feat(packages/stdlib): add last arrays util 2024-10-24 07:28:15 +07:00
ff71cfffac feat(packages/stdlib): add first arrays util 2024-10-24 07:28:01 +07:00
5419b0a479 feat(packages/stdlib): add cluster arrays util 2024-10-24 07:27:49 +07:00
0faafc1b52 feat(packages/stdlib): add unique arrays util 2024-10-24 07:27:37 +07:00
126bb7fa9d Merge pull request #41 from robonen/vue-tools
Vue tools
2024-10-23 08:02:23 +07:00
7c0dff595b chore(packages/platform): remove test scripts 2024-10-23 07:59:53 +07:00
c350c977d5 chore(packages/vue): update jsdoc, fill package.json 2024-10-23 07:48:40 +07:00
b6d5b5b92c chore(packages/stdlib): update license and bump version 0.0.2 2024-10-23 07:47:55 +07:00
dc5e45acda chore(packages): update jsr config 2024-10-23 07:46:45 +07:00
9e7d7d8fdb chore(packages/platform): update license, bump 0.0.2 version 2024-10-23 07:30:57 +07:00
ed76a867e6 chore(packages/platform): prepare for 0.0.1 release 2024-10-23 07:16:46 +07:00
95bfa4f0f1 chore(deps): bump unbuild to 3.0.0-rc.11 2024-10-23 07:16:01 +07:00
cc439019e9 refactor(packages/vue): clean test names for UseLastChanged, add new imports 2024-10-23 06:51:05 +07:00
5722494458 feat(packages/vue): init useEventListener composable 2024-10-23 06:49:57 +07:00
a5ba8ab13e feat(packages/vue): add useOffsetPagination composable 2024-10-23 06:48:47 +07:00
ae6154c4b6 feat(packages/vue): add useRenderInfo composable 2024-10-23 06:47:49 +07:00
0667f15f0c refactor(packages/vue): add counter on mounted for useRenderCount 2024-10-23 06:46:19 +07:00
8989701303 feat(packages/vue): add useContextFactory composable 2024-10-23 06:44:19 +07:00
aff1a95c2f feat(packages/vue): add useClamp composable 2024-10-23 06:43:29 +07:00
19d7a2ca76 refactor(packages/vue): fix import for useMounted 2024-10-23 06:42:53 +07:00
6814b16d4d feat(packages/vue): add useAppSharedState composable 2024-10-23 06:41:02 +07:00
7b4f2d0c0a feat(packages/vue): add tryOnScopeDispose composable 2024-10-23 06:40:32 +07:00
9cc8f08d43 feat(packages/vue): add tryOnMounted composable 2024-10-23 06:38:19 +07:00
29bbc6aa9c feat(packages/vue): add tryOnBeforeMount composable 2024-10-23 06:33:06 +07:00
b2b74d8e2d feat(packages/stdlib): add function and array types 2024-10-23 06:29:05 +07:00
679bced9f1 refactor(pacakages/stdlib): add base jsdoc for each function 2024-10-23 06:27:14 +07:00
eadf791942 feat(packages/stdlib): init async utils 2024-10-23 06:24:50 +07:00
d415e61ac0 feat(packages/platform): add global isClient util and build config 2024-10-23 06:21:12 +07:00
4e798acfdd chore(deps): update all deps and other packages stuff 2024-10-23 06:17:54 +07:00
6a89239a75 test(packages/vue): add test for tryOnMounted 2024-10-06 21:07:29 +07:00
4bbc3b45a2 feat(packages/vue): add build config 2024-10-06 07:31:29 +07:00
d48e6469a3 refactor(packages/vue): some fixes, add resumable type 2024-10-06 06:47:53 +07:00
2e5e477097 feat(packages/vue): add useAppSharedState composable 2024-10-06 06:47:27 +07:00
658d180a8a feat(packages/vue): add useLastChanged composable 2024-10-06 06:46:57 +07:00
e41c78cd1d feat(packages/stdlib): add AnyFunction type 2024-10-06 06:46:13 +07:00
e84187fa02 feat(packages/vue): add useContexFactory composable 2024-10-05 22:02:45 +07:00
b525d08363 feat(packages/vue): add custom error 2024-10-05 22:02:03 +07:00
2ff7196241 feat(packages/vue): add useSupported composable 2024-10-05 17:56:58 +07:00
5a91cd264f feat(packages/vue): add useMounted composable 2024-10-05 17:56:22 +07:00
11c099ab4a refactor(packages/stdlib): add symbol in EventsRecord 2024-10-05 05:44:35 +07:00
00fd5846aa feat(packages/stdlib): add timestamp and noop utils 2024-10-05 05:43:13 +07:00
8822325299 refactor(packages/vue): make getter param possible for useCached and useCounter 2024-10-05 05:00:27 +07:00
85fd7e34e0 chore(deps): update catalog packages 2024-10-05 04:20:09 +07:00
renovate[bot]
fcdc4e251f chore(deps): update all non-major dependencies 2024-10-05 04:16:19 +07:00
0d7b1de1b2 fix(app): change project in readme bage 2024-10-03 18:28:26 +07:00
9012929d86 chore(app): update readme 2024-10-03 18:20:58 +07:00
4fd4008caa chore(repo): replace image 2024-10-02 06:53:00 +07:00
9978c09cec chore(repo): update readme 2024-10-02 06:26:04 +07:00
a8292fa59c chore(repo): test readme 2024-10-02 06:13:34 +07:00
80db132e49 Merge pull request #39 from robonen/pubsub-edge-case-test
Pubsub edge case test
2024-10-01 07:14:08 +07:00
f8a684e91a chore(repo): add apps workspace 2024-10-01 07:13:34 +07:00
61c699381b test(packages/stdlib): add pubsub edge case check 2024-10-01 07:13:09 +07:00
307ec29787 Merge pull request #38 from robonen/platform
Platform
2024-09-30 06:57:10 +07:00
11734b96b8 chore(repo): add build all script, update ci tests 2024-09-30 06:55:47 +07:00
f32deb3cc6 chore(repo): add pnpm catalogs 2024-09-30 06:48:30 +07:00
174e1b02d8 Merge branch 'stdlib-types' into platform
# Conflicts:
#	packages/stdlib/package.json
#	packages/stdlib/src/math/basic/remap/index.ts
2024-09-30 06:30:24 +07:00
dba020efab fix(packages/platform): remove test script 2024-09-30 06:26:22 +07:00
067f4d370f chore(deps): update pnpm lock 2024-09-30 06:24:24 +07:00
469ef8cdc2 feat(packages): add version jsdoc tags 2024-09-30 06:20:45 +07:00
975ca98f9a feat(packages/stdlib): add bigint math utils 2024-09-30 06:20:09 +07:00
061d45f6fd refactor(packages/stdlib): separate math utils штещ basic and bigint versions 2024-09-30 06:19:16 +07:00
db7e35d152 Merge branch 'master' into platform
# Conflicts:
#	package.json
#	packages/renovate/package.json
#	packages/stdlib/package.json
#	packages/tsconfig/package.json
#	pnpm-lock.yaml
2024-09-29 21:21:17 +07:00
9805b30c75 Vhs (#37)
* chore(repo): update deps in cli

* refactor(packages/tsconfig): change target to ESNext

* chore(deps): update all deps

* feat(apps/vhs): add vhs app

* chore(deps): sync with master
2024-09-28 02:09:29 +07:00
65312d007e chore(deps): update all deps 2024-09-28 02:03:55 +07:00
renovate[bot]
746eae3a8e chore(deps): update all non-major dependencies to ^2.1.1 2024-09-19 01:49:07 +00:00
renovate[bot]
cf80ea4edd chore(deps): update all non-major dependencies 2024-09-13 01:44:40 +00:00
renovate[bot]
7ae3178277 chore(deps): update devdependency @types/node to ^20.16.5 2024-09-07 01:51:07 +00:00
renovate[bot]
1fabe0cf6a chore(deps): update all non-major dependencies 2024-08-31 01:53:31 +00:00
renovate[bot]
dd2cb68fc1 chore(deps): update all non-major dependencies 2024-08-15 01:29:36 +00:00
renovate[bot]
f848961a03 chore(deps): update all non-major dependencies 2024-08-06 01:17:17 +00:00
renovate[bot]
1a74a0eca4 chore(deps): update all non-major dependencies 2024-07-25 01:34:59 +00:00
renovate[bot]
7f32e50106 chore(deps): update all non-major dependencies (#27)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-20 05:43:55 +07:00
renovate[bot]
b3eacd8d99 chore(deps): update vitest monorepo to v2 (#26)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-20 05:42:12 +07:00
e243d55428 feat(packages/platform): init new package 2024-07-20 05:38:57 +07:00
renovate[bot]
e4b6ec6384 chore(deps): update all non-major dependencies 2024-07-09 01:25:02 +00:00
renovate[bot]
d3f00e0c20 chore(deps): update devdependency renovate to ^37.422.4 2024-07-04 01:43:29 +00:00
renovate[bot]
80378c46a1 chore(deps): update all non-major dependencies 2024-07-02 01:14:01 +00:00
renovate[bot]
b5dc1047af chore(deps): update all non-major dependencies 2024-06-21 01:30:16 +00:00
renovate[bot]
8201dd7331 chore(deps): update all non-major dependencies 2024-06-06 01:12:56 +00:00
renovate[bot]
b19ed7e60d chore(deps): update all non-major dependencies 2024-06-02 01:39:06 +00:00
4d52909804 feat(packages/vue): add useToggle composable 2024-05-31 01:19:56 +07:00
4ce8babde2 chore(docs): update vitepress configuration 2024-05-31 01:19:41 +07:00
220239400a chore(packages/vue): add @vue/test-utils as a devDependency 2024-05-31 01:18:55 +07:00
5566bdcf80 feat(packages/vue): add useCached composable 2024-05-31 01:18:10 +07:00
93065d46ca feat(packages/vue): update useCounter composable to include additional functionality 2024-05-31 01:17:35 +07:00
d9973af2ed feat(packages/vue): add useRenderCount composable 2024-05-31 01:16:33 +07:00
107e192b33 feat(packages/vue): add useSyncRefs composable 2024-05-31 01:15:30 +07:00
34c72146e2 chore(repo): update pnpm and node versions in cli tool 2024-05-30 04:17:57 +07:00
d7c32f2f45 feat(packages/vue): add useCounter composable 2024-05-30 04:16:45 +07:00
03245921da feat(packages/vue): new package with initial configuration files 2024-05-30 04:16:15 +07:00
c007a54522 chore(repo): update pnpm 2024-05-30 02:57:34 +07:00
d0c74be856 refactor(packages/stdlib): change getByPath type to string and add comments to template types 2024-05-30 02:51:08 +07:00
ba68e293b9 chore(packages/stdlib): update pnpm 2024-05-30 02:48:50 +07:00
925af11be4 Merge branch 'master' into stdlib-types 2024-05-30 02:44:37 +07:00
2a931bbeb9 chore(packages/tsconfig): update removeComments option to false 2024-05-28 00:24:13 +07:00
renovate[bot]
fe3311bdfb chore(deps): update all non-major dependencies (#17)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-27 14:05:59 +07:00
2b5d81420a chore(repo): updated the pnpm version to 9.1.0 and the Vite version to 5.2.11 in the CLI file. 2024-05-08 05:30:32 +07:00
renovate[bot]
a6d40a4482 chore(deps): update all non-major dependencies (#16)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-08 04:31:05 +07:00
dc48cbc44b feat(packages/stdlib): start getByPath and template tools 2024-05-04 06:59:35 +07:00
6931fc6f18 chore(packages/stdlib): add ts and js utility types 2024-05-04 06:57:46 +07:00
7091352be2 chore(packages/stdlib): update import statement to use single quotes in mapRange function, lowercase in test desciptions 2024-05-04 06:53:46 +07:00
30654d2fe6 Docs (#15)
* feat(docs): update Vitepress config to include @robonen/renovate and @robonen/stdlib packages

* chore(packages/renovate): mark package.json private to true
2024-05-01 05:41:34 +07:00
8a33f6945c chore(packages/renovate): mark package.json private to true 2024-05-01 01:23:07 +07:00
aa10ed0f13 feat(docs): update Vitepress config to include @robonen/renovate and @robonen/stdlib packages 2024-05-01 01:22:05 +07:00
renovate[bot]
a9975bd06b chore(deps): update all non-major dependencies (#14)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 22:47:03 +07:00
renovate[bot]
8d64b012a1 chore(deps): update all non-major dependencies (#13)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-28 17:02:43 +07:00
cafe245732 feat(packages/renovate): auto approve 2024-04-28 17:02:10 +07:00
5105c2374c chore(repo): update pnpm version to 9.0.6 and Node version to >=20.12.2 2024-04-27 06:34:53 +07:00
4b960a3ad3 chore(packages/stdlib): update description 2024-04-26 02:03:13 +07:00
a5f62c16a3 feat(packages/renovate): add @robonen/renovate package (#12) 2024-04-26 04:39:23 +07:00
26b2392f9b ops(repo): add CI workflow for running tests on pull requests (#11)
* ops(repo): add CI workflow for running tests on pull requests

* fix(repo): test command in CI workflow to use pnpm run all:test

* fix(repo): update Node version in CI workflow to 20.x

* fix(repo): update CI workflow to install dependencies using pnpm
2024-04-26 04:20:02 +07:00
f6a1e68d85 chore(repo): script to run tests from monorepo root 2024-04-26 01:18:44 +07:00
cfcf0818ad Merge pull request #10 from robonen/renovate/all-minor-patch
chore(deps): update all non-major dependencies
2024-04-26 01:32:21 +07:00
renovate[bot]
4e92ed18ed chore(deps): update all non-major dependencies 2024-04-25 18:30:47 +00:00
69f9ae0900 Merge pull request #8 from robonen/renovate/node-20.x
chore(deps): update node.js to v20
2024-04-26 01:28:04 +07:00
renovate[bot]
18c3e16bb2 chore(deps): update node.js to v20 2024-04-25 18:25:17 +00:00
4afbe234f8 Merge pull request #7 from robonen/renoveate-bot
feat(repo): add .github/CODEOWNERS file and renovate.json configuration
2024-04-26 01:24:39 +07:00
b2923964e5 feat(repo): add .github/CODEOWNERS file and renovate.json configuration 2024-04-25 22:28:04 +07:00
531e1721bb feat(packages/stdlib): change build system to unbuild 2024-04-19 02:29:27 +07:00
26a99b7d67 chore(repo): update deps 2024-04-19 01:10:07 +07:00
90328bf8d0 Merge pull request #5 from robonen/stdlib-structs
Stack impl
2024-04-18 21:44:16 +07:00
55440a83ba chore(repo): update packageManager version to pnpm@9.0.1 2024-04-18 10:26:53 +07:00
943e913e76 refactor(packages/stdlib): use toReversed method instead of reverese 2024-04-18 10:26:16 +07:00
c90c17db93 feat(packages/stdlib): add structs exports 2024-04-17 04:04:28 +07:00
7546f2b653 feat(packages/stdlib): add stack data structure and tests 2024-04-17 04:00:41 +07:00
784457a507 refactor(packages/stdlib): add more accurate expectation RangeError for bitsflags 2024-04-17 03:59:35 +07:00
5bf6317673 refactor(packages/stdlib): add jsdoc for pubsub 2024-04-17 02:01:29 +07:00
80d8e37c03 refactor(packages/stdlib): add bitflags export 2024-04-16 23:08:57 +07:00
8a5d063800 Merge pull request #4 from robonen/stdlib-bitsflags
feat(packages/stdlib): add flagsGenerator and bitwise operation funct…
2024-04-17 05:08:22 +07:00
bf9e811346 feat(packages/stdlib): add flagsGenerator and bitwise operation functions 2024-04-16 23:03:20 +07:00
841d172598 chore(packages/stdlib): bump vitest to 1.5.0 2024-04-16 21:04:34 +07:00
43796ecae2 Merge pull request #3 from robonen/stdlib-pubsub
Stdlib pubsub
2024-04-16 17:07:14 +07:00
e3ef3a693e feat(packages/stdlib): add patterns exports 2024-04-16 16:39:24 +07:00
f987f722df feat(packages/stdlib): add PubSub class and tests to patterns 2024-04-16 16:32:56 +07:00
fb76d11725 feat(packages/stdlib): add jsr support 2024-04-12 00:42:47 +07:00
7182706595 refactor(packages/stdlib): remove slow types from levenstein distance 2024-04-12 00:42:12 +07:00
f3a2ae53f4 feat(repo): add support jsr in cli 2024-04-12 00:41:07 +07:00
183 changed files with 16518 additions and 2061 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @robonen

35
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: CI
on:
pull_request:
branches:
- master
env:
NODE_VERSION: 22.x
jobs:
code-quality:
name: Code quality checks
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm build && pnpm test

78
.github/workflows/publish.yaml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: Publish to NPM
on:
push:
branches:
- master
env:
NODE_VERSION: 22.x
jobs:
check-and-publish:
name: Check version changes and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build & Test
run: pnpm build && pnpm test
- name: Check for version changes and publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
# Find all package.json files (excluding node_modules)
PACKAGE_FILES=$(find . -path "*/package.json" -not -path "*/node_modules/*")
for file in $PACKAGE_FILES; do
PACKAGE_DIR=$(dirname $file)
echo "Checking $PACKAGE_DIR for version changes..."
# Get package details
PACKAGE_NAME=$(node -p "require('$file').name")
CURRENT_VERSION=$(node -p "require('$file').version")
IS_PRIVATE=$(node -p "require('$file').private || false")
# Skip private packages
if [ "$IS_PRIVATE" == "true" ]; then
echo "Skipping private package $PACKAGE_NAME"
continue
fi
# Skip root package
if [ "$PACKAGE_DIR" == "." ]; then
echo "Skipping root package"
continue
fi
# Check if package exists on npm
NPM_VERSION=$(npm view $PACKAGE_NAME version 2>/dev/null || echo "0.0.0")
# Compare versions
if [ "$CURRENT_VERSION" != "$NPM_VERSION" ]; then
echo "Version changed for $PACKAGE_NAME: $NPM_VERSION → $CURRENT_VERSION"
echo "Publishing $PACKAGE_NAME@$CURRENT_VERSION"
cd $PACKAGE_DIR
pnpm publish --access public --no-git-checks
cd -
else
echo "No version change detected for $PACKAGE_NAME"
fi
done

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ node_modules
.nuxt
.nitro
.cache
cache
out
build
dist

View File

@@ -1,20 +0,0 @@
import { defineConfig } from 'vitepress';
export default defineConfig({
lang: 'ru-RU',
title: "Tools",
description: "A set of tools and utilities for web development",
rewrites: {
'packages/:pkg/README.md': 'packages/:pkg/index.md'
},
themeConfig: {
sidebar: [
{
text: 'Пакеты',
items: [
{ text: '@robonen/tsconfig', link: '/packages/tsconfig/' },
],
},
],
},
});

View File

@@ -1,57 +0,0 @@
# Changelog
## v0.0.1
### 🚀 Enhancements
- **repo:** Cli tool, base tscofig ([3fcc42e](https://github.com/robonen/tools/commit/3fcc42e))
- **repo:** Drop node_modules ([7dba5ac](https://github.com/robonen/tools/commit/7dba5ac))
- **repo:** Global gitignore ([00c2736](https://github.com/robonen/tools/commit/00c2736))
- **packages/tsconfig:** Readme ([afa15cd](https://github.com/robonen/tools/commit/afa15cd))
- **docs:** Add auto generated doc based on readme ([3960f86](https://github.com/robonen/tools/commit/3960f86))
- **packages/stdlib:** Create stdlib ([c985b95](https://github.com/robonen/tools/commit/c985b95))
- **packages/stdlib:** Base vite config ([0434725](https://github.com/robonen/tools/commit/0434725))
- **packages/stdlib:** Math/clamp util ([8515bff](https://github.com/robonen/tools/commit/8515bff))
- **packages/stdlib:** MapRange util ([d8a9a62](https://github.com/robonen/tools/commit/d8a9a62))
- **packages/stdlib:** Levenshtein distance util ([0022153](https://github.com/robonen/tools/commit/0022153))
- **packages/stdlib:** Add trigram distance utill ([5045852](https://github.com/robonen/tools/commit/5045852))
### 🩹 Fixes
- **repo:** Workspaces -> workspace ([80b87d7](https://github.com/robonen/tools/commit/80b87d7))
### 💅 Refactors
- **repo:** Cleanup ([bc2ebfc](https://github.com/robonen/tools/commit/bc2ebfc))
- **packages/tsconfig:** Readme remove extra spaces ([565e7d8](https://github.com/robonen/tools/commit/565e7d8))
- **docs:** Drop docs cache and dist ([03f755d](https://github.com/robonen/tools/commit/03f755d))
- **repo:** Add vitepress to gitignore ([cf71b8e](https://github.com/robonen/tools/commit/cf71b8e))
- **repo:** Add pathe lib to cli tool ([d7a2d15](https://github.com/robonen/tools/commit/d7a2d15))
- **packages/tsconfig:** Add description and publishConfig ([37d25bf](https://github.com/robonen/tools/commit/37d25bf))
- **repo:** Change cli generated exports in package.json ([a5d33ea](https://github.com/robonen/tools/commit/a5d33ea))
- **packages/tsconfig:** Disable declaration and source maps ([3f1d16b](https://github.com/robonen/tools/commit/3f1d16b))
- **packages/stdlib:** Add doc, update tests ([5280ace](https://github.com/robonen/tools/commit/5280ace))
- **packages/stdlib:** Add comments for math utils ([65ba312](https://github.com/robonen/tools/commit/65ba312))
- **packages/stdlib:** Levensthein fn replate to module export ([92721b3](https://github.com/robonen/tools/commit/92721b3))
- **packages/stdlib:** Rename arguments to left and right ([7d8f5be](https://github.com/robonen/tools/commit/7d8f5be))
- **packages/stdlib:** Reformat test files ([9031430](https://github.com/robonen/tools/commit/9031430))
- **packages/tsconfig:** Add exclude for .output and coverage folders ([769476d](https://github.com/robonen/tools/commit/769476d))
- **packages/stdlib:** Remove private from package.json ([5dadb50](https://github.com/robonen/tools/commit/5dadb50))
### 🏡 Chore
- **packages/stdlib:** Add bench script, add vscode workspace ([e9b8b0c](https://github.com/robonen/tools/commit/e9b8b0c))
- **release:** V0.0.1 ([725b73d](https://github.com/robonen/tools/commit/725b73d))
- **packages/stdlib:** Set 0.0.1 version ([c65113e](https://github.com/robonen/tools/commit/c65113e))
- **release:** V0.0.1 ([f77716a](https://github.com/robonen/tools/commit/f77716a))
### ✅ Tests
- **packages/stdlib:** Trigram distance tests ([4c10d38](https://github.com/robonen/tools/commit/4c10d38))
### ❤️ Contributors
- Robonen ([@robonen](http://github.com/robonen))

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
<div>
<img src="https://bage.robonen.ru/github?profile=robonen&project=tools&description=My%20most%20frequently%20used%20web%20tools">
</div>

View File

@@ -1,20 +1,57 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { defineCommand, runMain } from 'citty';
import { resolve } from 'pathe';
import { resolve } from 'node:path';
import { splitByCase } from 'scule';
const PACKAGE_MANAGER = 'pnpm@8.15.6';
const NODE_VERSION = '>=18.0.0';
const VITE_VERSION = '^5.2.8';
const VITE_DTS_VERSION = '^3.8.1';
const PATHE_VERSION = '^1.1.2'
async function getLatestPackageVersion(packageName: string) {
try {
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
const data = await response.json();
if (!response.ok) {
console.warn(`Failed to fetch latest version for ${packageName}, using fallback`);
return null;
}
const latestVersion = data['dist-tags']?.latest as string | undefined;
if (!latestVersion)
return null;
return {
version: latestVersion,
versionRange: `^${latestVersion}`,
};
} catch (error) {
console.warn(`Error fetching version for ${packageName}: ${error.message}`);
return null;
}
}
const PACKAGE_MANAGER_DEFAULT = 'pnpm@10.10.0';
const NODE_VERSION = '>=22.15.0';
const VITE_VERSION_DEFAULT = '^5.4.8';
const VITE_DTS_VERSION_DEFAULT = '^4.2.2';
const PATHE_VERSION_DEFAULT = '^1.1.2';
const DEFAULT_DIR = 'packages';
const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
const generatePackageJson = async (name: string, path: string, hasVite: boolean) => {
const [
packageManagerVersion,
viteVersion,
viteDtsVersion,
patheVersion,
] = await Promise.all([
getLatestPackageVersion('pnpm').then(v => v?.version || PACKAGE_MANAGER_DEFAULT),
hasVite ? getLatestPackageVersion('vite').then(v => v?.versionRange || VITE_VERSION_DEFAULT) : VITE_VERSION_DEFAULT,
hasVite ? getLatestPackageVersion('vite-plugin-dts').then(v => v?.versionRange || VITE_DTS_VERSION_DEFAULT) : VITE_DTS_VERSION_DEFAULT,
hasVite ? getLatestPackageVersion('pathe').then(v => v?.versionRange || PATHE_VERSION_DEFAULT) : PATHE_VERSION_DEFAULT,
]);
const data = {
name,
private: true,
version: '1.0.0',
version: '0.0.0',
license: 'UNLICENSED',
description: '',
keywords: [],
@@ -24,20 +61,17 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
url: 'git+https://github.com/robonen/tools.git',
directory: path,
},
packageManager: PACKAGE_MANAGER,
packageManager: `pnpm@${packageManagerVersion}`,
engines: {
node: NODE_VERSION,
},
type: 'module',
files: ['dist'],
main: './dist/index.umd.js',
module: './dist/index.js',
types: './dist/index.d.ts',
exports: {
'.': {
types: './dist/index.d.ts',
import: './dist/index.js',
require: './dist/index.umd.js',
types: './dist/index.d.ts',
},
},
scripts: {
@@ -51,9 +85,9 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
devDependencies: {
'@robonen/tsconfig': 'workspace:*',
...(hasVite && {
vite: VITE_VERSION,
'vite-plugin-dts': VITE_DTS_VERSION,
pathe: PATHE_VERSION,
vite: viteVersion,
'vite-plugin-dts': viteDtsVersion,
pathe: patheVersion,
}),
},
};
@@ -61,6 +95,16 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
return JSON.stringify(data, null, 2);
};
const generateJsrJson = (name: string) => {
const data = {
name,
version: '0.0.0',
exports: './src/index.ts',
};
return JSON.stringify(data, null, 2);
};
const generateViteConfig = () => `import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import { resolve } from 'pathe';
@@ -72,7 +116,10 @@ export default defineConfig({
},
},
plugins: [
dts({ insertTypesEntry: true }),
dts({
insertTypesEntry: true,
exclude: '**/*.test.ts',
}),
],
});
`;
@@ -119,13 +166,15 @@ const createCommand = defineCommand({
await mkdir(resolvedPath, { recursive: true });
writeFile(`${resolvedPath}/package.json`, generatePackageJson(args.name, path, hasVite));
writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig());
writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
const packageJson = await generatePackageJson(args.name, path, hasVite);
await writeFile(`${resolvedPath}/package.json`, packageJson);
await writeFile(`${resolvedPath}/jsr.json`, generateJsrJson(args.name));
await writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig());
await writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
if (hasVite) {
mkdir(`${resolvedPath}/src`, { recursive: true });
writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig());
await mkdir(`${resolvedPath}/src`, { recursive: true });
await writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig());
}
console.log(`Project created successfully`);

View File

@@ -31,7 +31,7 @@ pnpm install -D @robonen/tsconfig
"resolveJsonModule": true, // разрешить импортировать файлы JSON
"moduleDetection": "force", // заставляет TypeScript рассматривать все файлы как модули. Это помогает избежать ошибок cannot redeclare block-scoped variable»
"isolatedModules": true, // орабатывать каждый файл, как отдельный изолированный модуль
"removeComments": true, // удалять комментарии из исходного кода
"removeComments": false, // удалять комментарии из исходного кода
"verbatimModuleSyntax": true, // сохранять синтаксис модулей в исходном коде (важно при импорте типов)
"useDefineForClassFields": true, // использование классов стандарта TC39, а не TypeScript
"strict": true, // включить все строгие проверки (noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictPropertyInitialization)

View File

@@ -1,8 +1,7 @@
{
"name": "@robonen/tsconfig",
"private": true,
"version": "1.0.0",
"license": "UNLICENSED",
"version": "0.0.2",
"license": "Apache-2.0",
"description": "Base typescript configuration for projects",
"keywords": [
"tsconfig",
@@ -16,9 +15,9 @@
"url": "git+https://github.com/robonen/tools.git",
"directory": "packages/tsconfig"
},
"packageManager": "pnpm@8.15.6",
"packageManager": "pnpm@10.19.0",
"engines": {
"node": ">=18.0.0"
"node": ">=24.11.0"
},
"files": [
"**tsconfig.json"

View File

@@ -3,8 +3,9 @@
"display": "Base TypeScript Configuration",
"compilerOptions": {
/* Basic Options */
"module": "Preserve",
"module": "ESNext",
"noEmit": true,
"lib": ["ESNext"],
"moduleResolution": "Bundler",
"target": "ESNext",
"outDir": "dist",
@@ -12,11 +13,12 @@
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"removeComments": true,
"removeComments": false,
"verbatimModuleSyntax": true,
"useDefineForClassFields": true,
@@ -26,7 +28,7 @@
/* Library transpiling */
"declaration": true,
"composite": true,
// "composite": true,
"sourceMap": false,
"declarationMap": false
},

1
core/platform/README.md Normal file
View File

@@ -0,0 +1 @@
# @robonen/platform

View File

@@ -0,0 +1,16 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
entries: [
'src/browsers',
'src/multi',
],
clean: true,
declaration: true,
rollup: {
emitCJS: true,
esbuild: {
// minify: true,
},
},
});

7
core/platform/jsr.json Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@robonen/platform",
"license": "Apache-2.0",
"version": "0.0.2",
"exports": "./src/index.ts"
}

View File

@@ -0,0 +1,50 @@
{
"name": "@robonen/platform",
"version": "0.0.3",
"license": "Apache-2.0",
"description": "Platform dependent utilities for javascript development",
"keywords": [
"javascript",
"typescript",
"browser",
"platform",
"node",
"bun",
"deno"
],
"author": "Robonen Andrew <robonenandrew@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/robonen/tools.git",
"directory": "packages/platform"
},
"packageManager": "pnpm@10.19.0",
"engines": {
"node": ">=24.11.0"
},
"type": "module",
"files": [
"dist"
],
"exports": {
"./browsers": {
"types": "./dist/browsers.d.ts",
"import": "./dist/browsers.mjs",
"require": "./dist/browsers.cjs"
},
"./multi": {
"types": "./dist/multi.d.ts",
"import": "./dist/multi.mjs",
"require": "./dist/multi.cjs"
}
},
"scripts": {
"test": "vitest run",
"dev": "vitest dev",
"build": "unbuild"
},
"devDependencies": {
"@robonen/tsconfig": "workspace:*",
"unbuild": "catalog:"
}
}

View File

@@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { focusGuard, createGuardAttrs } from '.';
describe('focusGuard', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('initialize with the correct default namespace', () => {
const guard = focusGuard();
expect(guard.selector).toBe('data-focus-guard');
});
it('create focus guards in the DOM', () => {
const guard = focusGuard();
guard.createGuard();
const guards = document.querySelectorAll(`[${guard.selector}]`);
expect(guards.length).toBe(2);
guards.forEach((element) => {
expect(element.tagName).toBe('SPAN');
expect(element.getAttribute('tabindex')).toBe('0');
});
});
it('remove focus guards from the DOM correctly', () => {
const guard = focusGuard();
guard.createGuard();
guard.removeGuard();
const guards = document.querySelectorAll(`[${guard.selector}]`);
expect(guards.length).toBe(0);
});
it('reuse the same guards when calling createGuard multiple times', () => {
const guard = focusGuard();
guard.createGuard();
guard.createGuard();
guard.removeGuard();
const guards = document.querySelectorAll(`[${guard.selector}]`);
expect(guards.length).toBe(0);
});
it('allow custom namespaces', () => {
const namespace = 'custom-guard';
const guard = focusGuard(namespace);
guard.createGuard();
expect(guard.selector).toBe(`data-${namespace}`);
const guards = document.querySelectorAll(`[${guard.selector}]`);
expect(guards.length).toBe(2);
});
it('createGuardAttrs should create a valid guard element', () => {
const namespace = 'custom-guard';
const element = createGuardAttrs(namespace);
expect(element.tagName).toBe('SPAN');
expect(element.getAttribute(namespace)).toBe('');
expect(element.getAttribute('tabindex')).toBe('0');
expect(element.getAttribute('style')).toBe('outline: none; opacity: 0; pointer-events: none; position: fixed;');
});
});

View File

@@ -0,0 +1,50 @@
/**
* @name focusGuard
* @category Browsers
* @description Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior
*
* @param {string} namespace - The namespace to use for the guard attributes
* @returns {Object} - An object containing the selector, createGuard, and removeGuard functions
*
* @example
* const guard = focusGuard();
* guard.createGuard();
* guard.removeGuard();
*
* @example
* const guard = focusGuard('focus-guard');
* guard.createGuard();
* guard.removeGuard();
*
* @since 0.0.3
*/
export function focusGuard(namespace: string = 'focus-guard') {
const guardAttr = `data-${namespace}`;
const createGuard = () => {
const edges = document.querySelectorAll(`[${guardAttr}]`);
document.body.insertAdjacentElement('afterbegin', edges[0] ?? createGuardAttrs(guardAttr));
document.body.insertAdjacentElement('beforeend', edges[1] ?? createGuardAttrs(guardAttr));
};
const removeGuard = () => {
document.querySelectorAll(`[${guardAttr}]`).forEach((element) => element.remove());
};
return {
selector: guardAttr,
createGuard,
removeGuard,
};
}
export function createGuardAttrs(namespace: string) {
const element = document.createElement('span');
element.setAttribute(namespace, '');
element.setAttribute('tabindex', '0');
element.setAttribute('style', 'outline: none; opacity: 0; pointer-events: none; position: fixed;');
return element;
}

View File

@@ -0,0 +1 @@
export * from './focusGuard';

View File

@@ -0,0 +1,47 @@
export interface DebounceOptions {
/**
* Call the function on the leading edge of the timeout, instead of waiting for the trailing edge
*/
readonly immediate?: boolean;
/**
* Call the function on the trailing edge with the last used arguments.
* Result of call is from previous call
*/
readonly trailing?: boolean;
}
const DEFAULT_DEBOUNCE_OPTIONS: DebounceOptions = {
trailing: true,
}
export function debounce<FnArguments extends unknown[], FnReturn>(
fn: (...args: FnArguments) => PromiseLike<FnReturn> | FnReturn,
timeout: number = 20,
options: DebounceOptions = {},
) {
options = {
...DEFAULT_DEBOUNCE_OPTIONS,
...options,
};
if (!Number.isFinite(timeout) || timeout <= 0)
throw new TypeError('Debounce timeout must be a positive number');
// Last result for leading edge
let leadingValue: PromiseLike<FnReturn> | FnReturn;
// Debounce timeout id
let timeoutId: NodeJS.Timeout;
// Promises to be resolved when debounce is finished
let resolveList: Array<(value: unknown) => void> = [];
// State of currently resolving promise
let currentResolve: Promise<FnReturn>;
// Trailing call information
let trailingArgs: unknown[];
}

View File

@@ -0,0 +1,28 @@
// TODO: tests
/**
* @name _global
* @category Multi
* @description Global object that works in any environment
*
* @since 0.0.1
*/
export const _global =
typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: typeof self !== 'undefined'
? self
: undefined;
/**
* @name isClient
* @category Multi
* @description Check if the current environment is the client
*
* @since 0.0.1
*/
export const isClient = typeof window !== 'undefined' && typeof document !== 'undefined';

View File

@@ -0,0 +1,2 @@
export * from './global';
// export * from './debounce';

View File

@@ -1,3 +1,6 @@
{
"extends": "@robonen/tsconfig/tsconfig.json",
"compilerOptions": {
"lib": ["DOM"]
}
}

View File

@@ -0,0 +1,9 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
rollup: {
esbuild: {
// minify: true,
},
},
});

6
core/stdlib/jsr.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@robonen/stdlib",
"version": "0.0.7",
"exports": "./src/index.ts"
}

46
core/stdlib/package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "@robonen/stdlib",
"version": "0.0.7",
"license": "Apache-2.0",
"description": "A collection of tools, utilities, and helpers for TypeScript",
"keywords": [
"stdlib",
"utils",
"tools",
"helpers",
"math",
"algorithms",
"data-structures"
],
"author": "Robonen Andrew <robonenandrew@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/robonen/tools.git",
"directory": "packages/stdlib"
},
"packageManager": "pnpm@10.19.0",
"engines": {
"node": ">=24.11.0"
},
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"scripts": {
"test": "vitest run",
"dev": "vitest dev",
"build": "unbuild"
},
"devDependencies": {
"@robonen/tsconfig": "workspace:*",
"pathe": "catalog:",
"unbuild": "catalog:"
}
}

View File

@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import { cluster } from '.';
describe('cluster', () => {
it('cluster an array into subarrays of a specific size', () => {
const result = cluster([1, 2, 3, 4, 5, 6, 7, 8], 3);
expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7, 8]]);
});
it('handle arrays that are not perfectly divisible by the size', () => {
const result = cluster([1, 2, 3, 4, 5], 2);
expect(result).toEqual([[1, 2], [3, 4], [5]]);
});
it('return an array with each element in its own subarray if size is 1', () => {
const result = cluster([1, 2, 3, 4], 1);
expect(result).toEqual([[1], [2], [3], [4]]);
});
it('return an array with a single subarray if size is greater than the array length', () => {
const result = cluster([1, 2, 3], 5);
expect(result).toEqual([[1, 2, 3]]);
});
it('return an empty array if size is less than or equal to 0', () => {
const result = cluster([1, 2, 3, 4], -1);
expect(result).toEqual([]);
});
it('return an empty array if the input array is empty', () => {
const result = cluster([], 3);
expect(result).toEqual([]);
});
});

View File

@@ -0,0 +1,24 @@
/**
* @name cluster
* @category Arrays
* @description Cluster an array into subarrays of a specific size
*
* @param {Value[]} arr The array to cluster
* @param {number} size The size of each cluster
* @returns {Value[][]} The clustered array
*
* @example
* cluster([1, 2, 3, 4, 5, 6, 7, 8], 3) // => [[1, 2, 3], [4, 5, 6], [7, 8]]
*
* @example
* cluster([1, 2, 3, 4], -1) // => []
*
* @since 0.0.3
*/
export function cluster<Value>(arr: Value[], size: number): Value[][] {
if (size <= 0) return [];
const clusterLength = Math.ceil(arr.length / size);
return Array.from({ length: clusterLength }, (_, i) => arr.slice(i * size, i * size + size));
}

View File

@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { first } from '.';
describe('first', () => {
it('return the first element of a non-empty array', () => {
expect(first([1, 2, 3])).toBe(1);
expect(first(['a', 'b', 'c'])).toBe('a');
});
it('return undefined for an empty array without a default value', () => {
expect(first([])).toBeUndefined();
});
it('return the default value for an empty array with a default value', () => {
expect(first([], 42)).toBe(42);
expect(first([], 'default')).toBe('default');
});
it('return the first element even if a default value is provided', () => {
expect(first([1, 2, 3], 42)).toBe(1);
expect(first(['a', 'b', 'c'], 'default')).toBe('a');
});
});

View File

@@ -0,0 +1,20 @@
/**
* @name first
* @category Arrays
* @description Returns the first element of an array
*
* @param {Value[]} arr The array to get the first element from
* @param {Value} [defaultValue] The default value to return if the array is empty
* @returns {Value | undefined} The first element of the array, or the default value if the array is empty
*
* @example
* first([1, 2, 3]); // => 1
*
* @example
* first([]); // => undefined
*
* @since 0.0.3
*/
export function first<Value>(arr: Value[], defaultValue?: Value) {
return arr[0] ?? defaultValue;
}

View File

@@ -0,0 +1,5 @@
export * from './cluster';
export * from './first';
export * from './last';
export * from './sum';
export * from './unique';

View File

@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { last } from '.';
describe('last', () => {
it('return the last element of a non-empty array', () => {
expect(last([1, 2, 3, 4, 5])).toBe(5);
expect(last(['a', 'b', 'c'])).toBe('c');
});
it('return undefined if the array is empty and no default value is provided', () => {
expect(last([])).toBeUndefined();
});
it('return the default value for an empty array with a default value', () => {
expect(last([], 42)).toBe(42);
expect(last([], 'default')).toBe('default');
});
it('return the first element even if a default value is provided', () => {
expect(last([1, 2, 3], 42)).toBe(3);
expect(last(['a', 'b', 'c'], 'default')).toBe('c');
});
});

View File

@@ -0,0 +1,20 @@
/**
* @name last
* @section Arrays
* @description Gets the last element of an array
*
* @param {Value[]} arr The array to get the last element of
* @param {Value} [defaultValue] The default value to return if the array is empty
* @returns {Value | undefined} The last element of the array, or the default value if the array is empty
*
* @example
* last([1, 2, 3, 4, 5]); // => 5
*
* @example
* last([], 3); // => 3
*
* @since 0.0.3
*/
export function last<Value>(arr: Value[], defaultValue?: Value) {
return arr[arr.length - 1] ?? defaultValue;
}

View File

@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { sum } from '.';
describe('sum', () => {
it('return the sum of all elements in a number array', () => {
const result = sum([1, 2, 3, 4, 5]);
expect(result).toBe(15);
});
it('return 0 for an empty array', () => {
const result = sum([]);
expect(result).toBe(0);
});
it('return the sum of all elements using a getValue function', () => {
const result = sum([{ value: 1 }, { value: 2 }, { value: 3 }], (item) => item.value);
expect(result).toBe(6);
});
it('handle arrays with negative numbers', () => {
const result = sum([-1, -2, -3, -4, -5]);
expect(result).toBe(-15);
});
it('handle arrays with mixed positive and negative numbers', () => {
const result = sum([1, -2, 3, -4, 5]);
expect(result).toBe(3);
});
it('handle arrays with floating point numbers', () => {
const result = sum([1.5, 2.5, 3.5]);
expect(result).toBe(7.5);
});
it('handle arrays with a getValue function returning floating point numbers', () => {
const result = sum([{ value: 1.5 }, { value: 2.5 }, { value: 3.5 }], (item) => item.value);
expect(result).toBe(7.5);
});
});

View File

@@ -0,0 +1,26 @@
/**
* @name sum
* @category Arrays
* @description Returns the sum of all the elements in an array
*
* @param {Value[]} array - The array to sum
* @param {(item: Value) => number} [getValue] - A function that returns the value to sum from each element in the array
* @returns {number} The sum of all the elements in the array
*
* @example
* sum([1, 2, 3, 4, 5]) // => 15
*
* sum([{ value: 1 }, { value: 2 }, { value: 3 }], (item) => item.value) // => 6
*
* @since 0.0.3
*/
export function sum<Value extends number>(array: Value[]): number;
export function sum<Value>(array: Value[], getValue: (item: Value) => number): number;
export function sum<Value>(array: Value[], getValue?: (item: Value) => number): number {
// This check is necessary because the overload without the getValue argument
// makes tree-shaking based on argument types possible
if (!getValue)
return array.reduce((acc, item) => acc + (item as number), 0);
return array.reduce((acc, item) => acc + getValue(item), 0);
}

View File

@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import { unique } from '.';
describe('unique', () => {
it('return an array with unique numbers', () => {
const result = unique([1, 2, 3, 3, 4, 5, 5, 6]);
expect(result).toEqual([1, 2, 3, 4, 5, 6]);
});
it('return an array with unique objects based on id', () => {
const result = unique(
[{ id: 1 }, { id: 2 }, { id: 1 }],
(item) => item.id,
);
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
});
it('return the same array if all elements are unique', () => {
const result = unique([1, 2, 3, 4, 5]);
expect(result).toEqual([1, 2, 3, 4, 5]);
});
it('handle arrays with different types of values', () => {
const result = unique([1, '1', 2, '2', 2, 3, '3']);
expect(result).toEqual([1, '1', 2, '2', 3, '3']);
});
it('handle arrays with symbols', () => {
const sym1 = Symbol('a');
const sym2 = Symbol('b');
const result = unique([sym1, sym2, sym1]);
expect(result).toEqual([sym1, sym2]);
});
it('return an empty array when given an empty array', () => {
const result = unique([]);
expect(result).toEqual([]);
});
});

View File

@@ -0,0 +1,33 @@
export type UniqueKey = string | number | symbol;
export type Extractor<Value, Key extends UniqueKey> = (value: Value) => Key;
/**
* @name unique
* @category Arrays
* @description Returns a new array with unique values from the original array
*
* @param {Value[]} array - The array to filter
* @param {Function} [extractor] - The function to extract the value to compare
* @returns {Value[]} - The new array with unique values
*
* @example
* unique([1, 2, 3, 3, 4, 5, 5, 6]) //=> [1, 2, 3, 4, 5, 6]
*
* @example
* unique([{ id: 1 }, { id: 2 }, { id: 1 }], (a, b) => a.id === b.id) //=> [{ id: 1 }, { id: 2 }]
*
* @since 0.0.3
*/
export function unique<Value, Key extends UniqueKey>(
array: Value[],
extractor?: Extractor<Value, Key>,
) {
const values = new Map<Key, Value>();
for (const value of array) {
const key = extractor ? extractor(value) : value as any;
values.set(key, value);
}
return Array.from(values.values());
}

View File

@@ -0,0 +1,2 @@
export * from './sleep';
export * from './tryIt';

View File

@@ -0,0 +1,3 @@
export type AsyncPoolOptions = {
concurrency?: number;
}

View File

@@ -0,0 +1,38 @@
export interface RetryOptions {
times?: number;
delay?: number;
backoff: (options: RetryOptions & { count: number }) => number;
}
/**
* @name retry
* @category Async
* @description Retries a function a specified number of times with a delay between each retry
*
* @param {Promise<unknown>} fn - The function to retry
* @param {RetryOptions} options - The options for the retry
* @returns {Promise<unknown>} - The result of the function
*
* @example
* const result = await retry(() => {
* return fetch('https://jsonplaceholder.typicode.com/todos/1')
* .then(response => response.json())
* });
*
* @example
* const result = await retry(() => {
* return fetch('https://jsonplaceholder.typicode.com/todos/1')
* .then(response => response.json())
* }, { times: 3, delay: 1000 });
*
*/
export async function retry<Return>(
fn: () => Promise<Return>,
options: RetryOptions
) {
const {
times = 3,
} = options;
let count = 0;
}

View File

@@ -0,0 +1,19 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { sleep } from '.';
describe('sleep', () => {
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
});
it('delay execution by the specified amount of time', async () => {
const start = performance.now();
const delay = 100;
await sleep(delay);
const end = performance.now();
expect(end - start).toBeGreaterThan(delay - 5);
});
});

View File

@@ -0,0 +1,21 @@
/**
* @name sleep
* @category Async
* @description Delays the execution of the current function by the specified amount of time
*
* @param {number} ms - The amount of time to delay the execution of the current function
* @returns {Promise<void>} - A promise that resolves after the specified amount of time
*
* @example
* await sleep(1000);
*
* @example
* sleep(1000).then(() => {
* console.log('Hello, World!');
* });
*
* @since 0.0.3
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest';
import { tryIt } from '.';
describe('tryIt', () => {
it('handle synchronous functions without errors', () => {
const syncFn = (x: number) => x * 2;
const wrappedSyncFn = tryIt(syncFn);
const [error, result] = wrappedSyncFn(2);
expect(error).toBeUndefined();
expect(result).toBe(4);
});
it('handle synchronous functions with errors', () => {
const syncFn = (): void => { throw new Error('Test error') };
const wrappedSyncFn = tryIt(syncFn);
const [error, result] = wrappedSyncFn();
expect(error).toBeInstanceOf(Error);
expect(error?.message).toBe('Test error');
expect(result).toBeUndefined();
});
it('handle asynchronous functions without errors', async () => {
const asyncFn = async (x: number) => x * 2;
const wrappedAsyncFn = tryIt(asyncFn);
const [error, result] = await wrappedAsyncFn(2);
expect(error).toBeUndefined();
expect(result).toBe(4);
});
it('handle asynchronous functions with errors', async () => {
const asyncFn = async () => { throw new Error('Test error') };
const wrappedAsyncFn = tryIt(asyncFn);
const [error, result] = await wrappedAsyncFn();
expect(error).toBeInstanceOf(Error);
expect(error?.message).toBe('Test error');
expect(result).toBeUndefined();
});
it('handle promise-based functions without errors', async () => {
const promiseFn = (x: number) => Promise.resolve(x * 2);
const wrappedPromiseFn = tryIt(promiseFn);
const [error, result] = await wrappedPromiseFn(2);
expect(error).toBeUndefined();
expect(result).toBe(4);
});
it('handle promise-based functions with errors', async () => {
const promiseFn = () => Promise.reject(new Error('Test error'));
const wrappedPromiseFn = tryIt(promiseFn);
const [error, result] = await wrappedPromiseFn();
expect(error).toBeInstanceOf(Error);
expect(error?.message).toBe('Test error');
expect(result).toBeUndefined();
});
});

View File

@@ -0,0 +1,41 @@
import { isPromise } from '../../types';
export type TryItReturn<Return> = Return extends Promise<any>
? Promise<[Error, undefined] | [undefined, Awaited<Return>]>
: [Error, undefined] | [undefined, Return];
/**
* @name tryIt
* @category Async
* @description Wraps promise-based code in a try/catch block without forking the control flow
*
* @param {Function} fn - The function to try
* @returns {Function} - The function that will return a tuple with the error and the result
*
* @example
* const wrappedFetch = tryIt(fetch);
* const [error, result] = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1');
*
* @example
* const [error, result] = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1');
*
* @since 0.0.3
*/
export function tryIt<Args extends any[], Return>(
fn: (...args: Args) => Return,
) {
return (...args: Args): TryItReturn<Return> => {
try {
const result = fn(...args);
if (isPromise(result))
return result
.then((value) => [undefined, value])
.catch((error) => [error, undefined]) as TryItReturn<Return>;
return [undefined, result] as TryItReturn<Return>;
} catch (error) {
return [error, undefined] as TryItReturn<Return>;
}
};
}

View File

@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { flagsGenerator } from '.';
describe('flagsGenerator', () => {
it('generate unique flags', () => {
const generateFlag = flagsGenerator();
const flag1 = generateFlag();
const flag2 = generateFlag();
const flag3 = generateFlag();
expect(flag1).toBe(1);
expect(flag2).toBe(2);
expect(flag3).toBe(4);
});
it('throw an error if more than 31 flags are created', () => {
const generateFlag = flagsGenerator();
for (let i = 0; i < 31; i++) {
generateFlag();
}
expect(() => generateFlag()).toThrow(new RangeError('Cannot create more than 31 flags'));
});
});

View File

@@ -0,0 +1,22 @@
/**
* @name flagsGenerator
* @category Bits
* @description Create a function that generates unique flags
*
* @returns {Function} A function that generates unique flags
* @throws {RangeError} If more than 31 flags are created
*
* @since 0.0.2
*/
export function flagsGenerator() {
let lastFlag = 0;
return () => {
// 31 flags is the maximum number of flags that can be created
// (without zero) because of the 32-bit integer limit in bitwise operations
if (lastFlag & 0x40000000)
throw new RangeError('Cannot create more than 31 flags');
return (lastFlag = lastFlag === 0 ? 1 : lastFlag << 1);
};
}

View File

@@ -0,0 +1,95 @@
import { describe, it, expect } from 'vitest';
import { and, or, not, has, is, unset, toggle } from '.';
describe('flagsAnd', () => {
it('no effect on zero flags', () => {
const result = and();
expect(result).toBe(-1);
});
it('source flag is returned if no flags are provided', () => {
const result = and(0b1010);
expect(result).toBe(0b1010);
});
it('perform bitwise AND operation on flags', () => {
const result = and(0b1111, 0b1010, 0b1100);
expect(result).toBe(0b1000);
});
});
describe('flagsOr', () => {
it('no effect on zero flags', () => {
const result = or();
expect(result).toBe(0);
});
it('source flag is returned if no flags are provided', () => {
const result = or(0b1010);
expect(result).toBe(0b1010);
});
it('perform bitwise OR operation on flags', () => {
const result = or(0b1111, 0b1010, 0b1100);
expect(result).toBe(0b1111);
});
});
describe('flagsNot', () => {
it('perform bitwise NOT operation on a flag', () => {
const result = not(0b101);
expect(result).toBe(-0b110);
});
});
describe('flagsHas', () => {
it('check if a flag has a specific bit set', () => {
const result = has(0b1010, 0b1000);
expect(result).toBe(true);
});
it('check if a flag has a specific bit unset', () => {
const result = has(0b1010, 0b0100);
expect(result).toBe(false);
});
});
describe('flagsIs', () => {
it('check if a flag is set', () => {
const result = is(0b1010);
expect(result).toBe(true);
});
it('check if a flag is unset', () => {
const result = is(0);
expect(result).toBe(false);
});
});
describe('flagsUnset', () => {
it('unset a flag', () => {
const result = unset(0b1010, 0b1000);
expect(result).toBe(0b0010);
});
});
describe('flagsToggle', () => {
it('toggle a flag', () => {
const result = toggle(0b1010, 0b1000);
expect(result).toBe(0b0010);
});
});

View File

@@ -0,0 +1,100 @@
/**
* @name and
* @category Bits
* @description Function to combine multiple flags using the AND operator
*
* @param {number[]} flags - The flags to combine
* @returns {number} The combined flags
*
* @since 0.0.2
*/
export function and(...flags: number[]) {
return flags.reduce((acc, flag) => acc & flag, -1);
}
/**
* @name or
* @category Bits
* @description Function to combine multiple flags using the OR operator
*
* @param {number[]} flags - The flags to combine
* @returns {number} The combined flags
*
* @since 0.0.2
*/
export function or(...flags: number[]) {
return flags.reduce((acc, flag) => acc | flag, 0);
}
/**
* @name not
* @category Bits
* @description Function to combine multiple flags using the XOR operator
*
* @param {number} flag - The flag to apply the NOT operator to
* @returns {number} The result of the NOT operator
*
* @since 0.0.2
*/
export function not(flag: number) {
return ~flag;
}
/**
* @name has
* @category Bits
* @description Function to make sure a flag has a specific bit set
*
* @param {number} flag - The flag to check
* @param {number} other - Flag to check
* @returns {boolean} Whether the flag has the bit set
*
* @since 0.0.2
*/
export function has(flag: number, other: number) {
return (flag & other) === other;
}
/**
* @name is
* @category Bits
* @description Function to check if a flag is set
*
* @param {number} flag - The flag to check
* @returns {boolean} Whether the flag is set
*
* @since 0.0.2
*/
export function is(flag: number) {
return flag !== 0;
}
/**
* @name unset
* @category Bits
* @description Function to unset a flag
*
* @param {number} flag - Source flag
* @param {number} other - Flag to unset
* @returns {number} The new flag
*
* @since 0.0.2
*/
export function unset(flag: number, other: number) {
return flag & ~other;
}
/**
* @name toggle
* @category Bits
* @description Function to toggle (xor) a flag
*
* @param {number} flag - Source flag
* @param {number} other - Flag to toggle
* @returns {number} The new flag
*
* @since 0.0.2
*/
export function toggle(flag: number, other: number) {
return flag ^ other;
}

View File

@@ -0,0 +1 @@
export * from './flags';

View File

@@ -0,0 +1,63 @@
import { describe, it, expect } from 'vitest';
import { BitVector } from '.';
describe('BitVector', () => {
it('initialize with the correct size', () => {
const size = 16;
const expectedSize = Math.ceil(size / 8);
const bitVector = new BitVector(size);
expect(bitVector.length).toBe(expectedSize);
});
it('set and get bits correctly', () => {
const bitVector = new BitVector(16);
bitVector.setBit(5);
expect(bitVector.getBit(5)).toBe(true);
expect(bitVector.getBit(4)).toBe(false);
});
it('get out of bounds bits correctly', () => {
const bitVector = new BitVector(16);
expect(bitVector.getBit(155)).toBe(false);
});
it('clear bits correctly', () => {
const bitVector = new BitVector(16);
bitVector.setBit(5);
expect(bitVector.getBit(5)).toBe(true);
bitVector.clearBit(5);
expect(bitVector.getBit(5)).toBe(false);
});
it('find the previous bit correctly', () => {
const bitVector = new BitVector(100);
const indices = [99, 88, 66, 65, 64, 63, 15, 14, 1, 0];
const result = [];
indices.forEach(index => bitVector.setBit(index));
for (let i = bitVector.previousBit(100); i !== -1; i = bitVector.previousBit(i)) {
result.push(i);
}
expect(result).toEqual(indices);
});
it('return -1 when no previous bit is found', () => {
const bitVector = new BitVector(16);
expect(bitVector.previousBit(0)).toBe(-1);
});
it('throw RangeError when previousBit is called with an unreachable value', () => {
const bitVector = new BitVector(16);
bitVector.setBit(5);
expect(() => bitVector.previousBit(24)).toThrow(new RangeError('Unreachable value'));
});
});

View File

@@ -0,0 +1,61 @@
export interface BitVector {
getBit(index: number): boolean;
setBit(index: number): void;
clearBit(index: number): void;
previousBit(index: number): number;
}
/**
* @name BitVector
* @category Bits
* @description A bit vector is a vector of bits that can be used to store a collection of bits
*
* @since 0.0.3
*/
export class BitVector extends Uint8Array implements BitVector {
constructor(size: number) {
super(Math.ceil(size / 8));
}
getBit(index: number) {
const value = this[index >> 3]! & (1 << (index & 7));
return value !== 0;
}
setBit(index: number) {
this[index >> 3]! |= 1 << (index & 7);
}
clearBit(index: number): void {
this[index >> 3]! &= ~(1 << (index & 7));
}
previousBit(index: number): number {
while (index !== ((index >> 3) << 3)) {
--index;
if (this.getBit(index)) {
return index;
}
}
let byteIndex = (index >> 3) - 1;
while (byteIndex >= 0 && this[byteIndex] === 0)
--byteIndex;
if (byteIndex < 0)
return -1;
index = (byteIndex << 3) + 7;
while (index >= (byteIndex << 3)) {
if (this.getBit(index))
return index;
--index;
}
throw new RangeError('Unreachable value');
}
}

View File

@@ -0,0 +1,34 @@
import { type Collection, type Path } from '../../types';
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
K extends keyof O
? O[K]
: K extends keyof NonNullable<O>
? NonNullable<O>[K]
: never;
export type ExtractFromArray<A extends readonly any[], K> =
any[] extends A
? A extends readonly (infer T)[]
? T | undefined
: undefined
: K extends keyof A
? A[K]
: undefined;
export type ExtractFromCollection<O, K> =
K extends []
? O
: K extends [infer Key, ...infer Rest]
? O extends Record<PropertyKey, unknown>
? ExtractFromCollection<ExtractFromObject<O, Key>, Rest>
: O extends readonly any[]
? ExtractFromCollection<ExtractFromArray<O, Key>, Rest>
: never
: never;
type Get<O, K> = ExtractFromCollection<O, Path<K>>;
export function get<O extends Collection, K extends string>(obj: O, path: K) {
return path.split('.').reduce((acc, key) => (acc as any)?.[key], obj) as Get<O, K> | undefined;
}

View File

@@ -0,0 +1 @@
export * from './get';

11
core/stdlib/src/index.ts Normal file
View File

@@ -0,0 +1,11 @@
export * from './arrays';
export * from './async';
export * from './bits';
export * from './math';
export * from './objects';
export * from './patterns';
export * from './structs';
export * from './sync';
export * from './text';
export * from './types';
export * from './utils'

View File

@@ -43,4 +43,39 @@ describe('clamp', () => {
// negative range and value
expect(clamp(-10, -100, -5)).toBe(-10);
});
it('handle NaN and Infinity', () => {
// value is NaN
expect(clamp(NaN, 0, 100)).toBe(NaN);
// min is NaN
expect(clamp(50, NaN, 100)).toBe(NaN);
// max is NaN
expect(clamp(50, 0, NaN)).toBe(NaN);
// value is Infinity
expect(clamp(Infinity, 0, 100)).toBe(100);
// min is Infinity
expect(clamp(50, Infinity, 100)).toBe(100);
// max is Infinity
expect(clamp(50, 0, Infinity)).toBe(50);
// min and max are Infinity
expect(clamp(50, Infinity, Infinity)).toBe(Infinity);
// value is -Infinity
expect(clamp(-Infinity, 0, 100)).toBe(0);
// min is -Infinity
expect(clamp(50, -Infinity, 100)).toBe(50);
// max is -Infinity
expect(clamp(50, 0, -Infinity)).toBe(-Infinity);
// min and max are -Infinity
expect(clamp(50, -Infinity, -Infinity)).toBe(-Infinity);
});
});

View File

@@ -0,0 +1,15 @@
/**
* @name clamp
* @category Math
* @description Clamps a number between a minimum and maximum value
*
* @param {number} value The number to clamp
* @param {number} min Minimum value
* @param {number} max Maximum value
* @returns {number} The clamped number
*
* @since 0.0.1
*/
export function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}

View File

@@ -0,0 +1,61 @@
import {describe, it, expect} from 'vitest';
import {inverseLerp, lerp} from '.';
describe('lerp', () => {
it('interpolates between two values', () => {
const result = lerp(0, 10, 0.5);
expect(result).toBe(5);
});
it('returns start value when t is 0', () => {
const result = lerp(0, 10, 0);
expect(result).toBe(0);
});
it('returns end value when t is 1', () => {
const result = lerp(0, 10, 1);
expect(result).toBe(10);
});
it('handles negative interpolation values', () => {
const result = lerp(0, 10, -0.5);
expect(result).toBe(-5);
});
it('handles interpolation values greater than 1', () => {
const result = lerp(0, 10, 1.5);
expect(result).toBe(15);
});
});
describe('inverseLerp', () => {
it('returns 0 when value is start', () => {
const result = inverseLerp(0, 10, 0);
expect(result).toBe(0);
});
it('returns 1 when value is end', () => {
const result = inverseLerp(0, 10, 10);
expect(result).toBe(1);
});
it('interpolates correctly between two values', () => {
const result = inverseLerp(0, 10, 5);
expect(result).toBe(0.5);
});
it('handles values less than start', () => {
const result = inverseLerp(0, 10, -5);
expect(result).toBe(-0.5);
});
it('handles values greater than end', () => {
const result = inverseLerp(0, 10, 15);
expect(result).toBe(1.5);
});
it('handles same start and end values', () => {
const result = inverseLerp(10, 10, 10);
expect(result).toBe(0);
});
});

View File

@@ -0,0 +1,31 @@
/**
* @name lerp
* @category Math
* @description Linearly interpolates between two values
*
* @param {number} start The start value
* @param {number} end The end value
* @param {number} t The interpolation value
* @returns {number} The interpolated value
*
* @since 0.0.2
*/
export function lerp(start: number, end: number, t: number) {
return start + t * (end - start);
}
/**
* @name inverseLerp
* @category Math
* @description Inverse linear interpolation between two values
*
* @param {number} start The start value
* @param {number} end The end value
* @param {number} value The value to interpolate
* @returns {number} The interpolated value
*
* @since 0.0.2
*/
export function inverseLerp(start: number, end: number, value: number) {
return start === end ? 0 : (value - start) / (end - start);
}

View File

@@ -0,0 +1,46 @@
import {describe, expect, it} from 'vitest';
import {remap} from '.';
describe('remap', () => {
it('map values from one range to another', () => {
// value at midpoint
expect(remap(5, 0, 10, 0, 100)).toBe(50);
// value at min
expect(remap(0, 0, 10, 0, 100)).toBe(0);
// value at max
expect(remap(10, 0, 10, 0, 100)).toBe(100);
// value outside range (below)
expect(remap(-5, 0, 10, 0, 100)).toBe(0);
// value outside range (above)
expect(remap(15, 0, 10, 0, 100)).toBe(100);
// value at midpoint of negative range
expect(remap(75, 50, 100, -50, 50)).toBe(0);
// value at midpoint of negative range
expect(remap(-25, -50, 0, 0, 100)).toBe(50);
});
it('handle floating-point numbers correctly', () => {
// floating-point value
expect(remap(3.5, 0, 10, 0, 100)).toBe(35);
// positive floating-point ranges
expect(remap(1.25, 0, 2.5, 0, 100)).toBe(50);
// negative floating-point value
expect(remap(-2.5, -5, 0, 0, 100)).toBe(50);
// negative floating-point ranges
expect(remap(-1.25, -2.5, 0, 0, 100)).toBe(50);
});
it('handle edge cases', () => {
// input range is zero (should return output min)
expect(remap(5, 0, 0, 0, 100)).toBe(0);
});
});

View File

@@ -0,0 +1,25 @@
import { clamp } from '../clamp';
import {inverseLerp, lerp} from '../lerp';
/**
* @name remap
* @category Math
* @description Map a value from one range to another
*
* @param {number} value The value to map
* @param {number} in_min The minimum value of the input range
* @param {number} in_max The maximum value of the input range
* @param {number} out_min The minimum value of the output range
* @param {number} out_max The maximum value of the output range
* @returns {number} The mapped value
*
* @since 0.0.1
*/
export function remap(value: number, in_min: number, in_max: number, out_min: number, out_max: number) {
if (in_min === in_max)
return out_min;
const clampedValue = clamp(value, in_min, in_max);
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
}

View File

@@ -0,0 +1,35 @@
import {describe, it, expect} from 'vitest';
import {clampBigInt} from '.';
describe('clampBigInt', () => {
it('clamp a value within the given range', () => {
// value < min
expect(clampBigInt(-10n, 0n, 100n)).toBe(0n);
// value > max
expect(clampBigInt(200n, 0n, 100n)).toBe(100n);
// value within range
expect(clampBigInt(50n, 0n, 100n)).toBe(50n);
// value at min
expect(clampBigInt(0n, 0n, 100n)).toBe(0n);
// value at max
expect(clampBigInt(100n, 0n, 100n)).toBe(100n);
// value at midpoint
expect(clampBigInt(50n, 100n, 100n)).toBe(100n);
});
it('handle edge cases', () => {
// all values are the same
expect(clampBigInt(5n, 5n, 5n)).toBe(5n);
// min > max
expect(clampBigInt(10n, 100n, 50n)).toBe(50n);
// negative range and value
expect(clampBigInt(-10n, -100n, -5n)).toBe(-10n);
});
});

View File

@@ -0,0 +1,18 @@
import {minBigInt} from '../minBigInt';
import {maxBigInt} from '../maxBigInt';
/**
* @name clampBigInt
* @category Math
* @description Clamps a bigint between a minimum and maximum value
*
* @param {bigint} value The number to clamp
* @param {bigint} min Minimum value
* @param {bigint} max Maximum value
* @returns {bigint} The clamped number
*
* @since 0.0.2
*/
export function clampBigInt(value: bigint, min: bigint, max: bigint) {
return minBigInt(maxBigInt(value, min), max);
}

View File

@@ -0,0 +1,83 @@
import {describe, it, expect} from 'vitest';
import {inverseLerpBigInt, lerpBigInt} from '.';
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
describe('lerpBigInt', () => {
it('interpolates between two bigint values', () => {
const result = lerpBigInt(0n, 10n, 0.5);
expect(result).toBe(5n);
});
it('returns start value when t is 0', () => {
const result = lerpBigInt(0n, 10n, 0);
expect(result).toBe(0n);
});
it('returns end value when t is 1', () => {
const result = lerpBigInt(0n, 10n, 1);
expect(result).toBe(10n);
});
it('handles negative interpolation values', () => {
const result = lerpBigInt(0n, 10n, -0.5);
expect(result).toBe(-5n);
});
it('handles interpolation values greater than 1', () => {
const result = lerpBigInt(0n, 10n, 1.5);
expect(result).toBe(15n);
});
});
describe('inverseLerpBigInt', () => {
it('returns 0 when value is start', () => {
const result = inverseLerpBigInt(0n, 10n, 0n);
expect(result).toBe(0);
});
it('returns 1 when value is end', () => {
const result = inverseLerpBigInt(0n, 10n, 10n);
expect(result).toBe(1);
});
it('interpolates correctly between two bigint values', () => {
const result = inverseLerpBigInt(0n, 10n, 5n);
expect(result).toBe(0.5);
});
it('handles values less than start', () => {
const result = inverseLerpBigInt(0n, 10n, -5n);
expect(result).toBe(-0.5);
});
it('handles values greater than end', () => {
const result = inverseLerpBigInt(0n, 10n, 15n);
expect(result).toBe(1.5);
});
it('handles same start and end values', () => {
const result = inverseLerpBigInt(10n, 10n, 10n);
expect(result).toBe(0);
});
it('handles the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
expect(result).toBe(1);
});
it('handles values just above the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, 0n);
expect(result).toBe(0);
});
it('handles values just below the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);
expect(result).toBe(1);
});
it('handles values just above the maximum safe integer correctly', () => {
const result = inverseLerpBigInt(0n, 2n ** 128n, 2n ** 127n);
expect(result).toBe(0.5);
});
});

View File

@@ -0,0 +1,31 @@
/**
* @name lerpBigInt
* @category Math
* @description Linearly interpolates between bigint values
*
* @param {bigint} start The start value
* @param {bigint} end The end value
* @param {number} t The interpolation value
* @returns {bigint} The interpolated value
*
* @since 0.0.2
*/
export function lerpBigInt(start: bigint, end: bigint, t: number) {
return start + ((end - start) * BigInt(t * 10000)) / 10000n;
}
/**
* @name inverseLerpBigInt
* @category Math
* @description Inverse linear interpolation between two bigint values
*
* @param {bigint} start The start value
* @param {bigint} end The end value
* @param {bigint} value The value to interpolate
* @returns {number} The interpolated value
*
* @since 0.0.2
*/
export function inverseLerpBigInt(start: bigint, end: bigint, value: bigint) {
return start === end ? 0 : Number((value - start) * 10000n / (end - start)) / 10000;
}

View File

@@ -0,0 +1,39 @@
import { describe, it, expect } from 'vitest';
import { maxBigInt } from '.';
describe('maxBigInt', () => {
it('returns -Infinity when no values are provided', () => {
expect(() => maxBigInt()).toThrow(new TypeError('maxBigInt requires at least one argument'));
});
it('returns the largest value from a list of positive bigints', () => {
const result = maxBigInt(10n, 20n, 5n, 15n);
expect(result).toBe(20n);
});
it('returns the largest value from a list of negative bigints', () => {
const result = maxBigInt(-10n, -20n, -5n, -15n);
expect(result).toBe(-5n);
});
it('returns the largest value from a list of mixed positive and negative bigints', () => {
const result = maxBigInt(10n, -20n, 5n, -15n);
expect(result).toBe(10n);
});
it('returns the value itself when only one bigint is provided', () => {
const result = maxBigInt(10n);
expect(result).toBe(10n);
});
it('returns the largest value when all values are the same', () => {
const result = maxBigInt(10n, 10n, 10n);
expect(result).toBe(10n);
});
it('handles a large number of bigints', () => {
const values = Array.from({ length: 1000 }, (_, i) => BigInt(i));
const result = maxBigInt(...values);
expect(result).toBe(999n);
});
});

View File

@@ -0,0 +1,17 @@
/**
* @name maxBigInt
* @category Math
* @description Like `Math.max` but for BigInts
*
* @param {...bigint} values The values to compare
* @returns {bigint} The largest value
* @throws {TypeError} If no arguments are provided
*
* @since 0.0.2
*/
export function maxBigInt(...values: bigint[]) {
if (!values.length)
throw new TypeError('maxBigInt requires at least one argument');
return values.reduce((acc, val) => val > acc ? val : acc);
}

View File

@@ -0,0 +1,39 @@
import {describe, it, expect} from 'vitest';
import {minBigInt} from '.';
describe('minBigInt', () => {
it('returns Infinity when no values are provided', () => {
expect(() => minBigInt()).toThrow(new TypeError('minBigInt requires at least one argument'));
});
it('returns the smallest value from a list of positive bigints', () => {
const result = minBigInt(10n, 20n, 5n, 15n);
expect(result).toBe(5n);
});
it('returns the smallest value from a list of negative bigints', () => {
const result = minBigInt(-10n, -20n, -5n, -15n);
expect(result).toBe(-20n);
});
it('returns the smallest value from a list of mixed positive and negative bigints', () => {
const result = minBigInt(10n, -20n, 5n, -15n);
expect(result).toBe(-20n);
});
it('returns the value itself when only one bigint is provided', () => {
const result = minBigInt(10n);
expect(result).toBe(10n);
});
it('returns the smallest value when all values are the same', () => {
const result = minBigInt(10n, 10n, 10n);
expect(result).toBe(10n);
});
it('handles a large number of bigints', () => {
const values = Array.from({length: 1000}, (_, i) => BigInt(i));
const result = minBigInt(...values);
expect(result).toBe(0n);
});
});

View File

@@ -0,0 +1,17 @@
/**
* @name minBigInt
* @category Math
* @description Like `Math.min` but for BigInts
*
* @param {...bigint} values The values to compare
* @returns {bigint} The smallest value
* @throws {TypeError} If no arguments are provided
*
* @since 0.0.2
*/
export function minBigInt(...values: bigint[]) {
if (!values.length)
throw new TypeError('minBigInt requires at least one argument');
return values.reduce((acc, val) => val < acc ? val : acc);
}

View File

@@ -0,0 +1,32 @@
import {describe, expect, it} from 'vitest';
import {remapBigInt} from '.';
describe('remapBigInt', () => {
it('map values from one range to another', () => {
// value at midpoint
expect(remapBigInt(5n, 0n, 10n, 0n, 100n)).toBe(50n);
// value at min
expect(remapBigInt(0n, 0n, 10n, 0n, 100n)).toBe(0n);
// value at max
expect(remapBigInt(10n, 0n, 10n, 0n, 100n)).toBe(100n);
// value outside range (below)
expect(remapBigInt(-5n, 0n, 10n, 0n, 100n)).toBe(0n);
// value outside range (above)
expect(remapBigInt(15n, 0n, 10n, 0n, 100n)).toBe(100n);
// value at midpoint of negative range
expect(remapBigInt(75n, 50n, 100n, -50n, 50n)).toBe(0n);
// value at midpoint of negative range
expect(remapBigInt(-25n, -50n, 0n, 0n, 100n)).toBe(50n);
});
it('handle edge cases', () => {
// input range is zero (should return output min)
expect(remapBigInt(5n, 0n, 0n, 0n, 100n)).toBe(0n);
});
});

View File

@@ -0,0 +1,25 @@
import { clampBigInt } from '../clampBigInt';
import {inverseLerpBigInt, lerpBigInt} from '../lerpBigInt';
/**
* @name remapBigInt
* @category Math
* @description Map a bigint value from one range to another
*
* @param {bigint} value The value to map
* @param {bigint} in_min The minimum value of the input range
* @param {bigint} in_max The maximum value of the input range
* @param {bigint} out_min The minimum value of the output range
* @param {bigint} out_max The maximum value of the output range
* @returns {bigint} The mapped value
*
* @since 0.0.1
*/
export function remapBigInt(value: bigint, in_min: bigint, in_max: bigint, out_min: bigint, out_max: bigint) {
if (in_min === in_max)
return out_min;
const clampedValue = clampBigInt(value, in_min, in_max);
return lerpBigInt(out_min, out_max, inverseLerpBigInt(in_min, in_max, clampedValue));
}

View File

@@ -0,0 +1,9 @@
export * from './basic/clamp';
export * from './basic/lerp';
export * from './basic/remap';
export * from './bigint/clampBigInt';
export * from './bigint/lerpBigInt';
export * from './bigint/maxBigInt';
export * from './bigint/minBigInt';
export * from './bigint/remapBigInt';

View File

@@ -0,0 +1,2 @@
export * from './omit';
export * from './pick';

View File

@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest';
import { omit } from '.';
describe('omit', () => {
it('omit a single key from the object', () => {
const result = omit({ a: 1, b: 2, c: 3 }, 'a');
expect(result).toEqual({ b: 2, c: 3 });
});
it('omit multiple keys from the object', () => {
const result = omit({ a: 1, b: 2, c: 3 }, ['a', 'b']);
expect(result).toEqual({ c: 3 });
});
it('return the same object if no keys are omitted', () => {
const result = omit({ a: 1, b: 2, c: 3 }, []);
expect(result).toEqual({ a: 1, b: 2, c: 3 });
});
it('not modify the original object', () => {
const obj = { a: 1, b: 2, c: 3 };
const result = omit(obj, 'a');
expect(obj).toEqual({ a: 1, b: 2, c: 3 });
expect(result).toEqual({ b: 2, c: 3 });
});
it('handle an empty object', () => {
const result = omit({}, 'a' as any);
expect(result).toEqual({});
});
it('handle non-existent keys gracefully', () => {
const result = omit({ a: 1, b: 2, c: 3 } as Record<string, number>, 'd');
expect(result).toEqual({ a: 1, b: 2, c: 3 });
});
it('handle null gracefully', () => {
const emptyTarget = omit(null as any, 'a');
const emptyKeys = omit({ a: 1 }, null as any);
expect(emptyTarget).toEqual({});
expect(emptyKeys).toEqual({ a: 1 });
});
});

View File

@@ -0,0 +1,38 @@
import { isArray, type Arrayable } from '../../types';
/**
* @name omit
* @category Objects
* @description Returns a new object with the specified keys omitted
*
* @param {object} target - The object to omit keys from
* @param {Arrayable<keyof Target>} keys - The keys to omit
* @returns {Omit<Target, Key>} The new object with the specified keys omitted
*
* @example
* omit({ a: 1, b: 2, c: 3 }, 'a') // => { b: 2, c: 3 }
*
* @example
* omit({ a: 1, b: 2, c: 3 }, ['a', 'b']) // => { c: 3 }
*
* @since 0.0.3
*/
export function omit<Target extends object, Key extends keyof Target>(
target: Target,
keys: Arrayable<Key>
): Omit<Target, Key> {
const result = { ...target };
if (!target || !keys)
return result;
if (isArray(keys)) {
for (const key of keys) {
delete result[key];
}
} else {
delete result[keys];
}
return result;
}

View File

@@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest';
import { pick } from '.';
describe('pick', () => {
it('pick a single key', () => {
const result = pick({ a: 1, b: 2, c: 3 }, 'a');
expect(result).toEqual({ a: 1 });
});
it('pick multiple keys', () => {
const result = pick({ a: 1, b: 2, c: 3 }, ['a', 'b']);
expect(result).toEqual({ a: 1, b: 2 });
});
it('return an empty object when no keys are provided', () => {
const result = pick({ a: 1, b: 2 }, []);
expect(result).toEqual({});
});
it('handle non-existent keys by setting them to undefined', () => {
const result = pick({ a: 1, b: 2 }, ['a', 'c'] as any);
expect(result).toEqual({ a: 1, c: undefined });
});
it('return an empty object if target is null or undefined', () => {
const emptyTarget = pick(null as any, 'a');
const emptyKeys = pick({ a: 1 }, null as any);
expect(emptyTarget).toEqual({});
expect(emptyKeys).toEqual({});
});
});

View File

@@ -0,0 +1,38 @@
import { isArray, type Arrayable } from '../../types';
/**
* @name pick
* @category Objects
* @description Returns a partial copy of an object containing only the keys specified
*
* @param {object} target - The object to pick keys from
* @param {Arrayable<keyof Target>} keys - The keys to pick
* @returns {Pick<Target, Key>} The new object with the specified keys picked
*
* @example
* pick({ a: 1, b: 2, c: 3 }, 'a') // => { a: 1 }
*
* @example
* pick({ a: 1, b: 2, c: 3 }, ['a', 'b']) // => { a: 1, b: 2 }
*
* @since 0.0.3
*/
export function pick<Target extends object, Key extends keyof Target>(
target: Target,
keys: Arrayable<Key>
): Pick<Target, Key> {
const result = {} as Pick<Target, Key>;
if (!target || !keys)
return result;
if (isArray(keys)) {
for (const key of keys) {
result[key] = target[key];
}
} else {
result[keys] = target[keys];
}
return result;
}

View File

@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { PubSub } from '.';
describe('pubsub', () => {
const event3 = Symbol('event3');
let eventBus: PubSub<{
event1: (arg: string) => void;
event2: () => void;
[event3]: () => void;
}>;
beforeEach(() => {
eventBus = new PubSub();
});
it('add a listener and emit an event', () => {
const listener = vi.fn();
eventBus.on('event1', listener);
eventBus.emit('event1', 'Hello');
expect(listener).toHaveBeenCalledWith('Hello');
});
it('add multiple listeners and emit an event', () => {
const listener1 = vi.fn();
const listener2 = vi.fn();
eventBus.on('event1', listener1);
eventBus.on('event1', listener2);
eventBus.emit('event1', 'Hello');
expect(listener1).toHaveBeenCalledWith('Hello');
expect(listener2).toHaveBeenCalledWith('Hello');
});
it('emit symbol event', () => {
const listener = vi.fn();
eventBus.on(event3, listener);
eventBus.emit(event3);
expect(listener).toHaveBeenCalled();
});
it('add a one-time listener and emit an event', () => {
const listener = vi.fn();
eventBus.once('event1', listener);
eventBus.emit('event1', 'Hello');
eventBus.emit('event1', 'World');
expect(listener).toHaveBeenCalledWith('Hello');
expect(listener).not.toHaveBeenCalledWith('World');
});
it('add once listener and emit multiple events', () => {
const listener = vi.fn();
eventBus.once('event1', listener);
eventBus.emit('event1', 'Hello');
eventBus.emit('event1', 'World');
eventBus.emit('event1', '!');
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith('Hello');
});
it('remove a listener', () => {
const listener = vi.fn();
eventBus.on('event1', listener);
eventBus.off('event1', listener);
eventBus.emit('event1', 'Hello');
expect(listener).not.toHaveBeenCalled();
});
it('clear all listeners for an event', () => {
const listener1 = vi.fn();
const listener2 = vi.fn();
eventBus.on('event1', listener1);
eventBus.on('event1', listener2);
eventBus.clear('event1');
eventBus.emit('event1', 'Hello');
expect(listener1).not.toHaveBeenCalled();
expect(listener2).not.toHaveBeenCalled();
});
it('return true when emitting an event with listeners', () => {
const listener = vi.fn();
eventBus.on('event1', listener);
const result = eventBus.emit('event1', 'Hello');
expect(result).toBe(true);
});
it('return false when emitting an event without listeners', () => {
const result = eventBus.emit('event1', 'Hello');
expect(result).toBe(false);
});
it('calls listener only once when the same function is registered multiple times', () => {
const listener = vi.fn();
eventBus.on('event1', listener);
eventBus.on('event1', listener);
eventBus.on('event1', listener);
eventBus.emit('event1', 'Hello');
expect(listener).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,118 @@
import type { AnyFunction } from '../../../types';
export type Subscriber = AnyFunction;
export type EventHandlerMap = Record<PropertyKey, Subscriber>;
/**
* @name PubSub
* @category Patterns
* @description Simple PubSub implementation
*
* @since 0.0.2
*
* @template Events - Event map where all values are function types
*/
export class PubSub<Events extends EventHandlerMap> {
/**
* Events map
*
* @private
* @type {Map<keyof Events, Set<Events[keyof Events]>>}
*/
private events: Map<keyof Events, Set<Events[keyof Events]>>;
/**
* Creates an instance of PubSub
*/
constructor() {
this.events = new Map();
}
/**
* Subscribe to an event
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {Events[K]} listener Listener function
* @returns {this}
*/
public on<K extends keyof Events>(event: K, listener: Events[K]) {
const listeners = this.events.get(event);
if (listeners)
listeners.add(listener);
else
this.events.set(event, new Set([listener]));
return this;
}
/**
* Unsubscribe from an event
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {Events[K]} listener Listener function
* @returns {this}
*/
public off<K extends keyof Events>(event: K, listener: Events[K]) {
const listeners = this.events.get(event);
if (listeners)
listeners.delete(listener);
return this;
}
/**
* Subscribe to an event only once
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {Events[K]} listener Listener function
* @returns {this}
*/
public once<K extends keyof Events>(event: K, listener: Events[K]) {
const onceListener = (...args: Parameters<Events[K]>) => {
this.off(event, onceListener as Events[K]);
listener(...args);
};
this.on(event, onceListener as Events[K]);
return this;
}
/**
* Emit an event
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {...Parameters<Events[K]>} args Arguments for the listener
* @returns {boolean}
*/
public emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>) {
const listeners = this.events.get(event);
if (!listeners)
return false;
listeners.forEach((listener) => listener(...args));
return true;
}
/**
* Clear all listeners for an event
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @returns {this}
*/
public clear<K extends keyof Events>(event: K) {
this.events.delete(event);
return this;
}
}

View File

@@ -0,0 +1 @@
export * from './behavioral/pubsub';

View File

@@ -0,0 +1 @@
export * from './stack';

View File

@@ -0,0 +1,119 @@
import { describe, it, expect } from 'vitest';
import { Stack } from '.';
describe('stack', () => {
describe('constructor', () => {
it('create an empty stack if no initial values are provided', () => {
const stack = new Stack<number>();
expect(stack.length).toBe(0);
expect(stack.isEmpty).toBe(true);
});
it('create a stack with the provided initial values', () => {
const initialValues = [1, 2, 3];
const stack = new Stack(initialValues);
expect(stack.length).toBe(initialValues.length);
expect(stack.peek()).toBe(initialValues.at(-1));
});
it('create a stack with the provided initial value', () => {
const initialValue = 1;
const stack = new Stack(initialValue);
expect(stack.length).toBe(1);
expect(stack.peek()).toBe(initialValue);
});
it('create a stack with the provided options', () => {
const options = { maxSize: 5 };
const stack = new Stack<number>(undefined, options);
expect(stack.length).toBe(0);
expect(stack.isFull).toBe(false);
});
});
describe('push', () => {
it('push an element onto the stack', () => {
const stack = new Stack<number>();
stack.push(1);
expect(stack.length).toBe(1);
expect(stack.peek()).toBe(1);
});
it('throw an error if the stack is full', () => {
const options = { maxSize: 1 };
const stack = new Stack<number>(undefined, options);
stack.push(1);
expect(() => stack.push(2)).toThrow(new RangeError('Stack is full'));
});
});
describe('pop', () => {
it('pop an element from the stack', () => {
const stack = new Stack<number>([1, 2, 3]);
const poppedElement = stack.pop();
expect(poppedElement).toBe(3);
expect(stack.length).toBe(2);
});
it('return undefined if the stack is empty', () => {
const stack = new Stack<number>();
const poppedElement = stack.pop();
expect(poppedElement).toBeUndefined();
});
});
describe('peek', () => {
it('return the top element of the stack', () => {
const stack = new Stack<number>([1, 2, 3]);
const topElement = stack.peek();
expect(topElement).toBe(3);
expect(stack.length).toBe(3);
});
it('return undefined if the stack is empty', () => {
const stack = new Stack<number>();
const topElement = stack.peek();
expect(topElement).toBeUndefined();
});
});
describe('clear', () => {
it('clear the stack', () => {
const stack = new Stack<number>([1, 2, 3]);
stack.clear();
expect(stack.length).toBe(0);
expect(stack.isEmpty).toBe(true);
});
});
describe('iteration', () => {
it('iterate over the stack in LIFO order', () => {
const stack = new Stack<number>([1, 2, 3]);
const elements = [...stack];
expect(elements).toEqual([3, 2, 1]);
});
it('iterate over the stack asynchronously in LIFO order', async () => {
const stack = new Stack<number>([1, 2, 3]);
const elements: number[] = [];
for await (const element of stack) {
elements.push(element);
}
expect(elements).toEqual([3, 2, 1]);
});
});
});

View File

@@ -0,0 +1,152 @@
import { last } from '../../arrays';
import { isArray } from '../../types';
export type StackOptions = {
maxSize?: number;
};
/**
* @name Stack
* @category Data Structures
* @description Represents a stack data structure
*
* @since 0.0.2
*
* @template T The type of elements stored in the stack
*/
export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
/**
* The maximum number of elements that the stack can hold
*
* @private
* @type {number}
*/
private readonly maxSize: number;
/**
* The stack data structure
*
* @private
* @type {T[]}
*/
private readonly stack: T[];
/**
* Creates an instance of Stack
*
* @param {(T[] | T)} [initialValues] The initial values to add to the stack
* @param {StackOptions} [options] The options for the stack
* @memberof Stack
*/
constructor(initialValues?: T[] | T, options?: StackOptions) {
this.maxSize = options?.maxSize ?? Infinity;
this.stack = isArray(initialValues) ? initialValues : initialValues ? [initialValues] : [];
}
/**
* Gets the number of elements in the stack
* @returns {number} The number of elements in the stack
*/
public get length() {
return this.stack.length;
}
/**
* Checks if the stack is empty
* @returns {boolean} `true` if the stack is empty, `false` otherwise
*/
public get isEmpty() {
return this.stack.length === 0;
}
/**
* Checks if the stack is full
* @returns {boolean} `true` if the stack is full, `false` otherwise
*/
public get isFull() {
return this.stack.length === this.maxSize;
}
/**
* Pushes an element onto the stack
* @param {T} element The element to push onto the stack
* @returns {this}
* @throws {RangeError} If the stack is full
*/
public push(element: T) {
if (this.isFull)
throw new RangeError('Stack is full');
this.stack.push(element);
return this;
}
/**
* Pops an element from the stack
* @returns {T | undefined} The element popped from the stack
*/
public pop() {
return this.stack.pop();
}
/**
* Peeks at the top element of the stack
* @returns {T | undefined} The top element of the stack
*/
public peek() {
if (this.isEmpty)
return undefined;
return last(this.stack);
}
/**
* Clears the stack
*
* @returns {this}
*/
public clear() {
this.stack.length = 0;
return this;
}
/**
* Converts the stack to an array
*
* @returns {T[]}
*/
public toArray() {
return this.stack.toReversed();
}
/**
* Returns a string representation of the stack
*
* @returns {string}
*/
public toString() {
return this.toArray().toString();
}
/**
* Returns an iterator for the stack
*
* @returns {IterableIterator<T>}
*/
public [Symbol.iterator]() {
return this.toArray()[Symbol.iterator]();
}
/**
* Returns an async iterator for the stack
*
* @returns {AsyncIterableIterator<T>}
*/
public async *[Symbol.asyncIterator]() {
for (const element of this.toArray()) {
yield element;
}
}
}

View File

@@ -0,0 +1 @@
export * from './mutex';

View File

@@ -0,0 +1,94 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { SyncMutex } from '.';
describe('SyncMutex', () => {
let mutex: SyncMutex;
beforeEach(() => {
mutex = new SyncMutex();
});
it('unlocked by default', () => {
expect(mutex.isLocked).toBe(false);
});
it('lock the mutex', () => {
mutex.lock();
expect(mutex.isLocked).toBe(true);
});
it('remain locked when locked multiple times', () => {
mutex.lock();
mutex.lock();
expect(mutex.isLocked).toBe(true);
});
it('unlock a locked mutex', () => {
mutex.lock();
mutex.unlock();
expect(mutex.isLocked).toBe(false);
});
it('remain unlocked when unlocked multiple times', () => {
mutex.unlock();
mutex.unlock();
expect(mutex.isLocked).toBe(false);
});
it('reflect the current lock state', () => {
expect(mutex.isLocked).toBe(false);
mutex.lock();
expect(mutex.isLocked).toBe(true);
mutex.unlock();
expect(mutex.isLocked).toBe(false);
});
it('execute a callback when unlocked', async () => {
const callback = vi.fn(() => 'done');
const result = await mutex.execute(callback);
expect(result).toBe('done');
expect(callback).toHaveBeenCalled();
});
it('execute a promise callback when unlocked', async () => {
const callback = vi.fn(() => Promise.resolve('done'));
const result = await mutex.execute(callback);
expect(result).toBe('done');
expect(callback).toHaveBeenCalled();
});
it('execute concurrent callbacks only one at a time', async () => {
const callback = vi.fn(() => Promise.resolve('done'));
const result = await Promise.all([
mutex.execute(callback),
mutex.execute(callback),
mutex.execute(callback),
]);
expect(result).toEqual(['done', undefined, undefined]);
expect(callback).toHaveBeenCalledTimes(1);
});
it('does not execute a callback when locked', async () => {
const callback = vi.fn(() => 'done');
mutex.lock();
const result = await mutex.execute(callback);
expect(result).toBeUndefined();
expect(callback).not.toHaveBeenCalled();
});
it('unlocks after executing a callback', async () => {
const callback = vi.fn(() => 'done');
await mutex.execute(callback);
expect(mutex.isLocked).toBe(false);
});
});

View File

@@ -0,0 +1,47 @@
import type { MaybePromise } from "../../types";
/**
* @name SyncMutex
* @category Utils
* @description A simple synchronous mutex to provide more readable locking and unlocking of code blocks
*
* @example
* const mutex = new SyncMutex();
*
* mutex.lock();
*
* mutex.unlock();
*
* const result = await mutex.execute(() => {
* // do something
* return Promise.resolve('done');
* });
*
* @since 0.0.5
*/
export class SyncMutex {
private state = false;
public get isLocked() {
return this.state;
}
public lock() {
this.state = true;
}
public unlock() {
this.state = false;
}
public async execute<T>(callback: () => T) {
if (this.isLocked)
return;
this.lock();
const result = await callback();
this.unlock();
return result;
}
}

View File

@@ -0,0 +1,4 @@
export * from './levenshtein-distance';
export * from './trigram-distance';
// TODO: Template is not implemented yet
// export * from './template';

View File

@@ -1,15 +1,17 @@
/**
* Calculate the Levenshtein distance between two strings
* @name levenshteinDistance
* @category Text
* @description Calculate the Levenshtein distance between two strings
*
* @param {string} left First string
* @param {string} right Second string
* @returns {number} The Levenshtein distance between the two strings
*
* @since 0.0.1
*/
export function levenshteinDistance(left: string, right: string): number {
// If the strings are equal, the distance is 0
if (left === right) return 0;
// If either string is empty, the distance is the length of the other string
if (left.length === 0) return right.length;
if (right.length === 0) return left.length;

View File

@@ -0,0 +1,105 @@
import { describe, expectTypeOf, it } from 'vitest';
import type { ClearPlaceholder, ExtractPlaceholders } from './index';
describe.skip('template', () => {
describe('ClearPlaceholder', () => {
it('ignores strings without braces', () => {
type actual = ClearPlaceholder<'name'>;
type expected = 'name';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('removes all balanced braces from placeholders', () => {
type actual1 = ClearPlaceholder<'{name}'>;
type actual2 = ClearPlaceholder<'{{name}}'>;
type actual3 = ClearPlaceholder<'{{{name}}}'>;
type expected = 'name';
expectTypeOf<actual1>().toEqualTypeOf<expected>();
expectTypeOf<actual2>().toEqualTypeOf<expected>();
expectTypeOf<actual3>().toEqualTypeOf<expected>();
});
it('removes all unbalanced braces from placeholders', () => {
type actual1 = ClearPlaceholder<'{name}}'>;
type actual2 = ClearPlaceholder<'{{name}}}'>;
type expected = 'name';
expectTypeOf<actual1>().toEqualTypeOf<expected>();
expectTypeOf<actual2>().toEqualTypeOf<expected>();
});
});
describe('ExtractPlaceholders', () => {
it('string without placeholders', () => {
type actual = ExtractPlaceholders<'Hello name, how are?'>;
type expected = never;
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with one idexed placeholder', () => {
type actual = ExtractPlaceholders<'Hello {0}, how are you?'>;
type expected = '0';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with two indexed placeholders', () => {
type actual = ExtractPlaceholders<'Hello {0}, my name is {1}'>;
type expected = '0' | '1';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with one key placeholder', () => {
type actual = ExtractPlaceholders<'Hello {name}, how are you?'>;
type expected = 'name';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with two key placeholders', () => {
type actual = ExtractPlaceholders<'Hello {name}, my name is {managers.0.name}'>;
type expected = 'name' | 'managers.0.name';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with mixed placeholders', () => {
type actual = ExtractPlaceholders<'Hello {0}, how are you? My name is {1.name}'>;
type expected = '0' | '1.name';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with nested placeholder and balanced braces', () => {
type actual = ExtractPlaceholders<'Hello {{name}}, how are you?'>;
type expected = 'name';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with nested placeholder and unbalanced braces', () => {
type actual = ExtractPlaceholders<'Hello {{{name}, how are you?'>;
type expected = 'name';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with nested placeholders and balanced braces', () => {
type actual = ExtractPlaceholders<'Hello {{{name}{positions}}}, how are you?'>;
type expected = 'name' | 'positions';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
it('string with nested placeholders and unbalanced braces', () => {
type actual = ExtractPlaceholders<'Hello {{{name}{positions}, how are you?'>;
type expected = 'name' | 'positions';
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
});
});

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { templateObject } from '.';
describe.skip('templateObject', () => {
it('replace template placeholders with corresponding values from args', () => {
const template = 'Hello, {names.0}!';
const args = { names: ['John'] };
const result = templateObject(template, args);
expect(result).toBe('Hello, John!');
});
it('replace template placeholders with corresponding values from args', () => {
const template = 'Hello, {name}!';
const args = { name: 'John' };
const result = templateObject(template, args);
expect(result).toBe('Hello, John!');
});
it('replace template placeholders with fallback value if corresponding value is undefined', () => {
const template = 'Hello, {name}!';
const args = { age: 25 };
const fallback = 'Guest';
const result = templateObject(template, args, fallback);
expect(result).toBe('Hello, Guest!');
});
it(' replace template placeholders with fallback value returned by fallback function if corresponding value is undefined', () => {
const template = 'Hello, {name}!';
const args = { age: 25 };
const fallback = (key: string) => `Unknown ${key}`;
const result = templateObject(template, args, fallback);
expect(result).toBe('Hello, Unknown name!');
});
it('replace template placeholders with nested values from args', () => {
const result = templateObject('Hello {{user.name}, your address {user.addresses.0.street}', {
user: {
name: 'John Doe',
addresses: [
{ street: '123 Main St', city: 'Springfield'},
{ street: '456 Elm St', city: 'Shelbyville'}
]
}
});
expect(result).toBe('Hello {John Doe, your address 123 Main St');
});
});

View File

@@ -0,0 +1,73 @@
import { get } from '../../collections';
import { isFunction, type Path, type PathToType, type Stringable, type Trim, type UnionToIntersection } from '../../types';
/**
* Type of a value that will be used to replace a placeholder in a template.
*/
export type TemplateValue = Stringable | string;
/**
* Type of a fallback value when a template key is not found.
*/
export type TemplateFallback = string | ((key: string) => string);
/**
* Type of a template string with placeholders.
*/
const TEMPLATE_PLACEHOLDER = /\{\s*([^{}]+?)\s*\}/gm;
/**
* Removes the placeholder syntax from a template string.
*
* @example
* type Base = ClearPlaceholder<'{user.name}'>; // 'user.name'
* type Unbalanced = ClearPlaceholder<'{user.name'>; // 'user.name'
* type Spaces = ClearPlaceholder<'{ user.name }'>; // 'user.name'
*/
export type ClearPlaceholder<In extends string> =
In extends `${string}{${infer Template}`
? ClearPlaceholder<Template>
: In extends `${infer Template}}${string}`
? ClearPlaceholder<Template>
: Trim<In>;
/**
* Extracts all placeholders from a template string.
*
* @example
* type Base = ExtractPlaceholders<'Hello {user.name}, {user.addresses.0.street}'>; // 'user.name' | 'user.addresses.0.street'
*/
export type ExtractPlaceholders<In extends string> =
In extends `${infer Before}}${infer After}`
? Before extends `${string}{${infer Placeholder}`
? ClearPlaceholder<Placeholder> | ExtractPlaceholders<After>
: ExtractPlaceholders<After>
: never;
/**
* Generates a type for a template string with placeholders.
*
* @example
* type Base = GenerateTypes<'Hello {user.name}, your address {user.addresses.0.street}'>; // { user: { name: string; addresses: { 0: { street: string; }; }; }; }
* type WithTarget = GenerateTypes<'Hello {user.age}', number>; // { user: { age: number; }; }
*/
export type GenerateTypes<T extends string, Target = string> = UnionToIntersection<PathToType<Path<T>, Target>>;
export function templateObject<
T extends string,
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue>
>(template: T, args: A, fallback?: TemplateFallback) {
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
const value = get(args, key)?.toString();
return value !== undefined ? value : (isFunction(fallback) ? fallback(key) : '');
});
}
templateObject('Hello {user.name}, your address {user.addresses.0.city}', {
user: {
name: 'John',
addresses: [
{ city: 'Kolpa' },
],
},
});

View File

@@ -1,10 +1,14 @@
export type Trigrams = Map<string, number>;
/**
* Extracts trigrams from a text and returns a map of trigram to count
* @name trigramProfile
* @category Text
* @description Extracts trigrams from a text and returns a map of trigram to count
*
* @param {string} text The text to extract trigrams
* @returns {Trigrams} A map of trigram to count
*
* @since 0.0.1
*/
export function trigramProfile(text: string): Trigrams {
text = '\n\n' + text + '\n\n';
@@ -21,13 +25,17 @@ export function trigramProfile(text: string): Trigrams {
}
/**
* Calculates the trigram distance between two strings
* @name trigramDistance
* @category Text
* @description Calculates the trigram distance between two strings
*
* @param {Trigrams} left First text trigram profile
* @param {Trigrams} right Second text trigram profile
* @returns {number} The trigram distance between the two strings
*
* @since 0.0.1
*/
export function trigramDistance(left: Trigrams, right: Trigrams) {
export function trigramDistance(left: Trigrams, right: Trigrams): number {
let distance = -4;
let total = -4;

View File

@@ -0,0 +1,2 @@
export * from './js';
export * from './ts';

View File

@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { toString } from './casts';
describe('casts', () => {
describe('toString', () => {
it('correct string representation of a value', () => {
// Primitives
expect(toString(true)).toBe('[object Boolean]');
expect(toString(() => {})).toBe('[object Function]');
expect(toString(5)).toBe('[object Number]');
expect(toString(BigInt(5))).toBe('[object BigInt]');
expect(toString('hello')).toBe('[object String]');
expect(toString(Symbol('foo'))).toBe('[object Symbol]');
// Complex
expect(toString([])).toBe('[object Array]');
expect(toString({})).toBe('[object Object]');
expect(toString(undefined)).toBe('[object Undefined]');
expect(toString(null)).toBe('[object Null]');
expect(toString(/abc/)).toBe('[object RegExp]');
expect(toString(new Date())).toBe('[object Date]');
expect(toString(new Error())).toBe('[object Error]');
expect(toString(new Promise(() => {}))).toBe('[object Promise]');
expect(toString(new Map())).toBe('[object Map]');
expect(toString(new Set())).toBe('[object Set]');
expect(toString(new WeakMap())).toBe('[object WeakMap]');
expect(toString(new WeakSet())).toBe('[object WeakSet]');
});
});
});

View File

@@ -0,0 +1,11 @@
/**
* @name toString
* @category Types
* @description To string any value
*
* @param {any} value
* @returns {string}
*
* @since 0.0.2
*/
export const toString = (value: any): string => Object.prototype.toString.call(value);

View File

@@ -0,0 +1,183 @@
import { describe, expect, it } from 'vitest';
import { isArray, isObject, isRegExp, isDate, isError, isPromise, isMap, isSet, isWeakMap, isWeakSet } from './complex';
describe('complex', () => {
describe('isArray', () => {
it('true if the value is an array', () => {
expect(isArray([])).toBe(true);
expect(isArray([1, 2, 3])).toBe(true);
});
it('false if the value is not an array', () => {
expect(isArray('')).toBe(false);
expect(isArray(123)).toBe(false);
expect(isArray(true)).toBe(false);
expect(isArray(null)).toBe(false);
expect(isArray(undefined)).toBe(false);
expect(isArray({})).toBe(false);
expect(isArray(new Map())).toBe(false);
expect(isArray(new Set())).toBe(false);
});
});
describe('isObject', () => {
it('true if the value is an object', () => {
expect(isObject({})).toBe(true);
expect(isObject({ key: 'value' })).toBe(true);
});
it('false if the value is not an object', () => {
expect(isObject('')).toBe(false);
expect(isObject(123)).toBe(false);
expect(isObject(true)).toBe(false);
expect(isObject(null)).toBe(false);
expect(isObject(undefined)).toBe(false);
expect(isObject([])).toBe(false);
expect(isObject(new Map())).toBe(false);
expect(isObject(new Set())).toBe(false);
});
});
describe('isRegExp', () => {
it('true if the value is a regexp', () => {
expect(isRegExp(/test/)).toBe(true);
expect(isRegExp(new RegExp('test'))).toBe(true);
});
it('false if the value is not a regexp', () => {
expect(isRegExp('')).toBe(false);
expect(isRegExp(123)).toBe(false);
expect(isRegExp(true)).toBe(false);
expect(isRegExp(null)).toBe(false);
expect(isRegExp(undefined)).toBe(false);
expect(isRegExp([])).toBe(false);
expect(isRegExp({})).toBe(false);
expect(isRegExp(new Map())).toBe(false);
expect(isRegExp(new Set())).toBe(false);
});
});
describe('isDate', () => {
it('true if the value is a date', () => {
expect(isDate(new Date())).toBe(true);
});
it('false if the value is not a date', () => {
expect(isDate('')).toBe(false);
expect(isDate(123)).toBe(false);
expect(isDate(true)).toBe(false);
expect(isDate(null)).toBe(false);
expect(isDate(undefined)).toBe(false);
expect(isDate([])).toBe(false);
expect(isDate({})).toBe(false);
expect(isDate(new Map())).toBe(false);
expect(isDate(new Set())).toBe(false);
});
});
describe('isError', () => {
it('true if the value is an error', () => {
expect(isError(new Error())).toBe(true);
});
it('false if the value is not an error', () => {
expect(isError('')).toBe(false);
expect(isError(123)).toBe(false);
expect(isError(true)).toBe(false);
expect(isError(null)).toBe(false);
expect(isError(undefined)).toBe(false);
expect(isError([])).toBe(false);
expect(isError({})).toBe(false);
expect(isError(new Map())).toBe(false);
expect(isError(new Set())).toBe(false);
});
});
describe('isPromise', () => {
it('true if the value is a promise', () => {
expect(isPromise(new Promise(() => {}))).toBe(true);
});
it('false if the value is not a promise', () => {
expect(isPromise('')).toBe(false);
expect(isPromise(123)).toBe(false);
expect(isPromise(true)).toBe(false);
expect(isPromise(null)).toBe(false);
expect(isPromise(undefined)).toBe(false);
expect(isPromise([])).toBe(false);
expect(isPromise({})).toBe(false);
expect(isPromise(new Map())).toBe(false);
expect(isPromise(new Set())).toBe(false);
});
});
describe('isMap', () => {
it('true if the value is a map', () => {
expect(isMap(new Map())).toBe(true);
});
it('false if the value is not a map', () => {
expect(isMap('')).toBe(false);
expect(isMap(123)).toBe(false);
expect(isMap(true)).toBe(false);
expect(isMap(null)).toBe(false);
expect(isMap(undefined)).toBe(false);
expect(isMap([])).toBe(false);
expect(isMap({})).toBe(false);
expect(isMap(new Set())).toBe(false);
});
});
describe('isSet', () => {
it('true if the value is a set', () => {
expect(isSet(new Set())).toBe(true);
});
it('false if the value is not a set', () => {
expect(isSet('')).toBe(false);
expect(isSet(123)).toBe(false);
expect(isSet(true)).toBe(false);
expect(isSet(null)).toBe(false);
expect(isSet(undefined)).toBe(false);
expect(isSet([])).toBe(false);
expect(isSet({})).toBe(false);
expect(isSet(new Map())).toBe(false);
});
});
describe('isWeakMap', () => {
it('true if the value is a weakmap', () => {
expect(isWeakMap(new WeakMap())).toBe(true);
});
it('false if the value is not a weakmap', () => {
expect(isWeakMap('')).toBe(false);
expect(isWeakMap(123)).toBe(false);
expect(isWeakMap(true)).toBe(false);
expect(isWeakMap(null)).toBe(false);
expect(isWeakMap(undefined)).toBe(false);
expect(isWeakMap([])).toBe(false);
expect(isWeakMap({})).toBe(false);
expect(isWeakMap(new Map())).toBe(false);
expect(isWeakMap(new Set())).toBe(false);
});
});
describe('isWeakSet', () => {
it('true if the value is a weakset', () => {
expect(isWeakSet(new WeakSet())).toBe(true);
});
it('false if the value is not a weakset', () => {
expect(isWeakSet('')).toBe(false);
expect(isWeakSet(123)).toBe(false);
expect(isWeakSet(true)).toBe(false);
expect(isWeakSet(null)).toBe(false);
expect(isWeakSet(undefined)).toBe(false);
expect(isWeakSet([])).toBe(false);
expect(isWeakSet({})).toBe(false);
expect(isWeakSet(new Map())).toBe(false);
expect(isWeakSet(new Set())).toBe(false);
});
});
});

View File

@@ -0,0 +1,121 @@
import { toString } from '.';
/**
* @name isFunction
* @category Types
* @description Check if a value is an array
*
* @param {any} value
* @returns {value is any[]}
*
* @since 0.0.2
*/
export const isArray = (value: any): value is any[] => Array.isArray(value);
/**
* @name isObject
* @category Types
* @description Check if a value is an object
*
* @param {any} value
* @returns {value is object}
*
* @since 0.0.2
*/
export const isObject = (value: any): value is object => toString(value) === '[object Object]';
/**
* @name isRegExp
* @category Types
* @description Check if a value is a regexp
*
* @param {any} value
* @returns {value is RegExp}
*
* @since 0.0.2
*/
export const isRegExp = (value: any): value is RegExp => toString(value) === '[object RegExp]';
/**
* @name isDate
* @category Types
* @description Check if a value is a date
*
* @param {any} value
* @returns {value is Date}
*
* @since 0.0.2
*/
export const isDate = (value: any): value is Date => toString(value) === '[object Date]';
/**
* @name isError
* @category Types
* @description Check if a value is an error
*
* @param {any} value
* @returns {value is Error}
*
* @since 0.0.2
*/
export const isError = (value: any): value is Error => toString(value) === '[object Error]';
/**
* @name isPromise
* @category Types
* @description Check if a value is a promise
*
* @param {any} value
* @returns {value is Promise<any>}
*
* @since 0.0.2
*/
export const isPromise = (value: any): value is Promise<any> => toString(value) === '[object Promise]';
/**
* @name isMap
* @category Types
* @description Check if a value is a map
*
* @param {any} value
* @returns {value is Map<any, any>}
*
* @since 0.0.2
*/
export const isMap = (value: any): value is Map<any, any> => toString(value) === '[object Map]';
/**
* @name isSet
* @category Types
* @description Check if a value is a set
*
* @param {any} value
* @returns {value is Set<any>}
*
* @since 0.0.2
*/
export const isSet = (value: any): value is Set<any> => toString(value) === '[object Set]';
/**
* @name isWeakMap
* @category Types
* @description Check if a value is a weakmap
*
* @param {any} value
* @returns {value is WeakMap<object, any>}
*
* @since 0.0.2
*/
export const isWeakMap = (value: any): value is WeakMap<object, any> => toString(value) === '[object WeakMap]';
/**
* @name isWeakSet
* @category Types
* @description Check if a value is a weakset
*
* @param {any} value
* @returns {value is WeakSet<object>}
*
* @since 0.0.2
*/
export const isWeakSet = (value: any): value is WeakSet<object> => toString(value) === '[object WeakSet]';

View File

@@ -0,0 +1,3 @@
export * from './casts';
export * from './primitives';
export * from './complex';

View File

@@ -0,0 +1,101 @@
import { describe, expect, it } from 'vitest';
import { isBoolean, isFunction, isNumber, isBigInt, isString, isSymbol, isUndefined, isNull } from './primitives';
describe('primitives', () => {
describe('isBoolean', () => {
it('true if the value is a boolean', () => {
expect(isBoolean(true)).toBe(true);
expect(isBoolean(false)).toBe(true);
});
it('false if the value is not a boolean', () => {
expect(isBoolean(0)).toBe(false);
expect(isBoolean('true')).toBe(false);
expect(isBoolean(null)).toBe(false);
});
});
describe('isFunction', () => {
it('true if the value is a function', () => {
expect(isFunction(() => {})).toBe(true);
expect(isFunction(function() {})).toBe(true);
});
it('false if the value is not a function', () => {
expect(isFunction(123)).toBe(false);
expect(isFunction('function')).toBe(false);
expect(isFunction(null)).toBe(false);
});
});
describe('isNumber', () => {
it('true if the value is a number', () => {
expect(isNumber(123)).toBe(true);
expect(isNumber(3.14)).toBe(true);
});
it('false if the value is not a number', () => {
expect(isNumber('123')).toBe(false);
expect(isNumber(null)).toBe(false);
});
});
describe('isBigInt', () => {
it('true if the value is a bigint', () => {
expect(isBigInt(BigInt(123))).toBe(true);
});
it('false if the value is not a bigint', () => {
expect(isBigInt(123)).toBe(false);
expect(isBigInt('123')).toBe(false);
expect(isBigInt(null)).toBe(false);
});
});
describe('isString', () => {
it('true if the value is a string', () => {
expect(isString('hello')).toBe(true);
});
it('false if the value is not a string', () => {
expect(isString(123)).toBe(false);
expect(isString(null)).toBe(false);
});
});
describe('isSymbol', () => {
it('true if the value is a symbol', () => {
expect(isSymbol(Symbol())).toBe(true);
});
it('false if the value is not a symbol', () => {
expect(isSymbol(123)).toBe(false);
expect(isSymbol('symbol')).toBe(false);
expect(isSymbol(null)).toBe(false);
});
});
describe('isUndefined', () => {
it('true if the value is undefined', () => {
expect(isUndefined(undefined)).toBe(true);
});
it('false if the value is not undefined', () => {
expect(isUndefined(null)).toBe(false);
expect(isUndefined(123)).toBe(false);
expect(isUndefined('undefined')).toBe(false);
});
});
describe('isNull', () => {
it('true if the value is null', () => {
expect(isNull(null)).toBe(true);
});
it('false if the value is not null', () => {
expect(isNull(undefined)).toBe(false);
expect(isNull(123)).toBe(false);
expect(isNull('null')).toBe(false);
});
});
});

Some files were not shown because too many files have changed in this diff Show More