mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 19:04:46 +00:00
Compare commits
126 Commits
feat/templ
...
7a5bc07472
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a5bc07472 | ||
|
|
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 |
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -1,7 +1,9 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.x
|
||||
@@ -14,14 +16,14 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
@@ -30,4 +32,4 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
run: pnpm all:build && pnpm all:test
|
||||
run: pnpm build && 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
|
||||
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))
|
||||
|
||||
2
apps/vhs/.gitignore
vendored
2
apps/vhs/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
bin/**
|
||||
!bin/.gitkeep
|
||||
@@ -1 +0,0 @@
|
||||
# @robonen/vhs
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "@robonen/vhs",
|
||||
"version": "0.0.0",
|
||||
"exports": "./src/index.ts"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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));
|
||||
@@ -1,16 +1,53 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { defineCommand, runMain } from 'citty';
|
||||
import { resolve } from 'pathe';
|
||||
import { resolve } from 'node:path';
|
||||
import { splitByCase } from 'scule';
|
||||
|
||||
const PACKAGE_MANAGER = 'pnpm@9.11.0';
|
||||
const NODE_VERSION = '>=20.17.0';
|
||||
const VITE_VERSION = '^5.4.8';
|
||||
const VITE_DTS_VERSION = '^4.2.2';
|
||||
const PATHE_VERSION = '^1.1.2'
|
||||
async function getLatestPackageVersion(packageName: string) {
|
||||
try {
|
||||
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to fetch latest version for ${packageName}, using fallback`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const latestVersion = data['dist-tags']?.latest as string | undefined;
|
||||
|
||||
if (!latestVersion)
|
||||
return null;
|
||||
|
||||
return {
|
||||
version: latestVersion,
|
||||
versionRange: `^${latestVersion}`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching version for ${packageName}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const PACKAGE_MANAGER_DEFAULT = 'pnpm@10.10.0';
|
||||
const NODE_VERSION = '>=22.15.0';
|
||||
const VITE_VERSION_DEFAULT = '^5.4.8';
|
||||
const VITE_DTS_VERSION_DEFAULT = '^4.2.2';
|
||||
const PATHE_VERSION_DEFAULT = '^1.1.2';
|
||||
const DEFAULT_DIR = 'packages';
|
||||
|
||||
const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
||||
const generatePackageJson = async (name: string, path: string, hasVite: boolean) => {
|
||||
const [
|
||||
packageManagerVersion,
|
||||
viteVersion,
|
||||
viteDtsVersion,
|
||||
patheVersion,
|
||||
] = await Promise.all([
|
||||
getLatestPackageVersion('pnpm').then(v => v?.version || PACKAGE_MANAGER_DEFAULT),
|
||||
hasVite ? getLatestPackageVersion('vite').then(v => v?.versionRange || VITE_VERSION_DEFAULT) : VITE_VERSION_DEFAULT,
|
||||
hasVite ? getLatestPackageVersion('vite-plugin-dts').then(v => v?.versionRange || VITE_DTS_VERSION_DEFAULT) : VITE_DTS_VERSION_DEFAULT,
|
||||
hasVite ? getLatestPackageVersion('pathe').then(v => v?.versionRange || PATHE_VERSION_DEFAULT) : PATHE_VERSION_DEFAULT,
|
||||
]);
|
||||
|
||||
const data = {
|
||||
name,
|
||||
private: true,
|
||||
@@ -24,20 +61,17 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
||||
url: 'git+https://github.com/robonen/tools.git',
|
||||
directory: path,
|
||||
},
|
||||
packageManager: PACKAGE_MANAGER,
|
||||
packageManager: `pnpm@${packageManagerVersion}`,
|
||||
engines: {
|
||||
node: NODE_VERSION,
|
||||
},
|
||||
type: 'module',
|
||||
files: ['dist'],
|
||||
main: './dist/index.umd.js',
|
||||
module: './dist/index.js',
|
||||
types: './dist/index.d.ts',
|
||||
exports: {
|
||||
'.': {
|
||||
types: './dist/index.d.ts',
|
||||
import: './dist/index.js',
|
||||
require: './dist/index.umd.js',
|
||||
types: './dist/index.d.ts',
|
||||
},
|
||||
},
|
||||
scripts: {
|
||||
@@ -51,9 +85,9 @@ const generatePackageJson = (name: string, path: string, hasVite: boolean) => {
|
||||
devDependencies: {
|
||||
'@robonen/tsconfig': 'workspace:*',
|
||||
...(hasVite && {
|
||||
vite: VITE_VERSION,
|
||||
'vite-plugin-dts': VITE_DTS_VERSION,
|
||||
pathe: PATHE_VERSION,
|
||||
vite: viteVersion,
|
||||
'vite-plugin-dts': viteDtsVersion,
|
||||
pathe: patheVersion,
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -132,14 +166,15 @@ const createCommand = defineCommand({
|
||||
|
||||
await mkdir(resolvedPath, { recursive: true });
|
||||
|
||||
writeFile(`${resolvedPath}/package.json`, generatePackageJson(args.name, path, hasVite));
|
||||
writeFile(`${resolvedPath}/jsr.json`, generateJsrJson(args.name));
|
||||
writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig());
|
||||
writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
|
||||
const packageJson = await generatePackageJson(args.name, path, hasVite);
|
||||
await writeFile(`${resolvedPath}/package.json`, packageJson);
|
||||
await writeFile(`${resolvedPath}/jsr.json`, generateJsrJson(args.name));
|
||||
await writeFile(`${resolvedPath}/tsconfig.json`, generateTsConfig());
|
||||
await writeFile(`${resolvedPath}/README.md`, generateReadme(args.name));
|
||||
|
||||
if (hasVite) {
|
||||
mkdir(`${resolvedPath}/src`, { recursive: true });
|
||||
writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig());
|
||||
await mkdir(`${resolvedPath}/src`, { recursive: true });
|
||||
await writeFile(`${resolvedPath}/vite.config.ts`, generateViteConfig());
|
||||
}
|
||||
|
||||
console.log(`Project created successfully`);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@robonen/tsconfig",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Base typescript configuration for projects",
|
||||
"keywords": [
|
||||
@@ -15,9 +15,9 @@
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "packages/tsconfig"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"engines": {
|
||||
"node": ">=22.13.0"
|
||||
"node": ">=24.13.0"
|
||||
},
|
||||
"files": [
|
||||
"**tsconfig.json"
|
||||
@@ -13,6 +13,7 @@
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleDetection": "force",
|
||||
@@ -18,9 +18,9 @@
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "packages/platform"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"engines": {
|
||||
"node": ">=22.13.0"
|
||||
"node": ">=24.13.0"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
@@ -28,14 +28,14 @@
|
||||
],
|
||||
"exports": {
|
||||
"./browsers": {
|
||||
"types": "./dist/browsers.d.ts",
|
||||
"import": "./dist/browsers.mjs",
|
||||
"require": "./dist/browsers.cjs",
|
||||
"types": "./dist/browsers.d.ts"
|
||||
"require": "./dist/browsers.cjs"
|
||||
},
|
||||
"./multi": {
|
||||
"types": "./dist/multi.d.ts",
|
||||
"import": "./dist/multi.mjs",
|
||||
"require": "./dist/multi.cjs",
|
||||
"types": "./dist/multi.d.ts"
|
||||
"require": "./dist/multi.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -45,8 +45,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"jsdom": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"unbuild": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||
"name": "@robonen/stdlib",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.7",
|
||||
"exports": "./src/index.ts"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@robonen/stdlib",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"description": "A collection of tools, utilities, and helpers for TypeScript",
|
||||
"keywords": [
|
||||
@@ -18,22 +18,19 @@
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "packages/stdlib"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"engines": {
|
||||
"node": ">=22.13.0"
|
||||
"node": ">=24.13.0"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -43,9 +40,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"pathe": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"unbuild": "catalog:"
|
||||
}
|
||||
}
|
||||
38
core/stdlib/src/async/retry/index.ts
Normal file
38
core/stdlib/src/async/retry/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface RetryOptions {
|
||||
times?: number;
|
||||
delay?: number;
|
||||
backoff: (options: RetryOptions & { count: number }) => number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name retry
|
||||
* @category Async
|
||||
* @description Retries a function a specified number of times with a delay between each retry
|
||||
*
|
||||
* @param {Promise<unknown>} fn - The function to retry
|
||||
* @param {RetryOptions} options - The options for the retry
|
||||
* @returns {Promise<unknown>} - The result of the function
|
||||
*
|
||||
* @example
|
||||
* const result = await retry(() => {
|
||||
* return fetch('https://jsonplaceholder.typicode.com/todos/1')
|
||||
* .then(response => response.json())
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* const result = await retry(() => {
|
||||
* return fetch('https://jsonplaceholder.typicode.com/todos/1')
|
||||
* .then(response => response.json())
|
||||
* }, { times: 3, delay: 1000 });
|
||||
*
|
||||
*/
|
||||
export async function retry<Return>(
|
||||
fn: () => Promise<Return>,
|
||||
options: RetryOptions
|
||||
) {
|
||||
const {
|
||||
times = 3,
|
||||
} = options;
|
||||
|
||||
let count = 0;
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
* 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));
|
||||
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, type Path } from '../../types';
|
||||
|
||||
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
|
||||
K extends keyof O
|
||||
? O[K]
|
||||
: K extends keyof NonNullable<O>
|
||||
? NonNullable<O>[K]
|
||||
: never;
|
||||
|
||||
export type ExtractFromArray<A extends readonly any[], K> =
|
||||
any[] extends A
|
||||
? A extends readonly (infer T)[]
|
||||
? T | undefined
|
||||
: undefined
|
||||
: K extends keyof A
|
||||
? A[K]
|
||||
: undefined;
|
||||
|
||||
export type ExtractFromCollection<O, K> =
|
||||
K extends []
|
||||
? O
|
||||
: K extends [infer Key, ...infer Rest]
|
||||
? O extends Record<PropertyKey, unknown>
|
||||
? ExtractFromCollection<ExtractFromObject<O, Key>, Rest>
|
||||
: O extends readonly any[]
|
||||
? ExtractFromCollection<ExtractFromArray<O, Key>, Rest>
|
||||
: never
|
||||
: never;
|
||||
|
||||
type Get<O, K> = ExtractFromCollection<O, Path<K>>;
|
||||
|
||||
export function get<O extends Collection, K extends string>(obj: O, path: K) {
|
||||
return path.split('.').reduce((acc, key) => (acc as any)?.[key], obj) as Get<O, K> | undefined;
|
||||
}
|
||||
1
core/stdlib/src/collections/index.ts
Normal file
1
core/stdlib/src/collections/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './get';
|
||||
@@ -5,6 +5,7 @@ export * from './math';
|
||||
export * from './objects';
|
||||
export * from './patterns';
|
||||
export * from './structs';
|
||||
export * from './sync';
|
||||
export * from './text';
|
||||
export * from './types';
|
||||
export * from './utils'
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { AnyFunction } from '../../../types';
|
||||
|
||||
export type Subscriber = AnyFunction;
|
||||
export type EventsRecord = Record<string | symbol, Subscriber>;
|
||||
|
||||
export type EventHandlerMap = Record<PropertyKey, Subscriber>;
|
||||
|
||||
/**
|
||||
* @name PubSub
|
||||
@@ -10,9 +11,9 @@ export type EventsRecord = Record<string | symbol, Subscriber>;
|
||||
*
|
||||
* @since 0.0.2
|
||||
*
|
||||
* @template {EventsRecord} Events
|
||||
* @template Events - Event map where all values are function types
|
||||
*/
|
||||
export class PubSub<Events extends EventsRecord> {
|
||||
export class PubSub<Events extends EventHandlerMap> {
|
||||
/**
|
||||
* Events map
|
||||
*
|
||||
1
core/stdlib/src/sync/index.ts
Normal file
1
core/stdlib/src/sync/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './mutex';
|
||||
94
core/stdlib/src/sync/mutex/index.test.ts
Normal file
94
core/stdlib/src/sync/mutex/index.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { SyncMutex } from '.';
|
||||
|
||||
describe('SyncMutex', () => {
|
||||
let mutex: SyncMutex;
|
||||
|
||||
beforeEach(() => {
|
||||
mutex = new SyncMutex();
|
||||
});
|
||||
|
||||
it('unlocked by default', () => {
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
it('lock the mutex', () => {
|
||||
mutex.lock();
|
||||
|
||||
expect(mutex.isLocked).toBe(true);
|
||||
});
|
||||
|
||||
it('remain locked when locked multiple times', () => {
|
||||
mutex.lock();
|
||||
mutex.lock();
|
||||
|
||||
expect(mutex.isLocked).toBe(true);
|
||||
});
|
||||
|
||||
it('unlock a locked mutex', () => {
|
||||
mutex.lock();
|
||||
mutex.unlock();
|
||||
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
it('remain unlocked when unlocked multiple times', () => {
|
||||
mutex.unlock();
|
||||
mutex.unlock();
|
||||
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
it('reflect the current lock state', () => {
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
mutex.lock();
|
||||
expect(mutex.isLocked).toBe(true);
|
||||
mutex.unlock();
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
|
||||
it('execute a callback when unlocked', async () => {
|
||||
const callback = vi.fn(() => 'done');
|
||||
const result = await mutex.execute(callback);
|
||||
|
||||
expect(result).toBe('done');
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('execute a promise callback when unlocked', async () => {
|
||||
const callback = vi.fn(() => Promise.resolve('done'));
|
||||
const result = await mutex.execute(callback);
|
||||
|
||||
expect(result).toBe('done');
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('execute concurrent callbacks only one at a time', async () => {
|
||||
const callback = vi.fn(() => Promise.resolve('done'));
|
||||
|
||||
const result = await Promise.all([
|
||||
mutex.execute(callback),
|
||||
mutex.execute(callback),
|
||||
mutex.execute(callback),
|
||||
]);
|
||||
|
||||
expect(result).toEqual(['done', undefined, undefined]);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not execute a callback when locked', async () => {
|
||||
const callback = vi.fn(() => 'done');
|
||||
mutex.lock();
|
||||
const result = await mutex.execute(callback);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unlocks after executing a callback', async () => {
|
||||
const callback = vi.fn(() => 'done');
|
||||
await mutex.execute(callback);
|
||||
|
||||
expect(mutex.isLocked).toBe(false);
|
||||
});
|
||||
});
|
||||
47
core/stdlib/src/sync/mutex/index.ts
Normal file
47
core/stdlib/src/sync/mutex/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { MaybePromise } from "../../types";
|
||||
|
||||
/**
|
||||
* @name SyncMutex
|
||||
* @category Utils
|
||||
* @description A simple synchronous mutex to provide more readable locking and unlocking of code blocks
|
||||
*
|
||||
* @example
|
||||
* const mutex = new SyncMutex();
|
||||
*
|
||||
* mutex.lock();
|
||||
*
|
||||
* mutex.unlock();
|
||||
*
|
||||
* const result = await mutex.execute(() => {
|
||||
* // do something
|
||||
* return Promise.resolve('done');
|
||||
* });
|
||||
*
|
||||
* @since 0.0.5
|
||||
*/
|
||||
export class SyncMutex {
|
||||
private state = false;
|
||||
|
||||
public get isLocked() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public lock() {
|
||||
this.state = true;
|
||||
}
|
||||
|
||||
public unlock() {
|
||||
this.state = false;
|
||||
}
|
||||
|
||||
public async execute<T>(callback: () => T) {
|
||||
if (this.isLocked)
|
||||
return;
|
||||
|
||||
this.lock();
|
||||
const result = await callback();
|
||||
this.unlock();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expectTypeOf, it } from "vitest";
|
||||
import type { ClearPlaceholder, ExtractPlaceholders } from "./index";
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
import type { ClearPlaceholder, ExtractPlaceholders } from './index';
|
||||
|
||||
describe('template', () => {
|
||||
describe.skip('template', () => {
|
||||
describe('ClearPlaceholder', () => {
|
||||
it('ignores strings without braces', () => {
|
||||
type actual = ClearPlaceholder<'name'>;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { templateObject } from '.';
|
||||
|
||||
describe.todo('templateObject', () => {
|
||||
describe.skip('templateObject', () => {
|
||||
it('replace template placeholders with corresponding values from args', () => {
|
||||
const template = 'Hello, {names.0}!';
|
||||
const args = { names: ['John'] };
|
||||
73
core/stdlib/src/text/template/index.ts
Normal file
73
core/stdlib/src/text/template/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { get } from '../../collections';
|
||||
import { isFunction, type Path, type PathToType, type Stringable, type Trim, type UnionToIntersection } from '../../types';
|
||||
|
||||
/**
|
||||
* Type of a value that will be used to replace a placeholder in a template.
|
||||
*/
|
||||
export type TemplateValue = Stringable | string;
|
||||
|
||||
/**
|
||||
* Type of a fallback value when a template key is not found.
|
||||
*/
|
||||
export type TemplateFallback = string | ((key: string) => string);
|
||||
|
||||
/**
|
||||
* Type of a template string with placeholders.
|
||||
*/
|
||||
const TEMPLATE_PLACEHOLDER = /\{\s*([^{}]+?)\s*\}/gm;
|
||||
|
||||
/**
|
||||
* Removes the placeholder syntax from a template string.
|
||||
*
|
||||
* @example
|
||||
* type Base = ClearPlaceholder<'{user.name}'>; // 'user.name'
|
||||
* type Unbalanced = ClearPlaceholder<'{user.name'>; // 'user.name'
|
||||
* type Spaces = ClearPlaceholder<'{ user.name }'>; // 'user.name'
|
||||
*/
|
||||
export type ClearPlaceholder<In extends string> =
|
||||
In extends `${string}{${infer Template}`
|
||||
? ClearPlaceholder<Template>
|
||||
: In extends `${infer Template}}${string}`
|
||||
? ClearPlaceholder<Template>
|
||||
: Trim<In>;
|
||||
|
||||
/**
|
||||
* Extracts all placeholders from a template string.
|
||||
*
|
||||
* @example
|
||||
* type Base = ExtractPlaceholders<'Hello {user.name}, {user.addresses.0.street}'>; // 'user.name' | 'user.addresses.0.street'
|
||||
*/
|
||||
export type ExtractPlaceholders<In extends string> =
|
||||
In extends `${infer Before}}${infer After}`
|
||||
? Before extends `${string}{${infer Placeholder}`
|
||||
? ClearPlaceholder<Placeholder> | ExtractPlaceholders<After>
|
||||
: ExtractPlaceholders<After>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Generates a type for a template string with placeholders.
|
||||
*
|
||||
* @example
|
||||
* type Base = GenerateTypes<'Hello {user.name}, your address {user.addresses.0.street}'>; // { user: { name: string; addresses: { 0: { street: string; }; }; }; }
|
||||
* type WithTarget = GenerateTypes<'Hello {user.age}', number>; // { user: { age: number; }; }
|
||||
*/
|
||||
export type GenerateTypes<T extends string, Target = string> = UnionToIntersection<PathToType<Path<T>, Target>>;
|
||||
|
||||
export function templateObject<
|
||||
T extends string,
|
||||
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue>
|
||||
>(template: T, args: A, fallback?: TemplateFallback) {
|
||||
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
|
||||
const value = get(args, key)?.toString();
|
||||
return value !== undefined ? value : (isFunction(fallback) ? fallback(key) : '');
|
||||
});
|
||||
}
|
||||
|
||||
templateObject('Hello {user.name}, your address {user.addresses.0.city}', {
|
||||
user: {
|
||||
name: 'John',
|
||||
addresses: [
|
||||
{ city: 'Kolpa' },
|
||||
],
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user