@safagayret/bemirror 2.2.1 → 2.3.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.
- package/README.md +4 -2
- package/package.json +1 -1
- package/public/css/style.css +270 -10
- package/public/index.html +54 -59
- package/public/js/app.js +297 -25
- package/server.js +4 -2
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> **Fake mirror your backend, fast.**
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
**be.mirror** is an ultra-lightweight, zero-backend, local Node.js API mock server built for frontend teams. It allows you to visualize, create, and interact with complex mock endpoints completely locally without the heavy setup of databases or third-party cloud tools.
|
|
6
8
|
|
|
7
9
|
## ✨ Features
|
|
@@ -68,8 +70,8 @@ Once started, open `http://localhost:8000` (or your configured port) to use the
|
|
|
68
70
|
|
|
69
71
|
Your mock data is stored in JSON files in your project root directory:
|
|
70
72
|
|
|
71
|
-
- `endpoints.json`: Contains your project, entity, and endpoint configurations
|
|
72
|
-
- `variables.json`: Contains global variables for use in endpoints
|
|
73
|
+
- `bemirror-endpoints.json`: Contains your project, entity, and endpoint configurations
|
|
74
|
+
- `bemirror-variables.json`: Contains global variables for use in endpoints
|
|
73
75
|
|
|
74
76
|
These files are automatically created when you add data through the UI. You can commit them to your repository to share mock configurations with your team.
|
|
75
77
|
|
package/package.json
CHANGED
package/public/css/style.css
CHANGED
|
@@ -121,8 +121,8 @@ svg {
|
|
|
121
121
|
.app-layout {
|
|
122
122
|
display: flex;
|
|
123
123
|
height: 100vh;
|
|
124
|
-
padding:
|
|
125
|
-
gap:
|
|
124
|
+
padding: 5px;
|
|
125
|
+
gap: 5px;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
.app-footer {
|
|
@@ -440,14 +440,25 @@ svg {
|
|
|
440
440
|
|
|
441
441
|
.live-url-box {
|
|
442
442
|
background: rgba(88, 166, 255, 0.08);
|
|
443
|
-
border: 1px dashed var(--accent);
|
|
444
443
|
border-radius: 8px;
|
|
445
|
-
padding:
|
|
444
|
+
padding: 5px;
|
|
446
445
|
display: flex;
|
|
447
446
|
align-items: center;
|
|
448
447
|
gap: 12px;
|
|
449
448
|
}
|
|
450
449
|
|
|
450
|
+
.method-inline-wrap {
|
|
451
|
+
width: 110px;
|
|
452
|
+
flex-shrink: 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.method-inline-select {
|
|
456
|
+
font-family: var(--font-mono);
|
|
457
|
+
font-weight: 700;
|
|
458
|
+
letter-spacing: 0.6px;
|
|
459
|
+
text-transform: uppercase;
|
|
460
|
+
}
|
|
461
|
+
|
|
451
462
|
.url-badge {
|
|
452
463
|
font-weight: 600;
|
|
453
464
|
font-size: 12px;
|
|
@@ -471,11 +482,81 @@ svg {
|
|
|
471
482
|
text-decoration: underline;
|
|
472
483
|
}
|
|
473
484
|
|
|
485
|
+
.mock-url-editor {
|
|
486
|
+
display: flex;
|
|
487
|
+
align-items: center;
|
|
488
|
+
flex: 1;
|
|
489
|
+
min-width: 0;
|
|
490
|
+
border: 1px solid var(--input-border);
|
|
491
|
+
border-radius: 8px;
|
|
492
|
+
overflow: hidden;
|
|
493
|
+
background: rgba(0, 0, 0, 0.18);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.mock-url-base {
|
|
497
|
+
padding: 9px 12px;
|
|
498
|
+
font-family: var(--font-mono);
|
|
499
|
+
font-size: 12px;
|
|
500
|
+
color: var(--text-muted);
|
|
501
|
+
white-space: nowrap;
|
|
502
|
+
max-width: 260px;
|
|
503
|
+
overflow: hidden;
|
|
504
|
+
text-overflow: ellipsis;
|
|
505
|
+
border-right: 1px solid var(--input-border);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.mock-path-input {
|
|
509
|
+
border: none;
|
|
510
|
+
background: transparent;
|
|
511
|
+
color: var(--text-main);
|
|
512
|
+
font-family: var(--font-mono);
|
|
513
|
+
font-size: 13px;
|
|
514
|
+
padding: 9px 12px;
|
|
515
|
+
min-width: 140px;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.mock-path-input:focus {
|
|
519
|
+
outline: none;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.mock-url-open {
|
|
523
|
+
color: var(--accent);
|
|
524
|
+
font-size: 12px;
|
|
525
|
+
font-family: var(--font-mono);
|
|
526
|
+
text-decoration: none;
|
|
527
|
+
white-space: nowrap;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.mock-url-open:hover {
|
|
531
|
+
text-decoration: underline;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.url-actions-group {
|
|
535
|
+
display: flex;
|
|
536
|
+
gap: 6px;
|
|
537
|
+
margin-left: auto;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.btn-url-action {
|
|
541
|
+
border: 1px solid var(--panel-border);
|
|
542
|
+
background: rgba(255, 255, 255, 0.05);
|
|
543
|
+
color: var(--text-main);
|
|
544
|
+
padding: 6px 10px;
|
|
545
|
+
font-family: var(--font-mono);
|
|
546
|
+
font-size: 11px;
|
|
547
|
+
white-space: nowrap;
|
|
548
|
+
border-radius: 6px;
|
|
549
|
+
cursor: pointer;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.btn-url-action:hover {
|
|
553
|
+
border-color: var(--accent);
|
|
554
|
+
}
|
|
555
|
+
|
|
474
556
|
.prod-url-box {
|
|
475
557
|
background: rgba(46, 160, 67, 0.08);
|
|
476
|
-
border: 1px solid rgba(46, 160, 67, 0.4);
|
|
477
558
|
border-radius: 8px;
|
|
478
|
-
padding:
|
|
559
|
+
padding: 5px;
|
|
479
560
|
display: flex;
|
|
480
561
|
align-items: center;
|
|
481
562
|
gap: 12px;
|
|
@@ -486,6 +567,7 @@ svg {
|
|
|
486
567
|
background: transparent;
|
|
487
568
|
font-family: var(--font-mono);
|
|
488
569
|
font-size: 14px;
|
|
570
|
+
min-height: 38px;
|
|
489
571
|
flex: 1;
|
|
490
572
|
outline: none;
|
|
491
573
|
color: var(--text-main);
|
|
@@ -508,6 +590,15 @@ svg {
|
|
|
508
590
|
background: #2ea043;
|
|
509
591
|
}
|
|
510
592
|
|
|
593
|
+
.btn-send-secondary {
|
|
594
|
+
background: rgba(255, 255, 255, 0.1);
|
|
595
|
+
border-color: var(--panel-border);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.btn-send-secondary:hover {
|
|
599
|
+
background: rgba(255, 255, 255, 0.16);
|
|
600
|
+
}
|
|
601
|
+
|
|
511
602
|
/* PROD RESPONSE PANEL */
|
|
512
603
|
.prod-response-wrapper {
|
|
513
604
|
margin-bottom: 20px;
|
|
@@ -544,6 +635,140 @@ svg {
|
|
|
544
635
|
border-radius: 4px;
|
|
545
636
|
}
|
|
546
637
|
|
|
638
|
+
.response-diff-panel {
|
|
639
|
+
margin-top: 12px;
|
|
640
|
+
border: 1px solid var(--input-border);
|
|
641
|
+
border-radius: 8px;
|
|
642
|
+
background: rgba(13, 17, 23, 0.7);
|
|
643
|
+
padding: 12px;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.response-diff-header {
|
|
647
|
+
display: flex;
|
|
648
|
+
justify-content: space-between;
|
|
649
|
+
align-items: center;
|
|
650
|
+
margin-bottom: 8px;
|
|
651
|
+
font-size: 12px;
|
|
652
|
+
color: var(--text-muted);
|
|
653
|
+
font-weight: 600;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.response-diff-summary {
|
|
657
|
+
font-family: var(--font-mono);
|
|
658
|
+
font-size: 11px;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.response-diff-legend {
|
|
662
|
+
display: flex;
|
|
663
|
+
gap: 8px;
|
|
664
|
+
margin-bottom: 10px;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.legend-item {
|
|
668
|
+
border: 1px solid var(--panel-border);
|
|
669
|
+
border-radius: 999px;
|
|
670
|
+
padding: 2px 8px;
|
|
671
|
+
font-size: 10px;
|
|
672
|
+
font-family: var(--font-mono);
|
|
673
|
+
text-transform: uppercase;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.legend-added {
|
|
677
|
+
background: rgba(46, 160, 67, 0.18);
|
|
678
|
+
color: #59d185;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.legend-removed {
|
|
682
|
+
background: rgba(218, 54, 51, 0.18);
|
|
683
|
+
color: #ff8f87;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.legend-changed {
|
|
687
|
+
background: rgba(210, 153, 34, 0.2);
|
|
688
|
+
color: #f3c86f;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.response-diff-grid {
|
|
692
|
+
display: grid;
|
|
693
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
694
|
+
gap: 12px;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.response-diff-column h4 {
|
|
698
|
+
font-size: 11px;
|
|
699
|
+
color: var(--text-muted);
|
|
700
|
+
margin-bottom: 6px;
|
|
701
|
+
font-weight: 600;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.response-diff-content {
|
|
705
|
+
border: 1px solid var(--panel-border);
|
|
706
|
+
border-radius: 6px;
|
|
707
|
+
max-height: 240px;
|
|
708
|
+
overflow: auto;
|
|
709
|
+
background: rgba(0, 0, 0, 0.25);
|
|
710
|
+
font-family: var(--font-mono);
|
|
711
|
+
font-size: 11px;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.diff-line {
|
|
715
|
+
display: grid;
|
|
716
|
+
grid-template-columns: 36px 1fr;
|
|
717
|
+
gap: 8px;
|
|
718
|
+
padding: 2px 8px;
|
|
719
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
|
720
|
+
white-space: pre;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.diff-line:last-child {
|
|
724
|
+
border-bottom: none;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.diff-line-no {
|
|
728
|
+
text-align: right;
|
|
729
|
+
color: var(--text-muted);
|
|
730
|
+
opacity: 0.8;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.diff-line-text {
|
|
734
|
+
overflow-x: auto;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.diff-line.added,
|
|
738
|
+
.diff-line.changed {
|
|
739
|
+
border-left: 2px solid transparent;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.diff-line.removed,
|
|
743
|
+
.diff-line.changed-old {
|
|
744
|
+
border-left: 2px solid transparent;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.diff-line.added {
|
|
748
|
+
background: rgba(46, 160, 67, 0.15);
|
|
749
|
+
border-left-color: #2ea043;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.diff-line.removed {
|
|
753
|
+
background: rgba(218, 54, 51, 0.15);
|
|
754
|
+
border-left-color: #da3633;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.diff-line.changed {
|
|
758
|
+
background: rgba(210, 153, 34, 0.2);
|
|
759
|
+
border-left-color: #d29922;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.diff-line.changed-old {
|
|
763
|
+
background: rgba(210, 153, 34, 0.12);
|
|
764
|
+
border-left-color: #d29922;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.diff-placeholder {
|
|
768
|
+
padding: 12px;
|
|
769
|
+
color: var(--text-muted);
|
|
770
|
+
}
|
|
771
|
+
|
|
547
772
|
/* Forms */
|
|
548
773
|
.form-row {
|
|
549
774
|
display: flex;
|
|
@@ -880,16 +1105,51 @@ select:focus {
|
|
|
880
1105
|
align-items: flex-start;
|
|
881
1106
|
}
|
|
882
1107
|
|
|
883
|
-
|
|
884
|
-
#btnCopyFetch {
|
|
885
|
-
margin-left: 0;
|
|
886
|
-
margin-top: 10px;
|
|
1108
|
+
.method-inline-wrap {
|
|
887
1109
|
width: 100%;
|
|
888
1110
|
}
|
|
889
1111
|
|
|
890
1112
|
.live-url-box > div {
|
|
1113
|
+
width: 100%;
|
|
891
1114
|
flex-wrap: wrap;
|
|
892
1115
|
}
|
|
1116
|
+
|
|
1117
|
+
.mock-url-editor {
|
|
1118
|
+
width: 100%;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.mock-url-base {
|
|
1122
|
+
max-width: 100%;
|
|
1123
|
+
overflow: hidden;
|
|
1124
|
+
text-overflow: ellipsis;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.url-actions-group {
|
|
1128
|
+
width: 100%;
|
|
1129
|
+
margin-left: 0;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.btn-url-action {
|
|
1133
|
+
flex: 1;
|
|
1134
|
+
text-align: center;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.prod-url-box {
|
|
1138
|
+
flex-wrap: wrap;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.prod-url-input {
|
|
1142
|
+
min-width: 100%;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.btn-send,
|
|
1146
|
+
.btn-send-secondary {
|
|
1147
|
+
flex: 1;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.response-diff-grid {
|
|
1151
|
+
grid-template-columns: 1fr;
|
|
1152
|
+
}
|
|
893
1153
|
}
|
|
894
1154
|
|
|
895
1155
|
/* SPLIT EDITOR LAYOUT */
|
package/public/index.html
CHANGED
|
@@ -56,8 +56,9 @@
|
|
|
56
56
|
<br />
|
|
57
57
|
<p>
|
|
58
58
|
<strong>🔧 Variables:</strong> Use variables in your endpoints with {{variableName}} syntax.
|
|
59
|
-
Create a variables.json file in the project root to define global variables like base
|
|
60
|
-
usernames, and passwords. This file should be added to .gitignore to keep sensitive data
|
|
59
|
+
Create a bemirror-variables.json file in the project root to define global variables like base
|
|
60
|
+
URLs, usernames, and passwords. This file should be added to .gitignore to keep sensitive data
|
|
61
|
+
private.
|
|
61
62
|
</p>
|
|
62
63
|
</div>
|
|
63
64
|
<div class="modal-footer form-actions" style="margin-top: 20px">
|
|
@@ -310,43 +311,36 @@
|
|
|
310
311
|
|
|
311
312
|
<!-- MOCK URL AREA -->
|
|
312
313
|
<div class="http-clients-area">
|
|
313
|
-
<div class="live-url-box"
|
|
314
|
-
<div
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
314
|
+
<div class="live-url-box">
|
|
315
|
+
<div class="method-inline-wrap">
|
|
316
|
+
<select id="method" required class="method-inline-select">
|
|
317
|
+
<option value="GET">GET</option>
|
|
318
|
+
<option value="POST">POST</option>
|
|
319
|
+
<option value="PUT">PUT</option>
|
|
320
|
+
<option value="PATCH">PATCH</option>
|
|
321
|
+
<option value="DELETE">DELETE</option>
|
|
322
|
+
</select>
|
|
323
|
+
</div>
|
|
324
|
+
<div style="display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0">
|
|
325
|
+
<div class="mock-url-editor">
|
|
326
|
+
<span id="mockUrlBase" class="mock-url-base"></span>
|
|
327
|
+
<input type="text" id="mockPathInput" class="mock-path-input" placeholder="/users/profile" />
|
|
328
|
+
</div>
|
|
329
|
+
<a href="#" id="liveUrlOpenLink" target="_blank" class="mock-url-open">Open</a>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="url-actions-group">
|
|
332
|
+
<button id="btnCopyMockUrl" class="btn-url-action" title="Copy mock URL">Copy URL</button>
|
|
333
|
+
<button id="btnCopyCurl" class="btn-url-action" title="Copy terminal cURL command">Copy cURL</button>
|
|
334
|
+
<button id="btnCopyFetch" class="btn-url-action" title="Copy JS fetch()">Copy fetch</button>
|
|
318
335
|
</div>
|
|
319
|
-
<button id="btnCopyCurl" class="btn-icon" title="Copy terminal cURL command" style="
|
|
320
|
-
border: 1px solid var(--panel-border);
|
|
321
|
-
background: rgba(255, 255, 255, 0.05);
|
|
322
|
-
padding: 4px 10px;
|
|
323
|
-
font-family: monospace;
|
|
324
|
-
font-size: 11px;
|
|
325
|
-
white-space: nowrap;
|
|
326
|
-
border-radius: 4px;
|
|
327
|
-
margin-left: 10px;
|
|
328
|
-
">
|
|
329
|
-
Copy cURL
|
|
330
|
-
</button>
|
|
331
|
-
<button id="btnCopyFetch" class="btn-icon" title="Copy JS fetch()" style="
|
|
332
|
-
border: 1px solid var(--panel-border);
|
|
333
|
-
background: rgba(255, 255, 255, 0.05);
|
|
334
|
-
padding: 4px 10px;
|
|
335
|
-
font-family: monospace;
|
|
336
|
-
font-size: 11px;
|
|
337
|
-
white-space: nowrap;
|
|
338
|
-
border-radius: 4px;
|
|
339
|
-
margin-left: 5px;
|
|
340
|
-
">
|
|
341
|
-
Copy fetch
|
|
342
|
-
</button>
|
|
343
336
|
</div>
|
|
344
337
|
|
|
345
338
|
<!-- PROD URL TESTER AREA -->
|
|
346
339
|
<div class="prod-url-box">
|
|
347
|
-
<div class="url-badge" style="color: var(--success)"
|
|
340
|
+
<div class="url-badge" style="color: var(--success)">Prod URL</div>
|
|
348
341
|
<input type="text" id="prodUrlInput" placeholder="https://api.example.com/production/route"
|
|
349
342
|
class="prod-url-input" />
|
|
343
|
+
<button id="btnCopyProdUrl" class="btn-send btn-send-secondary">Copy URL</button>
|
|
350
344
|
<button id="btnSendProd" class="btn-send">Send Request 🚀</button>
|
|
351
345
|
</div>
|
|
352
346
|
</div>
|
|
@@ -366,34 +360,35 @@
|
|
|
366
360
|
</div>
|
|
367
361
|
</div>
|
|
368
362
|
<div id="editorProdResponse" class="ace-wrapper"
|
|
369
|
-
style="height:
|
|
363
|
+
style="height: 280px; border-radius: 0 0 8px 8px; border-top: none"></div>
|
|
364
|
+
<div id="responseDiffPanel" class="response-diff-panel" style="display: none">
|
|
365
|
+
<div class="response-diff-header">
|
|
366
|
+
<span>Expected vs Production Diff</span>
|
|
367
|
+
<div id="responseDiffSummary" class="response-diff-summary">No diff yet.</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="response-diff-legend">
|
|
370
|
+
<span class="legend-item legend-added">Added</span>
|
|
371
|
+
<span class="legend-item legend-removed">Removed</span>
|
|
372
|
+
<span class="legend-item legend-changed">Changed</span>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="response-diff-grid">
|
|
375
|
+
<div class="response-diff-column">
|
|
376
|
+
<h4>Expected (Mock)</h4>
|
|
377
|
+
<div id="expectedDiffContent" class="response-diff-content"></div>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="response-diff-column">
|
|
380
|
+
<h4>Received (Prod)</h4>
|
|
381
|
+
<div id="receivedDiffContent" class="response-diff-content"></div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
370
385
|
</div>
|
|
371
386
|
|
|
372
|
-
<form id="endpointForm"
|
|
387
|
+
<form id="endpointForm">
|
|
373
388
|
<input type="hidden" id="epId" />
|
|
374
389
|
<input type="hidden" id="epEntityId" />
|
|
375
390
|
<input type="hidden" id="epProjectId" />
|
|
376
391
|
|
|
377
|
-
<div class="form-row">
|
|
378
|
-
<div class="form-group flex-1">
|
|
379
|
-
<label for="method">Method</label>
|
|
380
|
-
<select id="method" required>
|
|
381
|
-
<option value="GET">GET</option>
|
|
382
|
-
<option value="POST">POST</option>
|
|
383
|
-
<option value="PUT">PUT</option>
|
|
384
|
-
<option value="PATCH">PATCH</option>
|
|
385
|
-
<option value="DELETE">DELETE</option>
|
|
386
|
-
</select>
|
|
387
|
-
</div>
|
|
388
|
-
<div class="form-group flex-4">
|
|
389
|
-
<label for="path">Final Path Route</label>
|
|
390
|
-
<div class="input-prefix">
|
|
391
|
-
<span class="prefix">Path:</span>
|
|
392
|
-
<input type="text" id="path" placeholder="/users/profile" required />
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
|
|
397
392
|
<!-- Tabs -->
|
|
398
393
|
<!-- Tabs & Response View -->
|
|
399
394
|
<div class="split-editor-layout">
|
|
@@ -420,14 +415,14 @@
|
|
|
420
415
|
<p class="help-text">
|
|
421
416
|
JSON format. Endpoint strict validation. Example: {"id": "5"}
|
|
422
417
|
</p>
|
|
423
|
-
<div id="editorParams" class="ace-wrapper" style="height:
|
|
418
|
+
<div id="editorParams" class="ace-wrapper" style="height: 380px"></div>
|
|
424
419
|
</div>
|
|
425
420
|
<div class="tab-pane active" id="tab-payload">
|
|
426
421
|
<label>Expected Body Payload</label>
|
|
427
422
|
<p class="help-text">
|
|
428
423
|
Mock server returns 400 Bad Request if missing top-level keys.
|
|
429
424
|
</p>
|
|
430
|
-
<div id="editorPayload" class="ace-wrapper" style="height:
|
|
425
|
+
<div id="editorPayload" class="ace-wrapper" style="height: 380px"></div>
|
|
431
426
|
</div>
|
|
432
427
|
<div class="tab-pane" id="tab-auth">
|
|
433
428
|
<label>Authorization</label>
|
|
@@ -455,7 +450,7 @@
|
|
|
455
450
|
<p class="help-text">
|
|
456
451
|
Add custom headers to the response. JSON format: [{"key": "Content-Type", "value": "application/json"}]
|
|
457
452
|
</p>
|
|
458
|
-
<div id="editorHeaders" class="ace-wrapper" style="height:
|
|
453
|
+
<div id="editorHeaders" class="ace-wrapper" style="height: 380px"></div>
|
|
459
454
|
</div>
|
|
460
455
|
</div>
|
|
461
456
|
</div>
|
|
@@ -481,12 +476,12 @@
|
|
|
481
476
|
</div>
|
|
482
477
|
</div>
|
|
483
478
|
<label>Provide Response Body (JSON or Text)</label>
|
|
484
|
-
<div id="editorResponse" class="ace-wrapper" style="flex: 1; min-height:
|
|
479
|
+
<div id="editorResponse" class="ace-wrapper" style="flex: 1; min-height: 320px"></div>
|
|
485
480
|
</div>
|
|
486
481
|
</div>
|
|
487
482
|
<div class="form-actions">
|
|
488
483
|
<button type="submit" class="btn-primary" id="saveEndpointBtn">
|
|
489
|
-
Save Configuration <span class="shortcut-hint">Ctrl +
|
|
484
|
+
Save Configuration <span class="shortcut-hint">Ctrl/Cmd + S</span>
|
|
490
485
|
</button>
|
|
491
486
|
</div>
|
|
492
487
|
</form>
|
package/public/js/app.js
CHANGED
|
@@ -620,6 +620,7 @@ window.addEndpoint = async (projectId, entityId) => {
|
|
|
620
620
|
expectedParams: '',
|
|
621
621
|
expectedPayload: '',
|
|
622
622
|
responseBody: '{"success": true}',
|
|
623
|
+
prodUrl: '',
|
|
623
624
|
}
|
|
624
625
|
ent.endpoints.push(ep)
|
|
625
626
|
await syncData()
|
|
@@ -657,33 +658,242 @@ const getActiveEndpointData = () =>
|
|
|
657
658
|
?.entities.find((e) => e.id === activeIds.entity)
|
|
658
659
|
?.endpoints.find((ep) => ep.id === activeIds.endpoint)
|
|
659
660
|
|
|
661
|
+
function getEndpointContext(projId, entId, epId) {
|
|
662
|
+
const project = state.projects.find((p) => p.id === projId)
|
|
663
|
+
const entity = project?.entities.find((e) => e.id === entId)
|
|
664
|
+
const endpoint = entity?.endpoints.find((ep) => ep.id === epId)
|
|
665
|
+
if (!project || !entity || !endpoint) return null
|
|
666
|
+
return { project, entity, endpoint }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function getMockBaseUrl(project, entity) {
|
|
670
|
+
return `${window.location.origin}/mock/${slugify(project.name)}/${slugify(entity.name)}`
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function getDefaultProdUrl(project, entity, endpoint) {
|
|
674
|
+
const bUrl = (project.baseUrl || '').replace(/\/$/, '')
|
|
675
|
+
const fPath = normalizePathInput(endpoint.path)
|
|
676
|
+
const eSlug = slugify(entity.name)
|
|
677
|
+
return bUrl ? `${bUrl}/${eSlug}${fPath}` : ''
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function truncateText(value, maxLen) {
|
|
681
|
+
if (!value || value.length <= maxLen) return value
|
|
682
|
+
return `${value.slice(0, maxLen)}...`
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function normalizePathInput(pathValue) {
|
|
686
|
+
const trimmed = (pathValue || '').trim()
|
|
687
|
+
if (!trimmed) return '/'
|
|
688
|
+
return trimmed.startsWith('/') ? trimmed : '/' + trimmed
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function resolveTemplateVariables(value, vars = {}) {
|
|
692
|
+
if (!value || typeof value !== 'string') return value
|
|
693
|
+
return value.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
694
|
+
return vars[key] !== undefined ? String(vars[key]) : match
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function getCurrentMockUrl(project, entity) {
|
|
699
|
+
const mockPathInput = document.getElementById('mockPathInput')
|
|
700
|
+
const pathValue = normalizePathInput(mockPathInput?.value || '/')
|
|
701
|
+
return `${getMockBaseUrl(project, entity)}${pathValue}`
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function updateMockUrlEditor(project, entity, endpoint) {
|
|
705
|
+
const base = getMockBaseUrl(project, entity)
|
|
706
|
+
const normalizedPath = normalizePathInput(endpoint.path)
|
|
707
|
+
const fullUrl = `${base}${normalizedPath}`
|
|
708
|
+
|
|
709
|
+
const mockBaseEl = document.getElementById('mockUrlBase')
|
|
710
|
+
mockBaseEl.innerText = truncateText(base, 25)
|
|
711
|
+
mockBaseEl.title = base
|
|
712
|
+
document.getElementById('mockPathInput').value = normalizedPath
|
|
713
|
+
document.getElementById('liveUrlOpenLink').href = fullUrl
|
|
714
|
+
document.getElementById('liveUrlOpenLink').title = fullUrl
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function refreshMockOpenLink() {
|
|
718
|
+
const context = getEndpointContext(activeIds.project, activeIds.entity, activeIds.endpoint)
|
|
719
|
+
if (!context) return
|
|
720
|
+
const fullUrl = getCurrentMockUrl(context.project, context.entity)
|
|
721
|
+
document.getElementById('liveUrlOpenLink').href = fullUrl
|
|
722
|
+
document.getElementById('liveUrlOpenLink').title = fullUrl
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function persistActiveProdUrl() {
|
|
726
|
+
const ep = getActiveEndpointData()
|
|
727
|
+
if (!ep) return
|
|
728
|
+
ep.prodUrl = document.getElementById('prodUrlInput').value.trim()
|
|
729
|
+
await syncData()
|
|
730
|
+
}
|
|
731
|
+
|
|
660
732
|
function computeLiveUrl(proj, ent, epObj) {
|
|
661
|
-
const base =
|
|
662
|
-
const finalPath = epObj.path
|
|
733
|
+
const base = getMockBaseUrl(proj, ent)
|
|
734
|
+
const finalPath = normalizePathInput(epObj.path)
|
|
663
735
|
return base + finalPath
|
|
664
736
|
}
|
|
665
737
|
|
|
738
|
+
function escapeHtml(str) {
|
|
739
|
+
return str
|
|
740
|
+
.replace(/&/g, '&')
|
|
741
|
+
.replace(/</g, '<')
|
|
742
|
+
.replace(/>/g, '>')
|
|
743
|
+
.replace(/"/g, '"')
|
|
744
|
+
.replace(/'/g, ''')
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function normalizeResponseText(rawText) {
|
|
748
|
+
if (!rawText || !rawText.trim()) return ''
|
|
749
|
+
try {
|
|
750
|
+
return JSON.stringify(JSON.parse(rawText), null, 2)
|
|
751
|
+
} catch (e) {
|
|
752
|
+
return rawText
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function clearResponseDiff(message = 'No diff yet.') {
|
|
757
|
+
const panel = document.getElementById('responseDiffPanel')
|
|
758
|
+
const summary = document.getElementById('responseDiffSummary')
|
|
759
|
+
const expected = document.getElementById('expectedDiffContent')
|
|
760
|
+
const received = document.getElementById('receivedDiffContent')
|
|
761
|
+
|
|
762
|
+
panel.style.display = 'none'
|
|
763
|
+
summary.innerText = message
|
|
764
|
+
expected.innerHTML = `<div class="diff-placeholder">${escapeHtml(message)}</div>`
|
|
765
|
+
received.innerHTML = `<div class="diff-placeholder">${escapeHtml(message)}</div>`
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function buildLineDiff(expectedLines, receivedLines) {
|
|
769
|
+
const n = expectedLines.length
|
|
770
|
+
const m = receivedLines.length
|
|
771
|
+
const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0))
|
|
772
|
+
|
|
773
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
774
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
775
|
+
if (expectedLines[i] === receivedLines[j]) {
|
|
776
|
+
dp[i][j] = dp[i + 1][j + 1] + 1
|
|
777
|
+
} else {
|
|
778
|
+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const events = []
|
|
784
|
+
let i = 0
|
|
785
|
+
let j = 0
|
|
786
|
+
while (i < n && j < m) {
|
|
787
|
+
if (expectedLines[i] === receivedLines[j]) {
|
|
788
|
+
events.push({ type: 'equal', left: expectedLines[i], right: receivedLines[j] })
|
|
789
|
+
i++
|
|
790
|
+
j++
|
|
791
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
792
|
+
events.push({ type: 'remove', left: expectedLines[i], right: '' })
|
|
793
|
+
i++
|
|
794
|
+
} else {
|
|
795
|
+
events.push({ type: 'add', left: '', right: receivedLines[j] })
|
|
796
|
+
j++
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
while (i < n) {
|
|
801
|
+
events.push({ type: 'remove', left: expectedLines[i], right: '' })
|
|
802
|
+
i++
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
while (j < m) {
|
|
806
|
+
events.push({ type: 'add', left: '', right: receivedLines[j] })
|
|
807
|
+
j++
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
for (let k = 0; k < events.length - 1; k++) {
|
|
811
|
+
if (events[k].type === 'remove' && events[k + 1].type === 'add') {
|
|
812
|
+
events[k].type = 'changed-old'
|
|
813
|
+
events[k + 1].type = 'changed'
|
|
814
|
+
k++
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return events
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function renderDiffColumnHtml(events, side) {
|
|
822
|
+
let lineNo = 0
|
|
823
|
+
const rows = events.map((event) => {
|
|
824
|
+
const text = side === 'left' ? event.left : event.right
|
|
825
|
+
if (text) lineNo++
|
|
826
|
+
const safeText = text ? escapeHtml(text) : ' '
|
|
827
|
+
const num = text ? lineNo : ''
|
|
828
|
+
return `<div class="diff-line ${event.type}"><span class="diff-line-no">${num}</span><span class="diff-line-text">${safeText}</span></div>`
|
|
829
|
+
})
|
|
830
|
+
return rows.join('')
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function renderResponseDiff(expectedRaw, receivedRaw) {
|
|
834
|
+
const panel = document.getElementById('responseDiffPanel')
|
|
835
|
+
const summary = document.getElementById('responseDiffSummary')
|
|
836
|
+
const expected = document.getElementById('expectedDiffContent')
|
|
837
|
+
const received = document.getElementById('receivedDiffContent')
|
|
838
|
+
|
|
839
|
+
const expectedText = normalizeResponseText(expectedRaw)
|
|
840
|
+
const receivedText = normalizeResponseText(receivedRaw)
|
|
841
|
+
|
|
842
|
+
if (!expectedText && !receivedText) {
|
|
843
|
+
clearResponseDiff('Expected and production responses are both empty.')
|
|
844
|
+
return
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const expectedLines = expectedText.split('\n')
|
|
848
|
+
const receivedLines = receivedText.split('\n')
|
|
849
|
+
const totalLines = expectedLines.length + receivedLines.length
|
|
850
|
+
const maxLineBudget = 1200
|
|
851
|
+
|
|
852
|
+
if (totalLines > maxLineBudget) {
|
|
853
|
+
panel.style.display = 'block'
|
|
854
|
+
summary.innerText = `Diff skipped: response too large (${totalLines} lines).`
|
|
855
|
+
expected.innerHTML = `<div class="diff-placeholder">Expected response is too large for detailed diff render.</div>`
|
|
856
|
+
received.innerHTML = `<div class="diff-placeholder">Received response is too large for detailed diff render.</div>`
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const events = buildLineDiff(expectedLines, receivedLines)
|
|
861
|
+
let added = 0
|
|
862
|
+
let removed = 0
|
|
863
|
+
let changed = 0
|
|
864
|
+
|
|
865
|
+
events.forEach((event) => {
|
|
866
|
+
if (event.type === 'add') added++
|
|
867
|
+
if (event.type === 'remove') removed++
|
|
868
|
+
if (event.type === 'changed') changed++
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
const hasDiff = added > 0 || removed > 0 || changed > 0
|
|
872
|
+
panel.style.display = 'block'
|
|
873
|
+
summary.innerText = hasDiff
|
|
874
|
+
? `Added: ${added} | Removed: ${removed} | Changed: ${changed}`
|
|
875
|
+
: 'No differences between expected and production response.'
|
|
876
|
+
|
|
877
|
+
expected.innerHTML = renderDiffColumnHtml(events, 'left')
|
|
878
|
+
received.innerHTML = renderDiffColumnHtml(events, 'right')
|
|
879
|
+
}
|
|
880
|
+
|
|
666
881
|
function openEndpointEditor() {
|
|
667
882
|
const ep = getActiveEndpointData()
|
|
668
883
|
const p = state.projects.find((p) => p.id === activeIds.project)
|
|
669
884
|
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
670
885
|
if (!ep) return
|
|
671
886
|
|
|
887
|
+
clearResponseDiff('Run a production request to see highlighted differences.')
|
|
888
|
+
|
|
672
889
|
document.getElementById('editorBreadcrumbs').innerText = `${p.name} > ${e.name}`
|
|
673
890
|
|
|
674
|
-
|
|
675
|
-
document.getElementById('liveUrlLink').innerText = fullUrl
|
|
676
|
-
document.getElementById('liveUrlLink').href = fullUrl
|
|
891
|
+
updateMockUrlEditor(p, e, ep)
|
|
677
892
|
|
|
678
|
-
|
|
679
|
-
const bUrl = (p.baseUrl || '').replace(/\/$/, '')
|
|
680
|
-
const fPath = ep.path.startsWith('/') ? ep.path : '/' + ep.path
|
|
681
|
-
const eSlug = slugify(e.name)
|
|
682
|
-
document.getElementById('prodUrlInput').value = bUrl ? `${bUrl}/${eSlug}${fPath}` : ''
|
|
893
|
+
document.getElementById('prodUrlInput').value = ep.prodUrl || getDefaultProdUrl(p, e, ep)
|
|
683
894
|
|
|
684
895
|
document.getElementById('epId').value = ep.id
|
|
685
896
|
document.getElementById('method').value = ep.method
|
|
686
|
-
document.getElementById('path').value = ep.path
|
|
687
897
|
document.getElementById('statusCode').value = ep.statusCode || 200
|
|
688
898
|
document.getElementById('delay').value = ep.delay || 0
|
|
689
899
|
|
|
@@ -705,9 +915,7 @@ document.getElementById('endpointForm').addEventListener('submit', async (ev) =>
|
|
|
705
915
|
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
706
916
|
|
|
707
917
|
ep.method = document.getElementById('method').value
|
|
708
|
-
|
|
709
|
-
if (!rawPath.startsWith('/')) rawPath = '/' + rawPath
|
|
710
|
-
ep.path = rawPath
|
|
918
|
+
ep.path = normalizePathInput(document.getElementById('mockPathInput').value)
|
|
711
919
|
ep.statusCode = parseInt(document.getElementById('statusCode').value, 10)
|
|
712
920
|
ep.delay = parseInt(document.getElementById('delay').value, 10)
|
|
713
921
|
ep.expectedPayload = editors.payload.getValue()
|
|
@@ -727,19 +935,16 @@ document.getElementById('endpointForm').addEventListener('submit', async (ev) =>
|
|
|
727
935
|
ep.headers = []
|
|
728
936
|
}
|
|
729
937
|
|
|
730
|
-
|
|
731
|
-
document.getElementById('liveUrlLink').innerText = fullUrl
|
|
732
|
-
document.getElementById('liveUrlLink').href = fullUrl
|
|
938
|
+
ep.prodUrl = document.getElementById('prodUrlInput').value.trim()
|
|
733
939
|
|
|
734
|
-
|
|
735
|
-
const eSlug = slugify(e.name)
|
|
736
|
-
document.getElementById('prodUrlInput').value = bUrl ? `${bUrl}/${eSlug}${ep.path}` : ''
|
|
940
|
+
updateMockUrlEditor(p, e, ep)
|
|
737
941
|
|
|
738
942
|
const btn = document.getElementById('saveEndpointBtn')
|
|
739
943
|
const originalHtml = btn.innerHTML
|
|
740
944
|
btn.innerHTML = '✓ Saved Successfully!'
|
|
741
945
|
btn.style.background = 'var(--success)'
|
|
742
946
|
await syncData()
|
|
947
|
+
clearResponseDiff('Expected response changed. Run production request to compare again.')
|
|
743
948
|
renderTree()
|
|
744
949
|
setTimeout(() => {
|
|
745
950
|
btn.innerHTML = originalHtml
|
|
@@ -777,6 +982,46 @@ function bindGlobalEvents() {
|
|
|
777
982
|
document.getElementById('saveEndpointBtn').click()
|
|
778
983
|
}
|
|
779
984
|
}
|
|
985
|
+
|
|
986
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
|
|
987
|
+
const variablesModalOpen = document.getElementById('modalVariables').style.display === 'flex'
|
|
988
|
+
const editorScreenOpen = document.getElementById('editorScreen').style.display !== 'none'
|
|
989
|
+
if (!variablesModalOpen && !editorScreenOpen) return
|
|
990
|
+
|
|
991
|
+
e.preventDefault()
|
|
992
|
+
|
|
993
|
+
if (variablesModalOpen) {
|
|
994
|
+
document.getElementById('btnSaveVariables').click()
|
|
995
|
+
return
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
document.getElementById('saveEndpointBtn').click()
|
|
999
|
+
}
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
document.getElementById('mockPathInput').addEventListener('input', () => {
|
|
1003
|
+
refreshMockOpenLink()
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
document.getElementById('mockPathInput').addEventListener('blur', () => {
|
|
1007
|
+
const input = document.getElementById('mockPathInput')
|
|
1008
|
+
input.value = normalizePathInput(input.value)
|
|
1009
|
+
refreshMockOpenLink()
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
document.getElementById('mockPathInput').addEventListener('keydown', (e) => {
|
|
1013
|
+
if (e.key !== 'Enter') return
|
|
1014
|
+
e.preventDefault()
|
|
1015
|
+
const input = document.getElementById('mockPathInput')
|
|
1016
|
+
input.value = normalizePathInput(input.value)
|
|
1017
|
+
refreshMockOpenLink()
|
|
1018
|
+
document.getElementById('saveEndpointBtn').click()
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
document.getElementById('prodUrlInput').addEventListener('keydown', async (e) => {
|
|
1022
|
+
if (e.key !== 'Enter') return
|
|
1023
|
+
e.preventDefault()
|
|
1024
|
+
await persistActiveProdUrl()
|
|
780
1025
|
})
|
|
781
1026
|
|
|
782
1027
|
// Mobile menu toggle
|
|
@@ -903,13 +1148,24 @@ function bindGlobalEvents() {
|
|
|
903
1148
|
})
|
|
904
1149
|
|
|
905
1150
|
// Export as cURL command
|
|
1151
|
+
document.getElementById('btnCopyMockUrl').addEventListener('click', () => {
|
|
1152
|
+
const context = getEndpointContext(activeIds.project, activeIds.entity, activeIds.endpoint)
|
|
1153
|
+
if (!context) return
|
|
1154
|
+
const mockUrl = getCurrentMockUrl(context.project, context.entity)
|
|
1155
|
+
navigator.clipboard.writeText(mockUrl).then(() => {
|
|
1156
|
+
const btn = document.getElementById('btnCopyMockUrl')
|
|
1157
|
+
btn.innerText = '✓ Copied!'
|
|
1158
|
+
setTimeout(() => (btn.innerText = 'Copy URL'), 2000)
|
|
1159
|
+
})
|
|
1160
|
+
})
|
|
1161
|
+
|
|
906
1162
|
document.getElementById('btnCopyCurl').addEventListener('click', () => {
|
|
907
1163
|
const ep = getActiveEndpointData()
|
|
908
1164
|
if (!ep) return
|
|
909
1165
|
const p = state.projects.find((p) => p.id === activeIds.project)
|
|
910
1166
|
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
911
1167
|
|
|
912
|
-
let finalUrl =
|
|
1168
|
+
let finalUrl = getCurrentMockUrl(p, e)
|
|
913
1169
|
if (ep.expectedParams) {
|
|
914
1170
|
try {
|
|
915
1171
|
const pg = JSON.parse(ep.expectedParams)
|
|
@@ -938,7 +1194,7 @@ function bindGlobalEvents() {
|
|
|
938
1194
|
const p = state.projects.find((p) => p.id === activeIds.project)
|
|
939
1195
|
const e = p.entities.find((e) => e.id === activeIds.entity)
|
|
940
1196
|
|
|
941
|
-
let finalUrl =
|
|
1197
|
+
let finalUrl = getCurrentMockUrl(p, e)
|
|
942
1198
|
if (ep.expectedParams) {
|
|
943
1199
|
try {
|
|
944
1200
|
const pg = JSON.parse(ep.expectedParams)
|
|
@@ -966,14 +1222,27 @@ function bindGlobalEvents() {
|
|
|
966
1222
|
})
|
|
967
1223
|
})
|
|
968
1224
|
|
|
1225
|
+
document.getElementById('btnCopyProdUrl').addEventListener('click', () => {
|
|
1226
|
+
const rawProdUrl = document.getElementById('prodUrlInput').value.trim()
|
|
1227
|
+
const resolvedProdUrl = resolveTemplateVariables(rawProdUrl, variables)
|
|
1228
|
+
if (!resolvedProdUrl) return
|
|
1229
|
+
navigator.clipboard.writeText(resolvedProdUrl).then(() => {
|
|
1230
|
+
const btn = document.getElementById('btnCopyProdUrl')
|
|
1231
|
+
btn.innerText = '✓ Copied!'
|
|
1232
|
+
setTimeout(() => (btn.innerText = 'Copy URL'), 2000)
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1235
|
+
|
|
969
1236
|
// Prod URL HTTP Client logic
|
|
970
1237
|
document.getElementById('btnSendProd').addEventListener('click', async () => {
|
|
971
|
-
const
|
|
972
|
-
|
|
1238
|
+
const rawUrl = document.getElementById('prodUrlInput').value.trim()
|
|
1239
|
+
const resolvedUrl = resolveTemplateVariables(rawUrl, variables)
|
|
1240
|
+
if (!resolvedUrl) return alert('Please enter a Production URL first.')
|
|
973
1241
|
const ep = getActiveEndpointData()
|
|
974
1242
|
|
|
975
1243
|
document.getElementById('prodResponseWrapper').style.display = 'block'
|
|
976
1244
|
editors.prodResp.setValue('Sending request...', -1)
|
|
1245
|
+
clearResponseDiff('Comparing responses...')
|
|
977
1246
|
document.getElementById('prodStatus').innerText = 'Pending'
|
|
978
1247
|
document.getElementById('prodTime').innerText = '--'
|
|
979
1248
|
|
|
@@ -982,7 +1251,7 @@ function bindGlobalEvents() {
|
|
|
982
1251
|
method: 'POST',
|
|
983
1252
|
headers: { 'Content-Type': 'application/json' },
|
|
984
1253
|
body: JSON.stringify({
|
|
985
|
-
url:
|
|
1254
|
+
url: resolvedUrl,
|
|
986
1255
|
method: ep.method,
|
|
987
1256
|
params: editors.params.getValue(),
|
|
988
1257
|
payload: editors.payload.getValue(),
|
|
@@ -993,16 +1262,19 @@ function bindGlobalEvents() {
|
|
|
993
1262
|
if (data.error && !data.status) {
|
|
994
1263
|
document.getElementById('prodStatus').innerText = 'ERROR'
|
|
995
1264
|
editors.prodResp.setValue(JSON.stringify(data.error, null, 2), -1)
|
|
1265
|
+
clearResponseDiff('Production request failed before diff could run.')
|
|
996
1266
|
} else {
|
|
997
1267
|
document.getElementById('prodStatus').innerText = data.status + ' Http St'
|
|
998
1268
|
document.getElementById('prodTime').innerText = data.time + 'ms'
|
|
999
1269
|
let outputStr =
|
|
1000
1270
|
typeof data.body === 'object' ? JSON.stringify(data.body, null, 2) : data.body
|
|
1001
1271
|
editors.prodResp.setValue(outputStr, -1)
|
|
1272
|
+
renderResponseDiff(editors.response.getValue(), outputStr || '')
|
|
1002
1273
|
}
|
|
1003
1274
|
} catch (err) {
|
|
1004
1275
|
document.getElementById('prodStatus').innerText = 'CRASH'
|
|
1005
1276
|
editors.prodResp.setValue('Network or proxy engine error.', -1)
|
|
1277
|
+
clearResponseDiff('Network or proxy error. Diff is unavailable.')
|
|
1006
1278
|
}
|
|
1007
1279
|
})
|
|
1008
1280
|
}
|
package/server.js
CHANGED
|
@@ -50,8 +50,10 @@ const saveJsonFile = (filePath, data) => {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const createBemirrorApp = (options = {}) => {
|
|
53
|
-
const ENDPOINTS_FILE =
|
|
54
|
-
|
|
53
|
+
const ENDPOINTS_FILE =
|
|
54
|
+
options.endpointsFile || path.join(__dirname, '../../..', 'bemirror-endpoints.json')
|
|
55
|
+
const VARIABLES_FILE =
|
|
56
|
+
options.variablesFile || path.join(__dirname, '../../..', 'bemirror-variables.json')
|
|
55
57
|
const PUBLIC_DIR = options.publicDir || path.join(__dirname, 'public')
|
|
56
58
|
|
|
57
59
|
const app = express()
|