@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/dist/build/build.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, injectJsenvScript, parseJsWithAcorn, visitHtmlNodes, analyzeScriptNode, getHtmlNodeText, getHtmlNodeAttribute, getHtmlNodePosition, getUrlForContentInsideHtml, setHtmlNodeAttributes, setHtmlNodeText, parseCssUrls, getHtmlNodeAttributePosition, parseSrcSet, removeHtmlNodeText, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode, findHtmlNode, removeHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
|
|
1
|
+
import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, injectJsenvScript, parseJsWithAcorn, visitHtmlNodes, analyzeScriptNode, getHtmlNodeText, getHtmlNodeAttribute, getHtmlNodePosition, getUrlForContentInsideHtml, setHtmlNodeAttributes, setHtmlNodeText, parseCssUrls, getHtmlNodeAttributePosition, parseSrcSet, removeHtmlNodeText, parseJsUrls, getUrlForContentInsideJs, visitJsAst, getImportMetaPropertyName, visitJsAstUntil, analyzeLinkNode, findHtmlNode, removeHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
|
|
2
2
|
import { bundleJsModules, jsenvPluginBundling } from "@jsenv/plugin-bundling";
|
|
3
3
|
import { jsenvPluginMinification } from "@jsenv/plugin-minification";
|
|
4
4
|
import { jsenvPluginTranspilation, jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
|
|
5
5
|
import { memoryUsage } from "node:process";
|
|
6
|
-
import { readFileSync, existsSync, readdirSync, lstatSync,
|
|
7
|
-
import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity,
|
|
6
|
+
import { readFileSync, existsSync, realpathSync, readdirSync, lstatSync, statSync } from "node:fs";
|
|
7
|
+
import { lookupPackageDirectory, urlIsOrIsInsideOf, registerDirectoryLifecycle, urlToRelativeUrl, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, ensureWindowsDriveLetter, setUrlFilename, moveUrl, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, normalizeUrl, ANSI, RUNTIME_COMPAT, CONTENT_TYPE, readPackageAtOrNull, urlToFilename, DATA_URL, errorToHTML, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, readCustomConditionsFromProcessArgs, collectFiles, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, isSpecifierForNodeBuiltin, injectQueryParams, renderDetails, humanizeDuration, humanizeFileSize, renderTable, renderBigSection, distributePercentages, humanizeMemory, comparePathnames, UNICODE, escapeRegexpSpecialChars, injectQueryParamIntoSpecifierWithoutEncoding, renderUrlOrRelativeUrlFilename, assertAndNormalizeDirectoryUrl, Abort, raceProcessTeardownEvents, startMonitoringMemoryUsage, inferRuntimeCompatFromClosestPackage, browserDefaultRuntimeCompat, nodeDefaultRuntimeCompat, clearDirectorySync, createTaskLog, createLookupPackageDirectory, ensureEmptyDirectory, updateJsonFileSync, createDynamicLog } from "./jsenv_core_packages.js";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
import { generateSourcemapFileUrl, createMagicSource, composeTwoSourcemaps, generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
|
|
10
10
|
import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
|
|
@@ -20,18 +20,21 @@ import "node:util";
|
|
|
20
20
|
const getDirectoryWatchPatterns = (
|
|
21
21
|
directoryUrl,
|
|
22
22
|
watchedDirectoryUrl,
|
|
23
|
-
{ sourceFilesConfig },
|
|
23
|
+
{ sourceFilesConfig, defaultPatterns = true },
|
|
24
24
|
) => {
|
|
25
25
|
const directoryUrlRelativeToWatchedDirectory = urlToRelativeUrl(
|
|
26
26
|
directoryUrl,
|
|
27
27
|
watchedDirectoryUrl,
|
|
28
28
|
);
|
|
29
|
-
const watchPatterns = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const watchPatterns = {};
|
|
30
|
+
if (defaultPatterns) {
|
|
31
|
+
Object.assign(watchPatterns, {
|
|
32
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/*`]: true, // by default watch everything inside the source directory
|
|
33
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*`]: false, // file starting with a dot -> do not watch
|
|
34
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*/`]: false, // directory starting with a dot -> do not watch
|
|
35
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/node_modules/`]: false, // node_modules directory -> do not watch
|
|
36
|
+
});
|
|
37
|
+
}
|
|
35
38
|
for (const key of Object.keys(sourceFilesConfig)) {
|
|
36
39
|
watchPatterns[`${directoryUrlRelativeToWatchedDirectory}${key}`] =
|
|
37
40
|
sourceFilesConfig[key];
|
|
@@ -52,11 +55,12 @@ const watchSourceFiles = (
|
|
|
52
55
|
// And jsenv should not consider these as source files and watch them (to not hurt performances)
|
|
53
56
|
const watchPatterns = {};
|
|
54
57
|
let watchedDirectoryUrl = "";
|
|
55
|
-
const addDirectoryToWatch = (directoryUrl) => {
|
|
58
|
+
const addDirectoryToWatch = (directoryUrl, { defaultPatterns } = {}) => {
|
|
56
59
|
Object.assign(
|
|
57
60
|
watchPatterns,
|
|
58
61
|
getDirectoryWatchPatterns(directoryUrl, watchedDirectoryUrl, {
|
|
59
62
|
sourceFilesConfig,
|
|
63
|
+
defaultPatterns,
|
|
60
64
|
}),
|
|
61
65
|
);
|
|
62
66
|
};
|
|
@@ -88,7 +92,6 @@ const watchSourceFiles = (
|
|
|
88
92
|
},
|
|
89
93
|
},
|
|
90
94
|
);
|
|
91
|
-
stopWatchingSourceFiles.watchPatterns = watchPatterns;
|
|
92
95
|
return stopWatchingSourceFiles;
|
|
93
96
|
};
|
|
94
97
|
|
|
@@ -108,16 +111,18 @@ const watchSourceFiles = (
|
|
|
108
111
|
}
|
|
109
112
|
watchedDirectoryUrl = packageDirectoryUrl;
|
|
110
113
|
for (const workspace of workspaces) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
const workspaceDirectoryUrl = workspace.endsWith("*")
|
|
115
|
+
? new URL(workspace.slice(0, -1), packageDirectoryUrl)
|
|
116
|
+
: new URL(workspace, packageDirectoryUrl);
|
|
117
|
+
addDirectoryToWatch(workspaceDirectoryUrl, {
|
|
118
|
+
// The source directory patterns already cover a workspace inside it.
|
|
119
|
+
// Every pattern is tested against every file found while watching, so
|
|
120
|
+
// the same patterns rooted at each workspace only multiply that work.
|
|
121
|
+
defaultPatterns: !urlIsOrIsInsideOf(
|
|
122
|
+
workspaceDirectoryUrl,
|
|
123
|
+
sourceDirectoryUrl,
|
|
124
|
+
),
|
|
125
|
+
});
|
|
121
126
|
}
|
|
122
127
|
// we are updating the root directory
|
|
123
128
|
// we must make the patterns relative to source directory relative to the new root directory
|
|
@@ -905,10 +910,9 @@ const prependJsClassicInJsClassic = (jsUrlInfo, urlInfoToPrepend) => {
|
|
|
905
910
|
const magicSource = createMagicSource(jsUrlInfo.content);
|
|
906
911
|
magicSource.prepend(`${urlInfoToPrepend.content}\n\n`);
|
|
907
912
|
const magicResult = magicSource.toContentAndSourcemap();
|
|
908
|
-
const sourcemap =
|
|
909
|
-
jsUrlInfo.sourcemap,
|
|
910
|
-
|
|
911
|
-
);
|
|
913
|
+
const sourcemap = jsUrlInfo.context.sourcemapsEnabled
|
|
914
|
+
? composeTwoSourcemaps(jsUrlInfo.sourcemap, magicResult.sourcemap)
|
|
915
|
+
: null;
|
|
912
916
|
jsUrlInfo.mutateContent({
|
|
913
917
|
content: magicResult.content,
|
|
914
918
|
sourcemap,
|
|
@@ -926,6 +930,7 @@ const prependJsClassicInJsModule = async (jsUrlInfo, urlInfoToPrepend) => {
|
|
|
926
930
|
input: jsUrlInfo.content,
|
|
927
931
|
inputIsJsModule: true,
|
|
928
932
|
inputUrl: jsUrlInfo.originalUrl,
|
|
933
|
+
options: { sourceMaps: jsUrlInfo.context.sourcemapsEnabled },
|
|
929
934
|
});
|
|
930
935
|
jsUrlInfo.mutateContent({
|
|
931
936
|
content: code,
|
|
@@ -1578,20 +1583,37 @@ const checkForDependencyRemovalEffects = (reference) => {
|
|
|
1578
1583
|
};
|
|
1579
1584
|
|
|
1580
1585
|
const traceFromUrlSite = (urlSite) => {
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
: ""
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1586
|
+
const { url, line, column, content } = urlSite;
|
|
1587
|
+
const trace = { url, line, column };
|
|
1588
|
+
// A file references many urls and an error on one of them is rare: the code
|
|
1589
|
+
// frame (and the message embedding it) is built the first time it is read,
|
|
1590
|
+
// not for every reference found.
|
|
1591
|
+
defineLazyProperty(trace, "codeFrame", () =>
|
|
1592
|
+
content ? generateContentFrame({ content, line, column }) : "",
|
|
1593
|
+
);
|
|
1594
|
+
defineLazyProperty(trace, "message", () => stringifyUrlSite(urlSite));
|
|
1595
|
+
return trace;
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
const defineLazyProperty = (object, property, compute) => {
|
|
1599
|
+
const setValue = (value) => {
|
|
1600
|
+
Object.defineProperty(object, property, {
|
|
1601
|
+
enumerable: true,
|
|
1602
|
+
configurable: true,
|
|
1603
|
+
writable: true,
|
|
1604
|
+
value,
|
|
1605
|
+
});
|
|
1594
1606
|
};
|
|
1607
|
+
Object.defineProperty(object, property, {
|
|
1608
|
+
enumerable: true,
|
|
1609
|
+
configurable: true,
|
|
1610
|
+
get: () => {
|
|
1611
|
+
const value = compute();
|
|
1612
|
+
setValue(value);
|
|
1613
|
+
return value;
|
|
1614
|
+
},
|
|
1615
|
+
set: setValue,
|
|
1616
|
+
});
|
|
1595
1617
|
};
|
|
1596
1618
|
|
|
1597
1619
|
const adjustUrlSite = (urlInfo, { url, line, column }) => {
|
|
@@ -2090,7 +2112,6 @@ const createUrlInfo = (url, context) => {
|
|
|
2090
2112
|
dereferencedTimestamp: 0,
|
|
2091
2113
|
originalContentEtag: null,
|
|
2092
2114
|
contentEtag: null,
|
|
2093
|
-
isWatched: false,
|
|
2094
2115
|
isValid: () => false,
|
|
2095
2116
|
data: {}, // plugins can put whatever they want here
|
|
2096
2117
|
referenceToOthersSet: new Set(),
|
|
@@ -2899,7 +2920,6 @@ const createUrlInfoTransformer = ({
|
|
|
2899
2920
|
contentAst, // undefined most of the time
|
|
2900
2921
|
contentEtag, // in practice always undefined
|
|
2901
2922
|
contentLength,
|
|
2902
|
-
sourcemap,
|
|
2903
2923
|
sourcemapIsWrong,
|
|
2904
2924
|
contentInjections,
|
|
2905
2925
|
} = transformations;
|
|
@@ -2926,11 +2946,14 @@ const createUrlInfoTransformer = ({
|
|
|
2926
2946
|
contentLength,
|
|
2927
2947
|
});
|
|
2928
2948
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2949
|
+
// "sourcemap" is read last, and only when it will be used: a plugin can
|
|
2950
|
+
// hand it back as a getter that generates the map on first read, so that
|
|
2951
|
+
// nothing is generated for a kitchen that throws sourcemaps away
|
|
2952
|
+
const sourcemap =
|
|
2953
|
+
mayHaveSourcemap(urlInfo) && shouldHandleSourcemap(urlInfo)
|
|
2954
|
+
? transformations.sourcemap
|
|
2955
|
+
: null;
|
|
2956
|
+
if (sourcemap) {
|
|
2934
2957
|
const sourcemapNormalized = normalizeSourcemap(urlInfo, sourcemap);
|
|
2935
2958
|
let currentSourcemap = urlInfo.sourcemap;
|
|
2936
2959
|
const finalSourcemap = composeTwoSourcemaps(
|
|
@@ -2963,6 +2986,14 @@ const createUrlInfoTransformer = ({
|
|
|
2963
2986
|
writeInsideOutDirectory(urlInfo);
|
|
2964
2987
|
};
|
|
2965
2988
|
|
|
2989
|
+
// Written synchronously on purpose, and measured: async is the tempting
|
|
2990
|
+
// choice, but here it loses. A cold load cooks hundreds of files; their
|
|
2991
|
+
// synchronous writes block the event loop ~160ms in total, while
|
|
2992
|
+
// asynchronous writes queue in the threadpool behind tens of MB of content
|
|
2993
|
+
// and sourcemaps, and a response that waits for its own write then waits
|
|
2994
|
+
// ~36ms on average (18s summed over 500 responses). Not waiting is not an
|
|
2995
|
+
// option either: the last write lands after the test that cooked it has
|
|
2996
|
+
// ended, and the side-effect snapshots lose it.
|
|
2966
2997
|
const writeInsideOutDirectory = (urlInfo) => {
|
|
2967
2998
|
// writing result inside ".jsenv" directory (debug purposes)
|
|
2968
2999
|
if (!outDirectoryUrl) {
|
|
@@ -3282,6 +3313,12 @@ const createKitchen = ({
|
|
|
3282
3313
|
INJECTIONS,
|
|
3283
3314
|
getPluginMeta: null,
|
|
3284
3315
|
sourcemaps,
|
|
3316
|
+
// a plugin producing a sourcemap can skip the work when it would be
|
|
3317
|
+
// thrown away (see shouldHandleSourcemap in url_info_transformations.js)
|
|
3318
|
+
sourcemapsEnabled:
|
|
3319
|
+
sourcemaps === "inline" ||
|
|
3320
|
+
sourcemaps === "file" ||
|
|
3321
|
+
sourcemaps === "programmatic",
|
|
3285
3322
|
outDirectoryUrl,
|
|
3286
3323
|
},
|
|
3287
3324
|
resolve: (specifier, importer = rootDirectoryUrl) => {
|
|
@@ -5751,8 +5788,7 @@ const parseAndTransformJsReferences = async (
|
|
|
5751
5788
|
for (const sequentialAction of sequentialActions) {
|
|
5752
5789
|
await sequentialAction();
|
|
5753
5790
|
}
|
|
5754
|
-
|
|
5755
|
-
return { content, sourcemap };
|
|
5791
|
+
return magicSource.toContentAndSourcemap();
|
|
5756
5792
|
};
|
|
5757
5793
|
|
|
5758
5794
|
const jsenvPluginReferenceExpectedTypes = () => {
|
|
@@ -6413,11 +6449,6 @@ const addRelationshipWithPackageJson = ({
|
|
|
6413
6449
|
String(packageJsonContentAsBuffer),
|
|
6414
6450
|
);
|
|
6415
6451
|
}
|
|
6416
|
-
// Checked on disk at every validation rather than trusted to the watcher:
|
|
6417
|
-
// what this file decides (the package version, hence the ?v= importers
|
|
6418
|
-
// embed) ends up in the browser's immutable cache, so a request racing the
|
|
6419
|
-
// watcher must never be answered from a stale package.json.
|
|
6420
|
-
packageJsonReference.urlInfo.revalidateOnFileSystem = true;
|
|
6421
6452
|
};
|
|
6422
6453
|
|
|
6423
6454
|
const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|
|
@@ -6759,6 +6790,240 @@ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
|
|
|
6759
6790
|
};
|
|
6760
6791
|
};
|
|
6761
6792
|
|
|
6793
|
+
const jsenvPluginFsRedirection = ({
|
|
6794
|
+
spa,
|
|
6795
|
+
directoryContentMagicName,
|
|
6796
|
+
magicExtensions = ["inherit", ".js"],
|
|
6797
|
+
magicDirectoryIndex = true,
|
|
6798
|
+
preserveSymlinks = false,
|
|
6799
|
+
}) => {
|
|
6800
|
+
return {
|
|
6801
|
+
name: "jsenv:fs_redirection",
|
|
6802
|
+
appliesDuring: "*",
|
|
6803
|
+
redirectReference: (reference) => {
|
|
6804
|
+
if (reference.url === "file:///") {
|
|
6805
|
+
return `ignore:file:///`;
|
|
6806
|
+
}
|
|
6807
|
+
if (reference.url === "file://") {
|
|
6808
|
+
return `ignore:file://`;
|
|
6809
|
+
}
|
|
6810
|
+
// ignore all new URL second arg
|
|
6811
|
+
if (reference.subtype === "new_url_second_arg") {
|
|
6812
|
+
if (reference.original) {
|
|
6813
|
+
return `ignore:${reference.original.specifier}`;
|
|
6814
|
+
}
|
|
6815
|
+
return `ignore:${reference.specifier}`;
|
|
6816
|
+
}
|
|
6817
|
+
// http, https, data, about, ...
|
|
6818
|
+
if (!reference.url.startsWith("file:")) {
|
|
6819
|
+
return null;
|
|
6820
|
+
}
|
|
6821
|
+
if (reference.original && !reference.original.url.startsWith("file:")) {
|
|
6822
|
+
return null;
|
|
6823
|
+
}
|
|
6824
|
+
if (reference.isInline) {
|
|
6825
|
+
return null;
|
|
6826
|
+
}
|
|
6827
|
+
|
|
6828
|
+
if (
|
|
6829
|
+
reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
|
|
6830
|
+
) {
|
|
6831
|
+
const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
|
|
6832
|
+
const directoryUrl = new URL(
|
|
6833
|
+
reference.specifierPathname
|
|
6834
|
+
.replace(`/${directoryContentMagicName}`, "/")
|
|
6835
|
+
.slice(1),
|
|
6836
|
+
rootDirectoryUrl,
|
|
6837
|
+
).href;
|
|
6838
|
+
return directoryUrl;
|
|
6839
|
+
}
|
|
6840
|
+
// ignore "./" on new URL("./")
|
|
6841
|
+
// if (
|
|
6842
|
+
// reference.subtype === "new_url_first_arg" &&
|
|
6843
|
+
// reference.specifier === "./"
|
|
6844
|
+
// ) {
|
|
6845
|
+
// return `ignore:${reference.url}`;
|
|
6846
|
+
// }
|
|
6847
|
+
const urlObject = new URL(reference.url);
|
|
6848
|
+
let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
|
|
6849
|
+
reference.fsStat = fsStat;
|
|
6850
|
+
const { search, hash } = urlObject;
|
|
6851
|
+
urlObject.search = "";
|
|
6852
|
+
urlObject.hash = "";
|
|
6853
|
+
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
6854
|
+
// trailing slash on directories
|
|
6855
|
+
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
6856
|
+
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
6857
|
+
const shouldApplyFilesystemMagicResolution =
|
|
6858
|
+
reference.type === "js_import";
|
|
6859
|
+
if (shouldApplyFilesystemMagicResolution) {
|
|
6860
|
+
const filesystemResolution = applyFileSystemMagicResolution(
|
|
6861
|
+
urlObject.href,
|
|
6862
|
+
{
|
|
6863
|
+
fileStat: fsStat,
|
|
6864
|
+
magicDirectoryIndex,
|
|
6865
|
+
magicExtensions: getExtensionsToTry(
|
|
6866
|
+
magicExtensions,
|
|
6867
|
+
reference.ownerUrlInfo.url,
|
|
6868
|
+
),
|
|
6869
|
+
},
|
|
6870
|
+
);
|
|
6871
|
+
if (filesystemResolution.stat) {
|
|
6872
|
+
fsStat = filesystemResolution.stat;
|
|
6873
|
+
reference.fsStat = fsStat;
|
|
6874
|
+
urlObject.href = filesystemResolution.url;
|
|
6875
|
+
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
if (spa) {
|
|
6879
|
+
// for SPA we want to serve the root HTML file most of the time
|
|
6880
|
+
if (!fsStat) {
|
|
6881
|
+
if (urlToExtension(urlObject)) {
|
|
6882
|
+
// url has an extension, we assume it's a file request -> let 404 happen
|
|
6883
|
+
return null;
|
|
6884
|
+
}
|
|
6885
|
+
if (specifierUsesTrailingSlash) {
|
|
6886
|
+
// the trailing slash asks for a directory and there is none here
|
|
6887
|
+
// -> let 404 happen (same reasoning as the extension above)
|
|
6888
|
+
return null;
|
|
6889
|
+
}
|
|
6890
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
6891
|
+
if (spaFallbackUrl) {
|
|
6892
|
+
return spaFallbackUrl;
|
|
6893
|
+
}
|
|
6894
|
+
return null;
|
|
6895
|
+
}
|
|
6896
|
+
if (fsStat.isDirectory()) {
|
|
6897
|
+
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
6898
|
+
const directoryEntryFileUrl = getDirectoryEntryFileUrl(urlObject);
|
|
6899
|
+
if (directoryEntryFileUrl) {
|
|
6900
|
+
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
6901
|
+
return directoryEntryFileUrl;
|
|
6902
|
+
}
|
|
6903
|
+
if (!specifierUsesTrailingSlash) {
|
|
6904
|
+
// the trailing slash is what tells a directory apart from a route:
|
|
6905
|
+
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
6906
|
+
// even when "join/" exists in the source files.
|
|
6907
|
+
// Without this a source directory would shadow the route having
|
|
6908
|
+
// the same name and the SPA would be unreachable in dev while
|
|
6909
|
+
// being perfectly fine once built
|
|
6910
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
6911
|
+
if (spaFallbackUrl) {
|
|
6912
|
+
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
6913
|
+
nullIfNotFound: true,
|
|
6914
|
+
});
|
|
6915
|
+
return spaFallbackUrl;
|
|
6916
|
+
}
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
if (!fsStat) {
|
|
6921
|
+
return null;
|
|
6922
|
+
}
|
|
6923
|
+
const urlBeforeSymlinkResolution = urlObject.href;
|
|
6924
|
+
if (preserveSymlinks) {
|
|
6925
|
+
return `${urlBeforeSymlinkResolution}${search}${hash}`;
|
|
6926
|
+
}
|
|
6927
|
+
const urlAfterSymlinkResolution = resolveSymlink(
|
|
6928
|
+
urlBeforeSymlinkResolution,
|
|
6929
|
+
);
|
|
6930
|
+
if (urlAfterSymlinkResolution !== urlBeforeSymlinkResolution) {
|
|
6931
|
+
reference.leadsToASymlink = true;
|
|
6932
|
+
// reference.baseUrl = urlBeforeSymlinkResolution;
|
|
6933
|
+
}
|
|
6934
|
+
const resolvedUrl = `${urlAfterSymlinkResolution}${search}${hash}`;
|
|
6935
|
+
return resolvedUrl;
|
|
6936
|
+
},
|
|
6937
|
+
};
|
|
6938
|
+
};
|
|
6939
|
+
|
|
6940
|
+
const applyFsStatEffectsOnUrlObject = (urlObject, fsStat) => {
|
|
6941
|
+
if (!fsStat) {
|
|
6942
|
+
return;
|
|
6943
|
+
}
|
|
6944
|
+
const { pathname } = urlObject;
|
|
6945
|
+
const pathnameUsesTrailingSlash = pathname.endsWith("/");
|
|
6946
|
+
// force trailing slash on directories
|
|
6947
|
+
if (fsStat.isDirectory()) {
|
|
6948
|
+
if (!pathnameUsesTrailingSlash) {
|
|
6949
|
+
urlObject.pathname = `${pathname}/`;
|
|
6950
|
+
}
|
|
6951
|
+
} else if (pathnameUsesTrailingSlash) {
|
|
6952
|
+
// otherwise remove trailing slash if any
|
|
6953
|
+
// a warning here? (because it's strange to reference a file with a trailing slash)
|
|
6954
|
+
urlObject.pathname = pathname.slice(0, -1);
|
|
6955
|
+
}
|
|
6956
|
+
};
|
|
6957
|
+
|
|
6958
|
+
const resolveSymlink = (fileUrl) => {
|
|
6959
|
+
const urlObject = new URL(fileUrl);
|
|
6960
|
+
const realpath = realpathSync(urlObject);
|
|
6961
|
+
const realUrlObject = pathToFileURL(realpath);
|
|
6962
|
+
if (urlObject.pathname.endsWith("/")) {
|
|
6963
|
+
realUrlObject.pathname += `/`;
|
|
6964
|
+
}
|
|
6965
|
+
return realUrlObject.href;
|
|
6966
|
+
};
|
|
6967
|
+
|
|
6968
|
+
const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
6969
|
+
const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
|
|
6970
|
+
if (existsSync(indexHtmlFileUrl)) {
|
|
6971
|
+
return indexHtmlFileUrl.href;
|
|
6972
|
+
}
|
|
6973
|
+
const filename = urlToFilename(directoryUrl);
|
|
6974
|
+
const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
|
|
6975
|
+
if (existsSync(htmlFileUrlCandidate)) {
|
|
6976
|
+
return htmlFileUrlCandidate.href;
|
|
6977
|
+
}
|
|
6978
|
+
return null;
|
|
6979
|
+
};
|
|
6980
|
+
const getSpaFallbackUrl = (reference) => {
|
|
6981
|
+
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
6982
|
+
reference.ownerUrlInfo.context;
|
|
6983
|
+
if (!requestedUrl) {
|
|
6984
|
+
// the SPA fallback answers a request; during build there is none
|
|
6985
|
+
return null;
|
|
6986
|
+
}
|
|
6987
|
+
const spaFallbackFileUrls = listSpaFallbackFileUrls(requestedUrl, {
|
|
6988
|
+
rootDirectoryUrl,
|
|
6989
|
+
mainFilePath,
|
|
6990
|
+
});
|
|
6991
|
+
for (const spaFallbackFileUrl of spaFallbackFileUrls) {
|
|
6992
|
+
if (existsSync(new URL(spaFallbackFileUrl))) {
|
|
6993
|
+
return spaFallbackFileUrl;
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6996
|
+
// none exists: the main file it is, and the 404 answering for it lists
|
|
6997
|
+
// what was tried (see directory listing)
|
|
6998
|
+
return new URL(mainFilePath, rootDirectoryUrl).href;
|
|
6999
|
+
};
|
|
7000
|
+
// The html files that can answer a route, closest first: the entry file of
|
|
7001
|
+
// the route's own directory ("index.html", then "<dirname>.html"), then of
|
|
7002
|
+
// each directory above it up to the server root, then the main file.
|
|
7003
|
+
const listSpaFallbackFileUrls = (
|
|
7004
|
+
requestedUrl,
|
|
7005
|
+
{ rootDirectoryUrl, mainFilePath },
|
|
7006
|
+
) => {
|
|
7007
|
+
const fileUrls = [];
|
|
7008
|
+
let directoryUrl = new URL("./", requestedUrl).href;
|
|
7009
|
+
while (urlIsOrIsInsideOf(directoryUrl, rootDirectoryUrl)) {
|
|
7010
|
+
fileUrls.push(new URL("index.html", directoryUrl).href);
|
|
7011
|
+
const filename = urlToFilename(directoryUrl);
|
|
7012
|
+
if (filename) {
|
|
7013
|
+
fileUrls.push(new URL(`${filename}.html`, directoryUrl).href);
|
|
7014
|
+
}
|
|
7015
|
+
if (directoryUrl === String(rootDirectoryUrl)) {
|
|
7016
|
+
break;
|
|
7017
|
+
}
|
|
7018
|
+
directoryUrl = new URL("../", directoryUrl).href;
|
|
7019
|
+
}
|
|
7020
|
+
const mainFileUrl = new URL(mainFilePath, rootDirectoryUrl).href;
|
|
7021
|
+
if (!fileUrls.includes(mainFileUrl)) {
|
|
7022
|
+
fileUrls.push(mainFileUrl);
|
|
7023
|
+
}
|
|
7024
|
+
return fileUrls;
|
|
7025
|
+
};
|
|
7026
|
+
|
|
6762
7027
|
/*
|
|
6763
7028
|
* NICE TO HAVE:
|
|
6764
7029
|
*
|
|
@@ -7136,6 +7401,24 @@ const generateDirectoryListingInjection = (
|
|
|
7136
7401
|
filePathExisting: `/${filePathExisting}`,
|
|
7137
7402
|
filePathNotFound,
|
|
7138
7403
|
});
|
|
7404
|
+
// a url without extension nor trailing slash is a route: in spa mode it
|
|
7405
|
+
// was answered with the closest html file, the 404 means none was found
|
|
7406
|
+
const urlNotFoundObject = new URL(urlNotFound);
|
|
7407
|
+
if (
|
|
7408
|
+
spa &&
|
|
7409
|
+
!urlToExtension(urlNotFoundObject) &&
|
|
7410
|
+
!urlNotFoundObject.pathname.endsWith("/")
|
|
7411
|
+
) {
|
|
7412
|
+
enoentDetails.spaFallbackFilePaths = listSpaFallbackFileUrls(
|
|
7413
|
+
urlNotFound,
|
|
7414
|
+
{ rootDirectoryUrl: serverRootDirectoryUrl, mainFilePath },
|
|
7415
|
+
).map((fileUrl) =>
|
|
7416
|
+
FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
7417
|
+
fileUrl,
|
|
7418
|
+
serverRootDirectoryUrl,
|
|
7419
|
+
),
|
|
7420
|
+
);
|
|
7421
|
+
}
|
|
7139
7422
|
}
|
|
7140
7423
|
|
|
7141
7424
|
return {
|
|
@@ -7207,223 +7490,6 @@ const getDirectoryContentItems = ({
|
|
|
7207
7490
|
return items;
|
|
7208
7491
|
};
|
|
7209
7492
|
|
|
7210
|
-
const jsenvPluginFsRedirection = ({
|
|
7211
|
-
spa,
|
|
7212
|
-
directoryContentMagicName,
|
|
7213
|
-
magicExtensions = ["inherit", ".js"],
|
|
7214
|
-
magicDirectoryIndex = true,
|
|
7215
|
-
preserveSymlinks = false,
|
|
7216
|
-
}) => {
|
|
7217
|
-
return {
|
|
7218
|
-
name: "jsenv:fs_redirection",
|
|
7219
|
-
appliesDuring: "*",
|
|
7220
|
-
redirectReference: (reference) => {
|
|
7221
|
-
if (reference.url === "file:///") {
|
|
7222
|
-
return `ignore:file:///`;
|
|
7223
|
-
}
|
|
7224
|
-
if (reference.url === "file://") {
|
|
7225
|
-
return `ignore:file://`;
|
|
7226
|
-
}
|
|
7227
|
-
// ignore all new URL second arg
|
|
7228
|
-
if (reference.subtype === "new_url_second_arg") {
|
|
7229
|
-
if (reference.original) {
|
|
7230
|
-
return `ignore:${reference.original.specifier}`;
|
|
7231
|
-
}
|
|
7232
|
-
return `ignore:${reference.specifier}`;
|
|
7233
|
-
}
|
|
7234
|
-
// http, https, data, about, ...
|
|
7235
|
-
if (!reference.url.startsWith("file:")) {
|
|
7236
|
-
return null;
|
|
7237
|
-
}
|
|
7238
|
-
if (reference.original && !reference.original.url.startsWith("file:")) {
|
|
7239
|
-
return null;
|
|
7240
|
-
}
|
|
7241
|
-
if (reference.isInline) {
|
|
7242
|
-
return null;
|
|
7243
|
-
}
|
|
7244
|
-
|
|
7245
|
-
if (
|
|
7246
|
-
reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
|
|
7247
|
-
) {
|
|
7248
|
-
const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
|
|
7249
|
-
const directoryUrl = new URL(
|
|
7250
|
-
reference.specifierPathname
|
|
7251
|
-
.replace(`/${directoryContentMagicName}`, "/")
|
|
7252
|
-
.slice(1),
|
|
7253
|
-
rootDirectoryUrl,
|
|
7254
|
-
).href;
|
|
7255
|
-
return directoryUrl;
|
|
7256
|
-
}
|
|
7257
|
-
// ignore "./" on new URL("./")
|
|
7258
|
-
// if (
|
|
7259
|
-
// reference.subtype === "new_url_first_arg" &&
|
|
7260
|
-
// reference.specifier === "./"
|
|
7261
|
-
// ) {
|
|
7262
|
-
// return `ignore:${reference.url}`;
|
|
7263
|
-
// }
|
|
7264
|
-
const urlObject = new URL(reference.url);
|
|
7265
|
-
let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
|
|
7266
|
-
reference.fsStat = fsStat;
|
|
7267
|
-
const { search, hash } = urlObject;
|
|
7268
|
-
urlObject.search = "";
|
|
7269
|
-
urlObject.hash = "";
|
|
7270
|
-
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
7271
|
-
// trailing slash on directories
|
|
7272
|
-
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
7273
|
-
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
7274
|
-
const shouldApplyFilesystemMagicResolution =
|
|
7275
|
-
reference.type === "js_import";
|
|
7276
|
-
if (shouldApplyFilesystemMagicResolution) {
|
|
7277
|
-
const filesystemResolution = applyFileSystemMagicResolution(
|
|
7278
|
-
urlObject.href,
|
|
7279
|
-
{
|
|
7280
|
-
fileStat: fsStat,
|
|
7281
|
-
magicDirectoryIndex,
|
|
7282
|
-
magicExtensions: getExtensionsToTry(
|
|
7283
|
-
magicExtensions,
|
|
7284
|
-
reference.ownerUrlInfo.url,
|
|
7285
|
-
),
|
|
7286
|
-
},
|
|
7287
|
-
);
|
|
7288
|
-
if (filesystemResolution.stat) {
|
|
7289
|
-
fsStat = filesystemResolution.stat;
|
|
7290
|
-
reference.fsStat = fsStat;
|
|
7291
|
-
urlObject.href = filesystemResolution.url;
|
|
7292
|
-
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
7293
|
-
}
|
|
7294
|
-
}
|
|
7295
|
-
if (spa) {
|
|
7296
|
-
// for SPA we want to serve the root HTML file most of the time
|
|
7297
|
-
if (!fsStat) {
|
|
7298
|
-
if (urlToExtension(urlObject)) {
|
|
7299
|
-
// url has an extension, we assume it's a file request -> let 404 happen
|
|
7300
|
-
return null;
|
|
7301
|
-
}
|
|
7302
|
-
if (specifierUsesTrailingSlash) {
|
|
7303
|
-
// the trailing slash asks for a directory and there is none here
|
|
7304
|
-
// -> let 404 happen (same reasoning as the extension above)
|
|
7305
|
-
return null;
|
|
7306
|
-
}
|
|
7307
|
-
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
7308
|
-
if (spaFallbackUrl) {
|
|
7309
|
-
return spaFallbackUrl;
|
|
7310
|
-
}
|
|
7311
|
-
return null;
|
|
7312
|
-
}
|
|
7313
|
-
if (fsStat.isDirectory()) {
|
|
7314
|
-
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
7315
|
-
const directoryEntryFileUrl = getDirectoryEntryFileUrl(urlObject);
|
|
7316
|
-
if (directoryEntryFileUrl) {
|
|
7317
|
-
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
7318
|
-
return directoryEntryFileUrl;
|
|
7319
|
-
}
|
|
7320
|
-
if (!specifierUsesTrailingSlash) {
|
|
7321
|
-
// the trailing slash is what tells a directory apart from a route:
|
|
7322
|
-
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
7323
|
-
// even when "join/" exists in the source files.
|
|
7324
|
-
// Without this a source directory would shadow the route having
|
|
7325
|
-
// the same name and the SPA would be unreachable in dev while
|
|
7326
|
-
// being perfectly fine once built
|
|
7327
|
-
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
7328
|
-
if (spaFallbackUrl) {
|
|
7329
|
-
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
7330
|
-
nullIfNotFound: true,
|
|
7331
|
-
});
|
|
7332
|
-
return spaFallbackUrl;
|
|
7333
|
-
}
|
|
7334
|
-
}
|
|
7335
|
-
}
|
|
7336
|
-
}
|
|
7337
|
-
if (!fsStat) {
|
|
7338
|
-
return null;
|
|
7339
|
-
}
|
|
7340
|
-
const urlBeforeSymlinkResolution = urlObject.href;
|
|
7341
|
-
if (preserveSymlinks) {
|
|
7342
|
-
return `${urlBeforeSymlinkResolution}${search}${hash}`;
|
|
7343
|
-
}
|
|
7344
|
-
const urlAfterSymlinkResolution = resolveSymlink(
|
|
7345
|
-
urlBeforeSymlinkResolution,
|
|
7346
|
-
);
|
|
7347
|
-
if (urlAfterSymlinkResolution !== urlBeforeSymlinkResolution) {
|
|
7348
|
-
reference.leadsToASymlink = true;
|
|
7349
|
-
// reference.baseUrl = urlBeforeSymlinkResolution;
|
|
7350
|
-
}
|
|
7351
|
-
const resolvedUrl = `${urlAfterSymlinkResolution}${search}${hash}`;
|
|
7352
|
-
return resolvedUrl;
|
|
7353
|
-
},
|
|
7354
|
-
};
|
|
7355
|
-
};
|
|
7356
|
-
|
|
7357
|
-
const applyFsStatEffectsOnUrlObject = (urlObject, fsStat) => {
|
|
7358
|
-
if (!fsStat) {
|
|
7359
|
-
return;
|
|
7360
|
-
}
|
|
7361
|
-
const { pathname } = urlObject;
|
|
7362
|
-
const pathnameUsesTrailingSlash = pathname.endsWith("/");
|
|
7363
|
-
// force trailing slash on directories
|
|
7364
|
-
if (fsStat.isDirectory()) {
|
|
7365
|
-
if (!pathnameUsesTrailingSlash) {
|
|
7366
|
-
urlObject.pathname = `${pathname}/`;
|
|
7367
|
-
}
|
|
7368
|
-
} else if (pathnameUsesTrailingSlash) {
|
|
7369
|
-
// otherwise remove trailing slash if any
|
|
7370
|
-
// a warning here? (because it's strange to reference a file with a trailing slash)
|
|
7371
|
-
urlObject.pathname = pathname.slice(0, -1);
|
|
7372
|
-
}
|
|
7373
|
-
};
|
|
7374
|
-
|
|
7375
|
-
const resolveSymlink = (fileUrl) => {
|
|
7376
|
-
const urlObject = new URL(fileUrl);
|
|
7377
|
-
const realpath = realpathSync(urlObject);
|
|
7378
|
-
const realUrlObject = pathToFileURL(realpath);
|
|
7379
|
-
if (urlObject.pathname.endsWith("/")) {
|
|
7380
|
-
realUrlObject.pathname += `/`;
|
|
7381
|
-
}
|
|
7382
|
-
return realUrlObject.href;
|
|
7383
|
-
};
|
|
7384
|
-
|
|
7385
|
-
const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
7386
|
-
const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
|
|
7387
|
-
if (existsSync(indexHtmlFileUrl)) {
|
|
7388
|
-
return indexHtmlFileUrl.href;
|
|
7389
|
-
}
|
|
7390
|
-
const filename = urlToFilename(directoryUrl);
|
|
7391
|
-
const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
|
|
7392
|
-
if (existsSync(htmlFileUrlCandidate)) {
|
|
7393
|
-
return htmlFileUrlCandidate.href;
|
|
7394
|
-
}
|
|
7395
|
-
return null;
|
|
7396
|
-
};
|
|
7397
|
-
const getSpaFallbackUrl = (reference) => {
|
|
7398
|
-
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
7399
|
-
reference.ownerUrlInfo.context;
|
|
7400
|
-
if (!requestedUrl) {
|
|
7401
|
-
// the SPA fallback answers a request; during build there is none
|
|
7402
|
-
return null;
|
|
7403
|
-
}
|
|
7404
|
-
const closestHtmlRootFile = getClosestHtmlRootFile(
|
|
7405
|
-
requestedUrl,
|
|
7406
|
-
rootDirectoryUrl,
|
|
7407
|
-
);
|
|
7408
|
-
if (closestHtmlRootFile) {
|
|
7409
|
-
return closestHtmlRootFile;
|
|
7410
|
-
}
|
|
7411
|
-
return String(new URL(mainFilePath, rootDirectoryUrl));
|
|
7412
|
-
};
|
|
7413
|
-
const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
|
|
7414
|
-
let directoryUrl = new URL("./", requestedUrl);
|
|
7415
|
-
while (true) {
|
|
7416
|
-
const directoryEntryFileUrl = getDirectoryEntryFileUrl(directoryUrl);
|
|
7417
|
-
if (directoryEntryFileUrl) {
|
|
7418
|
-
return directoryEntryFileUrl;
|
|
7419
|
-
}
|
|
7420
|
-
if (!urlIsOrIsInsideOf(directoryUrl, serverRootDirectoryUrl)) {
|
|
7421
|
-
return null;
|
|
7422
|
-
}
|
|
7423
|
-
directoryUrl = new URL("../", directoryUrl);
|
|
7424
|
-
}
|
|
7425
|
-
};
|
|
7426
|
-
|
|
7427
7493
|
const directoryContentMagicName = "...";
|
|
7428
7494
|
|
|
7429
7495
|
const jsenvPluginProtocolFile = ({
|
|
@@ -7577,7 +7643,16 @@ const jsenvPluginProtocolFile = ({
|
|
|
7577
7643
|
}
|
|
7578
7644
|
const serveFile = (url) => {
|
|
7579
7645
|
const contentType = CONTENT_TYPE.fromUrlExtension(url);
|
|
7580
|
-
const
|
|
7646
|
+
const urlObject = new URL(url);
|
|
7647
|
+
// taken before the read: a write landing in between moves the
|
|
7648
|
+
// stat past this one, so the dev server sees the content as
|
|
7649
|
+
// outdated (see isValid in the dev server) rather than the reverse
|
|
7650
|
+
const fileStat = statSync(urlObject);
|
|
7651
|
+
const fileBuffer = readFileSync(urlObject);
|
|
7652
|
+
urlInfo.data.fileStat = {
|
|
7653
|
+
mtimeMs: fileStat.mtimeMs,
|
|
7654
|
+
size: fileStat.size,
|
|
7655
|
+
};
|
|
7581
7656
|
const content = CONTENT_TYPE.isTextual(contentType)
|
|
7582
7657
|
? String(fileBuffer)
|
|
7583
7658
|
: fileBuffer;
|
|
@@ -8084,7 +8159,7 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
8084
8159
|
name: "jsenv:import_meta_scenario",
|
|
8085
8160
|
appliesDuring: "*",
|
|
8086
8161
|
transformUrlContent: {
|
|
8087
|
-
js_module:
|
|
8162
|
+
js_module: (urlInfo) => {
|
|
8088
8163
|
// Do not scan node modules for import.meta.dev/import.meta.build
|
|
8089
8164
|
// - node modules won't have this in their code
|
|
8090
8165
|
// - ;or should use other an other technic as this one won't be available
|
|
@@ -8100,40 +8175,42 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
8100
8175
|
) {
|
|
8101
8176
|
return null;
|
|
8102
8177
|
}
|
|
8103
|
-
const {
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8178
|
+
const importMetaScenarioNodes = { dev: [], build: [] };
|
|
8179
|
+
visitJsAst(urlInfo.contentAst, {
|
|
8180
|
+
MemberExpression: (node) => {
|
|
8181
|
+
const name = getImportMetaPropertyName(node);
|
|
8182
|
+
if (name === "dev" || name === "build") {
|
|
8183
|
+
importMetaScenarioNodes[name].push(node);
|
|
8184
|
+
}
|
|
8185
|
+
},
|
|
8109
8186
|
});
|
|
8110
|
-
const { dev
|
|
8187
|
+
const { dev, build } = importMetaScenarioNodes;
|
|
8111
8188
|
const replacements = [];
|
|
8112
|
-
const replace = (
|
|
8113
|
-
replacements.push({
|
|
8189
|
+
const replace = (node, value) => {
|
|
8190
|
+
replacements.push({ node, value });
|
|
8114
8191
|
};
|
|
8115
8192
|
if (urlInfo.context.build) {
|
|
8116
8193
|
// during build ensure replacement for tree-shaking
|
|
8117
|
-
dev.forEach((
|
|
8118
|
-
replace(
|
|
8194
|
+
dev.forEach((node) => {
|
|
8195
|
+
replace(node, "undefined");
|
|
8119
8196
|
});
|
|
8120
|
-
build.forEach((
|
|
8121
|
-
replace(
|
|
8197
|
+
build.forEach((node) => {
|
|
8198
|
+
replace(node, "true");
|
|
8122
8199
|
});
|
|
8123
8200
|
} else {
|
|
8124
8201
|
// during dev we can let "import.meta.build" untouched
|
|
8125
8202
|
// it will be evaluated to undefined.
|
|
8126
8203
|
// Moreover it can be surprising to see some "undefined"
|
|
8127
8204
|
// when source file contains "import.meta.build"
|
|
8128
|
-
dev.forEach((
|
|
8129
|
-
replace(
|
|
8205
|
+
dev.forEach((node) => {
|
|
8206
|
+
replace(node, "true");
|
|
8130
8207
|
});
|
|
8131
8208
|
}
|
|
8132
8209
|
const magicSource = createMagicSource(urlInfo.content);
|
|
8133
|
-
replacements.forEach(({
|
|
8210
|
+
replacements.forEach(({ node, value }) => {
|
|
8134
8211
|
magicSource.replace({
|
|
8135
|
-
start:
|
|
8136
|
-
end:
|
|
8212
|
+
start: node.start,
|
|
8213
|
+
end: node.end,
|
|
8137
8214
|
replacement: value,
|
|
8138
8215
|
});
|
|
8139
8216
|
});
|
|
@@ -8143,42 +8220,6 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
8143
8220
|
};
|
|
8144
8221
|
};
|
|
8145
8222
|
|
|
8146
|
-
const babelPluginMetadataImportMetaScenarios = () => {
|
|
8147
|
-
return {
|
|
8148
|
-
name: "metadata-import-meta-scenarios",
|
|
8149
|
-
visitor: {
|
|
8150
|
-
Program(programPath, state) {
|
|
8151
|
-
const importMetas = {};
|
|
8152
|
-
programPath.traverse({
|
|
8153
|
-
MemberExpression(path) {
|
|
8154
|
-
const { node } = path;
|
|
8155
|
-
const { object } = node;
|
|
8156
|
-
if (object.type !== "MetaProperty") {
|
|
8157
|
-
return;
|
|
8158
|
-
}
|
|
8159
|
-
const { property: objectProperty } = object;
|
|
8160
|
-
if (objectProperty.name !== "meta") {
|
|
8161
|
-
return;
|
|
8162
|
-
}
|
|
8163
|
-
const { property } = node;
|
|
8164
|
-
const { name } = property;
|
|
8165
|
-
const importMetaPaths = importMetas[name];
|
|
8166
|
-
if (importMetaPaths) {
|
|
8167
|
-
importMetaPaths.push(path);
|
|
8168
|
-
} else {
|
|
8169
|
-
importMetas[name] = [path];
|
|
8170
|
-
}
|
|
8171
|
-
},
|
|
8172
|
-
});
|
|
8173
|
-
state.file.metadata.importMetaScenarios = {
|
|
8174
|
-
dev: importMetas.dev,
|
|
8175
|
-
build: importMetas.build,
|
|
8176
|
-
};
|
|
8177
|
-
},
|
|
8178
|
-
},
|
|
8179
|
-
};
|
|
8180
|
-
};
|
|
8181
|
-
|
|
8182
8223
|
/*
|
|
8183
8224
|
* Source code can contain the following
|
|
8184
8225
|
* - __DEV__
|
|
@@ -8277,15 +8318,10 @@ const jsenvPluginImportMetaCss = () => {
|
|
|
8277
8318
|
if (!urlInfo.content.includes("import.meta.css")) {
|
|
8278
8319
|
return null;
|
|
8279
8320
|
}
|
|
8280
|
-
const
|
|
8281
|
-
|
|
8282
|
-
input: urlInfo.content,
|
|
8283
|
-
inputIsJsModule: true,
|
|
8284
|
-
inputUrl: urlInfo.originalUrl,
|
|
8285
|
-
outputUrl: urlInfo.generatedUrl,
|
|
8321
|
+
const importMetaCssNode = visitJsAstUntil(urlInfo.contentAst, {
|
|
8322
|
+
MemberExpression: (node) => getImportMetaPropertyName(node) === "css",
|
|
8286
8323
|
});
|
|
8287
|
-
|
|
8288
|
-
if (!usesImportMetaCss) {
|
|
8324
|
+
if (!importMetaCssNode) {
|
|
8289
8325
|
return null;
|
|
8290
8326
|
}
|
|
8291
8327
|
if (urlInfo.context.build) {
|
|
@@ -8306,6 +8342,8 @@ const jsenvPluginImportMetaCss = () => {
|
|
|
8306
8342
|
inputIsJsModule: true,
|
|
8307
8343
|
inputUrl: urlInfo.originalUrl,
|
|
8308
8344
|
outputUrl: urlInfo.generatedUrl,
|
|
8345
|
+
// the map would be dropped: the content sent back carries none
|
|
8346
|
+
options: { sourceMaps: false },
|
|
8309
8347
|
});
|
|
8310
8348
|
if (code === urlInfo.content) {
|
|
8311
8349
|
// all assignments were already in array form (pre-built file) — nothing to do
|
|
@@ -8365,37 +8403,6 @@ const babelPluginRewriteImportMetaCssAssignment = (
|
|
|
8365
8403
|
};
|
|
8366
8404
|
};
|
|
8367
8405
|
|
|
8368
|
-
const babelPluginMetadataUsesImportMetaCss = () => {
|
|
8369
|
-
return {
|
|
8370
|
-
name: "metadata-uses-import-meta-css",
|
|
8371
|
-
visitor: {
|
|
8372
|
-
Program(programPath, state) {
|
|
8373
|
-
let usesImportMetaCss = false;
|
|
8374
|
-
programPath.traverse({
|
|
8375
|
-
MemberExpression(path) {
|
|
8376
|
-
const { node } = path;
|
|
8377
|
-
const { object } = node;
|
|
8378
|
-
if (object.type !== "MetaProperty") {
|
|
8379
|
-
return;
|
|
8380
|
-
}
|
|
8381
|
-
const { property: objectProperty } = object;
|
|
8382
|
-
if (objectProperty.name !== "meta") {
|
|
8383
|
-
return;
|
|
8384
|
-
}
|
|
8385
|
-
const { property } = node;
|
|
8386
|
-
const { name } = property;
|
|
8387
|
-
if (name === "css") {
|
|
8388
|
-
usesImportMetaCss = true;
|
|
8389
|
-
path.stop();
|
|
8390
|
-
}
|
|
8391
|
-
},
|
|
8392
|
-
});
|
|
8393
|
-
state.file.metadata.usesImportMetaCss = usesImportMetaCss;
|
|
8394
|
-
},
|
|
8395
|
-
},
|
|
8396
|
-
};
|
|
8397
|
-
};
|
|
8398
|
-
|
|
8399
8406
|
const injectImportMetaCss = (
|
|
8400
8407
|
urlInfo,
|
|
8401
8408
|
{ content, importFrom, importName, importAs, hot },
|
|
@@ -8438,104 +8445,6 @@ ${importVariableName}(import.meta);
|
|
|
8438
8445
|
};
|
|
8439
8446
|
};
|
|
8440
8447
|
|
|
8441
|
-
// https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-stages-of-babel
|
|
8442
|
-
// https://github.com/cfware/babel-plugin-bundled-import-meta/blob/master/index.js
|
|
8443
|
-
// https://github.com/babel/babel/blob/f4edf62f6beeab8ae9f2b7f0b82f1b3b12a581af/packages/babel-helper-module-imports/src/index.js#L7
|
|
8444
|
-
|
|
8445
|
-
const babelPluginMetadataImportMetaHot = () => {
|
|
8446
|
-
return {
|
|
8447
|
-
name: "metadata-import-meta-hot",
|
|
8448
|
-
visitor: {
|
|
8449
|
-
Program(programPath, state) {
|
|
8450
|
-
Object.assign(
|
|
8451
|
-
state.file.metadata,
|
|
8452
|
-
collectImportMetaHotProperties(programPath),
|
|
8453
|
-
);
|
|
8454
|
-
},
|
|
8455
|
-
},
|
|
8456
|
-
};
|
|
8457
|
-
};
|
|
8458
|
-
const collectImportMetaHotProperties = (programPath) => {
|
|
8459
|
-
const importMetaHotPaths = [];
|
|
8460
|
-
let hotDecline = false;
|
|
8461
|
-
let hotAcceptSelf = false;
|
|
8462
|
-
let hotAcceptDependencies = [];
|
|
8463
|
-
programPath.traverse({
|
|
8464
|
-
MemberExpression(path) {
|
|
8465
|
-
const { node } = path;
|
|
8466
|
-
const { object } = node;
|
|
8467
|
-
if (object.type !== "MetaProperty") {
|
|
8468
|
-
return;
|
|
8469
|
-
}
|
|
8470
|
-
const { property: objectProperty } = object;
|
|
8471
|
-
if (objectProperty.name !== "meta") {
|
|
8472
|
-
return;
|
|
8473
|
-
}
|
|
8474
|
-
const { property } = node;
|
|
8475
|
-
const { name } = property;
|
|
8476
|
-
if (name === "hot") {
|
|
8477
|
-
importMetaHotPaths.push(path);
|
|
8478
|
-
}
|
|
8479
|
-
},
|
|
8480
|
-
CallExpression(path) {
|
|
8481
|
-
if (isImportMetaHotMethodCall(path, "accept")) {
|
|
8482
|
-
const callNode = path.node;
|
|
8483
|
-
const args = callNode.arguments;
|
|
8484
|
-
if (args.length === 0) {
|
|
8485
|
-
hotAcceptSelf = true;
|
|
8486
|
-
return;
|
|
8487
|
-
}
|
|
8488
|
-
const firstArg = args[0];
|
|
8489
|
-
if (firstArg.type === "StringLiteral") {
|
|
8490
|
-
hotAcceptDependencies = [
|
|
8491
|
-
{
|
|
8492
|
-
specifierPath: path.get("arguments")[0],
|
|
8493
|
-
},
|
|
8494
|
-
];
|
|
8495
|
-
return;
|
|
8496
|
-
}
|
|
8497
|
-
if (firstArg.type === "ArrayExpression") {
|
|
8498
|
-
const firstArgPath = path.get("arguments")[0];
|
|
8499
|
-
hotAcceptDependencies = firstArg.elements.map((arrayNode, index) => {
|
|
8500
|
-
if (arrayNode.type !== "StringLiteral") {
|
|
8501
|
-
throw new Error(
|
|
8502
|
-
`all array elements must be strings in "import.meta.hot.accept(array)"`,
|
|
8503
|
-
);
|
|
8504
|
-
}
|
|
8505
|
-
return {
|
|
8506
|
-
specifierPath: firstArgPath.get(String(index)),
|
|
8507
|
-
};
|
|
8508
|
-
});
|
|
8509
|
-
return;
|
|
8510
|
-
}
|
|
8511
|
-
// accept first arg can be "anything" such as
|
|
8512
|
-
// `const cb = () => {}; import.meta.accept(cb)`
|
|
8513
|
-
hotAcceptSelf = true;
|
|
8514
|
-
}
|
|
8515
|
-
if (isImportMetaHotMethodCall(path, "decline")) {
|
|
8516
|
-
hotDecline = true;
|
|
8517
|
-
}
|
|
8518
|
-
},
|
|
8519
|
-
});
|
|
8520
|
-
return {
|
|
8521
|
-
importMetaHotPaths,
|
|
8522
|
-
hotDecline,
|
|
8523
|
-
hotAcceptSelf,
|
|
8524
|
-
hotAcceptDependencies,
|
|
8525
|
-
};
|
|
8526
|
-
};
|
|
8527
|
-
const isImportMetaHotMethodCall = (path, methodName) => {
|
|
8528
|
-
const { property, object } = path.node.callee;
|
|
8529
|
-
return (
|
|
8530
|
-
property &&
|
|
8531
|
-
property.name === methodName &&
|
|
8532
|
-
object &&
|
|
8533
|
-
object.property &&
|
|
8534
|
-
object.property.name === "hot" &&
|
|
8535
|
-
object.object.type === "MetaProperty"
|
|
8536
|
-
);
|
|
8537
|
-
};
|
|
8538
|
-
|
|
8539
8448
|
// Some "smart" default applied to decide what should hot reload / fullreload:
|
|
8540
8449
|
// By default:
|
|
8541
8450
|
// - hot reload on <img src="./image.png" />
|
|
@@ -8683,6 +8592,75 @@ const htmlNodeCanHotReload = (node) => {
|
|
|
8683
8592
|
].includes(node.nodeName);
|
|
8684
8593
|
};
|
|
8685
8594
|
|
|
8595
|
+
const analyzeImportMetaHot = (ast) => {
|
|
8596
|
+
const importMetaHotNodes = [];
|
|
8597
|
+
let hotDecline = false;
|
|
8598
|
+
let hotAcceptSelf = false;
|
|
8599
|
+
let hotAcceptSpecifiers = [];
|
|
8600
|
+
visitJsAst(ast, {
|
|
8601
|
+
MemberExpression: (node) => {
|
|
8602
|
+
if (getImportMetaPropertyName(node) === "hot") {
|
|
8603
|
+
importMetaHotNodes.push(node);
|
|
8604
|
+
}
|
|
8605
|
+
},
|
|
8606
|
+
CallExpression: (node) => {
|
|
8607
|
+
const methodName = getImportMetaHotMethodName(node);
|
|
8608
|
+
if (methodName === "accept") {
|
|
8609
|
+
const args = node.arguments;
|
|
8610
|
+
if (args.length === 0) {
|
|
8611
|
+
hotAcceptSelf = true;
|
|
8612
|
+
return;
|
|
8613
|
+
}
|
|
8614
|
+
const [firstArg] = args;
|
|
8615
|
+
if (isStringLiteral(firstArg)) {
|
|
8616
|
+
hotAcceptSpecifiers = [firstArg.value];
|
|
8617
|
+
return;
|
|
8618
|
+
}
|
|
8619
|
+
if (firstArg.type === "ArrayExpression") {
|
|
8620
|
+
hotAcceptSpecifiers = firstArg.elements.map((element) => {
|
|
8621
|
+
if (!isStringLiteral(element)) {
|
|
8622
|
+
throw new Error(
|
|
8623
|
+
`all array elements must be strings in "import.meta.hot.accept(array)"`,
|
|
8624
|
+
);
|
|
8625
|
+
}
|
|
8626
|
+
return element.value;
|
|
8627
|
+
});
|
|
8628
|
+
return;
|
|
8629
|
+
}
|
|
8630
|
+
// accept first arg can be "anything" such as
|
|
8631
|
+
// `const cb = () => {}; import.meta.hot.accept(cb)`
|
|
8632
|
+
hotAcceptSelf = true;
|
|
8633
|
+
return;
|
|
8634
|
+
}
|
|
8635
|
+
if (methodName === "decline") {
|
|
8636
|
+
hotDecline = true;
|
|
8637
|
+
}
|
|
8638
|
+
},
|
|
8639
|
+
});
|
|
8640
|
+
return {
|
|
8641
|
+
importMetaHotNodes,
|
|
8642
|
+
hotDecline,
|
|
8643
|
+
hotAcceptSelf,
|
|
8644
|
+
hotAcceptSpecifiers,
|
|
8645
|
+
};
|
|
8646
|
+
};
|
|
8647
|
+
|
|
8648
|
+
// "import.meta.hot.<method>(...)"
|
|
8649
|
+
const getImportMetaHotMethodName = (callNode) => {
|
|
8650
|
+
const { callee } = callNode;
|
|
8651
|
+
if (callee.type !== "MemberExpression" || callee.computed) {
|
|
8652
|
+
return null;
|
|
8653
|
+
}
|
|
8654
|
+
if (getImportMetaPropertyName(callee.object) !== "hot") {
|
|
8655
|
+
return null;
|
|
8656
|
+
}
|
|
8657
|
+
return callee.property.name;
|
|
8658
|
+
};
|
|
8659
|
+
|
|
8660
|
+
const isStringLiteral = (node) => {
|
|
8661
|
+
return node.type === "Literal" && typeof node.value === "string";
|
|
8662
|
+
};
|
|
8663
|
+
|
|
8686
8664
|
const jsenvPluginImportMetaHot = () => {
|
|
8687
8665
|
const importMetaHotClientFileUrl = import.meta
|
|
8688
8666
|
.resolve("../client/import_meta_hot/import_meta_hot.js");
|
|
@@ -8731,7 +8709,7 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
8731
8709
|
cssUrlInfo.data.hotAcceptSelf = false;
|
|
8732
8710
|
cssUrlInfo.data.hotAcceptDependencies = [];
|
|
8733
8711
|
},
|
|
8734
|
-
js_module:
|
|
8712
|
+
js_module: (urlInfo) => {
|
|
8735
8713
|
// Do not scan node modules for import.meta.hot
|
|
8736
8714
|
// - unlikely to be there
|
|
8737
8715
|
// - we don't watch node modules (too expensive)
|
|
@@ -8743,27 +8721,22 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
8743
8721
|
if (!urlInfo.content.includes("import.meta.hot")) {
|
|
8744
8722
|
return null;
|
|
8745
8723
|
}
|
|
8746
|
-
const { metadata } = await applyBabelPlugins({
|
|
8747
|
-
babelPlugins: [babelPluginMetadataImportMetaHot],
|
|
8748
|
-
input: urlInfo.content,
|
|
8749
|
-
inputIsJsModule: true,
|
|
8750
|
-
inputUrl: urlInfo.originalUrl,
|
|
8751
|
-
outputUrl: urlInfo.generatedUrl,
|
|
8752
|
-
});
|
|
8753
8724
|
const {
|
|
8754
|
-
|
|
8725
|
+
importMetaHotNodes,
|
|
8755
8726
|
hotDecline,
|
|
8756
8727
|
hotAcceptSelf,
|
|
8757
|
-
|
|
8758
|
-
} =
|
|
8728
|
+
hotAcceptSpecifiers,
|
|
8729
|
+
} = analyzeImportMetaHot(urlInfo.contentAst);
|
|
8759
8730
|
urlInfo.data.hotDecline = hotDecline;
|
|
8760
8731
|
urlInfo.data.hotAcceptSelf = hotAcceptSelf;
|
|
8761
|
-
urlInfo.data.hotAcceptDependencies =
|
|
8762
|
-
|
|
8732
|
+
urlInfo.data.hotAcceptDependencies = hotAcceptSpecifiers.map(
|
|
8733
|
+
(specifier) => resolveHotAcceptSpecifier(urlInfo, specifier),
|
|
8734
|
+
);
|
|
8735
|
+
if (importMetaHotNodes.length === 0) {
|
|
8763
8736
|
return null;
|
|
8764
8737
|
}
|
|
8765
8738
|
if (urlInfo.context.build) {
|
|
8766
|
-
return removeImportMetaHots(urlInfo,
|
|
8739
|
+
return removeImportMetaHots(urlInfo, importMetaHotNodes);
|
|
8767
8740
|
}
|
|
8768
8741
|
return injectImportMetaHot(urlInfo, importMetaHotClientFileUrl);
|
|
8769
8742
|
},
|
|
@@ -8771,12 +8744,32 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
8771
8744
|
};
|
|
8772
8745
|
};
|
|
8773
8746
|
|
|
8774
|
-
|
|
8747
|
+
// The specifier given to import.meta.hot.accept() is one the file imports,
|
|
8748
|
+
// so its url is known from that import (autoreload compares urls). A
|
|
8749
|
+
// specifier the file does not import accepts nothing: it is kept as a plain
|
|
8750
|
+
// url, never resolved as a dependency (that could throw on a bare specifier).
|
|
8751
|
+
const resolveHotAcceptSpecifier = (urlInfo, specifier) => {
|
|
8752
|
+
for (const referenceToOther of urlInfo.referenceToOthersSet) {
|
|
8753
|
+
if (
|
|
8754
|
+
referenceToOther.type === "js_import" &&
|
|
8755
|
+
referenceToOther.specifier === specifier
|
|
8756
|
+
) {
|
|
8757
|
+
return referenceToOther.url;
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
try {
|
|
8761
|
+
return new URL(specifier, urlInfo.url).href;
|
|
8762
|
+
} catch {
|
|
8763
|
+
return specifier;
|
|
8764
|
+
}
|
|
8765
|
+
};
|
|
8766
|
+
|
|
8767
|
+
const removeImportMetaHots = (urlInfo, importMetaHotNodes) => {
|
|
8775
8768
|
const magicSource = createMagicSource(urlInfo.content);
|
|
8776
|
-
|
|
8769
|
+
importMetaHotNodes.forEach((node) => {
|
|
8777
8770
|
magicSource.replace({
|
|
8778
|
-
start:
|
|
8779
|
-
end:
|
|
8771
|
+
start: node.start,
|
|
8772
|
+
end: node.end,
|
|
8780
8773
|
replacement: "undefined",
|
|
8781
8774
|
});
|
|
8782
8775
|
});
|
|
@@ -9305,6 +9298,132 @@ const jsenvPluginAutoreload = ({
|
|
|
9305
9298
|
];
|
|
9306
9299
|
};
|
|
9307
9300
|
|
|
9301
|
+
/*
|
|
9302
|
+
* Tells the browser the static import graph of a page's module scripts, as
|
|
9303
|
+
* `Link: <url>; rel=modulepreload` response headers, so it can fetch every
|
|
9304
|
+
* module as soon as the page's headers arrive, without waiting to discover
|
|
9305
|
+
* them level by level (each level costing a round trip and, the first time,
|
|
9306
|
+
* a cook).
|
|
9307
|
+
*
|
|
9308
|
+
* Only what the graph already knows is listed: a module never cooked has no
|
|
9309
|
+
* known imports. The header is computed at each response from the graph as it
|
|
9310
|
+
* is then: a page gets its full list from its second load on, the first load
|
|
9311
|
+
* keeps the plain waterfall.
|
|
9312
|
+
*
|
|
9313
|
+
* Headers rather than <link> elements written into the page: the page content
|
|
9314
|
+
* stays what it is (served from memory, revalidated by etag) and the list can
|
|
9315
|
+
* grow without cooking the page again. And not references either: a reference
|
|
9316
|
+
* from the html to every module would make the html a dependent of each of
|
|
9317
|
+
* them, and a hot update propagating up the importers would reach the html
|
|
9318
|
+
* (which declines) instead of stopping at the module accepting it.
|
|
9319
|
+
*
|
|
9320
|
+
* OFF BY DEFAULT, to be enabled once the dev server speaks http/2 or http/3.
|
|
9321
|
+
* Over http/1.1 it makes pages slower — measured on a 500 modules page: first
|
|
9322
|
+
* paint 32ms -> 868ms, load 626ms -> 985ms. Knowing the urls earlier gives the
|
|
9323
|
+
* browser no extra resource: what it has is 6 connections per origin, and
|
|
9324
|
+
* every request goes through them in the order it was learned.
|
|
9325
|
+
* - Without the header, requests come in execution order: the parser asks
|
|
9326
|
+
* for the render-blocking supervisor script first and gets a connection at
|
|
9327
|
+
* once; transfer and execution overlap, a module executing and discovering
|
|
9328
|
+
* its imports while the others download. On localhost the server answers
|
|
9329
|
+
* from memory in 0.1ms, so the 6 connections are busy from the start.
|
|
9330
|
+
* - With the header, hundreds of requests take the 6 connections before the
|
|
9331
|
+
* parser asks for the supervisor script, which then waits for a free
|
|
9332
|
+
* connection behind 1MB+ transfers (520ms measured, 2s with 20ms of RTT).
|
|
9333
|
+
* Meanwhile nothing renders and nothing executes: transfer, then execution,
|
|
9334
|
+
* serialized. http/1.1 cannot reprioritize a queued request, and a
|
|
9335
|
+
* modulepreload has the same priority as a script.
|
|
9336
|
+
* - And there is nothing to hide: the discovery latency a preload removes is
|
|
9337
|
+
* ~1ms per graph level on localhost; with a real RTT a wide graph (more
|
|
9338
|
+
* than 6 modules pending at any time) keeps the 6 connections saturated
|
|
9339
|
+
* anyway, and the load takes requests / 6 * RTT whatever the order.
|
|
9340
|
+
* It pays off for a deep and narrow graph (connections idle, waiting for a
|
|
9341
|
+
* response to learn the next level), and over http/2 or http/3: no
|
|
9342
|
+
* connection limit, and the browser prioritizes the blocking script.
|
|
9343
|
+
*/
|
|
9344
|
+
|
|
9345
|
+
|
|
9346
|
+
const jsenvPluginModulepreload = () => {
|
|
9347
|
+
return {
|
|
9348
|
+
name: "jsenv:modulepreload",
|
|
9349
|
+
appliesDuring: "dev",
|
|
9350
|
+
augmentResponse: ({ urlInfo }) => {
|
|
9351
|
+
if (urlInfo.type !== "html") {
|
|
9352
|
+
return null;
|
|
9353
|
+
}
|
|
9354
|
+
const hrefs = collectStaticImportHrefs(urlInfo);
|
|
9355
|
+
if (hrefs.length === 0) {
|
|
9356
|
+
return null;
|
|
9357
|
+
}
|
|
9358
|
+
return {
|
|
9359
|
+
headers: {
|
|
9360
|
+
link: hrefs.map((href) => `<${href}>; rel=modulepreload`).join(", "),
|
|
9361
|
+
},
|
|
9362
|
+
};
|
|
9363
|
+
},
|
|
9364
|
+
};
|
|
9365
|
+
};
|
|
9366
|
+
|
|
9367
|
+
const collectStaticImportHrefs = (htmlUrlInfo) => {
|
|
9368
|
+
const hrefSet = new Set();
|
|
9369
|
+
const visitedSet = new Set();
|
|
9370
|
+
const addHref = (reference) => {
|
|
9371
|
+
const specifier = urlSpecifierEncoding.decode(reference);
|
|
9372
|
+
if (typeof specifier !== "string") {
|
|
9373
|
+
return;
|
|
9374
|
+
}
|
|
9375
|
+
// "?hot" belongs to the request that cooked the importer, the page loading
|
|
9376
|
+
// now imports the url without it
|
|
9377
|
+
hrefSet.add(injectQueryParamsIntoSpecifier(specifier, { hot: undefined }));
|
|
9378
|
+
};
|
|
9379
|
+
const visitJsModule = (jsModuleUrlInfo) => {
|
|
9380
|
+
if (visitedSet.has(jsModuleUrlInfo)) {
|
|
9381
|
+
return;
|
|
9382
|
+
}
|
|
9383
|
+
visitedSet.add(jsModuleUrlInfo);
|
|
9384
|
+
for (const reference of jsModuleUrlInfo.referenceToOthersSet) {
|
|
9385
|
+
if (
|
|
9386
|
+
reference.type !== "js_import" ||
|
|
9387
|
+
reference.isWeak ||
|
|
9388
|
+
reference.isImplicit ||
|
|
9389
|
+
!STATIC_IMPORT_SUBTYPES.includes(reference.subtype) ||
|
|
9390
|
+
!reference.url.startsWith("file:")
|
|
9391
|
+
) {
|
|
9392
|
+
continue;
|
|
9393
|
+
}
|
|
9394
|
+
const importedUrlInfo = reference.urlInfo;
|
|
9395
|
+
// never cooked: its content, hence its own imports, are unknown
|
|
9396
|
+
if (importedUrlInfo.type !== "js_module") {
|
|
9397
|
+
continue;
|
|
9398
|
+
}
|
|
9399
|
+
addHref(reference);
|
|
9400
|
+
visitJsModule(importedUrlInfo);
|
|
9401
|
+
}
|
|
9402
|
+
};
|
|
9403
|
+
for (const reference of htmlUrlInfo.referenceToOthersSet) {
|
|
9404
|
+
if (
|
|
9405
|
+
reference.type !== "script" ||
|
|
9406
|
+
reference.expectedType !== "js_module" ||
|
|
9407
|
+
// the scripts jsenv adds to every page are few, small and in memory
|
|
9408
|
+
reference.injected ||
|
|
9409
|
+
reference.isWeak
|
|
9410
|
+
) {
|
|
9411
|
+
continue;
|
|
9412
|
+
}
|
|
9413
|
+
const scriptUrlInfo = reference.urlInfo;
|
|
9414
|
+
if (scriptUrlInfo.type !== "js_module") {
|
|
9415
|
+
continue;
|
|
9416
|
+
}
|
|
9417
|
+
if (!reference.isInline && reference.url.startsWith("file:")) {
|
|
9418
|
+
addHref(reference);
|
|
9419
|
+
}
|
|
9420
|
+
visitJsModule(scriptUrlInfo);
|
|
9421
|
+
}
|
|
9422
|
+
return Array.from(hrefSet);
|
|
9423
|
+
};
|
|
9424
|
+
|
|
9425
|
+
const STATIC_IMPORT_SUBTYPES = ["import_static", "export_named", "export_all"];
|
|
9426
|
+
|
|
9308
9427
|
/*
|
|
9309
9428
|
* Tells the browser about the dependencies declared in package.json that
|
|
9310
9429
|
* node_modules does not match, so the page can say it is running with something
|
|
@@ -9946,6 +10065,7 @@ const getCorePlugins = ({
|
|
|
9946
10065
|
|
|
9947
10066
|
clientAutoreload,
|
|
9948
10067
|
clientAutoreloadOnServerRestart,
|
|
10068
|
+
modulepreload = false,
|
|
9949
10069
|
dependencyStatus,
|
|
9950
10070
|
cacheControl,
|
|
9951
10071
|
scenarioPlaceholders = true,
|
|
@@ -10047,6 +10167,7 @@ const getCorePlugins = ({
|
|
|
10047
10167
|
...(clientAutoreload && clientAutoreload.enabled
|
|
10048
10168
|
? [jsenvPluginAutoreload(clientAutoreload)]
|
|
10049
10169
|
: []),
|
|
10170
|
+
...(modulepreload ? [jsenvPluginModulepreload()] : []),
|
|
10050
10171
|
...(dependencyStatus
|
|
10051
10172
|
? [jsenvPluginDependencyStatus(dependencyStatus)]
|
|
10052
10173
|
: []),
|
|
@@ -10061,11 +10182,12 @@ const getCorePlugins = ({
|
|
|
10061
10182
|
];
|
|
10062
10183
|
};
|
|
10063
10184
|
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10185
|
+
// a phase can be a few milliseconds long (refine, mostly): shown as such
|
|
10186
|
+
const humanizePhaseDuration = (ms) => {
|
|
10187
|
+
if (ms < 1000) {
|
|
10188
|
+
return `${Math.round(ms)}ms`;
|
|
10189
|
+
}
|
|
10190
|
+
return humanizeDuration(ms, { short: true });
|
|
10069
10191
|
};
|
|
10070
10192
|
const humanizeProcessMemoryUsage = (value) => {
|
|
10071
10193
|
return humanizeMemory(value, { short: true, decimals: 0 });
|
|
@@ -10074,7 +10196,8 @@ const renderBuildDoneLog = ({
|
|
|
10074
10196
|
entryPointArray,
|
|
10075
10197
|
duration,
|
|
10076
10198
|
buildFileContents,
|
|
10077
|
-
|
|
10199
|
+
cpuUsage,
|
|
10200
|
+
phaseTimings,
|
|
10078
10201
|
processMemoryUsage,
|
|
10079
10202
|
}) => {
|
|
10080
10203
|
const buildContentReport = createBuildContentReport(buildFileContents);
|
|
@@ -10089,15 +10212,10 @@ const renderBuildDoneLog = ({
|
|
|
10089
10212
|
title = `build done (${entryPointCount} entry points)`;
|
|
10090
10213
|
}
|
|
10091
10214
|
|
|
10092
|
-
// cpu
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
med: humanizeProcessCpuUsage(processCpuUsage.median),
|
|
10097
|
-
min: humanizeProcessCpuUsage(processCpuUsage.min),
|
|
10098
|
-
max: humanizeProcessCpuUsage(processCpuUsage.max),
|
|
10099
|
-
});
|
|
10100
|
-
lines.push(cpuUsageLine);
|
|
10215
|
+
// cpu: the process cpu time over the build duration, 100% being one core
|
|
10216
|
+
// busy the whole time (more when a native module works on other threads)
|
|
10217
|
+
const cpuMs = (cpuUsage.user + cpuUsage.system) / 1000;
|
|
10218
|
+
lines.push(`cpu: ${Math.round((cpuMs / duration) * 100)}%`);
|
|
10101
10219
|
|
|
10102
10220
|
// memory usage
|
|
10103
10221
|
let memoryUsageLine = "memory: ";
|
|
@@ -10112,6 +10230,16 @@ const renderBuildDoneLog = ({
|
|
|
10112
10230
|
// duration
|
|
10113
10231
|
let durationLine = `duration: `;
|
|
10114
10232
|
durationLine += humanizeDuration(duration, { short: true });
|
|
10233
|
+
if (phaseTimings) {
|
|
10234
|
+
durationLine += renderDetails(
|
|
10235
|
+
Object.fromEntries(
|
|
10236
|
+
Object.keys(phaseTimings).map((phaseName) => [
|
|
10237
|
+
phaseName,
|
|
10238
|
+
humanizePhaseDuration(phaseTimings[phaseName]),
|
|
10239
|
+
]),
|
|
10240
|
+
),
|
|
10241
|
+
);
|
|
10242
|
+
}
|
|
10115
10243
|
lines.push(durationLine);
|
|
10116
10244
|
|
|
10117
10245
|
// content
|
|
@@ -10302,6 +10430,8 @@ const createBuildContentReport = (buildFileContents) => {
|
|
|
10302
10430
|
};
|
|
10303
10431
|
|
|
10304
10432
|
const determineCategory = (buildRelativeUrl) => {
|
|
10433
|
+
// a versioned url ("main.js?v=abc") is categorized by its file
|
|
10434
|
+
buildRelativeUrl = buildRelativeUrl.replace(/[?#].*$/, "");
|
|
10305
10435
|
if (buildRelativeUrl.endsWith(".map")) {
|
|
10306
10436
|
return "sourcemap";
|
|
10307
10437
|
}
|
|
@@ -11228,7 +11358,7 @@ const createBuildSpecifierManager = ({
|
|
|
11228
11358
|
}
|
|
11229
11359
|
if (mayUsePlaceholder(urlInfo)) {
|
|
11230
11360
|
const contentBeforeReplace = urlInfo.content;
|
|
11231
|
-
const
|
|
11361
|
+
const replaceResult = placeholderAPI.replaceAll(
|
|
11232
11362
|
contentBeforeReplace,
|
|
11233
11363
|
(placeholder) => {
|
|
11234
11364
|
const reference = placeholderToReferenceMap.get(placeholder);
|
|
@@ -11236,7 +11366,7 @@ const createBuildSpecifierManager = ({
|
|
|
11236
11366
|
return value;
|
|
11237
11367
|
},
|
|
11238
11368
|
);
|
|
11239
|
-
urlInfo.mutateContent(
|
|
11369
|
+
urlInfo.mutateContent(replaceResult);
|
|
11240
11370
|
}
|
|
11241
11371
|
},
|
|
11242
11372
|
);
|
|
@@ -12477,13 +12607,12 @@ entryPoints: {
|
|
|
12477
12607
|
});
|
|
12478
12608
|
}
|
|
12479
12609
|
|
|
12480
|
-
|
|
12481
|
-
|
|
12482
|
-
|
|
12610
|
+
// cpu is not sampled: cooking, rollup and terser hold the event loop for
|
|
12611
|
+
// seconds at a time, a sampler only runs in the gaps and would report the
|
|
12612
|
+
// idle time. What the build cost is process.cpuUsage() over its duration.
|
|
12483
12613
|
const memoryMonitoring = startMonitoringMemoryUsage();
|
|
12484
12614
|
const [processMemoryUsageMonitoring] = memoryMonitoring;
|
|
12485
12615
|
const interval = setInterval(() => {
|
|
12486
|
-
processCpuUsageMonitoring.measure();
|
|
12487
12616
|
processMemoryUsageMonitoring.measure();
|
|
12488
12617
|
}, 500).unref();
|
|
12489
12618
|
operation.addEndCallback(() => {
|
|
@@ -12553,19 +12682,24 @@ entryPoints: {
|
|
|
12553
12682
|
content += "\n";
|
|
12554
12683
|
return content;
|
|
12555
12684
|
};
|
|
12556
|
-
const renderBuildEndLog = ({
|
|
12685
|
+
const renderBuildEndLog = ({
|
|
12686
|
+
duration,
|
|
12687
|
+
buildFileContents,
|
|
12688
|
+
cpuUsage,
|
|
12689
|
+
phaseTimings,
|
|
12690
|
+
}) => {
|
|
12557
12691
|
// tell how many files are generated in build directory
|
|
12558
12692
|
// tell the repartition?
|
|
12559
12693
|
// this is not really useful for single build right?
|
|
12560
12694
|
|
|
12561
|
-
processCpuUsageMonitoring.end();
|
|
12562
12695
|
processMemoryUsageMonitoring.end();
|
|
12563
12696
|
|
|
12564
12697
|
return renderBuildDoneLog({
|
|
12565
12698
|
entryPointArray,
|
|
12566
12699
|
duration,
|
|
12567
12700
|
buildFileContents,
|
|
12568
|
-
|
|
12701
|
+
cpuUsage,
|
|
12702
|
+
phaseTimings,
|
|
12569
12703
|
processMemoryUsage: processMemoryUsageMonitoring.info,
|
|
12570
12704
|
});
|
|
12571
12705
|
};
|
|
@@ -12640,13 +12774,25 @@ entryPoints: {
|
|
|
12640
12774
|
}, renderDynamicLog());
|
|
12641
12775
|
};
|
|
12642
12776
|
},
|
|
12643
|
-
onBuildEnd: ({
|
|
12777
|
+
onBuildEnd: ({
|
|
12778
|
+
buildFileContents,
|
|
12779
|
+
duration,
|
|
12780
|
+
cpuUsage,
|
|
12781
|
+
phaseTimings,
|
|
12782
|
+
}) => {
|
|
12644
12783
|
clearInterval(interval);
|
|
12645
12784
|
dynamicLog.update("");
|
|
12646
12785
|
dynamicLog.destroy();
|
|
12647
12786
|
dynamicLog = null;
|
|
12648
12787
|
logger.info("");
|
|
12649
|
-
logger.info(
|
|
12788
|
+
logger.info(
|
|
12789
|
+
renderBuildEndLog({
|
|
12790
|
+
duration,
|
|
12791
|
+
buildFileContents,
|
|
12792
|
+
cpuUsage,
|
|
12793
|
+
phaseTimings,
|
|
12794
|
+
}),
|
|
12795
|
+
);
|
|
12650
12796
|
},
|
|
12651
12797
|
};
|
|
12652
12798
|
};
|
|
@@ -12673,8 +12819,20 @@ entryPoints: {
|
|
|
12673
12819
|
);
|
|
12674
12820
|
};
|
|
12675
12821
|
},
|
|
12676
|
-
onBuildEnd: ({
|
|
12677
|
-
|
|
12822
|
+
onBuildEnd: ({
|
|
12823
|
+
buildFileContents,
|
|
12824
|
+
duration,
|
|
12825
|
+
cpuUsage,
|
|
12826
|
+
phaseTimings,
|
|
12827
|
+
}) => {
|
|
12828
|
+
logger.info(
|
|
12829
|
+
renderBuildEndLog({
|
|
12830
|
+
duration,
|
|
12831
|
+
buildFileContents,
|
|
12832
|
+
cpuUsage,
|
|
12833
|
+
phaseTimings,
|
|
12834
|
+
}),
|
|
12835
|
+
);
|
|
12678
12836
|
},
|
|
12679
12837
|
};
|
|
12680
12838
|
};
|
|
@@ -12723,6 +12881,7 @@ entryPoints: {
|
|
|
12723
12881
|
|
|
12724
12882
|
const runBuild = async ({ signal }) => {
|
|
12725
12883
|
const startDate = Date.now();
|
|
12884
|
+
const cpuUsageAtStart = process.cpuUsage();
|
|
12726
12885
|
const { onBuildEnd, onEntryPointBuildStart } = startBuildLogs();
|
|
12727
12886
|
|
|
12728
12887
|
const buildUrlsGenerator = createBuildUrlsGenerator({
|
|
@@ -12831,6 +12990,7 @@ entryPoints: {
|
|
|
12831
12990
|
entryBuildInfo.buildInlineContents = result.buildInlineContents;
|
|
12832
12991
|
entryBuildInfo.buildManifest = result.buildManifest;
|
|
12833
12992
|
entryBuildInfo.buildSideEffectFiles = result.buildSideEffectFiles;
|
|
12993
|
+
entryBuildInfo.phaseTimings = result.phaseTimings;
|
|
12834
12994
|
entryBuildInfo.duration = Date.now() - entryPointBuildStartMs;
|
|
12835
12995
|
onEntryPointBuildEnd();
|
|
12836
12996
|
})();
|
|
@@ -12935,11 +13095,19 @@ entryPoints: {
|
|
|
12935
13095
|
}
|
|
12936
13096
|
}
|
|
12937
13097
|
}
|
|
13098
|
+
// entry points are built concurrently, interleaved on the same thread:
|
|
13099
|
+
// their phases overlap and only a single one can be told apart
|
|
13100
|
+
const phaseTimings =
|
|
13101
|
+
entryBuildInfoMap.size === 1
|
|
13102
|
+
? entryBuildInfoMap.values().next().value.phaseTimings
|
|
13103
|
+
: null;
|
|
12938
13104
|
onBuildEnd({
|
|
12939
13105
|
buildFileContents,
|
|
12940
13106
|
buildInlineContents,
|
|
12941
13107
|
buildManifest,
|
|
12942
13108
|
duration: Date.now() - startDate,
|
|
13109
|
+
cpuUsage: process.cpuUsage(cpuUsageAtStart),
|
|
13110
|
+
phaseTimings,
|
|
12943
13111
|
});
|
|
12944
13112
|
return {
|
|
12945
13113
|
...(returnBuildInlineContents ? { buildInlineContents } : {}),
|
|
@@ -13303,6 +13471,14 @@ const prepareEntryPointBuild = async (
|
|
|
13303
13471
|
return {
|
|
13304
13472
|
entryReference,
|
|
13305
13473
|
buildEntryPoint: async ({ getOtherEntryBuildInfo }) => {
|
|
13474
|
+
const phaseTimings = {};
|
|
13475
|
+
let phaseStartMs = Date.now();
|
|
13476
|
+
const endPhase = (name) => {
|
|
13477
|
+
const nowMs = Date.now();
|
|
13478
|
+
phaseTimings[name] = nowMs - phaseStartMs;
|
|
13479
|
+
phaseStartMs = nowMs;
|
|
13480
|
+
};
|
|
13481
|
+
|
|
13306
13482
|
{
|
|
13307
13483
|
_getOtherEntryBuildInfo = getOtherEntryBuildInfo;
|
|
13308
13484
|
if (outDirectoryUrl) {
|
|
@@ -13310,6 +13486,7 @@ const prepareEntryPointBuild = async (
|
|
|
13310
13486
|
}
|
|
13311
13487
|
await rawRootUrlInfo.cookDependencies({ operation: buildOperation });
|
|
13312
13488
|
}
|
|
13489
|
+
endPhase("craft");
|
|
13313
13490
|
|
|
13314
13491
|
const finalKitchen = createKitchen({
|
|
13315
13492
|
name: "shape",
|
|
@@ -13522,6 +13699,8 @@ const prepareEntryPointBuild = async (
|
|
|
13522
13699
|
}
|
|
13523
13700
|
}
|
|
13524
13701
|
|
|
13702
|
+
endPhase("bundle");
|
|
13703
|
+
|
|
13525
13704
|
{
|
|
13526
13705
|
finalKitchen.context.buildStep = "shape";
|
|
13527
13706
|
if (outDirectoryUrl) {
|
|
@@ -13541,6 +13720,8 @@ const prepareEntryPointBuild = async (
|
|
|
13541
13720
|
});
|
|
13542
13721
|
}
|
|
13543
13722
|
|
|
13723
|
+
endPhase("shape");
|
|
13724
|
+
|
|
13544
13725
|
const buildSideEffectFiles = [];
|
|
13545
13726
|
{
|
|
13546
13727
|
finalKitchen.context.buildStep = "refine";
|
|
@@ -13648,6 +13829,8 @@ const prepareEntryPointBuild = async (
|
|
|
13648
13829
|
}
|
|
13649
13830
|
}
|
|
13650
13831
|
}
|
|
13832
|
+
endPhase("refine");
|
|
13833
|
+
|
|
13651
13834
|
const {
|
|
13652
13835
|
buildFileContents,
|
|
13653
13836
|
buildFileVersions,
|
|
@@ -13667,6 +13850,7 @@ const prepareEntryPointBuild = async (
|
|
|
13667
13850
|
buildInlineContents,
|
|
13668
13851
|
buildManifest,
|
|
13669
13852
|
buildSideEffectFiles,
|
|
13853
|
+
phaseTimings,
|
|
13670
13854
|
};
|
|
13671
13855
|
},
|
|
13672
13856
|
};
|