@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/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 (!request.ressource.startsWith("/__open_in_editor__/")) {
12050
- return null;
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
- const file = request.ressource.slice("/__open_in_editor__/".length);
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
- if (!file) {
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: 400,
12058
- body: 'Missing "file" in url search params'
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
- const launch = requireFromJsenv("launch-editor");
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.1",
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, "&amp;")
235
+ .replace(/</g, "&lt;")
236
+ .replace(/>/g, "&gt;")
237
+ .replace(/"/g, "&quot;")
238
+ .replace(/'/g, "&#039;")
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>`