@jsenv/core 41.4.16 → 41.4.18
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 +573 -469
- package/dist/client/directory_listing/js/directory_listing.js +23 -2
- package/dist/start_dev_server/start_dev_server.js +780 -612
- package/package.json +7 -7
- 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/url_graph/references.js +30 -13
- package/src/kitchen/url_graph/url_graph.js +0 -1
- package/src/plugins/import_meta_css/jsenv_plugin_import_meta_css.js +8 -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/resolution_node_esm/node_esm_resolver.js +0 -5
- package/src/plugins/import_meta_hot/babel_plugin_metadata_import_meta_hot.js +0 -97
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler,
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, lstatSync,
|
|
3
|
-
import {
|
|
1
|
+
import { WebSocketResponse, pickContentType, ServerEvents, serverPluginErrorHandler, fetchDirectory, composeTwoResponses, serverPluginCORS, jsenvAccessControlAllowedHeaders, startServer } from "@jsenv/server";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, readdirSync, lstatSync, statSync } from "node:fs";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { urlToRelativeUrl, registerFileLifecycle, lookupPackageDirectory, readPackageAtOrNull, generateContentFrame, errorToHTML, DATA_URL, CONTENT_TYPE, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, urlToExtension, urlToBasename, applyNodeEsmResolution, URL_META, readCustomConditionsFromProcessArgs, urlIsOrIsInsideOf, collectFiles, registerDirectoryLifecycle, readEntryStatSync, applyFileSystemMagicResolution, getExtensionsToTry, urlToFilename, asUrlWithoutSearch, ensurePathnameTrailingSlash, compareFileUrls, setUrlExtension, createDetailedMessage, stringifyUrlSite, injectQueryParamsIntoSpecifier, isSpecifierForNodeBuiltin, injectQueryParams, urlToFileSystemPath, writeFileSync, moveUrl, ensureWindowsDriveLetter, validateResponseIntegrity, setUrlFilename, getCallerPosition, asSpecifierWithoutSearch, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, createLogger, normalizeUrl, ANSI, RUNTIME_COMPAT, formatError, assertAndNormalizeDirectoryUrl, createTaskLog } from "./jsenv_core_packages.js";
|
|
4
5
|
import { createPluginsController } from "@jsenv/server/src/plugins_controller.js";
|
|
5
|
-
import { parseHtml, injectJsenvScript, stringifyHtmlAst, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, applyBabelPlugins, analyzeLinkNode, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn } from "@jsenv/ast";
|
|
6
|
+
import { parseHtml, injectJsenvScript, stringifyHtmlAst, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, applyBabelPlugins, visitJsAst, getImportMetaPropertyName, visitJsAstUntil, analyzeLinkNode, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, generateUrlForInlineContent, parseJsWithAcorn } from "@jsenv/ast";
|
|
6
7
|
import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
|
|
7
8
|
import { jsenvPluginTranspilation } from "@jsenv/plugin-transpilation";
|
|
8
9
|
import { createMagicSource, composeTwoSourcemaps, generateSourcemapFileUrl, generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
|
|
9
|
-
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { bundleJsModules } from "@jsenv/plugin-bundling";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/plugins/filesystem/filesystem_error_to_response.js";
|
|
@@ -3129,11 +3129,6 @@ const addRelationshipWithPackageJson = ({
|
|
|
3129
3129
|
String(packageJsonContentAsBuffer),
|
|
3130
3130
|
);
|
|
3131
3131
|
}
|
|
3132
|
-
// Checked on disk at every validation rather than trusted to the watcher:
|
|
3133
|
-
// what this file decides (the package version, hence the ?v= importers
|
|
3134
|
-
// embed) ends up in the browser's immutable cache, so a request racing the
|
|
3135
|
-
// watcher must never be answered from a stale package.json.
|
|
3136
|
-
packageJsonReference.urlInfo.revalidateOnFileSystem = true;
|
|
3137
3132
|
};
|
|
3138
3133
|
|
|
3139
3134
|
const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|
|
@@ -3506,18 +3501,21 @@ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
|
|
|
3506
3501
|
const getDirectoryWatchPatterns = (
|
|
3507
3502
|
directoryUrl,
|
|
3508
3503
|
watchedDirectoryUrl,
|
|
3509
|
-
{ sourceFilesConfig },
|
|
3504
|
+
{ sourceFilesConfig, defaultPatterns = true },
|
|
3510
3505
|
) => {
|
|
3511
3506
|
const directoryUrlRelativeToWatchedDirectory = urlToRelativeUrl(
|
|
3512
3507
|
directoryUrl,
|
|
3513
3508
|
watchedDirectoryUrl,
|
|
3514
3509
|
);
|
|
3515
|
-
const watchPatterns = {
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3510
|
+
const watchPatterns = {};
|
|
3511
|
+
if (defaultPatterns) {
|
|
3512
|
+
Object.assign(watchPatterns, {
|
|
3513
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/*`]: true, // by default watch everything inside the source directory
|
|
3514
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*`]: false, // file starting with a dot -> do not watch
|
|
3515
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*/`]: false, // directory starting with a dot -> do not watch
|
|
3516
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/node_modules/`]: false, // node_modules directory -> do not watch
|
|
3517
|
+
});
|
|
3518
|
+
}
|
|
3521
3519
|
for (const key of Object.keys(sourceFilesConfig)) {
|
|
3522
3520
|
watchPatterns[`${directoryUrlRelativeToWatchedDirectory}${key}`] =
|
|
3523
3521
|
sourceFilesConfig[key];
|
|
@@ -3538,11 +3536,12 @@ const watchSourceFiles = (
|
|
|
3538
3536
|
// And jsenv should not consider these as source files and watch them (to not hurt performances)
|
|
3539
3537
|
const watchPatterns = {};
|
|
3540
3538
|
let watchedDirectoryUrl = "";
|
|
3541
|
-
const addDirectoryToWatch = (directoryUrl) => {
|
|
3539
|
+
const addDirectoryToWatch = (directoryUrl, { defaultPatterns } = {}) => {
|
|
3542
3540
|
Object.assign(
|
|
3543
3541
|
watchPatterns,
|
|
3544
3542
|
getDirectoryWatchPatterns(directoryUrl, watchedDirectoryUrl, {
|
|
3545
3543
|
sourceFilesConfig,
|
|
3544
|
+
defaultPatterns,
|
|
3546
3545
|
}),
|
|
3547
3546
|
);
|
|
3548
3547
|
};
|
|
@@ -3574,7 +3573,6 @@ const watchSourceFiles = (
|
|
|
3574
3573
|
},
|
|
3575
3574
|
},
|
|
3576
3575
|
);
|
|
3577
|
-
stopWatchingSourceFiles.watchPatterns = watchPatterns;
|
|
3578
3576
|
return stopWatchingSourceFiles;
|
|
3579
3577
|
};
|
|
3580
3578
|
|
|
@@ -3594,16 +3592,18 @@ const watchSourceFiles = (
|
|
|
3594
3592
|
}
|
|
3595
3593
|
watchedDirectoryUrl = packageDirectoryUrl;
|
|
3596
3594
|
for (const workspace of workspaces) {
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3595
|
+
const workspaceDirectoryUrl = workspace.endsWith("*")
|
|
3596
|
+
? new URL(workspace.slice(0, -1), packageDirectoryUrl)
|
|
3597
|
+
: new URL(workspace, packageDirectoryUrl);
|
|
3598
|
+
addDirectoryToWatch(workspaceDirectoryUrl, {
|
|
3599
|
+
// The source directory patterns already cover a workspace inside it.
|
|
3600
|
+
// Every pattern is tested against every file found while watching, so
|
|
3601
|
+
// the same patterns rooted at each workspace only multiply that work.
|
|
3602
|
+
defaultPatterns: !urlIsOrIsInsideOf(
|
|
3603
|
+
workspaceDirectoryUrl,
|
|
3604
|
+
sourceDirectoryUrl,
|
|
3605
|
+
),
|
|
3606
|
+
});
|
|
3607
3607
|
}
|
|
3608
3608
|
// we are updating the root directory
|
|
3609
3609
|
// we must make the patterns relative to source directory relative to the new root directory
|
|
@@ -3616,6 +3616,240 @@ const watchSourceFiles = (
|
|
|
3616
3616
|
return watch();
|
|
3617
3617
|
};
|
|
3618
3618
|
|
|
3619
|
+
const jsenvPluginFsRedirection = ({
|
|
3620
|
+
spa,
|
|
3621
|
+
directoryContentMagicName,
|
|
3622
|
+
magicExtensions = ["inherit", ".js"],
|
|
3623
|
+
magicDirectoryIndex = true,
|
|
3624
|
+
preserveSymlinks = false,
|
|
3625
|
+
}) => {
|
|
3626
|
+
return {
|
|
3627
|
+
name: "jsenv:fs_redirection",
|
|
3628
|
+
appliesDuring: "*",
|
|
3629
|
+
redirectReference: (reference) => {
|
|
3630
|
+
if (reference.url === "file:///") {
|
|
3631
|
+
return `ignore:file:///`;
|
|
3632
|
+
}
|
|
3633
|
+
if (reference.url === "file://") {
|
|
3634
|
+
return `ignore:file://`;
|
|
3635
|
+
}
|
|
3636
|
+
// ignore all new URL second arg
|
|
3637
|
+
if (reference.subtype === "new_url_second_arg") {
|
|
3638
|
+
if (reference.original) {
|
|
3639
|
+
return `ignore:${reference.original.specifier}`;
|
|
3640
|
+
}
|
|
3641
|
+
return `ignore:${reference.specifier}`;
|
|
3642
|
+
}
|
|
3643
|
+
// http, https, data, about, ...
|
|
3644
|
+
if (!reference.url.startsWith("file:")) {
|
|
3645
|
+
return null;
|
|
3646
|
+
}
|
|
3647
|
+
if (reference.original && !reference.original.url.startsWith("file:")) {
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
if (reference.isInline) {
|
|
3651
|
+
return null;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
if (
|
|
3655
|
+
reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
|
|
3656
|
+
) {
|
|
3657
|
+
const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
|
|
3658
|
+
const directoryUrl = new URL(
|
|
3659
|
+
reference.specifierPathname
|
|
3660
|
+
.replace(`/${directoryContentMagicName}`, "/")
|
|
3661
|
+
.slice(1),
|
|
3662
|
+
rootDirectoryUrl,
|
|
3663
|
+
).href;
|
|
3664
|
+
return directoryUrl;
|
|
3665
|
+
}
|
|
3666
|
+
// ignore "./" on new URL("./")
|
|
3667
|
+
// if (
|
|
3668
|
+
// reference.subtype === "new_url_first_arg" &&
|
|
3669
|
+
// reference.specifier === "./"
|
|
3670
|
+
// ) {
|
|
3671
|
+
// return `ignore:${reference.url}`;
|
|
3672
|
+
// }
|
|
3673
|
+
const urlObject = new URL(reference.url);
|
|
3674
|
+
let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
|
|
3675
|
+
reference.fsStat = fsStat;
|
|
3676
|
+
const { search, hash } = urlObject;
|
|
3677
|
+
urlObject.search = "";
|
|
3678
|
+
urlObject.hash = "";
|
|
3679
|
+
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
3680
|
+
// trailing slash on directories
|
|
3681
|
+
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
3682
|
+
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
3683
|
+
const shouldApplyFilesystemMagicResolution =
|
|
3684
|
+
reference.type === "js_import";
|
|
3685
|
+
if (shouldApplyFilesystemMagicResolution) {
|
|
3686
|
+
const filesystemResolution = applyFileSystemMagicResolution(
|
|
3687
|
+
urlObject.href,
|
|
3688
|
+
{
|
|
3689
|
+
fileStat: fsStat,
|
|
3690
|
+
magicDirectoryIndex,
|
|
3691
|
+
magicExtensions: getExtensionsToTry(
|
|
3692
|
+
magicExtensions,
|
|
3693
|
+
reference.ownerUrlInfo.url,
|
|
3694
|
+
),
|
|
3695
|
+
},
|
|
3696
|
+
);
|
|
3697
|
+
if (filesystemResolution.stat) {
|
|
3698
|
+
fsStat = filesystemResolution.stat;
|
|
3699
|
+
reference.fsStat = fsStat;
|
|
3700
|
+
urlObject.href = filesystemResolution.url;
|
|
3701
|
+
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
if (spa) {
|
|
3705
|
+
// for SPA we want to serve the root HTML file most of the time
|
|
3706
|
+
if (!fsStat) {
|
|
3707
|
+
if (urlToExtension(urlObject)) {
|
|
3708
|
+
// url has an extension, we assume it's a file request -> let 404 happen
|
|
3709
|
+
return null;
|
|
3710
|
+
}
|
|
3711
|
+
if (specifierUsesTrailingSlash) {
|
|
3712
|
+
// the trailing slash asks for a directory and there is none here
|
|
3713
|
+
// -> let 404 happen (same reasoning as the extension above)
|
|
3714
|
+
return null;
|
|
3715
|
+
}
|
|
3716
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
3717
|
+
if (spaFallbackUrl) {
|
|
3718
|
+
return spaFallbackUrl;
|
|
3719
|
+
}
|
|
3720
|
+
return null;
|
|
3721
|
+
}
|
|
3722
|
+
if (fsStat.isDirectory()) {
|
|
3723
|
+
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
3724
|
+
const directoryEntryFileUrl = getDirectoryEntryFileUrl(urlObject);
|
|
3725
|
+
if (directoryEntryFileUrl) {
|
|
3726
|
+
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
3727
|
+
return directoryEntryFileUrl;
|
|
3728
|
+
}
|
|
3729
|
+
if (!specifierUsesTrailingSlash) {
|
|
3730
|
+
// the trailing slash is what tells a directory apart from a route:
|
|
3731
|
+
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
3732
|
+
// even when "join/" exists in the source files.
|
|
3733
|
+
// Without this a source directory would shadow the route having
|
|
3734
|
+
// the same name and the SPA would be unreachable in dev while
|
|
3735
|
+
// being perfectly fine once built
|
|
3736
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
3737
|
+
if (spaFallbackUrl) {
|
|
3738
|
+
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
3739
|
+
nullIfNotFound: true,
|
|
3740
|
+
});
|
|
3741
|
+
return spaFallbackUrl;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
if (!fsStat) {
|
|
3747
|
+
return null;
|
|
3748
|
+
}
|
|
3749
|
+
const urlBeforeSymlinkResolution = urlObject.href;
|
|
3750
|
+
if (preserveSymlinks) {
|
|
3751
|
+
return `${urlBeforeSymlinkResolution}${search}${hash}`;
|
|
3752
|
+
}
|
|
3753
|
+
const urlAfterSymlinkResolution = resolveSymlink(
|
|
3754
|
+
urlBeforeSymlinkResolution,
|
|
3755
|
+
);
|
|
3756
|
+
if (urlAfterSymlinkResolution !== urlBeforeSymlinkResolution) {
|
|
3757
|
+
reference.leadsToASymlink = true;
|
|
3758
|
+
// reference.baseUrl = urlBeforeSymlinkResolution;
|
|
3759
|
+
}
|
|
3760
|
+
const resolvedUrl = `${urlAfterSymlinkResolution}${search}${hash}`;
|
|
3761
|
+
return resolvedUrl;
|
|
3762
|
+
},
|
|
3763
|
+
};
|
|
3764
|
+
};
|
|
3765
|
+
|
|
3766
|
+
const applyFsStatEffectsOnUrlObject = (urlObject, fsStat) => {
|
|
3767
|
+
if (!fsStat) {
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
const { pathname } = urlObject;
|
|
3771
|
+
const pathnameUsesTrailingSlash = pathname.endsWith("/");
|
|
3772
|
+
// force trailing slash on directories
|
|
3773
|
+
if (fsStat.isDirectory()) {
|
|
3774
|
+
if (!pathnameUsesTrailingSlash) {
|
|
3775
|
+
urlObject.pathname = `${pathname}/`;
|
|
3776
|
+
}
|
|
3777
|
+
} else if (pathnameUsesTrailingSlash) {
|
|
3778
|
+
// otherwise remove trailing slash if any
|
|
3779
|
+
// a warning here? (because it's strange to reference a file with a trailing slash)
|
|
3780
|
+
urlObject.pathname = pathname.slice(0, -1);
|
|
3781
|
+
}
|
|
3782
|
+
};
|
|
3783
|
+
|
|
3784
|
+
const resolveSymlink = (fileUrl) => {
|
|
3785
|
+
const urlObject = new URL(fileUrl);
|
|
3786
|
+
const realpath = realpathSync(urlObject);
|
|
3787
|
+
const realUrlObject = pathToFileURL(realpath);
|
|
3788
|
+
if (urlObject.pathname.endsWith("/")) {
|
|
3789
|
+
realUrlObject.pathname += `/`;
|
|
3790
|
+
}
|
|
3791
|
+
return realUrlObject.href;
|
|
3792
|
+
};
|
|
3793
|
+
|
|
3794
|
+
const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
3795
|
+
const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
|
|
3796
|
+
if (existsSync(indexHtmlFileUrl)) {
|
|
3797
|
+
return indexHtmlFileUrl.href;
|
|
3798
|
+
}
|
|
3799
|
+
const filename = urlToFilename(directoryUrl);
|
|
3800
|
+
const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
|
|
3801
|
+
if (existsSync(htmlFileUrlCandidate)) {
|
|
3802
|
+
return htmlFileUrlCandidate.href;
|
|
3803
|
+
}
|
|
3804
|
+
return null;
|
|
3805
|
+
};
|
|
3806
|
+
const getSpaFallbackUrl = (reference) => {
|
|
3807
|
+
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
3808
|
+
reference.ownerUrlInfo.context;
|
|
3809
|
+
if (!requestedUrl) {
|
|
3810
|
+
// the SPA fallback answers a request; during build there is none
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3813
|
+
const spaFallbackFileUrls = listSpaFallbackFileUrls(requestedUrl, {
|
|
3814
|
+
rootDirectoryUrl,
|
|
3815
|
+
mainFilePath,
|
|
3816
|
+
});
|
|
3817
|
+
for (const spaFallbackFileUrl of spaFallbackFileUrls) {
|
|
3818
|
+
if (existsSync(new URL(spaFallbackFileUrl))) {
|
|
3819
|
+
return spaFallbackFileUrl;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
// none exists: the main file it is, and the 404 answering for it lists
|
|
3823
|
+
// what was tried (see directory listing)
|
|
3824
|
+
return new URL(mainFilePath, rootDirectoryUrl).href;
|
|
3825
|
+
};
|
|
3826
|
+
// The html files that can answer a route, closest first: the entry file of
|
|
3827
|
+
// the route's own directory ("index.html", then "<dirname>.html"), then of
|
|
3828
|
+
// each directory above it up to the server root, then the main file.
|
|
3829
|
+
const listSpaFallbackFileUrls = (
|
|
3830
|
+
requestedUrl,
|
|
3831
|
+
{ rootDirectoryUrl, mainFilePath },
|
|
3832
|
+
) => {
|
|
3833
|
+
const fileUrls = [];
|
|
3834
|
+
let directoryUrl = new URL("./", requestedUrl).href;
|
|
3835
|
+
while (urlIsOrIsInsideOf(directoryUrl, rootDirectoryUrl)) {
|
|
3836
|
+
fileUrls.push(new URL("index.html", directoryUrl).href);
|
|
3837
|
+
const filename = urlToFilename(directoryUrl);
|
|
3838
|
+
if (filename) {
|
|
3839
|
+
fileUrls.push(new URL(`${filename}.html`, directoryUrl).href);
|
|
3840
|
+
}
|
|
3841
|
+
if (directoryUrl === String(rootDirectoryUrl)) {
|
|
3842
|
+
break;
|
|
3843
|
+
}
|
|
3844
|
+
directoryUrl = new URL("../", directoryUrl).href;
|
|
3845
|
+
}
|
|
3846
|
+
const mainFileUrl = new URL(mainFilePath, rootDirectoryUrl).href;
|
|
3847
|
+
if (!fileUrls.includes(mainFileUrl)) {
|
|
3848
|
+
fileUrls.push(mainFileUrl);
|
|
3849
|
+
}
|
|
3850
|
+
return fileUrls;
|
|
3851
|
+
};
|
|
3852
|
+
|
|
3619
3853
|
/*
|
|
3620
3854
|
* NICE TO HAVE:
|
|
3621
3855
|
*
|
|
@@ -3993,6 +4227,24 @@ const generateDirectoryListingInjection = (
|
|
|
3993
4227
|
filePathExisting: `/${filePathExisting}`,
|
|
3994
4228
|
filePathNotFound,
|
|
3995
4229
|
});
|
|
4230
|
+
// a url without extension nor trailing slash is a route: in spa mode it
|
|
4231
|
+
// was answered with the closest html file, the 404 means none was found
|
|
4232
|
+
const urlNotFoundObject = new URL(urlNotFound);
|
|
4233
|
+
if (
|
|
4234
|
+
spa &&
|
|
4235
|
+
!urlToExtension(urlNotFoundObject) &&
|
|
4236
|
+
!urlNotFoundObject.pathname.endsWith("/")
|
|
4237
|
+
) {
|
|
4238
|
+
enoentDetails.spaFallbackFilePaths = listSpaFallbackFileUrls(
|
|
4239
|
+
urlNotFound,
|
|
4240
|
+
{ rootDirectoryUrl: serverRootDirectoryUrl, mainFilePath },
|
|
4241
|
+
).map((fileUrl) =>
|
|
4242
|
+
FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
4243
|
+
fileUrl,
|
|
4244
|
+
serverRootDirectoryUrl,
|
|
4245
|
+
),
|
|
4246
|
+
);
|
|
4247
|
+
}
|
|
3996
4248
|
}
|
|
3997
4249
|
|
|
3998
4250
|
return {
|
|
@@ -4064,235 +4316,18 @@ const getDirectoryContentItems = ({
|
|
|
4064
4316
|
return items;
|
|
4065
4317
|
};
|
|
4066
4318
|
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
return `ignore:file:///`;
|
|
4080
|
-
}
|
|
4081
|
-
if (reference.url === "file://") {
|
|
4082
|
-
return `ignore:file://`;
|
|
4083
|
-
}
|
|
4084
|
-
// ignore all new URL second arg
|
|
4085
|
-
if (reference.subtype === "new_url_second_arg") {
|
|
4086
|
-
if (reference.original) {
|
|
4087
|
-
return `ignore:${reference.original.specifier}`;
|
|
4088
|
-
}
|
|
4089
|
-
return `ignore:${reference.specifier}`;
|
|
4090
|
-
}
|
|
4091
|
-
// http, https, data, about, ...
|
|
4092
|
-
if (!reference.url.startsWith("file:")) {
|
|
4093
|
-
return null;
|
|
4094
|
-
}
|
|
4095
|
-
if (reference.original && !reference.original.url.startsWith("file:")) {
|
|
4096
|
-
return null;
|
|
4097
|
-
}
|
|
4098
|
-
if (reference.isInline) {
|
|
4099
|
-
return null;
|
|
4100
|
-
}
|
|
4101
|
-
|
|
4102
|
-
if (
|
|
4103
|
-
reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
|
|
4104
|
-
) {
|
|
4105
|
-
const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
|
|
4106
|
-
const directoryUrl = new URL(
|
|
4107
|
-
reference.specifierPathname
|
|
4108
|
-
.replace(`/${directoryContentMagicName}`, "/")
|
|
4109
|
-
.slice(1),
|
|
4110
|
-
rootDirectoryUrl,
|
|
4111
|
-
).href;
|
|
4112
|
-
return directoryUrl;
|
|
4113
|
-
}
|
|
4114
|
-
// ignore "./" on new URL("./")
|
|
4115
|
-
// if (
|
|
4116
|
-
// reference.subtype === "new_url_first_arg" &&
|
|
4117
|
-
// reference.specifier === "./"
|
|
4118
|
-
// ) {
|
|
4119
|
-
// return `ignore:${reference.url}`;
|
|
4120
|
-
// }
|
|
4121
|
-
const urlObject = new URL(reference.url);
|
|
4122
|
-
let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
|
|
4123
|
-
reference.fsStat = fsStat;
|
|
4124
|
-
const { search, hash } = urlObject;
|
|
4125
|
-
urlObject.search = "";
|
|
4126
|
-
urlObject.hash = "";
|
|
4127
|
-
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
4128
|
-
// trailing slash on directories
|
|
4129
|
-
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
4130
|
-
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
4131
|
-
const shouldApplyFilesystemMagicResolution =
|
|
4132
|
-
reference.type === "js_import";
|
|
4133
|
-
if (shouldApplyFilesystemMagicResolution) {
|
|
4134
|
-
const filesystemResolution = applyFileSystemMagicResolution(
|
|
4135
|
-
urlObject.href,
|
|
4136
|
-
{
|
|
4137
|
-
fileStat: fsStat,
|
|
4138
|
-
magicDirectoryIndex,
|
|
4139
|
-
magicExtensions: getExtensionsToTry(
|
|
4140
|
-
magicExtensions,
|
|
4141
|
-
reference.ownerUrlInfo.url,
|
|
4142
|
-
),
|
|
4143
|
-
},
|
|
4144
|
-
);
|
|
4145
|
-
if (filesystemResolution.stat) {
|
|
4146
|
-
fsStat = filesystemResolution.stat;
|
|
4147
|
-
reference.fsStat = fsStat;
|
|
4148
|
-
urlObject.href = filesystemResolution.url;
|
|
4149
|
-
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
4150
|
-
}
|
|
4151
|
-
}
|
|
4152
|
-
if (spa) {
|
|
4153
|
-
// for SPA we want to serve the root HTML file most of the time
|
|
4154
|
-
if (!fsStat) {
|
|
4155
|
-
if (urlToExtension(urlObject)) {
|
|
4156
|
-
// url has an extension, we assume it's a file request -> let 404 happen
|
|
4157
|
-
return null;
|
|
4158
|
-
}
|
|
4159
|
-
if (specifierUsesTrailingSlash) {
|
|
4160
|
-
// the trailing slash asks for a directory and there is none here
|
|
4161
|
-
// -> let 404 happen (same reasoning as the extension above)
|
|
4162
|
-
return null;
|
|
4163
|
-
}
|
|
4164
|
-
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
4165
|
-
if (spaFallbackUrl) {
|
|
4166
|
-
return spaFallbackUrl;
|
|
4167
|
-
}
|
|
4168
|
-
return null;
|
|
4169
|
-
}
|
|
4170
|
-
if (fsStat.isDirectory()) {
|
|
4171
|
-
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
4172
|
-
const directoryEntryFileUrl = getDirectoryEntryFileUrl(urlObject);
|
|
4173
|
-
if (directoryEntryFileUrl) {
|
|
4174
|
-
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
4175
|
-
return directoryEntryFileUrl;
|
|
4176
|
-
}
|
|
4177
|
-
if (!specifierUsesTrailingSlash) {
|
|
4178
|
-
// the trailing slash is what tells a directory apart from a route:
|
|
4179
|
-
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
4180
|
-
// even when "join/" exists in the source files.
|
|
4181
|
-
// Without this a source directory would shadow the route having
|
|
4182
|
-
// the same name and the SPA would be unreachable in dev while
|
|
4183
|
-
// being perfectly fine once built
|
|
4184
|
-
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
4185
|
-
if (spaFallbackUrl) {
|
|
4186
|
-
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
4187
|
-
nullIfNotFound: true,
|
|
4188
|
-
});
|
|
4189
|
-
return spaFallbackUrl;
|
|
4190
|
-
}
|
|
4191
|
-
}
|
|
4192
|
-
}
|
|
4193
|
-
}
|
|
4194
|
-
if (!fsStat) {
|
|
4195
|
-
return null;
|
|
4196
|
-
}
|
|
4197
|
-
const urlBeforeSymlinkResolution = urlObject.href;
|
|
4198
|
-
if (preserveSymlinks) {
|
|
4199
|
-
return `${urlBeforeSymlinkResolution}${search}${hash}`;
|
|
4200
|
-
}
|
|
4201
|
-
const urlAfterSymlinkResolution = resolveSymlink(
|
|
4202
|
-
urlBeforeSymlinkResolution,
|
|
4203
|
-
);
|
|
4204
|
-
if (urlAfterSymlinkResolution !== urlBeforeSymlinkResolution) {
|
|
4205
|
-
reference.leadsToASymlink = true;
|
|
4206
|
-
// reference.baseUrl = urlBeforeSymlinkResolution;
|
|
4207
|
-
}
|
|
4208
|
-
const resolvedUrl = `${urlAfterSymlinkResolution}${search}${hash}`;
|
|
4209
|
-
return resolvedUrl;
|
|
4210
|
-
},
|
|
4211
|
-
};
|
|
4212
|
-
};
|
|
4213
|
-
|
|
4214
|
-
const applyFsStatEffectsOnUrlObject = (urlObject, fsStat) => {
|
|
4215
|
-
if (!fsStat) {
|
|
4216
|
-
return;
|
|
4217
|
-
}
|
|
4218
|
-
const { pathname } = urlObject;
|
|
4219
|
-
const pathnameUsesTrailingSlash = pathname.endsWith("/");
|
|
4220
|
-
// force trailing slash on directories
|
|
4221
|
-
if (fsStat.isDirectory()) {
|
|
4222
|
-
if (!pathnameUsesTrailingSlash) {
|
|
4223
|
-
urlObject.pathname = `${pathname}/`;
|
|
4224
|
-
}
|
|
4225
|
-
} else if (pathnameUsesTrailingSlash) {
|
|
4226
|
-
// otherwise remove trailing slash if any
|
|
4227
|
-
// a warning here? (because it's strange to reference a file with a trailing slash)
|
|
4228
|
-
urlObject.pathname = pathname.slice(0, -1);
|
|
4229
|
-
}
|
|
4230
|
-
};
|
|
4231
|
-
|
|
4232
|
-
const resolveSymlink = (fileUrl) => {
|
|
4233
|
-
const urlObject = new URL(fileUrl);
|
|
4234
|
-
const realpath = realpathSync(urlObject);
|
|
4235
|
-
const realUrlObject = pathToFileURL(realpath);
|
|
4236
|
-
if (urlObject.pathname.endsWith("/")) {
|
|
4237
|
-
realUrlObject.pathname += `/`;
|
|
4238
|
-
}
|
|
4239
|
-
return realUrlObject.href;
|
|
4240
|
-
};
|
|
4241
|
-
|
|
4242
|
-
const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
4243
|
-
const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
|
|
4244
|
-
if (existsSync(indexHtmlFileUrl)) {
|
|
4245
|
-
return indexHtmlFileUrl.href;
|
|
4246
|
-
}
|
|
4247
|
-
const filename = urlToFilename(directoryUrl);
|
|
4248
|
-
const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
|
|
4249
|
-
if (existsSync(htmlFileUrlCandidate)) {
|
|
4250
|
-
return htmlFileUrlCandidate.href;
|
|
4251
|
-
}
|
|
4252
|
-
return null;
|
|
4253
|
-
};
|
|
4254
|
-
const getSpaFallbackUrl = (reference) => {
|
|
4255
|
-
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
4256
|
-
reference.ownerUrlInfo.context;
|
|
4257
|
-
if (!requestedUrl) {
|
|
4258
|
-
// the SPA fallback answers a request; during build there is none
|
|
4259
|
-
return null;
|
|
4260
|
-
}
|
|
4261
|
-
const closestHtmlRootFile = getClosestHtmlRootFile(
|
|
4262
|
-
requestedUrl,
|
|
4263
|
-
rootDirectoryUrl,
|
|
4264
|
-
);
|
|
4265
|
-
if (closestHtmlRootFile) {
|
|
4266
|
-
return closestHtmlRootFile;
|
|
4267
|
-
}
|
|
4268
|
-
return String(new URL(mainFilePath, rootDirectoryUrl));
|
|
4269
|
-
};
|
|
4270
|
-
const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
|
|
4271
|
-
let directoryUrl = new URL("./", requestedUrl);
|
|
4272
|
-
while (true) {
|
|
4273
|
-
const directoryEntryFileUrl = getDirectoryEntryFileUrl(directoryUrl);
|
|
4274
|
-
if (directoryEntryFileUrl) {
|
|
4275
|
-
return directoryEntryFileUrl;
|
|
4276
|
-
}
|
|
4277
|
-
if (!urlIsOrIsInsideOf(directoryUrl, serverRootDirectoryUrl)) {
|
|
4278
|
-
return null;
|
|
4279
|
-
}
|
|
4280
|
-
directoryUrl = new URL("../", directoryUrl);
|
|
4281
|
-
}
|
|
4282
|
-
};
|
|
4283
|
-
|
|
4284
|
-
const directoryContentMagicName = "...";
|
|
4285
|
-
|
|
4286
|
-
const jsenvPluginProtocolFile = ({
|
|
4287
|
-
spa = true,
|
|
4288
|
-
magicExtensions,
|
|
4289
|
-
magicDirectoryIndex,
|
|
4290
|
-
preserveSymlinks,
|
|
4291
|
-
directoryListing,
|
|
4292
|
-
rootDirectoryUrl,
|
|
4293
|
-
mainFilePath,
|
|
4294
|
-
packageDirectory,
|
|
4295
|
-
sourceFilesConfig,
|
|
4319
|
+
const directoryContentMagicName = "...";
|
|
4320
|
+
|
|
4321
|
+
const jsenvPluginProtocolFile = ({
|
|
4322
|
+
spa = true,
|
|
4323
|
+
magicExtensions,
|
|
4324
|
+
magicDirectoryIndex,
|
|
4325
|
+
preserveSymlinks,
|
|
4326
|
+
directoryListing,
|
|
4327
|
+
rootDirectoryUrl,
|
|
4328
|
+
mainFilePath,
|
|
4329
|
+
packageDirectory,
|
|
4330
|
+
sourceFilesConfig,
|
|
4296
4331
|
}) => {
|
|
4297
4332
|
const listHtmlPages = createHtmlPageLister({ rootDirectoryUrl });
|
|
4298
4333
|
|
|
@@ -4434,7 +4469,16 @@ const jsenvPluginProtocolFile = ({
|
|
|
4434
4469
|
}
|
|
4435
4470
|
const serveFile = (url) => {
|
|
4436
4471
|
const contentType = CONTENT_TYPE.fromUrlExtension(url);
|
|
4437
|
-
const
|
|
4472
|
+
const urlObject = new URL(url);
|
|
4473
|
+
// taken before the read: a write landing in between moves the
|
|
4474
|
+
// stat past this one, so the dev server sees the content as
|
|
4475
|
+
// outdated (see isValid in the dev server) rather than the reverse
|
|
4476
|
+
const fileStat = statSync(urlObject);
|
|
4477
|
+
const fileBuffer = readFileSync(urlObject);
|
|
4478
|
+
urlInfo.data.fileStat = {
|
|
4479
|
+
mtimeMs: fileStat.mtimeMs,
|
|
4480
|
+
size: fileStat.size,
|
|
4481
|
+
};
|
|
4438
4482
|
const content = CONTENT_TYPE.isTextual(contentType)
|
|
4439
4483
|
? String(fileBuffer)
|
|
4440
4484
|
: fileBuffer;
|
|
@@ -5934,7 +5978,7 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
5934
5978
|
name: "jsenv:import_meta_scenario",
|
|
5935
5979
|
appliesDuring: "*",
|
|
5936
5980
|
transformUrlContent: {
|
|
5937
|
-
js_module:
|
|
5981
|
+
js_module: (urlInfo) => {
|
|
5938
5982
|
// Do not scan node modules for import.meta.dev/import.meta.build
|
|
5939
5983
|
// - node modules won't have this in their code
|
|
5940
5984
|
// - ;or should use other an other technic as this one won't be available
|
|
@@ -5950,40 +5994,42 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
5950
5994
|
) {
|
|
5951
5995
|
return null;
|
|
5952
5996
|
}
|
|
5953
|
-
const {
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5997
|
+
const importMetaScenarioNodes = { dev: [], build: [] };
|
|
5998
|
+
visitJsAst(urlInfo.contentAst, {
|
|
5999
|
+
MemberExpression: (node) => {
|
|
6000
|
+
const name = getImportMetaPropertyName(node);
|
|
6001
|
+
if (name === "dev" || name === "build") {
|
|
6002
|
+
importMetaScenarioNodes[name].push(node);
|
|
6003
|
+
}
|
|
6004
|
+
},
|
|
5959
6005
|
});
|
|
5960
|
-
const { dev
|
|
6006
|
+
const { dev, build } = importMetaScenarioNodes;
|
|
5961
6007
|
const replacements = [];
|
|
5962
|
-
const replace = (
|
|
5963
|
-
replacements.push({
|
|
6008
|
+
const replace = (node, value) => {
|
|
6009
|
+
replacements.push({ node, value });
|
|
5964
6010
|
};
|
|
5965
6011
|
if (urlInfo.context.build) {
|
|
5966
6012
|
// during build ensure replacement for tree-shaking
|
|
5967
|
-
dev.forEach((
|
|
5968
|
-
replace(
|
|
6013
|
+
dev.forEach((node) => {
|
|
6014
|
+
replace(node, "undefined");
|
|
5969
6015
|
});
|
|
5970
|
-
build.forEach((
|
|
5971
|
-
replace(
|
|
6016
|
+
build.forEach((node) => {
|
|
6017
|
+
replace(node, "true");
|
|
5972
6018
|
});
|
|
5973
6019
|
} else {
|
|
5974
6020
|
// during dev we can let "import.meta.build" untouched
|
|
5975
6021
|
// it will be evaluated to undefined.
|
|
5976
6022
|
// Moreover it can be surprising to see some "undefined"
|
|
5977
6023
|
// when source file contains "import.meta.build"
|
|
5978
|
-
dev.forEach((
|
|
5979
|
-
replace(
|
|
6024
|
+
dev.forEach((node) => {
|
|
6025
|
+
replace(node, "true");
|
|
5980
6026
|
});
|
|
5981
6027
|
}
|
|
5982
6028
|
const magicSource = createMagicSource(urlInfo.content);
|
|
5983
|
-
replacements.forEach(({
|
|
6029
|
+
replacements.forEach(({ node, value }) => {
|
|
5984
6030
|
magicSource.replace({
|
|
5985
|
-
start:
|
|
5986
|
-
end:
|
|
6031
|
+
start: node.start,
|
|
6032
|
+
end: node.end,
|
|
5987
6033
|
replacement: value,
|
|
5988
6034
|
});
|
|
5989
6035
|
});
|
|
@@ -5993,42 +6039,6 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
5993
6039
|
};
|
|
5994
6040
|
};
|
|
5995
6041
|
|
|
5996
|
-
const babelPluginMetadataImportMetaScenarios = () => {
|
|
5997
|
-
return {
|
|
5998
|
-
name: "metadata-import-meta-scenarios",
|
|
5999
|
-
visitor: {
|
|
6000
|
-
Program(programPath, state) {
|
|
6001
|
-
const importMetas = {};
|
|
6002
|
-
programPath.traverse({
|
|
6003
|
-
MemberExpression(path) {
|
|
6004
|
-
const { node } = path;
|
|
6005
|
-
const { object } = node;
|
|
6006
|
-
if (object.type !== "MetaProperty") {
|
|
6007
|
-
return;
|
|
6008
|
-
}
|
|
6009
|
-
const { property: objectProperty } = object;
|
|
6010
|
-
if (objectProperty.name !== "meta") {
|
|
6011
|
-
return;
|
|
6012
|
-
}
|
|
6013
|
-
const { property } = node;
|
|
6014
|
-
const { name } = property;
|
|
6015
|
-
const importMetaPaths = importMetas[name];
|
|
6016
|
-
if (importMetaPaths) {
|
|
6017
|
-
importMetaPaths.push(path);
|
|
6018
|
-
} else {
|
|
6019
|
-
importMetas[name] = [path];
|
|
6020
|
-
}
|
|
6021
|
-
},
|
|
6022
|
-
});
|
|
6023
|
-
state.file.metadata.importMetaScenarios = {
|
|
6024
|
-
dev: importMetas.dev,
|
|
6025
|
-
build: importMetas.build,
|
|
6026
|
-
};
|
|
6027
|
-
},
|
|
6028
|
-
},
|
|
6029
|
-
};
|
|
6030
|
-
};
|
|
6031
|
-
|
|
6032
6042
|
/*
|
|
6033
6043
|
* Source code can contain the following
|
|
6034
6044
|
* - __DEV__
|
|
@@ -6127,15 +6137,10 @@ const jsenvPluginImportMetaCss = () => {
|
|
|
6127
6137
|
if (!urlInfo.content.includes("import.meta.css")) {
|
|
6128
6138
|
return null;
|
|
6129
6139
|
}
|
|
6130
|
-
const
|
|
6131
|
-
|
|
6132
|
-
input: urlInfo.content,
|
|
6133
|
-
inputIsJsModule: true,
|
|
6134
|
-
inputUrl: urlInfo.originalUrl,
|
|
6135
|
-
outputUrl: urlInfo.generatedUrl,
|
|
6140
|
+
const importMetaCssNode = visitJsAstUntil(urlInfo.contentAst, {
|
|
6141
|
+
MemberExpression: (node) => getImportMetaPropertyName(node) === "css",
|
|
6136
6142
|
});
|
|
6137
|
-
|
|
6138
|
-
if (!usesImportMetaCss) {
|
|
6143
|
+
if (!importMetaCssNode) {
|
|
6139
6144
|
return null;
|
|
6140
6145
|
}
|
|
6141
6146
|
if (urlInfo.context.build) {
|
|
@@ -6215,37 +6220,6 @@ const babelPluginRewriteImportMetaCssAssignment = (
|
|
|
6215
6220
|
};
|
|
6216
6221
|
};
|
|
6217
6222
|
|
|
6218
|
-
const babelPluginMetadataUsesImportMetaCss = () => {
|
|
6219
|
-
return {
|
|
6220
|
-
name: "metadata-uses-import-meta-css",
|
|
6221
|
-
visitor: {
|
|
6222
|
-
Program(programPath, state) {
|
|
6223
|
-
let usesImportMetaCss = false;
|
|
6224
|
-
programPath.traverse({
|
|
6225
|
-
MemberExpression(path) {
|
|
6226
|
-
const { node } = path;
|
|
6227
|
-
const { object } = node;
|
|
6228
|
-
if (object.type !== "MetaProperty") {
|
|
6229
|
-
return;
|
|
6230
|
-
}
|
|
6231
|
-
const { property: objectProperty } = object;
|
|
6232
|
-
if (objectProperty.name !== "meta") {
|
|
6233
|
-
return;
|
|
6234
|
-
}
|
|
6235
|
-
const { property } = node;
|
|
6236
|
-
const { name } = property;
|
|
6237
|
-
if (name === "css") {
|
|
6238
|
-
usesImportMetaCss = true;
|
|
6239
|
-
path.stop();
|
|
6240
|
-
}
|
|
6241
|
-
},
|
|
6242
|
-
});
|
|
6243
|
-
state.file.metadata.usesImportMetaCss = usesImportMetaCss;
|
|
6244
|
-
},
|
|
6245
|
-
},
|
|
6246
|
-
};
|
|
6247
|
-
};
|
|
6248
|
-
|
|
6249
6223
|
const injectImportMetaCss = (
|
|
6250
6224
|
urlInfo,
|
|
6251
6225
|
{ content, importFrom, importName, importAs, hot },
|
|
@@ -6288,104 +6262,6 @@ ${importVariableName}(import.meta);
|
|
|
6288
6262
|
};
|
|
6289
6263
|
};
|
|
6290
6264
|
|
|
6291
|
-
// https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-stages-of-babel
|
|
6292
|
-
// https://github.com/cfware/babel-plugin-bundled-import-meta/blob/master/index.js
|
|
6293
|
-
// https://github.com/babel/babel/blob/f4edf62f6beeab8ae9f2b7f0b82f1b3b12a581af/packages/babel-helper-module-imports/src/index.js#L7
|
|
6294
|
-
|
|
6295
|
-
const babelPluginMetadataImportMetaHot = () => {
|
|
6296
|
-
return {
|
|
6297
|
-
name: "metadata-import-meta-hot",
|
|
6298
|
-
visitor: {
|
|
6299
|
-
Program(programPath, state) {
|
|
6300
|
-
Object.assign(
|
|
6301
|
-
state.file.metadata,
|
|
6302
|
-
collectImportMetaHotProperties(programPath),
|
|
6303
|
-
);
|
|
6304
|
-
},
|
|
6305
|
-
},
|
|
6306
|
-
};
|
|
6307
|
-
};
|
|
6308
|
-
const collectImportMetaHotProperties = (programPath) => {
|
|
6309
|
-
const importMetaHotPaths = [];
|
|
6310
|
-
let hotDecline = false;
|
|
6311
|
-
let hotAcceptSelf = false;
|
|
6312
|
-
let hotAcceptDependencies = [];
|
|
6313
|
-
programPath.traverse({
|
|
6314
|
-
MemberExpression(path) {
|
|
6315
|
-
const { node } = path;
|
|
6316
|
-
const { object } = node;
|
|
6317
|
-
if (object.type !== "MetaProperty") {
|
|
6318
|
-
return;
|
|
6319
|
-
}
|
|
6320
|
-
const { property: objectProperty } = object;
|
|
6321
|
-
if (objectProperty.name !== "meta") {
|
|
6322
|
-
return;
|
|
6323
|
-
}
|
|
6324
|
-
const { property } = node;
|
|
6325
|
-
const { name } = property;
|
|
6326
|
-
if (name === "hot") {
|
|
6327
|
-
importMetaHotPaths.push(path);
|
|
6328
|
-
}
|
|
6329
|
-
},
|
|
6330
|
-
CallExpression(path) {
|
|
6331
|
-
if (isImportMetaHotMethodCall(path, "accept")) {
|
|
6332
|
-
const callNode = path.node;
|
|
6333
|
-
const args = callNode.arguments;
|
|
6334
|
-
if (args.length === 0) {
|
|
6335
|
-
hotAcceptSelf = true;
|
|
6336
|
-
return;
|
|
6337
|
-
}
|
|
6338
|
-
const firstArg = args[0];
|
|
6339
|
-
if (firstArg.type === "StringLiteral") {
|
|
6340
|
-
hotAcceptDependencies = [
|
|
6341
|
-
{
|
|
6342
|
-
specifierPath: path.get("arguments")[0],
|
|
6343
|
-
},
|
|
6344
|
-
];
|
|
6345
|
-
return;
|
|
6346
|
-
}
|
|
6347
|
-
if (firstArg.type === "ArrayExpression") {
|
|
6348
|
-
const firstArgPath = path.get("arguments")[0];
|
|
6349
|
-
hotAcceptDependencies = firstArg.elements.map((arrayNode, index) => {
|
|
6350
|
-
if (arrayNode.type !== "StringLiteral") {
|
|
6351
|
-
throw new Error(
|
|
6352
|
-
`all array elements must be strings in "import.meta.hot.accept(array)"`,
|
|
6353
|
-
);
|
|
6354
|
-
}
|
|
6355
|
-
return {
|
|
6356
|
-
specifierPath: firstArgPath.get(String(index)),
|
|
6357
|
-
};
|
|
6358
|
-
});
|
|
6359
|
-
return;
|
|
6360
|
-
}
|
|
6361
|
-
// accept first arg can be "anything" such as
|
|
6362
|
-
// `const cb = () => {}; import.meta.accept(cb)`
|
|
6363
|
-
hotAcceptSelf = true;
|
|
6364
|
-
}
|
|
6365
|
-
if (isImportMetaHotMethodCall(path, "decline")) {
|
|
6366
|
-
hotDecline = true;
|
|
6367
|
-
}
|
|
6368
|
-
},
|
|
6369
|
-
});
|
|
6370
|
-
return {
|
|
6371
|
-
importMetaHotPaths,
|
|
6372
|
-
hotDecline,
|
|
6373
|
-
hotAcceptSelf,
|
|
6374
|
-
hotAcceptDependencies,
|
|
6375
|
-
};
|
|
6376
|
-
};
|
|
6377
|
-
const isImportMetaHotMethodCall = (path, methodName) => {
|
|
6378
|
-
const { property, object } = path.node.callee;
|
|
6379
|
-
return (
|
|
6380
|
-
property &&
|
|
6381
|
-
property.name === methodName &&
|
|
6382
|
-
object &&
|
|
6383
|
-
object.property &&
|
|
6384
|
-
object.property.name === "hot" &&
|
|
6385
|
-
object.object.type === "MetaProperty"
|
|
6386
|
-
);
|
|
6387
|
-
};
|
|
6388
|
-
|
|
6389
6265
|
// Some "smart" default applied to decide what should hot reload / fullreload:
|
|
6390
6266
|
// By default:
|
|
6391
6267
|
// - hot reload on <img src="./image.png" />
|
|
@@ -6533,6 +6409,75 @@ const htmlNodeCanHotReload = (node) => {
|
|
|
6533
6409
|
].includes(node.nodeName);
|
|
6534
6410
|
};
|
|
6535
6411
|
|
|
6412
|
+
const analyzeImportMetaHot = (ast) => {
|
|
6413
|
+
const importMetaHotNodes = [];
|
|
6414
|
+
let hotDecline = false;
|
|
6415
|
+
let hotAcceptSelf = false;
|
|
6416
|
+
let hotAcceptSpecifiers = [];
|
|
6417
|
+
visitJsAst(ast, {
|
|
6418
|
+
MemberExpression: (node) => {
|
|
6419
|
+
if (getImportMetaPropertyName(node) === "hot") {
|
|
6420
|
+
importMetaHotNodes.push(node);
|
|
6421
|
+
}
|
|
6422
|
+
},
|
|
6423
|
+
CallExpression: (node) => {
|
|
6424
|
+
const methodName = getImportMetaHotMethodName(node);
|
|
6425
|
+
if (methodName === "accept") {
|
|
6426
|
+
const args = node.arguments;
|
|
6427
|
+
if (args.length === 0) {
|
|
6428
|
+
hotAcceptSelf = true;
|
|
6429
|
+
return;
|
|
6430
|
+
}
|
|
6431
|
+
const [firstArg] = args;
|
|
6432
|
+
if (isStringLiteral(firstArg)) {
|
|
6433
|
+
hotAcceptSpecifiers = [firstArg.value];
|
|
6434
|
+
return;
|
|
6435
|
+
}
|
|
6436
|
+
if (firstArg.type === "ArrayExpression") {
|
|
6437
|
+
hotAcceptSpecifiers = firstArg.elements.map((element) => {
|
|
6438
|
+
if (!isStringLiteral(element)) {
|
|
6439
|
+
throw new Error(
|
|
6440
|
+
`all array elements must be strings in "import.meta.hot.accept(array)"`,
|
|
6441
|
+
);
|
|
6442
|
+
}
|
|
6443
|
+
return element.value;
|
|
6444
|
+
});
|
|
6445
|
+
return;
|
|
6446
|
+
}
|
|
6447
|
+
// accept first arg can be "anything" such as
|
|
6448
|
+
// `const cb = () => {}; import.meta.hot.accept(cb)`
|
|
6449
|
+
hotAcceptSelf = true;
|
|
6450
|
+
return;
|
|
6451
|
+
}
|
|
6452
|
+
if (methodName === "decline") {
|
|
6453
|
+
hotDecline = true;
|
|
6454
|
+
}
|
|
6455
|
+
},
|
|
6456
|
+
});
|
|
6457
|
+
return {
|
|
6458
|
+
importMetaHotNodes,
|
|
6459
|
+
hotDecline,
|
|
6460
|
+
hotAcceptSelf,
|
|
6461
|
+
hotAcceptSpecifiers,
|
|
6462
|
+
};
|
|
6463
|
+
};
|
|
6464
|
+
|
|
6465
|
+
// "import.meta.hot.<method>(...)"
|
|
6466
|
+
const getImportMetaHotMethodName = (callNode) => {
|
|
6467
|
+
const { callee } = callNode;
|
|
6468
|
+
if (callee.type !== "MemberExpression" || callee.computed) {
|
|
6469
|
+
return null;
|
|
6470
|
+
}
|
|
6471
|
+
if (getImportMetaPropertyName(callee.object) !== "hot") {
|
|
6472
|
+
return null;
|
|
6473
|
+
}
|
|
6474
|
+
return callee.property.name;
|
|
6475
|
+
};
|
|
6476
|
+
|
|
6477
|
+
const isStringLiteral = (node) => {
|
|
6478
|
+
return node.type === "Literal" && typeof node.value === "string";
|
|
6479
|
+
};
|
|
6480
|
+
|
|
6536
6481
|
const jsenvPluginImportMetaHot = () => {
|
|
6537
6482
|
const importMetaHotClientFileUrl = import.meta
|
|
6538
6483
|
.resolve("../client/import_meta_hot/import_meta_hot.js");
|
|
@@ -6581,7 +6526,7 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
6581
6526
|
cssUrlInfo.data.hotAcceptSelf = false;
|
|
6582
6527
|
cssUrlInfo.data.hotAcceptDependencies = [];
|
|
6583
6528
|
},
|
|
6584
|
-
js_module:
|
|
6529
|
+
js_module: (urlInfo) => {
|
|
6585
6530
|
// Do not scan node modules for import.meta.hot
|
|
6586
6531
|
// - unlikely to be there
|
|
6587
6532
|
// - we don't watch node modules (too expensive)
|
|
@@ -6593,27 +6538,22 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
6593
6538
|
if (!urlInfo.content.includes("import.meta.hot")) {
|
|
6594
6539
|
return null;
|
|
6595
6540
|
}
|
|
6596
|
-
const { metadata } = await applyBabelPlugins({
|
|
6597
|
-
babelPlugins: [babelPluginMetadataImportMetaHot],
|
|
6598
|
-
input: urlInfo.content,
|
|
6599
|
-
inputIsJsModule: true,
|
|
6600
|
-
inputUrl: urlInfo.originalUrl,
|
|
6601
|
-
outputUrl: urlInfo.generatedUrl,
|
|
6602
|
-
});
|
|
6603
6541
|
const {
|
|
6604
|
-
|
|
6542
|
+
importMetaHotNodes,
|
|
6605
6543
|
hotDecline,
|
|
6606
6544
|
hotAcceptSelf,
|
|
6607
|
-
|
|
6608
|
-
} =
|
|
6545
|
+
hotAcceptSpecifiers,
|
|
6546
|
+
} = analyzeImportMetaHot(urlInfo.contentAst);
|
|
6609
6547
|
urlInfo.data.hotDecline = hotDecline;
|
|
6610
6548
|
urlInfo.data.hotAcceptSelf = hotAcceptSelf;
|
|
6611
|
-
urlInfo.data.hotAcceptDependencies =
|
|
6612
|
-
|
|
6549
|
+
urlInfo.data.hotAcceptDependencies = hotAcceptSpecifiers.map(
|
|
6550
|
+
(specifier) => resolveHotAcceptSpecifier(urlInfo, specifier),
|
|
6551
|
+
);
|
|
6552
|
+
if (importMetaHotNodes.length === 0) {
|
|
6613
6553
|
return null;
|
|
6614
6554
|
}
|
|
6615
6555
|
if (urlInfo.context.build) {
|
|
6616
|
-
return removeImportMetaHots(urlInfo,
|
|
6556
|
+
return removeImportMetaHots(urlInfo, importMetaHotNodes);
|
|
6617
6557
|
}
|
|
6618
6558
|
return injectImportMetaHot(urlInfo, importMetaHotClientFileUrl);
|
|
6619
6559
|
},
|
|
@@ -6621,12 +6561,32 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
6621
6561
|
};
|
|
6622
6562
|
};
|
|
6623
6563
|
|
|
6624
|
-
|
|
6564
|
+
// The specifier given to import.meta.hot.accept() is one the file imports,
|
|
6565
|
+
// so its url is known from that import (autoreload compares urls). A
|
|
6566
|
+
// specifier the file does not import accepts nothing: it is kept as a plain
|
|
6567
|
+
// url, never resolved as a dependency (that could throw on a bare specifier).
|
|
6568
|
+
const resolveHotAcceptSpecifier = (urlInfo, specifier) => {
|
|
6569
|
+
for (const referenceToOther of urlInfo.referenceToOthersSet) {
|
|
6570
|
+
if (
|
|
6571
|
+
referenceToOther.type === "js_import" &&
|
|
6572
|
+
referenceToOther.specifier === specifier
|
|
6573
|
+
) {
|
|
6574
|
+
return referenceToOther.url;
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6577
|
+
try {
|
|
6578
|
+
return new URL(specifier, urlInfo.url).href;
|
|
6579
|
+
} catch {
|
|
6580
|
+
return specifier;
|
|
6581
|
+
}
|
|
6582
|
+
};
|
|
6583
|
+
|
|
6584
|
+
const removeImportMetaHots = (urlInfo, importMetaHotNodes) => {
|
|
6625
6585
|
const magicSource = createMagicSource(urlInfo.content);
|
|
6626
|
-
|
|
6586
|
+
importMetaHotNodes.forEach((node) => {
|
|
6627
6587
|
magicSource.replace({
|
|
6628
|
-
start:
|
|
6629
|
-
end:
|
|
6588
|
+
start: node.start,
|
|
6589
|
+
end: node.end,
|
|
6630
6590
|
replacement: "undefined",
|
|
6631
6591
|
});
|
|
6632
6592
|
});
|
|
@@ -7155,6 +7115,192 @@ const jsenvPluginAutoreload = ({
|
|
|
7155
7115
|
];
|
|
7156
7116
|
};
|
|
7157
7117
|
|
|
7118
|
+
const urlSpecifierEncoding = {
|
|
7119
|
+
encode: (reference) => {
|
|
7120
|
+
const { generatedSpecifier } = reference;
|
|
7121
|
+
if (generatedSpecifier.then) {
|
|
7122
|
+
return generatedSpecifier.then((value) => {
|
|
7123
|
+
reference.generatedSpecifier = value;
|
|
7124
|
+
return urlSpecifierEncoding.encode(reference);
|
|
7125
|
+
});
|
|
7126
|
+
}
|
|
7127
|
+
// allow plugin to return a function to bypas default formatting
|
|
7128
|
+
// (which is to use JSON.stringify when url is referenced inside js)
|
|
7129
|
+
if (typeof generatedSpecifier === "function") {
|
|
7130
|
+
return generatedSpecifier();
|
|
7131
|
+
}
|
|
7132
|
+
const formatter = formatters[reference.type];
|
|
7133
|
+
const value = formatter
|
|
7134
|
+
? formatter.encode(generatedSpecifier)
|
|
7135
|
+
: generatedSpecifier;
|
|
7136
|
+
if (reference.escape) {
|
|
7137
|
+
return reference.escape(value);
|
|
7138
|
+
}
|
|
7139
|
+
return value;
|
|
7140
|
+
},
|
|
7141
|
+
decode: (reference) => {
|
|
7142
|
+
const formatter = formatters[reference.type];
|
|
7143
|
+
return formatter
|
|
7144
|
+
? formatter.decode(reference.generatedSpecifier)
|
|
7145
|
+
: reference.generatedSpecifier;
|
|
7146
|
+
},
|
|
7147
|
+
};
|
|
7148
|
+
const formatters = {
|
|
7149
|
+
"js_import": { encode: JSON.stringify, decode: JSON.parse },
|
|
7150
|
+
"js_url": { encode: JSON.stringify, decode: JSON.parse },
|
|
7151
|
+
"css_@import": { encode: JSON.stringify, decode: JSON.stringify },
|
|
7152
|
+
// https://github.com/webpack-contrib/css-loader/pull/627/files
|
|
7153
|
+
"css_url": {
|
|
7154
|
+
encode: (url) => {
|
|
7155
|
+
// If url is already wrapped in quotes, remove them
|
|
7156
|
+
url = formatters.css_url.decode(url);
|
|
7157
|
+
// Should url be wrapped?
|
|
7158
|
+
// See https://drafts.csswg.org/css-values-3/#urls
|
|
7159
|
+
if (/["'() \t\n]/.test(url)) {
|
|
7160
|
+
return `"${url.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
7161
|
+
}
|
|
7162
|
+
return url;
|
|
7163
|
+
},
|
|
7164
|
+
decode: (url) => {
|
|
7165
|
+
const firstChar = url[0];
|
|
7166
|
+
const lastChar = url[url.length - 1];
|
|
7167
|
+
if (firstChar === `"` && lastChar === `"`) {
|
|
7168
|
+
return url.slice(1, -1);
|
|
7169
|
+
}
|
|
7170
|
+
if (firstChar === `'` && lastChar === `'`) {
|
|
7171
|
+
return url.slice(1, -1);
|
|
7172
|
+
}
|
|
7173
|
+
return url;
|
|
7174
|
+
},
|
|
7175
|
+
},
|
|
7176
|
+
};
|
|
7177
|
+
|
|
7178
|
+
/*
|
|
7179
|
+
* Tells the browser the static import graph of a page's module scripts, as
|
|
7180
|
+
* `Link: <url>; rel=modulepreload` response headers, so it can fetch every
|
|
7181
|
+
* module as soon as the page's headers arrive, without waiting to discover
|
|
7182
|
+
* them level by level (each level costing a round trip and, the first time,
|
|
7183
|
+
* a cook).
|
|
7184
|
+
*
|
|
7185
|
+
* Only what the graph already knows is listed: a module never cooked has no
|
|
7186
|
+
* known imports. The header is computed at each response from the graph as it
|
|
7187
|
+
* is then: a page gets its full list from its second load on, the first load
|
|
7188
|
+
* keeps the plain waterfall.
|
|
7189
|
+
*
|
|
7190
|
+
* Headers rather than <link> elements written into the page: the page content
|
|
7191
|
+
* stays what it is (served from memory, revalidated by etag) and the list can
|
|
7192
|
+
* grow without cooking the page again. And not references either: a reference
|
|
7193
|
+
* from the html to every module would make the html a dependent of each of
|
|
7194
|
+
* them, and a hot update propagating up the importers would reach the html
|
|
7195
|
+
* (which declines) instead of stopping at the module accepting it.
|
|
7196
|
+
*
|
|
7197
|
+
* OFF BY DEFAULT, to be enabled once the dev server speaks http/2 or http/3.
|
|
7198
|
+
* Over http/1.1 it makes pages slower — measured on a 500 modules page: first
|
|
7199
|
+
* paint 32ms -> 868ms, load 626ms -> 985ms. Knowing the urls earlier gives the
|
|
7200
|
+
* browser no extra resource: what it has is 6 connections per origin, and
|
|
7201
|
+
* every request goes through them in the order it was learned.
|
|
7202
|
+
* - Without the header, requests come in execution order: the parser asks
|
|
7203
|
+
* for the render-blocking supervisor script first and gets a connection at
|
|
7204
|
+
* once; transfer and execution overlap, a module executing and discovering
|
|
7205
|
+
* its imports while the others download. On localhost the server answers
|
|
7206
|
+
* from memory in 0.1ms, so the 6 connections are busy from the start.
|
|
7207
|
+
* - With the header, hundreds of requests take the 6 connections before the
|
|
7208
|
+
* parser asks for the supervisor script, which then waits for a free
|
|
7209
|
+
* connection behind 1MB+ transfers (520ms measured, 2s with 20ms of RTT).
|
|
7210
|
+
* Meanwhile nothing renders and nothing executes: transfer, then execution,
|
|
7211
|
+
* serialized. http/1.1 cannot reprioritize a queued request, and a
|
|
7212
|
+
* modulepreload has the same priority as a script.
|
|
7213
|
+
* - And there is nothing to hide: the discovery latency a preload removes is
|
|
7214
|
+
* ~1ms per graph level on localhost; with a real RTT a wide graph (more
|
|
7215
|
+
* than 6 modules pending at any time) keeps the 6 connections saturated
|
|
7216
|
+
* anyway, and the load takes requests / 6 * RTT whatever the order.
|
|
7217
|
+
* It pays off for a deep and narrow graph (connections idle, waiting for a
|
|
7218
|
+
* response to learn the next level), and over http/2 or http/3: no
|
|
7219
|
+
* connection limit, and the browser prioritizes the blocking script.
|
|
7220
|
+
*/
|
|
7221
|
+
|
|
7222
|
+
|
|
7223
|
+
const jsenvPluginModulepreload = () => {
|
|
7224
|
+
return {
|
|
7225
|
+
name: "jsenv:modulepreload",
|
|
7226
|
+
appliesDuring: "dev",
|
|
7227
|
+
augmentResponse: ({ urlInfo }) => {
|
|
7228
|
+
if (urlInfo.type !== "html") {
|
|
7229
|
+
return null;
|
|
7230
|
+
}
|
|
7231
|
+
const hrefs = collectStaticImportHrefs(urlInfo);
|
|
7232
|
+
if (hrefs.length === 0) {
|
|
7233
|
+
return null;
|
|
7234
|
+
}
|
|
7235
|
+
return {
|
|
7236
|
+
headers: {
|
|
7237
|
+
link: hrefs.map((href) => `<${href}>; rel=modulepreload`).join(", "),
|
|
7238
|
+
},
|
|
7239
|
+
};
|
|
7240
|
+
},
|
|
7241
|
+
};
|
|
7242
|
+
};
|
|
7243
|
+
|
|
7244
|
+
const collectStaticImportHrefs = (htmlUrlInfo) => {
|
|
7245
|
+
const hrefSet = new Set();
|
|
7246
|
+
const visitedSet = new Set();
|
|
7247
|
+
const addHref = (reference) => {
|
|
7248
|
+
const specifier = urlSpecifierEncoding.decode(reference);
|
|
7249
|
+
if (typeof specifier !== "string") {
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
// "?hot" belongs to the request that cooked the importer, the page loading
|
|
7253
|
+
// now imports the url without it
|
|
7254
|
+
hrefSet.add(injectQueryParamsIntoSpecifier(specifier, { hot: undefined }));
|
|
7255
|
+
};
|
|
7256
|
+
const visitJsModule = (jsModuleUrlInfo) => {
|
|
7257
|
+
if (visitedSet.has(jsModuleUrlInfo)) {
|
|
7258
|
+
return;
|
|
7259
|
+
}
|
|
7260
|
+
visitedSet.add(jsModuleUrlInfo);
|
|
7261
|
+
for (const reference of jsModuleUrlInfo.referenceToOthersSet) {
|
|
7262
|
+
if (
|
|
7263
|
+
reference.type !== "js_import" ||
|
|
7264
|
+
reference.isWeak ||
|
|
7265
|
+
reference.isImplicit ||
|
|
7266
|
+
!STATIC_IMPORT_SUBTYPES.includes(reference.subtype) ||
|
|
7267
|
+
!reference.url.startsWith("file:")
|
|
7268
|
+
) {
|
|
7269
|
+
continue;
|
|
7270
|
+
}
|
|
7271
|
+
const importedUrlInfo = reference.urlInfo;
|
|
7272
|
+
// never cooked: its content, hence its own imports, are unknown
|
|
7273
|
+
if (importedUrlInfo.type !== "js_module") {
|
|
7274
|
+
continue;
|
|
7275
|
+
}
|
|
7276
|
+
addHref(reference);
|
|
7277
|
+
visitJsModule(importedUrlInfo);
|
|
7278
|
+
}
|
|
7279
|
+
};
|
|
7280
|
+
for (const reference of htmlUrlInfo.referenceToOthersSet) {
|
|
7281
|
+
if (
|
|
7282
|
+
reference.type !== "script" ||
|
|
7283
|
+
reference.expectedType !== "js_module" ||
|
|
7284
|
+
// the scripts jsenv adds to every page are few, small and in memory
|
|
7285
|
+
reference.injected ||
|
|
7286
|
+
reference.isWeak
|
|
7287
|
+
) {
|
|
7288
|
+
continue;
|
|
7289
|
+
}
|
|
7290
|
+
const scriptUrlInfo = reference.urlInfo;
|
|
7291
|
+
if (scriptUrlInfo.type !== "js_module") {
|
|
7292
|
+
continue;
|
|
7293
|
+
}
|
|
7294
|
+
if (!reference.isInline && reference.url.startsWith("file:")) {
|
|
7295
|
+
addHref(reference);
|
|
7296
|
+
}
|
|
7297
|
+
visitJsModule(scriptUrlInfo);
|
|
7298
|
+
}
|
|
7299
|
+
return Array.from(hrefSet);
|
|
7300
|
+
};
|
|
7301
|
+
|
|
7302
|
+
const STATIC_IMPORT_SUBTYPES = ["import_static", "export_named", "export_all"];
|
|
7303
|
+
|
|
7158
7304
|
/*
|
|
7159
7305
|
* Tells the browser about the dependencies declared in package.json that
|
|
7160
7306
|
* node_modules does not match, so the page can say it is running with something
|
|
@@ -7796,6 +7942,7 @@ const getCorePlugins = ({
|
|
|
7796
7942
|
|
|
7797
7943
|
clientAutoreload,
|
|
7798
7944
|
clientAutoreloadOnServerRestart,
|
|
7945
|
+
modulepreload = false,
|
|
7799
7946
|
dependencyStatus,
|
|
7800
7947
|
cacheControl,
|
|
7801
7948
|
scenarioPlaceholders = true,
|
|
@@ -7897,6 +8044,7 @@ const getCorePlugins = ({
|
|
|
7897
8044
|
...(clientAutoreload && clientAutoreload.enabled
|
|
7898
8045
|
? [jsenvPluginAutoreload(clientAutoreload)]
|
|
7899
8046
|
: []),
|
|
8047
|
+
...(modulepreload ? [jsenvPluginModulepreload()] : []),
|
|
7900
8048
|
...(dependencyStatus
|
|
7901
8049
|
? [jsenvPluginDependencyStatus(dependencyStatus)]
|
|
7902
8050
|
: []),
|
|
@@ -8952,20 +9100,37 @@ const checkForDependencyRemovalEffects = (reference) => {
|
|
|
8952
9100
|
};
|
|
8953
9101
|
|
|
8954
9102
|
const traceFromUrlSite = (urlSite) => {
|
|
8955
|
-
const
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
: ""
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
9103
|
+
const { url, line, column, content } = urlSite;
|
|
9104
|
+
const trace = { url, line, column };
|
|
9105
|
+
// A file references many urls and an error on one of them is rare: the code
|
|
9106
|
+
// frame (and the message embedding it) is built the first time it is read,
|
|
9107
|
+
// not for every reference found.
|
|
9108
|
+
defineLazyProperty(trace, "codeFrame", () =>
|
|
9109
|
+
content ? generateContentFrame({ content, line, column }) : "",
|
|
9110
|
+
);
|
|
9111
|
+
defineLazyProperty(trace, "message", () => stringifyUrlSite(urlSite));
|
|
9112
|
+
return trace;
|
|
9113
|
+
};
|
|
9114
|
+
|
|
9115
|
+
const defineLazyProperty = (object, property, compute) => {
|
|
9116
|
+
const setValue = (value) => {
|
|
9117
|
+
Object.defineProperty(object, property, {
|
|
9118
|
+
enumerable: true,
|
|
9119
|
+
configurable: true,
|
|
9120
|
+
writable: true,
|
|
9121
|
+
value,
|
|
9122
|
+
});
|
|
8968
9123
|
};
|
|
9124
|
+
Object.defineProperty(object, property, {
|
|
9125
|
+
enumerable: true,
|
|
9126
|
+
configurable: true,
|
|
9127
|
+
get: () => {
|
|
9128
|
+
const value = compute();
|
|
9129
|
+
setValue(value);
|
|
9130
|
+
return value;
|
|
9131
|
+
},
|
|
9132
|
+
set: setValue,
|
|
9133
|
+
});
|
|
8969
9134
|
};
|
|
8970
9135
|
|
|
8971
9136
|
const adjustUrlSite = (urlInfo, { url, line, column }) => {
|
|
@@ -9227,66 +9392,6 @@ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced = (
|
|
|
9227
9392
|
seen.clear();
|
|
9228
9393
|
};
|
|
9229
9394
|
|
|
9230
|
-
const urlSpecifierEncoding = {
|
|
9231
|
-
encode: (reference) => {
|
|
9232
|
-
const { generatedSpecifier } = reference;
|
|
9233
|
-
if (generatedSpecifier.then) {
|
|
9234
|
-
return generatedSpecifier.then((value) => {
|
|
9235
|
-
reference.generatedSpecifier = value;
|
|
9236
|
-
return urlSpecifierEncoding.encode(reference);
|
|
9237
|
-
});
|
|
9238
|
-
}
|
|
9239
|
-
// allow plugin to return a function to bypas default formatting
|
|
9240
|
-
// (which is to use JSON.stringify when url is referenced inside js)
|
|
9241
|
-
if (typeof generatedSpecifier === "function") {
|
|
9242
|
-
return generatedSpecifier();
|
|
9243
|
-
}
|
|
9244
|
-
const formatter = formatters[reference.type];
|
|
9245
|
-
const value = formatter
|
|
9246
|
-
? formatter.encode(generatedSpecifier)
|
|
9247
|
-
: generatedSpecifier;
|
|
9248
|
-
if (reference.escape) {
|
|
9249
|
-
return reference.escape(value);
|
|
9250
|
-
}
|
|
9251
|
-
return value;
|
|
9252
|
-
},
|
|
9253
|
-
decode: (reference) => {
|
|
9254
|
-
const formatter = formatters[reference.type];
|
|
9255
|
-
return formatter
|
|
9256
|
-
? formatter.decode(reference.generatedSpecifier)
|
|
9257
|
-
: reference.generatedSpecifier;
|
|
9258
|
-
},
|
|
9259
|
-
};
|
|
9260
|
-
const formatters = {
|
|
9261
|
-
"js_import": { encode: JSON.stringify, decode: JSON.parse },
|
|
9262
|
-
"js_url": { encode: JSON.stringify, decode: JSON.parse },
|
|
9263
|
-
"css_@import": { encode: JSON.stringify, decode: JSON.stringify },
|
|
9264
|
-
// https://github.com/webpack-contrib/css-loader/pull/627/files
|
|
9265
|
-
"css_url": {
|
|
9266
|
-
encode: (url) => {
|
|
9267
|
-
// If url is already wrapped in quotes, remove them
|
|
9268
|
-
url = formatters.css_url.decode(url);
|
|
9269
|
-
// Should url be wrapped?
|
|
9270
|
-
// See https://drafts.csswg.org/css-values-3/#urls
|
|
9271
|
-
if (/["'() \t\n]/.test(url)) {
|
|
9272
|
-
return `"${url.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
9273
|
-
}
|
|
9274
|
-
return url;
|
|
9275
|
-
},
|
|
9276
|
-
decode: (url) => {
|
|
9277
|
-
const firstChar = url[0];
|
|
9278
|
-
const lastChar = url[url.length - 1];
|
|
9279
|
-
if (firstChar === `"` && lastChar === `"`) {
|
|
9280
|
-
return url.slice(1, -1);
|
|
9281
|
-
}
|
|
9282
|
-
if (firstChar === `'` && lastChar === `'`) {
|
|
9283
|
-
return url.slice(1, -1);
|
|
9284
|
-
}
|
|
9285
|
-
return url;
|
|
9286
|
-
},
|
|
9287
|
-
},
|
|
9288
|
-
};
|
|
9289
|
-
|
|
9290
9395
|
const createUrlGraph = ({
|
|
9291
9396
|
rootDirectoryUrl,
|
|
9292
9397
|
kitchen,
|
|
@@ -9464,7 +9569,6 @@ const createUrlInfo = (url, context) => {
|
|
|
9464
9569
|
dereferencedTimestamp: 0,
|
|
9465
9570
|
originalContentEtag: null,
|
|
9466
9571
|
contentEtag: null,
|
|
9467
|
-
isWatched: false,
|
|
9468
9572
|
isValid: () => false,
|
|
9469
9573
|
data: {}, // plugins can put whatever they want here
|
|
9470
9574
|
referenceToOthersSet: new Set(),
|
|
@@ -11282,11 +11386,10 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11282
11386
|
if (existing) {
|
|
11283
11387
|
return existing;
|
|
11284
11388
|
}
|
|
11285
|
-
const watchAssociations = URL_META.resolveAssociations(
|
|
11286
|
-
{ watch: stopWatchingSourceFiles.watchPatterns },
|
|
11287
|
-
sourceDirectoryUrl,
|
|
11288
|
-
);
|
|
11289
11389
|
let kitchen;
|
|
11390
|
+
// per url info, the file stat under which its content was last compared
|
|
11391
|
+
// with the file on disk and found identical (see isValid)
|
|
11392
|
+
const fileStatValidatedMap = new WeakMap();
|
|
11290
11393
|
clientFileChangeEventEmitter.on(({ url, event }) => {
|
|
11291
11394
|
const urlInfo = kitchen.graph.getUrlInfo(url);
|
|
11292
11395
|
if (urlInfo) {
|
|
@@ -11327,11 +11430,6 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11327
11430
|
packageDirectory,
|
|
11328
11431
|
});
|
|
11329
11432
|
kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
|
|
11330
|
-
const { watch } = URL_META.applyAssociations({
|
|
11331
|
-
url: urlInfoCreated.url,
|
|
11332
|
-
associations: watchAssociations,
|
|
11333
|
-
});
|
|
11334
|
-
urlInfoCreated.isWatched = watch;
|
|
11335
11433
|
// when an url depends on many others, we check all these (like package.json)
|
|
11336
11434
|
urlInfoCreated.isValid = () => {
|
|
11337
11435
|
const seenSet = new Set();
|
|
@@ -11351,37 +11449,64 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11351
11449
|
// was compared using etag and it has changed
|
|
11352
11450
|
return false;
|
|
11353
11451
|
}
|
|
11354
|
-
//
|
|
11355
|
-
//
|
|
11356
|
-
// the
|
|
11357
|
-
//
|
|
11358
|
-
//
|
|
11359
|
-
//
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11452
|
+
// The content held in memory is the file as it was read; a request
|
|
11453
|
+
// must never win a stale answer, whatever the watcher's latency,
|
|
11454
|
+
// and the ?v= a package.json decides even ends up in the browser's
|
|
11455
|
+
// immutable cache. So the file is checked on disk at every
|
|
11456
|
+
// validation — cheaply: a stat, compared with the one taken when
|
|
11457
|
+
// the content was read (or last found identical). Only a file
|
|
11458
|
+
// whose stat moved is read and hashed again.
|
|
11459
|
+
// Inline content (a <script> inside an html) has no file of its
|
|
11460
|
+
// own: it is as fresh as the html holding it, checked by the caller.
|
|
11461
|
+
if (!urlInfo.isInline) {
|
|
11462
|
+
let fileStat;
|
|
11363
11463
|
try {
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
return false;
|
|
11369
|
-
}
|
|
11464
|
+
fileStat = statSync(new URL(urlInfo.url), {
|
|
11465
|
+
throwIfNoEntry: false,
|
|
11466
|
+
});
|
|
11467
|
+
} catch {
|
|
11370
11468
|
return false;
|
|
11371
11469
|
}
|
|
11372
|
-
|
|
11373
|
-
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
11470
|
+
if (!fileStat) {
|
|
11374
11471
|
urlInfo.onModified();
|
|
11375
|
-
// restore content to be able to compare it again later
|
|
11376
|
-
urlInfo.kitchen.urlInfoTransformer.setContent(
|
|
11377
|
-
urlInfo,
|
|
11378
|
-
String(fileContentAsBuffer),
|
|
11379
|
-
{
|
|
11380
|
-
contentEtag: fileContentEtag,
|
|
11381
|
-
},
|
|
11382
|
-
);
|
|
11383
11472
|
return false;
|
|
11384
11473
|
}
|
|
11474
|
+
const fileStatKnown =
|
|
11475
|
+
fileStatValidatedMap.get(urlInfo) || urlInfo.data.fileStat;
|
|
11476
|
+
const fileUnchanged =
|
|
11477
|
+
fileStatKnown &&
|
|
11478
|
+
fileStatKnown.mtimeMs === fileStat.mtimeMs &&
|
|
11479
|
+
fileStatKnown.size === fileStat.size;
|
|
11480
|
+
if (!fileUnchanged) {
|
|
11481
|
+
let fileContentAsBuffer;
|
|
11482
|
+
try {
|
|
11483
|
+
fileContentAsBuffer = readFileSync(new URL(urlInfo.url));
|
|
11484
|
+
} catch (e) {
|
|
11485
|
+
if (e.code === "ENOENT") {
|
|
11486
|
+
urlInfo.onModified();
|
|
11487
|
+
return false;
|
|
11488
|
+
}
|
|
11489
|
+
return false;
|
|
11490
|
+
}
|
|
11491
|
+
const fileContentEtag = bufferToEtag(fileContentAsBuffer);
|
|
11492
|
+
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
11493
|
+
fileStatValidatedMap.delete(urlInfo);
|
|
11494
|
+
urlInfo.onModified();
|
|
11495
|
+
// restore content to be able to compare it again later
|
|
11496
|
+
urlInfo.kitchen.urlInfoTransformer.setContent(
|
|
11497
|
+
urlInfo,
|
|
11498
|
+
String(fileContentAsBuffer),
|
|
11499
|
+
{
|
|
11500
|
+
contentEtag: fileContentEtag,
|
|
11501
|
+
},
|
|
11502
|
+
);
|
|
11503
|
+
return false;
|
|
11504
|
+
}
|
|
11505
|
+
fileStatValidatedMap.set(urlInfo, {
|
|
11506
|
+
mtimeMs: fileStat.mtimeMs,
|
|
11507
|
+
size: fileStat.size,
|
|
11508
|
+
});
|
|
11509
|
+
}
|
|
11385
11510
|
}
|
|
11386
11511
|
for (const implicitUrl of urlInfo.implicitUrlSet) {
|
|
11387
11512
|
const implicitUrlInfo = urlInfo.graph.getUrlInfo(implicitUrl);
|
|
@@ -11497,6 +11622,40 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11497
11622
|
const ifNoneMatch = request.headers["if-none-match"];
|
|
11498
11623
|
const inlineParentUrlInfo = urlInfo.findParentIfInline();
|
|
11499
11624
|
const urlInfoTargetedByCache = inlineParentUrlInfo || urlInfo;
|
|
11625
|
+
// The content held in memory is the response when it is finalized
|
|
11626
|
+
// and still valid. Content can be defined while a cook is still in
|
|
11627
|
+
// flight (a file watcher invalidation re-cooking in the background,
|
|
11628
|
+
// for instance): at that point it holds the raw fetched content,
|
|
11629
|
+
// transformations not applied yet. Serving that would send an html
|
|
11630
|
+
// without any of the injected scripts. An inline url info (a
|
|
11631
|
+
// <script> inside an html) is cooked again whenever the html
|
|
11632
|
+
// containing it is cooked: its content is as fresh as the html's,
|
|
11633
|
+
// so both must be valid.
|
|
11634
|
+
const hasFreshContent = (urlInfo) =>
|
|
11635
|
+
urlInfo.content !== undefined &&
|
|
11636
|
+
urlInfo.contentFinalized &&
|
|
11637
|
+
urlInfo.isValid();
|
|
11638
|
+
const memoryContentIsFresh = () =>
|
|
11639
|
+
inlineParentUrlInfo
|
|
11640
|
+
? hasFreshContent(urlInfo) && hasFreshContent(inlineParentUrlInfo)
|
|
11641
|
+
: hasFreshContent(urlInfo);
|
|
11642
|
+
// a 304 goes through the hooks too: its headers stand for the
|
|
11643
|
+
// cached response's, they must say the same
|
|
11644
|
+
const augmentResponse = (response) => {
|
|
11645
|
+
const augmentResponseInfo = {
|
|
11646
|
+
...kitchen.context,
|
|
11647
|
+
reference,
|
|
11648
|
+
urlInfo,
|
|
11649
|
+
};
|
|
11650
|
+
kitchen.jsenvPluginsController.callHooks(
|
|
11651
|
+
"augmentResponse",
|
|
11652
|
+
augmentResponseInfo,
|
|
11653
|
+
(returnValue) => {
|
|
11654
|
+
response = composeTwoResponses(response, returnValue);
|
|
11655
|
+
},
|
|
11656
|
+
);
|
|
11657
|
+
return response;
|
|
11658
|
+
};
|
|
11500
11659
|
const respondWithNotModified = () => {
|
|
11501
11660
|
const headers = {
|
|
11502
11661
|
"cache-control": `private,max-age=0,must-revalidate`,
|
|
@@ -11506,23 +11665,21 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11506
11665
|
headers[key] = urlInfo.headers[key];
|
|
11507
11666
|
}
|
|
11508
11667
|
});
|
|
11509
|
-
return {
|
|
11668
|
+
return augmentResponse({
|
|
11510
11669
|
status: 304,
|
|
11511
11670
|
headers,
|
|
11512
|
-
};
|
|
11671
|
+
});
|
|
11513
11672
|
};
|
|
11514
11673
|
|
|
11515
11674
|
try {
|
|
11516
|
-
|
|
11517
|
-
// is cooked, so its content is only known after cooking; its etag is
|
|
11518
|
-
// compared below, once cooked
|
|
11519
|
-
if (!urlInfo.error && ifNoneMatch && !inlineParentUrlInfo) {
|
|
11675
|
+
if (!urlInfo.error && ifNoneMatch) {
|
|
11520
11676
|
const [clientOriginalContentEtag, clientContentEtag] =
|
|
11521
11677
|
ifNoneMatch.split("_");
|
|
11522
11678
|
if (
|
|
11523
|
-
|
|
11679
|
+
urlInfoTargetedByCache.originalContentEtag ===
|
|
11680
|
+
clientOriginalContentEtag &&
|
|
11524
11681
|
urlInfo.contentEtag === clientContentEtag &&
|
|
11525
|
-
|
|
11682
|
+
memoryContentIsFresh()
|
|
11526
11683
|
) {
|
|
11527
11684
|
return respondWithNotModified();
|
|
11528
11685
|
}
|
|
@@ -11540,18 +11697,9 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11540
11697
|
// client etag to match).
|
|
11541
11698
|
const servableFromMemory =
|
|
11542
11699
|
!urlInfo.error &&
|
|
11543
|
-
!inlineParentUrlInfo &&
|
|
11544
11700
|
!urlInfo.response &&
|
|
11545
|
-
urlInfo.content !== undefined &&
|
|
11546
|
-
// content can be defined while a cook is still in flight (a file
|
|
11547
|
-
// watcher invalidation re-cooking in the background, for
|
|
11548
|
-
// instance): at that point it holds the raw fetched content,
|
|
11549
|
-
// transformations not applied yet. Serving that would send an
|
|
11550
|
-
// html without any of the injected scripts. Only finalized
|
|
11551
|
-
// content is a complete response; anything else must go through
|
|
11552
|
-
// cook() below, which joins the pending cook (see debounceCook).
|
|
11553
|
-
urlInfo.contentFinalized &&
|
|
11554
11701
|
!cacheIsDisabledInResponseHeader(urlInfo) &&
|
|
11702
|
+
!cacheIsDisabledInResponseHeader(urlInfoTargetedByCache) &&
|
|
11555
11703
|
// a "?hot" request exists to bypass every cache, this one
|
|
11556
11704
|
// included: it must be cooked, because cooking is what rewrites
|
|
11557
11705
|
// its references so "?hot" cascades to the modified files below
|
|
@@ -11570,7 +11718,7 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11570
11718
|
// signal registry throwing on duplicate ids). Re-cooking under
|
|
11571
11719
|
// the normal request rewrites the references clean.
|
|
11572
11720
|
!urlInfo.contentCookedForHotRequest &&
|
|
11573
|
-
|
|
11721
|
+
memoryContentIsFresh();
|
|
11574
11722
|
if (!servableFromMemory) {
|
|
11575
11723
|
await urlInfo.cook({ request, reference });
|
|
11576
11724
|
urlInfo.contentCookedForHotRequest =
|
|
@@ -11628,19 +11776,7 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11628
11776
|
? { "served from memory cache": null }
|
|
11629
11777
|
: urlInfo.timing,
|
|
11630
11778
|
};
|
|
11631
|
-
|
|
11632
|
-
...kitchen.context,
|
|
11633
|
-
reference,
|
|
11634
|
-
urlInfo,
|
|
11635
|
-
};
|
|
11636
|
-
kitchen.jsenvPluginsController.callHooks(
|
|
11637
|
-
"augmentResponse",
|
|
11638
|
-
augmentResponseInfo,
|
|
11639
|
-
(returnValue) => {
|
|
11640
|
-
response = composeTwoResponses(response, returnValue);
|
|
11641
|
-
},
|
|
11642
|
-
);
|
|
11643
|
-
return response;
|
|
11779
|
+
return augmentResponse(response);
|
|
11644
11780
|
} catch (error) {
|
|
11645
11781
|
const originalError = error ? error.cause || error : error;
|
|
11646
11782
|
if (originalError.asResponse) {
|
|
@@ -11718,6 +11854,14 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11718
11854
|
"cache-control": "no-store",
|
|
11719
11855
|
},
|
|
11720
11856
|
};
|
|
11857
|
+
} finally {
|
|
11858
|
+
// What the request put on the url info context is for this
|
|
11859
|
+
// request only: a cook happening later for another reason (the
|
|
11860
|
+
// html holding an inline script is cooked again) must not see a
|
|
11861
|
+
// stale "requestedUrl" and take the inline script for a direct
|
|
11862
|
+
// request, which would make it pick the content of a previous
|
|
11863
|
+
// reference (see jsenv:inline_content_fetcher).
|
|
11864
|
+
forgetRequestFromContext(urlInfo.context);
|
|
11721
11865
|
}
|
|
11722
11866
|
},
|
|
11723
11867
|
},
|
|
@@ -11727,6 +11871,15 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11727
11871
|
return [devServerPluginRoutes, ...devServerJsenvPluginStore.allServerPlugins];
|
|
11728
11872
|
};
|
|
11729
11873
|
|
|
11874
|
+
const forgetRequestFromContext = (context) => {
|
|
11875
|
+
// own properties only: what is inherited from the owner context stays
|
|
11876
|
+
for (const key of ["request", "requestedUrl", "reference"]) {
|
|
11877
|
+
if (Object.hasOwn(context, key)) {
|
|
11878
|
+
delete context[key];
|
|
11879
|
+
}
|
|
11880
|
+
}
|
|
11881
|
+
};
|
|
11882
|
+
|
|
11730
11883
|
const cacheIsDisabledInResponseHeader = (urlInfo) => {
|
|
11731
11884
|
return (
|
|
11732
11885
|
urlInfo.headers["cache-control"] === "no-store" ||
|
|
@@ -11758,6 +11911,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
11758
11911
|
* @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).
|
|
11759
11912
|
* @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"`).
|
|
11760
11913
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
11914
|
+
* @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).
|
|
11761
11915
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
11762
11916
|
* @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`.
|
|
11763
11917
|
* @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
|
|
@@ -11799,6 +11953,7 @@ const startDevServer = async ({
|
|
|
11799
11953
|
sourceFilesConfig = {},
|
|
11800
11954
|
clientAutoreload = true,
|
|
11801
11955
|
clientAutoreloadOnServerRestart = true,
|
|
11956
|
+
modulepreload = false,
|
|
11802
11957
|
// server-timing response headers: devtools show how the time to answer is
|
|
11803
11958
|
// spent (cook measures come from the kitchen, see urlInfo.timing). Entries
|
|
11804
11959
|
// under minDuration are dropped so a human reads the measures that matter;
|
|
@@ -11888,9 +12043,12 @@ const startDevServer = async ({
|
|
|
11888
12043
|
}
|
|
11889
12044
|
|
|
11890
12045
|
const logger = createLogger({ logLevel });
|
|
11891
|
-
const startDevServerTask = createTaskLog(
|
|
11892
|
-
|
|
11893
|
-
|
|
12046
|
+
const startDevServerTask = createTaskLog(
|
|
12047
|
+
`start dev server for ${humanizeSourceDirectory(sourceDirectoryUrl)}`,
|
|
12048
|
+
{
|
|
12049
|
+
disabled: !logger.levels.info,
|
|
12050
|
+
},
|
|
12051
|
+
);
|
|
11894
12052
|
|
|
11895
12053
|
const serverStopCallbackSet = new Set();
|
|
11896
12054
|
const serverStopAbortController = new AbortController();
|
|
@@ -11969,6 +12127,7 @@ const startDevServer = async ({
|
|
|
11969
12127
|
|
|
11970
12128
|
clientAutoreload,
|
|
11971
12129
|
clientAutoreloadOnServerRestart,
|
|
12130
|
+
modulepreload,
|
|
11972
12131
|
dependencyStatus: {
|
|
11973
12132
|
dependencyProblemEventEmitter,
|
|
11974
12133
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
@@ -12074,4 +12233,13 @@ const startDevServer = async ({
|
|
|
12074
12233
|
};
|
|
12075
12234
|
};
|
|
12076
12235
|
|
|
12236
|
+
// "./src/" when inside the current working directory, the full path otherwise
|
|
12237
|
+
const humanizeSourceDirectory = (sourceDirectoryUrl) => {
|
|
12238
|
+
const cwdUrl = pathToFileURL(`${process.cwd()}/`).href;
|
|
12239
|
+
if (urlIsOrIsInsideOf(sourceDirectoryUrl, cwdUrl)) {
|
|
12240
|
+
return `./${urlToRelativeUrl(sourceDirectoryUrl, cwdUrl)}`;
|
|
12241
|
+
}
|
|
12242
|
+
return urlToFileSystemPath(sourceDirectoryUrl);
|
|
12243
|
+
};
|
|
12244
|
+
|
|
12077
12245
|
export { startDevServer };
|