mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 19:04:46 +00:00
Compare commits
53 Commits
feat/stdli
...
855f57cf2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
855f57cf2e | ||
| 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 |
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -16,14 +16,14 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- 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@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
@@ -31,5 +31,11 @@ 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 build && pnpm test
|
run: pnpm test
|
||||||
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
name: Check version changes and publish
|
name: Check version changes and publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|||||||
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));
|
||||||
52
configs/oxlint/package.json
Normal file
52
configs/oxlint/package.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
6
configs/oxlint/tsconfig.json
Normal file
6
configs/oxlint/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "."
|
||||||
|
}
|
||||||
|
}
|
||||||
9
configs/oxlint/tsdown.config.ts
Normal file
9
configs/oxlint/tsdown.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'tsdown';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
hash: false,
|
||||||
|
});
|
||||||
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,45 +1,27 @@
|
|||||||
# @robonen/tsconfig
|
# @robonen/tsconfig
|
||||||
|
|
||||||
Базовый конфигурационный файл для TypeScript
|
Shared base TypeScript configuration.
|
||||||
|
|
||||||
## Установка
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install -D @robonen/tsconfig
|
pnpm install -D @robonen/tsconfig
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Extend from it in your `tsconfig.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"extends": "@robonen/tsconfig/tsconfig.json"
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Описание основных параметров
|
## What's Included
|
||||||
|
|
||||||
```json
|
- **Target / Module**: ESNext with Bundler resolution
|
||||||
{
|
- **Strict mode**: `strict`, `noUncheckedIndexedAccess`
|
||||||
"module": "Preserve", // использовать ту же версию модуля, что и сборщик
|
- **Module safety**: `verbatimModuleSyntax`, `isolatedModules`
|
||||||
"noEmit": true, // не генерировать файлы
|
- **Declarations**: `declaration` enabled
|
||||||
"moduleResolution": "Bundler", // разрешение модулей на основе сборщика
|
- **Interop**: `esModuleInterop`, `allowJs`, `resolveJsonModule`
|
||||||
"target": "ESNext", // целевая версия JavaScript
|
|
||||||
|
|
||||||
|
|
||||||
"skipLibCheck": true, // не проверять типы, заданные во всех файлах описания типов (*.d.ts)
|
|
||||||
"esModuleInterop": true, // создать хелперы __importStar и __importDefault для обеспечения совместимости с экосистемой Babel и включить allowSyntheticDefaultImports для совместимости с системой типов
|
|
||||||
"allowSyntheticDefaultImports": true, // разрешить импортировать модули не имеющие внутри себя "import default"
|
|
||||||
"allowJs": true, // разрешить импортировать файлы JavaScript
|
|
||||||
"resolveJsonModule": true, // разрешить импортировать файлы JSON
|
|
||||||
"moduleDetection": "force", // заставляет TypeScript рассматривать все файлы как модули. Это помогает избежать ошибок cannot redeclare block-scoped variable»
|
|
||||||
"isolatedModules": true, // орабатывать каждый файл, как отдельный изолированный модуль
|
|
||||||
"removeComments": false, // удалять комментарии из исходного кода
|
|
||||||
"verbatimModuleSyntax": true, // сохранять синтаксис модулей в исходном коде (важно при импорте типов)
|
|
||||||
"useDefineForClassFields": true, // использование классов стандарта TC39, а не TypeScript
|
|
||||||
"strict": true, // включить все строгие проверки (noImplicitAny, noImplicitThis, alwaysStrict, strictNullChecks, strictFunctionTypes, strictPropertyInitialization)
|
|
||||||
"noUncheckedIndexedAccess": true, // запрещает доступ к массиву или объекту без предварительной проверки того, определен ли он
|
|
||||||
"declaration": true, // генерировать файлы описания типов (*.d.ts)
|
|
||||||
|
|
||||||
"composite": true, // указывает TypeScript создавать файлы .tsbuildinfo. Это сообщает TypeScript, что ваш проект является частью монорепозитория, а также помогает кэшировать сборки для более быстрой работы
|
|
||||||
"sourceMap": true, // генерировать карту исходного кода
|
|
||||||
"declarationMap": true // генерировать карту исходного кода для файлов описания типов (*.d.ts)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -15,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@10.13.1",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.17.1"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"**tsconfig.json"
|
"**tsconfig.json"
|
||||||
|
|||||||
@@ -1 +1,23 @@
|
|||||||
# @robonen/platform
|
# @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';
|
||||||
|
```
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { defineBuildConfig } from 'unbuild';
|
|
||||||
|
|
||||||
export default defineBuildConfig({
|
|
||||||
entries: [
|
|
||||||
'src/browsers',
|
|
||||||
'src/multi',
|
|
||||||
],
|
|
||||||
clean: true,
|
|
||||||
declaration: true,
|
|
||||||
rollup: {
|
|
||||||
emitCJS: true,
|
|
||||||
esbuild: {
|
|
||||||
// minify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -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.13.1",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.17.1"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -29,22 +29,25 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
"./browsers": {
|
"./browsers": {
|
||||||
"types": "./dist/browsers.d.ts",
|
"types": "./dist/browsers.d.ts",
|
||||||
"import": "./dist/browsers.mjs",
|
"import": "./dist/browsers.js",
|
||||||
"require": "./dist/browsers.cjs"
|
"require": "./dist/browsers.cjs"
|
||||||
},
|
},
|
||||||
"./multi": {
|
"./multi": {
|
||||||
"types": "./dist/multi.d.ts",
|
"types": "./dist/multi.d.ts",
|
||||||
"import": "./dist/multi.mjs",
|
"import": "./dist/multi.js",
|
||||||
"require": "./dist/multi.cjs"
|
"require": "./dist/multi.cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"build": "unbuild"
|
"build": "tsdown"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"unbuild": "catalog:"
|
"oxlint": "catalog:",
|
||||||
|
"tsdown": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*
|
*
|
||||||
* @since 0.0.3
|
* @since 0.0.3
|
||||||
*/
|
*/
|
||||||
export function focusGuard(namespace: string = 'focus-guard') {
|
export function focusGuard(namespace = 'focus-guard') {
|
||||||
const guardAttr = `data-${namespace}`;
|
const guardAttr = `data-${namespace}`;
|
||||||
|
|
||||||
const createGuard = () => {
|
const createGuard = () => {
|
||||||
@@ -39,7 +39,7 @@ export function focusGuard(namespace: string = 'focus-guard') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGuardAttrs(namespace: string) {
|
export function createGuardAttrs(namespace = 'focus-guard') {
|
||||||
const element = document.createElement('span');
|
const element = document.createElement('span');
|
||||||
|
|
||||||
element.setAttribute(namespace, '');
|
element.setAttribute(namespace, '');
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// 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
|
||||||
|
|||||||
12
core/platform/tsdown.config.ts
Normal file
12
core/platform/tsdown.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1 +1,32 @@
|
|||||||
# @robonen/stdlib
|
# @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';
|
||||||
|
```
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { defineBuildConfig } from 'unbuild';
|
|
||||||
|
|
||||||
export default defineBuildConfig({
|
|
||||||
rollup: {
|
|
||||||
esbuild: {
|
|
||||||
// minify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
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));
|
||||||
@@ -18,9 +18,9 @@
|
|||||||
"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.13.1",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.17.1"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -29,18 +29,20 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"build": "unbuild"
|
"build": "tsdown"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"pathe": "catalog:",
|
"oxlint": "catalog:",
|
||||||
"unbuild": "catalog:"
|
"tsdown": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export type AsyncPoolOptions = {
|
export interface AsyncPoolOptions {
|
||||||
concurrency?: number;
|
concurrency?: number;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// eslint-disable
|
||||||
export interface RetryOptions {
|
export interface RetryOptions {
|
||||||
times?: number;
|
times?: number;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface BitVector {
|
export interface BitVectorLike {
|
||||||
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 BitVector {
|
|||||||
*
|
*
|
||||||
* @since 0.0.3
|
* @since 0.0.3
|
||||||
*/
|
*/
|
||||||
export class BitVector extends Uint8Array implements BitVector {
|
export class BitVector extends Uint8Array implements BitVectorLike {
|
||||||
constructor(size: number) {
|
constructor(size: number) {
|
||||||
super(Math.ceil(size / 8));
|
super(Math.ceil(size / 8));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Collection, type Path } from '../../types';
|
import type { Collection, 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 readonly (infer T)[]
|
? A extends ReadonlyArray<infer T>
|
||||||
? T | undefined
|
? T | undefined
|
||||||
: undefined
|
: undefined
|
||||||
: K extends keyof A
|
: K extends keyof A
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ describe('clamp', () => {
|
|||||||
|
|
||||||
it('handle NaN and Infinity', () => {
|
it('handle NaN and Infinity', () => {
|
||||||
// value is NaN
|
// value is NaN
|
||||||
expect(clamp(NaN, 0, 100)).toBe(NaN);
|
expect(clamp(Number.NaN, 0, 100)).toBe(Number.NaN);
|
||||||
|
|
||||||
// min is NaN
|
// min is NaN
|
||||||
expect(clamp(50, NaN, 100)).toBe(NaN);
|
expect(clamp(50, Number.NaN, 100)).toBe(Number.NaN);
|
||||||
|
|
||||||
// max is NaN
|
// max is NaN
|
||||||
expect(clamp(50, 0, NaN)).toBe(NaN);
|
expect(clamp(50, 0, Number.NaN)).toBe(Number.NaN);
|
||||||
|
|
||||||
// value is Infinity
|
// value is Infinity
|
||||||
expect(clamp(Infinity, 0, 100)).toBe(100);
|
expect(clamp(Infinity, 0, 100)).toBe(100);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isArray, type Arrayable } from '../../types';
|
import { isArray } from '../../types';
|
||||||
|
import type { Arrayable } from '../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name omit
|
* @name omit
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isArray, type Arrayable } from '../../types';
|
import { isArray } from '../../types';
|
||||||
|
import type { Arrayable } from '../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name pick
|
* @name pick
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { last } from '../../arrays';
|
import { last } from '../../arrays';
|
||||||
import { isArray } from '../../types';
|
import { isArray } from '../../types';
|
||||||
|
|
||||||
export type StackOptions = {
|
export interface StackOptions {
|
||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name Stack
|
* @name Stack
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { MaybePromise } from "../../types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name SyncMutex
|
* @name SyncMutex
|
||||||
* @category Utils
|
* @category Utils
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { get } from '../../collections';
|
import { get } from '../../collections';
|
||||||
import { isFunction, type Path, type PathToType, type Stringable, type Trim, type UnionToIntersection } from '../../types';
|
import { isFunction } from '../../types';
|
||||||
|
import type { Collection, Path, PathToType, Stringable, Trim, UnionToIntersection } from '../../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type of a value that will be used to replace a placeholder in a template.
|
* Type of a value that will be used to replace a placeholder in a template.
|
||||||
@@ -55,7 +56,7 @@ export type GenerateTypes<T extends string, Target = string> = UnionToIntersecti
|
|||||||
|
|
||||||
export function templateObject<
|
export function templateObject<
|
||||||
T extends string,
|
T extends string,
|
||||||
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue>
|
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue> & Collection
|
||||||
>(template: T, args: A, fallback?: TemplateFallback) {
|
>(template: T, args: A, fallback?: TemplateFallback) {
|
||||||
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
|
return template.replace(TEMPLATE_PLACEHOLDER, (_, key) => {
|
||||||
const value = get(args, key)?.toString();
|
const value = get(args, key)?.toString();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export type Trigrams = Map<string, number>;
|
|||||||
* @since 0.0.1
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
export function trigramProfile(text: string): Trigrams {
|
export function trigramProfile(text: string): Trigrams {
|
||||||
text = '\n\n' + text + '\n\n';
|
text = `\n\n${text}\n\n`;
|
||||||
|
|
||||||
const trigrams = new Map<string, number>();
|
const trigrams = new Map<string, number>();
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('casts', () => {
|
|||||||
expect(toString(null)).toBe('[object Null]');
|
expect(toString(null)).toBe('[object Null]');
|
||||||
expect(toString(/abc/)).toBe('[object RegExp]');
|
expect(toString(/abc/)).toBe('[object RegExp]');
|
||||||
expect(toString(new Date())).toBe('[object Date]');
|
expect(toString(new Date())).toBe('[object Date]');
|
||||||
expect(toString(new Error())).toBe('[object Error]');
|
expect(toString(new Error('test'))).toBe('[object Error]');
|
||||||
expect(toString(new Promise(() => {}))).toBe('[object Promise]');
|
expect(toString(new Promise(() => {}))).toBe('[object Promise]');
|
||||||
expect(toString(new Map())).toBe('[object Map]');
|
expect(toString(new Map())).toBe('[object Map]');
|
||||||
expect(toString(new Set())).toBe('[object Set]');
|
expect(toString(new Set())).toBe('[object Set]');
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ describe('complex', () => {
|
|||||||
|
|
||||||
describe('isError', () => {
|
describe('isError', () => {
|
||||||
it('true if the value is an error', () => {
|
it('true if the value is an error', () => {
|
||||||
expect(isError(new Error())).toBe(true);
|
expect(isError(new Error('test'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('false if the value is not an error', () => {
|
it('false if the value is not an error', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { toString } from '.';
|
import { toString } from './casts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name isFunction
|
* @name isFunction
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { toString } from '.';
|
import { toString } from './casts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name isObject
|
* @name isObject
|
||||||
|
|||||||
@@ -35,35 +35,35 @@ describe('collections', () => {
|
|||||||
describe('PathToType', () => {
|
describe('PathToType', () => {
|
||||||
it('convert simple object path', () => {
|
it('convert simple object path', () => {
|
||||||
type actual = PathToType<['user', 'name']>;
|
type actual = PathToType<['user', 'name']>;
|
||||||
type expected = { user: { name: unknown } };
|
interface expected { user: { name: unknown } }
|
||||||
|
|
||||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('convert simple array path', () => {
|
it('convert simple array path', () => {
|
||||||
type actual = PathToType<['user', '0']>;
|
type actual = PathToType<['user', '0']>;
|
||||||
type expected = { user: unknown[] };
|
interface expected { user: unknown[] }
|
||||||
|
|
||||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('convert complex object path', () => {
|
it('convert complex object path', () => {
|
||||||
type actual = PathToType<['user', 'addresses', '0', 'street']>;
|
type actual = PathToType<['user', 'addresses', '0', 'street']>;
|
||||||
type expected = { user: { addresses: { street: unknown }[] } };
|
interface expected { user: { addresses: Array<{ street: unknown }> } }
|
||||||
|
|
||||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('convert double dot path', () => {
|
it('convert double dot path', () => {
|
||||||
type actual = PathToType<['user', '', 'name']>;
|
type actual = PathToType<['user', '', 'name']>;
|
||||||
type expected = { user: { '': { name: unknown } } };
|
interface expected { user: { '': { name: unknown } } }
|
||||||
|
|
||||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('convert to custom target', () => {
|
it('convert to custom target', () => {
|
||||||
type actual = PathToType<['user', 'name'], string>;
|
type actual = PathToType<['user', 'name'], string>;
|
||||||
type expected = { user: { name: string } };
|
interface expected { user: { name: string } }
|
||||||
|
|
||||||
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
expectTypeOf<actual>().toEqualTypeOf<expected>();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type PathToType<T extends string[], Target = unknown> =
|
|||||||
T extends [infer Head, ...infer Rest]
|
T extends [infer Head, ...infer Rest]
|
||||||
? Head extends `${number}`
|
? Head extends `${number}`
|
||||||
? Rest extends string[]
|
? Rest extends string[]
|
||||||
? PathToType<Rest, Target>[]
|
? Array<PathToType<Rest, Target>>
|
||||||
: never
|
: never
|
||||||
: Rest extends string[]
|
: Rest extends string[]
|
||||||
? { [K in Head & string]: PathToType<Rest, Target> }
|
? { [K in Head & string]: PathToType<Rest, Target> }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe('string', () => {
|
|||||||
expectTypeOf(Number(1)).toExtend<Stringable>();
|
expectTypeOf(Number(1)).toExtend<Stringable>();
|
||||||
expectTypeOf(String(1)).toExtend<Stringable>();
|
expectTypeOf(String(1)).toExtend<Stringable>();
|
||||||
expectTypeOf(Symbol()).toExtend<Stringable>();
|
expectTypeOf(Symbol()).toExtend<Stringable>();
|
||||||
expectTypeOf(new Array(1)).toExtend<Stringable>();
|
expectTypeOf([1]).toExtend<Stringable>();
|
||||||
expectTypeOf(new Object()).toExtend<Stringable>();
|
expectTypeOf(new Object()).toExtend<Stringable>();
|
||||||
expectTypeOf(new Date()).toExtend<Stringable>();
|
expectTypeOf(new Date()).toExtend<Stringable>();
|
||||||
});
|
});
|
||||||
|
|||||||
9
core/stdlib/tsdown.config.ts
Normal file
9
core/stdlib/tsdown.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'tsdown';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
hash: false,
|
||||||
|
});
|
||||||
7
core/stdlib/vitest.config.ts
Normal file
7
core/stdlib/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1 +1,21 @@
|
|||||||
# @robonen/renovate
|
# @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 1–3 AM)
|
||||||
@@ -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.13.1",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.17.1"
|
"node": ">=22.22.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": "^41.43.5"
|
"renovate": "^43.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -15,23 +15,24 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/robonen/tools.git"
|
"url": "git+https://github.com/robonen/tools.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.13.1",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.17.1"
|
"node": ">=22.22.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.19.11",
|
||||||
"citty": "^0.1.6",
|
|
||||||
"jiti": "^2.5.1",
|
|
||||||
"scule": "^1.3.0",
|
|
||||||
"jsdom": "catalog:",
|
|
||||||
"vitest": "catalog:",
|
|
||||||
"@vitest/coverage-v8": "catalog:",
|
"@vitest/coverage-v8": "catalog:",
|
||||||
"@vitest/ui": "catalog:"
|
"@vitest/ui": "catalog:",
|
||||||
|
"citty": "^0.2.1",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"jsdom": "catalog:",
|
||||||
|
"scule": "^1.3.0",
|
||||||
|
"vitest": "catalog:"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm -r build",
|
"build": "pnpm -r build",
|
||||||
|
"lint": "pnpm -r lint",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"create": "jiti ./bin/cli.ts"
|
"create": "jiti ./bin/cli.ts"
|
||||||
|
|||||||
7408
pnpm-lock.yaml
generated
7408
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,15 @@ packages:
|
|||||||
- core/*
|
- core/*
|
||||||
- infra/*
|
- infra/*
|
||||||
- web/*
|
- web/*
|
||||||
|
- docs
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
'@vitest/coverage-v8': ^3.2.4
|
'@vitest/coverage-v8': ^4.0.18
|
||||||
'@vue/test-utils': ^2.4.6
|
'@vue/test-utils': ^2.4.6
|
||||||
jsdom: ^26.1.0
|
jsdom: ^28.0.0
|
||||||
pathe: ^2.0.3
|
oxlint: ^1.47.0
|
||||||
unbuild: 3.6.0
|
tsdown: ^0.20.3
|
||||||
vitest: ^3.2.4
|
vitest: ^4.0.18
|
||||||
'@vitest/ui': ^3.2.4
|
'@vitest/ui': ^4.0.18
|
||||||
vue: ^3.5.18
|
vue: ^3.5.28
|
||||||
|
nuxt: ^4.3.1
|
||||||
|
|||||||
@@ -3,16 +3,11 @@ import { defineConfig } from 'vitest/config';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
projects: [
|
projects: [
|
||||||
{
|
'configs/oxlint/vitest.config.ts',
|
||||||
extends: true,
|
'core/stdlib/vitest.config.ts',
|
||||||
test: {
|
'core/platform/vitest.config.ts',
|
||||||
typecheck: {
|
'web/vue/vitest.config.ts',
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
environment: 'jsdom',
|
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
include: ['core/*', 'web/*'],
|
include: ['core/*', 'web/*'],
|
||||||
|
|||||||
@@ -1 +1,28 @@
|
|||||||
# @robonen/vue
|
# @robonen/vue
|
||||||
|
|
||||||
|
Collection of composables and utilities for Vue 3.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install @robonen/vue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Composables
|
||||||
|
|
||||||
|
| Category | Composables |
|
||||||
|
| -------------- | ------------------------------------------------------------------ |
|
||||||
|
| **browser** | `useEventListener`, `useFocusGuard`, `useSupported` |
|
||||||
|
| **component** | `unrefElement`, `useRenderCount`, `useRenderInfo` |
|
||||||
|
| **lifecycle** | `tryOnBeforeMount`, `tryOnMounted`, `tryOnScopeDispose`, `useMounted` |
|
||||||
|
| **math** | `useClamp` |
|
||||||
|
| **reactivity** | `broadcastedRef`, `useCached`, `useLastChanged`, `useSyncRefs` |
|
||||||
|
| **state** | `useAppSharedState`, `useAsyncState`, `useContextFactory`, `useCounter`, `useInjectionStore`, `useToggle` |
|
||||||
|
| **storage** | `useLocalStorage`, `useSessionStorage`, `useStorage`, `useStorageAsync` |
|
||||||
|
| **utilities** | `useOffsetPagination` |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useToggle, useEventListener } from '@robonen/vue';
|
||||||
|
```
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { defineBuildConfig } from 'unbuild';
|
|
||||||
|
|
||||||
export default defineBuildConfig({
|
|
||||||
externals: ['vue'],
|
|
||||||
rollup: {
|
|
||||||
inlineDependencies: true,
|
|
||||||
esbuild: {
|
|
||||||
// minify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
4
web/vue/oxlint.config.ts
Normal file
4
web/vue/oxlint.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
import { compose, base, typescript, vue, vitest, imports } from '@robonen/oxlint';
|
||||||
|
|
||||||
|
export default defineConfig(compose(base, typescript, vue, vitest, imports));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/vue",
|
"name": "@robonen/vue",
|
||||||
"version": "0.0.9",
|
"version": "0.0.11",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Collection of powerful tools for Vue",
|
"description": "Collection of powerful tools for Vue",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
"url": "git+https://github.com/robonen/tools.git",
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
"directory": "./packages/vue"
|
"directory": "./packages/vue"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.13.1",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.17.1"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -27,23 +27,26 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.js",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.cjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"build": "unbuild"
|
"build": "tsdown"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@robonen/platform": "workspace:*",
|
"@robonen/oxlint": "workspace:*",
|
||||||
"@robonen/stdlib": "workspace:*",
|
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"@vue/test-utils": "catalog:",
|
"@vue/test-utils": "catalog:",
|
||||||
"unbuild": "catalog:"
|
"oxlint": "catalog:",
|
||||||
|
"tsdown": "catalog:"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@robonen/platform": "workspace:*",
|
||||||
|
"@robonen/stdlib": "workspace:*",
|
||||||
"vue": "catalog:"
|
"vue": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
web/vue/src/composables/browser/index.ts
Normal file
3
web/vue/src/composables/browser/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './useEventListener';
|
||||||
|
export * from './useFocusGuard';
|
||||||
|
export * from './useSupported';
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { isArray, isString, noop, type Arrayable, type VoidFunction } from '@robonen/stdlib';
|
import { isArray, isString, noop } from '@robonen/stdlib';
|
||||||
|
import type { Arrayable, VoidFunction } from '@robonen/stdlib';
|
||||||
import type { MaybeRefOrGetter } from 'vue';
|
import type { MaybeRefOrGetter } from 'vue';
|
||||||
import { defaultWindow } from '../..';
|
import { defaultWindow } from '@/types';
|
||||||
|
|
||||||
// TODO: wip
|
// TODO: wip
|
||||||
|
|
||||||
@@ -9,9 +10,7 @@ interface InferEventTarget<Events> {
|
|||||||
removeEventListener: (event: Events, listener?: any, options?: any) => any;
|
removeEventListener: (event: Events, listener?: any, options?: any) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeneralEventListener<E = Event> {
|
export type GeneralEventListener<E = Event> = (evt: E) => void;
|
||||||
(evt: E): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WindowEventName = keyof WindowEventMap;
|
export type WindowEventName = keyof WindowEventMap;
|
||||||
export type DocumentEventName = keyof DocumentEventMap;
|
export type DocumentEventName = keyof DocumentEventMap;
|
||||||
@@ -19,7 +18,7 @@ export type ElementEventName = keyof HTMLElementEventMap;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useEventListener
|
* @name useEventListener
|
||||||
* @category Elements
|
* @category Browser
|
||||||
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
*
|
*
|
||||||
* Overload 1: Omitted window target
|
* Overload 1: Omitted window target
|
||||||
@@ -32,7 +31,7 @@ export function useEventListener<E extends WindowEventName>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useEventListener
|
* @name useEventListener
|
||||||
* @category Elements
|
* @category Browser
|
||||||
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
*
|
*
|
||||||
* Overload 2: Explicit window target
|
* Overload 2: Explicit window target
|
||||||
@@ -46,7 +45,7 @@ export function useEventListener<E extends WindowEventName>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useEventListener
|
* @name useEventListener
|
||||||
* @category Elements
|
* @category Browser
|
||||||
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
*
|
*
|
||||||
* Overload 3: Explicit document target
|
* Overload 3: Explicit document target
|
||||||
@@ -60,7 +59,7 @@ export function useEventListener<E extends DocumentEventName>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useEventListener
|
* @name useEventListener
|
||||||
* @category Elements
|
* @category Browser
|
||||||
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
*
|
*
|
||||||
* Overload 4: Explicit HTMLElement target
|
* Overload 4: Explicit HTMLElement target
|
||||||
@@ -74,7 +73,7 @@ export function useEventListener<E extends ElementEventName>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useEventListener
|
* @name useEventListener
|
||||||
* @category Elements
|
* @category Browser
|
||||||
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
*
|
*
|
||||||
* Overload 5: Custom target with inferred event type
|
* Overload 5: Custom target with inferred event type
|
||||||
@@ -84,11 +83,11 @@ export function useEventListener<Names extends string, EventType = Event>(
|
|||||||
event: Arrayable<Names>,
|
event: Arrayable<Names>,
|
||||||
listener: Arrayable<GeneralEventListener<EventType>>,
|
listener: Arrayable<GeneralEventListener<EventType>>,
|
||||||
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
|
||||||
)
|
): VoidFunction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useEventListener
|
* @name useEventListener
|
||||||
* @category Elements
|
* @category Browser
|
||||||
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
* @description Registers an event listener using the `addEventListener` on mounted and removes it automatically on unmounted
|
||||||
*
|
*
|
||||||
* Overload 6: Custom event target fallback
|
* Overload 6: Custom event target fallback
|
||||||
@@ -104,13 +103,13 @@ export function useEventListener(...args: any[]) {
|
|||||||
let target: MaybeRefOrGetter<EventTarget> | undefined;
|
let target: MaybeRefOrGetter<EventTarget> | undefined;
|
||||||
let events: Arrayable<string>;
|
let events: Arrayable<string>;
|
||||||
let listeners: Arrayable<Function>;
|
let listeners: Arrayable<Function>;
|
||||||
let options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined;
|
let _options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined;
|
||||||
|
|
||||||
if (isString(args[0]) || isArray(args[0])) {
|
if (isString(args[0]) || isArray(args[0])) {
|
||||||
[events, listeners, options] = args;
|
[events, listeners, _options] = args;
|
||||||
target = defaultWindow;
|
target = defaultWindow;
|
||||||
} else {
|
} else {
|
||||||
[target, events, listeners, options] = args;
|
[target, events, listeners, _options] = args;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target)
|
if (!target)
|
||||||
@@ -124,13 +123,16 @@ export function useEventListener(...args: any[]) {
|
|||||||
|
|
||||||
const cleanups: Function[] = [];
|
const cleanups: Function[] = [];
|
||||||
|
|
||||||
const cleanup = () => {
|
const _cleanup = () => {
|
||||||
cleanups.forEach(fn => fn());
|
cleanups.forEach(fn => fn());
|
||||||
cleanups.length = 0;
|
cleanups.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const register = (el: any, event: string, listener: any, options: any) => {
|
const _register = (el: any, event: string, listener: any, options: any) => {
|
||||||
el.addEventListener(event, listener, options);
|
el.addEventListener(event, listener, options);
|
||||||
return () => el.removeEventListener(event, listener, options);
|
return () => el.removeEventListener(event, listener, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _cleanup;
|
||||||
|
void _register;
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ const setupFocusGuard = (namespace?: string) => {
|
|||||||
const getFocusGuards = (namespace: string) =>
|
const getFocusGuards = (namespace: string) =>
|
||||||
document.querySelectorAll(`[data-${namespace}]`);
|
document.querySelectorAll(`[data-${namespace}]`);
|
||||||
|
|
||||||
describe('useFocusGuard', () => {
|
describe(useFocusGuard, () => {
|
||||||
let component: ReturnType<typeof setupFocusGuard>;
|
let component: ReturnType<typeof setupFocusGuard>;
|
||||||
const namespace = 'test-guard';
|
const namespace = 'test-guard';
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ describe('useFocusGuard', () => {
|
|||||||
component = setupFocusGuard(namespace);
|
component = setupFocusGuard(namespace);
|
||||||
|
|
||||||
const guards = getFocusGuards(namespace);
|
const guards = getFocusGuards(namespace);
|
||||||
expect(guards.length).toBe(2);
|
expect(guards).toHaveLength(2);
|
||||||
|
|
||||||
guards.forEach((guard) => {
|
guards.forEach((guard) => {
|
||||||
expect(guard.getAttribute('tabindex')).toBe('0');
|
expect(guard.getAttribute('tabindex')).toBe('0');
|
||||||
@@ -46,7 +46,7 @@ describe('useFocusGuard', () => {
|
|||||||
|
|
||||||
component.unmount();
|
component.unmount();
|
||||||
|
|
||||||
expect(getFocusGuards(namespace).length).toBe(0);
|
expect(getFocusGuards(namespace)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('correctly manage multiple instances with the same namespace', () => {
|
it('correctly manage multiple instances with the same namespace', () => {
|
||||||
@@ -54,16 +54,16 @@ describe('useFocusGuard', () => {
|
|||||||
const wrapper2 = setupFocusGuard(namespace);
|
const wrapper2 = setupFocusGuard(namespace);
|
||||||
|
|
||||||
// Guards should not be duplicated
|
// Guards should not be duplicated
|
||||||
expect(getFocusGuards(namespace).length).toBe(2);
|
expect(getFocusGuards(namespace)).toHaveLength(2);
|
||||||
|
|
||||||
wrapper1.unmount();
|
wrapper1.unmount();
|
||||||
|
|
||||||
// Second instance still keeps the guards
|
// Second instance still keeps the guards
|
||||||
expect(getFocusGuards(namespace).length).toBe(2);
|
expect(getFocusGuards(namespace)).toHaveLength(2);
|
||||||
|
|
||||||
wrapper2.unmount();
|
wrapper2.unmount();
|
||||||
|
|
||||||
// No guards left after all instances are unmounted
|
// No guards left after all instances are unmounted
|
||||||
expect(getFocusGuards(namespace).length).toBe(0);
|
expect(getFocusGuards(namespace)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -6,7 +6,7 @@ let counter = 0;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useFocusGuard
|
* @name useFocusGuard
|
||||||
* @category Utilities
|
* @category Browser
|
||||||
* @description Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior
|
* @description Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior
|
||||||
*
|
*
|
||||||
* @param {string} [namespace] - A namespace to group the focus guards
|
* @param {string} [namespace] - A namespace to group the focus guards
|
||||||
@@ -11,14 +11,14 @@ const ComponentStub = defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const isSupported = useSupported(() => props.location in window);
|
const isSupported = useSupported(() => props.location in globalThis);
|
||||||
|
|
||||||
return { isSupported };
|
return { isSupported };
|
||||||
},
|
},
|
||||||
template: `<div>{{ isSupported }}</div>`,
|
template: `<div>{{ isSupported }}</div>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useSupported', () => {
|
describe(useSupported, () => {
|
||||||
it('return whether the feature is supported', async () => {
|
it('return whether the feature is supported', async () => {
|
||||||
const component = mount(ComponentStub);
|
const component = mount(ComponentStub);
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useMounted } from '../useMounted';
|
import { useMounted } from '@/composables/lifecycle/useMounted';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useSupported
|
* @name useSupported
|
||||||
* @category Utilities
|
* @category Browser
|
||||||
* @description SSR-friendly way to check if a feature is supported
|
* @description SSR-friendly way to check if a feature is supported
|
||||||
*
|
*
|
||||||
* @param {Function} feature The feature to check for support
|
* @param {Function} feature The feature to check for support
|
||||||
@@ -22,6 +22,7 @@ export function useSupported(feature: () => unknown) {
|
|||||||
|
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
// add reactive dependency on isMounted
|
// add reactive dependency on isMounted
|
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
isMounted.value;
|
isMounted.value;
|
||||||
|
|
||||||
return Boolean(feature());
|
return Boolean(feature());
|
||||||
3
web/vue/src/composables/component/index.ts
Normal file
3
web/vue/src/composables/component/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './unrefElement';
|
||||||
|
export * from './useRenderCount';
|
||||||
|
export * from './useRenderInfo';
|
||||||
61
web/vue/src/composables/component/unrefElement/index.test.ts
Normal file
61
web/vue/src/composables/component/unrefElement/index.test.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { computed, defineComponent, nextTick, ref, shallowRef } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { unrefElement } from '.';
|
||||||
|
|
||||||
|
describe(unrefElement, () => {
|
||||||
|
it('returns a plain element when passed a raw element', () => {
|
||||||
|
const htmlEl = document.createElement('div');
|
||||||
|
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
|
||||||
|
expect(unrefElement(htmlEl)).toBe(htmlEl);
|
||||||
|
expect(unrefElement(svgEl)).toBe(svgEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns element when passed a ref or shallowRef to an element', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const elRef = ref<HTMLElement | null>(el);
|
||||||
|
const shallowElRef = shallowRef<HTMLElement | null>(el);
|
||||||
|
|
||||||
|
expect(unrefElement(elRef)).toBe(el);
|
||||||
|
expect(unrefElement(shallowElRef)).toBe(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns element when passed a computed ref or getter function', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const computedElRef = computed(() => el);
|
||||||
|
const elGetter = () => el;
|
||||||
|
|
||||||
|
expect(unrefElement(computedElRef)).toBe(el);
|
||||||
|
expect(unrefElement(elGetter)).toBe(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns component $el when passed a component instance', async () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
template: `<span class="child-el">child</span>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Parent = defineComponent({
|
||||||
|
components: { Child },
|
||||||
|
template: `<Child ref="childRef" />`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = mount(Parent);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const childInstance = (wrapper.vm as any).$refs.childRef;
|
||||||
|
const result = unrefElement(childInstance);
|
||||||
|
|
||||||
|
expect(result).toBe(childInstance.$el);
|
||||||
|
expect((result as HTMLElement).classList.contains('child-el')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles null and undefined values', () => {
|
||||||
|
expect(unrefElement(undefined)).toBe(undefined);
|
||||||
|
expect(unrefElement(null)).toBe(null);
|
||||||
|
expect(unrefElement(ref<null>(null))).toBe(null);
|
||||||
|
expect(unrefElement(ref<undefined>(undefined))).toBe(undefined);
|
||||||
|
expect(unrefElement(() => null)).toBe(null);
|
||||||
|
expect(unrefElement(() => undefined)).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
33
web/vue/src/composables/component/unrefElement/index.ts
Normal file
33
web/vue/src/composables/component/unrefElement/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter } from 'vue';
|
||||||
|
import { toValue } from 'vue';
|
||||||
|
|
||||||
|
export type VueInstance = ComponentPublicInstance;
|
||||||
|
export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null;
|
||||||
|
|
||||||
|
export type MaybeElementRef<El extends MaybeElement = MaybeElement> = MaybeRef<El>;
|
||||||
|
export type MaybeComputedElementRef<El extends MaybeElement = MaybeElement> = MaybeRefOrGetter<El>;
|
||||||
|
|
||||||
|
export type UnRefElementReturn<T extends MaybeElement = MaybeElement> = T extends VueInstance ? Exclude<MaybeElement, VueInstance> : T | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name unrefElement
|
||||||
|
* @category Component
|
||||||
|
* @description Unwraps a Vue element reference to get the underlying instance or DOM element.
|
||||||
|
*
|
||||||
|
* @param {MaybeComputedElementRef<El>} elRef - The element reference to unwrap.
|
||||||
|
* @returns {UnRefElementReturn<El>} - The unwrapped element or undefined.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const element = useTemplateRef<HTMLElement>('element');
|
||||||
|
* const result = unrefElement(element); // result is the element instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const component = useTemplateRef<Component>('component');
|
||||||
|
* const result = unrefElement(component); // result is the component instance
|
||||||
|
*
|
||||||
|
* @since 0.0.11
|
||||||
|
*/
|
||||||
|
export function unrefElement<El extends MaybeElement>(elRef: MaybeComputedElementRef<El>): UnRefElementReturn<El> {
|
||||||
|
const plain = toValue(elRef);
|
||||||
|
return (plain as VueInstance)?.$el ?? plain;
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ const ComponentStub = defineComponent({
|
|||||||
template: `<div>{{ visibleCount }}</div>`,
|
template: `<div>{{ visibleCount }}</div>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useRenderCount', () => {
|
describe(useRenderCount, () => {
|
||||||
it('return the number of times the component has been rendered', async () => {
|
it('return the number of times the component has been rendered', async () => {
|
||||||
const component = mount(ComponentStub);
|
const component = mount(ComponentStub);
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { onMounted, onUpdated, readonly, type ComponentInternalInstance } from 'vue';
|
import { onMounted, onUpdated, readonly } from 'vue';
|
||||||
import { useCounter } from '../useCounter';
|
import type { ComponentInternalInstance } from 'vue';
|
||||||
import { getLifeCycleTarger } from '../..';
|
import { useCounter } from '@/composables/state/useCounter';
|
||||||
|
import { getLifeCycleTarger } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useRenderCount
|
* @name useRenderCount
|
||||||
* @category Components
|
* @category Component
|
||||||
* @description Returns the number of times the component has been rendered into the DOM
|
* @description Returns the number of times the component has been rendered into the DOM
|
||||||
*
|
*
|
||||||
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
||||||
@@ -26,7 +26,7 @@ const UnnamedComponentStub = defineComponent({
|
|||||||
template: `<div>{{ visibleCount }}</div>`,
|
template: `<div>{{ visibleCount }}</div>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useRenderInfo', () => {
|
describe(useRenderInfo, () => {
|
||||||
it('return uid if component name is not available', async () => {
|
it('return uid if component name is not available', async () => {
|
||||||
const wrapper = mount(UnnamedComponentStub);
|
const wrapper = mount(UnnamedComponentStub);
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ describe('useRenderInfo', () => {
|
|||||||
expect(wrapper.vm.info.duration.value).toBeGreaterThan(0);
|
expect(wrapper.vm.info.duration.value).toBeGreaterThan(0);
|
||||||
expect(wrapper.vm.info.lastRendered).toBeGreaterThan(0);
|
expect(wrapper.vm.info.lastRendered).toBeGreaterThan(0);
|
||||||
|
|
||||||
let lastRendered = wrapper.vm.info.lastRendered;
|
const lastRendered = wrapper.vm.info.lastRendered;
|
||||||
let duration = wrapper.vm.info.duration.value;
|
const duration = wrapper.vm.info.duration.value;
|
||||||
|
|
||||||
// Will not trigger a render
|
// Will not trigger a render
|
||||||
wrapper.vm.hiddenCount++;
|
wrapper.vm.hiddenCount++;
|
||||||
@@ -76,8 +76,8 @@ describe('useRenderInfo', () => {
|
|||||||
expect(info.duration.value).toBe(0);
|
expect(info.duration.value).toBe(0);
|
||||||
expect(info.lastRendered).toBeGreaterThan(0);
|
expect(info.lastRendered).toBeGreaterThan(0);
|
||||||
|
|
||||||
let lastRendered = info.lastRendered;
|
const lastRendered = info.lastRendered;
|
||||||
let duration = info.duration.value;
|
const duration = info.duration.value;
|
||||||
|
|
||||||
// Will not trigger a render
|
// Will not trigger a render
|
||||||
wrapper.vm.hiddenCount++;
|
wrapper.vm.hiddenCount++;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { timestamp } from '@robonen/stdlib';
|
import { timestamp } from '@robonen/stdlib';
|
||||||
import { onBeforeMount, onBeforeUpdate, onMounted, onUpdated, readonly, ref, type ComponentInternalInstance } from 'vue';
|
import { onBeforeMount, onBeforeUpdate, onMounted, onUpdated, readonly, ref } from 'vue';
|
||||||
|
import type { ComponentInternalInstance } from 'vue';
|
||||||
import { useRenderCount } from '../useRenderCount';
|
import { useRenderCount } from '../useRenderCount';
|
||||||
import { getLifeCycleTarger } from '../..';
|
import { getLifeCycleTarger } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useRenderInfo
|
* @name useRenderInfo
|
||||||
* @category Components
|
* @category Component
|
||||||
* @description Returns information about the component's render count and the last time it was rendered
|
* @description Returns information about the component's render count and the last time it was rendered
|
||||||
*
|
*
|
||||||
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
||||||
@@ -24,7 +25,7 @@ export function useRenderInfo(instance?: ComponentInternalInstance) {
|
|||||||
const duration = ref(0);
|
const duration = ref(0);
|
||||||
let renderStartTime = 0;
|
let renderStartTime = 0;
|
||||||
|
|
||||||
const startMark = () => renderStartTime = performance.now();
|
const startMark = () => { renderStartTime = performance.now(); };
|
||||||
const endMark = () => {
|
const endMark = () => {
|
||||||
duration.value = Math.max(performance.now() - renderStartTime, 0);
|
duration.value = Math.max(performance.now() - renderStartTime, 0);
|
||||||
renderStartTime = 0;
|
renderStartTime = 0;
|
||||||
@@ -1,18 +1,8 @@
|
|||||||
export * from './tryOnBeforeMount';
|
export * from './browser';
|
||||||
export * from './tryOnMounted';
|
export * from './component';
|
||||||
export * from './tryOnScopeDispose';
|
export * from './lifecycle';
|
||||||
export * from './useAppSharedState';
|
export * from './math';
|
||||||
export * from './useAsyncState';
|
export * from './reactivity';
|
||||||
export * from './useCached';
|
export * from './state';
|
||||||
export * from './useClamp';
|
export * from './storage';
|
||||||
export * from './useContextFactory';
|
export * from './utilities';
|
||||||
export * from './useCounter';
|
|
||||||
export * from './useFocusGuard';
|
|
||||||
export * from './useInjectionStore';
|
|
||||||
export * from './useLastChanged';
|
|
||||||
export * from './useMounted';
|
|
||||||
export * from './useOffsetPagination';
|
|
||||||
export * from './useRenderCount';
|
|
||||||
export * from './useRenderInfo';
|
|
||||||
export * from './useSupported';
|
|
||||||
export * from './useSyncRefs';
|
|
||||||
|
|||||||
4
web/vue/src/composables/lifecycle/index.ts
Normal file
4
web/vue/src/composables/lifecycle/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './tryOnBeforeMount';
|
||||||
|
export * from './tryOnMounted';
|
||||||
|
export * from './tryOnScopeDispose';
|
||||||
|
export * from './useMounted';
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { onBeforeMount, nextTick, type ComponentInternalInstance } from 'vue';
|
import { onBeforeMount, nextTick } from 'vue';
|
||||||
import { getLifeCycleTarger } from '../..';
|
import type { ComponentInternalInstance } from 'vue';
|
||||||
|
import { getLifeCycleTarger } from '@/utils';
|
||||||
import type { VoidFunction } from '@robonen/stdlib';
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
|
||||||
// TODO: test
|
// TODO: test
|
||||||
@@ -11,7 +12,7 @@ export interface TryOnBeforeMountOptions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name tryOnBeforeMount
|
* @name tryOnBeforeMount
|
||||||
* @category Components
|
* @category Lifecycle
|
||||||
* @description Call onBeforeMount if it's inside a component lifecycle hook, otherwise just calls it
|
* @description Call onBeforeMount if it's inside a component lifecycle hook, otherwise just calls it
|
||||||
*
|
*
|
||||||
* @param {VoidFunction} fn - The function to run on before mount.
|
* @param {VoidFunction} fn - The function to run on before mount.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, it, vi, expect } from 'vitest';
|
import { describe, it, vi, expect } from 'vitest';
|
||||||
import { defineComponent, nextTick, type PropType } from 'vue';
|
import { defineComponent, nextTick } from 'vue';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
import { tryOnMounted } from '.';
|
import { tryOnMounted } from '.';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import type { VoidFunction } from '@robonen/stdlib';
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
@@ -11,12 +12,12 @@ const ComponentStub = defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
props.callback && tryOnMounted(props.callback);
|
if (props.callback) { tryOnMounted(props.callback); }
|
||||||
},
|
},
|
||||||
template: `<div></div>`,
|
template: `<div></div>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tryOnMounted', () => {
|
describe(tryOnMounted, () => {
|
||||||
it('run the callback when mounted', () => {
|
it('run the callback when mounted', () => {
|
||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { onMounted, nextTick, type ComponentInternalInstance } from 'vue';
|
import { onMounted, nextTick } from 'vue';
|
||||||
import { getLifeCycleTarger } from '../..';
|
import type { ComponentInternalInstance } from 'vue';
|
||||||
|
import { getLifeCycleTarger } from '@/utils';
|
||||||
import type { VoidFunction } from '@robonen/stdlib';
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
|
|
||||||
// TODO: tests
|
// TODO: tests
|
||||||
@@ -11,7 +12,7 @@ export interface TryOnMountedOptions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name tryOnMounted
|
* @name tryOnMounted
|
||||||
* @category Components
|
* @category Lifecycle
|
||||||
* @description Call onMounted if it's inside a component lifecycle hook, otherwise just calls it
|
* @description Call onMounted if it's inside a component lifecycle hook, otherwise just calls it
|
||||||
*
|
*
|
||||||
* @param {VoidFunction} fn The function to call
|
* @param {VoidFunction} fn The function to call
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { defineComponent, effectScope, type PropType } from 'vue';
|
import { defineComponent, effectScope } from 'vue';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
import { tryOnScopeDispose } from '.';
|
import { tryOnScopeDispose } from '.';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import type { VoidFunction } from '@robonen/stdlib';
|
import type { VoidFunction } from '@robonen/stdlib';
|
||||||
@@ -17,12 +18,12 @@ const ComponentStub = defineComponent({
|
|||||||
template: '<div></div>',
|
template: '<div></div>',
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tryOnScopeDispose', () => {
|
describe(tryOnScopeDispose, () => {
|
||||||
it('returns false when the scope is not active', () => {
|
it('returns false when the scope is not active', () => {
|
||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
const detectedScope = tryOnScopeDispose(callback);
|
const detectedScope = tryOnScopeDispose(callback);
|
||||||
|
|
||||||
expect(detectedScope).toBe(false);
|
expect(detectedScope).toBeFalsy();
|
||||||
expect(callback).not.toHaveBeenCalled();
|
expect(callback).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ describe('tryOnScopeDispose', () => {
|
|||||||
detectedScope = tryOnScopeDispose(callback);
|
detectedScope = tryOnScopeDispose(callback);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(detectedScope).toBe(true);
|
expect(detectedScope).toBeTruthy();
|
||||||
expect(callback).not.toHaveBeenCalled();
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
scope.stop();
|
scope.stop();
|
||||||
@@ -3,7 +3,7 @@ import { getCurrentScope, onScopeDispose } from 'vue';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name tryOnScopeDispose
|
* @name tryOnScopeDispose
|
||||||
* @category Components
|
* @category Lifecycle
|
||||||
* @description A composable that will run a callback when the scope is disposed or do nothing if the scope isn't available.
|
* @description A composable that will run a callback when the scope is disposed or do nothing if the scope isn't available.
|
||||||
*
|
*
|
||||||
* @param {VoidFunction} callback - The callback to run when the scope is disposed.
|
* @param {VoidFunction} callback - The callback to run when the scope is disposed.
|
||||||
@@ -12,7 +12,7 @@ const ComponentStub = defineComponent({
|
|||||||
template: `<div>{{ isMounted }}</div>`,
|
template: `<div>{{ isMounted }}</div>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useMounted', () => {
|
describe(useMounted, () => {
|
||||||
it('return the mounted state of the component', async () => {
|
it('return the mounted state of the component', async () => {
|
||||||
const component = mount(ComponentStub);
|
const component = mount(ComponentStub);
|
||||||
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { onMounted, readonly, ref, type ComponentInternalInstance } from 'vue';
|
import { onMounted, readonly, ref } from 'vue';
|
||||||
import { getLifeCycleTarger } from '../..';
|
import type { ComponentInternalInstance } from 'vue';
|
||||||
|
import { getLifeCycleTarger } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useMounted
|
* @name useMounted
|
||||||
* @category Components
|
* @category Lifecycle
|
||||||
* @description Returns a ref that tracks the mounted state of the component (doesn't track the unmounted state)
|
* @description Returns a ref that tracks the mounted state of the component (doesn't track the unmounted state)
|
||||||
*
|
*
|
||||||
* @param {ComponentInternalInstance} [instance] The component instance to track the mounted state for
|
* @param {ComponentInternalInstance} [instance] The component instance to track the mounted state for
|
||||||
@@ -21,7 +22,7 @@ export function useMounted(instance?: ComponentInternalInstance) {
|
|||||||
const isMounted = ref(false);
|
const isMounted = ref(false);
|
||||||
const targetInstance = getLifeCycleTarger(instance);
|
const targetInstance = getLifeCycleTarger(instance);
|
||||||
|
|
||||||
onMounted(() => isMounted.value = true, targetInstance);
|
onMounted(() => { isMounted.value = true; }, targetInstance);
|
||||||
|
|
||||||
return readonly(isMounted);
|
return readonly(isMounted);
|
||||||
}
|
}
|
||||||
1
web/vue/src/composables/math/index.ts
Normal file
1
web/vue/src/composables/math/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './useClamp';
|
||||||
@@ -2,7 +2,7 @@ import { ref, readonly, computed } from 'vue';
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { useClamp } from '.';
|
import { useClamp } from '.';
|
||||||
|
|
||||||
describe('useClamp', () => {
|
describe(useClamp, () => {
|
||||||
it('non-reactive values should be clamped', () => {
|
it('non-reactive values should be clamped', () => {
|
||||||
const clampedValue = useClamp(10, 0, 5);
|
const clampedValue = useClamp(10, 0, 5);
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { clamp, isFunction } from '@robonen/stdlib';
|
import { clamp, isFunction } from '@robonen/stdlib';
|
||||||
import { computed, isReadonly, ref, toValue, type ComputedRef, type MaybeRef, type MaybeRefOrGetter, type WritableComputedRef } from 'vue';
|
import { computed, isReadonly, ref, toValue } from 'vue';
|
||||||
|
import type { ComputedRef, MaybeRef, MaybeRefOrGetter, WritableComputedRef } from 'vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useClamp
|
* @name useClamp
|
||||||
41
web/vue/src/composables/reactivity/broadcastedRef/index.ts
Normal file
41
web/vue/src/composables/reactivity/broadcastedRef/index.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { customRef, onScopeDispose } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name broadcastedRef
|
||||||
|
* @category Reactivity
|
||||||
|
* @description Creates a custom ref that syncs its value across browser tabs via the BroadcastChannel API
|
||||||
|
*
|
||||||
|
* @param {string} key The channel key to use for broadcasting
|
||||||
|
* @param {T} initialValue The initial value of the ref
|
||||||
|
* @returns {Ref<T>} A custom ref that broadcasts value changes across tabs
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const count = broadcastedRef('counter', 0);
|
||||||
|
*
|
||||||
|
* @since 0.0.1
|
||||||
|
*/
|
||||||
|
export function broadcastedRef<T>(key: string, initialValue: T) {
|
||||||
|
const channel = new BroadcastChannel(key);
|
||||||
|
|
||||||
|
onScopeDispose(channel.close);
|
||||||
|
|
||||||
|
return customRef<T>((track, trigger) => {
|
||||||
|
channel.onmessage = (event) => {
|
||||||
|
track();
|
||||||
|
return event.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.postMessage(initialValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get() {
|
||||||
|
return initialValue;
|
||||||
|
},
|
||||||
|
set(newValue: T) {
|
||||||
|
initialValue = newValue;
|
||||||
|
channel.postMessage(newValue);
|
||||||
|
trigger();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
4
web/vue/src/composables/reactivity/index.ts
Normal file
4
web/vue/src/composables/reactivity/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './broadcastedRef';
|
||||||
|
export * from './useCached';
|
||||||
|
export * from './useLastChanged';
|
||||||
|
export * from './useSyncRefs';
|
||||||
@@ -4,7 +4,7 @@ import { useCached } from '.';
|
|||||||
|
|
||||||
const arrayEquals = (a: number[], b: number[]) => a.length === b.length && a.every((v, i) => v === b[i]);
|
const arrayEquals = (a: number[], b: number[]) => a.length === b.length && a.every((v, i) => v === b[i]);
|
||||||
|
|
||||||
describe('useCached', () => {
|
describe(useCached, () => {
|
||||||
it('default comparator', async () => {
|
it('default comparator', async () => {
|
||||||
const externalValue = ref(0);
|
const externalValue = ref(0);
|
||||||
const cachedValue = useCached(externalValue);
|
const cachedValue = useCached(externalValue);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ref, watch, toValue, type MaybeRefOrGetter, type Ref, type WatchOptions } from 'vue';
|
import { ref, watch, toValue } from 'vue';
|
||||||
|
import type { MaybeRefOrGetter, Ref, WatchOptions } from 'vue';
|
||||||
|
|
||||||
export type Comparator<Value> = (a: Value, b: Value) => boolean;
|
export type Comparator<Value> = (a: Value, b: Value) => boolean;
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { useLastChanged } from '.';
|
import { useLastChanged } from '.';
|
||||||
import { timestamp } from '@robonen/stdlib';
|
import { timestamp } from '@robonen/stdlib';
|
||||||
|
|
||||||
describe('useLastChanged', () => {
|
describe(useLastChanged, () => {
|
||||||
it('initialize with null if no initialValue is provided', () => {
|
it('initialize with null if no initialValue is provided', () => {
|
||||||
const source = ref(0);
|
const source = ref(0);
|
||||||
const lastChanged = useLastChanged(source);
|
const lastChanged = useLastChanged(source);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { timestamp } from '@robonen/stdlib';
|
import { timestamp } from '@robonen/stdlib';
|
||||||
import { ref, watch, type WatchSource, type WatchOptions, type Ref } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
import type { WatchSource, WatchOptions, Ref } from 'vue';
|
||||||
|
|
||||||
export interface UseLastChangedOptions<
|
export interface UseLastChangedOptions<
|
||||||
Immediate extends boolean,
|
Immediate extends boolean,
|
||||||
@@ -10,7 +11,7 @@ export interface UseLastChangedOptions<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name useLastChanged
|
* @name useLastChanged
|
||||||
* @category State
|
* @category Reactivity
|
||||||
* @description Records the last time a value changed
|
* @description Records the last time a value changed
|
||||||
*
|
*
|
||||||
* @param {WatchSource} source The value to track
|
* @param {WatchSource} source The value to track
|
||||||
@@ -32,7 +33,7 @@ export function useLastChanged(source: WatchSource, options: UseLastChangedOptio
|
|||||||
export function useLastChanged(source: WatchSource, options: UseLastChangedOptions<boolean, any> = {}): Ref<number | null> | Ref<number> {
|
export function useLastChanged(source: WatchSource, options: UseLastChangedOptions<boolean, any> = {}): Ref<number | null> | Ref<number> {
|
||||||
const lastChanged = ref<number | null>(options.initialValue ?? null);
|
const lastChanged = ref<number | null>(options.initialValue ?? null);
|
||||||
|
|
||||||
watch(source, () => lastChanged.value = timestamp(), options);
|
watch(source, () => { lastChanged.value = timestamp(); }, options);
|
||||||
|
|
||||||
return lastChanged;
|
return lastChanged;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useSyncRefs } from '.';
|
import { useSyncRefs } from '.';
|
||||||
|
|
||||||
describe('useSyncRefs', () => {
|
describe(useSyncRefs, () => {
|
||||||
it('sync the value of a source ref with multiple target refs', () => {
|
it('sync the value of a source ref with multiple target refs', () => {
|
||||||
const source = ref(0);
|
const source = ref(0);
|
||||||
const target1 = ref(0);
|
const target1 = ref(0);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { watch, type Ref, type WatchOptions, type WatchSource } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
import type { Ref, WatchOptions, WatchSource } from 'vue';
|
||||||
import { isArray } from '@robonen/stdlib';
|
import { isArray } from '@robonen/stdlib';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +41,7 @@ export function useSyncRefs<T = unknown>(
|
|||||||
|
|
||||||
return watch(
|
return watch(
|
||||||
source,
|
source,
|
||||||
(value) => targets.forEach((target) => target.value = value),
|
(value) => targets.forEach((target) => { target.value = value; }),
|
||||||
{ flush, deep, immediate },
|
{ flush, deep, immediate },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
6
web/vue/src/composables/state/index.ts
Normal file
6
web/vue/src/composables/state/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './useAppSharedState';
|
||||||
|
export * from './useAsyncState';
|
||||||
|
export * from './useContextFactory';
|
||||||
|
export * from './useCounter';
|
||||||
|
export * from './useInjectionStore';
|
||||||
|
export * from './useToggle';
|
||||||
@@ -2,7 +2,7 @@ import { describe, it, vi, expect } from 'vitest';
|
|||||||
import { ref, reactive } from 'vue';
|
import { ref, reactive } from 'vue';
|
||||||
import { useAppSharedState } from '.';
|
import { useAppSharedState } from '.';
|
||||||
|
|
||||||
describe('useAppSharedState', () => {
|
describe(useAppSharedState, () => {
|
||||||
it('initialize state only once', () => {
|
it('initialize state only once', () => {
|
||||||
const stateFactory = (initValue?: number) => {
|
const stateFactory = (initValue?: number) => {
|
||||||
const count = ref(initValue ?? 0);
|
const count = ref(initValue ?? 0);
|
||||||
@@ -2,7 +2,7 @@ import { isShallow, nextTick, ref } from 'vue';
|
|||||||
import { it, expect, describe, vi, beforeEach, afterEach } from 'vitest';
|
import { it, expect, describe, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { useAsyncState } from '.';
|
import { useAsyncState } from '.';
|
||||||
|
|
||||||
describe('useAsyncState', () => {
|
describe(useAsyncState, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
@@ -18,15 +18,15 @@ describe('useAsyncState', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(state.value).toBe('initial');
|
expect(state.value).toBe('initial');
|
||||||
expect(isReady.value).toBe(false);
|
expect(isReady.value).toBeFalsy();
|
||||||
expect(isLoading.value).toBe(true);
|
expect(isLoading.value).toBeTruthy();
|
||||||
expect(error.value).toBe(null);
|
expect(error.value).toBe(null);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(state.value).toBe('data');
|
expect(state.value).toBe('data');
|
||||||
expect(isReady.value).toBe(true);
|
expect(isReady.value).toBeTruthy();
|
||||||
expect(isLoading.value).toBe(false);
|
expect(isLoading.value).toBeFalsy();
|
||||||
expect(error.value).toBe(null);
|
expect(error.value).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,15 +37,15 @@ describe('useAsyncState', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(state.value).toBe('initial');
|
expect(state.value).toBe('initial');
|
||||||
expect(isReady.value).toBe(false);
|
expect(isReady.value).toBeFalsy();
|
||||||
expect(isLoading.value).toBe(true);
|
expect(isLoading.value).toBeTruthy();
|
||||||
expect(error.value).toBe(null);
|
expect(error.value).toBe(null);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(state.value).toBe('data');
|
expect(state.value).toBe('data');
|
||||||
expect(isReady.value).toBe(true);
|
expect(isReady.value).toBeTruthy();
|
||||||
expect(isLoading.value).toBe(false);
|
expect(isLoading.value).toBeFalsy();
|
||||||
expect(error.value).toBe(null);
|
expect(error.value).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -56,15 +56,15 @@ describe('useAsyncState', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(state.value).toBe('initial');
|
expect(state.value).toBe('initial');
|
||||||
expect(isReady.value).toBe(false);
|
expect(isReady.value).toBeFalsy();
|
||||||
expect(isLoading.value).toBe(true);
|
expect(isLoading.value).toBeTruthy();
|
||||||
expect(error.value).toBe(null);
|
expect(error.value).toBe(null);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(state.value).toBe('initial');
|
expect(state.value).toBe('initial');
|
||||||
expect(isReady.value).toBe(false);
|
expect(isReady.value).toBeFalsy();
|
||||||
expect(isLoading.value).toBe(false);
|
expect(isLoading.value).toBeFalsy();
|
||||||
expect(error.value).toEqual(new Error('test-error'));
|
expect(error.value).toEqual(new Error('test-error'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,14 +131,14 @@ describe('useAsyncState', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const promise = execute();
|
const promise = execute();
|
||||||
expect(isLoading.value).toBe(true);
|
expect(isLoading.value).toBeTruthy();
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(50);
|
await vi.advanceTimersByTimeAsync(50);
|
||||||
expect(isLoading.value).toBe(true);
|
expect(isLoading.value).toBeTruthy();
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(50);
|
await vi.advanceTimersByTimeAsync(50);
|
||||||
await promise;
|
await promise;
|
||||||
expect(isLoading.value).toBe(false);
|
expect(isLoading.value).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is awaitable', async () => {
|
it('is awaitable', async () => {
|
||||||
@@ -160,15 +160,15 @@ describe('useAsyncState', () => {
|
|||||||
executeImmediately();
|
executeImmediately();
|
||||||
|
|
||||||
expect(state.value).toBe('initial');
|
expect(state.value).toBe('initial');
|
||||||
expect(isLoading.value).toBe(true);
|
expect(isLoading.value).toBeTruthy();
|
||||||
expect(isReady.value).toBe(false);
|
expect(isReady.value).toBeFalsy();
|
||||||
expect(error.value).toBe(null);
|
expect(error.value).toBe(null);
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(state.value).toBe('data');
|
expect(state.value).toBe('data');
|
||||||
expect(isReady.value).toBe(true);
|
expect(isReady.value).toBeTruthy();
|
||||||
expect(isLoading.value).toBe(false);
|
expect(isLoading.value).toBeFalsy();
|
||||||
expect(error.value).toBe(null);
|
expect(error.value).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ describe('useAsyncState', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(state.value.a).toBe(1);
|
expect(state.value.a).toBe(1);
|
||||||
expect(isShallow(state)).toBe(true);
|
expect(isShallow(state)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses ref when shallow is false', async () => {
|
it('uses ref when shallow is false', async () => {
|
||||||
@@ -204,6 +204,6 @@ describe('useAsyncState', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(state.value.a).toBe(1);
|
expect(state.value.a).toBe(1);
|
||||||
expect(isShallow(state)).toBe(false);
|
expect(isShallow(state)).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user