effect-start 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/Development.test.ts +119 -0
- package/src/Development.ts +137 -0
- package/src/Entity.test.ts +1 -1
- package/src/Entity.ts +3 -6
- package/src/FileRouter.ts +2 -2
- package/src/Route.ts +4 -0
- package/src/RouteBody.test.ts +25 -45
- package/src/RouteBody.ts +11 -8
- package/src/RouteHttp.test.ts +4 -1
- package/src/RouteHttp.ts +22 -6
- package/src/RouteSse.test.ts +249 -0
- package/src/RouteSse.ts +195 -0
- package/src/Values.ts +9 -7
- package/src/bun/BunBundle.ts +0 -73
- package/src/bun/BunRoute.ts +0 -2
- package/src/hyper/HyperHtml.test.ts +119 -0
- package/src/hyper/HyperHtml.ts +10 -2
- package/src/hyper/HyperNode.ts +2 -0
- package/src/hyper/HyperRoute.test.tsx +197 -0
- package/src/hyper/HyperRoute.ts +61 -0
- package/src/hyper/index.ts +4 -0
- package/src/hyper/jsx.d.ts +15 -0
- package/src/index.ts +1 -0
- package/src/node/FileSystem.ts +8 -0
- package/src/FileSystemExtra.test.ts +0 -242
- package/src/FileSystemExtra.ts +0 -66
package/src/hyper/HyperHtml.ts
CHANGED
|
@@ -120,8 +120,13 @@ export function renderToString(
|
|
|
120
120
|
result += ` ${esc(key)}`
|
|
121
121
|
} else {
|
|
122
122
|
const resolvedKey = key === "className" ? "class" : key
|
|
123
|
+
const value = props[key]
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
if (key.startsWith("data-") && typeof value === "object") {
|
|
126
|
+
result += ` ${esc(resolvedKey)}="${esc(JSON.stringify(value))}"`
|
|
127
|
+
} else {
|
|
128
|
+
result += ` ${esc(resolvedKey)}="${esc(value)}"`
|
|
129
|
+
}
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
}
|
|
@@ -139,7 +144,10 @@ export function renderToString(
|
|
|
139
144
|
result += html
|
|
140
145
|
} else {
|
|
141
146
|
const children = props.children
|
|
142
|
-
|
|
147
|
+
|
|
148
|
+
if (type === "script" && typeof children === "function") {
|
|
149
|
+
result += `(${children.toString()})(window)`
|
|
150
|
+
} else if (Array.isArray(children)) {
|
|
143
151
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
144
152
|
stack.push(children[i])
|
|
145
153
|
}
|
package/src/hyper/HyperNode.ts
CHANGED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/** @jsxImportSource effect-start */
|
|
2
|
+
import * as test from "bun:test"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Entity from "../Entity.ts"
|
|
5
|
+
import * as Http from "../Http.ts"
|
|
6
|
+
import * as Route from "../Route.ts"
|
|
7
|
+
import * as RouteHttp from "../RouteHttp.ts"
|
|
8
|
+
import * as HyperRoute from "./HyperRoute.ts"
|
|
9
|
+
|
|
10
|
+
test.describe("HyperRoute.html", () => {
|
|
11
|
+
test.it("renders JSX to HTML string", async () => {
|
|
12
|
+
const handler = RouteHttp.toWebHandler(
|
|
13
|
+
Route.get(
|
|
14
|
+
HyperRoute.html(
|
|
15
|
+
<div>
|
|
16
|
+
Hello World
|
|
17
|
+
</div>,
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
23
|
+
|
|
24
|
+
test.expect(response.status).toBe(200)
|
|
25
|
+
test.expect(response.headers.get("Content-Type")).toBe("text/html; charset=utf-8")
|
|
26
|
+
test.expect(await response.text()).toBe("<div>Hello World</div>")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test.it("renders nested JSX elements", async () => {
|
|
30
|
+
const handler = RouteHttp.toWebHandler(
|
|
31
|
+
Route.get(
|
|
32
|
+
HyperRoute.html(
|
|
33
|
+
<div class="container">
|
|
34
|
+
<h1>
|
|
35
|
+
Title
|
|
36
|
+
</h1>
|
|
37
|
+
<p>
|
|
38
|
+
Paragraph
|
|
39
|
+
</p>
|
|
40
|
+
</div>,
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
46
|
+
|
|
47
|
+
test.expect(await response.text()).toBe(
|
|
48
|
+
"<div class=\"container\"><h1>Title</h1><p>Paragraph</p></div>",
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test.it("renders JSX from Effect", async () => {
|
|
53
|
+
const handler = RouteHttp.toWebHandler(
|
|
54
|
+
Route.get(
|
|
55
|
+
HyperRoute.html(
|
|
56
|
+
Effect.succeed(
|
|
57
|
+
<span>
|
|
58
|
+
From Effect
|
|
59
|
+
</span>,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
66
|
+
|
|
67
|
+
test.expect(await response.text()).toBe("<span>From Effect</span>")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test.it("renders JSX from generator function", async () => {
|
|
71
|
+
const handler = RouteHttp.toWebHandler(
|
|
72
|
+
Route.get(
|
|
73
|
+
HyperRoute.html(
|
|
74
|
+
Effect.gen(function*() {
|
|
75
|
+
const name = yield* Effect.succeed("World")
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
Hello {name}
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
87
|
+
|
|
88
|
+
test.expect(await response.text()).toBe("<div>Hello World</div>")
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test.it("renders JSX from handler function", async () => {
|
|
92
|
+
const handler = RouteHttp.toWebHandler(
|
|
93
|
+
Route.get(
|
|
94
|
+
HyperRoute.html((context) =>
|
|
95
|
+
Effect.succeed(
|
|
96
|
+
<div>
|
|
97
|
+
Request received
|
|
98
|
+
</div>,
|
|
99
|
+
)
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
105
|
+
|
|
106
|
+
test.expect(await response.text()).toBe("<div>Request received</div>")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test.it("renders JSX with dynamic content", async () => {
|
|
110
|
+
const items = ["Apple", "Banana", "Cherry"]
|
|
111
|
+
|
|
112
|
+
const handler = RouteHttp.toWebHandler(
|
|
113
|
+
Route.get(
|
|
114
|
+
HyperRoute.html(
|
|
115
|
+
<ul>
|
|
116
|
+
{items.map((item) => (
|
|
117
|
+
<li>
|
|
118
|
+
{item}
|
|
119
|
+
</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>,
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
127
|
+
|
|
128
|
+
test.expect(await response.text()).toBe(
|
|
129
|
+
"<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>",
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test.it("handles Entity with JSX body", async () => {
|
|
134
|
+
const handler = RouteHttp.toWebHandler(
|
|
135
|
+
Route.get(
|
|
136
|
+
HyperRoute.html(
|
|
137
|
+
Entity.make(
|
|
138
|
+
<div>
|
|
139
|
+
With Entity
|
|
140
|
+
</div>,
|
|
141
|
+
{ status: 201 },
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
148
|
+
|
|
149
|
+
test.expect(response.status).toBe(201)
|
|
150
|
+
test.expect(await response.text()).toBe("<div>With Entity</div>")
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test.it("renders data-* attributes with object values as JSON", async () => {
|
|
154
|
+
const handler = RouteHttp.toWebHandler(
|
|
155
|
+
Route.get(
|
|
156
|
+
HyperRoute.html(
|
|
157
|
+
<div
|
|
158
|
+
data-signals={{
|
|
159
|
+
draft: "",
|
|
160
|
+
pendingDraft: "",
|
|
161
|
+
username: "User123",
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
Content
|
|
165
|
+
</div>,
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
171
|
+
|
|
172
|
+
test.expect(await response.text()).toBe(
|
|
173
|
+
"<div data-signals=\"{"draft":"","pendingDraft":"","username":"User123"}\">Content</div>",
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test.it("renders script with function child as IIFE", async () => {
|
|
178
|
+
const handler = RouteHttp.toWebHandler(
|
|
179
|
+
Route.get(
|
|
180
|
+
HyperRoute.html(
|
|
181
|
+
<script>
|
|
182
|
+
{(window) => {
|
|
183
|
+
console.log("Hello from", window.document.title)
|
|
184
|
+
}}
|
|
185
|
+
</script>,
|
|
186
|
+
),
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const response = await Http.fetch(handler, { path: "/" })
|
|
191
|
+
const text = await response.text()
|
|
192
|
+
|
|
193
|
+
test.expect(text).toContain("<script>(")
|
|
194
|
+
test.expect(text).toContain(")(window)</script>")
|
|
195
|
+
test.expect(text).toContain("window.document.title")
|
|
196
|
+
})
|
|
197
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as Entity from "../Entity.ts"
|
|
3
|
+
import * as Route from "../Route.ts"
|
|
4
|
+
import type * as RouteBody from "../RouteBody.ts"
|
|
5
|
+
import * as HyperHtml from "./HyperHtml.ts"
|
|
6
|
+
import type { JSX } from "./jsx.d.ts"
|
|
7
|
+
|
|
8
|
+
function renderValue(
|
|
9
|
+
value: JSX.Children | Entity.Entity<JSX.Children>,
|
|
10
|
+
): string | Entity.Entity<string> {
|
|
11
|
+
if (Entity.isEntity(value)) {
|
|
12
|
+
return Entity.make(HyperHtml.renderToString(value.body), {
|
|
13
|
+
status: value.status,
|
|
14
|
+
url: value.url,
|
|
15
|
+
headers: value.headers,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
return HyperHtml.renderToString(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeToEffect<B, A, E, R>(
|
|
22
|
+
handler: RouteBody.HandlerInput<B, A, E, R>,
|
|
23
|
+
context: B,
|
|
24
|
+
next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<A>,
|
|
25
|
+
): Effect.Effect<A | Entity.Entity<A>, E, R> {
|
|
26
|
+
if (Effect.isEffect(handler)) {
|
|
27
|
+
return handler
|
|
28
|
+
}
|
|
29
|
+
if (typeof handler === "function") {
|
|
30
|
+
const result = (handler as Function)(context, next)
|
|
31
|
+
if (Effect.isEffect(result)) {
|
|
32
|
+
return result as Effect.Effect<A | Entity.Entity<A>, E, R>
|
|
33
|
+
}
|
|
34
|
+
return Effect.gen(function*() {
|
|
35
|
+
return yield* result
|
|
36
|
+
}) as Effect.Effect<A | Entity.Entity<A>, E, R>
|
|
37
|
+
}
|
|
38
|
+
return Effect.succeed(handler as A | Entity.Entity<A>)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function html<
|
|
42
|
+
D extends Route.RouteDescriptor.Any,
|
|
43
|
+
B extends {},
|
|
44
|
+
I extends Route.Route.Tuple,
|
|
45
|
+
E = never,
|
|
46
|
+
R = never,
|
|
47
|
+
>(
|
|
48
|
+
handler: RouteBody.HandlerInput<
|
|
49
|
+
NoInfer<D & B & Route.ExtractBindings<I> & { format: "html" }>,
|
|
50
|
+
JSX.Children,
|
|
51
|
+
E,
|
|
52
|
+
R
|
|
53
|
+
>,
|
|
54
|
+
) {
|
|
55
|
+
return Route.html<D, B, I, string, E, R>((context, next) =>
|
|
56
|
+
Effect.map(
|
|
57
|
+
normalizeToEffect(handler, context, next as never),
|
|
58
|
+
renderValue,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
}
|
package/src/hyper/jsx.d.ts
CHANGED
|
@@ -1818,6 +1818,21 @@ export namespace JSX {
|
|
|
1818
1818
|
event?: string | undefined
|
|
1819
1819
|
/** @deprecated */
|
|
1820
1820
|
language?: string | undefined
|
|
1821
|
+
|
|
1822
|
+
children?: Children
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Separate interface for script elements with function children.
|
|
1826
|
+
// This enables TypeScript to infer the `window` parameter type.
|
|
1827
|
+
//
|
|
1828
|
+
// Using a union in a single interface (`children?: Function | Children`)
|
|
1829
|
+
// doesn't work because TS can't infer callback parameter types from unions.
|
|
1830
|
+
// By splitting into two interfaces, TS can discriminate based on children
|
|
1831
|
+
interface ScriptHTMLAttributesWithHandler<T>
|
|
1832
|
+
extends Omit<ScriptHTMLAttributes<T>, "children" | "type">
|
|
1833
|
+
{
|
|
1834
|
+
children: (window: Window) => void
|
|
1835
|
+
type?: never
|
|
1821
1836
|
}
|
|
1822
1837
|
interface SelectHTMLAttributes<T> extends HTMLAttributes<T> {
|
|
1823
1838
|
autocomplete?: HTMLAutocomplete | undefined
|
package/src/index.ts
CHANGED
package/src/node/FileSystem.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from @effect/platform
|
|
3
|
+
*/
|
|
1
4
|
import { effectify } from "@effect/platform/Effectify"
|
|
2
5
|
import * as Error from "@effect/platform/Error"
|
|
3
6
|
import type {
|
|
@@ -715,6 +718,11 @@ const makeFileSystem = Effect.map(
|
|
|
715
718
|
|
|
716
719
|
export const layer = Layer.effect(FileSystem.FileSystem, makeFileSystem)
|
|
717
720
|
|
|
721
|
+
export {
|
|
722
|
+
Error,
|
|
723
|
+
FileSystem,
|
|
724
|
+
}
|
|
725
|
+
|
|
718
726
|
export function handleErrnoException(
|
|
719
727
|
module: SystemError["module"],
|
|
720
728
|
method: string,
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import * as FileSystem from "@effect/platform/FileSystem"
|
|
2
|
-
import * as test from "bun:test"
|
|
3
|
-
import { MemoryFileSystem } from "effect-memfs"
|
|
4
|
-
import * as Chunk from "effect/Chunk"
|
|
5
|
-
import * as Effect from "effect/Effect"
|
|
6
|
-
import * as Fiber from "effect/Fiber"
|
|
7
|
-
import * as Function from "effect/Function"
|
|
8
|
-
import * as Stream from "effect/Stream"
|
|
9
|
-
import * as FileSystemExtra from "./FileSystemExtra.ts"
|
|
10
|
-
|
|
11
|
-
test.describe(`${FileSystemExtra.watchSource.name}`, () => {
|
|
12
|
-
test.it("emits events for file creation", () =>
|
|
13
|
-
Effect
|
|
14
|
-
.gen(function*() {
|
|
15
|
-
const fs = yield* FileSystem.FileSystem
|
|
16
|
-
const watchDir = "/watch-test"
|
|
17
|
-
|
|
18
|
-
const fiber = yield* Function.pipe(
|
|
19
|
-
FileSystemExtra.watchSource({ path: watchDir }),
|
|
20
|
-
Stream.take(1),
|
|
21
|
-
Stream.runCollect,
|
|
22
|
-
Effect.fork,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
yield* Effect.sleep(1)
|
|
26
|
-
|
|
27
|
-
yield* fs.writeFileString(`${watchDir}/test.ts`, "const x = 1")
|
|
28
|
-
|
|
29
|
-
const events = yield* Fiber.join(fiber)
|
|
30
|
-
|
|
31
|
-
test
|
|
32
|
-
.expect(Chunk.size(events))
|
|
33
|
-
.toBeGreaterThan(0)
|
|
34
|
-
const first = Chunk.unsafeGet(events, 0)
|
|
35
|
-
test
|
|
36
|
-
.expect(first.path)
|
|
37
|
-
.toContain("test.ts")
|
|
38
|
-
test
|
|
39
|
-
.expect(["rename", "change"])
|
|
40
|
-
.toContain(first.eventType)
|
|
41
|
-
test
|
|
42
|
-
.expect(first.filename)
|
|
43
|
-
.toBe("test.ts")
|
|
44
|
-
})
|
|
45
|
-
.pipe(
|
|
46
|
-
Effect.scoped,
|
|
47
|
-
Effect.provide(
|
|
48
|
-
MemoryFileSystem.layerWith({ "/watch-test/.gitkeep": "" }),
|
|
49
|
-
),
|
|
50
|
-
Effect.runPromise,
|
|
51
|
-
))
|
|
52
|
-
|
|
53
|
-
test.it(
|
|
54
|
-
"emits change event for file modification",
|
|
55
|
-
() =>
|
|
56
|
-
Effect
|
|
57
|
-
.gen(function*() {
|
|
58
|
-
const fs = yield* FileSystem.FileSystem
|
|
59
|
-
const watchDir = "/watch-mod"
|
|
60
|
-
const filePath = `${watchDir}/file.ts`
|
|
61
|
-
|
|
62
|
-
const fiber = yield* Function.pipe(
|
|
63
|
-
FileSystemExtra.watchSource({ path: watchDir }),
|
|
64
|
-
Stream.take(1),
|
|
65
|
-
Stream.runCollect,
|
|
66
|
-
Effect.fork,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
yield* Effect.sleep(1)
|
|
70
|
-
yield* fs.writeFileString(filePath, "modified")
|
|
71
|
-
|
|
72
|
-
const events = yield* Fiber.join(fiber)
|
|
73
|
-
|
|
74
|
-
test
|
|
75
|
-
.expect(Chunk.size(events))
|
|
76
|
-
.toBeGreaterThan(0)
|
|
77
|
-
test
|
|
78
|
-
.expect(Chunk.unsafeGet(events, 0).eventType)
|
|
79
|
-
.toBe("change")
|
|
80
|
-
})
|
|
81
|
-
.pipe(
|
|
82
|
-
Effect.scoped,
|
|
83
|
-
Effect.provide(
|
|
84
|
-
MemoryFileSystem.layerWith({ "/watch-mod/file.ts": "initial" }),
|
|
85
|
-
),
|
|
86
|
-
Effect.runPromise,
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
test.it("applies custom filter", () =>
|
|
91
|
-
Effect
|
|
92
|
-
.gen(function*() {
|
|
93
|
-
const fs = yield* FileSystem.FileSystem
|
|
94
|
-
const watchDir = "/watch-filter"
|
|
95
|
-
|
|
96
|
-
const fiber = yield* Function.pipe(
|
|
97
|
-
FileSystemExtra.watchSource({
|
|
98
|
-
path: watchDir,
|
|
99
|
-
filter: FileSystemExtra.filterSourceFiles,
|
|
100
|
-
}),
|
|
101
|
-
Stream.take(1),
|
|
102
|
-
Stream.runCollect,
|
|
103
|
-
Effect.fork,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
yield* Effect.sleep(1)
|
|
107
|
-
yield* fs.writeFileString(`${watchDir}/ignored.txt`, "ignored")
|
|
108
|
-
yield* Effect.sleep(1)
|
|
109
|
-
yield* fs.writeFileString(`${watchDir}/included.ts`, "included")
|
|
110
|
-
|
|
111
|
-
const events = yield* Fiber.join(fiber)
|
|
112
|
-
|
|
113
|
-
test
|
|
114
|
-
.expect(Chunk.size(events))
|
|
115
|
-
.toBe(1)
|
|
116
|
-
test
|
|
117
|
-
.expect(Chunk.unsafeGet(events, 0).path)
|
|
118
|
-
.toContain("included.ts")
|
|
119
|
-
})
|
|
120
|
-
.pipe(
|
|
121
|
-
Effect.scoped,
|
|
122
|
-
Effect.provide(
|
|
123
|
-
MemoryFileSystem.layerWith({ "/watch-filter/.gitkeep": "" }),
|
|
124
|
-
),
|
|
125
|
-
Effect.runPromise,
|
|
126
|
-
))
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
test.describe(`${FileSystemExtra.filterSourceFiles.name}`, () => {
|
|
130
|
-
test.it("matches source file extensions", () => {
|
|
131
|
-
test
|
|
132
|
-
.expect(
|
|
133
|
-
FileSystemExtra.filterSourceFiles({
|
|
134
|
-
eventType: "change",
|
|
135
|
-
filename: "x",
|
|
136
|
-
path: "/a/b.ts",
|
|
137
|
-
}),
|
|
138
|
-
)
|
|
139
|
-
.toBe(true)
|
|
140
|
-
test
|
|
141
|
-
.expect(
|
|
142
|
-
FileSystemExtra.filterSourceFiles({
|
|
143
|
-
eventType: "change",
|
|
144
|
-
filename: "x",
|
|
145
|
-
path: "/a/b.tsx",
|
|
146
|
-
}),
|
|
147
|
-
)
|
|
148
|
-
.toBe(true)
|
|
149
|
-
test
|
|
150
|
-
.expect(
|
|
151
|
-
FileSystemExtra.filterSourceFiles({
|
|
152
|
-
eventType: "change",
|
|
153
|
-
filename: "x",
|
|
154
|
-
path: "/a/b.js",
|
|
155
|
-
}),
|
|
156
|
-
)
|
|
157
|
-
.toBe(true)
|
|
158
|
-
test
|
|
159
|
-
.expect(
|
|
160
|
-
FileSystemExtra.filterSourceFiles({
|
|
161
|
-
eventType: "change",
|
|
162
|
-
filename: "x",
|
|
163
|
-
path: "/a/b.jsx",
|
|
164
|
-
}),
|
|
165
|
-
)
|
|
166
|
-
.toBe(true)
|
|
167
|
-
test
|
|
168
|
-
.expect(
|
|
169
|
-
FileSystemExtra.filterSourceFiles({
|
|
170
|
-
eventType: "change",
|
|
171
|
-
filename: "x",
|
|
172
|
-
path: "/a/b.json",
|
|
173
|
-
}),
|
|
174
|
-
)
|
|
175
|
-
.toBe(true)
|
|
176
|
-
test
|
|
177
|
-
.expect(
|
|
178
|
-
FileSystemExtra.filterSourceFiles({
|
|
179
|
-
eventType: "change",
|
|
180
|
-
filename: "x",
|
|
181
|
-
path: "/a/b.css",
|
|
182
|
-
}),
|
|
183
|
-
)
|
|
184
|
-
.toBe(true)
|
|
185
|
-
test
|
|
186
|
-
.expect(
|
|
187
|
-
FileSystemExtra.filterSourceFiles({
|
|
188
|
-
eventType: "change",
|
|
189
|
-
filename: "x",
|
|
190
|
-
path: "/a/b.html",
|
|
191
|
-
}),
|
|
192
|
-
)
|
|
193
|
-
.toBe(true)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
test.it("rejects non-source files", () => {
|
|
197
|
-
test
|
|
198
|
-
.expect(
|
|
199
|
-
FileSystemExtra.filterSourceFiles({
|
|
200
|
-
eventType: "change",
|
|
201
|
-
filename: "x",
|
|
202
|
-
path: "/a/b.txt",
|
|
203
|
-
}),
|
|
204
|
-
)
|
|
205
|
-
.toBe(false)
|
|
206
|
-
test
|
|
207
|
-
.expect(
|
|
208
|
-
FileSystemExtra.filterSourceFiles({
|
|
209
|
-
eventType: "change",
|
|
210
|
-
filename: "x",
|
|
211
|
-
path: "/a/b.md",
|
|
212
|
-
}),
|
|
213
|
-
)
|
|
214
|
-
.toBe(false)
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
test.describe(`${FileSystemExtra.filterDirectory.name}`, () => {
|
|
219
|
-
test.it("matches directories", () => {
|
|
220
|
-
test
|
|
221
|
-
.expect(
|
|
222
|
-
FileSystemExtra.filterDirectory({
|
|
223
|
-
eventType: "change",
|
|
224
|
-
filename: "x",
|
|
225
|
-
path: "/a/b/",
|
|
226
|
-
}),
|
|
227
|
-
)
|
|
228
|
-
.toBe(true)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
test.it("rejects files", () => {
|
|
232
|
-
test
|
|
233
|
-
.expect(
|
|
234
|
-
FileSystemExtra.filterDirectory({
|
|
235
|
-
eventType: "change",
|
|
236
|
-
filename: "x",
|
|
237
|
-
path: "/a/b",
|
|
238
|
-
}),
|
|
239
|
-
)
|
|
240
|
-
.toBe(false)
|
|
241
|
-
})
|
|
242
|
-
})
|
package/src/FileSystemExtra.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as Error from "@effect/platform/Error"
|
|
2
|
-
import * as FileSystem from "@effect/platform/FileSystem"
|
|
3
|
-
import * as Effect from "effect/Effect"
|
|
4
|
-
import * as Function from "effect/Function"
|
|
5
|
-
import * as Stream from "effect/Stream"
|
|
6
|
-
import * as NPath from "node:path"
|
|
7
|
-
|
|
8
|
-
const SOURCE_FILENAME = /\.(tsx?|jsx?|html?|css|json)$/
|
|
9
|
-
|
|
10
|
-
export type WatchEvent = {
|
|
11
|
-
eventType: "rename" | "change"
|
|
12
|
-
filename: string
|
|
13
|
-
path: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const filterSourceFiles = (event: WatchEvent): boolean => {
|
|
17
|
-
return SOURCE_FILENAME.test(event.path)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const filterDirectory = (event: WatchEvent): boolean => {
|
|
21
|
-
return event.path.endsWith("/")
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const watchSource = (
|
|
25
|
-
opts?: {
|
|
26
|
-
path?: string
|
|
27
|
-
recursive?: boolean
|
|
28
|
-
filter?: (event: WatchEvent) => boolean
|
|
29
|
-
},
|
|
30
|
-
): Stream.Stream<WatchEvent, Error.PlatformError, FileSystem.FileSystem> => {
|
|
31
|
-
const baseDir = opts?.path ?? process.cwd()
|
|
32
|
-
const customFilter = opts?.filter
|
|
33
|
-
|
|
34
|
-
return Function.pipe(
|
|
35
|
-
Stream.unwrap(
|
|
36
|
-
Effect.map(
|
|
37
|
-
FileSystem.FileSystem,
|
|
38
|
-
fs => fs.watch(baseDir, { recursive: opts?.recursive ?? true }),
|
|
39
|
-
),
|
|
40
|
-
),
|
|
41
|
-
Stream.mapEffect(e =>
|
|
42
|
-
Effect.gen(function*() {
|
|
43
|
-
const fs = yield* FileSystem.FileSystem
|
|
44
|
-
const relativePath = NPath.relative(baseDir, e.path)
|
|
45
|
-
const eventType: "change" | "rename" = e._tag === "Update"
|
|
46
|
-
? "change"
|
|
47
|
-
: "rename"
|
|
48
|
-
const info = yield* Effect.either(fs.stat(e.path))
|
|
49
|
-
const isDir = info._tag === "Right" && info.right.type === "Directory"
|
|
50
|
-
return {
|
|
51
|
-
eventType,
|
|
52
|
-
filename: relativePath,
|
|
53
|
-
path: isDir ? `${e.path}/` : e.path,
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
),
|
|
57
|
-
customFilter ? Stream.filter(customFilter) : Function.identity,
|
|
58
|
-
Stream.rechunk(1),
|
|
59
|
-
Stream.throttle({
|
|
60
|
-
units: 1,
|
|
61
|
-
cost: () => 1,
|
|
62
|
-
duration: "400 millis",
|
|
63
|
-
strategy: "enforce",
|
|
64
|
-
}),
|
|
65
|
-
)
|
|
66
|
-
}
|