Skip to content

Commit

Permalink
Merge branch 'main' into feat/cloneDeep
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson authored Jul 12, 2024
2 parents 2018e9a + 2231c0e commit de2f7a5
Show file tree
Hide file tree
Showing 142 changed files with 1,773 additions and 98 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
PR_TITLE: '${{ github.event.pull_request.title }}'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
Expand All @@ -43,6 +45,8 @@ jobs:
run: echo "$PR_TITLE" | pnpm -s dlx [email protected]
- name: Refresh Bundle Impact
uses: actions/github-script@v6
env:
TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
Expand Down
11 changes: 11 additions & 0 deletions benchmarks/object/traverse.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as _ from 'radashi'
import { bench } from 'vitest'

describe('traverse', () => {
const root = {
a: { b: { c: { d: { e: 1 } } } },
}
bench('basic traversal', () => {
_.traverse(root, () => {})
})
})
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
244 changes: 244 additions & 0 deletions docs/object/traverse.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
---
title: traverse
description: Deeply enumerate an object and any nested objects
---

### Usage

Recursively visit each property of an object (or each element of an array) and its nested objects or arrays. To traverse non-array iterables (e.g. `Map`, `Set`) and class instances, see the [Traversing other objects](#traversing-other-objects) section.

Traversal is performed in a depth-first manner. That means the deepest object will be visited before the last property of the root object.

```ts
import * as _ from 'radashi'

const root = { a: { b: 2 }, c: [1, 2] }

_.traverse(root, (value, key, parent, context) => {
const depth = context.parents.length
console.log(' '.repeat(depth * 2), key, '=>', value)
})
// Logs the following:
// a => { b: 2 }
// b => 2
// c => [1, 2]
// 0 => 1
// 1 => 2
```

**Tip:** Check out the [Advanced](#advanced) section to see what else is possible.

:::tip[Did you know?]

- Sparse arrays don't have their holes visited.
- Circular references are skipped.

:::

## Types

### TraverseVisitor

The `TraverseVisitor` type represents the function passed to `traverse` as its 2nd argument. If you ever need to declare a visitor separate from a `traverse` call, you can do so by declaring a function with this type signature.

```ts
import { TraverseVisitor } from 'radashi'

const visitor: TraverseVisitor = (value, key, parent, context) => {
// ...
}
```

### TraverseContext

Every visit includes a context object typed with `TraverseContext`, which contains the following properties:

- `key`: The current key being visited.
- `parent`: The parent object of the current value.
- `parents`: An array of objects (from parent to child) that the current value is contained by.
- `path`: An array describing the key path to the current value from the root.
- `skip`: A function used for skipping traversal of an object. If no object is provided, the current value is skipped. See [Skipping objects](#skipping-objects) for more details.
- `skipped`: A set of objects that have been skipped.
- `value`: The current value being visited.

:::danger

The `path` and `parents` arrays are mutated by the `traverse` function. If you need to use them after the current visit, you should make a copy.

:::

### TraverseOptions

You may set these options for `traverse` using an object as its 3rd argument.

- `ownKeys`: A function that returns the own enumerable property names of an object.
- `rootNeedsVisit`: A boolean indicating whether the root object should be visited.

See the [Options](#options) section for more details.

## Options

### Traversing all properties

By default, non-enumerable properties and symbol properties are skipped. You can pass in a custom `ownKeys` implementation to control which object properties are visited.

This example shows how `Reflect.ownKeys` can be used to include non-enumerable properties and symbol properties. Note that symbol properties are always traversed last when using `Reflect.ownKeys`.

```ts
import * as _ from 'radashi'

const symbol = Symbol('b')
const root = { [symbol]: 1 }
Object.defineProperty(root, 'a', { value: 2, enumerable: false })

_.traverse(
root,
(value, key) => {
console.log(key, '=>', value)
},
{ ownKeys: Reflect.ownKeys },
)
// Logs the following:
// a => 2
// Symbol(b) => 1
```

### Visiting the root object

By default, your `visitor` callback will never receive the object passed into `traverse`. To override this behavior, set the `rootNeedsVisit` option to true.

When the root object is visited, the `key` will be `null`.

```ts
import * as _ from 'radashi'

const root = { a: 1 }

_.traverse(
root,
(value, key) => {
console.log(key, '=>', value)
},
{ rootNeedsVisit: true },
)
// Logs the following:
// null => { a: 1 }
// a => 1
```

## Advanced

### Traversing other objects

If traversing plain objects and arrays isn't enough, try calling `traverse` from within another `traverse` callback like follows. This takes advantage of the fact that the root object is always traversed.

```ts
import * as _ from 'radashi'

// Note how we're using a named visitor function so it can reference itself.
_.traverse(root, function visitor(value, key, parent, context, options) {
if (value instanceof MyClass) {
return _.traverse(value, visitor, options, context)
}
// TODO: Handle other values as needed.
})
```

If you didn't set any options, the `options` argument can be null:

```ts
return _.traverse(root, visitor, null, context)
```

### Skipping objects

Using the `TraverseContext::skip` method, you can prevent an object from being traversed. By calling `skip()` with no arguments, the current value won't be traversed.

```ts
import * as _ from 'radashi'

const root = {
a: { b: 1 },
c: { d: 2 },
}

_.traverse(root, (value, key, parent, context) => {
console.log(key, '=>', value)

// Skip traversal of the 'a' object.
if (key === 'a') {
context.skip()
}
})
// Logs the following:
// a => { b: 1 }
// c => { d: 2 }
// d => 2
```

You can pass any object to `skip()` to skip traversal of that object.

```ts
import * as _ from 'radashi'

const root = {
a: {
b: {
c: 1,
},
},
}

_.traverse(root, (value, key, parent, context) => {
console.log(key, '=>', value)

// Visit the properties of the current object, but skip any objects nested within.
Object.values(value).forEach(nestedValue => {
if (_.isObject(nestedValue)) {
context.skip(nestedValue)
}
})
})
// Logs the following:
// a => { b: { c: 1 } }
// b => { c: 1 }
```

### Exiting early

If your `visitor` callback returns false, `traverse` will exit early and also return false. This is useful if you found what you wanted, so you don't need to traverse the rest of the objects.

```ts
let found = null
_.traverse(root, value => {
if (isWhatImLookingFor(value)) {
found = value
return false
}
})
```

### Leave callbacks

If your `visitor` callback returns a function, it will be called once `traverse` has visited every visitable property/element within the current object. This is known as a “leave callback”.

Your leave callback can return false to exit traversal early.

```ts
_.traverse({ arr: ['a', 'b'] }, (value, key) => {
if (isArray(value)) {
console.log('start of array')
return () => {
console.log('end of array')
return false
}
} else {
console.log(key, '=>', value)
}
})
// Logs the following:
// start of array
// 0 => 'a'
// 1 => 'b'
// end of array
```
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion scripts/add-function.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fi

if [ ! -f "$SRC_FILE" ]; then
mkdir -p "$SRC_DIR"
echo -e "export function $FUNC_NAME(): void {}\n" > "$SRC_FILE"
echo -e "/**\n * Does a thing.\n *\n * @see https://radashi-org.github.io/reference/$GROUP_NAME/$FUNC_NAME\n * @example\n * \`\`\`ts\n * $FUNC_NAME()\n * \`\`\`\n */\nexport function $FUNC_NAME(): void {}\n" > "$SRC_FILE"
else
echo "Warning: $SRC_FILE already exists. Skipping."
fi
Expand Down
2 changes: 1 addition & 1 deletion scripts/pr-bundle-impact.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check
const { execSync } = require('child_process')

export async function run({ github, core, context }, exec = execSync) {
exports.run = async function run({ github, core, context }, exec = execSync) {
try {
// 1. Run `pnpm bundle-impact` to get the bundle impact
const bundleImpact = exec('pnpm -s bundle-impact').toString().trim()
Expand Down
21 changes: 12 additions & 9 deletions scripts/weigh-changed.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
set -e

# Try using gh to get the target branch, otherwise use 'main' as a fallback.
TARGET_BRANCH=$(which gh > /dev/null 2>&1 && gh pr view --json baseRefName --jq .baseRefName || echo "")
TARGET_BRANCH=${TARGET_BRANCH:-main}
if [[ -z "$TARGET_BRANCH" ]]; then
TARGET_BRANCH=$(which gh > /dev/null 2>&1 && gh pr view --json baseRefName --jq .baseRefName 2> /dev/null || echo "")
TARGET_BRANCH=${TARGET_BRANCH:-main}
fi

# Get the list of changed source files relative to the target branch.
CHANGES=$(git diff --name-status "$TARGET_BRANCH" HEAD -- 'src/*/*.ts')
CHANGES=$(git diff --name-status "origin/$TARGET_BRANCH" HEAD -- 'src/*/*.ts')

# Separate the file statuses and file names.
FILE_STATUSES=()
Expand Down Expand Up @@ -42,6 +44,7 @@ PREV_SIZES=()

# Collect previous sizes if there are no uncommitted changes.
if [ -z "$(git status -s)" ]; then
echo "Checking out $TARGET_BRANCH..."
git checkout "$TARGET_BRANCH" &> /dev/null

i=0
Expand All @@ -67,11 +70,11 @@ fi

if [[ -n "$CI" ]]; then
if [ "$column_count" -gt 2 ]; then
echo "| File | Size | Difference (%) |"
echo "|---|---|---|"
echo "| Status | File | Size | Difference (%) |"
echo "|---|---|---|---|"
else
echo "| File | Size |"
echo "|---|---|"
echo "| Status | File | Size |"
echo "|---|---|---|"
fi
fi

Expand Down Expand Up @@ -102,9 +105,9 @@ for file in "${FILE_NAMES[@]}"; do

if [[ -n "$CI" ]]; then
if [ "$column_count" -gt 2 ]; then
echo "| $file | $bytes | $diff$ratio |"
echo "| $status | \`$file\` | $bytes | $diff$ratio |"
else
echo "| $file | $bytes |"
echo "| $status | \`$file\` | $bytes |"
fi
else
if [ "$column_count" -gt 2 ] && [ "$prev_bytes" -ne 0 ]; then
Expand Down
2 changes: 2 additions & 0 deletions src/array/alphabetical.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Sort an array without modifying it and return the newly sorted
* value. Allows for a string sorting value.
*
* @see https://radashi-org.github.io/reference/array/alphabetical
*/
export function alphabetical<T>(
array: readonly T[],
Expand Down
4 changes: 3 additions & 1 deletion src/array/boil.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/**
* Go through a list of items, starting with the first item, and
* comparing with the second. Keep the one you want then compare that
* to the next item in the list with the same
* to the next item in the list with the same.
*
* @see https://radashi-org.github.io/reference/array/boil
* @example
* ```ts
* boil([1, 2, 3, 0], (a, b) => a > b ? a : b) // 3
* ```
Expand Down
2 changes: 2 additions & 0 deletions src/array/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Splits a single list into many lists of the desired size.
*
* @see https://radashi-org.github.io/reference/array/cluster
* @example
* ```ts
* cluster([1, 2, 3, 4, 5, 6], 2)
* // [[1, 2], [3, 4], [5, 6]]
Expand Down
2 changes: 2 additions & 0 deletions src/array/counting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Counts the occurrences of each unique value returned by the `identity`
* function when applied to each item in the array.
*
* @see https://radashi-org.github.io/reference/array/counting
* @example
* ```ts
* counting([1, 2, 3, 4], (n) => n % 2 === 0 ? 'even' : 'odd')
* // { even: 2, odd: 2 }
Expand Down
2 changes: 2 additions & 0 deletions src/array/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Returns all items from the first list that do not exist in the
* second list.
*
* @see https://radashi-org.github.io/reference/array/diff
* @example
* ```ts
* diff([1, 2, 3, 4], [2, 4])
* // [1, 3]
Expand Down
Loading

0 comments on commit de2f7a5

Please sign in to comment.