@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
|
@@ -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";
|
|
@@ -2467,8 +2467,7 @@ const parseAndTransformJsReferences = async (
|
|
|
2467
2467
|
for (const sequentialAction of sequentialActions) {
|
|
2468
2468
|
await sequentialAction();
|
|
2469
2469
|
}
|
|
2470
|
-
|
|
2471
|
-
return { content, sourcemap };
|
|
2470
|
+
return magicSource.toContentAndSourcemap();
|
|
2472
2471
|
};
|
|
2473
2472
|
|
|
2474
2473
|
const jsenvPluginReferenceExpectedTypes = () => {
|
|
@@ -3129,11 +3128,6 @@ const addRelationshipWithPackageJson = ({
|
|
|
3129
3128
|
String(packageJsonContentAsBuffer),
|
|
3130
3129
|
);
|
|
3131
3130
|
}
|
|
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
3131
|
};
|
|
3138
3132
|
|
|
3139
3133
|
const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|
|
@@ -3506,18 +3500,21 @@ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
|
|
|
3506
3500
|
const getDirectoryWatchPatterns = (
|
|
3507
3501
|
directoryUrl,
|
|
3508
3502
|
watchedDirectoryUrl,
|
|
3509
|
-
{ sourceFilesConfig },
|
|
3503
|
+
{ sourceFilesConfig, defaultPatterns = true },
|
|
3510
3504
|
) => {
|
|
3511
3505
|
const directoryUrlRelativeToWatchedDirectory = urlToRelativeUrl(
|
|
3512
3506
|
directoryUrl,
|
|
3513
3507
|
watchedDirectoryUrl,
|
|
3514
3508
|
);
|
|
3515
|
-
const watchPatterns = {
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3509
|
+
const watchPatterns = {};
|
|
3510
|
+
if (defaultPatterns) {
|
|
3511
|
+
Object.assign(watchPatterns, {
|
|
3512
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/*`]: true, // by default watch everything inside the source directory
|
|
3513
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*`]: false, // file starting with a dot -> do not watch
|
|
3514
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/.*/`]: false, // directory starting with a dot -> do not watch
|
|
3515
|
+
[`${directoryUrlRelativeToWatchedDirectory}**/node_modules/`]: false, // node_modules directory -> do not watch
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3521
3518
|
for (const key of Object.keys(sourceFilesConfig)) {
|
|
3522
3519
|
watchPatterns[`${directoryUrlRelativeToWatchedDirectory}${key}`] =
|
|
3523
3520
|
sourceFilesConfig[key];
|
|
@@ -3538,11 +3535,12 @@ const watchSourceFiles = (
|
|
|
3538
3535
|
// And jsenv should not consider these as source files and watch them (to not hurt performances)
|
|
3539
3536
|
const watchPatterns = {};
|
|
3540
3537
|
let watchedDirectoryUrl = "";
|
|
3541
|
-
const addDirectoryToWatch = (directoryUrl) => {
|
|
3538
|
+
const addDirectoryToWatch = (directoryUrl, { defaultPatterns } = {}) => {
|
|
3542
3539
|
Object.assign(
|
|
3543
3540
|
watchPatterns,
|
|
3544
3541
|
getDirectoryWatchPatterns(directoryUrl, watchedDirectoryUrl, {
|
|
3545
3542
|
sourceFilesConfig,
|
|
3543
|
+
defaultPatterns,
|
|
3546
3544
|
}),
|
|
3547
3545
|
);
|
|
3548
3546
|
};
|
|
@@ -3574,7 +3572,6 @@ const watchSourceFiles = (
|
|
|
3574
3572
|
},
|
|
3575
3573
|
},
|
|
3576
3574
|
);
|
|
3577
|
-
stopWatchingSourceFiles.watchPatterns = watchPatterns;
|
|
3578
3575
|
return stopWatchingSourceFiles;
|
|
3579
3576
|
};
|
|
3580
3577
|
|
|
@@ -3594,16 +3591,18 @@ const watchSourceFiles = (
|
|
|
3594
3591
|
}
|
|
3595
3592
|
watchedDirectoryUrl = packageDirectoryUrl;
|
|
3596
3593
|
for (const workspace of workspaces) {
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3594
|
+
const workspaceDirectoryUrl = workspace.endsWith("*")
|
|
3595
|
+
? new URL(workspace.slice(0, -1), packageDirectoryUrl)
|
|
3596
|
+
: new URL(workspace, packageDirectoryUrl);
|
|
3597
|
+
addDirectoryToWatch(workspaceDirectoryUrl, {
|
|
3598
|
+
// The source directory patterns already cover a workspace inside it.
|
|
3599
|
+
// Every pattern is tested against every file found while watching, so
|
|
3600
|
+
// the same patterns rooted at each workspace only multiply that work.
|
|
3601
|
+
defaultPatterns: !urlIsOrIsInsideOf(
|
|
3602
|
+
workspaceDirectoryUrl,
|
|
3603
|
+
sourceDirectoryUrl,
|
|
3604
|
+
),
|
|
3605
|
+
});
|
|
3607
3606
|
}
|
|
3608
3607
|
// we are updating the root directory
|
|
3609
3608
|
// we must make the patterns relative to source directory relative to the new root directory
|
|
@@ -3616,6 +3615,240 @@ const watchSourceFiles = (
|
|
|
3616
3615
|
return watch();
|
|
3617
3616
|
};
|
|
3618
3617
|
|
|
3618
|
+
const jsenvPluginFsRedirection = ({
|
|
3619
|
+
spa,
|
|
3620
|
+
directoryContentMagicName,
|
|
3621
|
+
magicExtensions = ["inherit", ".js"],
|
|
3622
|
+
magicDirectoryIndex = true,
|
|
3623
|
+
preserveSymlinks = false,
|
|
3624
|
+
}) => {
|
|
3625
|
+
return {
|
|
3626
|
+
name: "jsenv:fs_redirection",
|
|
3627
|
+
appliesDuring: "*",
|
|
3628
|
+
redirectReference: (reference) => {
|
|
3629
|
+
if (reference.url === "file:///") {
|
|
3630
|
+
return `ignore:file:///`;
|
|
3631
|
+
}
|
|
3632
|
+
if (reference.url === "file://") {
|
|
3633
|
+
return `ignore:file://`;
|
|
3634
|
+
}
|
|
3635
|
+
// ignore all new URL second arg
|
|
3636
|
+
if (reference.subtype === "new_url_second_arg") {
|
|
3637
|
+
if (reference.original) {
|
|
3638
|
+
return `ignore:${reference.original.specifier}`;
|
|
3639
|
+
}
|
|
3640
|
+
return `ignore:${reference.specifier}`;
|
|
3641
|
+
}
|
|
3642
|
+
// http, https, data, about, ...
|
|
3643
|
+
if (!reference.url.startsWith("file:")) {
|
|
3644
|
+
return null;
|
|
3645
|
+
}
|
|
3646
|
+
if (reference.original && !reference.original.url.startsWith("file:")) {
|
|
3647
|
+
return null;
|
|
3648
|
+
}
|
|
3649
|
+
if (reference.isInline) {
|
|
3650
|
+
return null;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
if (
|
|
3654
|
+
reference.specifierPathname.endsWith(`/${directoryContentMagicName}`)
|
|
3655
|
+
) {
|
|
3656
|
+
const { rootDirectoryUrl } = reference.ownerUrlInfo.context;
|
|
3657
|
+
const directoryUrl = new URL(
|
|
3658
|
+
reference.specifierPathname
|
|
3659
|
+
.replace(`/${directoryContentMagicName}`, "/")
|
|
3660
|
+
.slice(1),
|
|
3661
|
+
rootDirectoryUrl,
|
|
3662
|
+
).href;
|
|
3663
|
+
return directoryUrl;
|
|
3664
|
+
}
|
|
3665
|
+
// ignore "./" on new URL("./")
|
|
3666
|
+
// if (
|
|
3667
|
+
// reference.subtype === "new_url_first_arg" &&
|
|
3668
|
+
// reference.specifier === "./"
|
|
3669
|
+
// ) {
|
|
3670
|
+
// return `ignore:${reference.url}`;
|
|
3671
|
+
// }
|
|
3672
|
+
const urlObject = new URL(reference.url);
|
|
3673
|
+
let fsStat = readEntryStatSync(urlObject, { nullIfNotFound: true });
|
|
3674
|
+
reference.fsStat = fsStat;
|
|
3675
|
+
const { search, hash } = urlObject;
|
|
3676
|
+
urlObject.search = "";
|
|
3677
|
+
urlObject.hash = "";
|
|
3678
|
+
// must be read before applyFsStatEffectsOnUrlObject which forces the
|
|
3679
|
+
// trailing slash on directories
|
|
3680
|
+
const specifierUsesTrailingSlash = urlObject.pathname.endsWith("/");
|
|
3681
|
+
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
3682
|
+
const shouldApplyFilesystemMagicResolution =
|
|
3683
|
+
reference.type === "js_import";
|
|
3684
|
+
if (shouldApplyFilesystemMagicResolution) {
|
|
3685
|
+
const filesystemResolution = applyFileSystemMagicResolution(
|
|
3686
|
+
urlObject.href,
|
|
3687
|
+
{
|
|
3688
|
+
fileStat: fsStat,
|
|
3689
|
+
magicDirectoryIndex,
|
|
3690
|
+
magicExtensions: getExtensionsToTry(
|
|
3691
|
+
magicExtensions,
|
|
3692
|
+
reference.ownerUrlInfo.url,
|
|
3693
|
+
),
|
|
3694
|
+
},
|
|
3695
|
+
);
|
|
3696
|
+
if (filesystemResolution.stat) {
|
|
3697
|
+
fsStat = filesystemResolution.stat;
|
|
3698
|
+
reference.fsStat = fsStat;
|
|
3699
|
+
urlObject.href = filesystemResolution.url;
|
|
3700
|
+
applyFsStatEffectsOnUrlObject(urlObject, fsStat);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
if (spa) {
|
|
3704
|
+
// for SPA we want to serve the root HTML file most of the time
|
|
3705
|
+
if (!fsStat) {
|
|
3706
|
+
if (urlToExtension(urlObject)) {
|
|
3707
|
+
// url has an extension, we assume it's a file request -> let 404 happen
|
|
3708
|
+
return null;
|
|
3709
|
+
}
|
|
3710
|
+
if (specifierUsesTrailingSlash) {
|
|
3711
|
+
// the trailing slash asks for a directory and there is none here
|
|
3712
|
+
// -> let 404 happen (same reasoning as the extension above)
|
|
3713
|
+
return null;
|
|
3714
|
+
}
|
|
3715
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
3716
|
+
if (spaFallbackUrl) {
|
|
3717
|
+
return spaFallbackUrl;
|
|
3718
|
+
}
|
|
3719
|
+
return null;
|
|
3720
|
+
}
|
|
3721
|
+
if (fsStat.isDirectory()) {
|
|
3722
|
+
// When requesting a directory, check if we have an HTML entry file for that directory
|
|
3723
|
+
const directoryEntryFileUrl = getDirectoryEntryFileUrl(urlObject);
|
|
3724
|
+
if (directoryEntryFileUrl) {
|
|
3725
|
+
reference.fsStat = readEntryStatSync(directoryEntryFileUrl);
|
|
3726
|
+
return directoryEntryFileUrl;
|
|
3727
|
+
}
|
|
3728
|
+
if (!specifierUsesTrailingSlash) {
|
|
3729
|
+
// the trailing slash is what tells a directory apart from a route:
|
|
3730
|
+
// "/join/" is the directory, "/join" is a route owned by the SPA
|
|
3731
|
+
// even when "join/" exists in the source files.
|
|
3732
|
+
// Without this a source directory would shadow the route having
|
|
3733
|
+
// the same name and the SPA would be unreachable in dev while
|
|
3734
|
+
// being perfectly fine once built
|
|
3735
|
+
const spaFallbackUrl = getSpaFallbackUrl(reference);
|
|
3736
|
+
if (spaFallbackUrl) {
|
|
3737
|
+
reference.fsStat = readEntryStatSync(spaFallbackUrl, {
|
|
3738
|
+
nullIfNotFound: true,
|
|
3739
|
+
});
|
|
3740
|
+
return spaFallbackUrl;
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
if (!fsStat) {
|
|
3746
|
+
return null;
|
|
3747
|
+
}
|
|
3748
|
+
const urlBeforeSymlinkResolution = urlObject.href;
|
|
3749
|
+
if (preserveSymlinks) {
|
|
3750
|
+
return `${urlBeforeSymlinkResolution}${search}${hash}`;
|
|
3751
|
+
}
|
|
3752
|
+
const urlAfterSymlinkResolution = resolveSymlink(
|
|
3753
|
+
urlBeforeSymlinkResolution,
|
|
3754
|
+
);
|
|
3755
|
+
if (urlAfterSymlinkResolution !== urlBeforeSymlinkResolution) {
|
|
3756
|
+
reference.leadsToASymlink = true;
|
|
3757
|
+
// reference.baseUrl = urlBeforeSymlinkResolution;
|
|
3758
|
+
}
|
|
3759
|
+
const resolvedUrl = `${urlAfterSymlinkResolution}${search}${hash}`;
|
|
3760
|
+
return resolvedUrl;
|
|
3761
|
+
},
|
|
3762
|
+
};
|
|
3763
|
+
};
|
|
3764
|
+
|
|
3765
|
+
const applyFsStatEffectsOnUrlObject = (urlObject, fsStat) => {
|
|
3766
|
+
if (!fsStat) {
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
const { pathname } = urlObject;
|
|
3770
|
+
const pathnameUsesTrailingSlash = pathname.endsWith("/");
|
|
3771
|
+
// force trailing slash on directories
|
|
3772
|
+
if (fsStat.isDirectory()) {
|
|
3773
|
+
if (!pathnameUsesTrailingSlash) {
|
|
3774
|
+
urlObject.pathname = `${pathname}/`;
|
|
3775
|
+
}
|
|
3776
|
+
} else if (pathnameUsesTrailingSlash) {
|
|
3777
|
+
// otherwise remove trailing slash if any
|
|
3778
|
+
// a warning here? (because it's strange to reference a file with a trailing slash)
|
|
3779
|
+
urlObject.pathname = pathname.slice(0, -1);
|
|
3780
|
+
}
|
|
3781
|
+
};
|
|
3782
|
+
|
|
3783
|
+
const resolveSymlink = (fileUrl) => {
|
|
3784
|
+
const urlObject = new URL(fileUrl);
|
|
3785
|
+
const realpath = realpathSync(urlObject);
|
|
3786
|
+
const realUrlObject = pathToFileURL(realpath);
|
|
3787
|
+
if (urlObject.pathname.endsWith("/")) {
|
|
3788
|
+
realUrlObject.pathname += `/`;
|
|
3789
|
+
}
|
|
3790
|
+
return realUrlObject.href;
|
|
3791
|
+
};
|
|
3792
|
+
|
|
3793
|
+
const getDirectoryEntryFileUrl = (directoryUrl) => {
|
|
3794
|
+
const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
|
|
3795
|
+
if (existsSync(indexHtmlFileUrl)) {
|
|
3796
|
+
return indexHtmlFileUrl.href;
|
|
3797
|
+
}
|
|
3798
|
+
const filename = urlToFilename(directoryUrl);
|
|
3799
|
+
const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
|
|
3800
|
+
if (existsSync(htmlFileUrlCandidate)) {
|
|
3801
|
+
return htmlFileUrlCandidate.href;
|
|
3802
|
+
}
|
|
3803
|
+
return null;
|
|
3804
|
+
};
|
|
3805
|
+
const getSpaFallbackUrl = (reference) => {
|
|
3806
|
+
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
|
|
3807
|
+
reference.ownerUrlInfo.context;
|
|
3808
|
+
if (!requestedUrl) {
|
|
3809
|
+
// the SPA fallback answers a request; during build there is none
|
|
3810
|
+
return null;
|
|
3811
|
+
}
|
|
3812
|
+
const spaFallbackFileUrls = listSpaFallbackFileUrls(requestedUrl, {
|
|
3813
|
+
rootDirectoryUrl,
|
|
3814
|
+
mainFilePath,
|
|
3815
|
+
});
|
|
3816
|
+
for (const spaFallbackFileUrl of spaFallbackFileUrls) {
|
|
3817
|
+
if (existsSync(new URL(spaFallbackFileUrl))) {
|
|
3818
|
+
return spaFallbackFileUrl;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
// none exists: the main file it is, and the 404 answering for it lists
|
|
3822
|
+
// what was tried (see directory listing)
|
|
3823
|
+
return new URL(mainFilePath, rootDirectoryUrl).href;
|
|
3824
|
+
};
|
|
3825
|
+
// The html files that can answer a route, closest first: the entry file of
|
|
3826
|
+
// the route's own directory ("index.html", then "<dirname>.html"), then of
|
|
3827
|
+
// each directory above it up to the server root, then the main file.
|
|
3828
|
+
const listSpaFallbackFileUrls = (
|
|
3829
|
+
requestedUrl,
|
|
3830
|
+
{ rootDirectoryUrl, mainFilePath },
|
|
3831
|
+
) => {
|
|
3832
|
+
const fileUrls = [];
|
|
3833
|
+
let directoryUrl = new URL("./", requestedUrl).href;
|
|
3834
|
+
while (urlIsOrIsInsideOf(directoryUrl, rootDirectoryUrl)) {
|
|
3835
|
+
fileUrls.push(new URL("index.html", directoryUrl).href);
|
|
3836
|
+
const filename = urlToFilename(directoryUrl);
|
|
3837
|
+
if (filename) {
|
|
3838
|
+
fileUrls.push(new URL(`${filename}.html`, directoryUrl).href);
|
|
3839
|
+
}
|
|
3840
|
+
if (directoryUrl === String(rootDirectoryUrl)) {
|
|
3841
|
+
break;
|
|
3842
|
+
}
|
|
3843
|
+
directoryUrl = new URL("../", directoryUrl).href;
|
|
3844
|
+
}
|
|
3845
|
+
const mainFileUrl = new URL(mainFilePath, rootDirectoryUrl).href;
|
|
3846
|
+
if (!fileUrls.includes(mainFileUrl)) {
|
|
3847
|
+
fileUrls.push(mainFileUrl);
|
|
3848
|
+
}
|
|
3849
|
+
return fileUrls;
|
|
3850
|
+
};
|
|
3851
|
+
|
|
3619
3852
|
/*
|
|
3620
3853
|
* NICE TO HAVE:
|
|
3621
3854
|
*
|
|
@@ -3993,6 +4226,24 @@ const generateDirectoryListingInjection = (
|
|
|
3993
4226
|
filePathExisting: `/${filePathExisting}`,
|
|
3994
4227
|
filePathNotFound,
|
|
3995
4228
|
});
|
|
4229
|
+
// a url without extension nor trailing slash is a route: in spa mode it
|
|
4230
|
+
// was answered with the closest html file, the 404 means none was found
|
|
4231
|
+
const urlNotFoundObject = new URL(urlNotFound);
|
|
4232
|
+
if (
|
|
4233
|
+
spa &&
|
|
4234
|
+
!urlToExtension(urlNotFoundObject) &&
|
|
4235
|
+
!urlNotFoundObject.pathname.endsWith("/")
|
|
4236
|
+
) {
|
|
4237
|
+
enoentDetails.spaFallbackFilePaths = listSpaFallbackFileUrls(
|
|
4238
|
+
urlNotFound,
|
|
4239
|
+
{ rootDirectoryUrl: serverRootDirectoryUrl, mainFilePath },
|
|
4240
|
+
).map((fileUrl) =>
|
|
4241
|
+
FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
4242
|
+
fileUrl,
|
|
4243
|
+
serverRootDirectoryUrl,
|
|
4244
|
+
),
|
|
4245
|
+
);
|
|
4246
|
+
}
|
|
3996
4247
|
}
|
|
3997
4248
|
|
|
3998
4249
|
return {
|
|
@@ -4064,235 +4315,18 @@ const getDirectoryContentItems = ({
|
|
|
4064
4315
|
return items;
|
|
4065
4316
|
};
|
|
4066
4317
|
|
|
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,
|
|
4318
|
+
const directoryContentMagicName = "...";
|
|
4319
|
+
|
|
4320
|
+
const jsenvPluginProtocolFile = ({
|
|
4321
|
+
spa = true,
|
|
4322
|
+
magicExtensions,
|
|
4323
|
+
magicDirectoryIndex,
|
|
4324
|
+
preserveSymlinks,
|
|
4325
|
+
directoryListing,
|
|
4326
|
+
rootDirectoryUrl,
|
|
4327
|
+
mainFilePath,
|
|
4328
|
+
packageDirectory,
|
|
4329
|
+
sourceFilesConfig,
|
|
4296
4330
|
}) => {
|
|
4297
4331
|
const listHtmlPages = createHtmlPageLister({ rootDirectoryUrl });
|
|
4298
4332
|
|
|
@@ -4434,7 +4468,16 @@ const jsenvPluginProtocolFile = ({
|
|
|
4434
4468
|
}
|
|
4435
4469
|
const serveFile = (url) => {
|
|
4436
4470
|
const contentType = CONTENT_TYPE.fromUrlExtension(url);
|
|
4437
|
-
const
|
|
4471
|
+
const urlObject = new URL(url);
|
|
4472
|
+
// taken before the read: a write landing in between moves the
|
|
4473
|
+
// stat past this one, so the dev server sees the content as
|
|
4474
|
+
// outdated (see isValid in the dev server) rather than the reverse
|
|
4475
|
+
const fileStat = statSync(urlObject);
|
|
4476
|
+
const fileBuffer = readFileSync(urlObject);
|
|
4477
|
+
urlInfo.data.fileStat = {
|
|
4478
|
+
mtimeMs: fileStat.mtimeMs,
|
|
4479
|
+
size: fileStat.size,
|
|
4480
|
+
};
|
|
4438
4481
|
const content = CONTENT_TYPE.isTextual(contentType)
|
|
4439
4482
|
? String(fileBuffer)
|
|
4440
4483
|
: fileBuffer;
|
|
@@ -5934,7 +5977,7 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
5934
5977
|
name: "jsenv:import_meta_scenario",
|
|
5935
5978
|
appliesDuring: "*",
|
|
5936
5979
|
transformUrlContent: {
|
|
5937
|
-
js_module:
|
|
5980
|
+
js_module: (urlInfo) => {
|
|
5938
5981
|
// Do not scan node modules for import.meta.dev/import.meta.build
|
|
5939
5982
|
// - node modules won't have this in their code
|
|
5940
5983
|
// - ;or should use other an other technic as this one won't be available
|
|
@@ -5950,40 +5993,42 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
5950
5993
|
) {
|
|
5951
5994
|
return null;
|
|
5952
5995
|
}
|
|
5953
|
-
const {
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5996
|
+
const importMetaScenarioNodes = { dev: [], build: [] };
|
|
5997
|
+
visitJsAst(urlInfo.contentAst, {
|
|
5998
|
+
MemberExpression: (node) => {
|
|
5999
|
+
const name = getImportMetaPropertyName(node);
|
|
6000
|
+
if (name === "dev" || name === "build") {
|
|
6001
|
+
importMetaScenarioNodes[name].push(node);
|
|
6002
|
+
}
|
|
6003
|
+
},
|
|
5959
6004
|
});
|
|
5960
|
-
const { dev
|
|
6005
|
+
const { dev, build } = importMetaScenarioNodes;
|
|
5961
6006
|
const replacements = [];
|
|
5962
|
-
const replace = (
|
|
5963
|
-
replacements.push({
|
|
6007
|
+
const replace = (node, value) => {
|
|
6008
|
+
replacements.push({ node, value });
|
|
5964
6009
|
};
|
|
5965
6010
|
if (urlInfo.context.build) {
|
|
5966
6011
|
// during build ensure replacement for tree-shaking
|
|
5967
|
-
dev.forEach((
|
|
5968
|
-
replace(
|
|
6012
|
+
dev.forEach((node) => {
|
|
6013
|
+
replace(node, "undefined");
|
|
5969
6014
|
});
|
|
5970
|
-
build.forEach((
|
|
5971
|
-
replace(
|
|
6015
|
+
build.forEach((node) => {
|
|
6016
|
+
replace(node, "true");
|
|
5972
6017
|
});
|
|
5973
6018
|
} else {
|
|
5974
6019
|
// during dev we can let "import.meta.build" untouched
|
|
5975
6020
|
// it will be evaluated to undefined.
|
|
5976
6021
|
// Moreover it can be surprising to see some "undefined"
|
|
5977
6022
|
// when source file contains "import.meta.build"
|
|
5978
|
-
dev.forEach((
|
|
5979
|
-
replace(
|
|
6023
|
+
dev.forEach((node) => {
|
|
6024
|
+
replace(node, "true");
|
|
5980
6025
|
});
|
|
5981
6026
|
}
|
|
5982
6027
|
const magicSource = createMagicSource(urlInfo.content);
|
|
5983
|
-
replacements.forEach(({
|
|
6028
|
+
replacements.forEach(({ node, value }) => {
|
|
5984
6029
|
magicSource.replace({
|
|
5985
|
-
start:
|
|
5986
|
-
end:
|
|
6030
|
+
start: node.start,
|
|
6031
|
+
end: node.end,
|
|
5987
6032
|
replacement: value,
|
|
5988
6033
|
});
|
|
5989
6034
|
});
|
|
@@ -5993,42 +6038,6 @@ const jsenvPluginImportMetaScenarios = () => {
|
|
|
5993
6038
|
};
|
|
5994
6039
|
};
|
|
5995
6040
|
|
|
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
6041
|
/*
|
|
6033
6042
|
* Source code can contain the following
|
|
6034
6043
|
* - __DEV__
|
|
@@ -6127,15 +6136,10 @@ const jsenvPluginImportMetaCss = () => {
|
|
|
6127
6136
|
if (!urlInfo.content.includes("import.meta.css")) {
|
|
6128
6137
|
return null;
|
|
6129
6138
|
}
|
|
6130
|
-
const
|
|
6131
|
-
|
|
6132
|
-
input: urlInfo.content,
|
|
6133
|
-
inputIsJsModule: true,
|
|
6134
|
-
inputUrl: urlInfo.originalUrl,
|
|
6135
|
-
outputUrl: urlInfo.generatedUrl,
|
|
6139
|
+
const importMetaCssNode = visitJsAstUntil(urlInfo.contentAst, {
|
|
6140
|
+
MemberExpression: (node) => getImportMetaPropertyName(node) === "css",
|
|
6136
6141
|
});
|
|
6137
|
-
|
|
6138
|
-
if (!usesImportMetaCss) {
|
|
6142
|
+
if (!importMetaCssNode) {
|
|
6139
6143
|
return null;
|
|
6140
6144
|
}
|
|
6141
6145
|
if (urlInfo.context.build) {
|
|
@@ -6156,6 +6160,8 @@ const jsenvPluginImportMetaCss = () => {
|
|
|
6156
6160
|
inputIsJsModule: true,
|
|
6157
6161
|
inputUrl: urlInfo.originalUrl,
|
|
6158
6162
|
outputUrl: urlInfo.generatedUrl,
|
|
6163
|
+
// the map would be dropped: the content sent back carries none
|
|
6164
|
+
options: { sourceMaps: false },
|
|
6159
6165
|
});
|
|
6160
6166
|
if (code === urlInfo.content) {
|
|
6161
6167
|
// all assignments were already in array form (pre-built file) — nothing to do
|
|
@@ -6215,37 +6221,6 @@ const babelPluginRewriteImportMetaCssAssignment = (
|
|
|
6215
6221
|
};
|
|
6216
6222
|
};
|
|
6217
6223
|
|
|
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
6224
|
const injectImportMetaCss = (
|
|
6250
6225
|
urlInfo,
|
|
6251
6226
|
{ content, importFrom, importName, importAs, hot },
|
|
@@ -6288,104 +6263,6 @@ ${importVariableName}(import.meta);
|
|
|
6288
6263
|
};
|
|
6289
6264
|
};
|
|
6290
6265
|
|
|
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
6266
|
// Some "smart" default applied to decide what should hot reload / fullreload:
|
|
6390
6267
|
// By default:
|
|
6391
6268
|
// - hot reload on <img src="./image.png" />
|
|
@@ -6533,6 +6410,75 @@ const htmlNodeCanHotReload = (node) => {
|
|
|
6533
6410
|
].includes(node.nodeName);
|
|
6534
6411
|
};
|
|
6535
6412
|
|
|
6413
|
+
const analyzeImportMetaHot = (ast) => {
|
|
6414
|
+
const importMetaHotNodes = [];
|
|
6415
|
+
let hotDecline = false;
|
|
6416
|
+
let hotAcceptSelf = false;
|
|
6417
|
+
let hotAcceptSpecifiers = [];
|
|
6418
|
+
visitJsAst(ast, {
|
|
6419
|
+
MemberExpression: (node) => {
|
|
6420
|
+
if (getImportMetaPropertyName(node) === "hot") {
|
|
6421
|
+
importMetaHotNodes.push(node);
|
|
6422
|
+
}
|
|
6423
|
+
},
|
|
6424
|
+
CallExpression: (node) => {
|
|
6425
|
+
const methodName = getImportMetaHotMethodName(node);
|
|
6426
|
+
if (methodName === "accept") {
|
|
6427
|
+
const args = node.arguments;
|
|
6428
|
+
if (args.length === 0) {
|
|
6429
|
+
hotAcceptSelf = true;
|
|
6430
|
+
return;
|
|
6431
|
+
}
|
|
6432
|
+
const [firstArg] = args;
|
|
6433
|
+
if (isStringLiteral(firstArg)) {
|
|
6434
|
+
hotAcceptSpecifiers = [firstArg.value];
|
|
6435
|
+
return;
|
|
6436
|
+
}
|
|
6437
|
+
if (firstArg.type === "ArrayExpression") {
|
|
6438
|
+
hotAcceptSpecifiers = firstArg.elements.map((element) => {
|
|
6439
|
+
if (!isStringLiteral(element)) {
|
|
6440
|
+
throw new Error(
|
|
6441
|
+
`all array elements must be strings in "import.meta.hot.accept(array)"`,
|
|
6442
|
+
);
|
|
6443
|
+
}
|
|
6444
|
+
return element.value;
|
|
6445
|
+
});
|
|
6446
|
+
return;
|
|
6447
|
+
}
|
|
6448
|
+
// accept first arg can be "anything" such as
|
|
6449
|
+
// `const cb = () => {}; import.meta.hot.accept(cb)`
|
|
6450
|
+
hotAcceptSelf = true;
|
|
6451
|
+
return;
|
|
6452
|
+
}
|
|
6453
|
+
if (methodName === "decline") {
|
|
6454
|
+
hotDecline = true;
|
|
6455
|
+
}
|
|
6456
|
+
},
|
|
6457
|
+
});
|
|
6458
|
+
return {
|
|
6459
|
+
importMetaHotNodes,
|
|
6460
|
+
hotDecline,
|
|
6461
|
+
hotAcceptSelf,
|
|
6462
|
+
hotAcceptSpecifiers,
|
|
6463
|
+
};
|
|
6464
|
+
};
|
|
6465
|
+
|
|
6466
|
+
// "import.meta.hot.<method>(...)"
|
|
6467
|
+
const getImportMetaHotMethodName = (callNode) => {
|
|
6468
|
+
const { callee } = callNode;
|
|
6469
|
+
if (callee.type !== "MemberExpression" || callee.computed) {
|
|
6470
|
+
return null;
|
|
6471
|
+
}
|
|
6472
|
+
if (getImportMetaPropertyName(callee.object) !== "hot") {
|
|
6473
|
+
return null;
|
|
6474
|
+
}
|
|
6475
|
+
return callee.property.name;
|
|
6476
|
+
};
|
|
6477
|
+
|
|
6478
|
+
const isStringLiteral = (node) => {
|
|
6479
|
+
return node.type === "Literal" && typeof node.value === "string";
|
|
6480
|
+
};
|
|
6481
|
+
|
|
6536
6482
|
const jsenvPluginImportMetaHot = () => {
|
|
6537
6483
|
const importMetaHotClientFileUrl = import.meta
|
|
6538
6484
|
.resolve("../client/import_meta_hot/import_meta_hot.js");
|
|
@@ -6581,7 +6527,7 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
6581
6527
|
cssUrlInfo.data.hotAcceptSelf = false;
|
|
6582
6528
|
cssUrlInfo.data.hotAcceptDependencies = [];
|
|
6583
6529
|
},
|
|
6584
|
-
js_module:
|
|
6530
|
+
js_module: (urlInfo) => {
|
|
6585
6531
|
// Do not scan node modules for import.meta.hot
|
|
6586
6532
|
// - unlikely to be there
|
|
6587
6533
|
// - we don't watch node modules (too expensive)
|
|
@@ -6593,27 +6539,22 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
6593
6539
|
if (!urlInfo.content.includes("import.meta.hot")) {
|
|
6594
6540
|
return null;
|
|
6595
6541
|
}
|
|
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
6542
|
const {
|
|
6604
|
-
|
|
6543
|
+
importMetaHotNodes,
|
|
6605
6544
|
hotDecline,
|
|
6606
6545
|
hotAcceptSelf,
|
|
6607
|
-
|
|
6608
|
-
} =
|
|
6546
|
+
hotAcceptSpecifiers,
|
|
6547
|
+
} = analyzeImportMetaHot(urlInfo.contentAst);
|
|
6609
6548
|
urlInfo.data.hotDecline = hotDecline;
|
|
6610
6549
|
urlInfo.data.hotAcceptSelf = hotAcceptSelf;
|
|
6611
|
-
urlInfo.data.hotAcceptDependencies =
|
|
6612
|
-
|
|
6550
|
+
urlInfo.data.hotAcceptDependencies = hotAcceptSpecifiers.map(
|
|
6551
|
+
(specifier) => resolveHotAcceptSpecifier(urlInfo, specifier),
|
|
6552
|
+
);
|
|
6553
|
+
if (importMetaHotNodes.length === 0) {
|
|
6613
6554
|
return null;
|
|
6614
6555
|
}
|
|
6615
6556
|
if (urlInfo.context.build) {
|
|
6616
|
-
return removeImportMetaHots(urlInfo,
|
|
6557
|
+
return removeImportMetaHots(urlInfo, importMetaHotNodes);
|
|
6617
6558
|
}
|
|
6618
6559
|
return injectImportMetaHot(urlInfo, importMetaHotClientFileUrl);
|
|
6619
6560
|
},
|
|
@@ -6621,12 +6562,32 @@ const jsenvPluginImportMetaHot = () => {
|
|
|
6621
6562
|
};
|
|
6622
6563
|
};
|
|
6623
6564
|
|
|
6624
|
-
|
|
6565
|
+
// The specifier given to import.meta.hot.accept() is one the file imports,
|
|
6566
|
+
// so its url is known from that import (autoreload compares urls). A
|
|
6567
|
+
// specifier the file does not import accepts nothing: it is kept as a plain
|
|
6568
|
+
// url, never resolved as a dependency (that could throw on a bare specifier).
|
|
6569
|
+
const resolveHotAcceptSpecifier = (urlInfo, specifier) => {
|
|
6570
|
+
for (const referenceToOther of urlInfo.referenceToOthersSet) {
|
|
6571
|
+
if (
|
|
6572
|
+
referenceToOther.type === "js_import" &&
|
|
6573
|
+
referenceToOther.specifier === specifier
|
|
6574
|
+
) {
|
|
6575
|
+
return referenceToOther.url;
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
try {
|
|
6579
|
+
return new URL(specifier, urlInfo.url).href;
|
|
6580
|
+
} catch {
|
|
6581
|
+
return specifier;
|
|
6582
|
+
}
|
|
6583
|
+
};
|
|
6584
|
+
|
|
6585
|
+
const removeImportMetaHots = (urlInfo, importMetaHotNodes) => {
|
|
6625
6586
|
const magicSource = createMagicSource(urlInfo.content);
|
|
6626
|
-
|
|
6587
|
+
importMetaHotNodes.forEach((node) => {
|
|
6627
6588
|
magicSource.replace({
|
|
6628
|
-
start:
|
|
6629
|
-
end:
|
|
6589
|
+
start: node.start,
|
|
6590
|
+
end: node.end,
|
|
6630
6591
|
replacement: "undefined",
|
|
6631
6592
|
});
|
|
6632
6593
|
});
|
|
@@ -7155,6 +7116,192 @@ const jsenvPluginAutoreload = ({
|
|
|
7155
7116
|
];
|
|
7156
7117
|
};
|
|
7157
7118
|
|
|
7119
|
+
const urlSpecifierEncoding = {
|
|
7120
|
+
encode: (reference) => {
|
|
7121
|
+
const { generatedSpecifier } = reference;
|
|
7122
|
+
if (generatedSpecifier.then) {
|
|
7123
|
+
return generatedSpecifier.then((value) => {
|
|
7124
|
+
reference.generatedSpecifier = value;
|
|
7125
|
+
return urlSpecifierEncoding.encode(reference);
|
|
7126
|
+
});
|
|
7127
|
+
}
|
|
7128
|
+
// allow plugin to return a function to bypas default formatting
|
|
7129
|
+
// (which is to use JSON.stringify when url is referenced inside js)
|
|
7130
|
+
if (typeof generatedSpecifier === "function") {
|
|
7131
|
+
return generatedSpecifier();
|
|
7132
|
+
}
|
|
7133
|
+
const formatter = formatters[reference.type];
|
|
7134
|
+
const value = formatter
|
|
7135
|
+
? formatter.encode(generatedSpecifier)
|
|
7136
|
+
: generatedSpecifier;
|
|
7137
|
+
if (reference.escape) {
|
|
7138
|
+
return reference.escape(value);
|
|
7139
|
+
}
|
|
7140
|
+
return value;
|
|
7141
|
+
},
|
|
7142
|
+
decode: (reference) => {
|
|
7143
|
+
const formatter = formatters[reference.type];
|
|
7144
|
+
return formatter
|
|
7145
|
+
? formatter.decode(reference.generatedSpecifier)
|
|
7146
|
+
: reference.generatedSpecifier;
|
|
7147
|
+
},
|
|
7148
|
+
};
|
|
7149
|
+
const formatters = {
|
|
7150
|
+
"js_import": { encode: JSON.stringify, decode: JSON.parse },
|
|
7151
|
+
"js_url": { encode: JSON.stringify, decode: JSON.parse },
|
|
7152
|
+
"css_@import": { encode: JSON.stringify, decode: JSON.stringify },
|
|
7153
|
+
// https://github.com/webpack-contrib/css-loader/pull/627/files
|
|
7154
|
+
"css_url": {
|
|
7155
|
+
encode: (url) => {
|
|
7156
|
+
// If url is already wrapped in quotes, remove them
|
|
7157
|
+
url = formatters.css_url.decode(url);
|
|
7158
|
+
// Should url be wrapped?
|
|
7159
|
+
// See https://drafts.csswg.org/css-values-3/#urls
|
|
7160
|
+
if (/["'() \t\n]/.test(url)) {
|
|
7161
|
+
return `"${url.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
7162
|
+
}
|
|
7163
|
+
return url;
|
|
7164
|
+
},
|
|
7165
|
+
decode: (url) => {
|
|
7166
|
+
const firstChar = url[0];
|
|
7167
|
+
const lastChar = url[url.length - 1];
|
|
7168
|
+
if (firstChar === `"` && lastChar === `"`) {
|
|
7169
|
+
return url.slice(1, -1);
|
|
7170
|
+
}
|
|
7171
|
+
if (firstChar === `'` && lastChar === `'`) {
|
|
7172
|
+
return url.slice(1, -1);
|
|
7173
|
+
}
|
|
7174
|
+
return url;
|
|
7175
|
+
},
|
|
7176
|
+
},
|
|
7177
|
+
};
|
|
7178
|
+
|
|
7179
|
+
/*
|
|
7180
|
+
* Tells the browser the static import graph of a page's module scripts, as
|
|
7181
|
+
* `Link: <url>; rel=modulepreload` response headers, so it can fetch every
|
|
7182
|
+
* module as soon as the page's headers arrive, without waiting to discover
|
|
7183
|
+
* them level by level (each level costing a round trip and, the first time,
|
|
7184
|
+
* a cook).
|
|
7185
|
+
*
|
|
7186
|
+
* Only what the graph already knows is listed: a module never cooked has no
|
|
7187
|
+
* known imports. The header is computed at each response from the graph as it
|
|
7188
|
+
* is then: a page gets its full list from its second load on, the first load
|
|
7189
|
+
* keeps the plain waterfall.
|
|
7190
|
+
*
|
|
7191
|
+
* Headers rather than <link> elements written into the page: the page content
|
|
7192
|
+
* stays what it is (served from memory, revalidated by etag) and the list can
|
|
7193
|
+
* grow without cooking the page again. And not references either: a reference
|
|
7194
|
+
* from the html to every module would make the html a dependent of each of
|
|
7195
|
+
* them, and a hot update propagating up the importers would reach the html
|
|
7196
|
+
* (which declines) instead of stopping at the module accepting it.
|
|
7197
|
+
*
|
|
7198
|
+
* OFF BY DEFAULT, to be enabled once the dev server speaks http/2 or http/3.
|
|
7199
|
+
* Over http/1.1 it makes pages slower — measured on a 500 modules page: first
|
|
7200
|
+
* paint 32ms -> 868ms, load 626ms -> 985ms. Knowing the urls earlier gives the
|
|
7201
|
+
* browser no extra resource: what it has is 6 connections per origin, and
|
|
7202
|
+
* every request goes through them in the order it was learned.
|
|
7203
|
+
* - Without the header, requests come in execution order: the parser asks
|
|
7204
|
+
* for the render-blocking supervisor script first and gets a connection at
|
|
7205
|
+
* once; transfer and execution overlap, a module executing and discovering
|
|
7206
|
+
* its imports while the others download. On localhost the server answers
|
|
7207
|
+
* from memory in 0.1ms, so the 6 connections are busy from the start.
|
|
7208
|
+
* - With the header, hundreds of requests take the 6 connections before the
|
|
7209
|
+
* parser asks for the supervisor script, which then waits for a free
|
|
7210
|
+
* connection behind 1MB+ transfers (520ms measured, 2s with 20ms of RTT).
|
|
7211
|
+
* Meanwhile nothing renders and nothing executes: transfer, then execution,
|
|
7212
|
+
* serialized. http/1.1 cannot reprioritize a queued request, and a
|
|
7213
|
+
* modulepreload has the same priority as a script.
|
|
7214
|
+
* - And there is nothing to hide: the discovery latency a preload removes is
|
|
7215
|
+
* ~1ms per graph level on localhost; with a real RTT a wide graph (more
|
|
7216
|
+
* than 6 modules pending at any time) keeps the 6 connections saturated
|
|
7217
|
+
* anyway, and the load takes requests / 6 * RTT whatever the order.
|
|
7218
|
+
* It pays off for a deep and narrow graph (connections idle, waiting for a
|
|
7219
|
+
* response to learn the next level), and over http/2 or http/3: no
|
|
7220
|
+
* connection limit, and the browser prioritizes the blocking script.
|
|
7221
|
+
*/
|
|
7222
|
+
|
|
7223
|
+
|
|
7224
|
+
const jsenvPluginModulepreload = () => {
|
|
7225
|
+
return {
|
|
7226
|
+
name: "jsenv:modulepreload",
|
|
7227
|
+
appliesDuring: "dev",
|
|
7228
|
+
augmentResponse: ({ urlInfo }) => {
|
|
7229
|
+
if (urlInfo.type !== "html") {
|
|
7230
|
+
return null;
|
|
7231
|
+
}
|
|
7232
|
+
const hrefs = collectStaticImportHrefs(urlInfo);
|
|
7233
|
+
if (hrefs.length === 0) {
|
|
7234
|
+
return null;
|
|
7235
|
+
}
|
|
7236
|
+
return {
|
|
7237
|
+
headers: {
|
|
7238
|
+
link: hrefs.map((href) => `<${href}>; rel=modulepreload`).join(", "),
|
|
7239
|
+
},
|
|
7240
|
+
};
|
|
7241
|
+
},
|
|
7242
|
+
};
|
|
7243
|
+
};
|
|
7244
|
+
|
|
7245
|
+
const collectStaticImportHrefs = (htmlUrlInfo) => {
|
|
7246
|
+
const hrefSet = new Set();
|
|
7247
|
+
const visitedSet = new Set();
|
|
7248
|
+
const addHref = (reference) => {
|
|
7249
|
+
const specifier = urlSpecifierEncoding.decode(reference);
|
|
7250
|
+
if (typeof specifier !== "string") {
|
|
7251
|
+
return;
|
|
7252
|
+
}
|
|
7253
|
+
// "?hot" belongs to the request that cooked the importer, the page loading
|
|
7254
|
+
// now imports the url without it
|
|
7255
|
+
hrefSet.add(injectQueryParamsIntoSpecifier(specifier, { hot: undefined }));
|
|
7256
|
+
};
|
|
7257
|
+
const visitJsModule = (jsModuleUrlInfo) => {
|
|
7258
|
+
if (visitedSet.has(jsModuleUrlInfo)) {
|
|
7259
|
+
return;
|
|
7260
|
+
}
|
|
7261
|
+
visitedSet.add(jsModuleUrlInfo);
|
|
7262
|
+
for (const reference of jsModuleUrlInfo.referenceToOthersSet) {
|
|
7263
|
+
if (
|
|
7264
|
+
reference.type !== "js_import" ||
|
|
7265
|
+
reference.isWeak ||
|
|
7266
|
+
reference.isImplicit ||
|
|
7267
|
+
!STATIC_IMPORT_SUBTYPES.includes(reference.subtype) ||
|
|
7268
|
+
!reference.url.startsWith("file:")
|
|
7269
|
+
) {
|
|
7270
|
+
continue;
|
|
7271
|
+
}
|
|
7272
|
+
const importedUrlInfo = reference.urlInfo;
|
|
7273
|
+
// never cooked: its content, hence its own imports, are unknown
|
|
7274
|
+
if (importedUrlInfo.type !== "js_module") {
|
|
7275
|
+
continue;
|
|
7276
|
+
}
|
|
7277
|
+
addHref(reference);
|
|
7278
|
+
visitJsModule(importedUrlInfo);
|
|
7279
|
+
}
|
|
7280
|
+
};
|
|
7281
|
+
for (const reference of htmlUrlInfo.referenceToOthersSet) {
|
|
7282
|
+
if (
|
|
7283
|
+
reference.type !== "script" ||
|
|
7284
|
+
reference.expectedType !== "js_module" ||
|
|
7285
|
+
// the scripts jsenv adds to every page are few, small and in memory
|
|
7286
|
+
reference.injected ||
|
|
7287
|
+
reference.isWeak
|
|
7288
|
+
) {
|
|
7289
|
+
continue;
|
|
7290
|
+
}
|
|
7291
|
+
const scriptUrlInfo = reference.urlInfo;
|
|
7292
|
+
if (scriptUrlInfo.type !== "js_module") {
|
|
7293
|
+
continue;
|
|
7294
|
+
}
|
|
7295
|
+
if (!reference.isInline && reference.url.startsWith("file:")) {
|
|
7296
|
+
addHref(reference);
|
|
7297
|
+
}
|
|
7298
|
+
visitJsModule(scriptUrlInfo);
|
|
7299
|
+
}
|
|
7300
|
+
return Array.from(hrefSet);
|
|
7301
|
+
};
|
|
7302
|
+
|
|
7303
|
+
const STATIC_IMPORT_SUBTYPES = ["import_static", "export_named", "export_all"];
|
|
7304
|
+
|
|
7158
7305
|
/*
|
|
7159
7306
|
* Tells the browser about the dependencies declared in package.json that
|
|
7160
7307
|
* node_modules does not match, so the page can say it is running with something
|
|
@@ -7796,6 +7943,7 @@ const getCorePlugins = ({
|
|
|
7796
7943
|
|
|
7797
7944
|
clientAutoreload,
|
|
7798
7945
|
clientAutoreloadOnServerRestart,
|
|
7946
|
+
modulepreload = false,
|
|
7799
7947
|
dependencyStatus,
|
|
7800
7948
|
cacheControl,
|
|
7801
7949
|
scenarioPlaceholders = true,
|
|
@@ -7897,6 +8045,7 @@ const getCorePlugins = ({
|
|
|
7897
8045
|
...(clientAutoreload && clientAutoreload.enabled
|
|
7898
8046
|
? [jsenvPluginAutoreload(clientAutoreload)]
|
|
7899
8047
|
: []),
|
|
8048
|
+
...(modulepreload ? [jsenvPluginModulepreload()] : []),
|
|
7900
8049
|
...(dependencyStatus
|
|
7901
8050
|
? [jsenvPluginDependencyStatus(dependencyStatus)]
|
|
7902
8051
|
: []),
|
|
@@ -8093,7 +8242,10 @@ const devServerPluginOmegaErrorHandler = () => {
|
|
|
8093
8242
|
status: 403,
|
|
8094
8243
|
};
|
|
8095
8244
|
}
|
|
8096
|
-
|
|
8245
|
+
// the dev server runs with canExposeSensitiveData: file paths are welcome
|
|
8246
|
+
return convertFileSystemErrorToResponseProperties(error, {
|
|
8247
|
+
canExposeSensitiveData: true,
|
|
8248
|
+
});
|
|
8097
8249
|
};
|
|
8098
8250
|
const response = getResponseForError();
|
|
8099
8251
|
if (!response) {
|
|
@@ -8279,10 +8431,9 @@ const prependJsClassicInJsClassic = (jsUrlInfo, urlInfoToPrepend) => {
|
|
|
8279
8431
|
const magicSource = createMagicSource(jsUrlInfo.content);
|
|
8280
8432
|
magicSource.prepend(`${urlInfoToPrepend.content}\n\n`);
|
|
8281
8433
|
const magicResult = magicSource.toContentAndSourcemap();
|
|
8282
|
-
const sourcemap =
|
|
8283
|
-
jsUrlInfo.sourcemap,
|
|
8284
|
-
|
|
8285
|
-
);
|
|
8434
|
+
const sourcemap = jsUrlInfo.context.sourcemapsEnabled
|
|
8435
|
+
? composeTwoSourcemaps(jsUrlInfo.sourcemap, magicResult.sourcemap)
|
|
8436
|
+
: null;
|
|
8286
8437
|
jsUrlInfo.mutateContent({
|
|
8287
8438
|
content: magicResult.content,
|
|
8288
8439
|
sourcemap,
|
|
@@ -8300,6 +8451,7 @@ const prependJsClassicInJsModule = async (jsUrlInfo, urlInfoToPrepend) => {
|
|
|
8300
8451
|
input: jsUrlInfo.content,
|
|
8301
8452
|
inputIsJsModule: true,
|
|
8302
8453
|
inputUrl: jsUrlInfo.originalUrl,
|
|
8454
|
+
options: { sourceMaps: jsUrlInfo.context.sourcemapsEnabled },
|
|
8303
8455
|
});
|
|
8304
8456
|
jsUrlInfo.mutateContent({
|
|
8305
8457
|
content: code,
|
|
@@ -8952,20 +9104,37 @@ const checkForDependencyRemovalEffects = (reference) => {
|
|
|
8952
9104
|
};
|
|
8953
9105
|
|
|
8954
9106
|
const traceFromUrlSite = (urlSite) => {
|
|
8955
|
-
const
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
: ""
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
9107
|
+
const { url, line, column, content } = urlSite;
|
|
9108
|
+
const trace = { url, line, column };
|
|
9109
|
+
// A file references many urls and an error on one of them is rare: the code
|
|
9110
|
+
// frame (and the message embedding it) is built the first time it is read,
|
|
9111
|
+
// not for every reference found.
|
|
9112
|
+
defineLazyProperty(trace, "codeFrame", () =>
|
|
9113
|
+
content ? generateContentFrame({ content, line, column }) : "",
|
|
9114
|
+
);
|
|
9115
|
+
defineLazyProperty(trace, "message", () => stringifyUrlSite(urlSite));
|
|
9116
|
+
return trace;
|
|
9117
|
+
};
|
|
9118
|
+
|
|
9119
|
+
const defineLazyProperty = (object, property, compute) => {
|
|
9120
|
+
const setValue = (value) => {
|
|
9121
|
+
Object.defineProperty(object, property, {
|
|
9122
|
+
enumerable: true,
|
|
9123
|
+
configurable: true,
|
|
9124
|
+
writable: true,
|
|
9125
|
+
value,
|
|
9126
|
+
});
|
|
8968
9127
|
};
|
|
9128
|
+
Object.defineProperty(object, property, {
|
|
9129
|
+
enumerable: true,
|
|
9130
|
+
configurable: true,
|
|
9131
|
+
get: () => {
|
|
9132
|
+
const value = compute();
|
|
9133
|
+
setValue(value);
|
|
9134
|
+
return value;
|
|
9135
|
+
},
|
|
9136
|
+
set: setValue,
|
|
9137
|
+
});
|
|
8969
9138
|
};
|
|
8970
9139
|
|
|
8971
9140
|
const adjustUrlSite = (urlInfo, { url, line, column }) => {
|
|
@@ -9227,66 +9396,6 @@ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced = (
|
|
|
9227
9396
|
seen.clear();
|
|
9228
9397
|
};
|
|
9229
9398
|
|
|
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
9399
|
const createUrlGraph = ({
|
|
9291
9400
|
rootDirectoryUrl,
|
|
9292
9401
|
kitchen,
|
|
@@ -9464,7 +9573,6 @@ const createUrlInfo = (url, context) => {
|
|
|
9464
9573
|
dereferencedTimestamp: 0,
|
|
9465
9574
|
originalContentEtag: null,
|
|
9466
9575
|
contentEtag: null,
|
|
9467
|
-
isWatched: false,
|
|
9468
9576
|
isValid: () => false,
|
|
9469
9577
|
data: {}, // plugins can put whatever they want here
|
|
9470
9578
|
referenceToOthersSet: new Set(),
|
|
@@ -10068,7 +10176,6 @@ const createUrlInfoTransformer = ({
|
|
|
10068
10176
|
contentAst, // undefined most of the time
|
|
10069
10177
|
contentEtag, // in practice always undefined
|
|
10070
10178
|
contentLength,
|
|
10071
|
-
sourcemap,
|
|
10072
10179
|
sourcemapIsWrong,
|
|
10073
10180
|
contentInjections,
|
|
10074
10181
|
} = transformations;
|
|
@@ -10095,11 +10202,14 @@ const createUrlInfoTransformer = ({
|
|
|
10095
10202
|
contentLength,
|
|
10096
10203
|
});
|
|
10097
10204
|
}
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10205
|
+
// "sourcemap" is read last, and only when it will be used: a plugin can
|
|
10206
|
+
// hand it back as a getter that generates the map on first read, so that
|
|
10207
|
+
// nothing is generated for a kitchen that throws sourcemaps away
|
|
10208
|
+
const sourcemap =
|
|
10209
|
+
mayHaveSourcemap(urlInfo) && shouldHandleSourcemap(urlInfo)
|
|
10210
|
+
? transformations.sourcemap
|
|
10211
|
+
: null;
|
|
10212
|
+
if (sourcemap) {
|
|
10103
10213
|
const sourcemapNormalized = normalizeSourcemap(urlInfo, sourcemap);
|
|
10104
10214
|
let currentSourcemap = urlInfo.sourcemap;
|
|
10105
10215
|
const finalSourcemap = composeTwoSourcemaps(
|
|
@@ -10132,6 +10242,14 @@ const createUrlInfoTransformer = ({
|
|
|
10132
10242
|
writeInsideOutDirectory(urlInfo);
|
|
10133
10243
|
};
|
|
10134
10244
|
|
|
10245
|
+
// Written synchronously on purpose, and measured: async is the tempting
|
|
10246
|
+
// choice, but here it loses. A cold load cooks hundreds of files; their
|
|
10247
|
+
// synchronous writes block the event loop ~160ms in total, while
|
|
10248
|
+
// asynchronous writes queue in the threadpool behind tens of MB of content
|
|
10249
|
+
// and sourcemaps, and a response that waits for its own write then waits
|
|
10250
|
+
// ~36ms on average (18s summed over 500 responses). Not waiting is not an
|
|
10251
|
+
// option either: the last write lands after the test that cooked it has
|
|
10252
|
+
// ended, and the side-effect snapshots lose it.
|
|
10135
10253
|
const writeInsideOutDirectory = (urlInfo) => {
|
|
10136
10254
|
// writing result inside ".jsenv" directory (debug purposes)
|
|
10137
10255
|
if (!outDirectoryUrl) {
|
|
@@ -10451,6 +10569,12 @@ const createKitchen = ({
|
|
|
10451
10569
|
INJECTIONS,
|
|
10452
10570
|
getPluginMeta: null,
|
|
10453
10571
|
sourcemaps,
|
|
10572
|
+
// a plugin producing a sourcemap can skip the work when it would be
|
|
10573
|
+
// thrown away (see shouldHandleSourcemap in url_info_transformations.js)
|
|
10574
|
+
sourcemapsEnabled:
|
|
10575
|
+
sourcemaps === "inline" ||
|
|
10576
|
+
sourcemaps === "file" ||
|
|
10577
|
+
sourcemaps === "programmatic",
|
|
10454
10578
|
outDirectoryUrl,
|
|
10455
10579
|
},
|
|
10456
10580
|
resolve: (specifier, importer = rootDirectoryUrl) => {
|
|
@@ -11282,11 +11406,10 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11282
11406
|
if (existing) {
|
|
11283
11407
|
return existing;
|
|
11284
11408
|
}
|
|
11285
|
-
const watchAssociations = URL_META.resolveAssociations(
|
|
11286
|
-
{ watch: stopWatchingSourceFiles.watchPatterns },
|
|
11287
|
-
sourceDirectoryUrl,
|
|
11288
|
-
);
|
|
11289
11409
|
let kitchen;
|
|
11410
|
+
// per url info, the file stat under which its content was last compared
|
|
11411
|
+
// with the file on disk and found identical (see isValid)
|
|
11412
|
+
const fileStatValidatedMap = new WeakMap();
|
|
11290
11413
|
clientFileChangeEventEmitter.on(({ url, event }) => {
|
|
11291
11414
|
const urlInfo = kitchen.graph.getUrlInfo(url);
|
|
11292
11415
|
if (urlInfo) {
|
|
@@ -11327,11 +11450,6 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11327
11450
|
packageDirectory,
|
|
11328
11451
|
});
|
|
11329
11452
|
kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
|
|
11330
|
-
const { watch } = URL_META.applyAssociations({
|
|
11331
|
-
url: urlInfoCreated.url,
|
|
11332
|
-
associations: watchAssociations,
|
|
11333
|
-
});
|
|
11334
|
-
urlInfoCreated.isWatched = watch;
|
|
11335
11453
|
// when an url depends on many others, we check all these (like package.json)
|
|
11336
11454
|
urlInfoCreated.isValid = () => {
|
|
11337
11455
|
const seenSet = new Set();
|
|
@@ -11351,37 +11469,64 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11351
11469
|
// was compared using etag and it has changed
|
|
11352
11470
|
return false;
|
|
11353
11471
|
}
|
|
11354
|
-
//
|
|
11355
|
-
//
|
|
11356
|
-
// the
|
|
11357
|
-
//
|
|
11358
|
-
//
|
|
11359
|
-
//
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11472
|
+
// The content held in memory is the file as it was read; a request
|
|
11473
|
+
// must never win a stale answer, whatever the watcher's latency,
|
|
11474
|
+
// and the ?v= a package.json decides even ends up in the browser's
|
|
11475
|
+
// immutable cache. So the file is checked on disk at every
|
|
11476
|
+
// validation — cheaply: a stat, compared with the one taken when
|
|
11477
|
+
// the content was read (or last found identical). Only a file
|
|
11478
|
+
// whose stat moved is read and hashed again.
|
|
11479
|
+
// Inline content (a <script> inside an html) has no file of its
|
|
11480
|
+
// own: it is as fresh as the html holding it, checked by the caller.
|
|
11481
|
+
if (!urlInfo.isInline) {
|
|
11482
|
+
let fileStat;
|
|
11363
11483
|
try {
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
return false;
|
|
11369
|
-
}
|
|
11484
|
+
fileStat = statSync(new URL(urlInfo.url), {
|
|
11485
|
+
throwIfNoEntry: false,
|
|
11486
|
+
});
|
|
11487
|
+
} catch {
|
|
11370
11488
|
return false;
|
|
11371
11489
|
}
|
|
11372
|
-
|
|
11373
|
-
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
11490
|
+
if (!fileStat) {
|
|
11374
11491
|
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
11492
|
return false;
|
|
11384
11493
|
}
|
|
11494
|
+
const fileStatKnown =
|
|
11495
|
+
fileStatValidatedMap.get(urlInfo) || urlInfo.data.fileStat;
|
|
11496
|
+
const fileUnchanged =
|
|
11497
|
+
fileStatKnown &&
|
|
11498
|
+
fileStatKnown.mtimeMs === fileStat.mtimeMs &&
|
|
11499
|
+
fileStatKnown.size === fileStat.size;
|
|
11500
|
+
if (!fileUnchanged) {
|
|
11501
|
+
let fileContentAsBuffer;
|
|
11502
|
+
try {
|
|
11503
|
+
fileContentAsBuffer = readFileSync(new URL(urlInfo.url));
|
|
11504
|
+
} catch (e) {
|
|
11505
|
+
if (e.code === "ENOENT") {
|
|
11506
|
+
urlInfo.onModified();
|
|
11507
|
+
return false;
|
|
11508
|
+
}
|
|
11509
|
+
return false;
|
|
11510
|
+
}
|
|
11511
|
+
const fileContentEtag = bufferToEtag(fileContentAsBuffer);
|
|
11512
|
+
if (fileContentEtag !== urlInfo.originalContentEtag) {
|
|
11513
|
+
fileStatValidatedMap.delete(urlInfo);
|
|
11514
|
+
urlInfo.onModified();
|
|
11515
|
+
// restore content to be able to compare it again later
|
|
11516
|
+
urlInfo.kitchen.urlInfoTransformer.setContent(
|
|
11517
|
+
urlInfo,
|
|
11518
|
+
String(fileContentAsBuffer),
|
|
11519
|
+
{
|
|
11520
|
+
contentEtag: fileContentEtag,
|
|
11521
|
+
},
|
|
11522
|
+
);
|
|
11523
|
+
return false;
|
|
11524
|
+
}
|
|
11525
|
+
fileStatValidatedMap.set(urlInfo, {
|
|
11526
|
+
mtimeMs: fileStat.mtimeMs,
|
|
11527
|
+
size: fileStat.size,
|
|
11528
|
+
});
|
|
11529
|
+
}
|
|
11385
11530
|
}
|
|
11386
11531
|
for (const implicitUrl of urlInfo.implicitUrlSet) {
|
|
11387
11532
|
const implicitUrlInfo = urlInfo.graph.getUrlInfo(implicitUrl);
|
|
@@ -11497,6 +11642,40 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11497
11642
|
const ifNoneMatch = request.headers["if-none-match"];
|
|
11498
11643
|
const inlineParentUrlInfo = urlInfo.findParentIfInline();
|
|
11499
11644
|
const urlInfoTargetedByCache = inlineParentUrlInfo || urlInfo;
|
|
11645
|
+
// The content held in memory is the response when it is finalized
|
|
11646
|
+
// and still valid. Content can be defined while a cook is still in
|
|
11647
|
+
// flight (a file watcher invalidation re-cooking in the background,
|
|
11648
|
+
// for instance): at that point it holds the raw fetched content,
|
|
11649
|
+
// transformations not applied yet. Serving that would send an html
|
|
11650
|
+
// without any of the injected scripts. An inline url info (a
|
|
11651
|
+
// <script> inside an html) is cooked again whenever the html
|
|
11652
|
+
// containing it is cooked: its content is as fresh as the html's,
|
|
11653
|
+
// so both must be valid.
|
|
11654
|
+
const hasFreshContent = (urlInfo) =>
|
|
11655
|
+
urlInfo.content !== undefined &&
|
|
11656
|
+
urlInfo.contentFinalized &&
|
|
11657
|
+
urlInfo.isValid();
|
|
11658
|
+
const memoryContentIsFresh = () =>
|
|
11659
|
+
inlineParentUrlInfo
|
|
11660
|
+
? hasFreshContent(urlInfo) && hasFreshContent(inlineParentUrlInfo)
|
|
11661
|
+
: hasFreshContent(urlInfo);
|
|
11662
|
+
// a 304 goes through the hooks too: its headers stand for the
|
|
11663
|
+
// cached response's, they must say the same
|
|
11664
|
+
const augmentResponse = (response) => {
|
|
11665
|
+
const augmentResponseInfo = {
|
|
11666
|
+
...kitchen.context,
|
|
11667
|
+
reference,
|
|
11668
|
+
urlInfo,
|
|
11669
|
+
};
|
|
11670
|
+
kitchen.jsenvPluginsController.callHooks(
|
|
11671
|
+
"augmentResponse",
|
|
11672
|
+
augmentResponseInfo,
|
|
11673
|
+
(returnValue) => {
|
|
11674
|
+
response = composeTwoResponses(response, returnValue);
|
|
11675
|
+
},
|
|
11676
|
+
);
|
|
11677
|
+
return response;
|
|
11678
|
+
};
|
|
11500
11679
|
const respondWithNotModified = () => {
|
|
11501
11680
|
const headers = {
|
|
11502
11681
|
"cache-control": `private,max-age=0,must-revalidate`,
|
|
@@ -11506,23 +11685,21 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11506
11685
|
headers[key] = urlInfo.headers[key];
|
|
11507
11686
|
}
|
|
11508
11687
|
});
|
|
11509
|
-
return {
|
|
11688
|
+
return augmentResponse({
|
|
11510
11689
|
status: 304,
|
|
11511
11690
|
headers,
|
|
11512
|
-
};
|
|
11691
|
+
});
|
|
11513
11692
|
};
|
|
11514
11693
|
|
|
11515
11694
|
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) {
|
|
11695
|
+
if (!urlInfo.error && ifNoneMatch) {
|
|
11520
11696
|
const [clientOriginalContentEtag, clientContentEtag] =
|
|
11521
11697
|
ifNoneMatch.split("_");
|
|
11522
11698
|
if (
|
|
11523
|
-
|
|
11699
|
+
urlInfoTargetedByCache.originalContentEtag ===
|
|
11700
|
+
clientOriginalContentEtag &&
|
|
11524
11701
|
urlInfo.contentEtag === clientContentEtag &&
|
|
11525
|
-
|
|
11702
|
+
memoryContentIsFresh()
|
|
11526
11703
|
) {
|
|
11527
11704
|
return respondWithNotModified();
|
|
11528
11705
|
}
|
|
@@ -11540,18 +11717,9 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11540
11717
|
// client etag to match).
|
|
11541
11718
|
const servableFromMemory =
|
|
11542
11719
|
!urlInfo.error &&
|
|
11543
|
-
!inlineParentUrlInfo &&
|
|
11544
11720
|
!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
11721
|
!cacheIsDisabledInResponseHeader(urlInfo) &&
|
|
11722
|
+
!cacheIsDisabledInResponseHeader(urlInfoTargetedByCache) &&
|
|
11555
11723
|
// a "?hot" request exists to bypass every cache, this one
|
|
11556
11724
|
// included: it must be cooked, because cooking is what rewrites
|
|
11557
11725
|
// its references so "?hot" cascades to the modified files below
|
|
@@ -11570,7 +11738,7 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11570
11738
|
// signal registry throwing on duplicate ids). Re-cooking under
|
|
11571
11739
|
// the normal request rewrites the references clean.
|
|
11572
11740
|
!urlInfo.contentCookedForHotRequest &&
|
|
11573
|
-
|
|
11741
|
+
memoryContentIsFresh();
|
|
11574
11742
|
if (!servableFromMemory) {
|
|
11575
11743
|
await urlInfo.cook({ request, reference });
|
|
11576
11744
|
urlInfo.contentCookedForHotRequest =
|
|
@@ -11628,19 +11796,7 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11628
11796
|
? { "served from memory cache": null }
|
|
11629
11797
|
: urlInfo.timing,
|
|
11630
11798
|
};
|
|
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;
|
|
11799
|
+
return augmentResponse(response);
|
|
11644
11800
|
} catch (error) {
|
|
11645
11801
|
const originalError = error ? error.cause || error : error;
|
|
11646
11802
|
if (originalError.asResponse) {
|
|
@@ -11718,6 +11874,14 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11718
11874
|
"cache-control": "no-store",
|
|
11719
11875
|
},
|
|
11720
11876
|
};
|
|
11877
|
+
} finally {
|
|
11878
|
+
// What the request put on the url info context is for this
|
|
11879
|
+
// request only: a cook happening later for another reason (the
|
|
11880
|
+
// html holding an inline script is cooked again) must not see a
|
|
11881
|
+
// stale "requestedUrl" and take the inline script for a direct
|
|
11882
|
+
// request, which would make it pick the content of a previous
|
|
11883
|
+
// reference (see jsenv:inline_content_fetcher).
|
|
11884
|
+
forgetRequestFromContext(urlInfo.context);
|
|
11721
11885
|
}
|
|
11722
11886
|
},
|
|
11723
11887
|
},
|
|
@@ -11727,6 +11891,15 @@ const devServerPluginServeSourceFiles = ({
|
|
|
11727
11891
|
return [devServerPluginRoutes, ...devServerJsenvPluginStore.allServerPlugins];
|
|
11728
11892
|
};
|
|
11729
11893
|
|
|
11894
|
+
const forgetRequestFromContext = (context) => {
|
|
11895
|
+
// own properties only: what is inherited from the owner context stays
|
|
11896
|
+
for (const key of ["request", "requestedUrl", "reference"]) {
|
|
11897
|
+
if (Object.hasOwn(context, key)) {
|
|
11898
|
+
delete context[key];
|
|
11899
|
+
}
|
|
11900
|
+
}
|
|
11901
|
+
};
|
|
11902
|
+
|
|
11730
11903
|
const cacheIsDisabledInResponseHeader = (urlInfo) => {
|
|
11731
11904
|
return (
|
|
11732
11905
|
urlInfo.headers["cache-control"] === "no-store" ||
|
|
@@ -11758,6 +11931,7 @@ const EXECUTED_BY_TEST_PLAN = process.argv.includes("--jsenv-test");
|
|
|
11758
11931
|
* @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
11932
|
* @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
11933
|
* @param {boolean} [params.supervisor=true] - Script supervisor (better error reporting).
|
|
11934
|
+
* @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
11935
|
* @param {boolean|object} [params.directoryListing=true] - Directory listing pages.
|
|
11762
11936
|
* @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
11937
|
* @param {object} [params.runtimeCompat] - Target runtimes; warns when dev code wouldn't survive the build.
|
|
@@ -11799,6 +11973,7 @@ const startDevServer = async ({
|
|
|
11799
11973
|
sourceFilesConfig = {},
|
|
11800
11974
|
clientAutoreload = true,
|
|
11801
11975
|
clientAutoreloadOnServerRestart = true,
|
|
11976
|
+
modulepreload = false,
|
|
11802
11977
|
// server-timing response headers: devtools show how the time to answer is
|
|
11803
11978
|
// spent (cook measures come from the kitchen, see urlInfo.timing). Entries
|
|
11804
11979
|
// under minDuration are dropped so a human reads the measures that matter;
|
|
@@ -11888,9 +12063,12 @@ const startDevServer = async ({
|
|
|
11888
12063
|
}
|
|
11889
12064
|
|
|
11890
12065
|
const logger = createLogger({ logLevel });
|
|
11891
|
-
const startDevServerTask = createTaskLog(
|
|
11892
|
-
|
|
11893
|
-
|
|
12066
|
+
const startDevServerTask = createTaskLog(
|
|
12067
|
+
`start dev server for ${humanizeSourceDirectory(sourceDirectoryUrl)}`,
|
|
12068
|
+
{
|
|
12069
|
+
disabled: !logger.levels.info,
|
|
12070
|
+
},
|
|
12071
|
+
);
|
|
11894
12072
|
|
|
11895
12073
|
const serverStopCallbackSet = new Set();
|
|
11896
12074
|
const serverStopAbortController = new AbortController();
|
|
@@ -11969,6 +12147,7 @@ const startDevServer = async ({
|
|
|
11969
12147
|
|
|
11970
12148
|
clientAutoreload,
|
|
11971
12149
|
clientAutoreloadOnServerRestart,
|
|
12150
|
+
modulepreload,
|
|
11972
12151
|
dependencyStatus: {
|
|
11973
12152
|
dependencyProblemEventEmitter,
|
|
11974
12153
|
getDependencyProblems: dependencyWatcher.getProblems,
|
|
@@ -12074,4 +12253,13 @@ const startDevServer = async ({
|
|
|
12074
12253
|
};
|
|
12075
12254
|
};
|
|
12076
12255
|
|
|
12256
|
+
// "./src/" when inside the current working directory, the full path otherwise
|
|
12257
|
+
const humanizeSourceDirectory = (sourceDirectoryUrl) => {
|
|
12258
|
+
const cwdUrl = pathToFileURL(`${process.cwd()}/`).href;
|
|
12259
|
+
if (urlIsOrIsInsideOf(sourceDirectoryUrl, cwdUrl)) {
|
|
12260
|
+
return `./${urlToRelativeUrl(sourceDirectoryUrl, cwdUrl)}`;
|
|
12261
|
+
}
|
|
12262
|
+
return urlToFileSystemPath(sourceDirectoryUrl);
|
|
12263
|
+
};
|
|
12264
|
+
|
|
12077
12265
|
export { startDevServer };
|