shokupan 0.10.0 → 0.10.2

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.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './context';
2
2
  export * from './middleware';
3
+ export * from './router';
3
4
  export * from './shokupan';
4
5
  export * from './util/decorators';
5
6
  export * from './util/di';
@@ -7,6 +8,7 @@ export * from './util/request';
7
8
  export * from './util/response';
8
9
  export * from './util/symbol';
9
10
  export * from './util/types';
11
+ export * from './plugins/application/api-explorer/plugin';
10
12
  export * from './plugins/application/asyncapi/plugin';
11
13
  export * from './plugins/application/auth';
12
14
  export * from './plugins/application/cluster';
package/dist/index.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { inspect } from "node:util";
4
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
5
- import { dump } from "js-yaml";
6
- import { AsyncLocalStorage } from "node:async_hooks";
7
4
  import { RecordId, Surreal } from "surrealdb";
8
5
  import { Eta } from "eta";
9
6
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
10
7
  import { resolve, join, sep, basename } from "path";
8
+ import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
9
+ import { dump } from "js-yaml";
10
+ import { AsyncLocalStorage } from "node:async_hooks";
11
11
  import * as os from "node:os";
12
12
  import os__default from "node:os";
13
- import { dirname, join as join$1 } from "node:path";
14
- import { fileURLToPath } from "node:url";
15
13
  import renderToString from "preact-render-to-string";
16
14
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
15
+ import { dirname, join as join$1 } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
17
  import cluster from "node:cluster";
18
18
  import net from "node:net";
19
19
  import { monitorEventLoopDelay } from "node:perf_hooks";
@@ -1317,40 +1317,6 @@ async function generateOpenApi(rootRouter, options = {}) {
1317
1317
  "x-tagGroups": xTagGroups
1318
1318
  };
1319
1319
  }
1320
- class RequestContextStore {
1321
- request;
1322
- span;
1323
- }
1324
- const asyncContext = new AsyncLocalStorage();
1325
- class HttpError extends Error {
1326
- status;
1327
- constructor(message, status) {
1328
- super(message);
1329
- this.name = "HttpError";
1330
- this.status = status;
1331
- if (Error.captureStackTrace) {
1332
- Error.captureStackTrace(this, HttpError);
1333
- }
1334
- }
1335
- }
1336
- function getErrorStatus(err) {
1337
- if (!err || typeof err !== "object") {
1338
- return 500;
1339
- }
1340
- if (typeof err.status === "number") {
1341
- return err.status;
1342
- }
1343
- if (typeof err.statusCode === "number") {
1344
- return err.statusCode;
1345
- }
1346
- return 500;
1347
- }
1348
- class EventError extends HttpError {
1349
- constructor(message = "Event Error") {
1350
- super(message, 500);
1351
- this.name = "EventError";
1352
- }
1353
- }
1354
1320
  const eta = new Eta();
1355
1321
  function serveStatic(config, prefix) {
1356
1322
  const rootPath = resolve(config.root || ".");
@@ -1536,6 +1502,35 @@ function Inject(token) {
1536
1502
  });
1537
1503
  };
1538
1504
  }
1505
+ class HttpError extends Error {
1506
+ status;
1507
+ constructor(message, status) {
1508
+ super(message);
1509
+ this.name = "HttpError";
1510
+ this.status = status;
1511
+ if (Error.captureStackTrace) {
1512
+ Error.captureStackTrace(this, HttpError);
1513
+ }
1514
+ }
1515
+ }
1516
+ function getErrorStatus(err) {
1517
+ if (!err || typeof err !== "object") {
1518
+ return 500;
1519
+ }
1520
+ if (typeof err.status === "number") {
1521
+ return err.status;
1522
+ }
1523
+ if (typeof err.statusCode === "number") {
1524
+ return err.statusCode;
1525
+ }
1526
+ return 500;
1527
+ }
1528
+ class EventError extends HttpError {
1529
+ constructor(message = "Event Error") {
1530
+ super(message, 500);
1531
+ this.name = "EventError";
1532
+ }
1533
+ }
1539
1534
  const tracer = trace.getTracer("shokupan.middleware");
1540
1535
  function traceHandler(fn, name) {
1541
1536
  return async function(...args) {
@@ -1725,6 +1720,8 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1725
1720
  RouteParamType2["CONTEXT"] = "CONTEXT";
1726
1721
  return RouteParamType2;
1727
1722
  })(RouteParamType || {});
1723
+ const RouterRegistry = /* @__PURE__ */ new Map();
1724
+ const ShokupanApplicationTree = {};
1728
1725
  class ShokupanRouter {
1729
1726
  constructor(config) {
1730
1727
  this.config = config;
@@ -2719,6 +2716,11 @@ class ShokupanRouter {
2719
2716
  }
2720
2717
  }
2721
2718
  }
2719
+ class RequestContextStore {
2720
+ request;
2721
+ span;
2722
+ }
2723
+ const asyncContext = new AsyncLocalStorage();
2722
2724
  class SystemCpuMonitor {
2723
2725
  constructor(intervalMs = 1e3) {
2724
2726
  this.intervalMs = intervalMs;
@@ -3523,6 +3525,281 @@ function Event(eventName) {
3523
3525
  function RateLimit(options) {
3524
3526
  return Use(RateLimitMiddleware(options));
3525
3527
  }
3528
+ function ApiExplorerApp({ spec, asyncSpec, config }) {
3529
+ const hierarchy = /* @__PURE__ */ new Map();
3530
+ const addRoute = (groupKey, route) => {
3531
+ if (!hierarchy.has(groupKey)) {
3532
+ hierarchy.set(groupKey, []);
3533
+ }
3534
+ hierarchy.get(groupKey).push(route);
3535
+ };
3536
+ const getGroupKey = (op, source) => {
3537
+ if (op.tags && op.tags.length > 0) {
3538
+ const tag = typeof op.tags[0] === "string" ? op.tags[0] : op.tags[0].name;
3539
+ if (!tag.startsWith("/")) return tag;
3540
+ }
3541
+ if (source?.className) return source.className;
3542
+ if (source?.file) {
3543
+ const parts = source.file.split("/");
3544
+ const filename = parts[parts.length - 1].replace(/\.(ts|js)$/, "");
3545
+ return filename.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3546
+ }
3547
+ if (op.tags && op.tags.length > 0) {
3548
+ const tag = typeof op.tags[0] === "string" ? op.tags[0] : op.tags[0].name;
3549
+ return tag;
3550
+ }
3551
+ return "Ungrouped";
3552
+ };
3553
+ const createSubgroups = (routes, depth = 0) => {
3554
+ if (routes.length < 3 || depth > 5) {
3555
+ return routes.map((route) => ({
3556
+ name: route.path,
3557
+ type: "route",
3558
+ path: route.path,
3559
+ routes: [route]
3560
+ }));
3561
+ }
3562
+ const pathSegments = routes.map((r) => {
3563
+ const cleaned = r.path.replace(/^\/|\/$/g, "");
3564
+ return cleaned.split("/");
3565
+ });
3566
+ const prefixGroups = /* @__PURE__ */ new Map();
3567
+ const ungrouped = [];
3568
+ routes.forEach((route, idx) => {
3569
+ const segments = pathSegments[idx];
3570
+ if (segments.length <= depth) {
3571
+ ungrouped.push(route);
3572
+ return;
3573
+ }
3574
+ const prefix = segments.slice(0, depth + 1).join("/");
3575
+ if (!prefixGroups.has(prefix)) {
3576
+ prefixGroups.set(prefix, []);
3577
+ }
3578
+ prefixGroups.get(prefix).push(route);
3579
+ });
3580
+ const result = [];
3581
+ prefixGroups.forEach((groupRoutes, prefix) => {
3582
+ if (groupRoutes.length >= 3) {
3583
+ const prefixName = prefix.split("/").pop() || prefix;
3584
+ result.push({
3585
+ name: prefixName,
3586
+ type: "subgroup",
3587
+ path: "/" + prefix,
3588
+ children: createSubgroups(groupRoutes, depth + 1)
3589
+ });
3590
+ } else {
3591
+ ungrouped.push(...groupRoutes);
3592
+ }
3593
+ });
3594
+ ungrouped.forEach((route) => {
3595
+ result.push({
3596
+ name: route.path,
3597
+ type: "route",
3598
+ path: route.path,
3599
+ routes: [route]
3600
+ });
3601
+ });
3602
+ result.sort((a, b) => {
3603
+ if (a.type === "subgroup" && b.type !== "subgroup") return -1;
3604
+ if (a.type !== "subgroup" && b.type === "subgroup") return 1;
3605
+ return a.name.localeCompare(b.name);
3606
+ });
3607
+ return result;
3608
+ };
3609
+ Object.entries(spec.paths || {}).forEach(([path, methods]) => {
3610
+ Object.entries(methods).forEach(([method, op]) => {
3611
+ if (!op.operationId) {
3612
+ op.operationId = `${method}-${path.replace(/\//g, "-").replace(/[{}:]/g, "")}`;
3613
+ }
3614
+ const route = { method, path, op };
3615
+ const source = op["x-shokupan-source"];
3616
+ const groupKey = getGroupKey(op, source);
3617
+ addRoute(groupKey, route);
3618
+ });
3619
+ });
3620
+ Object.entries(asyncSpec?.channels || {}).forEach(([name, ch]) => {
3621
+ const operations = [];
3622
+ if (ch.publish) operations.push({ method: "recv", op: ch.publish });
3623
+ if (ch.subscribe) operations.push({ method: "send", op: ch.subscribe });
3624
+ operations.forEach(({ method, op }) => {
3625
+ if (!op.operationId) op.operationId = `${method}-${name.replace(/[^a-zA-Z0-9]/g, "-")}`;
3626
+ const route = { method, path: name, op };
3627
+ const source = op["x-shokupan-source"] || op["x-source-info"];
3628
+ const groupKey = getGroupKey(op, source);
3629
+ addRoute(groupKey, route);
3630
+ });
3631
+ });
3632
+ const hierarchicalGroups = Array.from(hierarchy.entries()).map(([name, routes]) => {
3633
+ routes.sort((a, b) => a.path.localeCompare(b.path));
3634
+ const children = createSubgroups(routes);
3635
+ return {
3636
+ name,
3637
+ type: "group",
3638
+ children
3639
+ };
3640
+ }).sort((a, b) => {
3641
+ if (a.name === "Ungrouped") return 1;
3642
+ if (b.name === "Ungrouped") return -1;
3643
+ return a.name.localeCompare(b.name);
3644
+ });
3645
+ const allRoutes = Array.from(hierarchy.values()).flat();
3646
+ return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
3647
+ /* @__PURE__ */ jsxs("head", { children: [
3648
+ /* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
3649
+ /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3650
+ /* @__PURE__ */ jsx("title", { children: spec.info?.title || "API Explorer" }),
3651
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "style.css" }),
3652
+ /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "theme.css" }),
3653
+ /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
3654
+ /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
3655
+ /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
3656
+ __html: `
3657
+ (function() {
3658
+ if (!window.location.pathname.endsWith('/') && !window.location.pathname.split('/').pop().includes('.')) {
3659
+ var newUrl = window.location.pathname + '/' + window.location.search + window.location.hash;
3660
+ window.history.replaceState(null, null, newUrl);
3661
+ window.location.reload();
3662
+ }
3663
+ })();
3664
+ `
3665
+ } })
3666
+ ] }),
3667
+ /* @__PURE__ */ jsxs("body", { class: "dark-theme", children: [
3668
+ /* @__PURE__ */ jsxs("div", { id: "app", children: [
3669
+ /* @__PURE__ */ jsx(Sidebar$1, { spec, hierarchicalGroups }),
3670
+ /* @__PURE__ */ jsx(MainContent$1, { allRoutes, config, spec })
3671
+ ] }),
3672
+ /* @__PURE__ */ jsx("script", { src: "explorer-client.mjs", type: "module" })
3673
+ ] })
3674
+ ] });
3675
+ }
3676
+ function Sidebar$1({ spec, hierarchicalGroups }) {
3677
+ const renderNavNode = (node, depth = 0) => {
3678
+ if (node.type === "route") {
3679
+ const route = node.routes[0];
3680
+ const source = route.op["x-shokupan-source"] || route.op["x-source-info"];
3681
+ const isRuntime = route.op["x-source-info"]?.isRuntime;
3682
+ return /* @__PURE__ */ jsxs("div", { class: "nav-item-wrapper", style: `padding-left: ${depth * 12}px;`, children: [
3683
+ /* @__PURE__ */ jsxs(
3684
+ "a",
3685
+ {
3686
+ href: `#${route.op.operationId}`,
3687
+ class: "nav-item",
3688
+ "data-id": route.op.operationId,
3689
+ title: route.path,
3690
+ children: [
3691
+ /* @__PURE__ */ jsx("span", { class: `badge badge-${route.method.toUpperCase()}`, children: route.method.toUpperCase() }),
3692
+ /* @__PURE__ */ jsx("span", { class: "nav-label", children: node.name }),
3693
+ isRuntime && /* @__PURE__ */ jsx("span", { class: "nav-warning", title: "Static Analysis Failed", children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
3694
+ /* @__PURE__ */ jsx("path", { d: "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
3695
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
3696
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "17", x2: "12.01", y2: "17" })
3697
+ ] }) })
3698
+ ]
3699
+ },
3700
+ route.op.operationId
3701
+ ),
3702
+ source?.file && /* @__PURE__ */ jsx(
3703
+ "a",
3704
+ {
3705
+ href: `vscode://file/${source.file}:${source.line || 1}`,
3706
+ class: "nav-source-link",
3707
+ title: `${source.file}:${source.line || 1}`,
3708
+ children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
3709
+ /* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
3710
+ /* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
3711
+ ] })
3712
+ }
3713
+ )
3714
+ ] });
3715
+ } else if (node.type === "subgroup") {
3716
+ return /* @__PURE__ */ jsxs("div", { class: "nav-subgroup collapsed", style: `padding-left: ${depth * 12}px;`, children: [
3717
+ /* @__PURE__ */ jsxs("div", { class: "nav-subgroup-title", children: [
3718
+ /* @__PURE__ */ jsx("span", { class: "chevron", children: /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3719
+ /* @__PURE__ */ jsx("span", { children: node.name })
3720
+ ] }),
3721
+ /* @__PURE__ */ jsx("div", { class: "nav-subgroup-items", children: node.children?.map((child) => renderNavNode(child, depth + 1)) })
3722
+ ] });
3723
+ }
3724
+ };
3725
+ return /* @__PURE__ */ jsxs("aside", { class: "sidebar", children: [
3726
+ /* @__PURE__ */ jsx("div", { class: "resize-handle" }),
3727
+ /* @__PURE__ */ jsxs("header", { class: "sidebar-header", children: [
3728
+ /* @__PURE__ */ jsx("button", { class: "toggle-sidebar", children: "☰" }),
3729
+ /* @__PURE__ */ jsx("h1", { children: spec.info?.title }),
3730
+ /* @__PURE__ */ jsx("div", { class: "version", children: spec.info?.version })
3731
+ ] }),
3732
+ /* @__PURE__ */ jsx("div", { class: "sidebar-collapse-trigger", children: "➔" }),
3733
+ /* @__PURE__ */ jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxs("div", { class: "nav-group collapsed", children: [
3734
+ /* @__PURE__ */ jsxs("div", { class: "nav-group-title", children: [
3735
+ /* @__PURE__ */ jsx("span", { class: "chevron", children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3736
+ " ",
3737
+ group.name
3738
+ ] }),
3739
+ /* @__PURE__ */ jsx("div", { class: "nav-items", children: group.children?.map((child) => renderNavNode(child, 0)) })
3740
+ ] }, group.name)) })
3741
+ ] });
3742
+ }
3743
+ function MainContent$1({ allRoutes, config, spec }) {
3744
+ const explorerData = JSON.stringify({
3745
+ routes: allRoutes,
3746
+ config,
3747
+ info: spec.info
3748
+ });
3749
+ const safeJson = explorerData.replace(/<\/script>/g, "<\\/script>");
3750
+ return /* @__PURE__ */ jsxs("main", { class: "content", id: "main-content", children: [
3751
+ /* @__PURE__ */ jsx("div", { id: "ide-container", children: /* @__PURE__ */ jsx("div", { class: "empty-state", children: "Select a request to view details" }) }),
3752
+ /* @__PURE__ */ jsx("script", { id: "explorer-data", type: "application/json", dangerouslySetInnerHTML: { __html: safeJson } })
3753
+ ] });
3754
+ }
3755
+ class ApiExplorerPlugin extends ShokupanRouter {
3756
+ constructor(pluginOptions) {
3757
+ super({ renderer: renderToString });
3758
+ this.pluginOptions = pluginOptions;
3759
+ pluginOptions.path ??= "/explorer";
3760
+ const serveFile = async (ctx, file, type) => {
3761
+ const content = await readFile$1(join(__dirname, "static", file), "utf-8");
3762
+ ctx.set("Content-Type", type);
3763
+ return ctx.send(content);
3764
+ };
3765
+ const stripSourceCode = (spec) => {
3766
+ if (!spec || !spec.paths) return spec;
3767
+ Object.values(spec.paths).forEach((methods) => {
3768
+ Object.values(methods).forEach((op) => {
3769
+ if (op["x-source-info"]?.snippet) {
3770
+ delete op["x-source-info"].snippet;
3771
+ }
3772
+ if (op["x-shokupan-source"]?.code) {
3773
+ delete op["x-shokupan-source"].code;
3774
+ }
3775
+ });
3776
+ });
3777
+ return spec;
3778
+ };
3779
+ this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
3780
+ this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
3781
+ this.get("/explorer-client.mjs", (ctx) => serveFile(ctx, "explorer-client.mjs", "application/javascript"));
3782
+ this.get("/_source", async (ctx) => {
3783
+ const file = ctx.query["file"];
3784
+ if (!file) return ctx.text("Missing file parameter", 400);
3785
+ try {
3786
+ const content = await readFile$1(file, "utf-8");
3787
+ return ctx.text(content);
3788
+ } catch (err) {
3789
+ return ctx.text("File not found", 404);
3790
+ }
3791
+ });
3792
+ this.get("/openapi.json", async (ctx) => {
3793
+ const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
3794
+ return ctx.json(stripSourceCode(spec));
3795
+ });
3796
+ this.get("/", async (ctx) => {
3797
+ const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
3798
+ const asyncSpec = ctx.app.asyncApiSpec;
3799
+ return ctx.jsx(ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec }));
3800
+ });
3801
+ }
3802
+ }
3526
3803
  function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3527
3804
  return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
3528
3805
  /* @__PURE__ */ jsxs("head", { children: [
@@ -6364,6 +6641,7 @@ export {
6364
6641
  $url,
6365
6642
  $ws,
6366
6643
  All,
6644
+ ApiExplorerPlugin,
6367
6645
  AsyncApiPlugin,
6368
6646
  AuthPlugin,
6369
6647
  Body,
@@ -6394,13 +6672,16 @@ export {
6394
6672
  RateLimitMiddleware,
6395
6673
  Req,
6396
6674
  RouteParamType,
6675
+ RouterRegistry,
6397
6676
  ScalarPlugin,
6398
6677
  SecurityHeaders,
6399
6678
  Session,
6400
6679
  Shokupan,
6680
+ ShokupanApplicationTree,
6401
6681
  ShokupanContext,
6402
6682
  ShokupanRequest,
6403
6683
  ShokupanResponse,
6684
+ ShokupanRouter,
6404
6685
  Spec,
6405
6686
  Use,
6406
6687
  ValidationError,