@jsenv/core 27.5.0 → 27.5.3

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]+)$/);
12074
+
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);
12054
12086
 
12055
- if (!file) {
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,
@@ -23641,14 +23684,15 @@ const startDevServer = async ({
23641
23684
  handleSIGINT = true,
23642
23685
  logLevel = "info",
23643
23686
  omegaServerLogLevel = "warn",
23644
- port = 3456,
23645
23687
  protocol = "http",
23646
- acceptAnyIp,
23647
23688
  // it's better to use http1 by default because it allows to get statusText in devtools
23648
23689
  // which gives valuable information when there is errors
23649
23690
  http2 = false,
23650
23691
  certificate,
23651
23692
  privateKey,
23693
+ host,
23694
+ port = 3456,
23695
+ acceptAnyIp,
23652
23696
  keepProcessAlive = true,
23653
23697
  services,
23654
23698
  rootDirectoryUrl,
@@ -23786,11 +23830,12 @@ const startDevServer = async ({
23786
23830
  logLevel: omegaServerLogLevel,
23787
23831
  keepProcessAlive,
23788
23832
  acceptAnyIp,
23789
- port,
23790
23833
  protocol,
23791
23834
  http2,
23792
23835
  certificate,
23793
23836
  privateKey,
23837
+ host,
23838
+ port,
23794
23839
  services,
23795
23840
  rootDirectoryUrl,
23796
23841
  scenario: "dev",
@@ -25670,7 +25715,7 @@ const executeTestPlan = async ({
25670
25715
  keepRunning = false,
25671
25716
  cooldownBetweenExecutions = 0,
25672
25717
  gcBetweenExecutions = logMemoryHeapUsage,
25673
- coverageEnabled = process.argv.includes("--cover") || process.argv.includes("--coverage"),
25718
+ coverageEnabled = process.argv.includes("--coverage"),
25674
25719
  coverageConfig = {
25675
25720
  "./src/": true
25676
25721
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "27.5.0",
3
+ "version": "27.5.3",
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",
@@ -15,14 +15,15 @@ export const startDevServer = async ({
15
15
  handleSIGINT = true,
16
16
  logLevel = "info",
17
17
  omegaServerLogLevel = "warn",
18
- port = 3456,
19
18
  protocol = "http",
20
- acceptAnyIp,
21
19
  // it's better to use http1 by default because it allows to get statusText in devtools
22
20
  // which gives valuable information when there is errors
23
21
  http2 = false,
24
22
  certificate,
25
23
  privateKey,
24
+ host,
25
+ port = 3456,
26
+ acceptAnyIp,
26
27
  keepProcessAlive = true,
27
28
  services,
28
29
 
@@ -148,11 +149,12 @@ export const startDevServer = async ({
148
149
  logLevel: omegaServerLogLevel,
149
150
  keepProcessAlive,
150
151
  acceptAnyIp,
151
- port,
152
152
  protocol,
153
153
  http2,
154
154
  certificate,
155
155
  privateKey,
156
+ host,
157
+ port,
156
158
  services,
157
159
 
158
160
  rootDirectoryUrl,
@@ -0,0 +1,305 @@
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 (
107
+ url &&
108
+ !stack &&
109
+ // ignore window.reportError() it gives no valuable info
110
+ !url.endsWith("html_supervisor_installer.js")
111
+ ) {
112
+ onErrorLocated(resolveUrlSite({ url, line, column }))
113
+ }
114
+
115
+ let text
116
+
117
+ if (message && stack) {
118
+ text = `${generateClickableText(message)}\n${generateClickableText(stack)}`
119
+ } else if (stack) {
120
+ text = generateClickableText(stack)
121
+ } else {
122
+ text = generateClickableText(message)
123
+ }
124
+
125
+ if (codeFrame) {
126
+ text += `\n\n${generateClickableText(codeFrame)}`
127
+ }
128
+
129
+ return {
130
+ theme:
131
+ error && error.cause && error.cause.code === "PARSE_ERROR"
132
+ ? "light"
133
+ : "dark",
134
+ title: "An error occured",
135
+ text,
136
+ codeFramePromise: codeFramePromiseReference.current,
137
+ tip: `${tip}
138
+ <br />
139
+ Click outside to close.`,
140
+ }
141
+ }
142
+
143
+ const formatUrlWithLineAndColumn = ({ url, line, column }) => {
144
+ return line === undefined && column === undefined
145
+ ? url
146
+ : column === undefined
147
+ ? `${url}:${line}`
148
+ : `${url}:${line}:${column}`
149
+ }
150
+
151
+ const normalizeErrorParts = (error) => {
152
+ if (error === undefined) {
153
+ return {
154
+ message: "undefined",
155
+ }
156
+ }
157
+ if (error === null) {
158
+ return {
159
+ message: "null",
160
+ }
161
+ }
162
+ if (typeof error === "string") {
163
+ return {
164
+ message: error,
165
+ }
166
+ }
167
+ if (error instanceof Error) {
168
+ if (error.name === "SyntaxError") {
169
+ return {
170
+ message: error.message,
171
+ }
172
+ }
173
+ if (error.cause && error.cause.code === "PARSE_ERROR") {
174
+ if (error.messageHTML) {
175
+ return {
176
+ message: error.messageHTML,
177
+ }
178
+ }
179
+ return {
180
+ message: error.message,
181
+ }
182
+ }
183
+ // stackTrace formatted by V8
184
+ if (Error.captureStackTrace) {
185
+ return {
186
+ message: error.message,
187
+ stack: getErrorStackWithoutErrorMessage(error),
188
+ }
189
+ }
190
+ return {
191
+ message: error.message,
192
+ stack: error.stack ? ` ${error.stack}` : null,
193
+ }
194
+ }
195
+ if (typeof error === "object") {
196
+ return error
197
+ }
198
+ return {
199
+ message: JSON.stringify(error),
200
+ }
201
+ }
202
+
203
+ const getErrorStackWithoutErrorMessage = (error) => {
204
+ let stack = error.stack
205
+ const messageInStack = `${error.name}: ${error.message}`
206
+ if (stack.startsWith(messageInStack)) {
207
+ stack = stack.slice(messageInStack.length)
208
+ }
209
+ const nextLineIndex = stack.indexOf("\n")
210
+ if (nextLineIndex > -1) {
211
+ stack = stack.slice(nextLineIndex + 1)
212
+ }
213
+ return stack
214
+ }
215
+
216
+ const formatTip = ({ reportedBy, requestedRessource }) => {
217
+ if (reportedBy === "browser") {
218
+ return `Reported by the browser while executing <code>${window.location.pathname}${window.location.search}</code>.`
219
+ }
220
+ return `Reported by the server while serving <code>${requestedRessource}</code>`
221
+ }
222
+
223
+ const makeLinksClickable = (string, { createLink = (url) => url }) => {
224
+ // normalize line breaks
225
+ string = string.replace(/\n/g, "\n")
226
+ string = escapeHtml(string)
227
+ // render links
228
+ string = stringToStringWithLink(string, {
229
+ transform: (url, { line, column }) => {
230
+ const { href, text } = createLink(url, { line, column })
231
+ return link({ href, text })
232
+ },
233
+ })
234
+ return string
235
+ }
236
+
237
+ const escapeHtml = (string) => {
238
+ return string
239
+ .replace(/&/g, "&amp;")
240
+ .replace(/</g, "&lt;")
241
+ .replace(/>/g, "&gt;")
242
+ .replace(/"/g, "&quot;")
243
+ .replace(/'/g, "&#039;")
244
+ }
245
+
246
+ // `Error: yo
247
+ // at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
248
+ // at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
249
+ // at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
250
+ // at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
251
+ // debugger
252
+ // })
253
+ const stringToStringWithLink = (
254
+ source,
255
+ {
256
+ transform = (url) => {
257
+ return {
258
+ href: url,
259
+ text: url,
260
+ }
261
+ },
262
+ } = {},
263
+ ) => {
264
+ return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
265
+ let linkHTML = ""
266
+
267
+ const lastChar = match[match.length - 1]
268
+
269
+ // hotfix because our url regex sucks a bit
270
+ const endsWithSeparationChar = lastChar === ")" || lastChar === ":"
271
+ if (endsWithSeparationChar) {
272
+ match = match.slice(0, -1)
273
+ }
274
+
275
+ const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/
276
+ const lineAndColumMatch = match.match(lineAndColumnPattern)
277
+ if (lineAndColumMatch) {
278
+ const lineAndColumnString = lineAndColumMatch[0]
279
+ const lineNumber = lineAndColumMatch[1]
280
+ const columnNumber = lineAndColumMatch[2]
281
+ linkHTML = transform(match.slice(0, -lineAndColumnString.length), {
282
+ line: lineNumber,
283
+ column: columnNumber,
284
+ })
285
+ } else {
286
+ const linePattern = /:([0-9]+)$/
287
+ const lineMatch = match.match(linePattern)
288
+ if (lineMatch) {
289
+ const lineString = lineMatch[0]
290
+ const lineNumber = lineMatch[1]
291
+ linkHTML = transform(match.slice(0, -lineString.length), {
292
+ line: lineNumber,
293
+ })
294
+ } else {
295
+ linkHTML = transform(match, {})
296
+ }
297
+ }
298
+ if (endsWithSeparationChar) {
299
+ return `${linkHTML}${lastChar}`
300
+ }
301
+ return linkHTML
302
+ })
303
+ }
304
+
305
+ const link = ({ href, text = href }) => `<a href="${href}">${text}</a>`