mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
Compare commits
21 Commits
71b4e49e4f
...
renovate/j
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dd876e05f | ||
| a83e2bb797 | |||
| 9bece480ca | |||
| c48de9a3d1 | |||
| 624e12ed96 | |||
| 3380d90cee | |||
| bb644579ca | |||
| e7d1021d27 | |||
| 1782184761 | |||
| 70d96b7f39 | |||
|
|
9587c92e50 | ||
| 678c18a08d | |||
| 68afec40b7 | |||
| 50b1498f3e | |||
| 7b5da22290 | |||
| 09fe8079c0 | |||
| ab9f45f908 | |||
| 49b9f2aa79 | |||
| 2a5412c3b8 | |||
| 5f9e0dc72d | |||
| 6565fa3de8 |
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -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
|
||||||
54
configs/oxlint/README.md
Normal file
54
configs/oxlint/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# @robonen/oxlint
|
||||||
|
|
||||||
|
Composable [oxlint](https://oxc.rs/docs/guide/usage/linter.html) configuration presets.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install -D @robonen/oxlint oxlint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Create `oxlint.config.ts` in your project root:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
import { compose, base, typescript, vue, vitest, imports } from '@robonen/oxlint';
|
||||||
|
|
||||||
|
export default defineConfig(
|
||||||
|
compose(base, typescript, vue, vitest, imports),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Append custom rules after presets to override them:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
compose(base, typescript, {
|
||||||
|
rules: { 'eslint/no-console': 'off' },
|
||||||
|
ignorePatterns: ['dist'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Presets
|
||||||
|
|
||||||
|
| Preset | Description |
|
||||||
|
| ------------ | -------------------------------------------------- |
|
||||||
|
| `base` | Core eslint, oxc, unicorn rules |
|
||||||
|
| `typescript` | TypeScript-specific rules (via overrides) |
|
||||||
|
| `vue` | Vue 3 Composition API / `<script setup>` rules |
|
||||||
|
| `vitest` | Test file rules (via overrides) |
|
||||||
|
| `imports` | Import rules (cycles, duplicates, ordering) |
|
||||||
|
| `node` | Node.js-specific rules |
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `compose(...configs: OxlintConfig[]): OxlintConfig`
|
||||||
|
|
||||||
|
Merges multiple configs into one:
|
||||||
|
|
||||||
|
- **plugins** — union (deduplicated)
|
||||||
|
- **rules / categories** — last wins
|
||||||
|
- **overrides / ignorePatterns** — concatenated
|
||||||
|
- **env / globals** — shallow merge
|
||||||
|
- **settings** — deep merge
|
||||||
4
configs/oxlint/oxlint.config.ts
Normal file
4
configs/oxlint/oxlint.config.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from 'oxlint';
|
||||||
|
import { compose, base, typescript, imports } from '@robonen/oxlint';
|
||||||
|
|
||||||
|
export default defineConfig(compose(base, typescript, imports));
|
||||||
53
configs/oxlint/package.json
Normal file
53
configs/oxlint/package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/oxlint",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "Composable oxlint configuration presets",
|
||||||
|
"keywords": [
|
||||||
|
"oxlint",
|
||||||
|
"oxc",
|
||||||
|
"linter",
|
||||||
|
"config",
|
||||||
|
"presets"
|
||||||
|
],
|
||||||
|
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
|
"directory": "configs/oxlint"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.29.3",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.13.1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"dev": "vitest dev",
|
||||||
|
"build": "tsdown"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"@robonen/tsdown": "workspace:*",
|
||||||
|
"oxlint": "catalog:",
|
||||||
|
"tsdown": "catalog:"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"oxlint": ">=1.0.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
103
configs/oxlint/src/compose.ts
Normal file
103
configs/oxlint/src/compose.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import type { OxlintConfig } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep merge two objects. Arrays are concatenated, objects are recursively merged.
|
||||||
|
*/
|
||||||
|
function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const result = { ...target };
|
||||||
|
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
const targetValue = target[key];
|
||||||
|
const sourceValue = source[key];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)
|
||||||
|
&& typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)
|
||||||
|
) {
|
||||||
|
result[key] = deepMerge(
|
||||||
|
targetValue as Record<string, unknown>,
|
||||||
|
sourceValue as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result[key] = sourceValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose multiple oxlint configurations into a single config.
|
||||||
|
*
|
||||||
|
* - `plugins` — union (deduplicated)
|
||||||
|
* - `categories` — later configs override earlier
|
||||||
|
* - `rules` — later configs override earlier
|
||||||
|
* - `overrides` — concatenated
|
||||||
|
* - `env` — merged (later overrides earlier)
|
||||||
|
* - `globals` — merged (later overrides earlier)
|
||||||
|
* - `settings` — deep-merged
|
||||||
|
* - `ignorePatterns` — concatenated
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { compose, base, typescript, vue } from '@robonen/oxlint';
|
||||||
|
* import { defineConfig } from 'oxlint';
|
||||||
|
*
|
||||||
|
* export default defineConfig(
|
||||||
|
* compose(base, typescript, vue, {
|
||||||
|
* rules: { 'eslint/no-console': 'off' },
|
||||||
|
* }),
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function compose(...configs: OxlintConfig[]): OxlintConfig {
|
||||||
|
const result: OxlintConfig = {};
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
// Plugins — union with dedup
|
||||||
|
if (config.plugins?.length) {
|
||||||
|
result.plugins = Array.from(new Set([...(result.plugins ?? []), ...config.plugins]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categories — shallow merge
|
||||||
|
if (config.categories) {
|
||||||
|
result.categories = { ...result.categories, ...config.categories };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules — shallow merge (later overrides earlier)
|
||||||
|
if (config.rules) {
|
||||||
|
result.rules = { ...result.rules, ...config.rules };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides — concatenate
|
||||||
|
if (config.overrides?.length) {
|
||||||
|
result.overrides = [...(result.overrides ?? []), ...config.overrides];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Env — shallow merge
|
||||||
|
if (config.env) {
|
||||||
|
result.env = { ...result.env, ...config.env };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globals — shallow merge
|
||||||
|
if (config.globals) {
|
||||||
|
result.globals = { ...result.globals, ...config.globals };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings — deep merge
|
||||||
|
if (config.settings) {
|
||||||
|
result.settings = deepMerge(
|
||||||
|
(result.settings ?? {}) as Record<string, unknown>,
|
||||||
|
config.settings as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore patterns — concatenate
|
||||||
|
if (config.ignorePatterns?.length) {
|
||||||
|
result.ignorePatterns = [...(result.ignorePatterns ?? []), ...config.ignorePatterns];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
17
configs/oxlint/src/index.ts
Normal file
17
configs/oxlint/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* Compose */
|
||||||
|
export { compose } from './compose';
|
||||||
|
|
||||||
|
/* Presets */
|
||||||
|
export { base, typescript, vue, vitest, imports, node } from './presets';
|
||||||
|
|
||||||
|
/* Types */
|
||||||
|
export type {
|
||||||
|
OxlintConfig,
|
||||||
|
OxlintOverride,
|
||||||
|
OxlintEnv,
|
||||||
|
OxlintGlobals,
|
||||||
|
AllowWarnDeny,
|
||||||
|
DummyRule,
|
||||||
|
DummyRuleMap,
|
||||||
|
RuleCategories,
|
||||||
|
} from './types';
|
||||||
73
configs/oxlint/src/presets/base.ts
Normal file
73
configs/oxlint/src/presets/base.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base configuration for any JavaScript/TypeScript project.
|
||||||
|
*
|
||||||
|
* Enables `correctness` category and opinionated rules from
|
||||||
|
* `eslint`, `oxc`, and `unicorn` plugins.
|
||||||
|
*/
|
||||||
|
export const base: OxlintConfig = {
|
||||||
|
plugins: ['eslint', 'oxc', 'unicorn'],
|
||||||
|
|
||||||
|
categories: {
|
||||||
|
correctness: 'error',
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
/* ── eslint core ──────────────────────────────────────── */
|
||||||
|
'eslint/eqeqeq': 'error',
|
||||||
|
'eslint/no-console': 'warn',
|
||||||
|
'eslint/no-debugger': 'error',
|
||||||
|
'eslint/no-eval': 'error',
|
||||||
|
'eslint/no-var': 'error',
|
||||||
|
'eslint/prefer-const': 'error',
|
||||||
|
'eslint/prefer-template': 'warn',
|
||||||
|
'eslint/no-useless-constructor': 'warn',
|
||||||
|
'eslint/no-useless-rename': 'warn',
|
||||||
|
'eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||||
|
'eslint/no-self-compare': 'error',
|
||||||
|
'eslint/no-template-curly-in-string': 'warn',
|
||||||
|
'eslint/no-throw-literal': 'error',
|
||||||
|
'eslint/no-return-assign': 'warn',
|
||||||
|
'eslint/no-else-return': 'warn',
|
||||||
|
'eslint/no-lonely-if': 'warn',
|
||||||
|
'eslint/no-unneeded-ternary': 'warn',
|
||||||
|
'eslint/prefer-object-spread': 'warn',
|
||||||
|
'eslint/prefer-exponentiation-operator': 'warn',
|
||||||
|
'eslint/no-useless-computed-key': 'warn',
|
||||||
|
'eslint/no-useless-concat': 'warn',
|
||||||
|
'eslint/curly': 'off',
|
||||||
|
|
||||||
|
/* ── unicorn ──────────────────────────────────────────── */
|
||||||
|
'unicorn/prefer-node-protocol': 'error',
|
||||||
|
'unicorn/no-instanceof-array': 'error',
|
||||||
|
'unicorn/no-new-array': 'error',
|
||||||
|
'unicorn/prefer-array-flat-map': 'warn',
|
||||||
|
'unicorn/prefer-array-flat': 'warn',
|
||||||
|
'unicorn/prefer-includes': 'warn',
|
||||||
|
'unicorn/prefer-string-slice': 'warn',
|
||||||
|
'unicorn/prefer-string-starts-ends-with': 'warn',
|
||||||
|
'unicorn/throw-new-error': 'error',
|
||||||
|
'unicorn/error-message': 'warn',
|
||||||
|
'unicorn/no-useless-spread': 'warn',
|
||||||
|
'unicorn/no-useless-undefined': 'off',
|
||||||
|
'unicorn/prefer-optional-catch-binding': 'warn',
|
||||||
|
'unicorn/prefer-type-error': 'warn',
|
||||||
|
'unicorn/no-thenable': 'error',
|
||||||
|
'unicorn/prefer-number-properties': 'warn',
|
||||||
|
'unicorn/prefer-global-this': 'warn',
|
||||||
|
|
||||||
|
/* ── oxc ──────────────────────────────────────────────── */
|
||||||
|
'oxc/no-accumulating-spread': 'warn',
|
||||||
|
'oxc/bad-comparison-sequence': 'error',
|
||||||
|
'oxc/bad-min-max-func': 'error',
|
||||||
|
'oxc/bad-object-literal-comparison': 'error',
|
||||||
|
'oxc/const-comparisons': 'error',
|
||||||
|
'oxc/double-comparisons': 'error',
|
||||||
|
'oxc/erasing-op': 'error',
|
||||||
|
'oxc/missing-throw': 'error',
|
||||||
|
'oxc/bad-bitwise-operator': 'error',
|
||||||
|
'oxc/bad-char-at-comparison': 'error',
|
||||||
|
'oxc/bad-replace-all-arg': 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
20
configs/oxlint/src/presets/imports.ts
Normal file
20
configs/oxlint/src/presets/imports.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import plugin rules for clean module boundaries.
|
||||||
|
*/
|
||||||
|
export const imports: OxlintConfig = {
|
||||||
|
plugins: ['import'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'import/no-duplicates': 'error',
|
||||||
|
'import/no-self-import': 'error',
|
||||||
|
'import/no-cycle': 'warn',
|
||||||
|
'import/first': 'warn',
|
||||||
|
'import/no-mutable-exports': 'error',
|
||||||
|
'import/no-amd': 'error',
|
||||||
|
'import/no-commonjs': 'warn',
|
||||||
|
'import/no-empty-named-blocks': 'warn',
|
||||||
|
'import/consistent-type-specifier-style': ['warn', 'prefer-top-level'],
|
||||||
|
},
|
||||||
|
};
|
||||||
6
configs/oxlint/src/presets/index.ts
Normal file
6
configs/oxlint/src/presets/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { base } from './base';
|
||||||
|
export { typescript } from './typescript';
|
||||||
|
export { vue } from './vue';
|
||||||
|
export { vitest } from './vitest';
|
||||||
|
export { imports } from './imports';
|
||||||
|
export { node } from './node';
|
||||||
17
configs/oxlint/src/presets/node.ts
Normal file
17
configs/oxlint/src/presets/node.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node.js-specific rules.
|
||||||
|
*/
|
||||||
|
export const node: OxlintConfig = {
|
||||||
|
plugins: ['node'],
|
||||||
|
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'node/no-exports-assign': 'error',
|
||||||
|
'node/no-new-require': 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
39
configs/oxlint/src/presets/typescript.ts
Normal file
39
configs/oxlint/src/presets/typescript.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript-specific rules.
|
||||||
|
*
|
||||||
|
* Applied via `overrides` for `*.ts`, `*.tsx`, `*.mts`, `*.cts` files.
|
||||||
|
*/
|
||||||
|
export const typescript: OxlintConfig = {
|
||||||
|
plugins: ['typescript'],
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'],
|
||||||
|
rules: {
|
||||||
|
'typescript/consistent-type-imports': 'error',
|
||||||
|
'typescript/no-explicit-any': 'off',
|
||||||
|
'typescript/no-non-null-assertion': 'off',
|
||||||
|
'typescript/prefer-as-const': 'error',
|
||||||
|
'typescript/no-empty-object-type': 'warn',
|
||||||
|
'typescript/no-wrapper-object-types': 'error',
|
||||||
|
'typescript/no-duplicate-enum-values': 'error',
|
||||||
|
'typescript/no-unsafe-declaration-merging': 'error',
|
||||||
|
'typescript/no-import-type-side-effects': 'error',
|
||||||
|
'typescript/no-useless-empty-export': 'warn',
|
||||||
|
'typescript/no-inferrable-types': 'warn',
|
||||||
|
'typescript/prefer-function-type': 'warn',
|
||||||
|
'typescript/ban-tslint-comment': 'error',
|
||||||
|
'typescript/consistent-type-definitions': ['warn', 'interface'],
|
||||||
|
'typescript/prefer-for-of': 'warn',
|
||||||
|
'typescript/no-unnecessary-type-constraint': 'warn',
|
||||||
|
'typescript/adjacent-overload-signatures': 'warn',
|
||||||
|
'typescript/array-type': ['warn', { default: 'array-simple' }],
|
||||||
|
'typescript/no-this-alias': 'error',
|
||||||
|
'typescript/triple-slash-reference': 'error',
|
||||||
|
'typescript/no-namespace': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
35
configs/oxlint/src/presets/vitest.ts
Normal file
35
configs/oxlint/src/presets/vitest.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitest rules for test files.
|
||||||
|
*
|
||||||
|
* Applied via `overrides` for common test file patterns.
|
||||||
|
*/
|
||||||
|
export const vitest: OxlintConfig = {
|
||||||
|
plugins: ['vitest'],
|
||||||
|
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/*.test.{ts,tsx,js,jsx}',
|
||||||
|
'**/*.spec.{ts,tsx,js,jsx}',
|
||||||
|
'**/test/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'**/__tests__/**/*.{ts,tsx,js,jsx}',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vitest/no-conditional-tests': 'warn',
|
||||||
|
'vitest/no-import-node-test': 'error',
|
||||||
|
'vitest/prefer-to-be-truthy': 'warn',
|
||||||
|
'vitest/prefer-to-be-falsy': 'warn',
|
||||||
|
'vitest/prefer-to-be-object': 'warn',
|
||||||
|
'vitest/prefer-to-have-length': 'warn',
|
||||||
|
'vitest/consistent-test-filename': 'warn',
|
||||||
|
'vitest/prefer-describe-function-title': 'warn',
|
||||||
|
|
||||||
|
/* relax strict rules in tests */
|
||||||
|
'eslint/no-unused-vars': 'off',
|
||||||
|
'typescript/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
26
configs/oxlint/src/presets/vue.ts
Normal file
26
configs/oxlint/src/presets/vue.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { OxlintConfig } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue.js-specific rules.
|
||||||
|
*
|
||||||
|
* Enforces Composition API with `<script setup>` and type-based declarations.
|
||||||
|
*/
|
||||||
|
export const vue: OxlintConfig = {
|
||||||
|
plugins: ['vue'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'vue/no-arrow-functions-in-watch': 'error',
|
||||||
|
'vue/no-deprecated-destroyed-lifecycle': 'error',
|
||||||
|
'vue/no-export-in-script-setup': 'error',
|
||||||
|
'vue/no-lifecycle-after-await': 'error',
|
||||||
|
'vue/no-multiple-slot-args': 'error',
|
||||||
|
'vue/no-import-compiler-macros': 'error',
|
||||||
|
'vue/define-emits-declaration': ['error', 'type-based'],
|
||||||
|
'vue/define-props-declaration': ['error', 'type-based'],
|
||||||
|
'vue/prefer-import-from-vue': 'error',
|
||||||
|
'vue/no-required-prop-with-default': 'warn',
|
||||||
|
'vue/valid-define-emits': 'error',
|
||||||
|
'vue/valid-define-props': 'error',
|
||||||
|
'vue/require-typed-ref': 'warn',
|
||||||
|
},
|
||||||
|
};
|
||||||
18
configs/oxlint/src/types.ts
Normal file
18
configs/oxlint/src/types.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Re-exported configuration types from `oxlint`.
|
||||||
|
*
|
||||||
|
* Keeps the preset API in sync with the oxlint CLI without
|
||||||
|
* maintaining a separate copy of the types.
|
||||||
|
*
|
||||||
|
* @see https://oxc.rs/docs/guide/usage/linter/config-file-reference.html
|
||||||
|
*/
|
||||||
|
export type {
|
||||||
|
OxlintConfig,
|
||||||
|
OxlintOverride,
|
||||||
|
OxlintEnv,
|
||||||
|
OxlintGlobals,
|
||||||
|
AllowWarnDeny,
|
||||||
|
DummyRule,
|
||||||
|
DummyRuleMap,
|
||||||
|
RuleCategories,
|
||||||
|
} from 'oxlint';
|
||||||
146
configs/oxlint/test/compose.test.ts
Normal file
146
configs/oxlint/test/compose.test.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { compose } from '../src/compose';
|
||||||
|
import type { OxlintConfig } from '../src/types';
|
||||||
|
|
||||||
|
describe('compose', () => {
|
||||||
|
it('should return empty config when no configs provided', () => {
|
||||||
|
expect(compose()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same config when one config provided', () => {
|
||||||
|
const config: OxlintConfig = {
|
||||||
|
plugins: ['eslint'],
|
||||||
|
rules: { 'eslint/no-console': 'warn' },
|
||||||
|
};
|
||||||
|
const result = compose(config);
|
||||||
|
expect(result.plugins).toEqual(['eslint']);
|
||||||
|
expect(result.rules).toEqual({ 'eslint/no-console': 'warn' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge plugins with dedup', () => {
|
||||||
|
const a: OxlintConfig = { plugins: ['eslint', 'oxc'] };
|
||||||
|
const b: OxlintConfig = { plugins: ['oxc', 'typescript'] };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.plugins).toEqual(['eslint', 'oxc', 'typescript']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override rules from later configs', () => {
|
||||||
|
const a: OxlintConfig = { rules: { 'eslint/no-console': 'error', 'eslint/eqeqeq': 'warn' } };
|
||||||
|
const b: OxlintConfig = { rules: { 'eslint/no-console': 'off' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.rules).toEqual({
|
||||||
|
'eslint/no-console': 'off',
|
||||||
|
'eslint/eqeqeq': 'warn',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override categories from later configs', () => {
|
||||||
|
const a: OxlintConfig = { categories: { correctness: 'error', suspicious: 'warn' } };
|
||||||
|
const b: OxlintConfig = { categories: { suspicious: 'off' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.categories).toEqual({
|
||||||
|
correctness: 'error',
|
||||||
|
suspicious: 'off',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should concatenate overrides', () => {
|
||||||
|
const a: OxlintConfig = {
|
||||||
|
overrides: [{ files: ['**/*.ts'], rules: { 'typescript/no-explicit-any': 'warn' } }],
|
||||||
|
};
|
||||||
|
const b: OxlintConfig = {
|
||||||
|
overrides: [{ files: ['**/*.test.ts'], rules: { 'eslint/no-unused-vars': 'off' } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.overrides).toHaveLength(2);
|
||||||
|
expect(result.overrides?.[0]?.files).toEqual(['**/*.ts']);
|
||||||
|
expect(result.overrides?.[1]?.files).toEqual(['**/*.test.ts']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge env', () => {
|
||||||
|
const a: OxlintConfig = { env: { browser: true } };
|
||||||
|
const b: OxlintConfig = { env: { node: true } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.env).toEqual({ browser: true, node: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge globals', () => {
|
||||||
|
const a: OxlintConfig = { globals: { MY_VAR: 'readonly' } };
|
||||||
|
const b: OxlintConfig = { globals: { ANOTHER: 'writable' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.globals).toEqual({ MY_VAR: 'readonly', ANOTHER: 'writable' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deep merge settings', () => {
|
||||||
|
const a: OxlintConfig = {
|
||||||
|
settings: {
|
||||||
|
react: { version: '18.2.0' },
|
||||||
|
next: { rootDir: 'apps/' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const b: OxlintConfig = {
|
||||||
|
settings: {
|
||||||
|
react: { linkComponents: [{ name: 'Link', linkAttribute: 'to', attributes: ['to'] }] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.settings).toEqual({
|
||||||
|
react: {
|
||||||
|
version: '18.2.0',
|
||||||
|
linkComponents: [{ name: 'Link', linkAttribute: 'to', attributes: ['to'] }],
|
||||||
|
},
|
||||||
|
next: { rootDir: 'apps/' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should concatenate ignorePatterns', () => {
|
||||||
|
const a: OxlintConfig = { ignorePatterns: ['dist'] };
|
||||||
|
const b: OxlintConfig = { ignorePatterns: ['node_modules', 'coverage'] };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.ignorePatterns).toEqual(['dist', 'node_modules', 'coverage']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle composing all presets together', () => {
|
||||||
|
const base: OxlintConfig = {
|
||||||
|
plugins: ['eslint', 'oxc'],
|
||||||
|
categories: { correctness: 'error' },
|
||||||
|
rules: { 'eslint/no-console': 'warn' },
|
||||||
|
};
|
||||||
|
const ts: OxlintConfig = {
|
||||||
|
plugins: ['typescript'],
|
||||||
|
overrides: [{ files: ['**/*.ts'], rules: { 'typescript/no-explicit-any': 'warn' } }],
|
||||||
|
};
|
||||||
|
const custom: OxlintConfig = {
|
||||||
|
rules: { 'eslint/no-console': 'off' },
|
||||||
|
ignorePatterns: ['dist'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = compose(base, ts, custom);
|
||||||
|
|
||||||
|
expect(result.plugins).toEqual(['eslint', 'oxc', 'typescript']);
|
||||||
|
expect(result.categories).toEqual({ correctness: 'error' });
|
||||||
|
expect(result.rules).toEqual({ 'eslint/no-console': 'off' });
|
||||||
|
expect(result.overrides).toHaveLength(1);
|
||||||
|
expect(result.ignorePatterns).toEqual(['dist']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip undefined/empty fields', () => {
|
||||||
|
const a: OxlintConfig = { plugins: ['eslint'] };
|
||||||
|
const b: OxlintConfig = { rules: { 'eslint/no-console': 'warn' } };
|
||||||
|
|
||||||
|
const result = compose(a, b);
|
||||||
|
expect(result.plugins).toEqual(['eslint']);
|
||||||
|
expect(result.rules).toEqual({ 'eslint/no-console': 'warn' });
|
||||||
|
expect(result.overrides).toBeUndefined();
|
||||||
|
expect(result.env).toBeUndefined();
|
||||||
|
expect(result.settings).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
3
configs/oxlint/tsconfig.json
Normal file
3
configs/oxlint/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
|
}
|
||||||
7
configs/oxlint/tsdown.config.ts
Normal file
7
configs/oxlint/tsdown.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'tsdown';
|
||||||
|
import { sharedConfig } from '@robonen/tsdown';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
});
|
||||||
7
configs/oxlint/vitest.config.ts
Normal file
7
configs/oxlint/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
30
configs/tsdown/package.json
Normal file
30
configs/tsdown/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@robonen/tsdown",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "Shared tsdown configuration for @robonen packages",
|
||||||
|
"keywords": [
|
||||||
|
"tsdown",
|
||||||
|
"config",
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/robonen/tools.git",
|
||||||
|
"directory": "configs/tsdown"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.29.3",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.13.1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"tsdown": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
configs/tsdown/src/index.ts
Normal file
13
configs/tsdown/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { Options } from 'tsdown';
|
||||||
|
|
||||||
|
const BANNER = '/*! @robonen/tools | (c) 2026 Robonen Andrew | Apache-2.0 */';
|
||||||
|
|
||||||
|
export const sharedConfig = {
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
hash: false,
|
||||||
|
outputOptions: {
|
||||||
|
banner: BANNER,
|
||||||
|
},
|
||||||
|
} satisfies Options;
|
||||||
3
configs/tsdown/tsconfig.json
Normal file
3
configs/tsdown/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
```
|
||||||
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/platform",
|
"name": "@robonen/platform",
|
||||||
"version": "0.0.3",
|
"version": "0.0.4",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Platform dependent utilities for javascript development",
|
"description": "Platform dependent utilities for javascript development",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -39,12 +39,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"build": "tsdown"
|
"build": "tsdown"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"@robonen/tsdown": "workspace:*",
|
||||||
|
"oxlint": "catalog:",
|
||||||
"tsdown": "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
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { defineConfig } from 'tsdown';
|
import { defineConfig } from 'tsdown';
|
||||||
|
import { sharedConfig } from '@robonen/tsdown';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
entry: {
|
entry: {
|
||||||
browsers: 'src/browsers/index.ts',
|
browsers: 'src/browsers/index.ts',
|
||||||
multi: 'src/multi/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';
|
||||||
|
```
|
||||||
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));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@robonen/stdlib",
|
"name": "@robonen/stdlib",
|
||||||
"version": "0.0.7",
|
"version": "0.0.9",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "A collection of tools, utilities, and helpers for TypeScript",
|
"description": "A collection of tools, utilities, and helpers for TypeScript",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -34,12 +34,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"build": "tsdown"
|
"build": "tsdown"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"@robonen/tsdown": "workspace:*",
|
||||||
|
"oxlint": "catalog:",
|
||||||
"tsdown": "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,3 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Precision scale for bigint interpolation (6 decimal places).
|
||||||
|
* BigInt has no overflow, so higher precision is free.
|
||||||
|
*/
|
||||||
|
const SCALE = 1_000_000;
|
||||||
|
const SCALE_N = BigInt(SCALE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name lerpBigInt
|
* @name lerpBigInt
|
||||||
* @category Math
|
* @category Math
|
||||||
@@ -11,7 +18,7 @@
|
|||||||
* @since 0.0.2
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function lerpBigInt(start: bigint, end: bigint, t: number) {
|
export function lerpBigInt(start: bigint, end: bigint, t: number) {
|
||||||
return start + ((end - start) * BigInt(t * 10000)) / 10000n;
|
return start + ((end - start) * BigInt(Math.round(t * SCALE))) / SCALE_N;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,5 +34,5 @@ export function lerpBigInt(start: bigint, end: bigint, t: number) {
|
|||||||
* @since 0.0.2
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export function inverseLerpBigInt(start: bigint, end: bigint, value: bigint) {
|
export function inverseLerpBigInt(start: bigint, end: bigint, value: bigint) {
|
||||||
return start === end ? 0 : Number((value - start) * 10000n / (end - start)) / 10000;
|
return start === end ? 0 : Number((value - start) * SCALE_N / (end - start)) / SCALE;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
55
core/stdlib/src/patterns/behavioral/Command/async.ts
Normal file
55
core/stdlib/src/patterns/behavioral/Command/async.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { BaseCommandHistory } from './base';
|
||||||
|
import type { AsyncCommand } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name AsyncCommandHistory
|
||||||
|
* @category Patterns
|
||||||
|
* @description Async command history with undo/redo/batch support
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*/
|
||||||
|
export class AsyncCommandHistory extends BaseCommandHistory<AsyncCommand> {
|
||||||
|
async execute(command: AsyncCommand): Promise<void> {
|
||||||
|
await command.execute();
|
||||||
|
this.pushUndo(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async undo(): Promise<boolean> {
|
||||||
|
const command = this.undoStack.pop();
|
||||||
|
|
||||||
|
if (!command)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await command.undo();
|
||||||
|
this.moveToRedo(command);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async redo(): Promise<boolean> {
|
||||||
|
const command = this.redoStack.pop();
|
||||||
|
|
||||||
|
if (!command)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await command.execute();
|
||||||
|
this.moveToUndo(command);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async batch(commands: AsyncCommand[]): Promise<void> {
|
||||||
|
const macro: AsyncCommand = {
|
||||||
|
execute: async () => {
|
||||||
|
for (const c of commands)
|
||||||
|
await c.execute();
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
for (let i = commands.length - 1; i >= 0; i--)
|
||||||
|
await commands[i]!.undo();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.execute(macro);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
core/stdlib/src/patterns/behavioral/Command/base.ts
Normal file
49
core/stdlib/src/patterns/behavioral/Command/base.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @name BaseCommandHistory
|
||||||
|
* @category Patterns
|
||||||
|
* @description Base class with shared undo/redo stack management
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*/
|
||||||
|
export abstract class BaseCommandHistory<C> {
|
||||||
|
protected undoStack: C[] = [];
|
||||||
|
protected redoStack: C[] = [];
|
||||||
|
protected readonly maxSize: number;
|
||||||
|
|
||||||
|
constructor(options?: { maxSize?: number }) {
|
||||||
|
this.maxSize = options?.maxSize ?? Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this.undoStack.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canUndo(): boolean {
|
||||||
|
return this.undoStack.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canRedo(): boolean {
|
||||||
|
return this.redoStack.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.undoStack.length = 0;
|
||||||
|
this.redoStack.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected pushUndo(command: C): void {
|
||||||
|
this.undoStack.push(command);
|
||||||
|
this.redoStack.length = 0;
|
||||||
|
|
||||||
|
if (this.undoStack.length > this.maxSize)
|
||||||
|
this.undoStack.splice(0, this.undoStack.length - this.maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected moveToRedo(command: C): void {
|
||||||
|
this.redoStack.push(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected moveToUndo(command: C): void {
|
||||||
|
this.undoStack.push(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
281
core/stdlib/src/patterns/behavioral/Command/index.test.ts
Normal file
281
core/stdlib/src/patterns/behavioral/Command/index.test.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { CommandHistory, AsyncCommandHistory } from '.';
|
||||||
|
import type { Command, AsyncCommand } from '.';
|
||||||
|
|
||||||
|
describe('commandHistory', () => {
|
||||||
|
let history: CommandHistory;
|
||||||
|
let items: string[];
|
||||||
|
|
||||||
|
function addItem(item: string): Command {
|
||||||
|
return {
|
||||||
|
execute: () => { items.push(item); },
|
||||||
|
undo: () => { items.pop(); },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
history = new CommandHistory();
|
||||||
|
items = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execute', () => {
|
||||||
|
it('executes a command', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
|
||||||
|
expect(items).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks size', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.execute(addItem('b'));
|
||||||
|
|
||||||
|
expect(history.size).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears redo stack on new execute', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.undo();
|
||||||
|
|
||||||
|
expect(history.canRedo).toBe(true);
|
||||||
|
|
||||||
|
history.execute(addItem('b'));
|
||||||
|
|
||||||
|
expect(history.canRedo).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('undo', () => {
|
||||||
|
it('undoes the last command', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.execute(addItem('b'));
|
||||||
|
history.undo();
|
||||||
|
|
||||||
|
expect(items).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when undo was performed', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
|
||||||
|
expect(history.undo()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when nothing to undo', () => {
|
||||||
|
expect(history.undo()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple undos', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.execute(addItem('b'));
|
||||||
|
history.execute(addItem('c'));
|
||||||
|
history.undo();
|
||||||
|
history.undo();
|
||||||
|
history.undo();
|
||||||
|
|
||||||
|
expect(items).toEqual([]);
|
||||||
|
expect(history.canUndo).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('redo', () => {
|
||||||
|
it('redoes the last undone command', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.undo();
|
||||||
|
history.redo();
|
||||||
|
|
||||||
|
expect(items).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when redo was performed', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.undo();
|
||||||
|
|
||||||
|
expect(history.redo()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when nothing to redo', () => {
|
||||||
|
expect(history.redo()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('undo then redo multiple times', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.execute(addItem('b'));
|
||||||
|
history.undo();
|
||||||
|
history.undo();
|
||||||
|
history.redo();
|
||||||
|
history.redo();
|
||||||
|
|
||||||
|
expect(items).toEqual(['a', 'b']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('batch', () => {
|
||||||
|
it('executes multiple commands', () => {
|
||||||
|
history.batch([addItem('a'), addItem('b'), addItem('c')]);
|
||||||
|
|
||||||
|
expect(items).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('undoes all batched commands in one step', () => {
|
||||||
|
history.batch([addItem('a'), addItem('b'), addItem('c')]);
|
||||||
|
history.undo();
|
||||||
|
|
||||||
|
expect(items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts as single history entry', () => {
|
||||||
|
history.batch([addItem('a'), addItem('b'), addItem('c')]);
|
||||||
|
|
||||||
|
expect(history.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('undoes batch in reverse order', () => {
|
||||||
|
const order: string[] = [];
|
||||||
|
|
||||||
|
history.batch([
|
||||||
|
{ execute: () => order.push('exec-1'), undo: () => order.push('undo-1') },
|
||||||
|
{ execute: () => order.push('exec-2'), undo: () => order.push('undo-2') },
|
||||||
|
{ execute: () => order.push('exec-3'), undo: () => order.push('undo-3') },
|
||||||
|
]);
|
||||||
|
history.undo();
|
||||||
|
|
||||||
|
expect(order).toEqual(['exec-1', 'exec-2', 'exec-3', 'undo-3', 'undo-2', 'undo-1']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('maxSize', () => {
|
||||||
|
it('limits undo stack', () => {
|
||||||
|
const limited = new CommandHistory({ maxSize: 2 });
|
||||||
|
items = [];
|
||||||
|
|
||||||
|
limited.execute(addItem('a'));
|
||||||
|
limited.execute(addItem('b'));
|
||||||
|
limited.execute(addItem('c'));
|
||||||
|
|
||||||
|
expect(limited.size).toBe(2);
|
||||||
|
expect(limited.canUndo).toBe(true);
|
||||||
|
|
||||||
|
limited.undo();
|
||||||
|
limited.undo();
|
||||||
|
|
||||||
|
expect(limited.canUndo).toBe(false);
|
||||||
|
expect(items).toEqual(['a']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('clears all history', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.execute(addItem('b'));
|
||||||
|
history.undo();
|
||||||
|
history.clear();
|
||||||
|
|
||||||
|
expect(history.canUndo).toBe(false);
|
||||||
|
expect(history.canRedo).toBe(false);
|
||||||
|
expect(history.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canUndo / canRedo', () => {
|
||||||
|
it('initially false', () => {
|
||||||
|
expect(history.canUndo).toBe(false);
|
||||||
|
expect(history.canRedo).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('canUndo after execute', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
|
||||||
|
expect(history.canUndo).toBe(true);
|
||||||
|
expect(history.canRedo).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('canRedo after undo', () => {
|
||||||
|
history.execute(addItem('a'));
|
||||||
|
history.undo();
|
||||||
|
|
||||||
|
expect(history.canUndo).toBe(false);
|
||||||
|
expect(history.canRedo).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('asyncCommandHistory', () => {
|
||||||
|
let history: AsyncCommandHistory;
|
||||||
|
let items: string[];
|
||||||
|
|
||||||
|
function addItemAsync(item: string): AsyncCommand {
|
||||||
|
return {
|
||||||
|
execute: async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
items.push(item);
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
items.pop();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
history = new AsyncCommandHistory();
|
||||||
|
items = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes async command', async () => {
|
||||||
|
await history.execute(addItemAsync('a'));
|
||||||
|
|
||||||
|
expect(items).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('undoes async command', async () => {
|
||||||
|
await history.execute(addItemAsync('a'));
|
||||||
|
await history.undo();
|
||||||
|
|
||||||
|
expect(items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redoes async command', async () => {
|
||||||
|
await history.execute(addItemAsync('a'));
|
||||||
|
await history.undo();
|
||||||
|
await history.redo();
|
||||||
|
|
||||||
|
expect(items).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('batches async commands', async () => {
|
||||||
|
await history.batch([addItemAsync('a'), addItemAsync('b'), addItemAsync('c')]);
|
||||||
|
|
||||||
|
expect(items).toEqual(['a', 'b', 'c']);
|
||||||
|
expect(history.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('undoes async batch', async () => {
|
||||||
|
await history.batch([addItemAsync('a'), addItemAsync('b')]);
|
||||||
|
await history.undo();
|
||||||
|
|
||||||
|
expect(items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with sync commands too', async () => {
|
||||||
|
await history.execute({
|
||||||
|
execute: () => { items.push('sync'); },
|
||||||
|
undo: () => { items.pop(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(items).toEqual(['sync']);
|
||||||
|
|
||||||
|
await history.undo();
|
||||||
|
|
||||||
|
expect(items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects maxSize', async () => {
|
||||||
|
const limited = new AsyncCommandHistory({ maxSize: 2 });
|
||||||
|
items = [];
|
||||||
|
|
||||||
|
await limited.execute(addItemAsync('a'));
|
||||||
|
await limited.execute(addItemAsync('b'));
|
||||||
|
await limited.execute(addItemAsync('c'));
|
||||||
|
|
||||||
|
expect(limited.size).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
3
core/stdlib/src/patterns/behavioral/Command/index.ts
Normal file
3
core/stdlib/src/patterns/behavioral/Command/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type * from './types';
|
||||||
|
export * from './sync';
|
||||||
|
export * from './async';
|
||||||
52
core/stdlib/src/patterns/behavioral/Command/sync.ts
Normal file
52
core/stdlib/src/patterns/behavioral/Command/sync.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { BaseCommandHistory } from './base';
|
||||||
|
import type { Command } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name CommandHistory
|
||||||
|
* @category Patterns
|
||||||
|
* @description Command history with undo/redo/batch support
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*/
|
||||||
|
export class CommandHistory extends BaseCommandHistory<Command> {
|
||||||
|
execute(command: Command): void {
|
||||||
|
command.execute();
|
||||||
|
this.pushUndo(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(): boolean {
|
||||||
|
const command = this.undoStack.pop();
|
||||||
|
|
||||||
|
if (!command)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
command.undo();
|
||||||
|
this.moveToRedo(command);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
redo(): boolean {
|
||||||
|
const command = this.redoStack.pop();
|
||||||
|
|
||||||
|
if (!command)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
command.execute();
|
||||||
|
this.moveToUndo(command);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch(commands: Command[]): void {
|
||||||
|
const macro: Command = {
|
||||||
|
execute: () => commands.forEach((c) => c.execute()),
|
||||||
|
undo: () => {
|
||||||
|
for (let i = commands.length - 1; i >= 0; i--)
|
||||||
|
commands[i]!.undo();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.execute(macro);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
core/stdlib/src/patterns/behavioral/Command/types.ts
Normal file
11
core/stdlib/src/patterns/behavioral/Command/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { MaybePromise } from '../../../types';
|
||||||
|
|
||||||
|
export interface Command {
|
||||||
|
execute: () => void;
|
||||||
|
undo: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncCommand {
|
||||||
|
execute: () => MaybePromise<void>;
|
||||||
|
undo: () => MaybePromise<void>;
|
||||||
|
}
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
import type { AnyFunction } from '../../../types';
|
|
||||||
|
|
||||||
export type Subscriber = AnyFunction;
|
|
||||||
|
|
||||||
export type EventHandlerMap = Record<PropertyKey, Subscriber>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name PubSub
|
* @name PubSub
|
||||||
* @category Patterns
|
* @category Patterns
|
||||||
@@ -11,9 +5,9 @@ export type EventHandlerMap = Record<PropertyKey, Subscriber>;
|
|||||||
*
|
*
|
||||||
* @since 0.0.2
|
* @since 0.0.2
|
||||||
*
|
*
|
||||||
* @template Events - Event map where all values are function types
|
* @template Events - Event map where keys are event names and values are listener signatures
|
||||||
*/
|
*/
|
||||||
export class PubSub<Events extends EventHandlerMap> {
|
export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
|
||||||
/**
|
/**
|
||||||
* Events map
|
* Events map
|
||||||
*
|
*
|
||||||
133
core/stdlib/src/patterns/behavioral/StateMachine/async.ts
Normal file
133
core/stdlib/src/patterns/behavioral/StateMachine/async.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { isString } from '../../../types';
|
||||||
|
import { BaseStateMachine } from './base';
|
||||||
|
import type { AsyncStateNodeConfig, ExtractStates, ExtractEvents } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name AsyncStateMachine
|
||||||
|
* @category Patterns
|
||||||
|
* @description Async finite state machine with support for async guards, actions, and hooks
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template States - Union of state names
|
||||||
|
* @template Events - Union of event names
|
||||||
|
* @template Context - Machine context type
|
||||||
|
*/
|
||||||
|
export class AsyncStateMachine<
|
||||||
|
States extends string = string,
|
||||||
|
Events extends string = string,
|
||||||
|
Context = undefined,
|
||||||
|
> extends BaseStateMachine<States, Events, Context, AsyncStateNodeConfig<Context>> {
|
||||||
|
/**
|
||||||
|
* Send an event to the machine, awaiting async guards, actions, and hooks
|
||||||
|
*
|
||||||
|
* @param event - Event name
|
||||||
|
* @returns The current state after processing the event
|
||||||
|
*/
|
||||||
|
async send(event: Events): Promise<States> {
|
||||||
|
const stateNode = this.states[this.currentState];
|
||||||
|
|
||||||
|
if (!stateNode?.on)
|
||||||
|
return this.currentState;
|
||||||
|
|
||||||
|
const transition = stateNode.on[event];
|
||||||
|
|
||||||
|
if (transition === undefined)
|
||||||
|
return this.currentState;
|
||||||
|
|
||||||
|
let target: string;
|
||||||
|
|
||||||
|
if (isString(transition)) {
|
||||||
|
target = transition;
|
||||||
|
} else {
|
||||||
|
if (transition.guard && !(await transition.guard(this.context)))
|
||||||
|
return this.currentState;
|
||||||
|
|
||||||
|
await transition.action?.(this.context);
|
||||||
|
target = transition.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stateNode.exit?.(this.context);
|
||||||
|
this.currentState = target as States;
|
||||||
|
await this.states[this.currentState]?.entry?.(this.context);
|
||||||
|
|
||||||
|
return this.currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an event can trigger a transition, awaiting async guards
|
||||||
|
*
|
||||||
|
* @param event - Event to check
|
||||||
|
*/
|
||||||
|
async can(event: Events): Promise<boolean> {
|
||||||
|
const stateNode = this.states[this.currentState];
|
||||||
|
|
||||||
|
if (!stateNode?.on)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const transition = stateNode.on[event];
|
||||||
|
|
||||||
|
if (transition === undefined)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!isString(transition) && transition.guard)
|
||||||
|
return await transition.guard(this.context);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a type-safe async finite state machine with context
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const machine = createAsyncMachine({
|
||||||
|
* initial: 'idle',
|
||||||
|
* context: { data: '' },
|
||||||
|
* states: {
|
||||||
|
* idle: {
|
||||||
|
* on: {
|
||||||
|
* FETCH: {
|
||||||
|
* target: 'loaded',
|
||||||
|
* guard: async () => await hasPermission(),
|
||||||
|
* action: async (ctx) => { ctx.data = await fetchData(); },
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* loaded: {
|
||||||
|
* entry: async (ctx) => { await saveToCache(ctx.data); },
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* await machine.send('FETCH'); // 'loaded'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createAsyncMachine<
|
||||||
|
const States extends Record<string, AsyncStateNodeConfig<Context>>,
|
||||||
|
Context,
|
||||||
|
>(config: {
|
||||||
|
initial: NoInfer<ExtractStates<States>>;
|
||||||
|
context: Context;
|
||||||
|
states: States;
|
||||||
|
}): AsyncStateMachine<ExtractStates<States>, ExtractEvents<States>, Context>;
|
||||||
|
|
||||||
|
export function createAsyncMachine<
|
||||||
|
const States extends Record<string, AsyncStateNodeConfig<undefined>>,
|
||||||
|
>(config: {
|
||||||
|
initial: NoInfer<ExtractStates<States>>;
|
||||||
|
states: States;
|
||||||
|
}): AsyncStateMachine<ExtractStates<States>, ExtractEvents<States>, undefined>;
|
||||||
|
|
||||||
|
export function createAsyncMachine(config: {
|
||||||
|
initial: string;
|
||||||
|
context?: unknown;
|
||||||
|
states: Record<string, AsyncStateNodeConfig<any>>;
|
||||||
|
}): AsyncStateMachine {
|
||||||
|
return new AsyncStateMachine(
|
||||||
|
config.initial,
|
||||||
|
config.states,
|
||||||
|
config.context as undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
44
core/stdlib/src/patterns/behavioral/StateMachine/base.ts
Normal file
44
core/stdlib/src/patterns/behavioral/StateMachine/base.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Base class for state machines — holds shared state, getters, and matches()
|
||||||
|
*
|
||||||
|
* @template States - Union of state names
|
||||||
|
* @template Events - Union of event names
|
||||||
|
* @template Context - Machine context type
|
||||||
|
* @template NodeConfig - State node configuration type
|
||||||
|
*/
|
||||||
|
export class BaseStateMachine<
|
||||||
|
States extends string,
|
||||||
|
_Events extends string,
|
||||||
|
Context,
|
||||||
|
NodeConfig,
|
||||||
|
> {
|
||||||
|
protected currentState: States;
|
||||||
|
protected readonly states: Record<string, NodeConfig>;
|
||||||
|
|
||||||
|
/** Machine context */
|
||||||
|
readonly context: Context;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
initial: States,
|
||||||
|
states: Record<string, NodeConfig>,
|
||||||
|
context: Context,
|
||||||
|
) {
|
||||||
|
this.currentState = initial;
|
||||||
|
this.context = context;
|
||||||
|
this.states = states;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Current state of the machine */
|
||||||
|
get current(): States {
|
||||||
|
return this.currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the machine is in a specific state
|
||||||
|
*
|
||||||
|
* @param state - State to check
|
||||||
|
*/
|
||||||
|
matches(state: States): boolean {
|
||||||
|
return this.currentState === state;
|
||||||
|
}
|
||||||
|
}
|
||||||
688
core/stdlib/src/patterns/behavioral/StateMachine/index.test.ts
Normal file
688
core/stdlib/src/patterns/behavioral/StateMachine/index.test.ts
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { createMachine, createAsyncMachine, StateMachine, AsyncStateMachine } from '.';
|
||||||
|
|
||||||
|
describe('stateMachine', () => {
|
||||||
|
describe('createMachine (without context)', () => {
|
||||||
|
let machine: ReturnType<typeof createSimpleMachine>;
|
||||||
|
|
||||||
|
function createSimpleMachine() {
|
||||||
|
return createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
START: 'running',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
running: {
|
||||||
|
on: {
|
||||||
|
STOP: 'idle',
|
||||||
|
PAUSE: 'paused',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paused: {
|
||||||
|
on: {
|
||||||
|
RESUME: 'running',
|
||||||
|
STOP: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
machine = createSimpleMachine();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes with the initial state', () => {
|
||||||
|
expect(machine.current).toBe('idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('transitions on send', () => {
|
||||||
|
machine.send('START');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('running');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns new state from send', () => {
|
||||||
|
const result = machine.send('START');
|
||||||
|
|
||||||
|
expect(result).toBe('running');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple transitions', () => {
|
||||||
|
machine.send('START');
|
||||||
|
machine.send('PAUSE');
|
||||||
|
machine.send('RESUME');
|
||||||
|
machine.send('STOP');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores unhandled events', () => {
|
||||||
|
machine.send('STOP');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores events not defined in current state', () => {
|
||||||
|
machine.send('PAUSE');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('idle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches current state', () => {
|
||||||
|
expect(machine.matches('idle')).toBe(true);
|
||||||
|
expect(machine.matches('running')).toBe(false);
|
||||||
|
|
||||||
|
machine.send('START');
|
||||||
|
|
||||||
|
expect(machine.matches('idle')).toBe(false);
|
||||||
|
expect(machine.matches('running')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks if event can be handled', () => {
|
||||||
|
expect(machine.can('START')).toBe(true);
|
||||||
|
expect(machine.can('STOP')).toBe(false);
|
||||||
|
expect(machine.can('PAUSE')).toBe(false);
|
||||||
|
|
||||||
|
machine.send('START');
|
||||||
|
|
||||||
|
expect(machine.can('START')).toBe(false);
|
||||||
|
expect(machine.can('STOP')).toBe(true);
|
||||||
|
expect(machine.can('PAUSE')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createMachine (with context)', () => {
|
||||||
|
function createContextMachine() {
|
||||||
|
return createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { count: 0, log: '' },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
START: {
|
||||||
|
target: 'running',
|
||||||
|
action: (ctx) => { ctx.count = 0; },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
running: {
|
||||||
|
on: {
|
||||||
|
INCREMENT: {
|
||||||
|
target: 'running',
|
||||||
|
action: (ctx) => { ctx.count++; },
|
||||||
|
},
|
||||||
|
STOP: 'idle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('provides typed context', () => {
|
||||||
|
const machine = createContextMachine();
|
||||||
|
|
||||||
|
expect(machine.context).toEqual({ count: 0, log: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs action on transition', () => {
|
||||||
|
const machine = createContextMachine();
|
||||||
|
|
||||||
|
machine.send('START');
|
||||||
|
machine.send('INCREMENT');
|
||||||
|
machine.send('INCREMENT');
|
||||||
|
machine.send('INCREMENT');
|
||||||
|
|
||||||
|
expect(machine.context.count).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets context via action', () => {
|
||||||
|
const machine = createContextMachine();
|
||||||
|
|
||||||
|
machine.send('START');
|
||||||
|
machine.send('INCREMENT');
|
||||||
|
machine.send('INCREMENT');
|
||||||
|
machine.send('STOP');
|
||||||
|
machine.send('START');
|
||||||
|
|
||||||
|
expect(machine.context.count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('guards', () => {
|
||||||
|
function createGuardedMachine() {
|
||||||
|
return createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { retries: 0 },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
TRY: {
|
||||||
|
target: 'attempting',
|
||||||
|
action: (ctx) => { ctx.retries++; },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attempting: {
|
||||||
|
on: {
|
||||||
|
FAIL: {
|
||||||
|
target: 'idle',
|
||||||
|
guard: (ctx) => ctx.retries < 3,
|
||||||
|
},
|
||||||
|
SUCCESS: 'done',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
done: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('allows transition when guard returns true', () => {
|
||||||
|
const machine = createGuardedMachine();
|
||||||
|
|
||||||
|
machine.send('TRY');
|
||||||
|
machine.send('FAIL');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('idle');
|
||||||
|
expect(machine.context.retries).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks transition when guard returns false', () => {
|
||||||
|
const machine = createGuardedMachine();
|
||||||
|
|
||||||
|
machine.send('TRY');
|
||||||
|
machine.send('FAIL');
|
||||||
|
machine.send('TRY');
|
||||||
|
machine.send('FAIL');
|
||||||
|
machine.send('TRY');
|
||||||
|
machine.send('FAIL');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('attempting');
|
||||||
|
expect(machine.context.retries).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reflects guard in can()', () => {
|
||||||
|
const machine = createGuardedMachine();
|
||||||
|
|
||||||
|
machine.send('TRY');
|
||||||
|
expect(machine.can('FAIL')).toBe(true);
|
||||||
|
|
||||||
|
machine.send('FAIL');
|
||||||
|
machine.send('TRY');
|
||||||
|
machine.send('FAIL');
|
||||||
|
machine.send('TRY');
|
||||||
|
|
||||||
|
expect(machine.can('FAIL')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('entry/exit hooks', () => {
|
||||||
|
it('calls exit on previous state and entry on next state', () => {
|
||||||
|
const exitIdle = vi.fn();
|
||||||
|
const enterRunning = vi.fn();
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: { START: 'running' },
|
||||||
|
exit: exitIdle,
|
||||||
|
},
|
||||||
|
running: {
|
||||||
|
on: { STOP: 'idle' },
|
||||||
|
entry: enterRunning,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
machine.send('START');
|
||||||
|
|
||||||
|
expect(exitIdle).toHaveBeenCalledOnce();
|
||||||
|
expect(enterRunning).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call hooks when transition is blocked by guard', () => {
|
||||||
|
const exitHook = vi.fn();
|
||||||
|
const entryHook = vi.fn();
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
initial: 'locked',
|
||||||
|
context: { unlocked: false },
|
||||||
|
states: {
|
||||||
|
locked: {
|
||||||
|
on: {
|
||||||
|
UNLOCK: {
|
||||||
|
target: 'unlocked',
|
||||||
|
guard: (ctx) => ctx.unlocked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: exitHook,
|
||||||
|
},
|
||||||
|
unlocked: {
|
||||||
|
entry: entryHook,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
machine.send('UNLOCK');
|
||||||
|
|
||||||
|
expect(exitHook).not.toHaveBeenCalled();
|
||||||
|
expect(entryHook).not.toHaveBeenCalled();
|
||||||
|
expect(machine.current).toBe('locked');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls hooks with context', () => {
|
||||||
|
const entryHook = vi.fn();
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { value: 42 },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: { GO: 'active' },
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
entry: entryHook,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
machine.send('GO');
|
||||||
|
|
||||||
|
expect(entryHook).toHaveBeenCalledWith({ value: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls exit and entry on self-transitions', () => {
|
||||||
|
const exitHook = vi.fn();
|
||||||
|
const entryHook = vi.fn();
|
||||||
|
|
||||||
|
const machine = createMachine({
|
||||||
|
initial: 'active',
|
||||||
|
states: {
|
||||||
|
active: {
|
||||||
|
on: { REFRESH: 'active' },
|
||||||
|
entry: entryHook,
|
||||||
|
exit: exitHook,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
machine.send('REFRESH');
|
||||||
|
|
||||||
|
expect(exitHook).toHaveBeenCalledOnce();
|
||||||
|
expect(entryHook).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('StateMachine class', () => {
|
||||||
|
it('can be instantiated directly', () => {
|
||||||
|
const machine = new StateMachine<'on' | 'off', 'TOGGLE'>(
|
||||||
|
'off',
|
||||||
|
{
|
||||||
|
off: { on: { TOGGLE: 'on' } },
|
||||||
|
on: { on: { TOGGLE: 'off' } },
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(machine.current).toBe('off');
|
||||||
|
|
||||||
|
machine.send('TOGGLE');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('on');
|
||||||
|
|
||||||
|
machine.send('TOGGLE');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('off');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('handles state with no transitions', () => {
|
||||||
|
const machine = createMachine({
|
||||||
|
initial: 'start',
|
||||||
|
states: {
|
||||||
|
start: {
|
||||||
|
on: { GO: 'end' },
|
||||||
|
},
|
||||||
|
end: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
machine.send('GO');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('end');
|
||||||
|
expect(machine.send('GO')).toBe('end');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles action that modifies context before guard on next transition', () => {
|
||||||
|
const machine = createMachine({
|
||||||
|
initial: 'a',
|
||||||
|
context: { step: 0 },
|
||||||
|
states: {
|
||||||
|
a: {
|
||||||
|
on: {
|
||||||
|
NEXT: {
|
||||||
|
target: 'b',
|
||||||
|
action: (ctx) => { ctx.step = 1; },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
on: {
|
||||||
|
NEXT: {
|
||||||
|
target: 'c',
|
||||||
|
guard: (ctx) => ctx.step === 1,
|
||||||
|
action: (ctx) => { ctx.step = 2; },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
c: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
machine.send('NEXT');
|
||||||
|
machine.send('NEXT');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('c');
|
||||||
|
expect(machine.context.step).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('asyncStateMachine', () => {
|
||||||
|
describe('createAsyncMachine (without context)', () => {
|
||||||
|
it('handles simple string transitions', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: { on: { START: 'running' } },
|
||||||
|
running: { on: { STOP: 'idle' } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await machine.send('START');
|
||||||
|
|
||||||
|
expect(result).toBe('running');
|
||||||
|
expect(machine.current).toBe('running');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores unhandled events', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: { on: { START: 'running' } },
|
||||||
|
running: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await machine.send('STOP');
|
||||||
|
|
||||||
|
expect(result).toBe('idle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('async guards', () => {
|
||||||
|
it('allows transition on async guard returning true', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { allowed: true },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
GO: {
|
||||||
|
target: 'active',
|
||||||
|
guard: async (ctx) => ctx.allowed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await machine.send('GO');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks transition on async guard returning false', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { allowed: false },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
GO: {
|
||||||
|
target: 'active',
|
||||||
|
guard: async (ctx) => ctx.allowed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await machine.send('GO');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('idle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('async actions', () => {
|
||||||
|
it('awaits async action before entering target', async () => {
|
||||||
|
const order: string[] = [];
|
||||||
|
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { data: '' },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
FETCH: {
|
||||||
|
target: 'done',
|
||||||
|
action: async (ctx) => {
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
ctx.data = 'fetched';
|
||||||
|
order.push('action');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
done: {
|
||||||
|
entry: () => { order.push('entry'); },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await machine.send('FETCH');
|
||||||
|
|
||||||
|
expect(machine.context.data).toBe('fetched');
|
||||||
|
expect(order).toEqual(['action', 'entry']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('async entry/exit hooks', () => {
|
||||||
|
it('awaits async exit and entry hooks in order', async () => {
|
||||||
|
const order: string[] = [];
|
||||||
|
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'a',
|
||||||
|
states: {
|
||||||
|
a: {
|
||||||
|
on: { GO: 'b' },
|
||||||
|
exit: async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
order.push('exit-a');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
entry: async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
|
order.push('entry-b');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await machine.send('GO');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('b');
|
||||||
|
expect(order).toEqual(['exit-a', 'entry-b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call hooks when async guard blocks', async () => {
|
||||||
|
const exitHook = vi.fn();
|
||||||
|
const entryHook = vi.fn();
|
||||||
|
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'locked',
|
||||||
|
context: { unlocked: false },
|
||||||
|
states: {
|
||||||
|
locked: {
|
||||||
|
on: {
|
||||||
|
UNLOCK: {
|
||||||
|
target: 'unlocked',
|
||||||
|
guard: async (ctx) => ctx.unlocked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exit: exitHook,
|
||||||
|
},
|
||||||
|
unlocked: {
|
||||||
|
entry: entryHook,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await machine.send('UNLOCK');
|
||||||
|
|
||||||
|
expect(exitHook).not.toHaveBeenCalled();
|
||||||
|
expect(entryHook).not.toHaveBeenCalled();
|
||||||
|
expect(machine.current).toBe('locked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('can()', () => {
|
||||||
|
it('evaluates async guard', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { ready: true },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
GO: {
|
||||||
|
target: 'active',
|
||||||
|
guard: async (ctx) => ctx.ready,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await machine.can('GO')).toBe(true);
|
||||||
|
|
||||||
|
machine.context.ready = false;
|
||||||
|
|
||||||
|
expect(await machine.can('GO')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for undefined events', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: { on: { START: 'running' } },
|
||||||
|
running: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await machine.can('STOP')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for transitions without guard', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: { on: { START: 'running' } },
|
||||||
|
running: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await machine.can('START')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('matches()', () => {
|
||||||
|
it('checks current state synchronously', async () => {
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
states: {
|
||||||
|
idle: { on: { GO: 'active' } },
|
||||||
|
active: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(machine.matches('idle')).toBe(true);
|
||||||
|
|
||||||
|
await machine.send('GO');
|
||||||
|
|
||||||
|
expect(machine.matches('active')).toBe(true);
|
||||||
|
expect(machine.matches('idle')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AsyncStateMachine class', () => {
|
||||||
|
it('can be instantiated directly', async () => {
|
||||||
|
const machine = new AsyncStateMachine<'on' | 'off', 'TOGGLE'>(
|
||||||
|
'off',
|
||||||
|
{
|
||||||
|
off: { on: { TOGGLE: 'on' } },
|
||||||
|
on: { on: { TOGGLE: 'off' } },
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(machine.current).toBe('off');
|
||||||
|
|
||||||
|
await machine.send('TOGGLE');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('on');
|
||||||
|
|
||||||
|
await machine.send('TOGGLE');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('off');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sync callbacks work too', () => {
|
||||||
|
it('handles sync guard/action/hooks in async machine', async () => {
|
||||||
|
const entryHook = vi.fn();
|
||||||
|
|
||||||
|
const machine = createAsyncMachine({
|
||||||
|
initial: 'idle',
|
||||||
|
context: { count: 0 },
|
||||||
|
states: {
|
||||||
|
idle: {
|
||||||
|
on: {
|
||||||
|
GO: {
|
||||||
|
target: 'active',
|
||||||
|
guard: (ctx) => ctx.count === 0,
|
||||||
|
action: (ctx) => { ctx.count++; },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
entry: entryHook,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await machine.send('GO');
|
||||||
|
|
||||||
|
expect(machine.current).toBe('active');
|
||||||
|
expect(machine.context.count).toBe(1);
|
||||||
|
expect(entryHook).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type * from './types';
|
||||||
|
export * from './sync';
|
||||||
|
export * from './async';
|
||||||
134
core/stdlib/src/patterns/behavioral/StateMachine/sync.ts
Normal file
134
core/stdlib/src/patterns/behavioral/StateMachine/sync.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { isString } from '../../../types';
|
||||||
|
import { BaseStateMachine } from './base';
|
||||||
|
import type { SyncStateNodeConfig, ExtractStates, ExtractEvents } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name StateMachine
|
||||||
|
* @category Patterns
|
||||||
|
* @description Simple, performant, and type-safe finite state machine
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template States - Union of state names
|
||||||
|
* @template Events - Union of event names
|
||||||
|
* @template Context - Machine context type
|
||||||
|
*/
|
||||||
|
export class StateMachine<
|
||||||
|
States extends string = string,
|
||||||
|
Events extends string = string,
|
||||||
|
Context = undefined,
|
||||||
|
> extends BaseStateMachine<States, Events, Context, SyncStateNodeConfig<Context>> {
|
||||||
|
/**
|
||||||
|
* Send an event to the machine, potentially causing a state transition
|
||||||
|
*
|
||||||
|
* @param event - Event name
|
||||||
|
* @returns The current state after processing the event
|
||||||
|
*/
|
||||||
|
send(event: Events): States {
|
||||||
|
const stateNode = this.states[this.currentState];
|
||||||
|
|
||||||
|
if (!stateNode?.on)
|
||||||
|
return this.currentState;
|
||||||
|
|
||||||
|
const transition = stateNode.on[event];
|
||||||
|
|
||||||
|
if (transition === undefined)
|
||||||
|
return this.currentState;
|
||||||
|
|
||||||
|
let target: string;
|
||||||
|
|
||||||
|
if (isString(transition)) {
|
||||||
|
target = transition;
|
||||||
|
} else {
|
||||||
|
if (transition.guard && !transition.guard(this.context))
|
||||||
|
return this.currentState;
|
||||||
|
|
||||||
|
transition.action?.(this.context);
|
||||||
|
target = transition.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateNode.exit?.(this.context);
|
||||||
|
this.currentState = target as States;
|
||||||
|
this.states[this.currentState]?.entry?.(this.context);
|
||||||
|
|
||||||
|
return this.currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an event can trigger a transition from the current state
|
||||||
|
*
|
||||||
|
* @param event - Event to check
|
||||||
|
*/
|
||||||
|
can(event: Events): boolean {
|
||||||
|
const stateNode = this.states[this.currentState];
|
||||||
|
|
||||||
|
if (!stateNode?.on)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const transition = stateNode.on[event];
|
||||||
|
|
||||||
|
if (transition === undefined)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!isString(transition) && transition.guard)
|
||||||
|
return transition.guard(this.context);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a type-safe synchronous finite state machine with context
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const machine = createMachine({
|
||||||
|
* initial: 'idle',
|
||||||
|
* context: { retries: 0 },
|
||||||
|
* states: {
|
||||||
|
* idle: {
|
||||||
|
* on: { START: 'running' },
|
||||||
|
* },
|
||||||
|
* running: {
|
||||||
|
* on: {
|
||||||
|
* FAIL: {
|
||||||
|
* target: 'idle',
|
||||||
|
* guard: (ctx) => ctx.retries < 3,
|
||||||
|
* action: (ctx) => { ctx.retries++; },
|
||||||
|
* },
|
||||||
|
* STOP: 'idle',
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* machine.send('START'); // 'running'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createMachine<
|
||||||
|
const States extends Record<string, SyncStateNodeConfig<Context>>,
|
||||||
|
Context,
|
||||||
|
>(config: {
|
||||||
|
initial: NoInfer<ExtractStates<States>>;
|
||||||
|
context: Context;
|
||||||
|
states: States;
|
||||||
|
}): StateMachine<ExtractStates<States>, ExtractEvents<States>, Context>;
|
||||||
|
|
||||||
|
export function createMachine<
|
||||||
|
const States extends Record<string, SyncStateNodeConfig<undefined>>,
|
||||||
|
>(config: {
|
||||||
|
initial: NoInfer<ExtractStates<States>>;
|
||||||
|
states: States;
|
||||||
|
}): StateMachine<ExtractStates<States>, ExtractEvents<States>, undefined>;
|
||||||
|
|
||||||
|
export function createMachine(config: {
|
||||||
|
initial: string;
|
||||||
|
context?: unknown;
|
||||||
|
states: Record<string, SyncStateNodeConfig<any>>;
|
||||||
|
}): StateMachine {
|
||||||
|
return new StateMachine(
|
||||||
|
config.initial,
|
||||||
|
config.states,
|
||||||
|
config.context as undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
64
core/stdlib/src/patterns/behavioral/StateMachine/types.ts
Normal file
64
core/stdlib/src/patterns/behavioral/StateMachine/types.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type { MaybePromise } from '../../../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for a state transition
|
||||||
|
*
|
||||||
|
* @template Context - Machine context type
|
||||||
|
* @template Guard - Guard return type (boolean or MaybePromise\<boolean\>)
|
||||||
|
* @template Action - Action return type (void or MaybePromise\<void\>)
|
||||||
|
*/
|
||||||
|
export interface TransitionConfig<
|
||||||
|
Context,
|
||||||
|
Guard = boolean,
|
||||||
|
Action = void,
|
||||||
|
> {
|
||||||
|
/** Target state to transition to */
|
||||||
|
target: string;
|
||||||
|
/** Guard condition — transition only occurs if this returns true */
|
||||||
|
guard?: (context: Context) => Guard;
|
||||||
|
/** Side effect executed during transition (before entering target state) */
|
||||||
|
action?: (context: Context) => Action;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transition can be a target state name or a detailed configuration
|
||||||
|
*/
|
||||||
|
export type Transition<
|
||||||
|
Context,
|
||||||
|
Guard = boolean,
|
||||||
|
Action = void,
|
||||||
|
> = string | TransitionConfig<Context, Guard, Action>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for a state node
|
||||||
|
*
|
||||||
|
* @template Context - Machine context type
|
||||||
|
* @template Guard - Guard return type
|
||||||
|
* @template Hook - Hook return type (entry/exit/action)
|
||||||
|
*/
|
||||||
|
export interface StateNodeConfig<
|
||||||
|
Context,
|
||||||
|
Guard = boolean,
|
||||||
|
Hook = void,
|
||||||
|
> {
|
||||||
|
/** Map of event names to transitions */
|
||||||
|
on?: Record<string, Transition<Context, Guard, Hook>>;
|
||||||
|
/** Hook called when entering this state */
|
||||||
|
entry?: (context: Context) => Hook;
|
||||||
|
/** Hook called when exiting this state */
|
||||||
|
exit?: (context: Context) => Hook;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sync state node config — guards return boolean, hooks return void */
|
||||||
|
export type SyncStateNodeConfig<Context> = StateNodeConfig<Context, boolean, void>;
|
||||||
|
|
||||||
|
/** Async state node config — guards return MaybePromise\<boolean\>, hooks return MaybePromise\<void\> */
|
||||||
|
export type AsyncStateNodeConfig<Context> = StateNodeConfig<Context, MaybePromise<boolean>, MaybePromise<void>>;
|
||||||
|
|
||||||
|
export type ExtractStates<T> = keyof T & string;
|
||||||
|
|
||||||
|
export type ExtractEvents<T> = {
|
||||||
|
[K in keyof T]: T[K] extends { readonly on?: Readonly<Record<infer E extends string, any>> }
|
||||||
|
? E
|
||||||
|
: never;
|
||||||
|
}[keyof T];
|
||||||
@@ -1 +1,3 @@
|
|||||||
export * from './behavioral/pubsub';
|
export * from './behavioral/Command';
|
||||||
|
export * from './behavioral/PubSub';
|
||||||
|
export * from './behavioral/StateMachine';
|
||||||
229
core/stdlib/src/structs/BinaryHeap/index.test.ts
Normal file
229
core/stdlib/src/structs/BinaryHeap/index.test.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { BinaryHeap } from '.';
|
||||||
|
|
||||||
|
describe('BinaryHeap', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create an empty heap', () => {
|
||||||
|
const heap = new BinaryHeap<number>();
|
||||||
|
|
||||||
|
expect(heap.length).toBe(0);
|
||||||
|
expect(heap.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a heap from single value', () => {
|
||||||
|
const heap = new BinaryHeap(42);
|
||||||
|
|
||||||
|
expect(heap.length).toBe(1);
|
||||||
|
expect(heap.peek()).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a heap from array (heapify)', () => {
|
||||||
|
const heap = new BinaryHeap([5, 3, 8, 1, 4]);
|
||||||
|
|
||||||
|
expect(heap.length).toBe(5);
|
||||||
|
expect(heap.peek()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a custom comparator for max-heap', () => {
|
||||||
|
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||||
|
comparator: (a, b) => b - a,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(heap.peek()).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('push', () => {
|
||||||
|
it('should insert elements maintaining heap property', () => {
|
||||||
|
const heap = new BinaryHeap<number>();
|
||||||
|
|
||||||
|
heap.push(5);
|
||||||
|
heap.push(3);
|
||||||
|
heap.push(8);
|
||||||
|
heap.push(1);
|
||||||
|
|
||||||
|
expect(heap.peek()).toBe(1);
|
||||||
|
expect(heap.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle duplicate values', () => {
|
||||||
|
const heap = new BinaryHeap<number>();
|
||||||
|
|
||||||
|
heap.push(3);
|
||||||
|
heap.push(3);
|
||||||
|
heap.push(3);
|
||||||
|
|
||||||
|
expect(heap.length).toBe(3);
|
||||||
|
expect(heap.peek()).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pop', () => {
|
||||||
|
it('should return undefined for empty heap', () => {
|
||||||
|
const heap = new BinaryHeap<number>();
|
||||||
|
|
||||||
|
expect(heap.pop()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract elements in min-heap order', () => {
|
||||||
|
const heap = new BinaryHeap([5, 3, 8, 1, 4, 2, 7, 6]);
|
||||||
|
const sorted: number[] = [];
|
||||||
|
|
||||||
|
while (!heap.isEmpty) {
|
||||||
|
sorted.push(heap.pop()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract elements in max-heap order with custom comparator', () => {
|
||||||
|
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||||
|
comparator: (a, b) => b - a,
|
||||||
|
});
|
||||||
|
const sorted: number[] = [];
|
||||||
|
|
||||||
|
while (!heap.isEmpty) {
|
||||||
|
sorted.push(heap.pop()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(sorted).toEqual([8, 5, 4, 3, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single element', () => {
|
||||||
|
const heap = new BinaryHeap(42);
|
||||||
|
|
||||||
|
expect(heap.pop()).toBe(42);
|
||||||
|
expect(heap.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peek', () => {
|
||||||
|
it('should return undefined for empty heap', () => {
|
||||||
|
const heap = new BinaryHeap<number>();
|
||||||
|
|
||||||
|
expect(heap.peek()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return root without removing it', () => {
|
||||||
|
const heap = new BinaryHeap([5, 3, 1]);
|
||||||
|
|
||||||
|
expect(heap.peek()).toBe(1);
|
||||||
|
expect(heap.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('should remove all elements', () => {
|
||||||
|
const heap = new BinaryHeap([1, 2, 3]);
|
||||||
|
|
||||||
|
const result = heap.clear();
|
||||||
|
|
||||||
|
expect(heap.length).toBe(0);
|
||||||
|
expect(heap.isEmpty).toBe(true);
|
||||||
|
expect(result).toBe(heap);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toArray', () => {
|
||||||
|
it('should return empty array for empty heap', () => {
|
||||||
|
const heap = new BinaryHeap<number>();
|
||||||
|
|
||||||
|
expect(heap.toArray()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a shallow copy', () => {
|
||||||
|
const heap = new BinaryHeap([3, 1, 2]);
|
||||||
|
const arr = heap.toArray();
|
||||||
|
|
||||||
|
arr.push(99);
|
||||||
|
|
||||||
|
expect(heap.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('should return formatted string', () => {
|
||||||
|
const heap = new BinaryHeap([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(heap.toString()).toBe('BinaryHeap(3)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('iterator', () => {
|
||||||
|
it('should iterate over heap elements', () => {
|
||||||
|
const heap = new BinaryHeap([5, 3, 8, 1]);
|
||||||
|
const elements = [...heap];
|
||||||
|
|
||||||
|
expect(elements.length).toBe(4);
|
||||||
|
expect(elements[0]).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('custom comparator', () => {
|
||||||
|
it('should work with string length comparator', () => {
|
||||||
|
const heap = new BinaryHeap(['banana', 'apple', 'kiwi', 'fig'], {
|
||||||
|
comparator: (a, b) => a.length - b.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(heap.pop()).toBe('fig');
|
||||||
|
expect(heap.pop()).toBe('kiwi');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with object comparator', () => {
|
||||||
|
interface Task {
|
||||||
|
priority: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const heap = new BinaryHeap<Task>(
|
||||||
|
[
|
||||||
|
{ priority: 3, name: 'low' },
|
||||||
|
{ priority: 1, name: 'high' },
|
||||||
|
{ priority: 2, name: 'medium' },
|
||||||
|
],
|
||||||
|
{ comparator: (a, b) => a.priority - b.priority },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(heap.pop()?.name).toBe('high');
|
||||||
|
expect(heap.pop()?.name).toBe('medium');
|
||||||
|
expect(heap.pop()?.name).toBe('low');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('heapify', () => {
|
||||||
|
it('should correctly heapify large arrays', () => {
|
||||||
|
const values = Array.from({ length: 1000 }, () => Math.random() * 1000 | 0);
|
||||||
|
const heap = new BinaryHeap(values);
|
||||||
|
const sorted: number[] = [];
|
||||||
|
|
||||||
|
while (!heap.isEmpty) {
|
||||||
|
sorted.push(heap.pop()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = [...values].sort((a, b) => a - b);
|
||||||
|
|
||||||
|
expect(sorted).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interleaved operations', () => {
|
||||||
|
it('should maintain heap property with mixed push and pop', () => {
|
||||||
|
const heap = new BinaryHeap<number>();
|
||||||
|
|
||||||
|
heap.push(10);
|
||||||
|
heap.push(5);
|
||||||
|
expect(heap.pop()).toBe(5);
|
||||||
|
|
||||||
|
heap.push(3);
|
||||||
|
heap.push(7);
|
||||||
|
expect(heap.pop()).toBe(3);
|
||||||
|
|
||||||
|
heap.push(1);
|
||||||
|
expect(heap.pop()).toBe(1);
|
||||||
|
expect(heap.pop()).toBe(7);
|
||||||
|
expect(heap.pop()).toBe(10);
|
||||||
|
expect(heap.pop()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
220
core/stdlib/src/structs/BinaryHeap/index.ts
Normal file
220
core/stdlib/src/structs/BinaryHeap/index.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import { first } from '../../arrays';
|
||||||
|
import { isArray } from '../../types';
|
||||||
|
import type { BinaryHeapLike, Comparator } from './types';
|
||||||
|
|
||||||
|
export type { BinaryHeapLike, Comparator } from './types';
|
||||||
|
|
||||||
|
export interface BinaryHeapOptions<T> {
|
||||||
|
comparator?: Comparator<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default min-heap comparator for numeric values
|
||||||
|
*
|
||||||
|
* @param {number} a First element
|
||||||
|
* @param {number} b Second element
|
||||||
|
* @returns {number} Negative if a < b, positive if a > b, zero if equal
|
||||||
|
*/
|
||||||
|
const defaultComparator: Comparator<any> = (a: number, b: number) => a - b;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name BinaryHeap
|
||||||
|
* @category Data Structures
|
||||||
|
* @description Binary heap backed by a flat array with configurable comparator
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template T The type of elements stored in the heap
|
||||||
|
*/
|
||||||
|
export class BinaryHeap<T> implements BinaryHeapLike<T> {
|
||||||
|
/**
|
||||||
|
* The comparator function used to order elements
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {Comparator<T>}
|
||||||
|
*/
|
||||||
|
private readonly comparator: Comparator<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal flat array backing the heap
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {T[]}
|
||||||
|
*/
|
||||||
|
private readonly heap: T[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of BinaryHeap
|
||||||
|
*
|
||||||
|
* @param {(T[] | T)} [initialValues] The initial values to heapify
|
||||||
|
* @param {BinaryHeapOptions<T>} [options] Heap configuration
|
||||||
|
*/
|
||||||
|
constructor(initialValues?: T[] | T, options?: BinaryHeapOptions<T>) {
|
||||||
|
this.comparator = options?.comparator ?? defaultComparator;
|
||||||
|
|
||||||
|
if (initialValues !== null && initialValues !== undefined) {
|
||||||
|
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||||
|
this.heap.push(...items);
|
||||||
|
this.heapify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of elements in the heap
|
||||||
|
* @returns {number} The number of elements in the heap
|
||||||
|
*/
|
||||||
|
public get length(): number {
|
||||||
|
return this.heap.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the heap is empty
|
||||||
|
* @returns {boolean} `true` if the heap is empty, `false` otherwise
|
||||||
|
*/
|
||||||
|
public get isEmpty(): boolean {
|
||||||
|
return this.heap.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes an element into the heap
|
||||||
|
* @param {T} element The element to insert
|
||||||
|
*/
|
||||||
|
public push(element: T): void {
|
||||||
|
this.heap.push(element);
|
||||||
|
this.siftUp(this.heap.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the root element (min or max depending on comparator)
|
||||||
|
* @returns {T | undefined} The root element, or `undefined` if the heap is empty
|
||||||
|
*/
|
||||||
|
public pop(): T | undefined {
|
||||||
|
if (this.heap.length === 0) return undefined;
|
||||||
|
|
||||||
|
const root = first(this.heap)!;
|
||||||
|
const last = this.heap.pop()!;
|
||||||
|
|
||||||
|
if (this.heap.length > 0) {
|
||||||
|
this.heap[0] = last;
|
||||||
|
this.siftDown(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root element without removing it
|
||||||
|
* @returns {T | undefined} The root element, or `undefined` if the heap is empty
|
||||||
|
*/
|
||||||
|
public peek(): T | undefined {
|
||||||
|
return first(this.heap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all elements from the heap
|
||||||
|
* @returns {this} The heap instance for chaining
|
||||||
|
*/
|
||||||
|
public clear(): this {
|
||||||
|
this.heap.length = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a shallow copy of the heap elements as an array (heap order, not sorted)
|
||||||
|
* @returns {T[]} Array of elements in heap order
|
||||||
|
*/
|
||||||
|
public toArray(): T[] {
|
||||||
|
return this.heap.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of the heap
|
||||||
|
* @returns {string} String representation
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
return `BinaryHeap(${this.heap.length})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator over heap elements in heap order
|
||||||
|
*/
|
||||||
|
public *[Symbol.iterator](): Iterator<T> {
|
||||||
|
yield* this.heap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async iterator over heap elements in heap order
|
||||||
|
*/
|
||||||
|
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||||
|
for (const element of this.heap)
|
||||||
|
yield element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores heap property by sifting an element up
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {number} index The index of the element to sift up
|
||||||
|
*/
|
||||||
|
private siftUp(index: number): void {
|
||||||
|
const heap = this.heap;
|
||||||
|
const cmp = this.comparator;
|
||||||
|
|
||||||
|
while (index > 0) {
|
||||||
|
const parent = (index - 1) >> 1;
|
||||||
|
|
||||||
|
if (cmp(heap[index]!, heap[parent]!) >= 0) break;
|
||||||
|
|
||||||
|
const temp = heap[index]!;
|
||||||
|
heap[index] = heap[parent]!;
|
||||||
|
heap[parent] = temp;
|
||||||
|
|
||||||
|
index = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores heap property by sifting an element down
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {number} index The index of the element to sift down
|
||||||
|
*/
|
||||||
|
private siftDown(index: number): void {
|
||||||
|
const heap = this.heap;
|
||||||
|
const cmp = this.comparator;
|
||||||
|
const length = heap.length;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
let smallest = index;
|
||||||
|
const left = 2 * index + 1;
|
||||||
|
const right = 2 * index + 2;
|
||||||
|
|
||||||
|
if (left < length && cmp(heap[left]!, heap[smallest]!) < 0) {
|
||||||
|
smallest = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right < length && cmp(heap[right]!, heap[smallest]!) < 0) {
|
||||||
|
smallest = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smallest === index) break;
|
||||||
|
|
||||||
|
const temp = heap[index]!;
|
||||||
|
heap[index] = heap[smallest]!;
|
||||||
|
heap[smallest] = temp;
|
||||||
|
|
||||||
|
index = smallest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds heap from unordered array in O(n) using Floyd's algorithm
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private heapify(): void {
|
||||||
|
for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {
|
||||||
|
this.siftDown(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
core/stdlib/src/structs/BinaryHeap/types.ts
Normal file
13
core/stdlib/src/structs/BinaryHeap/types.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export type Comparator<T> = (a: T, b: T) => number;
|
||||||
|
|
||||||
|
export interface BinaryHeapLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||||
|
readonly length: number;
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
|
||||||
|
push(element: T): void;
|
||||||
|
pop(): T | undefined;
|
||||||
|
peek(): T | undefined;
|
||||||
|
clear(): this;
|
||||||
|
toArray(): T[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
247
core/stdlib/src/structs/CircularBuffer/index.test.ts
Normal file
247
core/stdlib/src/structs/CircularBuffer/index.test.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { CircularBuffer } from '.';
|
||||||
|
|
||||||
|
describe('circularBuffer', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('create an empty buffer', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
|
||||||
|
expect(buf.length).toBe(0);
|
||||||
|
expect(buf.isEmpty).toBe(true);
|
||||||
|
expect(buf.capacity).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a buffer with initial array', () => {
|
||||||
|
const buf = new CircularBuffer([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(buf.length).toBe(3);
|
||||||
|
expect(buf.peekFront()).toBe(1);
|
||||||
|
expect(buf.peekBack()).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a buffer with a single value', () => {
|
||||||
|
const buf = new CircularBuffer(42);
|
||||||
|
|
||||||
|
expect(buf.length).toBe(1);
|
||||||
|
expect(buf.peekFront()).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a buffer with initial capacity hint', () => {
|
||||||
|
const buf = new CircularBuffer<number>(undefined, 32);
|
||||||
|
|
||||||
|
expect(buf.capacity).toBe(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('round capacity up to next power of two', () => {
|
||||||
|
const buf = new CircularBuffer<number>(undefined, 5);
|
||||||
|
|
||||||
|
expect(buf.capacity).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pushBack / popFront', () => {
|
||||||
|
it('FIFO order', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
buf.pushBack(1);
|
||||||
|
buf.pushBack(2);
|
||||||
|
buf.pushBack(3);
|
||||||
|
|
||||||
|
expect(buf.popFront()).toBe(1);
|
||||||
|
expect(buf.popFront()).toBe(2);
|
||||||
|
expect(buf.popFront()).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pushFront / popBack', () => {
|
||||||
|
it('LIFO order', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
buf.pushFront(1);
|
||||||
|
buf.pushFront(2);
|
||||||
|
buf.pushFront(3);
|
||||||
|
|
||||||
|
expect(buf.popBack()).toBe(1);
|
||||||
|
expect(buf.popBack()).toBe(2);
|
||||||
|
expect(buf.popBack()).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('popFront', () => {
|
||||||
|
it('return undefined if empty', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
|
||||||
|
expect(buf.popFront()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('popBack', () => {
|
||||||
|
it('return undefined if empty', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
|
||||||
|
expect(buf.popBack()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peekFront / peekBack', () => {
|
||||||
|
it('return elements without removing', () => {
|
||||||
|
const buf = new CircularBuffer([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(buf.peekFront()).toBe(1);
|
||||||
|
expect(buf.peekBack()).toBe(3);
|
||||||
|
expect(buf.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined if empty', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
|
||||||
|
expect(buf.peekFront()).toBeUndefined();
|
||||||
|
expect(buf.peekBack()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('access element by logical index', () => {
|
||||||
|
const buf = new CircularBuffer([10, 20, 30]);
|
||||||
|
|
||||||
|
expect(buf.get(0)).toBe(10);
|
||||||
|
expect(buf.get(1)).toBe(20);
|
||||||
|
expect(buf.get(2)).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined for out-of-bounds', () => {
|
||||||
|
const buf = new CircularBuffer([1, 2]);
|
||||||
|
|
||||||
|
expect(buf.get(-1)).toBeUndefined();
|
||||||
|
expect(buf.get(2)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('work correctly after wrap-around', () => {
|
||||||
|
const buf = new CircularBuffer<number>(undefined, 4);
|
||||||
|
|
||||||
|
buf.pushBack(1);
|
||||||
|
buf.pushBack(2);
|
||||||
|
buf.pushBack(3);
|
||||||
|
buf.pushBack(4);
|
||||||
|
buf.popFront();
|
||||||
|
buf.popFront();
|
||||||
|
buf.pushBack(5);
|
||||||
|
buf.pushBack(6);
|
||||||
|
|
||||||
|
expect(buf.get(0)).toBe(3);
|
||||||
|
expect(buf.get(1)).toBe(4);
|
||||||
|
expect(buf.get(2)).toBe(5);
|
||||||
|
expect(buf.get(3)).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('clear the buffer', () => {
|
||||||
|
const buf = new CircularBuffer([1, 2, 3]);
|
||||||
|
buf.clear();
|
||||||
|
|
||||||
|
expect(buf.length).toBe(0);
|
||||||
|
expect(buf.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return this for chaining', () => {
|
||||||
|
const buf = new CircularBuffer([1]);
|
||||||
|
|
||||||
|
expect(buf.clear()).toBe(buf);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('auto-grow', () => {
|
||||||
|
it('grow when capacity is exceeded', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
const initialCapacity = buf.capacity;
|
||||||
|
|
||||||
|
for (let i = 0; i < initialCapacity + 1; i++)
|
||||||
|
buf.pushBack(i);
|
||||||
|
|
||||||
|
expect(buf.length).toBe(initialCapacity + 1);
|
||||||
|
expect(buf.capacity).toBe(initialCapacity * 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserve order after grow', () => {
|
||||||
|
const buf = new CircularBuffer<number>(undefined, 4);
|
||||||
|
|
||||||
|
buf.pushBack(1);
|
||||||
|
buf.pushBack(2);
|
||||||
|
buf.popFront();
|
||||||
|
buf.pushBack(3);
|
||||||
|
buf.pushBack(4);
|
||||||
|
buf.pushBack(5);
|
||||||
|
buf.pushBack(6);
|
||||||
|
|
||||||
|
expect(buf.toArray()).toEqual([2, 3, 4, 5, 6]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('wrap-around', () => {
|
||||||
|
it('handle wrap-around correctly', () => {
|
||||||
|
const buf = new CircularBuffer<number>(undefined, 4);
|
||||||
|
|
||||||
|
buf.pushBack(1);
|
||||||
|
buf.pushBack(2);
|
||||||
|
buf.pushBack(3);
|
||||||
|
buf.pushBack(4);
|
||||||
|
buf.popFront();
|
||||||
|
buf.popFront();
|
||||||
|
buf.pushBack(5);
|
||||||
|
buf.pushBack(6);
|
||||||
|
|
||||||
|
expect(buf.toArray()).toEqual([3, 4, 5, 6]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle alternating front/back', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
|
||||||
|
buf.pushFront(3);
|
||||||
|
buf.pushBack(4);
|
||||||
|
buf.pushFront(2);
|
||||||
|
buf.pushBack(5);
|
||||||
|
buf.pushFront(1);
|
||||||
|
|
||||||
|
expect(buf.toArray()).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toArray', () => {
|
||||||
|
it('return elements front to back', () => {
|
||||||
|
const buf = new CircularBuffer([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(buf.toArray()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return empty array if empty', () => {
|
||||||
|
const buf = new CircularBuffer<number>();
|
||||||
|
|
||||||
|
expect(buf.toArray()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('return comma-separated string', () => {
|
||||||
|
const buf = new CircularBuffer([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(buf.toString()).toBe('1,2,3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('iteration', () => {
|
||||||
|
it('iterate front to back', () => {
|
||||||
|
const buf = new CircularBuffer([1, 2, 3]);
|
||||||
|
|
||||||
|
expect([...buf]).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iterate asynchronously', async () => {
|
||||||
|
const buf = new CircularBuffer([1, 2, 3]);
|
||||||
|
const elements: number[] = [];
|
||||||
|
|
||||||
|
for await (const element of buf)
|
||||||
|
elements.push(element);
|
||||||
|
|
||||||
|
expect(elements).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
277
core/stdlib/src/structs/CircularBuffer/index.ts
Normal file
277
core/stdlib/src/structs/CircularBuffer/index.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import { isArray } from '../../types';
|
||||||
|
import type { CircularBufferLike } from './types';
|
||||||
|
|
||||||
|
export type { CircularBufferLike } from './types';
|
||||||
|
|
||||||
|
const MIN_CAPACITY = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name CircularBuffer
|
||||||
|
* @category Data Structures
|
||||||
|
* @description A circular (ring) buffer with automatic growth, O(1) push/pop on both ends
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template T The type of elements stored in the buffer
|
||||||
|
*/
|
||||||
|
export class CircularBuffer<T> implements CircularBufferLike<T> {
|
||||||
|
/**
|
||||||
|
* The internal storage
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {(T | undefined)[]}
|
||||||
|
*/
|
||||||
|
private buffer: Array<T | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index of the front element
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
private head: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of elements in the buffer
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
private count: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of CircularBuffer
|
||||||
|
*
|
||||||
|
* @param {(T[] | T)} [initialValues] The initial values to add to the buffer
|
||||||
|
* @param {number} [initialCapacity] The initial capacity hint (rounded up to next power of two)
|
||||||
|
*/
|
||||||
|
constructor(initialValues?: T[] | T, initialCapacity?: number) {
|
||||||
|
this.head = 0;
|
||||||
|
this.count = 0;
|
||||||
|
|
||||||
|
const items = isArray(initialValues) ? initialValues : initialValues !== undefined ? [initialValues] : [];
|
||||||
|
const requested = Math.max(items.length, initialCapacity ?? 0);
|
||||||
|
const cap = Math.max(MIN_CAPACITY, nextPowerOfTwo(requested));
|
||||||
|
|
||||||
|
this.buffer = Array.from<T | undefined>({ length: cap });
|
||||||
|
|
||||||
|
for (const item of items)
|
||||||
|
this.pushBack(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of elements in the buffer
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get length() {
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current capacity of the buffer
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get capacity() {
|
||||||
|
return this.buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the buffer is empty
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get isEmpty() {
|
||||||
|
return this.count === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the buffer is at capacity (before auto-grow)
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get isFull() {
|
||||||
|
return this.count === this.buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an element to the back of the buffer
|
||||||
|
* @param {T} element The element to add
|
||||||
|
*/
|
||||||
|
pushBack(element: T) {
|
||||||
|
if (this.count === this.buffer.length)
|
||||||
|
this.grow();
|
||||||
|
|
||||||
|
this.buffer[(this.head + this.count) & (this.buffer.length - 1)] = element;
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an element to the front of the buffer
|
||||||
|
* @param {T} element The element to add
|
||||||
|
*/
|
||||||
|
pushFront(element: T) {
|
||||||
|
if (this.count === this.buffer.length)
|
||||||
|
this.grow();
|
||||||
|
|
||||||
|
this.head = (this.head - 1 + this.buffer.length) & (this.buffer.length - 1);
|
||||||
|
this.buffer[this.head] = element;
|
||||||
|
this.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the back element
|
||||||
|
* @returns {T | undefined} The back element, or undefined if empty
|
||||||
|
*/
|
||||||
|
popBack() {
|
||||||
|
if (this.isEmpty)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
const index = (this.head + this.count - 1) & (this.buffer.length - 1);
|
||||||
|
const element = this.buffer[index];
|
||||||
|
|
||||||
|
this.buffer[index] = undefined;
|
||||||
|
this.count--;
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the front element
|
||||||
|
* @returns {T | undefined} The front element, or undefined if empty
|
||||||
|
*/
|
||||||
|
popFront() {
|
||||||
|
if (this.isEmpty)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
const element = this.buffer[this.head];
|
||||||
|
|
||||||
|
this.buffer[this.head] = undefined;
|
||||||
|
this.head = (this.head + 1) & (this.buffer.length - 1);
|
||||||
|
this.count--;
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the back element without removing it
|
||||||
|
* @returns {T | undefined}
|
||||||
|
*/
|
||||||
|
peekBack() {
|
||||||
|
if (this.isEmpty)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
return this.buffer[(this.head + this.count - 1) & (this.buffer.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the front element without removing it
|
||||||
|
* @returns {T | undefined}
|
||||||
|
*/
|
||||||
|
peekFront() {
|
||||||
|
if (this.isEmpty)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
return this.buffer[this.head];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets element at logical index (0 = front)
|
||||||
|
* @param {number} index The logical index
|
||||||
|
* @returns {T | undefined}
|
||||||
|
*/
|
||||||
|
get(index: number) {
|
||||||
|
if (index < 0 || index >= this.count)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
return this.buffer[(this.head + index) & (this.buffer.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the buffer
|
||||||
|
*
|
||||||
|
* @returns {this}
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.buffer = Array.from<T | undefined>({ length: MIN_CAPACITY });
|
||||||
|
this.head = 0;
|
||||||
|
this.count = 0;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the buffer to an array from front to back
|
||||||
|
*
|
||||||
|
* @returns {T[]}
|
||||||
|
*/
|
||||||
|
toArray() {
|
||||||
|
const result = Array.from<T>({ length: this.count });
|
||||||
|
|
||||||
|
for (let i = 0; i < this.count; i++)
|
||||||
|
result[i] = this.buffer[(this.head + i) & (this.buffer.length - 1)] as T;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
return this.toArray().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator (front to back)
|
||||||
|
*
|
||||||
|
* @returns {IterableIterator<T>}
|
||||||
|
*/
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.toArray()[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an async iterator (front to back)
|
||||||
|
*
|
||||||
|
* @returns {AsyncIterableIterator<T>}
|
||||||
|
*/
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
for (const element of this)
|
||||||
|
yield element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doubles the buffer capacity and linearizes elements
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private grow() {
|
||||||
|
const newCapacity = this.buffer.length << 1;
|
||||||
|
const newBuffer = Array.from<T | undefined>({ length: newCapacity });
|
||||||
|
|
||||||
|
for (let i = 0; i < this.count; i++)
|
||||||
|
newBuffer[i] = this.buffer[(this.head + i) & (this.buffer.length - 1)];
|
||||||
|
|
||||||
|
this.buffer = newBuffer;
|
||||||
|
this.head = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next power of two >= n
|
||||||
|
*
|
||||||
|
* @param {number} n
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function nextPowerOfTwo(n: number): number {
|
||||||
|
if (n <= 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
n--;
|
||||||
|
n |= n >> 1;
|
||||||
|
n |= n >> 2;
|
||||||
|
n |= n >> 4;
|
||||||
|
n |= n >> 8;
|
||||||
|
n |= n >> 16;
|
||||||
|
|
||||||
|
return n + 1;
|
||||||
|
}
|
||||||
17
core/stdlib/src/structs/CircularBuffer/types.ts
Normal file
17
core/stdlib/src/structs/CircularBuffer/types.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export interface CircularBufferLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||||
|
readonly length: number;
|
||||||
|
readonly capacity: number;
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
pushBack(element: T): void;
|
||||||
|
pushFront(element: T): void;
|
||||||
|
popBack(): T | undefined;
|
||||||
|
popFront(): T | undefined;
|
||||||
|
peekBack(): T | undefined;
|
||||||
|
peekFront(): T | undefined;
|
||||||
|
get(index: number): T | undefined;
|
||||||
|
clear(): this;
|
||||||
|
toArray(): T[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
288
core/stdlib/src/structs/Deque/index.test.ts
Normal file
288
core/stdlib/src/structs/Deque/index.test.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Deque } from '.';
|
||||||
|
|
||||||
|
describe('deque', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('create an empty deque if no initial values are provided', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
expect(deque.length).toBe(0);
|
||||||
|
expect(deque.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a deque with the provided initial values', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.length).toBe(3);
|
||||||
|
expect(deque.peekFront()).toBe(1);
|
||||||
|
expect(deque.peekBack()).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a deque with a single initial value', () => {
|
||||||
|
const deque = new Deque(42);
|
||||||
|
|
||||||
|
expect(deque.length).toBe(1);
|
||||||
|
expect(deque.peekFront()).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a deque with the provided options', () => {
|
||||||
|
const deque = new Deque<number>(undefined, { maxSize: 5 });
|
||||||
|
|
||||||
|
expect(deque.length).toBe(0);
|
||||||
|
expect(deque.isFull).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pushBack', () => {
|
||||||
|
it('add an element to the back', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
deque.pushBack(1).pushBack(2);
|
||||||
|
|
||||||
|
expect(deque.peekFront()).toBe(1);
|
||||||
|
expect(deque.peekBack()).toBe(2);
|
||||||
|
expect(deque.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw an error if the deque is full', () => {
|
||||||
|
const deque = new Deque<number>(undefined, { maxSize: 1 });
|
||||||
|
deque.pushBack(1);
|
||||||
|
|
||||||
|
expect(() => deque.pushBack(2)).toThrow(new RangeError('Deque is full'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return this for chaining', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
expect(deque.pushBack(1)).toBe(deque);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pushFront', () => {
|
||||||
|
it('add an element to the front', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
deque.pushFront(1).pushFront(2);
|
||||||
|
|
||||||
|
expect(deque.peekFront()).toBe(2);
|
||||||
|
expect(deque.peekBack()).toBe(1);
|
||||||
|
expect(deque.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw an error if the deque is full', () => {
|
||||||
|
const deque = new Deque<number>(undefined, { maxSize: 1 });
|
||||||
|
deque.pushFront(1);
|
||||||
|
|
||||||
|
expect(() => deque.pushFront(2)).toThrow(new RangeError('Deque is full'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return this for chaining', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
expect(deque.pushFront(1)).toBe(deque);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('popBack', () => {
|
||||||
|
it('remove and return the back element', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.popBack()).toBe(3);
|
||||||
|
expect(deque.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined if the deque is empty', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
expect(deque.popBack()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('popFront', () => {
|
||||||
|
it('remove and return the front element', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.popFront()).toBe(1);
|
||||||
|
expect(deque.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined if the deque is empty', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
expect(deque.popFront()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peekBack', () => {
|
||||||
|
it('return the back element without removing it', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.peekBack()).toBe(3);
|
||||||
|
expect(deque.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined if the deque is empty', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
expect(deque.peekBack()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peekFront', () => {
|
||||||
|
it('return the front element without removing it', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.peekFront()).toBe(1);
|
||||||
|
expect(deque.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined if the deque is empty', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
expect(deque.peekFront()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('clear the deque', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
deque.clear();
|
||||||
|
|
||||||
|
expect(deque.length).toBe(0);
|
||||||
|
expect(deque.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return this for chaining', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.clear()).toBe(deque);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toArray', () => {
|
||||||
|
it('return elements from front to back', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.toArray()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return correct order after mixed operations', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
deque.pushBack(2);
|
||||||
|
deque.pushBack(3);
|
||||||
|
deque.pushFront(1);
|
||||||
|
deque.pushFront(0);
|
||||||
|
|
||||||
|
expect(deque.toArray()).toEqual([0, 1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('return comma-separated string', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(deque.toString()).toBe('1,2,3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('iteration', () => {
|
||||||
|
it('iterate in front-to-back order', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
|
||||||
|
expect([...deque]).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iterate asynchronously', async () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
const elements: number[] = [];
|
||||||
|
|
||||||
|
for await (const element of deque)
|
||||||
|
elements.push(element);
|
||||||
|
|
||||||
|
expect(elements).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('circular buffer behavior', () => {
|
||||||
|
it('handle wrap-around correctly', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++)
|
||||||
|
deque.pushBack(i);
|
||||||
|
|
||||||
|
deque.popFront();
|
||||||
|
deque.popFront();
|
||||||
|
deque.pushBack(4);
|
||||||
|
deque.pushBack(5);
|
||||||
|
|
||||||
|
expect(deque.toArray()).toEqual([2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('grow the buffer when needed', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++)
|
||||||
|
deque.pushBack(i);
|
||||||
|
|
||||||
|
expect(deque.length).toBe(100);
|
||||||
|
expect(deque.peekFront()).toBe(0);
|
||||||
|
expect(deque.peekBack()).toBe(99);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle alternating front/back operations', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
|
||||||
|
deque.pushFront(3);
|
||||||
|
deque.pushBack(4);
|
||||||
|
deque.pushFront(2);
|
||||||
|
deque.pushBack(5);
|
||||||
|
deque.pushFront(1);
|
||||||
|
|
||||||
|
expect(deque.toArray()).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
expect(deque.popFront()).toBe(1);
|
||||||
|
expect(deque.popBack()).toBe(5);
|
||||||
|
expect(deque.toArray()).toEqual([2, 3, 4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mixed operations', () => {
|
||||||
|
it('use as a stack (LIFO)', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
deque.pushBack(1).pushBack(2).pushBack(3);
|
||||||
|
|
||||||
|
expect(deque.popBack()).toBe(3);
|
||||||
|
expect(deque.popBack()).toBe(2);
|
||||||
|
expect(deque.popBack()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('use as a queue (FIFO)', () => {
|
||||||
|
const deque = new Deque<number>();
|
||||||
|
deque.pushBack(1).pushBack(2).pushBack(3);
|
||||||
|
|
||||||
|
expect(deque.popFront()).toBe(1);
|
||||||
|
expect(deque.popFront()).toBe(2);
|
||||||
|
expect(deque.popFront()).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuse deque after clear', () => {
|
||||||
|
const deque = new Deque([1, 2, 3]);
|
||||||
|
deque.clear();
|
||||||
|
deque.pushBack(4);
|
||||||
|
|
||||||
|
expect(deque.length).toBe(1);
|
||||||
|
expect(deque.peekFront()).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maxSize limits capacity', () => {
|
||||||
|
const deque = new Deque<number>(undefined, { maxSize: 3 });
|
||||||
|
deque.pushBack(1).pushBack(2).pushBack(3);
|
||||||
|
|
||||||
|
expect(deque.isFull).toBe(true);
|
||||||
|
expect(() => deque.pushFront(0)).toThrow(new RangeError('Deque is full'));
|
||||||
|
|
||||||
|
deque.popFront();
|
||||||
|
deque.pushFront(0);
|
||||||
|
|
||||||
|
expect(deque.toArray()).toEqual([0, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
180
core/stdlib/src/structs/Deque/index.ts
Normal file
180
core/stdlib/src/structs/Deque/index.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { CircularBuffer } from '../CircularBuffer';
|
||||||
|
import type { DequeLike } from './types';
|
||||||
|
|
||||||
|
export type { DequeLike } from './types';
|
||||||
|
|
||||||
|
export interface DequeOptions {
|
||||||
|
maxSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Deque
|
||||||
|
* @category Data Structures
|
||||||
|
* @description Represents a double-ended queue backed by a circular buffer
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template T The type of elements stored in the deque
|
||||||
|
*/
|
||||||
|
export class Deque<T> implements DequeLike<T> {
|
||||||
|
/**
|
||||||
|
* The maximum number of elements that the deque can hold
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
private readonly maxSize: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The underlying circular buffer
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {CircularBuffer<T>}
|
||||||
|
*/
|
||||||
|
private readonly buffer: CircularBuffer<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of Deque
|
||||||
|
*
|
||||||
|
* @param {(T[] | T)} [initialValues] The initial values to add to the deque
|
||||||
|
* @param {DequeOptions} [options] The options for the deque
|
||||||
|
*/
|
||||||
|
constructor(initialValues?: T[] | T, options?: DequeOptions) {
|
||||||
|
this.maxSize = options?.maxSize ?? Infinity;
|
||||||
|
this.buffer = new CircularBuffer(initialValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of elements in the deque
|
||||||
|
* @returns {number} The number of elements in the deque
|
||||||
|
*/
|
||||||
|
get length() {
|
||||||
|
return this.buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the deque is empty
|
||||||
|
* @returns {boolean} `true` if the deque is empty, `false` otherwise
|
||||||
|
*/
|
||||||
|
get isEmpty() {
|
||||||
|
return this.buffer.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the deque is full
|
||||||
|
* @returns {boolean} `true` if the deque is full, `false` otherwise
|
||||||
|
*/
|
||||||
|
get isFull() {
|
||||||
|
return this.buffer.length === this.maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an element to the back of the deque
|
||||||
|
* @param {T} element The element to add
|
||||||
|
* @returns {this}
|
||||||
|
* @throws {RangeError} If the deque is full
|
||||||
|
*/
|
||||||
|
pushBack(element: T) {
|
||||||
|
if (this.isFull)
|
||||||
|
throw new RangeError('Deque is full');
|
||||||
|
|
||||||
|
this.buffer.pushBack(element);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an element to the front of the deque
|
||||||
|
* @param {T} element The element to add
|
||||||
|
* @returns {this}
|
||||||
|
* @throws {RangeError} If the deque is full
|
||||||
|
*/
|
||||||
|
pushFront(element: T) {
|
||||||
|
if (this.isFull)
|
||||||
|
throw new RangeError('Deque is full');
|
||||||
|
|
||||||
|
this.buffer.pushFront(element);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the back element of the deque
|
||||||
|
* @returns {T | undefined} The back element, or undefined if empty
|
||||||
|
*/
|
||||||
|
popBack() {
|
||||||
|
return this.buffer.popBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the front element of the deque
|
||||||
|
* @returns {T | undefined} The front element, or undefined if empty
|
||||||
|
*/
|
||||||
|
popFront() {
|
||||||
|
return this.buffer.popFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the back element without removing it
|
||||||
|
* @returns {T | undefined} The back element, or undefined if empty
|
||||||
|
*/
|
||||||
|
peekBack() {
|
||||||
|
return this.buffer.peekBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the front element without removing it
|
||||||
|
* @returns {T | undefined} The front element, or undefined if empty
|
||||||
|
*/
|
||||||
|
peekFront() {
|
||||||
|
return this.buffer.peekFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the deque
|
||||||
|
*
|
||||||
|
* @returns {this}
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.buffer.clear();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the deque to an array from front to back
|
||||||
|
*
|
||||||
|
* @returns {T[]}
|
||||||
|
*/
|
||||||
|
toArray() {
|
||||||
|
return this.buffer.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of the deque
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
return this.buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator for the deque (front to back)
|
||||||
|
*
|
||||||
|
* @returns {IterableIterator<T>}
|
||||||
|
*/
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.buffer[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an async iterator for the deque (front to back)
|
||||||
|
*
|
||||||
|
* @returns {AsyncIterableIterator<T>}
|
||||||
|
*/
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
for (const element of this.buffer)
|
||||||
|
yield element;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
core/stdlib/src/structs/Deque/types.ts
Normal file
15
core/stdlib/src/structs/Deque/types.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export interface DequeLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||||
|
readonly length: number;
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
pushBack(element: T): this;
|
||||||
|
pushFront(element: T): this;
|
||||||
|
popBack(): T | undefined;
|
||||||
|
popFront(): T | undefined;
|
||||||
|
peekBack(): T | undefined;
|
||||||
|
peekFront(): T | undefined;
|
||||||
|
clear(): this;
|
||||||
|
toArray(): T[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
406
core/stdlib/src/structs/LinkedList/index.test.ts
Normal file
406
core/stdlib/src/structs/LinkedList/index.test.ts
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { LinkedList } from '.';
|
||||||
|
|
||||||
|
describe('LinkedList', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create an empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
expect(list.length).toBe(0);
|
||||||
|
expect(list.isEmpty).toBe(true);
|
||||||
|
expect(list.head).toBeUndefined();
|
||||||
|
expect(list.tail).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a list from single value', () => {
|
||||||
|
const list = new LinkedList(42);
|
||||||
|
|
||||||
|
expect(list.length).toBe(1);
|
||||||
|
expect(list.peekFront()).toBe(42);
|
||||||
|
expect(list.peekBack()).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a list from array', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(list.length).toBe(3);
|
||||||
|
expect(list.peekFront()).toBe(1);
|
||||||
|
expect(list.peekBack()).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pushBack', () => {
|
||||||
|
it('should append to empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
const node = list.pushBack(1);
|
||||||
|
|
||||||
|
expect(list.length).toBe(1);
|
||||||
|
expect(node.value).toBe(1);
|
||||||
|
expect(list.head).toBe(node);
|
||||||
|
expect(list.tail).toBe(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append to non-empty list', () => {
|
||||||
|
const list = new LinkedList([1, 2]);
|
||||||
|
|
||||||
|
list.pushBack(3);
|
||||||
|
|
||||||
|
expect(list.length).toBe(3);
|
||||||
|
expect(list.peekBack()).toBe(3);
|
||||||
|
expect(list.peekFront()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the created node', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
const node = list.pushBack(5);
|
||||||
|
|
||||||
|
expect(node.value).toBe(5);
|
||||||
|
expect(node.prev).toBeUndefined();
|
||||||
|
expect(node.next).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pushFront', () => {
|
||||||
|
it('should prepend to empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
const node = list.pushFront(1);
|
||||||
|
|
||||||
|
expect(list.length).toBe(1);
|
||||||
|
expect(list.head).toBe(node);
|
||||||
|
expect(list.tail).toBe(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prepend to non-empty list', () => {
|
||||||
|
const list = new LinkedList([2, 3]);
|
||||||
|
|
||||||
|
list.pushFront(1);
|
||||||
|
|
||||||
|
expect(list.length).toBe(3);
|
||||||
|
expect(list.peekFront()).toBe(1);
|
||||||
|
expect(list.peekBack()).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('popBack', () => {
|
||||||
|
it('should return undefined for empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
expect(list.popBack()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove and return last value', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(list.popBack()).toBe(3);
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
expect(list.peekBack()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single element', () => {
|
||||||
|
const list = new LinkedList(1);
|
||||||
|
|
||||||
|
expect(list.popBack()).toBe(1);
|
||||||
|
expect(list.isEmpty).toBe(true);
|
||||||
|
expect(list.head).toBeUndefined();
|
||||||
|
expect(list.tail).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('popFront', () => {
|
||||||
|
it('should return undefined for empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
expect(list.popFront()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove and return first value', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(list.popFront()).toBe(1);
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
expect(list.peekFront()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single element', () => {
|
||||||
|
const list = new LinkedList(1);
|
||||||
|
|
||||||
|
expect(list.popFront()).toBe(1);
|
||||||
|
expect(list.isEmpty).toBe(true);
|
||||||
|
expect(list.head).toBeUndefined();
|
||||||
|
expect(list.tail).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peekBack', () => {
|
||||||
|
it('should return undefined for empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
expect(list.peekBack()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last value without removing', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(list.peekBack()).toBe(3);
|
||||||
|
expect(list.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peekFront', () => {
|
||||||
|
it('should return undefined for empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
expect(list.peekFront()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return first value without removing', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(list.peekFront()).toBe(1);
|
||||||
|
expect(list.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insertBefore', () => {
|
||||||
|
it('should insert before head', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const node = list.pushBack(2);
|
||||||
|
|
||||||
|
list.insertBefore(node, 1);
|
||||||
|
|
||||||
|
expect(list.peekFront()).toBe(1);
|
||||||
|
expect(list.peekBack()).toBe(2);
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert before middle node', () => {
|
||||||
|
const list = new LinkedList([1, 3]);
|
||||||
|
const tail = list.tail!;
|
||||||
|
|
||||||
|
list.insertBefore(tail, 2);
|
||||||
|
|
||||||
|
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the created node', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const existing = list.pushBack(2);
|
||||||
|
|
||||||
|
const newNode = list.insertBefore(existing, 1);
|
||||||
|
|
||||||
|
expect(newNode.value).toBe(1);
|
||||||
|
expect(newNode.next).toBe(existing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insertAfter', () => {
|
||||||
|
it('should insert after tail', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const node = list.pushBack(1);
|
||||||
|
|
||||||
|
list.insertAfter(node, 2);
|
||||||
|
|
||||||
|
expect(list.peekFront()).toBe(1);
|
||||||
|
expect(list.peekBack()).toBe(2);
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert after middle node', () => {
|
||||||
|
const list = new LinkedList([1, 3]);
|
||||||
|
const head = list.head!;
|
||||||
|
|
||||||
|
list.insertAfter(head, 2);
|
||||||
|
|
||||||
|
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the created node', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const existing = list.pushBack(1);
|
||||||
|
|
||||||
|
const newNode = list.insertAfter(existing, 2);
|
||||||
|
|
||||||
|
expect(newNode.value).toBe(2);
|
||||||
|
expect(newNode.prev).toBe(existing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove', () => {
|
||||||
|
it('should remove head node', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
const head = list.head!;
|
||||||
|
|
||||||
|
const value = list.remove(head);
|
||||||
|
|
||||||
|
expect(value).toBe(1);
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
expect(list.peekFront()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove tail node', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
const tail = list.tail!;
|
||||||
|
|
||||||
|
const value = list.remove(tail);
|
||||||
|
|
||||||
|
expect(value).toBe(3);
|
||||||
|
expect(list.length).toBe(2);
|
||||||
|
expect(list.peekBack()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove middle node', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
const middle = list.head!.next!;
|
||||||
|
|
||||||
|
const value = list.remove(middle);
|
||||||
|
|
||||||
|
expect(value).toBe(2);
|
||||||
|
expect(list.toArray()).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove single element', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const node = list.pushBack(1);
|
||||||
|
|
||||||
|
list.remove(node);
|
||||||
|
|
||||||
|
expect(list.isEmpty).toBe(true);
|
||||||
|
expect(list.head).toBeUndefined();
|
||||||
|
expect(list.tail).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detach the removed node', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
const middle = list.head!.next!;
|
||||||
|
|
||||||
|
list.remove(middle);
|
||||||
|
|
||||||
|
expect(middle.prev).toBeUndefined();
|
||||||
|
expect(middle.next).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('should remove all elements', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
const result = list.clear();
|
||||||
|
|
||||||
|
expect(list.length).toBe(0);
|
||||||
|
expect(list.isEmpty).toBe(true);
|
||||||
|
expect(list.head).toBeUndefined();
|
||||||
|
expect(list.tail).toBeUndefined();
|
||||||
|
expect(result).toBe(list);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toArray', () => {
|
||||||
|
it('should return empty array for empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
expect(list.toArray()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return values from head to tail', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('should return comma-separated values', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(list.toString()).toBe('1,2,3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('iterator', () => {
|
||||||
|
it('should iterate from head to tail', () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
|
||||||
|
expect([...list]).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should yield nothing for empty list', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
expect([...list]).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('async iterator', () => {
|
||||||
|
it('should async iterate from head to tail', async () => {
|
||||||
|
const list = new LinkedList([1, 2, 3]);
|
||||||
|
const result: number[] = [];
|
||||||
|
|
||||||
|
for await (const value of list)
|
||||||
|
result.push(value);
|
||||||
|
|
||||||
|
expect(result).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('node linking', () => {
|
||||||
|
it('should maintain correct prev/next references', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const a = list.pushBack(1);
|
||||||
|
const b = list.pushBack(2);
|
||||||
|
const c = list.pushBack(3);
|
||||||
|
|
||||||
|
expect(a.next).toBe(b);
|
||||||
|
expect(b.prev).toBe(a);
|
||||||
|
expect(b.next).toBe(c);
|
||||||
|
expect(c.prev).toBe(b);
|
||||||
|
expect(a.prev).toBeUndefined();
|
||||||
|
expect(c.next).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update links after removal', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const a = list.pushBack(1);
|
||||||
|
const b = list.pushBack(2);
|
||||||
|
const c = list.pushBack(3);
|
||||||
|
|
||||||
|
list.remove(b);
|
||||||
|
|
||||||
|
expect(a.next).toBe(c);
|
||||||
|
expect(c.prev).toBe(a);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interleaved operations', () => {
|
||||||
|
it('should handle mixed push/pop from both ends', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
|
||||||
|
list.pushBack(1);
|
||||||
|
list.pushBack(2);
|
||||||
|
list.pushFront(0);
|
||||||
|
|
||||||
|
expect(list.popFront()).toBe(0);
|
||||||
|
expect(list.popBack()).toBe(2);
|
||||||
|
expect(list.popFront()).toBe(1);
|
||||||
|
expect(list.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle insert and remove by node reference', () => {
|
||||||
|
const list = new LinkedList<number>();
|
||||||
|
const a = list.pushBack(1);
|
||||||
|
const c = list.pushBack(3);
|
||||||
|
const b = list.insertAfter(a, 2);
|
||||||
|
const d = list.insertBefore(c, 2.5);
|
||||||
|
|
||||||
|
expect(list.toArray()).toEqual([1, 2, 2.5, 3]);
|
||||||
|
|
||||||
|
list.remove(b);
|
||||||
|
list.remove(d);
|
||||||
|
|
||||||
|
expect(list.toArray()).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
324
core/stdlib/src/structs/LinkedList/index.ts
Normal file
324
core/stdlib/src/structs/LinkedList/index.ts
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import { isArray } from '../../types';
|
||||||
|
import type { LinkedListLike, LinkedListNode } from './types';
|
||||||
|
|
||||||
|
export type { LinkedListLike, LinkedListNode } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new doubly linked list node
|
||||||
|
*
|
||||||
|
* @template T The type of the value
|
||||||
|
* @param {T} value The value to store
|
||||||
|
* @returns {LinkedListNode<T>} The created node
|
||||||
|
*/
|
||||||
|
function createNode<T>(value: T): LinkedListNode<T> {
|
||||||
|
return { value, prev: undefined, next: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name LinkedList
|
||||||
|
* @category Data Structures
|
||||||
|
* @description Doubly linked list with O(1) push/pop on both ends and O(1) insert/remove by node reference
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template T The type of elements stored in the list
|
||||||
|
*/
|
||||||
|
export class LinkedList<T> implements LinkedListLike<T> {
|
||||||
|
/**
|
||||||
|
* The number of elements in the list
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
private count = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first node in the list
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {LinkedListNode<T> | undefined}
|
||||||
|
*/
|
||||||
|
private first: LinkedListNode<T> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last node in the list
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {LinkedListNode<T> | undefined}
|
||||||
|
*/
|
||||||
|
private last: LinkedListNode<T> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of LinkedList
|
||||||
|
*
|
||||||
|
* @param {(T[] | T)} [initialValues] The initial values to add to the list
|
||||||
|
*/
|
||||||
|
constructor(initialValues?: T[] | T) {
|
||||||
|
if (initialValues !== null && initialValues !== undefined) {
|
||||||
|
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||||
|
|
||||||
|
for (const item of items)
|
||||||
|
this.pushBack(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of elements in the list
|
||||||
|
* @returns {number} The number of elements in the list
|
||||||
|
*/
|
||||||
|
public get length(): number {
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the list is empty
|
||||||
|
* @returns {boolean} `true` if the list is empty, `false` otherwise
|
||||||
|
*/
|
||||||
|
public get isEmpty(): boolean {
|
||||||
|
return this.count === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the first node
|
||||||
|
* @returns {LinkedListNode<T> | undefined} The first node, or `undefined` if the list is empty
|
||||||
|
*/
|
||||||
|
public get head(): LinkedListNode<T> | undefined {
|
||||||
|
return this.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the last node
|
||||||
|
* @returns {LinkedListNode<T> | undefined} The last node, or `undefined` if the list is empty
|
||||||
|
*/
|
||||||
|
public get tail(): LinkedListNode<T> | undefined {
|
||||||
|
return this.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a value to the end of the list
|
||||||
|
* @param {T} value The value to append
|
||||||
|
* @returns {LinkedListNode<T>} The created node
|
||||||
|
*/
|
||||||
|
public pushBack(value: T): LinkedListNode<T> {
|
||||||
|
const node = createNode(value);
|
||||||
|
|
||||||
|
if (this.last) {
|
||||||
|
node.prev = this.last;
|
||||||
|
this.last.next = node;
|
||||||
|
this.last = node;
|
||||||
|
} else {
|
||||||
|
this.first = node;
|
||||||
|
this.last = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.count++;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepends a value to the beginning of the list
|
||||||
|
* @param {T} value The value to prepend
|
||||||
|
* @returns {LinkedListNode<T>} The created node
|
||||||
|
*/
|
||||||
|
public pushFront(value: T): LinkedListNode<T> {
|
||||||
|
const node = createNode(value);
|
||||||
|
|
||||||
|
if (this.first) {
|
||||||
|
node.next = this.first;
|
||||||
|
this.first.prev = node;
|
||||||
|
this.first = node;
|
||||||
|
} else {
|
||||||
|
this.first = node;
|
||||||
|
this.last = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.count++;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the last value
|
||||||
|
* @returns {T | undefined} The last value, or `undefined` if the list is empty
|
||||||
|
*/
|
||||||
|
public popBack(): T | undefined {
|
||||||
|
if (!this.last) return undefined;
|
||||||
|
|
||||||
|
const node = this.last;
|
||||||
|
|
||||||
|
this.detach(node);
|
||||||
|
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the first value
|
||||||
|
* @returns {T | undefined} The first value, or `undefined` if the list is empty
|
||||||
|
*/
|
||||||
|
public popFront(): T | undefined {
|
||||||
|
if (!this.first) return undefined;
|
||||||
|
|
||||||
|
const node = this.first;
|
||||||
|
|
||||||
|
this.detach(node);
|
||||||
|
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last value without removing it
|
||||||
|
* @returns {T | undefined} The last value, or `undefined` if the list is empty
|
||||||
|
*/
|
||||||
|
public peekBack(): T | undefined {
|
||||||
|
return this.last?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first value without removing it
|
||||||
|
* @returns {T | undefined} The first value, or `undefined` if the list is empty
|
||||||
|
*/
|
||||||
|
public peekFront(): T | undefined {
|
||||||
|
return this.first?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a value before the given node
|
||||||
|
* @param {LinkedListNode<T>} node The reference node
|
||||||
|
* @param {T} value The value to insert
|
||||||
|
* @returns {LinkedListNode<T>} The created node
|
||||||
|
*/
|
||||||
|
public insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||||
|
const newNode = createNode(value);
|
||||||
|
|
||||||
|
newNode.next = node;
|
||||||
|
newNode.prev = node.prev;
|
||||||
|
|
||||||
|
if (node.prev) {
|
||||||
|
node.prev.next = newNode;
|
||||||
|
} else {
|
||||||
|
this.first = newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.prev = newNode;
|
||||||
|
this.count++;
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a value after the given node
|
||||||
|
* @param {LinkedListNode<T>} node The reference node
|
||||||
|
* @param {T} value The value to insert
|
||||||
|
* @returns {LinkedListNode<T>} The created node
|
||||||
|
*/
|
||||||
|
public insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||||
|
const newNode = createNode(value);
|
||||||
|
|
||||||
|
newNode.prev = node;
|
||||||
|
newNode.next = node.next;
|
||||||
|
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = newNode;
|
||||||
|
} else {
|
||||||
|
this.last = newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.next = newNode;
|
||||||
|
this.count++;
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a node from the list by reference in O(1)
|
||||||
|
* @param {LinkedListNode<T>} node The node to remove
|
||||||
|
* @returns {T} The value of the removed node
|
||||||
|
*/
|
||||||
|
public remove(node: LinkedListNode<T>): T {
|
||||||
|
this.detach(node);
|
||||||
|
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all elements from the list
|
||||||
|
* @returns {this} The list instance for chaining
|
||||||
|
*/
|
||||||
|
public clear(): this {
|
||||||
|
this.first = undefined;
|
||||||
|
this.last = undefined;
|
||||||
|
this.count = 0;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a shallow copy of the list values as an array
|
||||||
|
* @returns {T[]} Array of values from head to tail
|
||||||
|
*/
|
||||||
|
public toArray(): T[] {
|
||||||
|
const result = Array.from<T>({ length: this.count });
|
||||||
|
let current = this.first;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
result[i++] = current.value;
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of the list
|
||||||
|
* @returns {string} String representation
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
return this.toArray().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator over list values from head to tail
|
||||||
|
*/
|
||||||
|
public *[Symbol.iterator](): Iterator<T> {
|
||||||
|
let current = this.first;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
yield current.value;
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async iterator over list values from head to tail
|
||||||
|
*/
|
||||||
|
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||||
|
for (const value of this)
|
||||||
|
yield value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detaches a node from the list, updating head/tail and count
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {LinkedListNode<T>} node The node to detach
|
||||||
|
*/
|
||||||
|
private detach(node: LinkedListNode<T>): void {
|
||||||
|
if (node.prev) {
|
||||||
|
node.prev.next = node.next;
|
||||||
|
} else {
|
||||||
|
this.first = node.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.next) {
|
||||||
|
node.next.prev = node.prev;
|
||||||
|
} else {
|
||||||
|
this.last = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.prev = undefined;
|
||||||
|
node.next = undefined;
|
||||||
|
this.count--;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
core/stdlib/src/structs/LinkedList/types.ts
Normal file
28
core/stdlib/src/structs/LinkedList/types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export interface LinkedListNode<T> {
|
||||||
|
value: T;
|
||||||
|
prev: LinkedListNode<T> | undefined;
|
||||||
|
next: LinkedListNode<T> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkedListLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||||
|
readonly length: number;
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
|
||||||
|
readonly head: LinkedListNode<T> | undefined;
|
||||||
|
readonly tail: LinkedListNode<T> | undefined;
|
||||||
|
|
||||||
|
pushBack(value: T): LinkedListNode<T>;
|
||||||
|
pushFront(value: T): LinkedListNode<T>;
|
||||||
|
popBack(): T | undefined;
|
||||||
|
popFront(): T | undefined;
|
||||||
|
peekBack(): T | undefined;
|
||||||
|
peekFront(): T | undefined;
|
||||||
|
|
||||||
|
insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||||
|
insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||||
|
remove(node: LinkedListNode<T>): T;
|
||||||
|
|
||||||
|
clear(): this;
|
||||||
|
toArray(): T[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
213
core/stdlib/src/structs/PriorityQueue/index.test.ts
Normal file
213
core/stdlib/src/structs/PriorityQueue/index.test.ts
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { PriorityQueue } from '.';
|
||||||
|
|
||||||
|
describe('PriorityQueue', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create an empty queue', () => {
|
||||||
|
const pq = new PriorityQueue<number>();
|
||||||
|
|
||||||
|
expect(pq.length).toBe(0);
|
||||||
|
expect(pq.isEmpty).toBe(true);
|
||||||
|
expect(pq.isFull).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a queue from single value', () => {
|
||||||
|
const pq = new PriorityQueue(42);
|
||||||
|
|
||||||
|
expect(pq.length).toBe(1);
|
||||||
|
expect(pq.peek()).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a queue from array', () => {
|
||||||
|
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||||
|
|
||||||
|
expect(pq.length).toBe(5);
|
||||||
|
expect(pq.peek()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if initial values exceed maxSize', () => {
|
||||||
|
expect(() => new PriorityQueue([1, 2, 3], { maxSize: 2 }))
|
||||||
|
.toThrow('Initial values exceed maxSize');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enqueue', () => {
|
||||||
|
it('should enqueue elements by priority', () => {
|
||||||
|
const pq = new PriorityQueue<number>();
|
||||||
|
|
||||||
|
pq.enqueue(5);
|
||||||
|
pq.enqueue(1);
|
||||||
|
pq.enqueue(3);
|
||||||
|
|
||||||
|
expect(pq.peek()).toBe(1);
|
||||||
|
expect(pq.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when queue is full', () => {
|
||||||
|
const pq = new PriorityQueue<number>(undefined, { maxSize: 2 });
|
||||||
|
|
||||||
|
pq.enqueue(1);
|
||||||
|
pq.enqueue(2);
|
||||||
|
|
||||||
|
expect(() => pq.enqueue(3)).toThrow('PriorityQueue is full');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dequeue', () => {
|
||||||
|
it('should return undefined for empty queue', () => {
|
||||||
|
const pq = new PriorityQueue<number>();
|
||||||
|
|
||||||
|
expect(pq.dequeue()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dequeue elements in priority order (min-heap)', () => {
|
||||||
|
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||||
|
const result: number[] = [];
|
||||||
|
|
||||||
|
while (!pq.isEmpty) {
|
||||||
|
result.push(pq.dequeue()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result).toEqual([1, 3, 4, 5, 8]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dequeue elements in priority order (max-heap)', () => {
|
||||||
|
const pq = new PriorityQueue([5, 3, 8, 1, 4], {
|
||||||
|
comparator: (a, b) => b - a,
|
||||||
|
});
|
||||||
|
const result: number[] = [];
|
||||||
|
|
||||||
|
while (!pq.isEmpty) {
|
||||||
|
result.push(pq.dequeue()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(result).toEqual([8, 5, 4, 3, 1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peek', () => {
|
||||||
|
it('should return undefined for empty queue', () => {
|
||||||
|
const pq = new PriorityQueue<number>();
|
||||||
|
|
||||||
|
expect(pq.peek()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return highest-priority element without removing', () => {
|
||||||
|
const pq = new PriorityQueue([5, 1, 3]);
|
||||||
|
|
||||||
|
expect(pq.peek()).toBe(1);
|
||||||
|
expect(pq.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isFull', () => {
|
||||||
|
it('should be false when no maxSize', () => {
|
||||||
|
const pq = new PriorityQueue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(pq.isFull).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be true when at maxSize', () => {
|
||||||
|
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||||
|
|
||||||
|
expect(pq.isFull).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should become false after dequeue', () => {
|
||||||
|
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||||
|
|
||||||
|
pq.dequeue();
|
||||||
|
|
||||||
|
expect(pq.isFull).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('should remove all elements', () => {
|
||||||
|
const pq = new PriorityQueue([1, 2, 3]);
|
||||||
|
|
||||||
|
const result = pq.clear();
|
||||||
|
|
||||||
|
expect(pq.length).toBe(0);
|
||||||
|
expect(pq.isEmpty).toBe(true);
|
||||||
|
expect(result).toBe(pq);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toArray', () => {
|
||||||
|
it('should return empty array for empty queue', () => {
|
||||||
|
const pq = new PriorityQueue<number>();
|
||||||
|
|
||||||
|
expect(pq.toArray()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a shallow copy', () => {
|
||||||
|
const pq = new PriorityQueue([3, 1, 2]);
|
||||||
|
const arr = pq.toArray();
|
||||||
|
|
||||||
|
arr.push(99);
|
||||||
|
|
||||||
|
expect(pq.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('should return formatted string', () => {
|
||||||
|
const pq = new PriorityQueue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(pq.toString()).toBe('PriorityQueue(3)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('iterator', () => {
|
||||||
|
it('should iterate over elements', () => {
|
||||||
|
const pq = new PriorityQueue([5, 3, 1]);
|
||||||
|
const elements = [...pq];
|
||||||
|
|
||||||
|
expect(elements.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('custom comparator', () => {
|
||||||
|
it('should work with object priority', () => {
|
||||||
|
interface Job {
|
||||||
|
priority: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pq = new PriorityQueue<Job>(
|
||||||
|
[
|
||||||
|
{ priority: 3, name: 'low' },
|
||||||
|
{ priority: 1, name: 'critical' },
|
||||||
|
{ priority: 2, name: 'normal' },
|
||||||
|
],
|
||||||
|
{ comparator: (a, b) => a.priority - b.priority },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(pq.dequeue()?.name).toBe('critical');
|
||||||
|
expect(pq.dequeue()?.name).toBe('normal');
|
||||||
|
expect(pq.dequeue()?.name).toBe('low');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interleaved operations', () => {
|
||||||
|
it('should maintain priority with mixed enqueue and dequeue', () => {
|
||||||
|
const pq = new PriorityQueue<number>();
|
||||||
|
|
||||||
|
pq.enqueue(10);
|
||||||
|
pq.enqueue(5);
|
||||||
|
expect(pq.dequeue()).toBe(5);
|
||||||
|
|
||||||
|
pq.enqueue(3);
|
||||||
|
pq.enqueue(7);
|
||||||
|
expect(pq.dequeue()).toBe(3);
|
||||||
|
|
||||||
|
pq.enqueue(1);
|
||||||
|
expect(pq.dequeue()).toBe(1);
|
||||||
|
expect(pq.dequeue()).toBe(7);
|
||||||
|
expect(pq.dequeue()).toBe(10);
|
||||||
|
expect(pq.dequeue()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
144
core/stdlib/src/structs/PriorityQueue/index.ts
Normal file
144
core/stdlib/src/structs/PriorityQueue/index.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { BinaryHeap } from '../BinaryHeap';
|
||||||
|
import type { Comparator, PriorityQueueLike } from './types';
|
||||||
|
|
||||||
|
export type { PriorityQueueLike } from './types';
|
||||||
|
export type { Comparator } from './types';
|
||||||
|
|
||||||
|
export interface PriorityQueueOptions<T> {
|
||||||
|
comparator?: Comparator<T>;
|
||||||
|
maxSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name PriorityQueue
|
||||||
|
* @category Data Structures
|
||||||
|
* @description Priority queue backed by a binary heap with configurable comparator and optional max size
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template T The type of elements stored in the queue
|
||||||
|
*/
|
||||||
|
export class PriorityQueue<T> implements PriorityQueueLike<T> {
|
||||||
|
/**
|
||||||
|
* The maximum number of elements the queue can hold
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
private readonly maxSize: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal binary heap backing the queue
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {BinaryHeap<T>}
|
||||||
|
*/
|
||||||
|
private readonly heap: BinaryHeap<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of PriorityQueue
|
||||||
|
*
|
||||||
|
* @param {(T[] | T)} [initialValues] The initial values to add to the queue
|
||||||
|
* @param {PriorityQueueOptions<T>} [options] Queue configuration
|
||||||
|
*/
|
||||||
|
constructor(initialValues?: T[] | T, options?: PriorityQueueOptions<T>) {
|
||||||
|
this.maxSize = options?.maxSize ?? Infinity;
|
||||||
|
this.heap = new BinaryHeap(initialValues, { comparator: options?.comparator });
|
||||||
|
|
||||||
|
if (this.heap.length > this.maxSize) {
|
||||||
|
throw new RangeError('Initial values exceed maxSize');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of elements in the queue
|
||||||
|
* @returns {number} The number of elements in the queue
|
||||||
|
*/
|
||||||
|
public get length(): number {
|
||||||
|
return this.heap.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the queue is empty
|
||||||
|
* @returns {boolean} `true` if the queue is empty, `false` otherwise
|
||||||
|
*/
|
||||||
|
public get isEmpty(): boolean {
|
||||||
|
return this.heap.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the queue is full
|
||||||
|
* @returns {boolean} `true` if the queue has reached maxSize, `false` otherwise
|
||||||
|
*/
|
||||||
|
public get isFull(): boolean {
|
||||||
|
return this.heap.length >= this.maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues an element by priority
|
||||||
|
* @param {T} element The element to enqueue
|
||||||
|
* @throws {RangeError} If the queue is full
|
||||||
|
*/
|
||||||
|
public enqueue(element: T): void {
|
||||||
|
if (this.isFull)
|
||||||
|
throw new RangeError('PriorityQueue is full');
|
||||||
|
|
||||||
|
this.heap.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dequeues the highest-priority element
|
||||||
|
* @returns {T | undefined} The highest-priority element, or `undefined` if empty
|
||||||
|
*/
|
||||||
|
public dequeue(): T | undefined {
|
||||||
|
return this.heap.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the highest-priority element without removing it
|
||||||
|
* @returns {T | undefined} The highest-priority element, or `undefined` if empty
|
||||||
|
*/
|
||||||
|
public peek(): T | undefined {
|
||||||
|
return this.heap.peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all elements from the queue
|
||||||
|
* @returns {this} The queue instance for chaining
|
||||||
|
*/
|
||||||
|
public clear(): this {
|
||||||
|
this.heap.clear();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a shallow copy of elements in heap order
|
||||||
|
* @returns {T[]} Array of elements
|
||||||
|
*/
|
||||||
|
public toArray(): T[] {
|
||||||
|
return this.heap.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of the queue
|
||||||
|
* @returns {string} String representation
|
||||||
|
*/
|
||||||
|
public toString(): string {
|
||||||
|
return `PriorityQueue(${this.heap.length})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator over queue elements in heap order
|
||||||
|
*/
|
||||||
|
public *[Symbol.iterator](): Iterator<T> {
|
||||||
|
yield* this.heap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async iterator over queue elements in heap order
|
||||||
|
*/
|
||||||
|
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||||
|
for (const element of this.heap)
|
||||||
|
yield element;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
core/stdlib/src/structs/PriorityQueue/types.ts
Normal file
16
core/stdlib/src/structs/PriorityQueue/types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Comparator } from '../BinaryHeap';
|
||||||
|
|
||||||
|
export interface PriorityQueueLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||||
|
readonly length: number;
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
enqueue(element: T): void;
|
||||||
|
dequeue(): T | undefined;
|
||||||
|
peek(): T | undefined;
|
||||||
|
clear(): this;
|
||||||
|
toArray(): T[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Comparator };
|
||||||
207
core/stdlib/src/structs/Queue/index.test.ts
Normal file
207
core/stdlib/src/structs/Queue/index.test.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Queue } from '.';
|
||||||
|
|
||||||
|
describe('queue', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('create an empty queue if no initial values are provided', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
|
||||||
|
expect(queue.length).toBe(0);
|
||||||
|
expect(queue.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a queue with the provided initial values', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(queue.length).toBe(3);
|
||||||
|
expect(queue.peek()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a queue with a single initial value', () => {
|
||||||
|
const queue = new Queue(42);
|
||||||
|
|
||||||
|
expect(queue.length).toBe(1);
|
||||||
|
expect(queue.peek()).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a queue with the provided options', () => {
|
||||||
|
const queue = new Queue<number>(undefined, { maxSize: 5 });
|
||||||
|
|
||||||
|
expect(queue.length).toBe(0);
|
||||||
|
expect(queue.isFull).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('enqueue', () => {
|
||||||
|
it('add an element to the back of the queue', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
queue.enqueue(1);
|
||||||
|
|
||||||
|
expect(queue.length).toBe(1);
|
||||||
|
expect(queue.peek()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintain FIFO order', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
queue.enqueue(1).enqueue(2).enqueue(3);
|
||||||
|
|
||||||
|
expect(queue.peek()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throw an error if the queue is full', () => {
|
||||||
|
const queue = new Queue<number>(undefined, { maxSize: 1 });
|
||||||
|
queue.enqueue(1);
|
||||||
|
|
||||||
|
expect(() => queue.enqueue(2)).toThrow(new RangeError('Queue is full'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return this for chaining', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
const result = queue.enqueue(1);
|
||||||
|
|
||||||
|
expect(result).toBe(queue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dequeue', () => {
|
||||||
|
it('remove and return the front element', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
const element = queue.dequeue();
|
||||||
|
|
||||||
|
expect(element).toBe(1);
|
||||||
|
expect(queue.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined if the queue is empty', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
|
||||||
|
expect(queue.dequeue()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintain FIFO order across multiple dequeues', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(queue.dequeue()).toBe(1);
|
||||||
|
expect(queue.dequeue()).toBe(2);
|
||||||
|
expect(queue.dequeue()).toBe(3);
|
||||||
|
expect(queue.dequeue()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('compact internal storage after many dequeues', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++)
|
||||||
|
queue.enqueue(i);
|
||||||
|
|
||||||
|
for (let i = 0; i < 80; i++)
|
||||||
|
queue.dequeue();
|
||||||
|
|
||||||
|
expect(queue.length).toBe(20);
|
||||||
|
expect(queue.peek()).toBe(80);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('peek', () => {
|
||||||
|
it('return the front element without removing it', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(queue.peek()).toBe(1);
|
||||||
|
expect(queue.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return undefined if the queue is empty', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
|
||||||
|
expect(queue.peek()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('clear the queue', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
queue.clear();
|
||||||
|
|
||||||
|
expect(queue.length).toBe(0);
|
||||||
|
expect(queue.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return this for chaining', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(queue.clear()).toBe(queue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toArray', () => {
|
||||||
|
it('return elements in FIFO order', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(queue.toArray()).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return correct array after dequeues', () => {
|
||||||
|
const queue = new Queue([1, 2, 3, 4, 5]);
|
||||||
|
queue.dequeue();
|
||||||
|
queue.dequeue();
|
||||||
|
|
||||||
|
expect(queue.toArray()).toEqual([3, 4, 5]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toString', () => {
|
||||||
|
it('return comma-separated string', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect(queue.toString()).toBe('1,2,3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('iteration', () => {
|
||||||
|
it('iterate over the queue in FIFO order', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
|
||||||
|
expect([...queue]).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iterate correctly after dequeues', () => {
|
||||||
|
const queue = new Queue([1, 2, 3, 4]);
|
||||||
|
queue.dequeue();
|
||||||
|
|
||||||
|
expect([...queue]).toEqual([2, 3, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iterate over the queue asynchronously in FIFO order', async () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
const elements: number[] = [];
|
||||||
|
|
||||||
|
for await (const element of queue)
|
||||||
|
elements.push(element);
|
||||||
|
|
||||||
|
expect(elements).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mixed operations', () => {
|
||||||
|
it('interleave enqueue and dequeue', () => {
|
||||||
|
const queue = new Queue<number>();
|
||||||
|
|
||||||
|
queue.enqueue(1);
|
||||||
|
queue.enqueue(2);
|
||||||
|
expect(queue.dequeue()).toBe(1);
|
||||||
|
|
||||||
|
queue.enqueue(3);
|
||||||
|
expect(queue.dequeue()).toBe(2);
|
||||||
|
expect(queue.dequeue()).toBe(3);
|
||||||
|
expect(queue.isEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuse queue after clear', () => {
|
||||||
|
const queue = new Queue([1, 2, 3]);
|
||||||
|
queue.clear();
|
||||||
|
queue.enqueue(4);
|
||||||
|
|
||||||
|
expect(queue.length).toBe(1);
|
||||||
|
expect(queue.peek()).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
140
core/stdlib/src/structs/Queue/index.ts
Normal file
140
core/stdlib/src/structs/Queue/index.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { Deque } from '../Deque';
|
||||||
|
import type { QueueLike } from './types';
|
||||||
|
|
||||||
|
export type { QueueLike } from './types';
|
||||||
|
|
||||||
|
export interface QueueOptions {
|
||||||
|
maxSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name Queue
|
||||||
|
* @category Data Structures
|
||||||
|
* @description Represents a queue data structure (FIFO) backed by a Deque
|
||||||
|
*
|
||||||
|
* @since 0.0.8
|
||||||
|
*
|
||||||
|
* @template T The type of elements stored in the queue
|
||||||
|
*/
|
||||||
|
export class Queue<T> implements QueueLike<T> {
|
||||||
|
/**
|
||||||
|
* The underlying deque
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {Deque<T>}
|
||||||
|
*/
|
||||||
|
private readonly deque: Deque<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of Queue
|
||||||
|
*
|
||||||
|
* @param {(T[] | T)} [initialValues] The initial values to add to the queue
|
||||||
|
* @param {QueueOptions} [options] The options for the queue
|
||||||
|
*/
|
||||||
|
constructor(initialValues?: T[] | T, options?: QueueOptions) {
|
||||||
|
this.deque = new Deque(initialValues, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of elements in the queue
|
||||||
|
* @returns {number} The number of elements in the queue
|
||||||
|
*/
|
||||||
|
get length() {
|
||||||
|
return this.deque.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the queue is empty
|
||||||
|
* @returns {boolean} `true` if the queue is empty, `false` otherwise
|
||||||
|
*/
|
||||||
|
get isEmpty() {
|
||||||
|
return this.deque.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the queue is full
|
||||||
|
* @returns {boolean} `true` if the queue is full, `false` otherwise
|
||||||
|
*/
|
||||||
|
get isFull() {
|
||||||
|
return this.deque.isFull;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an element to the back of the queue
|
||||||
|
* @param {T} element The element to enqueue
|
||||||
|
* @returns {this}
|
||||||
|
* @throws {RangeError} If the queue is full
|
||||||
|
*/
|
||||||
|
enqueue(element: T) {
|
||||||
|
if (this.deque.isFull)
|
||||||
|
throw new RangeError('Queue is full');
|
||||||
|
|
||||||
|
this.deque.pushBack(element);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes and returns the front element of the queue
|
||||||
|
* @returns {T | undefined} The front element, or undefined if the queue is empty
|
||||||
|
*/
|
||||||
|
dequeue() {
|
||||||
|
return this.deque.popFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the front element without removing it
|
||||||
|
* @returns {T | undefined} The front element, or undefined if the queue is empty
|
||||||
|
*/
|
||||||
|
peek() {
|
||||||
|
return this.deque.peekFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the queue
|
||||||
|
*
|
||||||
|
* @returns {this}
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.deque.clear();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the queue to an array in FIFO order
|
||||||
|
*
|
||||||
|
* @returns {T[]}
|
||||||
|
*/
|
||||||
|
toArray() {
|
||||||
|
return this.deque.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representation of the queue
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
return this.deque.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator for the queue
|
||||||
|
*
|
||||||
|
* @returns {IterableIterator<T>}
|
||||||
|
*/
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.deque[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an async iterator for the queue
|
||||||
|
*
|
||||||
|
* @returns {AsyncIterableIterator<T>}
|
||||||
|
*/
|
||||||
|
async *[Symbol.asyncIterator]() {
|
||||||
|
for (const element of this.deque)
|
||||||
|
yield element;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
core/stdlib/src/structs/Queue/types.ts
Normal file
12
core/stdlib/src/structs/Queue/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface QueueLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||||
|
readonly length: number;
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
enqueue(element: T): this;
|
||||||
|
dequeue(): T | undefined;
|
||||||
|
peek(): T | undefined;
|
||||||
|
clear(): this;
|
||||||
|
toArray(): T[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { last } from '../../arrays';
|
import { last } from '../../arrays';
|
||||||
import { isArray } from '../../types';
|
import { isArray } from '../../types';
|
||||||
|
import type { StackLike } from './types';
|
||||||
|
|
||||||
export type StackOptions = {
|
export type { StackLike } from './types';
|
||||||
|
|
||||||
|
export interface StackOptions {
|
||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name Stack
|
* @name Stack
|
||||||
@@ -14,7 +17,7 @@ export type StackOptions = {
|
|||||||
*
|
*
|
||||||
* @template T The type of elements stored in the stack
|
* @template T The type of elements stored in the stack
|
||||||
*/
|
*/
|
||||||
export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
export class Stack<T> implements StackLike<T> {
|
||||||
/**
|
/**
|
||||||
* The maximum number of elements that the stack can hold
|
* The maximum number of elements that the stack can hold
|
||||||
*
|
*
|
||||||
12
core/stdlib/src/structs/Stack/types.ts
Normal file
12
core/stdlib/src/structs/Stack/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface StackLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||||
|
readonly length: number;
|
||||||
|
readonly isEmpty: boolean;
|
||||||
|
readonly isFull: boolean;
|
||||||
|
|
||||||
|
push(element: T): this;
|
||||||
|
pop(): T | undefined;
|
||||||
|
peek(): T | undefined;
|
||||||
|
clear(): this;
|
||||||
|
toArray(): T[];
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
@@ -1 +1,7 @@
|
|||||||
export * from './stack';
|
export * from './BinaryHeap';
|
||||||
|
export * from './CircularBuffer';
|
||||||
|
export * from './Deque';
|
||||||
|
export * from './LinkedList';
|
||||||
|
export * from './PriorityQueue';
|
||||||
|
export * from './Queue';
|
||||||
|
export * from './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>();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { defineConfig } from 'tsdown';
|
import { defineConfig } from 'tsdown';
|
||||||
|
import { sharedConfig } from '@robonen/tsdown';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
entry: ['src/index.ts'],
|
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)
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.29.3",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.18.0"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"default.json"
|
"default.json"
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.29.3",
|
"packageManager": "pnpm@10.29.3",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.18.0"
|
"node": ">=24.13.1"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.19.11",
|
"@types/node": "^24.10.13",
|
||||||
"@vitest/coverage-v8": "catalog:",
|
"@vitest/coverage-v8": "catalog:",
|
||||||
"@vitest/ui": "catalog:",
|
"@vitest/ui": "catalog:",
|
||||||
"citty": "^0.2.1",
|
"citty": "^0.2.1",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
},
|
},
|
||||||
"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"
|
||||||
|
|||||||
497
pnpm-lock.yaml
generated
497
pnpm-lock.yaml
generated
@@ -16,8 +16,11 @@ catalogs:
|
|||||||
specifier: ^2.4.6
|
specifier: ^2.4.6
|
||||||
version: 2.4.6
|
version: 2.4.6
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^28.0.0
|
specifier: ^29.0.1
|
||||||
version: 28.0.0
|
version: 29.0.1
|
||||||
|
oxlint:
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.47.0
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: ^0.12.5
|
specifier: ^0.12.5
|
||||||
version: 0.12.9
|
version: 0.12.9
|
||||||
@@ -33,8 +36,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.19.11
|
specifier: ^24.10.13
|
||||||
version: 22.19.11
|
version: 24.10.13
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.0.18(vitest@4.0.18)
|
version: 4.0.18(vitest@4.0.18)
|
||||||
@@ -49,30 +52,75 @@ importers:
|
|||||||
version: 2.6.1
|
version: 2.6.1
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 28.0.0
|
version: 29.0.1
|
||||||
scule:
|
scule:
|
||||||
specifier: ^1.3.0
|
specifier: ^1.3.0
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.11)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2)
|
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2)
|
||||||
|
|
||||||
|
configs/oxlint:
|
||||||
|
devDependencies:
|
||||||
|
'@robonen/oxlint':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: 'link:'
|
||||||
|
'@robonen/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../tsconfig
|
||||||
|
'@robonen/tsdown':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../tsdown
|
||||||
|
oxlint:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 1.47.0
|
||||||
|
tsdown:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 0.12.9(typescript@5.8.3)
|
||||||
|
|
||||||
configs/tsconfig: {}
|
configs/tsconfig: {}
|
||||||
|
|
||||||
core/platform:
|
configs/tsdown:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@robonen/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../tsconfig
|
||||||
|
tsdown:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 0.12.9(typescript@5.8.3)
|
||||||
|
|
||||||
|
core/platform:
|
||||||
|
devDependencies:
|
||||||
|
'@robonen/oxlint':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../configs/oxlint
|
||||||
'@robonen/tsconfig':
|
'@robonen/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../configs/tsconfig
|
version: link:../../configs/tsconfig
|
||||||
|
'@robonen/tsdown':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../configs/tsdown
|
||||||
|
oxlint:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 1.47.0
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.12.9(typescript@5.8.3)
|
version: 0.12.9(typescript@5.8.3)
|
||||||
|
|
||||||
core/stdlib:
|
core/stdlib:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@robonen/oxlint':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../configs/oxlint
|
||||||
'@robonen/tsconfig':
|
'@robonen/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../configs/tsconfig
|
version: link:../../configs/tsconfig
|
||||||
|
'@robonen/tsdown':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../configs/tsdown
|
||||||
|
oxlint:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 1.47.0
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.12.9(typescript@5.8.3)
|
version: 0.12.9(typescript@5.8.3)
|
||||||
@@ -95,29 +143,37 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 3.5.28(typescript@5.8.3)
|
version: 3.5.28(typescript@5.8.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@robonen/oxlint':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../configs/oxlint
|
||||||
'@robonen/tsconfig':
|
'@robonen/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../configs/tsconfig
|
version: link:../../configs/tsconfig
|
||||||
|
'@robonen/tsdown':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../configs/tsdown
|
||||||
'@vue/test-utils':
|
'@vue/test-utils':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 2.4.6
|
version: 2.4.6
|
||||||
|
oxlint:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 1.47.0
|
||||||
tsdown:
|
tsdown:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.12.9(typescript@5.8.3)
|
version: 0.12.9(typescript@5.8.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@acemir/cssom@0.9.31':
|
|
||||||
resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
|
|
||||||
|
|
||||||
'@arcanis/slice-ansi@1.1.1':
|
'@arcanis/slice-ansi@1.1.1':
|
||||||
resolution: {integrity: sha512-xguP2WR2Dv0gQ7Ykbdb7BNCnPnIPB94uTi0Z2NvkRBEnhbwjOQ7QyQKJXrVQg4qDpiD9hA5l5cCwy/z2OXgc3w==}
|
resolution: {integrity: sha512-xguP2WR2Dv0gQ7Ykbdb7BNCnPnIPB94uTi0Z2NvkRBEnhbwjOQ7QyQKJXrVQg4qDpiD9hA5l5cCwy/z2OXgc3w==}
|
||||||
|
|
||||||
'@asamuzakjp/css-color@4.1.2':
|
'@asamuzakjp/css-color@5.0.1':
|
||||||
resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
|
resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
'@asamuzakjp/dom-selector@6.7.8':
|
'@asamuzakjp/dom-selector@7.0.3':
|
||||||
resolution: {integrity: sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==}
|
resolution: {integrity: sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==}
|
||||||
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
'@asamuzakjp/nwsapi@2.3.9':
|
'@asamuzakjp/nwsapi@2.3.9':
|
||||||
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
||||||
@@ -377,6 +433,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@bramus/specificity@2.4.2':
|
||||||
|
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@breejs/later@4.2.0':
|
'@breejs/later@4.2.0':
|
||||||
resolution: {integrity: sha512-EVMD0SgJtOuFeg0lAVbCwa+qeTKILb87jqvLyUtQswGD9+ce2nB52Y5zbTF1Hc0MDFfbydcMcxb47jSdhikVHA==}
|
resolution: {integrity: sha512-EVMD0SgJtOuFeg0lAVbCwa+qeTKILb87jqvLyUtQswGD9+ce2nB52Y5zbTF1Hc0MDFfbydcMcxb47jSdhikVHA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -384,8 +444,8 @@ packages:
|
|||||||
'@cdktf/hcl2json@0.21.0':
|
'@cdktf/hcl2json@0.21.0':
|
||||||
resolution: {integrity: sha512-cwX3i/mSJI/cRrtqwEPRfawB7pXgNioriSlkvou8LWiCrrcDe9ZtTbAbu8W1tEJQpe1pnX9VEgpzf/BbM7xF8Q==}
|
resolution: {integrity: sha512-cwX3i/mSJI/cRrtqwEPRfawB7pXgNioriSlkvou8LWiCrrcDe9ZtTbAbu8W1tEJQpe1pnX9VEgpzf/BbM7xF8Q==}
|
||||||
|
|
||||||
'@csstools/color-helpers@6.0.1':
|
'@csstools/color-helpers@6.0.2':
|
||||||
resolution: {integrity: sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==}
|
resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==}
|
||||||
engines: {node: '>=20.19.0'}
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
'@csstools/css-calc@3.1.1':
|
'@csstools/css-calc@3.1.1':
|
||||||
@@ -395,8 +455,8 @@ packages:
|
|||||||
'@csstools/css-parser-algorithms': ^4.0.0
|
'@csstools/css-parser-algorithms': ^4.0.0
|
||||||
'@csstools/css-tokenizer': ^4.0.0
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
'@csstools/css-color-parser@4.0.1':
|
'@csstools/css-color-parser@4.0.2':
|
||||||
resolution: {integrity: sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==}
|
resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==}
|
||||||
engines: {node: '>=20.19.0'}
|
engines: {node: '>=20.19.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@csstools/css-parser-algorithms': ^4.0.0
|
'@csstools/css-parser-algorithms': ^4.0.0
|
||||||
@@ -408,8 +468,13 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@csstools/css-tokenizer': ^4.0.0
|
'@csstools/css-tokenizer': ^4.0.0
|
||||||
|
|
||||||
'@csstools/css-syntax-patches-for-csstree@1.0.27':
|
'@csstools/css-syntax-patches-for-csstree@1.1.1':
|
||||||
resolution: {integrity: sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==}
|
resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==}
|
||||||
|
peerDependencies:
|
||||||
|
css-tree: ^3.2.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
css-tree:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@csstools/css-tokenizer@4.0.0':
|
'@csstools/css-tokenizer@4.0.0':
|
||||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||||
@@ -580,8 +645,8 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@exodus/bytes@1.14.1':
|
'@exodus/bytes@1.15.0':
|
||||||
resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==}
|
resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==}
|
||||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@noble/hashes': ^1.8.0 || ^2.0.0
|
'@noble/hashes': ^1.8.0 || ^2.0.0
|
||||||
@@ -793,6 +858,128 @@ packages:
|
|||||||
'@oxc-project/types@0.113.0':
|
'@oxc-project/types@0.113.0':
|
||||||
resolution: {integrity: sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==}
|
resolution: {integrity: sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==}
|
||||||
|
|
||||||
|
'@oxlint/binding-android-arm-eabi@1.47.0':
|
||||||
|
resolution: {integrity: sha512-UHqo3te9K/fh29brCuQdHjN+kfpIi9cnTPABuD5S9wb9ykXYRGTOOMVuSV/CK43sOhU4wwb2nT1RVjcbrrQjFw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@oxlint/binding-android-arm64@1.47.0':
|
||||||
|
resolution: {integrity: sha512-xh02lsTF1TAkR+SZrRMYHR/xCx8Wg2MAHxJNdHVpAKELh9/yE9h4LJeqAOBbIb3YYn8o/D97U9VmkvkfJfrHfw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@oxlint/binding-darwin-arm64@1.47.0':
|
||||||
|
resolution: {integrity: sha512-OSOfNJqabOYbkyQDGT5pdoL+05qgyrmlQrvtCO58M4iKGEQ/xf3XkkKj7ws+hO+k8Y4VF4zGlBsJlwqy7qBcHA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@oxlint/binding-darwin-x64@1.47.0':
|
||||||
|
resolution: {integrity: sha512-hP2bOI4IWNS+F6pVXWtRshSTuJ1qCRZgDgVUg6EBUqsRy+ExkEPJkx+YmIuxgdCduYK1LKptLNFuQLJP8voPbQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@oxlint/binding-freebsd-x64@1.47.0':
|
||||||
|
resolution: {integrity: sha512-F55jIEH5xmGu7S661Uho8vGiLFk0bY3A/g4J8CTKiLJnYu/PSMZ2WxFoy5Hji6qvFuujrrM9Q8XXbMO0fKOYPg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm-gnueabihf@1.47.0':
|
||||||
|
resolution: {integrity: sha512-wxmOn/wns/WKPXUC1fo5mu9pMZPVOu8hsynaVDrgmmXMdHKS7on6bA5cPauFFN9tJXNdsjW26AK9lpfu3IfHBQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm-musleabihf@1.47.0':
|
||||||
|
resolution: {integrity: sha512-KJTmVIA/GqRlM2K+ZROH30VMdydEU7bDTY35fNg3tOPzQRIs2deLZlY/9JWwdWo1F/9mIYmpbdCmPqtKhWNOPg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm64-gnu@1.47.0':
|
||||||
|
resolution: {integrity: sha512-PF7ELcFg1GVlS0X0ZB6aWiXobjLrAKer3T8YEkwIoO8RwWiAMkL3n3gbleg895BuZkHVlJ2kPRUwfrhHrVkD1A==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm64-musl@1.47.0':
|
||||||
|
resolution: {integrity: sha512-4BezLRO5cu0asf0Jp1gkrnn2OHiXrPPPEfBTxq1k5/yJ2zdGGTmZxHD2KF2voR23wb8Elyu3iQawXo7wvIZq0Q==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-ppc64-gnu@1.47.0':
|
||||||
|
resolution: {integrity: sha512-aI5ds9jq2CPDOvjeapiIj48T/vlWp+f4prkxs+FVzrmVN9BWIj0eqeJ/hV8WgXg79HVMIz9PU6deI2ki09bR1w==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-riscv64-gnu@1.47.0':
|
||||||
|
resolution: {integrity: sha512-mO7ycp9Elvgt5EdGkQHCwJA6878xvo9tk+vlMfT1qg++UjvOMB8INsOCQIOH2IKErF/8/P21LULkdIrocMw9xA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-riscv64-musl@1.47.0':
|
||||||
|
resolution: {integrity: sha512-24D0wsYT/7hDFn3Ow32m3/+QT/1ZwrUhShx4/wRDAmz11GQHOZ1k+/HBuK/MflebdnalmXWITcPEy4BWTi7TCA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-s390x-gnu@1.47.0':
|
||||||
|
resolution: {integrity: sha512-8tPzPne882mtML/uy3mApvdCyuVOpthJ7xUv3b67gVfz63hOOM/bwO0cysSkPyYYFDFRn6/FnUb7Jhmsesntvg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-x64-gnu@1.47.0':
|
||||||
|
resolution: {integrity: sha512-q58pIyGIzeffEBhEgbRxLFHmHfV9m7g1RnkLiahQuEvyjKNiJcvdHOwKH2BdgZxdzc99Cs6hF5xTa86X40WzPw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-x64-musl@1.47.0':
|
||||||
|
resolution: {integrity: sha512-e7DiLZtETZUCwTa4EEHg9G+7g3pY+afCWXvSeMG7m0TQ29UHHxMARPaEQUE4mfKgSqIWnJaUk2iZzRPMRdga5g==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@oxlint/binding-openharmony-arm64@1.47.0':
|
||||||
|
resolution: {integrity: sha512-3AFPfQ0WKMleT/bKd7zsks3xoawtZA6E/wKf0DjwysH7wUiMMJkNKXOzYq1R/00G98JFgSU1AkrlOQrSdNNhlg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@oxlint/binding-win32-arm64-msvc@1.47.0':
|
||||||
|
resolution: {integrity: sha512-cLMVVM6TBxp+N7FldQJ2GQnkcLYEPGgiuEaXdvhgvSgODBk9ov3jed+khIXSAWtnFOW0wOnG3RjwqPh0rCuheA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@oxlint/binding-win32-ia32-msvc@1.47.0':
|
||||||
|
resolution: {integrity: sha512-VpFOSzvTnld77/Edje3ZdHgZWnlTb5nVWXyTgjD3/DKF/6t5bRRbwn3z77zOdnGy44xAMvbyAwDNOSeOdVUmRA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@oxlint/binding-win32-x64-msvc@1.47.0':
|
||||||
|
resolution: {integrity: sha512-+q8IWptxXx2HMTM6JluR67284t0h8X/oHJgqpxH1siowxPMqZeIpAcWCUq+tY+Rv2iQK8TUugjZnSBQAVV5CmA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1457,8 +1644,8 @@ packages:
|
|||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@22.19.11':
|
'@types/node@24.10.13':
|
||||||
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==}
|
||||||
|
|
||||||
'@types/normalize-package-data@2.4.4':
|
'@types/normalize-package-data@2.4.4':
|
||||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||||
@@ -1872,18 +2059,14 @@ packages:
|
|||||||
css-select@5.1.0:
|
css-select@5.1.0:
|
||||||
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||||
|
|
||||||
css-tree@3.1.0:
|
css-tree@3.2.1:
|
||||||
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
|
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||||
|
|
||||||
css-what@6.1.0:
|
css-what@6.1.0:
|
||||||
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
cssstyle@5.3.7:
|
|
||||||
resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@@ -2285,6 +2468,7 @@ packages:
|
|||||||
git-raw-commits@2.0.11:
|
git-raw-commits@2.0.11:
|
||||||
resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
|
resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
git-up@8.1.1:
|
git-up@8.1.1:
|
||||||
@@ -2608,9 +2792,9 @@ packages:
|
|||||||
jsbn@1.1.0:
|
jsbn@1.1.0:
|
||||||
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
|
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
|
||||||
|
|
||||||
jsdom@28.0.0:
|
jsdom@29.0.1:
|
||||||
resolution: {integrity: sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==}
|
resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==}
|
||||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
canvas: ^3.0.0
|
canvas: ^3.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@@ -2708,14 +2892,14 @@ packages:
|
|||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
lru-cache@11.2.1:
|
|
||||||
resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
lru-cache@11.2.6:
|
lru-cache@11.2.6:
|
||||||
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
lru-cache@11.2.7:
|
||||||
|
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
lru-cache@6.0.0:
|
lru-cache@6.0.0:
|
||||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2790,8 +2974,8 @@ packages:
|
|||||||
mdast-util-to-string@4.0.0:
|
mdast-util-to-string@4.0.0:
|
||||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||||
|
|
||||||
mdn-data@2.12.2:
|
mdn-data@2.27.1:
|
||||||
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||||
|
|
||||||
mdurl@2.0.0:
|
mdurl@2.0.0:
|
||||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||||
@@ -3131,6 +3315,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-pLzCU8IgyKXPSO11eeharQkQ4GzOKNWhXq79pQarIRZEMt1/ssyr+MIuWBv1mNoenJLg04gvPx+fi4gcKZ4bag==}
|
resolution: {integrity: sha512-pLzCU8IgyKXPSO11eeharQkQ4GzOKNWhXq79pQarIRZEMt1/ssyr+MIuWBv1mNoenJLg04gvPx+fi4gcKZ4bag==}
|
||||||
engines: {node: '>= 18.0.0'}
|
engines: {node: '>= 18.0.0'}
|
||||||
|
|
||||||
|
oxlint@1.47.0:
|
||||||
|
resolution: {integrity: sha512-v7xkK1iv1qdvTxJGclM97QzN8hHs5816AneFAQ0NGji1BMUquhiDAhXpMwp8+ls16uRVJtzVHxP9pAAXblDeGA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
oxlint-tsgolint: '>=0.11.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
oxlint-tsgolint:
|
||||||
|
optional: true
|
||||||
|
|
||||||
p-all@5.0.1:
|
p-all@5.0.1:
|
||||||
resolution: {integrity: sha512-LMT7WX9ZSaq3J1zjloApkIVmtz0ZdMFSIqbuiEa3txGYPLjUPOvgOPOx3nFjo+f37ZYL+1aY666I2SG7GVwLOA==}
|
resolution: {integrity: sha512-LMT7WX9ZSaq3J1zjloApkIVmtz0ZdMFSIqbuiEa3txGYPLjUPOvgOPOx3nFjo+f37ZYL+1aY666I2SG7GVwLOA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -3262,6 +3456,7 @@ packages:
|
|||||||
prebuild-install@7.1.2:
|
prebuild-install@7.1.2:
|
||||||
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
|
resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
prettier@3.6.2:
|
prettier@3.6.2:
|
||||||
@@ -3688,6 +3883,7 @@ packages:
|
|||||||
tar@7.5.7:
|
tar@7.5.7:
|
||||||
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
|
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
terser@5.44.0:
|
terser@5.44.0:
|
||||||
resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==}
|
resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==}
|
||||||
@@ -3747,8 +3943,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tough-cookie@6.0.0:
|
tough-cookie@6.0.1:
|
||||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
tr46@0.0.3:
|
tr46@0.0.3:
|
||||||
@@ -3849,11 +4045,11 @@ packages:
|
|||||||
underscore@1.13.6:
|
underscore@1.13.6:
|
||||||
resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==}
|
resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==}
|
||||||
|
|
||||||
undici-types@6.21.0:
|
undici-types@7.16.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
undici@7.21.0:
|
undici@7.24.5:
|
||||||
resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==}
|
resolution: {integrity: sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==}
|
||||||
engines: {node: '>=20.18.1'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
unicorn-magic@0.3.0:
|
unicorn-magic@0.3.0:
|
||||||
@@ -4026,8 +4222,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
|
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
whatwg-url@16.0.0:
|
whatwg-url@16.0.1:
|
||||||
resolution: {integrity: sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==}
|
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
@@ -4127,27 +4323,25 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@acemir/cssom@0.9.31': {}
|
|
||||||
|
|
||||||
'@arcanis/slice-ansi@1.1.1':
|
'@arcanis/slice-ansi@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
grapheme-splitter: 1.0.4
|
grapheme-splitter: 1.0.4
|
||||||
|
|
||||||
'@asamuzakjp/css-color@4.1.2':
|
'@asamuzakjp/css-color@5.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
'@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
'@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
'@csstools/css-tokenizer': 4.0.0
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
lru-cache: 11.2.6
|
lru-cache: 11.2.7
|
||||||
|
|
||||||
'@asamuzakjp/dom-selector@6.7.8':
|
'@asamuzakjp/dom-selector@7.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@asamuzakjp/nwsapi': 2.3.9
|
'@asamuzakjp/nwsapi': 2.3.9
|
||||||
bidi-js: 1.0.3
|
bidi-js: 1.0.3
|
||||||
css-tree: 3.1.0
|
css-tree: 3.2.1
|
||||||
is-potential-custom-element-name: 1.0.1
|
is-potential-custom-element-name: 1.0.1
|
||||||
lru-cache: 11.2.6
|
lru-cache: 11.2.7
|
||||||
|
|
||||||
'@asamuzakjp/nwsapi@2.3.9': {}
|
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||||
|
|
||||||
@@ -5058,22 +5252,26 @@ snapshots:
|
|||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
|
'@bramus/specificity@2.4.2':
|
||||||
|
dependencies:
|
||||||
|
css-tree: 3.2.1
|
||||||
|
|
||||||
'@breejs/later@4.2.0': {}
|
'@breejs/later@4.2.0': {}
|
||||||
|
|
||||||
'@cdktf/hcl2json@0.21.0':
|
'@cdktf/hcl2json@0.21.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
fs-extra: 11.3.0
|
fs-extra: 11.3.0
|
||||||
|
|
||||||
'@csstools/color-helpers@6.0.1': {}
|
'@csstools/color-helpers@6.0.2': {}
|
||||||
|
|
||||||
'@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
'@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
'@csstools/css-tokenizer': 4.0.0
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
'@csstools/css-color-parser@4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
'@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@csstools/color-helpers': 6.0.1
|
'@csstools/color-helpers': 6.0.2
|
||||||
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||||
'@csstools/css-tokenizer': 4.0.0
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
@@ -5082,7 +5280,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@csstools/css-tokenizer': 4.0.0
|
'@csstools/css-tokenizer': 4.0.0
|
||||||
|
|
||||||
'@csstools/css-syntax-patches-for-csstree@1.0.27': {}
|
'@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)':
|
||||||
|
optionalDependencies:
|
||||||
|
css-tree: 3.2.1
|
||||||
|
|
||||||
'@csstools/css-tokenizer@4.0.0': {}
|
'@csstools/css-tokenizer@4.0.0': {}
|
||||||
|
|
||||||
@@ -5180,7 +5380,7 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.25.9':
|
'@esbuild/win32-x64@0.25.9':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@exodus/bytes@1.14.1': {}
|
'@exodus/bytes@1.15.0': {}
|
||||||
|
|
||||||
'@gwhitney/detect-indent@7.0.1': {}
|
'@gwhitney/detect-indent@7.0.1': {}
|
||||||
|
|
||||||
@@ -5256,7 +5456,7 @@ snapshots:
|
|||||||
agent-base: 7.1.3
|
agent-base: 7.1.3
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
lru-cache: 11.2.1
|
lru-cache: 11.2.6
|
||||||
socks-proxy-agent: 8.0.3
|
socks-proxy-agent: 8.0.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5421,6 +5621,63 @@ snapshots:
|
|||||||
|
|
||||||
'@oxc-project/types@0.113.0': {}
|
'@oxc-project/types@0.113.0': {}
|
||||||
|
|
||||||
|
'@oxlint/binding-android-arm-eabi@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-android-arm64@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-darwin-arm64@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-darwin-x64@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-freebsd-x64@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm-gnueabihf@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm-musleabihf@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm64-gnu@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-arm64-musl@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-ppc64-gnu@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-riscv64-gnu@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-riscv64-musl@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-s390x-gnu@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-x64-gnu@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-linux-x64-musl@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-openharmony-arm64@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-win32-arm64-msvc@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-win32-ia32-msvc@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxlint/binding-win32-x64-msvc@1.47.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -6086,13 +6343,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/bunyan@1.8.11':
|
'@types/bunyan@1.8.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
|
|
||||||
'@types/cacheable-request@6.0.3':
|
'@types/cacheable-request@6.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-cache-semantics': 4.0.4
|
'@types/http-cache-semantics': 4.0.4
|
||||||
'@types/keyv': 3.1.4
|
'@types/keyv': 3.1.4
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
'@types/responselike': 1.0.3
|
'@types/responselike': 1.0.3
|
||||||
|
|
||||||
'@types/chai@5.2.2':
|
'@types/chai@5.2.2':
|
||||||
@@ -6113,7 +6370,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/keyv@3.1.4':
|
'@types/keyv@3.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6125,9 +6382,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@22.19.11':
|
'@types/node@24.10.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@types/normalize-package-data@2.4.4': {}
|
'@types/normalize-package-data@2.4.4': {}
|
||||||
|
|
||||||
@@ -6135,7 +6392,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/responselike@1.0.3':
|
'@types/responselike@1.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
|
|
||||||
'@types/semver@7.5.8': {}
|
'@types/semver@7.5.8': {}
|
||||||
|
|
||||||
@@ -6145,7 +6402,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitest/coverage-v8@4.0.18(vitest@4.0.18)':
|
'@vitest/coverage-v8@4.0.18(vitest@4.0.18)':
|
||||||
@@ -6160,7 +6417,7 @@ snapshots:
|
|||||||
obug: 2.1.1
|
obug: 2.1.1
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.11)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2)
|
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@vitest/expect@4.0.18':
|
'@vitest/expect@4.0.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6171,13 +6428,13 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
'@vitest/mocker@4.0.18(vite@7.1.4(@types/node@22.19.11)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2))':
|
'@vitest/mocker@4.0.18(vite@7.1.4(@types/node@24.10.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.0.18
|
'@vitest/spy': 4.0.18
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.1.4(@types/node@22.19.11)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2)
|
vite: 7.1.4(@types/node@24.10.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@vitest/pretty-format@4.0.18':
|
'@vitest/pretty-format@4.0.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6205,7 +6462,7 @@ snapshots:
|
|||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.11)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2)
|
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@vitest/utils@4.0.18':
|
'@vitest/utils@4.0.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6483,7 +6740,7 @@ snapshots:
|
|||||||
'@npmcli/fs': 5.0.0
|
'@npmcli/fs': 5.0.0
|
||||||
fs-minipass: 3.0.3
|
fs-minipass: 3.0.3
|
||||||
glob: 13.0.1
|
glob: 13.0.1
|
||||||
lru-cache: 11.2.1
|
lru-cache: 11.2.6
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
minipass-collect: 2.0.1
|
minipass-collect: 2.0.1
|
||||||
minipass-flush: 1.0.5
|
minipass-flush: 1.0.5
|
||||||
@@ -6613,20 +6870,13 @@ snapshots:
|
|||||||
domutils: 3.1.0
|
domutils: 3.1.0
|
||||||
nth-check: 2.1.1
|
nth-check: 2.1.1
|
||||||
|
|
||||||
css-tree@3.1.0:
|
css-tree@3.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
mdn-data: 2.12.2
|
mdn-data: 2.27.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
css-what@6.1.0: {}
|
css-what@6.1.0: {}
|
||||||
|
|
||||||
cssstyle@5.3.7:
|
|
||||||
dependencies:
|
|
||||||
'@asamuzakjp/css-color': 4.1.2
|
|
||||||
'@csstools/css-syntax-patches-for-csstree': 1.0.27
|
|
||||||
css-tree: 3.1.0
|
|
||||||
lru-cache: 11.2.6
|
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
dargs@7.0.0: {}
|
dargs@7.0.0: {}
|
||||||
@@ -6636,7 +6886,7 @@ snapshots:
|
|||||||
data-urls@7.0.0:
|
data-urls@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-mimetype: 5.0.0
|
whatwg-mimetype: 5.0.0
|
||||||
whatwg-url: 16.0.0
|
whatwg-url: 16.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@noble/hashes'
|
- '@noble/hashes'
|
||||||
|
|
||||||
@@ -7190,7 +7440,7 @@ snapshots:
|
|||||||
|
|
||||||
html-encoding-sniffer@6.0.0:
|
html-encoding-sniffer@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@exodus/bytes': 1.14.1
|
'@exodus/bytes': 1.15.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@noble/hashes'
|
- '@noble/hashes'
|
||||||
|
|
||||||
@@ -7204,6 +7454,7 @@ snapshots:
|
|||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
http2-wrapper@1.0.3:
|
http2-wrapper@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7374,31 +7625,31 @@ snapshots:
|
|||||||
jsbn@1.1.0:
|
jsbn@1.1.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
jsdom@28.0.0:
|
jsdom@29.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@acemir/cssom': 0.9.31
|
'@asamuzakjp/css-color': 5.0.1
|
||||||
'@asamuzakjp/dom-selector': 6.7.8
|
'@asamuzakjp/dom-selector': 7.0.3
|
||||||
'@exodus/bytes': 1.14.1
|
'@bramus/specificity': 2.4.2
|
||||||
cssstyle: 5.3.7
|
'@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1)
|
||||||
|
'@exodus/bytes': 1.15.0
|
||||||
|
css-tree: 3.2.1
|
||||||
data-urls: 7.0.0
|
data-urls: 7.0.0
|
||||||
decimal.js: 10.6.0
|
decimal.js: 10.6.0
|
||||||
html-encoding-sniffer: 6.0.0
|
html-encoding-sniffer: 6.0.0
|
||||||
http-proxy-agent: 7.0.2
|
|
||||||
https-proxy-agent: 7.0.6
|
|
||||||
is-potential-custom-element-name: 1.0.1
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
lru-cache: 11.2.7
|
||||||
parse5: 8.0.0
|
parse5: 8.0.0
|
||||||
saxes: 6.0.0
|
saxes: 6.0.0
|
||||||
symbol-tree: 3.2.4
|
symbol-tree: 3.2.4
|
||||||
tough-cookie: 6.0.0
|
tough-cookie: 6.0.1
|
||||||
undici: 7.21.0
|
undici: 7.24.5
|
||||||
w3c-xmlserializer: 5.0.0
|
w3c-xmlserializer: 5.0.0
|
||||||
webidl-conversions: 8.0.1
|
webidl-conversions: 8.0.1
|
||||||
whatwg-mimetype: 5.0.0
|
whatwg-mimetype: 5.0.0
|
||||||
whatwg-url: 16.0.0
|
whatwg-url: 16.0.1
|
||||||
xml-name-validator: 5.0.0
|
xml-name-validator: 5.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@noble/hashes'
|
- '@noble/hashes'
|
||||||
- supports-color
|
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
@@ -7481,10 +7732,10 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@11.2.1: {}
|
|
||||||
|
|
||||||
lru-cache@11.2.6: {}
|
lru-cache@11.2.6: {}
|
||||||
|
|
||||||
|
lru-cache@11.2.7: {}
|
||||||
|
|
||||||
lru-cache@6.0.0:
|
lru-cache@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
@@ -7643,7 +7894,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/mdast': 4.0.4
|
'@types/mdast': 4.0.4
|
||||||
|
|
||||||
mdn-data@2.12.2: {}
|
mdn-data@2.27.1: {}
|
||||||
|
|
||||||
mdurl@2.0.0: {}
|
mdurl@2.0.0: {}
|
||||||
|
|
||||||
@@ -8105,6 +8356,28 @@ snapshots:
|
|||||||
openpgp@6.3.0:
|
openpgp@6.3.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
oxlint@1.47.0:
|
||||||
|
optionalDependencies:
|
||||||
|
'@oxlint/binding-android-arm-eabi': 1.47.0
|
||||||
|
'@oxlint/binding-android-arm64': 1.47.0
|
||||||
|
'@oxlint/binding-darwin-arm64': 1.47.0
|
||||||
|
'@oxlint/binding-darwin-x64': 1.47.0
|
||||||
|
'@oxlint/binding-freebsd-x64': 1.47.0
|
||||||
|
'@oxlint/binding-linux-arm-gnueabihf': 1.47.0
|
||||||
|
'@oxlint/binding-linux-arm-musleabihf': 1.47.0
|
||||||
|
'@oxlint/binding-linux-arm64-gnu': 1.47.0
|
||||||
|
'@oxlint/binding-linux-arm64-musl': 1.47.0
|
||||||
|
'@oxlint/binding-linux-ppc64-gnu': 1.47.0
|
||||||
|
'@oxlint/binding-linux-riscv64-gnu': 1.47.0
|
||||||
|
'@oxlint/binding-linux-riscv64-musl': 1.47.0
|
||||||
|
'@oxlint/binding-linux-s390x-gnu': 1.47.0
|
||||||
|
'@oxlint/binding-linux-x64-gnu': 1.47.0
|
||||||
|
'@oxlint/binding-linux-x64-musl': 1.47.0
|
||||||
|
'@oxlint/binding-openharmony-arm64': 1.47.0
|
||||||
|
'@oxlint/binding-win32-arm64-msvc': 1.47.0
|
||||||
|
'@oxlint/binding-win32-ia32-msvc': 1.47.0
|
||||||
|
'@oxlint/binding-win32-x64-msvc': 1.47.0
|
||||||
|
|
||||||
p-all@5.0.1:
|
p-all@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-map: 6.0.0
|
p-map: 6.0.0
|
||||||
@@ -8192,7 +8465,7 @@ snapshots:
|
|||||||
|
|
||||||
path-scurry@2.0.0:
|
path-scurry@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
lru-cache: 11.2.1
|
lru-cache: 11.2.6
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
@@ -8256,7 +8529,7 @@ snapshots:
|
|||||||
'@protobufjs/path': 1.1.2
|
'@protobufjs/path': 1.1.2
|
||||||
'@protobufjs/pool': 1.1.0
|
'@protobufjs/pool': 1.1.0
|
||||||
'@protobufjs/utf8': 1.1.0
|
'@protobufjs/utf8': 1.1.0
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
long: 5.2.3
|
long: 5.2.3
|
||||||
|
|
||||||
protocols@2.0.1: {}
|
protocols@2.0.1: {}
|
||||||
@@ -8943,7 +9216,7 @@ snapshots:
|
|||||||
|
|
||||||
totalist@3.0.1: {}
|
totalist@3.0.1: {}
|
||||||
|
|
||||||
tough-cookie@6.0.0:
|
tough-cookie@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
tldts: 7.0.23
|
tldts: 7.0.23
|
||||||
|
|
||||||
@@ -9036,9 +9309,9 @@ snapshots:
|
|||||||
|
|
||||||
underscore@1.13.6: {}
|
underscore@1.13.6: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
undici@7.21.0: {}
|
undici@7.24.5: {}
|
||||||
|
|
||||||
unicorn-magic@0.3.0: {}
|
unicorn-magic@0.3.0: {}
|
||||||
|
|
||||||
@@ -9118,7 +9391,7 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.2
|
vfile-message: 4.0.2
|
||||||
|
|
||||||
vite@7.1.4(@types/node@22.19.11)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2):
|
vite@7.1.4(@types/node@24.10.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.9
|
esbuild: 0.25.9
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -9127,16 +9400,16 @@ snapshots:
|
|||||||
rollup: 4.50.0
|
rollup: 4.50.0
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
yaml: 2.8.2
|
yaml: 2.8.2
|
||||||
|
|
||||||
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.11)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@28.0.0)(terser@5.44.0)(yaml@2.8.2):
|
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@29.0.1)(terser@5.44.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.18
|
'@vitest/expect': 4.0.18
|
||||||
'@vitest/mocker': 4.0.18(vite@7.1.4(@types/node@22.19.11)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2))
|
'@vitest/mocker': 4.0.18(vite@7.1.4(@types/node@24.10.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2))
|
||||||
'@vitest/pretty-format': 4.0.18
|
'@vitest/pretty-format': 4.0.18
|
||||||
'@vitest/runner': 4.0.18
|
'@vitest/runner': 4.0.18
|
||||||
'@vitest/snapshot': 4.0.18
|
'@vitest/snapshot': 4.0.18
|
||||||
@@ -9153,13 +9426,13 @@ snapshots:
|
|||||||
tinyexec: 1.0.2
|
tinyexec: 1.0.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vite: 7.1.4(@types/node@22.19.11)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2)
|
vite: 7.1.4(@types/node@24.10.13)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.2)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@types/node': 22.19.11
|
'@types/node': 24.10.13
|
||||||
'@vitest/ui': 4.0.18(vitest@4.0.18)
|
'@vitest/ui': 4.0.18(vitest@4.0.18)
|
||||||
jsdom: 28.0.0
|
jsdom: 29.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
@@ -9197,9 +9470,9 @@ snapshots:
|
|||||||
|
|
||||||
whatwg-mimetype@5.0.0: {}
|
whatwg-mimetype@5.0.0: {}
|
||||||
|
|
||||||
whatwg-url@16.0.0:
|
whatwg-url@16.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@exodus/bytes': 1.14.1
|
'@exodus/bytes': 1.15.0
|
||||||
tr46: 6.0.0
|
tr46: 6.0.0
|
||||||
webidl-conversions: 8.0.1
|
webidl-conversions: 8.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ packages:
|
|||||||
catalog:
|
catalog:
|
||||||
'@vitest/coverage-v8': ^4.0.18
|
'@vitest/coverage-v8': ^4.0.18
|
||||||
'@vue/test-utils': ^2.4.6
|
'@vue/test-utils': ^2.4.6
|
||||||
jsdom: ^28.0.0
|
jsdom: ^29.0.1
|
||||||
|
oxlint: ^1.2.0
|
||||||
tsdown: ^0.12.5
|
tsdown: ^0.12.5
|
||||||
vitest: ^4.0.18
|
vitest: ^4.0.18
|
||||||
'@vitest/ui': ^4.0.18
|
'@vitest/ui': ^4.0.18
|
||||||
|
|||||||
@@ -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';
|
||||||
|
```
|
||||||
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.11",
|
"version": "0.0.13",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Collection of powerful tools for Vue",
|
"description": "Collection of powerful tools for Vue",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -32,13 +32,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"lint": "oxlint -c oxlint.config.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"build": "tsdown"
|
"build": "tsdown"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@robonen/oxlint": "workspace:*",
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
|
"@robonen/tsdown": "workspace:*",
|
||||||
"@vue/test-utils": "catalog:",
|
"@vue/test-utils": "catalog:",
|
||||||
|
"oxlint": "catalog:",
|
||||||
"tsdown": "catalog:"
|
"tsdown": "catalog:"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
4
web/vue/src/composables/browser/index.ts
Normal file
4
web/vue/src/composables/browser/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './useEventListener';
|
||||||
|
export * from './useFocusGuard';
|
||||||
|
export * from './useSupported';
|
||||||
|
export * from './useTabLeader';
|
||||||
375
web/vue/src/composables/browser/useEventListener/index.test.ts
Normal file
375
web/vue/src/composables/browser/useEventListener/index.test.ts
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
import { defineComponent, effectScope, nextTick, ref } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { useEventListener } from '.';
|
||||||
|
|
||||||
|
const mountWithEventListener = (
|
||||||
|
setup: () => Record<string, any> | void,
|
||||||
|
) => {
|
||||||
|
return mount(
|
||||||
|
defineComponent({
|
||||||
|
setup,
|
||||||
|
template: '<div></div>',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe(useEventListener, () => {
|
||||||
|
let component: ReturnType<typeof mountWithEventListener>;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
component?.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register and trigger a listener on an explicit target', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const target = document.createElement('div');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove listener when stop is called', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const target = document.createElement('div');
|
||||||
|
let stop: () => void;
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
stop = useEventListener(target, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
stop!();
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove listener when component is unmounted', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const target = document.createElement('div');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
component.unmount();
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register multiple events at once', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const target = document.createElement('div');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, ['click', 'focus'], listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
target.dispatchEvent(new Event('focus'));
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register multiple listeners at once', async () => {
|
||||||
|
const listener1 = vi.fn();
|
||||||
|
const listener2 = vi.fn();
|
||||||
|
const target = document.createElement('div');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', [listener1, listener2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
|
||||||
|
expect(listener1).toHaveBeenCalledOnce();
|
||||||
|
expect(listener2).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register multiple events and multiple listeners', async () => {
|
||||||
|
const listener1 = vi.fn();
|
||||||
|
const listener2 = vi.fn();
|
||||||
|
const target = document.createElement('div');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, ['click', 'focus'], [listener1, listener2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
target.dispatchEvent(new Event('focus'));
|
||||||
|
|
||||||
|
expect(listener1).toHaveBeenCalledTimes(2);
|
||||||
|
expect(listener2).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('react to a reactive target change', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const el1 = document.createElement('div');
|
||||||
|
const el2 = document.createElement('div');
|
||||||
|
const target = ref<HTMLElement>(el1);
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
el1.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
target.value = el2;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Old target should no longer trigger listener
|
||||||
|
el1.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// New target should trigger listener
|
||||||
|
el2.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleanup when reactive target becomes null', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const target = ref<HTMLElement | null>(el);
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
el.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
target.value = null;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
el.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return noop when target is undefined', () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const stop = useEventListener(undefined as any, 'click', listener);
|
||||||
|
|
||||||
|
expect(stop).toBeTypeOf('function');
|
||||||
|
stop(); // should not throw
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pass options to addEventListener', async () => {
|
||||||
|
const target = document.createElement('div');
|
||||||
|
const addSpy = vi.spyOn(target, 'addEventListener');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', () => {}, { capture: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(addSpy).toHaveBeenCalledWith('click', expect.any(Function), { capture: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('use window as default target when event string is passed directly', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const addSpy = vi.spyOn(globalThis, 'addEventListener');
|
||||||
|
const removeSpy = vi.spyOn(globalThis, 'removeEventListener');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener('click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(addSpy).toHaveBeenCalledWith('click', expect.any(Function), undefined);
|
||||||
|
|
||||||
|
component.unmount();
|
||||||
|
|
||||||
|
expect(removeSpy).toHaveBeenCalledWith('click', expect.any(Function), undefined);
|
||||||
|
|
||||||
|
addSpy.mockRestore();
|
||||||
|
removeSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('use window as default target when event array is passed directly', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const addSpy = vi.spyOn(globalThis, 'addEventListener');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(['click', 'keydown'], listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(addSpy).toHaveBeenCalledWith('click', expect.any(Function), undefined);
|
||||||
|
expect(addSpy).toHaveBeenCalledWith('keydown', expect.any(Function), undefined);
|
||||||
|
|
||||||
|
addSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('work with document target', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(document, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
document.dispatchEvent(new Event('click'));
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto cleanup when effectScope is disposed', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const target = document.createElement('div');
|
||||||
|
const scope = effectScope();
|
||||||
|
|
||||||
|
scope.run(() => {
|
||||||
|
useEventListener(target, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
scope.stop();
|
||||||
|
|
||||||
|
target.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-register when reactive options change', async () => {
|
||||||
|
const target = document.createElement('div');
|
||||||
|
const listener = vi.fn();
|
||||||
|
const options = ref<boolean | AddEventListenerOptions>(false);
|
||||||
|
const addSpy = vi.spyOn(target, 'addEventListener');
|
||||||
|
const removeSpy = vi.spyOn(target, 'removeEventListener');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', listener, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(addSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(addSpy).toHaveBeenLastCalledWith('click', listener, false);
|
||||||
|
|
||||||
|
options.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(removeSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(addSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(addSpy).toHaveBeenLastCalledWith('click', listener, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pass correct arguments to removeEventListener on stop', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const options = { capture: true };
|
||||||
|
const target = document.createElement('div');
|
||||||
|
const removeSpy = vi.spyOn(target, 'removeEventListener');
|
||||||
|
let stop: () => void;
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
stop = useEventListener(target, 'click', listener, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
stop!();
|
||||||
|
|
||||||
|
expect(removeSpy).toHaveBeenCalledWith('click', listener, { capture: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove all listeners for all events on stop', async () => {
|
||||||
|
const listener1 = vi.fn();
|
||||||
|
const listener2 = vi.fn();
|
||||||
|
const events = ['click', 'scroll', 'blur'];
|
||||||
|
const options = { capture: true };
|
||||||
|
const target = document.createElement('div');
|
||||||
|
const removeSpy = vi.spyOn(target, 'removeEventListener');
|
||||||
|
let stop: () => void;
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
stop = useEventListener(target, events, [listener1, listener2], options);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
stop!();
|
||||||
|
|
||||||
|
expect(removeSpy).toHaveBeenCalledTimes(events.length * 2);
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
expect(removeSpy).toHaveBeenCalledWith(event, listener1, { capture: true });
|
||||||
|
expect(removeSpy).toHaveBeenCalledWith(event, listener2, { capture: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clone object options to prevent reactive mutation issues', async () => {
|
||||||
|
const target = document.createElement('div');
|
||||||
|
const listener = vi.fn();
|
||||||
|
const options = ref<AddEventListenerOptions>({ capture: true });
|
||||||
|
const addSpy = vi.spyOn(target, 'addEventListener');
|
||||||
|
const removeSpy = vi.spyOn(target, 'removeEventListener');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', listener, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(addSpy).toHaveBeenCalledWith('click', listener, { capture: true });
|
||||||
|
|
||||||
|
// Change options reactively — old removal should use the snapshotted options
|
||||||
|
options.value = { capture: false };
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(removeSpy).toHaveBeenCalledWith('click', listener, { capture: true });
|
||||||
|
expect(addSpy).toHaveBeenLastCalledWith('click', listener, { capture: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not listen when reactive target starts as null', async () => {
|
||||||
|
const listener = vi.fn();
|
||||||
|
const target = ref<HTMLElement | null>(null);
|
||||||
|
const el = document.createElement('div');
|
||||||
|
|
||||||
|
component = mountWithEventListener(() => {
|
||||||
|
useEventListener(target, 'click', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
el.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Set target later
|
||||||
|
target.value = el;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
el.dispatchEvent(new Event('click'));
|
||||||
|
expect(listener).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user