@jsenv/core 41.4.0 → 41.4.2
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 +173 -4
- package/dist/build/jsenv_core_packages.js +91 -1
- package/dist/html/client_monitor_page.html +4 -1
- package/dist/html/clients_page.html +13 -5
- package/dist/js/client_reporter.js +185 -109
- package/dist/js/page_switcher.js +731 -0
- package/dist/start_dev_server/jsenv_core_packages.js +695 -4
- package/dist/start_dev_server/start_dev_server.js +357 -78
- package/package.json +3 -3
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +55 -5
- package/src/dev/start_dev_server.js +13 -3
- package/src/kitchen/kitchen.js +12 -3
- package/src/kitchen/url_graph/url_graph.js +11 -0
- package/src/plugins/client_monitoring/client/client_monitor_page.html +4 -1
- package/src/plugins/client_monitoring/client/client_reporter.js +185 -109
- package/src/plugins/client_monitoring/client/clients_page.html +13 -5
- package/src/plugins/client_monitoring/jsenv_plugin_client_monitoring.js +76 -68
- package/src/plugins/page_switcher/client/page_switcher.js +731 -0
- package/src/plugins/page_switcher/jsenv_plugin_page_switcher.js +42 -0
- package/src/plugins/protocol_file/html_pages.js +122 -0
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +24 -0
- package/src/plugins/resolution_node_esm/node_esm_resolver.js +5 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
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.
|
|
4
|
+
*
|
|
5
|
+
* The list is the one the filesystem plugin already publishes for everyone
|
|
6
|
+
* (GET /.internal/pages.json, see protocol_file/html_pages.js) — this only adds
|
|
7
|
+
* the way to reach it without leaving the page one is working on.
|
|
8
|
+
*
|
|
9
|
+
* Registered with the dev server rather than among the core plugins, next to
|
|
10
|
+
* the other things that inject a client script: the script it adds is a
|
|
11
|
+
* reference like any other, and it has to be added while references are still
|
|
12
|
+
* being resolved — added later it stays a file:// url the browser refuses.
|
|
13
|
+
*
|
|
14
|
+
* A shortcut on every page is a shortcut taken from every page, so the page
|
|
15
|
+
* comes first: the key is watched on the document, in the bubble phase, and
|
|
16
|
+
* anything that called preventDefault on its way there keeps it. A page with
|
|
17
|
+
* its own cmd+K owes nothing to this one.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { injectJsenvScript, parseHtml, stringifyHtmlAst } from "@jsenv/ast";
|
|
21
|
+
|
|
22
|
+
const clientFileUrl = new URL("./client/page_switcher.js", import.meta.url)
|
|
23
|
+
.href;
|
|
24
|
+
|
|
25
|
+
export const jsenvPluginPageSwitcher = () => {
|
|
26
|
+
return {
|
|
27
|
+
name: "jsenv:page_switcher",
|
|
28
|
+
// Dev only: it is a way around the source tree, which a built app has no
|
|
29
|
+
// business carrying.
|
|
30
|
+
appliesDuring: "dev",
|
|
31
|
+
transformUrlContent: {
|
|
32
|
+
html: (urlInfo) => {
|
|
33
|
+
const htmlAst = parseHtml({ html: urlInfo.content, url: urlInfo.url });
|
|
34
|
+
injectJsenvScript(htmlAst, {
|
|
35
|
+
src: clientFileUrl,
|
|
36
|
+
pluginName: "jsenv:page_switcher",
|
|
37
|
+
});
|
|
38
|
+
return stringifyHtmlAst(htmlAst);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The .html files under the served directory, as urls one can navigate to.
|
|
3
|
+
*
|
|
4
|
+
* Lives here, next to the plugin that owns the filesystem, because more than
|
|
5
|
+
* one feature wants the same list: the client dashboard sends a browser to one
|
|
6
|
+
* of them, the page switcher (cmd+K) opens one in the current tab. Whoever asks
|
|
7
|
+
* gets the same answer.
|
|
8
|
+
*
|
|
9
|
+
* Each page comes with what kind of page it is, read from where it sits and
|
|
10
|
+
* what it is called — the two conventions this repo already follows:
|
|
11
|
+
* - "experiment": something tried out, under a lab/ directory or named
|
|
12
|
+
* *_experiment.html;
|
|
13
|
+
* - "demo": something shown, under a demos/ directory or named *_demo.html;
|
|
14
|
+
* - "page": everything else.
|
|
15
|
+
*
|
|
16
|
+
* Scanned on demand rather than watched: the list is asked for when a human
|
|
17
|
+
* opens a picker, which is rare and never on a hot path, and a short cache is
|
|
18
|
+
* enough to keep a burst of asks from walking the tree twice.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { collectFiles } from "@jsenv/filesystem";
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
23
|
+
|
|
24
|
+
const SCAN_TTL_MS = 5000;
|
|
25
|
+
|
|
26
|
+
// What to walk and what to skip, in the shape @jsenv/url-meta reads: the last
|
|
27
|
+
// matching pattern wins, so the exclusions come after the "every .html" rule.
|
|
28
|
+
// Dependencies, build output and jsenv's own caches hold no page worth going to.
|
|
29
|
+
const HTML_PAGE_ASSOCIATIONS = {
|
|
30
|
+
page: {
|
|
31
|
+
"**/*.html": true,
|
|
32
|
+
"**/.*/": false,
|
|
33
|
+
"**/node_modules/": false,
|
|
34
|
+
"**/dist/": false,
|
|
35
|
+
"**/build/": false,
|
|
36
|
+
"**/coverage/": false,
|
|
37
|
+
"**/git_ignored/": false,
|
|
38
|
+
"**/old/": false,
|
|
39
|
+
},
|
|
40
|
+
experiment: {
|
|
41
|
+
"**/lab/**/*.html": true,
|
|
42
|
+
"**/*_experiment.html": true,
|
|
43
|
+
},
|
|
44
|
+
demo: {
|
|
45
|
+
"**/demos/**/*.html": true,
|
|
46
|
+
"**/*_demo.html": true,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// An experiment inside a demos/ directory is an experiment: the more specific
|
|
51
|
+
// of the two wins, and "shown" is the weaker claim.
|
|
52
|
+
const readKind = (meta) => {
|
|
53
|
+
if (meta.experiment) {
|
|
54
|
+
return "experiment";
|
|
55
|
+
}
|
|
56
|
+
if (meta.demo) {
|
|
57
|
+
return "demo";
|
|
58
|
+
}
|
|
59
|
+
return "page";
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Which package a page belongs to: the nearest directory above it holding a
|
|
63
|
+
// package.json, said as a url so whoever draws a tree can mark that very node.
|
|
64
|
+
// Not the root itself — everything is under it, and "the whole repo" is not a
|
|
65
|
+
// package one distinguishes from another. Memoized per directory: a scan asks
|
|
66
|
+
// the same question once per file and there are hundreds of them.
|
|
67
|
+
const createPackageDirectoryFinder = (rootDirectoryUrl) => {
|
|
68
|
+
const cache = new Map();
|
|
69
|
+
const find = (directoryUrl) => {
|
|
70
|
+
if (cache.has(directoryUrl)) {
|
|
71
|
+
return cache.get(directoryUrl);
|
|
72
|
+
}
|
|
73
|
+
let result = null;
|
|
74
|
+
if (directoryUrl.length > String(rootDirectoryUrl).length) {
|
|
75
|
+
result = existsSync(new URL("./package.json", directoryUrl))
|
|
76
|
+
? directoryUrl
|
|
77
|
+
: find(new URL("../", directoryUrl).href);
|
|
78
|
+
}
|
|
79
|
+
cache.set(directoryUrl, result);
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
return find;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const createHtmlPageLister = ({ rootDirectoryUrl }) => {
|
|
86
|
+
let cache = null;
|
|
87
|
+
let cachedAt = 0;
|
|
88
|
+
|
|
89
|
+
return async () => {
|
|
90
|
+
if (!rootDirectoryUrl) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
if (cache && now - cachedAt < SCAN_TTL_MS) {
|
|
95
|
+
return cache;
|
|
96
|
+
}
|
|
97
|
+
const fileResultArray = await collectFiles({
|
|
98
|
+
directoryUrl: rootDirectoryUrl,
|
|
99
|
+
associations: HTML_PAGE_ASSOCIATIONS,
|
|
100
|
+
predicate: (meta) => Boolean(meta.page),
|
|
101
|
+
});
|
|
102
|
+
const findPackageDirectory = createPackageDirectoryFinder(rootDirectoryUrl);
|
|
103
|
+
const pages = fileResultArray.map(({ relativeUrl, meta }) => {
|
|
104
|
+
const fileUrl = new URL(relativeUrl, rootDirectoryUrl).href;
|
|
105
|
+
const packageDirectoryUrl = findPackageDirectory(
|
|
106
|
+
new URL("./", fileUrl).href,
|
|
107
|
+
);
|
|
108
|
+
return {
|
|
109
|
+
url: `/${relativeUrl}`,
|
|
110
|
+
kind: readKind(meta),
|
|
111
|
+
// Relative to the root and without its trailing slash, which is how a
|
|
112
|
+
// tree names its own nodes.
|
|
113
|
+
packageUrl: packageDirectoryUrl
|
|
114
|
+
? `/${packageDirectoryUrl.slice(String(rootDirectoryUrl).length).replace(/\/$/, "")}`
|
|
115
|
+
: null,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
cache = pages;
|
|
119
|
+
cachedAt = now;
|
|
120
|
+
return pages;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
@@ -3,6 +3,7 @@ import { ensurePathnameTrailingSlash } from "@jsenv/urls";
|
|
|
3
3
|
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
4
4
|
import { readFileSync, readdirSync } from "node:fs";
|
|
5
5
|
import { FILE_AND_SERVER_URLS_CONVERTER } from "../../kitchen/file_and_server_urls_converter.js";
|
|
6
|
+
import { createHtmlPageLister } from "./html_pages.js";
|
|
6
7
|
import { jsenvPluginDirectoryListing } from "./jsenv_plugin_directory_listing.js";
|
|
7
8
|
import { jsenvPluginFsRedirection } from "./jsenv_plugin_fs_redirection.js";
|
|
8
9
|
|
|
@@ -19,6 +20,8 @@ export const jsenvPluginProtocolFile = ({
|
|
|
19
20
|
packageDirectory,
|
|
20
21
|
sourceFilesConfig,
|
|
21
22
|
}) => {
|
|
23
|
+
const listHtmlPages = createHtmlPageLister({ rootDirectoryUrl });
|
|
24
|
+
|
|
22
25
|
return [
|
|
23
26
|
jsenvPluginFsRedirection({
|
|
24
27
|
spa,
|
|
@@ -74,6 +77,27 @@ export const jsenvPluginProtocolFile = ({
|
|
|
74
77
|
);
|
|
75
78
|
},
|
|
76
79
|
},
|
|
80
|
+
{
|
|
81
|
+
name: "jsenv:html_pages",
|
|
82
|
+
appliesDuring: "dev",
|
|
83
|
+
serverRoutes: [
|
|
84
|
+
{
|
|
85
|
+
endpoint: "GET /.internal/pages.json",
|
|
86
|
+
description:
|
|
87
|
+
"The .html files served under the source directory, as urls to navigate to.",
|
|
88
|
+
availableMediaTypes: ["application/json"],
|
|
89
|
+
declarationSource: import.meta.url,
|
|
90
|
+
fetch: async () => ({
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: {
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
"cache-control": "no-store",
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify(await listHtmlPages()),
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
77
101
|
...(directoryListing
|
|
78
102
|
? [
|
|
79
103
|
jsenvPluginDirectoryListing({
|
|
@@ -471,6 +471,11 @@ const addRelationshipWithPackageJson = ({
|
|
|
471
471
|
String(packageJsonContentAsBuffer),
|
|
472
472
|
);
|
|
473
473
|
}
|
|
474
|
+
// Checked on disk at every validation rather than trusted to the watcher:
|
|
475
|
+
// what this file decides (the package version, hence the ?v= importers
|
|
476
|
+
// embed) ends up in the browser's immutable cache, so a request racing the
|
|
477
|
+
// watcher must never be answered from a stale package.json.
|
|
478
|
+
packageJsonReference.urlInfo.revalidateOnFileSystem = true;
|
|
474
479
|
};
|
|
475
480
|
|
|
476
481
|
const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|