effect-start 0.20.1 → 0.22.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.
Files changed (105) hide show
  1. package/README.md +1 -4
  2. package/dist/Cookies.js +392 -0
  3. package/dist/FileSystem.js +131 -0
  4. package/dist/Socket.js +37 -0
  5. package/package.json +39 -40
  6. package/src/Commander.ts +73 -130
  7. package/src/ContentNegotiation.ts +68 -100
  8. package/src/Cookies.ts +408 -0
  9. package/src/Development.ts +48 -63
  10. package/src/Effectify.ts +222 -206
  11. package/src/Entity.ts +59 -86
  12. package/src/FilePathPattern.ts +5 -5
  13. package/src/FileRouter.ts +38 -63
  14. package/src/FileRouterCodegen.ts +64 -56
  15. package/src/FileSystem.ts +390 -0
  16. package/src/Http.ts +17 -50
  17. package/src/PathPattern.ts +33 -41
  18. package/src/PlatformError.ts +29 -50
  19. package/src/PlatformRuntime.ts +39 -47
  20. package/src/Route.ts +68 -187
  21. package/src/RouteBody.ts +45 -161
  22. package/src/RouteHook.ts +22 -45
  23. package/src/RouteHttp.ts +88 -142
  24. package/src/RouteHttpTracer.ts +25 -26
  25. package/src/RouteMount.ts +100 -238
  26. package/src/RouteSchema.ts +67 -201
  27. package/src/RouteSse.ts +28 -82
  28. package/src/RouteTree.ts +31 -79
  29. package/src/RouteTrie.ts +13 -32
  30. package/src/SchemaExtra.ts +3 -5
  31. package/src/Socket.ts +51 -0
  32. package/src/Start.ts +20 -21
  33. package/src/StreamExtra.ts +93 -96
  34. package/src/TuplePathPattern.ts +54 -43
  35. package/src/Unique.ts +9 -15
  36. package/src/Values.ts +26 -30
  37. package/src/bun/BunBundle.ts +27 -73
  38. package/src/bun/BunImportTrackerPlugin.ts +67 -65
  39. package/src/bun/BunRoute.ts +12 -31
  40. package/src/bun/BunRuntime.ts +3 -10
  41. package/src/bun/BunServer.ts +50 -60
  42. package/src/bun/BunVirtualFilesPlugin.ts +1 -4
  43. package/src/bun/_BunEnhancedResolve.ts +17 -42
  44. package/src/bun/_empty.html +0 -1
  45. package/src/bundler/Bundle.ts +20 -36
  46. package/src/bundler/BundleFiles.ts +36 -56
  47. package/src/client/Overlay.ts +1 -2
  48. package/src/client/ScrollState.ts +5 -9
  49. package/src/client/index.ts +10 -13
  50. package/src/datastar/actions/fetch.ts +29 -48
  51. package/src/datastar/actions/peek.ts +1 -5
  52. package/src/datastar/actions/setAll.ts +2 -2
  53. package/src/datastar/actions/toggleAll.ts +2 -2
  54. package/src/datastar/attributes/attr.ts +17 -18
  55. package/src/datastar/attributes/bind.ts +41 -61
  56. package/src/datastar/attributes/class.ts +2 -5
  57. package/src/datastar/attributes/computed.ts +2 -10
  58. package/src/datastar/attributes/effect.ts +1 -2
  59. package/src/datastar/attributes/indicator.ts +2 -8
  60. package/src/datastar/attributes/init.ts +2 -10
  61. package/src/datastar/attributes/jsonSignals.ts +1 -6
  62. package/src/datastar/attributes/on.ts +4 -13
  63. package/src/datastar/attributes/onIntersect.ts +10 -22
  64. package/src/datastar/attributes/onInterval.ts +2 -10
  65. package/src/datastar/attributes/onSignalPatch.ts +18 -28
  66. package/src/datastar/attributes/ref.ts +1 -2
  67. package/src/datastar/attributes/show.ts +1 -2
  68. package/src/datastar/attributes/signals.ts +1 -5
  69. package/src/datastar/attributes/style.ts +6 -12
  70. package/src/datastar/attributes/text.ts +1 -2
  71. package/src/datastar/engine.ts +102 -158
  72. package/src/datastar/index.ts +2 -2
  73. package/src/datastar/utils.ts +16 -51
  74. package/src/datastar/watchers/patchElements.ts +35 -93
  75. package/src/datastar/watchers/patchSignals.ts +1 -2
  76. package/src/experimental/EncryptedCookies.ts +81 -175
  77. package/src/experimental/index.ts +0 -1
  78. package/src/hyper/Hyper.ts +14 -33
  79. package/src/hyper/HyperHtml.ts +13 -10
  80. package/src/hyper/HyperNode.ts +2 -7
  81. package/src/hyper/HyperRoute.ts +2 -5
  82. package/src/hyper/jsx-runtime.ts +2 -10
  83. package/src/hyper/jsx.d.ts +171 -440
  84. package/src/lint/plugin.js +276 -0
  85. package/src/node/NodeFileSystem.ts +140 -202
  86. package/src/node/NodeUtils.ts +1 -3
  87. package/src/testing/TestLogger.ts +9 -22
  88. package/src/testing/index.ts +0 -1
  89. package/src/testing/utils.ts +30 -31
  90. package/src/x/cloudflare/CloudflareTunnel.ts +53 -65
  91. package/src/x/datastar/Datastar.ts +3 -10
  92. package/src/x/datastar/index.ts +1 -3
  93. package/src/x/datastar/jsx-datastar.d.ts +1 -4
  94. package/src/x/tailwind/TailwindPlugin.ts +119 -112
  95. package/src/x/tailwind/compile.ts +10 -33
  96. package/src/x/tailwind/plugin.ts +2 -2
  97. package/src/HttpAppExtra.ts +0 -478
  98. package/src/HttpUtils.ts +0 -17
  99. package/src/bun/BunPlatformHttpServer.ts +0 -88
  100. package/src/bun/BunServerRequest.ts +0 -396
  101. package/src/bundler/BundleHttp.ts +0 -259
  102. package/src/experimental/SseHttpResponse.ts +0 -55
  103. package/src/middlewares/BasicAuthMiddleware.ts +0 -36
  104. package/src/middlewares/index.ts +0 -1
  105. package/src/testing/TestHttpClient.ts +0 -148
package/src/Cookies.ts ADDED
@@ -0,0 +1,408 @@
1
+ /*
2
+ * Minimal Cookies management adapted from @effect/platform
3
+ * We'll aim for full compatbility when it stabilizes.
4
+ */
5
+ import * as Duration from "effect/Duration"
6
+ import * as Inspectable from "effect/Inspectable"
7
+ import * as Option from "effect/Option"
8
+ import * as Pipeable from "effect/Pipeable"
9
+ import * as Predicate from "effect/Predicate"
10
+ import type * as Types from "effect/Types"
11
+
12
+ export const TypeId: unique symbol = Symbol.for("effect-start/Cookies")
13
+
14
+ export type TypeId = typeof TypeId
15
+
16
+ export const isCookies = (u: unknown): u is Cookies => Predicate.hasProperty(u, TypeId)
17
+
18
+ export interface Cookies extends Pipeable.Pipeable, Inspectable.Inspectable {
19
+ readonly [TypeId]: TypeId
20
+ readonly cookies: Record<string, Cookie>
21
+ }
22
+
23
+ export const CookieTypeId: unique symbol = Symbol.for("effect-start/Cookies/Cookie")
24
+
25
+ export type CookieTypeId = typeof CookieTypeId
26
+
27
+ export interface Cookie extends Inspectable.Inspectable {
28
+ readonly [CookieTypeId]: CookieTypeId
29
+ readonly name: string
30
+ readonly value: string
31
+ readonly valueEncoded: string
32
+ readonly options?:
33
+ | {
34
+ readonly domain?: string | undefined
35
+ readonly expires?: Date | undefined
36
+ readonly maxAge?: Duration.DurationInput | undefined
37
+ readonly path?: string | undefined
38
+ readonly priority?: "low" | "medium" | "high" | undefined
39
+ readonly httpOnly?: boolean | undefined
40
+ readonly secure?: boolean | undefined
41
+ readonly partitioned?: boolean | undefined
42
+ readonly sameSite?:
43
+ // send with top-level navigations and GET requests from third-party sites
44
+ | "lax"
45
+ // only send with same-site requests
46
+ | "strict"
47
+ // send with all requests (requires Secure)
48
+ | "none"
49
+ | undefined
50
+ }
51
+ | undefined
52
+ }
53
+
54
+ const CookiesProto: Omit<Cookies, "cookies"> = {
55
+ [TypeId]: TypeId,
56
+ ...Inspectable.BaseProto,
57
+ toJSON(this: Cookies) {
58
+ return {
59
+ _id: "effect-start/Cookies",
60
+ cookies: Object.fromEntries(Object.entries(this.cookies).map(([k, v]) => [k, v.toJSON()])),
61
+ }
62
+ },
63
+ pipe() {
64
+ return Pipeable.pipeArguments(this, arguments)
65
+ },
66
+ }
67
+
68
+ const CookieProto = {
69
+ [CookieTypeId]: CookieTypeId,
70
+ ...Inspectable.BaseProto,
71
+ toJSON(this: Cookie) {
72
+ return {
73
+ _id: "effect-start/Cookies/Cookie",
74
+ name: this.name,
75
+ value: this.value,
76
+ options: this.options,
77
+ }
78
+ },
79
+ }
80
+
81
+ const makeCookiesFromRecord = (cookies: Record<string, Cookie>): Cookies => {
82
+ const self = Object.create(CookiesProto)
83
+ self.cookies = cookies
84
+ return self
85
+ }
86
+
87
+ const cookieFromParts = (
88
+ name: string,
89
+ value: string,
90
+ valueEncoded: string,
91
+ options?: Cookie["options"],
92
+ ): Cookie =>
93
+ Object.assign(Object.create(CookieProto), {
94
+ name,
95
+ value,
96
+ valueEncoded,
97
+ options,
98
+ })
99
+
100
+ export const empty: Cookies = makeCookiesFromRecord({})
101
+
102
+ export const fromIterable = (cookies: Iterable<Cookie>): Cookies => {
103
+ const record: Record<string, Cookie> = {}
104
+ for (const cookie of cookies) {
105
+ record[cookie.name] = cookie
106
+ }
107
+ return makeCookiesFromRecord(record)
108
+ }
109
+
110
+ export const fromSetCookie = (headers: Iterable<string> | string): Cookies => {
111
+ const arrayHeaders = typeof headers === "string" ? [headers] : headers
112
+ const cookies: Array<Cookie> = []
113
+ for (const header of arrayHeaders) {
114
+ const cookie = parseSetCookie(header.trim())
115
+ if (Option.isSome(cookie)) {
116
+ cookies.push(cookie.value)
117
+ }
118
+ }
119
+ return fromIterable(cookies)
120
+ }
121
+
122
+ export const unsafeMakeCookie = (
123
+ name: string,
124
+ value: string,
125
+ options?: Cookie["options"] | undefined,
126
+ ): Cookie => cookieFromParts(name, value, encodeURIComponent(value), options)
127
+
128
+ export const isEmpty = (self: Cookies): boolean => {
129
+ for (const _ in self.cookies) return false
130
+ return true
131
+ }
132
+
133
+ export const get = (self: Cookies, name: string): Option.Option<Cookie> =>
134
+ name in self.cookies ? Option.some(self.cookies[name]) : Option.none()
135
+
136
+ export const getValue = (self: Cookies, name: string): Option.Option<string> =>
137
+ Option.map(get(self, name), (cookie) => cookie.value)
138
+
139
+ export const setCookie = (self: Cookies, cookie: Cookie): Cookies =>
140
+ makeCookiesFromRecord({ ...self.cookies, [cookie.name]: cookie })
141
+
142
+ export const unsafeSet = (
143
+ self: Cookies,
144
+ name: string,
145
+ value: string,
146
+ options?: Cookie["options"],
147
+ ): Cookies => setCookie(self, unsafeMakeCookie(name, value, options))
148
+
149
+ export const unsafeSetAll = (
150
+ self: Cookies,
151
+ cookies: Iterable<readonly [name: string, value: string, options?: Cookie["options"]]>,
152
+ ): Cookies => {
153
+ const record: Record<string, Cookie> = { ...self.cookies }
154
+ for (const [name, value, options] of cookies) {
155
+ record[name] = unsafeMakeCookie(name, value, options)
156
+ }
157
+ return makeCookiesFromRecord(record)
158
+ }
159
+
160
+ export const remove = (self: Cookies, name: string): Cookies => {
161
+ const { [name]: _, ...rest } = self.cookies
162
+ return makeCookiesFromRecord(rest)
163
+ }
164
+
165
+ export const merge = (self: Cookies, that: Cookies): Cookies =>
166
+ makeCookiesFromRecord({ ...self.cookies, ...that.cookies })
167
+
168
+ export function serializeCookie(self: Cookie): string {
169
+ let str = self.name + "=" + self.valueEncoded
170
+
171
+ if (self.options === undefined) {
172
+ return str
173
+ }
174
+ const options = self.options
175
+
176
+ if (options.maxAge !== undefined) {
177
+ const maxAge = Duration.toSeconds(options.maxAge)
178
+ str += "; Max-Age=" + Math.trunc(maxAge)
179
+ }
180
+
181
+ if (options.domain !== undefined) {
182
+ str += "; Domain=" + options.domain
183
+ }
184
+
185
+ if (options.path !== undefined) {
186
+ str += "; Path=" + options.path
187
+ }
188
+
189
+ if (options.priority !== undefined) {
190
+ switch (options.priority) {
191
+ case "low":
192
+ str += "; Priority=Low"
193
+ break
194
+ case "medium":
195
+ str += "; Priority=Medium"
196
+ break
197
+ case "high":
198
+ str += "; Priority=High"
199
+ break
200
+ }
201
+ }
202
+
203
+ if (options.expires !== undefined) {
204
+ str += "; Expires=" + options.expires.toUTCString()
205
+ }
206
+
207
+ if (options.httpOnly) {
208
+ str += "; HttpOnly"
209
+ }
210
+
211
+ if (options.secure) {
212
+ str += "; Secure"
213
+ }
214
+
215
+ if (options.partitioned) {
216
+ str += "; Partitioned"
217
+ }
218
+
219
+ if (options.sameSite !== undefined) {
220
+ switch (options.sameSite) {
221
+ case "lax":
222
+ str += "; SameSite=Lax"
223
+ break
224
+ case "strict":
225
+ str += "; SameSite=Strict"
226
+ break
227
+ case "none":
228
+ str += "; SameSite=None"
229
+ break
230
+ }
231
+ }
232
+
233
+ return str
234
+ }
235
+
236
+ export const toCookieHeader = (self: Cookies): string =>
237
+ Object.values(self.cookies)
238
+ .map((cookie) => `${cookie.name}=${cookie.valueEncoded}`)
239
+ .join("; ")
240
+
241
+ export const toRecord = (self: Cookies): Record<string, string> => {
242
+ const record: Record<string, string> = {}
243
+ for (const cookie of Object.values(self.cookies)) {
244
+ record[cookie.name] = cookie.value
245
+ }
246
+ return record
247
+ }
248
+
249
+ export const toSetCookieHeaders = (self: Cookies): Array<string> =>
250
+ Object.values(self.cookies).map(serializeCookie)
251
+
252
+ export function parseHeader(header: string): Record<string, string> {
253
+ const result: Record<string, string> = {}
254
+
255
+ const strLen = header.length
256
+ let pos = 0
257
+ let terminatorPos = 0
258
+
259
+ while (true) {
260
+ if (terminatorPos === strLen) break
261
+ terminatorPos = header.indexOf(";", pos)
262
+ if (terminatorPos === -1) terminatorPos = strLen
263
+
264
+ let eqIdx = header.indexOf("=", pos)
265
+ if (eqIdx === -1) break
266
+ if (eqIdx > terminatorPos) {
267
+ pos = terminatorPos + 1
268
+ continue
269
+ }
270
+
271
+ const key = header.substring(pos, eqIdx++).trim()
272
+ if (result[key] === undefined) {
273
+ const val =
274
+ header.charCodeAt(eqIdx) === 0x22
275
+ ? header.substring(eqIdx + 1, terminatorPos - 1).trim()
276
+ : header.substring(eqIdx, terminatorPos).trim()
277
+
278
+ result[key] = !(val.indexOf("%") === -1) ? tryDecodeURIComponent(val) : val
279
+ }
280
+
281
+ pos = terminatorPos + 1
282
+ }
283
+
284
+ return result
285
+ }
286
+
287
+ // eslint-disable-next-line no-control-regex
288
+ const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/
289
+
290
+ function parseSetCookie(header: string): Option.Option<Cookie> {
291
+ const parts = header
292
+ .split(";")
293
+ .map((_) => _.trim())
294
+ .filter((_) => _ !== "")
295
+ if (parts.length === 0) {
296
+ return Option.none()
297
+ }
298
+
299
+ const firstEqual = parts[0].indexOf("=")
300
+ if (firstEqual === -1) {
301
+ return Option.none()
302
+ }
303
+ const name = parts[0].slice(0, firstEqual)
304
+ if (!fieldContentRegExp.test(name)) {
305
+ return Option.none()
306
+ }
307
+
308
+ const valueEncoded = parts[0].slice(firstEqual + 1)
309
+ const value = tryDecodeURIComponent(valueEncoded)
310
+
311
+ if (parts.length === 1) {
312
+ return Option.some(cookieFromParts(name, value, valueEncoded))
313
+ }
314
+
315
+ const options: Types.Mutable<Cookie["options"]> = {}
316
+
317
+ for (let i = 1; i < parts.length; i++) {
318
+ const part = parts[i]
319
+ const equalIndex = part.indexOf("=")
320
+ const key = equalIndex === -1 ? part : part.slice(0, equalIndex).trim()
321
+ const value = equalIndex === -1 ? undefined : part.slice(equalIndex + 1).trim()
322
+
323
+ switch (key.toLowerCase()) {
324
+ case "domain": {
325
+ if (value === undefined) break
326
+ const domain = value.trim().replace(/^\./, "")
327
+ if (domain) options.domain = domain
328
+ break
329
+ }
330
+ case "expires": {
331
+ if (value === undefined) break
332
+ const date = new Date(value)
333
+ if (!isNaN(date.getTime())) options.expires = date
334
+ break
335
+ }
336
+ case "max-age": {
337
+ if (value === undefined) break
338
+ const maxAge = parseInt(value, 10)
339
+ if (!isNaN(maxAge)) options.maxAge = Duration.seconds(maxAge)
340
+ break
341
+ }
342
+ case "path": {
343
+ if (value === undefined) break
344
+ if (value[0] === "/") options.path = value
345
+ break
346
+ }
347
+ case "priority": {
348
+ if (value === undefined) break
349
+ switch (value.toLowerCase()) {
350
+ case "low":
351
+ options.priority = "low"
352
+ break
353
+ case "medium":
354
+ options.priority = "medium"
355
+ break
356
+ case "high":
357
+ options.priority = "high"
358
+ break
359
+ }
360
+ break
361
+ }
362
+ case "httponly": {
363
+ options.httpOnly = true
364
+ break
365
+ }
366
+ case "secure": {
367
+ options.secure = true
368
+ break
369
+ }
370
+ case "partitioned": {
371
+ options.partitioned = true
372
+ break
373
+ }
374
+ case "samesite": {
375
+ if (value === undefined) break
376
+ switch (value.toLowerCase()) {
377
+ case "lax":
378
+ options.sameSite = "lax"
379
+ break
380
+ case "strict":
381
+ options.sameSite = "strict"
382
+ break
383
+ case "none":
384
+ options.sameSite = "none"
385
+ break
386
+ }
387
+ break
388
+ }
389
+ }
390
+ }
391
+
392
+ return Option.some(
393
+ cookieFromParts(
394
+ name,
395
+ value,
396
+ valueEncoded,
397
+ Object.keys(options).length > 0 ? options : undefined,
398
+ ),
399
+ )
400
+ }
401
+
402
+ const tryDecodeURIComponent = (str: string): string => {
403
+ try {
404
+ return decodeURIComponent(str)
405
+ } catch {
406
+ return str
407
+ }
408
+ }
@@ -1,4 +1,4 @@
1
- import * as FileSystem from "@effect/platform/FileSystem"
1
+ import * as FileSystem from "./FileSystem.ts"
2
2
  import * as Context from "effect/Context"
3
3
  import * as Effect from "effect/Effect"
4
4
  import * as Function from "effect/Function"
@@ -7,21 +7,18 @@ import * as Layer from "effect/Layer"
7
7
  import * as Option from "effect/Option"
8
8
  import * as PubSub from "effect/PubSub"
9
9
  import * as Stream from "effect/Stream"
10
- import * as PlatformError from "./PlatformError.ts"
10
+ import type * as PlatformError from "./PlatformError.ts"
11
11
 
12
12
  export type DevelopmentEvent =
13
13
  | FileSystem.WatchEvent
14
14
  | {
15
- readonly _tag: "Reload"
16
- }
15
+ readonly _tag: "Reload"
16
+ }
17
17
 
18
- const devState = GlobalValue.globalValue(
19
- Symbol.for("effect-start/Development"),
20
- () => ({
21
- count: 0,
22
- pubsub: null as PubSub.PubSub<DevelopmentEvent> | null,
23
- }),
24
- )
18
+ const devState = GlobalValue.globalValue(Symbol.for("effect-start/Development"), () => ({
19
+ count: 0,
20
+ pubsub: null as PubSub.PubSub<DevelopmentEvent> | null,
21
+ }))
25
22
 
26
23
  /** @internal */
27
24
  export const _resetForTesting = () => {
@@ -48,25 +45,18 @@ export const filterDirectory = (event: FileSystem.WatchEvent): boolean => {
48
45
  return event.path.endsWith("/")
49
46
  }
50
47
 
51
- export const watchSource = (
52
- opts?: {
53
- path?: string
54
- recursive?: boolean
55
- filter?: (event: FileSystem.WatchEvent) => boolean
56
- },
57
- ): Stream.Stream<
58
- FileSystem.WatchEvent,
59
- PlatformError.PlatformError,
60
- FileSystem.FileSystem
61
- > => {
48
+ export const watchSource = (opts?: {
49
+ path?: string
50
+ recursive?: boolean
51
+ filter?: (event: FileSystem.WatchEvent) => boolean
52
+ }): Stream.Stream<FileSystem.WatchEvent, PlatformError.PlatformError, FileSystem.FileSystem> => {
62
53
  const baseDir = opts?.path ?? process.cwd()
63
54
  const customFilter = opts?.filter
64
55
 
65
56
  return Function.pipe(
66
57
  Stream.unwrap(
67
- Effect.map(
68
- FileSystem.FileSystem,
69
- fs => fs.watch(baseDir, { recursive: opts?.recursive ?? true }),
58
+ Effect.map(FileSystem.FileSystem, (fs) =>
59
+ fs.watch(baseDir, { recursive: opts?.recursive ?? true }),
70
60
  ),
71
61
  ),
72
62
  customFilter ? Stream.filter(customFilter) : Function.identity,
@@ -80,44 +70,39 @@ export const watchSource = (
80
70
  )
81
71
  }
82
72
 
83
- export const watch = (
84
- opts?: {
85
- path?: string
86
- recursive?: boolean
87
- filter?: (event: FileSystem.WatchEvent) => boolean
88
- },
89
- ) =>
90
- Effect
91
- .gen(function*() {
92
- devState.count++
93
-
94
- if (devState.count === 1) {
95
- const pubsub = yield* PubSub.unbounded<DevelopmentEvent>()
96
- devState.pubsub = pubsub
97
-
98
- yield* Function.pipe(
99
- watchSource({
100
- path: opts?.path,
101
- recursive: opts?.recursive,
102
- filter: opts?.filter ?? filterSourceFiles,
103
- }),
104
- Stream.runForEach((event) => PubSub.publish(pubsub, event)),
105
- Effect.fork,
106
- )
107
- } else {
108
- yield* PubSub.publish(devState.pubsub!, { _tag: "Reload" })
109
- }
110
-
111
- return { events: devState.pubsub! } satisfies DevelopmentService
112
- })
113
-
114
- export const layerWatch = (
115
- opts?: {
116
- path?: string
117
- recursive?: boolean
118
- filter?: (event: FileSystem.WatchEvent) => boolean
119
- },
120
- ) => Layer.scoped(Development, watch(opts))
73
+ export const watch = (opts?: {
74
+ path?: string
75
+ recursive?: boolean
76
+ filter?: (event: FileSystem.WatchEvent) => boolean
77
+ }) =>
78
+ Effect.gen(function* () {
79
+ devState.count++
80
+
81
+ if (devState.count === 1) {
82
+ const pubsub = yield* PubSub.unbounded<DevelopmentEvent>()
83
+ devState.pubsub = pubsub
84
+
85
+ yield* Function.pipe(
86
+ watchSource({
87
+ path: opts?.path,
88
+ recursive: opts?.recursive,
89
+ filter: opts?.filter ?? filterSourceFiles,
90
+ }),
91
+ Stream.runForEach((event) => PubSub.publish(pubsub, event)),
92
+ Effect.fork,
93
+ )
94
+ } else {
95
+ yield* PubSub.publish(devState.pubsub!, { _tag: "Reload" })
96
+ }
97
+
98
+ return { events: devState.pubsub! } satisfies DevelopmentService
99
+ })
100
+
101
+ export const layerWatch = (opts?: {
102
+ path?: string
103
+ recursive?: boolean
104
+ filter?: (event: FileSystem.WatchEvent) => boolean
105
+ }) => Layer.scoped(Development, watch(opts))
121
106
 
122
107
  export const stream = (): Stream.Stream<DevelopmentEvent> =>
123
108
  Stream.unwrap(