@dotpress/testing 2.0.0-alpha.3 → 2.0.0-alpha.5

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/dist/index.js CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ closeOf,
3
+ createFetchTarget,
4
+ createMemoryLogger,
5
+ toRequest
6
+ } from "./chunk-MUWNILMA.js";
7
+
1
8
  // src/assertions.ts
2
9
  import { expect } from "vitest";
3
10
  var ExpectationError = class extends Error {
@@ -16,17 +23,17 @@ var matches = (actual, expected) => {
16
23
  }
17
24
  };
18
25
  var show = (value) => {
19
- let json2;
26
+ let json;
20
27
  try {
21
- json2 = JSON.stringify(
28
+ json = JSON.stringify(
22
29
  value,
23
30
  (_key, item) => typeof item === "object" && item !== null && "asymmetricMatch" in item ? item.toAsymmetricMatcher?.() ?? String(item) : item
24
31
  );
25
32
  } catch {
26
- json2 = void 0;
33
+ json = void 0;
27
34
  }
28
- json2 ??= String(value);
29
- return json2.length > 300 ? `${json2.slice(0, 297)}...` : json2;
35
+ json ??= String(value);
36
+ return json.length > 300 ? `${json.slice(0, 297)}...` : json;
30
37
  };
31
38
  var checkTimes = (label, times) => {
32
39
  if (times !== void 0 && (!Number.isInteger(times) || times < 0)) {
@@ -42,1291 +49,6 @@ ${recorded}`
42
49
  );
43
50
  };
44
51
 
45
- // src/conformance.ts
46
- import {
47
- DEFAULT_BODY_LIMIT,
48
- createRouter,
49
- file,
50
- json,
51
- notFoundError,
52
- redirect,
53
- sse,
54
- stream,
55
- text,
56
- unauthorizedError
57
- } from "@dotpress/core";
58
- import { describe, expect as expect2, it, vi } from "vitest";
59
-
60
- // src/logger.ts
61
- var createMemoryLogger = () => {
62
- const entries = [];
63
- const create = () => ({
64
- debug: (...args) => void entries.push({ level: "debug", args }),
65
- info: (...args) => void entries.push({ level: "info", args }),
66
- warn: (...args) => void entries.push({ level: "warn", args }),
67
- error: (...args) => void entries.push({ level: "error", args }),
68
- child: () => create()
69
- });
70
- return Object.assign(create(), { entries });
71
- };
72
-
73
- // src/request.ts
74
- var toRequest = async (path, init) => {
75
- const request = new Request(new URL(path, "http://localhost"), init);
76
- if (!(init?.body instanceof FormData)) return request;
77
- return new Request(request, { body: await request.arrayBuffer() });
78
- };
79
-
80
- // src/conformance.ts
81
- var createHttpTarget = (baseUrl, close) => ({
82
- fetch: (path, init) => fetch(new URL(path, baseUrl), init),
83
- close
84
- });
85
- var createFetchTarget = (handle, close) => ({
86
- fetch: async (path, init) => handle(await toRequest(path, init)),
87
- close
88
- });
89
- var TIMEOUT = 2e3;
90
- var within = (promise, label) => Promise.race([
91
- promise,
92
- new Promise(
93
- (_, reject) => setTimeout(
94
- () => reject(new Error(`Timed out waiting for: ${label}`)),
95
- TIMEOUT
96
- )
97
- )
98
- ]);
99
- var deferred = () => {
100
- let resolve;
101
- const promise = new Promise((done) => {
102
- resolve = done;
103
- });
104
- return { promise, resolve };
105
- };
106
- var waitForAbort = (signal) => new Promise((resolve) => {
107
- if (signal.aborted) resolve();
108
- signal.addEventListener("abort", () => resolve(), { once: true });
109
- });
110
- var readChunk = async (reader) => {
111
- const { done, value } = await reader.read();
112
- return done ? void 0 : new TextDecoder().decode(value);
113
- };
114
- var runConformanceSuite = (options) => {
115
- const serve = async (define, appOptions = {}) => {
116
- const router = createRouter();
117
- define(router);
118
- return options.setup({
119
- logger: createMemoryLogger(),
120
- ...appOptions,
121
- routers: [router]
122
- });
123
- };
124
- const withApp = async (define, run, appOptions) => {
125
- const target = await serve(define, appOptions);
126
- try {
127
- await run(target);
128
- } finally {
129
- await target.close();
130
- }
131
- };
132
- const postJson = (body) => ({
133
- method: "POST",
134
- headers: { "content-type": "application/json" },
135
- body: JSON.stringify(body)
136
- });
137
- describe(`${options.name} adapter conformance`, () => {
138
- describe("routing", () => {
139
- it("serves JSON results", () => withApp(
140
- (router) => {
141
- router.defineRoute({ path: "/hello", handler: () => ({ hi: 1 }) });
142
- },
143
- async ({ fetch: fetch2 }) => {
144
- const res = await fetch2("/hello");
145
- expect2(res.status).toBe(200);
146
- expect2(res.headers.get("content-type")).toBe(
147
- "application/json; charset=utf-8"
148
- );
149
- expect2(await res.json()).toEqual({ hi: 1 });
150
- }
151
- ));
152
- it("routes every HTTP method", () => withApp(
153
- (router) => {
154
- for (const method of [
155
- "get",
156
- "post",
157
- "put",
158
- "patch",
159
- "delete",
160
- "options"
161
- ]) {
162
- router.defineRoute({
163
- method,
164
- path: "/resource",
165
- handler: ({ req }) => ({ method: req.method })
166
- });
167
- }
168
- router.defineRoute({
169
- method: "head",
170
- path: "/resource",
171
- handler: ({ res }) => {
172
- res.setHeader("x-head", "yes");
173
- }
174
- });
175
- },
176
- async ({ fetch: fetch2 }) => {
177
- for (const method of [
178
- "GET",
179
- "POST",
180
- "PUT",
181
- "PATCH",
182
- "DELETE",
183
- "OPTIONS"
184
- ]) {
185
- const res = await fetch2("/resource", { method });
186
- expect2(await res.json()).toEqual({ method });
187
- }
188
- const head = await fetch2("/resource", { method: "HEAD" });
189
- expect2(head.headers.get("x-head")).toBe("yes");
190
- }
191
- ));
192
- it("extracts decoded path parameters", () => withApp(
193
- (router) => {
194
- router.defineRoute({
195
- path: "/orgs/:orgId/files/:name",
196
- handler: ({ req }) => req.params
197
- });
198
- },
199
- async ({ fetch: fetch2 }) => {
200
- const res = await fetch2("/orgs/acme/files/a%20b%2Fc%C3%A9");
201
- expect2(await res.json()).toEqual({ orgId: "acme", name: "a b/c\xE9" });
202
- }
203
- ));
204
- it("gives static segments priority over parameters", () => withApp(
205
- (router) => {
206
- router.defineRoute({
207
- path: "/users/:id",
208
- handler: ({ req }) => ({ id: req.params.id })
209
- });
210
- router.defineRoute({ path: "/users/me", handler: () => "me" });
211
- },
212
- async ({ fetch: fetch2 }) => {
213
- expect2(await (await fetch2("/users/me")).json()).toBe("me");
214
- expect2(await (await fetch2("/users/42")).json()).toEqual({
215
- id: "42"
216
- });
217
- }
218
- ));
219
- it("ignores trailing slashes and matches case-sensitively", () => withApp(
220
- (router) => {
221
- router.defineRoute({ path: "/users", handler: () => "users" });
222
- },
223
- async ({ fetch: fetch2 }) => {
224
- expect2((await fetch2("/users/")).status).toBe(200);
225
- expect2((await fetch2("/Users")).status).toBe(404);
226
- }
227
- ));
228
- it("returns the standard 404 error for unknown paths", () => withApp(
229
- (router) => {
230
- router.defineRoute({ path: "/items", handler: () => [] });
231
- },
232
- async ({ fetch: fetch2 }) => {
233
- for (const method of ["GET", "POST"]) {
234
- const res = await fetch2("/missing", { method });
235
- expect2(res.status).toBe(404);
236
- expect2(res.headers.get("x-request-id")).toBeTruthy();
237
- expect2(await res.json()).toEqual({
238
- status: 404,
239
- error: "NOT_FOUND",
240
- message: "Route not found"
241
- });
242
- }
243
- }
244
- ));
245
- it("returns 405 with an Allow header for unsupported methods", () => withApp(
246
- (router) => {
247
- router.defineRoute({ path: "/items/:id", handler: () => ({}) });
248
- router.defineRoute({
249
- method: "delete",
250
- path: "/items/:id",
251
- handler: () => {
252
- }
253
- });
254
- },
255
- async ({ fetch: fetch2 }) => {
256
- for (const method of ["POST", "OPTIONS"]) {
257
- const res = await fetch2("/items/1", { method });
258
- expect2(res.status).toBe(405);
259
- expect2(res.headers.get("allow")).toBe("GET, HEAD, DELETE");
260
- expect2(res.headers.get("x-request-id")).toBeTruthy();
261
- expect2(await res.json()).toEqual({
262
- status: 405,
263
- error: "METHOD_NOT_ALLOWED",
264
- message: "Method not allowed"
265
- });
266
- }
267
- }
268
- ));
269
- it("answers HEAD requests with the GET route, without body", () => withApp(
270
- (router) => {
271
- router.defineRoute({ path: "/data", handler: () => ({ a: 1 }) });
272
- router.defineRoute({
273
- path: "/stream",
274
- handler: () => stream(["never sent"])
275
- });
276
- },
277
- async ({ fetch: fetch2 }) => {
278
- const res = await fetch2("/data", { method: "HEAD" });
279
- expect2(res.status).toBe(200);
280
- expect2(res.headers.get("content-type")).toBe(
281
- "application/json; charset=utf-8"
282
- );
283
- expect2(await res.text()).toBe("");
284
- const streamed = await fetch2("/stream", { method: "HEAD" });
285
- expect2(streamed.status).toBe(200);
286
- expect2(await streamed.text()).toBe("");
287
- }
288
- ));
289
- it("mounts groups with their prefix and middlewares", () => {
290
- const calls = [];
291
- const track = (name) => () => {
292
- calls.push(name);
293
- };
294
- return withApp(
295
- (router) => {
296
- const api = router.createGroup("/api", [track("api")]);
297
- api.createGroup("/admin", [track("admin")]).defineRoute({
298
- path: "/stats",
299
- middlewares: [track("route")],
300
- handler: () => ({ ok: true })
301
- });
302
- },
303
- async ({ fetch: fetch2 }) => {
304
- expect2(await (await fetch2("/api/admin/stats")).json()).toEqual({
305
- ok: true
306
- });
307
- expect2(calls).toEqual(["global", "api", "admin", "route"]);
308
- },
309
- { middlewares: [track("global")] }
310
- );
311
- });
312
- it("serves the routes under the base path only", () => withApp(
313
- (router) => {
314
- router.defineRoute({
315
- path: "/users/:id",
316
- handler: ({ req }) => ({ id: req.params.id, path: req.path })
317
- });
318
- },
319
- async ({ fetch: fetch2 }) => {
320
- const res = await fetch2("/my-app/api/users/42");
321
- expect2(res.status).toBe(200);
322
- expect2(await res.json()).toEqual({
323
- id: "42",
324
- path: "/my-app/api/users/42"
325
- });
326
- for (const path of ["/users/42", "/my-app/users/42"]) {
327
- const outside = await fetch2(path);
328
- expect2(outside.status, path).toBe(404);
329
- expect2(await outside.json()).toMatchObject({
330
- error: "NOT_FOUND"
331
- });
332
- }
333
- },
334
- { basePath: "/my-app/api" }
335
- ));
336
- });
337
- describe("request", () => {
338
- it("parses the query string", () => withApp(
339
- (router) => {
340
- router.defineRoute({
341
- path: "/search",
342
- handler: ({ req }) => req.query
343
- });
344
- },
345
- async ({ fetch: fetch2 }) => {
346
- const res = await fetch2(
347
- "/search?q=hello+world&tag=a&tag=b&name=%C3%A9&empty="
348
- );
349
- expect2(await res.json()).toEqual({
350
- q: "hello world",
351
- tag: ["a", "b"],
352
- name: "\xE9",
353
- empty: ""
354
- });
355
- }
356
- ));
357
- it("exposes the method, path, URL, headers and cookies", () => withApp(
358
- (router) => {
359
- router.defineRoute({
360
- method: "post",
361
- path: "/inspect",
362
- handler: ({ req }) => ({
363
- method: req.method,
364
- path: req.path,
365
- url: req.url && new URL(req.url).pathname + new URL(req.url).search,
366
- custom: req.header("X-Custom"),
367
- lowercase: req.headers["x-custom"],
368
- cookies: req.cookies
369
- })
370
- });
371
- },
372
- async ({ fetch: fetch2 }) => {
373
- const res = await fetch2("/inspect?a=1", {
374
- method: "POST",
375
- headers: {
376
- "X-Custom": "value",
377
- cookie: "session=abc; theme=dark%20blue"
378
- }
379
- });
380
- expect2(await res.json()).toEqual({
381
- method: "POST",
382
- path: "/inspect",
383
- url: "/inspect?a=1",
384
- custom: "value",
385
- lowercase: "value",
386
- cookies: { session: "abc", theme: "dark blue" }
387
- });
388
- }
389
- ));
390
- it("exposes the client IP address", () => withApp(
391
- (router) => {
392
- router.defineRoute({
393
- path: "/ip",
394
- handler: ({ req }) => ({ ip: req.ip })
395
- });
396
- },
397
- async ({ fetch: fetch2 }) => {
398
- const { ip } = await (await fetch2("/ip")).json();
399
- expect2(ip).toMatch(/^((::ffff:)?127\.0\.0\.1|::1)$/);
400
- }
401
- ));
402
- it("parses JSON bodies", () => withApp(
403
- (router) => {
404
- router.defineRoute({
405
- method: "post",
406
- path: "/echo",
407
- handler: ({ req }) => ({ body: req.body })
408
- });
409
- },
410
- async ({ fetch: fetch2 }) => {
411
- const res = await fetch2("/echo", postJson({ a: [1, "\xE9"], b: null }));
412
- expect2(await res.json()).toEqual({ body: { a: [1, "\xE9"], b: null } });
413
- }
414
- ));
415
- it("rejects malformed JSON bodies", () => withApp(
416
- (router) => {
417
- router.defineRoute({
418
- method: "post",
419
- path: "/echo",
420
- handler: () => ({ unreachable: true })
421
- });
422
- },
423
- async ({ fetch: fetch2 }) => {
424
- const res = await fetch2("/echo", {
425
- method: "POST",
426
- headers: { "content-type": "application/json" },
427
- body: '{"broken":'
428
- });
429
- expect2(res.status).toBe(400);
430
- expect2(res.headers.get("x-request-id")).toBeTruthy();
431
- expect2(await res.json()).toEqual({
432
- status: 400,
433
- error: "MALFORMED_BODY",
434
- message: "Malformed request body"
435
- });
436
- }
437
- ));
438
- it("parses URL-encoded, text and binary bodies", () => withApp(
439
- (router) => {
440
- router.defineRoute({
441
- method: "post",
442
- path: "/echo",
443
- handler: ({ req }) => req.body instanceof Uint8Array ? { bytes: [...req.body] } : { body: req.body }
444
- });
445
- },
446
- async ({ fetch: fetch2 }) => {
447
- const form = await fetch2("/echo", {
448
- method: "POST",
449
- headers: { "content-type": "application/x-www-form-urlencoded" },
450
- body: "name=Ada+Lovelace&tag=a&tag=b"
451
- });
452
- expect2(await form.json()).toEqual({
453
- body: { name: "Ada Lovelace", tag: ["a", "b"] }
454
- });
455
- const plain = await fetch2("/echo", {
456
- method: "POST",
457
- headers: { "content-type": "text/plain; charset=utf-8" },
458
- body: "h\xE9llo"
459
- });
460
- expect2(await plain.json()).toEqual({ body: "h\xE9llo" });
461
- const binary = await fetch2("/echo", {
462
- method: "POST",
463
- headers: { "content-type": "application/octet-stream" },
464
- body: new Uint8Array([0, 1, 254, 255])
465
- });
466
- expect2(await binary.json()).toEqual({ bytes: [0, 1, 254, 255] });
467
- }
468
- ));
469
- it("exposes the raw body on rawBody routes", () => withApp(
470
- (router) => {
471
- router.defineRoute({
472
- method: "post",
473
- path: "/webhooks",
474
- rawBody: true,
475
- schema: (z) => ({ body: z.object({ type: z.string() }) }),
476
- handler: ({ req }) => ({
477
- raw: new TextDecoder().decode(req.rawBody),
478
- type: req.body.type
479
- })
480
- });
481
- router.defineRoute({
482
- method: "post",
483
- path: "/ping",
484
- rawBody: true,
485
- handler: ({ req }) => ({ length: req.rawBody.byteLength })
486
- });
487
- },
488
- async ({ fetch: fetch2 }) => {
489
- const payload = '{ "type" : "invoice.paid", "id": "\xE9" }';
490
- const res = await fetch2("/webhooks", {
491
- method: "POST",
492
- headers: { "content-type": "application/json" },
493
- body: payload
494
- });
495
- expect2(await res.json()).toEqual({
496
- raw: payload,
497
- type: "invoice.paid"
498
- });
499
- const empty = await fetch2("/ping", { method: "POST" });
500
- expect2(await empty.json()).toEqual({ length: 0 });
501
- }
502
- ));
503
- it("gives an undefined body when there is none", () => withApp(
504
- (router) => {
505
- router.defineRoute({
506
- method: "post",
507
- path: "/empty",
508
- handler: ({ req }) => ({ hasBody: req.body !== void 0 })
509
- });
510
- },
511
- async ({ fetch: fetch2 }) => {
512
- const res = await fetch2("/empty", { method: "POST" });
513
- expect2(await res.json()).toEqual({ hasBody: false });
514
- }
515
- ));
516
- it("rejects bodies larger than the default limit", () => withApp(
517
- (router) => {
518
- router.defineRoute({
519
- method: "post",
520
- path: "/upload",
521
- handler: () => ({ unreachable: true })
522
- });
523
- },
524
- async ({ fetch: fetch2 }) => {
525
- const payload = new Uint8Array(DEFAULT_BODY_LIMIT + 1);
526
- const declared = await fetch2("/upload", {
527
- method: "POST",
528
- headers: { "content-type": "application/octet-stream" },
529
- body: payload
530
- });
531
- expect2(declared.status).toBe(413);
532
- expect2(await declared.json()).toEqual({
533
- status: 413,
534
- error: "PAYLOAD_TOO_LARGE",
535
- message: "Request body too large"
536
- });
537
- const chunked = await fetch2("/upload", {
538
- method: "POST",
539
- headers: { "content-type": "application/octet-stream" },
540
- body: new ReadableStream({
541
- start(controller) {
542
- controller.enqueue(payload.subarray(0, DEFAULT_BODY_LIMIT));
543
- controller.enqueue(new Uint8Array(1));
544
- controller.close();
545
- }
546
- }),
547
- duplex: "half"
548
- });
549
- expect2(chunked.status).toBe(413);
550
- }
551
- ));
552
- it("accepts bodies up to the default limit", () => withApp(
553
- (router) => {
554
- router.defineRoute({
555
- method: "post",
556
- path: "/upload",
557
- handler: ({ req }) => ({
558
- size: req.body.byteLength
559
- })
560
- });
561
- },
562
- async ({ fetch: fetch2 }) => {
563
- const res = await fetch2("/upload", {
564
- method: "POST",
565
- headers: { "content-type": "application/octet-stream" },
566
- body: new Uint8Array(DEFAULT_BODY_LIMIT)
567
- });
568
- expect2(await res.json()).toEqual({ size: DEFAULT_BODY_LIMIT });
569
- }
570
- ));
571
- it("rejects compressed request bodies", () => withApp(
572
- (router) => {
573
- router.defineRoute({
574
- method: "post",
575
- path: "/echo",
576
- handler: () => ({ unreachable: true })
577
- });
578
- },
579
- async ({ fetch: fetch2 }) => {
580
- const res = await fetch2("/echo", {
581
- method: "POST",
582
- headers: {
583
- "content-type": "application/json",
584
- "content-encoding": "gzip"
585
- },
586
- body: new Uint8Array([31, 139, 8, 0])
587
- });
588
- expect2(res.status).toBe(415);
589
- expect2(await res.json()).toMatchObject({
590
- error: "UNSUPPORTED_MEDIA_TYPE"
591
- });
592
- }
593
- ));
594
- });
595
- describe("uploads", () => {
596
- const formWith = (entries) => {
597
- const form = new FormData();
598
- for (const [name, value] of entries) form.append(name, value);
599
- return form;
600
- };
601
- it("parses multipart bodies into fields and files", () => {
602
- const bytes = Uint8Array.from({ length: 256 }, (_, index) => index);
603
- return withApp(
604
- (router) => {
605
- router.defineRoute({
606
- method: "post",
607
- path: "/documents",
608
- schema: (z) => ({
609
- body: z.object({ title: z.string() }),
610
- files: z.object({
611
- document: z.file().mime(["application/pdf"])
612
- })
613
- }),
614
- handler: async ({ req }) => ({
615
- title: req.body.title,
616
- name: req.files.document.name,
617
- type: req.files.document.type,
618
- bytes: [
619
- ...new Uint8Array(await req.files.document.arrayBuffer())
620
- ]
621
- })
622
- });
623
- router.defineRoute({
624
- method: "post",
625
- path: "/gallery",
626
- handler: ({ req }) => ({
627
- fields: req.body,
628
- files: Object.fromEntries(
629
- Object.entries(req.files).map(([name, value]) => [
630
- name,
631
- [value].flat().map((file2) => file2.name)
632
- ])
633
- )
634
- })
635
- });
636
- },
637
- async ({ fetch: fetch2 }) => {
638
- const res = await fetch2("/documents", {
639
- method: "POST",
640
- body: formWith([
641
- ["title", "Contrat sign\xE9"],
642
- [
643
- "document",
644
- new File([bytes], "contrat \xE9t\xE9.pdf", {
645
- type: "application/pdf"
646
- })
647
- ]
648
- ])
649
- });
650
- expect2(await res.json()).toEqual({
651
- title: "Contrat sign\xE9",
652
- name: "contrat \xE9t\xE9.pdf",
653
- type: "application/pdf",
654
- bytes: [...bytes]
655
- });
656
- const gallery = await fetch2("/gallery", {
657
- method: "POST",
658
- body: formWith([
659
- ["tag", "sea"],
660
- ["tag", "sun"],
661
- ["photos", new File(["a"], "a.jpg")],
662
- ["photos", new File(["b"], "b.jpg")],
663
- ["cover", new File(["c"], "c.jpg")]
664
- ])
665
- });
666
- expect2(await gallery.json()).toEqual({
667
- fields: { tag: ["sea", "sun"] },
668
- files: { photos: ["a.jpg", "b.jpg"], cover: ["c.jpg"] }
669
- });
670
- }
671
- );
672
- });
673
- it("validates files", () => withApp(
674
- (router) => {
675
- router.defineRoute({
676
- method: "post",
677
- path: "/avatar",
678
- schema: (z) => ({
679
- files: z.object({
680
- avatar: z.file().max(10).mime(["image/png"])
681
- })
682
- }),
683
- handler: () => ({ ok: true })
684
- });
685
- },
686
- async ({ fetch: fetch2 }) => {
687
- const res = await fetch2("/avatar", {
688
- method: "POST",
689
- body: formWith([
690
- [
691
- "avatar",
692
- new File([new Uint8Array(11)], "a.gif", {
693
- type: "image/gif"
694
- })
695
- ]
696
- ])
697
- });
698
- expect2(res.status).toBe(400);
699
- expect2(await res.json()).toMatchObject({
700
- error: "VALIDATION_FAILED",
701
- data: {
702
- issues: [
703
- { source: "files", path: ["avatar"], code: "too_big" },
704
- { source: "files", path: ["avatar"], code: "invalid_value" }
705
- ]
706
- }
707
- });
708
- }
709
- ));
710
- it("applies the route body limit", () => withApp(
711
- (router) => {
712
- router.defineRoute({
713
- method: "post",
714
- path: "/large",
715
- bodyLimit: 3 * DEFAULT_BODY_LIMIT,
716
- handler: ({ req }) => ({
717
- size: req.files.file.size
718
- })
719
- });
720
- router.defineRoute({
721
- method: "post",
722
- path: "/small",
723
- bodyLimit: 100,
724
- handler: () => ({ unreachable: true })
725
- });
726
- },
727
- async ({ fetch: fetch2 }) => {
728
- const size = 2 * DEFAULT_BODY_LIMIT;
729
- const large = await fetch2("/large", {
730
- method: "POST",
731
- body: formWith([
732
- ["file", new File([new Uint8Array(size)], "big.bin")]
733
- ])
734
- });
735
- expect2(await large.json()).toEqual({ size });
736
- const small = await fetch2("/small", {
737
- method: "POST",
738
- body: formWith([
739
- ["file", new File([new Uint8Array(200)], "a.bin")]
740
- ])
741
- });
742
- expect2(small.status).toBe(413);
743
- }
744
- ));
745
- it("rejects malformed multipart bodies", () => withApp(
746
- (router) => {
747
- router.defineRoute({
748
- method: "post",
749
- path: "/upload",
750
- handler: () => ({ unreachable: true })
751
- });
752
- },
753
- async ({ fetch: fetch2 }) => {
754
- const res = await fetch2("/upload", {
755
- method: "POST",
756
- headers: { "content-type": "multipart/form-data; boundary=xyz" },
757
- body: "not a multipart body"
758
- });
759
- expect2(res.status).toBe(400);
760
- expect2(await res.json()).toMatchObject({ error: "MALFORMED_BODY" });
761
- }
762
- ));
763
- });
764
- describe("validation", () => {
765
- it("returns VALIDATION_FAILED errors and parsed values", () => withApp(
766
- (router) => {
767
- router.defineRoute({
768
- method: "post",
769
- path: "/users/:id",
770
- schema: (z) => ({
771
- params: z.object({ id: z.coerce.number().int() }),
772
- query: z.object({ notify: z.enum(["yes", "no"]) }),
773
- body: z.object({ name: z.string().trim() })
774
- }),
775
- handler: ({ req }) => ({
776
- id: req.params.id,
777
- notify: req.query.notify,
778
- name: req.body.name
779
- })
780
- });
781
- },
782
- async ({ fetch: fetch2 }) => {
783
- const invalid = await fetch2("/users/abc?notify=maybe", postJson({}));
784
- expect2(invalid.status).toBe(400);
785
- expect2(await invalid.json()).toMatchObject({
786
- status: 400,
787
- error: "VALIDATION_FAILED",
788
- data: {
789
- issues: [
790
- { source: "params", path: ["id"] },
791
- { source: "query", path: ["notify"] },
792
- { source: "body", path: ["name"] }
793
- ]
794
- }
795
- });
796
- const valid = await fetch2(
797
- "/users/42?notify=yes",
798
- postJson({ name: " Ada " })
799
- );
800
- expect2(await valid.json()).toEqual({
801
- id: 42,
802
- notify: "yes",
803
- name: "Ada"
804
- });
805
- }
806
- ));
807
- });
808
- describe("errors", () => {
809
- it("serializes returned and thrown HTTP errors", () => withApp(
810
- (router) => {
811
- router.defineRoute({
812
- path: "/returned",
813
- handler: () => notFoundError("User not found", { id: "1" })
814
- });
815
- router.defineRoute({
816
- path: "/thrown",
817
- handler: ({ res }) => {
818
- res.setHeader("www-authenticate", "Bearer");
819
- throw unauthorizedError();
820
- }
821
- });
822
- },
823
- async ({ fetch: fetch2 }) => {
824
- const returned = await fetch2("/returned");
825
- expect2(returned.status).toBe(404);
826
- expect2(await returned.json()).toEqual({
827
- status: 404,
828
- error: "NOT_FOUND",
829
- message: "User not found",
830
- data: { id: "1" }
831
- });
832
- const thrown = await fetch2("/thrown");
833
- expect2(thrown.status).toBe(401);
834
- expect2(thrown.headers.get("www-authenticate")).toBe("Bearer");
835
- expect2(await thrown.json()).toEqual({
836
- status: 401,
837
- error: "UNAUTHORIZED",
838
- message: "Unauthorized"
839
- });
840
- }
841
- ));
842
- it("hides unexpected errors behind a 500 error and reports them", async () => {
843
- const onError = vi.fn();
844
- await withApp(
845
- (router) => {
846
- router.defineRoute({
847
- path: "/crash",
848
- handler: () => {
849
- throw new Error("Database password is hunter2");
850
- }
851
- });
852
- },
853
- async ({ fetch: fetch2 }) => {
854
- const res = await fetch2("/crash");
855
- expect2(res.status).toBe(500);
856
- expect2(res.headers.get("x-request-id")).toBeTruthy();
857
- expect2(await res.json()).toEqual({
858
- status: 500,
859
- error: "INTERNAL_ERROR",
860
- message: "Internal Server Error"
861
- });
862
- expect2(onError).toHaveBeenCalledOnce();
863
- },
864
- { onError }
865
- );
866
- });
867
- it("exposes error details when exposeErrors is enabled", () => withApp(
868
- (router) => {
869
- router.defineRoute({
870
- path: "/crash",
871
- handler: () => {
872
- throw new Error("Boom");
873
- }
874
- });
875
- },
876
- async ({ fetch: fetch2 }) => {
877
- expect2(await (await fetch2("/crash")).json()).toMatchObject({
878
- status: 500,
879
- data: { name: "Error", message: "Boom" }
880
- });
881
- },
882
- { exposeErrors: true }
883
- ));
884
- it("returns a 500 error when the response does not match its schema", () => withApp(
885
- (router) => {
886
- router.defineRoute({
887
- path: "/invalid",
888
- schema: (z) => ({ response: z.object({ id: z.string() }) }),
889
- // @ts-expect-error the result does not match the schema
890
- handler: () => ({ id: 1 })
891
- });
892
- },
893
- async ({ fetch: fetch2 }) => {
894
- expect2((await fetch2("/invalid")).status).toBe(500);
895
- }
896
- ));
897
- });
898
- describe("middlewares and filters", () => {
899
- it("stops the pipeline when a middleware returns a response", () => withApp(
900
- (router) => {
901
- router.defineRoute({
902
- path: "/private",
903
- middlewares: [
904
- ({ req }) => req.header("authorization") ? void 0 : unauthorizedError()
905
- ],
906
- handler: () => ({ secret: true })
907
- });
908
- router.defineRoute({
909
- path: "/legacy",
910
- middlewares: [() => redirect("/modern", 301)],
911
- handler: () => ({ unreachable: true })
912
- });
913
- },
914
- async ({ fetch: fetch2 }) => {
915
- expect2((await fetch2("/private")).status).toBe(401);
916
- expect2(
917
- await (await fetch2("/private", { headers: { authorization: "x" } })).json()
918
- ).toEqual({ secret: true });
919
- const legacy = await fetch2("/legacy", { redirect: "manual" });
920
- expect2(legacy.status).toBe(301);
921
- expect2(legacy.headers.get("location")).toBe("/modern");
922
- }
923
- ));
924
- it("applies app and router response filters", () => withApp(
925
- (router) => {
926
- router.addResponseFilter((_ctx, result) => ({
927
- ...result,
928
- router: true
929
- }));
930
- router.defineRoute({ path: "/data", handler: () => ({ a: 1 }) });
931
- },
932
- async ({ fetch: fetch2 }) => {
933
- expect2(await (await fetch2("/data")).json()).toEqual({
934
- a: 1,
935
- app: true,
936
- router: true
937
- });
938
- },
939
- {
940
- responseFilters: [
941
- (_ctx, result) => ({ ...result, app: true })
942
- ]
943
- }
944
- ));
945
- });
946
- describe("responses", () => {
947
- it("applies statuses, headers and cookies", () => withApp(
948
- (router) => {
949
- router.defineRoute({
950
- method: "post",
951
- path: "/created",
952
- status: 201,
953
- handler: ({ res }) => {
954
- res.setHeader("location", "/items/1");
955
- res.setCookie("session", "abc", { httpOnly: true });
956
- res.setCookie("theme", "dark");
957
- return { id: 1 };
958
- }
959
- });
960
- router.defineRoute({
961
- path: "/accepted",
962
- handler: ({ res }) => {
963
- res.setStatus(202);
964
- return json({ queued: true }, { headers: { "x-queue": "a" } });
965
- }
966
- });
967
- },
968
- async ({ fetch: fetch2 }) => {
969
- const created = await fetch2("/created", { method: "POST" });
970
- expect2(created.status).toBe(201);
971
- expect2(created.headers.get("location")).toBe("/items/1");
972
- expect2(created.headers.getSetCookie()).toEqual([
973
- "session=abc; Path=/; HttpOnly",
974
- "theme=dark; Path=/"
975
- ]);
976
- const accepted = await fetch2("/accepted");
977
- expect2(accepted.status).toBe(202);
978
- expect2(accepted.headers.get("x-queue")).toBe("a");
979
- expect2(await accepted.json()).toEqual({ queued: true });
980
- }
981
- ));
982
- it("answers 204 without body when the handler returns nothing", () => withApp(
983
- (router) => {
984
- router.defineRoute({
985
- method: "delete",
986
- path: "/items/:id",
987
- handler: () => {
988
- }
989
- });
990
- },
991
- async ({ fetch: fetch2 }) => {
992
- const res = await fetch2("/items/1", { method: "DELETE" });
993
- expect2(res.status).toBe(204);
994
- expect2(res.headers.get("content-type")).toBeNull();
995
- expect2(await res.text()).toBe("");
996
- }
997
- ));
998
- it("does not add framework headers", () => withApp(
999
- (router) => {
1000
- router.defineRoute({ path: "/plain", handler: () => "ok" });
1001
- },
1002
- async ({ fetch: fetch2 }) => {
1003
- const res = await fetch2("/plain");
1004
- expect2(res.headers.get("x-powered-by")).toBeNull();
1005
- expect2(res.headers.get("etag")).toBeNull();
1006
- }
1007
- ));
1008
- it("sends redirects", () => withApp(
1009
- (router) => {
1010
- router.defineRoute({
1011
- path: "/old",
1012
- handler: () => redirect("/new")
1013
- });
1014
- },
1015
- async ({ fetch: fetch2 }) => {
1016
- const res = await fetch2("/old", { redirect: "manual" });
1017
- expect2(res.status).toBe(302);
1018
- expect2(res.headers.get("location")).toBe("/new");
1019
- }
1020
- ));
1021
- it("sends text responses", () => withApp(
1022
- (router) => {
1023
- router.defineRoute({
1024
- path: "/page",
1025
- handler: () => text("<h1>H\xE9llo</h1>", {
1026
- headers: { "content-type": "text/html; charset=utf-8" }
1027
- })
1028
- });
1029
- router.defineRoute({ path: "/health", handler: () => text("OK") });
1030
- },
1031
- async ({ fetch: fetch2 }) => {
1032
- const page = await fetch2("/page");
1033
- expect2(page.headers.get("content-type")).toBe(
1034
- "text/html; charset=utf-8"
1035
- );
1036
- expect2(await page.text()).toBe("<h1>H\xE9llo</h1>");
1037
- const health = await fetch2("/health");
1038
- expect2(health.headers.get("content-type")).toBe(
1039
- "text/plain; charset=utf-8"
1040
- );
1041
- expect2(await health.text()).toBe("OK");
1042
- }
1043
- ));
1044
- it("sends files without altering their bytes", () => {
1045
- const bytes = Uint8Array.from({ length: 256 }, (_, index) => index);
1046
- return withApp(
1047
- (router) => {
1048
- router.defineRoute({
1049
- path: "/download",
1050
- handler: () => file(bytes, {
1051
- filename: "rapport \xE9t\xE9.bin",
1052
- contentType: "application/octet-stream"
1053
- })
1054
- });
1055
- router.defineRoute({
1056
- path: "/blob",
1057
- handler: () => file(new Blob([bytes], { type: "image/png" }), {
1058
- disposition: "inline"
1059
- })
1060
- });
1061
- },
1062
- async ({ fetch: fetch2 }) => {
1063
- const res = await fetch2("/download");
1064
- expect2(res.headers.get("content-length")).toBe("256");
1065
- expect2(res.headers.get("content-disposition")).toBe(
1066
- `attachment; filename="rapport _t_.bin"; filename*=UTF-8''rapport%20%C3%A9t%C3%A9.bin`
1067
- );
1068
- expect2(new Uint8Array(await res.arrayBuffer())).toEqual(bytes);
1069
- const blob = await fetch2("/blob");
1070
- expect2(blob.headers.get("content-type")).toBe("image/png");
1071
- expect2(new Uint8Array(await blob.arrayBuffer())).toEqual(bytes);
1072
- }
1073
- );
1074
- });
1075
- });
1076
- describe("cors", () => {
1077
- const APP = "https://app.example.com";
1078
- const EVIL = "https://evil.example.org";
1079
- const corsRoutes = (router) => {
1080
- router.defineRoute({ path: "/users", handler: () => [] });
1081
- router.defineRoute({
1082
- method: "post",
1083
- path: "/users",
1084
- handler: () => ({ created: true })
1085
- });
1086
- router.defineRoute({
1087
- path: "/private",
1088
- middlewares: [() => unauthorizedError()],
1089
- handler: () => ({ secret: true })
1090
- });
1091
- router.defineRoute({
1092
- method: "options",
1093
- path: "/custom",
1094
- handler: () => ({ custom: true })
1095
- });
1096
- router.defineRoute({
1097
- path: "/export.csv",
1098
- handler: () => stream(["a,b\n"], { contentType: "text/csv" })
1099
- });
1100
- };
1101
- const preflight = (origin = APP) => ({
1102
- method: "OPTIONS",
1103
- headers: {
1104
- origin,
1105
- "access-control-request-method": "POST",
1106
- "access-control-request-headers": "content-type, authorization"
1107
- }
1108
- });
1109
- it("answers preflight requests before the middlewares", () => withApp(
1110
- corsRoutes,
1111
- async ({ fetch: fetch2 }) => {
1112
- const res = await fetch2("/users", preflight());
1113
- expect2(res.status).toBe(204);
1114
- expect2(res.headers.get("access-control-allow-origin")).toBe(APP);
1115
- expect2(res.headers.get("access-control-allow-methods")).toBe(
1116
- "GET, HEAD, POST"
1117
- );
1118
- expect2(res.headers.get("access-control-allow-headers")).toBe(
1119
- "content-type, authorization"
1120
- );
1121
- expect2(res.headers.get("access-control-max-age")).toBe("600");
1122
- expect2(res.headers.get("vary")).toContain("Origin");
1123
- expect2(await res.text()).toBe("");
1124
- const private_ = await fetch2("/private", preflight());
1125
- expect2(private_.status).toBe(204);
1126
- const custom = await fetch2("/custom", preflight());
1127
- expect2(custom.status).toBe(204);
1128
- expect2(custom.headers.get("access-control-allow-methods")).toBe(
1129
- "OPTIONS"
1130
- );
1131
- const refused = await fetch2("/users", preflight(EVIL));
1132
- expect2(refused.status).toBe(405);
1133
- expect2(
1134
- refused.headers.get("access-control-allow-origin")
1135
- ).toBeNull();
1136
- expect2((await fetch2("/missing", preflight())).status).toBe(404);
1137
- },
1138
- { cors: { origin: [APP], maxAge: 600 } }
1139
- ));
1140
- it("adds the CORS headers to every response, errors included", () => withApp(
1141
- corsRoutes,
1142
- async ({ fetch: fetch2 }) => {
1143
- const requests = [
1144
- ["/users", {}, 200],
1145
- ["/private", {}, 401],
1146
- ["/missing", {}, 404],
1147
- ["/users", { method: "DELETE" }, 405],
1148
- [
1149
- "/users",
1150
- {
1151
- method: "POST",
1152
- headers: { "content-type": "application/json" },
1153
- body: "{oops"
1154
- },
1155
- 400
1156
- ],
1157
- ["/export.csv", {}, 200]
1158
- ];
1159
- for (const [path, init, status] of requests) {
1160
- const res = await fetch2(path, {
1161
- ...init,
1162
- headers: { ...init.headers, origin: APP }
1163
- });
1164
- expect2(res.status).toBe(status);
1165
- expect2(res.headers.get("access-control-allow-origin")).toBe(APP);
1166
- expect2(res.headers.get("access-control-allow-credentials")).toBe(
1167
- "true"
1168
- );
1169
- expect2(res.headers.get("access-control-expose-headers")).toBe(
1170
- "x-request-id"
1171
- );
1172
- expect2(res.headers.get("vary")).toContain("Origin");
1173
- await res.arrayBuffer();
1174
- }
1175
- const refused = await fetch2("/users", { headers: { origin: EVIL } });
1176
- expect2(refused.status).toBe(200);
1177
- expect2(
1178
- refused.headers.get("access-control-allow-origin")
1179
- ).toBeNull();
1180
- expect2(refused.headers.get("vary")).toContain("Origin");
1181
- },
1182
- {
1183
- cors: {
1184
- origin: APP,
1185
- credentials: true,
1186
- exposedHeaders: ["x-request-id"]
1187
- }
1188
- }
1189
- ));
1190
- });
1191
- describe("streaming", () => {
1192
- const incremental = options.bufferedResponses ? it.skip : it;
1193
- it("streams chunks", () => withApp(
1194
- (router) => {
1195
- router.defineRoute({
1196
- path: "/export.csv",
1197
- handler: () => stream(
1198
- async function* () {
1199
- yield "id,name\n";
1200
- yield new TextEncoder().encode("1,Ada\n");
1201
- },
1202
- { contentType: "text/csv" }
1203
- )
1204
- });
1205
- },
1206
- async ({ fetch: fetch2 }) => {
1207
- const res = await fetch2("/export.csv");
1208
- expect2(res.status).toBe(200);
1209
- expect2(res.headers.get("content-type")).toBe("text/csv");
1210
- expect2(await res.text()).toBe("id,name\n1,Ada\n");
1211
- }
1212
- ));
1213
- incremental("flushes Server-Sent Events as they are produced", () => {
1214
- const released = deferred();
1215
- return withApp(
1216
- (router) => {
1217
- router.defineRoute({
1218
- path: "/events",
1219
- handler: () => sse(
1220
- async function* ({ signal }) {
1221
- try {
1222
- yield { event: "ready", data: { ok: true } };
1223
- await waitForAbort(signal);
1224
- } finally {
1225
- released.resolve();
1226
- }
1227
- },
1228
- { heartbeat: false }
1229
- )
1230
- });
1231
- },
1232
- async ({ fetch: fetch2 }) => {
1233
- const res = await fetch2("/events");
1234
- expect2(res.headers.get("content-type")).toBe(
1235
- "text/event-stream; charset=utf-8"
1236
- );
1237
- expect2(res.headers.get("cache-control")).toBe("no-cache");
1238
- const reader = res.body.getReader();
1239
- expect2(await within(readChunk(reader), "first event")).toBe(
1240
- 'event: ready\ndata: {"ok":true}\n\n'
1241
- );
1242
- await reader.cancel();
1243
- await within(released.promise, "source release on disconnect");
1244
- }
1245
- );
1246
- });
1247
- incremental(
1248
- "cancels the response stream when the client disconnects",
1249
- () => {
1250
- const cancelled = deferred();
1251
- return withApp(
1252
- (router) => {
1253
- router.defineRoute({
1254
- path: "/download",
1255
- handler: () => file(
1256
- new ReadableStream({
1257
- pull: (controller) => controller.enqueue(new Uint8Array(64 * 1024)),
1258
- cancel: () => cancelled.resolve()
1259
- })
1260
- )
1261
- });
1262
- },
1263
- async ({ fetch: fetch2 }) => {
1264
- const res = await fetch2("/download");
1265
- const reader = res.body.getReader();
1266
- await reader.read();
1267
- await reader.cancel();
1268
- await within(cancelled.promise, "response stream cancel");
1269
- }
1270
- );
1271
- }
1272
- );
1273
- incremental("aborts req.signal when the client disconnects", () => {
1274
- const started = deferred();
1275
- const aborted = deferred();
1276
- return withApp(
1277
- (router) => {
1278
- router.defineRoute({
1279
- path: "/slow",
1280
- handler: async ({ req }) => {
1281
- started.resolve();
1282
- await waitForAbort(req.signal);
1283
- aborted.resolve();
1284
- return { late: true };
1285
- }
1286
- });
1287
- },
1288
- async ({ fetch: fetch2 }) => {
1289
- const controller = new AbortController();
1290
- const pending = fetch2("/slow", { signal: controller.signal }).catch(
1291
- () => void 0
1292
- );
1293
- await within(started.promise, "handler start");
1294
- controller.abort();
1295
- await within(aborted.promise, "req.signal abort");
1296
- await pending;
1297
- }
1298
- );
1299
- });
1300
- incremental(
1301
- "aborts the connection when a stream fails",
1302
- () => withApp(
1303
- (router) => {
1304
- router.defineRoute({
1305
- path: "/broken",
1306
- handler: () => stream(async function* () {
1307
- yield "partial";
1308
- throw new Error("Source failed");
1309
- })
1310
- });
1311
- },
1312
- async ({ fetch: fetch2 }) => {
1313
- const res = await fetch2("/broken");
1314
- expect2(res.status).toBe(200);
1315
- const outcome = await within(
1316
- res.text().then(
1317
- () => "completed",
1318
- () => "aborted"
1319
- ),
1320
- "body read"
1321
- );
1322
- expect2(outcome).toBe("aborted");
1323
- }
1324
- )
1325
- );
1326
- });
1327
- });
1328
- };
1329
-
1330
52
  // src/lambda.ts
1331
53
  var CHUNK = 32768;
1332
54
  var encodeBase64 = (bytes) => {
@@ -1336,7 +58,7 @@ var encodeBase64 = (bytes) => {
1336
58
  }
1337
59
  return btoa(binary);
1338
60
  };
1339
- var decodeBase64 = (text2) => Uint8Array.from(atob(text2), (char) => char.charCodeAt(0));
61
+ var decodeBase64 = (text) => Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
1340
62
  var TEXT_TYPES = /^\s*(text\/|application\/(json|x-www-form-urlencoded)\b|[^;]*\+json\b)/i;
1341
63
  var eventBody = (bytes, contentType) => {
1342
64
  if (bytes.byteLength === 0) return { body: null, isBase64Encoded: false };
@@ -1493,33 +215,47 @@ var lambdaContext = () => ({
1493
215
  getRemainingTimeInMillis: () => 3e4
1494
216
  });
1495
217
  var createLambdaTarget = (handler, options) => {
218
+ const close = closeOf("createLambdaTarget()", options, [
219
+ "format",
220
+ "close",
221
+ "sourceIp"
222
+ ]);
1496
223
  const invoke = handler;
1497
- return createFetchTarget(async (request) => {
1498
- const event = await toLambdaEvent(request, options.format, options.sourceIp);
1499
- let result;
1500
- try {
1501
- result = await invoke(event, lambdaContext());
1502
- } catch {
1503
- return Response.json(
1504
- { message: "Internal server error" },
1505
- { status: 502 }
224
+ return createFetchTarget(
225
+ async (request) => {
226
+ const event = await toLambdaEvent(
227
+ request,
228
+ options.format,
229
+ options.sourceIp
1506
230
  );
1507
- }
1508
- return fromLambdaResult(result);
1509
- }, options.close);
231
+ let result;
232
+ try {
233
+ result = await invoke(event, lambdaContext());
234
+ } catch {
235
+ return Response.json(
236
+ { message: "Internal server error" },
237
+ { status: 502 }
238
+ );
239
+ }
240
+ return fromLambdaResult(result);
241
+ },
242
+ { close }
243
+ );
1510
244
  };
1511
245
 
1512
246
  // src/memory.ts
1513
247
  import {
1514
- DEFAULT_BODY_LIMIT as DEFAULT_BODY_LIMIT2,
1515
- createApp,
248
+ createApp
249
+ } from "@dotpress/core";
250
+ import {
251
+ DEFAULT_BODY_LIMIT,
1516
252
  createRouteMatcher,
1517
253
  fromWebRequest,
1518
254
  readRequestBody,
1519
255
  toWebResponse
1520
- } from "@dotpress/core";
256
+ } from "@dotpress/core/adapter";
1521
257
  var memoryAdapter = (options = {}) => {
1522
- const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT2;
258
+ const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
1523
259
  let routes = [];
1524
260
  let matcher;
1525
261
  let runtime;
@@ -1796,8 +532,6 @@ var expectNoMailSent = (outbox, criteria = {}) => {
1796
532
  };
1797
533
  export {
1798
534
  ExpectationError,
1799
- createFetchTarget,
1800
- createHttpTarget,
1801
535
  createLambdaTarget,
1802
536
  createMemoryLogger,
1803
537
  createTestApp,
@@ -1807,7 +541,6 @@ export {
1807
541
  expectNoMailSent,
1808
542
  fromLambdaResult,
1809
543
  memoryAdapter,
1810
- runConformanceSuite,
1811
544
  toLambdaEvent
1812
545
  };
1813
546
  //# sourceMappingURL=index.js.map