shokupan 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,44 +1,45 @@
1
1
  # Shokupan 🍞
2
2
 
3
- > A low-lift modern web framework for Bun
3
+ > A delightful, type-safe web framework for Bun
4
4
 
5
- Shokupan is a high-performance, feature-rich web framework built specifically for Bun. It combines the familiarity of Express.js with modern NestJS-style architecture (Dependency Injection, Controllers) and seamless compatibility with the vast ecosystem of Express plugins — all while maintaining exceptional performance and built-in OpenAPI support.
5
+ **Built for Developer Experience**
6
+ Shokupan is designed to make building APIs delightful again. With zero-config defaults, instant startup times, and full type safety out of the box, you can focus on building your product, not configuring your framework.
6
7
 
7
8
  ### Note: Shokupan is still in alpha and is not guaranteed to be stable. Please use with caution. We will be adding more features and APIs in the future. Please file an issue if you find any bugs or have suggestions for improvement.
8
9
 
9
10
  ## ✨ Features
10
11
 
11
- - 🚀 **Built for Bun** - Native [Bun](https://bun.sh/) performance with optimized routing
12
- - 🎯 **TypeScript First** - Full type safety with decorators and generics
13
- - 📝 **Auto OpenAPI** - Generate [OpenAPI](https://www.openapis.org/) specs automatically from routes
14
- - 🔌 **Rich Plugin System** - CORS, Sessions, Auth, Validation, Rate Limiting, and more
15
- - 🌐 **Flexible Routing** - Express-style routes or decorator-based controllers
16
- - 🔀 **Express Compatible** - Works with [Express](https://expressjs.com/) middleware patterns
17
- - 📊 **Built-in Telemetry** - [OpenTelemetry](https://opentelemetry.io/) instrumentation out of the box
18
- - 🔐 **OAuth2 Support** - GitHub, Google, Microsoft, Apple, Auth0, Okta
19
- - **Multi-validator Support** - Zod, Ajv, TypeBox, Valibot
20
- - 📚 **OpenAPI Docs** - Beautiful OpenAPI documentation with [Scalar](https://scalar.dev/)
21
- - **Short shift** - Very simple migration from [Express](https://expressjs.com/) or [NestJS](https://nestjs.com/) to Shokupan
12
+ - 🎯 **TypeScript First** - End-to-end type safety with decorators and generics. No manual types needed.
13
+ - 🛠️ **Zero Config** - Works effectively out of the box. No complex setup or boilerplate.
14
+ - 🚀 **Built for Bun** - Native [Bun](https://bun.sh/) performance with instant startup.
15
+ - 🔍 **Debug Dashboard** - Visual inspector for your routes, middleware, and request flow.
16
+ - 📝 **Auto OpenAPI** - Generate [OpenAPI](https://www.openapis.org/) specs automatically from routes.
17
+ - 🔌 **Rich Plugin System** - CORS, Sessions, Auth, Validation, Rate Limiting, and more.
18
+ - 🌐 **Flexible Routing** - Express-style routes or decorator-based controllers.
19
+ - 🔀 **Express Compatible** - Works with [Express](https://expressjs.com/) middleware patterns.
20
+ - 📊 **Built-in Telemetry** - [OpenTelemetry](https://opentelemetry.io/) instrumentation out of the box.
21
+ - 🔐 **OAuth2 Support** - GitHub, Google, Microsoft, Apple, Auth0, Okta.
22
+ - **Multi-validator Support** - Zod, Ajv, TypeBox, Valibot.
23
+ - 📚 **OpenAPI Docs** - Beautiful OpenAPI documentation with [Scalar](https://scalar.dev/).
24
+ - ⏩ **Short shift** - Very simple migration from [Express](https://expressjs.com/) or [NestJS](https://nestjs.com/) to Shokupan.
22
25
 
23
- ## 📦 Installation
26
+ ![Shokupan Debug Dashboard](docs/src/assets/debug_dashboard_overview.png)
24
27
 
25
- ```bash
26
- bun add shokupan
27
- ```
28
28
 
29
29
  ## 🚀 Quick Start
30
30
 
31
+ > Bun and TypeScript are recommended for Shokupan, though it also supports Node.js and standard JavaScript.
32
+
31
33
  ```typescript
32
34
  import { Shokupan } from 'shokupan';
35
+ const app = new Shokupan();
33
36
 
34
- const app = new Shokupan({
35
- port: 3000,
36
- development: true
37
- });
37
+ app.get('/', (ctx) => ({ message: 'Hello, World!' }));
38
+ app.get('/hello', (ctx) => "world");
38
39
 
39
- app.get('/', (ctx) => {
40
- return { message: 'Hello, World!' };
41
- });
40
+ app.mount('/scalar', new ScalarPlugin({
41
+ enableStaticAnalysis: true
42
+ }));
42
43
 
43
44
  app.listen();
44
45
  ```
@@ -829,6 +830,119 @@ app.mount('/docs', new ScalarPlugin({
829
830
 
830
831
  The Scalar plugin automatically generates OpenAPI documentation from your routes and controllers!
831
832
 
833
+ ### Proxy
834
+
835
+ Create a reverse proxy to forward requests to another server:
836
+
837
+ ```typescript
838
+ import { Proxy } from 'shokupan';
839
+
840
+ app.use('/api/v1', Proxy({
841
+ target: 'https://api.example.com',
842
+ changeOrigin: true,
843
+ pathRewrite: (path) => path.replace('/api/v1', ''),
844
+ headers: {
845
+ 'X-Custom-Header': 'Proxy'
846
+ }
847
+ }));
848
+
849
+ // Proxy WebSockets
850
+ app.use('/socket', Proxy({
851
+ target: 'ws://ws.example.com',
852
+ ws: true
853
+ }));
854
+ ```
855
+
856
+ ### OpenAPI Validator
857
+
858
+ Validate incoming requests against your generated OpenAPI specification:
859
+
860
+ ```typescript
861
+ import { enableOpenApiValidation } from 'shokupan';
862
+
863
+ const app = new Shokupan({ enableOpenApiGen: true });
864
+
865
+ // Enable validation middleware
866
+ // This validates Body, Query, Params, and Headers against your OpenAPI definitions
867
+ enableOpenApiValidation(app);
868
+
869
+ app.post('/users', {
870
+ parameters: [
871
+ { name: 'apiKey', in: 'header', required: true, schema: { type: 'string' } }
872
+ ],
873
+ requestBody: {
874
+ content: {
875
+ 'application/json': {
876
+ schema: {
877
+ type: 'object',
878
+ required: ['name'],
879
+ properties: {
880
+ name: { type: 'string', minLength: 3 }
881
+ }
882
+ }
883
+ }
884
+ }
885
+ }
886
+ }, (ctx) => {
887
+ return { success: true };
888
+ });
889
+
890
+ // Invalid requests will throw a ValidationError (400 Bad Request)
891
+ ```
892
+
893
+ ### Idempotency
894
+
895
+ Ensure that multiple identical requests do not result in different outcomes (e.g., duplicate payments). This middleware caches the response of the first request and returns it for subsequent requests with the same idempotency key.
896
+
897
+ ```typescript
898
+ import { Idempotency } from 'shokupan';
899
+
900
+ app.post('/payments',
901
+ Idempotency({
902
+ header: 'Idempotency-Key', // default
903
+ ttl: 24 * 60 * 60 * 1000 // default 24h
904
+ }),
905
+ async (ctx) => {
906
+ // Process payment...
907
+ return { status: 'charged' };
908
+ }
909
+ );
910
+ ```
911
+
912
+ ### Failed Request Recorder
913
+
914
+ Automatically record failed requests (500s) for debugging and replay purposes.
915
+
916
+ ```typescript
917
+ import { FailedRequestRecorder } from 'shokupan';
918
+
919
+ app.use(FailedRequestRecorder({
920
+ maxCapacity: 1000,
921
+ ttl: 86400000 // 1 day
922
+ }));
923
+ ```
924
+
925
+ This works great when combined with the Debug Dashboard.
926
+
927
+ ### Debug Dashboard
928
+
929
+ A visual dashboard to inspect your application, view metrics, analyze the middleware graph, and replay failed requests.
930
+
931
+ ```typescript
932
+ import { DebugDashboard } from 'shokupan/plugins/debugview';
933
+
934
+ // Mount the dashboard
935
+ app.mount('/debug', new DebugDashboard({
936
+ retentionMs: 2 * 60 * 60 * 1000, // Keep 2 hours of logs
937
+ getRequestHeaders: () => ({
938
+ 'Authorization': 'Bearer ...' // Headers to using when replaying requests and accessing data APIs
939
+ })
940
+ }));
941
+
942
+ // Available at http://localhost:3000/debug
943
+ ```
944
+
945
+
832
946
  ## 🚀 Advanced Features
833
947
 
834
948
  ### Dependency Injection
@@ -999,8 +1113,40 @@ provider.addSpanProcessor(
999
1113
  provider.register();
1000
1114
  ```
1001
1115
 
1116
+ ### Server Factory (Node.js & Deno)
1117
+
1118
+ Run Shokupan on Node.js or Deno using the server adapter:
1119
+
1120
+ ```typescript
1121
+ import { Shokupan, createHttpServer } from 'shokupan';
1122
+
1123
+ const app = new Shokupan({
1124
+ // Use Node.js http module
1125
+ serverFactory: createHttpServer()
1126
+ });
1127
+
1128
+ app.get('/', () => ({ message: 'Running on Node!' }));
1129
+
1130
+ app.listen(3000);
1131
+ ```
1132
+
1133
+ ### Automatic Backpressure
1134
+
1135
+ Protect your server from overload by shedding load when CPU usage is high:
1136
+
1137
+ ```typescript
1138
+ const app = new Shokupan({
1139
+ // Monitor CPU and reject requests when usage > 80%
1140
+ autoBackpressureFeedback: true,
1141
+ autoBackpressureLevel: 80
1142
+ });
1143
+ ```
1144
+
1145
+ When the threshold is reached, the server will return `429 Too Many Requests`.
1146
+
1002
1147
  ## 📦 Migration Guides
1003
1148
 
1149
+
1004
1150
  ### From Express
1005
1151
 
1006
1152
  Shokupan is designed to feel familiar to Express developers. Here's how to migrate:
@@ -1397,6 +1543,10 @@ app.use(useExpress(compression()));
1397
1543
  ## 🧪 Testing
1398
1544
 
1399
1545
  Shokupan applications are easy to test using Bun's built-in test runner.
1546
+ It can directly pass requests to the application or a router without requiring the
1547
+ server to fully start, bind to a port or listen for connections. This makes mocking
1548
+ the server unnecessary and allows for faster and more reliable testing. Additionally
1549
+ you can directly test authenticated endpoints without the need for a session or cookie.
1400
1550
 
1401
1551
  ```typescript
1402
1552
  import { describe, it, expect } from 'bun:test';
@@ -1427,13 +1577,13 @@ Since Shokupan is built on Bun, deployment is straightforward.
1427
1577
  ### Using Bun
1428
1578
 
1429
1579
  ```bash
1430
- bun run src/index.ts
1580
+ bun run src/main.ts
1431
1581
  ```
1432
1582
 
1433
1583
  ### Docker
1434
1584
 
1435
1585
  ```dockerfile
1436
- FROM oven/bun:1
1586
+ FROM oven/bun:1-alpine
1437
1587
 
1438
1588
  WORKDIR /app
1439
1589
 
@@ -1442,7 +1592,7 @@ RUN bun install --production
1442
1592
 
1443
1593
  EXPOSE 3000
1444
1594
 
1445
- CMD ["bun", "run", "src/index.ts"]
1595
+ CMD ["bun", "run", "src/main.ts"]
1446
1596
  ```
1447
1597
 
1448
1598
  ## 🛠️ CLI Tools
@@ -1530,6 +1680,14 @@ const app = new Shokupan(config?: ShokupanConfig);
1530
1680
  - `hostname?: string` - Hostname (default: "localhost")
1531
1681
  - `development?: boolean` - Development mode (default: auto-detect)
1532
1682
  - `enableAsyncLocalStorage?: boolean` - Enable async context tracking
1683
+ - `enableTracing?: boolean` - Enable OpenTelemetry tracing
1684
+ - `enableOpenApiGen?: boolean` - Enable OpenAPI spec generation (default: true)
1685
+ - `controllersOnly?: boolean` - If true, only allows controllers, disabling app.get/post/etc (default: false)
1686
+ - `requestTimeout?: number` - Global request timeout (ms)
1687
+ - `readTimeout?: number` - Request body read timeout (ms)
1688
+ - `serverFactory?: ServerFactory` - Custom server factory (for Node.js/Deno support)
1689
+ - `autoBackpressureFeedback?: boolean` - Enable automatic load shedding based on CPU usage
1690
+ - `autoBackpressureLevel?: number` - CPU usage % threshold for backpressure (default: 60)
1533
1691
  - `logger?: Logger` - Custom logger instance
1534
1692
 
1535
1693
  **Methods:**
@@ -1551,7 +1709,13 @@ const app = new Shokupan(config?: ShokupanConfig);
1551
1709
 
1552
1710
  ### ShokupanRouter Class
1553
1711
 
1554
- Router for grouping routes.
1712
+ Router for grouping operations, applying middleware, and mounting controllers. Additionally
1713
+ they are effective for creating sub-applications that are independently tested. Routers can
1714
+ have OpenAPI spec applied to all endpoints of the router. Additionally they can be mounted
1715
+ onto the main application or other routers.
1716
+
1717
+ When a router is mounted to an app, if you are using the DebugView plugin you will be able to
1718
+ see it under the Registry tab and the Graph tab.
1555
1719
 
1556
1720
  ```typescript
1557
1721
  const router = new ShokupanRouter(config?: ShokupanRouteConfig);
@@ -1592,6 +1756,13 @@ Request context object.
1592
1756
  - `state: Record<string, any>` - Shared state object
1593
1757
  - `session: any` - Session data (with session plugin)
1594
1758
  - `response: ShokupanResponse` - Response builder
1759
+ - `ip: string` - Client IP address
1760
+ - `hostname: string` - Hostname (e.g. "localhost")
1761
+ - `host: string` - Host (e.g. "localhost:3000")
1762
+ - `protocol: string` - Protocol (http/https)
1763
+ - `secure: boolean` - Whether the request is secure over HTTPS
1764
+ - `origin: string` - Origin URL
1765
+ - `signal: AbortSignal` - Request abort signal (for standard fetch requests)
1595
1766
 
1596
1767
  **Methods:**
1597
1768
  - `set(name: string, value: string): ShokupanContext` - Set a response header
@@ -1602,6 +1773,7 @@ Request context object.
1602
1773
  - `json(data: any, status?: number): ShokupanContext` - Return JSON response
1603
1774
  - `text(data: string, status?: number): ShokupanContext` - Return text response
1604
1775
  - `html(data: string, status?: number): ShokupanContext` - Return HTML response
1776
+ - `jsx(element: any): ShokupanContext` - Render a JSX element
1605
1777
  - `redirect(url: string, status?: number): ShokupanContext` - Redirect response
1606
1778
  - `file(path: string, fileOptions?: BlobPropertyBag, responseOptions?: ResponseInit): Response` - Return file response
1607
1779
 
@@ -1639,7 +1811,6 @@ Container.clear();
1639
1811
  - 🚧 **Framework Plugins** - Drop-in adapters for [Express](https://expressjs.com/), [Koa](https://koajs.com/), and [Elysia](https://elysiajs.com/)
1640
1812
  - 🚧 **Enhanced WebSockets** - Event support and HTTP simulation
1641
1813
  - 🚧 **Benchmarks** - Comprehensive performance comparisons
1642
- - 🚧 **Scaling** - Automatic clustering support
1643
1814
  - 🚧 **RPC Support** - [tRPC](https://trpc.io/) and [gRPC](https://grpc.io/) integration
1644
1815
  - 🚧 **Binary Formats** - [Protobuf](https://protobuf.dev/) and [MessagePack](https://msgpack.org/) support
1645
1816
  - 🚧 **Reliability** - Circuit breaker pattern for resilience
@@ -1652,7 +1823,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
1652
1823
  1. Fork the repository
1653
1824
  2. Create your feature branch (`git checkout -b feature/amazing-feature`)
1654
1825
  3. Commit your changes (`git commit -m 'Add some amazing feature'`)
1655
- 4. Push to the branch (`git push origin feature/amazing-feature`)
1826
+ 4. Publish the branch (`git push origin feature/amazing-feature`)
1656
1827
  5. Open a Pull Request
1657
1828
 
1658
1829
  ## 📝 License
@@ -1664,7 +1835,8 @@ MIT License - see the [LICENSE](LICENSE) file for details.
1664
1835
  - Inspired by [Express](https://expressjs.com/), [Koa](https://koajs.com/), [NestJS](https://nestjs.com/), and [Elysia](https://elysiajs.com/)
1665
1836
  - Built for the amazing [Bun](https://bun.sh/) runtime
1666
1837
  - Powered by [Arctic](https://github.com/pilcrowonpaper/arctic) for OAuth2 support
1838
+ - Tests and Benchmarks created with Antigravity
1667
1839
 
1668
1840
  ---
1669
1841
 
1670
- **Made with 🍞 by the Shokupan team**
1842
+ **Made with ❤️ by the Shokupan team**
package/dist/context.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface HandlerStackItem {
9
9
  file: string;
10
10
  line: number;
11
11
  stateChanges?: Record<string, any>;
12
+ startTime?: number;
13
+ duration?: number;
12
14
  }
13
15
  export interface DebugCollector {
14
16
  trackStep(id: string | undefined, type: string, duration: number, status: 'success' | 'error', error?: any): void;
@@ -51,7 +53,7 @@ export declare class ShokupanContext<State extends Record<string, any> = Record<
51
53
  /**
52
54
  * Client IP address
53
55
  */
54
- get ip(): Bun.SocketAddress;
56
+ get ip(): any;
55
57
  /**
56
58
  * Request hostname (e.g. "localhost")
57
59
  */
package/dist/index.cjs CHANGED
@@ -1397,6 +1397,7 @@ const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).
1397
1397
  DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1398
1398
  DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1399
1399
  DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1400
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1400
1401
  `);
1401
1402
  });
1402
1403
  const datastore = {
@@ -1506,17 +1507,28 @@ class ShokupanRouter {
1506
1507
  currentGuards = [];
1507
1508
  // Registry Accessor
1508
1509
  getComponentRegistry() {
1509
- const routes = this[$routes].map((r) => ({
1510
- type: "route",
1511
- path: r.path,
1512
- method: r.method,
1513
- metadata: r.metadata,
1514
- handlerName: r.handler.name,
1515
- tags: r.handlerSpec?.tags,
1516
- order: r.order,
1517
- _fn: r.handler
1518
- // Expose handler for debugging instrumentation
1519
- }));
1510
+ const controllerRoutesMap = /* @__PURE__ */ new Map();
1511
+ const localRoutes = [];
1512
+ for (const r of this[$routes]) {
1513
+ const entry = {
1514
+ type: "route",
1515
+ path: r.path,
1516
+ method: r.method,
1517
+ metadata: r.metadata,
1518
+ handlerName: r.handler.name,
1519
+ tags: r.handlerSpec?.tags,
1520
+ order: r.order,
1521
+ _fn: r.handler
1522
+ };
1523
+ if (r.controller) {
1524
+ if (!controllerRoutesMap.has(r.controller)) {
1525
+ controllerRoutesMap.set(r.controller, []);
1526
+ }
1527
+ controllerRoutesMap.get(r.controller).push(entry);
1528
+ } else {
1529
+ localRoutes.push(entry);
1530
+ }
1531
+ }
1520
1532
  const mw = this.middleware;
1521
1533
  const middleware = mw ? mw.map((m) => ({
1522
1534
  name: m.name || "middleware",
@@ -1532,18 +1544,19 @@ class ShokupanRouter {
1532
1544
  children: r.getComponentRegistry()
1533
1545
  }));
1534
1546
  const controllers = this[$childControllers].map((c) => {
1547
+ const routes = controllerRoutesMap.get(c) || [];
1535
1548
  return {
1536
1549
  type: "controller",
1537
1550
  path: c[$mountPath] || "/",
1538
1551
  name: c.constructor.name,
1539
- metadata: c.metadata
1540
- // Check if we can store this
1552
+ metadata: c.metadata,
1553
+ children: { routes }
1541
1554
  };
1542
1555
  });
1543
1556
  return {
1544
1557
  metadata: this.metadata,
1545
1558
  middleware,
1546
- routes,
1559
+ routes: localRoutes,
1547
1560
  routers,
1548
1561
  controllers
1549
1562
  };
@@ -1633,7 +1646,7 @@ class ShokupanRouter {
1633
1646
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1634
1647
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1635
1648
  let routesAttached = 0;
1636
- for (const name of methods) {
1649
+ for (const name of Array.from(methods)) {
1637
1650
  if (name === "constructor") continue;
1638
1651
  if (["arguments", "caller", "callee"].includes(name)) continue;
1639
1652
  const originalHandler = instance[name];
@@ -1767,7 +1780,7 @@ class ShokupanRouter {
1767
1780
  const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1768
1781
  const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1769
1782
  const spec = { tags: [tagName], ...userSpec };
1770
- this.add({ method, path: normalizedPath, handler: finalHandler, spec });
1783
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
1771
1784
  }
1772
1785
  }
1773
1786
  if (routesAttached === 0) {
@@ -1988,8 +2001,23 @@ class ShokupanRouter {
1988
2001
  * @param handler - Route handler function
1989
2002
  * @param requestTimeout - Timeout for this route in milliseconds
1990
2003
  */
1991
- add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer }) {
2004
+ add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
1992
2005
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
2006
+ if (this.currentGuards.length > 0) {
2007
+ spec = spec || {};
2008
+ for (const guard of this.currentGuards) {
2009
+ if (guard.spec) {
2010
+ if (guard.spec.responses) {
2011
+ spec.responses = spec.responses || {};
2012
+ Object.assign(spec.responses, guard.spec.responses);
2013
+ }
2014
+ if (guard.spec.security) {
2015
+ spec.security = spec.security || [];
2016
+ spec.security.push(...guard.spec.security);
2017
+ }
2018
+ }
2019
+ }
2020
+ }
1993
2021
  let wrappedHandler = handler;
1994
2022
  const routeGuards = [...this.currentGuards];
1995
2023
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
@@ -2115,7 +2143,8 @@ class ShokupanRouter {
2115
2143
  metadata: {
2116
2144
  file,
2117
2145
  line
2118
- }
2146
+ },
2147
+ controller
2119
2148
  });
2120
2149
  this.trie.insert(method, path2, bakedHandler);
2121
2150
  return this;
@@ -2349,12 +2378,21 @@ class Shokupan extends ShokupanRouter {
2349
2378
  const c = ctx;
2350
2379
  if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2351
2380
  const metadata = middleware.metadata || {};
2352
- c.handlerStack.push({
2381
+ const start = performance.now();
2382
+ const item = {
2353
2383
  name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2354
2384
  file: metadata.file || file,
2355
2385
  line: metadata.line || line,
2356
- isBuiltin: metadata.isBuiltin
2357
- });
2386
+ isBuiltin: metadata.isBuiltin,
2387
+ startTime: start,
2388
+ duration: -1
2389
+ };
2390
+ c.handlerStack.push(item);
2391
+ try {
2392
+ return await middleware(ctx, next);
2393
+ } finally {
2394
+ item.duration = performance.now() - start;
2395
+ }
2358
2396
  }
2359
2397
  return middleware(ctx, next);
2360
2398
  };
@@ -2895,14 +2933,14 @@ function Compression(options = {}) {
2895
2933
  if (ctx._rawBody !== void 0) {
2896
2934
  if (typeof ctx._rawBody === "string") {
2897
2935
  const encoded = new TextEncoder().encode(ctx._rawBody);
2898
- body = encoded.buffer;
2936
+ body = encoded;
2899
2937
  bodySize = encoded.byteLength;
2900
2938
  } else if (ctx._rawBody instanceof Uint8Array) {
2901
- body = ctx._rawBody.buffer;
2939
+ body = ctx._rawBody;
2902
2940
  bodySize = ctx._rawBody.byteLength;
2903
2941
  } else {
2904
2942
  body = ctx._rawBody;
2905
- bodySize = ctx._rawBody.byteLength;
2943
+ bodySize = body.byteLength;
2906
2944
  }
2907
2945
  } else {
2908
2946
  body = await response.arrayBuffer();
@@ -2912,7 +2950,7 @@ function Compression(options = {}) {
2912
2950
  return new Response(body, {
2913
2951
  status: response.status,
2914
2952
  statusText: response.statusText,
2915
- headers: response.headers
2953
+ headers: new Headers(response.headers)
2916
2954
  });
2917
2955
  }
2918
2956
  let compressed;
@@ -3435,7 +3473,8 @@ function enableOpenApiValidation(app) {
3435
3473
  }
3436
3474
  const eta = new eta$2.Eta();
3437
3475
  class ScalarPlugin extends ShokupanRouter {
3438
- constructor(pluginOptions) {
3476
+ constructor(pluginOptions = {}) {
3477
+ pluginOptions.config ??= {};
3439
3478
  super();
3440
3479
  this.pluginOptions = pluginOptions;
3441
3480
  this.init();