shokupan 0.7.0 → 0.9.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.
Files changed (40) hide show
  1. package/README.md +53 -0
  2. package/dist/context.d.ts +50 -15
  3. package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  4. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  5. package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  6. package/dist/http-server-CCeagTyU.js.map +1 -0
  7. package/dist/index.cjs +998 -136
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +996 -135
  11. package/dist/index.js.map +1 -1
  12. package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
  13. package/dist/plugins/application/dashboard/plugin.d.ts +14 -8
  14. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  15. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  16. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  17. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  18. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  19. package/dist/plugins/application/dashboard/static/registry.css +131 -0
  20. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  21. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  22. package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
  23. package/dist/plugins/application/dashboard/static/styles.css +175 -0
  24. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  25. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  26. package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
  27. package/dist/plugins/application/dashboard/template.eta +246 -0
  28. package/dist/plugins/application/socket-io.d.ts +14 -0
  29. package/dist/router.d.ts +12 -0
  30. package/dist/shokupan.d.ts +21 -1
  31. package/dist/util/datastore.d.ts +4 -3
  32. package/dist/util/decorators.d.ts +5 -0
  33. package/dist/util/http-error.d.ts +38 -0
  34. package/dist/util/http-status.d.ts +30 -0
  35. package/dist/util/request.d.ts +1 -1
  36. package/dist/util/symbol.d.ts +19 -0
  37. package/dist/util/types.d.ts +30 -1
  38. package/package.json +6 -3
  39. package/dist/http-server-0xH174zz.js.map +0 -1
  40. package/dist/http-server-DFhwlK8e.cjs.map +0 -1
@@ -0,0 +1,131 @@
1
+ .tree-node {
2
+ margin-left: 1rem;
3
+ border-left: 1px solid #40516a;
4
+ padding-left: 1rem;
5
+ }
6
+
7
+ .tree-item {
8
+ margin: 0.25rem 0;
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 0.5rem;
12
+ }
13
+
14
+ .tree-icon {
15
+ color: var(--text-secondary);
16
+ font-size: 0.8em;
17
+ }
18
+
19
+ .tree-label {
20
+ color: var(--text-primary);
21
+ }
22
+
23
+ .tree-meta {
24
+ color: var(--text-secondary);
25
+ font-size: 0.8em;
26
+ margin-left: 0.5rem;
27
+ font-style: italic;
28
+ }
29
+
30
+ .path-param {
31
+ color: var(--text-emphasis);
32
+ }
33
+
34
+ .path-end,
35
+ .path-param {
36
+ font-weight: bold;
37
+ }
38
+
39
+ .badge {
40
+ padding: 2px 6px;
41
+ border-radius: 4px;
42
+ font-size: 0.7em;
43
+ font-weight: bold;
44
+ }
45
+
46
+ .badge-GET {
47
+ background: #0f172a;
48
+ color: #3b82f6;
49
+ border: 1px solid #3b82f6;
50
+ }
51
+
52
+ .badge-POST {
53
+ background: #0f172a;
54
+ color: #22c55e;
55
+ border: 1px solid #22c55e;
56
+ }
57
+
58
+ .badge-PUT {
59
+ background: #0f172a;
60
+ color: #eab308;
61
+ border: 1px solid #eab308;
62
+ }
63
+
64
+ .badge-PATCH {
65
+ background: #0f172a;
66
+ color: #08d3ea;
67
+ border: 1px solid #08eae6;
68
+ }
69
+
70
+ .badge-HEAD {
71
+ background: #0f172a;
72
+ color: #e3ea08;
73
+ border: 1px solid #e3ea08;
74
+ }
75
+
76
+ .badge-OPTIONS {
77
+ background: #0f172a;
78
+ color: #838383;
79
+ border: 1px solid #838383;
80
+ }
81
+
82
+ .badge-DELETE {
83
+ background: #0f172a;
84
+ color: #ef4444;
85
+ border: 1px solid #ef4444;
86
+ }
87
+
88
+ .badge-ROUTER {
89
+ background: #334155;
90
+ color: #f8fafc;
91
+ }
92
+
93
+ .badge-CONTROLLER {
94
+ background: #475569;
95
+ color: #f8fafc;
96
+ }
97
+
98
+
99
+ /* Tooltip Styling */
100
+ .tooltip {
101
+ position: relative;
102
+ display: block;
103
+ }
104
+
105
+ .tooltip .tooltip-text {
106
+ visibility: hidden;
107
+ width: 220px;
108
+ background-color: var(--bg-primary);
109
+ color: var(--text-primary);
110
+ text-align: left;
111
+ border-radius: 6px;
112
+ padding: 8px;
113
+ position: absolute;
114
+ z-index: 1000;
115
+ bottom: 125%;
116
+ /* Position above */
117
+ left: 50%;
118
+ margin-left: -110px;
119
+ opacity: 0;
120
+ transition: opacity 0.2s;
121
+ border: 1px solid var(--card-border);
122
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
123
+ backdrop-filter: blur(12px);
124
+ font-size: 0.85rem;
125
+ pointer-events: none;
126
+ }
127
+
128
+ .tooltip:hover .tooltip-text {
129
+ visibility: visible;
130
+ opacity: 1;
131
+ }
@@ -0,0 +1,269 @@
1
+
2
+ window.renderRegistry = function renderRegistry(node, container) {
3
+ const rootPath = "<%~ it.rootPath %>";
4
+ const linkPattern = "<%~ it.linkPattern %>";
5
+
6
+ if (!node) {
7
+ container.innerHTML = '<div style="color: var(--text-secondary)">No registry data available</div>';
8
+ return;
9
+ }
10
+
11
+ const wrapper = document.createElement('div');
12
+
13
+ // Helper to clean paths
14
+ const cleanPath = (p) => {
15
+ if (!p) return '';
16
+ if (p.startsWith(rootPath)) return p.slice(rootPath.length + 1);
17
+ return p;
18
+ };
19
+
20
+ // Helper to create file link
21
+ const createFileMeta = (metadata, defaultName) => {
22
+ if (metadata && metadata.file) {
23
+ const relative = cleanPath(metadata.file);
24
+ const absolute = metadata.file;
25
+ const line = metadata.line;
26
+
27
+ // Generate Link
28
+ let link = linkPattern
29
+ .replace('{{absolute}}', absolute)
30
+ .replace('{{relative}}', relative)
31
+ .replace('{{line}}', line);
32
+
33
+ const name = metadata.name || defaultName;
34
+
35
+ // Label Filtering
36
+ const blocklist = ['wrappedHandler', 'anonymous', 'finalHandler', 'routeHandler'];
37
+ let displayNameStr = '';
38
+ if (name && !blocklist.includes(name)) {
39
+ displayNameStr = ` <span style="color: #94a3b8; font-size: 0.8em;">(${name})</span>`;
40
+ }
41
+
42
+ const builtin = metadata.isBuiltin ? `<span class="badge" style="background: #059669; margin-left:10px;">BUILTIN</span>` : '';
43
+ const pluginName = metadata.pluginName ? `<span style="color: #6ee7b7; margin-left: 5px;">[${metadata.pluginName}]</span>` : '';
44
+
45
+ return `<a href="${link}" target="_blank" style="text-decoration: none; color: inherit;"><span class="tree-meta" style="cursor: pointer; text-decoration: underline;">${relative}:${line}</span></a>${displayNameStr} ${builtin} ${pluginName}`;
46
+ }
47
+ return '';
48
+ };
49
+
50
+ // 1. Flatten all items
51
+ const allItems = [];
52
+ if (node.middleware) node.middleware.forEach(i => allItems.push({ ...i, kind: 'middleware' }));
53
+ if (node.routes) node.routes.forEach(i => allItems.push({ ...i, kind: 'route' }));
54
+ if (node.routers) node.routers.forEach(i => allItems.push({ ...i, kind: 'router' }));
55
+ if (node.controllers) node.controllers.forEach(i => allItems.push({ ...i, kind: 'controller' }));
56
+
57
+ // 2. Sort by Order
58
+ const kindPriority = { 'middleware': 0, 'router': 1, 'controller': 2, 'route': 3 };
59
+ allItems.sort((a, b) => {
60
+ const pA = kindPriority[a.kind] !== undefined ? kindPriority[a.kind] : 99;
61
+ const pB = kindPriority[b.kind] !== undefined ? kindPriority[b.kind] : 99;
62
+ if (pA !== pB) return pA - pB;
63
+ return (a.order || 0) - (b.order || 0);
64
+ });
65
+
66
+ // Deduplication (by ID if available, or path+method)
67
+ const uniqueItems = [];
68
+ const seenIds = new Set();
69
+ allItems.forEach(item => {
70
+ const uniqueKey = item.id || (item.kind + ':' + (item.path || item.name));
71
+ if (!seenIds.has(uniqueKey)) {
72
+ seenIds.add(uniqueKey);
73
+ uniqueItems.push(item);
74
+ }
75
+ });
76
+ // Replace allItems with deduplicated list
77
+ allItems.length = 0;
78
+ allItems.push(...uniqueItems);
79
+
80
+ // 3. Render
81
+ const renderedRoutes = new Set(); // Track rendered routes to avoid duplication when grouping
82
+
83
+ // Let's rebuild the controller groups map using the flattened objects
84
+ const controllerGroups = new Map();
85
+ allItems.forEach(item => {
86
+ if (item.kind === 'route' && item.tags && item.tags.length > 0) {
87
+ const tag = item.tags[0];
88
+ if (!controllerGroups.has(tag)) controllerGroups.set(tag, []);
89
+ controllerGroups.get(tag).push(item);
90
+ }
91
+ });
92
+
93
+ function getTooltipHtml(id) {
94
+ // Return default 0 metrics if not found, rather than empty string, so tooltip always appears if intended
95
+ const metrics = window.metrics || {};
96
+ const m = (metrics.nodeMetrics && metrics.nodeMetrics[id]) ? metrics.nodeMetrics[id] : { requests: 0, failures: 0 };
97
+ const totalReqs = metrics.totalRequests || 1; // avoid div/0
98
+ const percent = ((m.requests / totalReqs) * 100).toFixed(1);
99
+ const failRate = m.requests > 0 ? ((m.failures / m.requests) * 100).toFixed(1) : '0.0';
100
+
101
+ return `
102
+ <div class="tooltip-text">
103
+ <div style="font-weight:bold; margin-bottom:4px; border-bottom:1px solid var(--text-secondary); padding-bottom:2px;">Metrics</div>
104
+ <div style="display:flex; justify-content:space-between;"><span>Requests:</span> <span style="font-family:monospace">${m.requests}</span></div>
105
+ <div style="display:flex; justify-content:space-between;"><span>Traffic:</span> <span style="font-family:monospace">${percent}%</span></div>
106
+ <div style="display:flex; justify-content:space-between;"><span>Failures:</span> <span style="font-family:monospace; color:${m.failures > 0 ? '#ef4444' : 'inherit'}">${m.failures} (${failRate}%)</span></div>
107
+ </div>
108
+ `;
109
+ }
110
+
111
+ function renderPath(path) {
112
+ const parts = path.split('/').slice(1);
113
+
114
+ let out = '';
115
+ parts.forEach((part, index) => {
116
+ if (part.startsWith(":")) {
117
+ out += `/<span class="path-segment path-param">${part}</span>`;
118
+ return;
119
+ }
120
+ if (index === parts.length - 1) {
121
+ out += `/<span class="path-segment path-end">${part}</span>`;
122
+ return;
123
+ };
124
+ out += `/<span class="path-segment">${part}</span>`;
125
+ });
126
+
127
+ return out;
128
+ }
129
+
130
+ allItems.forEach(item => {
131
+ // Middleware
132
+ if (item.kind === 'middleware') {
133
+ const div = document.createElement('div');
134
+ const mwContainer = document.createElement('div');
135
+ mwContainer.className = 'tree-node';
136
+
137
+ const mwDiv = document.createElement('div');
138
+ mwDiv.className = 'tree-item tooltip'; // Add tooltip class
139
+ const meta = createFileMeta(item.metadata, item.name);
140
+ const tooltipHtml = getTooltipHtml(item.id);
141
+ mwDiv.innerHTML = `<span class="badge" style="background: #9333ea; color: white;">MIDDLEWARE</span> <span class="tree-label">${item.name}</span>${meta}${tooltipHtml}`;
142
+
143
+ mwContainer.appendChild(mwDiv);
144
+ div.appendChild(mwDiv);
145
+ wrapper.appendChild(div);
146
+ }
147
+
148
+ // Router
149
+ else if (item.kind === 'router') {
150
+ const div = document.createElement('div');
151
+ const header = document.createElement('div');
152
+ header.className = 'tree-item tooltip'; // Add tooltip class
153
+ const meta = createFileMeta(item.metadata, 'Router');
154
+ const tooltipHtml = getTooltipHtml(item.id);
155
+ header.innerHTML = `
156
+ <span class="badge badge-ROUTER">ROUTER</span>
157
+ <span class="tree-label">${renderPath(item.path)}</span>
158
+ ${meta}
159
+ ${tooltipHtml}
160
+ `;
161
+ div.appendChild(header);
162
+
163
+ if (item.children) {
164
+ const childrenContainer = document.createElement('div');
165
+ childrenContainer.className = 'tree-node';
166
+ renderRegistry(item.children, childrenContainer);
167
+ div.appendChild(childrenContainer);
168
+ }
169
+ wrapper.appendChild(div);
170
+ }
171
+
172
+ // Controller
173
+ else if (item.kind === 'controller') {
174
+ // Render Controller Group
175
+ const name = item.name;
176
+ const cPath = item.path || '';
177
+
178
+ // Render Header
179
+ const div = document.createElement('div');
180
+ const header = document.createElement('div');
181
+ header.className = 'tree-item tooltip'; // Add tooltip class
182
+ const meta = createFileMeta(item.metadata, name);
183
+ const tooltipHtml = getTooltipHtml(item.id);
184
+ header.innerHTML = `
185
+ <span class="badge badge-CONTROLLER">CTRL</span>
186
+ <span class="tree-label" style="font-weight: bold;">${name} <span style="color:var(--text-secondary); font-weight:normal;">(${renderPath(cPath)})</span></span>
187
+ ${meta}
188
+ ${tooltipHtml}
189
+ `;
190
+ div.appendChild(header);
191
+
192
+ // Render Routes belonging to this controller
193
+ const routes = item.children.routes || [];
194
+ if (routes.length > 0) {
195
+ const routesContainer = document.createElement('div');
196
+ routesContainer.className = 'tree-node';
197
+
198
+ routes.forEach(r => {
199
+ renderedRoutes.add(r); // Mark as rendered
200
+
201
+ const rDiv = document.createElement('div');
202
+ rDiv.className = 'tree-item tooltip'; // Add tooltip class
203
+ const method = r.method.toUpperCase();
204
+ const badgeClass = `badge-${method}`;
205
+ const rMeta = createFileMeta(r.metadata, r.handlerName);
206
+ const tHtml = getTooltipHtml(r.id);
207
+ rDiv.innerHTML = `
208
+ <span class="badge ${badgeClass}" style="width: 50px; text-align: center; display: inline-block;">${method}</span>
209
+ <span class="tree-label">${renderPath(r.path)}</span>
210
+ ${rMeta}
211
+ ${tHtml}
212
+ `;
213
+ routesContainer.appendChild(rDiv);
214
+ });
215
+ div.appendChild(routesContainer);
216
+ } else {
217
+ // Empty controller or no tagged routes found
218
+ // It's still valid to show it
219
+ const note = document.createElement('div');
220
+ note.className = 'tree-node';
221
+ note.innerHTML = '<span style="color: #64748b; font-style: italic;">(No routes detected)</span>';
222
+ div.appendChild(note);
223
+ }
224
+ wrapper.appendChild(div);
225
+ }
226
+
227
+ // Route (Loose)
228
+ else if (item.kind === 'route') {
229
+ if (renderedRoutes.has(item)) return; // Skip if already rendered in controller
230
+
231
+ const div = document.createElement('div');
232
+ div.className = 'tree-item tooltip'; // Add tooltip class
233
+ const method = item.method.toUpperCase();
234
+ const badgeClass = `badge-${method}`;
235
+ const meta = createFileMeta(item.metadata, item.handlerName);
236
+ const tHtml = getTooltipHtml(item.id);
237
+
238
+ div.innerHTML = `
239
+ <span class="badge ${badgeClass}" style="width: 50px; text-align: center; display: inline-block;">${method}</span>
240
+ <span class="tree-label">${renderPath(item.path)}</span>
241
+ ${meta}
242
+ ${tHtml}
243
+ `;
244
+ wrapper.appendChild(div);
245
+ }
246
+ });
247
+ container.innerHTML = '';
248
+ container.appendChild(wrapper);
249
+ };
250
+
251
+ window.fetchRegistry = async function fetchRegistry() {
252
+ const registryContainer = document.getElementById('registry-tree');
253
+ if (!registryContainer) return;
254
+
255
+ const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
256
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
257
+ const url = basePath + '/registry';
258
+
259
+ try {
260
+ const res = await fetch(url, { headers });
261
+ const { registry } = await res.json();
262
+ renderRegistry(window.registryData = registry, registryContainer);
263
+ } catch (e) {
264
+ console.error("Failed to fetch registry", e);
265
+ }
266
+ };
267
+
268
+ document.addEventListener('DOMContentLoaded', fetchRegistry);
269
+
@@ -0,0 +1,118 @@
1
+
2
+ // Initialize Requests Table
3
+ let requestsTable;
4
+
5
+ document.addEventListener('DOMContentLoaded', () => {
6
+ requestsTable = new Tabulator("#requests-list-container", {
7
+ layout: "fitColumns",
8
+ placeholder: "No requests found",
9
+ selectable: 1,
10
+ columns: [
11
+ { title: "Method", field: "method", width: 100 },
12
+ { title: "URL", field: "url" },
13
+ {
14
+ title: "Status",
15
+ field: "status",
16
+ width: 100,
17
+ formatter: function (cell) {
18
+ const status = cell.getValue();
19
+ const color = status >= 500 ? 'red' : status >= 400 ? 'orange' : 'green';
20
+ return `<span style="color: ${color}; font-weight: bold;">${status}</span>`;
21
+ }
22
+ },
23
+ { title: "Duration (ms)", field: "duration", width: 150, formatter: (cell) => printDuration(cell.getValue()) },
24
+ {
25
+ title: "Time",
26
+ field: "timestamp",
27
+ width: 200,
28
+ formatter: function (cell) {
29
+ return new Date(cell.getValue()).toLocaleString();
30
+ }
31
+ },
32
+ {
33
+ title: "",
34
+ width: 80,
35
+ formatter: function (cell) {
36
+ const el = document.createElement("div");
37
+ el.onclick = () => showRequestDetails(cell.getData());
38
+ el.innerHTML = "View";
39
+ return el;
40
+ }
41
+ }
42
+ ],
43
+ data: []
44
+ });
45
+
46
+ // Auto-fetch on load if tab is active (or just fetch initially)
47
+ fetchRequests();
48
+ });
49
+
50
+ function fetchRequests() {
51
+ const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
52
+
53
+ // Determine base path for API requests
54
+ const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
55
+ const url = basePath + '/';
56
+
57
+ fetch(url + 'requests', { headers })
58
+ .then(res => res.json())
59
+ .then(data => {
60
+ if (requestsTable) {
61
+ requestsTable.setData(data.requests);
62
+ }
63
+ })
64
+ .catch(err => console.error("Failed to fetch requests", err));
65
+ }
66
+
67
+ function showRequestDetails(request) {
68
+ const container = document.getElementById('request-details-container');
69
+ const content = document.getElementById('request-details-content');
70
+ const traceContainer = document.getElementById('middleware-trace-container');
71
+
72
+ container.style.display = 'block';
73
+
74
+ // Render Summary
75
+ content.innerHTML = `
76
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
77
+ <div><strong>Method:</strong> ${request.method}</div>
78
+ <div><strong>URL:</strong> ${request.url}</div>
79
+ <div><strong>Status:</strong> ${request.status}</div>
80
+ <div><strong>Duration:</strong> ${printDuration(request.duration)} ms</div>
81
+ <div><strong>Timestamp:</strong> ${new Date(request.timestamp).toLocaleString()}</div>
82
+ </div>
83
+ `;
84
+
85
+ // Render Trace
86
+ if (request.handlerStack && request.handlerStack.length > 0) {
87
+ let html = '<div style="display: flex; flex-direction: column; gap: 4px;">';
88
+
89
+ request.handlerStack.forEach((item, index) => {
90
+ const duration = item.duration || 0;
91
+
92
+ if (index !== 0) {
93
+ html += `<div style="align-self: center">⬇︎</div>`;
94
+ }
95
+
96
+ html += `
97
+ <div style="padding: 8px; border-radius: 4px; background: var(--bg-secondary);">
98
+ <div style="display: flex; justify-content: space-between;">
99
+ <span style="font-weight: bold;">${item.name}</span>
100
+ <span>${printDuration(duration)}</span>
101
+ </div>
102
+ <div style="font-size: 0.8rem; color: var(--text-secondary);">
103
+ ${item.file}:${item.line}
104
+ </div>
105
+ ${item.stateChanges ? `<div style="font-size: 0.8rem; margin-top: 4px; color: #aaa;">State Changes: ${Object.keys(item.stateChanges).join(', ')}</div>` : ''}
106
+ </div>
107
+ `;
108
+ });
109
+
110
+ html += '</div>';
111
+ traceContainer.innerHTML = html;
112
+ } else {
113
+ traceContainer.innerHTML = '<div style="color: var(--text-secondary);">No middleware trace available.</div>';
114
+ }
115
+
116
+ // Scroll to details
117
+ container.scrollIntoView({ behavior: 'smooth' });
118
+ }
@@ -0,0 +1,24 @@
1
+ ::-webkit-scrollbar {
2
+ width: 8px;
3
+ height: 8px;
4
+ background-color: #22355a;
5
+ }
6
+
7
+ ::-webkit-scrollbar-thumb {
8
+ border-radius: 10px;
9
+ background-color: #2a406a;
10
+ box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
11
+ }
12
+
13
+ *:hover::-webkit-scrollbar-thumb {
14
+ background-color: #22468a;
15
+ }
16
+
17
+ ::-webkit-scrollbar-track {
18
+ border-radius: 10px;
19
+ background-color: #0b0f17;
20
+ }
21
+
22
+ ::-webkit-scrollbar-corner {
23
+ box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
24
+ }