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.
- package/README.md +1 -4
- package/dist/Cookies.js +392 -0
- package/dist/FileSystem.js +131 -0
- package/dist/Socket.js +37 -0
- package/package.json +39 -40
- package/src/Commander.ts +73 -130
- package/src/ContentNegotiation.ts +68 -100
- package/src/Cookies.ts +408 -0
- package/src/Development.ts +48 -63
- package/src/Effectify.ts +222 -206
- package/src/Entity.ts +59 -86
- package/src/FilePathPattern.ts +5 -5
- package/src/FileRouter.ts +38 -63
- package/src/FileRouterCodegen.ts +64 -56
- package/src/FileSystem.ts +390 -0
- package/src/Http.ts +17 -50
- package/src/PathPattern.ts +33 -41
- package/src/PlatformError.ts +29 -50
- package/src/PlatformRuntime.ts +39 -47
- package/src/Route.ts +68 -187
- package/src/RouteBody.ts +45 -161
- package/src/RouteHook.ts +22 -45
- package/src/RouteHttp.ts +88 -142
- package/src/RouteHttpTracer.ts +25 -26
- package/src/RouteMount.ts +100 -238
- package/src/RouteSchema.ts +67 -201
- package/src/RouteSse.ts +28 -82
- package/src/RouteTree.ts +31 -79
- package/src/RouteTrie.ts +13 -32
- package/src/SchemaExtra.ts +3 -5
- package/src/Socket.ts +51 -0
- package/src/Start.ts +20 -21
- package/src/StreamExtra.ts +93 -96
- package/src/TuplePathPattern.ts +54 -43
- package/src/Unique.ts +9 -15
- package/src/Values.ts +26 -30
- package/src/bun/BunBundle.ts +27 -73
- package/src/bun/BunImportTrackerPlugin.ts +67 -65
- package/src/bun/BunRoute.ts +12 -31
- package/src/bun/BunRuntime.ts +3 -10
- package/src/bun/BunServer.ts +50 -60
- package/src/bun/BunVirtualFilesPlugin.ts +1 -4
- package/src/bun/_BunEnhancedResolve.ts +17 -42
- package/src/bun/_empty.html +0 -1
- package/src/bundler/Bundle.ts +20 -36
- package/src/bundler/BundleFiles.ts +36 -56
- package/src/client/Overlay.ts +1 -2
- package/src/client/ScrollState.ts +5 -9
- package/src/client/index.ts +10 -13
- package/src/datastar/actions/fetch.ts +29 -48
- package/src/datastar/actions/peek.ts +1 -5
- package/src/datastar/actions/setAll.ts +2 -2
- package/src/datastar/actions/toggleAll.ts +2 -2
- package/src/datastar/attributes/attr.ts +17 -18
- package/src/datastar/attributes/bind.ts +41 -61
- package/src/datastar/attributes/class.ts +2 -5
- package/src/datastar/attributes/computed.ts +2 -10
- package/src/datastar/attributes/effect.ts +1 -2
- package/src/datastar/attributes/indicator.ts +2 -8
- package/src/datastar/attributes/init.ts +2 -10
- package/src/datastar/attributes/jsonSignals.ts +1 -6
- package/src/datastar/attributes/on.ts +4 -13
- package/src/datastar/attributes/onIntersect.ts +10 -22
- package/src/datastar/attributes/onInterval.ts +2 -10
- package/src/datastar/attributes/onSignalPatch.ts +18 -28
- package/src/datastar/attributes/ref.ts +1 -2
- package/src/datastar/attributes/show.ts +1 -2
- package/src/datastar/attributes/signals.ts +1 -5
- package/src/datastar/attributes/style.ts +6 -12
- package/src/datastar/attributes/text.ts +1 -2
- package/src/datastar/engine.ts +102 -158
- package/src/datastar/index.ts +2 -2
- package/src/datastar/utils.ts +16 -51
- package/src/datastar/watchers/patchElements.ts +35 -93
- package/src/datastar/watchers/patchSignals.ts +1 -2
- package/src/experimental/EncryptedCookies.ts +81 -175
- package/src/experimental/index.ts +0 -1
- package/src/hyper/Hyper.ts +14 -33
- package/src/hyper/HyperHtml.ts +13 -10
- package/src/hyper/HyperNode.ts +2 -7
- package/src/hyper/HyperRoute.ts +2 -5
- package/src/hyper/jsx-runtime.ts +2 -10
- package/src/hyper/jsx.d.ts +171 -440
- package/src/lint/plugin.js +276 -0
- package/src/node/NodeFileSystem.ts +140 -202
- package/src/node/NodeUtils.ts +1 -3
- package/src/testing/TestLogger.ts +9 -22
- package/src/testing/index.ts +0 -1
- package/src/testing/utils.ts +30 -31
- package/src/x/cloudflare/CloudflareTunnel.ts +53 -65
- package/src/x/datastar/Datastar.ts +3 -10
- package/src/x/datastar/index.ts +1 -3
- package/src/x/datastar/jsx-datastar.d.ts +1 -4
- package/src/x/tailwind/TailwindPlugin.ts +119 -112
- package/src/x/tailwind/compile.ts +10 -33
- package/src/x/tailwind/plugin.ts +2 -2
- package/src/HttpAppExtra.ts +0 -478
- package/src/HttpUtils.ts +0 -17
- package/src/bun/BunPlatformHttpServer.ts +0 -88
- package/src/bun/BunServerRequest.ts +0 -396
- package/src/bundler/BundleHttp.ts +0 -259
- package/src/experimental/SseHttpResponse.ts +0 -55
- package/src/middlewares/BasicAuthMiddleware.ts +0 -36
- package/src/middlewares/index.ts +0 -1
- package/src/testing/TestHttpClient.ts +0 -148
package/src/FileRouterCodegen.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as FileSystem from "
|
|
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
|
|
65
|
-
|
|
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
|
-
|
|
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<
|
|
113
|
-
|
|
114
|
-
|
|
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(
|
|
216
|
-
|
|
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(
|
|
240
|
-
|
|
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(
|
|
253
|
-
|
|
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(
|
|
272
|
-
|
|
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(
|
|
292
|
-
|
|
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
|
+
}
|