@fragno-dev/core 0.1.11 → 0.2.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/.turbo/turbo-build.log +50 -42
- package/CHANGELOG.md +51 -0
- package/dist/api/api.d.ts +19 -1
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js.map +1 -1
- package/dist/api/fragment-definition-builder.d.ts +17 -7
- package/dist/api/fragment-definition-builder.d.ts.map +1 -1
- package/dist/api/fragment-definition-builder.js +3 -2
- package/dist/api/fragment-definition-builder.js.map +1 -1
- package/dist/api/fragment-instantiator.d.ts +23 -16
- package/dist/api/fragment-instantiator.d.ts.map +1 -1
- package/dist/api/fragment-instantiator.js +163 -19
- package/dist/api/fragment-instantiator.js.map +1 -1
- package/dist/api/request-input-context.d.ts +57 -1
- package/dist/api/request-input-context.d.ts.map +1 -1
- package/dist/api/request-input-context.js +67 -0
- package/dist/api/request-input-context.js.map +1 -1
- package/dist/api/request-middleware.d.ts +1 -1
- package/dist/api/request-middleware.d.ts.map +1 -1
- package/dist/api/request-middleware.js.map +1 -1
- package/dist/api/route.d.ts +7 -7
- package/dist/api/route.d.ts.map +1 -1
- package/dist/api/route.js.map +1 -1
- package/dist/client/client.d.ts +4 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +103 -7
- package/dist/client/client.js.map +1 -1
- package/dist/client/vue.d.ts +7 -3
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +16 -1
- package/dist/client/vue.js.map +1 -1
- package/dist/internal/trace-context.d.ts +23 -0
- package/dist/internal/trace-context.d.ts.map +1 -0
- package/dist/internal/trace-context.js +14 -0
- package/dist/internal/trace-context.js.map +1 -0
- package/dist/mod-client.d.ts +3 -17
- package/dist/mod-client.d.ts.map +1 -1
- package/dist/mod-client.js +20 -10
- package/dist/mod-client.js.map +1 -1
- package/dist/mod.d.ts +3 -2
- package/dist/mod.js +2 -1
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +33 -0
- package/dist/runtime.js.map +1 -0
- package/dist/test/test.d.ts +2 -2
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js.map +1 -1
- package/package.json +23 -17
- package/src/api/api.ts +22 -0
- package/src/api/fragment-definition-builder.ts +36 -17
- package/src/api/fragment-instantiator.test.ts +286 -0
- package/src/api/fragment-instantiator.ts +338 -31
- package/src/api/internal/path-runtime.test.ts +7 -0
- package/src/api/request-input-context.test.ts +152 -0
- package/src/api/request-input-context.ts +85 -0
- package/src/api/request-middleware.test.ts +47 -1
- package/src/api/request-middleware.ts +1 -1
- package/src/api/route.ts +7 -2
- package/src/client/client.test.ts +195 -0
- package/src/client/client.ts +185 -10
- package/src/client/vue.test.ts +253 -3
- package/src/client/vue.ts +44 -1
- package/src/internal/trace-context.ts +35 -0
- package/src/mod-client.ts +51 -7
- package/src/mod.ts +6 -1
- package/src/runtime.ts +48 -0
- package/src/test/test.ts +13 -4
- package/tsdown.config.ts +1 -0
|
@@ -416,6 +416,56 @@ describe("fragment-instantiator", () => {
|
|
|
416
416
|
expect(data).toEqual({ userId: "123" });
|
|
417
417
|
});
|
|
418
418
|
|
|
419
|
+
it("should URL decode path params", async () => {
|
|
420
|
+
const definition = defineFragment("test-fragment").build();
|
|
421
|
+
|
|
422
|
+
const route = defineRoute({
|
|
423
|
+
method: "GET",
|
|
424
|
+
path: "/users/:name",
|
|
425
|
+
handler: async (input, { json }) => {
|
|
426
|
+
return json({ userName: input.pathParams.name });
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const fragment = instantiate(definition)
|
|
431
|
+
.withRoutes([route])
|
|
432
|
+
.withOptions({ mountRoute: "/api" })
|
|
433
|
+
.build();
|
|
434
|
+
|
|
435
|
+
// URL with encoded space: "a%20b" should be decoded to "a b"
|
|
436
|
+
const request = new Request("http://localhost/api/users/a%20b");
|
|
437
|
+
const response = await fragment.handler(request);
|
|
438
|
+
|
|
439
|
+
expect(response.status).toBe(200);
|
|
440
|
+
const data = await response.json();
|
|
441
|
+
expect(data).toEqual({ userName: "a b" });
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("should URL decode path params with special characters", async () => {
|
|
445
|
+
const definition = defineFragment("test-fragment").build();
|
|
446
|
+
|
|
447
|
+
const route = defineRoute({
|
|
448
|
+
method: "GET",
|
|
449
|
+
path: "/files/:path",
|
|
450
|
+
handler: async (input, { json }) => {
|
|
451
|
+
return json({ filePath: input.pathParams.path });
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const fragment = instantiate(definition)
|
|
456
|
+
.withRoutes([route])
|
|
457
|
+
.withOptions({ mountRoute: "/api" })
|
|
458
|
+
.build();
|
|
459
|
+
|
|
460
|
+
// URL with encoded slash: "folder%2Fsubfolder" should be decoded to "folder/subfolder"
|
|
461
|
+
const request = new Request("http://localhost/api/files/folder%2Fsubfolder");
|
|
462
|
+
const response = await fragment.handler(request);
|
|
463
|
+
|
|
464
|
+
expect(response.status).toBe(200);
|
|
465
|
+
const data = await response.json();
|
|
466
|
+
expect(data).toEqual({ filePath: "folder/subfolder" });
|
|
467
|
+
});
|
|
468
|
+
|
|
419
469
|
it("should handle POST requests with body", async () => {
|
|
420
470
|
const definition = defineFragment("test-fragment").build();
|
|
421
471
|
|
|
@@ -450,6 +500,170 @@ describe("fragment-instantiator", () => {
|
|
|
450
500
|
const data = await response.json();
|
|
451
501
|
expect(data).toEqual({ received: { test: "data" } });
|
|
452
502
|
});
|
|
503
|
+
|
|
504
|
+
it("should accept FormData for routes with contentType: multipart/form-data", async () => {
|
|
505
|
+
const definition = defineFragment("test-fragment").build();
|
|
506
|
+
|
|
507
|
+
const route = defineRoute({
|
|
508
|
+
method: "POST",
|
|
509
|
+
path: "/upload",
|
|
510
|
+
contentType: "multipart/form-data",
|
|
511
|
+
handler: async (ctx, { json }) => {
|
|
512
|
+
const formData = ctx.formData();
|
|
513
|
+
const description = formData.get("description") as string;
|
|
514
|
+
return json({ description });
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const fragment = instantiate(definition)
|
|
519
|
+
.withRoutes([route])
|
|
520
|
+
.withOptions({ mountRoute: "/api" })
|
|
521
|
+
.build();
|
|
522
|
+
|
|
523
|
+
const formData = new FormData();
|
|
524
|
+
formData.append("description", "Test file upload");
|
|
525
|
+
|
|
526
|
+
const request = new Request("http://localhost/api/upload", {
|
|
527
|
+
method: "POST",
|
|
528
|
+
body: formData,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const response = await fragment.handler(request);
|
|
532
|
+
|
|
533
|
+
expect(response.status).toBe(200);
|
|
534
|
+
const data = await response.json();
|
|
535
|
+
expect(data).toEqual({ description: "Test file upload" });
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("should reject FormData for JSON routes (default contentType)", async () => {
|
|
539
|
+
const definition = defineFragment("test-fragment").build();
|
|
540
|
+
|
|
541
|
+
const route = defineRoute({
|
|
542
|
+
method: "POST",
|
|
543
|
+
path: "/json-only",
|
|
544
|
+
inputSchema: z.object({ name: z.string() }),
|
|
545
|
+
handler: async ({ input }, { json }) => {
|
|
546
|
+
const body = await input.valid();
|
|
547
|
+
return json(body);
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
const fragment = instantiate(definition)
|
|
552
|
+
.withRoutes([route])
|
|
553
|
+
.withOptions({ mountRoute: "/api" })
|
|
554
|
+
.build();
|
|
555
|
+
|
|
556
|
+
const formData = new FormData();
|
|
557
|
+
formData.append("name", "test");
|
|
558
|
+
|
|
559
|
+
const request = new Request("http://localhost/api/json-only", {
|
|
560
|
+
method: "POST",
|
|
561
|
+
body: formData,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const response = await fragment.handler(request);
|
|
565
|
+
|
|
566
|
+
expect(response.status).toBe(415);
|
|
567
|
+
const data = await response.json();
|
|
568
|
+
expect(data.code).toBe("UNSUPPORTED_MEDIA_TYPE");
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("should reject JSON for FormData routes", async () => {
|
|
572
|
+
const definition = defineFragment("test-fragment").build();
|
|
573
|
+
|
|
574
|
+
const route = defineRoute({
|
|
575
|
+
method: "POST",
|
|
576
|
+
path: "/upload",
|
|
577
|
+
contentType: "multipart/form-data",
|
|
578
|
+
handler: async (ctx, { json }) => {
|
|
579
|
+
// Verify formData() works (would throw if not FormData)
|
|
580
|
+
ctx.formData();
|
|
581
|
+
return json({ success: true });
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const fragment = instantiate(definition)
|
|
586
|
+
.withRoutes([route])
|
|
587
|
+
.withOptions({ mountRoute: "/api" })
|
|
588
|
+
.build();
|
|
589
|
+
|
|
590
|
+
const request = new Request("http://localhost/api/upload", {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "Content-Type": "application/json" },
|
|
593
|
+
body: JSON.stringify({ name: "test" }),
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const response = await fragment.handler(request);
|
|
597
|
+
|
|
598
|
+
expect(response.status).toBe(415);
|
|
599
|
+
const data = await response.json();
|
|
600
|
+
expect(data.code).toBe("UNSUPPORTED_MEDIA_TYPE");
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("should accept octet-stream for routes with contentType: application/octet-stream", async () => {
|
|
604
|
+
const definition = defineFragment("test-fragment").build();
|
|
605
|
+
|
|
606
|
+
const route = defineRoute({
|
|
607
|
+
method: "PUT",
|
|
608
|
+
path: "/stream",
|
|
609
|
+
contentType: "application/octet-stream",
|
|
610
|
+
handler: async (ctx, { json }) => {
|
|
611
|
+
const stream = ctx.bodyStream();
|
|
612
|
+
const text = await new Response(stream).text();
|
|
613
|
+
return json({ received: text });
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const fragment = instantiate(definition)
|
|
618
|
+
.withRoutes([route])
|
|
619
|
+
.withOptions({ mountRoute: "/api" })
|
|
620
|
+
.build();
|
|
621
|
+
|
|
622
|
+
const request = new Request("http://localhost/api/stream", {
|
|
623
|
+
method: "PUT",
|
|
624
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
625
|
+
body: new TextEncoder().encode("hello"),
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const response = await fragment.handler(request);
|
|
629
|
+
|
|
630
|
+
expect(response.status).toBe(200);
|
|
631
|
+
const data = await response.json();
|
|
632
|
+
expect(data).toEqual({ received: "hello" });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("should reject JSON for octet-stream routes", async () => {
|
|
636
|
+
const definition = defineFragment("test-fragment").build();
|
|
637
|
+
|
|
638
|
+
const route = defineRoute({
|
|
639
|
+
method: "PUT",
|
|
640
|
+
path: "/stream",
|
|
641
|
+
contentType: "application/octet-stream",
|
|
642
|
+
handler: async (ctx, { json }) => {
|
|
643
|
+
if (!ctx.isBodyStream()) {
|
|
644
|
+
throw new Error("Expected stream body");
|
|
645
|
+
}
|
|
646
|
+
return json({ ok: true });
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const fragment = instantiate(definition)
|
|
651
|
+
.withRoutes([route])
|
|
652
|
+
.withOptions({ mountRoute: "/api" })
|
|
653
|
+
.build();
|
|
654
|
+
|
|
655
|
+
const request = new Request("http://localhost/api/stream", {
|
|
656
|
+
method: "PUT",
|
|
657
|
+
headers: { "Content-Type": "application/json" },
|
|
658
|
+
body: JSON.stringify({ hello: "world" }),
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const response = await fragment.handler(request);
|
|
662
|
+
|
|
663
|
+
expect(response.status).toBe(415);
|
|
664
|
+
const data = await response.json();
|
|
665
|
+
expect(data.code).toBe("UNSUPPORTED_MEDIA_TYPE");
|
|
666
|
+
});
|
|
453
667
|
});
|
|
454
668
|
|
|
455
669
|
describe("callRoute", () => {
|
|
@@ -1285,6 +1499,78 @@ describe("fragment-instantiator", () => {
|
|
|
1285
1499
|
expect(linkedFragment?.name).toBe("linked-fragment");
|
|
1286
1500
|
});
|
|
1287
1501
|
|
|
1502
|
+
it("should mount internal linked fragment routes under /_internal", async () => {
|
|
1503
|
+
const linkedFragmentDef = defineFragment("linked-fragment").build();
|
|
1504
|
+
const linkedRoutes = defineRoutes(linkedFragmentDef).create(({ defineRoute }) => [
|
|
1505
|
+
defineRoute({
|
|
1506
|
+
method: "GET",
|
|
1507
|
+
path: "/status",
|
|
1508
|
+
handler: async (_input, { json }) => {
|
|
1509
|
+
return json({ ok: true });
|
|
1510
|
+
},
|
|
1511
|
+
}),
|
|
1512
|
+
]);
|
|
1513
|
+
|
|
1514
|
+
const definition = defineFragment("main-fragment")
|
|
1515
|
+
.withLinkedFragment("_fragno_internal", ({ config, options }) => {
|
|
1516
|
+
return instantiate(linkedFragmentDef)
|
|
1517
|
+
.withConfig(config)
|
|
1518
|
+
.withOptions(options)
|
|
1519
|
+
.withRoutes([linkedRoutes])
|
|
1520
|
+
.build();
|
|
1521
|
+
})
|
|
1522
|
+
.build();
|
|
1523
|
+
|
|
1524
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
1525
|
+
|
|
1526
|
+
const response = await fragment.callRouteRaw("GET", "/_internal/status" as never);
|
|
1527
|
+
expect(response.status).toBe(200);
|
|
1528
|
+
await expect(response.json()).resolves.toEqual({ ok: true });
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
it("should run middleware set on the internal fragment", async () => {
|
|
1532
|
+
const linkedFragmentDef = defineFragment("linked-fragment").build();
|
|
1533
|
+
const linkedRoutes = defineRoutes(linkedFragmentDef).create(({ defineRoute }) => [
|
|
1534
|
+
defineRoute({
|
|
1535
|
+
method: "GET",
|
|
1536
|
+
path: "/status",
|
|
1537
|
+
handler: async (_input, { json }) => {
|
|
1538
|
+
return json({ ok: true });
|
|
1539
|
+
},
|
|
1540
|
+
}),
|
|
1541
|
+
]);
|
|
1542
|
+
|
|
1543
|
+
const definition = defineFragment("main-fragment")
|
|
1544
|
+
.withLinkedFragment("_fragno_internal", ({ config, options }) => {
|
|
1545
|
+
return instantiate(linkedFragmentDef)
|
|
1546
|
+
.withConfig(config)
|
|
1547
|
+
.withOptions(options)
|
|
1548
|
+
.withRoutes([linkedRoutes])
|
|
1549
|
+
.build();
|
|
1550
|
+
})
|
|
1551
|
+
.build();
|
|
1552
|
+
|
|
1553
|
+
const fragment = instantiate(definition).withOptions({ mountRoute: "/api" }).build();
|
|
1554
|
+
|
|
1555
|
+
const internalFragment = fragment.$internal.linkedFragments._fragno_internal;
|
|
1556
|
+
internalFragment.withMiddleware(async ({ ifMatchesRoute }) => {
|
|
1557
|
+
const result = await ifMatchesRoute("GET", "/status", async (_input, { json }) => {
|
|
1558
|
+
return json({ ok: false, source: "internal-middleware" }, 418);
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
return result;
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
const response = await fragment.handler(
|
|
1565
|
+
new Request("http://localhost/api/_internal/status", {
|
|
1566
|
+
method: "GET",
|
|
1567
|
+
}),
|
|
1568
|
+
);
|
|
1569
|
+
|
|
1570
|
+
expect(response.status).toBe(418);
|
|
1571
|
+
expect(await response.json()).toEqual({ ok: false, source: "internal-middleware" });
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1288
1574
|
it("should pass config and options to linked fragments", () => {
|
|
1289
1575
|
interface Config {
|
|
1290
1576
|
value: string;
|