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

2 Commits

236 changed files with 5849 additions and 7863 deletions

View File

@@ -1,9 +1,7 @@
name: CI name: CI
on: on:
pull_request: - pull_request
branches:
- master
env: env:
NODE_VERSION: 22.x NODE_VERSION: 22.x
@@ -16,14 +14,14 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
run_install: false run_install: false
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: pnpm cache: pnpm
@@ -31,11 +29,5 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Lint
run: pnpm lint
- name: Test - name: Test
run: pnpm test run: pnpm all:build && pnpm all:test

View File

@@ -1,78 +0,0 @@
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

57
CHANGELOG.md Normal file
View File

@@ -0,0 +1,57 @@
# 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))

2
apps/vhs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin/**
!bin/.gitkeep

1
apps/vhs/README.md Normal file
View File

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

0
apps/vhs/bin/.gitkeep Normal file
View File

5
apps/vhs/jsr.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "@robonen/vhs",
"version": "0.0.0",
"exports": "./src/index.ts"
}

26
apps/vhs/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "@robonen/vhs",
"private": true,
"version": "0.0.1",
"license": "UNLICENSED",
"description": "",
"keywords": [],
"author": "Robonen Andrew <robonenandrew@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/robonen/tools.git",
"directory": "./apps/vhs"
},
"packageManager": "pnpm@9.15.4",
"engines": {
"bun": ">=1.1.27"
},
"type": "module",
"scripts": {
"start": "bun run src/index.ts"
},
"devDependencies": {
"@robonen/tsconfig": "workspace:*",
"@types/bun": "^1.1.17"
}
}

36
apps/vhs/src/index.ts Normal file
View File

@@ -0,0 +1,36 @@
import { version } from '../package.json';
import { resolve } from 'path';
import { $, Glob } from 'bun';
async function ffmpegMergeAndTranscodeAvi(files: Set<string>) {
const ffmpeg = resolve('bin/ffmpeg');
const output = resolve('output.mp4');
const input = Array.from(files).toSorted((a, b) => a.localeCompare(b)).join('|');
const shell = $`${ffmpeg} -i "concat:${input}" -stats -c:v libx264 -crf 23 -preset veryfast -c:a aac ${output}`;
for await (const line of shell.lines()) {
console.log(line);
}
}
const path = Bun.argv[2];
if (!path) {
console.error('Please provide a path to a file or directory');
process.exit(1);
}
console.info(`Welcome to VHS v${version} 📼`);
console.info(`Scanning ${path}...`);
const glob = new Glob(resolve(path));
const files = new Set<string>();
for await (const file of glob.scan({ followSymlinks: false })) {
files.add(file);
}
console.info(`Found ${files.size} files`);
console.info(await ffmpegMergeAndTranscodeAvi(files));

View File

@@ -1,53 +1,16 @@
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 'node:path'; import { resolve } from 'pathe';
import { splitByCase } from 'scule'; import { splitByCase } from 'scule';
async function getLatestPackageVersion(packageName: string) { const PACKAGE_MANAGER = 'pnpm@9.11.0';
try { const NODE_VERSION = '>=20.17.0';
const response = await fetch(`https://registry.npmjs.org/${packageName}`); const VITE_VERSION = '^5.4.8';
const data = await response.json(); const VITE_DTS_VERSION = '^4.2.2';
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 = async (name: string, path: string, hasVite: boolean) => { const generatePackageJson = (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,
@@ -61,17 +24,20 @@ const generatePackageJson = async (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: `pnpm@${packageManagerVersion}`, packageManager: PACKAGE_MANAGER,
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: {
@@ -85,9 +51,9 @@ const generatePackageJson = async (name: string, path: string, hasVite: boolean)
devDependencies: { devDependencies: {
'@robonen/tsconfig': 'workspace:*', '@robonen/tsconfig': 'workspace:*',
...(hasVite && { ...(hasVite && {
vite: viteVersion, vite: VITE_VERSION,
'vite-plugin-dts': viteDtsVersion, 'vite-plugin-dts': VITE_DTS_VERSION,
pathe: patheVersion, pathe: PATHE_VERSION,
}), }),
}, },
}; };
@@ -166,15 +132,14 @@ const createCommand = defineCommand({
await mkdir(resolvedPath, { recursive: true }); await mkdir(resolvedPath, { recursive: true });
const packageJson = await generatePackageJson(args.name, path, hasVite); writeFile(`${resolvedPath}/package.json`, generatePackageJson(args.name, path, hasVite));
await writeFile(`${resolvedPath}/package.json`, packageJson); writeFile(`${resolvedPath}/jsr.json`, generateJsrJson(args.name));
await writeFile(`${resolvedPath}/jsr.json`, generateJsrJson(args.name)); writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig());
await writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig()); writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
await writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
if (hasVite) { if (hasVite) {
await mkdir(`${resolvedPath}/src`, { recursive: true }); mkdir(`${resolvedPath}/src`, { recursive: true });
await writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig()); writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig());
} }
console.log(`Project created successfully`); console.log(`Project created successfully`);

View File

@@ -1,54 +0,0 @@
# @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

View File

@@ -1,4 +0,0 @@
import { defineConfig } from 'oxlint';
import { compose, base, typescript, imports } from '@robonen/oxlint';
export default defineConfig(compose(base, typescript, imports));

View File

@@ -1,52 +0,0 @@
{
"name": "@robonen/oxlint",
"version": "0.0.1",
"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": ">=22.22.0"
},
"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:*",
"oxlint": "catalog:",
"tsdown": "catalog:"
},
"peerDependencies": {
"oxlint": ">=1.47.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -1,103 +0,0 @@
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;
}

View File

@@ -1,17 +0,0 @@
/* 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';

View File

@@ -1,73 +0,0 @@
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',
},
};

View File

@@ -1,20 +0,0 @@
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'],
},
};

View File

@@ -1,6 +0,0 @@
export { base } from './base';
export { typescript } from './typescript';
export { vue } from './vue';
export { vitest } from './vitest';
export { imports } from './imports';
export { node } from './node';

View File

@@ -1,17 +0,0 @@
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',
},
};

View File

@@ -1,39 +0,0 @@
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',
},
},
],
};

View File

@@ -1,35 +0,0 @@
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',
},
},
],
};

View File

@@ -1,26 +0,0 @@
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',
},
};

View File

@@ -1,18 +0,0 @@
/**
* 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';

View File

@@ -1,146 +0,0 @@
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();
});
});

View File

@@ -1,9 +0,0 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
hash: false,
});

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});

View File

@@ -1,27 +0,0 @@
# @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`

View File

@@ -1,23 +0,0 @@
# @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';
```

View File

@@ -1,15 +0,0 @@
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',
},
},
],
}),
);

View File

@@ -1,12 +0,0 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: {
browsers: 'src/browsers/index.ts',
multi: 'src/multi/index.ts',
},
format: ['esm', 'cjs'],
dts: true,
clean: true,
hash: false,
});

View File

@@ -1,32 +0,0 @@
# @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';
```

View File

@@ -1,4 +0,0 @@
import { defineConfig } from 'oxlint';
import { compose, base, typescript, imports } from '@robonen/oxlint';
export default defineConfig(compose(base, typescript, imports));

View File

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

View File

@@ -1,39 +0,0 @@
// 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;
}

View File

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

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
hash: false,
});

BIN
cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

22
docs/.vitepress/config.ts Normal file
View File

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

14
docs/index.md Normal file
View File

@@ -0,0 +1,14 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: Toolkit
tagline: A collection of typescript and javascript development tools
actions:
- theme: brand
text: Get Started
link: /
- theme: alt
text: View on GitHub
link: /

View File

@@ -1,21 +0,0 @@
# @robonen/renovate
Shared [Renovate](https://docs.renovatebot.com/) configuration preset.
## Usage
Reference it in your `renovate.json`:
```json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>robonen/tools//infra/renovate/default.json"]
}
```
## What's Included
- Extends `config:base` and `group:allNonMajor`
- Semantic commit type: `chore`
- Range strategy: `bump`
- Auto-approves & auto-merges minor, patch, pin, and digest updates (scheduled 13 AM)

View File

@@ -15,26 +15,25 @@
"type": "git", "type": "git",
"url": "git+https://github.com/robonen/tools.git" "url": "git+https://github.com/robonen/tools.git"
}, },
"packageManager": "pnpm@10.29.3", "packageManager": "pnpm@9.15.4",
"engines": { "engines": {
"node": ">=22.22.0" "node": ">=22.13.0"
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^22.19.11", "@types/node": "^22.10.7",
"@vitest/coverage-v8": "catalog:", "citty": "^0.1.6",
"@vitest/ui": "catalog:", "jiti": "^2.4.2",
"citty": "^0.2.1", "pathe": "^2.0.2",
"jiti": "^2.6.1",
"jsdom": "catalog:",
"scule": "^1.3.0", "scule": "^1.3.0",
"vitest": "catalog:" "vitepress": "^1.5.0"
}, },
"scripts": { "scripts": {
"build": "pnpm -r build", "all:build": "pnpm -r build",
"lint": "pnpm -r lint", "all:test": "pnpm -r test",
"test": "vitest run", "create": "jiti ./cli.ts",
"test:ui": "vitest --ui", "docs:dev": "vitepress dev .",
"create": "jiti ./bin/cli.ts" "docs:build": "vitepress build .",
"docs:preview": "vitepress preview ."
} }
} }

View File

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

View File

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

View File

@@ -18,9 +18,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "packages/platform" "directory": "packages/platform"
}, },
"packageManager": "pnpm@10.29.3", "packageManager": "pnpm@9.15.4",
"engines": { "engines": {
"node": ">=24.13.1" "node": ">=22.13.0"
}, },
"type": "module", "type": "module",
"files": [ "files": [
@@ -28,26 +28,25 @@
], ],
"exports": { "exports": {
"./browsers": { "./browsers": {
"types": "./dist/browsers.d.ts", "import": "./dist/browsers.mjs",
"import": "./dist/browsers.js", "require": "./dist/browsers.cjs",
"require": "./dist/browsers.cjs" "types": "./dist/browsers.d.ts"
}, },
"./multi": { "./multi": {
"types": "./dist/multi.d.ts", "import": "./dist/multi.mjs",
"import": "./dist/multi.js", "require": "./dist/multi.cjs",
"require": "./dist/multi.cjs" "types": "./dist/multi.d.ts"
} }
}, },
"scripts": { "scripts": {
"lint": "oxlint -c oxlint.config.ts",
"test": "vitest run", "test": "vitest run",
"dev": "vitest dev", "dev": "vitest dev",
"build": "tsdown" "build": "unbuild"
}, },
"devDependencies": { "devDependencies": {
"@robonen/oxlint": "workspace:*",
"@robonen/tsconfig": "workspace:*", "@robonen/tsconfig": "workspace:*",
"oxlint": "catalog:", "jsdom": "catalog:",
"tsdown": "catalog:" "unbuild": "catalog:",
"vitest": "catalog:"
} }
} }

View File

@@ -18,7 +18,7 @@
* *
* @since 0.0.3 * @since 0.0.3
*/ */
export function focusGuard(namespace = 'focus-guard') { export function focusGuard(namespace: string = 'focus-guard') {
const guardAttr = `data-${namespace}`; const guardAttr = `data-${namespace}`;
const createGuard = () => { const createGuard = () => {
@@ -39,7 +39,7 @@ export function focusGuard(namespace = 'focus-guard') {
}; };
} }
export function createGuardAttrs(namespace = 'focus-guard') { export function createGuardAttrs(namespace: string) {
const element = document.createElement('span'); const element = document.createElement('span');
element.setAttribute(namespace, ''); element.setAttribute(namespace, '');

View File

@@ -1,5 +1,3 @@
// eslint-disable
export interface DebounceOptions { export interface DebounceOptions {
/** /**
* Call the function on the leading edge of the timeout, instead of waiting for the trailing edge * Call the function on the leading edge of the timeout, instead of waiting for the trailing edge

View File

@@ -5,4 +5,3 @@ export default defineConfig({
environment: 'jsdom', environment: 'jsdom',
}, },
}); });

View File

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

View File

@@ -16,9 +16,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "packages/renovate" "directory": "packages/renovate"
}, },
"packageManager": "pnpm@10.29.3", "packageManager": "pnpm@9.15.4",
"engines": { "engines": {
"node": ">=22.22.0" "node": ">=22.13.0"
}, },
"files": [ "files": [
"default.json" "default.json"
@@ -27,6 +27,6 @@
"test": "renovate-config-validator ./default.json" "test": "renovate-config-validator ./default.json"
}, },
"devDependencies": { "devDependencies": {
"renovate": "^43.14.1" "renovate": "^39.115.2"
} }
} }

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@robonen/stdlib", "name": "@robonen/stdlib",
"version": "0.0.7", "version": "0.0.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "A collection of tools, utilities, and helpers for TypeScript", "description": "A collection of tools, utilities, and helpers for TypeScript",
"keywords": [ "keywords": [
@@ -18,31 +18,34 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "packages/stdlib" "directory": "packages/stdlib"
}, },
"packageManager": "pnpm@10.29.3", "packageManager": "pnpm@9.15.4",
"engines": { "engines": {
"node": ">=24.13.1" "node": ">=22.13.0"
}, },
"type": "module", "type": "module",
"files": [ "files": [
"dist" "dist"
], ],
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts", "import": "./dist/index.mjs",
"import": "./dist/index.js", "require": "./dist/index.cjs",
"require": "./dist/index.cjs" "types": "./dist/index.d.ts"
} }
}, },
"scripts": { "scripts": {
"lint": "oxlint -c oxlint.config.ts",
"test": "vitest run", "test": "vitest run",
"dev": "vitest dev", "dev": "vitest dev",
"build": "tsdown" "build": "unbuild"
}, },
"devDependencies": { "devDependencies": {
"@robonen/oxlint": "workspace:*",
"@robonen/tsconfig": "workspace:*", "@robonen/tsconfig": "workspace:*",
"oxlint": "catalog:", "@vitest/coverage-v8": "catalog:",
"tsdown": "catalog:" "pathe": "catalog:",
"unbuild": "catalog:",
"vitest": "catalog:"
} }
} }

View File

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

View File

@@ -13,8 +13,6 @@
* sleep(1000).then(() => { * sleep(1000).then(() => {
* console.log('Hello, World!'); * console.log('Hello, World!');
* }); * });
*
* @since 0.0.3
*/ */
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));

View File

@@ -1,4 +1,4 @@
export interface BitVectorLike { export interface BitVector {
getBit(index: number): boolean; getBit(index: number): boolean;
setBit(index: number): void; setBit(index: number): void;
clearBit(index: number): void; clearBit(index: number): void;
@@ -12,7 +12,7 @@ export interface BitVectorLike {
* *
* @since 0.0.3 * @since 0.0.3
*/ */
export class BitVector extends Uint8Array implements BitVectorLike { export class BitVector extends Uint8Array implements BitVector {
constructor(size: number) { constructor(size: number) {
super(Math.ceil(size / 8)); super(Math.ceil(size / 8));
} }

View File

@@ -1,4 +1,4 @@
import type { Collection, Path } from '../../types'; import { type Collection, type Path } from '../../types';
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> = export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
K extends keyof O K extends keyof O
@@ -9,7 +9,7 @@ export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
export type ExtractFromArray<A extends readonly any[], K> = export type ExtractFromArray<A extends readonly any[], K> =
any[] extends A any[] extends A
? A extends ReadonlyArray<infer T> ? A extends readonly (infer T)[]
? T | undefined ? T | undefined
: undefined : undefined
: K extends keyof A : K extends keyof A
@@ -29,6 +29,7 @@ export type ExtractFromCollection<O, K> =
type Get<O, K> = ExtractFromCollection<O, Path<K>>; type Get<O, K> = ExtractFromCollection<O, Path<K>>;
export function get<O extends Collection, K extends string>(obj: 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; return path.split('.').reduce((acc, key) => (acc as any)?.[key], obj) as Get<O, K> | undefined;
} }

View File

@@ -5,7 +5,6 @@ export * from './math';
export * from './objects'; export * from './objects';
export * from './patterns'; export * from './patterns';
export * from './structs'; export * from './structs';
export * from './sync';
export * from './text'; export * from './text';
export * from './types'; export * from './types';
export * from './utils' export * from './utils'

View File

@@ -46,13 +46,13 @@ describe('clamp', () => {
it('handle NaN and Infinity', () => { it('handle NaN and Infinity', () => {
// value is NaN // value is NaN
expect(clamp(Number.NaN, 0, 100)).toBe(Number.NaN); expect(clamp(NaN, 0, 100)).toBe(NaN);
// min is NaN // min is NaN
expect(clamp(50, Number.NaN, 100)).toBe(Number.NaN); expect(clamp(50, NaN, 100)).toBe(NaN);
// max is NaN // max is NaN
expect(clamp(50, 0, Number.NaN)).toBe(Number.NaN); expect(clamp(50, 0, NaN)).toBe(NaN);
// value is Infinity // value is Infinity
expect(clamp(Infinity, 0, 100)).toBe(100); expect(clamp(Infinity, 0, 100)).toBe(100);

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