Add object utilities and comprehensive text helpers

Add typed object selection, transformation, clone, merge, freeze, flatten and diff helpers. Add text normalization, truncation, search, masking, parsing, wrapping and formatting utilities. Document behavior and update bundle budgets.
This commit is contained in:
warkanum committed 2026-09-23 20:00:21 +02:00
1 parent 538ec38632
commit 280a7eadd7
17 files changed
+1091 -351

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@warkypublic/artemis-kit": minor
---
Add object selection, mapping, compaction, inversion, cloning, merging, freezing, flattening, and diff helpers. Export DeepPartial, DeepReadonly, DeepRequired, Mutable, KeysOfType, and Prettify types, with examples and documented handling of cycles, arrays, and property names.
+5
View File
@@ -0,0 +1,5 @@
---
"@warkypublic/artemis-kit": minor
---
Add string helpers for truncation, normalization, searching, interpolation, masking, affixes, splitting, prefixes, wrapping, indentation, UTF-8 byte length, initials, labels, slugs, and natural sorting. Export the helpers and MaskOptions from the strings module and package root, with documented Unicode behavior and edge cases.
+4
View File
@@ -6,6 +6,10 @@ Gitea at https://git.warky.dev; workflows belong in `.gitea/workflows/`.
## Development checks ## Development checks
`pnpm-workspace.yaml` explicitly allows esbuild install scripts through
`allowBuilds` for pnpm versions that require dependency build approval. Keep
approvals scoped to the dependencies that need them.
Run `pnpm test` for the test suite and `pnpm check:bundle` for a production build Run `pnpm test` for the test suite and `pnpm check:bundle` for a production build
with bundle checks. Builds fail on direct eval warnings. The bundle check measures with bundle checks. Builds fail on direct eval warnings. The bundle check measures
raw and gzip bytes for every emitted JavaScript file against raw and gzip bytes for every emitted JavaScript file against
+208
View File
@@ -113,6 +113,71 @@ import { ... } from "@warkypublic/artemis-kit/strings"
--- ---
### Text helpers (`strings`)
These functions are exported from `@warkypublic/artemis-kit/strings` and the package root.
| Function | Behavior |
|---|---|
| `truncate(text, maxLength, suffix = "…")` | Limit total character count, including suffix; clip the suffix if necessary |
| `truncateWords(text, maxWords, suffix = "…")` | Keep whitespace-delimited words; preserve original text if it fits |
| `slugify(text)` | Lowercase, strip decomposable accents, join Unicode letters/numbers with hyphens |
| `escapeRegExp(text)` | Escape regex metacharacters for a literal pattern |
| `normalizeWhitespace(text)` | Collapse whitespace to spaces and trim |
| `stripDiacritics(text)` | Remove decomposable accents and combining marks; not transliteration |
| `includesIgnoreCase(text, query, locale?)` | Literal substring match after locale-aware lowercasing |
| `interpolate(template, values)` | Replace `{name}` using own properties only; missing keys remain unchanged |
| `mask(text, options?)` | Hide characters; defaults to showing the last four |
| `splitOnce(text, separator)` | Return `[before, after]` at the first literal separator |
| `isBlank(text)` | True for null, undefined, empty, or whitespace-only strings |
| `toSearchKey(text, locale?)` | Lowercase, remove accents, and normalize whitespace |
| `humanize(text)` | Turn identifiers such as `customer_id` or `customerId` into `Customer id` |
| `getInitials(name, max = 2)` | Uppercase the first character of up to `max` whitespace-delimited words |
| `ensurePrefix(text, prefix)` / `ensureSuffix(text, suffix)` | Add an exact affix only when absent |
| `removePrefix(text, prefix)` / `removeSuffix(text, suffix)` | Remove an exact affix once |
| `substringBefore(text, separator)` / `substringAfter(text, separator)` | Extract either side of the first literal separator |
| `countOccurrences(text, search, overlap = false)` | Count literal matches; empty search returns zero |
| `commonPrefix(values)` | Longest shared prefix; empty list returns an empty string |
| `wrapText(text, width)` | Wrap at whitespace, preserve explicit line breaks, leave long words intact |
| `dedent(text)` | Remove shared literal spaces/tabs and outer blank lines |
| `normalizeLineEndings(text)` | Convert CRLF and CR to LF |
| `utf8ByteLength(text)` | Count UTF-8 bytes, including replacement bytes for lone surrogates |
| `compareNatural(a, b)` | Sort numeric text naturally using the runtime locale, with empty strings last |
```ts
import {
truncate, toSearchKey, interpolate, mask, compareNatural,
} from "@warkypublic/artemis-kit/strings"
truncate("Hello world", 8) // "Hello w…"
toSearchKey(" CAFÉ au lait ") // "cafe au lait"
interpolate("Hello {name}", { name: "Ada" }) // "Hello Ada"
mask("123456789", { start: 2, end: 2, character: "#" }) // "12#####89"
const files = ["file10", "file2"].sort(compareNatural) // ["file2", "file10"]
```
Character-based helpers use grapheme clusters when `Intl.Segmenter` is available,
keeping combined emoji and accented characters together. Older runtimes fall back
to Unicode code points, which keep surrogate pairs intact but may separate combined
characters. Length limits must be nonnegative safe integers; `wrapText` requires a
positive width. Invalid limits throw `RangeError`. A zero truncation limit returns
an empty string.
`mask` accepts `{ start?: number, end?: number, character?: string }`. The mask
character must be one character; overlapping visible ranges leave the text visible.
Masking changes only the displayed string. `interpolate` stringifies numbers and
booleans, turns null/undefined values into empty strings, and does not evaluate or
recursively substitute values. It does not escape HTML.
A missing separator yields `[text, ""]` from `splitOnce`; an empty separator yields
`["", text]`. The substring helpers follow the same rules. Affixes are case-sensitive
and empty affixes leave text unchanged. `includesIgnoreCase` does not remove accents
or perform full Unicode case folding; use `toSearchKey` on both operands for
accent-insensitive search. `wrapText` collapses whitespace within lines; `dedent`
treats tabs literally rather than expanding them to visual columns.
---
### `base64` ### `base64`
```ts ```ts
import { ... } from "@warkypublic/artemis-kit/base64" import { ... } from "@warkypublic/artemis-kit/base64"
@@ -145,6 +210,149 @@ import { ... } from "@warkypublic/artemis-kit/object"
| `retrocycle` | `($)` | Restore circular refs from `{"$ref": PATH}` objects | | `retrocycle` | `($)` | Restore circular refs from `{"$ref": PATH}` objects |
| `stringify_json` | `(object)` | `JSON.stringify` safe for circular references | | `stringify_json` | `(object)` | `JSON.stringify` safe for circular references |
#### Selecting and transforming properties
```ts
import {
pick, omit, pickBy, omitBy, mapValues, mapKeys,
isPlainObject, hasOwn, compactObject, invert,
} from "@warkypublic/artemis-kit/object"
const user = { id: 1, name: "Ada", password: "secret" }
pick(user, ["id", "name"]) // { id: 1, name: "Ada" }
omit(user, ["password"]) // { id: 1, name: "Ada" }
pickBy({ a: 1, b: 0 }, value => value > 0) // { a: 1 }
omitBy({ a: 1, b: 0 }, value => value > 0) // { b: 0 }
mapValues({ a: 2, b: 3 }, value => value * 2) // { a: 4, b: 6 }
mapKeys({ first: "Ada" }, key => key.toUpperCase()) // { FIRST: "Ada" }
isPlainObject({}) // true
isPlainObject(new Date()) // false
hasOwn({ id: 1 }, "id") // true
hasOwn({}, "toString") // false
compactObject({ a: null, b: undefined, c: 0, d: false, e: "" })
// { c: 0, d: false, e: "" }
invert({ a: "red", b: "red", c: "blue" })
// Map { "red" => ["a", "b"], "blue" => ["c"] }
```
These helpers produce shallow results and preserve value references. They operate
on own enumerable string and symbol properties; `pick` can also select an explicitly
named non-enumerable own property. Results contain ordinary writable data properties,
not the original descriptors. Accessors are read when selected or transformed.
`mapKeys` keeps the last value when mapped keys collide. `invert` groups duplicate
values using Map equality, so object values are compared by identity.
`isPlainObject` accepts objects with this realm's `Object.prototype` or a null
prototype. Class instances and objects from another realm are not plain objects.
Keys such as `__proto__` are copied as own data properties without changing prototypes.
Use these helpers on data records: static types cannot describe property enumeration
or distinguish inherited keys from own keys.
#### Cloning, merging, and freezing
```ts
import { deepClone, deepMerge, deepFreeze } from "@warkypublic/artemis-kit/object"
const original = { user: { name: "Ada" }, created: new Date(0) }
const copy = deepClone(original)
copy.user === original.user // false
copy.created.getTime() // 0
const defaults = { theme: { color: "blue", size: 12 }, tags: ["base"] }
const overrides = { theme: { size: 14 }, tags: ["custom"] }
deepMerge(defaults, overrides)
// { theme: { color: "blue", size: 14 }, tags: ["custom"] }
deepMerge(defaults, overrides, { arrays: "concat" })
// { theme: { color: "blue", size: 14 }, tags: ["base", "custom"] }
const config = deepFreeze({ nested: { enabled: true }, tags: ["fixed"] })
Object.isFrozen(config.nested) // true
Object.isFrozen(config.tags) // true
```
`deepClone` preserves cycles and shared references. It supports primitives, plain
objects, arrays, Date, RegExp (including `lastIndex`), Map, Set, ArrayBuffer, typed
arrays, and DataView. Buffer views share the cloned backing buffer when the originals
share one. Sparse array holes and null prototypes are preserved. Plain objects and
arrays copy own enumerable string/symbol values; accessors become evaluated values.
Custom properties on built-ins are not copied. Functions, custom classes, WeakMap,
WeakSet, SharedArrayBuffer, and other unsupported objects throw `TypeError`.
`deepMerge` requires plain-object roots and returns a new object, leaving both inputs
unchanged. Nested plain objects merge recursively; source values, including undefined,
replace other values. Arrays are replaced unless `arrays: "concat"` is selected.
Other values use `deepClone`'s supported types. Cycles through traversed plain objects
or arrays throw `TypeError`; shared identity across independently merged branches is
not guaranteed. The result type is `Record<PropertyKey, unknown>` because overlapping
values may have different types; narrow values before use.
`deepFreeze` freezes its input **in place** and returns it as `DeepReadonly<T>`.
It supports cycles, plain objects, and arrays, including non-enumerable and symbol
properties. Accessors, functions, and built-ins such as Date, Map, Set, and typed
arrays throw `TypeError` before any part of the graph is frozen. This avoids implying
that freezing an object also prevents mutation through built-in methods.
#### Flattening and changes
```ts
import { flattenObject, unflattenObject, diffObjects } from "@warkypublic/artemis-kit/object"
const flat = flattenObject({ user: { name: "Ada" }, tags: ["admin"], empty: {} })
// { "/user/name": "Ada", "/tags": ["admin"], "/empty": {} }
unflattenObject(flat)
// { user: { name: "Ada" }, tags: ["admin"], empty: {} }
flattenObject({ "a/b": { "~key": 1 } })
// { "/a~1b/~0key": 1 }
diffObjects({ user: { name: "Ada" }, old: true }, { user: { name: "Grace" }, active: true })
// [
// { type: "changed", path: ["user", "name"], before: "Ada", after: "Grace" },
// { type: "removed", path: ["old"], before: true },
// { type: "added", path: ["active"], after: true },
// ]
```
Flattening traverses only nested plain objects with enumerable string keys. Paths
start with `/`; key characters `~` and `/` become `~0` and `~1`. Empty keys are
supported (`"/"` is the path for an empty root key). Arrays, special objects, and
empty objects are leaves and keep their original references. Empty roots produce
`{}`. `unflattenObject` reconstructs plain objects without inferring arrays from
numeric keys, and rejects invalid paths and parent/child collisions such as `/a`
and `/a/b`. Symbol and non-enumerable properties are outside this representation.
`diffObjects` compares own enumerable string keys in plain objects recursively.
Other values, including arrays and dates, use `Object.is`: distinct but equal-content
arrays report a change. Added/removed subtrees are reported as a single entry;
absence differs from an own property containing undefined. Paths are key arrays so
punctuation in keys is unambiguous. Returned values retain input references.
Flattening and diffing reject cycles through the plain objects they traverse.
#### TypeScript utility types
```ts
import type {
DeepPartial, DeepReadonly, DeepRequired, Mutable, KeysOfType, Prettify,
} from "@warkypublic/artemis-kit/object"
type Settings = { name: string; nested: { enabled: boolean }; tags: string[] }
const patch: DeepPartial<Settings> = { nested: {} }
type FrozenSettings = DeepReadonly<Settings> // nested fields and arrays are readonly
const complete: DeepRequired<{ nested?: { enabled?: boolean } }> = {
nested: { enabled: true },
}
type Editable = Mutable<{ readonly id: number }> // { id: number }
type TextKeys = KeysOfType<{ id: number; name: string }, string> // "name"
type User = Prettify<{ id: number } & { name: string }> // expanded editor display
```
`DeepPartial` and `DeepRequired` preserve tuple structure and recurse through object
properties, map values, and set elements. Map keys are unchanged. `DeepReadonly`
also makes map keys/values and set elements recursively readonly. These types preserve
functions, Date, and RegExp as-is; they target data shapes, not arbitrary class
instances. Readonly typing does not prevent mutation through Date/RegExp methods.
`Mutable` removes only top-level readonly modifiers. `KeysOfType` selects properties
whose entire declared value type extends the requested type. These are compile-time
utilities, not runtime validation or freezing.
--- ---
### `dataqueue` ### `dataqueue`
-1
View File
@@ -173,7 +173,6 @@
"typescript-eslint": "^8.17.0", "typescript-eslint": "^8.17.0",
"vite": "^6.0.2", "vite": "^6.0.2",
"vite-plugin-dts": "^4.3.0", "vite-plugin-dts": "^4.3.0",
"@anthropic-ai/claude-code": "^0.2.99",
"vitest": "^2.1.8" "vitest": "^2.1.8"
}, },
"engines": { "engines": {
-344
View File
@@ -15,9 +15,6 @@ importers:
specifier: ^11.0.3 specifier: ^11.0.3
version: 11.0.3 version: 11.0.3
devDependencies: devDependencies:
'@anthropic-ai/claude-code':
specifier: ^0.2.99
version: 0.2.99
'@changesets/cli': '@changesets/cli':
specifier: ^2.27.10 specifier: ^2.27.10
version: 2.27.10 version: 2.27.10
@@ -54,11 +51,6 @@ importers:
packages: packages:
'@anthropic-ai/claude-code@0.2.99':
resolution: {integrity: sha512-KG+pFtiqSRmrSzcBRtgiPSqu9zaGg7GIlppKLEyDBlTn9M9JJbe1SLY5W8eB2AXC/fro+ePmM9cHzHO+zhkr7g==}
engines: {node: '>=18.0.0'}
hasBin: true
'@babel/helper-string-parser@7.25.9': '@babel/helper-string-parser@7.25.9':
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -471,45 +463,6 @@ packages:
resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==}
engines: {node: '>=18.18'} engines: {node: '>=18.18'}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.0': '@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
@@ -874,22 +827,10 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-path-resolve@1.0.0: better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'} engines: {node: '>=4'}
better-sqlite3@11.9.1:
resolution: {integrity: sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
brace-expansion@1.1.11: brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -900,9 +841,6 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
cac@6.7.14: cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -926,9 +864,6 @@ packages:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
ci-info@3.9.0: ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -983,18 +918,10 @@ packages:
decimal.js@10.4.3: decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
deep-eql@5.0.2: deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -1006,17 +933,10 @@ packages:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'} engines: {node: '>=8'}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
dir-glob@3.0.1: dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'} engines: {node: '>=8'}
end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
enquirer@2.4.1: enquirer@2.4.1:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
@@ -1095,10 +1015,6 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
expect-type@1.1.0: expect-type@1.1.0:
resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -1130,9 +1046,6 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
fill-range@7.1.1: fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1156,9 +1069,6 @@ packages:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fs-extra@7.0.1: fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'} engines: {node: '>=6 <7 || >=8'}
@@ -1175,9 +1085,6 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
glob-parent@5.1.2: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -1239,9 +1146,6 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2: ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -1258,12 +1162,6 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
is-core-module@2.15.1: is-core-module@2.15.1:
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1386,10 +1284,6 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
minimatch@3.0.8: minimatch@3.0.8:
resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==}
@@ -1400,12 +1294,6 @@ packages:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
mlly@1.7.3: mlly@1.7.3:
resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==}
@@ -1424,22 +1312,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-abi@3.75.0:
resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==}
engines: {node: '>=10'}
nwsapi@2.2.16: nwsapi@2.2.16:
resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1536,11 +1414,6 @@ packages:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
hasBin: true
prelude-ls@1.2.1: prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1550,9 +1423,6 @@ packages:
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
hasBin: true hasBin: true
pump@3.0.2:
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1560,18 +1430,10 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
read-yaml-file@1.1.0: read-yaml-file@1.1.0:
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
engines: {node: '>=6'} engines: {node: '>=6'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
regenerator-runtime@0.14.1: regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@@ -1606,9 +1468,6 @@ packages:
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -1641,12 +1500,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'} engines: {node: '>=14'}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
slash@3.0.0: slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1675,9 +1528,6 @@ packages:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'} engines: {node: '>=0.6.19'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1: strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1686,10 +1536,6 @@ packages:
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'} engines: {node: '>=4'}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
strip-json-comments@3.1.1: strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1709,13 +1555,6 @@ packages:
symbol-tree@3.2.4: symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
tar-fs@2.1.2:
resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
term-size@2.2.1: term-size@2.2.1:
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1767,9 +1606,6 @@ packages:
peerDependencies: peerDependencies:
typescript: '>=4.2.0' typescript: '>=4.2.0'
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
type-check@0.4.0: type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -1804,9 +1640,6 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@11.0.3: uuid@11.0.3:
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
hasBin: true hasBin: true
@@ -1959,9 +1792,6 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.18.0: ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -1990,15 +1820,6 @@ packages:
snapshots: snapshots:
'@anthropic-ai/claude-code@0.2.99':
dependencies:
better-sqlite3: 11.9.1
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-win32-x64': 0.33.5
'@babel/helper-string-parser@7.25.9': {} '@babel/helper-string-parser@7.25.9': {}
'@babel/helper-validator-identifier@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {}
@@ -2351,33 +2172,6 @@ snapshots:
'@humanwhocodes/retry@0.4.1': {} '@humanwhocodes/retry@0.4.1': {}
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.0.4':
optional: true
'@img/sharp-libvips-linux-arm@1.0.5':
optional: true
'@img/sharp-libvips-linux-x64@1.0.4':
optional: true
'@img/sharp-linux-arm@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
optional: true
'@img/sharp-linux-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
'@img/sharp-win32-x64@0.33.5':
optional: true
'@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/sourcemap-codec@1.5.0': {}
'@manypkg/find-root@1.1.0': '@manypkg/find-root@1.1.0':
@@ -2777,27 +2571,10 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
base64-js@1.5.1: {}
better-path-resolve@1.0.0: better-path-resolve@1.0.0:
dependencies: dependencies:
is-windows: 1.0.2 is-windows: 1.0.2
better-sqlite3@11.9.1:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
brace-expansion@1.1.11: brace-expansion@1.1.11:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -2811,11 +2588,6 @@ snapshots:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
cac@6.7.14: {} cac@6.7.14: {}
callsites@3.1.0: {} callsites@3.1.0: {}
@@ -2837,8 +2609,6 @@ snapshots:
check-error@2.1.1: {} check-error@2.1.1: {}
chownr@1.1.4: {}
ci-info@3.9.0: {} ci-info@3.9.0: {}
color-convert@2.0.1: color-convert@2.0.1:
@@ -2882,30 +2652,18 @@ snapshots:
decimal.js@10.4.3: {} decimal.js@10.4.3: {}
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
deep-eql@5.0.2: {} deep-eql@5.0.2: {}
deep-extend@0.6.0: {}
deep-is@0.1.4: {} deep-is@0.1.4: {}
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
detect-indent@6.1.0: {} detect-indent@6.1.0: {}
detect-libc@2.0.4: {}
dir-glob@3.0.1: dir-glob@3.0.1:
dependencies: dependencies:
path-type: 4.0.0 path-type: 4.0.0
end-of-stream@1.4.4:
dependencies:
once: 1.4.0
enquirer@2.4.1: enquirer@2.4.1:
dependencies: dependencies:
ansi-colors: 4.1.3 ansi-colors: 4.1.3
@@ -3044,8 +2802,6 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
expand-template@2.0.3: {}
expect-type@1.1.0: {} expect-type@1.1.0: {}
extendable-error@0.1.7: {} extendable-error@0.1.7: {}
@@ -3078,8 +2834,6 @@ snapshots:
dependencies: dependencies:
flat-cache: 4.0.1 flat-cache: 4.0.1
file-uri-to-path@1.0.0: {}
fill-range@7.1.1: fill-range@7.1.1:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
@@ -3107,8 +2861,6 @@ snapshots:
combined-stream: 1.0.8 combined-stream: 1.0.8
mime-types: 2.1.35 mime-types: 2.1.35
fs-constants@1.0.0: {}
fs-extra@7.0.1: fs-extra@7.0.1:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -3126,8 +2878,6 @@ snapshots:
function-bind@1.1.2: {} function-bind@1.1.2: {}
github-from-package@0.0.0: {}
glob-parent@5.1.2: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -3189,8 +2939,6 @@ snapshots:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {} ignore@5.3.2: {}
import-fresh@3.3.0: import-fresh@3.3.0:
@@ -3202,10 +2950,6 @@ snapshots:
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
inherits@2.0.4: {}
ini@1.3.8: {}
is-core-module@2.15.1: is-core-module@2.15.1:
dependencies: dependencies:
hasown: 2.0.2 hasown: 2.0.2
@@ -3332,8 +3076,6 @@ snapshots:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
mimic-response@3.1.0: {}
minimatch@3.0.8: minimatch@3.0.8:
dependencies: dependencies:
brace-expansion: 1.1.11 brace-expansion: 1.1.11
@@ -3346,10 +3088,6 @@ snapshots:
dependencies: dependencies:
brace-expansion: 2.0.1 brace-expansion: 2.0.1
minimist@1.2.8: {}
mkdirp-classic@0.5.3: {}
mlly@1.7.3: mlly@1.7.3:
dependencies: dependencies:
acorn: 8.14.0 acorn: 8.14.0
@@ -3365,20 +3103,10 @@ snapshots:
nanoid@3.3.8: {} nanoid@3.3.8: {}
napi-build-utils@2.0.0: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
node-abi@3.75.0:
dependencies:
semver: 7.6.3
nwsapi@2.2.16: {} nwsapi@2.2.16: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@@ -3460,41 +3188,14 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.0.4
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
node-abi: 3.75.0
pump: 3.0.2
rc: 1.2.8
simple-get: 4.0.1
tar-fs: 2.1.2
tunnel-agent: 0.6.0
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@2.8.8: {} prettier@2.8.8: {}
pump@3.0.2:
dependencies:
end-of-stream: 1.4.4
once: 1.4.0
punycode@2.3.1: {} punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.8
strip-json-comments: 2.0.1
read-yaml-file@1.1.0: read-yaml-file@1.1.0:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@@ -3502,12 +3203,6 @@ snapshots:
pify: 4.0.1 pify: 4.0.1
strip-bom: 3.0.0 strip-bom: 3.0.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
regenerator-runtime@0.14.1: {} regenerator-runtime@0.14.1: {}
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
@@ -3554,8 +3249,6 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
saxes@6.0.0: saxes@6.0.0:
@@ -3578,14 +3271,6 @@ snapshots:
signal-exit@4.1.0: {} signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
once: 1.4.0
simple-concat: 1.0.1
slash@3.0.0: {} slash@3.0.0: {}
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
@@ -3605,18 +3290,12 @@ snapshots:
string-argv@0.3.2: {} string-argv@0.3.2: {}
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1: strip-ansi@6.0.1:
dependencies: dependencies:
ansi-regex: 5.0.1 ansi-regex: 5.0.1
strip-bom@3.0.0: {} strip-bom@3.0.0: {}
strip-json-comments@2.0.1: {}
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
supports-color@7.2.0: supports-color@7.2.0:
@@ -3631,21 +3310,6 @@ snapshots:
symbol-tree@3.2.4: {} symbol-tree@3.2.4: {}
tar-fs@2.1.2:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.2
tar-stream: 2.2.0
tar-stream@2.2.0:
dependencies:
bl: 4.1.0
end-of-stream: 1.4.4
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.2
term-size@2.2.1: {} term-size@2.2.1: {}
tinybench@2.9.0: {} tinybench@2.9.0: {}
@@ -3684,10 +3348,6 @@ snapshots:
dependencies: dependencies:
typescript: 5.7.2 typescript: 5.7.2
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
type-check@0.4.0: type-check@0.4.0:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@@ -3715,8 +3375,6 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
util-deprecate@1.0.2: {}
uuid@11.0.3: {} uuid@11.0.3: {}
vite-node@2.1.8: vite-node@2.1.8:
@@ -3837,8 +3495,6 @@ snapshots:
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
wrappy@1.0.2: {}
ws@8.18.0: {} ws@8.18.0: {}
xml-name-validator@5.0.0: {} xml-name-validator@5.0.0: {}
+5
View File
@@ -0,0 +1,5 @@
packages:
- '.'
allowBuilds:
esbuild: true
+6 -6
View File
@@ -20,8 +20,8 @@
"gzip": 2400 "gzip": 2400
}, },
"index.js": { "index.js": {
"bytes": 3800, "bytes": 4900,
"gzip": 1400 "gzip": 1800
}, },
"llm.js": { "llm.js": {
"bytes": 3000, "bytes": 3000,
@@ -36,8 +36,8 @@
"gzip": 26000 "gzip": 26000
}, },
"object.js": { "object.js": {
"bytes": 4500, "bytes": 11500,
"gzip": 1800 "gzip": 3800
}, },
"promise.js": { "promise.js": {
"bytes": 1000, "bytes": 1000,
@@ -48,7 +48,7 @@
"gzip": 700 "gzip": 700
}, },
"strings.js": { "strings.js": {
"bytes": 14500, "bytes": 19500,
"gzip": 4900 "gzip": 6600
} }
} }
+90
View File
@@ -0,0 +1,90 @@
import { hasOwn, isPlainObject, ownKeys, setOwn } from './helpers';
import type { DeepReadonly } from './types';
/**
* Clone primitives, plain objects, arrays, Date, RegExp, Map, Set, ArrayBuffer,
* and buffer views. Preserve cycles and shared references. Reject other objects
* and functions. Copy enumerable own values on plain objects/arrays, not descriptors.
*/
export function deepClone<T>(value: T): T {
const seen = new WeakMap<object, any>();
function clone(input: any): any {
if (typeof input === 'function') throw new TypeError('Cannot clone functions');
if (input === null || typeof input !== 'object') return input;
if (seen.has(input)) return seen.get(input);
let result: any;
if (Array.isArray(input)) result = new Array(input.length);
else if (isPlainObject(input)) result = Object.create(Object.getPrototypeOf(input));
else if (input instanceof Date) result = new Date(input.getTime());
else if (input instanceof RegExp) {
result = new RegExp(input.source, input.flags);
result.lastIndex = input.lastIndex;
} else if (input instanceof Map) result = new Map();
else if (input instanceof Set) result = new Set();
else if (input instanceof ArrayBuffer) result = input.slice(0);
else if (ArrayBuffer.isView(input)) {
const buffer = clone(input.buffer);
result = input instanceof DataView
? new DataView(buffer, input.byteOffset, input.byteLength)
: new (input.constructor as any)(buffer, input.byteOffset, (input as any).length);
} else throw new TypeError('Unsupported object in deepClone');
seen.set(input, result);
if (input instanceof Map) input.forEach((v, k) => result.set(clone(k), clone(v)));
else if (input instanceof Set) input.forEach(v => result.add(clone(v)));
else if (Array.isArray(input) || isPlainObject(input)) {
for (const key of ownKeys(input)) setOwn(result, key, clone(input[key]));
}
return result;
}
return clone(value);
}
export interface DeepMergeOptions {
/** Replace arrays by default, or concatenate target then source. */
arrays?: 'replace' | 'concat';
}
/** Reject cycles among the containers traversed by the operation. */
export function assertAcyclic(value: unknown, arrays: boolean, ancestors = new Set<object>()): void {
if (!isPlainObject(value) && !(arrays && Array.isArray(value))) return;
if (ancestors.has(value as object)) throw new TypeError('Circular containers are not supported');
ancestors.add(value as object);
for (const key of ownKeys(value as object)) assertAcyclic(value[key], arrays, ancestors);
ancestors.delete(value as object);
}
/** Merge plain-object roots without mutating inputs; source values win. */
export function deepMerge<T extends object, U extends object>(target: T, source: U, options: DeepMergeOptions = {}): Record<PropertyKey, unknown> {
if (!isPlainObject(target) || !isPlainObject(source)) throw new TypeError('deepMerge requires plain objects');
assertAcyclic(target, true);
assertAcyclic(source, true);
function merge(left: any, right: any): any {
if (Array.isArray(left) && Array.isArray(right) && options.arrays === 'concat') return deepClone([...left, ...right]);
if (!isPlainObject(left) || !isPlainObject(right)) return deepClone(right);
const result = deepClone(left);
for (const key of ownKeys(right)) {
setOwn(result, key, hasOwn(left, key) ? merge(left[key], right[key]) : deepClone(right[key]));
}
return result;
}
return merge(target, source);
}
/** Freeze a graph of plain objects and arrays in place; cycles are supported. */
export function deepFreeze<T>(object: T): DeepReadonly<T> {
const seen = new Set<object>();
function visit(value: any): void {
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return;
if (seen.has(value)) return;
if (!Array.isArray(value) && !isPlainObject(value)) throw new TypeError('deepFreeze supports only plain objects and arrays');
seen.add(value);
for (const key of Reflect.ownKeys(value)) {
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
if (!('value' in descriptor)) throw new TypeError('deepFreeze does not support accessors');
visit(descriptor.value);
}
}
visit(object);
for (const value of seen) Object.freeze(value);
return object as DeepReadonly<T>;
}
+177
View File
@@ -0,0 +1,177 @@
import { describe, expect, it, expectTypeOf } from 'vitest';
import {
pick, omit, pickBy, omitBy, mapValues, mapKeys, isPlainObject, hasOwn, compactObject,
invert, deepClone, deepMerge, deepFreeze, flattenObject, unflattenObject, diffObjects,
} from './index';
import type { DeepPartial, DeepReadonly, DeepRequired, Mutable, KeysOfType, Prettify } from './index';
describe('object helpers', () => {
it('selects and omits own keys, including symbols and numeric keys', () => {
const symbol = Symbol('key');
const object = { a: 1, b: 2, 0: 3, [symbol]: 4 };
expect(pick(object, ['a', symbol])).toEqual({ a: 1, [symbol]: 4 });
expect(omit(object, ['b', 0])).toEqual({ a: 1, [symbol]: 4 });
expect(pick(Object.create({ a: 1 }), ['a'])).toEqual({});
expect(omit(object, [])).not.toBe(object);
expect(pickBy(object, value => value > 2)).toEqual({ 0: 3, [symbol]: 4 });
expect(omitBy(object, value => value > 2)).toEqual({ a: 1, b: 2 });
});
it('maps values and keys with last-value-wins collisions', () => {
expect(mapValues({ a: 1, b: 2 }, (value, key) => `${key}:${value}`)).toEqual({ a: 'a:1', b: 'b:2' });
expect(mapKeys({ a: 1, b: 2 }, () => 'key')).toEqual({ key: 2 });
expect(mapKeys({ a: 1 }, key => key.toUpperCase())).toEqual({ A: 1 });
});
it('handles dangerous-looking keys as ordinary own properties', () => {
const source = JSON.parse('{"__proto__":{"polluted":true},"constructor":1}');
for (const result of [pick(source, ['__proto__']), omit(source, []), mapValues(source, x => x), deepClone(source), deepMerge({}, source)]) {
expect(hasOwn(result, '__proto__')).toBe(true);
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
}
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
});
it('identifies plain objects and safely checks ownership', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject(Object.create(null))).toBe(true);
for (const value of [null, [], new Date(), new Map(), new (class Example {})(), () => 1]) {
expect(isPlainObject(value)).toBe(false);
}
expect(hasOwn(Object.assign(Object.create(null), { x: 1 }), 'x')).toBe(true);
expect(hasOwn({}, 'toString')).toBe(false);
});
it('compacts only nullish values and groups duplicate inverted values', () => {
expect(compactObject({ a: null, b: undefined, c: 0, d: false, e: '', f: [] }))
.toEqual({ c: 0, d: false, e: '', f: [] });
expect([...invert({ a: 'x', b: 'x', c: 'y' })]).toEqual([['x', ['a', 'b']], ['y', ['c']]]);
const value = {};
expect(invert({ a: value, b: value }).get(value)).toEqual(['a', 'b']);
});
it('clones cycles, shared references, sparse arrays, and null prototypes', () => {
const source: any = Object.assign(Object.create(null), { a: { n: 1 }, sparse: new Array(3) });
source.b = source.a;
source.self = source;
const clone = deepClone(source);
expect(Object.getPrototypeOf(clone)).toBeNull();
expect(clone.self).toBe(clone);
expect(clone.a).toBe(clone.b);
expect(clone.a).not.toBe(source.a);
expect(clone.sparse.length).toBe(3);
expect(0 in clone.sparse).toBe(false);
});
it('clones maps, sets, dates, regexes, and shared backing buffers', () => {
const key = { id: 1 };
const buffer = new ArrayBuffer(8);
const bytes = new Uint8Array(buffer, 2, 3);
bytes[0] = 42;
const regex = /x/gi;
regex.lastIndex = 2;
const map = new Map<any, any>();
map.set(key, map);
const source = { key, map, set: new Set([key]), date: new Date(123), regex, buffer, bytes, view: new DataView(buffer, 1, 4) };
const clone = deepClone(source);
expect(clone.map.get(clone.key)).toBe(clone.map);
expect(clone.set.has(clone.key)).toBe(true);
expect(clone.date.getTime()).toBe(123);
expect(clone.regex.source).toBe('x');
expect(clone.regex.flags).toBe('gi');
expect(clone.regex.lastIndex).toBe(2);
expect(clone.buffer).not.toBe(buffer);
expect(clone.bytes.buffer).toBe(clone.buffer);
expect(clone.view.buffer).toBe(clone.buffer);
expect(clone.bytes[0]).toBe(42);
expect(clone.bytes.byteOffset).toBe(2);
});
it('rejects unsupported clone values', () => {
for (const value of [() => 1, new WeakMap(), new (class Example {})()]) {
expect(() => deepClone(value)).toThrow(TypeError);
}
});
it('merges objects without mutation, with explicit array behavior', () => {
const left = { nested: { a: 1, b: 2 }, list: [{ x: 1 }] };
const right = { nested: { b: 3 }, list: [{ x: 2 }] };
expect(deepMerge(left, right)).toEqual({ nested: { a: 1, b: 3 }, list: [{ x: 2 }] });
expect(deepMerge(left, right, { arrays: 'concat' }).list).toEqual([{ x: 1 }, { x: 2 }]);
expect(left.list).toEqual([{ x: 1 }]);
expect(right.list).toEqual([{ x: 2 }]);
expect(deepMerge(left, right).list).not.toBe(right.list);
expect(deepMerge({ x: 1 }, { x: undefined })).toEqual({ x: undefined });
const circular: any = {};
circular.self = circular;
expect(() => deepMerge({}, circular)).toThrow(TypeError);
expect(() => deepMerge([], {})).toThrow(TypeError);
});
it('freezes cyclic graphs including non-enumerable properties', () => {
const object: any = { list: [{ x: 1 }] };
object.self = object;
Object.defineProperty(object, 'hidden', { value: { y: 2 } });
expect(deepFreeze(object)).toBe(object);
expect(Object.isFrozen(object)).toBe(true);
expect(Object.isFrozen(object.list)).toBe(true);
expect(Object.isFrozen(object.list[0])).toBe(true);
expect(Object.isFrozen(object.hidden)).toBe(true);
});
it('validates freeze inputs before mutating anything', () => {
const object = { child: {}, map: new Map() };
expect(() => deepFreeze(object)).toThrow(TypeError);
expect(Object.isFrozen(object.child)).toBe(false);
expect(() => deepFreeze({ get x() { return {}; } })).toThrow(TypeError);
});
it('roundtrips nested objects and escaped paths, preserving array leaves', () => {
const list = [1, { a: 2 }];
const object = { a: { b: 1 }, 'a/b': { '~': 2 }, '': { '': 3 }, empty: {}, list };
const flat = flattenObject(object);
expect(flat).toEqual({ '/a/b': 1, '/a~1b/~0': 2, '//': 3, '/empty': {}, '/list': list });
const restored = unflattenObject(flat);
expect(restored).toEqual(object);
expect(restored.list).toBe(list);
expect(flattenObject({})).toEqual({});
expect(unflattenObject({})).toEqual({});
expect(unflattenObject({ '/__proto__/x': 1 })).toEqual(JSON.parse('{"__proto__":{"x":1}}'));
expect(({} as Record<string, unknown>).x).toBeUndefined();
});
it('rejects malformed paths, conflicts, and recursive flattening cycles', () => {
for (const flat of [{ a: 1 }, { '/a~2': 1 }, { '/a': {}, '/a/b': 1 }, { '/a/b': 1, '/a': 2 }]) {
expect(() => unflattenObject(flat)).toThrow(TypeError);
}
const circular: any = {};
circular.self = circular;
expect(() => flattenObject(circular)).toThrow(TypeError);
});
it('reports added, removed, and changed paths with atomic array comparison', () => {
const before = { nested: { n: 1 }, removed: true, same: NaN };
const after = { nested: { n: 2 }, added: undefined, same: NaN };
expect(diffObjects(before, after)).toEqual([
{ type: 'changed', path: ['nested', 'n'], before: 1, after: 2 },
{ type: 'removed', path: ['removed'], before: true },
{ type: 'added', path: ['added'], after: undefined },
]);
const list = [1];
expect(diffObjects({ list }, { list })).toEqual([]);
expect(diffObjects({ list }, { list: [1] })[0].type).toBe('changed');
expect(diffObjects({}, {})).toEqual([]);
});
it('exposes useful mapped types', () => {
type Model = { readonly name: string; child?: { score?: number }; pair: [number, string] };
expectTypeOf<Mutable<{ readonly x: number }>>().toEqualTypeOf<{ x: number }>();
expectTypeOf<KeysOfType<Model, string>>().toEqualTypeOf<'name'>();
expectTypeOf<Prettify<{ a: number } & { b: string }>>().toEqualTypeOf<{ a: number; b: string }>();
expectTypeOf<DeepPartial<{ a: { b: number } }>>().toEqualTypeOf<{ a?: { b?: number } }>();
expectTypeOf<DeepRequired<{ a?: { b?: number } }>>().toEqualTypeOf<{ a: { b: number } }>();
expectTypeOf<DeepReadonly<[number, { x: string }]>>().toEqualTypeOf<readonly [number, { readonly x: string }]>();
expectTypeOf<DeepReadonly<Map<string, { x: number }>>>().toEqualTypeOf<ReadonlyMap<string, { readonly x: number }>>();
expectTypeOf(pick({ a: 1, b: 'b' }, ['a'])).toEqualTypeOf<{ a: number }>();
});
});
+75
View File
@@ -0,0 +1,75 @@
/** Check own properties without consulting the object's prototype. */
export function hasOwn<K extends PropertyKey>(object: object, key: K): object is object & Record<K, unknown> {
return Object.prototype.hasOwnProperty.call(object, key);
}
/** Plain objects from this realm, including objects with a null prototype. */
export function isPlainObject(value: unknown): value is Record<PropertyKey, unknown> {
if (value === null || typeof value !== 'object') return false;
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
export function ownKeys<T extends object>(object: T): (keyof T)[] {
return Reflect.ownKeys(object).filter(key => Object.prototype.propertyIsEnumerable.call(object, key)) as (keyof T)[];
}
export function setOwn(object: object, key: PropertyKey, value: unknown): void {
Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
}
/** Select own properties. Values are not cloned. */
export function pick<T extends object, const K extends readonly (keyof T)[]>(object: T, keys: K): Pick<T, K[number]> {
const result = {} as Pick<T, K[number]>;
for (const key of keys) if (hasOwn(object, key)) setOwn(result, key, object[key]);
return result;
}
/** Exclude keys from the object's own enumerable properties. */
export function omit<T extends object, const K extends readonly (keyof T)[]>(object: T, keys: K): Omit<T, K[number]> {
// JavaScript stores numeric property keys as strings.
const excluded = new Set<PropertyKey>(keys.map(key => typeof key === 'number' ? String(key) : key));
const result = {} as Omit<T, K[number]>;
for (const key of ownKeys(object)) if (!excluded.has(key)) setOwn(result, key, object[key]);
return result;
}
export function pickBy<T extends object>(object: T, predicate: (value: T[keyof T], key: keyof T) => boolean): Partial<T> {
const result: Partial<T> = {};
for (const key of ownKeys(object)) if (predicate(object[key], key)) setOwn(result, key, object[key]);
return result;
}
export function omitBy<T extends object>(object: T, predicate: (value: T[keyof T], key: keyof T) => boolean): Partial<T> {
return pickBy(object, (value, key) => !predicate(value, key));
}
export function mapValues<T extends object, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V): { [K in keyof T]: V } {
const result = {} as { [K in keyof T]: V };
for (const key of ownKeys(object)) setOwn(result, key, mapper(object[key], key));
return result;
}
/** Rename keys; the last enumerated property wins on a collision. */
export function mapKeys<T extends object, K extends PropertyKey>(object: T, mapper: (key: keyof T, value: T[keyof T]) => K): Partial<Record<K, T[keyof T]>> {
const result: Partial<Record<K, T[keyof T]>> = {};
for (const key of ownKeys(object)) setOwn(result, mapper(key, object[key]), object[key]);
return result;
}
/** Remove null and undefined values, keeping all other values. */
export function compactObject<T extends object>(object: T): Partial<T> {
return pickBy(object, value => value !== null && value !== undefined);
}
/** Group original keys by value, using Map equality and preserving key order. */
export function invert<T extends object>(object: T): Map<T[keyof T], (keyof T)[]> {
const result = new Map<T[keyof T], (keyof T)[]>();
for (const key of ownKeys(object)) {
const value = object[key];
const keys = result.get(value);
if (keys) keys.push(key);
else result.set(value, [key]);
}
return result;
}
+6
View File
@@ -3,3 +3,9 @@ export {objectCompare} from './compare'
export {createSelectOptions} from './util' export {createSelectOptions} from './util'
export {decycle,retrocycle, stringify_json} from './decycle' export {decycle,retrocycle, stringify_json} from './decycle'
export type {RefObject} from './decycle' export type {RefObject} from './decycle'
export { pick, omit, pickBy, omitBy, mapValues, mapKeys, isPlainObject, hasOwn, compactObject, invert } from './helpers';
export { deepClone, deepMerge, deepFreeze } from './deep';
export type { DeepMergeOptions } from './deep';
export { flattenObject, unflattenObject, diffObjects } from './paths';
export type { ObjectChange } from './paths';
export type { DeepPartial, DeepReadonly, DeepRequired, Mutable, KeysOfType, Prettify } from './types';
+72
View File
@@ -0,0 +1,72 @@
import { hasOwn, isPlainObject, setOwn } from './helpers';
import { assertAcyclic } from './deep';
/**
* Flatten nested plain objects into slash-prefixed paths. Escape ~ as ~0 and /
* as ~1. Arrays and other values are leaves; empty nested objects are retained.
* Only enumerable string keys are traversed. Leaf references are not cloned.
*/
export function flattenObject(object: object): Record<string, unknown> {
if (!isPlainObject(object)) throw new TypeError('flattenObject requires a plain object');
assertAcyclic(object, false);
const result: Record<string, unknown> = {};
function visit(value: unknown, path: string): void {
if (isPlainObject(value) && Object.keys(value).length) {
for (const key of Object.keys(value)) visit(value[key], path + '/' + key.replace(/~/g, '~0').replace(/\//g, '~1'));
} else setOwn(result, path, value);
}
for (const key of Object.keys(object)) visit(object[key], '/' + key.replace(/~/g, '~0').replace(/\//g, '~1'));
return result;
}
/** Reverse flattenObject. Reject malformed paths and parent/child collisions. */
export function unflattenObject(flat: Readonly<Record<string, unknown>>): Record<string, unknown> {
const result: Record<string, unknown> = {};
const paths = Object.keys(flat).map(path => {
if (!path.startsWith('/') || /~(?![01])/g.test(path)) throw new TypeError('Invalid flattened path');
return { path, keys: path.slice(1).split('/').map(key => key.replace(/~1/g, '/').replace(/~0/g, '~')) };
}).sort((a, b) => a.keys.length - b.keys.length);
// Track leaf locations separately, including scalar leaves and empty objects.
const assigned = new Map<object, Set<string>>();
for (const { path, keys } of paths) {
let current = result;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (assigned.get(current)?.has(key)) throw new TypeError('Conflicting flattened paths');
if (i === keys.length - 1) {
if (hasOwn(current, key)) throw new TypeError('Conflicting flattened paths');
setOwn(current, key, flat[path]);
if (!assigned.has(current)) assigned.set(current, new Set());
assigned.get(current)!.add(key);
} else {
if (!hasOwn(current, key)) setOwn(current, key, {});
current = current[key] as Record<string, unknown>;
}
}
}
return result;
}
export type ObjectChange =
| { type: 'added'; path: string[]; after: unknown }
| { type: 'removed'; path: string[]; before: unknown }
| { type: 'changed'; path: string[]; before: unknown; after: unknown };
/** Compare nested plain objects. Other values (including arrays) use Object.is. */
export function diffObjects(before: object, after: object): ObjectChange[] {
if (!isPlainObject(before) || !isPlainObject(after)) throw new TypeError('diffObjects requires plain objects');
assertAcyclic(before, false);
assertAcyclic(after, false);
const changes: ObjectChange[] = [];
function visit(left: Record<string, unknown>, right: Record<string, unknown>, path: string[]): void {
for (const key of new Set([...Object.keys(left), ...Object.keys(right)])) {
const next = [...path, key];
if (!hasOwn(right, key)) changes.push({ type: 'removed', path: next, before: left[key] });
else if (!hasOwn(left, key)) changes.push({ type: 'added', path: next, after: right[key] });
else if (isPlainObject(left[key]) && isPlainObject(right[key])) visit(left[key], right[key], next);
else if (!Object.is(left[key], right[key])) changes.push({ type: 'changed', path: next, before: left[key], after: right[key] });
}
}
visit(before, after, []);
return changes;
}
+24
View File
@@ -0,0 +1,24 @@
/** Make every nested property optional; preserve callable and built-in values. */
export type DeepPartial<T> = T extends (...args: any[]) => any ? T
: T extends Date | RegExp ? T
: T extends ReadonlyMap<infer K, infer V> ? Map<K, DeepPartial<V>>
: T extends ReadonlySet<infer V> ? Set<DeepPartial<V>>
: T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
/** Recursively readonly properties, arrays, maps, and sets. */
export type DeepReadonly<T> = T extends (...args: any[]) => any ? T
: T extends Date | RegExp ? T
: T extends ReadonlyMap<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends ReadonlySet<infer V> ? ReadonlySet<DeepReadonly<V>>
: T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
/** Make nested properties required; does not perform runtime validation. */
export type DeepRequired<T> = T extends (...args: any[]) => any ? T
: T extends Date | RegExp ? T
: T extends ReadonlyMap<infer K, infer V> ? Map<K, DeepRequired<V>>
: T extends ReadonlySet<infer V> ? Set<DeepRequired<V>>
: T extends object ? { [K in keyof T]-?: DeepRequired<T[K]> } : T;
export type Mutable<T> = { -readonly [K in keyof T]: T[K] };
export type KeysOfType<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
+2
View File
@@ -8,3 +8,5 @@ export * from "./uuid";
export * from "./time"; export * from "./time";
export * from "./blankValue"; export * from "./blankValue";
export * from "./baseNumber"; export * from "./baseNumber";
export * from "./text";
+185
View File
@@ -0,0 +1,185 @@
import { describe, expect, it } from 'vitest';
import {
truncate, truncateWords, slugify, escapeRegExp, normalizeWhitespace, stripDiacritics,
includesIgnoreCase, interpolate, mask, splitOnce, isBlank, toSearchKey, humanize,
getInitials, ensurePrefix, ensureSuffix, removePrefix, removeSuffix, substringBefore,
substringAfter, countOccurrences, commonPrefix, wrapText, dedent, normalizeLineEndings,
utf8ByteLength, compareNatural,
} from './index';
describe('text helpers', () => {
it('truncates within a total grapheme budget including the suffix', () => {
expect(truncate('hello', 4)).toBe('hel…');
expect(truncate('hello', 2, '...')).toBe('..');
expect(truncate('hello', 0)).toBe('');
expect(truncate('hello', 5)).toBe('hello');
expect(truncate('👨‍👩‍👧‍👦e\u0301xy', 3)).toBe('👨‍👩‍👧‍👦e\u0301…');
expect(truncate('😀ab', 2, '')).toBe('😀a');
});
it('falls back to code points when Segmenter is unavailable', () => {
const original = Intl.Segmenter;
try {
Object.defineProperty(Intl, 'Segmenter', { value: undefined, configurable: true });
expect(truncate('😀ab', 2)).toBe('😀…');
} finally {
Object.defineProperty(Intl, 'Segmenter', { value: original, configurable: true });
}
});
it('truncates whitespace-separated words and preserves unchanged input', () => {
expect(truncateWords(' one two\nthree ', 2)).toBe('one two…');
expect(truncateWords(' one two ', 2)).toBe(' one two ');
expect(truncateWords('one two', 1, '...')).toBe('one...');
expect(truncateWords('one', 0)).toBe('');
expect(truncateWords('', 2)).toBe('');
});
it.each([-1, 1.5, NaN, Infinity])('rejects invalid limits: %s', value => {
expect(() => truncate('text', value)).toThrow(RangeError);
expect(() => truncateWords('text', value)).toThrow(RangeError);
expect(() => getInitials('Ada', value)).toThrow(RangeError);
expect(() => mask('text', { start: value })).toThrow(RangeError);
expect(() => mask('text', { end: value })).toThrow(RangeError);
expect(() => wrapText('text', value)).toThrow(RangeError);
});
it('normalizes whitespace and decomposable diacritics', () => {
expect(normalizeWhitespace(' \tCafé\u00a0 au\n lait ')).toBe('Café au lait');
expect(stripDiacritics('Crème Brûlée e\u0301')).toBe('Creme Brulee e');
expect(stripDiacritics('東京 ø')).toBe('東京 ø');
expect(toSearchKey(' CAFÉ\t au LAIT ')).toBe('cafe au lait');
expect(toSearchKey('I İ', 'tr')).toBe('ı i');
});
it('creates Unicode slugs without leading or trailing separators', () => {
expect(slugify(' Héllo, WORLD! ')).toBe('hello-world');
expect(slugify('東京 & 大阪')).toBe('東京-大阪');
expect(slugify('---')).toBe('');
});
it('escapes literal regex metacharacters', () => {
const text = 'a.*+?^${}()|[]\\/z';
expect(new RegExp(`^${escapeRegExp(text)}$`).test(text)).toBe(true);
expect(escapeRegExp('')).toBe('');
});
it('matches case-insensitively with optional locale', () => {
expect(includesIgnoreCase('Hello WORLD', 'world')).toBe(true);
expect(includesIgnoreCase('ISTANBUL', 'ıstan', 'tr')).toBe(true);
expect(includesIgnoreCase('café', 'cafe')).toBe(false);
expect(includesIgnoreCase('abc', '')).toBe(true);
expect(includesIgnoreCase('', 'a')).toBe(false);
});
it.each([null, undefined, '', ' \t\r\n', '\u00a0'])('recognizes blank text: %s', value => {
expect(isBlank(value)).toBe(true);
});
it('does not consider zero or invisible non-whitespace text blank', () => {
expect(isBlank('0')).toBe(false);
expect(isBlank('\u200b')).toBe(false);
});
it('humanizes identifiers and extracts name initials', () => {
expect(humanize('customer_id')).toBe('Customer id');
expect(humanize('HTTPResponse-code')).toBe('Http response code');
expect(humanize('')).toBe('');
expect(getInitials(' Ada Lovelace Byron ')).toBe('AL');
expect(getInitials('Ada Lovelace Byron', 3)).toBe('ALB');
expect(getInitials('e\u0301mile 王')).toBe('E\u0301王');
expect(getInitials('Ada', 0)).toBe('');
expect(getInitials(' ')).toBe('');
});
it('interpolates own literal values once and preserves missing placeholders', () => {
const values = Object.assign(Object.create({ inherited: 'no' }), {
name: '$&{other}', count: 0, enabled: false, empty: null,
});
expect(interpolate('{name}: {count} {enabled} {empty} {missing} {inherited}', values))
.toBe('$&{other}: 0 false {missing} {inherited}');
expect(interpolate('{constructor} {__proto__}', {})).toBe('{constructor} {__proto__}');
});
it('masks character ranges without breaking emoji', () => {
expect(mask('123456789')).toBe('*****6789');
expect(mask('123456789', { start: 2, end: 2, character: '#' })).toBe('12#####89');
expect(mask('😀👨‍👩‍👧‍👦ab', { end: 1 })).toBe('***b');
expect(mask('abc', { end: 0 })).toBe('***');
expect(mask('abc', { start: 2, end: 2 })).toBe('abc');
expect(mask('')).toBe('');
expect(() => mask('a', { character: '' })).toThrow(RangeError);
expect(() => mask('a', { character: '**' })).toThrow(RangeError);
});
it('adds and removes exact affixes once', () => {
expect(ensurePrefix('path', '/')).toBe('/path');
expect(ensurePrefix('/path', '/')).toBe('/path');
expect(ensureSuffix('file', '.txt')).toBe('file.txt');
expect(ensureSuffix('file.txt', '.txt')).toBe('file.txt');
expect(removePrefix('preprefix', 'pre')).toBe('prefix');
expect(removeSuffix('name.txt.txt', '.txt')).toBe('name.txt');
for (const fn of [ensurePrefix, ensureSuffix, removePrefix, removeSuffix]) {
expect(fn('text', '')).toBe('text');
}
expect(removePrefix('text', 'other')).toBe('text');
expect(removeSuffix('text', 'other')).toBe('text');
});
it('splits only on the first literal delimiter', () => {
expect(splitOnce('a::b::c', '::')).toEqual(['a', 'b::c']);
expect(splitOnce('abc', ':')).toEqual(['abc', '']);
expect(splitOnce('abc', '')).toEqual(['', 'abc']);
expect(splitOnce('', ':')).toEqual(['', '']);
expect(substringBefore('a:b:c', ':')).toBe('a');
expect(substringAfter('a:b:c', ':')).toBe('b:c');
expect(substringBefore('abc', ':')).toBe('abc');
expect(substringAfter('abc', ':')).toBe('');
});
it('counts literal matches with optional overlaps', () => {
expect(countOccurrences('aaaa', 'aa')).toBe(2);
expect(countOccurrences('aaaa', 'aa', true)).toBe(3);
expect(countOccurrences('a.b.c', '.')).toBe(2);
expect(countOccurrences('abc', '')).toBe(0);
expect(countOccurrences('', 'a')).toBe(0);
expect(countOccurrences('😀😀', '😀')).toBe(2);
});
it('finds common prefixes without partial graphemes', () => {
expect(commonPrefix(['customer_id', 'customer_name'])).toBe('customer_');
expect(commonPrefix(['😀a', '😀b'])).toBe('😀');
expect(commonPrefix(['👨‍👩‍👧', '👨‍👩‍👦'])).toBe('');
expect(commonPrefix(['abc', 'abd', 'a'])).toBe('a');
expect(commonPrefix(['abc', ''])).toBe('');
expect(commonPrefix([])).toBe('');
expect(commonPrefix(['abc'])).toBe('abc');
});
it('wraps at word boundaries while preserving explicit blank lines', () => {
expect(wrapText('one two three', 7)).toBe('one two\nthree');
expect(wrapText('toolong word', 3)).toBe('toolong\nword');
expect(wrapText('one\r\n\r\ntwo', 10)).toBe('one\n\ntwo');
expect(wrapText('😀 😀 😀', 3)).toBe('😀 😀\n😀');
expect(wrapText('', 1)).toBe('');
expect(() => wrapText('a', 0)).toThrow(RangeError);
});
it('dedents shared literal indentation and normalizes line endings', () => {
expect(dedent('\n one\r\n two\r\n three\n')).toBe('one\n two\nthree');
expect(dedent('\tfirst\n\t\tsecond')).toBe('first\n\tsecond');
expect(dedent(' first\n\n second')).toBe('first\n\nsecond');
expect(dedent(' one\n\ttwo')).toBe(' one\n\ttwo');
expect(dedent(' \n\t')).toBe('');
expect(normalizeLineEndings('a\r\nb\rc\n')).toBe('a\nb\nc\n');
});
it.each([['', 0], ['abc', 3], ['é', 2], ['雪', 3], ['😀', 4], ['\ud800', 3]] as const)(
'counts UTF-8 bytes for %j', (text, bytes) => expect(utf8ByteLength(text)).toBe(bytes),
);
it('compares natural numeric strings with empty strings last', () => {
expect(['file10', '', 'file2', 'file1'].sort(compareNatural)).toEqual(['file1', 'file2', 'file10', '']);
expect(compareNatural('same', 'same')).toBe(0);
});
});
+227
View File
@@ -0,0 +1,227 @@
import { splitCamelCase } from './caseConversion';
import { createComparator } from '../sorting';
function limit(value: number, name: string, minimum = 0): void {
if (!Number.isSafeInteger(value) || value < minimum) {
throw new RangeError(`${name} must be a safe integer >= ${minimum}`);
}
}
// Older runtimes without Segmenter fall back to Unicode code points.
function characters(text: string): string[] {
if (typeof Intl.Segmenter === 'function') {
return Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(text), part => part.segment);
}
return Array.from(text);
}
/** Shorten to maxLength characters, including the suffix (default …). */
export function truncate(text: string, maxLength: number, suffix = '…'): string {
limit(maxLength, 'maxLength');
const parts = characters(text);
if (parts.length <= maxLength) return text;
const ending = characters(suffix).slice(0, maxLength);
return parts.slice(0, maxLength - ending.length).join('') + ending.join('');
}
/** Keep at most maxWords whitespace-delimited words; preserve text if it fits. */
export function truncateWords(text: string, maxWords: number, suffix = '…'): string {
limit(maxWords, 'maxWords');
if (maxWords === 0) return '';
const words = text.match(/\S+/g) ?? [];
return words.length <= maxWords ? text : words.slice(0, maxWords).join(' ') + suffix;
}
/** Collapse Unicode whitespace to single spaces and trim. */
export function normalizeWhitespace(text: string): string {
return text.replace(/\s+/g, ' ').trim();
}
/** Remove decomposable accents and combining marks; not a transliterator. */
export function stripDiacritics(text: string): string {
return text.normalize('NFD').replace(new RegExp('\\p{M}+', 'gu'), '').normalize('NFC');
}
/** Lowercase Unicode slug, retaining letters and numbers from any script. */
export function slugify(text: string): string {
return stripDiacritics(text).toLowerCase()
.replace(new RegExp('[^\\p{L}\\p{N}]+', 'gu'), '-').replace(/^-+|-+$/g, '');
}
/** Escape regex metacharacters for a literal pattern (not a replacement string). */
export function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/** Locale-aware lowercasing followed by literal substring matching. */
export function includesIgnoreCase(text: string, query: string, locale?: string | string[]): boolean {
return text.toLocaleLowerCase(locale).includes(query.toLocaleLowerCase(locale));
}
/** Null, undefined, empty, or whitespace-only text. */
export function isBlank(text: string | null | undefined): boolean {
return text === null || text === undefined || text.trim().length === 0;
}
/** Normalize case, accents, and whitespace for search indexing. */
export function toSearchKey(text: string, locale?: string | string[]): string {
return normalizeWhitespace(stripDiacritics(text.toLocaleLowerCase(locale)));
}
/** Convert snake_case, kebab-case, and camelCase to a sentence-case label. */
export function humanize(text: string): string {
const label = normalizeWhitespace(splitCamelCase(text).replace(/[_-]+/g, ' ')).toLowerCase();
const parts = characters(label);
return parts.length ? parts[0].toUpperCase() + parts.slice(1).join('') : '';
}
/** Initials of whitespace-delimited names; defaults to the first two words. */
export function getInitials(name: string, max = 2): string {
limit(max, 'max');
return (name.match(/\S+/g) ?? []).slice(0, max)
.map(word => characters(word)[0].toUpperCase()).join('');
}
/** Replace {name} placeholders with own properties. Missing keys stay intact. */
export function interpolate(
template: string,
values: Readonly<Record<string, string | number | boolean | null | undefined>>,
): string {
return template.replace(/\{([^{}]+)\}/g, (placeholder, key: string) =>
Object.prototype.hasOwnProperty.call(values, key) ? String(values[key] ?? '') : placeholder);
}
export interface MaskOptions {
/** Number of visible characters at the beginning. Defaults to zero. */
start?: number;
/** Number of visible characters at the end. Defaults to four. */
end?: number;
/** One character per hidden character. Defaults to *. */
character?: string;
}
/** Mask a display value; this does not encrypt or remove the original value. */
export function mask(text: string, options: MaskOptions = {}): string {
const { start = 0, end = 4, character = '*' } = options;
limit(start, 'start');
limit(end, 'end');
if (characters(character).length !== 1) throw new RangeError('character must be one character');
const parts = characters(text);
return parts.map((part, i) => i < start || i >= parts.length - end ? part : character).join('');
}
export function ensurePrefix(text: string, prefix: string): string {
return text.startsWith(prefix) ? text : prefix + text;
}
export function ensureSuffix(text: string, suffix: string): string {
return text.endsWith(suffix) ? text : text + suffix;
}
export function removePrefix(text: string, prefix: string): string {
return text.startsWith(prefix) ? text.slice(prefix.length) : text;
}
export function removeSuffix(text: string, suffix: string): string {
return suffix && text.endsWith(suffix) ? text.slice(0, -suffix.length) : text;
}
/** Split at the first literal separator. Missing separator returns [text, '']. */
export function splitOnce(text: string, separator: string): [string, string] {
const index = text.indexOf(separator);
return index < 0 ? [text, ''] : [text.slice(0, index), text.slice(index + separator.length)];
}
/** Missing separator returns the original text; empty separator returns ''. */
export function substringBefore(text: string, separator: string): string {
return splitOnce(text, separator)[0];
}
/** Missing separator returns ''; empty separator returns the original text. */
export function substringAfter(text: string, separator: string): string {
return splitOnce(text, separator)[1];
}
/** Count literal occurrences. Empty search returns zero. */
export function countOccurrences(text: string, search: string, overlap = false): number {
if (!search) return 0;
let count = 0;
let offset = 0;
while ((offset = text.indexOf(search, offset)) !== -1) {
count++;
offset += overlap ? 1 : search.length;
}
return count;
}
/** Longest common prefix, respecting character boundaries. */
export function commonPrefix(values: readonly string[]): string {
if (!values.length) return '';
const prefix = characters(values[0]);
let length = prefix.length;
for (const value of values.slice(1)) {
const parts = characters(value);
length = Math.min(length, parts.length);
let i = 0;
while (i < length && prefix[i] === parts[i]) i++;
length = i;
if (!length) break;
}
return prefix.slice(0, length).join('');
}
/** Convert Windows and old Mac line endings to LF. */
export function normalizeLineEndings(text: string): string {
return text.replace(/\r\n?/g, '\n');
}
/** Wrap on whitespace, preserving explicit line breaks and leaving long words intact. */
export function wrapText(text: string, width: number): string {
limit(width, 'width', 1);
return normalizeLineEndings(text).split('\n').map(line => {
const words = line.match(/\S+/g) ?? [];
const lines: string[] = [];
let current = '';
let length = 0;
for (const word of words) {
const size = characters(word).length;
if (current && length + 1 + size > width) {
lines.push(current);
current = '';
length = 0;
}
if (current) { current += ' '; length++; }
current += word;
length += size;
}
lines.push(current);
return lines.join('\n');
}).join('\n');
}
/** Remove shared spaces/tabs from nonblank lines; discard outer blank lines. */
export function dedent(text: string): string {
const lines = normalizeLineEndings(text).split('\n');
while (lines.length && isBlank(lines[0])) lines.shift();
while (lines.length && isBlank(lines[lines.length - 1])) lines.pop();
const indents = lines.filter(line => !isBlank(line)).map(line => line.match(/^[\t ]*/)[0]);
const indent = commonPrefix(indents);
return lines.map(line => isBlank(line) ? '' : removePrefix(line, indent)).join('\n');
}
/** UTF-8 byte count, treating lone surrogates as replacement characters. */
export function utf8ByteLength(text: string): number {
let bytes = 0;
for (const point of text) {
const code = point.codePointAt(0)!;
bytes += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
}
return bytes;
}
const naturalComparator = createComparator({ collator: { numeric: true } });
/** Natural order with the runtime locale and empty strings last. */
export function compareNatural(a: string, b: string): number {
return naturalComparator(a, b);
}