@jsenv/core 38.4.17 → 38.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/html/directory.html +18 -0
- package/dist/html/html_404_and_parent_dir.html +19 -0
- package/dist/html/html_404_and_parent_dir_is_empty.html +16 -0
- package/dist/html/html_syntax_error.html +14 -0
- package/dist/js/autoreload.js +135 -132
- package/dist/jsenv_core.js +302 -28
- package/package.json +5 -6
- package/src/dev/start_dev_server.js +5 -0
- package/src/plugins/autoreload/client/autoreload.js +138 -137
- package/src/plugins/autoreload/jsenv_plugin_autoreload_client.js +10 -1
- package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +4 -1
- package/src/plugins/protocol_file/directory.html +20 -0
- package/src/plugins/protocol_file/html_404_and_parent_dir.html +21 -0
- package/src/plugins/protocol_file/html_404_and_parent_dir_is_empty.html +18 -0
- package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +173 -13
- package/src/plugins/reference_analysis/html/html_syntax_error.html +14 -0
- package/src/plugins/reference_analysis/html/jsenv_plugin_html_reference_analysis.js +95 -3
- package/src/plugins/ribbon/jsenv_plugin_ribbon.js +1 -1
|
@@ -6,134 +6,134 @@ import {
|
|
|
6
6
|
getDOMNodesUsingUrl,
|
|
7
7
|
} from "./reload.js";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
export const initAutoreload = ({ mainFilePath }) => {
|
|
10
|
+
let debug = false;
|
|
11
|
+
const reloader = {
|
|
12
|
+
urlHotMetas,
|
|
13
|
+
status: {
|
|
14
|
+
value: "idle",
|
|
15
|
+
onchange: () => {},
|
|
16
|
+
goTo: (value) => {
|
|
17
|
+
reloader.status.value = value;
|
|
18
|
+
reloader.status.onchange();
|
|
19
|
+
},
|
|
18
20
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
autoreload: {
|
|
22
|
+
enabled: ["1", null].includes(window.localStorage.getItem("autoreload")),
|
|
23
|
+
onchange: () => {},
|
|
24
|
+
enable: () => {
|
|
25
|
+
reloader.autoreload.enabled = true;
|
|
26
|
+
window.localStorage.setItem("autoreload", "1");
|
|
27
|
+
reloader.autoreload.onchange();
|
|
28
|
+
},
|
|
29
|
+
disable: () => {
|
|
30
|
+
reloader.autoreload.enabled = false;
|
|
31
|
+
window.localStorage.setItem("autoreload", "0");
|
|
32
|
+
reloader.autoreload.onchange();
|
|
33
|
+
},
|
|
27
34
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
changes: {
|
|
35
|
-
value: [],
|
|
36
|
-
onchange: () => {},
|
|
37
|
-
add: (reloadMessage) => {
|
|
38
|
-
if (debug) {
|
|
39
|
-
console.debug("received reload message", reloadMessage);
|
|
40
|
-
}
|
|
41
|
-
reloader.changes.value.push(reloadMessage);
|
|
42
|
-
reloader.changes.onchange();
|
|
43
|
-
if (reloader.autoreload.enabled) {
|
|
44
|
-
reloader.reload();
|
|
45
|
-
} else {
|
|
46
|
-
reloader.status.goTo("can_reload");
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
remove: (reloadMessage) => {
|
|
50
|
-
const index = reloader.changes.value.indexOf(reloadMessage);
|
|
51
|
-
if (index > -1) {
|
|
52
|
-
reloader.changes.value.splice(index, 1);
|
|
53
|
-
if (reloader.changes.value.length === 0) {
|
|
54
|
-
reloader.status.goTo("idle");
|
|
35
|
+
changes: {
|
|
36
|
+
value: [],
|
|
37
|
+
onchange: () => {},
|
|
38
|
+
add: (reloadMessage) => {
|
|
39
|
+
if (debug) {
|
|
40
|
+
console.debug("received reload message", reloadMessage);
|
|
55
41
|
}
|
|
42
|
+
reloader.changes.value.push(reloadMessage);
|
|
56
43
|
reloader.changes.onchange();
|
|
44
|
+
if (reloader.autoreload.enabled) {
|
|
45
|
+
reloader.reload();
|
|
46
|
+
} else {
|
|
47
|
+
reloader.status.goTo("can_reload");
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
remove: (reloadMessage) => {
|
|
51
|
+
const index = reloader.changes.value.indexOf(reloadMessage);
|
|
52
|
+
if (index > -1) {
|
|
53
|
+
reloader.changes.value.splice(index, 1);
|
|
54
|
+
if (reloader.changes.value.length === 0) {
|
|
55
|
+
reloader.status.goTo("idle");
|
|
56
|
+
}
|
|
57
|
+
reloader.changes.onchange();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
currentExecution: null,
|
|
62
|
+
reload: () => {
|
|
63
|
+
const someEffectIsFullReload = reloader.changes.value.some(
|
|
64
|
+
(reloadMessage) => reloadMessage.type === "full",
|
|
65
|
+
);
|
|
66
|
+
if (someEffectIsFullReload) {
|
|
67
|
+
reloadHtmlPage();
|
|
68
|
+
return;
|
|
57
69
|
}
|
|
70
|
+
reloader.status.goTo("reloading");
|
|
71
|
+
const onApplied = (reloadMessage) => {
|
|
72
|
+
reloader.changes.remove(reloadMessage);
|
|
73
|
+
};
|
|
74
|
+
const setReloadMessagePromise = (reloadMessage, promise) => {
|
|
75
|
+
promise.then(
|
|
76
|
+
() => {
|
|
77
|
+
onApplied(reloadMessage);
|
|
78
|
+
reloader.currentExecution = null;
|
|
79
|
+
},
|
|
80
|
+
(e) => {
|
|
81
|
+
reloader.status.goTo("failed");
|
|
82
|
+
if (typeof window.reportError === "function") {
|
|
83
|
+
window.reportError(e);
|
|
84
|
+
} else {
|
|
85
|
+
console.error(e);
|
|
86
|
+
}
|
|
87
|
+
console.error(
|
|
88
|
+
`[jsenv] Hot reload failed after ${reloadMessage.reason}.
|
|
89
|
+
This could be due to syntax errors or importing non-existent modules (see errors in console)`,
|
|
90
|
+
);
|
|
91
|
+
reloader.currentExecution = null;
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
reloader.changes.value.forEach((reloadMessage) => {
|
|
96
|
+
if (reloadMessage.type === "hot") {
|
|
97
|
+
const promise = addToHotQueue(() => {
|
|
98
|
+
return applyHotReload(reloadMessage);
|
|
99
|
+
});
|
|
100
|
+
setReloadMessagePromise(reloadMessage, promise);
|
|
101
|
+
} else {
|
|
102
|
+
setReloadMessagePromise(reloadMessage, Promise.resolve());
|
|
103
|
+
}
|
|
104
|
+
});
|
|
58
105
|
},
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let pendingCallbacks = [];
|
|
109
|
+
let running = false;
|
|
110
|
+
const addToHotQueue = async (callback) => {
|
|
111
|
+
pendingCallbacks.push(callback);
|
|
112
|
+
dequeue();
|
|
113
|
+
};
|
|
114
|
+
const dequeue = async () => {
|
|
115
|
+
if (running) {
|
|
67
116
|
return;
|
|
68
117
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (typeof window.reportError === "function") {
|
|
82
|
-
window.reportError(e);
|
|
83
|
-
} else {
|
|
84
|
-
console.error(e);
|
|
85
|
-
}
|
|
86
|
-
console.error(
|
|
87
|
-
`[jsenv] Hot reload failed after ${reloadMessage.reason}.
|
|
88
|
-
This could be due to syntax errors or importing non-existent modules (see errors in console)`,
|
|
89
|
-
);
|
|
90
|
-
reloader.currentExecution = null;
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
};
|
|
94
|
-
reloader.changes.value.forEach((reloadMessage) => {
|
|
95
|
-
if (reloadMessage.type === "hot") {
|
|
96
|
-
const promise = addToHotQueue(() => {
|
|
97
|
-
return applyHotReload(reloadMessage);
|
|
98
|
-
});
|
|
99
|
-
setReloadMessagePromise(reloadMessage, promise);
|
|
100
|
-
} else {
|
|
101
|
-
setReloadMessagePromise(reloadMessage, Promise.resolve());
|
|
118
|
+
const callbacks = pendingCallbacks.slice();
|
|
119
|
+
pendingCallbacks = [];
|
|
120
|
+
running = true;
|
|
121
|
+
try {
|
|
122
|
+
await callbacks.reduce(async (previous, callback) => {
|
|
123
|
+
await previous;
|
|
124
|
+
await callback();
|
|
125
|
+
}, Promise.resolve());
|
|
126
|
+
} finally {
|
|
127
|
+
running = false;
|
|
128
|
+
if (pendingCallbacks.length) {
|
|
129
|
+
dequeue();
|
|
102
130
|
}
|
|
103
|
-
});
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
let pendingCallbacks = [];
|
|
108
|
-
let running = false;
|
|
109
|
-
const addToHotQueue = async (callback) => {
|
|
110
|
-
pendingCallbacks.push(callback);
|
|
111
|
-
dequeue();
|
|
112
|
-
};
|
|
113
|
-
const dequeue = async () => {
|
|
114
|
-
if (running) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const callbacks = pendingCallbacks.slice();
|
|
118
|
-
pendingCallbacks = [];
|
|
119
|
-
running = true;
|
|
120
|
-
try {
|
|
121
|
-
await callbacks.reduce(async (previous, callback) => {
|
|
122
|
-
await previous;
|
|
123
|
-
await callback();
|
|
124
|
-
}, Promise.resolve());
|
|
125
|
-
} finally {
|
|
126
|
-
running = false;
|
|
127
|
-
if (pendingCallbacks.length) {
|
|
128
|
-
dequeue();
|
|
129
131
|
}
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
+
};
|
|
132
133
|
|
|
133
|
-
const applyHotReload = async ({ cause, hotInstructions }) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
await previous;
|
|
134
|
+
const applyHotReload = async ({ cause, hotInstructions }) => {
|
|
135
|
+
for (const instruction of hotInstructions) {
|
|
136
|
+
const { type, boundary, acceptedBy } = instruction;
|
|
137
137
|
|
|
138
138
|
const hot = Date.now();
|
|
139
139
|
const urlToFetch = new URL(boundary, `${window.location.origin}/`).href;
|
|
@@ -141,7 +141,6 @@ const applyHotReload = async ({ cause, hotInstructions }) => {
|
|
|
141
141
|
// there is no url hot meta when:
|
|
142
142
|
// - code was not executed (code splitting with dynamic import)
|
|
143
143
|
// - import.meta.hot.accept() is not called (happens for HTML and CSS)
|
|
144
|
-
|
|
145
144
|
if (type === "prune") {
|
|
146
145
|
if (urlHotMeta) {
|
|
147
146
|
delete urlHotMetas[urlToFetch];
|
|
@@ -154,9 +153,8 @@ const applyHotReload = async ({ cause, hotInstructions }) => {
|
|
|
154
153
|
console.groupEnd();
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
|
-
|
|
156
|
+
continue;
|
|
158
157
|
}
|
|
159
|
-
|
|
160
158
|
if (acceptedBy === boundary) {
|
|
161
159
|
console.groupCollapsed(`[jsenv] hot reloading ${boundary} (${cause})`);
|
|
162
160
|
} else {
|
|
@@ -167,7 +165,7 @@ const applyHotReload = async ({ cause, hotInstructions }) => {
|
|
|
167
165
|
if (type === "js_module") {
|
|
168
166
|
if (!urlHotMeta) {
|
|
169
167
|
// code was not executed, no need to re-execute it
|
|
170
|
-
|
|
168
|
+
continue;
|
|
171
169
|
}
|
|
172
170
|
if (urlHotMeta.disposeCallback) {
|
|
173
171
|
console.log(`call dispose callback`);
|
|
@@ -184,18 +182,23 @@ const applyHotReload = async ({ cause, hotInstructions }) => {
|
|
|
184
182
|
}
|
|
185
183
|
console.log(`js module import done`);
|
|
186
184
|
console.groupEnd();
|
|
187
|
-
|
|
185
|
+
continue;
|
|
188
186
|
}
|
|
189
187
|
if (type === "html") {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
new URL(urlToFetch).pathname.slice(1).indexOf("/") === -1
|
|
188
|
+
let isRootHtmlFile;
|
|
189
|
+
if (window.location.pathname === "/") {
|
|
190
|
+
if (new URL(urlToFetch).pathname.slice(1).indexOf("/") === -1) {
|
|
191
|
+
isRootHtmlFile = true;
|
|
192
|
+
} else if (new URL(urlToFetch).pathname === mainFilePath) {
|
|
193
|
+
isRootHtmlFile = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
193
196
|
if (
|
|
194
197
|
!isRootHtmlFile &&
|
|
195
198
|
!compareTwoUrlPaths(urlToFetch, window.location.href)
|
|
196
199
|
) {
|
|
197
200
|
// we are not in that HTML page
|
|
198
|
-
|
|
201
|
+
continue;
|
|
199
202
|
}
|
|
200
203
|
const urlToReload = new URL(acceptedBy, `${window.location.origin}/`)
|
|
201
204
|
.href;
|
|
@@ -213,18 +216,16 @@ const applyHotReload = async ({ cause, hotInstructions }) => {
|
|
|
213
216
|
});
|
|
214
217
|
}
|
|
215
218
|
console.groupEnd();
|
|
216
|
-
|
|
219
|
+
continue;
|
|
217
220
|
}
|
|
218
221
|
console.warn(`unknown update type: "${type}"`);
|
|
219
|
-
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
window.__reloader__ = reloader;
|
|
226
|
+
window.__server_events__.listenEvents({
|
|
227
|
+
reload: (reloadServerEvent) => {
|
|
228
|
+
reloader.changes.add(reloadServerEvent.data);
|
|
220
229
|
},
|
|
221
|
-
|
|
222
|
-
);
|
|
230
|
+
});
|
|
223
231
|
};
|
|
224
|
-
|
|
225
|
-
window.__reloader__ = reloader;
|
|
226
|
-
window.__server_events__.listenEvents({
|
|
227
|
-
reload: (reloadServerEvent) => {
|
|
228
|
-
reloader.changes.add(reloadServerEvent.data);
|
|
229
|
-
},
|
|
230
|
-
});
|
|
@@ -26,12 +26,21 @@ export const jsenvPluginAutoreloadClient = () => {
|
|
|
26
26
|
expectedType: "js_module",
|
|
27
27
|
specifier: autoreloadClientFileUrl,
|
|
28
28
|
});
|
|
29
|
+
const paramsJson = JSON.stringify(
|
|
30
|
+
{
|
|
31
|
+
mainFilePath: `/${htmlUrlInfo.kitchen.context.mainFilePath}`,
|
|
32
|
+
},
|
|
33
|
+
null,
|
|
34
|
+
" ",
|
|
35
|
+
);
|
|
29
36
|
injectHtmlNodeAsEarlyAsPossible(
|
|
30
37
|
htmlAst,
|
|
31
38
|
createHtmlNode({
|
|
32
39
|
tagName: "script",
|
|
33
40
|
type: "module",
|
|
34
|
-
|
|
41
|
+
textContent: `import { initAutoreload } from "${autoreloadClientReference.generatedSpecifier}";
|
|
42
|
+
|
|
43
|
+
initAutoreload(${paramsJson});`,
|
|
35
44
|
}),
|
|
36
45
|
"jsenv:autoreload_client",
|
|
37
46
|
);
|
|
@@ -38,7 +38,10 @@ export const jsenvPluginAutoreloadServer = ({
|
|
|
38
38
|
: `a dependent file accepts hot reload`,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
urlInfo.data.hotDecline ||
|
|
43
|
+
urlInfo.firstReference?.type === "http_request"
|
|
44
|
+
) {
|
|
42
45
|
return {
|
|
43
46
|
declined: true,
|
|
44
47
|
reason: `file declines hot reload`,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Directory explorer</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<link rel="icon" href="data:," />
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<h1>
|
|
11
|
+
<a jsenv-ignore href="/${directoryRelativeUrl}"
|
|
12
|
+
>/${directoryRelativeUrl}</a
|
|
13
|
+
>
|
|
14
|
+
directory content:
|
|
15
|
+
</h1>
|
|
16
|
+
<ul>
|
|
17
|
+
${directoryContent}
|
|
18
|
+
</ul>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>404 ENOENT</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<link rel="icon" href="data:," />
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<p>No entry on the filesystem for <strong>/${fileRelativeUrl}</strong></p>
|
|
11
|
+
<p>
|
|
12
|
+
<a jsenv-ignore href="/${parentDirectoryRelativeUrl}"
|
|
13
|
+
>/${parentDirectoryRelativeUrl}</a
|
|
14
|
+
>
|
|
15
|
+
directory content:
|
|
16
|
+
</p>
|
|
17
|
+
<ul>
|
|
18
|
+
${parentDirectoryContent}
|
|
19
|
+
</ul>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>404 ENOENT</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<link rel="icon" href="data:," />
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<p>No entry on the filesystem for <strong>/${fileRelativeUrl}</strong></p>
|
|
11
|
+
<p>
|
|
12
|
+
<a jsenv-ignore href="/${parentDirectoryRelativeUrl}"
|
|
13
|
+
>/${parentDirectoryRelativeUrl}</a
|
|
14
|
+
>
|
|
15
|
+
directory is empty.
|
|
16
|
+
</p>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
realpathSync,
|
|
5
|
+
statSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
} from "node:fs";
|
|
3
9
|
import { pathToFileURL } from "node:url";
|
|
4
10
|
import {
|
|
5
11
|
urlIsInsideOf,
|
|
@@ -11,7 +17,19 @@ import {
|
|
|
11
17
|
applyFileSystemMagicResolution,
|
|
12
18
|
getExtensionsToTry,
|
|
13
19
|
} from "@jsenv/node-esm-resolution";
|
|
20
|
+
import { pickContentType } from "@jsenv/server";
|
|
14
21
|
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
22
|
+
import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem";
|
|
23
|
+
|
|
24
|
+
const html404AndParentDirIsEmptyFileUrl = new URL(
|
|
25
|
+
"./html_404_and_parent_dir_is_empty.html",
|
|
26
|
+
import.meta.url,
|
|
27
|
+
);
|
|
28
|
+
const html404AndParentDirFileUrl = new URL(
|
|
29
|
+
"./html_404_and_parent_dir.html",
|
|
30
|
+
import.meta.url,
|
|
31
|
+
);
|
|
32
|
+
const htmlFileUrlForDirectory = new URL("./directory.html", import.meta.url);
|
|
15
33
|
|
|
16
34
|
export const jsenvPluginProtocolFile = ({
|
|
17
35
|
magicExtensions = ["inherit", ".js"],
|
|
@@ -99,7 +117,9 @@ export const jsenvPluginProtocolFile = ({
|
|
|
99
117
|
reference.leadsToADirectory = stat && stat.isDirectory();
|
|
100
118
|
if (reference.leadsToADirectory) {
|
|
101
119
|
let actionForDirectory;
|
|
102
|
-
if (
|
|
120
|
+
if (reference.type === "a_href") {
|
|
121
|
+
actionForDirectory = "ignore";
|
|
122
|
+
} else if (
|
|
103
123
|
reference.type === "http_request" ||
|
|
104
124
|
reference.type === "filesystem"
|
|
105
125
|
) {
|
|
@@ -184,17 +204,33 @@ export const jsenvPluginProtocolFile = ({
|
|
|
184
204
|
urlInfo.filenameHint = `${urlToFilename(urlInfo.url)}/`;
|
|
185
205
|
}
|
|
186
206
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
const directoryContentArray = readdirSync(urlObject);
|
|
208
|
+
if (urlInfo.firstReference.type === "filesystem") {
|
|
209
|
+
const content = JSON.stringify(directoryContentArray, null, " ");
|
|
210
|
+
return {
|
|
211
|
+
type: "directory",
|
|
212
|
+
contentType: "application/json",
|
|
213
|
+
content,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const acceptsHtml = urlInfo.context.request
|
|
217
|
+
? pickContentType(urlInfo.context.request, ["text/html"])
|
|
218
|
+
: false;
|
|
219
|
+
if (acceptsHtml) {
|
|
220
|
+
const html = generateHtmlForDirectory(
|
|
221
|
+
urlObject.href,
|
|
222
|
+
directoryContentArray,
|
|
223
|
+
urlInfo.context.rootDirectoryUrl,
|
|
224
|
+
);
|
|
225
|
+
return {
|
|
226
|
+
contentType: "text/html",
|
|
227
|
+
content: html,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
193
230
|
return {
|
|
194
231
|
type: "directory",
|
|
195
|
-
contentType:
|
|
196
|
-
|
|
197
|
-
content: body,
|
|
232
|
+
contentType: "application/json",
|
|
233
|
+
content: JSON.stringify(directoryContentArray, null, " "),
|
|
198
234
|
};
|
|
199
235
|
}
|
|
200
236
|
if (
|
|
@@ -204,20 +240,144 @@ export const jsenvPluginProtocolFile = ({
|
|
|
204
240
|
urlInfo.dirnameHint =
|
|
205
241
|
urlInfo.firstReference.ownerUrlInfo.filenameHint;
|
|
206
242
|
}
|
|
207
|
-
const fileBuffer = readFileSync(urlObject);
|
|
208
243
|
const contentType = CONTENT_TYPE.fromUrlExtension(urlInfo.url);
|
|
244
|
+
if (contentType === "text/html") {
|
|
245
|
+
try {
|
|
246
|
+
const fileBuffer = readFileSync(urlObject);
|
|
247
|
+
const content = String(fileBuffer);
|
|
248
|
+
return {
|
|
249
|
+
content,
|
|
250
|
+
contentType,
|
|
251
|
+
contentLength: fileBuffer.length,
|
|
252
|
+
};
|
|
253
|
+
} catch (e) {
|
|
254
|
+
if (e.code !== "ENOENT") {
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
const parentDirectoryUrl = new URL("./", urlInfo.url);
|
|
258
|
+
if (!existsSync(parentDirectoryUrl)) {
|
|
259
|
+
throw e;
|
|
260
|
+
}
|
|
261
|
+
const parentDirectoryContentArray = readdirSync(
|
|
262
|
+
new URL(parentDirectoryUrl),
|
|
263
|
+
);
|
|
264
|
+
const html = generateHtmlForENOENTOnHtmlFile(
|
|
265
|
+
urlInfo.url,
|
|
266
|
+
parentDirectoryContentArray,
|
|
267
|
+
parentDirectoryUrl,
|
|
268
|
+
urlInfo.context.rootDirectoryUrl,
|
|
269
|
+
);
|
|
270
|
+
return {
|
|
271
|
+
contentType: "text/html",
|
|
272
|
+
content: html,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const fileBuffer = readFileSync(urlObject);
|
|
209
277
|
const content = CONTENT_TYPE.isTextual(contentType)
|
|
210
278
|
? String(fileBuffer)
|
|
211
279
|
: fileBuffer;
|
|
212
280
|
return {
|
|
213
281
|
content,
|
|
214
282
|
contentType,
|
|
283
|
+
contentLength: fileBuffer.length,
|
|
215
284
|
};
|
|
216
285
|
},
|
|
217
286
|
},
|
|
218
287
|
];
|
|
219
288
|
};
|
|
220
289
|
|
|
290
|
+
const generateHtmlForDirectory = (
|
|
291
|
+
directoryUrl,
|
|
292
|
+
directoryContentArray,
|
|
293
|
+
rootDirectoryUrl,
|
|
294
|
+
) => {
|
|
295
|
+
directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
|
|
296
|
+
const htmlForDirectory = String(readFileSync(htmlFileUrlForDirectory));
|
|
297
|
+
const replacers = {
|
|
298
|
+
directoryRelativeUrl: urlToRelativeUrl(directoryUrl, rootDirectoryUrl),
|
|
299
|
+
directoryUrl,
|
|
300
|
+
directoryContent: () =>
|
|
301
|
+
generateDirectoryContent(
|
|
302
|
+
directoryContentArray,
|
|
303
|
+
directoryUrl,
|
|
304
|
+
rootDirectoryUrl,
|
|
305
|
+
),
|
|
306
|
+
};
|
|
307
|
+
const html = replacePlaceholders(htmlForDirectory, replacers);
|
|
308
|
+
return html;
|
|
309
|
+
};
|
|
310
|
+
const generateHtmlForENOENTOnHtmlFile = (
|
|
311
|
+
url,
|
|
312
|
+
parentDirectoryContentArray,
|
|
313
|
+
parentDirectoryUrl,
|
|
314
|
+
rootDirectoryUrl,
|
|
315
|
+
) => {
|
|
316
|
+
if (parentDirectoryContentArray.length === 0) {
|
|
317
|
+
const htmlFor404AndParentDirIsEmpty = String(
|
|
318
|
+
readFileSync(html404AndParentDirIsEmptyFileUrl),
|
|
319
|
+
);
|
|
320
|
+
return replacePlaceholders(htmlFor404AndParentDirIsEmpty, {
|
|
321
|
+
fileRelativeUrl: urlToRelativeUrl(url, rootDirectoryUrl),
|
|
322
|
+
parentDirectoryRelativeUrl: urlToRelativeUrl(
|
|
323
|
+
parentDirectoryUrl,
|
|
324
|
+
rootDirectoryUrl,
|
|
325
|
+
),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const htmlFor404AndParentDir = String(
|
|
329
|
+
readFileSync(html404AndParentDirFileUrl),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const replacers = {
|
|
333
|
+
fileUrl: url,
|
|
334
|
+
fileRelativeUrl: urlToRelativeUrl(url, rootDirectoryUrl),
|
|
335
|
+
parentDirectoryUrl,
|
|
336
|
+
parentDirectoryRelativeUrl: urlToRelativeUrl(
|
|
337
|
+
parentDirectoryUrl,
|
|
338
|
+
rootDirectoryUrl,
|
|
339
|
+
),
|
|
340
|
+
parentDirectoryContent: () =>
|
|
341
|
+
generateDirectoryContent(
|
|
342
|
+
parentDirectoryContentArray,
|
|
343
|
+
parentDirectoryUrl,
|
|
344
|
+
rootDirectoryUrl,
|
|
345
|
+
),
|
|
346
|
+
};
|
|
347
|
+
const html = replacePlaceholders(htmlFor404AndParentDir, replacers);
|
|
348
|
+
return html;
|
|
349
|
+
};
|
|
350
|
+
const generateDirectoryContent = (
|
|
351
|
+
directoryContentArray,
|
|
352
|
+
directoryUrl,
|
|
353
|
+
rootDirectoryUrl,
|
|
354
|
+
) => {
|
|
355
|
+
return directoryContentArray.map((filename) => {
|
|
356
|
+
const fileUrlObject = new URL(filename, directoryUrl);
|
|
357
|
+
const fileUrl = String(fileUrlObject);
|
|
358
|
+
let fileUrlRelative = urlToRelativeUrl(fileUrl, rootDirectoryUrl);
|
|
359
|
+
if (lstatSync(fileUrlObject).isDirectory()) {
|
|
360
|
+
fileUrlRelative += "/";
|
|
361
|
+
}
|
|
362
|
+
return `<li>
|
|
363
|
+
<a href="/${fileUrlRelative}">/${fileUrlRelative}</a>
|
|
364
|
+
</li>`;
|
|
365
|
+
}).join(`
|
|
366
|
+
`);
|
|
367
|
+
};
|
|
368
|
+
const replacePlaceholders = (html, replacers) => {
|
|
369
|
+
return html.replace(/\${([\w]+)}/g, (match, name) => {
|
|
370
|
+
const replacer = replacers[name];
|
|
371
|
+
if (replacer === undefined) {
|
|
372
|
+
return match;
|
|
373
|
+
}
|
|
374
|
+
if (typeof replacer === "function") {
|
|
375
|
+
return replacer();
|
|
376
|
+
}
|
|
377
|
+
return replacer;
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
|
|
221
381
|
const resolveSymlink = (fileUrl) => {
|
|
222
382
|
const urlObject = new URL(fileUrl);
|
|
223
383
|
const realpath = realpathSync(urlObject);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Syntax error in HTML</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<link rel="icon" href="data:," />
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<p>Syntax error: <strong>${reasonCode}</strong></p>
|
|
11
|
+
<a jsenv-ignore href="${errorLinkHref}">${errorLinkText}</a>
|
|
12
|
+
<pre>${syntaxError}</pre>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|