mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
Compare commits
342 Commits
v0.0.1
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d9bd6fea1 | ||
|
|
6b2707e24a | ||
|
|
da17d2d068 | ||
|
|
d9e9ee4e7f | ||
|
|
0b64e91eba | ||
| a83e2bb797 | |||
| 9bece480ca | |||
| c48de9a3d1 | |||
| 624e12ed96 | |||
| 3380d90cee | |||
| bb644579ca | |||
| e7d1021d27 | |||
| 1782184761 | |||
| 70d96b7f39 | |||
|
|
9587c92e50 | ||
| 678c18a08d | |||
| 68afec40b7 | |||
| 50b1498f3e | |||
| 7b5da22290 | |||
| 09fe8079c0 | |||
| ab9f45f908 | |||
| 49b9f2aa79 | |||
| 2a5412c3b8 | |||
| 5f9e0dc72d | |||
| 6565fa3de8 | |||
| 7dce7ed482 | |||
| df13f0b827 | |||
| 3da393ed08 | |||
| efadb5fe28 | |||
|
|
07e6d3eadc | ||
|
|
6fcc9d5a51 | ||
|
|
289d0d5af1 | ||
| 4bade839e7 | |||
|
|
c4321a2039 | ||
| f6b3bfbca6 | |||
|
|
7541e6aad4 | ||
|
|
a4d9b4c88a | ||
|
|
3b39f64734 | ||
|
|
6ab2d5cebf | ||
|
|
54f1facc4f | ||
|
|
717c41ef88 | ||
|
|
3747f5213e | ||
| daf18871a0 | |||
|
|
8bf9943e9e | ||
|
|
0e67715d9e | ||
|
|
3e43e4db3d | ||
| b8308e383c | |||
|
|
93c878cc35 | ||
| 7653975fa4 | |||
|
|
e2cb3f5a75 | ||
|
|
67fbad8930 | ||
|
|
e49c49e320 | ||
|
|
43cdc3b5e6 | ||
| a9a6c04176 | |||
| a6d3e8971f | |||
| 40dfdabd08 | |||
|
|
876a815fd3 | ||
| b1b9889ad2 | |||
|
|
9d2a393372 | ||
|
|
4071e49ad6 | ||
| 88bd87f9b0 | |||
|
|
ac265c05a8 | ||
| 69e5ebc085 | |||
| 48a85dbae2 | |||
| 0cfdce7456 | |||
| e035d1abca | |||
| 1851d5c80c | |||
| 48626a9fe5 | |||
| 04aa9e4721 | |||
| d55e3989f3 | |||
|
|
acee7e4167 | ||
|
|
a633bd8da0 | ||
| e194ba3883 | |||
|
|
d7c978bf9e | ||
|
|
5674095073 | ||
| 77bab6055c | |||
| 7fcafae467 | |||
| 52a5add405 | |||
| bd5fdab6a0 | |||
| e8d7cccfe0 | |||
| be13ec7079 | |||
| 55438b63f9 | |||
| 1e9859da83 | |||
|
|
aa8a0f00f3 | ||
|
|
e1e879ebbb | ||
|
|
6339b21f56 | ||
|
|
1d4f5c5512 | ||
| 2cc0efd556 | |||
| bef0aea14c | |||
| 40d1d6962b | |||
|
|
eb8514fe89 | ||
| b6200aa7a3 | |||
| a67322ca66 | |||
| 8297e47086 | |||
| 95e1bcd0c4 | |||
| 6d68246d16 | |||
| 049b5b351a | |||
| 890d984aad | |||
| 9d01b12160 | |||
| 6f2311afeb | |||
| f7312b1060 | |||
| 3d15f7b3b2 | |||
| 32bf20899f | |||
| 8355477e0e | |||
| 968cf26fd0 | |||
| 78fb4da82a | |||
| d55737df2f | |||
| 39ce28a5ef | |||
|
|
3d813d22b9 | ||
| 4f558270ce | |||
| 4d6922e06a | |||
| fa726eecc4 | |||
| c5f34efe05 | |||
| ead9c019cd | |||
| 8ee6970674 | |||
| 27c80d24ef | |||
| c596e8aa29 | |||
| f8b37cacd3 | |||
| 40d8194134 | |||
|
|
11d1ac232e | ||
| 7c1d801c8e | |||
| de391fa80d | |||
| 8ab58078ba | |||
| 88f6cec9b2 | |||
| 09e72d904c | |||
| 695647470b | |||
| b2beb6a5fc | |||
| c7048be9fb | |||
| 4ead7fb18c | |||
| 3994f349f4 | |||
| 8d6f08c332 | |||
| 3a2837c1a1 | |||
|
|
82a0c0f746 | ||
|
|
e8667d6a0a | ||
| 6ed7d39a11 | |||
|
|
74c170e853 | ||
| fa96b9ddee | |||
| ff4a88b896 | |||
|
|
871e0cfad2 | ||
|
|
849d444172 | ||
|
|
cea221ed57 | ||
|
|
49dacf071f | ||
| a07ac35db9 | |||
| 8c5252986e | |||
| fad1284cd3 | |||
| ca0a63ea38 | |||
| 7bfbb8e52a | |||
| 30b72fb2f0 | |||
| 5594cef31e | |||
| caa7c4221a | |||
| 6ae3c939d8 | |||
| 1bada217e9 | |||
|
|
c813bd174c | ||
|
|
a2f49b6286 | ||
|
|
987b8d4abd | ||
| c68436a36a | |||
|
|
cca2e2e798 | ||
| 2936c5a8d6 | |||
|
|
d6bc42d568 | ||
|
|
552b6afc54 | ||
|
|
2eb4665f4f | ||
|
|
4d5c05538a | ||
|
|
45bec99eb6 | ||
| 90dbdf4c88 | |||
| 41e3d90e41 | |||
| e93b1ccb68 | |||
| f88a466262 | |||
| 46ea487222 | |||
| 1823771b4a | |||
| 3e2b88d871 | |||
|
|
bb90892af9 | ||
| 4b91a425f7 | |||
| f49e85286c | |||
| 1b1a34c63b | |||
| a89723126e | |||
| 046ebbd172 | |||
| e4905ef87e | |||
| 50257463b7 | |||
| 979fd6e6df | |||
| bdc8fab071 | |||
| 0c87de4573 | |||
| f87ee85c0b | |||
| 9f2df92371 | |||
|
|
f385d497f4 | ||
| 276ba9736f | |||
| 9ccde9b040 | |||
| dd0f481e19 | |||
| 6408c1b328 | |||
| 23541a5476 | |||
| 3daa47dc83 | |||
| a1747ea535 | |||
|
|
cbffd80555 | ||
| 58cb287f93 | |||
|
|
17a7cbb936 | ||
| 96749c8510 | |||
| 3cc500f22b | |||
| fc774bc1af | |||
| c96213137e | |||
| 8080e7eafe | |||
| 53c969370a | |||
| 22fc55ce01 | |||
| 9b5eef04c7 | |||
| 5bc3dd5ee0 | |||
| 29d8aa086c | |||
| 85eb28a5dc | |||
| 759d418d88 | |||
| ff71cfffac | |||
| 5419b0a479 | |||
| 0faafc1b52 | |||
| 126bb7fa9d | |||
| 7c0dff595b | |||
| c350c977d5 | |||
| b6d5b5b92c | |||
| dc5e45acda | |||
| 9e7d7d8fdb | |||
| ed76a867e6 | |||
| 95bfa4f0f1 | |||
| cc439019e9 | |||
| 5722494458 | |||
| a5ba8ab13e | |||
| ae6154c4b6 | |||
| 0667f15f0c | |||
| 8989701303 | |||
| aff1a95c2f | |||
| 19d7a2ca76 | |||
| 6814b16d4d | |||
| 7b4f2d0c0a | |||
| 9cc8f08d43 | |||
| 29bbc6aa9c | |||
| b2b74d8e2d | |||
| 679bced9f1 | |||
| eadf791942 | |||
| d415e61ac0 | |||
| 4e798acfdd | |||
| 6a89239a75 | |||
| 4bbc3b45a2 | |||
| d48e6469a3 | |||
| 2e5e477097 | |||
| 658d180a8a | |||
| e41c78cd1d | |||
| e84187fa02 | |||
| b525d08363 | |||
| 2ff7196241 | |||
| 5a91cd264f | |||
| 11c099ab4a | |||
| 00fd5846aa | |||
| 8822325299 | |||
| 85fd7e34e0 | |||
|
|
fcdc4e251f | ||
| 0d7b1de1b2 | |||
| 9012929d86 | |||
| 4fd4008caa | |||
| 9978c09cec | |||
| a8292fa59c | |||
| 80db132e49 | |||
| f8a684e91a | |||
| 61c699381b | |||
| 307ec29787 | |||
| 11734b96b8 | |||
| f32deb3cc6 | |||
| 174e1b02d8 | |||
| dba020efab | |||
| 067f4d370f | |||
| 469ef8cdc2 | |||
| 975ca98f9a | |||
| 061d45f6fd | |||
| db7e35d152 | |||
| 9805b30c75 | |||
| 65312d007e | |||
|
|
746eae3a8e | ||
|
|
cf80ea4edd | ||
|
|
7ae3178277 | ||
|
|
1fabe0cf6a | ||
|
|
dd2cb68fc1 | ||
|
|
f848961a03 | ||
|
|
1a74a0eca4 | ||
|
|
7f32e50106 | ||
|
|
b3eacd8d99 | ||
| e243d55428 | |||
|
|
e4b6ec6384 | ||
|
|
d3f00e0c20 | ||
|
|
80378c46a1 | ||
|
|
b5dc1047af | ||
|
|
8201dd7331 | ||
|
|
b19ed7e60d | ||
| 4d52909804 | |||
| 4ce8babde2 | |||
| 220239400a | |||
| 5566bdcf80 | |||
| 93065d46ca | |||
| d9973af2ed | |||
| 107e192b33 | |||
| 34c72146e2 | |||
| d7c32f2f45 | |||
| 03245921da | |||
| c007a54522 | |||
| d0c74be856 | |||
| ba68e293b9 | |||
| 925af11be4 | |||
| 2a931bbeb9 | |||
|
|
fe3311bdfb | ||
| 2b5d81420a | |||
|
|
a6d40a4482 | ||
| dc48cbc44b | |||
| 6931fc6f18 | |||
| 7091352be2 | |||
| 30654d2fe6 | |||
| 8a33f6945c | |||
| aa10ed0f13 | |||
|
|
a9975bd06b | ||
|
|
8d64b012a1 | ||
| cafe245732 | |||
| 5105c2374c | |||
| 4b960a3ad3 | |||
| a5f62c16a3 | |||
| 26b2392f9b | |||
| f6a1e68d85 | |||
| cfcf0818ad | |||
|
|
4e92ed18ed | ||
| 69f9ae0900 | |||
|
|
18c3e16bb2 | ||
| 4afbe234f8 | |||
| b2923964e5 | |||
| 531e1721bb | |||
| 26a99b7d67 | |||
| 90328bf8d0 | |||
| 55440a83ba | |||
| 943e913e76 | |||
| c90c17db93 | |||
| 7546f2b653 | |||
| 784457a507 | |||
| 5bf6317673 | |||
| 80d8e37c03 | |||
| 8a5d063800 | |||
| bf9e811346 | |||
| 841d172598 | |||
| 43796ecae2 | |||
| e3ef3a693e | |||
| f987f722df | |||
| fb76d11725 | |||
| 7182706595 | |||
| f3a2ae53f4 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* @robonen
|
||||||
41
.github/workflows/ci.yaml
vendored
Normal file
41
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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@v6
|
||||||
|
|
||||||
|
- 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: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm test
|
||||||
78
.github/workflows/publish.yaml
vendored
Normal file
78
.github/workflows/publish.yaml
vendored
Normal 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@v6
|
||||||
|
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
1
.gitignore
vendored
@@ -17,6 +17,7 @@ node_modules
|
|||||||
.nuxt
|
.nuxt
|
||||||
.nitro
|
.nitro
|
||||||
.cache
|
.cache
|
||||||
|
cache
|
||||||
out
|
out
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -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/' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -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
3
README.md
Normal 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>
|
||||||
@@ -1,20 +1,57 @@
|
|||||||
import { mkdir, writeFile } from 'node:fs/promises';
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
import { defineCommand, runMain } from 'citty';
|
import { defineCommand, runMain } from 'citty';
|
||||||
import { resolve } from 'pathe';
|
import { resolve } from 'node:path';
|
||||||
import { splitByCase } from 'scule';
|
import { splitByCase } from 'scule';
|
||||||
|
|
||||||
const PACKAGE_MANAGER = 'pnpm@8.15.6';
|
async function getLatestPackageVersion(packageName: string) {
|
||||||
const NODE_VERSION = '>=18.0.0';
|
try {
|
||||||
const VITE_VERSION = '^5.2.8';
|
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
||||||
const VITE_DTS_VERSION = '^3.8.1';
|
const data = await response.json();
|
||||||
const PATHE_VERSION = '^1.1.2'
|
|
||||||
|
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 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 = {
|
const data = {
|
||||||
name,
|
name,
|
||||||
private: true,
|
private: true,
|
||||||
version: '1.0.0',
|
version: '0.0.0',
|
||||||
license: 'UNLICENSED',
|
license: 'UNLICENSED',
|
||||||
description: '',
|
description: '',
|
||||||
keywords: [],
|
keywords: [],
|
||||||
@@ -24,20 +61,17 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
|||||||
url: 'git+https://github.com/robonen/tools.git',
|
url: 'git+https://github.com/robonen/tools.git',
|
||||||
directory: path,
|
directory: path,
|
||||||
},
|
},
|
||||||
packageManager: PACKAGE_MANAGER,
|
packageManager: `pnpm@${packageManagerVersion}`,
|
||||||
engines: {
|
engines: {
|
||||||
node: NODE_VERSION,
|
node: NODE_VERSION,
|
||||||
},
|
},
|
||||||
type: 'module',
|
type: 'module',
|
||||||
files: ['dist'],
|
files: ['dist'],
|
||||||
main: './dist/index.umd.js',
|
|
||||||
module: './dist/index.js',
|
|
||||||
types: './dist/index.d.ts',
|
|
||||||
exports: {
|
exports: {
|
||||||
'.': {
|
'.': {
|
||||||
|
types: './dist/index.d.ts',
|
||||||
import: './dist/index.js',
|
import: './dist/index.js',
|
||||||
require: './dist/index.umd.js',
|
require: './dist/index.umd.js',
|
||||||
types: './dist/index.d.ts',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scripts: {
|
scripts: {
|
||||||
@@ -51,9 +85,9 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
|||||||
devDependencies: {
|
devDependencies: {
|
||||||
'@robonen/tsconfig': 'workspace:*',
|
'@robonen/tsconfig': 'workspace:*',
|
||||||
...(hasVite && {
|
...(hasVite && {
|
||||||
vite: VITE_VERSION,
|
vite: viteVersion,
|
||||||
'vite-plugin-dts': VITE_DTS_VERSION,
|
'vite-plugin-dts': viteDtsVersion,
|
||||||
pathe: PATHE_VERSION,
|
pathe: patheVersion,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -61,6 +95,16 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
|||||||
return JSON.stringify(data, null, 2);
|
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';
|
const generateViteConfig = () => `import { defineConfig } from 'vite';
|
||||||
import dts from 'vite-plugin-dts';
|
import dts from 'vite-plugin-dts';
|
||||||
import { resolve } from 'pathe';
|
import { resolve } from 'pathe';
|
||||||
@@ -72,7 +116,10 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
dts({ insertTypesEntry: true }),
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
exclude: '**/*.test.ts',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
`;
|
`;
|
||||||
@@ -119,13 +166,15 @@ const createCommand = defineCommand({
|
|||||||
|
|
||||||
await mkdir(resolvedPath, { recursive: true });
|
await mkdir(resolvedPath, { recursive: true });
|
||||||
|
|
||||||
writeFile(`${resolvedPath}/package.json`, generatePackageJson(args.name, path, hasVite));
|
const packageJson = await generatePackageJson(args.name, path, hasVite);
|
||||||
writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig());
|
await writeFile(`${resolvedPath}/package.json`, packageJson);
|
||||||
writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
|
await writeFile(`${resolvedPath}/jsr.json`, generateJsrJson(args.name));
|
||||||
|
await writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig());
|
||||||
|
await writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
|
||||||
|
|
||||||
if (hasVite) {
|
if (hasVite) {
|
||||||
mkdir(`${resolvedPath}/src`, { recursive: true });
|
await mkdir(`${resolvedPath}/src`, { recursive: true });
|
||||||
writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig());
|
await writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig());
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Project created successfully`);
|
console.log(`Project created successfully`);
|
||||||
54
configs/oxlint/README.md
Normal file
54
configs/oxlint/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# @robonen/oxlint
|
||||||
|
|
||||||
|
Composable [oxlint](https://oxc.rs/docs/guide/usage/linter.html) configuration presets.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install -D @robonen/oxlint oxlint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Create `oxlint.config.ts` in your project root:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
import { compose, base, typescript, vue, vitest, imports } from '@robonen/oxlint';
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
compose(base, typescript, vue, vitest, imports),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Append custom rules after presets to override them:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
compose(base, typescript, {
|
||||||
|
rules: { 'eslint/no-console': 'off' },
|
||||||
|
ignorePatterns: ['dist'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Presets
|
||||||
|
|
||||||
|
| Preset | Description |
|
||||||
|
| ------------ | -------------------------------------------------- |
|
||||||
|
| `base` | Core eslint, oxc, unicorn rules |
|
||||||
|
| `typescript` | TypeScript-specific rules (via overrides) |
|
||||||
|
| `vue` | Vue 3 Composition API / `<script setup>` rules |
|
||||||
|
| `vitest` | Test file rules (via overrides) |
|
||||||
|
| `imports` | Import rules (cycles, duplicates, ordering) |
|
||||||
|
| `node` | Node.js-specific rules |
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `compose(...configs: OxlintConfig[]): OxlintConfig`
|
||||||
|
|
||||||
|
Merges multiple configs into one:
|
||||||
|
|
||||||
|
- **plugins** — union (deduplicated)
|
||||||
|
- **rules / categories** — last wins
|
||||||
|
- **overrides / ignorePatterns** — concatenated
|
||||||
|
- **env / globals** — shallow merge
|
||||||
|
- **settings** — deep merge
|
||||||
4
configs/oxlint/oxlint.config.ts
Normal file
4
configs/oxlint/oxlint.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
import { compose, base, typescript, imports } from '@robonen/oxlint';
|
||||||
|
|
||||||
|
export default defineConfig(compose(base, typescript, imports));
|
||||||
53
configs/oxlint/package.json
Normal file
53
configs/oxlint/package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/oxlint",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "Composable oxlint configuration presets",
|
||||||
|
"keywords": [
|
||||||
|
"oxlint",
|
||||||
|
"oxc",
|
||||||
|
"linter",
|
||||||
|
"config",
|
||||||
|
"presets"
|
||||||
|
],
|
||||||
|
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
|
"directory": "configs/oxlint"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.29.3",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.13.1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"dev": "vitest dev",
|
||||||
|
"build": "tsdown"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"@robonen/tsdown": "workspace:*",
|
||||||
|
"oxlint": "catalog:",
|
||||||
|
"tsdown": "catalog:"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"oxlint": ">=1.0.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
103
configs/oxlint/src/compose.ts
Normal file
103
configs/oxlint/src/compose.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import type { OxlintConfig } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep merge two objects. Arrays are concatenated, objects are recursively merged.
|
||||||
|
*/
|
||||||
|
function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const result = { ...target };
|
||||||
|
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
const targetValue = target[key];
|
||||||
|
const sourceValue = source[key];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)
|
||||||
|
&& typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)
|
||||||
|
) {
|
||||||
|
result[key] = deepMerge(
|
||||||
|
targetValue as Record<string, unknown>,
|
||||||
|
sourceValue as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result[key] = sourceValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose multiple oxlint configurations into a single config.
|
||||||
|
*
|
||||||
|
* - `plugins` — union (deduplicated)
|
||||||
|
* - `categories` — later configs override earlier
|
||||||
|
* - `rules` — later configs override earlier
|
||||||
|
* - `overrides` — concatenated
|
||||||
|
* - `env` — merged (later overrides earlier)
|
||||||
|
* - `globals` — merged (later overrides earlier)
|
||||||
|
* - `settings` — deep-merged
|
||||||
|
* - `ignorePatterns` — concatenated
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { compose, base, typescript, vue } from '@robonen/oxlint';
|
||||||
|
* import { defineConfig } from 'oxlint';
|
||||||
|
*
|
||||||
|
* export default defineConfig(
|
||||||
|
* compose(base, typescript, vue, {
|
||||||
|
* rules: { 'eslint/no-console': 'off' },
|
||||||
|
* }),
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function compose(...configs: OxlintConfig[]): OxlintConfig {
|
||||||
|
const result: OxlintConfig = {};
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
// Plugins — union with dedup
|
||||||
|
if (config.plugins?.length) {
|
||||||
|
result.plugins = Array.from(new Set([...(result.plugins ?? []), ...config.plugins]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categories — shallow merge
|
||||||
|
if (config.categories) {
|
||||||
|
result.categories = { ...result.categories, ...config.categories };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules — shallow merge (later overrides earlier)
|
||||||
|
if (config.rules) {
|
||||||
|
result.rules = { ...result.rules, ...config.rules };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides — concatenate
|
||||||
|
if (config.overrides?.length) {
|
||||||
|
result.overrides = [...(result.overrides ?? []), ...config.overrides];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Env — shallow merge
|
||||||
|
if (config.env) {
|
||||||
|
result.env = { ...result.env, ...config.env };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globals — shallow merge
|
||||||
|
if (config.globals) {
|
||||||
|
result.globals = { ...result.globals, ...config.globals };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings — deep merge
|
||||||
|
if (config.settings) {
|
||||||
|
result.settings = deepMerge(
|
||||||
|
(result.settings ?? {}) as Record<string, unknown>,
|
||||||
|
config.settings as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore patterns — concatenate
|
||||||
|
if (config.ignorePatterns?.length) {
|
||||||
|
result.ignorePatterns = [...(result.ignorePatterns ?? []), ...config.ignorePatterns];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
17
configs/oxlint/src/index.ts
Normal file
17
configs/oxlint/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* Compose */
|
||||||
|
export { compose } from './compose';
|
||||||
|
|
||||||
|
/* Presets */
|
||||||
|
export { base, typescript, vue, vitest, imports, node } from './presets';
|
||||||
|
|
||||||
|
/* Types */
|
||||||
|
export type {
|
||||||
|
OxlintConfig,
|
||||||
|
OxlintOverride,
|
||||||
|
OxlintEnv,
|
||||||
|
OxlintGlobals,
|
||||||
|
AllowWarnDeny,
|
||||||
|
DummyRule,
|
||||||
|
DummyRuleMap,
|
||||||
|
RuleCategories,
|
||||||
|
} from './types';
|
||||||
73
configs/oxlint/src/presets/base.ts
Normal file
73
configs/oxlint/src/presets/base.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base configuration for any JavaScript/TypeScript project.
|
||||||
|
*
|
||||||
|
* Enables `correctness` category and opinionated rules from
|
||||||
|
* `eslint`, `oxc`, and `unicorn` plugins.
|
||||||
|
*/
|
||||||
|
export const base: OxlintConfig = {
|
||||||
|
plugins: ['eslint', 'oxc', 'unicorn'],
|
||||||
|
|
||||||
|
categories: {
|
||||||
|
correctness: 'error',
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
/* ── eslint core ──────────────────────────────────────── */
|
||||||
|
'eslint/eqeqeq': 'error',
|
||||||
|
'eslint/no-console': 'warn',
|
||||||
|
'eslint/no-debugger': 'error',
|
||||||
|
'eslint/no-eval': 'error',
|
||||||
|
'eslint/no-var': 'error',
|
||||||
|
'eslint/prefer-const': 'error',
|
||||||
|
'eslint/prefer-template': 'warn',
|
||||||
|
'eslint/no-useless-constructor': 'warn',
|
||||||
|
'eslint/no-useless-rename': 'warn',
|
||||||
|
'eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||||
|
'eslint/no-self-compare': 'error',
|
||||||
|
'eslint/no-template-curly-in-string': 'warn',
|
||||||
|
'eslint/no-throw-literal': 'error',
|
||||||
|
'eslint/no-return-assign': 'warn',
|
||||||
|
'eslint/no-else-return': 'warn',
|
||||||
|
'eslint/no-lonely-if': 'warn',
|
||||||
|
'eslint/no-unneeded-ternary': 'warn',
|
||||||
|
'eslint/prefer-object-spread': 'warn',
|
||||||
|
'eslint/prefer-exponentiation-operator': 'warn',
|
||||||
|
'eslint/no-useless-computed-key': 'warn',
|
||||||
|
'eslint/no-useless-concat': 'warn',
|
||||||
|
'eslint/curly': 'off',
|
||||||
|
|
||||||
|
/* ── unicorn ──────────────────────────────────────────── */
|
||||||
|
'unicorn/prefer-node-protocol': 'error',
|
||||||
|
'unicorn/no-instanceof-array': 'error',
|
||||||
|
'unicorn/no-new-array': 'error',
|
||||||
|
'unicorn/prefer-array-flat-map': 'warn',
|
||||||
|
'unicorn/prefer-array-flat': 'warn',
|
||||||
|
'unicorn/prefer-includes': 'warn',
|
||||||
|
'unicorn/prefer-string-slice': 'warn',
|
||||||
|
'unicorn/prefer-string-starts-ends-with': 'warn',
|
||||||
|
'unicorn/throw-new-error': 'error',
|
||||||
|
'unicorn/error-message': 'warn',
|
||||||
|
'unicorn/no-useless-spread': 'warn',
|
||||||
|
'unicorn/no-useless-undefined': 'off',
|
||||||
|
'unicorn/prefer-optional-catch-binding': 'warn',
|
||||||
|
'unicorn/prefer-type-error': 'warn',
|
||||||
|
'unicorn/no-thenable': 'error',
|
||||||
|
'unicorn/prefer-number-properties': 'warn',
|
||||||
|
'unicorn/prefer-global-this': 'warn',
|
||||||
|
|
||||||
|
/* ── oxc ──────────────────────────────────────────────── */
|
||||||
|
'oxc/no-accumulating-spread': 'warn',
|
||||||
|
'oxc/bad-comparison-sequence': 'error',
|
||||||
|
'oxc/bad-min-max-func': 'error',
|
||||||
|
'oxc/bad-object-literal-comparison': 'error',
|
||||||
|
'oxc/const-comparisons': 'error',
|
||||||
|
'oxc/double-comparisons': 'error',
|
||||||
|
'oxc/erasing-op': 'error',
|
||||||
|
'oxc/missing-throw': 'error',
|
||||||
|
'oxc/bad-bitwise-operator': 'error',
|
||||||
|
'oxc/bad-char-at-comparison': 'error',
|
||||||
|
'oxc/bad-replace-all-arg': 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
20
configs/oxlint/src/presets/imports.ts
Normal file
20
configs/oxlint/src/presets/imports.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import plugin rules for clean module boundaries.
|
||||||
|
*/
|
||||||
|
export const imports: OxlintConfig = {
|
||||||
|
plugins: ['import'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'import/no-duplicates': 'error',
|
||||||
|
'import/no-self-import': 'error',
|
||||||
|
'import/no-cycle': 'warn',
|
||||||
|
'import/first': 'warn',
|
||||||
|
'import/no-mutable-exports': 'error',
|
||||||
|
'import/no-amd': 'error',
|
||||||
|
'import/no-commonjs': 'warn',
|
||||||
|
'import/no-empty-named-blocks': 'warn',
|
||||||
|
'import/consistent-type-specifier-style': ['warn', 'prefer-top-level'],
|
||||||
|
},
|
||||||
|
};
|
||||||
6
configs/oxlint/src/presets/index.ts
Normal file
6
configs/oxlint/src/presets/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { base } from './base';
|
||||||
|
export { typescript } from './typescript';
|
||||||
|
export { vue } from './vue';
|
||||||
|
export { vitest } from './vitest';
|
||||||
|
export { imports } from './imports';
|
||||||
|
export { node } from './node';
|
||||||
17
configs/oxlint/src/presets/node.ts
Normal file
17
configs/oxlint/src/presets/node.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node.js-specific rules.
|
||||||
|
*/
|
||||||
|
export const node: OxlintConfig = {
|
||||||
|
plugins: ['node'],
|
||||||
|
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'node/no-exports-assign': 'error',
|
||||||
|
'node/no-new-require': 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
39
configs/oxlint/src/presets/typescript.ts
Normal file
39
configs/oxlint/src/presets/typescript.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript-specific rules.
|
||||||
|
*
|
||||||
|
* Applied via `overrides` for `*.ts`, `*.tsx`, `*.mts`, `*.cts` files.
|
||||||
|
*/
|
||||||
|
export const typescript: OxlintConfig = {
|
||||||
|
plugins: ['typescript'],
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
||||||
|
rules: {
|
||||||
|
'typescript/consistent-type-imports': 'error',
|
||||||
|
'typescript/no-explicit-any': 'off',
|
||||||
|
'typescript/no-non-null-assertion': 'off',
|
||||||
|
'typescript/prefer-as-const': 'error',
|
||||||
|
'typescript/no-empty-object-type': 'warn',
|
||||||
|
'typescript/no-wrapper-object-types': 'error',
|
||||||
|
'typescript/no-duplicate-enum-values': 'error',
|
||||||
|
'typescript/no-unsafe-declaration-merging': 'error',
|
||||||
|
'typescript/no-import-type-side-effects': 'error',
|
||||||
|
'typescript/no-useless-empty-export': 'warn',
|
||||||
|
'typescript/no-inferrable-types': 'warn',
|
||||||
|
'typescript/prefer-function-type': 'warn',
|
||||||
|
'typescript/ban-tslint-comment': 'error',
|
||||||
|
'typescript/consistent-type-definitions': ['warn', 'interface'],
|
||||||
|
'typescript/prefer-for-of': 'warn',
|
||||||
|
'typescript/no-unnecessary-type-constraint': 'warn',
|
||||||
|
'typescript/adjacent-overload-signatures': 'warn',
|
||||||
|
'typescript/array-type': ['warn', { default: 'array-simple' }],
|
||||||
|
'typescript/no-this-alias': 'error',
|
||||||
|
'typescript/triple-slash-reference': 'error',
|
||||||
|
'typescript/no-namespace': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
configs/oxlint/src/presets/vitest.ts
Normal file
35
configs/oxlint/src/presets/vitest.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitest rules for test files.
|
||||||
|
*
|
||||||
|
* Applied via `overrides` for common test file patterns.
|
||||||
|
*/
|
||||||
|
export const vitest: OxlintConfig = {
|
||||||
|
plugins: ['vitest'],
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/*.test.{ts,tsx,js,jsx}',
|
||||||
|
'**/*.spec.{ts,tsx,js,jsx}',
|
||||||
|
'**/test/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'**/__tests__/**/*.{ts,tsx,js,jsx}',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vitest/no-conditional-tests': 'warn',
|
||||||
|
'vitest/no-import-node-test': 'error',
|
||||||
|
'vitest/prefer-to-be-truthy': 'warn',
|
||||||
|
'vitest/prefer-to-be-falsy': 'warn',
|
||||||
|
'vitest/prefer-to-be-object': 'warn',
|
||||||
|
'vitest/prefer-to-have-length': 'warn',
|
||||||
|
'vitest/consistent-test-filename': 'warn',
|
||||||
|
'vitest/prefer-describe-function-title': 'warn',
|
||||||
|
|
||||||
|
/* relax strict rules in tests */
|
||||||
|
'eslint/no-unused-vars': 'off',
|
||||||
|
'typescript/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
26
configs/oxlint/src/presets/vue.ts
Normal file
26
configs/oxlint/src/presets/vue.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue.js-specific rules.
|
||||||
|
*
|
||||||
|
* Enforces Composition API with `<script setup>` and type-based declarations.
|
||||||
|
*/
|
||||||
|
export const vue: OxlintConfig = {
|
||||||
|
plugins: ['vue'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'vue/no-arrow-functions-in-watch': 'error',
|
||||||
|
'vue/no-deprecated-destroyed-lifecycle': 'error',
|
||||||
|
'vue/no-export-in-script-setup': 'error',
|
||||||
|
'vue/no-lifecycle-after-await': 'error',
|
||||||
|
'vue/no-multiple-slot-args': 'error',
|
||||||
|
'vue/no-import-compiler-macros': 'error',
|
||||||
|
'vue/define-emits-declaration': ['error', 'type-based'],
|
||||||
|
'vue/define-props-declaration': ['error', 'type-based'],
|
||||||
|
'vue/prefer-import-from-vue': 'error',
|
||||||
|
'vue/no-required-prop-with-default': 'warn',
|
||||||
|
'vue/valid-define-emits': 'error',
|
||||||
|
'vue/valid-define-props': 'error',
|
||||||
|
'vue/require-typed-ref': 'warn',
|
||||||
|
},
|
||||||
|
};
|
||||||
18
configs/oxlint/src/types.ts
Normal file
18
configs/oxlint/src/types.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Re-exported configuration types from `oxlint`.
|
||||||
|
*
|
||||||
|
* Keeps the preset API in sync with the oxlint CLI without
|
||||||
|
* maintaining a separate copy of the types.
|
||||||
|
*
|
||||||
|
* @see https://oxc.rs/docs/guide/usage/linter/config-file-reference.html
|
||||||
|
*/
|
||||||
|
export type {
|
||||||
|
OxlintConfig,
|
||||||
|
OxlintOverride,
|
||||||
|
OxlintEnv,
|
||||||
|
OxlintGlobals,
|
||||||
|
AllowWarnDeny,
|
||||||
|
DummyRule,
|
||||||
|
DummyRuleMap,
|
||||||
|
RuleCategories,
|
||||||
|
} from 'oxlint';
|
||||||
146
configs/oxlint/test/compose.test.ts
Normal file
146
configs/oxlint/test/compose.test.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { compose } from '../src/compose';
|
||||||
|
import type { OxlintConfig } from '../src/types';
|
||||||
|
|
||||||
|
describe('compose', () => {
|
||||||
|
it('should return empty config when no configs provided', () => {
|
||||||
|
expect(compose()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same config when one config provided', () => {
|
||||||
|
const config: OxlintConfig = {
|
||||||
|
plugins: ['eslint'],
|
||||||
|
rules: { 'eslint/no-console': 'warn' },
|
||||||
|
};
|
||||||
|
const result = compose(config);
|
||||||
|
expect(result.plugins).toEqual(['eslint']);
|
||||||
|
expect(result.rules).toEqual({ 'eslint/no-console': 'warn' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge plugins with dedup', () => {
|
||||||
|
const a: OxlintConfig = { plugins: ['eslint', 'oxc'] };
|
||||||
|
const b: OxlintConfig = { plugins: ['oxc', 'typescript'] };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.plugins).toEqual(['eslint', 'oxc', 'typescript']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override rules from later configs', () => {
|
||||||
|
const a: OxlintConfig = { rules: { 'eslint/no-console': 'error', 'eslint/eqeqeq': 'warn' } };
|
||||||
|
const b: OxlintConfig = { rules: { 'eslint/no-console': 'off' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.rules).toEqual({
|
||||||
|
'eslint/no-console': 'off',
|
||||||
|
'eslint/eqeqeq': 'warn',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override categories from later configs', () => {
|
||||||
|
const a: OxlintConfig = { categories: { correctness: 'error', suspicious: 'warn' } };
|
||||||
|
const b: OxlintConfig = { categories: { suspicious: 'off' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.categories).toEqual({
|
||||||
|
correctness: 'error',
|
||||||
|
suspicious: 'off',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should concatenate overrides', () => {
|
||||||
|
const a: OxlintConfig = {
|
||||||
|
overrides: [{ files: ['**/*.ts'], rules: { 'typescript/no-explicit-any': 'warn' } }],
|
||||||
|
};
|
||||||
|
const b: OxlintConfig = {
|
||||||
|
overrides: [{ files: ['**/*.test.ts'], rules: { 'eslint/no-unused-vars': 'off' } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.overrides).toHaveLength(2);
|
||||||
|
expect(result.overrides?.[0]?.files).toEqual(['**/*.ts']);
|
||||||
|
expect(result.overrides?.[1]?.files).toEqual(['**/*.test.ts']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge env', () => {
|
||||||
|
const a: OxlintConfig = { env: { browser: true } };
|
||||||
|
const b: OxlintConfig = { env: { node: true } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.env).toEqual({ browser: true, node: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge globals', () => {
|
||||||
|
const a: OxlintConfig = { globals: { MY_VAR: 'readonly' } };
|
||||||
|
const b: OxlintConfig = { globals: { ANOTHER: 'writable' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.globals).toEqual({ MY_VAR: 'readonly', ANOTHER: 'writable' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deep merge settings', () => {
|
||||||
|
const a: OxlintConfig = {
|
||||||
|
settings: {
|
||||||
|
react: { version: '18.2.0' },
|
||||||
|
next: { rootDir: 'apps/' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const b: OxlintConfig = {
|
||||||
|
settings: {
|
||||||
|
react: { linkComponents: [{ name: 'Link', linkAttribute: 'to', attributes: ['to'] }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.settings).toEqual({
|
||||||
|
react: {
|
||||||
|
version: '18.2.0',
|
||||||
|
linkComponents: [{ name: 'Link', linkAttribute: 'to', attributes: ['to'] }],
|
||||||
|
},
|
||||||
|
next: { rootDir: 'apps/' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should concatenate ignorePatterns', () => {
|
||||||
|
const a: OxlintConfig = { ignorePatterns: ['dist'] };
|
||||||
|
const b: OxlintConfig = { ignorePatterns: ['node_modules', 'coverage'] };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.ignorePatterns).toEqual(['dist', 'node_modules', 'coverage']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle composing all presets together', () => {
|
||||||
|
const base: OxlintConfig = {
|
||||||
|
plugins: ['eslint', 'oxc'],
|
||||||
|
categories: { correctness: 'error' },
|
||||||
|
rules: { 'eslint/no-console': 'warn' },
|
||||||
|
};
|
||||||
|
const ts: OxlintConfig = {
|
||||||
|
plugins: ['typescript'],
|
||||||
|
overrides: [{ files: ['**/*.ts'], rules: { 'typescript/no-explicit-any': 'warn' } }],
|
||||||
|
};
|
||||||
|
const custom: OxlintConfig = {
|
||||||
|
rules: { 'eslint/no-console': 'off' },
|
||||||
|
ignorePatterns: ['dist'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = compose(base, ts, custom);
|
||||||
|
|
||||||
|
expect(result.plugins).toEqual(['eslint', 'oxc', 'typescript']);
|
||||||
|
expect(result.categories).toEqual({ correctness: 'error' });
|
||||||
|
expect(result.rules).toEqual({ 'eslint/no-console': 'off' });
|
||||||
|
expect(result.overrides).toHaveLength(1);
|
||||||
|
expect(result.ignorePatterns).toEqual(['dist']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip undefined/empty fields', () => {
|
||||||
|
const a: OxlintConfig = { plugins: ['eslint'] };
|
||||||
|
const b: OxlintConfig = { rules: { 'eslint/no-console': 'warn' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.plugins).toEqual(['eslint']);
|
||||||
|
expect(result.rules).toEqual({ 'eslint/no-console': 'warn' });
|
||||||
|
expect(result.overrides).toBeUndefined();
|
||||||
|
expect(result.env).toBeUndefined();
|
||||||
|
expect(result.settings).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
3
configs/oxlint/tsconfig.json
Normal file
3
configs/oxlint/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
|
}
|
||||||
7
configs/oxlint/tsdown.config.ts
Normal file
7
configs/oxlint/tsdown.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'tsdown';
|
||||||
|
import { sharedConfig } from '@robonen/tsdown';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
});
|
||||||
7
configs/oxlint/vitest.config.ts
Normal file
7
configs/oxlint/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
27
configs/tsconfig/README.md
Normal file
27
configs/tsconfig/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# @robonen/tsconfig
|
||||||
|
|
||||||
|
Shared base TypeScript configuration.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install -D @robonen/tsconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Extend from it in your `tsconfig.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's Included
|
||||||
|
|
||||||
|
- **Target / Module**: ESNext with Bundler resolution
|
||||||
|
- **Strict mode**: `strict`, `noUncheckedIndexedAccess`
|
||||||
|
- **Module safety**: `verbatimModuleSyntax`, `isolatedModules`
|
||||||
|
- **Declarations**: `declaration` enabled
|
||||||
|
- **Interop**: `esModuleInterop`, `allowJs`, `resolveJsonModule`
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/tsconfig",
|
"name": "@robonen/tsconfig",
|
||||||
"private": true,
|
"version": "0.0.2",
|
||||||
"version": "1.0.0",
|
"license": "Apache-2.0",
|
||||||
"license": "UNLICENSED",
|
|
||||||
"description": "Base typescript configuration for projects",
|
"description": "Base typescript configuration for projects",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tsconfig",
|
"tsconfig",
|
||||||
@@ -16,9 +15,9 @@
|
|||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "packages/tsconfig"
|
"directory": "packages/tsconfig"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.15.6",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"**tsconfig.json"
|
"**tsconfig.json"
|
||||||
@@ -26,4 +25,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
"display": "Base TypeScript Configuration",
|
"display": "Base TypeScript Configuration",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"module": "Preserve",
|
"module": "ESNext",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"lib": ["ESNext"],
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
@@ -12,11 +13,12 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"removeComments": true,
|
"removeComments": false,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
|
|
||||||
/* Library transpiling */
|
/* Library transpiling */
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"composite": true,
|
// "composite": true,
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"declarationMap": false
|
"declarationMap": false
|
||||||
},
|
},
|
||||||
30
configs/tsdown/package.json
Normal file
30
configs/tsdown/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/tsdown",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "Shared tsdown configuration for @robonen packages",
|
||||||
|
"keywords": [
|
||||||
|
"tsdown",
|
||||||
|
"config",
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
|
"directory": "configs/tsdown"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.29.3",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.13.1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"tsdown": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
configs/tsdown/src/index.ts
Normal file
13
configs/tsdown/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Options } from 'tsdown';
|
||||||
|
|
||||||
|
const BANNER = '/*! @robonen/tools | (c) 2026 Robonen Andrew | Apache-2.0 */';
|
||||||
|
|
||||||
|
export const sharedConfig = {
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
hash: false,
|
||||||
|
outputOptions: {
|
||||||
|
banner: BANNER,
|
||||||
|
},
|
||||||
|
} satisfies Options;
|
||||||
3
configs/tsdown/tsconfig.json
Normal file
3
configs/tsdown/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
|
}
|
||||||
23
core/platform/README.md
Normal file
23
core/platform/README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# @robonen/platform
|
||||||
|
|
||||||
|
Platform-dependent utilities for browser & multi-runtime environments.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install @robonen/platform
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Entry | Utilities | Description |
|
||||||
|
| ------------------ | ------------- | -------------------------------- |
|
||||||
|
| `@robonen/platform/browsers` | `focusGuard` | Browser-specific helpers |
|
||||||
|
| `@robonen/platform/multi` | `global` | Cross-runtime (Node/Bun/Deno) utilities |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { focusGuard } from '@robonen/platform/browsers';
|
||||||
|
import { global } from '@robonen/platform/multi';
|
||||||
|
```
|
||||||
7
core/platform/jsr.json
Normal file
7
core/platform/jsr.json
Normal 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"
|
||||||
|
}
|
||||||
15
core/platform/oxlint.config.ts
Normal file
15
core/platform/oxlint.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
import { compose, base, typescript, imports } from '@robonen/oxlint';
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
compose(base, typescript, imports, {
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['src/multi/global/index.ts'],
|
||||||
|
rules: {
|
||||||
|
'unicorn/prefer-global-this': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
54
core/platform/package.json
Normal file
54
core/platform/package.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/platform",
|
||||||
|
"version": "0.0.4",
|
||||||
|
"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.29.3",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.13.1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
"./browsers": {
|
||||||
|
"types": "./dist/browsers.d.ts",
|
||||||
|
"import": "./dist/browsers.js",
|
||||||
|
"require": "./dist/browsers.cjs"
|
||||||
|
},
|
||||||
|
"./multi": {
|
||||||
|
"types": "./dist/multi.d.ts",
|
||||||
|
"import": "./dist/multi.js",
|
||||||
|
"require": "./dist/multi.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"dev": "vitest dev",
|
||||||
|
"build": "tsdown"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"@robonen/tsdown": "workspace:*",
|
||||||
|
"oxlint": "catalog:",
|
||||||
|
"tsdown": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
69
core/platform/src/browsers/focusGuard/index.test.ts
Normal file
69
core/platform/src/browsers/focusGuard/index.test.ts
Normal 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;');
|
||||||
|
});
|
||||||
|
});
|
||||||
50
core/platform/src/browsers/focusGuard/index.ts
Normal file
50
core/platform/src/browsers/focusGuard/index.ts
Normal 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 = '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 = 'focus-guard') {
|
||||||
|
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;
|
||||||
|
}
|
||||||
1
core/platform/src/browsers/index.ts
Normal file
1
core/platform/src/browsers/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './focusGuard';
|
||||||
49
core/platform/src/multi/debounce/index.ts
Normal file
49
core/platform/src/multi/debounce/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// eslint-disable
|
||||||
|
|
||||||
|
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[];
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
28
core/platform/src/multi/global/index.ts
Normal file
28
core/platform/src/multi/global/index.ts
Normal 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';
|
||||||
2
core/platform/src/multi/index.ts
Normal file
2
core/platform/src/multi/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './global';
|
||||||
|
// export * from './debounce';
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@robonen/tsconfig/tsconfig.json",
|
"extends": "@robonen/tsconfig/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
10
core/platform/tsdown.config.ts
Normal file
10
core/platform/tsdown.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'tsdown';
|
||||||
|
import { sharedConfig } from '@robonen/tsdown';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
|
entry: {
|
||||||
|
browsers: 'src/browsers/index.ts',
|
||||||
|
multi: 'src/multi/index.ts',
|
||||||
|
},
|
||||||
|
});
|
||||||
8
core/platform/vitest.config.ts
Normal file
8
core/platform/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
32
core/stdlib/README.md
Normal file
32
core/stdlib/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# @robonen/stdlib
|
||||||
|
|
||||||
|
Standard library of platform-independent utilities for TypeScript.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install @robonen/stdlib
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | Utilities |
|
||||||
|
| --------------- | --------------------------------------------------------------- |
|
||||||
|
| **arrays** | `cluster`, `first`, `last`, `sum`, `unique` |
|
||||||
|
| **async** | `sleep`, `tryIt` |
|
||||||
|
| **bits** | `flags` |
|
||||||
|
| **collections** | `get` |
|
||||||
|
| **math** | `clamp`, `lerp`, `remap` + BigInt variants |
|
||||||
|
| **objects** | `omit`, `pick` |
|
||||||
|
| **patterns** | `pubsub` |
|
||||||
|
| **structs** | `stack` |
|
||||||
|
| **sync** | `mutex` |
|
||||||
|
| **text** | `levenshteinDistance`, `trigramDistance` |
|
||||||
|
| **types** | JS & TS type utilities |
|
||||||
|
| **utils** | `timestamp`, `noop` |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { first, sleep, clamp } from '@robonen/stdlib';
|
||||||
|
```
|
||||||
6
core/stdlib/jsr.json
Normal file
6
core/stdlib/jsr.json
Normal 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"
|
||||||
|
}
|
||||||
4
core/stdlib/oxlint.config.ts
Normal file
4
core/stdlib/oxlint.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
import { compose, base, typescript, imports } from '@robonen/oxlint';
|
||||||
|
|
||||||
|
export default defineConfig(compose(base, typescript, imports));
|
||||||
49
core/stdlib/package.json
Normal file
49
core/stdlib/package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/stdlib",
|
||||||
|
"version": "0.0.9",
|
||||||
|
"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.29.3",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.13.1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"dev": "vitest dev",
|
||||||
|
"build": "tsdown"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"@robonen/tsdown": "workspace:*",
|
||||||
|
"oxlint": "catalog:",
|
||||||
|
"tsdown": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
core/stdlib/src/arrays/cluster/index.test.ts
Normal file
40
core/stdlib/src/arrays/cluster/index.test.ts
Normal 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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
24
core/stdlib/src/arrays/cluster/index.ts
Normal file
24
core/stdlib/src/arrays/cluster/index.ts
Normal 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));
|
||||||
|
}
|
||||||
23
core/stdlib/src/arrays/first/index.test.ts
Normal file
23
core/stdlib/src/arrays/first/index.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
20
core/stdlib/src/arrays/first/index.ts
Normal file
20
core/stdlib/src/arrays/first/index.ts
Normal 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;
|
||||||
|
}
|
||||||
5
core/stdlib/src/arrays/index.ts
Normal file
5
core/stdlib/src/arrays/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './cluster';
|
||||||
|
export * from './first';
|
||||||
|
export * from './last';
|
||||||
|
export * from './sum';
|
||||||
|
export * from './unique';
|
||||||
23
core/stdlib/src/arrays/last/index.test.ts
Normal file
23
core/stdlib/src/arrays/last/index.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
20
core/stdlib/src/arrays/last/index.ts
Normal file
20
core/stdlib/src/arrays/last/index.ts
Normal 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;
|
||||||
|
}
|
||||||
46
core/stdlib/src/arrays/sum/index.test.ts
Normal file
46
core/stdlib/src/arrays/sum/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
core/stdlib/src/arrays/sum/index.ts
Normal file
26
core/stdlib/src/arrays/sum/index.ts
Normal 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);
|
||||||
|
}
|
||||||
45
core/stdlib/src/arrays/unique/index.test.ts
Normal file
45
core/stdlib/src/arrays/unique/index.test.ts
Normal 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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
33
core/stdlib/src/arrays/unique/index.ts
Normal file
33
core/stdlib/src/arrays/unique/index.ts
Normal 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());
|
||||||
|
}
|
||||||
69
core/stdlib/src/async/cancellablePromise/index.test.ts
Normal file
69
core/stdlib/src/async/cancellablePromise/index.test.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { cancellablePromise, CancelledError } from '.';
|
||||||
|
|
||||||
|
describe('cancellablePromise', () => {
|
||||||
|
it('resolve the promise normally when not cancelled', async () => {
|
||||||
|
const { promise } = cancellablePromise(Promise.resolve('data'));
|
||||||
|
|
||||||
|
await expect(promise).resolves.toBe('data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject the promise normally when not cancelled', async () => {
|
||||||
|
const error = new Error('test-error');
|
||||||
|
const { promise } = cancellablePromise(Promise.reject(error));
|
||||||
|
|
||||||
|
await expect(promise).rejects.toThrow(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject with CancelledError when cancelled before resolve', async () => {
|
||||||
|
const { promise, cancel } = cancellablePromise(
|
||||||
|
new Promise<string>((resolve) => setTimeout(() => resolve('data'), 100)),
|
||||||
|
);
|
||||||
|
|
||||||
|
cancel();
|
||||||
|
|
||||||
|
await expect(promise).rejects.toBeInstanceOf(CancelledError);
|
||||||
|
await expect(promise).rejects.toThrow('Promise was cancelled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject with CancelledError with custom reason', async () => {
|
||||||
|
const { promise, cancel } = cancellablePromise(
|
||||||
|
new Promise<string>((resolve) => setTimeout(() => resolve('data'), 100)),
|
||||||
|
);
|
||||||
|
|
||||||
|
cancel('Request aborted');
|
||||||
|
|
||||||
|
await expect(promise).rejects.toBeInstanceOf(CancelledError);
|
||||||
|
await expect(promise).rejects.toThrow('Request aborted');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancel prevents then callback from being called', async () => {
|
||||||
|
const onFulfilled = vi.fn();
|
||||||
|
|
||||||
|
const { promise, cancel } = cancellablePromise(
|
||||||
|
new Promise<string>((resolve) => setTimeout(() => resolve('data'), 100)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const chained = promise.then(onFulfilled).catch(() => {});
|
||||||
|
|
||||||
|
cancel();
|
||||||
|
|
||||||
|
await chained;
|
||||||
|
|
||||||
|
expect(onFulfilled).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CancelledError has correct name property', () => {
|
||||||
|
const error = new CancelledError();
|
||||||
|
|
||||||
|
expect(error.name).toBe('CancelledError');
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
expect(error.message).toBe('Promise was cancelled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('CancelledError accepts custom message', () => {
|
||||||
|
const error = new CancelledError('Custom reason');
|
||||||
|
|
||||||
|
expect(error.message).toBe('Custom reason');
|
||||||
|
});
|
||||||
|
});
|
||||||
49
core/stdlib/src/async/cancellablePromise/index.ts
Normal file
49
core/stdlib/src/async/cancellablePromise/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export class CancelledError extends Error {
|
||||||
|
constructor(reason?: string) {
|
||||||
|
super(reason ?? 'Promise was cancelled');
|
||||||
|
this.name = 'CancelledError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CancellablePromise<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
cancel: (reason?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name cancellablePromise
|
||||||
|
* @category Async
|
||||||
|
* @description Wraps a promise with a cancel capability, allowing the promise to be rejected with a CancelledError
|
||||||
|
*
|
||||||
|
* @param {Promise<T>} promise - The promise to make cancellable
|
||||||
|
* @returns {CancellablePromise<T>} - An object with the wrapped promise and a cancel function
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { promise, cancel } = cancellablePromise(fetch('/api/data'));
|
||||||
|
* cancel(); // Rejects with CancelledError
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { promise, cancel } = cancellablePromise(longRunningTask());
|
||||||
|
* setTimeout(() => cancel('Timeout'), 5000);
|
||||||
|
* const [error] = await tryIt(() => promise)();
|
||||||
|
*
|
||||||
|
* @since 0.0.10
|
||||||
|
*/
|
||||||
|
export function cancellablePromise<T>(promise: Promise<T>): CancellablePromise<T> {
|
||||||
|
let rejectPromise: (reason: CancelledError) => void;
|
||||||
|
|
||||||
|
const wrappedPromise = new Promise<T>((resolve, reject) => {
|
||||||
|
rejectPromise = reject;
|
||||||
|
|
||||||
|
promise.then(resolve, reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = (reason?: string) => {
|
||||||
|
rejectPromise(new CancelledError(reason));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise: wrappedPromise,
|
||||||
|
cancel,
|
||||||
|
};
|
||||||
|
}
|
||||||
3
core/stdlib/src/async/index.ts
Normal file
3
core/stdlib/src/async/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './cancellablePromise';
|
||||||
|
export * from './sleep';
|
||||||
|
export * from './tryIt';
|
||||||
3
core/stdlib/src/async/pool/index.ts
Normal file
3
core/stdlib/src/async/pool/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface AsyncPoolOptions {
|
||||||
|
concurrency?: number;
|
||||||
|
}
|
||||||
39
core/stdlib/src/async/retry/index.ts
Normal file
39
core/stdlib/src/async/retry/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// eslint-disable
|
||||||
|
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;
|
||||||
|
}
|
||||||
19
core/stdlib/src/async/sleep/index.test.ts
Normal file
19
core/stdlib/src/async/sleep/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
21
core/stdlib/src/async/sleep/index.ts
Normal file
21
core/stdlib/src/async/sleep/index.ts
Normal 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));
|
||||||
|
}
|
||||||
67
core/stdlib/src/async/tryIt/index.test.ts
Normal file
67
core/stdlib/src/async/tryIt/index.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
41
core/stdlib/src/async/tryIt/index.ts
Normal file
41
core/stdlib/src/async/tryIt/index.ts
Normal 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>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
26
core/stdlib/src/bits/flags/index.test.ts
Normal file
26
core/stdlib/src/bits/flags/index.test.ts
Normal 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'));
|
||||||
|
});
|
||||||
|
});
|
||||||
22
core/stdlib/src/bits/flags/index.ts
Normal file
22
core/stdlib/src/bits/flags/index.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
95
core/stdlib/src/bits/helpers/index.test.ts
Normal file
95
core/stdlib/src/bits/helpers/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
100
core/stdlib/src/bits/helpers/index.ts
Normal file
100
core/stdlib/src/bits/helpers/index.ts
Normal 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;
|
||||||
|
}
|
||||||
1
core/stdlib/src/bits/index.ts
Normal file
1
core/stdlib/src/bits/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './flags';
|
||||||
63
core/stdlib/src/bits/vector/index.test.ts
Normal file
63
core/stdlib/src/bits/vector/index.test.ts
Normal 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'));
|
||||||
|
});
|
||||||
|
});
|
||||||
61
core/stdlib/src/bits/vector/index.ts
Normal file
61
core/stdlib/src/bits/vector/index.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
export interface BitVectorLike {
|
||||||
|
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 BitVectorLike {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
core/stdlib/src/collections/get/index.ts
Normal file
34
core/stdlib/src/collections/get/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Collection, 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 ReadonlyArray<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;
|
||||||
|
}
|
||||||
1
core/stdlib/src/collections/index.ts
Normal file
1
core/stdlib/src/collections/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './get';
|
||||||
11
core/stdlib/src/index.ts
Normal file
11
core/stdlib/src/index.ts
Normal 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'
|
||||||
@@ -43,4 +43,39 @@ describe('clamp', () => {
|
|||||||
// negative range and value
|
// negative range and value
|
||||||
expect(clamp(-10, -100, -5)).toBe(-10);
|
expect(clamp(-10, -100, -5)).toBe(-10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handle NaN and Infinity', () => {
|
||||||
|
// value is NaN
|
||||||
|
expect(clamp(Number.NaN, 0, 100)).toBe(Number.NaN);
|
||||||
|
|
||||||
|
// min is NaN
|
||||||
|
expect(clamp(50, Number.NaN, 100)).toBe(Number.NaN);
|
||||||
|
|
||||||
|
// max is NaN
|
||||||
|
expect(clamp(50, 0, Number.NaN)).toBe(Number.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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
15
core/stdlib/src/math/basic/clamp/index.ts
Normal file
15
core/stdlib/src/math/basic/clamp/index.ts
Normal 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);
|
||||||
|
}
|
||||||
61
core/stdlib/src/math/basic/lerp/index.test.ts
Normal file
61
core/stdlib/src/math/basic/lerp/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
31
core/stdlib/src/math/basic/lerp/index.ts
Normal file
31
core/stdlib/src/math/basic/lerp/index.ts
Normal 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);
|
||||||
|
}
|
||||||
46
core/stdlib/src/math/basic/remap/index.test.ts
Normal file
46
core/stdlib/src/math/basic/remap/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
core/stdlib/src/math/basic/remap/index.ts
Normal file
25
core/stdlib/src/math/basic/remap/index.ts
Normal 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));
|
||||||
|
}
|
||||||
35
core/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal file
35
core/stdlib/src/math/bigint/clampBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
18
core/stdlib/src/math/bigint/clampBigInt/index.ts
Normal file
18
core/stdlib/src/math/bigint/clampBigInt/index.ts
Normal 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);
|
||||||
|
}
|
||||||
83
core/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal file
83
core/stdlib/src/math/bigint/lerpBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
38
core/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal file
38
core/stdlib/src/math/bigint/lerpBigInt/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Precision scale for bigint interpolation (6 decimal places).
|
||||||
|
* BigInt has no overflow, so higher precision is free.
|
||||||
|
*/
|
||||||
|
const SCALE = 1_000_000;
|
||||||
|
const SCALE_N = BigInt(SCALE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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(Math.round(t * SCALE))) / SCALE_N;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) * SCALE_N / (end - start)) / SCALE;
|
||||||
|
}
|
||||||
39
core/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal file
39
core/stdlib/src/math/bigint/maxBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
core/stdlib/src/math/bigint/maxBigInt/index.ts
Normal file
17
core/stdlib/src/math/bigint/maxBigInt/index.ts
Normal 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);
|
||||||
|
}
|
||||||
39
core/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal file
39
core/stdlib/src/math/bigint/minBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
core/stdlib/src/math/bigint/minBigInt/index.ts
Normal file
17
core/stdlib/src/math/bigint/minBigInt/index.ts
Normal 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);
|
||||||
|
}
|
||||||
32
core/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal file
32
core/stdlib/src/math/bigint/remapBigInt/index.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
25
core/stdlib/src/math/bigint/remapBigInt/index.ts
Normal file
25
core/stdlib/src/math/bigint/remapBigInt/index.ts
Normal 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));
|
||||||
|
}
|
||||||
9
core/stdlib/src/math/index.ts
Normal file
9
core/stdlib/src/math/index.ts
Normal 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';
|
||||||
2
core/stdlib/src/objects/index.ts
Normal file
2
core/stdlib/src/objects/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './omit';
|
||||||
|
export * from './pick';
|
||||||
50
core/stdlib/src/objects/omit/index.test.ts
Normal file
50
core/stdlib/src/objects/omit/index.test.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
39
core/stdlib/src/objects/omit/index.ts
Normal file
39
core/stdlib/src/objects/omit/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { isArray } from '../../types';
|
||||||
|
import 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;
|
||||||
|
}
|
||||||
36
core/stdlib/src/objects/pick/index.test.ts
Normal file
36
core/stdlib/src/objects/pick/index.test.ts
Normal 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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
39
core/stdlib/src/objects/pick/index.ts
Normal file
39
core/stdlib/src/objects/pick/index.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { isArray } from '../../types';
|
||||||
|
import 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;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user