diff --git a/.cliffignore b/.cliffignore index 3e671373..a1815f5d 100644 --- a/.cliffignore +++ b/.cliffignore @@ -1,2 +1,3 @@ 9e4933d7382649f3c3aba8e3ab3eac3d1bb9c735 -069c986ac3649b060d926ec5f8447f0ac6f568b2 \ No newline at end of file +069c986ac3649b060d926ec5f8447f0ac6f568b2 +24307efdb7a84fa0b17df1b71d1d19bbc681eb25 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..a9f9af6c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Radashi", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bookworm", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers-contrib/features/pnpm": { + "version": "9.1.3" + }, + "ghcr.io/joshuanianji/devcontainer-features/mount-pnpm-store": {} + }, + "postCreateCommand": "pnpm i", + "customizations": { + "vscode": { + "extensions": [ + "vitest.explorer", + "biomejs.biome", + "esbenp.prettier-vscode" + ] + } + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7ef377e3..0d28faeb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -31,9 +31,3 @@ Yes No - -## Bundle impact - - - -_Calculating..._ diff --git a/.github/workflows/bundle-impact.yml b/.github/workflows/bundle-impact.yml new file mode 100644 index 00000000..0bd090df --- /dev/null +++ b/.github/workflows/bundle-impact.yml @@ -0,0 +1,34 @@ +name: Bundle Impact + +on: + pull_request_target: + branches: [main, next] + types: [opened, synchronize] + +jobs: + bundle-impact: + name: Calculate Bundle Impact + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: pnpm + + - run: | + pnpm install -C scripts/bundle-impact + + - name: Run Bundle Impact + uses: actions/github-script@v7 + env: + TARGET_BRANCH: ${{ github.base_ref }} + NODE_OPTIONS: '--import ./scripts/bundle-impact/node_modules/tsx/dist/esm/index.mjs' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = await import('${{ github.workspace }}/scripts/bundle-impact/pr-bundle-impact.ts') + await script.run({ github, core, context }) diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml index 02b54158..80a7f131 100644 --- a/.github/workflows/check-pr.yml +++ b/.github/workflows/check-pr.yml @@ -6,23 +6,6 @@ on: types: [opened, synchronize, reopened, edited] jobs: - test: - name: Test - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x, 20.x, 22.x] - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: pnpm - - run: pnpm install - - run: pnpm test - validate: name: Validate runs-on: ubuntu-latest @@ -40,18 +23,6 @@ jobs: - run: pnpm install - run: pnpm install @commitlint/config-conventional@19.x - - name: Refresh Bundle Impact - uses: actions/github-script@v6 - if: ${{ github.head_ref != 'next' }} - continue-on-error: true - env: - TARGET_BRANCH: ${{ github.event.pull_request.base.ref }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { run } = require('./scripts/pr-bundle-impact.cjs'); - run({ github, core, context }); - - name: Lint run: pnpm lint diff --git a/.github/workflows/publish-pr.yml b/.github/workflows/publish-pr.yml index a5e2c2b0..0a5a9cdb 100644 --- a/.github/workflows/publish-pr.yml +++ b/.github/workflows/publish-pr.yml @@ -19,12 +19,14 @@ jobs: [Your preview release is being published.](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) :hourglass: token: ${{ secrets.GITHUB_TOKEN }} - - uses: xt0rted/pull-request-comment-branch@v2 + - uses: radashi-org/pull-request-comment-branch@v2 id: comment-branch + - uses: actions/checkout@v4 if: success() with: ref: ${{ steps.comment-branch.outputs.head_ref }} + repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }} - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 diff --git a/.github/workflows/register-pr.yml b/.github/workflows/register-pr.yml new file mode 100644 index 00000000..29c88209 --- /dev/null +++ b/.github/workflows/register-pr.yml @@ -0,0 +1,26 @@ +name: Register PR In Database + +on: + pull_request_target: + branches: [main] + types: [opened, reopened, synchronize, closed] + +jobs: + register-pr: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + cache: pnpm + - uses: actions/github-script@v6 + env: + ALGOLIA_KEY: ${{ secrets.ALGOLIA_KEY }} + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = await import('${{ github.workspace }}/scripts/radashi-db/ci-register-pr.ts') + await script.run(context, github, console) diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml new file mode 100644 index 00000000..4e316da4 --- /dev/null +++ b/.github/workflows/test-pr.yml @@ -0,0 +1,24 @@ +name: Test Pull Request + +on: + pull_request: + branches: [main, next] + types: [opened, synchronize] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + - run: pnpm install + - run: pnpm test diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 575b3d21..982e18f7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,7 @@ { "recommendations": [ "vitest.explorer", - "rohit-gohri.format-code-action", - "biomejs.biome" + "biomejs.biome", + "esbenp.prettier-vscode" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a200ed..74dc877d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add reverse argument to `castComparator` by [@aleclarson](https://github.com/aleclarson) in [1d7937e](https://github.com/radashi-org/radashi/commit/1d7937ef006139883aedac782ad032c1d6269c7a) - Add `isBoolean` function by [@aleclarson](https://github.com/aleclarson) in [adc419d](https://github.com/radashi-org/radashi/commit/adc419d5bbb1786d75619ed3d7f41a45f68c9857) - Add `noop` and `always` functions by [@aleclarson](https://github.com/aleclarson) in [eb77c8f](https://github.com/radashi-org/radashi/commit/eb77c8f004a35f1499968f6e40d01b3595384848) +- Add `similarity` function by [@aleclarson](https://github.com/aleclarson) in [#122](https://github.com/radashi-org/radashi/pull/122) + +- **(throttle)** Add `trailing` option by [@crishoj](https://github.com/crishoj) in [#127](https://github.com/radashi-org/radashi/pull/127) + +- **(throttle)** Add `trigger` method to ThrottleFunction by [@aleclarson](https://github.com/aleclarson) in [#135](https://github.com/radashi-org/radashi/pull/135) + #### Changed - **(intersects)** Let `identity` callback return any value by [@aleclarson](https://github.com/aleclarson) in [#11](https://github.com/radashi-org/radashi/pull/11) @@ -120,8 +126,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve the return type of `filterKey` by [@aleclarson](https://github.com/aleclarson) in [bc298c6](https://github.com/radashi-org/radashi/commit/bc298c6cfcaaf74726e1f2b901e210dea1fed641) - Handle tuples in `isArray` return type by [@aleclarson](https://github.com/aleclarson) in [9257535](https://github.com/radashi-org/radashi/commit/925753578761bda277838bf8fbbcc24b3813f2b9) - Make `select` more option-friendly by [@aleclarson](https://github.com/aleclarson) in [c9cfcd0](https://github.com/radashi-org/radashi/commit/c9cfcd0a7eb1af98682f5d9b56555162c92b7085) +- Let `shift` accept a readonly array type by [@nnmrts](https://github.com/nnmrts) in [#126](https://github.com/radashi-org/radashi/pull/126) + +- Remove type constraint for mapped array passed to `sum` by [@MarlonPassos-git](https://github.com/MarlonPassos-git) in [dea0f50](https://github.com/radashi-org/radashi/commit/dea0f504f417b23aaf2b91495943501c894a172a) ### New Contributors +* [@crishoj](https://github.com/crishoj) made their first contribution in [#127](https://github.com/radashi-org/radashi/pull/127) +* [@nnmrts](https://github.com/nnmrts) made their first contribution in [#126](https://github.com/radashi-org/radashi/pull/126) * [@stefaanv](https://github.com/stefaanv) made their first contribution in [#95](https://github.com/radashi-org/radashi/pull/95) * [@eumkz](https://github.com/eumkz) made their first contribution in [#76](https://github.com/radashi-org/radashi/pull/76) * [@cimbraien](https://github.com/cimbraien) made their first contribution in [#58](https://github.com/radashi-org/radashi/pull/58) diff --git a/benchmarks/string/similarity.bench.ts b/benchmarks/string/similarity.bench.ts new file mode 100644 index 00000000..280cb5e7 --- /dev/null +++ b/benchmarks/string/similarity.bench.ts @@ -0,0 +1,11 @@ +import * as _ from 'radashi' +import { bench } from 'vitest' + +describe('similarity', () => { + const string1 = 'h'.repeat(100) + const string2 = 'ha'.repeat(50) + + bench('with 50% similar characters', () => { + _.similarity(string1, string2) + }) +}) diff --git a/docs/array/list.mdx b/docs/array/list.mdx index 97e4e476..0117056f 100644 --- a/docs/array/list.mdx +++ b/docs/array/list.mdx @@ -26,8 +26,6 @@ _.list(0, 6, i => i, 2) // [0, 2, 4, 6] ## Signatures -The list function can do a lot with different arguments. - ### list(size) When givin a single argument, it's treated as the `size`. Returns a list with values from 0 to `size`. diff --git a/docs/array/selectFirst.mdx b/docs/array/selectFirst.mdx index 65bec505..0753c6f0 100644 --- a/docs/array/selectFirst.mdx +++ b/docs/array/selectFirst.mdx @@ -1,37 +1,53 @@ --- title: selectFirst -description: Array find + map +description: Find and map the first array element meeting a condition --- ### Usage -Returns the mapped value for the first element that satisfies the specified condition--else undefined. -If the filter is omitted, returns the first non-nullish mapped value. +The `selectFirst` function combines the functionality of `find` and `map` operations on an array. It iterates through the array, applying a mapper function to each element, and returns the first mapped value that satisfies a given condition. If no condition is provided, it returns the first non-nullish mapped value. -```ts +This function is particularly useful when you need to find and transform an element in a single operation, potentially saving time and improving code readability. + +**Key features:** + +- Short-circuits on the first element that satisfies the condition +- Allows for separate mapping and condition functions +- Returns `undefined` if no element satisfies the condition or if the array is empty/nullish + +```typescript import * as _ from 'radashi' -const fish = [ - { - name: 'Marlin', - weight: 105, - source: 'ocean', - }, - { - name: 'Bass', - weight: 8, - source: 'lake', - }, - { - name: 'Trout', - weight: 13, - source: 'lake', - }, -] +// Find the first even number and double it +_.selectFirst( + [1, 3, 4, 6, 8], + x => x * 2, + x => x % 2 === 0, +) +// => 8 +// Find the first non-empty string and convert to uppercase _.selectFirst( - fish, - f => f.weight, - f => f.source === 'lake', -) // => 8 + ['', null, 'hello', 'world'], + s => s?.toUpperCase(), + s => s !== null && s !== '', +) +// => 'HELLO' + +// Find the first object with a specific property and extract a value +const users = [ + { id: 1, name: 'Alice', age: 30 }, + { id: 2, name: 'Bob', age: 25 }, + { id: 3, name: 'Charlie', age: 35 }, +] +_.selectFirst( + users, + user => user.name, + user => user.age > 30, +) +// => 'Charlie' + +// Using default condition (non-nullish) +_.selectFirst([null, undefined, 0, '', false, 'found'], x => x) +// => 0 ``` diff --git a/docs/async/all.mdx b/docs/async/all.mdx index 693e96c2..a20c7168 100644 --- a/docs/async/all.mdx +++ b/docs/async/all.mdx @@ -7,7 +7,7 @@ The `all` function is similar to the builtin Promise.all or Promise.allSettled functions. Given a list (or object) of promises, if any errors are thrown, all errors are gathered and thrown in an AggregateError. -## Using an Array +### Using an Array Passing an array as an argument will return the resolved promise values as an array in the same order. @@ -21,7 +21,7 @@ const [user] = await _.all([ ]) ``` -## Using an Object +### Using an Object Passing an object as an argument will return an object with the same keys and the values as the resolved promise values. diff --git a/docs/async/parallel.mdx b/docs/async/parallel.mdx index 35dd59a2..38f4bc19 100644 --- a/docs/async/parallel.mdx +++ b/docs/async/parallel.mdx @@ -21,7 +21,7 @@ const users = await _.parallel(3, userIds, async userId => { }) ``` -## Errors +### Errors When all work is complete parallel will check for errors. If any occurred they will all be thrown in a single `AggregateError` that diff --git a/docs/curry/debounce.mdx b/docs/curry/debounce.mdx index 26320460..e25e2f2c 100644 --- a/docs/curry/debounce.mdx +++ b/docs/curry/debounce.mdx @@ -20,7 +20,7 @@ const makeSearchRequest = event => { input.addEventListener('change', _.debounce({ delay: 100 }, makeSearchRequest)) ``` -## Timing +### Timing A visual of the debounce behavior when `delay` is `100`. The debounce function returned by `debounce` can be called every millisecond but it will only call diff --git a/docs/curry/memo.mdx b/docs/curry/memo.mdx index 8976de3f..67bbff50 100644 --- a/docs/curry/memo.mdx +++ b/docs/curry/memo.mdx @@ -18,7 +18,7 @@ const later = timestamp() now === later // => true ``` -## Expiration +### Expiration You can optionally pass a `ttl` (time to live) that will expire memoized results. In versions prior to version 10, `ttl` had a value of 300 milliseconds if not specified. @@ -40,7 +40,7 @@ now === later // => true now === muchLater // => false ``` -## Key Function +### Key Function You can optionally customize how values are stored when memoized. diff --git a/docs/curry/once.mdx b/docs/curry/once.mdx index 8dd8bce2..76175f60 100644 --- a/docs/curry/once.mdx +++ b/docs/curry/once.mdx @@ -15,7 +15,7 @@ fn() // 0.5 fn() // 0.5 ``` -## Resetting the function +### Resetting the function The `once.reset` function clears the stored result of a function that was previously wrapped with `once`. This allows the function to be executed again as if it were never called before, enabling dynamic reuse of the function with fresh computations. diff --git a/docs/curry/throttle.mdx b/docs/curry/throttle.mdx index 94b62e7d..3691013e 100644 --- a/docs/curry/throttle.mdx +++ b/docs/curry/throttle.mdx @@ -1,45 +1,64 @@ --- title: throttle -description: Create a throttled callback function +description: Creates a throttled function that limits invocations to a specified interval --- ### Usage -Throttle accepts an options object with an `interval` and a source function to call -when invoked. When the returned function is invoked it will only call the source -function if the `interval` milliseconds of time has passed. Otherwise, it will ignore -the invocation. +The `throttle` function creates a new function that, when called, will only execute the original function at most once per specified time interval. This is useful for limiting the rate at which a function can fire, especially for performance-intensive operations like handling scroll or resize events. -```ts -import * as _ from 'radashi' +The function accepts two parameters: -const onMouseMove = () => { - rerender() +1. An options object with: + - `interval`: The minimum time (in milliseconds) between function invocations + - `trailing` (optional): If true, also calls the function after the throttle period if it was invoked during the throttle +2. The function to be throttled + +The returned throttled function also includes these methods: + +- `isThrottled(): boolean`: To check if there's currently an active throttle +- `trigger(...args): void`: To invoke the wrapped function without waiting for the next interval + +```typescript +import { throttle } from 'radashi' + +// Throttle a scroll event handler +const handleScroll = () => { + console.log('Scroll position:', window.scrollY) } +const throttledScroll = throttle({ interval: 200 }, handleScroll) +window.addEventListener('scroll', throttledScroll) -addEventListener('mousemove', _.throttle({ interval: 200 }, onMouseMove)) +// Throttle an API call +const throttledFetch = throttle( + { interval: 5000, trailing: true }, + async () => { + const response = await fetch('https://api.example.com/data') + const data = await response.json() + console.log(data) + }, +) + +// Check if throttled +console.log('Is throttled:', throttledFetch.isThrottled()) ``` -## Timing +### Timing -A visual of the throttle behavior when `interval` is `200`. The throttle function -returned by `throttle` can be called every millisecond but it will only call -the given callback after `interval` milliseconds have passed. +A visual representation of the throttle behavior when `interval` is set to `200ms`: -```sh +``` Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - - Throttle Invocations: x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x - - - Source Invocations: x - - - - - - - - - - - - x - - - - - - - - - - - - - x - - - - - - ``` -### isThrottled - -The function returned by `throttle` has a `isThrottled` method that when called will return if there is any active throttle. +When the `trailing` option is set to `true`, an additional invocation occurs after the throttle period if any calls were made during the throttled time: -```ts -const debounced = _.throttle({ interval: 200 }, onMouseMove) - -// ... sometime later - -debounced.isThrottled() ``` +Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - - +Throttle Invocations: x x x x x x x x x x x x x x x x x x x x x - - - - - - - - - - - - - +Source Invocations: x - - - - - - - - - - - - x - - - - - - - - - - - - - x - - - - - - +``` + +In this diagram, 'x' represents function invocations, and '-' represents time passing. diff --git a/docs/number/lerp.mdx b/docs/number/lerp.mdx index fcec1206..4b27abcd 100644 --- a/docs/number/lerp.mdx +++ b/docs/number/lerp.mdx @@ -15,7 +15,7 @@ _.lerp(5, 15, 0.2) // => 7 _.lerp(-10, 10, 0.75) // => 5 ``` -## Etymology +### Etymology The name `lerp` is short for "linear interpolation". It's a term from computer graphics that means "interpolate linearly between two values". diff --git a/docs/number/round.mdx b/docs/number/round.mdx index 7557374d..849ccaa5 100644 --- a/docs/number/round.mdx +++ b/docs/number/round.mdx @@ -14,12 +14,12 @@ _.round(123.456) // => 123 _.round(1234.56, -2) // => 1200 ``` -## Precision +### Precision The `precision` argument is limited to be within the range of -323 to +292. Without this limit, precision values outside this range can result in NaN. -## Rounding Method +### Rounding Method You may provide a custom rounding method. The default is `Math.round`. diff --git a/docs/object/pick.mdx b/docs/object/pick.mdx index 6c7613de..7e69a707 100644 --- a/docs/object/pick.mdx +++ b/docs/object/pick.mdx @@ -45,7 +45,7 @@ When used with a predicate function, `pick` is potentially unsafe, because of pa ```typescript // Example demonstrating potential inaccuracy in `key` and `value` types within `_.pick` callback -import _ from 'lodash' +import * as _ from 'radashi' interface User { name: string diff --git a/docs/series/series.mdx b/docs/series/series.mdx index 36fc2497..2caa01f0 100644 --- a/docs/series/series.mdx +++ b/docs/series/series.mdx @@ -31,7 +31,7 @@ weekdays.next('friday', weekdays.first()) // => 'monday' weekdays.spin('monday', 3) // => 'thursday' ``` -## Complex Data Types +### Complex Data Types When working with objects you'll want to provide a second argument to `series`, a function that converts non-primitive values into an identity that can be checked for equality. diff --git a/docs/string/similarity.mdx b/docs/string/similarity.mdx new file mode 100644 index 00000000..cdd489a4 --- /dev/null +++ b/docs/string/similarity.mdx @@ -0,0 +1,42 @@ +--- +title: similarity +description: Calculate the similarity between two strings using the Levenshtein distance algorithm +--- + +### Usage + +The `similarity` function computes the Levenshtein distance between two input strings. This distance represents the minimum number of single-character edits (insertions, deletions, or substitutions) required to change one string into the other. + +This function is useful for various applications, including: + +- Spell checking and autocorrect features +- Fuzzy string matching +- DNA sequence analysis +- Plagiarism detection + +The function is case-sensitive and treats whitespace as significant characters. The order of the input strings doesn't affect the result, as the Levenshtein distance is symmetric. + +```typescript +import * as _ from 'radashi' + +// Identical strings +_.similarity('hello', 'hello') // => 0 + +// One character difference +_.similarity('kitten', 'sitten') // => 1 + +// Multiple differences +_.similarity('saturday', 'sunday') // => 3 + +// Case sensitivity +_.similarity('foo', 'FOO') // => 3 + +// Whitespace significance +_.similarity('bar ', 'bar') // => 1 + +// Argument order doesn't matter +_.similarity('abc', 'cba') // => 2 +_.similarity('cba', 'abc') // => 2 +``` + +The function returns a `number` representing the Levenshtein distance between the two input strings. A lower number indicates higher similarity, with 0 meaning the strings are identical. diff --git a/package.json b/package.json index d5d1c381..f0d77c69 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "add-function": "bash ./scripts/add-function.sh", "bench": "vitest bench", "build": "tsup --clean", - "bundle-impact": "bash ./scripts/weigh-changed.sh", + "bundle-impact": "bash ./scripts/bundle-impact.sh", "dev": "tsup --clean --watch --sourcemap", "format": "bash ./scripts/format.sh", "lint": "concurrently -c=auto -g --kill-others-on-fail 'npm:lint:*'", @@ -51,7 +51,7 @@ "@biomejs/biome": "^1.8.3", "@codspeed/vitest-plugin": "^3.1.0", "@typescript-eslint/parser": "^7.16.1", - "@vitest/coverage-v8": "1.6.0", + "@vitest/coverage-v8": "2.0.3", "concurrently": "^8.2.2", "eslint-plugin-compat": "^6.0.0", "glob": "^11.0.0", @@ -60,7 +60,7 @@ "prettier-plugin-sh": "^0.14.0", "tsup": "^8.1.0", "typescript": "^5.5.2", - "vitest": "1.6.0" + "vitest": "2.0.2" }, "sideEffects": false, "browserslist": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75c63bb7..549cd38d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,13 +18,13 @@ importers: version: 1.8.3 '@codspeed/vitest-plugin': specifier: ^3.1.0 - version: 3.1.0(patch_hash=6rp3ze2ne4vx24jyfnecpajldi)(vite@5.3.1(@types/node@20.14.8))(vitest@1.6.0(@types/node@20.14.8)) + version: 3.1.0(patch_hash=6rp3ze2ne4vx24jyfnecpajldi)(vite@5.3.1(@types/node@20.14.8))(vitest@2.0.2(@types/node@20.14.8)) '@typescript-eslint/parser': specifier: ^7.16.1 version: 7.16.1(eslint@8.57.0)(typescript@5.5.2) '@vitest/coverage-v8': - specifier: 1.6.0 - version: 1.6.0(vitest@1.6.0(@types/node@20.14.8)) + specifier: 2.0.3 + version: 2.0.3(vitest@2.0.2(@types/node@20.14.8)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -50,8 +50,8 @@ importers: specifier: ^5.5.2 version: 5.5.2 vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.8) + specifier: 2.0.2 + version: 2.0.2(@types/node@20.14.8) packages: @@ -322,10 +322,6 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -443,9 +439,6 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -486,35 +479,37 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitest/coverage-v8@1.6.0': - resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} + '@vitest/coverage-v8@2.0.3': + resolution: {integrity: sha512-53d+6jXFdYbasXBmsL6qaGIfcY5eBQq0sP57AjdasOcSiGNj4qxkkpDKIitUNfjxcfAfUfQ8BD0OR2fSey64+g==} peerDependencies: - vitest: 1.6.0 + vitest: 2.0.3 + + '@vitest/expect@2.0.2': + resolution: {integrity: sha512-nKAvxBYqcDugYZ4nJvnm5OR8eDJdgWjk4XM9owQKUjzW70q0icGV2HVnQOyYsp906xJaBDUXw0+9EHw2T8e0mQ==} - '@vitest/expect@1.6.0': - resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + '@vitest/pretty-format@2.0.2': + resolution: {integrity: sha512-SBCyOXfGVvddRd9r2PwoVR0fonQjh9BMIcBMlSzbcNwFfGr6ZhOhvBzurjvi2F4ryut2HcqiFhNeDVGwru8tLg==} - '@vitest/runner@1.6.0': - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + '@vitest/pretty-format@2.0.4': + resolution: {integrity: sha512-RYZl31STbNGqf4l2eQM1nvKPXE0NhC6Eq0suTTePc4mtMQ1Fn8qZmjV4emZdEdG2NOWGKSCrHZjmTqDCDoeFBw==} - '@vitest/snapshot@1.6.0': - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + '@vitest/runner@2.0.2': + resolution: {integrity: sha512-OCh437Vi8Wdbif1e0OvQcbfM3sW4s2lpmOjAE7qfLrpzJX2M7J1IQlNvEcb/fu6kaIB9n9n35wS0G2Q3en5kHg==} - '@vitest/spy@1.6.0': - resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + '@vitest/snapshot@2.0.2': + resolution: {integrity: sha512-Yc2ewhhZhx+0f9cSUdfzPRcsM6PhIb+S43wxE7OG0kTxqgqzo8tHkXFuFlndXeDMp09G3sY/X5OAo/RfYydf1g==} - '@vitest/utils@1.6.0': - resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@vitest/spy@2.0.2': + resolution: {integrity: sha512-MgwJ4AZtCgqyp2d7WcQVE8aNG5vQ9zu9qMPYQHjsld/QVsrvg78beNrXdO4HYkP0lDahCO3P4F27aagIag+SGQ==} + + '@vitest/utils@2.0.2': + resolution: {integrity: sha512-pxCY1v7kmOCWYWjzc0zfjGTA3Wmn8PKnlPvSrsA643P1NHl1fOyXj2Q9SaNlrlFE+ivCsxM80Ov3AR82RmHCWQ==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.3: - resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} - engines: {node: '>=0.4.0'} - acorn@8.12.0: resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} engines: {node: '>=0.4.0'} @@ -535,10 +530,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -557,8 +548,9 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} ast-metadata-inferer@0.8.0: resolution: {integrity: sha512-jOMKcHht9LxYIEQu+RVd22vtgrPaVCtDRQ/16IGmurdzxvYbDd5ynxjnyrzLnieG96eTcAyaoj/wN/4/1FyyeA==} @@ -608,16 +600,17 @@ packages: caniuse-lite@1.0.30001642: resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==} - chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -650,9 +643,6 @@ packages: engines: {node: ^14.13.0 || >=16.0.0} hasBin: true - confbox@0.1.7: - resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} - cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -670,8 +660,8 @@ packages: supports-color: optional: true - deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} deep-is@0.1.4: @@ -681,10 +671,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -961,8 +947,8 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.4: - resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} istanbul-reports@3.1.7: @@ -1015,10 +1001,6 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1039,8 +1021,8 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@3.1.1: + resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} lru-cache@10.2.2: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} @@ -1102,9 +1084,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mlly@1.7.1: - resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} - ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -1168,10 +1147,6 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -1222,8 +1197,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -1236,9 +1212,6 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - pkg-types@1.1.1: - resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} - postcss-load-config@4.0.2: resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -1276,10 +1249,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -1290,9 +1259,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -1427,9 +1393,9 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -1444,12 +1410,16 @@ packages: tinybench@2.8.0: resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} - tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + tinypool@1.0.0: + resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} engines: {node: '>=14.0.0'} to-fast-properties@2.0.0: @@ -1502,10 +1472,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -1515,9 +1481,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.5.3: - resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} - undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -1530,8 +1493,8 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite-node@1.6.0: - resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} + vite-node@2.0.2: + resolution: {integrity: sha512-w4vkSz1Wo+NIQg8pjlEn0jQbcM/0D+xVaYjhw3cvarTanLLBh54oNiRbsT8PNK5GfuST0IlVXjsNRoNlqvY/fw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -1563,15 +1526,15 @@ packages: terser: optional: true - vitest@1.6.0: - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + vitest@2.0.2: + resolution: {integrity: sha512-WlpZ9neRIjNBIOQwBYfBSr0+of5ZCbxT2TVGKW4Lv0c8+srCFIiRdsP7U009t8mMn821HQ4XKgkx5dVWpyoyLw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.0 - '@vitest/ui': 1.6.0 + '@vitest/browser': 2.0.2 + '@vitest/ui': 2.0.2 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -1715,11 +1678,11 @@ snapshots: transitivePeerDependencies: - debug - '@codspeed/vitest-plugin@3.1.0(patch_hash=6rp3ze2ne4vx24jyfnecpajldi)(vite@5.3.1(@types/node@20.14.8))(vitest@1.6.0(@types/node@20.14.8))': + '@codspeed/vitest-plugin@3.1.0(patch_hash=6rp3ze2ne4vx24jyfnecpajldi)(vite@5.3.1(@types/node@20.14.8))(vitest@2.0.2(@types/node@20.14.8))': dependencies: '@codspeed/core': 3.1.0 vite: 5.3.1(@types/node@20.14.8) - vitest: 1.6.0(@types/node@20.14.8) + vitest: 2.0.2(@types/node@20.14.8) transitivePeerDependencies: - debug @@ -1838,10 +1801,6 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.8 - '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -1924,8 +1883,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.18.0': optional: true - '@sinclair/typebox@0.27.8': {} - '@types/estree@1.0.5': {} '@types/node@20.14.8': @@ -1975,62 +1932,66 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.8))': + '@vitest/coverage-v8@2.0.3(vitest@2.0.2(@types/node@20.14.8))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 debug: 4.3.5 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.4 + istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 magic-string: 0.30.10 magicast: 0.3.4 - picocolors: 1.0.1 std-env: 3.7.0 strip-literal: 2.1.0 - test-exclude: 6.0.0 - vitest: 1.6.0(@types/node@20.14.8) + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.0.2(@types/node@20.14.8) transitivePeerDependencies: - supports-color - '@vitest/expect@1.6.0': + '@vitest/expect@2.0.2': + dependencies: + '@vitest/spy': 2.0.2 + '@vitest/utils': 2.0.2 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.2': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.4': dependencies: - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - chai: 4.4.1 + tinyrainbow: 1.2.0 - '@vitest/runner@1.6.0': + '@vitest/runner@2.0.2': dependencies: - '@vitest/utils': 1.6.0 - p-limit: 5.0.0 + '@vitest/utils': 2.0.2 pathe: 1.1.2 - '@vitest/snapshot@1.6.0': + '@vitest/snapshot@2.0.2': dependencies: + '@vitest/pretty-format': 2.0.2 magic-string: 0.30.10 pathe: 1.1.2 - pretty-format: 29.7.0 - '@vitest/spy@1.6.0': + '@vitest/spy@2.0.2': dependencies: - tinyspy: 2.2.1 + tinyspy: 3.0.0 - '@vitest/utils@1.6.0': + '@vitest/utils@2.0.2': dependencies: - diff-sequences: 29.6.3 + '@vitest/pretty-format': 2.0.2 estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 + loupe: 3.1.1 + tinyrainbow: 1.2.0 acorn-jsx@5.3.2(acorn@8.12.0): dependencies: acorn: 8.12.0 - acorn-walk@8.3.3: - dependencies: - acorn: 8.12.0 - acorn@8.12.0: {} ajv@6.12.6: @@ -2048,8 +2009,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} any-promise@1.3.0: {} @@ -2063,7 +2022,7 @@ snapshots: array-union@2.1.0: {} - assertion-error@1.1.0: {} + assertion-error@2.0.1: {} ast-metadata-inferer@0.8.0: dependencies: @@ -2114,24 +2073,20 @@ snapshots: caniuse-lite@1.0.30001642: {} - chai@4.4.1: + chai@5.1.1: dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.0.8 + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.1 + pathval: 2.0.0 chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 + check-error@2.1.1: {} chokidar@3.6.0: dependencies: @@ -2177,8 +2132,6 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 - confbox@0.1.7: {} - cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -2193,16 +2146,12 @@ snapshots: dependencies: ms: 2.1.2 - deep-eql@4.1.4: - dependencies: - type-detect: 4.0.8 + deep-eql@5.0.2: {} deep-is@0.1.4: {} delayed-stream@1.0.0: {} - diff-sequences@29.6.3: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -2533,7 +2482,7 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.4: + istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 debug: 4.3.5 @@ -2587,11 +2536,6 @@ snapshots: load-tsconfig@0.2.5: {} - local-pkg@0.5.0: - dependencies: - mlly: 1.7.1 - pkg-types: 1.1.1 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -2608,7 +2552,7 @@ snapshots: lodash@4.17.21: {} - loupe@2.3.7: + loupe@3.1.1: dependencies: get-func-name: 2.0.2 @@ -2663,13 +2607,6 @@ snapshots: minipass@7.1.2: {} - mlly@1.7.1: - dependencies: - acorn: 8.12.0 - pathe: 1.1.2 - pkg-types: 1.1.1 - ufo: 1.5.3 - ms@2.1.2: {} mvdan-sh@0.10.1: {} @@ -2729,10 +2666,6 @@ snapshots: dependencies: yocto-queue: 1.0.0 - p-limit@5.0.0: - dependencies: - yocto-queue: 1.0.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -2771,7 +2704,7 @@ snapshots: pathe@1.1.2: {} - pathval@1.1.1: {} + pathval@2.0.0: {} picocolors@1.0.1: {} @@ -2779,12 +2712,6 @@ snapshots: pirates@4.0.6: {} - pkg-types@1.1.1: - dependencies: - confbox: 0.1.7 - mlly: 1.7.1 - pathe: 1.1.2 - postcss-load-config@4.0.2(postcss@8.4.38): dependencies: lilconfig: 3.1.2 @@ -2812,20 +2739,12 @@ snapshots: prettier@3.3.2: {} - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - proxy-from-env@1.1.0: {} punycode@2.3.1: {} queue-microtask@1.2.3: {} - react-is@18.3.1: {} - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -2956,11 +2875,11 @@ snapshots: dependencies: has-flag: 4.0.0 - test-exclude@6.0.0: + test-exclude@7.0.1: dependencies: '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 + glob: 10.4.2 + minimatch: 9.0.4 text-table@0.2.0: {} @@ -2974,9 +2893,11 @@ snapshots: tinybench@2.8.0: {} - tinypool@0.8.4: {} + tinypool@1.0.0: {} + + tinyrainbow@1.2.0: {} - tinyspy@2.2.1: {} + tinyspy@3.0.0: {} to-fast-properties@2.0.0: {} @@ -3025,14 +2946,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-detect@4.0.8: {} - type-fest@0.20.2: {} typescript@5.5.2: {} - ufo@1.5.3: {} - undici-types@5.26.5: optional: true @@ -3046,12 +2963,12 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@1.6.0(@types/node@20.14.8): + vite-node@2.0.2(@types/node@20.14.8): dependencies: cac: 6.7.14 debug: 4.3.5 pathe: 1.1.2 - picocolors: 1.0.1 + tinyrainbow: 1.2.0 vite: 5.3.1(@types/node@20.14.8) transitivePeerDependencies: - '@types/node' @@ -3072,27 +2989,26 @@ snapshots: '@types/node': 20.14.8 fsevents: 2.3.3 - vitest@1.6.0(@types/node@20.14.8): + vitest@2.0.2(@types/node@20.14.8): dependencies: - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.3 - chai: 4.4.1 + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.2 + '@vitest/pretty-format': 2.0.4 + '@vitest/runner': 2.0.2 + '@vitest/snapshot': 2.0.2 + '@vitest/spy': 2.0.2 + '@vitest/utils': 2.0.2 + chai: 5.1.1 debug: 4.3.5 execa: 8.0.1 - local-pkg: 0.5.0 magic-string: 0.30.10 pathe: 1.1.2 - picocolors: 1.0.1 std-env: 3.7.0 - strip-literal: 2.1.0 tinybench: 2.8.0 - tinypool: 0.8.4 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 vite: 5.3.1(@types/node@20.14.8) - vite-node: 1.6.0(@types/node@20.14.8) + vite-node: 2.0.2(@types/node@20.14.8) why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.14.8 diff --git a/scripts/bundle-impact.sh b/scripts/bundle-impact.sh new file mode 100644 index 00000000..54092057 --- /dev/null +++ b/scripts/bundle-impact.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +if [ ! -d "scripts/release-notes/node_modules" ]; then + echo "Node modules not found. Installing dependencies..." + pnpm install -C scripts/release-notes +fi + +pnpm -s tsx -e ' +import {weighChangedFunctions} from "./scripts/bundle-impact/weigh-changed.ts"; +weighChangedFunctions().then(console.log) +' diff --git a/scripts/bundle-impact/dedent.ts b/scripts/bundle-impact/dedent.ts new file mode 100644 index 00000000..ab9e3099 --- /dev/null +++ b/scripts/bundle-impact/dedent.ts @@ -0,0 +1,86 @@ +import { isArray } from 'radashi' + +/** + * Remove indentation from a string. The given string is expected to + * be consistently indented (i.e. the leading whitespace of the first + * non-empty line is the minimum required for all non-empty lines). + * + * If the `indent` argument is nullish, the indentation is detected + * from the first non-empty line. Detection is cheap and robust for + * most use cases, so you should only set an explicit `indent` if + * necessary. + * + * @see https://radashi-org.github.io/reference/string/dedent + * @example + * ```ts + * // This is indented with 4 spaces. + * const input = ` + * Hello + * World + * ` + * + * // Explicit indentation + * dedent(input, ' ') + * // => ' Hello\n World\n' + * + * // Detected indentation + * dedent(input) + * // => 'Hello\nWorld\n' + * + * // Tagged template strings + * const str = dedent` + * Foo ${1 + 1} + * Bar ${2 * 2} + * ` + * // => 'Foo 2\nBar 4' + * ``` + */ +export function dedent( + template: TemplateStringsArray, + ...values: unknown[] +): string + +export function dedent(text: string, indent?: string | null): string + +export function dedent( + text: string | TemplateStringsArray, + ...values: unknown[] +): string { + // Support tagged template strings + if (isArray(text)) { + if (values.length > 0) { + return dedent( + text.reduce((acc, input, i) => { + let value = String(values[i] ?? '') + + // Detect the indentation before this embedded string. + const indent = + value.includes('\n') && input.match(/[ \t]*(?=[^\n]*$)/)?.[0] + + // Ensure the multi-line, embedded string can be correctly + // dedented. + if (indent) { + value = value.replace(/\n(?=[^\n]*?\S)/g, '\n' + indent) + } + + return acc + input + value + }, ''), + ) + } + + text = text[0] + } + + const indent = values[0] ?? detectIndent(text) + const output = indent + ? text.replace(new RegExp(`^${indent}`, 'gm'), '') + : text + + // Remove the first and last lines (if empty). + return output.replace(/^[ \t]*\n|\n[ \t]*$/g, '') +} + +// Find the indentation of the first non-empty line. +function detectIndent(text: string) { + return text.match(/^[ \t]*(?=\S)/m)?.[0] +} diff --git a/scripts/bundle-impact/package.json b/scripts/bundle-impact/package.json new file mode 100644 index 00000000..3b522f4f --- /dev/null +++ b/scripts/bundle-impact/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "execa": "^9.3.0", + "radashi": "12.2.0-beta.7fb6e89", + "tsx": "^4.17.0" + } +} diff --git a/scripts/bundle-impact/pnpm-lock.yaml b/scripts/bundle-impact/pnpm-lock.yaml new file mode 100644 index 00000000..1c4cec16 --- /dev/null +++ b/scripts/bundle-impact/pnpm-lock.yaml @@ -0,0 +1,470 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + execa: + specifier: ^9.3.0 + version: 9.3.0 + radashi: + specifier: 12.2.0-beta.7fb6e89 + version: 12.2.0-beta.7fb6e89 + tsx: + specifier: ^4.17.0 + version: 4.17.0 + +packages: + + '@esbuild/aix-ppc64@0.23.0': + resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.0': + resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.0': + resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.0': + resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.0': + resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.0': + resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.0': + resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.0': + resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.0': + resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.0': + resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.0': + resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.0': + resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.0': + resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.0': + resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.0': + resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.0': + resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.0': + resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.0': + resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.0': + resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.0': + resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.0': + resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.0': + resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.0': + resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.0': + resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + esbuild@0.23.0: + resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} + engines: {node: '>=18'} + hasBin: true + + execa@9.3.0: + resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + engines: {node: ^18.19.0 || >=20.5.0} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.7.6: + resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + + human-signals@7.0.0: + resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} + engines: {node: '>=18.18.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + engines: {node: '>=18'} + + radashi@12.2.0-beta.7fb6e89: + resolution: {integrity: sha512-zBJajRnOB3vnX1mBNOMBrfblepRxSJ7/peTndW0wnfvw53mZ2Ra77ZXU9XCfpyO003y6cjJBqEioLX39jhyAsg==} + engines: {node: '>=16.0.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + tsx@4.17.0: + resolution: {integrity: sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==} + engines: {node: '>=18.0.0'} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + +snapshots: + + '@esbuild/aix-ppc64@0.23.0': + optional: true + + '@esbuild/android-arm64@0.23.0': + optional: true + + '@esbuild/android-arm@0.23.0': + optional: true + + '@esbuild/android-x64@0.23.0': + optional: true + + '@esbuild/darwin-arm64@0.23.0': + optional: true + + '@esbuild/darwin-x64@0.23.0': + optional: true + + '@esbuild/freebsd-arm64@0.23.0': + optional: true + + '@esbuild/freebsd-x64@0.23.0': + optional: true + + '@esbuild/linux-arm64@0.23.0': + optional: true + + '@esbuild/linux-arm@0.23.0': + optional: true + + '@esbuild/linux-ia32@0.23.0': + optional: true + + '@esbuild/linux-loong64@0.23.0': + optional: true + + '@esbuild/linux-mips64el@0.23.0': + optional: true + + '@esbuild/linux-ppc64@0.23.0': + optional: true + + '@esbuild/linux-riscv64@0.23.0': + optional: true + + '@esbuild/linux-s390x@0.23.0': + optional: true + + '@esbuild/linux-x64@0.23.0': + optional: true + + '@esbuild/netbsd-x64@0.23.0': + optional: true + + '@esbuild/openbsd-arm64@0.23.0': + optional: true + + '@esbuild/openbsd-x64@0.23.0': + optional: true + + '@esbuild/sunos-x64@0.23.0': + optional: true + + '@esbuild/win32-arm64@0.23.0': + optional: true + + '@esbuild/win32-ia32@0.23.0': + optional: true + + '@esbuild/win32-x64@0.23.0': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + esbuild@0.23.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.0 + '@esbuild/android-arm': 0.23.0 + '@esbuild/android-arm64': 0.23.0 + '@esbuild/android-x64': 0.23.0 + '@esbuild/darwin-arm64': 0.23.0 + '@esbuild/darwin-x64': 0.23.0 + '@esbuild/freebsd-arm64': 0.23.0 + '@esbuild/freebsd-x64': 0.23.0 + '@esbuild/linux-arm': 0.23.0 + '@esbuild/linux-arm64': 0.23.0 + '@esbuild/linux-ia32': 0.23.0 + '@esbuild/linux-loong64': 0.23.0 + '@esbuild/linux-mips64el': 0.23.0 + '@esbuild/linux-ppc64': 0.23.0 + '@esbuild/linux-riscv64': 0.23.0 + '@esbuild/linux-s390x': 0.23.0 + '@esbuild/linux-x64': 0.23.0 + '@esbuild/netbsd-x64': 0.23.0 + '@esbuild/openbsd-arm64': 0.23.0 + '@esbuild/openbsd-x64': 0.23.0 + '@esbuild/sunos-x64': 0.23.0 + '@esbuild/win32-arm64': 0.23.0 + '@esbuild/win32-ia32': 0.23.0 + '@esbuild/win32-x64': 0.23.0 + + execa@9.3.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.3 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 7.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 5.3.0 + pretty-ms: 9.1.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.0.0 + + fsevents@2.3.3: + optional: true + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.7.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + human-signals@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.0.0: {} + + isexe@2.0.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pretty-ms@9.1.0: + dependencies: + parse-ms: 4.0.0 + + radashi@12.2.0-beta.7fb6e89: {} + + resolve-pkg-maps@1.0.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + strip-final-newline@4.0.0: {} + + tsx@4.17.0: + dependencies: + esbuild: 0.23.0 + get-tsconfig: 4.7.6 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + yoctocolors@2.1.1: {} diff --git a/scripts/bundle-impact/pr-bundle-impact.test.cjs b/scripts/bundle-impact/pr-bundle-impact.test.cjs new file mode 100644 index 00000000..b31a4999 --- /dev/null +++ b/scripts/bundle-impact/pr-bundle-impact.test.cjs @@ -0,0 +1,138 @@ +import { test, vi } from 'vitest' +import { dedent } from './dedent' +import { run } from './pr-bundle-impact' + + +vi.mock('./weigh-changed', () => ({ + weighChangedFunctions: vi.fn(), +})) + +test('adds the bundle impact to the PR body', async () => { + const pulls = { + 'radashi-org/radashi#1': { + body: dedent` + ## Summary + + This is a summary of the PR. + `, + }, + } + + const env = { + context: { + repo: { + owner: 'radashi-org', + repo: 'radashi', + }, + issue: { number: 1 }, + }, + github: { + rest: { + pulls: { + get({ owner, repo, pull_number }) { + return Promise.resolve({ + data: pulls[`${owner}/${repo}#${pull_number}`], + }) + }, + update: vi.fn(), + }, + }, + }, + core: { + info: vi.fn(), + setFailed: vi.fn(), + }, + } + + const { weighChangedFunctions } = await import('./weigh-changed') + + weighChangedFunctions.mockResolvedValue( + dedent` + | Status | File | Size | Difference (%) | + | --- | --- | --- | --- | + | M | src/foo/bar.ts | 110 | +10 (+10%) | + `, + ) + + await run(env) + + expect(env.github.rest.pulls.update).toHaveBeenCalled() + expect(env.core.info).toHaveBeenCalled() + expect(env.core.setFailed.mock.calls).toEqual([]) + + expect(env.github.rest.pulls.update.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "body": "## Summary + + This is a summary of the PR. + + ## Bundle impact + + | Status | File | Size | Difference (%) | + | --- | --- | --- | --- | + | M | src/foo/bar.ts | 110 | +10 (+10%) | + + ", + "owner": "radashi-org", + "pull_number": 1, + "repo": "radashi", + }, + ], + ] + `) + expect(env.core.info.mock.calls).toMatchInlineSnapshot(` + [ + [ + "fetching PR #1 data from radashi-org/radashi...", + ], + [ + "calculating bundle impact...", + ], + [ + "updating PR description...", + ], + [ + "PR description updated with bundle impact.", + ], + ] + `) + + // Now that we've tested adding the "Bundle Impact" section, let's test + // updating that section upon a subsequent run. + weighChangedFunctions.mockResolvedValue( + dedent` + | Status | File | Size | Difference (%) | + | --- | --- | --- | --- | + | M | src/foo/bar.ts | 120 | +20 (+20%) | + `, + ) + + env.github.rest.pulls.update.mockClear() + + await run(env) + + expect(env.github.rest.pulls.update.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "body": "## Summary + + This is a summary of the PR. + + ## Bundle impact + + | Status | File | Size | Difference (%) | + | --- | --- | --- | --- | + | M | src/foo/bar.ts | 120 | +20 (+20%) | + + ", + "owner": "radashi-org", + "pull_number": 1, + "repo": "radashi", + }, + ], + ] + `) +}) diff --git a/scripts/bundle-impact/pr-bundle-impact.ts b/scripts/bundle-impact/pr-bundle-impact.ts new file mode 100644 index 00000000..f51fb01a --- /dev/null +++ b/scripts/bundle-impact/pr-bundle-impact.ts @@ -0,0 +1,40 @@ +import { weighChangedFunctions } from './weigh-changed' + +export async function run({ github, core, context }) { + const repo = `${context.repo.owner}/${context.repo.repo}` + core.info(`fetching PR #${context.issue.number} data from ${repo}...`) + + const { data: pullRequest } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }) + + core.info('calculating bundle impact...') + let bundleImpact = await weighChangedFunctions() + if (bundleImpact) { + bundleImpact = `## Bundle impact\n\n${bundleImpact}\n\n` + } + + const originalBody = pullRequest.body + const bundleImpactRegex = /## Bundle impact[\s\S]*?(?=##|$)/ + + let updatedBody: string + if (bundleImpactRegex.test(originalBody)) { + updatedBody = originalBody.replace(bundleImpactRegex, bundleImpact) + } else { + updatedBody = `${originalBody}\n\n${bundleImpact}` + } + + if (updatedBody !== originalBody) { + core.info('updating PR description...') + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + body: updatedBody, + }) + + core.info('PR description updated with bundle impact.') + } +} diff --git a/scripts/bundle-impact/weigh-changed.ts b/scripts/bundle-impact/weigh-changed.ts new file mode 100644 index 00000000..ba408d06 --- /dev/null +++ b/scripts/bundle-impact/weigh-changed.ts @@ -0,0 +1,176 @@ +import { execa } from 'execa' +import * as readline from 'node:readline/promises' +import { cluster, map, select } from 'radashi' + +export async function weighChangedFunctions() { + const targetBranch = await getTargetBranch() + const changedFiles = await getChangedFiles(targetBranch) + + await ensureEsbuildInstalled() + + const prevSizes = await getPreviousSizes(changedFiles, targetBranch) + + const columnCount = prevSizes.some(size => size !== 0) ? 3 : 2 + + let result = '' + + const addLine = (line: string) => (result += line + '\n') + + if (process.env.CI && changedFiles.length > 0) { + if (columnCount > 2) { + addLine('| Status | File | Size | Difference (%) |') + addLine('|---|---|---|---|') + } else { + addLine('| Status | File | Size |') + addLine('|---|---|---|') + } + } + + await map(changedFiles, async ({ status, name }, i) => { + const prevBytes = prevSizes[i] + + const bytes = await getFileSize(name, status) + + const diff = bytes - prevBytes + const diffStr = (diff >= 0 ? '+' : '') + diff + + let ratioStr = '' + if (columnCount > 2 && prevBytes !== 0) { + const ratio = Math.round((bytes / prevBytes - 1) * 100) + ratioStr = ` (${ratio >= 0 ? '+' : ''}${ratio}%)` + } + + if (process.env.CI) { + const sizeStr = i === 0 ? `${bytes} [^1337]` : `${bytes}` + if (columnCount > 2) { + addLine( + `| ${status} | \`${name}\` | ${sizeStr} | ${diffStr}${ratioStr} |`, + ) + } else { + addLine(`| ${status} | \`${name}\` | ${sizeStr} |`) + } + } else { + if (columnCount > 2 && prevBytes !== 0) { + addLine(`${name}: ${bytes} bytes (${diffStr} bytes)${ratioStr}`) + } else { + addLine(`${name}: ${bytes} bytes`) + } + } + }) + + if (process.env.CI && changedFiles.length > 0) { + addLine('') + addLine( + '[^1337]: Function size includes the `import` dependencies of the function.', + ) + addLine('') + } + + return result +} + +async function getTargetBranch(): Promise { + if (process.env.TARGET_BRANCH) { + return process.env.TARGET_BRANCH + } + + try { + const { stdout } = await execa('gh', [ + 'pr', + 'view', + '--json', + 'baseRefName', + '--jq', + '.baseRefName', + ]) + return stdout.trim() || 'main' + } catch { + return 'main' + } +} + +async function getChangedFiles(targetBranch: string) { + const { stdout } = await execa('git', [ + 'diff', + '--name-status', + `origin/${targetBranch}`, + 'HEAD', + '--', + 'src/**/*.ts', + ]) + + return select( + cluster(stdout.trim().split(/[\r\n\t]+/), 2), + ([status, name]) => ({ status, name }), + // Ignore changes to files like src/mod.ts and src/types.ts + ([, name]) => /[\\/]/.test(name), + ) +} + +async function installEsbuild(): Promise { + if (!process.env.CI) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + const answer = await rl.question( + 'Install esbuild to pnpm global store? (Y/n) ', + ) + + rl.close() + + if (answer.toLowerCase() === 'n') { + process.exit(1) + } + } + + await execa('pnpm', ['install', '-g', 'esbuild']) +} + +async function ensureEsbuildInstalled(): Promise { + try { + await execa('which', ['esbuild']) + } catch { + await installEsbuild() + } +} + +async function getPreviousSizes( + changedFiles: { status: string; name: string }[], + targetBranch: string, +): Promise { + const prevSizes: number[] = [] + + if ( + process.env.CI || + (await execa('git', ['status', '-s'])).stdout.trim() === '' + ) { + await execa('git', ['checkout', targetBranch]) + + for (const { status, name } of changedFiles) { + if (status === 'A') { + prevSizes.push(0) + } else { + const { stdout } = await execa('esbuild', [ + '--bundle', + '--minify', + name, + ]) + prevSizes.push(stdout.length) + } + } + + await execa('git', ['checkout', '-']) + } + + return prevSizes +} + +async function getFileSize(file: string, status: string): Promise { + if (status === 'D') { + return 0 + } + const { stdout } = await execa('esbuild', ['--bundle', '--minify', file]) + return stdout.length +} diff --git a/scripts/pr-bundle-impact.cjs b/scripts/pr-bundle-impact.cjs deleted file mode 100644 index 4bfad9e8..00000000 --- a/scripts/pr-bundle-impact.cjs +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-check -const { execSync } = require('child_process') - -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() - if (!bundleImpact) { - return - } - - // 2. Update the original post of the pull request - const { data: pullRequest } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - }) - - const originalBody = pullRequest.body - const bundleImpactRegex = /## Bundle impact[\s\S]*?(?=##|$)/ - - let updatedBody - if (bundleImpactRegex.test(originalBody)) { - updatedBody = originalBody.replace( - bundleImpactRegex, - `## Bundle impact\n\n${bundleImpact}\n\n`, - ) - } else { - updatedBody = `${originalBody}\n\n## Bundle impact\n\n${bundleImpact}\n\n` - } - - if (updatedBody !== originalBody) { - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.issue.number, - body: updatedBody, - }) - - core.info('PR description updated with bundle impact.') - } - } catch (error) { - core.setFailed(`Action failed with error: ${error}`) - } -} diff --git a/scripts/pr-bundle-impact.test.cjs b/scripts/pr-bundle-impact.test.cjs deleted file mode 100644 index 9150b125..00000000 --- a/scripts/pr-bundle-impact.test.cjs +++ /dev/null @@ -1,80 +0,0 @@ -import { test, vi } from 'vitest' -import { run } from './pr-bundle-impact.cjs' - -test('updates PR body with bundle impact', async () => { - const pulls = { - 'radashi-org/radashi#1': { - body: ` -## Bundle impact - -_Calculating..._ -`, - }, - } - - const env = { - context: { - repo: { - owner: 'radashi-org', - repo: 'radashi', - }, - issue: { number: 1 }, - }, - github: { - rest: { - pulls: { - get({ owner, repo, pull_number }) { - return Promise.resolve({ - data: pulls[`${owner}/${repo}#${pull_number}`], - }) - }, - update: vi.fn(), - }, - }, - }, - core: { - info: vi.fn(), - setFailed: vi.fn(), - }, - } - - await run(env, command => { - expect(command).toEqual('pnpm -s bundle-impact') - return ` -| File | Size | Difference (%) | -| --- | --- | --- | -| src/foo/bar.ts | 110 | +10 (+10%) | -` - }) - - expect(env.github.rest.pulls.update).toHaveBeenCalled() - expect(env.core.info).toHaveBeenCalled() - expect(env.core.setFailed.mock.calls).toEqual([]) - - expect(env.github.rest.pulls.update.mock.calls).toMatchInlineSnapshot(` - [ - [ - { - "body": " - ## Bundle impact - - | File | Size | Difference (%) | - | --- | --- | --- | - | src/foo/bar.ts | 110 | +10 (+10%) | - - ", - "owner": "radashi-org", - "pull_number": 1, - "repo": "radashi", - }, - ], - ] - `) - expect(env.core.info.mock.calls).toMatchInlineSnapshot(` - [ - [ - "PR description updated with bundle impact.", - ], - ] - `) -}) diff --git a/scripts/radashi-db/README.md b/scripts/radashi-db/README.md new file mode 100644 index 00000000..67c71b24 --- /dev/null +++ b/scripts/radashi-db/README.md @@ -0,0 +1,11 @@ +## ./scripts/radashi-db/ + +This folder contains scripts for managing the Radashi database (hosted on Supabase and Algolia). For example, when a pull request is updated, the `registerPullRequest` function from the `register-pr.ts` module is called to update the database with the latest information about the pull request. This data is then used on certain pages of the Radashi website and the Radashi VSCode extension. + +### Environment Variables + +To use these scripts, you may need these environment variables: + +- `SUPABASE_KEY`: A private API key for Supabase +- `ALGOLIA_KEY`: A private API key for Algolia +- `GITHUB_TOKEN`: A GitHub token with access to the Radashi organization diff --git a/scripts/radashi-db/ci-register-pr.ts b/scripts/radashi-db/ci-register-pr.ts new file mode 100644 index 00000000..b9da804f --- /dev/null +++ b/scripts/radashi-db/ci-register-pr.ts @@ -0,0 +1,137 @@ +import type { Octokit } from '@octokit/rest' +import { registerPullRequest } from './src/register-pr' + +export async function run( + context: Context, + github: Octokit, + console?: Pick, +): Promise { + // https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=synchronize#pull_request + const pr = context.payload.pull_request + if (!pr) { + throw new Error('No pull request found in context') + } + + const owner = pr.head.repo.owner.login + const repo = pr.head.repo.name + + const { data: files } = await github.pulls.listFiles({ + owner, + repo, + pull_number: pr.number, + }) + const { data: checkRuns } = await github.checks.listForRef({ + owner, + repo, + ref: pr.head.sha, + }) + + await registerPullRequest(pr.number, { + status: pr.draft ? 'draft' : pr.merged_at ? 'merged' : pr.state, + sha: pr.head.sha, + repo, + owner, + ownerAvatarUrl: pr.user.avatar_url, + branch: pr.head.ref, + checksPassed: checkRuns.check_runs.every( + run => run.conclusion === 'success', + ), + console, + files, + breaking: (pr.labels as { name: string }[]).some( + label => label.name === 'breaking', + ), + getApprovalRating: async () => { + const { data: reactions } = await github.reactions.listForIssue({ + owner: 'radashi-org', + repo: 'radashi', + issue_number: pr.number, + }) + return reactions.filter(reaction => reaction.content === '+1').length + }, + getCommit: async () => { + return { + sha: pr.head.sha, + author: pr.user.name, + date: pr.created_at, + } + }, + getFileContent: async path => { + const { data } = await github.repos.getContent({ + owner, + repo, + path, + ref: pr.head.sha, + }) + if (!('content' in data)) { + throw new Error(`File ${path} has no content`) + } + return Buffer.from(data.content, 'base64').toString('utf-8') + }, + getIssueBody: async () => { + return pr.body ?? null + }, + }) +} + +/** + * @source https://github.com/actions/toolkit/blob/f003268/packages/github/src/context.ts + */ +interface Context { + payload: WebhookPayload + eventName: string + sha: string + ref: string + workflow: string + action: string + actor: string + job: string + runAttempt: number + runNumber: number + runId: number + apiUrl: string + serverUrl: string + graphqlUrl: string +} + +interface PayloadRepository { + [key: string]: any + full_name?: string + name: string + owner: { + [key: string]: any + login: string + name?: string + } + html_url?: string +} + +interface WebhookPayload { + [key: string]: any + repository?: PayloadRepository + issue?: { + [key: string]: any + number: number + html_url?: string + body?: string + } + pull_request?: { + [key: string]: any + number: number + html_url?: string + body?: string + } + sender?: { + [key: string]: any + type: string + } + action?: string + installation?: { + id: number + [key: string]: any + } + comment?: { + id: number + [key: string]: any + } +} diff --git a/scripts/radashi-db/package.json b/scripts/radashi-db/package.json new file mode 100644 index 00000000..3b1c9272 --- /dev/null +++ b/scripts/radashi-db/package.json @@ -0,0 +1,25 @@ +{ + "name": "@radashi-org/radashi-db", + "private": true, + "type": "module", + "main": "./index.js", + "dependencies": { + "@octokit/rest": "^21.0.1", + "@supabase/supabase-js": "^2.45.0", + "algoliasearch": "^4.24.0", + "execa": "^9.3.0", + "fast-glob": "^3.3.2", + "markdown-it": "^14.1.0", + "markdown-it-front-matter": "^0.2.4", + "mri": "^1.2.0", + "radashi": "12.2.0-beta.83909af", + "sucrase": "^3.35.0", + "tsx": "^4.17.0", + "ultrahtml": "^1.5.3", + "yaml": "^2.5.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node": "^22.0.0" + } +} \ No newline at end of file diff --git a/scripts/radashi-db/pnpm-lock.yaml b/scripts/radashi-db/pnpm-lock.yaml new file mode 100644 index 00000000..9f75f42d --- /dev/null +++ b/scripts/radashi-db/pnpm-lock.yaml @@ -0,0 +1,1433 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@octokit/rest': + specifier: ^21.0.1 + version: 21.0.1 + '@supabase/supabase-js': + specifier: ^2.45.0 + version: 2.45.0 + algoliasearch: + specifier: ^4.24.0 + version: 4.24.0 + execa: + specifier: ^9.3.0 + version: 9.3.0 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 + markdown-it-front-matter: + specifier: ^0.2.4 + version: 0.2.4 + mri: + specifier: ^1.2.0 + version: 1.2.0 + radashi: + specifier: 12.2.0-beta.83909af + version: 12.2.0-beta.83909af + sucrase: + specifier: ^3.35.0 + version: 3.35.0 + tsx: + specifier: ^4.17.0 + version: 4.17.0 + ultrahtml: + specifier: ^1.5.3 + version: 1.5.3 + yaml: + specifier: ^2.5.0 + version: 2.5.0 + devDependencies: + '@types/markdown-it': + specifier: ^14.1.2 + version: 14.1.2 + '@types/node': + specifier: ^22.0.0 + version: 22.0.0 + +packages: + + '@algolia/cache-browser-local-storage@4.24.0': + resolution: {integrity: sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==} + + '@algolia/cache-common@4.24.0': + resolution: {integrity: sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==} + + '@algolia/cache-in-memory@4.24.0': + resolution: {integrity: sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==} + + '@algolia/client-account@4.24.0': + resolution: {integrity: sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==} + + '@algolia/client-analytics@4.24.0': + resolution: {integrity: sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==} + + '@algolia/client-common@4.24.0': + resolution: {integrity: sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==} + + '@algolia/client-personalization@4.24.0': + resolution: {integrity: sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==} + + '@algolia/client-search@4.24.0': + resolution: {integrity: sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==} + + '@algolia/logger-common@4.24.0': + resolution: {integrity: sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==} + + '@algolia/logger-console@4.24.0': + resolution: {integrity: sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==} + + '@algolia/recommend@4.24.0': + resolution: {integrity: sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==} + + '@algolia/requester-browser-xhr@4.24.0': + resolution: {integrity: sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==} + + '@algolia/requester-common@4.24.0': + resolution: {integrity: sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==} + + '@algolia/requester-node-http@4.24.0': + resolution: {integrity: sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==} + + '@algolia/transporter@4.24.0': + resolution: {integrity: sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==} + + '@esbuild/aix-ppc64@0.23.0': + resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.0': + resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.0': + resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.0': + resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.0': + resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.0': + resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.0': + resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.0': + resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.0': + resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.0': + resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.0': + resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.0': + resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.0': + resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.0': + resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.0': + resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.0': + resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.0': + resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.0': + resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.0': + resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.0': + resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.0': + resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.0': + resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.0': + resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.0': + resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@octokit/auth-token@5.1.1': + resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.2': + resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.1': + resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.1.1': + resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + + '@octokit/plugin-paginate-rest@11.3.3': + resolution: {integrity: sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.2.4': + resolution: {integrity: sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.4': + resolution: {integrity: sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==} + engines: {node: '>= 18'} + + '@octokit/request@9.1.3': + resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==} + engines: {node: '>= 18'} + + '@octokit/rest@21.0.1': + resolution: {integrity: sha512-RWA6YU4CqK0h0J6tfYlUFnH3+YgBADlxaHXaKSG+BVr2y4PTfbU2tlKuaQoQZ83qaTbi4CUxLNAmbAqR93A6mQ==} + engines: {node: '>= 18'} + + '@octokit/types@13.5.0': + resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@supabase/auth-js@2.64.4': + resolution: {integrity: sha512-9ITagy4WP4FLl+mke1rchapOH0RQpf++DI+WSG2sO1OFOZ0rW3cwAM0nCrMOxu+Zw4vJ4zObc08uvQrXx590Tg==} + + '@supabase/functions-js@2.4.1': + resolution: {integrity: sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.15.8': + resolution: {integrity: sha512-YunjXpoQjQ0a0/7vGAvGZA2dlMABXFdVI/8TuVKtlePxyT71sl6ERl6ay1fmIeZcqxiuFQuZw/LXUuStUG9bbg==} + + '@supabase/realtime-js@2.10.2': + resolution: {integrity: sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==} + + '@supabase/storage-js@2.6.0': + resolution: {integrity: sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==} + + '@supabase/supabase-js@2.45.0': + resolution: {integrity: sha512-j66Mfs8RhzCQCKxKogAFQYH9oNhRmgIdKk6pexguI2Oc7hi+nL9UNJug5aL1tKnBdaBM3h65riPLQSdL6sWa3Q==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/node@22.0.0': + resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} + + '@types/phoenix@1.6.5': + resolution: {integrity: sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==} + + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + + algoliasearch@4.24.0: + resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.23.0: + resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} + engines: {node: '>=18'} + hasBin: true + + execa@9.3.0: + resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + engines: {node: ^18.19.0 || >=20.5.0} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + foreground-child@3.2.1: + resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.7.6: + resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + human-signals@7.0.0: + resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} + engines: {node: '>=18.18.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + markdown-it-front-matter@0.2.4: + resolution: {integrity: sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + engines: {node: '>=18'} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + radashi@12.2.0-beta.83909af: + resolution: {integrity: sha512-nPQqyCP3iDi9I85WypLXJgPCP1XTeXwBP+vhRnnFWDoKOeslwJy5IXdM84L8sE0YJ1zypicJfvo+wPwFvWgYyw==} + engines: {node: '>=16.0.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsx@4.17.0: + resolution: {integrity: sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==} + engines: {node: '>=18.0.0'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ultrahtml@1.5.3: + resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} + + undici-types@6.11.1: + resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} + + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.5.0: + resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} + engines: {node: '>= 14'} + hasBin: true + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + +snapshots: + + '@algolia/cache-browser-local-storage@4.24.0': + dependencies: + '@algolia/cache-common': 4.24.0 + + '@algolia/cache-common@4.24.0': {} + + '@algolia/cache-in-memory@4.24.0': + dependencies: + '@algolia/cache-common': 4.24.0 + + '@algolia/client-account@4.24.0': + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/transporter': 4.24.0 + + '@algolia/client-analytics@4.24.0': + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + + '@algolia/client-common@4.24.0': + dependencies: + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + + '@algolia/client-personalization@4.24.0': + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + + '@algolia/client-search@4.24.0': + dependencies: + '@algolia/client-common': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/transporter': 4.24.0 + + '@algolia/logger-common@4.24.0': {} + + '@algolia/logger-console@4.24.0': + dependencies: + '@algolia/logger-common': 4.24.0 + + '@algolia/recommend@4.24.0': + dependencies: + '@algolia/cache-browser-local-storage': 4.24.0 + '@algolia/cache-common': 4.24.0 + '@algolia/cache-in-memory': 4.24.0 + '@algolia/client-common': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/logger-common': 4.24.0 + '@algolia/logger-console': 4.24.0 + '@algolia/requester-browser-xhr': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/requester-node-http': 4.24.0 + '@algolia/transporter': 4.24.0 + + '@algolia/requester-browser-xhr@4.24.0': + dependencies: + '@algolia/requester-common': 4.24.0 + + '@algolia/requester-common@4.24.0': {} + + '@algolia/requester-node-http@4.24.0': + dependencies: + '@algolia/requester-common': 4.24.0 + + '@algolia/transporter@4.24.0': + dependencies: + '@algolia/cache-common': 4.24.0 + '@algolia/logger-common': 4.24.0 + '@algolia/requester-common': 4.24.0 + + '@esbuild/aix-ppc64@0.23.0': + optional: true + + '@esbuild/android-arm64@0.23.0': + optional: true + + '@esbuild/android-arm@0.23.0': + optional: true + + '@esbuild/android-x64@0.23.0': + optional: true + + '@esbuild/darwin-arm64@0.23.0': + optional: true + + '@esbuild/darwin-x64@0.23.0': + optional: true + + '@esbuild/freebsd-arm64@0.23.0': + optional: true + + '@esbuild/freebsd-x64@0.23.0': + optional: true + + '@esbuild/linux-arm64@0.23.0': + optional: true + + '@esbuild/linux-arm@0.23.0': + optional: true + + '@esbuild/linux-ia32@0.23.0': + optional: true + + '@esbuild/linux-loong64@0.23.0': + optional: true + + '@esbuild/linux-mips64el@0.23.0': + optional: true + + '@esbuild/linux-ppc64@0.23.0': + optional: true + + '@esbuild/linux-riscv64@0.23.0': + optional: true + + '@esbuild/linux-s390x@0.23.0': + optional: true + + '@esbuild/linux-x64@0.23.0': + optional: true + + '@esbuild/netbsd-x64@0.23.0': + optional: true + + '@esbuild/openbsd-arm64@0.23.0': + optional: true + + '@esbuild/openbsd-x64@0.23.0': + optional: true + + '@esbuild/sunos-x64@0.23.0': + optional: true + + '@esbuild/win32-arm64@0.23.0': + optional: true + + '@esbuild/win32-ia32@0.23.0': + optional: true + + '@esbuild/win32-x64@0.23.0': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@octokit/auth-token@5.1.1': {} + + '@octokit/core@6.1.2': + dependencies: + '@octokit/auth-token': 5.1.1 + '@octokit/graphql': 8.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.1': + dependencies: + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.1.1': + dependencies: + '@octokit/request': 9.1.3 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@22.2.0': {} + + '@octokit/plugin-paginate-rest@11.3.3(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.5.0 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + + '@octokit/plugin-rest-endpoint-methods@13.2.4(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.5.0 + + '@octokit/request-error@6.1.4': + dependencies: + '@octokit/types': 13.5.0 + + '@octokit/request@9.1.3': + dependencies: + '@octokit/endpoint': 10.1.1 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/rest@21.0.1': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.2) + '@octokit/plugin-rest-endpoint-methods': 13.2.4(@octokit/core@6.1.2) + + '@octokit/types@13.5.0': + dependencies: + '@octokit/openapi-types': 22.2.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@supabase/auth-js@2.64.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.15.8': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.10.2': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.5 + '@types/ws': 8.5.12 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.6.0': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.45.0': + dependencies: + '@supabase/auth-js': 2.64.4 + '@supabase/functions-js': 2.4.1 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.15.8 + '@supabase/realtime-js': 2.10.2 + '@supabase/storage-js': 2.6.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + + '@types/node@22.0.0': + dependencies: + undici-types: 6.11.1 + + '@types/phoenix@1.6.5': {} + + '@types/ws@8.5.12': + dependencies: + '@types/node': 22.0.0 + + algoliasearch@4.24.0: + dependencies: + '@algolia/cache-browser-local-storage': 4.24.0 + '@algolia/cache-common': 4.24.0 + '@algolia/cache-in-memory': 4.24.0 + '@algolia/client-account': 4.24.0 + '@algolia/client-analytics': 4.24.0 + '@algolia/client-common': 4.24.0 + '@algolia/client-personalization': 4.24.0 + '@algolia/client-search': 4.24.0 + '@algolia/logger-common': 4.24.0 + '@algolia/logger-console': 4.24.0 + '@algolia/recommend': 4.24.0 + '@algolia/requester-browser-xhr': 4.24.0 + '@algolia/requester-common': 4.24.0 + '@algolia/requester-node-http': 4.24.0 + '@algolia/transporter': 4.24.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + before-after-hook@3.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@4.1.1: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + esbuild@0.23.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.0 + '@esbuild/android-arm': 0.23.0 + '@esbuild/android-arm64': 0.23.0 + '@esbuild/android-x64': 0.23.0 + '@esbuild/darwin-arm64': 0.23.0 + '@esbuild/darwin-x64': 0.23.0 + '@esbuild/freebsd-arm64': 0.23.0 + '@esbuild/freebsd-x64': 0.23.0 + '@esbuild/linux-arm': 0.23.0 + '@esbuild/linux-arm64': 0.23.0 + '@esbuild/linux-ia32': 0.23.0 + '@esbuild/linux-loong64': 0.23.0 + '@esbuild/linux-mips64el': 0.23.0 + '@esbuild/linux-ppc64': 0.23.0 + '@esbuild/linux-riscv64': 0.23.0 + '@esbuild/linux-s390x': 0.23.0 + '@esbuild/linux-x64': 0.23.0 + '@esbuild/netbsd-x64': 0.23.0 + '@esbuild/openbsd-arm64': 0.23.0 + '@esbuild/openbsd-x64': 0.23.0 + '@esbuild/sunos-x64': 0.23.0 + '@esbuild/win32-arm64': 0.23.0 + '@esbuild/win32-ia32': 0.23.0 + '@esbuild/win32-x64': 0.23.0 + + execa@9.3.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.3 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 7.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 5.3.0 + pretty-ms: 9.1.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.0.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.2.1: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.7.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.2.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + + human-signals@7.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + lines-and-columns@1.2.4: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lru-cache@10.4.3: {} + + markdown-it-front-matter@0.2.4: {} + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + mdurl@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + mri@1.2.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-assign@4.1.1: {} + + package-json-from-dist@1.0.0: {} + + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picomatch@2.3.1: {} + + pirates@4.0.6: {} + + pretty-ms@9.1.0: + dependencies: + parse-ms: 4.0.0 + + punycode.js@2.3.1: {} + + queue-microtask@1.2.3: {} + + radashi@12.2.0-beta.83909af: {} + + resolve-pkg-maps@1.0.0: {} + + reusify@1.0.4: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strip-final-newline@4.0.0: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-interface-checker@0.1.13: {} + + tsx@4.17.0: + dependencies: + esbuild: 0.23.0 + get-tsconfig: 4.7.6 + optionalDependencies: + fsevents: 2.3.3 + + uc.micro@2.1.0: {} + + ultrahtml@1.5.3: {} + + undici-types@6.11.1: {} + + universal-user-agent@7.0.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + ws@8.18.0: {} + + yaml@2.5.0: {} + + yoctocolors@2.1.1: {} diff --git a/scripts/radashi-db/seed-merged-functions.ts b/scripts/radashi-db/seed-merged-functions.ts new file mode 100644 index 00000000..cb2cab80 --- /dev/null +++ b/scripts/radashi-db/seed-merged-functions.ts @@ -0,0 +1,88 @@ +import { execa } from 'execa' +import glob from 'fast-glob' +import fs from 'node:fs/promises' +import path from 'node:path' +import { algolia, supabase } from './src/db' +import { renderPageMarkdown } from './src/util/markdown' + +async function seedMergedFunctions() { + const rootDir = new URL('../../', import.meta.url).pathname + const sourceFiles = await glob(['src/**/*.ts', '!src/*.ts'], { cwd: rootDir }) + + for (const sourceFile of sourceFiles) { + // How romantic. + const firstDateCmd = await execa( + 'git', + ['log', '--format=%aI', '--reverse', sourceFile], + { cwd: rootDir }, + ) + + const firstCommitDate = firstDateCmd.stdout.split('\n')[0] + console.log('Got original commit date', firstCommitDate) + + const firstAuthorCmd = await execa( + 'git', + ['log', '--format=%an', '--reverse', '--follow', sourceFile], + { cwd: rootDir }, + ) + const firstCommitAuthor = firstAuthorCmd.stdout.split('\n')[0] + console.log('Got original commit author', firstCommitAuthor) + + const docFile = sourceFile.replace(/^src/, 'docs').replace(/\.ts$/, '.mdx') + const docFilePath = path.join(rootDir, docFile) + + let name: string | undefined + let description: string | undefined + let documentation: string | undefined + try { + interface PageData { + title: string + description?: string + } + const docContent = await fs.readFile(docFilePath, 'utf-8') + const renderResult = await renderPageMarkdown(docContent) + if (renderResult) { + name = renderResult.data?.title + description = renderResult.data?.description + documentation = renderResult.text + } + } catch (error) { + console.error(`Error reading or parsing ${docFile}:`, error) + continue + } + + name ??= path.basename(sourceFile, '.ts') + + const record = { + ref: 'radashi-org/radashi#main', + group: sourceFile.split('/').slice(1, -1).join('/'), + name, + description, + documentation, + committed_at: firstCommitDate, + committed_by: firstCommitAuthor, + } + + console.log('Would insert', record) + + try { + await supabase.from('merged_functions').insert(record) + console.log(`Inserted ${name} into Supabase`) + } catch (error) { + console.error(`Error inserting ${name} into Supabase:`, error) + } + + try { + const index = algolia.initIndex('merged_functions') + await index.saveObject({ + objectID: name, + ...record, + }) + console.log(`Inserted ${name} into Algolia`) + } catch (error) { + console.error(`Error inserting ${name} into Algolia:`, error) + } + } +} + +seedMergedFunctions() diff --git a/scripts/radashi-db/seed-proposed-functions.ts b/scripts/radashi-db/seed-proposed-functions.ts new file mode 100644 index 00000000..392bc9c9 --- /dev/null +++ b/scripts/radashi-db/seed-proposed-functions.ts @@ -0,0 +1,182 @@ +import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest' +import { parallel } from 'radashi' +import { registerPullRequest } from './src/register-pr' +import { bottleneck } from './src/util/bottleneck' + +async function seedProposedFunctions() { + const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, + }) + + const limit = bottleneck( + { interval: 1000 / 5 }, + async (fn: () => Promise) => fn(), + ) + + type ListPullsResponse = RestEndpointMethodTypes['pulls']['list']['response'] + type GetPullResponse = RestEndpointMethodTypes['pulls']['get']['response'] + + type ListPullsResponseItem = ListPullsResponse['data'][number] + type GetPullResponseData = GetPullResponse['data'] + type PullRequest = GetPullResponseData | ListPullsResponseItem + type PullRequestRepo = Exclude + + async function onPullRequest(pr: { + number: PullRequest['number'] + head: { + sha: PullRequest['head']['sha'] + ref: PullRequest['head']['ref'] + repo: { + owner: PullRequestRepo['owner'] + name: PullRequestRepo['name'] + } | null + } + title: PullRequest['title'] + state: PullRequest['state'] + draft?: PullRequest['draft'] + merged_at: PullRequest['merged_at'] + // mergeable: PullRequest[''] + user: PullRequest['user'] + labels: { name: string }[] + }) { + console.log(`Processing PR ${pr.number}`) + + const status = + pr.state === 'open' + ? pr.draft + ? 'draft' + : 'open' + : pr.merged_at != null + ? 'merged' + : 'closed' + + const { data: files } = await limit(() => + octokit.pulls.listFiles({ + owner: 'radashi-org', + repo: 'radashi', + pull_number: pr.number, + }), + ) + + const repoOwner = pr.head.repo?.owner + const repoName = pr.head.repo?.name + + // Get the status of CI checks for the PR + const { data: checkRuns } = await limit(() => + octokit.checks.listForRef({ + owner: 'radashi-org', + repo: 'radashi', + ref: pr.head.sha, + }), + ) + + const checksPassed = checkRuns.check_runs.every( + run => run.conclusion === 'success', + ) + + await registerPullRequest(pr.number, { + sha: pr.head.sha, + files, + status, + branch: pr.head.ref, + owner: repoOwner?.login, + ownerAvatarUrl: repoOwner?.avatar_url, + repo: repoName, + breaking: pr.labels.some(label => label.name === 'BREAKING CHANGE'), + checksPassed, + getApprovalRating: async () => { + const { data: reactions } = await limit(() => + octokit.reactions.listForIssue({ + owner: 'radashi-org', + repo: 'radashi', + issue_number: pr.number, + }), + ) + + return reactions.filter(reaction => reaction.content === '+1').length + }, + getIssueBody: async () => { + const { data } = await limit(() => + octokit.issues.get({ + owner: 'radashi-org', + repo: 'radashi', + issue_number: pr.number, + }), + ) + return data.body ?? null + }, + getCommit: async (ref, owner = 'radashi-org', repo = 'radashi') => { + const { data: commit } = await limit(() => + octokit.repos.getCommit({ + owner, + repo, + ref, + }), + ) + return { + sha: commit.sha, + date: commit.commit.author?.date, + author: commit.commit.author?.name, + } + }, + getFileContent: async path => { + const { data } = await limit(() => + octokit.repos.getContent({ + owner: 'radashi-org', + repo: 'radashi', + path, + ref: pr.head.sha, + }), + ) + if (!('content' in data)) { + throw new Error(`File ${path} has no content`) + } + return Buffer.from(data.content, 'base64').toString('utf-8') + }, + }) + } + + try { + for await (const response of octokit.paginate.iterator( + octokit.rest.pulls.list, + { + owner: 'radashi-org', + repo: 'radashi', + state: 'open', + per_page: 100, + }, + )) { + await parallel(3, response.data, onPullRequest) + } + + for await (const response of octokit.paginate.iterator( + octokit.rest.search.issuesAndPullRequests, + { + q: 'repo:radashi-org/radashi is:pr is:closed is:unmerged label:"open library" feat', + per_page: 100, + }, + )) { + await parallel(3, response.data, async result => { + if (!result.pull_request) { + return + } + + const { data: pr } = await limit(() => + octokit.pulls.get({ + owner: 'radashi-org', + repo: 'radashi', + pull_number: result.number, + }), + ) + + onPullRequest(pr) + }) + } + + console.log('Radashi database seeded successfully') + } catch (error) { + console.error('Error seeding Radashi database:', error) + } +} + +seedProposedFunctions() diff --git a/scripts/radashi-db/src/db.ts b/scripts/radashi-db/src/db.ts new file mode 100644 index 00000000..5876dc1a --- /dev/null +++ b/scripts/radashi-db/src/db.ts @@ -0,0 +1,9 @@ +import { createClient as createSupabase } from '@supabase/supabase-js' +import { default as createAlgolia } from 'algoliasearch' + +export const supabase = createSupabase( + 'https://yucyhkpmrdbucitpovyj.supabase.co', + process.env.SUPABASE_KEY!, +) + +export const algolia = createAlgolia('7YYOXVJ9K7', process.env.ALGOLIA_KEY!) diff --git a/scripts/radashi-db/src/register-pr.ts b/scripts/radashi-db/src/register-pr.ts new file mode 100644 index 00000000..03da181f --- /dev/null +++ b/scripts/radashi-db/src/register-pr.ts @@ -0,0 +1,274 @@ +import path from 'node:path' +import { memo } from 'radashi' +import { algolia, supabase } from './db' +import { renderPageMarkdown } from './util/markdown' + +type PrFileStatus = + | 'added' + | 'removed' + | 'modified' + | 'renamed' + | 'copied' + | 'changed' + | 'unchanged' + +type PrStatus = 'draft' | 'open' | 'merged' | 'closed' + +interface PrFile { + status: PrFileStatus + filename: string + sha: string +} + +export interface Commit { + sha: string + date: string | undefined + author: string | undefined +} + +interface Context { + sha: string + /** + * The branch name (e.g. "main") + * + * @default "main" + */ + branch?: string + /** + * The owner name (e.g. "radashi-org" or your GitHub username) + * + * @default "radashi-org" + */ + owner?: string + ownerAvatarUrl?: string + /** + * The repository name (e.g. "radashi") + * + * @default "radashi" + */ + repo?: string + status: PrStatus + breaking: boolean + checksPassed: boolean + files: PrFile[] + getCommit: (ref: string, owner?: string, repo?: string) => Promise + getFileContent: (file: string) => Promise + getApprovalRating: () => Promise + getIssueBody: () => Promise + console?: Pick +} + +/** + * Register a pull request with the Radashi database. + * + * @param prNumber - The pull request number. + * @param context - The context object containing PR information. + */ +export const registerPullRequest = async ( + prNumber: number, + context: Context, +): Promise => { + const { console = globalThis.console } = context + const getCommit = memo(context.getCommit) + + try { + const newFunctions = context.files.filter( + file => + file.status === 'added' && + file.filename.startsWith('src/') && + file.filename.endsWith('.ts') && + !file.filename.endsWith('.test.ts'), + ) + + if (newFunctions.length === 0) { + console.log('No new functions added in this PR.') + return + } + + const newFunctionNames = newFunctions.map(f => + path.basename(f.filename, '.ts'), + ) + + console.log(`Fetching existing functions for PR #${prNumber}`) + const { data: existingFunctions, error: fetchError } = await supabase + .from('proposed_functions') + .select('name') + .eq('pr_number', prNumber) + + if (fetchError) { + console.error('Error fetching existing functions:', fetchError) + return + } + + const existingFunctionNames = existingFunctions.map(f => f.name) + + for (const name of existingFunctionNames) { + if (!newFunctionNames.includes(name)) { + console.log(`Deleting ${name}#${prNumber} from Radashi database`) + } + + await supabase + .from('proposed_functions') + .delete() + .match({ pr_number: prNumber, name }) + } + + let body: string | null | undefined + let approvalRating: number | null = null + + for (const file of newFunctions) { + const name = path.basename(file.filename, '.ts') + + if (context.status === 'merged') { + if (!existingFunctionNames.includes(name)) { + console.log(`Deleting ${name}#${prNumber} from Radashi database`) + + await supabase + .from('proposed_functions') + .delete() + .match({ pr_number: prNumber, name }) + + // Delete from Algolia + if (process.env.ALGOLIA_KEY) { + const index = algolia.initIndex('proposed_functions') + + try { + console.log(`Deleting ${name}#${prNumber} from Algolia index`) + await index.deleteObject(`${name}#${prNumber}`) + } catch (error) { + console.error( + `Error deleting ${name}#${prNumber} from Algolia:`, + error, + ) + } + } + } + continue + } + + const docFilename = file.filename + .replace(/^src/, 'docs') + .replace(/\.ts$/, '.mdx') + + let documentation: string | null = null + try { + documentation = await context.getFileContent(docFilename) + } catch { + console.log(`Documentation file not found for ${name}#${prNumber}`) + } + + let description: string | null = null + + if (documentation == null) { + if (body === undefined) { + body = await context.getIssueBody() + } + + if (body !== null) { + console.log(`Falling back to PR body for ${name}#${prNumber}`) + + const sections = body.split(/^(#+\s+)/m) + const summaryIndex = sections.findIndex( + (_section, index) => + index % 2 === 1 && + /^(Summary|Description)\b/.test(sections[index + 1]), + ) + if (summaryIndex !== -1) { + const summaryDepth = sections[summaryIndex].trim().length + const summaryContent = sections + .slice(summaryIndex + 1) + .join('') + .split(new RegExp(`^#{1,${summaryDepth}}\\s+`, 'm'))[0] + .trim() + + documentation = summaryContent + } else { + console.log( + `No "Summary" or "Description" section found in PR body for ${name}#${prNumber}`, + ) + } + } + } + + if (documentation) { + interface PageData { + title: string + description?: string + } + const renderResult = await renderPageMarkdown(documentation) + if (renderResult) { + documentation = renderResult.text + if (renderResult.data?.description) { + description = renderResult.data.description + } + } + } + + if (approvalRating === null) { + try { + approvalRating = await context.getApprovalRating() + } catch (error) { + console.error( + `Error getting approval rating for ${name}#${prNumber}:`, + error, + ) + approvalRating = 0 + } + } + + const commit = await getCommit(context.sha, context.owner, context.repo) + + const record = { + ref: `${context.owner ?? 'radashi-org'}/${context.repo ?? 'radashi'}#${context.branch ?? 'main'}`, + group: file.filename.split('/').slice(1, -1).join('/'), + name, + pr_number: prNumber, + approval_rating: approvalRating, + documentation, + status: context.status, + breaking: context.breaking, + description, + committed_at: commit?.date, + committed_by: commit?.author, + checks_passed: context.checksPassed, + pr_author: + context.owner && context.ownerAvatarUrl + ? { login: context.owner, avatar_url: context.ownerAvatarUrl } + : undefined, + } + + const { error } = await supabase.from('proposed_functions').insert(record) + + if (error) { + console.error( + `Error inserting ${name}#${prNumber} into Supabase:`, + error, + ) + } else { + console.log(`Successfully registered ${name}#${prNumber} in Supabase`) + } + + // Insert record into Algolia + if (process.env.ALGOLIA_KEY) { + try { + const index = algolia.initIndex('proposed_functions') + + const algoliaRecord = { + objectID: `${name}#${prNumber}`, + ...record, + } + + await index.saveObject(algoliaRecord) + console.log(`Successfully indexed ${name}#${prNumber} in Algolia`) + } catch (algoliaError) { + console.error( + `Error indexing ${name}#${prNumber} in Algolia:`, + algoliaError, + ) + } + } + } + } catch (error) { + console.error('Error processing PR:', error) + } +} diff --git a/scripts/radashi-db/src/util/bottleneck.ts b/scripts/radashi-db/src/util/bottleneck.ts new file mode 100644 index 00000000..06d268eb --- /dev/null +++ b/scripts/radashi-db/src/util/bottleneck.ts @@ -0,0 +1,158 @@ +declare const setTimeout: (callback: () => void, delay: number) => unknown + +/** + * The options for the `bottleneck` function. + * + * @see https://radashi-org.github.io/reference/async/bottleneck + */ +export interface BottleneckOptions { + /** + * The maximum number of calls to allow per interval. + * + * @default 1 + */ + max?: number + /** + * The interval at which to allow the maximum number of calls. + */ + interval: number + /** + * The maximum number of calls to allow at once. + * + * @default Infinity + */ + concurrency?: number +} + +/** + * The return type of the `bottleneck` function. + * + * @see https://radashi-org.github.io/reference/async/bottleneck + */ +export type BottledFunction any> = Fn & { + /** + * Prevent any throttled calls from ever running. + * + * Currently executing calls are not affected. + * + * @example + * ```ts + * const fn = bottleneck({ interval: 1000 }, () => console.log('hello')) + * fn() // <- Runs immediately + * fn() // <- Queued + * + * fn.cancel() + * // Now, your function won't run until another call. + * ``` + */ + cancel(): void +} + +/** + * Limit the rate at which a function is called. + * + * A maximum of `max` calls are allowed per `interval` milliseconds. + * + * Use the `concurrency` option for limiting the number of concurrent + * calls. + * + * @see https://radashi-org.github.io/reference/async/bottleneck + * @example + * ```ts + * const double = bottleneck( + * { max: 1, interval: 1000 }, + * async (x: number) => x * 2 + * ) + * double(1) // <- Runs immediately + * double(2) // <- Will wait 1 second + * double(3) // <- Will wait 2 seconds + * ``` + * @example Limited concurrency + * ```ts + * const double = bottleneck( + * { max: 5, interval: 1000, concurrency: 1 }, + * async (x: number) => x * 2 + * ) + * double(1) // <- Runs immediately + * double(2) // <- Will wait for 1 to finish + * double(3) // <- Will wait for 2 to finish + * ``` + */ +export function bottleneck any>( + { + max = 1, + interval, + concurrency = Number.POSITIVE_INFINITY, + }: BottleneckOptions, + fn: Fn, +): BottledFunction { + let numCalls = 0 + let numRunning = 0 + let startTime: number | undefined + + type QueueItem = { + args: TArgs + resolve: (value: TReturn | PromiseLike) => void + reject: (error: any) => void + } + + const queue: QueueItem[] = [] + + async function run(input: TArgs | QueueItem) { + const now = Date.now() + startTime ??= now + + if (now - startTime >= interval) { + startTime = now + numCalls = 0 + } + + if (numCalls < max && numRunning < concurrency) { + // If this is the first call, schedule the flush. + if (!numCalls && Number.isFinite(interval)) { + setTimeout(next, interval) + } + + let result: any + + numCalls++ + numRunning++ + try { + const args = Array.isArray(input) ? input : input.args + result = await fn(...args) + } catch (error) { + if (Array.isArray(input)) { + throw error + } + return input.reject(error) + } finally { + numRunning-- + next() + } + + return Array.isArray(input) ? result : input.resolve(result) + } + + if (Array.isArray(input)) { + // Return a queue promise for the throttled call. + return new Promise((resolve, reject) => { + queue.push({ args: input, resolve, reject }) + }) + } + + // Return the unused queue item to the queue. + queue.unshift(input) + } + + // This function is called when the interval has elapsed and after + // every finished call. + const next = () => queue.length && run(queue.shift()!) + + const bottled: BottledFunction = (...args) => run(args) + + bottled.cancel = () => { + queue.length = 0 + } + + return bottled +} diff --git a/scripts/radashi-db/src/util/markdown.ts b/scripts/radashi-db/src/util/markdown.ts new file mode 100644 index 00000000..94b909b3 --- /dev/null +++ b/scripts/radashi-db/src/util/markdown.ts @@ -0,0 +1,56 @@ +import MarkdownIt from 'markdown-it' +import mdFrontMatter from 'markdown-it-front-matter' +import { transform } from 'ultrahtml' +import sanitize from 'ultrahtml/transformers/sanitize' +import * as yaml from 'yaml' + +interface RenderedPage { + text: string + data: Data | undefined +} + +/** + * Render a markdown page and extract the front matter. + * + * If the front matter contains a `title` key, it will be added as a + * level 1 heading to the top of the rendered markdown. + */ +export async function renderPageMarkdown( + text: string | null | undefined, +): Promise | null> { + if (text == null) { + return null + } + + const markdown = new MarkdownIt({ html: true }) + + let data!: Data | undefined + markdown.use(mdFrontMatter, (text: string) => { + data = yaml.parse(text) + }) + + try { + text = markdown.render(text) + if (data && 'title' in data) { + text = markdown.render('# ' + data.title + '\n\n') + '\n\n' + text + } + } catch (error) { + if (error instanceof Error) { + error.message = 'Markdown renderer failed. ' + error.message + } + throw error + } + + if (text) { + try { + text = await transform(text, [sanitize({ allowComments: true })]) + } catch (error) { + if (error instanceof Error) { + error.message = 'Sanitization failed. ' + error.message + } + throw error + } + } + + return { text, data } +} diff --git a/scripts/radashi-db/tsconfig.json b/scripts/radashi-db/tsconfig.json new file mode 100644 index 00000000..bdb959bf --- /dev/null +++ b/scripts/radashi-db/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "moduleResolution": "node", + "outDir": "./dist/tmp", + "noEmitOnError": true, + "declaration": true, + "emitDeclarationOnly": true, + "module": "esnext", + "target": "esnext", + "lib": ["es2020"], + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh new file mode 100755 index 00000000..d411cf32 --- /dev/null +++ b/scripts/release-notes.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if [ ! -d "scripts/release-notes/node_modules" ]; then + echo "Node modules not found. Installing dependencies..." + pnpm install -C scripts/release-notes +fi + +pnpm -s tsx scripts/release-notes/release-notes.ts "$@" diff --git a/scripts/release-notes/README.md b/scripts/release-notes/README.md new file mode 100644 index 00000000..37e34c6c --- /dev/null +++ b/scripts/release-notes/README.md @@ -0,0 +1,5 @@ +This script collects all "fix" and "feat" commits since the last stable version. Then it concatenates their `git log -p` outputs into a single file, which can be fed into an LLM for high-quality release notes. + +``` +tsx scripts/release-notes/release-notes.ts > release-notes.diff +``` diff --git a/scripts/release-notes/dedent.ts b/scripts/release-notes/dedent.ts new file mode 100644 index 00000000..ab9e3099 --- /dev/null +++ b/scripts/release-notes/dedent.ts @@ -0,0 +1,86 @@ +import { isArray } from 'radashi' + +/** + * Remove indentation from a string. The given string is expected to + * be consistently indented (i.e. the leading whitespace of the first + * non-empty line is the minimum required for all non-empty lines). + * + * If the `indent` argument is nullish, the indentation is detected + * from the first non-empty line. Detection is cheap and robust for + * most use cases, so you should only set an explicit `indent` if + * necessary. + * + * @see https://radashi-org.github.io/reference/string/dedent + * @example + * ```ts + * // This is indented with 4 spaces. + * const input = ` + * Hello + * World + * ` + * + * // Explicit indentation + * dedent(input, ' ') + * // => ' Hello\n World\n' + * + * // Detected indentation + * dedent(input) + * // => 'Hello\nWorld\n' + * + * // Tagged template strings + * const str = dedent` + * Foo ${1 + 1} + * Bar ${2 * 2} + * ` + * // => 'Foo 2\nBar 4' + * ``` + */ +export function dedent( + template: TemplateStringsArray, + ...values: unknown[] +): string + +export function dedent(text: string, indent?: string | null): string + +export function dedent( + text: string | TemplateStringsArray, + ...values: unknown[] +): string { + // Support tagged template strings + if (isArray(text)) { + if (values.length > 0) { + return dedent( + text.reduce((acc, input, i) => { + let value = String(values[i] ?? '') + + // Detect the indentation before this embedded string. + const indent = + value.includes('\n') && input.match(/[ \t]*(?=[^\n]*$)/)?.[0] + + // Ensure the multi-line, embedded string can be correctly + // dedented. + if (indent) { + value = value.replace(/\n(?=[^\n]*?\S)/g, '\n' + indent) + } + + return acc + input + value + }, ''), + ) + } + + text = text[0] + } + + const indent = values[0] ?? detectIndent(text) + const output = indent + ? text.replace(new RegExp(`^${indent}`, 'gm'), '') + : text + + // Remove the first and last lines (if empty). + return output.replace(/^[ \t]*\n|\n[ \t]*$/g, '') +} + +// Find the indentation of the first non-empty line. +function detectIndent(text: string) { + return text.match(/^[ \t]*(?=\S)/m)?.[0] +} diff --git a/scripts/release-notes/package.json b/scripts/release-notes/package.json new file mode 100644 index 00000000..91570ac7 --- /dev/null +++ b/scripts/release-notes/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "dependencies": { + "@anthropic-ai/sdk": "^0.25.0", + "@types/node": "^22.1.0", + "execa": "^9.3.0", + "mri": "^1.2.0", + "octokit": "^4.0.2", + "radashi": "12.2.0-beta.7fb6e89", + "tsx": "^4.17.0" + } +} \ No newline at end of file diff --git a/scripts/release-notes/pnpm-lock.yaml b/scripts/release-notes/pnpm-lock.yaml new file mode 100644 index 00000000..b2d85b46 --- /dev/null +++ b/scripts/release-notes/pnpm-lock.yaml @@ -0,0 +1,969 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@anthropic-ai/sdk': + specifier: ^0.25.0 + version: 0.25.0 + '@types/node': + specifier: ^22.1.0 + version: 22.1.0 + execa: + specifier: ^9.3.0 + version: 9.3.0 + mri: + specifier: ^1.2.0 + version: 1.2.0 + octokit: + specifier: ^4.0.2 + version: 4.0.2 + radashi: + specifier: 12.2.0-beta.7fb6e89 + version: 12.2.0-beta.7fb6e89 + tsx: + specifier: ^4.17.0 + version: 4.17.0 + +packages: + + '@anthropic-ai/sdk@0.25.0': + resolution: {integrity: sha512-nill47zLtX+Tx6YacvuML1WMA7vuFA+I2uGh+8mGig4D3HwKFLThf45cS1itcmYVnjUQ+ohrSnkRyu1t+Xbh2w==} + + '@esbuild/aix-ppc64@0.23.0': + resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.23.0': + resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.23.0': + resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.23.0': + resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.23.0': + resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.0': + resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.23.0': + resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.0': + resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.23.0': + resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.23.0': + resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.23.0': + resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.23.0': + resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.23.0': + resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.23.0': + resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.0': + resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.23.0': + resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.23.0': + resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.23.0': + resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.0': + resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.0': + resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.23.0': + resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.23.0': + resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.23.0': + resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.23.0': + resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@octokit/app@15.1.0': + resolution: {integrity: sha512-TkBr7QgOmE6ORxvIAhDbZsqPkF7RSqTY4pLTtUQCvr6dTXqvi2fFo46q3h1lxlk/sGMQjqyZ0kEahkD/NyzOHg==} + engines: {node: '>= 18'} + + '@octokit/auth-app@7.1.0': + resolution: {integrity: sha512-cazGaJPSgeZ8NkVYeM/C5l/6IQ5vZnsI8p1aMucadCkt/bndI+q+VqwrlnWbASRmenjOkf1t1RpCKrif53U8gw==} + engines: {node: '>= 18'} + + '@octokit/auth-oauth-app@8.1.1': + resolution: {integrity: sha512-5UtmxXAvU2wfcHIPPDWzVSAWXVJzG3NWsxb7zCFplCWEmMCArSZV0UQu5jw5goLQXbFyOr5onzEH37UJB3zQQg==} + engines: {node: '>= 18'} + + '@octokit/auth-oauth-device@7.1.1': + resolution: {integrity: sha512-HWl8lYueHonuyjrKKIup/1tiy0xcmQCdq5ikvMO1YwkNNkxb6DXfrPjrMYItNLyCP/o2H87WuijuE+SlBTT8eg==} + engines: {node: '>= 18'} + + '@octokit/auth-oauth-user@5.1.1': + resolution: {integrity: sha512-rRkMz0ErOppdvEfnemHJXgZ9vTPhBuC6yASeFaB7I2yLMd7QpjfrL1mnvRPlyKo+M6eeLxrKanXJ9Qte29SRsw==} + engines: {node: '>= 18'} + + '@octokit/auth-token@5.1.1': + resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} + engines: {node: '>= 18'} + + '@octokit/auth-unauthenticated@6.1.0': + resolution: {integrity: sha512-zPSmfrUAcspZH/lOFQnVnvjQZsIvmfApQH6GzJrkIunDooU1Su2qt2FfMTSVPRp7WLTQyC20Kd55lF+mIYaohQ==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.2': + resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.1': + resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.1.1': + resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==} + engines: {node: '>= 18'} + + '@octokit/oauth-app@7.1.3': + resolution: {integrity: sha512-EHXbOpBkSGVVGF1W+NLMmsnSsJRkcrnVmDKt0TQYRBb6xWfWzoi9sBD4DIqZ8jGhOWO/V8t4fqFyJ4vDQDn9bg==} + engines: {node: '>= 18'} + + '@octokit/oauth-authorization-url@7.1.1': + resolution: {integrity: sha512-ooXV8GBSabSWyhLUowlMIVd9l1s2nsOGQdlP2SQ4LnkEsGXzeCvbSbCPdZThXhEFzleGPwbapT0Sb+YhXRyjCA==} + engines: {node: '>= 18'} + + '@octokit/oauth-methods@5.1.2': + resolution: {integrity: sha512-C5lglRD+sBlbrhCUTxgJAFjWgJlmTx5bQ7Ch0+2uqRjYv7Cfb5xpX4WuSC9UgQna3sqRGBL9EImX9PvTpMaQ7g==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + + '@octokit/openapi-webhooks-types@8.3.0': + resolution: {integrity: sha512-vKLsoR4xQxg4Z+6rU/F65ItTUz/EXbD+j/d4mlq2GW8TsA4Tc8Kdma2JTAAJ5hrKWUQzkR/Esn2fjsqiVRYaQg==} + + '@octokit/plugin-paginate-graphql@5.2.2': + resolution: {integrity: sha512-7znSVvlNAOJisCqAnjN1FtEziweOHSjPGAuc5W58NeGNAr/ZB57yCsjQbXDlWsVryA7hHQaEQPcBbJYFawlkyg==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-paginate-rest@11.3.3': + resolution: {integrity: sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.2.4': + resolution: {integrity: sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-retry@7.1.1': + resolution: {integrity: sha512-G9Ue+x2odcb8E1XIPhaFBnTTIrrUDfXN05iFXiqhR+SeeeDMMILcAnysOsxUpEWcQp2e5Ft397FCXTcPkiPkLw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-throttling@9.3.1': + resolution: {integrity: sha512-Qd91H4liUBhwLB2h6jZ99bsxoQdhgPk6TdwnClPyTBSDAdviGPceViEgUwj+pcQDmB/rfAXAXK7MTochpHM3yQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^6.0.0 + + '@octokit/request-error@6.1.4': + resolution: {integrity: sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==} + engines: {node: '>= 18'} + + '@octokit/request@9.1.3': + resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==} + engines: {node: '>= 18'} + + '@octokit/types@13.5.0': + resolution: {integrity: sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==} + + '@octokit/webhooks-methods@5.1.0': + resolution: {integrity: sha512-yFZa3UH11VIxYnnoOYCVoJ3q4ChuSOk2IVBBQ0O3xtKX4x9bmKb/1t+Mxixv2iUhzMdOl1qeWJqEhouXXzB3rQ==} + engines: {node: '>= 18'} + + '@octokit/webhooks@13.3.0': + resolution: {integrity: sha512-TUkJLtI163Bz5+JK0O+zDkQpn4gKwN+BovclUvCj6pI/6RXrFqQvUMRS2M+Rt8Rv0qR3wjoMoOPmpJKeOh0nBg==} + engines: {node: '>= 18'} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@types/aws-lambda@8.10.143': + resolution: {integrity: sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg==} + + '@types/node-fetch@2.6.11': + resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + + '@types/node@18.19.43': + resolution: {integrity: sha512-Mw/YlgXnyJdEwLoFv2dpuJaDFriX+Pc+0qOBJ57jC1H6cDxIj2xc5yUrdtArDVG0m+KV6622a4p2tenEqB3C/g==} + + '@types/node@22.1.0': + resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + + bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + esbuild@0.23.0: + resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} + engines: {node: '>=18'} + hasBin: true + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + execa@9.3.0: + resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + engines: {node: ^18.19.0 || >=20.5.0} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.7.6: + resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==} + + human-signals@7.0.0: + resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} + engines: {node: '>=18.18.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + octokit@4.0.2: + resolution: {integrity: sha512-wbqF4uc1YbcldtiBFfkSnquHtECEIpYD78YUXI6ri1Im5OO2NLo6ZVpRdbJpdnpZ05zMrVPssNiEo6JQtea+Qg==} + engines: {node: '>= 18'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + engines: {node: '>=18'} + + radashi@12.2.0-beta.7fb6e89: + resolution: {integrity: sha512-zBJajRnOB3vnX1mBNOMBrfblepRxSJ7/peTndW0wnfvw53mZ2Ra77ZXU9XCfpyO003y6cjJBqEioLX39jhyAsg==} + engines: {node: '>=16.0.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tsx@4.17.0: + resolution: {integrity: sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==} + engines: {node: '>=18.0.0'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.13.0: + resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} + + universal-github-app-jwt@2.2.0: + resolution: {integrity: sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==} + + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + +snapshots: + + '@anthropic-ai/sdk@0.25.0': + dependencies: + '@types/node': 18.19.43 + '@types/node-fetch': 2.6.11 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@esbuild/aix-ppc64@0.23.0': + optional: true + + '@esbuild/android-arm64@0.23.0': + optional: true + + '@esbuild/android-arm@0.23.0': + optional: true + + '@esbuild/android-x64@0.23.0': + optional: true + + '@esbuild/darwin-arm64@0.23.0': + optional: true + + '@esbuild/darwin-x64@0.23.0': + optional: true + + '@esbuild/freebsd-arm64@0.23.0': + optional: true + + '@esbuild/freebsd-x64@0.23.0': + optional: true + + '@esbuild/linux-arm64@0.23.0': + optional: true + + '@esbuild/linux-arm@0.23.0': + optional: true + + '@esbuild/linux-ia32@0.23.0': + optional: true + + '@esbuild/linux-loong64@0.23.0': + optional: true + + '@esbuild/linux-mips64el@0.23.0': + optional: true + + '@esbuild/linux-ppc64@0.23.0': + optional: true + + '@esbuild/linux-riscv64@0.23.0': + optional: true + + '@esbuild/linux-s390x@0.23.0': + optional: true + + '@esbuild/linux-x64@0.23.0': + optional: true + + '@esbuild/netbsd-x64@0.23.0': + optional: true + + '@esbuild/openbsd-arm64@0.23.0': + optional: true + + '@esbuild/openbsd-x64@0.23.0': + optional: true + + '@esbuild/sunos-x64@0.23.0': + optional: true + + '@esbuild/win32-arm64@0.23.0': + optional: true + + '@esbuild/win32-ia32@0.23.0': + optional: true + + '@esbuild/win32-x64@0.23.0': + optional: true + + '@octokit/app@15.1.0': + dependencies: + '@octokit/auth-app': 7.1.0 + '@octokit/auth-unauthenticated': 6.1.0 + '@octokit/core': 6.1.2 + '@octokit/oauth-app': 7.1.3 + '@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2) + '@octokit/types': 13.5.0 + '@octokit/webhooks': 13.3.0 + + '@octokit/auth-app@7.1.0': + dependencies: + '@octokit/auth-oauth-app': 8.1.1 + '@octokit/auth-oauth-user': 5.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + lru-cache: 10.4.3 + universal-github-app-jwt: 2.2.0 + universal-user-agent: 7.0.2 + + '@octokit/auth-oauth-app@8.1.1': + dependencies: + '@octokit/auth-oauth-device': 7.1.1 + '@octokit/auth-oauth-user': 5.1.1 + '@octokit/request': 9.1.3 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/auth-oauth-device@7.1.1': + dependencies: + '@octokit/oauth-methods': 5.1.2 + '@octokit/request': 9.1.3 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/auth-oauth-user@5.1.1': + dependencies: + '@octokit/auth-oauth-device': 7.1.1 + '@octokit/oauth-methods': 5.1.2 + '@octokit/request': 9.1.3 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/auth-token@5.1.1': {} + + '@octokit/auth-unauthenticated@6.1.0': + dependencies: + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + + '@octokit/core@6.1.2': + dependencies: + '@octokit/auth-token': 5.1.1 + '@octokit/graphql': 8.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.1': + dependencies: + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.1.1': + dependencies: + '@octokit/request': 9.1.3 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/oauth-app@7.1.3': + dependencies: + '@octokit/auth-oauth-app': 8.1.1 + '@octokit/auth-oauth-user': 5.1.1 + '@octokit/auth-unauthenticated': 6.1.0 + '@octokit/core': 6.1.2 + '@octokit/oauth-authorization-url': 7.1.1 + '@octokit/oauth-methods': 5.1.2 + '@types/aws-lambda': 8.10.143 + universal-user-agent: 7.0.2 + + '@octokit/oauth-authorization-url@7.1.1': {} + + '@octokit/oauth-methods@5.1.2': + dependencies: + '@octokit/oauth-authorization-url': 7.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + + '@octokit/openapi-types@22.2.0': {} + + '@octokit/openapi-webhooks-types@8.3.0': {} + + '@octokit/plugin-paginate-graphql@5.2.2(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + + '@octokit/plugin-paginate-rest@11.3.3(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.5.0 + + '@octokit/plugin-rest-endpoint-methods@13.2.4(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.5.0 + + '@octokit/plugin-retry@7.1.1(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + bottleneck: 2.19.5 + + '@octokit/plugin-throttling@9.3.1(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.5.0 + bottleneck: 2.19.5 + + '@octokit/request-error@6.1.4': + dependencies: + '@octokit/types': 13.5.0 + + '@octokit/request@9.1.3': + dependencies: + '@octokit/endpoint': 10.1.1 + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + universal-user-agent: 7.0.2 + + '@octokit/types@13.5.0': + dependencies: + '@octokit/openapi-types': 22.2.0 + + '@octokit/webhooks-methods@5.1.0': {} + + '@octokit/webhooks@13.3.0': + dependencies: + '@octokit/openapi-webhooks-types': 8.3.0 + '@octokit/request-error': 6.1.4 + '@octokit/webhooks-methods': 5.1.0 + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@types/aws-lambda@8.10.143': {} + + '@types/node-fetch@2.6.11': + dependencies: + '@types/node': 22.1.0 + form-data: 4.0.0 + + '@types/node@18.19.43': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.1.0': + dependencies: + undici-types: 6.13.0 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + agentkeepalive@4.5.0: + dependencies: + humanize-ms: 1.2.1 + + asynckit@0.4.0: {} + + before-after-hook@3.0.2: {} + + bottleneck@2.19.5: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + delayed-stream@1.0.0: {} + + esbuild@0.23.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.0 + '@esbuild/android-arm': 0.23.0 + '@esbuild/android-arm64': 0.23.0 + '@esbuild/android-x64': 0.23.0 + '@esbuild/darwin-arm64': 0.23.0 + '@esbuild/darwin-x64': 0.23.0 + '@esbuild/freebsd-arm64': 0.23.0 + '@esbuild/freebsd-x64': 0.23.0 + '@esbuild/linux-arm': 0.23.0 + '@esbuild/linux-arm64': 0.23.0 + '@esbuild/linux-ia32': 0.23.0 + '@esbuild/linux-loong64': 0.23.0 + '@esbuild/linux-mips64el': 0.23.0 + '@esbuild/linux-ppc64': 0.23.0 + '@esbuild/linux-riscv64': 0.23.0 + '@esbuild/linux-s390x': 0.23.0 + '@esbuild/linux-x64': 0.23.0 + '@esbuild/netbsd-x64': 0.23.0 + '@esbuild/openbsd-arm64': 0.23.0 + '@esbuild/openbsd-x64': 0.23.0 + '@esbuild/sunos-x64': 0.23.0 + '@esbuild/win32-arm64': 0.23.0 + '@esbuild/win32-ia32': 0.23.0 + '@esbuild/win32-x64': 0.23.0 + + event-target-shim@5.0.1: {} + + execa@9.3.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.3 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 7.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 5.3.0 + pretty-ms: 9.1.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.0.0 + + form-data-encoder@1.7.2: {} + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + fsevents@2.3.3: + optional: true + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.7.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + human-signals@7.0.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.0.0: {} + + isexe@2.0.0: {} + + lru-cache@10.4.3: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mri@1.2.0: {} + + ms@2.1.3: {} + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + octokit@4.0.2: + dependencies: + '@octokit/app': 15.1.0 + '@octokit/core': 6.1.2 + '@octokit/oauth-app': 7.1.3 + '@octokit/plugin-paginate-graphql': 5.2.2(@octokit/core@6.1.2) + '@octokit/plugin-paginate-rest': 11.3.3(@octokit/core@6.1.2) + '@octokit/plugin-rest-endpoint-methods': 13.2.4(@octokit/core@6.1.2) + '@octokit/plugin-retry': 7.1.1(@octokit/core@6.1.2) + '@octokit/plugin-throttling': 9.3.1(@octokit/core@6.1.2) + '@octokit/request-error': 6.1.4 + '@octokit/types': 13.5.0 + + parse-ms@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pretty-ms@9.1.0: + dependencies: + parse-ms: 4.0.0 + + radashi@12.2.0-beta.7fb6e89: {} + + resolve-pkg-maps@1.0.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + strip-final-newline@4.0.0: {} + + tr46@0.0.3: {} + + tsx@4.17.0: + dependencies: + esbuild: 0.23.0 + get-tsconfig: 4.7.6 + optionalDependencies: + fsevents: 2.3.3 + + undici-types@5.26.5: {} + + undici-types@6.13.0: {} + + universal-github-app-jwt@2.2.0: {} + + universal-user-agent@7.0.2: {} + + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + yoctocolors@2.1.1: {} diff --git a/scripts/release-notes/release-notes.ts b/scripts/release-notes/release-notes.ts new file mode 100644 index 00000000..00eff53a --- /dev/null +++ b/scripts/release-notes/release-notes.ts @@ -0,0 +1,335 @@ +import { Anthropic } from '@anthropic-ai/sdk' +import { execa } from 'execa' +import mri from 'mri' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { uid } from 'radashi' +import { dedent } from './dedent' + +/** + * Generate release notes using `git log -p` output and Anthropic + * Claude. + * + * Required Environment Variables: + * + * - ANTHROPIC_API_KEY + * - GITHUB_TOKEN (only required if --publish is used) + * + * Usage: + * + * ```sh + * # Generate release notes for changes between the last two + * # stable versions. + * scripts/release-notes.sh + * + * # Generate release notes for changes between the last stable + * # version and a given commit ref (e.g. HEAD). + * scripts/release-notes.sh + * ``` + * + * Options: + * + * -o, --output \ + * Write the release notes to a file. + * + * --publish \ + * Publish the release notes to GitHub. + * + * --draft \ + * Publish the release notes as a draft. + * + * --prerelease \ + * Publish the release notes as a prerelease. + * + * --limit \ + * Limit the number of commits to include in each section. (For testing purposes) + */ +async function main() { + if (!process.env.ANTHROPIC_API_KEY) { + console.error('Error: ANTHROPIC_API_KEY is not set') + process.exit(1) + } + + const argv = mri(process.argv.slice(2)) + const outFile = argv.o || argv.output + const limit = argv.limit ? +argv.limit : Number.POSITIVE_INFINITY + + if (!outFile && !argv.publish) { + console.error('Error: No --output file or --publish flag provided') + process.exit(1) + } else if (outFile && argv.publish) { + console.error('Error: Cannot use --output file and --publish flag together') + process.exit(1) + } + + if (argv.publish && !process.env.GITHUB_TOKEN) { + console.error('Error: GITHUB_TOKEN is not set') + process.exit(1) + } + + const version: string = JSON.parse( + fs.readFileSync('package.json', 'utf8'), + ).version + + log('Generating release notes for v' + version) + + /** + * If no commit ref is provided, use the latest two tags. + */ + let commitRange: string + if (argv._.length) { + commitRange = 'v' + version + '..' + argv._[0] + } else { + const tags = await execa('git', [ + 'tag', + '--format=%(refname:short)', + '--sort=-version:refname', + '-n', + 'v*', + ]).then(result => + result.stdout + .split('\n') + .filter(tag => !tag.includes('-')) + .slice(0, 2), + ) + + const [currentVersion, previousVersion] = tags + commitRange = previousVersion + '..' + currentVersion + } + + const commits = await execa('git', [ + 'log', + '--format=%H %s', + commitRange, + ]).then(result => + result.stdout + .trim() + .split('\n') + .map(line => { + const [, sha, message] = /^(\w+) (.+)$/.exec(line)! + return { sha, message, diff: '' } + }), + ) + + log('Grouping commits and fetching diffs...') + + const sections = getSections() + for (const commit of commits) { + for (const section of sections) { + if ( + section.match.test(commit.message) && + !section.exclude?.test(commit.message) + ) { + const diff = await execa('git', [ + 'log', + '-p', + commit.sha + '^..' + commit.sha, + 'src', + 'docs', + ]) + commit.diff = diff.stdout + section.commits ??= [] + section.commits.push(commit) + } + } + } + + const anthropic = new Anthropic() + + for (const section of sections) { + if (!section.commits?.length) { + continue + } + + log('Generating release notes for', section.name) + + section.commits.length = Math.min(section.commits.length, limit) + + const linkLocation = + section.noun === 'feature' || section.noun === 'fix' + ? 'immediately after the heading' + : 'at the end' + + const rules = [ + `You're tasked with writing in-depth release notes (using Markdown) in a professional tone.`, + 'Never converse with me.', + 'Always mention every change I give you.', + `Always link to the relevant PR (or the commit if there's no PR) ${linkLocation} of each ${section.noun} in a format like "[→ PR #110](…)" or "[→ commit {short-hash}](…)". The GitHub URL is "https://github.com/radashi-org/radashi".`, + `Never include headings like "Release Notes" or "v1.0.0".`, + ...section.rules(section.noun), + ] + + log('Sending request to Anthropic...') + + const response = await anthropic.messages.create({ + model: 'claude-3-haiku-20240307', + max_tokens: 4096, + messages: [ + { + role: 'user', + content: dedent` + - ${rules.join('\n- ')} + + The following changes are from \`git log -p\`: + + + ${section.commits.map(commit => commit.diff).join('\n\n')} + + `, + }, + ], + }) + + const [message] = response.content + if (message.type !== 'text') { + console.error('Expected a text message, got:', message) + process.exit(1) + } + + section.notes = message.text + } + + let notes = sections + .filter(section => section.notes) + .map(section => `## ${section.name}\n\n${section.notes}`) + .join('\n\n') + + const tmpFile = path.join(os.tmpdir(), 'release-notes.' + uid(20) + '.md') + fs.writeFileSync(tmpFile, notes) + + try { + const editor = await getPreferredEditor() + log('Opening', tmpFile, 'with', editor) + + // Open the generated release notes in the user's preferred text editor + await execa(editor, [tmpFile], { stdio: 'inherit' }) + + // Read the potentially modified content after the editor is closed + notes = fs.readFileSync(tmpFile, 'utf-8') + } finally { + fs.unlinkSync(tmpFile) + } + + if (argv.publish) { + const { Octokit } = await import('octokit') + const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) + + log('Publishing release notes for version', version) + + try { + await octokit.rest.repos.createRelease({ + owner: 'radashi-org', + repo: 'radashi', + tag_name: `v${version}`, + name: `v${version}`, + body: notes, + draft: !!argv.draft, + prerelease: !!argv.prerelease, + }) + + log('Successfully published release notes to GitHub') + } catch (error) { + console.error('Failed to publish release notes:', error) + process.exit(1) + } + } else { + fs.writeFileSync(outFile, notes) + log('Saved release notes to', path.resolve(outFile)) + } +} + +main() + +function getSections(): Section[] { + const getFormattingRules = (noun: string) => [ + `Use an H4 (####) for the heading of each ${noun}.`, + 'Headings must be in sentence case.', + noun === 'feature' && + `Each heading must describe what the ${noun} enables, not simply what the change is (e.g. "Allow throttled function to be triggered immediately" instead of "Add trigger method to throttle function").`, + 'Be concise but not vague.', + 'Omit prefixes like "Fix:" from headings.', + `The paragraph(s) after each heading must describe the ${noun} in more detail (but be brief where possible).`, + ] + + const getCodeExampleRules = (noun: string) => [ + `Every ${noun} needs a concise code example to showcase it.`, + 'Never preface examples with "Example:" or similar.', + dedent` + In each example, import the functions or types like this: + \`\`\`ts + import { sum } from 'radashi' + \`\`\` + `, + ] + + const getBulletedListRules = (noun: string) => [ + `Describe each ${noun} in a bulleted list, without being vague.`, + 'Never use headings.', + 'Only give me the bulleted list. No prefacing like “Here are the changes” or similar.', + ] + + return [ + { + name: 'Features', + match: /^feat/, + exclude: /\((types|perf)\)/, + noun: 'feature', + rules: noun => [ + ...getFormattingRules(noun), + ...getCodeExampleRules(noun), + ], + }, + { + name: 'Bug Fixes', + match: /^fix/, + exclude: /\((types|perf)\)/, + noun: 'fix', + rules: noun => [ + ...getFormattingRules(noun), + ...getCodeExampleRules(noun), + ], + }, + { + name: 'Performance', + match: /^(perf|\w+\(perf\))/, + noun: 'improvement', + rules: noun => [...getBulletedListRules(noun)], + }, + { + name: 'Types', + match: /^(fix|feat)\(types\)/, + noun: 'change', + rules: noun => [...getBulletedListRules(noun)], + }, + ] +} + +function log(message: string, ...args: any[]) { + console.log('• ' + message, ...args) +} + +async function getPreferredEditor() { + const { stdout: gitEditor } = await execa('git', [ + 'config', + '--global', + 'core.editor', + ]) + return gitEditor.trim() || process.env.EDITOR || 'nano' +} + +type Section = { + name: string + match: RegExp + exclude?: RegExp + noun: string + rules: (noun: string) => (string | false)[] + commits?: Commit[] + notes?: string +} + +type Commit = { + sha: string + message: string + diff: string +} diff --git a/scripts/test-branch.sh b/scripts/test-branch.sh index d1f21488..2241e16f 100644 --- a/scripts/test-branch.sh +++ b/scripts/test-branch.sh @@ -39,7 +39,7 @@ while [ $index -lt ${#SOURCE_FILES[@]} ]; do func_name=$(basename $file) func_name=${func_name/.ts/} - if command -v rgz &> /dev/null; then + if command -v rg &> /dev/null; then IMPORTERS=$(rg "import[^}]*?\b$func_name\b" -U -l -- src) for importer in $IMPORTERS; do diff --git a/scripts/weigh-changed.sh b/scripts/weigh-changed.sh deleted file mode 100644 index 6d1d694f..00000000 --- a/scripts/weigh-changed.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/bin/bash -set -e - -# Try using gh to get the target branch, otherwise use 'main' as a fallback. -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 "origin/$TARGET_BRANCH" HEAD -- 'src/*/*.ts') - -# Separate the file statuses and file names. -FILE_STATUSES=() -FILE_NAMES=() - -i=0 -for item in $CHANGES; do - if [ $((i % 2)) -eq 0 ]; then - FILE_STATUSES+=($item) - else - FILE_NAMES+=($item) - fi - i=$((i + 1)) -done - -# If esbuild is not found, install it. -if ! which esbuild > /dev/null 2>&1; then - # If not CI, confirm the installation: - if [[ -z "$CI" ]]; then - read -n 1 -p "Install esbuild to pnpm global store? (Y/n) " confirm - echo - if [ "$confirm" == "n" ]; then - exit 1 - fi - - pnpm install -g esbuild - else - pnpm -s install -g esbuild - fi -fi - -# The sizes of the changed functions before the changes. -PREV_SIZES=() - -# Collect previous sizes if there are no uncommitted changes or it's CI. -if [ -n "$CI" ] || [ -z "$(git status -s)" ]; then - git checkout "$TARGET_BRANCH" &> /dev/null - - i=0 - for file in "${FILE_NAMES[@]}"; do - status=${FILE_STATUSES[$i]} - - if [ ! -f "$file" ]; then - PREV_SIZES+=(0) - else - PREV_SIZES+=($(esbuild --bundle --minify "$file" | wc -c | tr -d '[:space:]')) - fi - - i=$((i + 1)) - done - - git checkout - &> /dev/null -fi - -column_count=2 -if [ ${#PREV_SIZES[@]} -gt 0 ]; then - for size in "${PREV_SIZES[@]}"; do - if [ "$size" -ne 0 ]; then - column_count=3 - break - fi - done -fi - -echo -e "\n\n" - -if [[ -n "$CI" ]]; then - if [ "$column_count" -gt 2 ]; then - echo "| Status | File | Size | Difference (%) |" - echo "|---|---|---|---|" - else - echo "| Status | File | Size |" - echo "|---|---|---|" - fi -fi - -i=0 -for file in "${FILE_NAMES[@]}"; do - status=${FILE_STATUSES[$i]} - - if [ "$column_count" -gt 2 ]; then - prev_bytes=${PREV_SIZES[$i]} - fi - - if [ "$status" == "D" ]; then - bytes=0 - else - bytes=$(esbuild --bundle --minify "$file" | wc -c | tr -d '[:space:]') - fi - - diff=$((bytes - prev_bytes)) - diff=$(if [ "$diff" -ge 0 ]; then echo "+"; fi)$diff - - if [ "$column_count" -gt 2 ] && [ "$prev_bytes" -ne 0 ]; then - ratio=$(echo "scale=0; (100 * $bytes / $prev_bytes) - 100" | bc -l) - ratio=$(if [ "$ratio" -ge 0 ]; then echo "+"; fi)$ratio - ratio=" ($ratio%)" - else - ratio="" - fi - - if [[ -n "$CI" ]]; then - if [ "$column_count" -gt 2 ]; then - echo "| $status | \`$file\` | $bytes[^1337] | $diff$ratio |" - else - echo "| $status | \`$file\` | $bytes[^1337] |" - fi - else - if [ "$column_count" -gt 2 ] && [ "$prev_bytes" -ne 0 ]; then - echo "$file: $bytes bytes ($diff bytes)$ratio" - else - echo "$file: $bytes bytes" - fi - fi - - i=$((i + 1)) -done - -echo "" -echo "[^1337]: Function size includes the \`import\` dependencies of the function." -echo "" diff --git a/src/array/shift.ts b/src/array/shift.ts index 2bfbfcec..67fec418 100644 --- a/src/array/shift.ts +++ b/src/array/shift.ts @@ -10,15 +10,15 @@ * shift([1, 2, 3], -1) // [2, 3, 1] * ``` */ -export function shift(arr: T[], n: number): T[] { +export function shift(arr: readonly T[], n: number): T[] { if (arr.length === 0) { - return arr + return [...arr] } const shiftNumber = n % arr.length if (shiftNumber === 0) { - return arr + return [...arr] } return [...arr.slice(-shiftNumber, arr.length), ...arr.slice(0, -shiftNumber)] diff --git a/src/async/AggregateError.ts b/src/async/AggregateError.ts index 6c0d630f..42eed88c 100644 --- a/src/async/AggregateError.ts +++ b/src/async/AggregateError.ts @@ -11,19 +11,20 @@ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError */ const AggregateErrorOrPolyfill: AggregateErrorConstructor = - // eslint-disable-next-line compat/compat - globalThis.AggregateError ?? - (class AggregateError extends Error { - errors: Error[] - constructor(errors: Error[] = []) { - super() - const name = errors.find(e => e.name)?.name ?? '' - this.name = `AggregateError(${name}...)` - this.message = `AggregateError with ${errors.length} errors` - this.stack = errors.find(e => e.stack)?.stack ?? this.stack - this.errors = errors - } - } as unknown as AggregateErrorConstructor) + /* @__PURE__ */ (() => + // eslint-disable-next-line compat/compat + globalThis.AggregateError ?? + (class AggregateError extends Error { + errors: Error[] + constructor(errors: Error[] = []) { + super() + const name = errors.find(e => e.name)?.name ?? '' + this.name = `AggregateError(${name}...)` + this.message = `AggregateError with ${errors.length} errors` + this.stack = errors.find(e => e.stack)?.stack ?? this.stack + this.errors = errors + } + } as unknown as AggregateErrorConstructor))() // Do not export directly, so the polyfill isn't renamed to // `AggregateError2` at build time (which ESBuild does to prevent diff --git a/src/curry/once.ts b/src/curry/once.ts index 5aa962f0..47090729 100644 --- a/src/curry/once.ts +++ b/src/curry/once.ts @@ -12,23 +12,7 @@ export interface OnceFunction< [onceSymbol]?: Return | typeof onceSymbol } -/** - * Create a function that runs at most once, no matter how many times - * it's called. If it was already called before, returns the result - * from the first call. This is a lighter version of `memo()`. - * - * To allow your `once`-wrapped function to be called again, see the - * `once.reset` function. - * - * @see https://radashi-org.github.io/reference/curry/once - * @example - * ```ts - * const fn = once(() => Math.random()) - * fn() // 0.5 - * fn() // 0.5 - * ``` - */ -export const once: { +type OnceImplementation = { ( fn: (this: This, ...args: Args) => Return, ): (this: This, ...args: Args) => Return @@ -47,17 +31,38 @@ export const once: { * ``` */ reset(fn: OnceFunction): void -} = fn => { - const onceFn = function (...args: any) { - if (onceFn[onceSymbol] === onceSymbol) { - onceFn[onceSymbol] = fn.apply(this as any, args) - } - return onceFn[onceSymbol] - } as OnceFunction - onceFn[onceSymbol] = onceSymbol - return onceFn as typeof fn } -once.reset = (fn: OnceFunction): void => { - fn[onceSymbol] = onceSymbol -} +/** + * Create a function that runs at most once, no matter how many times + * it's called. If it was already called before, returns the result + * from the first call. This is a lighter version of `memo()`. + * + * To allow your `once`-wrapped function to be called again, see the + * `once.reset` function. + * + * @see https://radashi-org.github.io/reference/curry/once + * @example + * ```ts + * const fn = once(() => Math.random()) + * fn() // 0.5 + * fn() // 0.5 + * ``` + */ +export const once: OnceImplementation = /* @__PURE__ */ (() => { + const once: OnceImplementation = fn => { + const onceFn = function (...args: any) { + if (onceFn[onceSymbol] === onceSymbol) { + onceFn[onceSymbol] = fn.apply(this as any, args) + } + return onceFn[onceSymbol] + } as OnceFunction + + onceFn[onceSymbol] = onceSymbol + return onceFn as typeof fn + } + once.reset = (fn: OnceFunction): void => { + fn[onceSymbol] = onceSymbol + } + return once +})() diff --git a/src/curry/throttle.ts b/src/curry/throttle.ts index 42b0f6ff..546ada72 100644 --- a/src/curry/throttle.ts +++ b/src/curry/throttle.ts @@ -1,4 +1,5 @@ declare const setTimeout: (fn: () => void, ms: number) => unknown +declare const clearTimeout: (timer: unknown) => void export type ThrottledFunction = { (...args: TArgs): void @@ -6,6 +7,29 @@ export type ThrottledFunction = { * Checks if there is any invocation throttled */ isThrottled(): boolean + /** + * Call the throttled function immediately, ignoring any throttling + * that may be in effect. After, a new throttled call will be allowed + * after the interval has passed. + * + * @example + * ```ts + * const logMessage = (message: string) => { + * console.log(`Message: ${message}`) + * } + * const throttledLog = throttle({ interval: 1000 }, logMessage) + * + * throttledLog('First call') // Logs immediately + * throttledLog('Throttled') // Doesn't log (throttled) + * + * // Force a log, bypassing the throttle + * throttledLog.trigger('Forced log') // Logs immediately + * + * // Check if it's still throttled + * throttledLog.isThrottled() // => true + * ``` + */ + trigger(...args: TArgs): void } /** @@ -26,25 +50,38 @@ export type ThrottledFunction = { * ``` */ export function throttle( - { interval }: { interval: number }, + { interval, trailing }: { interval: number; trailing?: boolean }, func: (...args: TArgs) => any, ): ThrottledFunction { - let ready = true - let timer: unknown = undefined + let timer: unknown + let lastCalled = 0 + let trailingArgs: TArgs | undefined const throttled: ThrottledFunction = (...args: TArgs) => { - if (!ready) { - return + if (!isThrottled()) { + trigger(...args) + } else if (trailing) { + trailingArgs = args } - func(...args) - ready = false - timer = setTimeout(() => { - ready = true - timer = undefined - }, interval) - } - throttled.isThrottled = () => { - return timer !== undefined } + + const isThrottled = () => Date.now() - lastCalled < interval + throttled.isThrottled = isThrottled + + const trigger = (throttled.trigger = (...args: TArgs) => { + func(...args) + lastCalled = Date.now() + + if (trailing) { + trailingArgs = undefined + + clearTimeout(timer) + timer = setTimeout( + () => trailingArgs && trigger(...trailingArgs), + interval, + ) + } + }) + return throttled } diff --git a/src/mod.ts b/src/mod.ts index 426f63a5..ae47f9bf 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -100,6 +100,7 @@ export * from './string/camel.ts' export * from './string/capitalize.ts' export * from './string/dash.ts' export * from './string/pascal.ts' +export * from './string/similarity.ts' export * from './string/snake.ts' export * from './string/template.ts' export * from './string/title.ts' diff --git a/src/number/sum.ts b/src/number/sum.ts index 556f90f2..7dc539d1 100644 --- a/src/number/sum.ts +++ b/src/number/sum.ts @@ -1,6 +1,8 @@ /** - * Sum all numbers in an array. Optionally provide a function to - * convert objects in the array to number values. + * Add up numbers related to an array in 1 of 2 ways: + * 1. Sum all numbers in an array of numbers + * 2. Sum all numbers returned by a callback function that maps + * each item in an array to a number. * * @see https://radashi-org.github.io/reference/array/sum * @example @@ -14,14 +16,14 @@ * {value: 3} * ], (item) => item.value) * // => 6 + * + * sum([true, false, true], (item) => item ? 1 : 0) + * // => 2 * ``` */ -export function sum(array: readonly T[]): number -export function sum( - array: readonly T[], - fn: (item: T) => number, -): number -export function sum( +export function sum(array: readonly number[]): number +export function sum(array: readonly T[], fn: (item: T) => number): number +export function sum( array: readonly any[], fn?: (item: T) => number, ): number { diff --git a/src/object/cloneDeep.ts b/src/object/cloneDeep.ts index 76f588aa..9e516023 100644 --- a/src/object/cloneDeep.ts +++ b/src/object/cloneDeep.ts @@ -42,7 +42,7 @@ export interface CloningStrategy { ) => T | null } -export const DefaultCloningStrategy = { +export const DefaultCloningStrategy: CloningStrategy = { cloneMap( input: Map, track: (newParent: Map) => Map, diff --git a/src/object/set.ts b/src/object/set.ts index 468e02c7..180307c5 100644 --- a/src/object/set.ts +++ b/src/object/set.ts @@ -23,9 +23,6 @@ export function set( return initial } - // NOTE: One day, when structuredClone has more compatability use it - // to clone the value - // https://developer.mozilla.org/en-US/docs/Web/API/structuredClone const root: any = clone(initial) const keys = path.match(/[^.[\]]+/g) if (keys) { diff --git a/src/string/similarity.ts b/src/string/similarity.ts new file mode 100644 index 00000000..7d5facf8 --- /dev/null +++ b/src/string/similarity.ts @@ -0,0 +1,85 @@ +/** + * Calculate the similarity between two strings using the Levenshtein + * distance algorithm. + * + * One thing to note is that the argument order is unimportant. The + * algorithm will always return the same result regardless of the + * order of the arguments. + * + * Adapted from + * [@fabiospampinato/tiny-levenshtein](https://github.com/fabiospampinato/tiny-levenshtein) + * with ❤️. + * + * @see https://radashi-org.github.io/reference/string/similarity + * @example + * ```ts + * similarity('abc', 'abc') // 0 + * similarity('a', 'b') // 1 + * similarity('ab', 'ac') // 1 + * similarity('ac', 'bc') // 1 + * similarity('abc', 'axc') // 1 + * similarity('kitten', 'sitting') // 3 + * ``` + */ +export function similarity(str1: string, str2: string): number { + // Early return if strings are identical + if (str1 === str2) { + return 0 + } + + // Find common prefix and suffix + let start = 0 + let end1 = str1.length - 1 + let end2 = str2.length - 1 + + while (start <= end1 && start <= end2 && str1[start] === str2[start]) { + start++ + } + + while (end1 >= start && end2 >= start && str1[end1] === str2[end2]) { + end1-- + end2-- + } + + // Calculate lengths of trimmed strings + const length1 = end1 - start + 1 + const length2 = end2 - start + 1 + + // Handle cases where one string is a substring of the other + if (length1 === 0) { + return length2 + } + if (length2 === 0) { + return length1 + } + + const numRows = length1 + 1 + const numColumns = length2 + 1 + + const distances = new Array(numRows * numColumns).fill(0) + + for (let x = 1; x < numColumns; x++) { + distances[x] = x + } + for (let y = 1; y < numRows; y++) { + distances[y * numColumns] = y + } + + for (let x = 1; x < numColumns; x++) { + for (let y = 1; y < numRows; y++) { + const i = y * numColumns + x + distances[i] = Math.min( + // Cost of a deletion. + distances[i - numColumns] + 1, + // Cost of an insertion. + distances[i - 1] + 1, + // Cost of a substitution. + distances[i - numColumns - 1] + + (str1[start + y - 1] === str2[start + x - 1] ? 0 : 1), + ) + } + } + + // Return the Levenshtein distance + return distances[length1 * numColumns + length2] +} diff --git a/src/typed/isArray.ts b/src/typed/isArray.ts index 35dfb4df..e19b0726 100644 --- a/src/typed/isArray.ts +++ b/src/typed/isArray.ts @@ -10,7 +10,7 @@ import type { StrictExtract } from 'radashi' * isArray('hello') // => false * ``` */ -export const isArray = Array.isArray as ( +export const isArray = /* @__PURE__ */ (() => Array.isArray)() as ( value: Input, ) => value is ExtractArray diff --git a/src/typed/isInt.ts b/src/typed/isInt.ts index 537d5822..2273b575 100644 --- a/src/typed/isInt.ts +++ b/src/typed/isInt.ts @@ -8,4 +8,6 @@ * isInt(0.1) // => false * ``` */ -export const isInt = Number.isInteger as (value: unknown) => value is number +export const isInt = /* @__PURE__ */ (() => Number.isInteger)() as ( + value: unknown, +) => value is number diff --git a/tests/curry/throttle.test.ts b/tests/curry/throttle.test.ts index edfacce1..692670cf 100644 --- a/tests/curry/throttle.test.ts +++ b/tests/curry/throttle.test.ts @@ -2,6 +2,7 @@ import * as _ from 'radashi' describe('throttle', () => { const interval = 600 + const smidge = 10 beforeEach(() => { vi.useFakeTimers() @@ -14,25 +15,127 @@ describe('throttle', () => { func() func() expect(calls).toBe(1) - vi.advanceTimersByTime(interval + 10) + vi.advanceTimersByTime(interval + smidge) func() func() func() expect(calls).toBe(2) }) - test('returns if the throttle is active', async () => { - const results = [] - const func = _.throttle({ interval }, () => {}) - results.push(func.isThrottled()) - func() - results.push(func.isThrottled()) - func() - results.push(func.isThrottled()) - func() - results.push(func.isThrottled()) - vi.advanceTimersByTime(interval + 10) - results.push(func.isThrottled()) - assert.deepEqual(results, [false, true, true, true, false]) + describe('trailing option', () => { + test('single call with trailing set to true', async () => { + let calls = 0 + const func = _.throttle({ interval, trailing: true }, () => calls++) + func() + expect(calls).toBe(1) + vi.advanceTimersByTime(interval + smidge) + expect(calls).toBe(1) + }) + + test('repeated calls with trailing set to true', async () => { + let calls = 0 + const func = _.throttle({ interval, trailing: true }, () => calls++) + func() + expect(calls).toBe(1) + vi.advanceTimersByTime(smidge) + func() + vi.advanceTimersByTime(interval + smidge) + expect(calls).toBe(2) + }) + + test('', async () => { + const wrapped = vi.fn() + const func = _.throttle({ interval, trailing: true }, wrapped) + + func() + func() + + expect(wrapped).toHaveBeenCalledTimes(1) + + // Advance time a bit (but still before the interval). + vi.advanceTimersByTime(smidge) + + // Still throttled. + func() + func() + + expect(wrapped).toHaveBeenCalledTimes(1) + + vi.advanceTimersByTime(interval) + + // By now, the trailing call should have occurred + expect(wrapped).toHaveBeenCalledTimes(2) + + // The trailing call should re-throttle the function. + expect(func.isThrottled()).toBe(true) + + // So these will be throttled. + func() + func() + expect(wrapped).toHaveBeenCalledTimes(2) + + vi.advanceTimersByTime(interval + smidge) + + // By now another trailing call should have happened + expect(wrapped).toHaveBeenCalledTimes(3) + }) + }) + + describe('isThrottled method', () => { + test('returns if the throttle is active', async () => { + const results = [] + const func = _.throttle({ interval }, () => {}) + results.push(func.isThrottled()) + func() + results.push(func.isThrottled()) + func() + results.push(func.isThrottled()) + func() + results.push(func.isThrottled()) + vi.advanceTimersByTime(interval + smidge) + results.push(func.isThrottled()) + assert.deepEqual(results, [false, true, true, true, false]) + }) + }) + + describe('trigger method', () => { + test('ignore any throttle in place', () => { + const wrapped = vi.fn() + const func = _.throttle({ interval }, wrapped) + + func() + expect(wrapped).toHaveBeenCalledTimes(1) + func() + expect(wrapped).toHaveBeenCalledTimes(1) + func() + expect(wrapped).toHaveBeenCalledTimes(1) + + func.trigger() + expect(wrapped).toHaveBeenCalledTimes(2) + }) + + test('clears trailing state', () => { + const wrapped = vi.fn() + const func = _.throttle({ interval, trailing: true }, wrapped) + + func() + func() // <-- trailing call + func.trigger() + expect(wrapped).toHaveBeenCalledTimes(2) + + // Since trigger was called, the trailing call was cancelled. + vi.advanceTimersByTime(interval + smidge) + expect(wrapped).toHaveBeenCalledTimes(2) + + func() + func.trigger() + func() // <-- trailing call after trigger + expect(wrapped).toHaveBeenCalledTimes(4) + + // Since the trailing call was queued after the trigger, it will + // still be called. + vi.advanceTimersByTime(interval + smidge) + expect(wrapped).toHaveBeenCalledTimes(5) + }) }) }) diff --git a/tests/number/sum.test.ts b/tests/number/sum.test.ts index 73d9b892..12dbfaa4 100644 --- a/tests/number/sum.test.ts +++ b/tests/number/sum.test.ts @@ -17,4 +17,19 @@ describe('sum', () => { const result = _.sum(cast(null)) expect(result).toBe(0) }) + test('gracefully handles boolean input list', () => { + const list = [true, false, true, false, true] + const result = _.sum(list, x => (x ? 1 : 0)) + expect(result).toBe(3) + }) + + test('gracefully handles matrix input', () => { + const list = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ] + const result = _.sum(list, x => _.sum(x)) + expect(result).toBe(45) + }) }) diff --git a/tests/string/similarity.test.ts b/tests/string/similarity.test.ts new file mode 100644 index 00000000..ebb6f3a9 --- /dev/null +++ b/tests/string/similarity.test.ts @@ -0,0 +1,33 @@ +import * as _ from 'radashi' + +describe('similarity', () => { + // https://github.com/fabiospampinato/tiny-levenshtein/blob/master/test/index.js + test('returns the distance between two strings', () => { + expect(_.similarity('abc', 'abc')).toBe(0) + expect(_.similarity('a', 'b')).toBe(1) + expect(_.similarity('ab', 'ac')).toBe(1) + expect(_.similarity('ac', 'bc')).toBe(1) + expect(_.similarity('abc', 'axc')).toBe(1) + expect(_.similarity('kitten', 'sitting')).toBe(3) + expect(_.similarity('xabxcdxxefxgx', '1ab2cd34ef5g6')).toBe(6) + expect(_.similarity('cat', 'cow')).toBe(2) + expect(_.similarity('xabxcdxxefxgx', 'abcdefg')).toBe(6) + expect(_.similarity('javawasneat', 'scalaisgreat')).toBe(7) + expect(_.similarity('example', 'samples')).toBe(3) + expect(_.similarity('sturgeon', 'urgently')).toBe(6) + expect(_.similarity('levenshtein', 'frankenstein')).toBe(6) + expect(_.similarity('distance', 'difference')).toBe(5) + expect( + _.similarity( + '因為我是中國人所以我會說中文', + '因為我是英國人所以我會說英文', + ), + ).toBe(2) + }) + test('containment', () => { + expect(_.similarity('abababab', 'ab')).toBe(6) + expect(_.similarity('ab', 'abababab')).toBe(6) + expect(_.similarity('abc', 'ab')).toBe(1) + expect(_.similarity('ab', 'abc')).toBe(1) + }) +}) diff --git a/tsup.config.ts b/tsup.config.ts index afa03779..b5cc7789 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -5,4 +5,10 @@ export default defineConfig({ format: ['cjs', 'esm'], dts: true, target: 'node16', + pure: ['Symbol'], + treeshake: { + preset: 'smallest', + propertyReadSideEffects: false, + moduleSideEffects: false, + }, }) diff --git a/vitest.config.ts b/vitest.config.ts index 9ea38a34..086e895a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,12 +9,7 @@ export default defineConfig(({ mode }) => ({ globals: true, coverage: { thresholds: { 100: true }, - exclude: [ - '*.config.ts', - 'benchmarks/**', - 'scripts/**', - 'tests/**/*.test-d.ts', - ], + include: ['src/**'], }, }, resolve: {