@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,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseHtml } from "@jsenv/ast";
|
|
2
2
|
import { createMagicSource } from "@jsenv/sourcemap";
|
|
3
3
|
|
|
4
|
-
import { babelPluginMetadataImportMetaHot } from "./babel_plugin_metadata_import_meta_hot.js";
|
|
5
4
|
import { collectHotDataFromHtmlAst } from "./html_hot_dependencies.js";
|
|
5
|
+
import { analyzeImportMetaHot } from "./import_meta_hot_analysis.js";
|
|
6
6
|
|
|
7
7
|
export const jsenvPluginImportMetaHot = () => {
|
|
8
8
|
const importMetaHotClientFileUrl = import.meta
|
|
@@ -52,7 +52,7 @@ export const jsenvPluginImportMetaHot = () => {
|
|
|
52
52
|
cssUrlInfo.data.hotAcceptSelf = false;
|
|
53
53
|
cssUrlInfo.data.hotAcceptDependencies = [];
|
|
54
54
|
},
|
|
55
|
-
js_module:
|
|
55
|
+
js_module: (urlInfo) => {
|
|
56
56
|
// Do not scan node modules for import.meta.hot
|
|
57
57
|
// - unlikely to be there
|
|
58
58
|
// - we don't watch node modules (too expensive)
|
|
@@ -64,27 +64,22 @@ export const jsenvPluginImportMetaHot = () => {
|
|
|
64
64
|
if (!urlInfo.content.includes("import.meta.hot")) {
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
|
-
const { metadata } = await applyBabelPlugins({
|
|
68
|
-
babelPlugins: [babelPluginMetadataImportMetaHot],
|
|
69
|
-
input: urlInfo.content,
|
|
70
|
-
inputIsJsModule: true,
|
|
71
|
-
inputUrl: urlInfo.originalUrl,
|
|
72
|
-
outputUrl: urlInfo.generatedUrl,
|
|
73
|
-
});
|
|
74
67
|
const {
|
|
75
|
-
|
|
68
|
+
importMetaHotNodes,
|
|
76
69
|
hotDecline,
|
|
77
70
|
hotAcceptSelf,
|
|
78
|
-
|
|
79
|
-
} =
|
|
71
|
+
hotAcceptSpecifiers,
|
|
72
|
+
} = analyzeImportMetaHot(urlInfo.contentAst);
|
|
80
73
|
urlInfo.data.hotDecline = hotDecline;
|
|
81
74
|
urlInfo.data.hotAcceptSelf = hotAcceptSelf;
|
|
82
|
-
urlInfo.data.hotAcceptDependencies =
|
|
83
|
-
|
|
75
|
+
urlInfo.data.hotAcceptDependencies = hotAcceptSpecifiers.map(
|
|
76
|
+
(specifier) => resolveHotAcceptSpecifier(urlInfo, specifier),
|
|
77
|
+
);
|
|
78
|
+
if (importMetaHotNodes.length === 0) {
|
|
84
79
|
return null;
|
|
85
80
|
}
|
|
86
81
|
if (urlInfo.context.build) {
|
|
87
|
-
return removeImportMetaHots(urlInfo,
|
|
82
|
+
return removeImportMetaHots(urlInfo, importMetaHotNodes);
|
|
88
83
|
}
|
|
89
84
|
return injectImportMetaHot(urlInfo, importMetaHotClientFileUrl);
|
|
90
85
|
},
|
|
@@ -92,12 +87,32 @@ export const jsenvPluginImportMetaHot = () => {
|
|
|
92
87
|
};
|
|
93
88
|
};
|
|
94
89
|
|
|
95
|
-
|
|
90
|
+
// The specifier given to import.meta.hot.accept() is one the file imports,
|
|
91
|
+
// so its url is known from that import (autoreload compares urls). A
|
|
92
|
+
// specifier the file does not import accepts nothing: it is kept as a plain
|
|
93
|
+
// url, never resolved as a dependency (that could throw on a bare specifier).
|
|
94
|
+
const resolveHotAcceptSpecifier = (urlInfo, specifier) => {
|
|
95
|
+
for (const referenceToOther of urlInfo.referenceToOthersSet) {
|
|
96
|
+
if (
|
|
97
|
+
referenceToOther.type === "js_import" &&
|
|
98
|
+
referenceToOther.specifier === specifier
|
|
99
|
+
) {
|
|
100
|
+
return referenceToOther.url;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return new URL(specifier, urlInfo.url).href;
|
|
105
|
+
} catch {
|
|
106
|
+
return specifier;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const removeImportMetaHots = (urlInfo, importMetaHotNodes) => {
|
|
96
111
|
const magicSource = createMagicSource(urlInfo.content);
|
|
97
|
-
|
|
112
|
+
importMetaHotNodes.forEach((node) => {
|
|
98
113
|
magicSource.replace({
|
|
99
|
-
start:
|
|
100
|
-
end:
|
|
114
|
+
start: node.start,
|
|
115
|
+
end: node.end,
|
|
101
116
|
replacement: "undefined",
|
|
102
117
|
});
|
|
103
118
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* TODO: ideally during dev we would keep import.meta.dev and ensure we set it to true rather than replacing it with true?
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { getImportMetaPropertyName, visitJsAst } from "@jsenv/ast";
|
|
14
14
|
import { createMagicSource } from "@jsenv/sourcemap";
|
|
15
15
|
|
|
16
16
|
export const jsenvPluginImportMetaScenarios = () => {
|
|
@@ -18,7 +18,7 @@ export const jsenvPluginImportMetaScenarios = () => {
|
|
|
18
18
|
name: "jsenv:import_meta_scenario",
|
|
19
19
|
appliesDuring: "*",
|
|
20
20
|
transformUrlContent: {
|
|
21
|
-
js_module:
|
|
21
|
+
js_module: (urlInfo) => {
|
|
22
22
|
// Do not scan node modules for import.meta.dev/import.meta.build
|
|
23
23
|
// - node modules won't have this in their code
|
|
24
24
|
// - ;or should use other an other technic as this one won't be available
|
|
@@ -34,40 +34,42 @@ export const jsenvPluginImportMetaScenarios = () => {
|
|
|
34
34
|
) {
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
|
-
const {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const importMetaScenarioNodes = { dev: [], build: [] };
|
|
38
|
+
visitJsAst(urlInfo.contentAst, {
|
|
39
|
+
MemberExpression: (node) => {
|
|
40
|
+
const name = getImportMetaPropertyName(node);
|
|
41
|
+
if (name === "dev" || name === "build") {
|
|
42
|
+
importMetaScenarioNodes[name].push(node);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
43
45
|
});
|
|
44
|
-
const { dev
|
|
46
|
+
const { dev, build } = importMetaScenarioNodes;
|
|
45
47
|
const replacements = [];
|
|
46
|
-
const replace = (
|
|
47
|
-
replacements.push({
|
|
48
|
+
const replace = (node, value) => {
|
|
49
|
+
replacements.push({ node, value });
|
|
48
50
|
};
|
|
49
51
|
if (urlInfo.context.build) {
|
|
50
52
|
// during build ensure replacement for tree-shaking
|
|
51
|
-
dev.forEach((
|
|
52
|
-
replace(
|
|
53
|
+
dev.forEach((node) => {
|
|
54
|
+
replace(node, "undefined");
|
|
53
55
|
});
|
|
54
|
-
build.forEach((
|
|
55
|
-
replace(
|
|
56
|
+
build.forEach((node) => {
|
|
57
|
+
replace(node, "true");
|
|
56
58
|
});
|
|
57
59
|
} else {
|
|
58
60
|
// during dev we can let "import.meta.build" untouched
|
|
59
61
|
// it will be evaluated to undefined.
|
|
60
62
|
// Moreover it can be surprising to see some "undefined"
|
|
61
63
|
// when source file contains "import.meta.build"
|
|
62
|
-
dev.forEach((
|
|
63
|
-
replace(
|
|
64
|
+
dev.forEach((node) => {
|
|
65
|
+
replace(node, "true");
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
const magicSource = createMagicSource(urlInfo.content);
|
|
67
|
-
replacements.forEach(({
|
|
69
|
+
replacements.forEach(({ node, value }) => {
|
|
68
70
|
magicSource.replace({
|
|
69
|
-
start:
|
|
70
|
-
end:
|
|
71
|
+
start: node.start,
|
|
72
|
+
end: node.end,
|
|
71
73
|
replacement: value,
|
|
72
74
|
});
|
|
73
75
|
});
|
|
@@ -76,39 +78,3 @@ export const jsenvPluginImportMetaScenarios = () => {
|
|
|
76
78
|
},
|
|
77
79
|
};
|
|
78
80
|
};
|
|
79
|
-
|
|
80
|
-
const babelPluginMetadataImportMetaScenarios = () => {
|
|
81
|
-
return {
|
|
82
|
-
name: "metadata-import-meta-scenarios",
|
|
83
|
-
visitor: {
|
|
84
|
-
Program(programPath, state) {
|
|
85
|
-
const importMetas = {};
|
|
86
|
-
programPath.traverse({
|
|
87
|
-
MemberExpression(path) {
|
|
88
|
-
const { node } = path;
|
|
89
|
-
const { object } = node;
|
|
90
|
-
if (object.type !== "MetaProperty") {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const { property: objectProperty } = object;
|
|
94
|
-
if (objectProperty.name !== "meta") {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const { property } = node;
|
|
98
|
-
const { name } = property;
|
|
99
|
-
const importMetaPaths = importMetas[name];
|
|
100
|
-
if (importMetaPaths) {
|
|
101
|
-
importMetaPaths.push(path);
|
|
102
|
-
} else {
|
|
103
|
-
importMetas[name] = [path];
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
state.file.metadata.importMetaScenarios = {
|
|
108
|
-
dev: importMetas.dev,
|
|
109
|
-
build: importMetas.build,
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tells the browser the static import graph of a page's module scripts, as
|
|
3
|
+
* `Link: <url>; rel=modulepreload` response headers, so it can fetch every
|
|
4
|
+
* module as soon as the page's headers arrive, without waiting to discover
|
|
5
|
+
* them level by level (each level costing a round trip and, the first time,
|
|
6
|
+
* a cook).
|
|
7
|
+
*
|
|
8
|
+
* Only what the graph already knows is listed: a module never cooked has no
|
|
9
|
+
* known imports. The header is computed at each response from the graph as it
|
|
10
|
+
* is then: a page gets its full list from its second load on, the first load
|
|
11
|
+
* keeps the plain waterfall.
|
|
12
|
+
*
|
|
13
|
+
* Headers rather than <link> elements written into the page: the page content
|
|
14
|
+
* stays what it is (served from memory, revalidated by etag) and the list can
|
|
15
|
+
* grow without cooking the page again. And not references either: a reference
|
|
16
|
+
* from the html to every module would make the html a dependent of each of
|
|
17
|
+
* them, and a hot update propagating up the importers would reach the html
|
|
18
|
+
* (which declines) instead of stopping at the module accepting it.
|
|
19
|
+
*
|
|
20
|
+
* OFF BY DEFAULT, to be enabled once the dev server speaks http/2 or http/3.
|
|
21
|
+
* Over http/1.1 it makes pages slower — measured on a 500 modules page: first
|
|
22
|
+
* paint 32ms -> 868ms, load 626ms -> 985ms. Knowing the urls earlier gives the
|
|
23
|
+
* browser no extra resource: what it has is 6 connections per origin, and
|
|
24
|
+
* every request goes through them in the order it was learned.
|
|
25
|
+
* - Without the header, requests come in execution order: the parser asks
|
|
26
|
+
* for the render-blocking supervisor script first and gets a connection at
|
|
27
|
+
* once; transfer and execution overlap, a module executing and discovering
|
|
28
|
+
* its imports while the others download. On localhost the server answers
|
|
29
|
+
* from memory in 0.1ms, so the 6 connections are busy from the start.
|
|
30
|
+
* - With the header, hundreds of requests take the 6 connections before the
|
|
31
|
+
* parser asks for the supervisor script, which then waits for a free
|
|
32
|
+
* connection behind 1MB+ transfers (520ms measured, 2s with 20ms of RTT).
|
|
33
|
+
* Meanwhile nothing renders and nothing executes: transfer, then execution,
|
|
34
|
+
* serialized. http/1.1 cannot reprioritize a queued request, and a
|
|
35
|
+
* modulepreload has the same priority as a script.
|
|
36
|
+
* - And there is nothing to hide: the discovery latency a preload removes is
|
|
37
|
+
* ~1ms per graph level on localhost; with a real RTT a wide graph (more
|
|
38
|
+
* than 6 modules pending at any time) keeps the 6 connections saturated
|
|
39
|
+
* anyway, and the load takes requests / 6 * RTT whatever the order.
|
|
40
|
+
* It pays off for a deep and narrow graph (connections idle, waiting for a
|
|
41
|
+
* response to learn the next level), and over http/2 or http/3: no
|
|
42
|
+
* connection limit, and the browser prioritizes the blocking script.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { injectQueryParamsIntoSpecifier } from "@jsenv/urls";
|
|
46
|
+
|
|
47
|
+
import { urlSpecifierEncoding } from "../../kitchen/url_graph/url_specifier_encoding.js";
|
|
48
|
+
|
|
49
|
+
export const jsenvPluginModulepreload = () => {
|
|
50
|
+
return {
|
|
51
|
+
name: "jsenv:modulepreload",
|
|
52
|
+
appliesDuring: "dev",
|
|
53
|
+
augmentResponse: ({ urlInfo }) => {
|
|
54
|
+
if (urlInfo.type !== "html") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const hrefs = collectStaticImportHrefs(urlInfo);
|
|
58
|
+
if (hrefs.length === 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
headers: {
|
|
63
|
+
link: hrefs.map((href) => `<${href}>; rel=modulepreload`).join(", "),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const collectStaticImportHrefs = (htmlUrlInfo) => {
|
|
71
|
+
const hrefSet = new Set();
|
|
72
|
+
const visitedSet = new Set();
|
|
73
|
+
const addHref = (reference) => {
|
|
74
|
+
const specifier = urlSpecifierEncoding.decode(reference);
|
|
75
|
+
if (typeof specifier !== "string") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// "?hot" belongs to the request that cooked the importer, the page loading
|
|
79
|
+
// now imports the url without it
|
|
80
|
+
hrefSet.add(injectQueryParamsIntoSpecifier(specifier, { hot: undefined }));
|
|
81
|
+
};
|
|
82
|
+
const visitJsModule = (jsModuleUrlInfo) => {
|
|
83
|
+
if (visitedSet.has(jsModuleUrlInfo)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
visitedSet.add(jsModuleUrlInfo);
|
|
87
|
+
for (const reference of jsModuleUrlInfo.referenceToOthersSet) {
|
|
88
|
+
if (
|
|
89
|
+
reference.type !== "js_import" ||
|
|
90
|
+
reference.isWeak ||
|
|
91
|
+
reference.isImplicit ||
|
|
92
|
+
!STATIC_IMPORT_SUBTYPES.includes(reference.subtype) ||
|
|
93
|
+
!reference.url.startsWith("file:")
|
|
94
|
+
) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const importedUrlInfo = reference.urlInfo;
|
|
98
|
+
// never cooked: its content, hence its own imports, are unknown
|
|
99
|
+
if (importedUrlInfo.type !== "js_module") {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
addHref(reference);
|
|
103
|
+
visitJsModule(importedUrlInfo);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
for (const reference of htmlUrlInfo.referenceToOthersSet) {
|
|
107
|
+
if (
|
|
108
|
+
reference.type !== "script" ||
|
|
109
|
+
reference.expectedType !== "js_module" ||
|
|
110
|
+
// the scripts jsenv adds to every page are few, small and in memory
|
|
111
|
+
reference.injected ||
|
|
112
|
+
reference.isWeak
|
|
113
|
+
) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const scriptUrlInfo = reference.urlInfo;
|
|
117
|
+
if (scriptUrlInfo.type !== "js_module") {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!reference.isInline && reference.url.startsWith("file:")) {
|
|
121
|
+
addHref(reference);
|
|
122
|
+
}
|
|
123
|
+
visitJsModule(scriptUrlInfo);
|
|
124
|
+
}
|
|
125
|
+
return Array.from(hrefSet);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const STATIC_IMPORT_SUBTYPES = ["import_static", "export_named", "export_all"];
|
package/src/plugins/plugins.js
CHANGED
|
@@ -20,6 +20,7 @@ import { jsenvPluginImportMetaCss } from "./import_meta_css/jsenv_plugin_import_
|
|
|
20
20
|
// autoreload
|
|
21
21
|
import { jsenvPluginImportMetaHot } from "./import_meta_hot/jsenv_plugin_import_meta_hot.js";
|
|
22
22
|
import { jsenvPluginAutoreload } from "./autoreload/jsenv_plugin_autoreload.js";
|
|
23
|
+
import { jsenvPluginModulepreload } from "./modulepreload/jsenv_plugin_modulepreload.js";
|
|
23
24
|
import { jsenvPluginDependencyStatus } from "./dependency_status/jsenv_plugin_dependency_status.js";
|
|
24
25
|
import { jsenvPluginCacheControl } from "./cache_control/jsenv_plugin_cache_control.js";
|
|
25
26
|
import { jsenvPluginCustomElementsRedefine } from "./custom_elements_redefine/jsenv_plugin_custom_elements_redefine.js";
|
|
@@ -56,6 +57,7 @@ export const getCorePlugins = ({
|
|
|
56
57
|
|
|
57
58
|
clientAutoreload,
|
|
58
59
|
clientAutoreloadOnServerRestart,
|
|
60
|
+
modulepreload = false,
|
|
59
61
|
dependencyStatus,
|
|
60
62
|
cacheControl,
|
|
61
63
|
scenarioPlaceholders = true,
|
|
@@ -157,6 +159,7 @@ export const getCorePlugins = ({
|
|
|
157
159
|
...(clientAutoreload && clientAutoreload.enabled
|
|
158
160
|
? [jsenvPluginAutoreload(clientAutoreload)]
|
|
159
161
|
: []),
|
|
162
|
+
...(modulepreload ? [jsenvPluginModulepreload()] : []),
|
|
160
163
|
...(dependencyStatus
|
|
161
164
|
? [jsenvPluginDependencyStatus(dependencyStatus)]
|
|
162
165
|
: []),
|
|
@@ -56,10 +56,31 @@ const DirectoryListing = () => {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
const ErrorMessage = () => {
|
|
59
|
-
const { filePathExisting, filePathNotFound } =
|
|
59
|
+
const { filePathExisting, filePathNotFound, spaFallbackFilePaths } =
|
|
60
|
+
enoentDetails;
|
|
60
61
|
|
|
61
62
|
let errorText;
|
|
62
63
|
let errorSuggestion;
|
|
64
|
+
let spaExplanation = null;
|
|
65
|
+
if (spaFallbackFilePaths) {
|
|
66
|
+
spaExplanation = (
|
|
67
|
+
<>
|
|
68
|
+
<strong>SPA mode:</strong> a url without extension is a route, served
|
|
69
|
+
with the closest html file. None of these exists:
|
|
70
|
+
<ul
|
|
71
|
+
className="spa_fallback_list"
|
|
72
|
+
style="margin: 6px 0 0; padding-left: 20px;"
|
|
73
|
+
>
|
|
74
|
+
{spaFallbackFilePaths.map((filePath) => (
|
|
75
|
+
<li key={filePath}>
|
|
76
|
+
<code>{filePath}</code>
|
|
77
|
+
{filePath === `/${mainFilePath}` ? " (main file)" : ""}
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
63
84
|
errorText = (
|
|
64
85
|
<>
|
|
65
86
|
<strong>File not found:</strong>
|
|
@@ -82,6 +103,11 @@ const ErrorMessage = () => {
|
|
|
82
103
|
return (
|
|
83
104
|
<div className="error_message">
|
|
84
105
|
<p className="error_text">{errorText}</p>
|
|
106
|
+
{spaExplanation ? (
|
|
107
|
+
<p className="error_spa" style="margin-top: 10px;">
|
|
108
|
+
{spaExplanation}
|
|
109
|
+
</p>
|
|
110
|
+
) : null}
|
|
85
111
|
<p
|
|
86
112
|
className="error_suggestion"
|
|
87
113
|
style="font-size: 0.8em; margin-top: 10px;"
|
|
@@ -32,12 +32,14 @@ import {
|
|
|
32
32
|
asUrlWithoutSearch,
|
|
33
33
|
ensurePathnameTrailingSlash,
|
|
34
34
|
urlIsOrIsInsideOf,
|
|
35
|
+
urlToExtension,
|
|
35
36
|
urlToFilename,
|
|
36
37
|
urlToRelativeUrl,
|
|
37
38
|
} from "@jsenv/urls";
|
|
38
39
|
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
39
40
|
import { getDirectoryWatchPatterns } from "../../helpers/watch_source_files.js";
|
|
40
41
|
import { FILE_AND_SERVER_URLS_CONVERTER } from "../../kitchen/file_and_server_urls_converter.js";
|
|
42
|
+
import { listSpaFallbackFileUrls } from "./jsenv_plugin_fs_redirection.js";
|
|
41
43
|
|
|
42
44
|
const htmlFileUrlForDirectory = import.meta
|
|
43
45
|
.resolve("./client/directory_listing.html");
|
|
@@ -391,6 +393,24 @@ const generateDirectoryListingInjection = (
|
|
|
391
393
|
filePathExisting: `/${filePathExisting}`,
|
|
392
394
|
filePathNotFound,
|
|
393
395
|
});
|
|
396
|
+
// a url without extension nor trailing slash is a route: in spa mode it
|
|
397
|
+
// was answered with the closest html file, the 404 means none was found
|
|
398
|
+
const urlNotFoundObject = new URL(urlNotFound);
|
|
399
|
+
if (
|
|
400
|
+
spa &&
|
|
401
|
+
!urlToExtension(urlNotFoundObject) &&
|
|
402
|
+
!urlNotFoundObject.pathname.endsWith("/")
|
|
403
|
+
) {
|
|
404
|
+
enoentDetails.spaFallbackFilePaths = listSpaFallbackFileUrls(
|
|
405
|
+
urlNotFound,
|
|
406
|
+
{ rootDirectoryUrl: serverRootDirectoryUrl, mainFilePath },
|
|
407
|
+
).map((fileUrl) =>
|
|
408
|
+
FILE_AND_SERVER_URLS_CONVERTER.asServerUrl(
|
|
409
|
+
fileUrl,
|
|
410
|
+
serverRootDirectoryUrl,
|
|
411
|
+
),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
394
414
|
}
|
|
395
415
|
|
|
396
416
|
return {
|
|
@@ -201,25 +201,42 @@ const getSpaFallbackUrl = (reference) => {
|
|
|
201
201
|
// the SPA fallback answers a request; during build there is none
|
|
202
202
|
return null;
|
|
203
203
|
}
|
|
204
|
-
const
|
|
205
|
-
requestedUrl,
|
|
204
|
+
const spaFallbackFileUrls = listSpaFallbackFileUrls(requestedUrl, {
|
|
206
205
|
rootDirectoryUrl,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
mainFilePath,
|
|
207
|
+
});
|
|
208
|
+
for (const spaFallbackFileUrl of spaFallbackFileUrls) {
|
|
209
|
+
if (existsSync(new URL(spaFallbackFileUrl))) {
|
|
210
|
+
return spaFallbackFileUrl;
|
|
211
|
+
}
|
|
210
212
|
}
|
|
211
|
-
|
|
213
|
+
// none exists: the main file it is, and the 404 answering for it lists
|
|
214
|
+
// what was tried (see directory listing)
|
|
215
|
+
return new URL(mainFilePath, rootDirectoryUrl).href;
|
|
212
216
|
};
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
// The html files that can answer a route, closest first: the entry file of
|
|
218
|
+
// the route's own directory ("index.html", then "<dirname>.html"), then of
|
|
219
|
+
// each directory above it up to the server root, then the main file.
|
|
220
|
+
export const listSpaFallbackFileUrls = (
|
|
221
|
+
requestedUrl,
|
|
222
|
+
{ rootDirectoryUrl, mainFilePath },
|
|
223
|
+
) => {
|
|
224
|
+
const fileUrls = [];
|
|
225
|
+
let directoryUrl = new URL("./", requestedUrl).href;
|
|
226
|
+
while (urlIsOrIsInsideOf(directoryUrl, rootDirectoryUrl)) {
|
|
227
|
+
fileUrls.push(new URL("index.html", directoryUrl).href);
|
|
228
|
+
const filename = urlToFilename(directoryUrl);
|
|
229
|
+
if (filename) {
|
|
230
|
+
fileUrls.push(new URL(`${filename}.html`, directoryUrl).href);
|
|
219
231
|
}
|
|
220
|
-
if (
|
|
221
|
-
|
|
232
|
+
if (directoryUrl === String(rootDirectoryUrl)) {
|
|
233
|
+
break;
|
|
222
234
|
}
|
|
223
|
-
directoryUrl = new URL("../", directoryUrl);
|
|
235
|
+
directoryUrl = new URL("../", directoryUrl).href;
|
|
236
|
+
}
|
|
237
|
+
const mainFileUrl = new URL(mainFilePath, rootDirectoryUrl).href;
|
|
238
|
+
if (!fileUrls.includes(mainFileUrl)) {
|
|
239
|
+
fileUrls.push(mainFileUrl);
|
|
224
240
|
}
|
|
241
|
+
return fileUrls;
|
|
225
242
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readEntryStatSync } from "@jsenv/filesystem";
|
|
2
2
|
import { ensurePathnameTrailingSlash } from "@jsenv/urls";
|
|
3
3
|
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
4
|
-
import { readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
5
5
|
import { FILE_AND_SERVER_URLS_CONVERTER } from "../../kitchen/file_and_server_urls_converter.js";
|
|
6
6
|
import { createHtmlPageLister } from "./html_pages.js";
|
|
7
7
|
import { jsenvPluginDirectoryListing } from "./jsenv_plugin_directory_listing.js";
|
|
@@ -160,7 +160,16 @@ export const jsenvPluginProtocolFile = ({
|
|
|
160
160
|
}
|
|
161
161
|
const serveFile = (url) => {
|
|
162
162
|
const contentType = CONTENT_TYPE.fromUrlExtension(url);
|
|
163
|
-
const
|
|
163
|
+
const urlObject = new URL(url);
|
|
164
|
+
// taken before the read: a write landing in between moves the
|
|
165
|
+
// stat past this one, so the dev server sees the content as
|
|
166
|
+
// outdated (see isValid in the dev server) rather than the reverse
|
|
167
|
+
const fileStat = statSync(urlObject);
|
|
168
|
+
const fileBuffer = readFileSync(urlObject);
|
|
169
|
+
urlInfo.data.fileStat = {
|
|
170
|
+
mtimeMs: fileStat.mtimeMs,
|
|
171
|
+
size: fileStat.size,
|
|
172
|
+
};
|
|
164
173
|
const content = CONTENT_TYPE.isTextual(contentType)
|
|
165
174
|
? String(fileBuffer)
|
|
166
175
|
: fileBuffer;
|
|
@@ -478,11 +478,6 @@ const addRelationshipWithPackageJson = ({
|
|
|
478
478
|
String(packageJsonContentAsBuffer),
|
|
479
479
|
);
|
|
480
480
|
}
|
|
481
|
-
// Checked on disk at every validation rather than trusted to the watcher:
|
|
482
|
-
// what this file decides (the package version, hence the ?v= importers
|
|
483
|
-
// embed) ends up in the browser's immutable cache, so a request racing the
|
|
484
|
-
// watcher must never be answered from a stale package.json.
|
|
485
|
-
packageJsonReference.urlInfo.revalidateOnFileSystem = true;
|
|
486
481
|
};
|
|
487
482
|
|
|
488
483
|
const createResolverWithFallbackOnError = (mainResolver, fallbackResolver) => {
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md#toc-stages-of-babel
|
|
2
|
-
// https://github.com/cfware/babel-plugin-bundled-import-meta/blob/master/index.js
|
|
3
|
-
// https://github.com/babel/babel/blob/f4edf62f6beeab8ae9f2b7f0b82f1b3b12a581af/packages/babel-helper-module-imports/src/index.js#L7
|
|
4
|
-
|
|
5
|
-
export const babelPluginMetadataImportMetaHot = () => {
|
|
6
|
-
return {
|
|
7
|
-
name: "metadata-import-meta-hot",
|
|
8
|
-
visitor: {
|
|
9
|
-
Program(programPath, state) {
|
|
10
|
-
Object.assign(
|
|
11
|
-
state.file.metadata,
|
|
12
|
-
collectImportMetaHotProperties(programPath),
|
|
13
|
-
);
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
const collectImportMetaHotProperties = (programPath) => {
|
|
19
|
-
const importMetaHotPaths = [];
|
|
20
|
-
let hotDecline = false;
|
|
21
|
-
let hotAcceptSelf = false;
|
|
22
|
-
let hotAcceptDependencies = [];
|
|
23
|
-
programPath.traverse({
|
|
24
|
-
MemberExpression(path) {
|
|
25
|
-
const { node } = path;
|
|
26
|
-
const { object } = node;
|
|
27
|
-
if (object.type !== "MetaProperty") {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const { property: objectProperty } = object;
|
|
31
|
-
if (objectProperty.name !== "meta") {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const { property } = node;
|
|
35
|
-
const { name } = property;
|
|
36
|
-
if (name === "hot") {
|
|
37
|
-
importMetaHotPaths.push(path);
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
CallExpression(path) {
|
|
41
|
-
if (isImportMetaHotMethodCall(path, "accept")) {
|
|
42
|
-
const callNode = path.node;
|
|
43
|
-
const args = callNode.arguments;
|
|
44
|
-
if (args.length === 0) {
|
|
45
|
-
hotAcceptSelf = true;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const firstArg = args[0];
|
|
49
|
-
if (firstArg.type === "StringLiteral") {
|
|
50
|
-
hotAcceptDependencies = [
|
|
51
|
-
{
|
|
52
|
-
specifierPath: path.get("arguments")[0],
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (firstArg.type === "ArrayExpression") {
|
|
58
|
-
const firstArgPath = path.get("arguments")[0];
|
|
59
|
-
hotAcceptDependencies = firstArg.elements.map((arrayNode, index) => {
|
|
60
|
-
if (arrayNode.type !== "StringLiteral") {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`all array elements must be strings in "import.meta.hot.accept(array)"`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
specifierPath: firstArgPath.get(String(index)),
|
|
67
|
-
};
|
|
68
|
-
});
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
// accept first arg can be "anything" such as
|
|
72
|
-
// `const cb = () => {}; import.meta.accept(cb)`
|
|
73
|
-
hotAcceptSelf = true;
|
|
74
|
-
}
|
|
75
|
-
if (isImportMetaHotMethodCall(path, "decline")) {
|
|
76
|
-
hotDecline = true;
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
return {
|
|
81
|
-
importMetaHotPaths,
|
|
82
|
-
hotDecline,
|
|
83
|
-
hotAcceptSelf,
|
|
84
|
-
hotAcceptDependencies,
|
|
85
|
-
};
|
|
86
|
-
};
|
|
87
|
-
const isImportMetaHotMethodCall = (path, methodName) => {
|
|
88
|
-
const { property, object } = path.node.callee;
|
|
89
|
-
return (
|
|
90
|
-
property &&
|
|
91
|
-
property.name === methodName &&
|
|
92
|
-
object &&
|
|
93
|
-
object.property &&
|
|
94
|
-
object.property.name === "hot" &&
|
|
95
|
-
object.object.type === "MetaProperty"
|
|
96
|
-
);
|
|
97
|
-
};
|