@jsenv/core 40.0.7 → 40.0.9
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 +10765 -0
- package/dist/jsenv_core.js +25 -13
- package/dist/jsenv_core_packages.js +16728 -7022
- package/dist/{js → start_build_server}/start_build_server.js +7 -8
- package/dist/{plugins.js → start_dev_server/start_dev_server.js} +964 -25
- package/package.json +11 -11
- package/src/build/build.js +15 -14
- package/src/build/build_specifier_manager.js +4 -0
- package/src/build/build_urls_generator.js +4 -0
- package/src/build/jsenv_plugin_mappings.js +69 -0
- package/src/build/jsenv_plugin_subbuilds.js +4 -0
- package/src/kitchen/kitchen.js +12 -0
- package/src/kitchen/out_directory_url.js +1 -0
- package/src/plugins/protocol_http/jsenv_plugin_protocol_http.js +6 -0
- package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +6 -1
- package/dist/js/build.js +0 -2636
- package/dist/js/start_dev_server.js +0 -840
- package/dist/main.js +0 -117
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl,
|
|
1
|
+
import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl, moveUrl, urlIsInsideOf, ensureWindowsDriveLetter, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, setUrlFilename, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, CONTENT_TYPE, DATA_URL, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, defaultLookupPackageScope, defaultReadPackageJson, readEntryStatSync, urlToFilename, ensurePathnameTrailingSlash, comparePathnames, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, jsenvPluginTranspilation, memoizeByFirstArgument, assertAndNormalizeDirectoryUrl, createTaskLog } from "../jsenv_core_packages.js";
|
|
2
|
+
import { WebSocketResponse, pickContentType, ServerEvents, jsenvServiceCORS, jsenvAccessControlAllowedHeaders, composeTwoResponses, serveDirectory, jsenvServiceErrorHandler, startServer } from "@jsenv/server";
|
|
3
|
+
import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js";
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
2
5
|
import { RUNTIME_COMPAT } from "@jsenv/runtime-compat";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
3
7
|
import { generateSourcemapFileUrl, createMagicSource, composeTwoSourcemaps, generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
|
|
4
8
|
import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, parseJsWithAcorn, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode, injectJsenvScript } from "@jsenv/ast";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
9
|
import { performance } from "node:perf_hooks";
|
|
7
|
-
import { readFileSync, existsSync, readdirSync, lstatSync, realpathSync } from "node:fs";
|
|
8
10
|
import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import "string-width";
|
|
13
|
+
import "node:process";
|
|
14
|
+
import "node:os";
|
|
15
|
+
import "node:tty";
|
|
16
|
+
import "node:path";
|
|
17
|
+
import "node:crypto";
|
|
18
|
+
import "@jsenv/js-module-fallback";
|
|
11
19
|
|
|
12
20
|
// default runtimeCompat corresponds to
|
|
13
21
|
// "we can keep <script type="module"> intact":
|
|
@@ -22,13 +30,6 @@ const defaultRuntimeCompat = {
|
|
|
22
30
|
safari: "11.3",
|
|
23
31
|
samsung: "9.2",
|
|
24
32
|
};
|
|
25
|
-
const logsDefault = {
|
|
26
|
-
level: "info",
|
|
27
|
-
disabled: false,
|
|
28
|
-
animation: true,
|
|
29
|
-
};
|
|
30
|
-
const getDefaultBase = (runtimeCompat) =>
|
|
31
|
-
runtimeCompat.node ? "./" : "/";
|
|
32
33
|
|
|
33
34
|
const createEventEmitter = () => {
|
|
34
35
|
const callbackSet = new Set();
|
|
@@ -162,7 +163,33 @@ const watchSourceFiles = (
|
|
|
162
163
|
return watch();
|
|
163
164
|
};
|
|
164
165
|
|
|
165
|
-
const
|
|
166
|
+
const WEB_URL_CONVERTER = {
|
|
167
|
+
asWebUrl: (fileUrl, webServer) => {
|
|
168
|
+
if (urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
|
|
169
|
+
return moveUrl({
|
|
170
|
+
url: fileUrl,
|
|
171
|
+
from: webServer.rootDirectoryUrl,
|
|
172
|
+
to: `${webServer.origin}/`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const fsRootUrl = ensureWindowsDriveLetter("file:///", fileUrl);
|
|
176
|
+
return `${webServer.origin}/@fs/${fileUrl.slice(fsRootUrl.length)}`;
|
|
177
|
+
},
|
|
178
|
+
asFileUrl: (webUrl, webServer) => {
|
|
179
|
+
const { pathname, search } = new URL(webUrl);
|
|
180
|
+
if (pathname.startsWith("/@fs/")) {
|
|
181
|
+
const fsRootRelativeUrl = pathname.slice("/@fs/".length);
|
|
182
|
+
return `file:///${fsRootRelativeUrl}${search}`;
|
|
183
|
+
}
|
|
184
|
+
return moveUrl({
|
|
185
|
+
url: webUrl,
|
|
186
|
+
from: `${webServer.origin}/`,
|
|
187
|
+
to: webServer.rootDirectoryUrl,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const jsenvCoreDirectoryUrl = new URL("../", import.meta.url);
|
|
166
193
|
|
|
167
194
|
const createResolveUrlError = ({
|
|
168
195
|
pluginController,
|
|
@@ -568,6 +595,7 @@ const determineSourcemapFileUrl = (urlInfo) => {
|
|
|
568
595
|
generatedUrlObject.searchParams.delete("as_json_module");
|
|
569
596
|
generatedUrlObject.searchParams.delete("as_text_module");
|
|
570
597
|
generatedUrlObject.searchParams.delete("dynamic_import");
|
|
598
|
+
generatedUrlObject.searchParams.delete("dynamic_import_id");
|
|
571
599
|
generatedUrlObject.searchParams.delete("cjs_as_js_module");
|
|
572
600
|
const urlForSourcemap = generatedUrlObject.href;
|
|
573
601
|
return generateSourcemapFileUrl(urlForSourcemap);
|
|
@@ -2637,7 +2665,7 @@ const shouldHandleSourcemap = (urlInfo) => {
|
|
|
2637
2665
|
};
|
|
2638
2666
|
|
|
2639
2667
|
const inlineContentClientFileUrl = new URL(
|
|
2640
|
-
"
|
|
2668
|
+
"../client/inline_content/inline_content.js",
|
|
2641
2669
|
import.meta.url,
|
|
2642
2670
|
).href;
|
|
2643
2671
|
|
|
@@ -2668,6 +2696,9 @@ const createKitchen = ({
|
|
|
2668
2696
|
}) => {
|
|
2669
2697
|
const logger = createLogger({ logLevel });
|
|
2670
2698
|
|
|
2699
|
+
const nodeRuntimeEnabled = Object.keys(runtimeCompat).includes("node");
|
|
2700
|
+
const packageConditions = [nodeRuntimeEnabled ? "node" : "browser", "import"];
|
|
2701
|
+
|
|
2671
2702
|
const kitchen = {
|
|
2672
2703
|
context: {
|
|
2673
2704
|
...initialContext,
|
|
@@ -2687,6 +2718,14 @@ const createKitchen = ({
|
|
|
2687
2718
|
sourcemaps,
|
|
2688
2719
|
outDirectoryUrl,
|
|
2689
2720
|
},
|
|
2721
|
+
resolve: (specifier, importer) => {
|
|
2722
|
+
const { url, packageDirectoryUrl, packageJson } = applyNodeEsmResolution({
|
|
2723
|
+
conditions: packageConditions,
|
|
2724
|
+
parentUrl: importer,
|
|
2725
|
+
specifier,
|
|
2726
|
+
});
|
|
2727
|
+
return { url, packageDirectoryUrl, packageJson };
|
|
2728
|
+
},
|
|
2690
2729
|
graph: null,
|
|
2691
2730
|
urlInfoTransformer: null,
|
|
2692
2731
|
pluginController: null,
|
|
@@ -3339,7 +3378,7 @@ const inferUrlInfoType = (urlInfo) => {
|
|
|
3339
3378
|
|
|
3340
3379
|
const jsenvPluginHtmlSyntaxErrorFallback = () => {
|
|
3341
3380
|
const htmlSyntaxErrorFileUrl = import.meta.resolve(
|
|
3342
|
-
"
|
|
3381
|
+
"../client/html_syntax_error/html_syntax_error.html",
|
|
3343
3382
|
);
|
|
3344
3383
|
|
|
3345
3384
|
return {
|
|
@@ -3406,7 +3445,7 @@ const generateHtmlForSyntaxError = (
|
|
|
3406
3445
|
errorLinkText: `${htmlRelativeUrl}:${line}:${column}`,
|
|
3407
3446
|
syntaxError: escapeHtml(htmlErrorContentFrame),
|
|
3408
3447
|
};
|
|
3409
|
-
const html = replacePlaceholders(htmlForSyntaxError, replacers);
|
|
3448
|
+
const html = replacePlaceholders$1(htmlForSyntaxError, replacers);
|
|
3410
3449
|
return html;
|
|
3411
3450
|
};
|
|
3412
3451
|
const escapeHtml = (string) => {
|
|
@@ -3417,7 +3456,7 @@ const escapeHtml = (string) => {
|
|
|
3417
3456
|
.replace(/"/g, """)
|
|
3418
3457
|
.replace(/'/g, "'");
|
|
3419
3458
|
};
|
|
3420
|
-
const replacePlaceholders = (html, replacers) => {
|
|
3459
|
+
const replacePlaceholders$1 = (html, replacers) => {
|
|
3421
3460
|
return html.replace(/\$\{(\w+)\}/g, (match, name) => {
|
|
3422
3461
|
const replacer = replacers[name];
|
|
3423
3462
|
if (replacer === undefined) {
|
|
@@ -4330,7 +4369,12 @@ const jsenvPluginHtmlReferenceAnalysis = ({
|
|
|
4330
4369
|
});
|
|
4331
4370
|
} else {
|
|
4332
4371
|
setHtmlNodeText(node, inlineUrlInfo.content, {
|
|
4333
|
-
indentation:
|
|
4372
|
+
indentation:
|
|
4373
|
+
inlineUrlInfo.type === "js_classic" ||
|
|
4374
|
+
inlineUrlInfo.type === "js_module"
|
|
4375
|
+
? // indentation would mess with stack trace and sourcemap
|
|
4376
|
+
false
|
|
4377
|
+
: "auto",
|
|
4334
4378
|
});
|
|
4335
4379
|
setHtmlNodeAttributes(node, {
|
|
4336
4380
|
"jsenv-cooked-by": "jsenv:html_inline_content_analysis",
|
|
@@ -5425,6 +5469,103 @@ const FILE_AND_SERVER_URLS_CONVERTER = {
|
|
|
5425
5469
|
},
|
|
5426
5470
|
};
|
|
5427
5471
|
|
|
5472
|
+
const jsenvPluginInjections = (rawAssociations) => {
|
|
5473
|
+
let resolvedAssociations;
|
|
5474
|
+
|
|
5475
|
+
return {
|
|
5476
|
+
name: "jsenv:injections",
|
|
5477
|
+
appliesDuring: "*",
|
|
5478
|
+
init: (context) => {
|
|
5479
|
+
resolvedAssociations = URL_META.resolveAssociations(
|
|
5480
|
+
{ injectionsGetter: rawAssociations },
|
|
5481
|
+
context.rootDirectoryUrl,
|
|
5482
|
+
);
|
|
5483
|
+
},
|
|
5484
|
+
transformUrlContent: async (urlInfo) => {
|
|
5485
|
+
const { injectionsGetter } = URL_META.applyAssociations({
|
|
5486
|
+
url: asUrlWithoutSearch(urlInfo.url),
|
|
5487
|
+
associations: resolvedAssociations,
|
|
5488
|
+
});
|
|
5489
|
+
if (!injectionsGetter) {
|
|
5490
|
+
return null;
|
|
5491
|
+
}
|
|
5492
|
+
if (typeof injectionsGetter !== "function") {
|
|
5493
|
+
throw new TypeError("injectionsGetter must be a function");
|
|
5494
|
+
}
|
|
5495
|
+
const injections = await injectionsGetter(urlInfo);
|
|
5496
|
+
if (!injections) {
|
|
5497
|
+
return null;
|
|
5498
|
+
}
|
|
5499
|
+
const keys = Object.keys(injections);
|
|
5500
|
+
if (keys.length === 0) {
|
|
5501
|
+
return null;
|
|
5502
|
+
}
|
|
5503
|
+
return replacePlaceholders(urlInfo.content, injections, urlInfo);
|
|
5504
|
+
},
|
|
5505
|
+
};
|
|
5506
|
+
};
|
|
5507
|
+
|
|
5508
|
+
const injectionSymbol = Symbol.for("jsenv_injection");
|
|
5509
|
+
const INJECTIONS = {
|
|
5510
|
+
optional: (value) => {
|
|
5511
|
+
return { [injectionSymbol]: "optional", value };
|
|
5512
|
+
},
|
|
5513
|
+
};
|
|
5514
|
+
|
|
5515
|
+
// we export this because it is imported by jsenv_plugin_placeholder.js and unit test
|
|
5516
|
+
const replacePlaceholders = (content, replacements, urlInfo) => {
|
|
5517
|
+
const magicSource = createMagicSource(content);
|
|
5518
|
+
for (const key of Object.keys(replacements)) {
|
|
5519
|
+
let index = content.indexOf(key);
|
|
5520
|
+
const replacement = replacements[key];
|
|
5521
|
+
let isOptional;
|
|
5522
|
+
let value;
|
|
5523
|
+
if (replacement && replacement[injectionSymbol]) {
|
|
5524
|
+
const valueBehindSymbol = replacement[injectionSymbol];
|
|
5525
|
+
isOptional = valueBehindSymbol === "optional";
|
|
5526
|
+
value = replacement.value;
|
|
5527
|
+
} else {
|
|
5528
|
+
value = replacement;
|
|
5529
|
+
}
|
|
5530
|
+
if (index === -1) {
|
|
5531
|
+
if (!isOptional) {
|
|
5532
|
+
urlInfo.context.logger.warn(
|
|
5533
|
+
`placeholder "${key}" not found in ${urlInfo.url}.
|
|
5534
|
+
--- suggestion a ---
|
|
5535
|
+
Add "${key}" in that file.
|
|
5536
|
+
--- suggestion b ---
|
|
5537
|
+
Fix eventual typo in "${key}"?
|
|
5538
|
+
--- suggestion c ---
|
|
5539
|
+
Mark injection as optional using INJECTIONS.optional():
|
|
5540
|
+
import { INJECTIONS } from "@jsenv/core";
|
|
5541
|
+
|
|
5542
|
+
return {
|
|
5543
|
+
"${key}": INJECTIONS.optional(${JSON.stringify(value)}),
|
|
5544
|
+
};`,
|
|
5545
|
+
);
|
|
5546
|
+
}
|
|
5547
|
+
continue;
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
while (index !== -1) {
|
|
5551
|
+
const start = index;
|
|
5552
|
+
const end = index + key.length;
|
|
5553
|
+
magicSource.replace({
|
|
5554
|
+
start,
|
|
5555
|
+
end,
|
|
5556
|
+
replacement:
|
|
5557
|
+
urlInfo.type === "js_classic" ||
|
|
5558
|
+
urlInfo.type === "js_module" ||
|
|
5559
|
+
urlInfo.type === "html"
|
|
5560
|
+
? JSON.stringify(value, null, " ")
|
|
5561
|
+
: value,
|
|
5562
|
+
});
|
|
5563
|
+
index = content.indexOf(key, end);
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
return magicSource.toContentAndSourcemap();
|
|
5567
|
+
};
|
|
5568
|
+
|
|
5428
5569
|
/*
|
|
5429
5570
|
* NICE TO HAVE:
|
|
5430
5571
|
*
|
|
@@ -5451,7 +5592,7 @@ const FILE_AND_SERVER_URLS_CONVERTER = {
|
|
|
5451
5592
|
|
|
5452
5593
|
|
|
5453
5594
|
const htmlFileUrlForDirectory = import.meta.resolve(
|
|
5454
|
-
"
|
|
5595
|
+
"../client/directory_listing/directory_listing.html",
|
|
5455
5596
|
);
|
|
5456
5597
|
|
|
5457
5598
|
const jsenvPluginDirectoryListing = ({
|
|
@@ -5533,7 +5674,7 @@ const jsenvPluginDirectoryListing = ({
|
|
|
5533
5674
|
}
|
|
5534
5675
|
const request = urlInfo.context.request;
|
|
5535
5676
|
const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
|
|
5536
|
-
return replacePlaceholders
|
|
5677
|
+
return replacePlaceholders(
|
|
5537
5678
|
urlInfo.content,
|
|
5538
5679
|
{
|
|
5539
5680
|
...generateDirectoryListingInjection(requestedUrl, {
|
|
@@ -6143,6 +6284,12 @@ const jsenvPluginProtocolHttp = ({ include }) => {
|
|
|
6143
6284
|
// }
|
|
6144
6285
|
// return null;
|
|
6145
6286
|
// },
|
|
6287
|
+
init: (context) => {
|
|
6288
|
+
const outDirectoryUrl = context.outDirectoryUrl;
|
|
6289
|
+
if (!outDirectoryUrl) {
|
|
6290
|
+
throw new Error(`need outDirectoryUrl to write http files`);
|
|
6291
|
+
}
|
|
6292
|
+
},
|
|
6146
6293
|
redirectReference: (reference) => {
|
|
6147
6294
|
if (!reference.url.startsWith("http")) {
|
|
6148
6295
|
return null;
|
|
@@ -6847,7 +6994,7 @@ const babelPluginMetadataImportMetaScenarios = () => {
|
|
|
6847
6994
|
|
|
6848
6995
|
const jsenvPluginGlobalScenarios = () => {
|
|
6849
6996
|
const transformIfNeeded = (urlInfo) => {
|
|
6850
|
-
return replacePlaceholders
|
|
6997
|
+
return replacePlaceholders(
|
|
6851
6998
|
urlInfo.content,
|
|
6852
6999
|
{
|
|
6853
7000
|
__DEV__: INJECTIONS.optional(urlInfo.context.dev),
|
|
@@ -7128,7 +7275,7 @@ const htmlNodeCanHotReload = (node) => {
|
|
|
7128
7275
|
|
|
7129
7276
|
const jsenvPluginImportMetaHot = () => {
|
|
7130
7277
|
const importMetaHotClientFileUrl = import.meta.resolve(
|
|
7131
|
-
"
|
|
7278
|
+
"../client/import_meta_hot/import_meta_hot.js",
|
|
7132
7279
|
);
|
|
7133
7280
|
|
|
7134
7281
|
return {
|
|
@@ -7240,7 +7387,7 @@ import.meta.hot = createImportMetaHot(import.meta.url);
|
|
|
7240
7387
|
};
|
|
7241
7388
|
|
|
7242
7389
|
const jsenvPluginAutoreloadClient = () => {
|
|
7243
|
-
const autoreloadClientFileUrl = import.meta.resolve("
|
|
7390
|
+
const autoreloadClientFileUrl = import.meta.resolve("../client/autoreload/autoreload.js");
|
|
7244
7391
|
|
|
7245
7392
|
return {
|
|
7246
7393
|
name: "jsenv:autoreload_client",
|
|
@@ -7759,7 +7906,7 @@ const jsenvPluginRibbon = ({
|
|
|
7759
7906
|
rootDirectoryUrl,
|
|
7760
7907
|
htmlInclude = "/**/*.html",
|
|
7761
7908
|
}) => {
|
|
7762
|
-
const ribbonClientFileUrl = import.meta.resolve("
|
|
7909
|
+
const ribbonClientFileUrl = import.meta.resolve("../client/ribbon/ribbon.js");
|
|
7763
7910
|
const associations = URL_META.resolveAssociations(
|
|
7764
7911
|
{
|
|
7765
7912
|
ribbon: {
|
|
@@ -7941,4 +8088,796 @@ const getCorePlugins = ({
|
|
|
7941
8088
|
];
|
|
7942
8089
|
};
|
|
7943
8090
|
|
|
7944
|
-
|
|
8091
|
+
/*
|
|
8092
|
+
* This plugin is very special because it is here
|
|
8093
|
+
* to provide "serverEvents" used by other plugins
|
|
8094
|
+
*/
|
|
8095
|
+
|
|
8096
|
+
|
|
8097
|
+
const serverEventsClientFileUrl = new URL(
|
|
8098
|
+
"../client/server_events_client/server_events_client.js",
|
|
8099
|
+
import.meta.url,
|
|
8100
|
+
).href;
|
|
8101
|
+
|
|
8102
|
+
const jsenvPluginServerEvents = ({ clientAutoreload }) => {
|
|
8103
|
+
let serverEvents = new ServerEvents({
|
|
8104
|
+
actionOnClientLimitReached: "kick-oldest",
|
|
8105
|
+
});
|
|
8106
|
+
const { clientServerEventsConfig } = clientAutoreload;
|
|
8107
|
+
const { logs = true } = clientServerEventsConfig;
|
|
8108
|
+
|
|
8109
|
+
return {
|
|
8110
|
+
name: "jsenv:server_events",
|
|
8111
|
+
appliesDuring: "dev",
|
|
8112
|
+
effect: ({ kitchenContext, otherPlugins }) => {
|
|
8113
|
+
const allServerEvents = {};
|
|
8114
|
+
for (const otherPlugin of otherPlugins) {
|
|
8115
|
+
const { serverEvents } = otherPlugin;
|
|
8116
|
+
if (!serverEvents) {
|
|
8117
|
+
continue;
|
|
8118
|
+
}
|
|
8119
|
+
for (const serverEventName of Object.keys(serverEvents)) {
|
|
8120
|
+
// we could throw on serverEvent name conflict
|
|
8121
|
+
// we could throw if serverEvents[serverEventName] is not a function
|
|
8122
|
+
allServerEvents[serverEventName] = serverEvents[serverEventName];
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
const serverEventNames = Object.keys(allServerEvents);
|
|
8126
|
+
if (serverEventNames.length === 0) {
|
|
8127
|
+
return false;
|
|
8128
|
+
}
|
|
8129
|
+
|
|
8130
|
+
const onabort = () => {
|
|
8131
|
+
serverEvents.close();
|
|
8132
|
+
};
|
|
8133
|
+
kitchenContext.signal.addEventListener("abort", onabort);
|
|
8134
|
+
for (const serverEventName of Object.keys(allServerEvents)) {
|
|
8135
|
+
const serverEventInfo = {
|
|
8136
|
+
...kitchenContext,
|
|
8137
|
+
// serverEventsDispatcher variable is safe, we can disable esling warning
|
|
8138
|
+
// eslint-disable-next-line no-loop-func
|
|
8139
|
+
sendServerEvent: (data) => {
|
|
8140
|
+
if (!serverEvents) {
|
|
8141
|
+
// this can happen if a plugin wants to send a server event but
|
|
8142
|
+
// server is closing or the plugin got destroyed but still wants to do things
|
|
8143
|
+
// if plugin code is correctly written it is never supposed to happen
|
|
8144
|
+
// because it means a plugin is still trying to do stuff after being destroyed
|
|
8145
|
+
return;
|
|
8146
|
+
}
|
|
8147
|
+
serverEvents.sendEventToAllClients({
|
|
8148
|
+
type: serverEventName,
|
|
8149
|
+
data,
|
|
8150
|
+
});
|
|
8151
|
+
},
|
|
8152
|
+
};
|
|
8153
|
+
const serverEventInit = allServerEvents[serverEventName];
|
|
8154
|
+
serverEventInit(serverEventInfo);
|
|
8155
|
+
}
|
|
8156
|
+
return () => {
|
|
8157
|
+
kitchenContext.signal.removeEventListener("abort", onabort);
|
|
8158
|
+
serverEvents.close();
|
|
8159
|
+
serverEvents = undefined;
|
|
8160
|
+
};
|
|
8161
|
+
},
|
|
8162
|
+
transformUrlContent: {
|
|
8163
|
+
html: (urlInfo) => {
|
|
8164
|
+
const htmlAst = parseHtml({
|
|
8165
|
+
html: urlInfo.content,
|
|
8166
|
+
url: urlInfo.url,
|
|
8167
|
+
});
|
|
8168
|
+
injectJsenvScript(htmlAst, {
|
|
8169
|
+
src: serverEventsClientFileUrl,
|
|
8170
|
+
initCall: {
|
|
8171
|
+
callee: "window.__server_events__.setup",
|
|
8172
|
+
params: {
|
|
8173
|
+
logs,
|
|
8174
|
+
},
|
|
8175
|
+
},
|
|
8176
|
+
pluginName: "jsenv:server_events",
|
|
8177
|
+
});
|
|
8178
|
+
return stringifyHtmlAst(htmlAst);
|
|
8179
|
+
},
|
|
8180
|
+
},
|
|
8181
|
+
devServerRoutes: [
|
|
8182
|
+
{
|
|
8183
|
+
endpoint: "GET /.internal/events.websocket",
|
|
8184
|
+
description: `Jsenv dev server emit server events on this endpoint. When a file is saved the "reload" event is sent here.`,
|
|
8185
|
+
fetch: serverEvents.fetch,
|
|
8186
|
+
declarationSource: import.meta.url,
|
|
8187
|
+
},
|
|
8188
|
+
],
|
|
8189
|
+
};
|
|
8190
|
+
};
|
|
8191
|
+
|
|
8192
|
+
const requireFromJsenv = createRequire(import.meta.url);
|
|
8193
|
+
|
|
8194
|
+
const parseUserAgentHeader = memoizeByFirstArgument((userAgent) => {
|
|
8195
|
+
if (userAgent.includes("node-fetch/")) {
|
|
8196
|
+
// it's not really node and conceptually we can't assume the node version
|
|
8197
|
+
// but good enough for now
|
|
8198
|
+
return {
|
|
8199
|
+
runtimeName: "node",
|
|
8200
|
+
runtimeVersion: process.version.slice(1),
|
|
8201
|
+
};
|
|
8202
|
+
}
|
|
8203
|
+
const UA = requireFromJsenv("@financial-times/polyfill-useragent-normaliser");
|
|
8204
|
+
const { ua } = new UA(userAgent);
|
|
8205
|
+
const { family, major, minor, patch } = ua;
|
|
8206
|
+
return {
|
|
8207
|
+
runtimeName: family.toLowerCase(),
|
|
8208
|
+
runtimeVersion:
|
|
8209
|
+
family === "Other" ? "unknown" : `${major}.${minor}${patch}`,
|
|
8210
|
+
};
|
|
8211
|
+
});
|
|
8212
|
+
|
|
8213
|
+
const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
8214
|
+
|
|
8215
|
+
/**
|
|
8216
|
+
* Starts the development server.
|
|
8217
|
+
*
|
|
8218
|
+
* @param {Object} [params={}] - Configuration params for the dev server.
|
|
8219
|
+
* @param {number} [params.port=3456] - Port number the server should listen on.
|
|
8220
|
+
* @param {string} [params.hostname="localhost"] - Hostname to bind the server to.
|
|
8221
|
+
* @param {boolean} [params.https=false] - Whether to use HTTPS.
|
|
8222
|
+
*
|
|
8223
|
+
* @returns {Promise<Object>} A promise that resolves to the server instance.
|
|
8224
|
+
* @throws {Error} Will throw an error if the server fails to start or is called with unexpected params.
|
|
8225
|
+
*
|
|
8226
|
+
* @example
|
|
8227
|
+
* // Start a basic dev server
|
|
8228
|
+
* const server = await startDevServer();
|
|
8229
|
+
* console.log(`Server started at ${server.origin}`);
|
|
8230
|
+
*
|
|
8231
|
+
* @example
|
|
8232
|
+
* // Start a server with custom params
|
|
8233
|
+
* const server = await startDevServer({
|
|
8234
|
+
* port: 8080,
|
|
8235
|
+
* });
|
|
8236
|
+
*/
|
|
8237
|
+
const startDevServer = async ({
|
|
8238
|
+
sourceDirectoryUrl,
|
|
8239
|
+
sourceMainFilePath = "./index.html",
|
|
8240
|
+
ignore,
|
|
8241
|
+
port = 3456,
|
|
8242
|
+
hostname,
|
|
8243
|
+
acceptAnyIp,
|
|
8244
|
+
https,
|
|
8245
|
+
// it's better to use http1 by default because it allows to get statusText in devtools
|
|
8246
|
+
// which gives valuable information when there is errors
|
|
8247
|
+
http2 = false,
|
|
8248
|
+
logLevel = EXECUTED_BY_TEST_PLAN ? "warn" : "info",
|
|
8249
|
+
serverLogLevel = "warn",
|
|
8250
|
+
services = [],
|
|
8251
|
+
|
|
8252
|
+
signal = new AbortController().signal,
|
|
8253
|
+
handleSIGINT = true,
|
|
8254
|
+
keepProcessAlive = true,
|
|
8255
|
+
onStop = () => {},
|
|
8256
|
+
|
|
8257
|
+
sourceFilesConfig = {},
|
|
8258
|
+
clientAutoreload = true,
|
|
8259
|
+
|
|
8260
|
+
// runtimeCompat is the runtimeCompat for the build
|
|
8261
|
+
// when specified, dev server use it to warn in case
|
|
8262
|
+
// code would be supported during dev but not after build
|
|
8263
|
+
runtimeCompat = defaultRuntimeCompat,
|
|
8264
|
+
plugins = [],
|
|
8265
|
+
referenceAnalysis = {},
|
|
8266
|
+
nodeEsmResolution,
|
|
8267
|
+
supervisor = true,
|
|
8268
|
+
magicExtensions,
|
|
8269
|
+
magicDirectoryIndex,
|
|
8270
|
+
directoryListing,
|
|
8271
|
+
injections,
|
|
8272
|
+
transpilation,
|
|
8273
|
+
cacheControl = true,
|
|
8274
|
+
ribbon = true,
|
|
8275
|
+
// toolbar = false,
|
|
8276
|
+
onKitchenCreated = () => {},
|
|
8277
|
+
|
|
8278
|
+
sourcemaps = "inline",
|
|
8279
|
+
sourcemapsSourcesContent,
|
|
8280
|
+
outDirectoryUrl,
|
|
8281
|
+
...rest
|
|
8282
|
+
}) => {
|
|
8283
|
+
// params type checking
|
|
8284
|
+
{
|
|
8285
|
+
const unexpectedParamNames = Object.keys(rest);
|
|
8286
|
+
if (unexpectedParamNames.length > 0) {
|
|
8287
|
+
throw new TypeError(
|
|
8288
|
+
`${unexpectedParamNames.join(",")}: there is no such param`,
|
|
8289
|
+
);
|
|
8290
|
+
}
|
|
8291
|
+
sourceDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
8292
|
+
sourceDirectoryUrl,
|
|
8293
|
+
"sourceDirectoryUrl",
|
|
8294
|
+
);
|
|
8295
|
+
if (!existsSync(new URL(sourceDirectoryUrl))) {
|
|
8296
|
+
throw new Error(`ENOENT on sourceDirectoryUrl at ${sourceDirectoryUrl}`);
|
|
8297
|
+
}
|
|
8298
|
+
if (typeof sourceMainFilePath !== "string") {
|
|
8299
|
+
throw new TypeError(
|
|
8300
|
+
`sourceMainFilePath must be a string, got ${sourceMainFilePath}`,
|
|
8301
|
+
);
|
|
8302
|
+
}
|
|
8303
|
+
sourceMainFilePath = urlToRelativeUrl(
|
|
8304
|
+
new URL(sourceMainFilePath, sourceDirectoryUrl),
|
|
8305
|
+
sourceDirectoryUrl,
|
|
8306
|
+
);
|
|
8307
|
+
if (outDirectoryUrl === undefined) {
|
|
8308
|
+
if (
|
|
8309
|
+
process.env.CAPTURING_SIDE_EFFECTS ||
|
|
8310
|
+
(false)
|
|
8311
|
+
) {
|
|
8312
|
+
outDirectoryUrl = new URL("../.jsenv/", sourceDirectoryUrl);
|
|
8313
|
+
} else {
|
|
8314
|
+
const packageDirectoryUrl = lookupPackageDirectory(sourceDirectoryUrl);
|
|
8315
|
+
if (packageDirectoryUrl) {
|
|
8316
|
+
outDirectoryUrl = `${packageDirectoryUrl}.jsenv/`;
|
|
8317
|
+
}
|
|
8318
|
+
}
|
|
8319
|
+
} else if (outDirectoryUrl !== null && outDirectoryUrl !== false) {
|
|
8320
|
+
outDirectoryUrl = assertAndNormalizeDirectoryUrl(
|
|
8321
|
+
outDirectoryUrl,
|
|
8322
|
+
"outDirectoryUrl",
|
|
8323
|
+
);
|
|
8324
|
+
}
|
|
8325
|
+
}
|
|
8326
|
+
|
|
8327
|
+
// params normalization
|
|
8328
|
+
{
|
|
8329
|
+
if (clientAutoreload === true) {
|
|
8330
|
+
clientAutoreload = {};
|
|
8331
|
+
}
|
|
8332
|
+
if (clientAutoreload === false) {
|
|
8333
|
+
clientAutoreload = { enabled: false };
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
|
|
8337
|
+
const logger = createLogger({ logLevel });
|
|
8338
|
+
const startDevServerTask = createTaskLog("start dev server", {
|
|
8339
|
+
disabled: !logger.levels.info,
|
|
8340
|
+
});
|
|
8341
|
+
|
|
8342
|
+
const serverStopCallbackSet = new Set();
|
|
8343
|
+
const serverStopAbortController = new AbortController();
|
|
8344
|
+
serverStopCallbackSet.add(() => {
|
|
8345
|
+
serverStopAbortController.abort();
|
|
8346
|
+
});
|
|
8347
|
+
const serverStopAbortSignal = serverStopAbortController.signal;
|
|
8348
|
+
const kitchenCache = new Map();
|
|
8349
|
+
|
|
8350
|
+
const finalServices = [];
|
|
8351
|
+
// x-server-inspect service
|
|
8352
|
+
{
|
|
8353
|
+
finalServices.push({
|
|
8354
|
+
name: "jsenv:server_header",
|
|
8355
|
+
routes: [
|
|
8356
|
+
{
|
|
8357
|
+
endpoint: "GET /.internal/server.json",
|
|
8358
|
+
description: "Get information about jsenv dev server",
|
|
8359
|
+
availableMediaTypes: ["application/json"],
|
|
8360
|
+
declarationSource: import.meta.url,
|
|
8361
|
+
fetch: () =>
|
|
8362
|
+
Response.json({
|
|
8363
|
+
server: "jsenv_dev_server/1",
|
|
8364
|
+
sourceDirectoryUrl,
|
|
8365
|
+
}),
|
|
8366
|
+
},
|
|
8367
|
+
],
|
|
8368
|
+
injectResponseProperties: () => {
|
|
8369
|
+
return {
|
|
8370
|
+
headers: {
|
|
8371
|
+
server: "jsenv_dev_server/1",
|
|
8372
|
+
},
|
|
8373
|
+
};
|
|
8374
|
+
},
|
|
8375
|
+
});
|
|
8376
|
+
}
|
|
8377
|
+
// cors service
|
|
8378
|
+
{
|
|
8379
|
+
finalServices.push(
|
|
8380
|
+
jsenvServiceCORS({
|
|
8381
|
+
accessControlAllowRequestOrigin: true,
|
|
8382
|
+
accessControlAllowRequestMethod: true,
|
|
8383
|
+
accessControlAllowRequestHeaders: true,
|
|
8384
|
+
accessControlAllowedRequestHeaders: [
|
|
8385
|
+
...jsenvAccessControlAllowedHeaders,
|
|
8386
|
+
"x-jsenv-execution-id",
|
|
8387
|
+
],
|
|
8388
|
+
accessControlAllowCredentials: true,
|
|
8389
|
+
timingAllowOrigin: true,
|
|
8390
|
+
}),
|
|
8391
|
+
);
|
|
8392
|
+
}
|
|
8393
|
+
// custom services
|
|
8394
|
+
{
|
|
8395
|
+
finalServices.push(...services);
|
|
8396
|
+
}
|
|
8397
|
+
// file_service
|
|
8398
|
+
{
|
|
8399
|
+
const clientFileChangeEventEmitter = createEventEmitter();
|
|
8400
|
+
const clientFileDereferencedEventEmitter = createEventEmitter();
|
|
8401
|
+
clientAutoreload = {
|
|
8402
|
+
enabled: true,
|
|
8403
|
+
clientServerEventsConfig: {},
|
|
8404
|
+
clientFileChangeEventEmitter,
|
|
8405
|
+
clientFileDereferencedEventEmitter,
|
|
8406
|
+
...clientAutoreload,
|
|
8407
|
+
};
|
|
8408
|
+
const stopWatchingSourceFiles = watchSourceFiles(
|
|
8409
|
+
sourceDirectoryUrl,
|
|
8410
|
+
(fileInfo) => {
|
|
8411
|
+
clientFileChangeEventEmitter.emit(fileInfo);
|
|
8412
|
+
},
|
|
8413
|
+
{
|
|
8414
|
+
sourceFilesConfig,
|
|
8415
|
+
keepProcessAlive: false,
|
|
8416
|
+
cooldownBetweenFileEvents: clientAutoreload.cooldownBetweenFileEvents,
|
|
8417
|
+
},
|
|
8418
|
+
);
|
|
8419
|
+
serverStopCallbackSet.add(stopWatchingSourceFiles);
|
|
8420
|
+
|
|
8421
|
+
const devServerPluginStore = createPluginStore([
|
|
8422
|
+
jsenvPluginServerEvents({ clientAutoreload }),
|
|
8423
|
+
...plugins,
|
|
8424
|
+
...getCorePlugins({
|
|
8425
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
8426
|
+
mainFilePath: sourceMainFilePath,
|
|
8427
|
+
runtimeCompat,
|
|
8428
|
+
sourceFilesConfig,
|
|
8429
|
+
|
|
8430
|
+
referenceAnalysis,
|
|
8431
|
+
nodeEsmResolution,
|
|
8432
|
+
magicExtensions,
|
|
8433
|
+
magicDirectoryIndex,
|
|
8434
|
+
directoryListing,
|
|
8435
|
+
supervisor,
|
|
8436
|
+
injections,
|
|
8437
|
+
transpilation,
|
|
8438
|
+
|
|
8439
|
+
clientAutoreload,
|
|
8440
|
+
cacheControl,
|
|
8441
|
+
ribbon,
|
|
8442
|
+
}),
|
|
8443
|
+
]);
|
|
8444
|
+
const getOrCreateKitchen = (request) => {
|
|
8445
|
+
const { runtimeName, runtimeVersion } = parseUserAgentHeader(
|
|
8446
|
+
request.headers["user-agent"] || "",
|
|
8447
|
+
);
|
|
8448
|
+
const runtimeId = `${runtimeName}@${runtimeVersion}`;
|
|
8449
|
+
const existing = kitchenCache.get(runtimeId);
|
|
8450
|
+
if (existing) {
|
|
8451
|
+
return existing;
|
|
8452
|
+
}
|
|
8453
|
+
const watchAssociations = URL_META.resolveAssociations(
|
|
8454
|
+
{ watch: stopWatchingSourceFiles.watchPatterns },
|
|
8455
|
+
sourceDirectoryUrl,
|
|
8456
|
+
);
|
|
8457
|
+
let kitchen;
|
|
8458
|
+
clientFileChangeEventEmitter.on(({ url, event }) => {
|
|
8459
|
+
const urlInfo = kitchen.graph.getUrlInfo(url);
|
|
8460
|
+
if (urlInfo) {
|
|
8461
|
+
if (event === "removed") {
|
|
8462
|
+
urlInfo.onRemoved();
|
|
8463
|
+
} else {
|
|
8464
|
+
urlInfo.onModified();
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
});
|
|
8468
|
+
const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
|
|
8469
|
+
|
|
8470
|
+
kitchen = createKitchen({
|
|
8471
|
+
name: runtimeId,
|
|
8472
|
+
signal: serverStopAbortSignal,
|
|
8473
|
+
logLevel,
|
|
8474
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
8475
|
+
mainFilePath: sourceMainFilePath,
|
|
8476
|
+
ignore,
|
|
8477
|
+
dev: true,
|
|
8478
|
+
runtimeCompat,
|
|
8479
|
+
clientRuntimeCompat,
|
|
8480
|
+
supervisor,
|
|
8481
|
+
sourcemaps,
|
|
8482
|
+
sourcemapsSourcesContent,
|
|
8483
|
+
outDirectoryUrl: outDirectoryUrl
|
|
8484
|
+
? new URL(`${runtimeName}@${runtimeVersion}/`, outDirectoryUrl)
|
|
8485
|
+
: undefined,
|
|
8486
|
+
});
|
|
8487
|
+
kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
|
|
8488
|
+
const { watch } = URL_META.applyAssociations({
|
|
8489
|
+
url: urlInfoCreated.url,
|
|
8490
|
+
associations: watchAssociations,
|
|
8491
|
+
});
|
|
8492
|
+
urlInfoCreated.isWatched = watch;
|
|
8493
|
+
// when an url depends on many others, we check all these (like package.json)
|
|
8494
|
+
urlInfoCreated.isValid = () => {
|
|
8495
|
+
if (!urlInfoCreated.url.startsWith("file:")) {
|
|
8496
|
+
return false;
|
|
8497
|
+
}
|
|
8498
|
+
if (urlInfoCreated.content === undefined) {
|
|
8499
|
+
// urlInfo content is undefined when:
|
|
8500
|
+
// - url info content never fetched
|
|
8501
|
+
// - it is considered as modified because undelying file is watched and got saved
|
|
8502
|
+
// - it is considered as modified because underlying file content
|
|
8503
|
+
// was compared using etag and it has changed
|
|
8504
|
+
return false;
|
|
8505
|
+
}
|
|
8506
|
+
if (!watch) {
|
|
8507
|
+
// file is not watched, check the filesystem
|
|
8508
|
+
let fileContentAsBuffer;
|
|
8509
|
+
try {
|
|
8510
|
+
fileContentAsBuffer = readFileSync(new URL(urlInfoCreated.url));
|
|
8511
|
+
} catch (e) {
|
|
8512
|
+
if (e.code === "ENOENT") {
|
|
8513
|
+
urlInfoCreated.onModified();
|
|
8514
|
+
return false;
|
|
8515
|
+
}
|
|
8516
|
+
return false;
|
|
8517
|
+
}
|
|
8518
|
+
const fileContentEtag = bufferToEtag(fileContentAsBuffer);
|
|
8519
|
+
if (fileContentEtag !== urlInfoCreated.originalContentEtag) {
|
|
8520
|
+
urlInfoCreated.onModified();
|
|
8521
|
+
// restore content to be able to compare it again later
|
|
8522
|
+
urlInfoCreated.kitchen.urlInfoTransformer.setContent(
|
|
8523
|
+
urlInfoCreated,
|
|
8524
|
+
String(fileContentAsBuffer),
|
|
8525
|
+
{
|
|
8526
|
+
contentEtag: fileContentEtag,
|
|
8527
|
+
},
|
|
8528
|
+
);
|
|
8529
|
+
return false;
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
|
|
8533
|
+
const implicitUrlInfo =
|
|
8534
|
+
urlInfoCreated.graph.getUrlInfo(implicitUrl);
|
|
8535
|
+
if (!implicitUrlInfo) {
|
|
8536
|
+
continue;
|
|
8537
|
+
}
|
|
8538
|
+
if (implicitUrlInfo.content === undefined) {
|
|
8539
|
+
// happens when we explicitely load an url with a search param
|
|
8540
|
+
// - it creates an implicit url info to the url without params
|
|
8541
|
+
// - we never explicitely request the url without search param so it has no content
|
|
8542
|
+
// in that case the underlying urlInfo cannot be invalidate by the implicit
|
|
8543
|
+
// we use modifiedTimestamp to detect if the url was loaded once
|
|
8544
|
+
// or is just here to be used later
|
|
8545
|
+
if (implicitUrlInfo.modifiedTimestamp) {
|
|
8546
|
+
return false;
|
|
8547
|
+
}
|
|
8548
|
+
continue;
|
|
8549
|
+
}
|
|
8550
|
+
if (!implicitUrlInfo.isValid()) {
|
|
8551
|
+
return false;
|
|
8552
|
+
}
|
|
8553
|
+
}
|
|
8554
|
+
return true;
|
|
8555
|
+
};
|
|
8556
|
+
});
|
|
8557
|
+
kitchen.graph.urlInfoDereferencedEventEmitter.on(
|
|
8558
|
+
(urlInfoDereferenced, lastReferenceFromOther) => {
|
|
8559
|
+
clientFileDereferencedEventEmitter.emit(
|
|
8560
|
+
urlInfoDereferenced,
|
|
8561
|
+
lastReferenceFromOther,
|
|
8562
|
+
);
|
|
8563
|
+
},
|
|
8564
|
+
);
|
|
8565
|
+
const devServerPluginController = createPluginController(
|
|
8566
|
+
devServerPluginStore,
|
|
8567
|
+
kitchen,
|
|
8568
|
+
);
|
|
8569
|
+
kitchen.setPluginController(devServerPluginController);
|
|
8570
|
+
|
|
8571
|
+
serverStopCallbackSet.add(() => {
|
|
8572
|
+
devServerPluginController.callHooks("destroy", kitchen.context);
|
|
8573
|
+
});
|
|
8574
|
+
kitchenCache.set(runtimeId, kitchen);
|
|
8575
|
+
onKitchenCreated(kitchen);
|
|
8576
|
+
return kitchen;
|
|
8577
|
+
};
|
|
8578
|
+
|
|
8579
|
+
finalServices.push({
|
|
8580
|
+
name: "jsenv:dev_server_routes",
|
|
8581
|
+
augmentRouteFetchSecondArg: (request) => {
|
|
8582
|
+
const kitchen = getOrCreateKitchen(request);
|
|
8583
|
+
return { kitchen };
|
|
8584
|
+
},
|
|
8585
|
+
routes: [
|
|
8586
|
+
...devServerPluginStore.allDevServerRoutes,
|
|
8587
|
+
{
|
|
8588
|
+
endpoint: "GET *",
|
|
8589
|
+
description: "Serve project files.",
|
|
8590
|
+
declarationSource: import.meta.url,
|
|
8591
|
+
fetch: async (request, { kitchen }) => {
|
|
8592
|
+
const { rootDirectoryUrl, mainFilePath } = kitchen.context;
|
|
8593
|
+
let requestResource = request.resource;
|
|
8594
|
+
let requestedUrl;
|
|
8595
|
+
if (requestResource.startsWith("/@fs/")) {
|
|
8596
|
+
const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
|
|
8597
|
+
requestedUrl = `file:///${fsRootRelativeUrl}`;
|
|
8598
|
+
} else {
|
|
8599
|
+
const requestedUrlObject = new URL(
|
|
8600
|
+
requestResource === "/"
|
|
8601
|
+
? mainFilePath
|
|
8602
|
+
: requestResource.slice(1),
|
|
8603
|
+
rootDirectoryUrl,
|
|
8604
|
+
);
|
|
8605
|
+
requestedUrlObject.searchParams.delete("hot");
|
|
8606
|
+
requestedUrl = requestedUrlObject.href;
|
|
8607
|
+
}
|
|
8608
|
+
const { referer } = request.headers;
|
|
8609
|
+
const parentUrl = referer
|
|
8610
|
+
? WEB_URL_CONVERTER.asFileUrl(referer, {
|
|
8611
|
+
origin: request.origin,
|
|
8612
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
8613
|
+
})
|
|
8614
|
+
: sourceDirectoryUrl;
|
|
8615
|
+
let reference = kitchen.graph.inferReference(
|
|
8616
|
+
request.resource,
|
|
8617
|
+
parentUrl,
|
|
8618
|
+
);
|
|
8619
|
+
if (reference) {
|
|
8620
|
+
reference.urlInfo.context.request = request;
|
|
8621
|
+
reference.urlInfo.context.requestedUrl = requestedUrl;
|
|
8622
|
+
} else {
|
|
8623
|
+
const rootUrlInfo = kitchen.graph.rootUrlInfo;
|
|
8624
|
+
rootUrlInfo.context.request = request;
|
|
8625
|
+
rootUrlInfo.context.requestedUrl = requestedUrl;
|
|
8626
|
+
reference = rootUrlInfo.dependencies.createResolveAndFinalize({
|
|
8627
|
+
trace: { message: parentUrl },
|
|
8628
|
+
type: "http_request",
|
|
8629
|
+
specifier: request.resource,
|
|
8630
|
+
});
|
|
8631
|
+
rootUrlInfo.context.request = null;
|
|
8632
|
+
rootUrlInfo.context.requestedUrl = null;
|
|
8633
|
+
}
|
|
8634
|
+
const urlInfo = reference.urlInfo;
|
|
8635
|
+
const ifNoneMatch = request.headers["if-none-match"];
|
|
8636
|
+
const urlInfoTargetedByCache =
|
|
8637
|
+
urlInfo.findParentIfInline() || urlInfo;
|
|
8638
|
+
|
|
8639
|
+
try {
|
|
8640
|
+
if (!urlInfo.error && ifNoneMatch) {
|
|
8641
|
+
const [clientOriginalContentEtag, clientContentEtag] =
|
|
8642
|
+
ifNoneMatch.split("_");
|
|
8643
|
+
if (
|
|
8644
|
+
urlInfoTargetedByCache.originalContentEtag ===
|
|
8645
|
+
clientOriginalContentEtag &&
|
|
8646
|
+
urlInfoTargetedByCache.contentEtag === clientContentEtag &&
|
|
8647
|
+
urlInfoTargetedByCache.isValid()
|
|
8648
|
+
) {
|
|
8649
|
+
const headers = {
|
|
8650
|
+
"cache-control": `private,max-age=0,must-revalidate`,
|
|
8651
|
+
};
|
|
8652
|
+
Object.keys(urlInfo.headers).forEach((key) => {
|
|
8653
|
+
if (key !== "content-length") {
|
|
8654
|
+
headers[key] = urlInfo.headers[key];
|
|
8655
|
+
}
|
|
8656
|
+
});
|
|
8657
|
+
return {
|
|
8658
|
+
status: 304,
|
|
8659
|
+
headers,
|
|
8660
|
+
};
|
|
8661
|
+
}
|
|
8662
|
+
}
|
|
8663
|
+
await urlInfo.cook({ request, reference });
|
|
8664
|
+
let { response } = urlInfo;
|
|
8665
|
+
if (response) {
|
|
8666
|
+
return response;
|
|
8667
|
+
}
|
|
8668
|
+
response = {
|
|
8669
|
+
url: reference.url,
|
|
8670
|
+
status: 200,
|
|
8671
|
+
headers: {
|
|
8672
|
+
// when we send eTag to the client the next request to the server
|
|
8673
|
+
// will send etag in request headers.
|
|
8674
|
+
// If they match jsenv bypass cooking and returns 304
|
|
8675
|
+
// This must not happen when a plugin uses "no-store" or "no-cache" as it means
|
|
8676
|
+
// plugin logic wants to happens for every request to this url
|
|
8677
|
+
...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
|
|
8678
|
+
? {
|
|
8679
|
+
"cache-control": "no-store", // for inline file we force no-store when parent is no-store
|
|
8680
|
+
}
|
|
8681
|
+
: {
|
|
8682
|
+
"cache-control": `private,max-age=0,must-revalidate`,
|
|
8683
|
+
// it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
|
|
8684
|
+
"eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
|
|
8685
|
+
}),
|
|
8686
|
+
...urlInfo.headers,
|
|
8687
|
+
"content-type": urlInfo.contentType,
|
|
8688
|
+
"content-length": urlInfo.contentLength,
|
|
8689
|
+
},
|
|
8690
|
+
body: urlInfo.content,
|
|
8691
|
+
timing: urlInfo.timing, // TODO: use something else
|
|
8692
|
+
};
|
|
8693
|
+
const augmentResponseInfo = {
|
|
8694
|
+
...kitchen.context,
|
|
8695
|
+
reference,
|
|
8696
|
+
urlInfo,
|
|
8697
|
+
};
|
|
8698
|
+
kitchen.pluginController.callHooks(
|
|
8699
|
+
"augmentResponse",
|
|
8700
|
+
augmentResponseInfo,
|
|
8701
|
+
(returnValue) => {
|
|
8702
|
+
response = composeTwoResponses(response, returnValue);
|
|
8703
|
+
},
|
|
8704
|
+
);
|
|
8705
|
+
return response;
|
|
8706
|
+
} catch (error) {
|
|
8707
|
+
const originalError = error ? error.cause || error : error;
|
|
8708
|
+
if (originalError.asResponse) {
|
|
8709
|
+
return originalError.asResponse();
|
|
8710
|
+
}
|
|
8711
|
+
const code = originalError.code;
|
|
8712
|
+
if (code === "PARSE_ERROR") {
|
|
8713
|
+
// when possible let browser re-throw the syntax error
|
|
8714
|
+
// it's not possible to do that when url info content is not available
|
|
8715
|
+
// (happens for js_module_fallback for instance)
|
|
8716
|
+
if (urlInfo.content !== undefined) {
|
|
8717
|
+
kitchen.context.logger
|
|
8718
|
+
.error(`Error while handling ${request.url}:
|
|
8719
|
+
${originalError.reasonCode || originalError.code}
|
|
8720
|
+
${error.trace?.message}`);
|
|
8721
|
+
return {
|
|
8722
|
+
url: reference.url,
|
|
8723
|
+
status: 200,
|
|
8724
|
+
// reason becomes the http response statusText, it must not contain invalid chars
|
|
8725
|
+
// https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
|
|
8726
|
+
statusText: error.reason,
|
|
8727
|
+
statusMessage: originalError.message,
|
|
8728
|
+
headers: {
|
|
8729
|
+
"content-type": urlInfo.contentType,
|
|
8730
|
+
"content-length": urlInfo.contentLength,
|
|
8731
|
+
"cache-control": "no-store",
|
|
8732
|
+
},
|
|
8733
|
+
body: urlInfo.content,
|
|
8734
|
+
};
|
|
8735
|
+
}
|
|
8736
|
+
return {
|
|
8737
|
+
url: reference.url,
|
|
8738
|
+
status: 500,
|
|
8739
|
+
statusText: error.reason,
|
|
8740
|
+
statusMessage: originalError.message,
|
|
8741
|
+
headers: {
|
|
8742
|
+
"cache-control": "no-store",
|
|
8743
|
+
},
|
|
8744
|
+
body: urlInfo.content,
|
|
8745
|
+
};
|
|
8746
|
+
}
|
|
8747
|
+
if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
|
|
8748
|
+
return serveDirectory(reference.url, {
|
|
8749
|
+
headers: {
|
|
8750
|
+
accept: "text/html",
|
|
8751
|
+
},
|
|
8752
|
+
canReadDirectory: true,
|
|
8753
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
8754
|
+
});
|
|
8755
|
+
}
|
|
8756
|
+
if (code === "NOT_ALLOWED") {
|
|
8757
|
+
return {
|
|
8758
|
+
url: reference.url,
|
|
8759
|
+
status: 403,
|
|
8760
|
+
statusText: originalError.reason,
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
if (code === "NOT_FOUND") {
|
|
8764
|
+
return {
|
|
8765
|
+
url: reference.url,
|
|
8766
|
+
status: 404,
|
|
8767
|
+
statusText: originalError.reason,
|
|
8768
|
+
statusMessage: originalError.message,
|
|
8769
|
+
};
|
|
8770
|
+
}
|
|
8771
|
+
return {
|
|
8772
|
+
url: reference.url,
|
|
8773
|
+
status: 500,
|
|
8774
|
+
statusText: error.reason,
|
|
8775
|
+
statusMessage: error.stack,
|
|
8776
|
+
headers: {
|
|
8777
|
+
"cache-control": "no-store",
|
|
8778
|
+
},
|
|
8779
|
+
};
|
|
8780
|
+
}
|
|
8781
|
+
},
|
|
8782
|
+
},
|
|
8783
|
+
],
|
|
8784
|
+
});
|
|
8785
|
+
}
|
|
8786
|
+
// jsenv error handler service
|
|
8787
|
+
{
|
|
8788
|
+
finalServices.push({
|
|
8789
|
+
name: "jsenv:omega_error_handler",
|
|
8790
|
+
handleError: (error) => {
|
|
8791
|
+
const getResponseForError = () => {
|
|
8792
|
+
if (error && error.asResponse) {
|
|
8793
|
+
return error.asResponse();
|
|
8794
|
+
}
|
|
8795
|
+
if (error && error.statusText === "Unexpected directory operation") {
|
|
8796
|
+
return {
|
|
8797
|
+
status: 403,
|
|
8798
|
+
};
|
|
8799
|
+
}
|
|
8800
|
+
return convertFileSystemErrorToResponseProperties(error);
|
|
8801
|
+
};
|
|
8802
|
+
const response = getResponseForError();
|
|
8803
|
+
if (!response) {
|
|
8804
|
+
return null;
|
|
8805
|
+
}
|
|
8806
|
+
const body = JSON.stringify({
|
|
8807
|
+
status: response.status,
|
|
8808
|
+
statusText: response.statusText,
|
|
8809
|
+
headers: response.headers,
|
|
8810
|
+
body: response.body,
|
|
8811
|
+
});
|
|
8812
|
+
return {
|
|
8813
|
+
status: 200,
|
|
8814
|
+
headers: {
|
|
8815
|
+
"content-type": "application/json",
|
|
8816
|
+
"content-length": Buffer.byteLength(body),
|
|
8817
|
+
},
|
|
8818
|
+
body,
|
|
8819
|
+
};
|
|
8820
|
+
},
|
|
8821
|
+
});
|
|
8822
|
+
}
|
|
8823
|
+
// default error handler
|
|
8824
|
+
{
|
|
8825
|
+
finalServices.push(
|
|
8826
|
+
jsenvServiceErrorHandler({
|
|
8827
|
+
sendErrorDetails: true,
|
|
8828
|
+
}),
|
|
8829
|
+
);
|
|
8830
|
+
}
|
|
8831
|
+
|
|
8832
|
+
const server = await startServer({
|
|
8833
|
+
signal,
|
|
8834
|
+
stopOnExit: false,
|
|
8835
|
+
stopOnSIGINT: handleSIGINT,
|
|
8836
|
+
stopOnInternalError: false,
|
|
8837
|
+
keepProcessAlive,
|
|
8838
|
+
logLevel: serverLogLevel,
|
|
8839
|
+
startLog: false,
|
|
8840
|
+
|
|
8841
|
+
https,
|
|
8842
|
+
http2,
|
|
8843
|
+
acceptAnyIp,
|
|
8844
|
+
hostname,
|
|
8845
|
+
port,
|
|
8846
|
+
requestWaitingMs: 60_000,
|
|
8847
|
+
services: finalServices,
|
|
8848
|
+
});
|
|
8849
|
+
server.stoppedPromise.then((reason) => {
|
|
8850
|
+
onStop();
|
|
8851
|
+
for (const serverStopCallback of serverStopCallbackSet) {
|
|
8852
|
+
serverStopCallback(reason);
|
|
8853
|
+
}
|
|
8854
|
+
serverStopCallbackSet.clear();
|
|
8855
|
+
});
|
|
8856
|
+
startDevServerTask.done();
|
|
8857
|
+
if (hostname) {
|
|
8858
|
+
delete server.origins.localip;
|
|
8859
|
+
delete server.origins.externalip;
|
|
8860
|
+
}
|
|
8861
|
+
logger.info(``);
|
|
8862
|
+
Object.keys(server.origins).forEach((key) => {
|
|
8863
|
+
logger.info(`- ${server.origins[key]}`);
|
|
8864
|
+
});
|
|
8865
|
+
logger.info(``);
|
|
8866
|
+
return {
|
|
8867
|
+
origin: server.origin,
|
|
8868
|
+
sourceDirectoryUrl,
|
|
8869
|
+
stop: () => {
|
|
8870
|
+
server.stop();
|
|
8871
|
+
},
|
|
8872
|
+
kitchenCache,
|
|
8873
|
+
};
|
|
8874
|
+
};
|
|
8875
|
+
|
|
8876
|
+
const cacheIsDisabledInResponseHeader = (urlInfo) => {
|
|
8877
|
+
return (
|
|
8878
|
+
urlInfo.headers["cache-control"] === "no-store" ||
|
|
8879
|
+
urlInfo.headers["cache-control"] === "no-cache"
|
|
8880
|
+
);
|
|
8881
|
+
};
|
|
8882
|
+
|
|
8883
|
+
export { startDevServer };
|