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
@@ -1,4 +1,4 @@
1
- import * as FileSystem from "@effect/platform/FileSystem"
1
+ import * as FileSystem from "./FileSystem.ts"
2
2
  import * as Effect from "effect/Effect"
3
3
  import * as Either from "effect/Either"
4
4
  import * as Schema from "effect/Schema"
@@ -7,9 +7,7 @@ import * as FilePathPattern from "./FilePathPattern.ts"
7
7
  import * as FileRouter from "./FileRouter.ts"
8
8
  import * as SchemaExtra from "./SchemaExtra.ts"
9
9
 
10
- export function validateRouteModule(
11
- module: unknown,
12
- ): module is FileRouter.RouteModule {
10
+ export function validateRouteModule(module: unknown): module is FileRouter.RouteModule {
13
11
  if (typeof module !== "object" || module === null) {
14
12
  return false
15
13
  }
@@ -25,10 +23,7 @@ export function validateRouteModule(
25
23
  export function generatePathParamsSchema(
26
24
  segments: ReadonlyArray<FilePathPattern.Segment>,
27
25
  ): Schema.Struct<any> | null {
28
- const fields: Record<
29
- PropertyKey,
30
- Schema.Schema.Any | Schema.PropertySignature.All
31
- > = {}
26
+ const fields: Record<PropertyKey, Schema.Schema.Any | Schema.PropertySignature.All> = {}
32
27
 
33
28
  for (const segment of segments) {
34
29
  if (segment._tag === "ParamSegment") {
@@ -53,17 +48,17 @@ export function validateRouteModules(
53
48
  path: string,
54
49
  routes: FileRouter.OrderedFileRoutes,
55
50
  ): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem> {
56
- return Effect.gen(function*() {
51
+ return Effect.gen(function* () {
57
52
  const fs = yield* FileSystem.FileSystem
58
- const routeHandles = routes.filter(h => h.handle === "route")
53
+ const routeHandles = routes.filter((h) => h.handle === "route")
59
54
 
60
55
  for (const handle of routeHandles) {
61
56
  const routeModulePath = NPath.resolve(path, handle.modulePath)
62
57
  const expectedSchema = generatePathParamsSchema(handle.segments)
63
58
 
64
- const fileExists = yield* fs.exists(routeModulePath).pipe(
65
- Effect.catchAll(() => Effect.succeed(false)),
66
- )
59
+ const fileExists = yield* fs
60
+ .exists(routeModulePath)
61
+ .pipe(Effect.catchAll(() => Effect.succeed(false)))
67
62
  if (!fileExists) {
68
63
  continue
69
64
  }
@@ -79,9 +74,7 @@ export function validateRouteModules(
79
74
  })
80
75
 
81
76
  if (!validateRouteModule(module)) {
82
- yield* Effect.logWarning(
83
- `Route module ${routeModulePath} should export default Route`,
84
- )
77
+ yield* Effect.logWarning(`Route module ${routeModulePath} should export default Route`)
85
78
  continue
86
79
  }
87
80
 
@@ -89,30 +82,27 @@ export function validateRouteModules(
89
82
  // extract user schema
90
83
  const userSchema = undefined
91
84
 
92
- if (
93
- expectedSchema
94
- && userSchema
95
- && !SchemaExtra.schemaEqual(userSchema, expectedSchema)
96
- ) {
85
+ if (expectedSchema && userSchema && !SchemaExtra.schemaEqual(userSchema, expectedSchema)) {
97
86
  const relativeFilePath = NPath.relative(process.cwd(), routeModulePath)
98
87
  yield* Effect.logError(
99
- `Route '${relativeFilePath}' has incorrect PathParams schema, expected schemaPathParams(${
100
- SchemaExtra.formatSchemaCode(expectedSchema)
101
- })`,
88
+ `Route '${relativeFilePath}' has incorrect PathParams schema, expected schemaPathParams(${SchemaExtra.formatSchemaCode(
89
+ expectedSchema,
90
+ )})`,
102
91
  )
103
92
  }
104
93
  }
105
94
  })
106
95
  }
107
96
 
108
- export function generateCode(
109
- fileRoutes: FileRouter.OrderedFileRoutes,
110
- ): string | null {
97
+ export function generateCode(fileRoutes: FileRouter.OrderedFileRoutes): string | null {
111
98
  // Group routes by path to find layers
112
- const routesByPath = new Map<string, {
113
- route?: FileRouter.FileRoute
114
- layers: FileRouter.FileRoute[]
115
- }>()
99
+ const routesByPath = new Map<
100
+ string,
101
+ {
102
+ route?: FileRouter.FileRoute
103
+ layers: Array<FileRouter.FileRoute>
104
+ }
105
+ >()
116
106
 
117
107
  for (const fileRoute of fileRoutes) {
118
108
  const existing = routesByPath.get(fileRoute.routePath) || { layers: [] }
@@ -125,31 +115,26 @@ export function generateCode(
125
115
  }
126
116
 
127
117
  // Helper to check if layer's path is an ancestor of route's path
128
- const layerMatchesRoute = (
129
- layer: FileRouter.FileRoute,
130
- route: FileRouter.FileRoute,
131
- ): boolean => {
118
+ const layerMatchesRoute = (layer: FileRouter.FileRoute, route: FileRouter.FileRoute): boolean => {
132
119
  const layerDir = layer.modulePath.replace(/\/(layer)\.(tsx?|jsx?)$/, "")
133
120
  if (layerDir === "/") return true
134
121
  return route.modulePath.startsWith(layerDir + "/")
135
122
  }
136
123
 
137
124
  // Build entries for each route path
138
- const entries: Array<{ path: string; loaders: string[] }> = []
125
+ const entries: Array<{ path: string; loaders: Array<string> }> = []
139
126
 
140
127
  for (const [path, { route }] of routesByPath) {
141
128
  if (!route) continue
142
129
 
143
130
  // Collect all parent layers that match the route's groups
144
- const allLayers: FileRouter.FileRoute[] = []
131
+ const allLayers: Array<FileRouter.FileRoute> = []
145
132
  let currentPath = path
146
133
 
147
134
  while (true) {
148
135
  const pathData = routesByPath.get(currentPath)
149
136
  if (pathData?.layers) {
150
- const matchingLayers = pathData.layers.filter(layer =>
151
- layerMatchesRoute(layer, route)
152
- )
137
+ const matchingLayers = pathData.layers.filter((layer) => layerMatchesRoute(layer, route))
153
138
  allLayers.unshift(...matchingLayers)
154
139
  }
155
140
 
@@ -167,11 +152,9 @@ export function generateCode(
167
152
  const pathPattern = pathPatternResult.right
168
153
 
169
154
  // Order: route first, then layers from innermost to outermost
170
- const loaders: string[] = [
155
+ const loaders: Array<string> = [
171
156
  `() => import(".${route.modulePath}")`,
172
- ...allLayers.reverse().map(layer =>
173
- `() => import(".${layer.modulePath}")`
174
- ),
157
+ ...allLayers.reverse().map((layer) => `() => import(".${layer.modulePath}")`),
175
158
  ]
176
159
 
177
160
  entries.push({ path: pathPattern, loaders })
@@ -207,13 +190,18 @@ export function update(
207
190
  routesPath: string,
208
191
  treePath = "server.gen.ts",
209
192
  ): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem> {
210
- return Effect.gen(function*() {
193
+ return Effect.gen(function* () {
211
194
  treePath = NPath.resolve(routesPath, treePath)
212
195
 
213
196
  const fs = yield* FileSystem.FileSystem
214
197
  const files = yield* fs.readDirectory(routesPath, { recursive: true }).pipe(
215
- Effect.mapError((cause) =>
216
- new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: routesPath })
198
+ Effect.mapError(
199
+ (cause) =>
200
+ new FileRouter.FileRouterError({
201
+ reason: "FileSystem",
202
+ cause,
203
+ path: routesPath,
204
+ }),
217
205
  ),
218
206
  )
219
207
  const fileRoutes = yield* FileRouter.getFileRoutes(files)
@@ -236,8 +224,13 @@ export function update(
236
224
  if (existingCode !== emptyCode) {
237
225
  yield* Effect.logDebug(`Clearing file routes tree: ${treePath}`)
238
226
  yield* fs.writeFileString(treePath, emptyCode).pipe(
239
- Effect.mapError((cause) =>
240
- new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })
227
+ Effect.mapError(
228
+ (cause) =>
229
+ new FileRouter.FileRouterError({
230
+ reason: "FileSystem",
231
+ cause,
232
+ path: treePath,
233
+ }),
241
234
  ),
242
235
  )
243
236
  }
@@ -249,8 +242,13 @@ export function update(
249
242
  if (existingCode !== newCode) {
250
243
  yield* Effect.logDebug(`Updating file routes tree: ${treePath}`)
251
244
  yield* fs.writeFileString(treePath, newCode).pipe(
252
- Effect.mapError((cause) =>
253
- new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })
245
+ Effect.mapError(
246
+ (cause) =>
247
+ new FileRouter.FileRouterError({
248
+ reason: "FileSystem",
249
+ cause,
250
+ path: treePath,
251
+ }),
254
252
  ),
255
253
  )
256
254
  } else {
@@ -263,13 +261,18 @@ export function dump(
263
261
  routesPath: string,
264
262
  treePath = "server.gen.ts",
265
263
  ): Effect.Effect<void, FileRouter.FileRouterError, FileSystem.FileSystem> {
266
- return Effect.gen(function*() {
264
+ return Effect.gen(function* () {
267
265
  treePath = NPath.resolve(routesPath, treePath)
268
266
 
269
267
  const fs = yield* FileSystem.FileSystem
270
268
  const files = yield* fs.readDirectory(routesPath, { recursive: true }).pipe(
271
- Effect.mapError((cause) =>
272
- new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: routesPath })
269
+ Effect.mapError(
270
+ (cause) =>
271
+ new FileRouter.FileRouterError({
272
+ reason: "FileSystem",
273
+ cause,
274
+ path: routesPath,
275
+ }),
273
276
  ),
274
277
  )
275
278
  const fileRoutes = yield* FileRouter.getFileRoutes(files)
@@ -288,8 +291,13 @@ export function dump(
288
291
  yield* Effect.logDebug(`Generating file routes tree: ${treePath}`)
289
292
 
290
293
  yield* fs.writeFileString(treePath, code).pipe(
291
- Effect.mapError((cause) =>
292
- new FileRouter.FileRouterError({ reason: "FileSystem", cause, path: treePath })
294
+ Effect.mapError(
295
+ (cause) =>
296
+ new FileRouter.FileRouterError({
297
+ reason: "FileSystem",
298
+ cause,
299
+ path: treePath,
300
+ }),
293
301
  ),
294
302
  )
295
303
  })
@@ -0,0 +1,390 @@
1
+ /*
2
+ * Adapted from @effect/platform
3
+ */
4
+ import * as Brand from "effect/Brand"
5
+ import * as Channel from "effect/Channel"
6
+ import * as Chunk from "effect/Chunk"
7
+ import * as Context from "effect/Context"
8
+ import * as Data from "effect/Data"
9
+ import * as Effect from "effect/Effect"
10
+ import * as Function from "effect/Function"
11
+ import * as Option from "effect/Option"
12
+ import * as Sink from "effect/Sink"
13
+ import type * as Scope from "effect/Scope"
14
+ import * as Stream from "effect/Stream"
15
+ import * as PlatformError from "./PlatformError.ts"
16
+
17
+ export interface FileSystem {
18
+ readonly access: (
19
+ path: string,
20
+ options?: AccessFileOptions,
21
+ ) => Effect.Effect<void, PlatformError.PlatformError>
22
+ readonly copy: (
23
+ fromPath: string,
24
+ toPath: string,
25
+ options?: CopyOptions,
26
+ ) => Effect.Effect<void, PlatformError.PlatformError>
27
+ readonly copyFile: (
28
+ fromPath: string,
29
+ toPath: string,
30
+ ) => Effect.Effect<void, PlatformError.PlatformError>
31
+ readonly chmod: (path: string, mode: number) => Effect.Effect<void, PlatformError.PlatformError>
32
+ readonly chown: (
33
+ path: string,
34
+ uid: number,
35
+ gid: number,
36
+ ) => Effect.Effect<void, PlatformError.PlatformError>
37
+ readonly exists: (path: string) => Effect.Effect<boolean, PlatformError.PlatformError>
38
+ readonly link: (
39
+ fromPath: string,
40
+ toPath: string,
41
+ ) => Effect.Effect<void, PlatformError.PlatformError>
42
+ readonly makeDirectory: (
43
+ path: string,
44
+ options?: MakeDirectoryOptions,
45
+ ) => Effect.Effect<void, PlatformError.PlatformError>
46
+ readonly makeTempDirectory: (
47
+ options?: MakeTempDirectoryOptions,
48
+ ) => Effect.Effect<string, PlatformError.PlatformError>
49
+ readonly makeTempDirectoryScoped: (
50
+ options?: MakeTempDirectoryOptions,
51
+ ) => Effect.Effect<string, PlatformError.PlatformError, Scope.Scope>
52
+ readonly makeTempFile: (
53
+ options?: MakeTempFileOptions,
54
+ ) => Effect.Effect<string, PlatformError.PlatformError>
55
+ readonly makeTempFileScoped: (
56
+ options?: MakeTempFileOptions,
57
+ ) => Effect.Effect<string, PlatformError.PlatformError, Scope.Scope>
58
+ readonly open: (
59
+ path: string,
60
+ options?: OpenFileOptions,
61
+ ) => Effect.Effect<File, PlatformError.PlatformError, Scope.Scope>
62
+ readonly readDirectory: (
63
+ path: string,
64
+ options?: ReadDirectoryOptions,
65
+ ) => Effect.Effect<Array<string>, PlatformError.PlatformError>
66
+ readonly readFile: (path: string) => Effect.Effect<Uint8Array, PlatformError.PlatformError>
67
+ readonly readFileString: (
68
+ path: string,
69
+ encoding?: string,
70
+ ) => Effect.Effect<string, PlatformError.PlatformError>
71
+ readonly readLink: (path: string) => Effect.Effect<string, PlatformError.PlatformError>
72
+ readonly realPath: (path: string) => Effect.Effect<string, PlatformError.PlatformError>
73
+ readonly remove: (
74
+ path: string,
75
+ options?: RemoveOptions,
76
+ ) => Effect.Effect<void, PlatformError.PlatformError>
77
+ readonly rename: (
78
+ oldPath: string,
79
+ newPath: string,
80
+ ) => Effect.Effect<void, PlatformError.PlatformError>
81
+ readonly sink: (
82
+ path: string,
83
+ options?: SinkOptions,
84
+ ) => Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>
85
+ readonly stat: (path: string) => Effect.Effect<File.Info, PlatformError.PlatformError>
86
+ readonly stream: (
87
+ path: string,
88
+ options?: StreamOptions,
89
+ ) => Stream.Stream<Uint8Array, PlatformError.PlatformError>
90
+ readonly symlink: (
91
+ fromPath: string,
92
+ toPath: string,
93
+ ) => Effect.Effect<void, PlatformError.PlatformError>
94
+ readonly truncate: (
95
+ path: string,
96
+ length?: SizeInput,
97
+ ) => Effect.Effect<void, PlatformError.PlatformError>
98
+ readonly utimes: (
99
+ path: string,
100
+ atime: Date | number,
101
+ mtime: Date | number,
102
+ ) => Effect.Effect<void, PlatformError.PlatformError>
103
+ readonly watch: (
104
+ path: string,
105
+ options?: WatchOptions,
106
+ ) => Stream.Stream<WatchEvent, PlatformError.PlatformError>
107
+ readonly writeFile: (
108
+ path: string,
109
+ data: Uint8Array,
110
+ options?: WriteFileOptions,
111
+ ) => Effect.Effect<void, PlatformError.PlatformError>
112
+ readonly writeFileString: (
113
+ path: string,
114
+ data: string,
115
+ options?: WriteFileStringOptions,
116
+ ) => Effect.Effect<void, PlatformError.PlatformError>
117
+ }
118
+
119
+ export const FileSystem: Context.Tag<FileSystem, FileSystem> = Context.GenericTag<FileSystem>(
120
+ "@effect/platform/FileSystem",
121
+ )
122
+
123
+ export type Size = Brand.Branded<bigint, "Size">
124
+
125
+ export type SizeInput = bigint | number | Size
126
+
127
+ export const Size = (bytes: SizeInput): Size =>
128
+ typeof bytes === "bigint" ? (bytes as Size) : (BigInt(bytes) as Size)
129
+
130
+ export type OpenFlag = "r" | "r+" | "w" | "wx" | "w+" | "wx+" | "a" | "ax" | "a+" | "ax+"
131
+
132
+ export type SeekMode = "start" | "current"
133
+
134
+ export interface AccessFileOptions {
135
+ readonly ok?: boolean
136
+ readonly readable?: boolean
137
+ readonly writable?: boolean
138
+ }
139
+
140
+ export interface MakeDirectoryOptions {
141
+ readonly recursive?: boolean
142
+ readonly mode?: number
143
+ }
144
+
145
+ export interface CopyOptions {
146
+ readonly overwrite?: boolean
147
+ readonly preserveTimestamps?: boolean
148
+ }
149
+
150
+ export interface MakeTempDirectoryOptions {
151
+ readonly directory?: string
152
+ readonly prefix?: string
153
+ }
154
+
155
+ export interface MakeTempFileOptions {
156
+ readonly directory?: string
157
+ readonly prefix?: string
158
+ readonly suffix?: string
159
+ }
160
+
161
+ export interface OpenFileOptions {
162
+ readonly flag?: OpenFlag
163
+ readonly mode?: number
164
+ }
165
+
166
+ export interface ReadDirectoryOptions {
167
+ readonly recursive?: boolean
168
+ }
169
+
170
+ export interface RemoveOptions {
171
+ readonly recursive?: boolean
172
+ readonly force?: boolean
173
+ }
174
+
175
+ export interface SinkOptions extends OpenFileOptions {}
176
+
177
+ export interface StreamOptions {
178
+ readonly bufferSize?: number
179
+ readonly bytesToRead?: SizeInput
180
+ readonly chunkSize?: SizeInput
181
+ readonly offset?: SizeInput
182
+ }
183
+
184
+ export interface WriteFileOptions {
185
+ readonly flag?: OpenFlag
186
+ readonly mode?: number
187
+ }
188
+
189
+ export interface WriteFileStringOptions {
190
+ readonly flag?: OpenFlag
191
+ readonly mode?: number
192
+ }
193
+
194
+ export interface WatchOptions {
195
+ readonly recursive?: boolean
196
+ }
197
+
198
+ export const FileTypeId: unique symbol = Symbol.for("@effect/platform/FileSystem/File")
199
+
200
+ export type FileTypeId = typeof FileTypeId
201
+
202
+ export interface File {
203
+ readonly [FileTypeId]: FileTypeId
204
+ readonly fd: File.Descriptor
205
+ readonly stat: Effect.Effect<File.Info, PlatformError.PlatformError>
206
+ readonly seek: (offset: SizeInput, from: SeekMode) => Effect.Effect<void>
207
+ readonly sync: Effect.Effect<void, PlatformError.PlatformError>
208
+ readonly read: (buffer: Uint8Array) => Effect.Effect<Size, PlatformError.PlatformError>
209
+ readonly readAlloc: (
210
+ size: SizeInput,
211
+ ) => Effect.Effect<Option.Option<Uint8Array>, PlatformError.PlatformError>
212
+ readonly truncate: (length?: SizeInput) => Effect.Effect<void, PlatformError.PlatformError>
213
+ readonly write: (buffer: Uint8Array) => Effect.Effect<Size, PlatformError.PlatformError>
214
+ readonly writeAll: (buffer: Uint8Array) => Effect.Effect<void, PlatformError.PlatformError>
215
+ }
216
+
217
+ export declare namespace File {
218
+ export type Descriptor = Brand.Branded<number, "FileDescriptor">
219
+
220
+ export type Type =
221
+ | "File"
222
+ | "Directory"
223
+ | "SymbolicLink"
224
+ | "BlockDevice"
225
+ | "CharacterDevice"
226
+ | "FIFO"
227
+ | "Socket"
228
+ | "Unknown"
229
+
230
+ export interface Info {
231
+ readonly type: Type
232
+ readonly mtime: Option.Option<Date>
233
+ readonly atime: Option.Option<Date>
234
+ readonly birthtime: Option.Option<Date>
235
+ readonly dev: number
236
+ readonly ino: Option.Option<number>
237
+ readonly mode: number
238
+ readonly nlink: Option.Option<number>
239
+ readonly uid: Option.Option<number>
240
+ readonly gid: Option.Option<number>
241
+ readonly rdev: Option.Option<number>
242
+ readonly size: Size
243
+ readonly blksize: Option.Option<Size>
244
+ readonly blocks: Option.Option<number>
245
+ }
246
+ }
247
+
248
+ export const FileDescriptor = Brand.nominal<File.Descriptor>()
249
+
250
+ export type WatchEvent = WatchEvent.Create | WatchEvent.Update | WatchEvent.Remove
251
+
252
+ export declare namespace WatchEvent {
253
+ export interface Create {
254
+ readonly _tag: "Create"
255
+ readonly path: string
256
+ }
257
+
258
+ export interface Update {
259
+ readonly _tag: "Update"
260
+ readonly path: string
261
+ }
262
+
263
+ export interface Remove {
264
+ readonly _tag: "Remove"
265
+ readonly path: string
266
+ }
267
+ }
268
+
269
+ export const WatchEventCreate: Data.Case.Constructor<WatchEvent.Create, "_tag"> =
270
+ Data.tagged<WatchEvent.Create>("Create")
271
+
272
+ export const WatchEventUpdate: Data.Case.Constructor<WatchEvent.Update, "_tag"> =
273
+ Data.tagged<WatchEvent.Update>("Update")
274
+
275
+ export const WatchEventRemove: Data.Case.Constructor<WatchEvent.Remove, "_tag"> =
276
+ Data.tagged<WatchEvent.Remove>("Remove")
277
+
278
+ export class WatchBackend extends Context.Tag("@effect/platform/FileSystem/WatchBackend")<
279
+ WatchBackend,
280
+ {
281
+ readonly register: (
282
+ path: string,
283
+ stat: File.Info,
284
+ options?: WatchOptions,
285
+ ) => Option.Option<Stream.Stream<WatchEvent, PlatformError.PlatformError>>
286
+ }
287
+ >() {}
288
+
289
+ export const make = (
290
+ impl: Omit<FileSystem, "exists" | "readFileString" | "stream" | "sink" | "writeFileString">,
291
+ ): FileSystem => {
292
+ return FileSystem.of({
293
+ ...impl,
294
+ exists: (path) =>
295
+ Function.pipe(
296
+ impl.access(path),
297
+ Effect.as(true),
298
+ Effect.catchTag("SystemError", (e) =>
299
+ e.reason === "NotFound" ? Effect.succeed(false) : Effect.fail(e),
300
+ ),
301
+ ),
302
+ readFileString: (path, encoding) =>
303
+ Effect.tryMap(impl.readFile(path), {
304
+ try: (_) => new TextDecoder(encoding).decode(_),
305
+ catch: (cause) =>
306
+ new PlatformError.BadArgument({
307
+ module: "FileSystem",
308
+ method: "readFileString",
309
+ description: "invalid encoding",
310
+ cause,
311
+ }),
312
+ }),
313
+ stream: (path, options) =>
314
+ Function.pipe(
315
+ impl.open(path, { flag: "r" }),
316
+ options?.offset
317
+ ? Effect.tap((file) => file.seek(options.offset!, "start"))
318
+ : Function.identity,
319
+ Effect.map((file) => fileStream(file, options)),
320
+ Stream.unwrapScoped,
321
+ ),
322
+ sink: (path, options) =>
323
+ Function.pipe(
324
+ impl.open(path, { flag: "w", ...options }),
325
+ Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))),
326
+ Sink.unwrapScoped,
327
+ ),
328
+ writeFileString: (path, data, options) =>
329
+ Effect.flatMap(
330
+ Effect.try({
331
+ try: () => new TextEncoder().encode(data),
332
+ catch: (cause) =>
333
+ new PlatformError.BadArgument({
334
+ module: "FileSystem",
335
+ method: "writeFileString",
336
+ description: "could not encode string",
337
+ cause,
338
+ }),
339
+ }),
340
+ (_) => impl.writeFile(path, _, options),
341
+ ),
342
+ })
343
+ }
344
+
345
+ const fileStream = (
346
+ file: File,
347
+ {
348
+ bufferSize = 16,
349
+ bytesToRead: bytesToRead_,
350
+ chunkSize: chunkSize_ = Size(64 * 1024),
351
+ }: StreamOptions = {},
352
+ ) => {
353
+ const bytesToRead = bytesToRead_ !== undefined ? Size(bytesToRead_) : undefined
354
+ const chunkSize = Size(chunkSize_)
355
+
356
+ function loop(
357
+ totalBytesRead: bigint,
358
+ ): Channel.Channel<
359
+ Chunk.Chunk<Uint8Array>,
360
+ unknown,
361
+ PlatformError.PlatformError,
362
+ unknown,
363
+ void,
364
+ unknown
365
+ > {
366
+ if (bytesToRead !== undefined && bytesToRead <= totalBytesRead) {
367
+ return Channel.void
368
+ }
369
+
370
+ const toRead =
371
+ bytesToRead !== undefined && bytesToRead - totalBytesRead < chunkSize
372
+ ? bytesToRead - totalBytesRead
373
+ : chunkSize
374
+
375
+ return Channel.flatMap(
376
+ file.readAlloc(toRead),
377
+ Option.match({
378
+ onNone: () => Channel.void,
379
+ onSome: (buf) =>
380
+ Channel.flatMap(Channel.write(Chunk.of(buf)), () =>
381
+ loop(totalBytesRead + BigInt(buf.length)),
382
+ ),
383
+ }),
384
+ )
385
+ }
386
+
387
+ return Stream.bufferChunks(Stream.fromChannel(loop(BigInt(0))), {
388
+ capacity: bufferSize,
389
+ })
390
+ }