@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,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Lets another device (a phone) trust the root certificate that signs the dev
|
|
3
|
+
* server certificate, so it opens the dev server over https without a warning.
|
|
4
|
+
*
|
|
5
|
+
* Everything under HTTPS_TRUST_PATHNAME is served over plain http too (see the
|
|
6
|
+
* redirectHttpToHttps passed by startDevServer): it is the one page that must
|
|
7
|
+
* load before the certificate is trusted, over https it would show the very
|
|
8
|
+
* warning it exists to remove. Only the certificate is served, never its key.
|
|
9
|
+
*
|
|
10
|
+
* The page is sent as written, not cooked like the project pages: it needs
|
|
11
|
+
* nothing cooking injects, and a project plugin rewriting html must not be able
|
|
12
|
+
* to break the one page reached before trust.
|
|
13
|
+
*
|
|
14
|
+
* What it means for the device, and why the fingerprint must be compared with
|
|
15
|
+
* the terminal: docs/users/b_dev/b_dev.md, "Trusting it on a phone".
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
export const HTTPS_TRUST_PATHNAME = "/.internal/https/";
|
|
21
|
+
|
|
22
|
+
const httpsTrustPageFileUrl = new URL(
|
|
23
|
+
"./client/https_trust.html",
|
|
24
|
+
import.meta.url,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// rootCertificate is a node:crypto X509Certificate
|
|
28
|
+
export const devServerPluginHttpsTrust = ({ rootCertificate }) => {
|
|
29
|
+
const commonName = readCommonName(rootCertificate.subject);
|
|
30
|
+
const rootCertificateInfo = {
|
|
31
|
+
fingerprint256: rootCertificate.fingerprint256,
|
|
32
|
+
commonName,
|
|
33
|
+
};
|
|
34
|
+
// "https local root certificate" -> "https_local_root_certificate.crt"
|
|
35
|
+
const rootCertificateFilename = `${commonName.replace(/[^\w.-]+/g, "_")}.crt`;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: "jsenv:https_trust",
|
|
39
|
+
routes: [
|
|
40
|
+
{
|
|
41
|
+
endpoint: `GET ${HTTPS_TRUST_PATHNAME}`,
|
|
42
|
+
description:
|
|
43
|
+
"Page to trust the dev server https on another device (a phone). Served over http.",
|
|
44
|
+
declarationSource: import.meta.url,
|
|
45
|
+
fetch: () => {
|
|
46
|
+
return new Response(readFileSync(httpsTrustPageFileUrl), {
|
|
47
|
+
headers: { "content-type": "text/html" },
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
endpoint: `GET ${HTTPS_TRUST_PATHNAME}root.crt`,
|
|
53
|
+
description: "The root certificate signing the dev server certificate.",
|
|
54
|
+
declarationSource: import.meta.url,
|
|
55
|
+
fetch: () => {
|
|
56
|
+
return new Response(rootCertificate.toString(), {
|
|
57
|
+
headers: {
|
|
58
|
+
"content-type": "application/x-x509-ca-cert",
|
|
59
|
+
"content-disposition": `attachment; filename="${rootCertificateFilename}"`,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
endpoint: `GET ${HTTPS_TRUST_PATHNAME}root.json`,
|
|
66
|
+
description: "Fingerprint and name of the root certificate.",
|
|
67
|
+
declarationSource: import.meta.url,
|
|
68
|
+
fetch: () => {
|
|
69
|
+
return Response.json(rootCertificateInfo);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
endpoint: `GET ${HTTPS_TRUST_PATHNAME}ping`,
|
|
74
|
+
description:
|
|
75
|
+
"Fetched over https by the page: it fails as long as the device does not trust the certificate.",
|
|
76
|
+
declarationSource: import.meta.url,
|
|
77
|
+
fetch: () => {
|
|
78
|
+
return new Response(null, { status: 204 });
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// "CN=https local root certificate\nO=..." -> "https local root certificate"
|
|
86
|
+
const readCommonName = (subject) => {
|
|
87
|
+
for (const line of subject.split("\n")) {
|
|
88
|
+
if (line.startsWith("CN=")) {
|
|
89
|
+
return line.slice("CN=".length);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return subject;
|
|
93
|
+
};
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
browserDefaultRuntimeCompat,
|
|
18
18
|
inferRuntimeCompatFromClosestPackage,
|
|
19
19
|
} from "@jsenv/runtime-compat";
|
|
20
|
+
import { X509Certificate } from "node:crypto";
|
|
20
21
|
import { existsSync } from "node:fs";
|
|
21
22
|
import { pathToFileURL } from "node:url";
|
|
22
23
|
|
|
@@ -31,6 +32,10 @@ import { jsenvPluginPatches } from "../plugins/patches/jsenv_plugin_patches.js";
|
|
|
31
32
|
import { getCorePlugins } from "../plugins/plugins.js";
|
|
32
33
|
import { jsenvPluginServerEvents } from "../plugins/server_events/jsenv_plugin_server_events.js";
|
|
33
34
|
import { devServerPluginChromeDevToolsJson } from "./dev_server_plugins/dev_server_plugin_chrome_devtools_json.js";
|
|
35
|
+
import {
|
|
36
|
+
HTTPS_TRUST_PATHNAME,
|
|
37
|
+
devServerPluginHttpsTrust,
|
|
38
|
+
} from "./dev_server_plugins/dev_server_plugin_https_trust.js";
|
|
34
39
|
import { devServerPluginInjectServerResponseHeader } from "./dev_server_plugins/dev_server_plugin_inject_server_response_header.js";
|
|
35
40
|
import { devServerPluginOmegaErrorHandler } from "./dev_server_plugins/dev_server_plugin_omega_error_handler.js";
|
|
36
41
|
import { devServerPluginServeSourceFiles } from "./dev_server_plugins/dev_server_plugin_serve_source_files.js";
|
|
@@ -51,7 +56,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
51
56
|
* @param {number} [params.port=3456] - Port to listen on (0 = a free port).
|
|
52
57
|
* @param {string} [params.hostname] - Hostname to bind to.
|
|
53
58
|
* @param {boolean} [params.acceptAnyIp=false] - Also accept connections on the machine's IPs (so other devices on the network — a phone — can reach the server). Off by default: exposing the dev server beyond localhost is an explicit choice, not something a dev tool decides.
|
|
54
|
-
* @param {
|
|
59
|
+
* @param {object} [params.https] - HTTPS as `{ certificate, privateKey }` (PEM strings). With `rootCertificate` (the PEM of the authority that signed `certificate`) and `acceptAnyIp`, the dev server also serves `http://<ip>:<port>/.internal/https/`, a page to trust that authority on a phone (see docs/users/b_dev/b_dev.md, "Trusting it on a phone").
|
|
55
60
|
* @param {boolean} [params.http2=false] - HTTP/2 (requires https).
|
|
56
61
|
* @param {Array} [params.plugins=[]] - jsenv plugins (transformUrlContent, serverRoutes, serverEvents, effect, …).
|
|
57
62
|
* @param {Array} [params.serverPlugins=[]] - `@jsenv/server`-level plugins.
|
|
@@ -184,6 +189,7 @@ export const startDevServer = async ({
|
|
|
184
189
|
);
|
|
185
190
|
}
|
|
186
191
|
}
|
|
192
|
+
let httpsTrust = null;
|
|
187
193
|
// params normalization
|
|
188
194
|
{
|
|
189
195
|
if (runtimeCompat === undefined) {
|
|
@@ -198,6 +204,16 @@ export const startDevServer = async ({
|
|
|
198
204
|
if (clientAutoreload === false) {
|
|
199
205
|
clientAutoreload = { enabled: false };
|
|
200
206
|
}
|
|
207
|
+
if (https) {
|
|
208
|
+
const { certificate, privateKey, rootCertificate } = https;
|
|
209
|
+
https = { certificate, privateKey };
|
|
210
|
+
// a localhost-only server has no phone to serve
|
|
211
|
+
if (rootCertificate && acceptAnyIp) {
|
|
212
|
+
httpsTrust = {
|
|
213
|
+
rootCertificate: new X509Certificate(rootCertificate),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
const logger = createLogger({ logLevel });
|
|
@@ -355,6 +371,7 @@ export const startDevServer = async ({
|
|
|
355
371
|
}),
|
|
356
372
|
// chrome devtools
|
|
357
373
|
devServerPluginChromeDevToolsJson({ sourceDirectoryUrl }),
|
|
374
|
+
...(httpsTrust ? [devServerPluginHttpsTrust(httpsTrust)] : []),
|
|
358
375
|
...serverPlugins,
|
|
359
376
|
devServerPluginServeSourceFiles({
|
|
360
377
|
packageDirectory,
|
|
@@ -393,6 +410,12 @@ export const startDevServer = async ({
|
|
|
393
410
|
startLog: false,
|
|
394
411
|
|
|
395
412
|
https,
|
|
413
|
+
...(httpsTrust
|
|
414
|
+
? {
|
|
415
|
+
redirectHttpToHttps: ({ resource }) =>
|
|
416
|
+
!resource.startsWith(HTTPS_TRUST_PATHNAME),
|
|
417
|
+
}
|
|
418
|
+
: {}),
|
|
396
419
|
http2,
|
|
397
420
|
acceptAnyIp,
|
|
398
421
|
hostname,
|
|
@@ -418,6 +441,17 @@ export const startDevServer = async ({
|
|
|
418
441
|
logger.info(``);
|
|
419
442
|
Object.keys(server.origins).forEach((key) => {
|
|
420
443
|
logger.info(`- ${server.origins[key]}`);
|
|
444
|
+
if (httpsTrust && key === "externalip") {
|
|
445
|
+
const externalHostname = new URL(server.origins.externalip).hostname;
|
|
446
|
+
const httpsTrustPageUrl = new URL(
|
|
447
|
+
HTTPS_TRUST_PATHNAME,
|
|
448
|
+
`http://${externalHostname}:${server.port}`,
|
|
449
|
+
);
|
|
450
|
+
logger.info(` trust it on a phone: ${httpsTrustPageUrl}`);
|
|
451
|
+
logger.info(
|
|
452
|
+
` root certificate SHA-256: ${httpsTrust.rootCertificate.fingerprint256}`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
421
455
|
});
|
|
422
456
|
logger.info(``);
|
|
423
457
|
return {
|
package/src/kitchen/errors.js
CHANGED
|
@@ -140,7 +140,7 @@ ${reason}`,
|
|
|
140
140
|
return createFailedToFetchUrlContentError({
|
|
141
141
|
code: "NOT_FOUND",
|
|
142
142
|
reason: "no entry on filesystem",
|
|
143
|
-
...
|
|
143
|
+
...detailsFromHtmlUrlNotFound(urlInfo.firstReference),
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -400,6 +400,31 @@ const detailsFromInjectionsOnOwner = (reference) => {
|
|
|
400
400
|
};
|
|
401
401
|
};
|
|
402
402
|
|
|
403
|
+
// An url of an html attribute leading to no file may be meant for a server rather
|
|
404
|
+
// than for jsenv: a route of a backend, which "jsenv-ignore" leaves alone. Only the
|
|
405
|
+
// build is concerned: in dev the browser requests the url whatever jsenv does with it.
|
|
406
|
+
const detailsFromHtmlUrlNotFound = (reference) => {
|
|
407
|
+
const detailsFromInjections = detailsFromInjectionsOnOwner(reference);
|
|
408
|
+
if (detailsFromInjections.suggestion) {
|
|
409
|
+
return detailsFromInjections;
|
|
410
|
+
}
|
|
411
|
+
if (!reference) {
|
|
412
|
+
return {};
|
|
413
|
+
}
|
|
414
|
+
const ownerUrlInfo = reference.ownerUrlInfo;
|
|
415
|
+
if (ownerUrlInfo.type !== "html" || !ownerUrlInfo.context.build) {
|
|
416
|
+
return {};
|
|
417
|
+
}
|
|
418
|
+
const { node, attributeName } = reference.astInfo || {};
|
|
419
|
+
if (!node || !attributeName) {
|
|
420
|
+
return {};
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
suggestion: `when "${reference.specifier}" is answered by a server rather than a file, add "jsenv-ignore" so the build leaves that url alone:
|
|
424
|
+
<${node.nodeName} jsenv-ignore ${attributeName}="${reference.specifier}" />`,
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
|
|
403
428
|
const detailsFromPluginController = (jsenvPluginsController) => {
|
|
404
429
|
const currentPlugin = jsenvPluginsController.getCurrentPlugin();
|
|
405
430
|
if (!currentPlugin) {
|