effect-start 0.10.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.
package/src/Router.ts CHANGED
@@ -1,10 +1,23 @@
1
1
  import * as Context from "effect/Context"
2
+ import * as Data from "effect/Data"
2
3
  import * as Effect from "effect/Effect"
3
4
  import * as Function from "effect/Function"
4
5
  import * as Layer from "effect/Layer"
6
+ import * as Pipeable from "effect/Pipeable"
7
+ import * as Predicate from "effect/Predicate"
5
8
  import * as FileRouter from "./FileRouter.ts"
6
9
  import * as Route from "./Route"
7
10
 
11
+ export type RouterErrorReason =
12
+ | "UnsupportedPattern"
13
+ | "ProxyError"
14
+
15
+ export class RouterError extends Data.TaggedError("RouterError")<{
16
+ reason: RouterErrorReason
17
+ pattern: string
18
+ message: string
19
+ }> {}
20
+
8
21
  export type ServerModule = {
9
22
  default: Route.RouteSet.Default
10
23
  }
@@ -56,3 +69,269 @@ export function layerPromise(
56
69
  }),
57
70
  )
58
71
  }
72
+
73
+ const RouterBuilderTypeId: unique symbol = Symbol.for(
74
+ "effect-start/RouterBuilder",
75
+ )
76
+
77
+ type RouterModule = typeof import("./Router.ts")
78
+
79
+ type Self =
80
+ | RouterBuilder<any, any>
81
+ | RouterModule
82
+ | undefined
83
+
84
+ export type RouterEntry = {
85
+ path: `/${string}`
86
+ route: Route.RouteSet.Default
87
+ layers: Route.RouteLayer[]
88
+ }
89
+
90
+ type RouterBuilderMethods = {
91
+ use: typeof use
92
+ mount: typeof mount
93
+ }
94
+
95
+ export interface RouterBuilder<
96
+ out E = never,
97
+ out R = never,
98
+ > extends Pipeable.Pipeable, RouterBuilderMethods {
99
+ [RouterBuilderTypeId]: typeof RouterBuilderTypeId
100
+ readonly entries: readonly RouterEntry[]
101
+ readonly globalLayers: readonly Route.RouteLayer[]
102
+ readonly mounts: Record<`/${string}`, Route.RouteSet.Default>
103
+ readonly _E: () => E
104
+ readonly _R: () => R
105
+ }
106
+
107
+ export namespace RouterBuilder {
108
+ export type Any = RouterBuilder<any, any>
109
+
110
+ export type Error<T> = T extends RouterBuilder<infer E, any> ? E : never
111
+ export type Context<T> = T extends RouterBuilder<any, infer R> ? R : never
112
+ }
113
+
114
+ const RouterBuilderProto: RouterBuilderMethods & {
115
+ [RouterBuilderTypeId]: typeof RouterBuilderTypeId
116
+ pipe: Pipeable.Pipeable["pipe"]
117
+ } = {
118
+ [RouterBuilderTypeId]: RouterBuilderTypeId,
119
+
120
+ pipe() {
121
+ return Pipeable.pipeArguments(this, arguments)
122
+ },
123
+
124
+ use,
125
+ mount,
126
+ }
127
+
128
+ type ExtractRouteSetError<T> = T extends Route.RouteSet<infer Routes, any>
129
+ ? Routes[number] extends Route.Route<any, any, infer H, any>
130
+ ? H extends Route.RouteHandler<any, infer E, any> ? E : never
131
+ : never
132
+ : never
133
+
134
+ type ExtractRouteSetContext<T> = T extends Route.RouteSet<infer Routes, any>
135
+ ? Routes[number] extends Route.Route<any, any, infer H, any>
136
+ ? H extends Route.RouteHandler<any, any, infer R> ? R : never
137
+ : never
138
+ : never
139
+
140
+ function addRoute<
141
+ E,
142
+ R,
143
+ RouteE,
144
+ RouteR,
145
+ >(
146
+ builder: RouterBuilder<E, R>,
147
+ path: `/${string}`,
148
+ route: Route.RouteSet.Default,
149
+ ): RouterBuilder<E | RouteE, R | RouteR> {
150
+ const existingEntry = builder.entries.find((e) => e.path === path)
151
+ if (existingEntry) {
152
+ const updatedEntry: RouterEntry = {
153
+ ...existingEntry,
154
+ route: Route.merge(existingEntry.route, route),
155
+ }
156
+ return makeBuilder(
157
+ builder.entries.map((e) => (e.path === path ? updatedEntry : e)),
158
+ builder.globalLayers,
159
+ )
160
+ }
161
+
162
+ const newEntry: RouterEntry = {
163
+ path,
164
+ route,
165
+ layers: [...builder.globalLayers],
166
+ }
167
+
168
+ return makeBuilder([...builder.entries, newEntry], builder.globalLayers)
169
+ }
170
+
171
+ function addGlobalLayer<E, R>(
172
+ builder: RouterBuilder<E, R>,
173
+ layerRoute: Route.RouteLayer,
174
+ ): RouterBuilder<E, R> {
175
+ const newGlobalLayers = [...builder.globalLayers, layerRoute]
176
+ return makeBuilder(builder.entries, newGlobalLayers)
177
+ }
178
+
179
+
180
+ function findMatchingLayerRoutes(
181
+ route: Route.Route.Default,
182
+ layers: readonly Route.RouteLayer[],
183
+ ): Route.Route.Default[] {
184
+ const matchingRoutes: Route.Route.Default[] = []
185
+ for (const layer of layers) {
186
+ for (const layerRoute of layer.set) {
187
+ if (Route.matches(layerRoute, route)) {
188
+ matchingRoutes.push(layerRoute)
189
+ }
190
+ }
191
+ }
192
+ return matchingRoutes
193
+ }
194
+
195
+ function wrapWithLayerRoute(
196
+ innerRoute: Route.Route.Default,
197
+ layerRoute: Route.Route.Default,
198
+ ): Route.Route.Default {
199
+ const handler: Route.RouteHandler = (context) => {
200
+ const contextWithNext: Route.RouteContext = {
201
+ ...context,
202
+ next: () => innerRoute.handler(context),
203
+ }
204
+ return layerRoute.handler(contextWithNext)
205
+ }
206
+
207
+ return Route.make({
208
+ method: layerRoute.method,
209
+ media: layerRoute.media,
210
+ handler,
211
+ schemas: {},
212
+ })
213
+ }
214
+
215
+ function applyLayersToRoute(
216
+ route: Route.Route.Default,
217
+ layers: readonly Route.RouteLayer[],
218
+ ): Route.Route.Default {
219
+ const matchingLayerRoutes = findMatchingLayerRoutes(route, layers)
220
+ let wrappedRoute = route
221
+
222
+ for (const layerRoute of matchingLayerRoutes.reverse()) {
223
+ wrappedRoute = wrapWithLayerRoute(wrappedRoute, layerRoute)
224
+ }
225
+
226
+ return wrappedRoute
227
+ }
228
+
229
+ function applyLayersToRouteSet(
230
+ routeSet: Route.RouteSet.Default,
231
+ layers: readonly Route.RouteLayer[],
232
+ ): Route.RouteSet.Default {
233
+ if (layers.length === 0) {
234
+ return routeSet
235
+ }
236
+
237
+ const wrappedRoutes = routeSet.set.map((route) =>
238
+ applyLayersToRoute(route, layers)
239
+ )
240
+
241
+ return {
242
+ set: wrappedRoutes,
243
+ schema: routeSet.schema,
244
+ } as unknown as Route.RouteSet.Default
245
+ }
246
+
247
+ function makeBuilder<E, R>(
248
+ entries: readonly RouterEntry[],
249
+ globalLayers: readonly Route.RouteLayer[] = [],
250
+ ): RouterBuilder<E, R> {
251
+ const mounts: Record<`/${string}`, Route.RouteSet.Default> = {}
252
+
253
+ for (const entry of entries) {
254
+ if (entry.route.set.length > 0) {
255
+ mounts[entry.path] = applyLayersToRouteSet(entry.route, entry.layers)
256
+ }
257
+ }
258
+
259
+ return Object.assign(Object.create(RouterBuilderProto), {
260
+ entries,
261
+ globalLayers,
262
+ mounts,
263
+ })
264
+ }
265
+
266
+ export function isRouterBuilder(input: unknown): input is RouterBuilder.Any {
267
+ return Predicate.hasProperty(input, RouterBuilderTypeId)
268
+ }
269
+
270
+ export function use<
271
+ S extends Self,
272
+ >(
273
+ this: S,
274
+ layerRoute: Route.RouteLayer,
275
+ ): S extends RouterBuilder<infer E, infer R> ? RouterBuilder<E, R>
276
+ : RouterBuilder<never, never>
277
+ {
278
+ const builder = isRouterBuilder(this)
279
+ ? this
280
+ : makeBuilder<never, never>([], [])
281
+ return addGlobalLayer(builder, layerRoute) as any
282
+ }
283
+
284
+ export function mount<
285
+ S extends Self,
286
+ Routes extends Route.Route.Tuple,
287
+ Schemas extends Route.RouteSchemas,
288
+ >(
289
+ this: S,
290
+ path: `/${string}`,
291
+ route: Route.RouteSet<Routes, Schemas>,
292
+ ): S extends RouterBuilder<infer E, infer R> ? RouterBuilder<
293
+ E | ExtractRouteSetError<Route.RouteSet<Routes, Schemas>>,
294
+ R | ExtractRouteSetContext<Route.RouteSet<Routes, Schemas>>
295
+ >
296
+ : RouterBuilder<
297
+ ExtractRouteSetError<Route.RouteSet<Routes, Schemas>>,
298
+ ExtractRouteSetContext<Route.RouteSet<Routes, Schemas>>
299
+ >
300
+ {
301
+ const builder = isRouterBuilder(this)
302
+ ? this
303
+ : makeBuilder<never, never>([], [])
304
+ return addRoute(builder, path, route as Route.RouteSet.Default) as any
305
+ }
306
+
307
+ export function fromManifest(
308
+ manifest: RouterManifest,
309
+ ): Effect.Effect<RouterBuilder.Any> {
310
+ return Effect.gen(function*() {
311
+ const loadedEntries = yield* Effect.forEach(
312
+ manifest.routes,
313
+ (lazyRoute) =>
314
+ Effect.gen(function*() {
315
+ const routeModule = yield* Effect.promise(() => lazyRoute.load())
316
+ const layerModules = lazyRoute.layers
317
+ ? yield* Effect.forEach(
318
+ lazyRoute.layers,
319
+ (loadLayer) => Effect.promise(() => loadLayer()),
320
+ )
321
+ : []
322
+
323
+ const layers = layerModules
324
+ .map((m: any) => m.default)
325
+ .filter(Route.isRouteLayer)
326
+
327
+ return {
328
+ path: lazyRoute.path,
329
+ route: routeModule.default,
330
+ layers,
331
+ }
332
+ }),
333
+ )
334
+
335
+ return makeBuilder(loadedEntries, [])
336
+ })
337
+ }
@@ -213,10 +213,36 @@ t.describe(`${RouterPattern.toColon.name}`, () => {
213
213
  "/files/file_:id.txt",
214
214
  ])
215
215
  })
216
+ })
217
+
218
+ t.describe(`${RouterPattern.toBun.name}`, () => {
219
+ t.test("literal path unchanged", () => {
220
+ t.expect(RouterPattern.toBun("/")).toEqual(["/"])
221
+ t.expect(RouterPattern.toBun("/about")).toEqual(["/about"])
222
+ })
223
+
224
+ t.test("param [param] -> :param", () => {
225
+ t.expect(RouterPattern.toBun("/users/[id]")).toEqual(["/users/:id"])
226
+ })
227
+
228
+ t.test("optional param [[param]] -> two routes", () => {
229
+ t.expect(RouterPattern.toBun("/users/[[id]]")).toEqual([
230
+ "/users",
231
+ "/users/:id",
232
+ ])
233
+ t.expect(RouterPattern.toBun("/[[id]]")).toEqual(["/", "/:id"])
234
+ })
235
+
236
+ t.test("rest param [...param] -> *", () => {
237
+ t.expect(RouterPattern.toBun("/docs/[...path]")).toEqual(["/docs/*"])
238
+ })
216
239
 
217
- t.test("toHono and toBun are aliases", () => {
218
- t.expect(RouterPattern.toHono).toBe(RouterPattern.toColon)
219
- t.expect(RouterPattern.toBun).toBe(RouterPattern.toColon)
240
+ t.test("optional rest param [[...param]] -> two routes", () => {
241
+ t.expect(RouterPattern.toBun("/docs/[[...path]]")).toEqual([
242
+ "/docs",
243
+ "/docs/*",
244
+ ])
245
+ t.expect(RouterPattern.toBun("/[[...path]]")).toEqual(["/", "/*"])
220
246
  })
221
247
  })
222
248
 
@@ -365,13 +365,38 @@ export function toRemix(path: Route.RoutePattern): string[] {
365
365
  return [joined ? "/" + joined : "/"]
366
366
  }
367
367
 
368
- export const toBun = toColon
369
-
370
368
  /**
371
- * @deprecated Use toEffectHttpRouterPath instead
369
+ * Converts to Bun.serve path pattern.
370
+ *
371
+ * Since Bun doesn't support optional params (`:param?`), optional segments
372
+ * are expanded into multiple routes recursively.
373
+ *
374
+ * - `[param]` → `:param`
375
+ * - `[[param]]` → `/`, `/:param` (two routes)
376
+ * - `[...param]` → `*`
377
+ * - `[[...param]]` → `/`, `/*` (two routes)
378
+ * - `pk_[id]` → `pk_:id`
372
379
  */
373
- export function toHttpPath(path: Route.RoutePattern): string {
374
- return toEffect(path)[0]
380
+ export function toBun(path: Route.RoutePattern): string[] {
381
+ const segments = parse(path)
382
+
383
+ const optionalIndex = segments.findIndex(
384
+ (s) =>
385
+ (s._tag === "ParamSegment" || s._tag === "RestSegment") && s.optional,
386
+ )
387
+
388
+ if (optionalIndex === -1) {
389
+ return buildPaths(segments, colonParamSegment, "/*")
390
+ }
391
+
392
+ const before = segments.slice(0, optionalIndex)
393
+ const optional = { ...segments[optionalIndex], optional: false }
394
+ const after = segments.slice(optionalIndex + 1)
395
+
396
+ return [
397
+ ...toBun(format(before)),
398
+ ...toBun(format([...before, optional, ...after])),
399
+ ]
375
400
  }
376
401
 
377
402
  type ExtractSegment<S extends string> = S extends `[[...${infer Name}]]`
@@ -52,3 +52,32 @@ t.it("not found", () =>
52
52
  )
53
53
  .toEqual("Not Found")
54
54
  }))
55
+
56
+ t.describe("FetchHandler", () => {
57
+ const FetchClient = TestHttpClient.make((req) =>
58
+ new Response(`Hello from ${req.url}`, { status: 200 })
59
+ )
60
+
61
+ t.it("works with sync handler", () =>
62
+ effect(function*() {
63
+ const res = yield* FetchClient.get("/test")
64
+
65
+ t.expect(res.status).toEqual(200)
66
+ t.expect(yield* res.text).toContain("/test")
67
+ }))
68
+
69
+ const AsyncFetchClient = TestHttpClient.make(async (req) => {
70
+ await Promise.resolve()
71
+ return new Response(`Async: ${req.method} ${new URL(req.url).pathname}`, {
72
+ status: 201,
73
+ })
74
+ })
75
+
76
+ t.it("works with async handler", () =>
77
+ effect(function*() {
78
+ const res = yield* AsyncFetchClient.post("/async-path")
79
+
80
+ t.expect(res.status).toEqual(201)
81
+ t.expect(yield* res.text).toEqual("Async: POST /async-path")
82
+ }))
83
+ })
@@ -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"