@jsenv/core 41.4.17 → 41.4.19
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 +693 -509
- package/dist/build/jsenv_core_packages.js +3 -187
- package/dist/client/directory_listing/js/directory_listing.js +23 -2
- package/dist/start_dev_server/start_dev_server.js +813 -625
- package/package.json +8 -8
- package/src/build/build.js +66 -15
- package/src/build/build_content_report.js +24 -15
- package/src/build/build_specifier_manager.js +2 -2
- package/src/dev/dev_server_plugins/dev_server_plugin_omega_error_handler.js +4 -1
- package/src/dev/dev_server_plugins/dev_server_plugin_serve_source_files.js +116 -68
- package/src/dev/start_dev_server.js +24 -4
- package/src/helpers/watch_source_files.js +25 -20
- package/src/kitchen/kitchen.js +6 -0
- package/src/kitchen/prepend_content.js +4 -4
- package/src/kitchen/url_graph/references.js +30 -13
- package/src/kitchen/url_graph/url_graph.js +0 -1
- package/src/kitchen/url_graph/url_info_transformations.js +16 -6
- package/src/plugins/import_meta_css/jsenv_plugin_import_meta_css.js +10 -40
- package/src/plugins/import_meta_hot/import_meta_hot_analysis.js +70 -0
- package/src/plugins/import_meta_hot/jsenv_plugin_import_meta_hot.js +35 -20
- package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +22 -56
- package/src/plugins/modulepreload/jsenv_plugin_modulepreload.js +128 -0
- package/src/plugins/plugins.js +3 -0
- package/src/plugins/protocol_file/client/directory_listing.jsx +27 -1
- package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +20 -0
- package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +32 -15
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +11 -2
- package/src/plugins/reference_analysis/js/jsenv_plugin_js_reference_analysis.js +1 -2
- package/src/plugins/resolution_node_esm/node_esm_resolver.js +0 -5
- package/src/plugins/import_meta_hot/babel_plugin_metadata_import_meta_hot.js +0 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "41.4.
|
|
3
|
+
"version": "41.4.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tool to develop, test and build js projects",
|
|
6
6
|
"repository": {
|
|
@@ -72,13 +72,13 @@
|
|
|
72
72
|
"test:snapshot_clear": "npx @jsenv/filesystem clear **/tests/**/side_effects/"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@jsenv/ast": "6.9.
|
|
76
|
-
"@jsenv/js-module-fallback": "1.5.
|
|
77
|
-
"@jsenv/plugin-bundling": "2.10.
|
|
78
|
-
"@jsenv/plugin-minification": "1.7.
|
|
79
|
-
"@jsenv/plugin-supervisor": "1.8.
|
|
80
|
-
"@jsenv/plugin-transpilation": "1.6.
|
|
81
|
-
"@jsenv/server": "17.6.
|
|
75
|
+
"@jsenv/ast": "6.9.2",
|
|
76
|
+
"@jsenv/js-module-fallback": "1.5.1",
|
|
77
|
+
"@jsenv/plugin-bundling": "2.10.18",
|
|
78
|
+
"@jsenv/plugin-minification": "1.7.10",
|
|
79
|
+
"@jsenv/plugin-supervisor": "1.8.13",
|
|
80
|
+
"@jsenv/plugin-transpilation": "1.6.1",
|
|
81
|
+
"@jsenv/server": "17.6.2",
|
|
82
82
|
"@jsenv/sourcemap": "1.4.2",
|
|
83
83
|
"react-table": "7.8.0"
|
|
84
84
|
},
|
package/src/build/build.js
CHANGED
|
@@ -38,10 +38,7 @@ import {
|
|
|
38
38
|
UNICODE,
|
|
39
39
|
} from "@jsenv/humanize";
|
|
40
40
|
import { applyNodeEsmResolution } from "@jsenv/node-esm-resolution";
|
|
41
|
-
import {
|
|
42
|
-
startMonitoringCpuUsage,
|
|
43
|
-
startMonitoringMemoryUsage,
|
|
44
|
-
} from "@jsenv/os-metrics";
|
|
41
|
+
import { startMonitoringMemoryUsage } from "@jsenv/os-metrics";
|
|
45
42
|
import { jsenvPluginBundling } from "@jsenv/plugin-bundling";
|
|
46
43
|
import { jsenvPluginMinification } from "@jsenv/plugin-minification";
|
|
47
44
|
import { jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
|
|
@@ -395,13 +392,12 @@ entryPoints: {
|
|
|
395
392
|
});
|
|
396
393
|
}
|
|
397
394
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
395
|
+
// cpu is not sampled: cooking, rollup and terser hold the event loop for
|
|
396
|
+
// seconds at a time, a sampler only runs in the gaps and would report the
|
|
397
|
+
// idle time. What the build cost is process.cpuUsage() over its duration.
|
|
401
398
|
const memoryMonitoring = startMonitoringMemoryUsage();
|
|
402
399
|
const [processMemoryUsageMonitoring] = memoryMonitoring;
|
|
403
400
|
const interval = setInterval(() => {
|
|
404
|
-
processCpuUsageMonitoring.measure();
|
|
405
401
|
processMemoryUsageMonitoring.measure();
|
|
406
402
|
}, 500).unref();
|
|
407
403
|
operation.addEndCallback(() => {
|
|
@@ -471,19 +467,24 @@ entryPoints: {
|
|
|
471
467
|
content += "\n";
|
|
472
468
|
return content;
|
|
473
469
|
};
|
|
474
|
-
const renderBuildEndLog = ({
|
|
470
|
+
const renderBuildEndLog = ({
|
|
471
|
+
duration,
|
|
472
|
+
buildFileContents,
|
|
473
|
+
cpuUsage,
|
|
474
|
+
phaseTimings,
|
|
475
|
+
}) => {
|
|
475
476
|
// tell how many files are generated in build directory
|
|
476
477
|
// tell the repartition?
|
|
477
478
|
// this is not really useful for single build right?
|
|
478
479
|
|
|
479
|
-
processCpuUsageMonitoring.end();
|
|
480
480
|
processMemoryUsageMonitoring.end();
|
|
481
481
|
|
|
482
482
|
return renderBuildDoneLog({
|
|
483
483
|
entryPointArray,
|
|
484
484
|
duration,
|
|
485
485
|
buildFileContents,
|
|
486
|
-
|
|
486
|
+
cpuUsage,
|
|
487
|
+
phaseTimings,
|
|
487
488
|
processMemoryUsage: processMemoryUsageMonitoring.info,
|
|
488
489
|
});
|
|
489
490
|
};
|
|
@@ -558,13 +559,25 @@ entryPoints: {
|
|
|
558
559
|
}, renderDynamicLog());
|
|
559
560
|
};
|
|
560
561
|
},
|
|
561
|
-
onBuildEnd: ({
|
|
562
|
+
onBuildEnd: ({
|
|
563
|
+
buildFileContents,
|
|
564
|
+
duration,
|
|
565
|
+
cpuUsage,
|
|
566
|
+
phaseTimings,
|
|
567
|
+
}) => {
|
|
562
568
|
clearInterval(interval);
|
|
563
569
|
dynamicLog.update("");
|
|
564
570
|
dynamicLog.destroy();
|
|
565
571
|
dynamicLog = null;
|
|
566
572
|
logger.info("");
|
|
567
|
-
logger.info(
|
|
573
|
+
logger.info(
|
|
574
|
+
renderBuildEndLog({
|
|
575
|
+
duration,
|
|
576
|
+
buildFileContents,
|
|
577
|
+
cpuUsage,
|
|
578
|
+
phaseTimings,
|
|
579
|
+
}),
|
|
580
|
+
);
|
|
568
581
|
},
|
|
569
582
|
};
|
|
570
583
|
};
|
|
@@ -591,8 +604,20 @@ entryPoints: {
|
|
|
591
604
|
);
|
|
592
605
|
};
|
|
593
606
|
},
|
|
594
|
-
onBuildEnd: ({
|
|
595
|
-
|
|
607
|
+
onBuildEnd: ({
|
|
608
|
+
buildFileContents,
|
|
609
|
+
duration,
|
|
610
|
+
cpuUsage,
|
|
611
|
+
phaseTimings,
|
|
612
|
+
}) => {
|
|
613
|
+
logger.info(
|
|
614
|
+
renderBuildEndLog({
|
|
615
|
+
duration,
|
|
616
|
+
buildFileContents,
|
|
617
|
+
cpuUsage,
|
|
618
|
+
phaseTimings,
|
|
619
|
+
}),
|
|
620
|
+
);
|
|
596
621
|
},
|
|
597
622
|
};
|
|
598
623
|
};
|
|
@@ -642,6 +667,7 @@ entryPoints: {
|
|
|
642
667
|
|
|
643
668
|
const runBuild = async ({ signal }) => {
|
|
644
669
|
const startDate = Date.now();
|
|
670
|
+
const cpuUsageAtStart = process.cpuUsage();
|
|
645
671
|
const { onBuildEnd, onEntryPointBuildStart } = startBuildLogs();
|
|
646
672
|
|
|
647
673
|
const buildUrlsGenerator = createBuildUrlsGenerator({
|
|
@@ -750,6 +776,7 @@ entryPoints: {
|
|
|
750
776
|
entryBuildInfo.buildInlineContents = result.buildInlineContents;
|
|
751
777
|
entryBuildInfo.buildManifest = result.buildManifest;
|
|
752
778
|
entryBuildInfo.buildSideEffectFiles = result.buildSideEffectFiles;
|
|
779
|
+
entryBuildInfo.phaseTimings = result.phaseTimings;
|
|
753
780
|
entryBuildInfo.duration = Date.now() - entryPointBuildStartMs;
|
|
754
781
|
onEntryPointBuildEnd();
|
|
755
782
|
})();
|
|
@@ -854,11 +881,19 @@ entryPoints: {
|
|
|
854
881
|
}
|
|
855
882
|
}
|
|
856
883
|
}
|
|
884
|
+
// entry points are built concurrently, interleaved on the same thread:
|
|
885
|
+
// their phases overlap and only a single one can be told apart
|
|
886
|
+
const phaseTimings =
|
|
887
|
+
entryBuildInfoMap.size === 1
|
|
888
|
+
? entryBuildInfoMap.values().next().value.phaseTimings
|
|
889
|
+
: null;
|
|
857
890
|
onBuildEnd({
|
|
858
891
|
buildFileContents,
|
|
859
892
|
buildInlineContents,
|
|
860
893
|
buildManifest,
|
|
861
894
|
duration: Date.now() - startDate,
|
|
895
|
+
cpuUsage: process.cpuUsage(cpuUsageAtStart),
|
|
896
|
+
phaseTimings,
|
|
862
897
|
});
|
|
863
898
|
return {
|
|
864
899
|
...(returnBuildInlineContents ? { buildInlineContents } : {}),
|
|
@@ -1222,6 +1257,14 @@ const prepareEntryPointBuild = async (
|
|
|
1222
1257
|
return {
|
|
1223
1258
|
entryReference,
|
|
1224
1259
|
buildEntryPoint: async ({ getOtherEntryBuildInfo }) => {
|
|
1260
|
+
const phaseTimings = {};
|
|
1261
|
+
let phaseStartMs = Date.now();
|
|
1262
|
+
const endPhase = (name) => {
|
|
1263
|
+
const nowMs = Date.now();
|
|
1264
|
+
phaseTimings[name] = nowMs - phaseStartMs;
|
|
1265
|
+
phaseStartMs = nowMs;
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1225
1268
|
craft: {
|
|
1226
1269
|
_getOtherEntryBuildInfo = getOtherEntryBuildInfo;
|
|
1227
1270
|
if (outDirectoryUrl) {
|
|
@@ -1229,6 +1272,7 @@ const prepareEntryPointBuild = async (
|
|
|
1229
1272
|
}
|
|
1230
1273
|
await rawRootUrlInfo.cookDependencies({ operation: buildOperation });
|
|
1231
1274
|
}
|
|
1275
|
+
endPhase("craft");
|
|
1232
1276
|
|
|
1233
1277
|
const finalKitchen = createKitchen({
|
|
1234
1278
|
name: "shape",
|
|
@@ -1443,6 +1487,8 @@ const prepareEntryPointBuild = async (
|
|
|
1443
1487
|
}
|
|
1444
1488
|
}
|
|
1445
1489
|
|
|
1490
|
+
endPhase("bundle");
|
|
1491
|
+
|
|
1446
1492
|
shape: {
|
|
1447
1493
|
finalKitchen.context.buildStep = "shape";
|
|
1448
1494
|
if (outDirectoryUrl) {
|
|
@@ -1462,6 +1508,8 @@ const prepareEntryPointBuild = async (
|
|
|
1462
1508
|
});
|
|
1463
1509
|
}
|
|
1464
1510
|
|
|
1511
|
+
endPhase("shape");
|
|
1512
|
+
|
|
1465
1513
|
const buildSideEffectFiles = [];
|
|
1466
1514
|
refine: {
|
|
1467
1515
|
finalKitchen.context.buildStep = "refine";
|
|
@@ -1569,6 +1617,8 @@ const prepareEntryPointBuild = async (
|
|
|
1569
1617
|
}
|
|
1570
1618
|
}
|
|
1571
1619
|
}
|
|
1620
|
+
endPhase("refine");
|
|
1621
|
+
|
|
1572
1622
|
const {
|
|
1573
1623
|
buildFileContents,
|
|
1574
1624
|
buildFileVersions,
|
|
@@ -1588,6 +1638,7 @@ const prepareEntryPointBuild = async (
|
|
|
1588
1638
|
buildInlineContents,
|
|
1589
1639
|
buildManifest,
|
|
1590
1640
|
buildSideEffectFiles,
|
|
1641
|
+
phaseTimings,
|
|
1591
1642
|
};
|
|
1592
1643
|
},
|
|
1593
1644
|
};
|
|
@@ -19,11 +19,12 @@ ${createRepartitionMessage(buildContentReport, { indent })}
|
|
|
19
19
|
--------------------`;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
// a phase can be a few milliseconds long (refine, mostly): shown as such
|
|
23
|
+
const humanizePhaseDuration = (ms) => {
|
|
24
|
+
if (ms < 1000) {
|
|
25
|
+
return `${Math.round(ms)}ms`;
|
|
26
|
+
}
|
|
27
|
+
return humanizeDuration(ms, { short: true });
|
|
27
28
|
};
|
|
28
29
|
const humanizeProcessMemoryUsage = (value) => {
|
|
29
30
|
return humanizeMemory(value, { short: true, decimals: 0 });
|
|
@@ -32,7 +33,8 @@ export const renderBuildDoneLog = ({
|
|
|
32
33
|
entryPointArray,
|
|
33
34
|
duration,
|
|
34
35
|
buildFileContents,
|
|
35
|
-
|
|
36
|
+
cpuUsage,
|
|
37
|
+
phaseTimings,
|
|
36
38
|
processMemoryUsage,
|
|
37
39
|
}) => {
|
|
38
40
|
const buildContentReport = createBuildContentReport(buildFileContents);
|
|
@@ -47,15 +49,10 @@ export const renderBuildDoneLog = ({
|
|
|
47
49
|
title = `build done (${entryPointCount} entry points)`;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
// cpu
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
med: humanizeProcessCpuUsage(processCpuUsage.median),
|
|
55
|
-
min: humanizeProcessCpuUsage(processCpuUsage.min),
|
|
56
|
-
max: humanizeProcessCpuUsage(processCpuUsage.max),
|
|
57
|
-
});
|
|
58
|
-
lines.push(cpuUsageLine);
|
|
52
|
+
// cpu: the process cpu time over the build duration, 100% being one core
|
|
53
|
+
// busy the whole time (more when a native module works on other threads)
|
|
54
|
+
const cpuMs = (cpuUsage.user + cpuUsage.system) / 1000;
|
|
55
|
+
lines.push(`cpu: ${Math.round((cpuMs / duration) * 100)}%`);
|
|
59
56
|
|
|
60
57
|
// memory usage
|
|
61
58
|
let memoryUsageLine = "memory: ";
|
|
@@ -70,6 +67,16 @@ export const renderBuildDoneLog = ({
|
|
|
70
67
|
// duration
|
|
71
68
|
let durationLine = `duration: `;
|
|
72
69
|
durationLine += humanizeDuration(duration, { short: true });
|
|
70
|
+
if (phaseTimings) {
|
|
71
|
+
durationLine += renderDetails(
|
|
72
|
+
Object.fromEntries(
|
|
73
|
+
Object.keys(phaseTimings).map((phaseName) => [
|
|
74
|
+
phaseName,
|
|
75
|
+
humanizePhaseDuration(phaseTimings[phaseName]),
|
|
76
|
+
]),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
73
80
|
lines.push(durationLine);
|
|
74
81
|
|
|
75
82
|
// content
|
|
@@ -269,6 +276,8 @@ const createBuildContentReport = (buildFileContents) => {
|
|
|
269
276
|
};
|
|
270
277
|
|
|
271
278
|
const determineCategory = (buildRelativeUrl) => {
|
|
279
|
+
// a versioned url ("main.js?v=abc") is categorized by its file
|
|
280
|
+
buildRelativeUrl = buildRelativeUrl.replace(/[?#].*$/, "");
|
|
272
281
|
if (buildRelativeUrl.endsWith(".map")) {
|
|
273
282
|
return "sourcemap";
|
|
274
283
|
}
|
|
@@ -801,7 +801,7 @@ export const createBuildSpecifierManager = ({
|
|
|
801
801
|
}
|
|
802
802
|
if (mayUsePlaceholder(urlInfo)) {
|
|
803
803
|
const contentBeforeReplace = urlInfo.content;
|
|
804
|
-
const
|
|
804
|
+
const replaceResult = placeholderAPI.replaceAll(
|
|
805
805
|
contentBeforeReplace,
|
|
806
806
|
(placeholder) => {
|
|
807
807
|
const reference = placeholderToReferenceMap.get(placeholder);
|
|
@@ -809,7 +809,7 @@ export const createBuildSpecifierManager = ({
|
|
|
809
809
|
return value;
|
|
810
810
|
},
|
|
811
811
|
);
|
|
812
|
-
urlInfo.mutateContent(
|
|
812
|
+
urlInfo.mutateContent(replaceResult);
|
|
813
813
|
}
|
|
814
814
|
},
|
|
815
815
|
);
|
|
@@ -15,7 +15,10 @@ export const devServerPluginOmegaErrorHandler = () => {
|
|
|
15
15
|
status: 403,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
// the dev server runs with canExposeSensitiveData: file paths are welcome
|
|
19
|
+
return convertFileSystemErrorToResponseProperties(error, {
|
|
20
|
+
canExposeSensitiveData: true,
|
|
21
|
+
});
|
|
19
22
|
};
|
|
20
23
|
const response = getResponseForError();
|
|
21
24
|
if (!response) {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { bufferToEtag } from "@jsenv/filesystem";
|
|
2
2
|
import { formatError } from "@jsenv/humanize";
|
|
3
3
|
import { composeTwoResponses, fetchDirectory } from "@jsenv/server";
|
|
4
|
-
import { URL_META } from "@jsenv/url-meta";
|
|
5
4
|
import { normalizeUrl } from "@jsenv/urls";
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, statSync } from "node:fs";
|
|
7
6
|
|
|
8
7
|
import { watchSourceFiles } from "../../helpers/watch_source_files.js";
|
|
9
8
|
import { WEB_URL_CONVERTER } from "../../helpers/web_url_converter.js";
|
|
@@ -56,11 +55,10 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
56
55
|
if (existing) {
|
|
57
56
|
return existing;
|
|
58
57
|
}
|
|
59
|
-
const watchAssociations = URL_META.resolveAssociations(
|
|
60
|
-
{ watch: stopWatchingSourceFiles.watchPatterns },
|
|
61
|
-
sourceDirectoryUrl,
|
|
62
|
-
);
|
|
63
58
|
let kitchen;
|
|
59
|
+
// per url info, the file stat under which its content was last compared
|
|
60
|
+
// with the file on disk and found identical (see isValid)
|
|
61
|
+
const fileStatValidatedMap = new WeakMap();
|
|
64
62
|
clientFileChangeEventEmitter.on(({ url, event }) => {
|
|
65
63
|
const urlInfo = kitchen.graph.getUrlInfo(url);
|
|
66
64
|
if (urlInfo) {
|
|
@@ -101,11 +99,6 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
101
99
|
packageDirectory,
|
|
102
100
|
});
|
|
103
101
|
kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
|
|
104
|
-
const { watch } = URL_META.applyAssociations({
|
|
105
|
-
url: urlInfoCreated.url,
|
|
106
|
-
associations: watchAssociations,
|
|
107
|
-
});
|
|
108
|
-
urlInfoCreated.isWatched = watch;
|
|
109
102
|
// when an url depends on many others, we check all these (like package.json)
|
|
110
103
|
urlInfoCreated.isValid = () => {
|
|
111
104
|
const seenSet = new Set();
|
|
@@ -125,37 +118,64 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
125
118
|
// was compared using etag and it has changed
|
|
126
119
|
return false;
|
|
127
120
|
}
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// the
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
// The content held in memory is the file as it was read; a request
|
|
122
|
+
// must never win a stale answer, whatever the watcher's latency,
|
|
123
|
+
// and the ?v= a package.json decides even ends up in the browser's
|
|
124
|
+
// immutable cache. So the file is checked on disk at every
|
|
125
|
+
// validation — cheaply: a stat, compared with the one taken when
|
|
126
|
+
// the content was read (or last found identical). Only a file
|
|
127
|
+
// whose stat moved is read and hashed again.
|
|
128
|
+
// Inline content (a <script> inside an html) has no file of its
|
|
129
|
+
// own: it is as fresh as the html holding it, checked by the caller.
|
|
130
|
+
if (!urlInfo.isInline) {
|
|
131
|
+
let fileStat;
|
|
137
132
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
133
|
+
fileStat = statSync(new URL(urlInfo.url), {
|
|
134
|
+
throwIfNoEntry: false,
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
144
137
|
return false;
|
|
145
138
|
}
|
|
146
|
-
|
|
147
|
-
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
139
|
+
if (!fileStat) {
|
|
148
140
|
urlInfo.onModified();
|
|
149
|
-
// restore content to be able to compare it again later
|
|
150
|
-
urlInfo.kitchen.urlInfoTransformer.setContent(
|
|
151
|
-
urlInfo,
|
|
152
|
-
String(fileContentAsBuffer),
|
|
153
|
-
{
|
|
154
|
-
contentEtag: fileContentEtag,
|
|
155
|
-
},
|
|
156
|
-
);
|
|
157
141
|
return false;
|
|
158
142
|
}
|
|
143
|
+
const fileStatKnown =
|
|
144
|
+
fileStatValidatedMap.get(urlInfo) || urlInfo.data.fileStat;
|
|
145
|
+
const fileUnchanged =
|
|
146
|
+
fileStatKnown &&
|
|
147
|
+
fileStatKnown.mtimeMs === fileStat.mtimeMs &&
|
|
148
|
+
fileStatKnown.size === fileStat.size;
|
|
149
|
+
if (!fileUnchanged) {
|
|
150
|
+
let fileContentAsBuffer;
|
|
151
|
+
try {
|
|
152
|
+
fileContentAsBuffer = readFileSync(new URL(urlInfo.url));
|
|
153
|
+
} catch (e) {
|
|
154
|
+
if (e.code === "ENOENT") {
|
|
155
|
+
urlInfo.onModified();
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const fileContentEtag = bufferToEtag(fileContentAsBuffer);
|
|
161
|
+
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
162
|
+
fileStatValidatedMap.delete(urlInfo);
|
|
163
|
+
urlInfo.onModified();
|
|
164
|
+
// restore content to be able to compare it again later
|
|
165
|
+
urlInfo.kitchen.urlInfoTransformer.setContent(
|
|
166
|
+
urlInfo,
|
|
167
|
+
String(fileContentAsBuffer),
|
|
168
|
+
{
|
|
169
|
+
contentEtag: fileContentEtag,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
fileStatValidatedMap.set(urlInfo, {
|
|
175
|
+
mtimeMs: fileStat.mtimeMs,
|
|
176
|
+
size: fileStat.size,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
for (const implicitUrl of urlInfo.implicitUrlSet) {
|
|
161
181
|
const implicitUrlInfo = urlInfo.graph.getUrlInfo(implicitUrl);
|
|
@@ -271,6 +291,40 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
271
291
|
const ifNoneMatch = request.headers["if-none-match"];
|
|
272
292
|
const inlineParentUrlInfo = urlInfo.findParentIfInline();
|
|
273
293
|
const urlInfoTargetedByCache = inlineParentUrlInfo || urlInfo;
|
|
294
|
+
// The content held in memory is the response when it is finalized
|
|
295
|
+
// and still valid. Content can be defined while a cook is still in
|
|
296
|
+
// flight (a file watcher invalidation re-cooking in the background,
|
|
297
|
+
// for instance): at that point it holds the raw fetched content,
|
|
298
|
+
// transformations not applied yet. Serving that would send an html
|
|
299
|
+
// without any of the injected scripts. An inline url info (a
|
|
300
|
+
// <script> inside an html) is cooked again whenever the html
|
|
301
|
+
// containing it is cooked: its content is as fresh as the html's,
|
|
302
|
+
// so both must be valid.
|
|
303
|
+
const hasFreshContent = (urlInfo) =>
|
|
304
|
+
urlInfo.content !== undefined &&
|
|
305
|
+
urlInfo.contentFinalized &&
|
|
306
|
+
urlInfo.isValid();
|
|
307
|
+
const memoryContentIsFresh = () =>
|
|
308
|
+
inlineParentUrlInfo
|
|
309
|
+
? hasFreshContent(urlInfo) && hasFreshContent(inlineParentUrlInfo)
|
|
310
|
+
: hasFreshContent(urlInfo);
|
|
311
|
+
// a 304 goes through the hooks too: its headers stand for the
|
|
312
|
+
// cached response's, they must say the same
|
|
313
|
+
const augmentResponse = (response) => {
|
|
314
|
+
const augmentResponseInfo = {
|
|
315
|
+
...kitchen.context,
|
|
316
|
+
reference,
|
|
317
|
+
urlInfo,
|
|
318
|
+
};
|
|
319
|
+
kitchen.jsenvPluginsController.callHooks(
|
|
320
|
+
"augmentResponse",
|
|
321
|
+
augmentResponseInfo,
|
|
322
|
+
(returnValue) => {
|
|
323
|
+
response = composeTwoResponses(response, returnValue);
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
return response;
|
|
327
|
+
};
|
|
274
328
|
const respondWithNotModified = () => {
|
|
275
329
|
const headers = {
|
|
276
330
|
"cache-control": `private,max-age=0,must-revalidate`,
|
|
@@ -280,23 +334,21 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
280
334
|
headers[key] = urlInfo.headers[key];
|
|
281
335
|
}
|
|
282
336
|
});
|
|
283
|
-
return {
|
|
337
|
+
return augmentResponse({
|
|
284
338
|
status: 304,
|
|
285
339
|
headers,
|
|
286
|
-
};
|
|
340
|
+
});
|
|
287
341
|
};
|
|
288
342
|
|
|
289
343
|
try {
|
|
290
|
-
|
|
291
|
-
// is cooked, so its content is only known after cooking; its etag is
|
|
292
|
-
// compared below, once cooked
|
|
293
|
-
if (!urlInfo.error && ifNoneMatch && !inlineParentUrlInfo) {
|
|
344
|
+
if (!urlInfo.error && ifNoneMatch) {
|
|
294
345
|
const [clientOriginalContentEtag, clientContentEtag] =
|
|
295
346
|
ifNoneMatch.split("_");
|
|
296
347
|
if (
|
|
297
|
-
|
|
348
|
+
urlInfoTargetedByCache.originalContentEtag ===
|
|
349
|
+
clientOriginalContentEtag &&
|
|
298
350
|
urlInfo.contentEtag === clientContentEtag &&
|
|
299
|
-
|
|
351
|
+
memoryContentIsFresh()
|
|
300
352
|
) {
|
|
301
353
|
return respondWithNotModified();
|
|
302
354
|
}
|
|
@@ -314,18 +366,9 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
314
366
|
// client etag to match).
|
|
315
367
|
const servableFromMemory =
|
|
316
368
|
!urlInfo.error &&
|
|
317
|
-
!inlineParentUrlInfo &&
|
|
318
369
|
!urlInfo.response &&
|
|
319
|
-
urlInfo.content !== undefined &&
|
|
320
|
-
// content can be defined while a cook is still in flight (a file
|
|
321
|
-
// watcher invalidation re-cooking in the background, for
|
|
322
|
-
// instance): at that point it holds the raw fetched content,
|
|
323
|
-
// transformations not applied yet. Serving that would send an
|
|
324
|
-
// html without any of the injected scripts. Only finalized
|
|
325
|
-
// content is a complete response; anything else must go through
|
|
326
|
-
// cook() below, which joins the pending cook (see debounceCook).
|
|
327
|
-
urlInfo.contentFinalized &&
|
|
328
370
|
!cacheIsDisabledInResponseHeader(urlInfo) &&
|
|
371
|
+
!cacheIsDisabledInResponseHeader(urlInfoTargetedByCache) &&
|
|
329
372
|
// a "?hot" request exists to bypass every cache, this one
|
|
330
373
|
// included: it must be cooked, because cooking is what rewrites
|
|
331
374
|
// its references so "?hot" cascades to the modified files below
|
|
@@ -344,7 +387,7 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
344
387
|
// signal registry throwing on duplicate ids). Re-cooking under
|
|
345
388
|
// the normal request rewrites the references clean.
|
|
346
389
|
!urlInfo.contentCookedForHotRequest &&
|
|
347
|
-
|
|
390
|
+
memoryContentIsFresh();
|
|
348
391
|
if (!servableFromMemory) {
|
|
349
392
|
await urlInfo.cook({ request, reference });
|
|
350
393
|
urlInfo.contentCookedForHotRequest =
|
|
@@ -402,19 +445,7 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
402
445
|
? { "served from memory cache": null }
|
|
403
446
|
: urlInfo.timing,
|
|
404
447
|
};
|
|
405
|
-
|
|
406
|
-
...kitchen.context,
|
|
407
|
-
reference,
|
|
408
|
-
urlInfo,
|
|
409
|
-
};
|
|
410
|
-
kitchen.jsenvPluginsController.callHooks(
|
|
411
|
-
"augmentResponse",
|
|
412
|
-
augmentResponseInfo,
|
|
413
|
-
(returnValue) => {
|
|
414
|
-
response = composeTwoResponses(response, returnValue);
|
|
415
|
-
},
|
|
416
|
-
);
|
|
417
|
-
return response;
|
|
448
|
+
return augmentResponse(response);
|
|
418
449
|
} catch (error) {
|
|
419
450
|
const originalError = error ? error.cause || error : error;
|
|
420
451
|
if (originalError.asResponse) {
|
|
@@ -492,6 +523,14 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
492
523
|
"cache-control": "no-store",
|
|
493
524
|
},
|
|
494
525
|
};
|
|
526
|
+
} finally {
|
|
527
|
+
// What the request put on the url info context is for this
|
|
528
|
+
// request only: a cook happening later for another reason (the
|
|
529
|
+
// html holding an inline script is cooked again) must not see a
|
|
530
|
+
// stale "requestedUrl" and take the inline script for a direct
|
|
531
|
+
// request, which would make it pick the content of a previous
|
|
532
|
+
// reference (see jsenv:inline_content_fetcher).
|
|
533
|
+
forgetRequestFromContext(urlInfo.context);
|
|
495
534
|
}
|
|
496
535
|
},
|
|
497
536
|
},
|
|
@@ -501,6 +540,15 @@ export const devServerPluginServeSourceFiles = ({
|
|
|
501
540
|
return [devServerPluginRoutes, ...devServerJsenvPluginStore.allServerPlugins];
|
|
502
541
|
};
|
|
503
542
|
|
|
543
|
+
const forgetRequestFromContext = (context) => {
|
|
544
|
+
// own properties only: what is inherited from the owner context stays
|
|
545
|
+
for (const key of ["request", "requestedUrl", "reference"]) {
|
|
546
|
+
if (Object.hasOwn(context, key)) {
|
|
547
|
+
delete context[key];
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
504
552
|
const cacheIsDisabledInResponseHeader = (urlInfo) => {
|
|
505
553
|
return (
|
|
506
554
|
urlInfo.headers["cache-control"] === "no-store" ||
|
|
@@ -8,8 +8,13 @@ import {
|
|
|
8
8
|
serverPluginCORS,
|
|
9
9
|
startServer,
|
|
10
10
|
} from "@jsenv/server";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
urlIsOrIsInsideOf,
|
|
13
|
+
urlToFileSystemPath,
|
|
14
|
+
urlToRelativeUrl,
|
|
15
|
+
} from "@jsenv/urls";
|
|
12
16
|
import { existsSync } from "node:fs";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
13
18
|
|
|
14
19
|
import { defaultRuntimeCompat } from "../build/build_params.js";
|
|
15
20
|
import { createEventEmitter } from "../helpers/event_emitter.js";
|
|
@@ -50,6 +55,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
50
55
|
* @param {boolean|object} [params.serverTiming={ minDuration: 0.5 }] - server-timing response headers; `minDuration` (ms) drops entries that took less (0 when run by the test plan, so tests see every entry).
|
|
51
56
|
* @param {boolean|object} [params.ribbon=true] - The "ribbon" overlay marking the page as non-production. As an object: `text` (defaults to `"DEV"`), `color` (background, defaults to `"orange"`), `textColor` (defaults to dark or light depending on `color`), `href` (turns the ribbon into a link; only then does it capture clicks), `target` (defaults to `"_blank"`), `position` (`"top-right"` (default), `"top-left"`, `"bottom-right"`, `"bottom-left"` draw a diagonal corner ribbon; `"top"`, `"bottom"` draw a full width band), `htmlInclude` (defaults to `"/**\/*.html"`).
|
|
52
57
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
58
|
+
* @param {boolean} [params.modulepreload=false] - Send `Link: <url>; rel=modulepreload` response headers listing the static import graph of a page (as far as the graph knows it). Off by default, to enable once the server runs on http/2 or http/3: over http/1.1 the preloads take the 6 connections per origin ahead of the render-blocking scripts, and pages get slower (the why, with measures, in jsenv_plugin_modulepreload.js).
|
|
53
59
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
54
60
|
* @param {object} [params.injections] - Values to inject into files, as `{ urlPattern: getInjections }`. Keys are url patterns relative to sourceDirectoryUrl (`"./index.html"`, `"**\/*.js"`), values are functions receiving `urlInfo` and returning (or resolving to) an object of placeholders to replace, named `__LIKE_THIS__` by convention. In JS the value is injected as a JS literal (a string brings its own quotes), everywhere else as-is so it can be concatenated: `href="__BACKEND_URL__/users/me"`. An html url pattern also covers what is inlined in that html, so `<script>window.backendUrl = __BACKEND_URL__;</script>` shares the value with every js file of the page. See `INJECTIONS.optional` and `INJECTIONS.global`.
|
|
55
61
|
* @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
|
|
@@ -91,6 +97,7 @@ export const startDevServer = async ({
|
|
|
91
97
|
sourceFilesConfig = {},
|
|
92
98
|
clientAutoreload = true,
|
|
93
99
|
clientAutoreloadOnServerRestart = true,
|
|
100
|
+
modulepreload = false,
|
|
94
101
|
// server-timing response headers: devtools show how the time to answer is
|
|
95
102
|
// spent (cook measures come from the kitchen, see urlInfo.timing). Entries
|
|
96
103
|
// under minDuration are dropped so a human reads the measures that matter;
|
|
@@ -181,9 +188,12 @@ export const startDevServer = async ({
|
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
const logger = createLogger({ logLevel });
|
|
184
|
-
const startDevServerTask = createTaskLog(
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
const startDevServerTask = createTaskLog(
|
|
192
|
+
`start dev server for ${humanizeSourceDirectory(sourceDirectoryUrl)}`,
|
|
193
|
+
{
|
|
194
|
+
disabled: !logger.levels.info,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
187
197
|
|
|
188
198
|
const serverStopCallbackSet = new Set();
|
|
189
199
|
const serverStopAbortController = new AbortController();
|
|
@@ -262,6 +272,7 @@ export const startDevServer = async ({
|
|
|
262
272
|
|
|
263
273
|
clientAutoreload,
|
|
264
274
|
clientAutoreloadOnServerRestart,
|
|
275
|
+
modulepreload,
|
|
265
276
|
dependencyStatus: {
|
|
266
277
|
dependencyProblemEventEmitter,
|
|
267
278
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
@@ -366,3 +377,12 @@ export const startDevServer = async ({
|
|
|
366
377
|
kitchenCache,
|
|
367
378
|
};
|
|
368
379
|
};
|
|
380
|
+
|
|
381
|
+
// "./src/" when inside the current working directory, the full path otherwise
|
|
382
|
+
const humanizeSourceDirectory = (sourceDirectoryUrl) => {
|
|
383
|
+
const cwdUrl = pathToFileURL(`${process.cwd()}/`).href;
|
|
384
|
+
if (urlIsOrIsInsideOf(sourceDirectoryUrl, cwdUrl)) {
|
|
385
|
+
return `./${urlToRelativeUrl(sourceDirectoryUrl, cwdUrl)}`;
|
|
386
|
+
}
|
|
387
|
+
return urlToFileSystemPath(sourceDirectoryUrl);
|
|
388
|
+
};
|