Compare commits

...

4 Commits

Author SHA1 Message Date
Julien Valverdé
3e78121d26 Regenerate lockfile
All checks were successful
Lint / lint (push) Successful in 13s
2026-04-23 15:53:31 +02:00
ffa23718a8 Update dependency npm-check-updates to v21 (#46)
Some checks failed
Lint / lint (push) Failing after 7s
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [npm-check-updates](https://github.com/raineorshine/npm-check-updates) | [`^19.6.6` → `^21.0.0`](https://renovatebot.com/diffs/npm/npm-check-updates/19.6.6/21.0.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/npm-check-updates/21.0.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/npm-check-updates/19.6.6/21.0.3?slim=true) |

---

### Release Notes

<details>
<summary>raineorshine/npm-check-updates (npm-check-updates)</summary>

### [`v21.0.3`](https://github.com/raineorshine/npm-check-updates/releases/tag/v21.0.3)

[Compare Source](https://github.com/raineorshine/npm-check-updates/compare/v21.0.2...v21.0.3)

#### What's Changed

- fix: chmod build/cli.js executable after vite build by [@&#8203;raineorshine](https://github.com/raineorshine) in [#&#8203;1678](https://github.com/raineorshine/npm-check-updates/pull/1678)
- fix: migrate from rc-config-loader to cosmiconfig for ESM config support (closes [#&#8203;1674](https://github.com/raineorshine/npm-check-updates/issues/1674)) by [@&#8203;onemen](https://github.com/onemen) in [#&#8203;1676](https://github.com/raineorshine/npm-check-updates/pull/1676)

**Full Changelog**: <https://github.com/raineorshine/npm-check-updates/compare/v21.0.2...v21.0.3>

### [`v21.0.2`](https://github.com/raineorshine/npm-check-updates/releases/tag/v21.0.2)

[Compare Source](https://github.com/raineorshine/npm-check-updates/compare/v21.0.1...v21.0.2)

#### What's Changed

- fix: skip intersects() for non-semver specs like catalog: in peer dep checks by [@&#8203;terminalchai](https://github.com/terminalchai) in [#&#8203;1675](https://github.com/raineorshine/npm-check-updates/pull/1675)

**Full Changelog**: <https://github.com/raineorshine/npm-check-updates/compare/v21.0.1...v21.0.2>

### [`v21.0.1`](https://github.com/raineorshine/npm-check-updates/releases/tag/v21.0.1)

[Compare Source](https://github.com/raineorshine/npm-check-updates/compare/v21.0.0...v21.0.1)

#### What's Changed

- fix: seeing catalog when inside workspaces by [@&#8203;Zamiell](https://github.com/Zamiell) in [#&#8203;1656](https://github.com/raineorshine/npm-check-updates/pull/1656)
- Bump [@&#8203;types/bun](https://github.com/types/bun) from 1.3.11 to 1.3.12 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1663](https://github.com/raineorshine/npm-check-updates/pull/1663)
- Bump globals from 17.4.0 to 17.5.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1664](https://github.com/raineorshine/npm-check-updates/pull/1664)
- Bump [@&#8203;typescript-eslint/parser](https://github.com/typescript-eslint/parser) from 8.58.1 to 8.58.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1665](https://github.com/raineorshine/npm-check-updates/pull/1665)
- Bump prettier from 3.8.1 to 3.8.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1667](https://github.com/raineorshine/npm-check-updates/pull/1667)
- Bump verdaccio from 6.4.0 to 6.5.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1669](https://github.com/raineorshine/npm-check-updates/pull/1669)
- Bump sinon from 21.0.3 to 21.1.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1668](https://github.com/raineorshine/npm-check-updates/pull/1668)
- Skip CI on PRs with no file changes by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1672](https://github.com/raineorshine/npm-check-updates/pull/1672)
- Bump [@&#8203;typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/eslint-plugin) from 8.58.1 to 8.58.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1666](https://github.com/raineorshine/npm-check-updates/pull/1666)
- Fix wrong message when all packages are within cooldown window by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1671](https://github.com/raineorshine/npm-check-updates/pull/1671)

**Full Changelog**: <https://github.com/raineorshine/npm-check-updates/compare/v21.0.0...v21.0.1>

### [`v21.0.0`](https://github.com/raineorshine/npm-check-updates/blob/HEAD/CHANGELOG.md#2100---2026-04-14)

[Compare Source](https://github.com/raineorshine/npm-check-updates/compare/v20.0.2...v21.0.0)

##### ⚠️ Breaking Changes

This is a **major breaking change** with significant architectural updates.

##### ESM Migration & Module System

- **Pure ESM:** Converted to pure ESM with dual-build support (ESM/CJS) via Vite 8.
- **Import Syntax:** Programmatic usage now requires named imports or namespace imports.
  - **Old:** `import ncu from 'npm-check-updates'`
  - **New:** `import * as ncu from 'npm-check-updates'` or `import { run } from 'npm-check-updates'`
- **Node.js Requirements:** Now requires `^20.19.0 || ^22.12.0 || >=24.0.0`. This is required for native `require(esm)` support and the Rolldown engine.
- **npm Requirements:** Minimum version increased to `>=10.0.0`.

##### Configuration Files (`.ncurc.js`)

- Files named `.ncurc.js` that use `module.exports` will now fail in projects that are `"type": "module"`.
- **Fix:** Rename these files to `.ncurc.cjs` or convert them to use `export default`.

##### Dependency Updates (Pure ESM versions)

| Package            | Old Version | New Version |
| :----------------- | :---------- | :---------- |
| `camelcase`        | `^6.3.0`    | `^9.0.0`    |
| `chai`             | `^4.3.10`   | `^6.2.2`    |
| `chai-as-promised` | `^7.1.2`    | `^8.0.2`    |
| `find-up`          | `5.0.0`     | `8.0.0`     |
| `p-map`            | `^4.0.0`    | `^7.0.4`    |
| `untildify`        | `^4.0.0`    | `^6.0.0`    |

##### Tooling & Build Changes

- **Vite 8 Upgrade:** Migrated to Vite 8 with the new Rust-based **Rolldown** bundler (10-30x faster builds).
- **TypeScript 6.0:** Adopted latest type-system features and performance improvements.
- **Strip ANSI:** Replaced `strip-ansi` with Node.js built-in `util.stripVTControlCharacters`.
- **Test Runner:** Replaced `vite-node` with `tsx` for TypeScript support in ESM context.

***

##### Migration Guide

If you are upgrading to v21 from earlier versions:

##### 1. Environment Check

- Ensure you meet the new Node.js requirement: `^20.19.0 || ^22.12.0 || >=24.0.0`.
- Update npm to at least `10.0.0`.

##### 2. Update Configuration Files

If you have a `.ncurc.js` file:

- **Option A:** Rename it to `.ncurc.cjs`.
- **Option B:** Convert it to ESM:

  ```js
  import { defineConfig } from 'npm-check-updates'

  export default defineConfig({
    upgrade: true,
    filter: name => name.startsWith('@&#8203;myorg/'),
  })
  ```

##### 3. Update Programmatic Usage

If you import `npm-check-updates` in your scripts:

- **ESM:** Change `import ncu from ...` to `import * as ncu from 'npm-check-updates'`.
- **CommonJS:** Ensure you are destructuring the named exports or using the full object:

```js
const ncu = require('npm-check-updates')
// Use ncu.run(...)
```

***

##### Testing

Tests now use `tsx` for module loading. When running tests manually:

```sh
mocha --node-option import=tsx 'test/**/*.test.ts'
```

Or use the npm script:

```sh
npm test
```

##### Related Issues & PRs

[PR 1649](https://github.com/raineorshine/npm-check-updates/pull/1649)

***

### [`v20.0.2`](https://github.com/raineorshine/npm-check-updates/releases/tag/v20.0.2)

[Compare Source](https://github.com/raineorshine/npm-check-updates/compare/v20.0.1...v20.0.2)

#### What's Changed

- Show auto-detected cooldown source at normal log level; fix test isolation by [@&#8203;bayraak](https://github.com/bayraak) in [#&#8203;1662](https://github.com/raineorshine/npm-check-updates/pull/1662)

#### New Contributors

- [@&#8203;bayraak](https://github.com/bayraak) made their first contribution in [#&#8203;1662](https://github.com/raineorshine/npm-check-updates/pull/1662)

**Full Changelog**: <https://github.com/raineorshine/npm-check-updates/compare/v20.0.1...v20.0.2>

### [`v20.0.1`](https://github.com/raineorshine/npm-check-updates/releases/tag/v20.0.1)

[Compare Source](https://github.com/raineorshine/npm-check-updates/compare/v20.0.0...v20.0.1)

#### What's Changed

- Add CI workflow to verify build output is committed by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1645](https://github.com/raineorshine/npm-check-updates/pull/1645)
- Bump lodash-es from 4.17.23 to 4.18.1 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1647](https://github.com/raineorshine/npm-check-updates/pull/1647)
- Add Node v24 to test workflow by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1608](https://github.com/raineorshine/npm-check-updates/pull/1608)
- feat: remove pre-push git hook by [@&#8203;Zamiell](https://github.com/Zamiell) in [#&#8203;1658](https://github.com/raineorshine/npm-check-updates/pull/1658)
- feat: add verbose output when packages are skipped due to cooldown by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1659](https://github.com/raineorshine/npm-check-updates/pull/1659)

**Full Changelog**: <https://github.com/raineorshine/npm-check-updates/compare/v20.0.0...v20.0.1>

### [`v20.0.0`](https://github.com/raineorshine/npm-check-updates/blob/HEAD/CHANGELOG.md#2000---2026-03-31)

[Compare Source](https://github.com/raineorshine/npm-check-updates/compare/v19.6.6...v20.0.0)

##### Auto Cooldown

The cooldown option is now automatically applied from the respective package manager's config:

- **npm** - `min-release-age` ([#&#8203;1632](https://github.com/raineorshine/npm-check-updates/issues/1632))
- **yarn** - `npmMinimalAgeGate` (excluding `npmPreapprovedPackages`) ([#&#8203;1643](https://github.com/raineorshine/npm-check-updates/issues/1643))
- **pnpm** - `minimumReleaseAge` (excluding `minimumReleaseAgeExclude`) ([#&#8203;1639](https://github.com/raineorshine/npm-check-updates/issues/1639))

Why is this a breaking change?

- If you use any of the above configs, npm-check-updates will automatically exclude releases that do not exceed the specified minimum age as described in <https://github.com/raineorshine/npm-check-updates#cooldown>.
- Otherwise, you don't need to do anything.

##### Other changes

- Bump strip-ansi from 7.1.2 to 7.2.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1620](https://github.com/raineorshine/npm-check-updates/pull/1620)
- Bump lodash and [@&#8203;types/lodash](https://github.com/types/lodash) by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1615](https://github.com/raineorshine/npm-check-updates/pull/1615)
- Bump [@&#8203;typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/eslint-plugin) from 8.44.1 to 8.57.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1619](https://github.com/raineorshine/npm-check-updates/pull/1619)
- Bump hosted-git-info from 9.0.0 to 9.0.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1622](https://github.com/raineorshine/npm-check-updates/pull/1622)
- Bump glob and markdownlint-cli by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;1625](https://github.com/raineorshine/npm-check-updates/pull/1625)
- update dependencies; fix vulnerabilities by [@&#8203;onemen](https://github.com/onemen) in [#&#8203;1630](https://github.com/raineorshine/npm-check-updates/pull/1630)
- Potential fix for code scanning alert no. 13: Incomplete string escaping or encoding by [@&#8203;raineorshine](https://github.com/raineorshine) in [#&#8203;1640](https://github.com/raineorshine/npm-check-updates/pull/1640)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjEuMCIsInVwZGF0ZWRJblZlciI6IjQzLjEzNy4wIiwidGFyZ2V0QnJhbmNoIjoibmV4dCIsImxhYmVscyI6W119-->

Reviewed-on: https://git.valverde.cloud/Thilawyn/effect-fc/pulls/46
Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud>
Co-committed-by: Renovate Bot <renovate-bot@valverde.cloud>
2026-04-23 10:36:40 +02:00
78a2d2dede Update bun minor+patch updates (#45)
Some checks failed
Lint / lint (push) Has been cancelled
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@docusaurus/core](https://github.com/facebook/docusaurus) ([source](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus)) | [`3.9.2` → `3.10.0`](https://renovatebot.com/diffs/npm/@docusaurus%2fcore/3.9.2/3.10.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@docusaurus%2fcore/3.10.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@docusaurus%2fcore/3.9.2/3.10.0?slim=true) |
| [@docusaurus/module-type-aliases](https://github.com/facebook/docusaurus) ([source](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-module-type-aliases)) | [`3.9.2` → `3.10.0`](https://renovatebot.com/diffs/npm/@docusaurus%2fmodule-type-aliases/3.9.2/3.10.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@docusaurus%2fmodule-type-aliases/3.10.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@docusaurus%2fmodule-type-aliases/3.9.2/3.10.0?slim=true) |
| [@docusaurus/preset-classic](https://github.com/facebook/docusaurus) ([source](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic)) | [`3.9.2` → `3.10.0`](https://renovatebot.com/diffs/npm/@docusaurus%2fpreset-classic/3.9.2/3.10.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@docusaurus%2fpreset-classic/3.10.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@docusaurus%2fpreset-classic/3.9.2/3.10.0?slim=true) |
| [@docusaurus/tsconfig](https://github.com/facebook/docusaurus) ([source](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-tsconfig)) | [`3.9.2` → `3.10.0`](https://renovatebot.com/diffs/npm/@docusaurus%2ftsconfig/3.9.2/3.10.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@docusaurus%2ftsconfig/3.10.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@docusaurus%2ftsconfig/3.9.2/3.10.0?slim=true) |
| [@docusaurus/types](https://github.com/facebook/docusaurus) ([source](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-types)) | [`3.9.2` → `3.10.0`](https://renovatebot.com/diffs/npm/@docusaurus%2ftypes/3.9.2/3.10.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@docusaurus%2ftypes/3.10.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@docusaurus%2ftypes/3.9.2/3.10.0?slim=true) |
| [@effect/language-service](https://github.com/Effect-TS/language-service) | [`^0.84.2` → `^0.85.0`](https://renovatebot.com/diffs/npm/@effect%2flanguage-service/0.84.3/0.85.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@effect%2flanguage-service/0.85.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@effect%2flanguage-service/0.84.3/0.85.1?slim=true) |

---

### Release Notes

<details>
<summary>facebook/docusaurus (@&#8203;docusaurus/core)</summary>

### [`v3.10.0`](https://github.com/facebook/docusaurus/blob/HEAD/CHANGELOG.md#3100-2026-04-07)

[Compare Source](https://github.com/facebook/docusaurus/compare/v3.9.2...v3.10.0)

##### 🚀 New Feature

- `docusaurus-types`, `docusaurus`
  - [#&#8203;11896](https://github.com/facebook/docusaurus/pull/11896) feat(core): add `future.v4.mdx1CompatDisabledByDefault` flag ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11797](https://github.com/facebook/docusaurus/pull/11797) feat(core): promote `siteConfig.storage` to stable + add `future.v4.siteStorageNamespacing` flag \[Claude] ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11571](https://github.com/facebook/docusaurus/pull/11571) feat(core): support custom html elements in head tags ([@&#8203;lebalz](https://github.com/lebalz))
- `create-docusaurus`
  - [#&#8203;11897](https://github.com/facebook/docusaurus/pull/11897) feat(create-docusaurus): update init template to `.mdx` extension and strict MDX syntax ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11696](https://github.com/facebook/docusaurus/pull/11696) feat(create-docusaurus): Newly initialized TS sites should use "strict: true" ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11611](https://github.com/facebook/docusaurus/pull/11611) feat(create-docusaurus): enable creation in current directory ([@&#8203;Mcheung7272](https://github.com/Mcheung7272))
- Other
  - [#&#8203;11874](https://github.com/facebook/docusaurus/pull/11874) feat(ci): improve npm supply chain security - improve Dependabot config ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11712](https://github.com/facebook/docusaurus/pull/11712) feat(publish): Use trusted publishing (OIDC) for canary releases ([@&#8203;slorber](https://github.com/slorber))
- `create-docusaurus`, `docusaurus-bundler`, `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-plugin-content-pages`, `docusaurus-plugin-pwa`, `docusaurus-types`, `docusaurus`
  - [#&#8203;11802](https://github.com/facebook/docusaurus/pull/11802) feat(core): Docusaurus Faster is stable + v4 future flag turns it on by default ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-mdx-loader`, `docusaurus-utils`, `docusaurus`
  - [#&#8203;11777](https://github.com/facebook/docusaurus/pull/11777) feat(cli): `write-heading-ids` CLI now supports the `--syntax` and `--migrate` options ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-mdx-loader`
  - [#&#8203;11755](https://github.com/facebook/docusaurus/pull/11755) feat(mdx-loader): add support for explicit `headingId` based on MD/MDX comments ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-theme-live-codeblock`, `docusaurus-theme-translations`
  - [#&#8203;11675](https://github.com/facebook/docusaurus/pull/11675) feat(theme-live-codeblock): reset button + wire `position` prop ([@&#8203;NPX2218](https://github.com/NPX2218))
- `docusaurus-theme-classic`, `docusaurus-theme-common`
  - [#&#8203;11734](https://github.com/facebook/docusaurus/pull/11734) feat(theme): Split `<DocCard>`, improve extensibility, better handling of emoji icons, stable classNames ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11733](https://github.com/facebook/docusaurus/pull/11733) feat(theme): Use React context for `<Tabs>`, allow custom `<TabItem>` components ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-faster`, `docusaurus`
  - [#&#8203;11715](https://github.com/facebook/docusaurus/pull/11715) feat(bundler): upgrade to Rspack 1.7, remove useless experimental feature flags ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-plugin-content-pages`
  - [#&#8203;11666](https://github.com/facebook/docusaurus/pull/11666) feat(pages): add support for Markdown file path links ([@&#8203;VedantMadane](https://github.com/VedantMadane))
- `docusaurus-mdx-loader`, `docusaurus-theme-classic`
  - [#&#8203;11642](https://github.com/facebook/docusaurus/pull/11642) feat(mdx-loader): add admonitions directive support for class/id shortcuts ([@&#8203;lebalz](https://github.com/lebalz))
- `docusaurus-theme-classic`
  - [#&#8203;11635](https://github.com/facebook/docusaurus/pull/11635) feat(theme): add MDXComponents/Li to swizzle config ([@&#8203;moskalakamil](https://github.com/moskalakamil))
- `docusaurus-theme-search-algolia`
  - [#&#8203;11581](https://github.com/facebook/docusaurus/pull/11581) feat(theme-search-algolia): allow overriding transformSearchClient ([@&#8203;hugohaggmark](https://github.com/hugohaggmark))
  - [#&#8203;11541](https://github.com/facebook/docusaurus/pull/11541) feat(theme-search-algolia): add support for DocSearch v4.3.2 and new Suggested Questions ([@&#8203;NatanTechofNY](https://github.com/NatanTechofNY))
- `create-docusaurus`, `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-plugin-content-pages`, `docusaurus-plugin-sitemap`, `docusaurus-types`, `docusaurus-utils`, `docusaurus`
  - [#&#8203;11512](https://github.com/facebook/docusaurus/pull/11512) feat(core): New siteConfig `future.experimental_vcs` API + `future.experimental_faster.gitEagerVcs` flag ([@&#8203;slorber](https://github.com/slorber))

##### 🐛 Bug Fix

- `docusaurus`
  - [#&#8203;11844](https://github.com/facebook/docusaurus/pull/11844) fix(core): fix `url.resolve()` Node.js deprecation warning ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11833](https://github.com/facebook/docusaurus/pull/11833) fix(core): upgrade serve handler min version to for upgrade users to a secure version ([@&#8203;BearAlliance](https://github.com/BearAlliance))
  - [#&#8203;11763](https://github.com/facebook/docusaurus/pull/11763) fix(cli): fix `write-heading-ids` CLI when no files provided ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11693](https://github.com/facebook/docusaurus/pull/11693) fix(core): Remove deprecated experiments.lazyBarrel config for RsPack ([@&#8203;VedikaGupt](https://github.com/VedikaGupt))
  - [#&#8203;11604](https://github.com/facebook/docusaurus/pull/11604) fix(core): webpack aliases shouldn't be created for test files and typedefs ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11603](https://github.com/facebook/docusaurus/pull/11603) fix(core): Fix openBrowser AppleScript support for Arc ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11579](https://github.com/facebook/docusaurus/pull/11579) fix(core): in `isInternalUrl()`, URI protocol scheme detection should implement the spec more strictly ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11550](https://github.com/facebook/docusaurus/pull/11550) fix(core): optimize i18n integration for site builds + improve inference of locale config ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-faster`, `docusaurus`
  - [#&#8203;11817](https://github.com/facebook/docusaurus/pull/11817) fix(faster): upgrade Rspack, fix Yarn PnP support ([@&#8203;slorber](https://github.com/slorber))
- `create-docusaurus`, `docusaurus-logger`, `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-plugin-google-gtag`, `docusaurus-plugin-pwa`, `docusaurus`
  - [#&#8203;11843](https://github.com/facebook/docusaurus/pull/11843) fix(create-docusaurus): fix support for TypeScript 6.0 + fix our CI ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-utils`
  - [#&#8203;11804](https://github.com/facebook/docusaurus/pull/11804) fix(utils): Git Eager VSC should have better DX ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-theme-classic`
  - [#&#8203;11796](https://github.com/facebook/docusaurus/pull/11796) fix(theme): restore copy-text-to-clipboard as lazy fallback for non-secure contexts ([@&#8203;dmoranp](https://github.com/dmoranp))
  - [#&#8203;11513](https://github.com/facebook/docusaurus/pull/11513) fix(a11y): add Space key support for navbar dropdowns ([@&#8203;TheCyperpunk](https://github.com/TheCyperpunk))
  - [#&#8203;11565](https://github.com/facebook/docusaurus/pull/11565) fix(theme): Change code block line from span to div, fix Firefox text selection/copy bug ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-plugin-content-docs`
  - [#&#8203;11794](https://github.com/facebook/docusaurus/pull/11794) fix(content-docs): translate generated-index category titles in pagination links ([@&#8203;dmoranp](https://github.com/dmoranp))
  - [#&#8203;11743](https://github.com/facebook/docusaurus/pull/11743) fix(content-docs): use category key for generated-index translation lookup ([@&#8203;4RH1T3CT0R7](https://github.com/4RH1T3CT0R7))
  - [#&#8203;11616](https://github.com/facebook/docusaurus/pull/11616) fix(docs): breadcrumb APIs only return category/docs items, ignoring links ([@&#8203;Chesars](https://github.com/Chesars))
- `docusaurus-plugin-google-gtag`
  - [#&#8203;11770](https://github.com/facebook/docusaurus/pull/11770) fix(create-docusaurus): update [@&#8203;types/gtag](https://github.com/types/gtag).js to 0.0.20 ([@&#8203;fresh3nough](https://github.com/fresh3nough))
- `docusaurus-theme-search-algolia`
  - [#&#8203;11683](https://github.com/facebook/docusaurus/pull/11683) fix(algolia): upgrade to DocSearch 4.5 + fix types ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11560](https://github.com/facebook/docusaurus/pull/11560) fix(theme-search-algolia): preserve query strings in useSearchResultUrlProcessor ([@&#8203;pyrytakala](https://github.com/pyrytakala))
- `docusaurus-plugin-content-blog`
  - [#&#8203;11736](https://github.com/facebook/docusaurus/pull/11736) fix(content-blog): fix wrong path variable in feed XSLT CSS file validation ([@&#8203;akshatsinha0](https://github.com/akshatsinha0))
  - [#&#8203;11577](https://github.com/facebook/docusaurus/pull/11577) fix(blog): Fix author paginated page url: `/blog/authors/<author>/page/2` ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11562](https://github.com/facebook/docusaurus/pull/11562) chore(blog): refactor blog Content, remove useless `blogListPaginated` attribute ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11559](https://github.com/facebook/docusaurus/pull/11559) fix(content-blog): filter unlisted posts from author pages ([@&#8203;pyrytakala](https://github.com/pyrytakala))
- `docusaurus-theme-classic`, `docusaurus-theme-common`
  - [#&#8203;11713](https://github.com/facebook/docusaurus/pull/11713) fix(a11y): remove `useKeyboardNavigation` hook ([@&#8203;nmggithub](https://github.com/nmggithub))
- `docusaurus-plugin-ideal-image`
  - [#&#8203;11659](https://github.com/facebook/docusaurus/pull/11659) fix(ideal-image): `<IdealImage>` should forward remaining props to the underlying component ([@&#8203;tempoz](https://github.com/tempoz))
- `eslint-plugin`
  - [#&#8203;11587](https://github.com/facebook/docusaurus/pull/11587) fix(eslint-plugin): specify exact type of `no-untranslated-text` rule options ([@&#8203;andreww2012](https://github.com/andreww2012))
- `docusaurus-mdx-loader`
  - [#&#8203;11530](https://github.com/facebook/docusaurus/pull/11530) fix(mdx-loader): fix url.parse deprecation warning on Node 24+ ([@&#8203;kou029w](https://github.com/kou029w))
- `docusaurus-bundler`, `docusaurus-faster`, `docusaurus-theme-mermaid`
  - [#&#8203;11496](https://github.com/facebook/docusaurus/pull/11496) fix(faster): fix server build SWC / browserslist node target ([@&#8203;slorber](https://github.com/slorber))

##### :running\_woman: Performance

- `docusaurus-plugin-content-blog`
  - [#&#8203;11707](https://github.com/facebook/docusaurus/pull/11707) refactor(content-blog): decouple getTagsFile from generateBlogPosts ([@&#8203;garry00107](https://github.com/garry00107))
- `create-docusaurus`, `docusaurus-utils`, `docusaurus`
  - [#&#8203;11684](https://github.com/facebook/docusaurus/pull/11684) refactor(create-docusaurus): remove useless dependencies (docusaurus-utils, execa, fs-extra) + simplify some code ([@&#8203;slorber](https://github.com/slorber))
- `create-docusaurus`
  - [#&#8203;11653](https://github.com/facebook/docusaurus/pull/11653) refactor(create-docusaurus): replace lodash with native implementation ([@&#8203;torresgol10](https://github.com/torresgol10))

##### 📝 Documentation

- `docusaurus`
  - [#&#8203;11779](https://github.com/facebook/docusaurus/pull/11779) chore(website): migrate MDX heading ids to comment syntax + upgrade Crowdin parser version ([@&#8203;slorber](https://github.com/slorber))
- Other
  - [#&#8203;11784](https://github.com/facebook/docusaurus/pull/11784) docs(website): change recommended syntax for math equations ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11623](https://github.com/facebook/docusaurus/pull/11623) docs: Add expose-markdown-docusaurus-plugin resource ([@&#8203;FlyNumber](https://github.com/FlyNumber))

##### 🤖 Dependencies

- Other
  - [#&#8203;11886](https://github.com/facebook/docusaurus/pull/11886) chore(deps): bump react-json-view-lite from 2.3.0 to 2.5.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11885](https://github.com/facebook/docusaurus/pull/11885) chore(deps): bump postcss from 8.5.4 to 8.5.8 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11888](https://github.com/facebook/docusaurus/pull/11888) chore(deps): bump lodash from 4.17.23 to 4.18.1 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11882](https://github.com/facebook/docusaurus/pull/11882) chore(deps): bump [@&#8203;babel/core](https://github.com/babel/core) from 7.28.6 to 7.29.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11880](https://github.com/facebook/docusaurus/pull/11880) chore(deps): bump fs-extra and [@&#8203;types/fs-extra](https://github.com/types/fs-extra) ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11861](https://github.com/facebook/docusaurus/pull/11861) chore(deps): bump preactjs/compressed-size-action from 2.9.0 to 2.9.1 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11851](https://github.com/facebook/docusaurus/pull/11851) chore(deps): bump handlebars from 4.7.7 to 4.7.9 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11849](https://github.com/facebook/docusaurus/pull/11849) chore(deps): bump convict from 6.2.4 to 6.2.5 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11857](https://github.com/facebook/docusaurus/pull/11857) chore(deps): bump node-forge from 1.3.2 to 1.4.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11838](https://github.com/facebook/docusaurus/pull/11838) chore(deps-dev): bump picomatch from 2.3.1 to 2.3.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11822](https://github.com/facebook/docusaurus/pull/11822) chore(deps): bump flatted from 3.3.1 to 3.4.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11810](https://github.com/facebook/docusaurus/pull/11810) chore(deps): bump marocchino/sticky-pull-request-comment from 2.9.4 to 3.0.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11811](https://github.com/facebook/docusaurus/pull/11811) chore(deps): bump treosh/lighthouse-ci-action from 12.6.1 to 12.6.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11813](https://github.com/facebook/docusaurus/pull/11813) chore(deps): bump socket.io-parser from 4.2.4 to 4.2.6 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11806](https://github.com/facebook/docusaurus/pull/11806) chore(deps): bump yauzl from 3.1.3 to 3.2.1 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11789](https://github.com/facebook/docusaurus/pull/11789) chore(deps): bump actions/dependency-review-action from 4.8.3 to 4.9.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11790](https://github.com/facebook/docusaurus/pull/11790) chore(deps): bump actions/setup-node from 6.2.0 to 6.3.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11776](https://github.com/facebook/docusaurus/pull/11776) chore(deps): bump dompurify from 3.2.5 to 3.3.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11768](https://github.com/facebook/docusaurus/pull/11768) chore(deps): bump actions/upload-artifact from 6.0.0 to 7.0.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11774](https://github.com/facebook/docusaurus/pull/11774) chore(deps): bump svgo from 3.2.0 to 3.3.3 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11762](https://github.com/facebook/docusaurus/pull/11762) chore(deps): bump rollup from 2.79.2 to 2.80.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11756](https://github.com/facebook/docusaurus/pull/11756) chore(deps): bump actions/dependency-review-action from 4.8.2 to 4.8.3 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11692](https://github.com/facebook/docusaurus/pull/11692) chore(deps): bump actions/checkout from 6.0.1 to 6.0.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11679](https://github.com/facebook/docusaurus/pull/11679) chore(deps): bump lodash from 4.17.21 to 4.17.23 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11674](https://github.com/facebook/docusaurus/pull/11674) chore(deps): bump actions/setup-node from 6.1.0 to 6.2.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11625](https://github.com/facebook/docusaurus/pull/11625) chore(deps): bump preactjs/compressed-size-action from 2.8.0 to 2.9.0 - pin all remaining GitHub actions ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11608](https://github.com/facebook/docusaurus/pull/11608) chore(deps): bump actions/setup-node from 6.0.0 to 6.1.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11609](https://github.com/facebook/docusaurus/pull/11609) chore(deps): bump actions/checkout from 6.0.0 to 6.0.1 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11589](https://github.com/facebook/docusaurus/pull/11589) chore(deps): bump mdast-util-to-hast from 13.2.0 to 13.2.1 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11574](https://github.com/facebook/docusaurus/pull/11574) chore(deps): bump node-forge from 1.3.1 to 1.3.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11557](https://github.com/facebook/docusaurus/pull/11557) chore(deps): bump actions/dependency-review-action from 4.8.1 to 4.8.2 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11569](https://github.com/facebook/docusaurus/pull/11569) chore(deps): bump actions/checkout from 5.0.0 to 6.0.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11551](https://github.com/facebook/docusaurus/pull/11551) chore(deps): bump js-yaml from 4.1.0 to 4.1.1 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11514](https://github.com/facebook/docusaurus/pull/11514) chore(deps): bump actions/upload-artifact from 4 to 5 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11515](https://github.com/facebook/docusaurus/pull/11515) chore(deps): bump github/codeql-action from 4.30.9 to 4.31.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11504](https://github.com/facebook/docusaurus/pull/11504) chore(deps): bump github/codeql-action from 4.30.8 to 4.30.9 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
  - [#&#8203;11503](https://github.com/facebook/docusaurus/pull/11503) chore(deps): bump actions/setup-node from 5.0.0 to 6.0.0 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))
- `docusaurus-bundler`, `docusaurus-mdx-loader`
  - [#&#8203;11717](https://github.com/facebook/docusaurus/pull/11717) chore(deps): bump webpack from 5.95.0 to 5.104.1 ([@&#8203;dependabot\[bot\]](https://github.com/apps/dependabot))

##### 🔧 Maintenance

- Other
  - [#&#8203;11846](https://github.com/facebook/docusaurus/pull/11846) chore(website): disable `mdx1Compat.comments` on our site ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11845](https://github.com/facebook/docusaurus/pull/11845) chore(website): Upgrade to Algolia v4.6 ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11795](https://github.com/facebook/docusaurus/pull/11795) chore(ci): canary/trusted publishing shouldn't use any caching ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11753](https://github.com/facebook/docusaurus/pull/11753) chore: Add basic AGENTS.md ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11639](https://github.com/facebook/docusaurus/pull/11639) test(jest): simplify Jest snapshotPathNormalizer.ts ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11626](https://github.com/facebook/docusaurus/pull/11626) chore(website): upgrade to DocSearch 4.4.0 + fix little website theming issues ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11553](https://github.com/facebook/docusaurus/pull/11553) chore(ci): upgrade Netlify to Node 24 (LTS) + add `git backfill` command ([@&#8203;slorber](https://github.com/slorber))
- `create-docusaurus`, `docusaurus-babel`, `docusaurus-bundler`, `docusaurus-cssnano-preset`, `docusaurus-faster`, `docusaurus-logger`, `docusaurus-mdx-loader`, `docusaurus-module-type-aliases`, `docusaurus-plugin-client-redirects`, `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-plugin-content-pages`, `docusaurus-plugin-css-cascade-layers`, `docusaurus-plugin-debug`, `docusaurus-plugin-google-analytics`, `docusaurus-plugin-google-gtag`, `docusaurus-plugin-google-tag-manager`, `docusaurus-plugin-ideal-image`, `docusaurus-plugin-pwa`, `docusaurus-plugin-rsdoctor`, `docusaurus-plugin-sitemap`, `docusaurus-plugin-svgr`, `docusaurus-plugin-vercel-analytics`, `docusaurus-preset-classic`, `docusaurus-remark-plugin-npm2yarn`, `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus-theme-live-codeblock`, `docusaurus-theme-mermaid`, `docusaurus-theme-search-algolia`, `docusaurus-theme-translations`, `docusaurus-tsconfig`, `docusaurus-types`, `docusaurus-utils-common`, `docusaurus-utils-validation`, `docusaurus-utils`, `docusaurus`, `eslint-plugin`, `lqip-loader`, `stylelint-copyright`
  - [#&#8203;11823](https://github.com/facebook/docusaurus/pull/11823) chore(ci): fixes for the npm trusted publishing workflow ([@&#8203;slorber](https://github.com/slorber))
  - [#&#8203;11819](https://github.com/facebook/docusaurus/pull/11819) chore(ci): add Trusted Publishing release workflow through dispatch action ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-plugin-content-docs`, `docusaurus-plugin-ideal-image`, `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus-theme-mermaid`, `docusaurus-utils`, `docusaurus`
  - [#&#8203;11698](https://github.com/facebook/docusaurus/pull/11698) chore(monorepo): upgrade React packages to v19 ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-cssnano-preset`, `docusaurus-logger`, `docusaurus-mdx-loader`, `docusaurus-plugin-client-redirects`, `docusaurus-plugin-content-blog`, `docusaurus-plugin-content-docs`, `docusaurus-plugin-content-pages`, `docusaurus-plugin-ideal-image`, `docusaurus-remark-plugin-npm2yarn`, `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus-utils-validation`, `docusaurus-utils`, `docusaurus`
  - [#&#8203;11702](https://github.com/facebook/docusaurus/pull/11702) chore(monorepo): upgrade to Jest 30 ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-theme-classic`, `docusaurus-theme-common`, `docusaurus`
  - [#&#8203;11697](https://github.com/facebook/docusaurus/pull/11697) chore(monorepo): upgrade React monorepo types to v19 ([@&#8203;slorber](https://github.com/slorber))
- `docusaurus-babel`
  - [#&#8203;11586](https://github.com/facebook/docusaurus/pull/11586) chore(deps): remove unused [@&#8203;babel/runtime-corejs3](https://github.com/babel/runtime-corejs3) dependency ([@&#8203;JustinBeckwith](https://github.com/JustinBeckwith))
- `docusaurus-plugin-content-blog`
  - [#&#8203;11564](https://github.com/facebook/docusaurus/pull/11564) test(blog): Add basic tests for blog routes. ([@&#8203;slorber](https://github.com/slorber))

##### :globe\_with\_meridians: Translations

- `docusaurus-theme-translations`
  - [#&#8203;11632](https://github.com/facebook/docusaurus/pull/11632) feat(i18n): add Urdu (ur) default theme translations ([@&#8203;hammadurrehman2006](https://github.com/hammadurrehman2006))
  - [#&#8203;11533](https://github.com/facebook/docusaurus/pull/11533) fix(translations): complete theme translations for Algolia pt-br ([@&#8203;luicfrr](https://github.com/luicfrr))

##### Committers: 41

- Akshat Sinha ([@&#8203;akshatsinha0](https://github.com/akshatsinha0))
- Aleksandar Zgonjan ([@&#8203;acosoft](https://github.com/acosoft))
- Andrew Kazakov ([@&#8203;andreww2012](https://github.com/andreww2012))
- Anukool Pandey ([@&#8203;ANUKOOL324](https://github.com/ANUKOOL324))
- Artem Lytkin ([@&#8203;4RH1T3CT0R7](https://github.com/4RH1T3CT0R7))
- Balthasar Hofer ([@&#8203;lebalz](https://github.com/lebalz))
- Bhoomi Sharma ([@&#8203;Bhoomi070](https://github.com/Bhoomi070))
- Cesar Garcia ([@&#8203;Chesars](https://github.com/Chesars))
- Denny Morán ([@&#8203;dmoranp](https://github.com/dmoranp))
- Dmitriy Rotaenko ([@&#8203;dmitriyrotaenko](https://github.com/dmitriyrotaenko))
- Eoin Shaughnessy ([@&#8203;EoinTrial](https://github.com/EoinTrial))
- Gaurav Sulsule ([@&#8203;garry00107](https://github.com/garry00107))
- Gnana Eswar Gunturu ([@&#8203;GnanaEswarGunturu](https://github.com/GnanaEswarGunturu))
- Hugo Häggmark ([@&#8203;hugohaggmark](https://github.com/hugohaggmark))
- Ivan Torres ([@&#8203;torresgol10](https://github.com/torresgol10))
- Justin Beckwith ([@&#8203;JustinBeckwith](https://github.com/JustinBeckwith))
- Kamil Moskała ([@&#8203;moskalakamil](https://github.com/moskalakamil))
- Kohei Watanabe ([@&#8203;kou029w](https://github.com/kou029w))
- Kuldeep Prasad Mishra ([@&#8203;kmish9685](https://github.com/kmish9685))
- Kunwardeep Singh ([@&#8203;work109677-sudo](https://github.com/work109677-sudo))
- Luiz Carlos ([@&#8203;luicfrr](https://github.com/luicfrr))
- Matthew Cheung ([@&#8203;Mcheung7272](https://github.com/Mcheung7272))
- Max Clayton Clowes ([@&#8203;mcclowes](https://github.com/mcclowes))
- Misrilal ([@&#8203;Misrilal-Sah](https://github.com/Misrilal-Sah))
- Muhammad Hammad ur Rehman ([@&#8203;hammadurrehman2006](https://github.com/hammadurrehman2006))
- Nader Jaber ([@&#8203;FlyNumber](https://github.com/FlyNumber))
- Natan Yagudayev ([@&#8203;NatanTechofNY](https://github.com/NatanTechofNY))
- Neel Bansal ([@&#8203;NPX2218](https://github.com/NPX2218))
- Nick Cacace ([@&#8203;BearAlliance](https://github.com/BearAlliance))
- Noah Gregory ([@&#8203;nmggithub](https://github.com/nmggithub))
- Poetry Of Code ([@&#8203;poetryofcode](https://github.com/poetryofcode))
- Pyry Takala ([@&#8203;pyrytakala](https://github.com/pyrytakala))
- Salman Chishti ([@&#8203;salmanmkc](https://github.com/salmanmkc))
- Sreehari Upas ([@&#8203;SreehariU](https://github.com/SreehariU))
- Sébastien Lorber ([@&#8203;slorber](https://github.com/slorber))
- Vedant Madane ([@&#8203;VedantMadane](https://github.com/VedantMadane))
- Vedika Gupta ([@&#8203;VedikaGupt](https://github.com/VedikaGupt))
- Zoey Greer ([@&#8203;tempoz](https://github.com/tempoz))
- [@&#8203;TheCyperpunk](https://github.com/TheCyperpunk)
- [@&#8203;snikkrs](https://github.com/snikkrs)
- fre$h ([@&#8203;fresh3nough](https://github.com/fresh3nough)

</details>

<details>
<summary>Effect-TS/language-service (@&#8203;effect/language-service)</summary>

### [`v0.85.1`](https://github.com/Effect-TS/language-service/releases/tag/%40effect/language-service%400.85.1)

[Compare Source](https://github.com/Effect-TS/language-service/compare/@effect/language-service@0.85.0...@effect/language-service@0.85.1)

##### Patch Changes

- [#&#8203;726](https://github.com/Effect-TS/language-service/pull/726) [`fd4a8da`](fd4a8da7f4) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Update the Effect v4 beta examples and type parsing to match the renamed Context APIs in the latest 4.0.0-beta releases.

- [#&#8203;724](https://github.com/Effect-TS/language-service/pull/724) [`14d5798`](14d57985e2) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Refactor Effect context tracking to use cached node context flags and direct generator lookups.

  This aligns the TypeScript implementation more closely with the TSGo version and simplifies diagnostics that need to detect whether code is inside an Effect generator.

### [`v0.85.0`](https://github.com/Effect-TS/language-service/releases/tag/%40effect/language-service%400.85.0)

[Compare Source](https://github.com/Effect-TS/language-service/compare/@effect/language-service@0.84.3...@effect/language-service@0.85.0)

##### Minor Changes

- [#&#8203;720](https://github.com/Effect-TS/language-service/pull/720) [`4229bb9`](4229bb9ec8) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add the `nestedEffectGenYield` diagnostic to detect `yield* Effect.gen(...)` inside an existing Effect generator context.

  Example:

  ```ts
  Effect.gen(function* () {
    yield* Effect.gen(function* () {
      yield* Effect.succeed(1);
    });
  });
  ```

- [#&#8203;723](https://github.com/Effect-TS/language-service/pull/723) [`da9cc4b`](da9cc4bed7) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add the `effectMapFlatten` style diagnostic for `Effect.map(...)` immediately followed by `Effect.flatten` in pipe flows.

  Example:

  ```ts
  import { Effect } from "effect";

  const program = Effect.succeed(1).pipe(
    Effect.map((n) => Effect.succeed(n + 1)),
    Effect.flatten
  );
  ```

- [#&#8203;718](https://github.com/Effect-TS/language-service/pull/718) [`0af7c0f`](0af7c0f48f) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add the `lazyPromiseInEffectSync` diagnostic to catch `Effect.sync(() => Promise...)` patterns and suggest using `Effect.promise` or `Effect.tryPromise` for async work.

  Example:

  ```ts
  Effect.sync(() => Promise.resolve(1));
  ```

- [#&#8203;714](https://github.com/Effect-TS/language-service/pull/714) [`32985b2`](32985b2cf6) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add `processEnv` and `processEnvInEffect` diagnostics to guide `process.env.*` reads toward Effect `Config` APIs.

  Examples:

  - `process.env.PORT`
  - `process.env["API_KEY"]`

- [#&#8203;721](https://github.com/Effect-TS/language-service/pull/721) [`f05ae89`](f05ae898bb) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add the `unnecessaryArrowBlock` style diagnostic for arrow functions whose block body only returns an expression.

  Example:

  ```ts
  const trim = (value: string) => {
    return value.trim();
  };
  ```

- [#&#8203;717](https://github.com/Effect-TS/language-service/pull/717) [`b77848a`](b77848a6ed) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add `newPromise` and `asyncFunction` effect-native diagnostics to report manual `Promise` construction and async function declarations, with guidance toward Effect-based async control flow.

- [#&#8203;722](https://github.com/Effect-TS/language-service/pull/722) [`6f19858`](6f19858808) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add the `effectDoNotation` style diagnostic for `Effect.Do` usage and suggest migrating to `Effect.gen` or `Effect.fn`.

  Example:

  ```ts
  import { pipe } from "effect/Function";
  import { Effect } from "effect";

  const program = pipe(
    Effect.Do,
    Effect.bind("a", () => Effect.succeed(1)),
    Effect.let("b", ({ a }) => a + 1)
  );
  ```

- [#&#8203;716](https://github.com/Effect-TS/language-service/pull/716) [`c3f67b0`](c3f67b0411) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Add `cryptoRandomUUID` and `cryptoRandomUUIDInEffect` diagnostics for Effect v4 to discourage `crypto.randomUUID()` in favor of the Effect `Random` module, which uses Effect-injected randomness instead of the global crypto implementation.

##### Patch Changes

- [#&#8203;719](https://github.com/Effect-TS/language-service/pull/719) [`d23980a`](d23980a785) Thanks [@&#8203;mattiamanzati](https://github.com/mattiamanzati)! - Update the Effect v4 beta dependencies to `4.0.0-beta.43` for the language service and v4 harness packages.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDkuNSIsInVwZGF0ZWRJblZlciI6IjQzLjExMC4xNCIsInRhcmdldEJyYW5jaCI6Im5leHQiLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: #45
Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud>
Co-committed-by: Renovate Bot <renovate-bot@valverde.cloud>
2026-04-23 10:36:24 +02:00
ff13e941e3 0.2.5 (#43)
All checks were successful
Publish / publish (push) Successful in 52s
Lint / lint (push) Successful in 14s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud>
Reviewed-on: #43
2026-03-31 21:01:12 +02:00
20 changed files with 1056 additions and 796 deletions

713
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@effect-fc/monorepo", "name": "@effect-fc/monorepo",
"packageManager": "bun@1.3.6", "packageManager": "bun@1.3.11",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"./packages/*" "./packages/*"
@@ -15,12 +15,12 @@
"clean:modules": "turbo clean:modules && rm -rf node_modules" "clean:modules": "turbo clean:modules && rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.11", "@biomejs/biome": "^2.4.9",
"@effect/language-service": "^0.75.0", "@effect/language-service": "^0.85.0",
"@types/bun": "^1.3.6", "@types/bun": "^1.3.11",
"npm-check-updates": "^19.3.1", "npm-check-updates": "^21.0.0",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
"turbo": "^2.7.5", "turbo": "^2.8.21",
"typescript": "^5.9.3" "typescript": "^6.0.2"
} }
} }

View File

@@ -15,8 +15,8 @@
"typecheck": "tsc" "typecheck": "tsc"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "3.9.2", "@docusaurus/core": "3.10.0",
"@docusaurus/preset-classic": "3.9.2", "@docusaurus/preset-classic": "3.10.0",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0", "prism-react-renderer": "^2.3.0",
@@ -24,10 +24,10 @@
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/module-type-aliases": "3.10.0",
"@docusaurus/tsconfig": "3.9.2", "@docusaurus/tsconfig": "3.10.0",
"@docusaurus/types": "3.9.2", "@docusaurus/types": "3.10.0",
"typescript": "~5.6.2" "typescript": "~6.0.0"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@@ -1,7 +1,7 @@
{ {
"name": "effect-fc", "name": "effect-fc",
"description": "Write React function components with Effect", "description": "Write React function components with Effect",
"version": "0.2.4", "version": "0.2.5",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -38,11 +38,14 @@
"clean:modules": "rm -rf node_modules" "clean:modules": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@effect/platform-browser": "^0.74.0" "@effect/platform-browser": "^0.76.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"effect": "^3.19.0", "effect": "^3.21.0",
"react": "^19.2.0" "react": "^19.2.0"
},
"dependencies": {
"effect-lens": "^0.1.4"
} }
} }

View File

@@ -1,105 +1,101 @@
import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect" import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, Function, identity, Option, ParseResult, Pipeable, Predicate, Schema, type Scope, Stream } from "effect"
import type * as React from "react" import type * as React from "react"
import * as Component from "./Component.js" import * as Component from "./Component.js"
import * as Lens from "./Lens.js"
import * as Mutation from "./Mutation.js" import * as Mutation from "./Mutation.js"
import * as PropertyPath from "./PropertyPath.js"
import * as Result from "./Result.js" import * as Result from "./Result.js"
import * as Subscribable from "./Subscribable.js" import * as Subscribable from "./Subscribable.js"
import * as SubscriptionRef from "./SubscriptionRef.js" import * as SubscriptionRef from "./SubscriptionRef.js"
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form") export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
export type FormTypeId = typeof FormTypeId export type FormTypeId = typeof FormTypeId
export interface Form<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never> export interface Form<out P extends readonly PropertyKey[], in out A, in out I = A, in out ER = never, in out EW = never>
extends Pipeable.Pipeable { extends Pipeable.Pipeable {
readonly [FormTypeId]: FormTypeId readonly [FormTypeId]: FormTypeId
readonly path: P
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[], never, never>
readonly isValidating: Subscribable.Subscribable<boolean, ER, never>
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>
readonly isSubmitting: Subscribable.Subscribable<boolean, never, never>
}
export class FormImpl<out P extends readonly PropertyKey[], in out A, in out I = A, in out ER = never, in out EW = never>
extends Pipeable.Class() implements Form<P, A, I, ER, EW> {
readonly [FormTypeId]: FormTypeId = FormTypeId
constructor(
readonly path: P,
readonly value: Subscribable.Subscribable<Option.Option<A>, ER, never>,
readonly encodedValue: Lens.Lens<I, ER, EW, never, never>,
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[], never, never>,
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>,
readonly isSubmitting: Subscribable.Subscribable<boolean, never, never>,
) {
super()
}
}
export const RootFormTypeId: unique symbol = Symbol.for("@effect-fc/Form/RootForm")
export type RootFormTypeId = typeof RootFormTypeId
export interface RootForm<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends Form<readonly [], A, I, never, never> {
readonly schema: Schema.Schema<A, I, R> readonly schema: Schema.Schema<A, I, R>
readonly context: Context.Context<Scope.Scope | R> readonly context: Context.Context<Scope.Scope | R>
readonly mutation: Mutation.Mutation< readonly mutation: Mutation.Mutation<
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>], readonly [value: A, form: RootForm<A, I, R, unknown, unknown, unknown>],
MA, ME, MR, MP MA, ME, MR, MP
> >
readonly autosubmit: boolean readonly autosubmit: boolean
readonly debounce: Option.Option<Duration.DurationInput>
readonly value: Subscribable.Subscribable<Option.Option<A>> readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never>
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
readonly canSubmit: Subscribable.Subscribable<boolean>
field<const P extends PropertyPath.Paths<I>>(
path: P
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
readonly run: Effect.Effect<void> readonly run: Effect.Effect<void>
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
} }
export class FormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never> export class RootFormImpl<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> { extends Pipeable.Class() implements RootForm<A, I, R, MA, ME, MR, MP> {
readonly [FormTypeId]: FormTypeId = FormTypeId readonly [FormTypeId]: FormTypeId = FormTypeId
readonly [RootFormTypeId]: RootFormTypeId = RootFormTypeId
readonly path = [] as const
constructor( constructor(
readonly schema: Schema.Schema<A, I, R>, readonly schema: Schema.Schema<A, I, R>,
readonly context: Context.Context<Scope.Scope | R>, readonly context: Context.Context<Scope.Scope | R>,
readonly mutation: Mutation.Mutation< readonly mutation: Mutation.Mutation<
readonly [value: A, form: Form<A, I, R, unknown, unknown, unknown>], readonly [value: A, form: RootForm<A, I, R, unknown, unknown, unknown>],
MA, ME, MR, MP MA, ME, MR, MP
>, >,
readonly autosubmit: boolean, readonly autosubmit: boolean,
readonly debounce: Option.Option<Duration.DurationInput>,
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>, readonly value: Lens.Lens<Option.Option<A>, never, never, never, never>,
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>, readonly encodedValue: Lens.Lens<I, never, never, never, never>,
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>, readonly issues: Lens.Lens<readonly ParseResult.ArrayFormatterIssue[], never, never, never, never>,
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>, readonly validationFiber: Lens.Lens<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>, never, never, never, never>,
readonly isValidating: Subscribable.Subscribable<boolean, never, never>,
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>,
readonly isSubmitting: Subscribable.Subscribable<boolean, never, never>,
readonly runSemaphore: Effect.Semaphore, readonly runSemaphore: Effect.Semaphore,
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
) { ) {
super() super()
this.canSubmit = Subscribable.map(
Subscribable.zipLatestAll(this.value, this.error, this.validationFiber, this.mutation.result),
([value, error, validationFiber, result]) => (
Option.isSome(value) &&
Option.isNone(error) &&
Option.isNone(validationFiber) &&
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
),
)
} }
field<const P extends PropertyPath.Paths<I>>(
path: P
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>> {
const key = new FormFieldKey(path)
return this.fieldCache.pipe(
Effect.map(HashMap.get(key)),
Effect.flatMap(Option.match({
onSome: v => Effect.succeed(v as FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>),
onNone: () => Effect.tap(
Effect.succeed(makeFormField(this as Form<A, I, R, MA, ME, MR, MP>, path)),
v => Ref.update(this.fieldCache, HashMap.set(key, v as FormField<unknown, unknown>)),
),
})),
)
}
readonly canSubmit: Subscribable.Subscribable<boolean>
get run(): Effect.Effect<void> { get run(): Effect.Effect<void> {
return this.runSemaphore.withPermits(1)(Stream.runForEach( return this.runSemaphore.withPermits(1)(Stream.runForEach(
this.encodedValue.changes.pipe( this.encodedValue.changes,
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
),
encodedValue => this.validationFiber.pipe( encodedValue => Lens.get(this.validationFiber).pipe(
Effect.andThen(Option.match({ Effect.andThen(Option.match({
onSome: Fiber.interrupt, onSome: Fiber.interrupt,
onNone: () => Effect.void, onNone: () => Effect.void,
@@ -110,18 +106,21 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
exit => Effect.andThen( exit => Effect.andThen(
Exit.matchEffect(exit, { Exit.matchEffect(exit, {
onSuccess: v => Effect.andThen( onSuccess: v => Effect.andThen(
Ref.set(this.value, Option.some(v)), Lens.set(this.value, Option.some(v)),
Ref.set(this.error, Option.none()), Lens.set(this.issues, Array.empty()),
), ),
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), { onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
onSome: e => Ref.set(this.error, Option.some(e)), onSome: e => Effect.flatMap(
ParseResult.ArrayFormatter.formatError(e),
v => Lens.set(this.issues, v),
),
onNone: () => Effect.void, onNone: () => Effect.void,
}), }),
}), }),
Ref.set(this.validationFiber, Option.none()), Lens.set(this.validationFiber, Option.none()),
), ),
)).pipe( )).pipe(
Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))), Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))),
Effect.andThen(Fiber.join), Effect.andThen(Fiber.join),
Effect.andThen(value => this.autosubmit Effect.andThen(value => this.autosubmit
? Effect.asVoid(Effect.forkScoped(this.submitValue(value))) ? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
@@ -136,7 +135,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
} }
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> { get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
return this.value.pipe( return Lens.get(this.value).pipe(
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(value => this.submitValue(value)), Effect.andThen(value => this.submitValue(value)),
) )
@@ -153,7 +152,10 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
e => e._tag === "ParseError", e => e._tag === "ParseError",
), ),
{ {
onSome: e => Ref.set(this.error, Option.some(e)), onSome: e => Effect.flatMap(
ParseResult.ArrayFormatter.formatError(e),
v => Lens.set(this.issues, v),
),
onNone: () => Effect.void, onNone: () => Effect.void,
}, },
) )
@@ -164,42 +166,59 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
} }
} }
export const isForm = (u: unknown): u is Form<unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
export const isForm = (u: unknown): u is Form<readonly PropertyKey[], unknown, unknown> => Predicate.hasProperty(u, FormTypeId)
export const isRootForm = (u: unknown): u is RootForm<readonly PropertyKey[], unknown, unknown, unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, RootFormTypeId)
export declare namespace make { export declare namespace make {
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never> export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
extends Mutation.make.Options< extends Mutation.make.Options<
readonly [value: NoInfer<A>, form: Form<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>], readonly [value: NoInfer<A>, form: RootForm<NoInfer<A>, NoInfer<I>, NoInfer<R>, unknown, unknown, unknown>],
MA, ME, MR, MP MA, ME, MR, MP
> { > {
readonly schema: Schema.Schema<A, I, R> readonly schema: Schema.Schema<A, I, R>
readonly initialEncodedValue: NoInfer<I> readonly initialEncodedValue: NoInfer<I>
readonly autosubmit?: boolean readonly autosubmit?: boolean
readonly debounce?: Duration.DurationInput
} }
} }
export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>( export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
options: make.Options<A, I, R, MA, ME, MR, MP> options: make.Options<A, I, R, MA, ME, MR, MP>
): Effect.fn.Return< ): Effect.fn.Return<
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>, RootForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
never, never,
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP> Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP>
> { > {
return new FormImpl( const mutation = yield* Mutation.make(options)
const valueLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<A>()))
const issuesLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make<readonly ParseResult.ArrayFormatterIssue[]>(Array.empty()))
const validationFiberLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()))
return new RootFormImpl(
options.schema, options.schema,
yield* Effect.context<Scope.Scope | R>(), yield* Effect.context<Scope.Scope | R>(),
yield* Mutation.make(options), mutation,
options.autosubmit ?? false, options.autosubmit ?? false,
Option.fromNullable(options.debounce),
yield* SubscriptionRef.make(Option.none<A>()), valueLens,
yield* SubscriptionRef.make(options.initialEncodedValue), Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)),
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()), issuesLens,
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()), validationFiberLens,
Subscribable.map(validationFiberLens, Option.isSome),
Subscribable.map(
Subscribable.zipLatestAll(valueLens, issuesLens, validationFiberLens, mutation.result),
([value, issues, validationFiber, result]) => (
Option.isSome(value) &&
Array.isEmptyReadonlyArray(issues) &&
Option.isNone(validationFiber) &&
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
),
),
Subscribable.map(mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
yield* Effect.makeSemaphore(1), yield* Effect.makeSemaphore(1),
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
) )
}) })
@@ -211,83 +230,125 @@ export declare namespace service {
export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>( export const service = <A, I = A, R = never, MA = void, ME = never, MR = never, MP = never>(
options: service.Options<A, I, R, MA, ME, MR, MP> options: service.Options<A, I, R, MA, ME, MR, MP>
): Effect.Effect< ): Effect.Effect<
Form<A, I, R, MA, ME, Result.forkEffect.OutputContext<MA, ME, MR, MP>, MP>, RootForm<A, I, R, MA, ME, Result.forkEffect.OutputContext<MR, MP>, MP>,
never, never,
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP> Scope.Scope | R | Result.forkEffect.OutputContext<MR, MP>
> => Effect.tap( > => Effect.tap(
make(options), make(options),
form => Effect.forkScoped(form.run), form => Effect.forkScoped(form.run),
) )
export const FormFieldTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormField") const filterIssuesByPath = (
export type FormFieldTypeId = typeof FormFieldTypeId issues: readonly ParseResult.ArrayFormatterIssue[],
path: readonly PropertyKey[],
export interface FormField<in out A, in out I = A> ): readonly ParseResult.ArrayFormatterIssue[] => Array.filter(issues, issue =>
extends Pipeable.Pipeable { issue.path.length >= path.length && Array.every(path, (p, i) => p === issue.path[i])
readonly [FormFieldTypeId]: FormFieldTypeId
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
readonly isValidating: Subscribable.Subscribable<boolean>
readonly isSubmitting: Subscribable.Subscribable<boolean>
}
class FormFieldImpl<in out A, in out I = A>
extends Pipeable.Class() implements FormField<A, I> {
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
constructor(
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
readonly isValidating: Subscribable.Subscribable<boolean>,
readonly isSubmitting: Subscribable.Subscribable<boolean>,
) {
super()
}
}
const FormFieldKeyTypeId: unique symbol = Symbol.for("@effect-fc/Form/FormFieldKey")
type FormFieldKeyTypeId = typeof FormFieldKeyTypeId
class FormFieldKey implements Equal.Equal {
readonly [FormFieldKeyTypeId]: FormFieldKeyTypeId = FormFieldKeyTypeId
constructor(readonly path: PropertyPath.PropertyPath) {}
[Equal.symbol](that: Equal.Equal) {
return isFormFieldKey(that) && PropertyPath.equivalence(this.path, that.path)
}
[Hash.symbol]() {
return Hash.array(this.path)
}
}
export const isFormField = (u: unknown): u is FormField<unknown, unknown> => Predicate.hasProperty(u, FormFieldTypeId)
const isFormFieldKey = (u: unknown): u is FormFieldKey => Predicate.hasProperty(u, FormFieldKeyTypeId)
export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyPath.Paths<NoInfer<I>>>(
self: Form<A, I, R, MA, ME, MR, MP>,
path: P,
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
return new FormFieldImpl(
Subscribable.mapEffect(self.value, Option.match({
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
onNone: () => Option.some(Option.none()),
})),
SubscriptionSubRef.makeFromPath(self.encodedValue, path),
Subscribable.mapEffect(self.error, Option.match({
onSome: flow(
ParseResult.ArrayFormatter.formatError,
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
),
onNone: () => Effect.succeed([]),
})),
Subscribable.map(self.validationFiber, Option.isSome),
Subscribable.map(self.mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
) )
}
export const focusObjectField: {
<P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>(
self: Form<P, A, I, ER, EW>,
key: K,
): Form<readonly [...P, K], A[K], I[K], ER, EW>
<P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>(
key: K,
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, K], A[K], I[K], ER, EW>
} = Function.dual(2, <P extends readonly PropertyKey[], A extends object, I extends object, ER, EW, K extends keyof A & keyof I>(
self: Form<P, A, I, ER, EW>,
key: K,
): Form<readonly [...P, K], A[K], I[K], ER, EW> => {
const form = self as FormImpl<P, A, I, ER, EW>
const path = [...form.path, key] as const
return new FormImpl(
path,
Subscribable.mapOption(form.value, a => a[key]),
Lens.focusObjectField(form.encodedValue, key),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
)
})
export const focusArrayAt: {
<P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
self: Form<P, A, I, ER, EW>,
index: number,
): Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException>
<P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
index: number,
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException>
} = Function.dual(2, <P extends readonly PropertyKey[], A extends readonly any[], I extends readonly any[], ER, EW>(
self: Form<P, A, I, ER, EW>,
index: number,
): Form<readonly [...P, number], A[number], I[number], ER | Cause.NoSuchElementException, EW | Cause.NoSuchElementException> => {
const form = self as FormImpl<P, A, I, ER, EW>
const path = [...form.path, index] as const
return new FormImpl(
path,
Subscribable.mapOptionEffect(form.value, Array.get(index)),
Lens.focusArrayAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
)
})
export const focusTupleAt: {
<P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
self: Form<P, A, I, ER, EW>,
index: K,
): Form<readonly [...P, K], A[K], I[K], ER, EW>
<P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
index: K,
): (self: Form<P, A, I, ER, EW>) => Form<readonly [...P, K], A[K], I[K], ER, EW>
} = Function.dual(2, <P extends readonly PropertyKey[], A extends readonly [any, ...any[]], I extends readonly [any, ...any[]], ER, EW, K extends number>(
self: Form<P, A, I, ER, EW>,
index: K,
): Form<readonly [...P, K], A[K], I[K], ER, EW> => {
const form = self as FormImpl<P, A, I, ER, EW>
const path = [...form.path, index] as const
return new FormImpl(
path,
Subscribable.mapOption(form.value, Array.unsafeGet(index)),
Lens.focusTupleAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
)
})
export const focusChunkAt: {
<P extends readonly PropertyKey[], A, I, ER, EW>(
self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>,
index: number,
): Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW>
<P extends readonly PropertyKey[], A, I, ER, EW>(
index: number,
): (self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>) => Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW>
} = Function.dual(2, <P extends readonly PropertyKey[], A, I, ER, EW>(
self: Form<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>,
index: number,
): Form<readonly [...P, number], A, I, ER | Cause.NoSuchElementException, EW> => {
const form = self as FormImpl<P, Chunk.Chunk<A>, Chunk.Chunk<I>, ER, EW>
const path = [...form.path, index] as const
return new FormImpl(
path,
Subscribable.mapOptionEffect(form.value, Chunk.get(index)),
Lens.focusChunkAt(form.encodedValue, index),
Subscribable.map(form.issues, issues => filterIssuesByPath(issues, path)),
form.isValidating,
form.canSubmit,
form.isSubmitting,
)
})
export namespace useInput { export namespace useInput {
@@ -301,33 +362,39 @@ export namespace useInput {
} }
} }
export const useInput = Effect.fnUntraced(function* <A, I>( export const useInput = Effect.fnUntraced(function* <P extends readonly PropertyKey[], A, I, ER, EW>(
field: FormField<A, I>, form: Form<P, A, I, ER, EW>,
options?: useInput.Options, options?: useInput.Options,
): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> { ): Effect.fn.Return<useInput.Success<I>, ER, Scope.Scope> {
const internalValueRef = yield* Component.useOnChange(() => Effect.tap( const internalValueLens = yield* Component.useOnChange(() => Effect.gen(function*() {
Effect.andThen(field.encodedValue, SubscriptionRef.make), const internalValueLens = yield* Lens.get(form.encodedValue).pipe(
internalValueRef => Effect.forkScoped(Effect.all([ Effect.flatMap(SubscriptionRef.make),
Effect.map(Lens.fromSubscriptionRef),
)
yield* Effect.forkScoped(Effect.all([
Stream.runForEach( Stream.runForEach(
Stream.drop(field.encodedValue, 1), Stream.drop(form.encodedValue.changes, 1),
upstreamEncodedValue => Effect.whenEffect( upstreamEncodedValue => Effect.whenEffect(
Ref.set(internalValueRef, upstreamEncodedValue), Lens.set(internalValueLens, upstreamEncodedValue),
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), Effect.andThen(Lens.get(internalValueLens), internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
), ),
), ),
Stream.runForEach( Stream.runForEach(
internalValueRef.changes.pipe( internalValueLens.changes.pipe(
Stream.drop(1), Stream.drop(1),
Stream.changesWith(Equal.equivalence()), Stream.changesWith(Equal.equivalence()),
options?.debounce ? Stream.debounce(options.debounce) : identity, options?.debounce ? Stream.debounce(options.debounce) : identity,
), ),
internalValue => Ref.set(field.encodedValue, internalValue), internalValue => Lens.set(form.encodedValue, internalValue),
), ),
], { concurrency: "unbounded" })), ], { concurrency: "unbounded" }))
), [field, options?.debounce])
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) return internalValueLens
}), [form, options?.debounce])
const [value, setValue] = yield* Lens.useState(internalValueLens)
return { value, setValue } return { value, setValue }
}) })
@@ -342,55 +409,63 @@ export namespace useOptionalInput {
} }
} }
export const useOptionalInput = Effect.fnUntraced(function* <A, I>( export const useOptionalInput = Effect.fnUntraced(function* <P extends readonly PropertyKey[], A, I, ER, EW>(
field: FormField<A, Option.Option<I>>, field: Form<P, A, Option.Option<I>, ER, EW>,
options: useOptionalInput.Options<I>, options: useOptionalInput.Options<I>,
): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> { ): Effect.fn.Return<useOptionalInput.Success<I>, ER, Scope.Scope> {
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap( const [enabledLens, internalValueLens] = yield* Component.useOnChange(() => Effect.gen(function*() {
Effect.andThen( const [enabledLens, internalValueLens] = yield* Effect.flatMap(
field.encodedValue, Lens.get(field.encodedValue),
Option.match({ Option.match({
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]), onSome: v => Effect.all([
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]), Effect.map(SubscriptionRef.make(true), Lens.fromSubscriptionRef),
Effect.map(SubscriptionRef.make(v), Lens.fromSubscriptionRef),
]),
onNone: () => Effect.all([
Effect.map(SubscriptionRef.make(false), Lens.fromSubscriptionRef),
Effect.map(SubscriptionRef.make(options.defaultValue), Lens.fromSubscriptionRef),
]),
}), }),
), )
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([ yield* Effect.forkScoped(Effect.all([
Stream.runForEach( Stream.runForEach(
Stream.drop(field.encodedValue, 1), Stream.drop(field.encodedValue.changes, 1),
upstreamEncodedValue => Effect.whenEffect( upstreamEncodedValue => Effect.whenEffect(
Option.match(upstreamEncodedValue, { Option.match(upstreamEncodedValue, {
onSome: v => Effect.andThen( onSome: v => Effect.andThen(
Ref.set(enabledRef, true), Lens.set(enabledLens, true),
Ref.set(internalValueRef, v), Lens.set(internalValueLens, v),
), ),
onNone: () => Effect.andThen( onNone: () => Effect.andThen(
Ref.set(enabledRef, false), Lens.set(enabledLens, false),
Ref.set(internalValueRef, options.defaultValue), Lens.set(internalValueLens, options.defaultValue),
), ),
}), }),
Effect.andThen( Effect.andThen(
Effect.all([enabledRef, internalValueRef]), Effect.all([Lens.get(enabledLens), Lens.get(internalValueLens)]),
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()), ([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
), ),
), ),
), ),
Stream.runForEach( Stream.runForEach(
enabledRef.changes.pipe( enabledLens.changes.pipe(
Stream.zipLatest(internalValueRef.changes), Stream.zipLatest(internalValueLens.changes),
Stream.drop(1), Stream.drop(1),
Stream.changesWith(Equal.equivalence()), Stream.changesWith(Equal.equivalence()),
options?.debounce ? Stream.debounce(options.debounce) : identity, options?.debounce ? Stream.debounce(options.debounce) : identity,
), ),
([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()), ([enabled, internalValue]) => Lens.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
), ),
], { concurrency: "unbounded" })), ], { concurrency: "unbounded" }))
), [field, options.debounce])
const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef) return [enabledLens, internalValueLens] as const
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef) }), [field, options.debounce])
const [enabled, setEnabled] = yield* Lens.useState(enabledLens)
const [value, setValue] = yield* Lens.useState(internalValueLens)
return { enabled, setEnabled, value, setValue } return { enabled, setEnabled, value, setValue }
}) })

View File

@@ -0,0 +1,63 @@
import { Effect, Equivalence, Stream } from "effect"
import { Lens } from "effect-lens"
import * as React from "react"
import * as Component from "./Component.js"
import * as SetStateAction from "./SetStateAction.js"
import * as SubscriptionRef from "./SubscriptionRef.js"
export * from "effect-lens/Lens"
export declare namespace useState {
export interface Options<A> {
readonly equivalence?: Equivalence.Equivalence<A>
}
}
export const useState = Effect.fnUntraced(function* <A, ER, EW, RR, RW>(
lens: Lens.Lens<A, ER, EW, RR, RW>,
options?: useState.Options<NoInfer<A>>,
): Effect.fn.Return<readonly [A, React.Dispatch<React.SetStateAction<A>>], ER, RR | RW> {
const [reactStateValue, setReactStateValue] = React.useState(yield* Component.useOnMount(() => Lens.get(lens)))
yield* Component.useReactEffect(() => Effect.forkScoped(
Stream.runForEach(
Stream.changesWith(lens.changes, options?.equivalence ?? Equivalence.strict()),
v => Effect.sync(() => setReactStateValue(v)),
)
), [lens])
const setValue = yield* Component.useCallbackSync(
(setStateAction: React.SetStateAction<A>) => Effect.andThen(
Lens.updateAndGet(lens, prevState => SetStateAction.value(setStateAction, prevState)),
v => setReactStateValue(v),
),
[lens],
)
return [reactStateValue, setValue]
})
export declare namespace useFromState {
export interface Options<A> {
readonly equivalence?: Equivalence.Equivalence<A>
}
}
export const useFromState = Effect.fnUntraced(function* <A>(
[value, setValue]: readonly [A, React.Dispatch<React.SetStateAction<A>>],
options?: useFromState.Options<NoInfer<A>>,
): Effect.fn.Return<Lens.Lens<A, never, never, never, never>> {
const lens = yield* Component.useOnMount(() => Effect.map(
SubscriptionRef.make(value),
Lens.fromSubscriptionRef,
))
yield* Component.useReactEffect(() => Effect.forkScoped(Stream.runForEach(
Stream.changesWith(lens.changes, options?.equivalence ?? Equivalence.strict()),
v => Effect.sync(() => setValue(v)),
)), [setValue])
yield* Component.useReactEffect(() => Lens.set(lens, value), [value])
return lens
})

View File

@@ -111,12 +111,12 @@ export declare namespace make {
export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never>( export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey = never, A = void, E = never, R = never, P = never>(
options: make.Options<K, A, E, R, P> options: make.Options<K, A, E, R, P>
): Effect.fn.Return< ): Effect.fn.Return<
Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>, Mutation<K, A, E, Result.forkEffect.OutputContext<R, P>, P>,
never, never,
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P> Scope.Scope | Result.forkEffect.OutputContext<R, P>
> { > {
return new MutationImpl( return new MutationImpl(
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(), yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<R, P>>(),
options.f as any, options.f as any,
options.initialProgress as P, options.initialProgress as P,

View File

@@ -0,0 +1,67 @@
import { describe, expect, test } from "bun:test"
import { Option } from "effect"
import * as PropertyPath from "./PropertyPath.js"
describe("immutableSet with arrays", () => {
test("sets a top-level array element", () => {
const arr = [1, 2, 3]
const result = PropertyPath.immutableSet(arr, [1], 99)
expect(result).toEqual(Option.some([1, 99, 3]))
})
test("does not mutate the original array", () => {
const arr = [1, 2, 3]
PropertyPath.immutableSet(arr, [0], 42)
expect(arr).toEqual([1, 2, 3])
})
test("sets the first element of an array", () => {
const arr = ["a", "b", "c"]
const result = PropertyPath.immutableSet(arr, [0], "z")
expect(result).toEqual(Option.some(["z", "b", "c"]))
})
test("sets the last element of an array", () => {
const arr = [10, 20, 30]
const result = PropertyPath.immutableSet(arr, [2], 99)
expect(result).toEqual(Option.some([10, 20, 99]))
})
test("sets a nested array element inside an object", () => {
const obj = { tags: ["foo", "bar", "baz"] }
const result = PropertyPath.immutableSet(obj, ["tags", 1], "qux")
expect(result).toEqual(Option.some({ tags: ["foo", "qux", "baz"] }))
})
test("sets a deeply nested value inside an array of objects", () => {
const obj = { items: [{ name: "alice" }, { name: "bob" }] }
const result = PropertyPath.immutableSet(obj, ["items", 0, "name"], "charlie")
expect(result).toEqual(Option.some({ items: [{ name: "charlie" }, { name: "bob" }] }))
})
test("sets a value in a nested array", () => {
const matrix = [[1, 2], [3, 4]]
const result = PropertyPath.immutableSet(matrix, [1, 0], 99)
expect(result).toEqual(Option.some([[1, 2], [99, 4]]))
})
test("returns Option.none() for an out-of-bounds index", () => {
const arr = [1, 2, 3]
const result = PropertyPath.immutableSet(arr, [5], 99)
expect(result).toEqual(Option.none())
})
test("returns Option.none() for a non-numeric key on an array", () => {
const arr = [1, 2, 3]
// @ts-expect-error intentionally wrong key type
const result = PropertyPath.immutableSet(arr, ["length"], 0)
expect(result).toEqual(Option.none())
})
test("empty path returns Option.some of the value itself", () => {
const arr = [1, 2, 3]
const result = PropertyPath.immutableSet(arr, [], [9, 9, 9] as any)
expect(result).toEqual(Option.some([9, 9, 9]))
})
})

View File

@@ -281,14 +281,14 @@ export declare namespace make {
export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>( export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
options: make.Options<K, A, KE, KR, E, R, P> options: make.Options<K, A, KE, KR, E, R, P>
): Effect.fn.Return< ): Effect.fn.Return<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>, Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
never, never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P> Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
> { > {
const client = yield* QueryClient.QueryClient const client = yield* QueryClient.QueryClient
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>( return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>(
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(), yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>>(),
options.key, options.key,
options.f as any, options.f as any,
options.initialProgress as P, options.initialProgress as P,
@@ -308,9 +308,9 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, KE =
export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>( export const service = <K extends Query.AnyKey, A, KE = never, KR = never, E = never, R = never, P = never>(
options: make.Options<K, A, KE, KR, E, R, P> options: make.Options<K, A, KE, KR, E, R, P>
): Effect.Effect< ): Effect.Effect<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>, Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
never, never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P> Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
> => Effect.tap( > => Effect.tap(
make(options), make(options),
query => Effect.forkScoped(query.run), query => Effect.forkScoped(query.run),

View File

@@ -1,4 +1,5 @@
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect" import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, pipe, Pipeable, Predicate, PubSub, Ref, type Scope, Stream, type Subscribable, SynchronizedRef } from "effect"
import { Lens } from "effect-lens"
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result") export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
@@ -15,10 +16,6 @@ export type Final<A, E = never, P = never> = (Success<A> | Failure<E>) & ({} | F
export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P> export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P>
export declare namespace Result { export declare namespace Result {
export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
readonly [ResultTypeId]: ResultTypeId
}
export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never export type Success<R extends Result<any, any, any>> = [R] extends [Result<infer A, infer _E, infer _P>] ? A : never
export type Failure<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer E, infer _P>] ? E : never export type Failure<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer E, infer _P>] ? E : never
export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
@@ -28,21 +25,21 @@ export declare namespace Flags {
export type Keys = keyof WillFetch & WillRefresh & Refreshing<any> export type Keys = keyof WillFetch & WillRefresh & Refreshing<any>
} }
export interface Initial extends Result.Prototype { export interface Initial extends ResultPrototype {
readonly _tag: "Initial" readonly _tag: "Initial"
} }
export interface Running<P = never> extends Result.Prototype { export interface Running<P = never> extends ResultPrototype {
readonly _tag: "Running" readonly _tag: "Running"
readonly progress: P readonly progress: P
} }
export interface Success<A> extends Result.Prototype { export interface Success<A> extends ResultPrototype {
readonly _tag: "Success" readonly _tag: "Success"
readonly value: A readonly value: A
} }
export interface Failure<E = never> extends Result.Prototype { export interface Failure<E = never> extends ResultPrototype {
readonly _tag: "Failure" readonly _tag: "Failure"
readonly cause: Cause.Cause<E> readonly cause: Cause.Cause<E>
} }
@@ -61,7 +58,11 @@ export interface Refreshing<P = never> {
} }
const ResultPrototype = Object.freeze({ export interface ResultPrototype extends Pipeable.Pipeable, Equal.Equal {
readonly [ResultTypeId]: ResultTypeId
}
export const ResultPrototype: ResultPrototype = Object.freeze({
...Pipeable.Prototype, ...Pipeable.Prototype,
[ResultTypeId]: ResultTypeId, [ResultTypeId]: ResultTypeId,
@@ -95,7 +96,7 @@ const ResultPrototype = Object.freeze({
Hash.cached(this), Hash.cached(this),
) )
}, },
} as const satisfies Result.Prototype) } as const)
export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId) export const isResult = (u: unknown): u is Result<unknown, unknown, unknown> => Predicate.hasProperty(u, ResultTypeId)
@@ -162,52 +163,40 @@ export const toExit: {
} }
export interface State<A, E = never, P = never> {
readonly get: Effect.Effect<Result<A, E, P>>
readonly set: (v: Result<A, E, P>) => Effect.Effect<void>
}
export const State = <A, E = never, P = never>(): Context.Tag<State<A, E, P>, State<A, E, P>> => Context.GenericTag("@effect-fc/Result/State")
export interface Progress<P = never> { export interface Progress<P = never> {
readonly update: <E, R>( readonly progress: Lens.Lens<P, PreviousResultNotRunningNorRefreshing, never, never, never>
f: (previous: P) => Effect.Effect<P, E, R>
) => Effect.Effect<void, PreviousResultNotRunningNorRefreshing | E, R>
} }
export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress")
export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{ export class PreviousResultNotRunningNorRefreshing extends Data.TaggedError("@effect-fc/Result/PreviousResultNotRunningNorRefreshing")<{
readonly previous: Result<unknown, unknown, unknown> readonly previous: Result<unknown, unknown, unknown>
}> {} }> {}
export const Progress = <P = never>(): Context.Tag<Progress<P>, Progress<P>> => Context.GenericTag("@effect-fc/Result/Progress") export const makeProgressLayer = <A, E, P = never>(
state: Lens.Lens<Result<A, E, P>, never, never, never, never>
export const makeProgressLayer = <A, E, P = never>(): Layer.Layer< ): Layer.Layer<Progress<P> | Progress<never>, never, never> => Layer.succeed(
Progress<P>, Progress<P>() as Context.Tag<Progress<P> | Progress<never>, Progress<P> | Progress<never>>,
never, {
State<A, E, P> progress: state.pipe(
> => Layer.effect(Progress<P>(), Effect.gen(function*() { Lens.mapEffect(
const state = yield* State<A, E, P>() a => (isRunning(a) || hasRefreshingFlag(a))
? Effect.succeed(a)
return { : Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous: a })),
update: <FE, FR>(f: (previous: P) => Effect.Effect<P, FE, FR>) => Effect.Do.pipe( (_, b) => Effect.succeed(b),
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
(isRunning(previous) || hasRefreshingFlag(previous))
? Effect.succeed(previous)
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
)),
Effect.bind("progress", ({ previous }) => f(previous.progress)),
Effect.let("next", ({ previous, progress }) => isRunning(previous)
? running(progress)
: refreshing(previous, progress) as Final<A, E, P> & Refreshing<P>
), ),
Effect.andThen(({ next }) => state.set(next)), Lens.map(
a => a.progress,
(a, b) => isRunning(a)
? running(b)
: refreshing(a, b) as Final<A, E, P> & Refreshing<P>,
), ),
} )
})) },
)
export namespace unsafeForkEffect { export namespace unsafeForkEffect {
export type OutputContext<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>> export type OutputContext<R, P> = Exclude<R, Progress<P> | Progress<never>>
export interface Options<A, E, P> { export interface Options<A, E, P> {
readonly initial?: Initial | Final<A, E, P> readonly initial?: Initial | Final<A, E, P>
@@ -215,55 +204,56 @@ export namespace unsafeForkEffect {
} }
} }
export const unsafeForkEffect = <A, E, R, P = never>( export const unsafeForkEffect = Effect.fnUntraced(function* <A, E, R, P = never>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>, options?: unsafeForkEffect.Options<NoInfer<A>, NoInfer<E>, P>,
): Effect.Effect< ): Effect.fn.Return<
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>], readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
never, never,
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P> Scope.Scope | unsafeForkEffect.OutputContext<R, P>
> => Effect.Do.pipe( > {
Effect.bind("ref", () => Ref.make(options?.initial ?? initial<A, E, P>())), const ref = yield* SynchronizedRef.make<Result<A, E, P>>(options?.initial ?? initial<A, E, P>())
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()), const pubsub = yield* PubSub.unbounded<Result<A, E, P>>()
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
Effect.andThen(state => state.set( const state = Lens.make<Result<A, E, P>, never, never, never, never>({
(isFinal(options?.initial) && hasWillRefreshFlag(options?.initial)) get get() { return Ref.get(ref) },
? refreshing(options.initial, options?.initialProgress) as Result<A, E, P> get changes() {
: running(options?.initialProgress) return Stream.unwrapScoped(Effect.map(
).pipe(
Effect.andThen(effect),
Effect.onExit(exit => Effect.andThen(
state.set(fromExit(exit)),
Effect.forkScoped(PubSub.shutdown(pubsub)),
)),
)),
Effect.provide(Layer.empty.pipe(
Layer.provideMerge(makeProgressLayer<A, E, P>()),
Layer.provideMerge(Layer.succeed(State<A, E, P>(), {
get: Ref.get(ref),
set: v => Effect.andThen(Ref.set(ref, v), PubSub.publish(pubsub, v))
})),
)),
))),
Effect.map(({ ref, pubsub, fiber }) => [
Subscribable.make({
get: Ref.get(ref),
changes: Stream.unwrapScoped(Effect.map(
Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]), Effect.all([Ref.get(ref), Stream.fromPubSub(pubsub, { scoped: true })]),
([latest, stream]) => Stream.concat(Stream.make(latest), stream), ([latest, stream]) => Stream.concat(Stream.make(latest), stream),
))
},
modify: f => Ref.get(ref).pipe(
Effect.flatMap(f),
Effect.flatMap(([b, a]) => Ref.set(ref, a).pipe(
Effect.as(b),
Effect.zipLeft(PubSub.publish(pubsub, a))
)), )),
}), ),
fiber, })
]),
) as Effect.Effect< const fiber = yield* Effect.gen(function*() {
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>], yield* Lens.set(
never, state,
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P> (isFinal(options?.initial) && hasWillRefreshFlag(options?.initial))
> ? refreshing(options.initial, options?.initialProgress) as Result<A, E, P>
: running(options?.initialProgress),
)
return yield* Effect.onExit(effect, exit => Effect.andThen(
Lens.set(state, fromExit(exit)),
Effect.forkScoped(PubSub.shutdown(pubsub)),
))
}).pipe(
Effect.forkScoped,
Effect.provide(makeProgressLayer(state)),
)
return [state, fiber] as const
})
export namespace forkEffect { export namespace forkEffect {
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
export type OutputContext<A, E, R, P> = unsafeForkEffect.OutputContext<A, E, R, P> export type OutputContext<R, P> = unsafeForkEffect.OutputContext<R, P>
export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {} export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {}
} }
@@ -274,6 +264,6 @@ export const forkEffect: {
): Effect.Effect< ): Effect.Effect<
readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>], readonly [result: Subscribable.Subscribable<Result<A, E, P>, never, never>, fiber: Fiber.Fiber<A, E>],
never, never,
Scope.Scope | forkEffect.OutputContext<A, E, R, P> Scope.Scope | forkEffect.OutputContext<R, P>
> >
} = unsafeForkEffect } = unsafeForkEffect

View File

@@ -3,8 +3,8 @@ import type * as React from "react"
export const value: { export const value: {
<S>(prevState: S): (self: React.SetStateAction<S>) => S
<S>(self: React.SetStateAction<S>, prevState: S): S <S>(self: React.SetStateAction<S>, prevState: S): S
<S>(prevState: S): (self: React.SetStateAction<S>) => S
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S => } = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
typeof self === "function" typeof self === "function"
? (self as (prevState: S) => S)(prevState) ? (self as (prevState: S) => S)(prevState)

View File

@@ -1,8 +1,11 @@
import { Effect, Equivalence, Stream, Subscribable } from "effect" import { Effect, Equivalence, Stream } from "effect"
import { Subscribable } from "effect-lens"
import * as React from "react" import * as React from "react"
import * as Component from "./Component.js" import * as Component from "./Component.js"
export * from "effect-lens/Subscribable"
export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>( export const zipLatestAll = <const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
...elements: T ...elements: T
): Subscribable.Subscribable< ): Subscribable.Subscribable<
@@ -48,5 +51,3 @@ export const useSubscribables = Effect.fnUntraced(function* <const T extends rea
return reactStateValue as any return reactStateValue as any
}) })
export * from "effect/Subscribable"

View File

@@ -0,0 +1,183 @@
import { describe, expect, test } from "bun:test"
import { Chunk, Effect, Ref, SubscriptionRef } from "effect"
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
describe("SubscriptionSubRef with array refs", () => {
test("creates a subref for a single array element using path", async () => {
const value = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
return subref.get
},
),
)
expect(value).toBe("bob")
})
test("modifies a single array element via subref", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ name: "alice" }, { name: "bob" }, { name: "charlie" }]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "name"])
return Effect.flatMap(
Ref.set(subref, "bob-updated"),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([{ name: "alice" }, { name: "bob-updated" }, { name: "charlie" }])
})
test("modifies array element at index 0", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([10, 20, 30]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [0])
return Effect.flatMap(
Ref.set(subref, 99),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([99, 20, 30])
})
test("modifies array element at last index", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(["a", "b", "c"]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [2])
return Effect.flatMap(
Ref.set(subref, "z"),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual(["a", "b", "z"])
})
test("modifies nested array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([[1, 2], [3, 4], [5, 6]]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, 0])
return Effect.flatMap(
Ref.set(subref, 99),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([[1, 2], [99, 4], [5, 6]])
})
test("uses modifyEffect to transform array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ count: 1 }, { count: 2 }, { count: 3 }]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1, "count"])
return Effect.flatMap(
Ref.update(subref, count => count + 100),
() => Effect.map(Ref.get(parent), parentValue => ({ result: 102, parentValue })),
)
},
),
)
expect(result.result).toBe(102) // count + 100
expect(result.parentValue).toEqual([{ count: 1 }, { count: 102 }, { count: 3 }]) // count + 100
})
test("uses modify to transform array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([10, 20, 30]),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [1])
return Effect.flatMap(
Ref.update(subref, x => x + 5),
() => Effect.map(Ref.get(parent), parentValue => ({ result: 25, parentValue })),
)
},
),
)
expect(result.result).toBe(25) // 20 + 5
expect(result.parentValue).toEqual([10, 25, 30]) // 20 + 5
})
test("makeFromChunkIndex modifies chunk element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(Chunk.make(100, 200, 300)),
parent => {
const subref = SubscriptionSubRef.makeFromChunkIndex(parent, 1)
return Effect.flatMap(
Ref.set(subref, 999),
() => Ref.get(parent),
)
},
),
)
expect(Chunk.toReadonlyArray(result)).toEqual([100, 999, 300])
})
test("makeFromGetSet with custom getter/setter for array element", async () => {
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make([{ id: 1, value: "a" }, { id: 2, value: "b" }]),
parent => {
const subref = SubscriptionSubRef.makeFromGetSet(parent, {
get: arr => arr[0].value,
set: (arr, newValue) => [
{ ...arr[0], value: newValue },
...arr.slice(1),
],
})
return Effect.flatMap(
Ref.set(subref, "updated"),
() => Ref.get(parent),
)
},
),
)
expect(result).toEqual([{ id: 1, value: "updated" }, { id: 2, value: "b" }])
})
test("does not mutate original array when modifying via subref", async () => {
const original = [{ name: "alice" }, { name: "bob" }]
const result = await Effect.runPromise(
Effect.flatMap(
SubscriptionRef.make(original),
parent => {
const subref = SubscriptionSubRef.makeFromPath(parent, [0, "name"])
return Effect.flatMap(
Ref.set(subref, "alice-updated"),
() => Ref.get(parent),
)
},
),
)
expect(original).toEqual([{ name: "alice" }, { name: "bob" }]) // original unchanged
expect(result).toEqual([{ name: "alice-updated" }, { name: "bob" }]) // new value in ref
})
})

View File

@@ -2,6 +2,7 @@ export * as Async from "./Async.js"
export * as Component from "./Component.js" export * as Component from "./Component.js"
export * as ErrorObserver from "./ErrorObserver.js" export * as ErrorObserver from "./ErrorObserver.js"
export * as Form from "./Form.js" export * as Form from "./Form.js"
export * as Lens from "./Lens.js"
export * as Memoized from "./Memoized.js" export * as Memoized from "./Memoized.js"
export * as Mutation from "./Mutation.js" export * as Mutation from "./Mutation.js"
export * as PropertyPath from "./PropertyPath.js" export * as PropertyPath from "./PropertyPath.js"

View File

@@ -25,6 +25,7 @@
"noPropertyAccessFromIndexSignature": false, "noPropertyAccessFromIndexSignature": false,
// Build // Build
"rootDir": "./src",
"outDir": "./dist", "outDir": "./dist",
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
@@ -34,5 +35,6 @@
] ]
}, },
"include": ["./src"] "include": ["./src"],
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
} }

View File

@@ -13,30 +13,30 @@
"clean:modules": "rm -rf node_modules" "clean:modules": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/react-router": "^1.154.12", "@tanstack/react-router": "^1.168.3",
"@tanstack/react-router-devtools": "^1.154.12", "@tanstack/react-router-devtools": "^1.166.11",
"@tanstack/router-plugin": "^1.154.12", "@tanstack/router-plugin": "^1.167.4",
"@types/react": "^19.2.9", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^6.0.1",
"globals": "^17.0.0", "globals": "^17.4.0",
"react": "^19.2.3", "react": "^19.2.4",
"react-dom": "^19.2.3", "react-dom": "^19.2.4",
"type-fest": "^5.4.1", "type-fest": "^5.5.0",
"vite": "^7.3.1" "vite": "^8.0.2"
}, },
"dependencies": { "dependencies": {
"@effect/platform": "^0.94.2", "@effect/platform": "^0.96.0",
"@effect/platform-browser": "^0.74.0", "@effect/platform-browser": "^0.76.0",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.3.0",
"@typed/id": "^0.17.2", "@typed/id": "^0.17.2",
"effect": "^3.19.15", "effect": "^3.21.0",
"effect-fc": "workspace:*", "effect-fc": "workspace:*",
"react-icons": "^5.5.0" "react-icons": "^5.6.0"
}, },
"overrides": { "overrides": {
"@types/react": "^19.2.9", "@types/react": "^19.2.14",
"effect": "^3.19.15", "effect": "^3.21.0",
"react": "^19.2.3" "react": "^19.2.4"
} }
} }

View File

@@ -1,24 +1,22 @@
import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes" import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes"
import { Array, Option } from "effect" import { Array, Option, Struct } from "effect"
import { Component, Form, Subscribable } from "effect-fc" import { Component, Form, Subscribable } from "effect-fc"
export declare namespace TextFieldFormInputView { export declare namespace TextFieldFormInputView {
export interface Props export interface Props extends Omit<TextField.RootProps, "form">, Form.useInput.Options {
extends TextField.RootProps, Form.useInput.Options { readonly form: Form.Form<readonly PropertyKey[], any, string>
readonly field: Form.FormField<any, string>
} }
} }
export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*( export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*(
props: TextFieldFormInputView.Props props: TextFieldFormInputView.Props
) { ) {
const input = yield* Form.useInput(props.field, props) const input = yield* Form.useInput(props.form, props)
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([ const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
props.field.issues, props.form.issues,
props.field.isValidating, props.form.isValidating,
props.field.isSubmitting, props.form.isSubmitting,
]) ])
return ( return (
@@ -27,7 +25,7 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
value={input.value} value={input.value}
onChange={e => input.setValue(e.target.value)} onChange={e => input.setValue(e.target.value)}
disabled={isSubmitting} disabled={isSubmitting}
{...props} {...Struct.omit(props, "form")}
> >
{isValidating && {isValidating &&
<TextField.Slot side="right"> <TextField.Slot side="right">

View File

@@ -4,21 +4,19 @@ import { Component, Form, Subscribable } from "effect-fc"
export declare namespace TextFieldOptionalFormInputView { export declare namespace TextFieldOptionalFormInputView {
export interface Props export interface Props extends Omit<TextField.RootProps, "form" | "defaultValue">, Form.useOptionalInput.Options<string> {
extends Omit<TextField.RootProps, "defaultValue">, Form.useOptionalInput.Options<string> { readonly form: Form.Form<readonly PropertyKey[], any, Option.Option<string>>
readonly field: Form.FormField<any, Option.Option<string>>
} }
} }
export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*( export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*(
props: TextFieldOptionalFormInputView.Props props: TextFieldOptionalFormInputView.Props
) { ) {
const input = yield* Form.useOptionalInput(props.field, props) const input = yield* Form.useOptionalInput(props.form, props)
const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([ const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([
props.field.issues, props.form.issues,
props.field.isValidating, props.form.isValidating,
props.field.isSubmitting, props.form.isSubmitting,
]) ])
return ( return (
@@ -27,7 +25,7 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
value={input.value} value={input.value}
onChange={e => input.setValue(e.target.value)} onChange={e => input.setValue(e.target.value)}
disabled={!input.enabled || isSubmitting} disabled={!input.enabled || isSubmitting}
{...Struct.omit(props, "defaultValue")} {...Struct.omit(props, "form", "defaultValue")}
> >
<TextField.Slot side="left"> <TextField.Slot side="left">
<Switch <Switch

View File

@@ -1,11 +1,11 @@
import { Button, Container, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView" import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView" import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema" import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { runtime } from "@/runtime" import { runtime } from "@/runtime"
import { Button, Container, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
const email = Schema.pattern<typeof Schema.String>( const email = Schema.pattern<typeof Schema.String>(
@@ -40,7 +40,8 @@ const RegisterFormSubmitSchema = Schema.Struct({
}) })
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", { class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
scoped: Form.service({ scoped: Effect.gen(function*() {
const form = yield* Form.service({
schema: RegisterFormSchema.pipe( schema: RegisterFormSchema.pipe(
Schema.compose( Schema.compose(
Schema.transformOrFail( Schema.transformOrFail(
@@ -59,15 +60,22 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
yield* Effect.sleep("500 millis") yield* Effect.sleep("500 millis")
return yield* Schema.decode(RegisterFormSubmitSchema)(value) return yield* Schema.decode(RegisterFormSubmitSchema)(value)
}), }),
debounce: "500 millis", })
return {
form,
emailField: Form.focusObjectField(form, "email"),
passwordField: Form.focusObjectField(form, "password"),
birthField: Form.focusObjectField(form, "birth"),
} as const
}) })
}) {} }) {}
class RegisterFormView extends Component.make("RegisterFormView")(function*() { class RegisterFormView extends Component.make("RegisterFormView")(function*() {
const form = yield* RegisterFormService const form = yield* RegisterFormService
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([ const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
form.canSubmit, form.form.canSubmit,
form.mutation.result, form.form.mutation.result,
]) ])
const runPromise = yield* Component.useRunPromise() const runPromise = yield* Component.useRunPromise()
@@ -84,20 +92,22 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
<Container width="300"> <Container width="300">
<form onSubmit={e => { <form onSubmit={e => {
e.preventDefault() e.preventDefault()
void runPromise(form.submit) void runPromise(form.form.submit)
}}> }}>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<TextFieldFormInput <TextFieldFormInput
field={yield* form.field(["email"])} form={form.emailField}
debounce="250 millis"
/> />
<TextFieldFormInput <TextFieldFormInput
field={yield* form.field(["password"])} form={form.passwordField}
debounce="250 millis"
/> />
<TextFieldOptionalFormInput <TextFieldOptionalFormInput
type="datetime-local" type="datetime-local"
field={yield* form.field(["birth"])} form={form.birthField}
defaultValue="" defaultValue=""
/> />

View File

@@ -1,13 +1,13 @@
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
import { Component, Form, Subscribable } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import * as Domain from "@/domain" import * as Domain from "@/domain"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView" import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView" import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema" import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
import { Component, Form, Lens, Subscribable } from "effect-fc"
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
import { FaDeleteLeft } from "react-icons/fa6"
import { TodosState } from "./TodosState" import { TodosState } from "./TodosState"
@@ -59,20 +59,19 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe( Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
Effect.andThen(makeTodo), Effect.andThen(makeTodo),
Effect.andThen(Schema.encode(TodoFormSchema)), Effect.andThen(Schema.encode(TodoFormSchema)),
Effect.andThen(v => Ref.set(form.encodedValue, v)), Effect.andThen(v => Lens.set(form.encodedValue, v)),
)), )),
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)), Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
Match.exhaustive, Match.exhaustive,
), ),
autosubmit: props._tag === "edit", autosubmit: props._tag === "edit",
debounce: "250 millis",
}) })
return [ return [
indexRef, indexRef,
form, form,
yield* form.field(["content"]), Form.focusObjectField(form, "content"),
yield* form.field(["completedAt"]), Form.focusObjectField(form, "completedAt"),
] as const ] as const
}), [props._tag, props._tag === "edit" ? props.id : undefined]) }), [props._tag, props._tag === "edit" ? props.id : undefined])
@@ -92,11 +91,14 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
<Flex direction="row" align="center" gap="2"> <Flex direction="row" align="center" gap="2">
<Box flexGrow="1"> <Box flexGrow="1">
<Flex direction="column" align="stretch" gap="2"> <Flex direction="column" align="stretch" gap="2">
<TextFieldFormInput field={contentField} /> <TextFieldFormInput
form={contentField}
debounce="250 millis"
/>
<Flex direction="row" justify="center" align="center" gap="2"> <Flex direction="row" justify="center" align="center" gap="2">
<TextFieldOptionalFormInput <TextFieldOptionalFormInput
field={completedAtField} form={completedAtField}
type="datetime-local" type="datetime-local"
defaultValue="" defaultValue=""
/> />