effect-start 0.9.0 → 0.11.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 (54) hide show
  1. package/package.json +15 -14
  2. package/src/BundleHttp.test.ts +1 -1
  3. package/src/Commander.test.ts +15 -15
  4. package/src/Commander.ts +58 -88
  5. package/src/EncryptedCookies.test.ts +4 -4
  6. package/src/FileHttpRouter.test.ts +85 -16
  7. package/src/FileHttpRouter.ts +119 -32
  8. package/src/FileRouter.ts +62 -166
  9. package/src/FileRouterCodegen.test.ts +252 -66
  10. package/src/FileRouterCodegen.ts +13 -56
  11. package/src/FileRouterPattern.test.ts +116 -0
  12. package/src/FileRouterPattern.ts +59 -0
  13. package/src/FileRouter_path.test.ts +63 -102
  14. package/src/FileSystemExtra.test.ts +226 -0
  15. package/src/FileSystemExtra.ts +24 -60
  16. package/src/HttpAppExtra.test.ts +84 -0
  17. package/src/HttpAppExtra.ts +399 -47
  18. package/src/HttpUtils.test.ts +68 -0
  19. package/src/HttpUtils.ts +15 -0
  20. package/src/HyperHtml.ts +24 -5
  21. package/src/JsModule.test.ts +1 -1
  22. package/src/NodeFileSystem.ts +764 -0
  23. package/src/Random.ts +59 -0
  24. package/src/Route.test.ts +515 -18
  25. package/src/Route.ts +321 -166
  26. package/src/RouteRender.ts +40 -0
  27. package/src/Router.test.ts +416 -0
  28. package/src/Router.ts +288 -31
  29. package/src/RouterPattern.test.ts +655 -0
  30. package/src/RouterPattern.ts +416 -0
  31. package/src/Start.ts +14 -52
  32. package/src/TestHttpClient.test.ts +29 -0
  33. package/src/TestHttpClient.ts +122 -73
  34. package/src/assets.d.ts +39 -0
  35. package/src/bun/BunBundle.test.ts +0 -3
  36. package/src/bun/BunHttpServer.test.ts +74 -0
  37. package/src/bun/BunHttpServer.ts +259 -0
  38. package/src/bun/BunHttpServer_web.ts +384 -0
  39. package/src/bun/BunRoute.test.ts +514 -0
  40. package/src/bun/BunRoute.ts +427 -0
  41. package/src/bun/BunRoute_bundles.test.ts +218 -0
  42. package/src/bun/BunRuntime.ts +33 -0
  43. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  44. package/src/bun/_empty.html +1 -0
  45. package/src/bun/index.ts +2 -1
  46. package/src/index.ts +14 -14
  47. package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
  48. package/src/middlewares/BasicAuthMiddleware.ts +36 -0
  49. package/src/testing.ts +12 -3
  50. package/src/Datastar.test.ts +0 -267
  51. package/src/Datastar.ts +0 -68
  52. package/src/bun/BunFullstackServer.ts +0 -45
  53. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  54. package/src/jsx-datastar.d.ts +0 -63
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import * as HttpApp from "@effect/platform/HttpApp"
3
2
  import * as HttpClient from "@effect/platform/HttpClient"
4
3
  import * as HttpClientError from "@effect/platform/HttpClientError"
@@ -7,15 +6,38 @@ import * as HttpClientResponse from "@effect/platform/HttpClientResponse"
7
6
  import { RouteNotFound } from "@effect/platform/HttpServerError"
8
7
  import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
9
8
  import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
9
+ import * as UrlParams from "@effect/platform/UrlParams"
10
10
  import * as Effect from "effect/Effect"
11
+ import * as Either from "effect/Either"
11
12
  import * as Function from "effect/Function"
12
13
  import * as Scope from "effect/Scope"
13
14
  import * as Stream from "effect/Stream"
14
15
 
15
16
  const WebHeaders = globalThis.Headers
16
17
 
17
- export const make = <E = any, R = any>(
18
- httpApp: HttpApp.Default<E, R>,
18
+ export type FetchHandler = (req: Request) => Response | Promise<Response>
19
+
20
+ export const isFetchHandler = (
21
+ app: unknown,
22
+ ): app is FetchHandler => typeof app === "function" && !Effect.isEffect(app)
23
+
24
+ const fromFetchHandler = (
25
+ handler: FetchHandler,
26
+ ): HttpApp.Default<never, never> =>
27
+ Effect.gen(function*() {
28
+ const serverRequest = yield* HttpServerRequest.HttpServerRequest
29
+ const webRequest = serverRequest.source as Request
30
+ const response = yield* Effect.promise(async () => handler(webRequest))
31
+ const body = yield* Effect.promise(() => response.arrayBuffer())
32
+ return HttpServerResponse.raw(new Uint8Array(body), {
33
+ status: response.status,
34
+ statusText: response.statusText,
35
+ headers: Object.fromEntries(response.headers.entries()),
36
+ })
37
+ })
38
+
39
+ export const make = <E, R>(
40
+ appOrHandler: HttpApp.Default<E, R> | FetchHandler,
19
41
  opts?: {
20
42
  baseUrl?: string | null
21
43
  handleRouteNotFound?: (
@@ -24,77 +46,104 @@ export const make = <E = any, R = any>(
24
46
  },
25
47
  ): HttpClient.HttpClient.With<
26
48
  HttpClientError.HttpClientError | E,
27
- Exclude<
28
- Scope.Scope | R,
29
- HttpServerRequest.HttpServerRequest
30
- >
31
- > =>
32
- HttpClient
33
- .make(
34
- (request, url, signal) => {
35
- const send = (
36
- body: BodyInit | undefined,
37
- ) => {
38
- const app = httpApp
39
- const serverRequest = HttpServerRequest.fromWeb(
40
- new Request(url.toString(), {
41
- method: request.method,
42
- headers: new WebHeaders(request.headers),
43
- body,
44
- duplex: request.body._tag === "Stream" ? "half" : undefined,
45
- signal,
46
- } as any),
47
- )
49
+ Exclude<R, HttpServerRequest.HttpServerRequest>
50
+ > => {
51
+ const httpApp: HttpApp.Default<E, R> = isFetchHandler(appOrHandler)
52
+ ? fromFetchHandler(appOrHandler) as HttpApp.Default<E, R>
53
+ : appOrHandler
48
54
 
49
- return Function.pipe(
50
- app,
51
- Effect.provideService(
52
- HttpServerRequest.HttpServerRequest,
53
- serverRequest,
54
- ),
55
- Effect.andThen(HttpServerResponse.toWeb),
56
- Effect.andThen(res => HttpClientResponse.fromWeb(request, res)),
57
- opts?.handleRouteNotFound === null
58
- ? Function.identity
59
- : Effect.catchTag("RouteNotFound", e =>
60
- Effect
61
- .succeed(HttpClientResponse.fromWeb(
62
- e.request,
63
- new Response("Failed with RouteNotFound", {
64
- status: 404,
65
- }),
66
- ))),
67
- )
68
- }
55
+ const execute = (
56
+ request: HttpClientRequest.HttpClientRequest,
57
+ ): Effect.Effect<
58
+ HttpClientResponse.HttpClientResponse,
59
+ HttpClientError.HttpClientError | E,
60
+ Exclude<R, HttpServerRequest.HttpServerRequest>
61
+ > => {
62
+ const urlResult = UrlParams.makeUrl(
63
+ request.url,
64
+ request.urlParams,
65
+ request.hash,
66
+ )
67
+ if (Either.isLeft(urlResult)) {
68
+ return Effect.die(urlResult.left)
69
+ }
70
+ const url = urlResult.right
71
+ const controller = new AbortController()
72
+ const signal = controller.signal
69
73
 
70
- switch (
71
- request
72
- .body
73
- ._tag
74
- ) {
75
- case "Raw":
76
- case "Uint8Array":
77
- return send(
78
- request
79
- .body
80
- .body as any,
81
- )
82
- case "FormData":
83
- return send(request.body.formData)
84
- case "Stream":
85
- return Effect.flatMap(
86
- Stream.toReadableStreamEffect(request.body.stream),
87
- send,
88
- )
89
- }
74
+ const send = (
75
+ body: BodyInit | undefined,
76
+ ): Effect.Effect<
77
+ HttpClientResponse.HttpClientResponse,
78
+ E,
79
+ Exclude<R, HttpServerRequest.HttpServerRequest>
80
+ > => {
81
+ const serverRequest = HttpServerRequest.fromWeb(
82
+ new Request(url.toString(), {
83
+ method: request.method,
84
+ headers: new WebHeaders(request.headers),
85
+ body,
86
+ duplex: request.body._tag === "Stream" ? "half" : undefined,
87
+ signal,
88
+ } as RequestInit),
89
+ )
90
90
 
91
- return send(undefined)
92
- },
93
- )
94
- .pipe(
95
- opts?.baseUrl === null
96
- ? Function.identity
97
- : HttpClient.mapRequest(
98
- HttpClientRequest.prependUrl(opts?.baseUrl ?? "http://localhost"),
91
+ return Function.pipe(
92
+ httpApp,
93
+ Effect.provideService(
94
+ HttpServerRequest.HttpServerRequest,
95
+ serverRequest,
99
96
  ),
100
- )
97
+ Effect.andThen(HttpServerResponse.toWeb),
98
+ Effect.andThen(res => HttpClientResponse.fromWeb(request, res)),
99
+ opts?.handleRouteNotFound === null
100
+ ? Function.identity
101
+ : Effect.catchAll((e) =>
102
+ e instanceof RouteNotFound
103
+ ? Effect.succeed(HttpClientResponse.fromWeb(
104
+ request,
105
+ new Response("Failed with RouteNotFound", {
106
+ status: 404,
107
+ }),
108
+ ))
109
+ : Effect.fail(e)
110
+ ),
111
+ ) as Effect.Effect<
112
+ HttpClientResponse.HttpClientResponse,
113
+ E,
114
+ Exclude<R, HttpServerRequest.HttpServerRequest>
115
+ >
116
+ }
117
+
118
+ switch (request.body._tag) {
119
+ case "Raw":
120
+ case "Uint8Array":
121
+ return send(request.body.body as BodyInit)
122
+ case "FormData":
123
+ return send(request.body.formData)
124
+ case "Stream":
125
+ return Effect.flatMap(
126
+ Stream.toReadableStreamEffect(request.body.stream),
127
+ send,
128
+ )
129
+ }
130
+
131
+ return send(undefined)
132
+ }
133
+
134
+ const client = HttpClient.makeWith(
135
+ (requestEffect) => Effect.flatMap(requestEffect, execute),
136
+ (request) => Effect.succeed(request),
137
+ )
138
+
139
+ return client.pipe(
140
+ opts?.baseUrl === null
141
+ ? Function.identity
142
+ : HttpClient.mapRequest(
143
+ HttpClientRequest.prependUrl(opts?.baseUrl ?? "http://localhost"),
144
+ ),
145
+ ) as HttpClient.HttpClient.With<
146
+ HttpClientError.HttpClientError | E,
147
+ Exclude<R, HttpServerRequest.HttpServerRequest>
148
+ >
149
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Allow importing static assets as modules.
3
+ * Bun/Vite resolves these to file paths.
4
+ * For WASM you're better of
5
+ */
6
+
7
+ declare module "*.png"
8
+ declare module "*.gif"
9
+ declare module "*.webp"
10
+ declare module "*.avif"
11
+ declare module "*.ico"
12
+ declare module "*.bmp"
13
+ declare module "*.tiff"
14
+ declare module "*.svg"
15
+ declare module "*.jpeg"
16
+ declare module "*.jpg"
17
+
18
+ declare module "*.woff"
19
+ declare module "*.woff2"
20
+ declare module "*.ttf"
21
+ declare module "*.otf"
22
+ declare module "*.eot"
23
+
24
+ declare module "*.mp4"
25
+ declare module "*.webm"
26
+ declare module "*.ogg"
27
+ declare module "*.mov"
28
+ declare module "*.mp3"
29
+ declare module "*.wav"
30
+ declare module "*.flac"
31
+ declare module "*.aac"
32
+
33
+ declare module "*.pdf"
34
+ declare module "*.xml"
35
+ declare module "*.csv"
36
+ declare module "*.txt"
37
+ declare module "*.md"
38
+
39
+ declare module "*.css"
@@ -1,7 +1,4 @@
1
- import * as BunContext from "@effect/platform-bun/BunContext"
2
- import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
3
1
  import * as HttpRouter from "@effect/platform/HttpRouter"
4
- import * as HttpServer from "@effect/platform/HttpServer"
5
2
  import * as t from "bun:test"
6
3
  import * as Effect from "effect/Effect"
7
4
  import * as Layer from "effect/Layer"
@@ -0,0 +1,74 @@
1
+ import * as t from "bun:test"
2
+ import * as Effect from "effect/Effect"
3
+ import * as BunHttpServer from "./BunHttpServer.ts"
4
+
5
+ t.describe("BunHttpServer smart port selection", () => {
6
+ // Skip when running in TTY because the random port logic requires !isTTY && CLAUDECODE,
7
+ // and process.stdout.isTTY cannot be mocked
8
+ t.test.skipIf(process.stdout.isTTY)(
9
+ "uses random port when PORT not set, isTTY=false, CLAUDECODE set",
10
+ async () => {
11
+ const originalPort = process.env.PORT
12
+ const originalClaudeCode = process.env.CLAUDECODE
13
+
14
+ try {
15
+ delete process.env.PORT
16
+ process.env.CLAUDECODE = "1"
17
+
18
+ const port = await Effect.runPromise(
19
+ Effect.scoped(
20
+ Effect.gen(function*() {
21
+ const bunServer = yield* BunHttpServer.make({})
22
+ return bunServer.server.port
23
+ }),
24
+ ),
25
+ )
26
+
27
+ t.expect(port).not.toBe(3000)
28
+ } finally {
29
+ if (originalPort !== undefined) {
30
+ process.env.PORT = originalPort
31
+ } else {
32
+ delete process.env.PORT
33
+ }
34
+ if (originalClaudeCode !== undefined) {
35
+ process.env.CLAUDECODE = originalClaudeCode
36
+ } else {
37
+ delete process.env.CLAUDECODE
38
+ }
39
+ }
40
+ },
41
+ )
42
+
43
+ t.test("uses explicit PORT even when CLAUDECODE is set", async () => {
44
+ const originalPort = process.env.PORT
45
+ const originalClaudeCode = process.env.CLAUDECODE
46
+
47
+ try {
48
+ process.env.PORT = "5678"
49
+ process.env.CLAUDECODE = "1"
50
+
51
+ const port = await Effect.runPromise(
52
+ Effect.scoped(
53
+ Effect.gen(function*() {
54
+ const bunServer = yield* BunHttpServer.make({})
55
+ return bunServer.server.port
56
+ }),
57
+ ),
58
+ )
59
+
60
+ t.expect(port).toBe(5678)
61
+ } finally {
62
+ if (originalPort !== undefined) {
63
+ process.env.PORT = originalPort
64
+ } else {
65
+ delete process.env.PORT
66
+ }
67
+ if (originalClaudeCode !== undefined) {
68
+ process.env.CLAUDECODE = originalClaudeCode
69
+ } else {
70
+ delete process.env.CLAUDECODE
71
+ }
72
+ }
73
+ })
74
+ })
@@ -0,0 +1,259 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as HttpServer from "@effect/platform/HttpServer"
3
+ import * as HttpServerError from "@effect/platform/HttpServerError"
4
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
5
+ import * as Socket from "@effect/platform/Socket"
6
+ import * as Bun from "bun"
7
+ import * as Config from "effect/Config"
8
+ import * as Context from "effect/Context"
9
+ import * as Deferred from "effect/Deferred"
10
+ import * as Effect from "effect/Effect"
11
+ import * as Exit from "effect/Exit"
12
+ import * as FiberSet from "effect/FiberSet"
13
+ import * as Layer from "effect/Layer"
14
+ import * as Option from "effect/Option"
15
+ import type * as Scope from "effect/Scope"
16
+ import * as Random from "../Random.ts"
17
+ import * as Router from "../Router.ts"
18
+ import EmptyHTML from "./_empty.html"
19
+ import {
20
+ makeResponse,
21
+ ServerRequestImpl,
22
+ WebSocketContext,
23
+ } from "./BunHttpServer_web.ts"
24
+ import * as BunRoute from "./BunRoute.ts"
25
+
26
+ type FetchHandler = (
27
+ request: Request,
28
+ server: Bun.Server<WebSocketContext>,
29
+ ) => Response | Promise<Response>
30
+
31
+ /**
32
+ * Basically `Omit<Bun.Serve.Options, "fetch" | "error" | "websocket">`
33
+ * TypeScript 5.9 cannot verify discriminated union types used in
34
+ * {@link Bun.serve} so we need to define them explicitly.
35
+ */
36
+ interface ServeOptions {
37
+ readonly port?: number
38
+ readonly hostname?: string
39
+ readonly reusePort?: boolean
40
+ readonly ipv6Only?: boolean
41
+ readonly idleTimeout?: number
42
+ readonly development?: boolean
43
+ }
44
+
45
+ export type BunServer = {
46
+ readonly server: Bun.Server<WebSocketContext>
47
+ readonly addRoutes: (routes: BunRoute.BunRoutes) => void
48
+ // TODO: we probably don't want to expose these methods publicly
49
+ readonly pushHandler: (fetch: FetchHandler) => void
50
+ readonly popHandler: () => void
51
+ }
52
+
53
+ export const BunServer = Context.GenericTag<BunServer>(
54
+ "effect-start/BunServer",
55
+ )
56
+
57
+ export const make = (
58
+ options: ServeOptions,
59
+ ): Effect.Effect<
60
+ BunServer,
61
+ never,
62
+ Scope.Scope
63
+ > =>
64
+ Effect.gen(function*() {
65
+ const port = yield* Config.number("PORT").pipe(
66
+ Effect.catchTag("ConfigError", () => {
67
+ if (
68
+ typeof process !== "undefined"
69
+ && !process.stdout.isTTY
70
+ && process.env.CLAUDECODE
71
+ ) {
72
+ return Effect.succeed(0)
73
+ }
74
+
75
+ return Effect.succeed(3000)
76
+ }),
77
+ )
78
+ const hostname = yield* Config.string("HOSTNAME").pipe(
79
+ Effect.catchTag("ConfigError", () => Effect.succeed(undefined)),
80
+ )
81
+
82
+ const handlerStack: Array<FetchHandler> = [
83
+ function(_request, _server) {
84
+ return new Response("not found", { status: 404 })
85
+ },
86
+ ]
87
+
88
+ let currentRoutes: BunRoute.BunRoutes = {}
89
+
90
+ // Bun HMR doesn't work on successive calls to `server.reload` if there are no routes
91
+ // on server start. We workaround that by passing a dummy HTMLBundle [2025-11-26]
92
+ // see: https://github.com/oven-sh/bun/issues/23564
93
+ currentRoutes[`/.BunEmptyHtml-${Random.token(6)}`] = EmptyHTML
94
+
95
+ const websocket: Bun.WebSocketHandler<WebSocketContext> = {
96
+ open(ws) {
97
+ Deferred.unsafeDone(ws.data.deferred, Exit.succeed(ws))
98
+ },
99
+ message(ws, message) {
100
+ ws.data.run(message)
101
+ },
102
+ close(ws, code, closeReason) {
103
+ Deferred.unsafeDone(
104
+ ws.data.closeDeferred,
105
+ Socket.defaultCloseCodeIsError(code)
106
+ ? Exit.fail(
107
+ new Socket.SocketCloseError({
108
+ reason: "Close",
109
+ code,
110
+ closeReason,
111
+ }),
112
+ )
113
+ : Exit.void,
114
+ )
115
+ },
116
+ }
117
+
118
+ const server = Bun.serve({
119
+ port,
120
+ hostname,
121
+ ...options,
122
+ routes: currentRoutes,
123
+ fetch: handlerStack[0],
124
+ websocket,
125
+ })
126
+
127
+ yield* Effect.addFinalizer(() =>
128
+ Effect.sync(() => {
129
+ server.stop()
130
+ })
131
+ )
132
+
133
+ const reload = () => {
134
+ server.reload({
135
+ fetch: handlerStack[handlerStack.length - 1],
136
+ routes: currentRoutes,
137
+ websocket,
138
+ })
139
+ }
140
+
141
+ return BunServer.of({
142
+ server,
143
+ pushHandler(fetch) {
144
+ handlerStack.push(fetch)
145
+ reload()
146
+ },
147
+ popHandler() {
148
+ handlerStack.pop()
149
+ reload()
150
+ },
151
+ addRoutes(routes) {
152
+ currentRoutes = {
153
+ ...currentRoutes,
154
+ ...routes,
155
+ }
156
+ reload()
157
+ },
158
+ })
159
+ })
160
+
161
+ export const layer = (
162
+ options?: ServeOptions,
163
+ ): Layer.Layer<BunServer> => Layer.scoped(BunServer, make(options ?? {}))
164
+
165
+ export const makeHttpServer: Effect.Effect<
166
+ HttpServer.HttpServer,
167
+ never,
168
+ Scope.Scope | BunServer
169
+ > = Effect.gen(function*() {
170
+ const bunServer = yield* BunServer
171
+
172
+ return HttpServer.make({
173
+ address: {
174
+ _tag: "TcpAddress",
175
+ port: bunServer.server.port!,
176
+ hostname: bunServer.server.hostname!,
177
+ },
178
+ serve(httpApp, middleware) {
179
+ return Effect.gen(function*() {
180
+ const runFork = yield* FiberSet.makeRuntime<never>()
181
+ const runtime = yield* Effect.runtime<never>()
182
+ const app = HttpApp.toHandled(
183
+ httpApp,
184
+ (request, response) =>
185
+ Effect.sync(() => {
186
+ ;(request as ServerRequestImpl).resolve(
187
+ makeResponse(request, response, runtime),
188
+ )
189
+ }),
190
+ middleware,
191
+ )
192
+
193
+ function handler(
194
+ request: Request,
195
+ server: Bun.Server<WebSocketContext>,
196
+ ) {
197
+ return new Promise<Response>((resolve, _reject) => {
198
+ const fiber = runFork(Effect.provideService(
199
+ app,
200
+ HttpServerRequest.HttpServerRequest,
201
+ new ServerRequestImpl(
202
+ request,
203
+ resolve,
204
+ removeHost(request.url),
205
+ server,
206
+ ),
207
+ ))
208
+ request.signal.addEventListener("abort", () => {
209
+ runFork(
210
+ fiber.interruptAsFork(HttpServerError.clientAbortFiberId),
211
+ )
212
+ }, { once: true })
213
+ })
214
+ }
215
+
216
+ yield* Effect.acquireRelease(
217
+ Effect.sync(() => {
218
+ bunServer.pushHandler(handler)
219
+ }),
220
+ () =>
221
+ Effect.sync(() => {
222
+ bunServer.popHandler()
223
+ }),
224
+ )
225
+ })
226
+ },
227
+ })
228
+ })
229
+
230
+ export const layerServer = (
231
+ options?: ServeOptions,
232
+ ): Layer.Layer<HttpServer.HttpServer | BunServer> =>
233
+ Layer.provideMerge(
234
+ Layer.scoped(HttpServer.HttpServer, makeHttpServer),
235
+ layer(options ?? {}),
236
+ )
237
+
238
+ export function layerRoutes() {
239
+ return Layer.effectDiscard(
240
+ Effect.gen(function*() {
241
+ const bunServer = yield* BunServer
242
+ const routerContext = yield* Effect.serviceOption(Router.Router)
243
+
244
+ if (Option.isSome(routerContext)) {
245
+ const router = yield* Router.fromManifest(routerContext.value)
246
+ const bunRoutes = yield* BunRoute.routesFromRouter(router)
247
+ bunServer.addRoutes(bunRoutes)
248
+ }
249
+ }),
250
+ )
251
+ }
252
+
253
+ const removeHost = (url: string) => {
254
+ if (url[0] === "/") {
255
+ return url
256
+ }
257
+ const index = url.indexOf("/", url.indexOf("//") + 2)
258
+ return index === -1 ? "/" : url.slice(index)
259
+ }