Finalize the Effect View rename and refresh docs, examples, and tooling (#59)
Publish / publish (push) Successful in 3m23s
Lint / lint (push) Successful in 48s

## Summary

- Rename `effect-fc-next` to `effect-view` and update package metadata, internal symbols, imports, and repository references.
- Promote the Effect 4 example to `packages/example` and move the legacy Effect 3 example to `packages/effect-fc-example`.
- Add `effect-view` to the npm publishing workflow.
- Update the Vite Fast Refresh plugin for `effect-view` and rename `effectViewPlugin()` to `effectView()`.
- Upgrade Effect, React, TypeScript, build tooling, and related dependencies.
- Refresh the documentation and homepage.

## Documentation

- Add a dedicated Async guide covering:
  - Suspense fallbacks
  - Hook ordering
  - Memoization
  - Structural prop equality
- Clarify synchronous and asynchronous component behavior.
- Document runner scope and service requirements.
- Explain stable construction of Query, Mutation, and Form instances.
- Expand focused Lens and Form examples, including curried focus helpers.
- Document the optional `@effect/platform-browser` dependency for window-focus query refresh.
- Improve the Forms guide structure and examples.
- Fix responsive homepage headline wrapping and update homepage copy.

## Notable API changes

- Package rename:

  ```diff
  - effect-fc-next
  + effect-view

---------

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: #59
This commit was merged in pull request #59.
This commit is contained in:
2026-07-27 04:11:42 +02:00
parent ec21d49dce
commit 4b712de788
111 changed files with 1536 additions and 1278 deletions
+5 -5
View File
@@ -3,26 +3,26 @@
Experimental Vite Fast Refresh support for Effect View components.
```ts
import { effectViewPlugin } from "@effect-view/vite-plugin"
import { effectView } from "@effect-view/vite-plugin"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [
effectViewPlugin(),
effectView(),
react(),
],
})
```
The plugin recognizes `Component.make` and `Component.makeUntraced`
definitions imported from `effect-fc-next`. The legacy `effect-fc` package is
not supported. `effect-fc-next` owns the bundler-neutral refresh cell protocol;
definitions imported from `effect-view`. The legacy `effect-fc` package is
not supported. `effect-view` owns the bundler-neutral refresh cell protocol;
this package is the Vite adapter that retains those cells in HMR data. The
plugin assigns stable development IDs and self-accepts successfully
instrumented modules.
The adapter-facing protocol is exported from `effect-fc-next/Refreshable`.
The adapter-facing protocol is exported from `effect-view/Refreshable`.
Renaming or removing an instrumented View invalidates the module upward instead
of leaving a stale mounted descriptor.
+3 -3
View File
@@ -9,7 +9,7 @@
],
"license": "MIT",
"repository": {
"url": "git+https://github.com/Thiladev/effect-fc.git"
"url": "git+https://github.com/Thiladev/effect-view.git"
},
"types": "./dist/index.d.ts",
"exports": {
@@ -36,12 +36,12 @@
"typescript": "^6.0.3"
},
"devDependencies": {
"effect-fc-next": "workspace:*",
"effect-view": "workspace:*",
"vite": "^8.0.16",
"vitest": "^3.2.4"
},
"peerDependencies": {
"effect-fc-next": "0.1.0-beta.0",
"effect-view": "^0.1.0",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
}
+3 -3
View File
@@ -47,8 +47,8 @@ const refreshIdentifier = "__effectViewRefresh"
const acceptIdentifier = "__effectViewAccept"
const isSupportedPackage = (source: string): boolean =>
source === "effect-fc-next"
|| source === "effect-fc-next/Component"
source === "effect-view"
|| source === "effect-view/Component"
const collectImports = (
sourceFile: ts.SourceFile,
@@ -342,7 +342,7 @@ const scriptKind = (id: string): ts.ScriptKind => id.endsWith(".tsx")
*
* Place this plugin before `@vitejs/plugin-react`.
*/
export function effectViewPlugin(
export function effectView(
options: EffectViewRefreshOptions = {},
): Plugin {
const include = options.include ?? defaultInclude
+9 -9
View File
@@ -1,7 +1,7 @@
import path from "node:path"
import type { Plugin } from "vite"
import { describe, expect, it } from "vitest"
import { effectViewPlugin } from "./index.js"
import { effectView } from "./index.js"
type TransformHook = Extract<
@@ -31,12 +31,12 @@ const runTransform = async (
const transform = (
code: string,
id = path.join(process.cwd(), "src/View.tsx"),
): Promise<string | undefined> => runTransform(effectViewPlugin(), code, id)
): Promise<string | undefined> => runTransform(effectView(), code, id)
describe("effectViewPlugin", () => {
describe("effectView", () => {
it("wraps a const Effect View descriptor", async () => {
const result = await transform(`
import { Component } from "effect-fc-next"
import { Component } from "effect-view"
import * as React from "react"
export const CounterView = Component.make("CounterView")(function*() {
@@ -53,7 +53,7 @@ export const CounterView = Component.make("CounterView")(function*() {
it("registers a pipeline before withContext", async () => {
const result = await transform(`
import { Component } from "effect-fc-next"
import { Component } from "effect-view"
const Page = Component.make("Page")(function*() {
return <main />
@@ -69,7 +69,7 @@ const Page = Component.make("Page")(function*() {
it("wraps a class's complete trait pipeline", async () => {
const result = await transform(`
import { Async, Component, Memoized } from "effect-fc-next"
import { Async, Component, Memoized } from "effect-view"
class PostView extends Component.make("PostView")(function*() {
const value = yield* Component.useOnMount(load)
@@ -88,7 +88,7 @@ class PostView extends Component.make("PostView")(function*() {
it("supports aliased Component imports and refresh reset", async () => {
const result = await transform(`
// @refresh reset
import { Component as View } from "effect-fc-next"
import { Component as View } from "effect-view"
const Probe = View.makeUntraced(function*() {
return null
@@ -109,11 +109,11 @@ export function View() {
})
it("keeps instrumenting a module when all Effect Views are removed", async () => {
const plugin = effectViewPlugin() as Plugin
const plugin = effectView() as Plugin
const id = path.join(process.cwd(), "src/Removed.tsx")
await runTransform(plugin, `
import { Component } from "effect-fc-next"
import { Component } from "effect-view"
export const Probe = Component.makeUntraced(function*() {
return null
})
+2 -2
View File
@@ -1,5 +1,5 @@
import type * as Component from "effect-fc-next/Component"
import * as Refreshable from "effect-fc-next/Refreshable"
import type * as Component from "effect-view/Component"
import * as Refreshable from "effect-view/Refreshable"
import { describe, expect, it, vi } from "vitest"
import { accept, register } from "./runtime.js"
+2 -2
View File
@@ -1,5 +1,5 @@
import type * as Component from "effect-fc-next/Component"
import * as Refreshable from "effect-fc-next/Refreshable"
import type * as Component from "effect-view/Component"
import * as Refreshable from "effect-view/Refreshable"
export interface HotContext {
readonly data: Record<string, unknown>