@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.
@@ -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
- const isSwitcherKey = (event) => {
60
- if (event.key !== "k" && event.key !== "K") {
61
- return false;
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
- input.placeholder = "Go to page…";
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 || !isSwitcherKey(event)) {
818
+ if (event.defaultPrevented) {
712
819
  return;
713
820
  }
714
- // Ours now: the browser has its own use for cmd+K (the address bar), which
715
- // it must not get.
716
- event.preventDefault();
717
- openSwitcher();
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 what kind of page it is, read from where it sits and
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,