2 Commits

Author SHA1 Message Date
66047b468e Update dependency @vitejs/plugin-react to v6
Some checks failed
Lint / lint (push) Failing after 33s
Test build / test-build (pull_request) Failing after 7s
2026-03-16 01:04:53 +01:00
154e27bcc0 Update dependency vite to v8 (#37)
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/) |
|---|---|---|---|
| [vite](https://vite.dev) ([source](https://github.com/vitejs/vite/tree/HEAD/packages/vite)) | [`^7.3.1` → `^8.0.0`](https://renovatebot.com/diffs/npm/vite/7.3.1/8.0.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vite/8.0.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/7.3.1/8.0.0?slim=true) |

---

### Release Notes

<details>
<summary>vitejs/vite (vite)</summary>

### [`v8.0.0`](https://github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#800-2026-03-12)

[Compare Source](https://github.com/vitejs/vite/compare/v7.3.1...v8.0.0)

![Vite 8 is here!](../../docs/public/og-image-announcing-vite8.webp)

Today, we're thrilled to announce the release of the next Vite major:

- **[Vite 8.0 announcement blog post](https://vite.dev/blog/announcing-vite8.html)**
- [Docs](https://vite.dev/) (translations: [简体中文](https://cn.vite.dev/), [日本語](https://ja.vite.dev/), [Español](https://es.vite.dev/), [Português](https://pt.vite.dev/), [한국어](https://ko.vite.dev/), [Deutsch](https://de.vite.dev/), [فارسی](https://fa.vite.dev/))
- [Migration Guide](https://vite.dev/guide/migration.html)

##### ⚠ BREAKING CHANGES

- remove `import.meta.hot.accept` resolution fallback ([#&#8203;21382](https://github.com/vitejs/vite/issues/21382))
- update default browser target ([#&#8203;21193](https://github.com/vitejs/vite/issues/21193))
- the epic `rolldown-vite` merge ([#&#8203;21189](https://github.com/vitejs/vite/issues/21189))

##### Features

- update rolldown to 1.0.0-rc.9 ([#&#8203;21813](https://github.com/vitejs/vite/issues/21813)) ([f05be0e](f05be0eabf))
- warn when `vite-tsconfig-paths` plugin is detected ([#&#8203;21781](https://github.com/vitejs/vite/issues/21781)) ([ada493e](ada493e421))
- **css:** support es2025 build target for lightningcss ([#&#8203;21769](https://github.com/vitejs/vite/issues/21769)) ([08906e7](08906e76f2))
- forward browser console logs and errors to dev server terminal ([#&#8203;20916](https://github.com/vitejs/vite/issues/20916)) ([2540ed0](2540ed06d0))
- update rolldown to 1.0.0-rc.8 ([#&#8203;21790](https://github.com/vitejs/vite/issues/21790)) ([a0c950e](a0c950e309))
- export `Visitor` and `ESTree` from `rolldown/utils` ([#&#8203;21664](https://github.com/vitejs/vite/issues/21664)) ([45de31e](45de31e5ff))
- update rolldown to 1.0.0-rc.6 ([#&#8203;21714](https://github.com/vitejs/vite/issues/21714)) ([37a65f8](37a65f8c31))
- use util.inspect for CLI error display ([#&#8203;21668](https://github.com/vitejs/vite/issues/21668)) ([5f425a9](5f425a9126))
- update rolldown to 1.0.0-rc.5 ([#&#8203;21660](https://github.com/vitejs/vite/issues/21660)) ([b3ddbc5](b3ddbc54ee))
- update rolldown to 1.0.0-rc.4 ([#&#8203;21617](https://github.com/vitejs/vite/issues/21617)) ([1ee5c7f](1ee5c7f796))
- **wasm:** add SSR support for `.wasm?init` ([#&#8203;21102](https://github.com/vitejs/vite/issues/21102)) ([216a3b5](216a3b53c6))
- integrate devtools ([#&#8203;21331](https://github.com/vitejs/vite/issues/21331)) ([acbf507](acbf507bcb))
- update rolldown to 1.0.0-rc.3 ([#&#8203;21554](https://github.com/vitejs/vite/issues/21554)) ([43358e9](43358e97cd))
- **manifest:** add `assets` field for standalone CSS entry points ([#&#8203;21015](https://github.com/vitejs/vite/issues/21015)) ([f289b9b](f289b9b0ce))
- update rolldown to 1.0.0-rc.2 ([#&#8203;21512](https://github.com/vitejs/vite/issues/21512)) ([fa136a9](fa136a9e68))
- **bundled-dev:** support worker in initial bundle ([#&#8203;21415](https://github.com/vitejs/vite/issues/21415)) ([f3d3149](f3d31499c7))
- **dev:** detect port conflicts on wildcard hosts ([#&#8203;21381](https://github.com/vitejs/vite/issues/21381)) ([b0dd5a9](b0dd5a993f))
- shortcuts case insensitive ([#&#8203;21224](https://github.com/vitejs/vite/issues/21224)) ([7796ade](7796aded76))
- update rolldown to 1.0.0-rc.1 ([#&#8203;21463](https://github.com/vitejs/vite/issues/21463)) ([ff9dd7f](ff9dd7fef0))
- warn if `envPrefix` contains spaces ([#&#8203;21292](https://github.com/vitejs/vite/issues/21292)) ([9fcde3c](9fcde3c870))
- update rolldown to 1.0.0-beta.60 ([#&#8203;21408](https://github.com/vitejs/vite/issues/21408)) ([c33aa7c](c33aa7cfd1))
- update rolldown to 1.0.0-beta.59 ([#&#8203;21374](https://github.com/vitejs/vite/issues/21374)) ([0037943](00379439fa))
- add `ignoreOutdatedRequests` option to `optimizeDeps` ([#&#8203;21364](https://github.com/vitejs/vite/issues/21364)) ([b2e75aa](b2e75aabe9))
- add ios to default esbuild targets ([#&#8203;21342](https://github.com/vitejs/vite/issues/21342)) ([daae6e9](daae6e9f5d))
- update rolldown to 1.0.0-beta.58 ([#&#8203;21354](https://github.com/vitejs/vite/issues/21354)) ([ba40cef](ba40cef16d))
- update rolldown to 1.0.0-beta.57 ([#&#8203;21335](https://github.com/vitejs/vite/issues/21335)) ([d5412ef](d5412ef4c4))
- **css:** support es2024 build target for lightningcss ([#&#8203;21294](https://github.com/vitejs/vite/issues/21294)) ([bd33b8e](bd33b8e087))
- update rolldown to 1.0.0-beta.56 ([#&#8203;21323](https://github.com/vitejs/vite/issues/21323)) ([9847a63](9847a634cf))
- introduce v2 native plugins and enable it by default ([#&#8203;21268](https://github.com/vitejs/vite/issues/21268)) ([42f2ab3](42f2ab3aec))
- **ssr:** avoid errors when rewriting already rewritten stacktrace ([#&#8203;21269](https://github.com/vitejs/vite/issues/21269)) ([98d9a33](98d9a33274))
- update rolldown to 1.0.0-beta.55 ([#&#8203;21300](https://github.com/vitejs/vite/issues/21300)) ([2c8db85](2c8db858d7))
- update rolldown to 1.0.0-beta.54 ([#&#8203;21267](https://github.com/vitejs/vite/issues/21267)) ([c751172](c75117213c))
- add a warning that is output when a plugin sets esbuild related options ([#&#8203;21218](https://github.com/vitejs/vite/issues/21218)) ([200646b](200646b143))
- highly experimental full bundle mode ([#&#8203;21235](https://github.com/vitejs/vite/issues/21235)) ([83d8c99](83d8c99753))
- print esbuild options when both esbuild and oxc options are set ([#&#8203;21216](https://github.com/vitejs/vite/issues/21216)) ([08ae87b](08ae87b14a))
- update default browser target ([#&#8203;21193](https://github.com/vitejs/vite/issues/21193)) ([8c3dd06](8c3dd06bd9))
- the epic `rolldown-vite` merge ([#&#8203;21189](https://github.com/vitejs/vite/issues/21189)) ([4a7f8d4](4a7f8d43e6))

##### Bug Fixes

- **deps:** update all non-major dependencies ([#&#8203;21786](https://github.com/vitejs/vite/issues/21786)) ([eaa4352](eaa4352af8))
- use `watch.watcher` instead of `watch.notify` ([#&#8203;21793](https://github.com/vitejs/vite/issues/21793)) ([88953b3](88953b331d))
- **css:** apply `server.origin` to public file URLs in CSS (fix [#&#8203;18457](https://github.com/vitejs/vite/issues/18457)) ([#&#8203;21697](https://github.com/vitejs/vite/issues/21697)) ([c967f48](c967f48b2e))
- **deps:** update all non-major dependencies ([#&#8203;21732](https://github.com/vitejs/vite/issues/21732)) ([5c921ca](5c921ca9bf))
- **dev:** disable extglobs for consistency ([#&#8203;21745](https://github.com/vitejs/vite/issues/21745)) ([1958eeb](1958eeb34f))
- **lib:** keep annotation comments for es output ([#&#8203;21740](https://github.com/vitejs/vite/issues/21740)) ([dd3c4f4](dd3c4f4cf0))
- **optimizer:** avoid error happening with a package with asset entrypoint ([#&#8203;21766](https://github.com/vitejs/vite/issues/21766)) ([f7e1d07](f7e1d0720e))
- **ssr:** throw friendly error when calling `ssrLoadModule` with non-runnable ssr env ([#&#8203;21739](https://github.com/vitejs/vite/issues/21739)) ([1fa736e](1fa736e802))
- **types:** remove extends ImportMeta from ModuleRunnerImportMeta ([#&#8203;21710](https://github.com/vitejs/vite/issues/21710)) ([0176d45](0176d45deb))
- **wasm:** reset assetUrlRE.lastIndex before .test() in SSR builds ([#&#8203;21780](https://github.com/vitejs/vite/issues/21780)) ([3a0d8d9](3a0d8d94a8))
- **deps:** update all non-major dependencies ([#&#8203;21691](https://github.com/vitejs/vite/issues/21691)) ([521fdc0](521fdc0ced))
- **optimizer:** avoid duplicate modules when `preserveSymlinks` is enabled ([#&#8203;21720](https://github.com/vitejs/vite/issues/21720)) ([72165e0](72165e0f58))
- **dev:** only treat EADDRINUSE as port conflict in wildcard pre-check ([#&#8203;21642](https://github.com/vitejs/vite/issues/21642)) ([e54e25f](e54e25fbb9))
- **dev:** prevent concurrent server restarts ([#&#8203;21636](https://github.com/vitejs/vite/issues/21636)) ([8ce23a3](8ce23a3b6e))
- **dev:** return "502 Bad Gateway" on proxy failures instead of 500 ([#&#8203;21652](https://github.com/vitejs/vite/issues/21652)) ([e240df2](e240df2ea4))
- clear tsconfig cache only when tsconfig.json is cached ([#&#8203;21622](https://github.com/vitejs/vite/issues/21622)) ([50c9675](50c9675aa6))
- **deps:** update all non-major dependencies ([#&#8203;21594](https://github.com/vitejs/vite/issues/21594)) ([becdc5d](becdc5dcc4))
- **lib:** CSS injection point error with nested name IIFE output ([#&#8203;21606](https://github.com/vitejs/vite/issues/21606)) ([5003de6](5003de6253))
- **module-runner:** incorrect column with `sourcemapInterceptor: "prepareStackTrace"` ([#&#8203;21562](https://github.com/vitejs/vite/issues/21562)) ([416c095](416c0959eb))
- **module-runner:** prevent crash on negative column in stacktrace ([#&#8203;21585](https://github.com/vitejs/vite/issues/21585)) ([a075590](a075590c40))
- rolldownOptions/rollupOptions merging at environment level ([#&#8203;21612](https://github.com/vitejs/vite/issues/21612)) ([db2ecc7](db2ecc7675))
- **scanner:** respect tsconfig.json ([#&#8203;21547](https://github.com/vitejs/vite/issues/21547)) ([c6c04db](c6c04db9c6))
- avoid registering customization hook for import meta resolver multiple times ([#&#8203;21518](https://github.com/vitejs/vite/issues/21518)) ([8bb3203](8bb3203679))
- **config:** avoid watching rolldown runtime virtual module ([#&#8203;21545](https://github.com/vitejs/vite/issues/21545)) ([d18b139](d18b13957b))
- **deps:** update all non-major dependencies ([#&#8203;21540](https://github.com/vitejs/vite/issues/21540)) ([9ebaeaa](9ebaeaac09))
- populate originalFileNames when resolving CSS asset paths ([#&#8203;21542](https://github.com/vitejs/vite/issues/21542)) ([8b47ff7](8b47ff76d2))
- **deps:** update all non-major dependencies ([#&#8203;21488](https://github.com/vitejs/vite/issues/21488)) ([2b32ca2](2b32ca24fe))
- disable `tsconfig` option when loading config ([#&#8203;21517](https://github.com/vitejs/vite/issues/21517)) ([5025c35](5025c358d1))
- **optimizer:** map relative `new URL` paths to correct relative file location ([#&#8203;21434](https://github.com/vitejs/vite/issues/21434)) ([ca96cbc](ca96cbc8ef))
- avoid using deprecated `output.inlineDynamicImport` option ([#&#8203;21464](https://github.com/vitejs/vite/issues/21464)) ([471ce62](471ce62756))
- use separate hook object for each environment ([#&#8203;21472](https://github.com/vitejs/vite/issues/21472)) ([66347f6](66347f6df0))
- **deps:** update all non-major dependencies ([#&#8203;21440](https://github.com/vitejs/vite/issues/21440)) ([1835995](18359959cb))
- **dev:** avoid event emitter leak caused by `server.listen` callback ([#&#8203;21451](https://github.com/vitejs/vite/issues/21451)) ([602d786](602d7865db))
- lazy hook filter should work ([#&#8203;21443](https://github.com/vitejs/vite/issues/21443)) ([bc0c207](bc0c207f53))
- **optimizer:** skip `rolldownCjsExternalPlugin` for `platform: neutral` ([#&#8203;21452](https://github.com/vitejs/vite/issues/21452)) ([d2fc4be](d2fc4be044))
- **deps:** update all non-major dependencies ([#&#8203;21389](https://github.com/vitejs/vite/issues/21389)) ([30f48df](30f48df33e))
- **deps:** update esbuild peerDependency version ([#&#8203;21398](https://github.com/vitejs/vite/issues/21398)) ([4266c97](4266c97808))
- **hmr:** trigger prune event when last import is removed ([#&#8203;20781](https://github.com/vitejs/vite/issues/20781)) ([#&#8203;21093](https://github.com/vitejs/vite/issues/21093)) ([7576735](757673528c))
- **module-runner:** use `process.getBuiltinModule` instead of `import('node:module')` ([#&#8203;21402](https://github.com/vitejs/vite/issues/21402)) ([6633bcb](6633bcb941))
- support .env file mounts (FIFOs) ([#&#8203;21365](https://github.com/vitejs/vite/issues/21365)) ([6e6f82a](6e6f82a067))
- **css:** stylus Evaluator support ([#&#8203;21376](https://github.com/vitejs/vite/issues/21376)) ([cf9ace1](cf9ace1b40))
- **deps:** update all non-major dependencies ([#&#8203;21321](https://github.com/vitejs/vite/issues/21321)) ([9bc7c2e](9bc7c2ed4f))
- **import-analysis:** avoid cjs interop for built browser external module ([#&#8203;21333](https://github.com/vitejs/vite/issues/21333)) ([dc5a2fb](dc5a2fb86f))
- **worker:** handle `new Worker(..., new URL(import.meta.url))` with trailing comma ([#&#8203;21325](https://github.com/vitejs/vite/issues/21325)) ([4a47241](4a472418c0))
- detect `import.meta.resolve` when formatted across multiple lines ([#&#8203;21312](https://github.com/vitejs/vite/issues/21312)) ([130e718](130e7181a5))
- allow no-cors requests for non-script tag requests ([#&#8203;21299](https://github.com/vitejs/vite/issues/21299)) ([ef3d596](ef3d59648f))
- **deps:** update all non-major dependencies ([#&#8203;21285](https://github.com/vitejs/vite/issues/21285)) ([4635b2e](4635b2e90f))
- unreachable error when building with `experimental.bundledDev` is enabled ([#&#8203;21296](https://github.com/vitejs/vite/issues/21296)) ([e81c183](e81c183f8c))
- **deps:** update all non-major dependencies ([#&#8203;21231](https://github.com/vitejs/vite/issues/21231)) ([859789c](859789c856))
- don't strip base from imports ([#&#8203;21221](https://github.com/vitejs/vite/issues/21221)) ([7da742b](7da742b478))
- allow exiting process before optimizer cleanup is done ([#&#8203;21170](https://github.com/vitejs/vite/issues/21170)) ([55ceffc](55ceffc897))
- plugin shortcut support ([#&#8203;21211](https://github.com/vitejs/vite/issues/21211)) ([6a3aca0](6a3aca0843))

##### Performance Improvements

- **ssr:** skip circular import check for already-evaluated modules ([#&#8203;21632](https://github.com/vitejs/vite/issues/21632)) ([235140b](235140b2d5))
- use tsconfig cache for oxc transform in dev ([#&#8203;21643](https://github.com/vitejs/vite/issues/21643)) ([57ff177](57ff177575))

##### Documentation

- bulk of typo fixes ([#&#8203;21507](https://github.com/vitejs/vite/issues/21507)) ([80755da](80755dacab))
- update `build.dynamicImportVarsOptions` ([#&#8203;21477](https://github.com/vitejs/vite/issues/21477)) ([54ce2ed](54ce2ed15a))
- clarify the pronunciation of `vite` in IPA symbols ([#&#8203;21238](https://github.com/vitejs/vite/issues/21238)) ([9b1d4d6](9b1d4d6f34))
- ensure https links ([#&#8203;21266](https://github.com/vitejs/vite/issues/21266)) ([2eb259a](2eb259a848))

##### Miscellaneous Chores

- **deps-dev:** bump rollup from 4.57.1 to 4.59.0 ([#&#8203;21717](https://github.com/vitejs/vite/issues/21717)) ([25227bb](25227bbdc7))
- **deps:** update dependency cac to v7 ([#&#8203;21788](https://github.com/vitejs/vite/issues/21788)) ([44e33ae](44e33ae6a7))
- **deps:** update dependency rolldown-plugin-dts to ^0.22.2 ([#&#8203;21731](https://github.com/vitejs/vite/issues/21731)) ([d8ea652](d8ea652a8b))
- **deps:** remove `fdir` and `@rollup/plugin-commonjs` ([#&#8203;21639](https://github.com/vitejs/vite/issues/21639)) ([5abffd5](5abffd5d04))
- **deps:** update dependency [@&#8203;rollup/plugin-alias](https://github.com/rollup/plugin-alias) to v6 ([#&#8203;21097](https://github.com/vitejs/vite/issues/21097)) ([44b5bdf](44b5bdfcf2))
- fix broken link for future deprecations ([#&#8203;21603](https://github.com/vitejs/vite/issues/21603)) ([25f4501](25f45013b9))
- update `customResolver` deprecation message to mention `enforce: 'pre'` ([#&#8203;21576](https://github.com/vitejs/vite/issues/21576)) ([2ce34d5](2ce34d5580))
- update rolldown-plugin-dts to 0.22.1 ([#&#8203;21559](https://github.com/vitejs/vite/issues/21559)) ([77aab4b](77aab4b7f1))
- **deps:** update dependency rolldown-plugin-dts to ^0.21.8 ([#&#8203;21539](https://github.com/vitejs/vite/issues/21539)) ([33881cb](33881cb34f))
- add missing versions to changelog ([#&#8203;21515](https://github.com/vitejs/vite/issues/21515)) ([4bfb239](4bfb239686))
- **deps:** update rolldown-related dependencies ([#&#8203;21487](https://github.com/vitejs/vite/issues/21487)) ([5863e51](5863e513fa))
- **deps:** update rolldown-related dependencies ([#&#8203;21390](https://github.com/vitejs/vite/issues/21390)) ([be9dd4e](be9dd4e08d))
- fix typo in plugin.ts comment ([#&#8203;21435](https://github.com/vitejs/vite/issues/21435)) ([d31fc66](d31fc6685b))
- replace caniuse link for ES2024 ([#&#8203;21355](https://github.com/vitejs/vite/issues/21355)) ([2ba4e99](2ba4e99019))
- cleanup changelog ([#&#8203;21202](https://github.com/vitejs/vite/issues/21202)) ([8c8c56e](8c8c56e1eb))
- **deps:** update rolldown-related dependencies ([#&#8203;21230](https://github.com/vitejs/vite/issues/21230)) ([9349446](9349446e93))
- fix spelling error ([#&#8203;21223](https://github.com/vitejs/vite/issues/21223)) ([cc10e20](cc10e207ae))

##### Code Refactoring

- don't add `optimization.inlineConst: { mode: 'smart' }` as it's enabled by default ([#&#8203;21794](https://github.com/vitejs/vite/issues/21794)) ([22b3d11](22b3d111c3))
- enable some native plugins even with enable native plugin false ([#&#8203;21744](https://github.com/vitejs/vite/issues/21744)) ([fc46c79](fc46c79797))
- avoid deprecated `legalComments` option ([#&#8203;21721](https://github.com/vitejs/vite/issues/21721)) ([e06496e](e06496ef25))
- use `ESTree` types from `rolldown/utils` ([#&#8203;21719](https://github.com/vitejs/vite/issues/21719)) ([9239750](9239750e61))
- deprecate `customResolver` in `resolve.alias` ([#&#8203;21476](https://github.com/vitejs/vite/issues/21476)) ([81275c9](81275c9072))
- remove unnecessary `@rolldown/pluginutils` ([#&#8203;21560](https://github.com/vitejs/vite/issues/21560)) ([c367b62](c367b62693))
- enable some native plugins even with enable native plugin false ([#&#8203;21608](https://github.com/vitejs/vite/issues/21608)) ([5a4f692](5a4f692426))
- use `rolldown/utils` ([#&#8203;21577](https://github.com/vitejs/vite/issues/21577)) ([e56103f](e56103f180))
- use internal devtools config ([#&#8203;21609](https://github.com/vitejs/vite/issues/21609)) ([9aea20f](9aea20f4a1))
- use parseEnv ([#&#8203;21586](https://github.com/vitejs/vite/issues/21586)) ([f859d2c](f859d2cdfc))
- **wasm:** remove native wasm helper plugin usage ([#&#8203;21566](https://github.com/vitejs/vite/issues/21566)) ([71a86be](71a86be6d9))
- enable some native plugins even with enable native plugin false ([#&#8203;21511](https://github.com/vitejs/vite/issues/21511)) ([b40292c](b40292ce6a))
- remove `experimental.enableNativePlugin: 'resolver'` ([#&#8203;21510](https://github.com/vitejs/vite/issues/21510)) ([f9d9213](f9d92130fa))
- use `import.meta.dirname` everywhere ([#&#8203;21509](https://github.com/vitejs/vite/issues/21509)) ([7becf5f](7becf5f8fe))
- **optimizer:** simplify `rolldownCjsExternalPlugin` ([#&#8203;21450](https://github.com/vitejs/vite/issues/21450)) ([ebda8fd](ebda8fd3c1))
- remove `import.meta.hot.accept` resolution fallback ([#&#8203;21382](https://github.com/vitejs/vite/issues/21382)) ([71d0797](71d0797a71))
- **optimizer:** remove dead code ([#&#8203;21334](https://github.com/vitejs/vite/issues/21334)) ([e9a2cdb](e9a2cdbb7d))

##### Tests

- **ssr:** incorrect `handleInvoke` was called in server-worker-runner.invoke test ([#&#8203;21751](https://github.com/vitejs/vite/issues/21751)) ([b95ca22](b95ca22460))
- add more type tests for `defineConfig` ([#&#8203;21698](https://github.com/vitejs/vite/issues/21698)) ([4fedbbd](4fedbbdd91))
- test case for catching invalid package resolution error ([#&#8203;21601](https://github.com/vitejs/vite/issues/21601)) ([c9b9359](c9b9359fe8))
- **bundled-dev:** add worker test cases ([#&#8203;21557](https://github.com/vitejs/vite/issues/21557)) ([569bc98](569bc98d6b))

##### Beta Changelogs

##### [8.0.0-beta.18](https://github.com/vitejs/vite/compare/v8.0.0-beta.17...v8.0.0-beta.18) (2026-03-09)

See [8.0.0-beta.18 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.18/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.17](https://github.com/vitejs/vite/compare/v8.0.0-beta.16...v8.0.0-beta.17) (2026-03-09)

See [8.0.0-beta.17 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.17/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.16](https://github.com/vitejs/vite/compare/v8.0.0-beta.15...v8.0.0-beta.16) (2026-02-27)

See [8.0.0-beta.16 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.16/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.15](https://github.com/vitejs/vite/compare/v8.0.0-beta.14...v8.0.0-beta.15) (2026-02-19)

See [8.0.0-beta.15 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.15/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.14](https://github.com/vitejs/vite/compare/v8.0.0-beta.13...v8.0.0-beta.14) (2026-02-12)

See [8.0.0-beta.14 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.14/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.13](https://github.com/vitejs/vite/compare/v8.0.0-beta.12...v8.0.0-beta.13) (2026-02-05)

See [8.0.0-beta.13 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.13/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.12](https://github.com/vitejs/vite/compare/v8.0.0-beta.11...v8.0.0-beta.12) (2026-02-03)

See [8.0.0-beta.12 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.12/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.11](https://github.com/vitejs/vite/compare/v8.0.0-beta.10...v8.0.0-beta.11) (2026-01-29)

See [8.0.0-beta.11 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.11/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.10](https://github.com/vitejs/vite/compare/v8.0.0-beta.9...v8.0.0-beta.10) (2026-01-24)

See [8.0.0-beta.10 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.10/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.9](https://github.com/vitejs/vite/compare/v8.0.0-beta.8...v8.0.0-beta.9) (2026-01-22)

See [8.0.0-beta.9 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.9/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.8](https://github.com/vitejs/vite/compare/v8.0.0-beta.7...v8.0.0-beta.8) (2026-01-15)

See [8.0.0-beta.8 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.8/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.7](https://github.com/vitejs/vite/compare/v8.0.0-beta.6...v8.0.0-beta.7) (2026-01-08)

See [8.0.0-beta.7 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.7/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.6](https://github.com/vitejs/vite/compare/v8.0.0-beta.5...v8.0.0-beta.6) (2026-01-07)

See [8.0.0-beta.6 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.6/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.5](https://github.com/vitejs/vite/compare/v8.0.0-beta.4...v8.0.0-beta.5) (2025-12-25)

See [8.0.0-beta.5 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.5/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.4](https://github.com/vitejs/vite/compare/v8.0.0-beta.3...v8.0.0-beta.4) (2025-12-22)

See [8.0.0-beta.4 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.4/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.3](https://github.com/vitejs/vite/compare/v8.0.0-beta.2...v8.0.0-beta.3) (2025-12-18)

See [8.0.0-beta.3 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.3/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.2](https://github.com/vitejs/vite/compare/v8.0.0-beta.1...v8.0.0-beta.2) (2025-12-12)

See [8.0.0-beta.2 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.2/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.1](https://github.com/vitejs/vite/compare/v8.0.0-beta.0...v8.0.0-beta.1) (2025-12-08)

See [8.0.0-beta.1 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.1/packages/vite/CHANGELOG.md)

##### [8.0.0-beta.0](https://github.com/vitejs/vite/compare/v7.2.4...v8.0.0-beta.0) (2025-12-03)

See [8.0.0-beta.0 changelog](https://github.com/vitejs/vite/blob/v8.0.0-beta.0/packages/vite/CHANGELOG.md)

##### Rolldown-Vite changelogs

See [rolldown-vite changelog](https://github.com/vitejs/rolldown-vite/blob/v7.2.10/packages/vite/CHANGELOG.md)

</details>

---

### Configuration

📅 **Schedule**: 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:eyJjcmVhdGVkSW5WZXIiOiI0My42Ni41IiwidXBkYXRlZEluVmVyIjoiNDMuNzYuMiIsInRhcmdldEJyYW5jaCI6Im5leHQiLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: #37
Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud>
Co-committed-by: Renovate Bot <renovate-bot@valverde.cloud>
2026-03-16 01:04:45 +01:00
20 changed files with 758 additions and 1020 deletions

645
bun.lock

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -27,7 +27,7 @@
"@docusaurus/module-type-aliases": "3.9.2",
"@docusaurus/tsconfig": "3.9.2",
"@docusaurus/types": "3.9.2",
"typescript": "~6.0.0"
"typescript": "~5.6.2"
},
"browserslist": {
"production": [

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
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>(
options: make.Options<K, A, E, R, P>
): Effect.fn.Return<
Mutation<K, A, E, Result.forkEffect.OutputContext<R, P>, P>,
Mutation<K, A, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never,
Scope.Scope | Result.forkEffect.OutputContext<R, P>
Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>
> {
return new MutationImpl(
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<R, P>>(),
yield* Effect.context<Scope.Scope | Result.forkEffect.OutputContext<A, E, R, P>>(),
options.f as any,
options.initialProgress as P,

View File

@@ -1,67 +0,0 @@
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>(
options: make.Options<K, A, KE, KR, E, R, P>
): Effect.fn.Return<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
> {
const client = yield* QueryClient.QueryClient
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<R, P>>(),
return new QueryImpl<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>(
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>>(),
options.key,
options.f as any,
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>(
options: make.Options<K, A, KE, KR, E, R, P>
): Effect.Effect<
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<R, P>, P>,
Query<K, A, KE, KR, E, Result.forkEffect.OutputContext<A, E, R, P>, P>,
never,
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<R, P>
Scope.Scope | QueryClient.QueryClient | KR | Result.forkEffect.OutputContext<A, E, R, P>
> => Effect.tap(
make(options),
query => Effect.forkScoped(query.run),

View File

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

View File

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

View File

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

View File

@@ -1,183 +0,0 @@
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,7 +2,6 @@ export * as Async from "./Async.js"
export * as Component from "./Component.js"
export * as ErrorObserver from "./ErrorObserver.js"
export * as Form from "./Form.js"
export * as Lens from "./Lens.js"
export * as Memoized from "./Memoized.js"
export * as Mutation from "./Mutation.js"
export * as PropertyPath from "./PropertyPath.js"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
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"
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
import { runtime } from "@/runtime"
const email = Schema.pattern<typeof Schema.String>(
@@ -40,42 +40,34 @@ const RegisterFormSubmitSchema = Schema.Struct({
})
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
scoped: Effect.gen(function*() {
const form = yield* Form.service({
schema: RegisterFormSchema.pipe(
Schema.compose(
Schema.transformOrFail(
Schema.typeSchema(RegisterFormSchema),
Schema.typeSchema(RegisterFormSchema),
{
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
encode: ParseResult.succeed,
},
),
scoped: Form.service({
schema: RegisterFormSchema.pipe(
Schema.compose(
Schema.transformOrFail(
Schema.typeSchema(RegisterFormSchema),
Schema.typeSchema(RegisterFormSchema),
{
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
encode: ParseResult.succeed,
},
),
),
),
initialEncodedValue: { email: "", password: "", birth: Option.none() },
f: Effect.fnUntraced(function*([value]) {
yield* Effect.sleep("500 millis")
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
}),
})
return {
form,
emailField: Form.focusObjectField(form, "email"),
passwordField: Form.focusObjectField(form, "password"),
birthField: Form.focusObjectField(form, "birth"),
} as const
initialEncodedValue: { email: "", password: "", birth: Option.none() },
f: Effect.fnUntraced(function*([value]) {
yield* Effect.sleep("500 millis")
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
}),
debounce: "500 millis",
})
}) {}
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
const form = yield* RegisterFormService
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
form.form.canSubmit,
form.form.mutation.result,
form.canSubmit,
form.mutation.result,
])
const runPromise = yield* Component.useRunPromise()
@@ -92,22 +84,20 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {
<Container width="300">
<form onSubmit={e => {
e.preventDefault()
void runPromise(form.form.submit)
void runPromise(form.submit)
}}>
<Flex direction="column" gap="2">
<TextFieldFormInput
form={form.emailField}
debounce="250 millis"
field={yield* form.field(["email"])}
/>
<TextFieldFormInput
form={form.passwordField}
debounce="250 millis"
field={yield* form.field(["password"])}
/>
<TextFieldOptionalFormInput
type="datetime-local"
form={form.birthField}
field={yield* form.field(["birth"])}
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 { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
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"
@@ -59,19 +59,20 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
Effect.andThen(makeTodo),
Effect.andThen(Schema.encode(TodoFormSchema)),
Effect.andThen(v => Lens.set(form.encodedValue, v)),
Effect.andThen(v => Ref.set(form.encodedValue, v)),
)),
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
Match.exhaustive,
),
autosubmit: props._tag === "edit",
debounce: "250 millis",
})
return [
indexRef,
form,
Form.focusObjectField(form, "content"),
Form.focusObjectField(form, "completedAt"),
yield* form.field(["content"]),
yield* form.field(["completedAt"]),
] as const
}), [props._tag, props._tag === "edit" ? props.id : undefined])
@@ -91,14 +92,11 @@ export class TodoView extends Component.make("TodoView")(function*(props: TodoPr
<Flex direction="row" align="center" gap="2">
<Box flexGrow="1">
<Flex direction="column" align="stretch" gap="2">
<TextFieldFormInput
form={contentField}
debounce="250 millis"
/>
<TextFieldFormInput field={contentField} />
<Flex direction="row" justify="center" align="center" gap="2">
<TextFieldOptionalFormInput
form={completedAtField}
field={completedAtField}
type="datetime-local"
defaultValue=""
/>