@@ -0,0 +1,132 @@
|
||||
import path from "node:path"
|
||||
import type { Plugin, TransformPluginContext } from "vite"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { effectViewPlugin } from "./index.js"
|
||||
|
||||
|
||||
const runTransform = async (
|
||||
plugin: Plugin,
|
||||
code: string,
|
||||
id: string,
|
||||
): Promise<string | undefined> => {
|
||||
const hook = plugin.transform
|
||||
if (!hook)
|
||||
return undefined
|
||||
|
||||
const result = typeof hook === "function"
|
||||
? await hook.call({} as TransformPluginContext, code, id)
|
||||
: await hook.handler.call({} as TransformPluginContext, code, id)
|
||||
|
||||
if (!result)
|
||||
return undefined
|
||||
return typeof result === "string" ? result : result.code
|
||||
}
|
||||
|
||||
const transform = (
|
||||
code: string,
|
||||
id = path.join(process.cwd(), "src/View.tsx"),
|
||||
): Promise<string | undefined> => runTransform(effectViewPlugin(), code, id)
|
||||
|
||||
describe("effectViewPlugin", () => {
|
||||
it("wraps a const Effect View descriptor", async () => {
|
||||
const result = await transform(`
|
||||
import { Component } from "effect-fc-next"
|
||||
import * as React from "react"
|
||||
|
||||
export const CounterView = Component.make("CounterView")(function*() {
|
||||
const [count] = React.useState(0)
|
||||
return <div>{count}</div>
|
||||
})
|
||||
`)
|
||||
|
||||
expect(result).toContain("import { accept as __effectViewAccept, register as __effectViewRefresh } from \"@effect-view/vite-plugin/runtime\"")
|
||||
expect(result).toContain("__effectViewRefresh(Component.make(\"CounterView\")")
|
||||
expect(result).toContain("\"src/View.tsx:CounterView\"")
|
||||
expect(result).toContain("__effectViewAccept(import.meta.hot, [\"src/View.tsx:CounterView\"])")
|
||||
})
|
||||
|
||||
it("registers a pipeline before withContext", async () => {
|
||||
const result = await transform(`
|
||||
import { Component } from "effect-fc-next"
|
||||
|
||||
const Page = Component.make("Page")(function*() {
|
||||
return <main />
|
||||
}).pipe(
|
||||
Component.withContext(runtime.context),
|
||||
)
|
||||
`)
|
||||
|
||||
expect(result).toContain("(__effectViewRefreshView) => __effectViewRefresh(__effectViewRefreshView")
|
||||
expect(result).toContain("Component.withContext(runtime.context)")
|
||||
expect(result).not.toContain("__effectViewRefresh(Component.make(\"Page\")")
|
||||
})
|
||||
|
||||
it("wraps a class's complete trait pipeline", async () => {
|
||||
const result = await transform(`
|
||||
import { Async, Component, Memoized } from "effect-fc-next"
|
||||
|
||||
class PostView extends Component.make("PostView")(function*() {
|
||||
const value = yield* Component.useOnMount(load)
|
||||
return <div>{value}</div>
|
||||
}).pipe(
|
||||
Async.async,
|
||||
Memoized.memoized,
|
||||
) {}
|
||||
`)
|
||||
|
||||
expect(result).toContain("class PostView extends __effectViewRefresh(Component.make(\"PostView\")")
|
||||
expect(result).toContain("Memoized.memoized,")
|
||||
expect(result).toContain("\"src/View.tsx:PostView\"")
|
||||
})
|
||||
|
||||
it("supports aliased Component imports and refresh reset", async () => {
|
||||
const result = await transform(`
|
||||
// @refresh reset
|
||||
import { Component as View } from "effect-fc-next"
|
||||
|
||||
const Probe = View.makeUntraced(function*() {
|
||||
return null
|
||||
})
|
||||
`)
|
||||
|
||||
expect(result).toContain("__effectViewRefresh(View.makeUntraced")
|
||||
expect(result).toMatch(/"src\/View\.tsx:Probe", "[a-z0-9]+", true/)
|
||||
})
|
||||
|
||||
it("ignores modules without Effect View definitions", async () => {
|
||||
expect(await transform(`
|
||||
import * as React from "react"
|
||||
export function View() {
|
||||
return <div />
|
||||
}
|
||||
`)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("keeps instrumenting a module when all Effect Views are removed", async () => {
|
||||
const plugin = effectViewPlugin() as Plugin
|
||||
const id = path.join(process.cwd(), "src/Removed.tsx")
|
||||
|
||||
await runTransform(plugin, `
|
||||
import { Component } from "effect-fc-next"
|
||||
export const Probe = Component.makeUntraced(function*() {
|
||||
return null
|
||||
})
|
||||
`, id)
|
||||
|
||||
const result = await runTransform(plugin, `
|
||||
export const value = 1
|
||||
`, id)
|
||||
|
||||
expect(result).toContain("__effectViewAccept(import.meta.hot, [])")
|
||||
expect(result).not.toContain("__effectViewRefresh(")
|
||||
})
|
||||
|
||||
it("ignores the legacy effect-fc package", async () => {
|
||||
expect(await transform(`
|
||||
import { Component } from "effect-fc"
|
||||
export const Legacy = Component.makeUntraced(function*() {
|
||||
return null
|
||||
})
|
||||
`)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import type * as Component from "effect-fc-next/Component"
|
||||
import * as Refreshable from "effect-fc-next/Refreshable"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { accept, register } from "./runtime.js"
|
||||
|
||||
|
||||
describe("refresh runtime", () => {
|
||||
const component = <A extends object>(value: A): A & Component.Component.Any =>
|
||||
value as A & Component.Component.Any
|
||||
|
||||
it("retains a cell and notifies subscribers for compatible updates", async () => {
|
||||
const hot = {
|
||||
data: {},
|
||||
accept: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
}
|
||||
const first = register(component({ body: "first" }), hot, "module:View", "hooks")
|
||||
const cell = (first as Record<PropertyKey, unknown>)[Refreshable.RefreshableTypeId] as Refreshable.Cell
|
||||
const listener = vi.fn()
|
||||
cell.subscribe(listener)
|
||||
|
||||
const second = register(component({ body: "second" }), hot, "module:View", "hooks")
|
||||
const secondCell = (second as Record<PropertyKey, unknown>)[Refreshable.RefreshableTypeId]
|
||||
|
||||
expect(secondCell).toBe(cell)
|
||||
expect(cell.current).toBe(second)
|
||||
expect(cell.snapshot).toEqual({
|
||||
revision: 1,
|
||||
resetRevision: 0,
|
||||
})
|
||||
|
||||
await Promise.resolve()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("increments resetRevision for incompatible and forced updates", () => {
|
||||
const hot = {
|
||||
data: {},
|
||||
accept: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
}
|
||||
const first = register(component({}), hot, "module:View", "one")
|
||||
const cell = (first as Record<PropertyKey, unknown>)[Refreshable.RefreshableTypeId] as Refreshable.Cell
|
||||
|
||||
register(component({}), hot, "module:View", "two")
|
||||
expect(cell.snapshot.resetRevision).toBe(1)
|
||||
|
||||
register(component({}), hot, "module:View", "two", true)
|
||||
expect(cell.snapshot.resetRevision).toBe(2)
|
||||
})
|
||||
|
||||
it("is inert outside a Vite hot context", () => {
|
||||
const descriptor = component({})
|
||||
expect(register(descriptor, undefined, "module:View", "hooks")).toBe(descriptor)
|
||||
expect((descriptor as Record<PropertyKey, unknown>)[Refreshable.RefreshableTypeId]).toBeUndefined()
|
||||
})
|
||||
|
||||
it("invalidates when the module's View IDs change", () => {
|
||||
const hot = {
|
||||
data: {},
|
||||
accept: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
}
|
||||
|
||||
accept(hot, ["module:First"])
|
||||
expect(hot.accept).toHaveBeenCalledTimes(1)
|
||||
expect(hot.invalidate).not.toHaveBeenCalled()
|
||||
|
||||
accept(hot, ["module:Second"])
|
||||
expect(hot.invalidate).toHaveBeenCalledWith("[effect-view] Effect View exports changed")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user