@jsenv/server 15.5.2 → 16.0.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 (55) hide show
  1. package/README.md +79 -86
  2. package/dist/html/4xx.html +196 -0
  3. package/dist/html/500.html +17 -0
  4. package/dist/html/route_inspector.html +2435 -0
  5. package/dist/js/formidable.js +17 -0
  6. package/dist/js/vendors.js +2001 -0
  7. package/dist/js/ws.js +6863 -0
  8. package/dist/jsenv_server.js +8829 -0
  9. package/package.json +20 -9
  10. package/src/content_negotiation/pick_content_version.js +63 -0
  11. package/src/{fetch_filesystem.js → filesystem/fetch_filesystem.js} +57 -72
  12. package/src/{serve_directory.js → filesystem/serve_directory.js} +1 -2
  13. package/src/interfacing_with_node/from_node_request.js +455 -0
  14. package/src/interfacing_with_node/get_observable_value_type.js +43 -0
  15. package/src/interfacing_with_node/observable.js +91 -75
  16. package/src/interfacing_with_node/observable_from.js +34 -0
  17. package/src/interfacing_with_node/observable_from_file_handle.js +30 -0
  18. package/src/interfacing_with_node/observable_from_node_stream.js +44 -46
  19. package/src/interfacing_with_node/observable_from_node_web_readable_stream.js +32 -0
  20. package/src/interfacing_with_node/observable_from_promise.js +13 -0
  21. package/src/interfacing_with_node/write_node_response.js +80 -35
  22. package/src/internal/colorizeResponseStatus.js +2 -0
  23. package/src/internal/headers_composition.js +42 -15
  24. package/src/internal/multiple-header.js +27 -12
  25. package/src/internal/object_composition.js +2 -2
  26. package/src/internal/response_composition.js +16 -3
  27. package/src/main.js +15 -11
  28. package/src/progressive_response.js +38 -0
  29. package/src/replace_placeholder_in_html.js +12 -0
  30. package/src/request_body_handling.js +22 -0
  31. package/src/router/router.js +878 -0
  32. package/src/server_events.js +431 -0
  33. package/src/service_controller.js +79 -73
  34. package/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js +125 -0
  35. package/src/services/autoreload_on_server_restart/jsenv_service_autoreload_on_server_restart.js +45 -0
  36. package/src/services/cors/jsenv_service_cors.js +10 -18
  37. package/src/services/default_body_4xx_5xx/client/4xx.html +65 -0
  38. package/src/services/default_body_4xx_5xx/jsenv_service_default_body_4xx_5xx.js +82 -0
  39. package/src/services/error_handler/client/500.html +17 -0
  40. package/src/services/error_handler/jsenv_service_error_handler.js +47 -28
  41. package/src/services/internal_client_files/jsenv_service_internal_client_files.js +34 -0
  42. package/src/services/open_file/jsenv_service_open_file.js +37 -0
  43. package/src/services/route_inspector/client/code_block.js +215 -0
  44. package/src/services/route_inspector/client/route_inspector.html +1363 -0
  45. package/src/services/route_inspector/jsenv_service_route_inspector.js +39 -0
  46. package/src/start_server.js +575 -607
  47. package/src/web_socket_response.js +53 -0
  48. package/src/from_fetch_response.js +0 -15
  49. package/src/interfacing_with_node/body.js +0 -81
  50. package/src/interfacing_with_node/request_factory.js +0 -125
  51. package/src/readRequestBody.js +0 -32
  52. package/src/server_timing/timing_measure.js +0 -26
  53. package/src/service_composition/path_to_regexp.js +0 -436
  54. package/src/service_composition/routing.js +0 -31
  55. package/src/sse/sse_room.js +0 -313
@@ -0,0 +1,1363 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Route inspector</title>
7
+ <script type="importmap">
8
+ {
9
+ "imports": {
10
+ "preact": "https://esm.sh/preact@10.23.1",
11
+ "preact/": "https://esm.sh/preact@10.23.1/",
12
+ "@preact/signals": "https://esm.sh/@preact/signals@1.3.0?external=preact",
13
+ "htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact"
14
+ }
15
+ }
16
+ </script>
17
+ <script src="/@jsenv/server/src/services/route_inspector/client/code_block.js?inline"></script>
18
+ <script src="/@jsenv/server/src/services/autoreload_on_server_restart/client/autoreload_on_server_restart.js?inline"></script>
19
+ </head>
20
+ <body>
21
+ <style>
22
+ :root {
23
+ --background-color: #fff;
24
+ --text-color: #333;
25
+ --border-color: #e0e0e0;
26
+ --primary-color: #1976d2;
27
+ --secondary-color: #f5f5f5;
28
+ --success-color: #4caf50;
29
+ --warning-color: #ff9800;
30
+ --method-get-color: #61affe;
31
+ --method-post-color: #49cc90;
32
+ --method-put-color: #ff9800;
33
+ --method-delete-color: #f44336;
34
+ --method-patch-color: #9c27b0;
35
+ --method-options-color: #607d8b;
36
+ --method-head-color: #00bcd4;
37
+ }
38
+
39
+ :root[data-theme="light"] {
40
+ --background-color: #fff;
41
+ --text-color: #333;
42
+ --border-color: #e0e0e0;
43
+ --primary-color: #1976d2;
44
+ --secondary-color: #f5f5f5;
45
+ }
46
+ :root[data-theme="dark"] {
47
+ --background-color: #121212;
48
+ --text-color: #e0e0e0;
49
+ --border-color: #333;
50
+ --primary-color: #4f9eff;
51
+ --secondary-color: #1e1e1e;
52
+ }
53
+
54
+ :root[data-theme="dark"] a {
55
+ color: dodgerblue;
56
+ }
57
+
58
+ body {
59
+ font-family:
60
+ system-ui,
61
+ -apple-system,
62
+ BlinkMacSystemFont,
63
+ "Segoe UI",
64
+ Roboto,
65
+ Arial,
66
+ sans-serif;
67
+ line-height: 1.5;
68
+ color: var(--text-color);
69
+ background-color: var(--background-color);
70
+ margin: 0;
71
+
72
+ transition:
73
+ background-color 0.3s ease,
74
+ color 0.3s ease;
75
+ }
76
+
77
+ .container {
78
+ display: flex;
79
+ flex-direction: row;
80
+ }
81
+ aside {
82
+ width: 250px;
83
+ flex-shrink: 0;
84
+ border-radius: 6px;
85
+ overflow: hidden;
86
+ background: var(--background-color);
87
+ transition:
88
+ width 0.3s ease,
89
+ margin-left 0.3s ease;
90
+ max-height: calc(100vh - 40px);
91
+ overflow-y: auto;
92
+ margin-right: 20px;
93
+ padding-top: 20px;
94
+ padding-left: 10px;
95
+ }
96
+ main {
97
+ flex-grow: 1;
98
+ flex-basis: 0;
99
+ min-width: 0;
100
+ transition: margin-left 0.3s ease;
101
+ padding-top: 10px;
102
+ padding-right: 20px;
103
+ }
104
+
105
+ .header_row {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 10px;
109
+ }
110
+ .note_button_open {
111
+ background: none;
112
+ border: none;
113
+ cursor: pointer;
114
+ border-radius: 50%;
115
+ transition: background-color 0.2s;
116
+ padding: 0;
117
+ color: var(--text-color);
118
+ opacity: 0.7;
119
+ height: 24px;
120
+ }
121
+ .note_button_open:hover {
122
+ background-color: color-mix(
123
+ in srgb,
124
+ var(--primary-color) 15%,
125
+ transparent
126
+ );
127
+ opacity: 1;
128
+ }
129
+ .note_title {
130
+ font-weight: bold;
131
+ font-size: 1.1rem;
132
+ margin-bottom: 10px;
133
+ display: flex;
134
+ align-items: center;
135
+ }
136
+ .note_button_close {
137
+ position: absolute;
138
+ top: 15px;
139
+ right: 15px;
140
+ background: none;
141
+ border: none;
142
+ cursor: pointer;
143
+ padding: 5px;
144
+ border-radius: 50%;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ }
149
+ .note_button_close:hover {
150
+ background-color: var(--secondary-color);
151
+ }
152
+ .note_content {
153
+ background-color: var(--background-color);
154
+ border-radius: 8px;
155
+ padding: 20px;
156
+ max-width: 500px;
157
+ width: 90%;
158
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
159
+ position: relative;
160
+ transform: translateY(-20px);
161
+ transition: transform 0.3s ease;
162
+ }
163
+
164
+ .theme-toggle {
165
+ display: flex;
166
+ align-items: center;
167
+ }
168
+
169
+ #theme-toggle-btn {
170
+ background: none;
171
+ border: none;
172
+ cursor: pointer;
173
+ width: 40px;
174
+ height: 40px;
175
+ border-radius: 50%;
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ transition: background-color 0.2s;
180
+ background-color: var(--secondary-color);
181
+ }
182
+
183
+ #theme-toggle-btn:hover {
184
+ background-color: color-mix(
185
+ in srgb,
186
+ var(--primary-color) 15%,
187
+ transparent
188
+ );
189
+ }
190
+
191
+ #theme-toggle-btn svg {
192
+ fill: var(--text-color);
193
+ }
194
+
195
+ .sun-icon {
196
+ display: none;
197
+ }
198
+ .moon-icon {
199
+ display: none;
200
+ }
201
+ :root[data-theme="dark"] .sun-icon {
202
+ display: block;
203
+ }
204
+ :root[data-theme="light"] .moon-icon {
205
+ display: block;
206
+ }
207
+
208
+ .progress-container {
209
+ width: 100%;
210
+ height: 100%;
211
+ background-color: rgba(25, 118, 210, 0.2);
212
+ border-radius: 4px;
213
+ overflow: hidden;
214
+ position: relative;
215
+ margin: 20px 0;
216
+ }
217
+ .progress-indeterminate .progress-bar-long,
218
+ .progress-indeterminate .progress-bar-short {
219
+ position: absolute;
220
+ top: 0;
221
+ height: 100%;
222
+ background-color: var(--primary-color);
223
+ border-radius: 4px;
224
+ animation-duration: 2.2s;
225
+ animation-iteration-count: infinite;
226
+ }
227
+
228
+ .progress-indeterminate .progress-bar-long {
229
+ animation-name: indeterminate-ltr;
230
+ width: 75%;
231
+ }
232
+
233
+ .progress-indeterminate .progress-bar-short {
234
+ animation-name: indeterminate-short-ltr;
235
+ width: 20%;
236
+ }
237
+
238
+ @keyframes indeterminate-ltr {
239
+ 0% {
240
+ left: -90%;
241
+ right: 100%;
242
+ }
243
+ 60% {
244
+ left: -90%;
245
+ right: 100%;
246
+ }
247
+ 100% {
248
+ left: 100%;
249
+ right: -35%;
250
+ }
251
+ }
252
+
253
+ @keyframes indeterminate-short-ltr {
254
+ 0% {
255
+ left: -200%;
256
+ right: 100%;
257
+ }
258
+ 60% {
259
+ left: 107%;
260
+ right: -8%;
261
+ }
262
+ 100% {
263
+ left: 107%;
264
+ right: -8%;
265
+ }
266
+ }
267
+
268
+ /* Styles pour les routes */
269
+ .auto_head_note {
270
+ margin-top: 10px;
271
+ font-size: 14px;
272
+ color: var(--method-head-color);
273
+ display: flex;
274
+ align-items: center;
275
+ }
276
+ .note {
277
+ background-color: rgba(255, 152, 0, 0.1);
278
+ border-left: 4px solid var(--warning-color);
279
+ padding: 12px;
280
+ margin-bottom: 20px;
281
+ border-radius: 4px;
282
+ }
283
+ .info_pill {
284
+ display: inline-flex;
285
+ align-items: center;
286
+ background-color: rgba(0, 0, 0, 0.05);
287
+ border-radius: 14px;
288
+ padding: 4px 10px;
289
+ font-size: 13px;
290
+ margin-left: 10px;
291
+ }
292
+
293
+ .route_container {
294
+ display: flex;
295
+ gap: 20px;
296
+ align-items: flex-start;
297
+ }
298
+ .filters_sidebar {
299
+ width: 250px;
300
+ flex-shrink: 0;
301
+ border-radius: 6px;
302
+ overflow: hidden;
303
+ border: 1px solid var(--border-color);
304
+ background: var(--secondary-color);
305
+ }
306
+ .sidebar_header {
307
+ padding: 12px 15px;
308
+ font-weight: 500;
309
+ border-bottom: 1px solid var(--border-color);
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: space-between;
313
+ }
314
+ .filters_content {
315
+ padding: 15px;
316
+ background: var(--background-color);
317
+ }
318
+ .filters_count {
319
+ background-color: var(--primary-color);
320
+ color: white;
321
+ padding: 2px 8px;
322
+ border-radius: 12px;
323
+ font-size: 0.8rem;
324
+ font-weight: normal;
325
+ }
326
+ @media (max-width: 768px) {
327
+ .route_container {
328
+ flex-direction: column;
329
+ }
330
+
331
+ .filters_sidebar {
332
+ width: 100%;
333
+ margin-bottom: 20px;
334
+ }
335
+ }
336
+
337
+ .route_list {
338
+ margin-top: 20px;
339
+ flex-grow: 1;
340
+ min-width: 0; /* Prevent flexbox from expanding beyond available space */
341
+ }
342
+ .route_details {
343
+ margin-bottom: 8px;
344
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.19);
345
+ border: 1px solid #000;
346
+ border-radius: 4px;
347
+ margin: 0 0 15px;
348
+ border-color: var(--method-color);
349
+ background: color-mix(in srgb, var(--method-color), transparent 90%);
350
+ }
351
+ .route_details > summary {
352
+ cursor: pointer;
353
+ list-style: none;
354
+ }
355
+ .route_details > summary::marker {
356
+ display: none;
357
+ }
358
+ .route_summary_container {
359
+ display: flex;
360
+ align-items: center;
361
+ padding: 5px;
362
+ }
363
+ .route_method {
364
+ border-radius: 3px;
365
+ color: #fff;
366
+ font-family: sans-serif;
367
+ font-size: 14px;
368
+ font-weight: 700;
369
+ min-width: 80px;
370
+ padding: 6px 0;
371
+ text-align: center;
372
+ text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
373
+ background: var(--method-color);
374
+ }
375
+ .route_resource {
376
+ margin-left: 8px;
377
+ font-family: monospace;
378
+ font-size: 16px;
379
+ font-weight: 600;
380
+ word-break: break-word;
381
+ margin-top: 0;
382
+ margin-bottom: 0;
383
+ }
384
+ .route_details[data-method="HEAD"] {
385
+ --method-color: var(--method-head-color);
386
+ }
387
+ .route_details[data-method="OPTIONS"] {
388
+ --method-color: var(--method-options-color);
389
+ }
390
+ .route_details[data-method="GET"] {
391
+ --method-color: var(--method-get-color);
392
+ }
393
+ .route_details[data-method="POST"] {
394
+ --method-color: var(--method-post-color);
395
+ }
396
+ .route_details[data-method="PUT"] {
397
+ --method-color: var(--method-put-color);
398
+ }
399
+ .route_details[data-method="PATCH"] {
400
+ --method-color: var(--method-patch-color);
401
+ }
402
+ .route_details[data-method="DELETE"] {
403
+ --method-color: var(--method-delete-color);
404
+ }
405
+
406
+ .route_owner_name {
407
+ background-color: #ffefd2;
408
+ color: #666;
409
+ padding: 4px 8px;
410
+ text-align: center;
411
+ border-radius: 5px;
412
+ }
413
+ :root[data-theme="dark"] .route_owner_name {
414
+ background-color: rgb(96, 59, 13);
415
+ color: white;
416
+ }
417
+
418
+ .route_details_content {
419
+ border-top: 1px solid var(--method-color);
420
+ padding: 10px;
421
+ }
422
+ .route_description {
423
+ margin: 0;
424
+ }
425
+ .route_content_nego_title {
426
+ font-weight: bold;
427
+ }
428
+
429
+ .filter_container {
430
+ margin-bottom: 20px;
431
+ border-radius: 4px;
432
+ overflow: hidden;
433
+ border: 1px solid var(--border-color);
434
+ background: var(--secondary-color);
435
+ }
436
+ .filter_container::marker {
437
+ display: none;
438
+ }
439
+ .filter_summary {
440
+ cursor: pointer;
441
+ font-weight: 500;
442
+ display: flex;
443
+ flex-direction: row;
444
+ align-items: center;
445
+ justify-content: space-between;
446
+ user-select: none;
447
+ }
448
+ .filter:open .arrow_down {
449
+ transform: rotate(180deg);
450
+ }
451
+ .filter_list {
452
+ background: var(--background-color);
453
+ border-top: 1px solid var(--border-color);
454
+ overflow: hidden;
455
+ }
456
+ .filter_item {
457
+ margin: 15px;
458
+ }
459
+ .filter_badge {
460
+ background-color: var(--primary-color);
461
+ color: white;
462
+ padding: 2px 8px;
463
+ border-radius: 12px;
464
+ font-size: 0.8rem;
465
+ margin-left: 10px;
466
+ }
467
+
468
+ select {
469
+ min-width: 200px;
470
+ padding: 8px;
471
+ border-radius: 4px;
472
+ border: 1px solid var(--border-color);
473
+ background-color: var(--background-color);
474
+ color: var(--text-color);
475
+ font-family: inherit;
476
+ }
477
+ select option {
478
+ padding: 8px;
479
+ }
480
+ select option:checked {
481
+ background-color: var(--primary-color);
482
+ color: white;
483
+ }
484
+ .filter_form {
485
+ display: flex;
486
+ flex-direction: column;
487
+ flex-wrap: wrap;
488
+ gap: 8px;
489
+ margin-top: 15px;
490
+ margin-bottom: 15px;
491
+ user-select: none;
492
+ }
493
+ .filter_form label {
494
+ display: flex;
495
+ align-items: center;
496
+ gap: 5px;
497
+ cursor: pointer;
498
+ width: 100%;
499
+ word-break: keep-all;
500
+ overflow: hidden;
501
+ text-overflow: ellipsis;
502
+ white-space: nowrap;
503
+ }
504
+ label .filter_text {
505
+ text-overflow: ellipsis;
506
+ overflow: hidden;
507
+ }
508
+ .filter_text[data-method] {
509
+ border-left: 5px solid var(--method-color);
510
+ padding-left: 4px;
511
+ }
512
+ .filter_text[data-method="all"] {
513
+ --method-color: transparent;
514
+ border-image-slice: 1;
515
+ }
516
+ .filter_text[data-method="HEAD"] {
517
+ --method-color: var(--method-head-color);
518
+ }
519
+ .filter_text[data-method="OPTIONS"] {
520
+ --method-color: var(--method-options-color);
521
+ }
522
+ .filter_text[data-method="GET"] {
523
+ --method-color: var(--method-get-color);
524
+ }
525
+ .filter_text[data-method="POST"] {
526
+ --method-color: var(--method-post-color);
527
+ }
528
+ .filter_text[data-method="PUT"] {
529
+ --method-color: var(--method-put-color);
530
+ }
531
+ .filter_text[data-method="PATCH"] {
532
+ --method-color: var(--method-patch-color);
533
+ }
534
+ .filter_text[data-method="DELETE"] {
535
+ --method-color: var(--method-delete-color);
536
+ }
537
+
538
+ .filter_count {
539
+ background-color: #eee;
540
+ color: #666;
541
+ opacity: 0.8;
542
+ padding: 2px 8px;
543
+ border-radius: 12px;
544
+ font-size: 0.8rem;
545
+ margin-left: auto;
546
+ }
547
+
548
+ .filter_actions {
549
+ display: flex;
550
+ gap: 10px;
551
+ }
552
+ .filter_button {
553
+ background-color: var(--secondary-color);
554
+ border: 1px solid var(--border-color);
555
+ color: var(--text-color);
556
+ padding: 6px 12px;
557
+ border-radius: 4px;
558
+ cursor: pointer;
559
+ font-size: 0.9rem;
560
+ transition: all 0.2s;
561
+ }
562
+ .filter_button:hover {
563
+ background-color: color-mix(
564
+ in srgb,
565
+ var(--primary-color) 15%,
566
+ transparent
567
+ );
568
+ }
569
+ </style>
570
+ <div class="container">
571
+ <aside></aside>
572
+ <main>
573
+ <div class="header_row">
574
+ <h1 style="margin: 0">Route inspector</h1>
575
+ <button
576
+ class="note_button_open"
577
+ aria-label="Show information"
578
+ title="About HTTP Methods"
579
+ >
580
+ <svg
581
+ xmlns="http://www.w3.org/2000/svg"
582
+ viewBox="0 0 24 24"
583
+ width="24"
584
+ height="24"
585
+ fill="none"
586
+ stroke="currentColor"
587
+ stroke-width="2"
588
+ stroke-linecap="round"
589
+ stroke-linejoin="round"
590
+ >
591
+ <circle cx="12" cy="12" r="10"></circle>
592
+ <line x1="12" y1="16" x2="12" y2="12"></line>
593
+ <line x1="12" y1="8" x2="12.01" y2="8"></line>
594
+ </svg>
595
+ </button>
596
+ <div style="flex: 1"></div>
597
+ <div class="theme-toggle">
598
+ <button id="theme-toggle-btn" aria-label="Toggle dark/light mode">
599
+ <svg
600
+ class="sun-icon"
601
+ xmlns="http://www.w3.org/2000/svg"
602
+ height="24"
603
+ viewBox="0 -960 960 960"
604
+ width="24"
605
+ >
606
+ <path
607
+ d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-440q0 17 11.5 28.5T120-400h80q17 0 28.5-11.5T240-440q0-17-11.5-28.5T200-480h-80q-17 0-28.5 11.5T80-440Zm680 0q0 17 11.5 28.5T800-400h80q17 0 28.5-11.5T920-440q0-17-11.5-28.5T880-480h-80q-17 0-28.5 11.5T760-440ZM440-760q0 17 11.5 28.5T480-720q17 0 28.5-11.5T520-760v-80q0-17-11.5-28.5T480-880q-17 0-28.5 11.5T440-840v80Zm0 680q0 17 11.5 28.5T480-40q17 0 28.5-11.5T520-80v-80q0-17-11.5-28.5T480-200q-17 0-28.5 11.5T440-160v80ZM226-678l56 56q12 12 28.5 12t28.5-12q12-12 12-28.5T339-679l-56-56q-12-12-28.5-12T226-735q-12 12-12 28.5t12 28.5Zm452 452 56 56q12 12 28.5 12t28.5-12q12-12 12-28.5T791-227l-56-56q-12-12-28.5-12T678-283q-12 12-12 28.5t12 28.5ZM791-678q12 12 28.5 12t28.5-12q12-12 12-28.5T848-735l-56-56q-12-12-28.5-12T735-791q-12 12-12 28.5t12 28.5l56 56Zm-508 452q12 12 28.5 12t28.5-12q12-12 12-28.5T340-283l-56-56q-12-12-28.5-12T227-339q-12 12-12 28.5t12 28.5l56 56Z"
608
+ />
609
+ </svg>
610
+ <svg
611
+ class="moon-icon"
612
+ xmlns="http://www.w3.org/2000/svg"
613
+ height="24"
614
+ viewBox="0 -960 960 960"
615
+ width="24"
616
+ >
617
+ <path
618
+ d="M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 83 58.5 141.5T643-460q52 0 99-20t79-56q1 14 1.5 23t.5 17q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z"
619
+ />
620
+ </svg>
621
+ </button>
622
+ </div>
623
+ </div>
624
+ <dialog class="node_dialog">
625
+ <button class="note_button_close" aria-label="Close">
626
+ <svg
627
+ width="24"
628
+ height="24"
629
+ xmlns="http://www.w3.org/2000/svg"
630
+ viewBox="0 0 24 24"
631
+ fill="none"
632
+ stroke="currentColor"
633
+ stroke-width="2"
634
+ stroke-linecap="round"
635
+ stroke-linejoin="round"
636
+ >
637
+ <line x1="18" y1="6" x2="6" y2="18"></line>
638
+ <line x1="6" y1="6" x2="18" y2="18"></line>
639
+ </svg>
640
+ </button>
641
+ <div class="note_title">Explanation about this page</div>
642
+ <div class="note_text">
643
+ <p>This page displays all routes configured in your server.</p>
644
+ <p>
645
+ Every GET route also automatically supports HEAD requests without
646
+ additional configuration.
647
+ </p>
648
+ </div>
649
+ </dialog>
650
+ <script>
651
+ const noteDialog = document.querySelector(".node_dialog");
652
+ const noteButtonOpen = document.querySelector(".note_button_open");
653
+ const nodeButtonClose = document.querySelector(".note_button_close");
654
+
655
+ noteDialog.onclick = (e) => {
656
+ if (e.target === noteDialog) {
657
+ noteDialog.close();
658
+ }
659
+ };
660
+ nodeButtonClose.onclick = () => {
661
+ noteDialog.close();
662
+ };
663
+ noteButtonOpen.onclick = () => {
664
+ noteDialog.showModal();
665
+ };
666
+ </script>
667
+ <div id="app"></div>
668
+ </main>
669
+ </div>
670
+ <script>
671
+ // Theme toggling functionality
672
+ const toggleThemeButton = document.getElementById("theme-toggle-btn");
673
+
674
+ // Check for saved theme preference or use system preference
675
+ const savedTheme = localStorage.getItem("theme");
676
+ const prefersDark = window.matchMedia(
677
+ "(prefers-color-scheme: dark)",
678
+ ).matches;
679
+
680
+ // Apply theme based on saved preference or system preference
681
+ if (savedTheme === "dark" || (savedTheme === null && prefersDark)) {
682
+ document.documentElement.setAttribute("data-theme", "dark");
683
+ } else {
684
+ document.documentElement.setAttribute("data-theme", "light");
685
+ }
686
+
687
+ // Toggle theme when button is clicked
688
+ toggleThemeButton.addEventListener("click", () => {
689
+ if (document.documentElement.getAttribute("data-theme") === "dark") {
690
+ document.documentElement.setAttribute("data-theme", "light");
691
+ localStorage.setItem("theme", "light");
692
+ } else {
693
+ document.documentElement.setAttribute("data-theme", "dark");
694
+ localStorage.setItem("theme", "dark");
695
+ }
696
+ });
697
+ </script>
698
+ <script type="module">
699
+ // https://preactjs.com/guide/v10/no-build-workflows/
700
+ import { render } from "preact";
701
+ import { html } from "htm/preact";
702
+ import { signal, computed } from "@preact/signals";
703
+ import { useState, useRef, useCallback } from "preact/hooks";
704
+
705
+ const routesLoadingSignal = signal(false);
706
+ const routeLoadingErrorSignal = signal(null);
707
+ const routesSignal = signal([]);
708
+ const NO_PACKAGE_NAME = "unknown";
709
+
710
+ const repartitionsSignal = computed(() => {
711
+ const routes = routesSignal.value;
712
+ const methodRepartition = {};
713
+ const packageRepartition = {};
714
+ for (const route of routes) {
715
+ const method = route.method;
716
+ if (!methodRepartition[method]) {
717
+ methodRepartition[method] = 0;
718
+ }
719
+ methodRepartition[method]++;
720
+
721
+ const packageName = route.meta.packageName || NO_PACKAGE_NAME;
722
+ if (!packageRepartition[packageName]) {
723
+ packageRepartition[packageName] = 0;
724
+ }
725
+ packageRepartition[packageName]++;
726
+ }
727
+ return {
728
+ methodRepartition,
729
+ packageRepartition,
730
+ };
731
+ });
732
+ const methodRepartitionSignal = computed(() => {
733
+ return repartitionsSignal.value.methodRepartition;
734
+ });
735
+ const packageRepartitionSignal = computed(() => {
736
+ return repartitionsSignal.value.packageRepartition;
737
+ });
738
+ const availableMethodNamesSignal = computed(() => {
739
+ return Object.keys(methodRepartitionSignal.value);
740
+ });
741
+ const availablePackageNamesSignal = computed(() => {
742
+ return Object.keys(packageRepartitionSignal.value);
743
+ });
744
+
745
+ const loadRoutes = async () => {
746
+ routesLoadingSignal.value = true;
747
+ routeLoadingErrorSignal.value = null;
748
+ try {
749
+ const response = await fetch("/.internal/routes.json");
750
+ const routes = await response.json();
751
+ routesLoadingSignal.value = false;
752
+ routesSignal.value = routes;
753
+ } catch (e) {
754
+ routesLoadingSignal.value = false;
755
+ routeLoadingErrorSignal.value = e;
756
+ throw e;
757
+ }
758
+ };
759
+ loadRoutes();
760
+
761
+ const Sidebar = () => {
762
+ const routes = routesSignal.value;
763
+ if (routes.length === 0) {
764
+ return null;
765
+ }
766
+ return html` <${FiltersPanel} routes=${routes} /> `;
767
+ };
768
+
769
+ const App = () => {
770
+ return html` <${RouteList} /> `;
771
+ };
772
+ const ProgressBar = ({ width = "100%", height = 6, loading }) => {
773
+ if (!loading) {
774
+ return null;
775
+ }
776
+ return html`
777
+ <div
778
+ class="progress-bar"
779
+ style=${{
780
+ width: typeof Number(width) === "number" ? `${width}px` : width,
781
+ height:
782
+ typeof Number(height) === "number" ? `${height}px` : height,
783
+ }}
784
+ >
785
+ <div class="progress-container progress-indeterminate">
786
+ <div class="progress-bar-long"></div>
787
+ <div class="progress-bar-short"></div>
788
+ </div>
789
+ </div>
790
+ `;
791
+ };
792
+ const RouteList = () => {
793
+ const routes = routesSignal.value;
794
+ const loading = routesLoadingSignal.value;
795
+ const routeLoadingError = routeLoadingErrorSignal.value;
796
+
797
+ if (loading) {
798
+ return html`<${ProgressBar} width="150" loading=${true} />`;
799
+ }
800
+
801
+ if (routeLoadingError) {
802
+ return html`<p>
803
+ Error loading routes: ${routeLoadingError.message}
804
+ </p>`;
805
+ }
806
+
807
+ if (!routes || routes.length === 0) {
808
+ return html`<p>No routes configured.</p>`;
809
+ }
810
+
811
+ return html`<div class="route_container">
812
+ <div class="route_list">
813
+ ${routes.map((route) => html`<${RouteDetails} ...${route} />`)}
814
+ </div>
815
+ </div>`;
816
+ };
817
+
818
+ const documentUrlSignal = signal(window.location.href);
819
+ window.addEventListener("popstate", async (popstateEvent) => {
820
+ documentUrlSignal.value = popstateEvent.target.location.href;
821
+ });
822
+ const goTo = (url, { replace } = {}) => {
823
+ if (replace) {
824
+ window.history.replaceState(null, null, url);
825
+ documentUrlSignal.value = url;
826
+ } else {
827
+ window.history.pushState(null, null, url);
828
+ documentUrlSignal.value = url;
829
+ }
830
+ };
831
+ goTo(window.location.href, { replace: true });
832
+
833
+ let useUrlStringParam;
834
+ url_string_param: {
835
+ useUrlStringParam = (name, { availableValues } = {}) => {
836
+ const urlStringParamSignal = signalForUrlParam(name, (url) => {
837
+ return new URL(url).searchParams.get(name);
838
+ });
839
+ const urlStringParam = urlStringParamSignal.value;
840
+ let value;
841
+ if (urlStringParam) {
842
+ value = urlStringParam;
843
+ if (availableValues && !availableValues.includes(value)) {
844
+ value = "";
845
+ }
846
+ } else {
847
+ value = "";
848
+ }
849
+
850
+ const set = useCallback(
851
+ (newValue) => {
852
+ const urlObject = new URL(documentUrlSignal.peek());
853
+ const { searchParams } = urlObject;
854
+ if (!newValue) {
855
+ searchParams.delete(name);
856
+ const urlWithoutStringParam = urlObject.href;
857
+ if (urlWithoutStringParam !== window.location.href) {
858
+ goTo(urlWithoutStringParam);
859
+ }
860
+ return;
861
+ }
862
+ if (availableValues && !availableValues.includes(newValue)) {
863
+ return;
864
+ }
865
+ searchParams.set(name, newValue);
866
+ const urlWithStringParam = urlObject.href;
867
+ if (urlWithStringParam !== window.location.href) {
868
+ goTo(urlWithStringParam);
869
+ }
870
+ return;
871
+ },
872
+ [name],
873
+ );
874
+ return [value, set];
875
+ };
876
+
877
+ const urlParamSignalMap = new Map();
878
+ // can be called multiple times by hooks
879
+ const signalForUrlParam = (name, getter) => {
880
+ const existingSignal = urlParamSignalMap.get(name);
881
+ if (existingSignal) {
882
+ return existingSignal;
883
+ }
884
+ const signalForFirstCall = computed(() => {
885
+ const url = documentUrlSignal.value;
886
+ const valueInUrl = getter(url);
887
+ return valueInUrl;
888
+ });
889
+ urlParamSignalMap.set(name, signalForFirstCall);
890
+ return signalForFirstCall;
891
+ };
892
+ }
893
+
894
+ const useMethodUrlStringParam = () => {
895
+ const availableMethodNames = availableMethodNamesSignal.value;
896
+ return useUrlStringParam("method", {
897
+ availableValues: availableMethodNames,
898
+ });
899
+ };
900
+
901
+ const usePackageUrlStringParam = () => {
902
+ const availablePackageNames = availablePackageNamesSignal.value;
903
+ return useUrlStringParam("package", {
904
+ availableValues: availablePackageNames,
905
+ });
906
+ };
907
+
908
+ let FiltersPanel;
909
+ filters: {
910
+ FiltersPanel = ({ routes }) => {
911
+ return html`
912
+ <div class="filter_container">
913
+ <div class="filter_list">
914
+ <div class="filter_item">
915
+ <${MethodFilter} routes=${routes} />
916
+ </div>
917
+ <div class="filter_item">
918
+ <${PackageFilter} routes=${routes} />
919
+ </div>
920
+ </div>
921
+ </div>
922
+ `;
923
+ };
924
+ const MethodFilter = ({ routes }) => {
925
+ const availableMethodNames = availableMethodNamesSignal.value;
926
+ const availableMethodRepartition = methodRepartitionSignal.value;
927
+ const [selectedMethod, setSelectedMethod] = useMethodUrlStringParam();
928
+
929
+ // "all" border image is a combination of available method colors
930
+ const getMethodColor = (methodName) => {
931
+ const bodyStyle = window.getComputedStyle(document.body);
932
+ const methodColor = bodyStyle.getPropertyValue(
933
+ `--method-${methodName.toLowerCase()}-color`,
934
+ );
935
+ return methodColor;
936
+ };
937
+ const percentagePerColor = Math.floor(
938
+ 100 / (availableMethodNames.length - 1),
939
+ );
940
+ const gradientSteps = [];
941
+ let progress = 0;
942
+ let index = 0;
943
+ for (const methodName of availableMethodNames) {
944
+ if (index === availableMethodNames.length - 1) {
945
+ progress = 100;
946
+ }
947
+ gradientSteps.push(`${getMethodColor(methodName)} ${progress}%`);
948
+ progress += percentagePerColor;
949
+ index++;
950
+ }
951
+ const gradient = `linear-gradient(to bottom, ${gradientSteps.join(", ")})`;
952
+
953
+ const options = [
954
+ {
955
+ value: "",
956
+ text: html`
957
+ <span
958
+ class="filter_text"
959
+ data-method="all"
960
+ style=${{
961
+ borderImageSource: gradient,
962
+ }}
963
+ >all</span
964
+ >
965
+ <span class="filter_count">${routes.length}</span>
966
+ `,
967
+ selected: !selectedMethod,
968
+ },
969
+ ...availableMethodNames.map((availableMethodName) => {
970
+ return {
971
+ value: availableMethodName,
972
+ text: html`
973
+ <span class="filter_text" data-method="${availableMethodName}"
974
+ >${availableMethodName}
975
+ </span>
976
+ <span class="filter_count">
977
+ ${availableMethodRepartition[availableMethodName]}
978
+ </span>
979
+ `,
980
+ selected: availableMethodName === selectedMethod,
981
+ };
982
+ }),
983
+ ];
984
+
985
+ return html`
986
+ <${FilterSelector}
987
+ label="Methods"
988
+ options=${options}
989
+ onChange=${(methodToSelect) => {
990
+ setSelectedMethod(methodToSelect);
991
+ }}
992
+ />
993
+ `;
994
+ };
995
+ const PackageFilter = ({ routes }) => {
996
+ const availablePackageNames = availablePackageNamesSignal.value;
997
+ const packageRepartition = packageRepartitionSignal.value;
998
+ const [selectedPackageName, setSelectedPackageName] =
999
+ usePackageUrlStringParam();
1000
+
1001
+ const options = [
1002
+ {
1003
+ value: "",
1004
+ text: html`<span>all</span>
1005
+ <span class="filter_count">${routes.length}</span>`,
1006
+ selected: !selectedPackageName,
1007
+ },
1008
+ ...availablePackageNames.map((availablePackageName) => {
1009
+ return {
1010
+ value: availablePackageName,
1011
+ text: html` <span
1012
+ class="filter_text"
1013
+ title="${availablePackageName}"
1014
+ >${availablePackageName}</span
1015
+ >
1016
+ <span class="filter_count"
1017
+ >${packageRepartition[availablePackageName]}</span
1018
+ >`,
1019
+ selected: availablePackageName === selectedPackageName,
1020
+ };
1021
+ }),
1022
+ ];
1023
+ return html`
1024
+ <${FilterSelector}
1025
+ label="Packages"
1026
+ options=${options}
1027
+ onChange=${(packageNameToSelect) => {
1028
+ setSelectedPackageName(packageNameToSelect);
1029
+ }}
1030
+ />
1031
+ `;
1032
+ };
1033
+ const FilterSelector = ({
1034
+ label,
1035
+ multiple,
1036
+ options,
1037
+ onChange,
1038
+ actions,
1039
+ }) => {
1040
+ const formRef = useRef();
1041
+
1042
+ return html`<details open class="filter">
1043
+ <summary class="filter_summary">
1044
+ ${label}
1045
+ <${ArrowDown} />
1046
+ </summary>
1047
+ <form ref=${formRef} class="filter_form">
1048
+ ${options.map((option) => {
1049
+ return html`<label>
1050
+ <input
1051
+ type="${multiple ? "checkbox" : "radio"}"
1052
+ name="${label}"
1053
+ checked=${option.selected}
1054
+ value=${option.value}
1055
+ onChange=${multiple
1056
+ ? () => {
1057
+ const formNode = formRef.current;
1058
+ // even if we create a checkbox list the HTML spec
1059
+ // return a radio node list and we have to gather the checked values
1060
+ const radioNodeList = formNode.elements[label];
1061
+ const selectedValues = [];
1062
+ for (const checkboxNode of Array.from(
1063
+ radioNodeList,
1064
+ )) {
1065
+ if (checkboxNode.checked) {
1066
+ selectedValues.push(checkboxNode.value);
1067
+ }
1068
+ }
1069
+ onChange(selectedValues);
1070
+ }
1071
+ : () => {
1072
+ const formNode = formRef.current;
1073
+ const radioNodeList = formNode.elements[label];
1074
+ const selectedValue = radioNodeList.value;
1075
+ onChange(selectedValue);
1076
+ }}
1077
+ />
1078
+ ${option.text}
1079
+ </label>`;
1080
+ })}
1081
+ </form>
1082
+ ${
1083
+ actions &&
1084
+ html` <div class="filter_actions">
1085
+ <button
1086
+ type="button"
1087
+ class="filter_button"
1088
+ onClick=${() => {
1089
+ onChange(options.map((option) => option.value));
1090
+ }}
1091
+ >
1092
+ Select All
1093
+ </button>
1094
+ <button
1095
+ type="button"
1096
+ class="filter_button"
1097
+ onClick=${() => {
1098
+ onChange([]);
1099
+ }}
1100
+ >
1101
+ Clear All
1102
+ </button>
1103
+ </div>`
1104
+ }
1105
+ </div>`;
1106
+ };
1107
+ }
1108
+
1109
+ const RouteDetails = ({
1110
+ meta,
1111
+ method,
1112
+ resource,
1113
+ description,
1114
+ availableMediaTypes,
1115
+ availableLanguages,
1116
+ availableVersions,
1117
+ availableEncodings,
1118
+ acceptedMediaTypes,
1119
+ websocket,
1120
+ clientCodeExample,
1121
+ declarationSource,
1122
+ }) => {
1123
+ const [opened, setOpened] = useState(false);
1124
+ const [selectedMethod] = useUrlStringParam("method");
1125
+ const [selectedPackageName] = usePackageUrlStringParam();
1126
+
1127
+ if (selectedMethod && method !== selectedMethod) {
1128
+ return null;
1129
+ }
1130
+ const packageName = meta.packageName || NO_PACKAGE_NAME;
1131
+ if (selectedPackageName && packageName !== selectedPackageName) {
1132
+ return null;
1133
+ }
1134
+
1135
+ return html`<details
1136
+ class="route_details"
1137
+ onToggle=${(e) => {
1138
+ setOpened(e.newState === "open");
1139
+ }}
1140
+ data-method=${method}
1141
+ >
1142
+ <summary>
1143
+ <div class="route_summary_container">
1144
+ <span class="route_method">${method}</span>
1145
+ <p class="route_resource">${resource}</p>
1146
+ <span style="flex:1"></span>
1147
+ <${RouteBadge} meta=${meta} />
1148
+ ${opened ? html`<${ArrowUp} />` : html`<${ArrowDown} />`}
1149
+ </div>
1150
+ </summary>
1151
+ <${RouteDetailsContent}
1152
+ method=${method}
1153
+ resource=${resource}
1154
+ description=${description}
1155
+ availableMediaTypes=${availableMediaTypes}
1156
+ availableLanguages=${availableLanguages}
1157
+ availableVersions=${availableVersions}
1158
+ availableEncodings=${availableEncodings}
1159
+ acceptedMediaTypes=${acceptedMediaTypes}
1160
+ websocket=${websocket}
1161
+ clientCodeExample=${clientCodeExample}
1162
+ declarationSource=${declarationSource}
1163
+ meta=${meta}
1164
+ />
1165
+ </details>`;
1166
+ };
1167
+
1168
+ const RouteBadge = ({ meta }) => {
1169
+ if (!meta.packageName) {
1170
+ return null;
1171
+ }
1172
+ let text = meta.packageName;
1173
+ let title = `This endpoint is declared by ${meta.packageName}`;
1174
+ return html`<span title=${title} class="route_owner_name"
1175
+ >${text}</span
1176
+ >`;
1177
+ };
1178
+
1179
+ const RouteDetailsContent = ({
1180
+ method,
1181
+ resource,
1182
+ description,
1183
+ availableMediaTypes,
1184
+ availableLanguages,
1185
+ availableVersions,
1186
+ availableEncodings,
1187
+ acceptedMediaTypes,
1188
+ websocket,
1189
+ clientCodeExample,
1190
+ declarationSource,
1191
+ meta,
1192
+ }) => {
1193
+ return html`<div class="route_details_content">
1194
+ <${RoutedeclarationSource}
1195
+ meta=${meta}
1196
+ declarationSource=${declarationSource}
1197
+ />
1198
+ <p style="margin-top: 0"><strong>Description:</strong></p>
1199
+ <p class="route_description">
1200
+ ${description || "No description available"}
1201
+ </p>
1202
+ ${availableMediaTypes.length > 0 &&
1203
+ html`<${RouteContentNegotiationSection}
1204
+ type="content-type"
1205
+ title="Available media types"
1206
+ availableValues=${availableMediaTypes}
1207
+ />`}
1208
+ ${availableLanguages.length > 0 &&
1209
+ html`<${RouteContentNegotiationSection}
1210
+ type="language"
1211
+ title="Available languages"
1212
+ availableValues=${availableLanguages}
1213
+ />`}
1214
+ ${availableVersions.length > 0 &&
1215
+ html`<${RouteContentNegotiationSection}
1216
+ type="version"
1217
+ title="Available versions"
1218
+ availableValues=${availableVersions}
1219
+ />`}
1220
+ ${availableEncodings.length > 0 &&
1221
+ html`<${RouteContentNegotiationSection}
1222
+ type="encoding"
1223
+ title="Available encodings"
1224
+ availableValues=${availableEncodings}
1225
+ />`}
1226
+ ${acceptedMediaTypes.length > 0 &&
1227
+ html`<${RouteAcceptableContentSection}
1228
+ acceptedMediaTypes=${acceptedMediaTypes}
1229
+ />`}
1230
+ <p style="margin-bottom:0; font-weight:bold">Example:</p>
1231
+ <${RouteCodeExample}
1232
+ method=${method}
1233
+ resource=${resource}
1234
+ availableMediaTypes=${availableMediaTypes}
1235
+ websocket=${websocket}
1236
+ clientCodeExample=${clientCodeExample}
1237
+ />
1238
+ </div>`;
1239
+ };
1240
+ const RoutedeclarationSource = ({ meta, declarationSource }) => {
1241
+ if (!declarationSource) {
1242
+ return null;
1243
+ }
1244
+ return html`<p style="margin-top: 0"><strong>Declaration:</strong></p>
1245
+ <p>
1246
+ <${RouteDeclarationText}
1247
+ ownerLink=${meta.ownerLink}
1248
+ declarationLink=${meta.declarationLink}
1249
+ />
1250
+ </p>`;
1251
+ };
1252
+ const RouteDeclarationText = ({ declarationLink }) => {
1253
+ return html`This route is declared in${" "}
1254
+ <a href="${declarationLink.url}">${declarationLink.text}</a>`;
1255
+ };
1256
+ const RouteContentNegotiationSection = ({
1257
+ type,
1258
+ title,
1259
+ availableValues,
1260
+ }) => {
1261
+ return html`<div class="route_content_nego" data-nego-type="${type}">
1262
+ <p class="route_content_nego_title">${title}:</p>
1263
+ <ul>
1264
+ ${availableValues.map(
1265
+ (availableValue) => html`<li>${availableValue}</li>`,
1266
+ )}
1267
+ </ul>
1268
+ </div>`;
1269
+ };
1270
+ const RouteAcceptableContentSection = ({ acceptedMediaTypes }) => {
1271
+ return html`<div class="route_accepted_content_type">
1272
+ <p><strong>Accepted content types:</strong></p>
1273
+ <ul>
1274
+ ${acceptedMediaTypes.map(
1275
+ (acceptedMediaType) => html`<li>${acceptedMediaType}</li>`,
1276
+ )}
1277
+ </ul>
1278
+ </div>`;
1279
+ };
1280
+
1281
+ const RouteCodeExample = ({
1282
+ method,
1283
+ resource,
1284
+ availableMediaTypes,
1285
+ websocket,
1286
+ clientCodeExample,
1287
+ }) => {
1288
+ let jsSource;
1289
+ if (clientCodeExample) {
1290
+ jsSource = clientCodeExample;
1291
+ } else if (websocket) {
1292
+ const websocketScheme =
1293
+ self.location.protocol === "https:" ? "wss" : "ws";
1294
+ const websocketOrigin = `${websocketScheme}://${self.location.host}`;
1295
+ jsSource = /*js */ `
1296
+ const websocket = new WebSocket("${websocketOrigin}${resource}");
1297
+ websocket.onerror = () => {
1298
+ console.error("Failed to connect to websocket endpoint");
1299
+ };
1300
+ websocket.onopen = () => {
1301
+ console.info("Connected to websocket endpoint")
1302
+ };
1303
+ websocket.onmessage = (event) => {
1304
+ console.log("Received message", event.data);
1305
+ };
1306
+ `;
1307
+ } else {
1308
+ let fetchLine;
1309
+ if (method === "GET") {
1310
+ fetchLine = `const response = await window.fetch("${resource}");`;
1311
+ } else {
1312
+ fetchLine = `const response = await window.fetch("${resource}", {
1313
+ method: "${method}",
1314
+ }`;
1315
+ }
1316
+ let readResponseLine;
1317
+ if (availableMediaTypes.includes("application/json")) {
1318
+ readResponseLine = `const json = await response.json();`;
1319
+ } else {
1320
+ readResponseLine = `const text = await response.text();`;
1321
+ }
1322
+
1323
+ jsSource = /*js */ `
1324
+ ${fetchLine}
1325
+ ${readResponseLine}
1326
+ `;
1327
+ }
1328
+
1329
+ return html`<code-block lang="js" data-escaped data-copy-button
1330
+ >${jsSource}</code-block
1331
+ >`;
1332
+ };
1333
+
1334
+ const ArrowDown = () => {
1335
+ return html`<svg
1336
+ xmlns="http://www.w3.org/2000/svg"
1337
+ viewBox="0 -960 960 960"
1338
+ width="24"
1339
+ height="24"
1340
+ fill="currentColor"
1341
+ class="arrow_down"
1342
+ >
1343
+ <path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" />
1344
+ </svg>`;
1345
+ };
1346
+ const ArrowUp = () => {
1347
+ return html`<svg
1348
+ xmlns="http://www.w3.org/2000/svg"
1349
+ viewBox="0 -960 960 960"
1350
+ width="24"
1351
+ height="24"
1352
+ fill="currentColor"
1353
+ class="arrow_up"
1354
+ >
1355
+ <path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z" />
1356
+ </svg>`;
1357
+ };
1358
+
1359
+ render(html`<${Sidebar} />`, document.querySelector("aside"));
1360
+ render(html`<${App} />`, document.querySelector("#app"));
1361
+ </script>
1362
+ </body>
1363
+ </html>