shokupan 0.10.1 → 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
@@ -8,6 +8,7 @@ export * from './util/request';
8
8
  export * from './util/response';
9
9
  export * from './util/symbol';
10
10
  export * from './util/types';
11
+ export * from './plugins/application/api-explorer/plugin';
11
12
  export * from './plugins/application/asyncapi/plugin';
12
13
  export * from './plugins/application/auth';
13
14
  export * from './plugins/application/cluster';
package/dist/index.js CHANGED
@@ -10,10 +10,10 @@ import { dump } from "js-yaml";
10
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";
@@ -3525,6 +3525,281 @@ function Event(eventName) {
3525
3525
  function RateLimit(options) {
3526
3526
  return Use(RateLimitMiddleware(options));
3527
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
+ }
3528
3803
  function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3529
3804
  return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
3530
3805
  /* @__PURE__ */ jsxs("head", { children: [
@@ -6366,6 +6641,7 @@ export {
6366
6641
  $url,
6367
6642
  $ws,
6368
6643
  All,
6644
+ ApiExplorerPlugin,
6369
6645
  AsyncApiPlugin,
6370
6646
  AuthPlugin,
6371
6647
  Body,