@jsenv/core 41.4.3 → 41.4.5
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 +167 -16
- package/dist/client/html_syntax_error/html_syntax_error.html +1 -1
- package/dist/js/page_switcher.js +136 -13
- package/dist/jsenv_core.js +11 -1
- package/dist/start_dev_server/start_dev_server.js +136 -17
- package/package.json +6 -6
- package/src/build/build.js +30 -0
- package/src/build/build_specifier_manager.js +5 -0
- package/src/dev/start_dev_server.js +1 -0
- package/src/kitchen/errors.js +28 -0
- package/src/kitchen/url_graph/url_info_injections.js +34 -8
- package/src/plugins/injections/jsenv_plugin_injections.js +64 -8
- package/src/plugins/page_switcher/client/page_switcher.js +136 -13
- package/src/plugins/page_switcher/jsenv_plugin_page_switcher.js +3 -1
- package/src/plugins/protocol_file/html_pages.js +7 -1
- package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +4 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* cmd+K / ctrl+K on any dev-served page: the .html files the server serves, as a
|
|
3
3
|
* tree one walks, filter as you type, Enter to go there.
|
|
4
4
|
*
|
|
5
|
+
* cmd+E / ctrl+E is the other half of the same question: instead of going to a
|
|
6
|
+
* page, open its file in the editor (the server does it, see
|
|
7
|
+
* GET /.internal/open_file/*). Pressed on the page itself it opens the page one
|
|
8
|
+
* is on; pressed inside the switcher it opens the row one is looking at.
|
|
9
|
+
*
|
|
5
10
|
* A tree rather than a list of paths, because the paths are mostly the same
|
|
6
11
|
* path: folding every directory that holds a single thing turns
|
|
7
12
|
* "packages/frontend/navi/src/layout/demos/…" repeated forty times into one
|
|
@@ -25,6 +30,10 @@
|
|
|
25
30
|
// another injected client. A function scope owes nothing to anybody.
|
|
26
31
|
(() => {
|
|
27
32
|
const PAGES_ENDPOINT = "/.internal/pages.json";
|
|
33
|
+
// The server asks the OS to open a file in whatever editor is configured
|
|
34
|
+
// (VSCode here) — it takes a file url, which is why the page list carries one
|
|
35
|
+
// per page (see html_pages.js).
|
|
36
|
+
const OPEN_FILE_ENDPOINT = "/.internal/open_file/";
|
|
28
37
|
const STORAGE_KEY = "jsenv_page_switcher";
|
|
29
38
|
// Open across a reload: this is a dev tool on a page one is editing, and a hot
|
|
30
39
|
// reload in the middle of looking for the next page should not close what one
|
|
@@ -56,15 +65,20 @@
|
|
|
56
65
|
}
|
|
57
66
|
};
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
// cmd on mac, ctrl elsewhere — the same split every editor makes.
|
|
64
|
-
return window.navigator.platform.toLowerCase().includes("mac")
|
|
68
|
+
// cmd on mac, ctrl elsewhere — the same split every editor makes.
|
|
69
|
+
const isCommandKey = (event) =>
|
|
70
|
+
window.navigator.platform.toLowerCase().includes("mac")
|
|
65
71
|
? event.metaKey && !event.ctrlKey
|
|
66
72
|
: event.ctrlKey && !event.metaKey;
|
|
67
|
-
|
|
73
|
+
|
|
74
|
+
const isSwitcherKey = (event) =>
|
|
75
|
+
(event.key === "k" || event.key === "K") && isCommandKey(event);
|
|
76
|
+
// E for edit, next to K for the same reason the two belong together: K asks
|
|
77
|
+
// "which page", E asks "where does this page live". Outside the switcher it
|
|
78
|
+
// means the page one is on; inside it, the row one is looking at — so the
|
|
79
|
+
// same press reads the same way in both places.
|
|
80
|
+
const isEditorKey = (event) =>
|
|
81
|
+
(event.key === "e" || event.key === "E") && isCommandKey(event);
|
|
68
82
|
|
|
69
83
|
const STYLE_TEXT = /* css */ `
|
|
70
84
|
:host {
|
|
@@ -230,6 +244,32 @@
|
|
|
230
244
|
}
|
|
231
245
|
`;
|
|
232
246
|
|
|
247
|
+
const FLASH_STYLE_TEXT = /* css */ `
|
|
248
|
+
:host {
|
|
249
|
+
position: fixed;
|
|
250
|
+
right: 16px;
|
|
251
|
+
bottom: 16px;
|
|
252
|
+
/* Above the switcher's own panel: it is the switcher that triggers it. */
|
|
253
|
+
z-index: 2147483647;
|
|
254
|
+
display: block;
|
|
255
|
+
font-family: system-ui, sans-serif;
|
|
256
|
+
pointer-events: none;
|
|
257
|
+
}
|
|
258
|
+
.flash {
|
|
259
|
+
padding: 8px 14px;
|
|
260
|
+
color: light-dark(#0f172a, #e2e8f0);
|
|
261
|
+
font-size: 13px;
|
|
262
|
+
background: light-dark(white, #1e293b);
|
|
263
|
+
border-radius: 8px;
|
|
264
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
265
|
+
color-scheme: light dark;
|
|
266
|
+
}
|
|
267
|
+
.flash[data-error] {
|
|
268
|
+
color: light-dark(#991b1b, #fecaca);
|
|
269
|
+
background: light-dark(#fee2e2, #7f1d1d);
|
|
270
|
+
}
|
|
271
|
+
`;
|
|
272
|
+
|
|
233
273
|
let pagesPromise = null;
|
|
234
274
|
const loadPages = () => {
|
|
235
275
|
// Once per page load: the list is a filesystem scan behind a short cache
|
|
@@ -240,6 +280,54 @@
|
|
|
240
280
|
return pagesPromise;
|
|
241
281
|
};
|
|
242
282
|
|
|
283
|
+
// Opening a file in an editor happens in another application, on another
|
|
284
|
+
// screen sometimes: without a word here, a press that failed and a press that
|
|
285
|
+
// worked look exactly the same. Its own host and its own shadow root, so it
|
|
286
|
+
// can be shown whether or not the switcher is open.
|
|
287
|
+
let flashHost = null;
|
|
288
|
+
let flashBox = null;
|
|
289
|
+
let flashTimeout = null;
|
|
290
|
+
const flash = (message, isError) => {
|
|
291
|
+
if (!flashHost) {
|
|
292
|
+
flashHost = document.createElement("div");
|
|
293
|
+
const shadow = flashHost.attachShadow({ mode: "open" });
|
|
294
|
+
const style = document.createElement("style");
|
|
295
|
+
style.textContent = FLASH_STYLE_TEXT;
|
|
296
|
+
flashBox = document.createElement("div");
|
|
297
|
+
flashBox.className = "flash";
|
|
298
|
+
shadow.append(style, flashBox);
|
|
299
|
+
}
|
|
300
|
+
flashBox.textContent = message;
|
|
301
|
+
flashBox.toggleAttribute("data-error", Boolean(isError));
|
|
302
|
+
// Appended last every time, so it sits above the switcher's host when both
|
|
303
|
+
// are on the page and they share the same z-index.
|
|
304
|
+
document.body.append(flashHost);
|
|
305
|
+
window.clearTimeout(flashTimeout);
|
|
306
|
+
flashTimeout = window.setTimeout(() => flashHost.remove(), 2500);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const openInEditor = async (file) => {
|
|
310
|
+
if (!file || !file.fileUrl) {
|
|
311
|
+
flash("This page is not a file the server lists.", true);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
flash(`Opening ${file.name || file.url} in editor…`);
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(
|
|
317
|
+
`${OPEN_FILE_ENDPOINT}${encodeURIComponent(file.fileUrl)}`,
|
|
318
|
+
);
|
|
319
|
+
if (response.status === 404) {
|
|
320
|
+
// The route exists only when the server is willing to expose the
|
|
321
|
+
// machine it runs on (see start_server.js).
|
|
322
|
+
flash("This server does not open files in an editor.", true);
|
|
323
|
+
} else if (!response.ok) {
|
|
324
|
+
flash(`Editor said no (${response.status}).`, true);
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
flash("Could not reach the dev server.", true);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
243
331
|
const readStoredState = () => {
|
|
244
332
|
try {
|
|
245
333
|
const stored = JSON.parse(
|
|
@@ -396,7 +484,11 @@
|
|
|
396
484
|
panel.className = "panel";
|
|
397
485
|
const input = document.createElement("input");
|
|
398
486
|
input.type = "search";
|
|
399
|
-
|
|
487
|
+
// The other key is written where one is already looking: a shortcut nobody
|
|
488
|
+
// is told about is a shortcut nobody presses. Both names, not the one this
|
|
489
|
+
// platform uses — the reader knows which of the two their keyboard has, and
|
|
490
|
+
// it keeps what the panel says the same everywhere.
|
|
491
|
+
input.placeholder = "Go to page… (cmd/ctrl+E to open in editor)";
|
|
400
492
|
input.setAttribute("aria-label", "Go to page");
|
|
401
493
|
const kindsRow = document.createElement("div");
|
|
402
494
|
kindsRow.className = "kinds";
|
|
@@ -664,6 +756,21 @@
|
|
|
664
756
|
toggleCollapsed(row.node.path);
|
|
665
757
|
return;
|
|
666
758
|
}
|
|
759
|
+
if (isEditorKey(event)) {
|
|
760
|
+
// Taken whatever the row is: let go of on a directory it would reach
|
|
761
|
+
// the page below and open the page one came from, which is not what a
|
|
762
|
+
// key pressed inside an open switcher can be asking for.
|
|
763
|
+
stop();
|
|
764
|
+
const row = rows[currentIndex];
|
|
765
|
+
if (!row || row.type !== "file") {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
// Done with the switcher: the answer to "where does this live" arrives
|
|
769
|
+
// in the editor, not here.
|
|
770
|
+
close();
|
|
771
|
+
openInEditor(row.file);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
667
774
|
if (isSwitcherKey(event)) {
|
|
668
775
|
stop();
|
|
669
776
|
close();
|
|
@@ -708,15 +815,31 @@
|
|
|
708
815
|
// preventDefault, the key was theirs and nothing happens here.
|
|
709
816
|
const listenSwitcherKey = () => {
|
|
710
817
|
window.addEventListener("keydown", (event) => {
|
|
711
|
-
if (event.defaultPrevented
|
|
818
|
+
if (event.defaultPrevented) {
|
|
712
819
|
return;
|
|
713
820
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
821
|
+
if (isSwitcherKey(event)) {
|
|
822
|
+
// Ours now: the browser has its own use for cmd+K (the address bar),
|
|
823
|
+
// which it must not get.
|
|
824
|
+
event.preventDefault();
|
|
825
|
+
openSwitcher();
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (isEditorKey(event)) {
|
|
829
|
+
event.preventDefault();
|
|
830
|
+
openCurrentPageInEditor();
|
|
831
|
+
}
|
|
718
832
|
});
|
|
719
833
|
};
|
|
834
|
+
// The page one is looking at, in the editor. The list is where the file url
|
|
835
|
+
// comes from, so a page the server does not list (an @fs url, something under
|
|
836
|
+
// node_modules) says so rather than opening the wrong thing.
|
|
837
|
+
const openCurrentPageInEditor = async () => {
|
|
838
|
+
const here = currentPageUrl();
|
|
839
|
+
const pages = await loadPages();
|
|
840
|
+
const page = pages.find((candidate) => candidate.url === here);
|
|
841
|
+
openInEditor(page && { ...page, name: here.split("/").pop() });
|
|
842
|
+
};
|
|
720
843
|
const setup = () => {
|
|
721
844
|
listenSwitcherKey();
|
|
722
845
|
if (wasOpen()) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* cmd+K (ctrl+K elsewhere) on any page the dev server serves opens a list of
|
|
3
|
-
* the .html files it serves, filter as you type, Enter to go there.
|
|
3
|
+
* the .html files it serves, filter as you type, Enter to go there. cmd+E
|
|
4
|
+
* (ctrl+E elsewhere) opens a page's file in the editor instead of going to it:
|
|
5
|
+
* the current page from anywhere, the selected row from inside the switcher.
|
|
4
6
|
*
|
|
5
7
|
* The list is the one the filesystem plugin already publishes for everyone
|
|
6
8
|
* (GET /.internal/pages.json, see protocol_file/html_pages.js) — this only adds
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* of them, the page switcher (cmd+K) opens one in the current tab. Whoever asks
|
|
7
7
|
* gets the same answer.
|
|
8
8
|
*
|
|
9
|
-
* Each page comes with
|
|
9
|
+
* Each page comes with where its file is (so it can be opened in an editor as
|
|
10
|
+
* well as in the browser) and with what kind of page it is, read from where it
|
|
11
|
+
* sits and
|
|
10
12
|
* what it is called — the two conventions this repo already follows:
|
|
11
13
|
* - "experiment": something tried out, under a lab/ directory or named
|
|
12
14
|
* *_experiment.html;
|
|
@@ -107,6 +109,10 @@ export const createHtmlPageLister = ({ rootDirectoryUrl }) => {
|
|
|
107
109
|
);
|
|
108
110
|
return {
|
|
109
111
|
url: `/${relativeUrl}`,
|
|
112
|
+
// Where the file actually is, so whoever wants to open it in an editor
|
|
113
|
+
// rather than in the browser has what GET /.internal/open_file/* asks
|
|
114
|
+
// for (a file url) without having to know the root directory.
|
|
115
|
+
fileUrl,
|
|
110
116
|
kind: readKind(meta),
|
|
111
117
|
// Relative to the root and without its trailing slash, which is how a
|
|
112
118
|
// tree names its own nodes.
|
|
@@ -98,6 +98,10 @@ export const jsenvPluginFsRedirection = ({
|
|
|
98
98
|
}
|
|
99
99
|
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
100
100
|
reference.ownerUrlInfo.context;
|
|
101
|
+
if (!requestedUrl) {
|
|
102
|
+
// the SPA fallback answers a request; during build there is none
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
101
105
|
const closestHtmlRootFile = getClosestHtmlRootFile(
|
|
102
106
|
requestedUrl,
|
|
103
107
|
rootDirectoryUrl,
|