@jsenv/core 27.5.1 → 27.5.2
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/js/html_supervisor_installer.js +334 -267
- package/dist/main.js +57 -14
- package/package.json +2 -1
- package/src/plugins/html_supervisor/client/error_formatter.js +300 -0
- package/src/plugins/html_supervisor/client/error_overlay.js +39 -268
- package/src/plugins/html_supervisor/client/html_supervisor_installer.js +5 -8
- package/src/plugins/html_supervisor/jsenv_plugin_html_supervisor.js +53 -15
package/dist/main.js
CHANGED
|
@@ -12035,7 +12035,8 @@ const jsenvPluginHtmlSupervisor = ({
|
|
|
12035
12035
|
logs = false,
|
|
12036
12036
|
measurePerf = false,
|
|
12037
12037
|
errorOverlay = true,
|
|
12038
|
-
openInEditor = true
|
|
12038
|
+
openInEditor = true,
|
|
12039
|
+
errorBaseUrl
|
|
12039
12040
|
}) => {
|
|
12040
12041
|
const htmlSupervisorSetupFileUrl = new URL("./js/html_supervisor_setup.js", import.meta.url).href;
|
|
12041
12042
|
const htmlSupervisorInstallerFileUrl = new URL("./js/html_supervisor_installer.js", import.meta.url).href;
|
|
@@ -12045,26 +12046,67 @@ const jsenvPluginHtmlSupervisor = ({
|
|
|
12045
12046
|
dev: true,
|
|
12046
12047
|
test: true
|
|
12047
12048
|
},
|
|
12048
|
-
serve: request => {
|
|
12049
|
-
if (
|
|
12050
|
-
|
|
12049
|
+
serve: (request, context) => {
|
|
12050
|
+
if (request.ressource.startsWith("/__open_in_editor__/")) {
|
|
12051
|
+
const file = request.ressource.slice("/__open_in_editor__/".length);
|
|
12052
|
+
|
|
12053
|
+
if (!file) {
|
|
12054
|
+
return {
|
|
12055
|
+
status: 400,
|
|
12056
|
+
body: "Missing file in url"
|
|
12057
|
+
};
|
|
12058
|
+
}
|
|
12059
|
+
|
|
12060
|
+
const launch = requireFromJsenv("launch-editor");
|
|
12061
|
+
launch(fileURLToPath(file), () => {// ignore error for now
|
|
12062
|
+
});
|
|
12063
|
+
return {
|
|
12064
|
+
status: 200,
|
|
12065
|
+
headers: {
|
|
12066
|
+
"cache-control": "no-store"
|
|
12067
|
+
}
|
|
12068
|
+
};
|
|
12051
12069
|
}
|
|
12052
12070
|
|
|
12053
|
-
|
|
12071
|
+
if (request.ressource.startsWith("/__get_code_frame__/")) {
|
|
12072
|
+
const url = request.ressource.slice("/__get_code_frame__/".length);
|
|
12073
|
+
const match = url.match(/:([0-9]+):([0-9]+)$/);
|
|
12054
12074
|
|
|
12055
|
-
|
|
12075
|
+
if (!match) {
|
|
12076
|
+
return {
|
|
12077
|
+
status: 400,
|
|
12078
|
+
body: "Missing line and column in url"
|
|
12079
|
+
};
|
|
12080
|
+
}
|
|
12081
|
+
|
|
12082
|
+
const file = url.slice(0, match.index);
|
|
12083
|
+
const line = parseInt(match[1]);
|
|
12084
|
+
const column = parseInt(match[2]);
|
|
12085
|
+
const urlInfo = context.urlGraph.getUrlInfo(file);
|
|
12086
|
+
|
|
12087
|
+
if (!urlInfo) {
|
|
12088
|
+
return {
|
|
12089
|
+
status: 404
|
|
12090
|
+
};
|
|
12091
|
+
}
|
|
12092
|
+
|
|
12093
|
+
const codeFrame = stringifyUrlSite({
|
|
12094
|
+
url: file,
|
|
12095
|
+
line,
|
|
12096
|
+
column,
|
|
12097
|
+
content: urlInfo.originalContent
|
|
12098
|
+
});
|
|
12056
12099
|
return {
|
|
12057
|
-
status:
|
|
12058
|
-
|
|
12100
|
+
status: 200,
|
|
12101
|
+
headers: {
|
|
12102
|
+
"content-type": "text/plain",
|
|
12103
|
+
"content-length": Buffer.byteLength(codeFrame)
|
|
12104
|
+
},
|
|
12105
|
+
body: codeFrame
|
|
12059
12106
|
};
|
|
12060
12107
|
}
|
|
12061
12108
|
|
|
12062
|
-
|
|
12063
|
-
launch(fileURLToPath(file), () => {// ignore error for now
|
|
12064
|
-
});
|
|
12065
|
-
return {
|
|
12066
|
-
status: 200
|
|
12067
|
-
};
|
|
12109
|
+
return null;
|
|
12068
12110
|
},
|
|
12069
12111
|
transformUrlContent: {
|
|
12070
12112
|
html: ({
|
|
@@ -12193,6 +12235,7 @@ const jsenvPluginHtmlSupervisor = ({
|
|
|
12193
12235
|
import { installHtmlSupervisor } from ${htmlSupervisorInstallerFileReference.generatedSpecifier}
|
|
12194
12236
|
installHtmlSupervisor(${JSON.stringify({
|
|
12195
12237
|
rootDirectoryUrl: context.rootDirectoryUrl,
|
|
12238
|
+
errorBaseUrl,
|
|
12196
12239
|
logs,
|
|
12197
12240
|
measurePerf,
|
|
12198
12241
|
errorOverlay,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "27.5.
|
|
3
|
+
"version": "27.5.2",
|
|
4
4
|
"description": "Tool to develop, test and build js projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"performances": "node --expose-gc ./scripts/performance/generate_performance_report.mjs --log --once",
|
|
51
51
|
"file-size": "node ./scripts/file_size/file_size.mjs --log",
|
|
52
52
|
"start_file_server": "node ./scripts/dev/start_file_server.mjs",
|
|
53
|
+
"generate-dev-errors-snapshot-files": "node --conditions=development ./tests/dev_server/errors/generate_snapshot_files.mjs",
|
|
53
54
|
"prettier": "prettier --write .",
|
|
54
55
|
"playwright-install": "npx playwright install-deps && npx playwright install",
|
|
55
56
|
"certificate-install": "node ./scripts/dev/install_certificate_authority.mjs",
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
export const formatError = (
|
|
2
|
+
error,
|
|
3
|
+
{
|
|
4
|
+
rootDirectoryUrl,
|
|
5
|
+
errorBaseUrl,
|
|
6
|
+
openInEditor,
|
|
7
|
+
url,
|
|
8
|
+
line,
|
|
9
|
+
column,
|
|
10
|
+
codeFrame,
|
|
11
|
+
requestedRessource,
|
|
12
|
+
reportedBy,
|
|
13
|
+
},
|
|
14
|
+
) => {
|
|
15
|
+
let { message, stack } = normalizeErrorParts(error)
|
|
16
|
+
let codeFramePromiseReference = { current: null }
|
|
17
|
+
let tip = formatTip({ reportedBy, requestedRessource })
|
|
18
|
+
let errorUrlSite
|
|
19
|
+
|
|
20
|
+
const resolveUrlSite = ({ url, line, column }) => {
|
|
21
|
+
const inlineUrlMatch = url.match(/@L([0-9]+)\-L([0-9]+)\.[\w]+$/)
|
|
22
|
+
if (inlineUrlMatch) {
|
|
23
|
+
const htmlUrl = url.slice(0, inlineUrlMatch.index)
|
|
24
|
+
const tagLine = parseInt(inlineUrlMatch[1])
|
|
25
|
+
const tagColumn = parseInt(inlineUrlMatch[2])
|
|
26
|
+
url = htmlUrl
|
|
27
|
+
line = tagLine + parseInt(line) - 1
|
|
28
|
+
column = tagColumn + parseInt(column)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let urlObject = new URL(url)
|
|
32
|
+
if (urlObject.origin === window.origin) {
|
|
33
|
+
urlObject = new URL(
|
|
34
|
+
`${urlObject.pathname.slice(1)}${urlObject.search}`,
|
|
35
|
+
rootDirectoryUrl,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
if (urlObject.href.startsWith("file:")) {
|
|
39
|
+
const atFsIndex = urlObject.pathname.indexOf("/@fs/")
|
|
40
|
+
if (atFsIndex > -1) {
|
|
41
|
+
const afterAtFs = urlObject.pathname.slice(atFsIndex + "/@fs/".length)
|
|
42
|
+
url = new URL(afterAtFs, "file:///").href
|
|
43
|
+
} else {
|
|
44
|
+
url = urlObject.href
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
url = urlObject.href
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
url,
|
|
52
|
+
line,
|
|
53
|
+
column,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const generateClickableText = (text) => {
|
|
58
|
+
const textWithHtmlLinks = makeLinksClickable(text, {
|
|
59
|
+
createLink: (url, { line, column }) => {
|
|
60
|
+
const urlSite = resolveUrlSite({ url, line, column })
|
|
61
|
+
if (!errorUrlSite && text === stack) {
|
|
62
|
+
onErrorLocated(urlSite)
|
|
63
|
+
}
|
|
64
|
+
if (errorBaseUrl) {
|
|
65
|
+
if (urlSite.url.startsWith(rootDirectoryUrl)) {
|
|
66
|
+
urlSite.url = `${errorBaseUrl}${urlSite.url.slice(
|
|
67
|
+
rootDirectoryUrl.length,
|
|
68
|
+
)}`
|
|
69
|
+
} else {
|
|
70
|
+
urlSite.url = "file:///mocked_for_snapshots"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const urlWithLineAndColumn = formatUrlWithLineAndColumn(urlSite)
|
|
74
|
+
return {
|
|
75
|
+
href:
|
|
76
|
+
url.startsWith("file:") && openInEditor
|
|
77
|
+
? `javascript:window.fetch('/__open_in_editor__/${urlWithLineAndColumn}')`
|
|
78
|
+
: urlSite.url,
|
|
79
|
+
text: urlWithLineAndColumn,
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
return textWithHtmlLinks
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const onErrorLocated = (urlSite) => {
|
|
87
|
+
errorUrlSite = urlSite
|
|
88
|
+
if (codeFrame) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if (reportedBy !== "browser") {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
codeFramePromiseReference.current = (async () => {
|
|
95
|
+
const response = await window.fetch(
|
|
96
|
+
`/__get_code_frame__/${formatUrlWithLineAndColumn(urlSite)}`,
|
|
97
|
+
)
|
|
98
|
+
const codeFrame = await response.text()
|
|
99
|
+
const codeFrameClickable = generateClickableText(codeFrame)
|
|
100
|
+
return codeFrameClickable
|
|
101
|
+
})()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// error.stack is more reliable than url/line/column reported on window error events
|
|
105
|
+
// so use it only when error.stack is not available
|
|
106
|
+
if (url && !stack) {
|
|
107
|
+
onErrorLocated(resolveUrlSite({ url, line, column }))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let text
|
|
111
|
+
|
|
112
|
+
if (message && stack) {
|
|
113
|
+
text = `${generateClickableText(message)}\n${generateClickableText(stack)}`
|
|
114
|
+
} else if (stack) {
|
|
115
|
+
text = generateClickableText(stack)
|
|
116
|
+
} else {
|
|
117
|
+
text = generateClickableText(message)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (codeFrame) {
|
|
121
|
+
text += `\n\n${generateClickableText(codeFrame)}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
theme:
|
|
126
|
+
error && error.cause && error.cause.code === "PARSE_ERROR"
|
|
127
|
+
? "light"
|
|
128
|
+
: "dark",
|
|
129
|
+
title: "An error occured",
|
|
130
|
+
text,
|
|
131
|
+
codeFramePromise: codeFramePromiseReference.current,
|
|
132
|
+
tip: `${tip}
|
|
133
|
+
<br />
|
|
134
|
+
Click outside to close.`,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const formatUrlWithLineAndColumn = ({ url, line, column }) => {
|
|
139
|
+
return line === undefined && column === undefined
|
|
140
|
+
? url
|
|
141
|
+
: column === undefined
|
|
142
|
+
? `${url}:${line}`
|
|
143
|
+
: `${url}:${line}:${column}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const normalizeErrorParts = (error) => {
|
|
147
|
+
if (error === undefined) {
|
|
148
|
+
return {
|
|
149
|
+
message: "undefined",
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (error === null) {
|
|
153
|
+
return {
|
|
154
|
+
message: "null",
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (typeof error === "string") {
|
|
158
|
+
return {
|
|
159
|
+
message: error,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (error instanceof Error) {
|
|
163
|
+
if (error.name === "SyntaxError") {
|
|
164
|
+
return {
|
|
165
|
+
message: error.message,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (error.cause && error.cause.code === "PARSE_ERROR") {
|
|
169
|
+
if (error.messageHTML) {
|
|
170
|
+
return {
|
|
171
|
+
message: error.messageHTML,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
message: error.message,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// stackTrace formatted by V8
|
|
179
|
+
if (Error.captureStackTrace) {
|
|
180
|
+
return {
|
|
181
|
+
message: error.message,
|
|
182
|
+
stack: getErrorStackWithoutErrorMessage(error),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
message: error.message,
|
|
187
|
+
stack: error.stack ? ` ${error.stack}` : null,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (typeof error === "object") {
|
|
191
|
+
return error
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
message: JSON.stringify(error),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const getErrorStackWithoutErrorMessage = (error) => {
|
|
199
|
+
let stack = error.stack
|
|
200
|
+
const messageInStack = `${error.name}: ${error.message}`
|
|
201
|
+
if (stack.startsWith(messageInStack)) {
|
|
202
|
+
stack = stack.slice(messageInStack.length)
|
|
203
|
+
}
|
|
204
|
+
const nextLineIndex = stack.indexOf("\n")
|
|
205
|
+
if (nextLineIndex > -1) {
|
|
206
|
+
stack = stack.slice(nextLineIndex + 1)
|
|
207
|
+
}
|
|
208
|
+
return stack
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const formatTip = ({ reportedBy, requestedRessource }) => {
|
|
212
|
+
if (reportedBy === "browser") {
|
|
213
|
+
return `Reported by the browser while executing <code>${window.location.pathname}${window.location.search}</code>.`
|
|
214
|
+
}
|
|
215
|
+
return `Reported by the server while serving <code>${requestedRessource}</code>`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const makeLinksClickable = (string, { createLink = (url) => url }) => {
|
|
219
|
+
// normalize line breaks
|
|
220
|
+
string = string.replace(/\n/g, "\n")
|
|
221
|
+
string = escapeHtml(string)
|
|
222
|
+
// render links
|
|
223
|
+
string = stringToStringWithLink(string, {
|
|
224
|
+
transform: (url, { line, column }) => {
|
|
225
|
+
const { href, text } = createLink(url, { line, column })
|
|
226
|
+
return link({ href, text })
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
return string
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const escapeHtml = (string) => {
|
|
233
|
+
return string
|
|
234
|
+
.replace(/&/g, "&")
|
|
235
|
+
.replace(/</g, "<")
|
|
236
|
+
.replace(/>/g, ">")
|
|
237
|
+
.replace(/"/g, """)
|
|
238
|
+
.replace(/'/g, "'")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// `Error: yo
|
|
242
|
+
// at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
|
|
243
|
+
// at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
|
|
244
|
+
// at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
|
|
245
|
+
// at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
|
|
246
|
+
// debugger
|
|
247
|
+
// })
|
|
248
|
+
const stringToStringWithLink = (
|
|
249
|
+
source,
|
|
250
|
+
{
|
|
251
|
+
transform = (url) => {
|
|
252
|
+
return {
|
|
253
|
+
href: url,
|
|
254
|
+
text: url,
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
} = {},
|
|
258
|
+
) => {
|
|
259
|
+
return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
|
|
260
|
+
let linkHTML = ""
|
|
261
|
+
|
|
262
|
+
const lastChar = match[match.length - 1]
|
|
263
|
+
|
|
264
|
+
// hotfix because our url regex sucks a bit
|
|
265
|
+
const endsWithSeparationChar = lastChar === ")" || lastChar === ":"
|
|
266
|
+
if (endsWithSeparationChar) {
|
|
267
|
+
match = match.slice(0, -1)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/
|
|
271
|
+
const lineAndColumMatch = match.match(lineAndColumnPattern)
|
|
272
|
+
if (lineAndColumMatch) {
|
|
273
|
+
const lineAndColumnString = lineAndColumMatch[0]
|
|
274
|
+
const lineNumber = lineAndColumMatch[1]
|
|
275
|
+
const columnNumber = lineAndColumMatch[2]
|
|
276
|
+
linkHTML = transform(match.slice(0, -lineAndColumnString.length), {
|
|
277
|
+
line: lineNumber,
|
|
278
|
+
column: columnNumber,
|
|
279
|
+
})
|
|
280
|
+
} else {
|
|
281
|
+
const linePattern = /:([0-9]+)$/
|
|
282
|
+
const lineMatch = match.match(linePattern)
|
|
283
|
+
if (lineMatch) {
|
|
284
|
+
const lineString = lineMatch[0]
|
|
285
|
+
const lineNumber = lineMatch[1]
|
|
286
|
+
linkHTML = transform(match.slice(0, -lineString.length), {
|
|
287
|
+
line: lineNumber,
|
|
288
|
+
})
|
|
289
|
+
} else {
|
|
290
|
+
linkHTML = transform(match, {})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (endsWithSeparationChar) {
|
|
294
|
+
return `${linkHTML}${lastChar}`
|
|
295
|
+
}
|
|
296
|
+
return linkHTML
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const link = ({ href, text = href }) => `<a href="${href}">${text}</a>`
|