@jsenv/core 41.5.35 → 41.5.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/build.js +27 -2
- package/dist/client/client_monitoring/clients_page.html +1459 -0
- package/dist/client/https_trust/https_trust.html +362 -0
- package/dist/{page_picker.js → client/page_switcher/page_switcher.js} +222 -1
- package/dist/start_dev_server/start_dev_server.js +155 -8
- package/package.json +2 -2
- package/src/dev/dev_server_plugins/client/https_trust.html +352 -0
- package/src/dev/dev_server_plugins/dev_server_plugin_https_trust.js +93 -0
- package/src/dev/start_dev_server.js +35 -1
- package/src/kitchen/errors.js +26 -1
- package/dist/html/clients_page.html +0 -651
- package/dist/js/page_switcher.js +0 -224
- /package/dist/{html → client/client_monitoring}/client_monitor_page.html +0 -0
- /package/dist/{js → client/client_monitoring}/client_reporter.js +0 -0
- /package/dist/{js → client/dependency_status}/dependency_status.js +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
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">
|
|
6
|
+
<title>Trust the dev server https</title>
|
|
7
|
+
<style>:root {
|
|
8
|
+
--lightningcss-light: initial;
|
|
9
|
+
--lightningcss-dark: ;
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--ink: #111827;
|
|
12
|
+
--muted: #6b7280;
|
|
13
|
+
--paper: #f3f4f6;
|
|
14
|
+
--card: #fff;
|
|
15
|
+
--line: #e5e7eb;
|
|
16
|
+
--accent: #2563eb;
|
|
17
|
+
--accent-ink: #fff;
|
|
18
|
+
--ok: #15803d;
|
|
19
|
+
--ok-paper: #dcfce7;
|
|
20
|
+
--warn: #b45309;
|
|
21
|
+
--warn-paper: #fef3c7;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
:root {
|
|
26
|
+
--lightningcss-light: ;
|
|
27
|
+
--lightningcss-dark: initial;
|
|
28
|
+
--ink: #e5e7eb;
|
|
29
|
+
--muted: #9ca3af;
|
|
30
|
+
--paper: #0b1020;
|
|
31
|
+
--card: #111827;
|
|
32
|
+
--line: #1f2937;
|
|
33
|
+
--accent: #3b82f6;
|
|
34
|
+
--ok: #4ade80;
|
|
35
|
+
--ok-paper: #052e16;
|
|
36
|
+
--warn: #fbbf24;
|
|
37
|
+
--warn-paper: #2d1a02;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
body {
|
|
42
|
+
color: var(--ink);
|
|
43
|
+
background: var(--paper);
|
|
44
|
+
margin: 0;
|
|
45
|
+
padding: 16px;
|
|
46
|
+
font: 15px / 1.5 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main {
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 16px;
|
|
52
|
+
max-width: 34rem;
|
|
53
|
+
margin: 0 auto;
|
|
54
|
+
display: flex;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h1 {
|
|
58
|
+
margin: 8px 0 0;
|
|
59
|
+
font-size: 1.35rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
h2 {
|
|
63
|
+
margin: 0;
|
|
64
|
+
font-size: 1rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
p {
|
|
68
|
+
margin: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ol {
|
|
72
|
+
margin: 0;
|
|
73
|
+
padding-left: 1.3em;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
li + li {
|
|
77
|
+
margin-top: 6px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
code {
|
|
81
|
+
font: .85em ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.muted {
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.card {
|
|
89
|
+
background: var(--card);
|
|
90
|
+
border: 1px solid var(--line);
|
|
91
|
+
border-radius: 10px;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: 10px;
|
|
94
|
+
padding: 14px 16px;
|
|
95
|
+
display: flex;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.button {
|
|
99
|
+
color: var(--accent);
|
|
100
|
+
text-align: center;
|
|
101
|
+
border: 1px solid var(--accent);
|
|
102
|
+
border-radius: 8px;
|
|
103
|
+
padding: 12px 16px;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
text-decoration: none;
|
|
106
|
+
display: block;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.button[data-primary] {
|
|
110
|
+
color: var(--accent-ink);
|
|
111
|
+
background: var(--accent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#status {
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#status_card[data-state="trusted"] {
|
|
119
|
+
background: var(--ok-paper);
|
|
120
|
+
border-color: var(--ok);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#status_card[data-state="trusted"] #status {
|
|
124
|
+
color: var(--ok);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#status_card[data-state="untrusted"] {
|
|
128
|
+
background: var(--warn-paper);
|
|
129
|
+
border-color: var(--warn);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#status_card[data-state="untrusted"] #status {
|
|
133
|
+
color: var(--warn);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#fingerprint {
|
|
137
|
+
word-break: break-all;
|
|
138
|
+
display: block;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
details {
|
|
142
|
+
background: var(--card);
|
|
143
|
+
border: 1px solid var(--line);
|
|
144
|
+
border-radius: 10px;
|
|
145
|
+
padding: 14px 16px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
summary {
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
details[open] summary {
|
|
154
|
+
margin-bottom: 10px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
details .this_device {
|
|
158
|
+
color: var(--muted);
|
|
159
|
+
font-weight: 400;
|
|
160
|
+
display: none;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
details[data-current] .this_device {
|
|
164
|
+
display: inline;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
details p + ol, details ol + p, details p + p {
|
|
168
|
+
margin-top: 10px;
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<main>
|
|
174
|
+
<h1>Trust the dev server https</h1>
|
|
175
|
+
<p class="muted">
|
|
176
|
+
Install the root certificate of the computer running the dev server and
|
|
177
|
+
this device opens it over https without a warning. Service workers,
|
|
178
|
+
push, install and the other secure-context APIs need it.
|
|
179
|
+
</p>
|
|
180
|
+
|
|
181
|
+
<div id="status_card" class="card" data-state="checking">
|
|
182
|
+
<p id="status">Checking whether this device trusts the dev server…</p>
|
|
183
|
+
<a id="continue" class="button" href="/">
|
|
184
|
+
Continue to https
|
|
185
|
+
</a>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="card">
|
|
189
|
+
<a id="download" class="button" href="./root.crt" data-primary="">
|
|
190
|
+
Download the root certificate
|
|
191
|
+
</a>
|
|
192
|
+
<p>
|
|
193
|
+
<span class="muted">SHA-256 fingerprint</span>
|
|
194
|
+
<code id="fingerprint">…</code>
|
|
195
|
+
</p>
|
|
196
|
+
<p class="muted">
|
|
197
|
+
Once installed, the fingerprint the device shows must match the one
|
|
198
|
+
the dev server prints in its terminal: this page travels over plain
|
|
199
|
+
http, so the network could swap the file.
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div id="platforms">
|
|
204
|
+
<details data-platform="android" open="">
|
|
205
|
+
<summary>
|
|
206
|
+
Android <span class="this_device">(this device)</span>
|
|
207
|
+
</summary>
|
|
208
|
+
<ol>
|
|
209
|
+
<li>
|
|
210
|
+
Tap <b>Download the root certificate</b>. Opening the file only
|
|
211
|
+
says CA certificates must be installed from Settings: that is
|
|
212
|
+
expected.
|
|
213
|
+
</li>
|
|
214
|
+
<li>
|
|
215
|
+
In <b>Settings</b>, search for <b>CA certificate</b>. Or go to
|
|
216
|
+
Security → Encryption & credentials → Install a certificate →
|
|
217
|
+
CA certificate.
|
|
218
|
+
</li>
|
|
219
|
+
<li>
|
|
220
|
+
Tap <b>Install anyway</b> and pick the file in Downloads. Android
|
|
221
|
+
asks for a screen lock if there is none.
|
|
222
|
+
</li>
|
|
223
|
+
<li>Come back here, the check above turns green.</li>
|
|
224
|
+
</ol>
|
|
225
|
+
<p class="muted">
|
|
226
|
+
Android then shows “Network may be monitored”: expected, the device
|
|
227
|
+
trusts a certificate installed by hand. Chrome honors it, most apps
|
|
228
|
+
do not.
|
|
229
|
+
</p>
|
|
230
|
+
</details>
|
|
231
|
+
<details data-platform="ios" open="">
|
|
232
|
+
<summary>
|
|
233
|
+
iPhone, iPad <span class="this_device">(this device)</span>
|
|
234
|
+
</summary>
|
|
235
|
+
<ol>
|
|
236
|
+
<li>
|
|
237
|
+
Open this page in <b>Safari</b>, tap
|
|
238
|
+
<b>Download the root certificate</b>, then <b>Allow</b>.
|
|
239
|
+
</li>
|
|
240
|
+
<li>
|
|
241
|
+
In <b>Settings</b>, tap <b>Profile Downloaded</b> →
|
|
242
|
+
<b>Install</b>.
|
|
243
|
+
</li>
|
|
244
|
+
<li>
|
|
245
|
+
Then
|
|
246
|
+
<b>Settings → General → About → Certificate Trust Settings</b> and
|
|
247
|
+
turn on <b class="common_name">the root certificate</b>. Without
|
|
248
|
+
this step the certificate is installed but not trusted.
|
|
249
|
+
</li>
|
|
250
|
+
<li>Come back here, the check above turns green.</li>
|
|
251
|
+
</ol>
|
|
252
|
+
</details>
|
|
253
|
+
<details data-platform="desktop" open="">
|
|
254
|
+
<summary>
|
|
255
|
+
Computer <span class="this_device">(this device)</span>
|
|
256
|
+
</summary>
|
|
257
|
+
<p>
|
|
258
|
+
On the computer running the dev server,
|
|
259
|
+
<code>npx @jsenv/https-local init</code> installs and trusts the
|
|
260
|
+
root certificate.
|
|
261
|
+
</p>
|
|
262
|
+
<p>
|
|
263
|
+
On another computer, download it and add it to the trusted root
|
|
264
|
+
certificates of the system. On macOS: open the file, then set it to
|
|
265
|
+
“Always Trust” in Keychain Access.
|
|
266
|
+
</p>
|
|
267
|
+
</details>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<details open="">
|
|
271
|
+
<summary>Good to know</summary>
|
|
272
|
+
<p>
|
|
273
|
+
The device trusts anything signed by this root certificate. Its key
|
|
274
|
+
never leaves the computer running the dev server, only the certificate
|
|
275
|
+
is served: the trade-off already made on that computer.
|
|
276
|
+
</p>
|
|
277
|
+
<p>
|
|
278
|
+
<b>See the fingerprint, or remove it.</b> Android: Settings →
|
|
279
|
+
Encryption & credentials → Trusted credentials → User → tap it.
|
|
280
|
+
iPhone, iPad: Settings → General → VPN & Device Management → the
|
|
281
|
+
profile.
|
|
282
|
+
</p>
|
|
283
|
+
</details>
|
|
284
|
+
</main>
|
|
285
|
+
|
|
286
|
+
<script type="module">const readPlatform = () => {
|
|
287
|
+
const userAgent = window.navigator.userAgent;
|
|
288
|
+
if (/android/i.test(userAgent)) {
|
|
289
|
+
return "android";
|
|
290
|
+
}
|
|
291
|
+
if (/iPhone|iPad|iPod/.test(userAgent)) {
|
|
292
|
+
return "ios";
|
|
293
|
+
}
|
|
294
|
+
// iPadOS Safari introduces itself as a Mac
|
|
295
|
+
if (
|
|
296
|
+
/Macintosh/.test(userAgent) &&
|
|
297
|
+
window.navigator.maxTouchPoints > 1
|
|
298
|
+
) {
|
|
299
|
+
return "ios";
|
|
300
|
+
}
|
|
301
|
+
return "desktop";
|
|
302
|
+
};
|
|
303
|
+
const platform = readPlatform();
|
|
304
|
+
const platforms = document.querySelector("#platforms");
|
|
305
|
+
for (const details of platforms.querySelectorAll("[data-platform]")) {
|
|
306
|
+
if (details.getAttribute("data-platform") === platform) {
|
|
307
|
+
details.setAttribute("data-current", "");
|
|
308
|
+
platforms.prepend(details);
|
|
309
|
+
} else {
|
|
310
|
+
details.open = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// http and https share the port of the dev server
|
|
315
|
+
const port =
|
|
316
|
+
window.location.port ||
|
|
317
|
+
(window.location.protocol === "https:" ? "443" : "80");
|
|
318
|
+
const httpsOrigin = new URL(`https://${window.location.hostname}:${port}`)
|
|
319
|
+
.origin;
|
|
320
|
+
const continueLink = document.querySelector("#continue");
|
|
321
|
+
continueLink.href = `${httpsOrigin}/`;
|
|
322
|
+
|
|
323
|
+
const statusCard = document.querySelector("#status_card");
|
|
324
|
+
const status = document.querySelector("#status");
|
|
325
|
+
const download = document.querySelector("#download");
|
|
326
|
+
const checkTrust = async () => {
|
|
327
|
+
try {
|
|
328
|
+
// rejects as long as the certificate is not trusted, unless this
|
|
329
|
+
// browser clicked through the certificate warning for this host
|
|
330
|
+
await fetch(`${httpsOrigin}${window.location.pathname}ping`, {
|
|
331
|
+
mode: "no-cors",
|
|
332
|
+
cache: "no-store",
|
|
333
|
+
});
|
|
334
|
+
} catch {
|
|
335
|
+
statusCard.setAttribute("data-state", "untrusted");
|
|
336
|
+
status.textContent = "This device does not trust the dev server yet.";
|
|
337
|
+
continueLink.removeAttribute("data-primary");
|
|
338
|
+
download.setAttribute("data-primary", "");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
statusCard.setAttribute("data-state", "trusted");
|
|
342
|
+
status.textContent = "✓ This device trusts the dev server.";
|
|
343
|
+
continueLink.setAttribute("data-primary", "");
|
|
344
|
+
download.removeAttribute("data-primary");
|
|
345
|
+
};
|
|
346
|
+
checkTrust();
|
|
347
|
+
// back from the settings where the certificate was installed
|
|
348
|
+
document.addEventListener("visibilitychange", () => {
|
|
349
|
+
if (document.visibilityState === "visible") {
|
|
350
|
+
checkTrust();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const response = await fetch("./root.json");
|
|
355
|
+
const { fingerprint256, commonName } = await response.json();
|
|
356
|
+
document.querySelector("#fingerprint").textContent = fingerprint256;
|
|
357
|
+
for (const element of document.querySelectorAll(".common_name")) {
|
|
358
|
+
element.textContent = commonName;
|
|
359
|
+
}
|
|
360
|
+
</script>
|
|
361
|
+
</body>
|
|
362
|
+
</html>
|
|
@@ -808,4 +808,225 @@ const isCommandKey = (event) =>
|
|
|
808
808
|
const isEditorKey = (event) =>
|
|
809
809
|
(event.key === "e" || event.key === "E") && isCommandKey(event);
|
|
810
810
|
|
|
811
|
-
|
|
811
|
+
/*
|
|
812
|
+
* cmd+K / ctrl+K on any dev-served page: the .html files the server serves, as a
|
|
813
|
+
* tree one walks, filter as you type, Enter to go there — cmd/ctrl+Enter to go
|
|
814
|
+
* there in another tab and stay here, the same thing cmd+click on a row does.
|
|
815
|
+
*
|
|
816
|
+
* The tree itself is not this file's: it is the page picker the dev server ships
|
|
817
|
+
* for everyone who has to ask "which page?" (see protocol_file/client/page_picker.js
|
|
818
|
+
* — the clients dashboard asks it too, to send another browser somewhere). What
|
|
819
|
+
* belongs here is the key, and what taking a row means in this tab.
|
|
820
|
+
*
|
|
821
|
+
* cmd+E / ctrl+E is the other half of the same question: instead of going to a
|
|
822
|
+
* page, open its file in the editor (the server does it, see
|
|
823
|
+
* GET /.internal/open_file/*). Pressed on the page itself it opens the page one
|
|
824
|
+
* is on; pressed inside the switcher it opens the row one is looking at.
|
|
825
|
+
*
|
|
826
|
+
* Injected into every page (see jsenv_plugin_page_switcher.js), which is why it
|
|
827
|
+
* is careful about what it takes:
|
|
828
|
+
* - the key is watched at the end of the bubble, on window, and a press that
|
|
829
|
+
* was already handled (defaultPrevented) is left alone — a page with its own
|
|
830
|
+
* cmd+K keeps it, and gets it first;
|
|
831
|
+
* - nothing is fetched, built or styled until the first time it opens;
|
|
832
|
+
* - everything it renders lives in a shadow root, so the page's own CSS cannot
|
|
833
|
+
* reach it and it cannot reach the page's.
|
|
834
|
+
*/
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
// The server asks the OS to open a file in whatever editor is configured
|
|
838
|
+
// (VSCode here) — it takes a file url, which is why the page list carries one
|
|
839
|
+
// per page (see html_pages.js).
|
|
840
|
+
const OPEN_FILE_ENDPOINT = "/.internal/open_file/";
|
|
841
|
+
// Open across a reload: this is a dev tool on a page one is editing, and a hot
|
|
842
|
+
// reload in the middle of looking for the next page should not close what one
|
|
843
|
+
// was looking at. In sessionStorage, not localStorage: it says what THIS tab was
|
|
844
|
+
// doing a second ago, which is not something to remember tomorrow.
|
|
845
|
+
const OPEN_KEY = "jsenv_page_switcher_open";
|
|
846
|
+
|
|
847
|
+
// The page this script is running in, as the list spells its urls.
|
|
848
|
+
const currentPageUrl = () => asPageUrl(window.location.href);
|
|
849
|
+
|
|
850
|
+
const isSwitcherKey = (event) =>
|
|
851
|
+
(event.key === "k" || event.key === "K") && isCommandKey(event);
|
|
852
|
+
|
|
853
|
+
const FLASH_STYLE_TEXT = /* css */ `
|
|
854
|
+
:host {
|
|
855
|
+
position: fixed;
|
|
856
|
+
right: 16px;
|
|
857
|
+
bottom: 16px;
|
|
858
|
+
/* Above the picker's own panel: it is the switcher that triggers it. */
|
|
859
|
+
z-index: 2147483647;
|
|
860
|
+
display: block;
|
|
861
|
+
font-family: system-ui, sans-serif;
|
|
862
|
+
pointer-events: none;
|
|
863
|
+
}
|
|
864
|
+
.flash {
|
|
865
|
+
padding: 8px 14px;
|
|
866
|
+
color: light-dark(#0f172a, #e2e8f0);
|
|
867
|
+
font-size: 13px;
|
|
868
|
+
background: light-dark(white, #1e293b);
|
|
869
|
+
border-radius: 8px;
|
|
870
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
871
|
+
color-scheme: light dark;
|
|
872
|
+
}
|
|
873
|
+
.flash[data-error] {
|
|
874
|
+
color: light-dark(#991b1b, #fecaca);
|
|
875
|
+
background: light-dark(#fee2e2, #7f1d1d);
|
|
876
|
+
}
|
|
877
|
+
`;
|
|
878
|
+
|
|
879
|
+
// Opening a file in an editor happens in another application, on another
|
|
880
|
+
// screen sometimes: without a word here, a press that failed and a press that
|
|
881
|
+
// worked look exactly the same. Its own host and its own shadow root, so it
|
|
882
|
+
// can be shown whether or not the switcher is open.
|
|
883
|
+
let flashHost = null;
|
|
884
|
+
let flashBox = null;
|
|
885
|
+
let flashTimeout = null;
|
|
886
|
+
const flash = (message, isError) => {
|
|
887
|
+
if (!flashHost) {
|
|
888
|
+
flashHost = document.createElement("div");
|
|
889
|
+
const shadow = flashHost.attachShadow({ mode: "open" });
|
|
890
|
+
const style = document.createElement("style");
|
|
891
|
+
style.textContent = FLASH_STYLE_TEXT;
|
|
892
|
+
flashBox = document.createElement("div");
|
|
893
|
+
flashBox.className = "flash";
|
|
894
|
+
shadow.append(style, flashBox);
|
|
895
|
+
}
|
|
896
|
+
flashBox.textContent = message;
|
|
897
|
+
flashBox.toggleAttribute("data-error", Boolean(isError));
|
|
898
|
+
// Appended last every time, so it sits above the picker's host when both
|
|
899
|
+
// are on the page and they share the same z-index.
|
|
900
|
+
document.body.append(flashHost);
|
|
901
|
+
window.clearTimeout(flashTimeout);
|
|
902
|
+
flashTimeout = window.setTimeout(() => flashHost.remove(), 2500);
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const openInEditor = async (file) => {
|
|
906
|
+
if (!file || !file.fileUrl) {
|
|
907
|
+
flash("This page is not a file the server lists.", true);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
flash(`Opening ${file.name || file.url} in editor…`);
|
|
911
|
+
try {
|
|
912
|
+
const response = await fetch(
|
|
913
|
+
`${OPEN_FILE_ENDPOINT}${encodeURIComponent(file.fileUrl)}`,
|
|
914
|
+
);
|
|
915
|
+
if (response.status === 404) {
|
|
916
|
+
// The route exists only when the server is willing to expose the
|
|
917
|
+
// machine it runs on (see start_server.js).
|
|
918
|
+
flash("This server does not open files in an editor.", true);
|
|
919
|
+
} else if (!response.ok) {
|
|
920
|
+
flash(`Editor said no (${response.status}).`, true);
|
|
921
|
+
}
|
|
922
|
+
} catch {
|
|
923
|
+
flash("Could not reach the dev server.", true);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const rememberOpen = (isOpen) => {
|
|
928
|
+
try {
|
|
929
|
+
if (isOpen) {
|
|
930
|
+
window.sessionStorage.setItem(OPEN_KEY, "1");
|
|
931
|
+
} else {
|
|
932
|
+
window.sessionStorage.removeItem(OPEN_KEY);
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
// private mode, quota — it just will not come back
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
const wasOpen = () => {
|
|
939
|
+
try {
|
|
940
|
+
return window.sessionStorage.getItem(OPEN_KEY) === "1";
|
|
941
|
+
} catch {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
let picker = null;
|
|
947
|
+
|
|
948
|
+
// The same key opens it and puts it away: pressed while it is up, cmd+K is the
|
|
949
|
+
// reader saying "not this, back to what I was doing".
|
|
950
|
+
const toggleSwitcher = () => {
|
|
951
|
+
if (picker) {
|
|
952
|
+
picker.close();
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
openSwitcher();
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const openSwitcher = () => {
|
|
959
|
+
picker = openPagePicker({
|
|
960
|
+
// The other key is written where one is already looking: a shortcut nobody
|
|
961
|
+
// is told about is a shortcut nobody presses. Both names, not the one this
|
|
962
|
+
// platform uses — the reader knows which of the two their keyboard has, and
|
|
963
|
+
// it keeps what the panel says the same everywhere.
|
|
964
|
+
placeholder: "Go to page… (cmd/ctrl+Enter: new tab, cmd/ctrl+E: editor)",
|
|
965
|
+
hereUrl: window.location.href,
|
|
966
|
+
// Going there is something this browser can do, so a row is a link: its
|
|
967
|
+
// menu, its middle click and its cmd+click all work.
|
|
968
|
+
getHref: (page) => page.url,
|
|
969
|
+
onPick: (page) => {
|
|
970
|
+
// Going somewhere is done with it: what reopens across a reload is a
|
|
971
|
+
// switcher one was still using.
|
|
972
|
+
rememberOpen(false);
|
|
973
|
+
window.location.href = page.url;
|
|
974
|
+
},
|
|
975
|
+
onEdit: (page) => {
|
|
976
|
+
// Done with the switcher: the answer to "where does this live" arrives
|
|
977
|
+
// in the editor, not here.
|
|
978
|
+
rememberOpen(false);
|
|
979
|
+
openInEditor(page);
|
|
980
|
+
},
|
|
981
|
+
onClose: () => {
|
|
982
|
+
picker = null;
|
|
983
|
+
rememberOpen(false);
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
rememberOpen(true);
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// The page one is looking at, in the editor. The list is where the file url
|
|
990
|
+
// comes from, so a page the server does not list (an @fs url, something under
|
|
991
|
+
// node_modules) says so rather than opening the wrong thing.
|
|
992
|
+
const openCurrentPageInEditor = async () => {
|
|
993
|
+
const here = currentPageUrl();
|
|
994
|
+
const pages = await loadPages();
|
|
995
|
+
const page = pages.find((candidate) => candidate.url === here);
|
|
996
|
+
openInEditor(page && { ...page, name: here.split("/").pop() });
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// Last in line, on purpose: on window (the end of the bubble), and registered
|
|
1000
|
+
// once the page has loaded — so every listener the page set up while parsing is
|
|
1001
|
+
// already in place and has already had this press. If any of them called
|
|
1002
|
+
// preventDefault, the key was theirs and nothing happens here.
|
|
1003
|
+
const listenSwitcherKey = () => {
|
|
1004
|
+
window.addEventListener("keydown", (event) => {
|
|
1005
|
+
if (event.defaultPrevented) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (isSwitcherKey(event)) {
|
|
1009
|
+
// Ours now: the browser has its own use for cmd+K (the address bar),
|
|
1010
|
+
// which it must not get.
|
|
1011
|
+
event.preventDefault();
|
|
1012
|
+
toggleSwitcher();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (isEditorKey(event)) {
|
|
1016
|
+
event.preventDefault();
|
|
1017
|
+
openCurrentPageInEditor();
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
const setup = () => {
|
|
1023
|
+
listenSwitcherKey();
|
|
1024
|
+
if (wasOpen()) {
|
|
1025
|
+
openSwitcher();
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
if (document.readyState === "complete") {
|
|
1029
|
+
setup();
|
|
1030
|
+
} else {
|
|
1031
|
+
window.addEventListener("load", setup, { once: true });
|
|
1032
|
+
}
|