@jsenv/core 27.5.2 → 27.6.1

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.
@@ -34,17 +34,11 @@ export const createFileService = ({
34
34
  cooldownBetweenFileEvents,
35
35
  explorer,
36
36
  sourcemaps,
37
+ sourcemapsSourcesProtocol,
38
+ sourcemapsSourcesContent,
37
39
  writeGeneratedFiles,
38
40
  }) => {
39
41
  const jsenvDirectoryUrl = new URL(".jsenv/", rootDirectoryUrl).href
40
- const onErrorWhileServingFileCallbacks = []
41
- const onErrorWhileServingFile = (data) => {
42
- onErrorWhileServingFileCallbacks.forEach(
43
- (onErrorWhileServingFileCallback) => {
44
- onErrorWhileServingFileCallback(data)
45
- },
46
- )
47
- }
48
42
 
49
43
  const clientFileChangeCallbackList = []
50
44
  const clientFilesPruneCallbackList = []
@@ -113,6 +107,9 @@ export const createFileService = ({
113
107
  rootDirectoryUrl,
114
108
  scenario,
115
109
  runtimeCompat,
110
+ clientRuntimeCompat: {
111
+ [runtimeName]: runtimeVersion,
112
+ },
116
113
  urlGraph,
117
114
  plugins: [
118
115
  ...plugins,
@@ -134,6 +131,8 @@ export const createFileService = ({
134
131
  }),
135
132
  ],
136
133
  sourcemaps,
134
+ sourcemapsSourcesProtocol,
135
+ sourcemapsSourcesContent,
137
136
  writeGeneratedFiles,
138
137
  })
139
138
  serverStopCallbacks.push(() => {
@@ -170,28 +169,6 @@ export const createFileService = ({
170
169
  },
171
170
  })
172
171
  })
173
- onErrorWhileServingFileCallbacks.push((data) => {
174
- serverEventsDispatcher.dispatchToRoomsMatching(
175
- {
176
- type: "error_while_serving_file",
177
- data,
178
- },
179
- (room) => {
180
- // send only to page depending on this file
181
- const errorFileUrl = data.url
182
- const roomEntryPointUrl = new URL(
183
- room.request.ressource.slice(1),
184
- rootDirectoryUrl,
185
- ).href
186
- const isErrorRelatedToEntryPoint = Boolean(
187
- urlGraph.findDependent(errorFileUrl, (dependentUrlInfo) => {
188
- return dependentUrlInfo.url === roomEntryPointUrl
189
- }),
190
- )
191
- return isErrorRelatedToEntryPoint
192
- },
193
- )
194
- })
195
172
  // "unshift" because serve must come first to catch event source client request
196
173
  kitchen.pluginController.unshiftPlugin({
197
174
  name: "jsenv:provide_server_events",
@@ -286,6 +263,7 @@ export const createFileService = ({
286
263
  !urlInfo.isInline &&
287
264
  urlInfo.type !== "sourcemap"
288
265
  ) {
266
+ urlInfo.error = null
289
267
  urlInfo.sourcemap = null
290
268
  urlInfo.sourcemapReference = null
291
269
  urlInfo.content = null
@@ -298,9 +276,6 @@ export const createFileService = ({
298
276
  await kitchen.cook(urlInfo, {
299
277
  request,
300
278
  reference,
301
- clientRuntimeCompat: {
302
- [runtimeName]: runtimeVersion,
303
- },
304
279
  outDirectoryUrl:
305
280
  scenario === "dev"
306
281
  ? `${rootDirectoryUrl}.jsenv/${runtimeName}@${runtimeVersion}/`
@@ -333,18 +308,9 @@ export const createFileService = ({
333
308
  )
334
309
  return response
335
310
  } catch (e) {
311
+ urlInfo.error = e
336
312
  const code = e.code
337
313
  if (code === "PARSE_ERROR") {
338
- onErrorWhileServingFile({
339
- requestedRessource: request.ressource,
340
- code: "PARSE_ERROR",
341
- message: e.reason,
342
- url: e.url,
343
- traceUrl: e.traceUrl,
344
- traceLine: e.traceLine,
345
- traceColumn: e.traceColumn,
346
- traceMessage: e.traceMessage,
347
- })
348
314
  return {
349
315
  url: reference.url,
350
316
  status: 200, // let the browser re-throw the syntax error
@@ -375,19 +341,6 @@ export const createFileService = ({
375
341
  }
376
342
  }
377
343
  if (code === "NOT_FOUND") {
378
- onErrorWhileServingFile({
379
- requestedRessource: request.ressource,
380
- isFaviconAutoRequest:
381
- request.ressource === "/favicon.ico" &&
382
- reference.type === "http_request",
383
- code: "NOT_FOUND",
384
- message: e.reason,
385
- url: e.url,
386
- traceUrl: e.traceUrl,
387
- traceLine: e.traceLine,
388
- traceColumn: e.traceColumn,
389
- traceMessage: e.traceMessage,
390
- })
391
344
  return {
392
345
  url: reference.url,
393
346
  status: 404,
@@ -395,16 +348,6 @@ export const createFileService = ({
395
348
  statusMessage: e.message,
396
349
  }
397
350
  }
398
- onErrorWhileServingFile({
399
- requestedRessource: request.ressource,
400
- code: "UNEXPECTED",
401
- stack: e.stack,
402
- url: e.url,
403
- traceUrl: e.traceUrl,
404
- traceLine: e.traceLine,
405
- traceColumn: e.traceColumn,
406
- traceMessage: e.traceMessage,
407
- })
408
351
  return {
409
352
  url: reference.url,
410
353
  status: 500,
@@ -7,7 +7,6 @@ export const loadUrlGraph = async ({
7
7
  startLoading,
8
8
  writeGeneratedFiles,
9
9
  outDirectoryUrl,
10
- clientRuntimeCompat,
11
10
  }) => {
12
11
  if (writeGeneratedFiles && outDirectoryUrl) {
13
12
  await ensureEmptyDirectory(outDirectoryUrl)
@@ -19,7 +18,6 @@ export const loadUrlGraph = async ({
19
18
  if (promiseFromData) return promiseFromData
20
19
  const promise = _cook(urlInfo, {
21
20
  outDirectoryUrl,
22
- clientRuntimeCompat,
23
21
  ...context,
24
22
  })
25
23
  promises.push(promise)
@@ -11,12 +11,29 @@ import {
11
11
  export const createUrlInfoTransformer = ({
12
12
  logger,
13
13
  sourcemaps,
14
+ sourcemapsSourcesProtocol,
14
15
  sourcemapsSourcesContent,
15
16
  sourcemapsRelativeSources,
16
17
  urlGraph,
18
+ clientRuntimeCompat,
17
19
  injectSourcemapPlaceholder,
18
20
  foundSourcemap,
19
21
  }) => {
22
+ const runtimeNames = Object.keys(clientRuntimeCompat)
23
+ const chromeAsSingleRuntime =
24
+ runtimeNames.length === 1 && runtimeNames[0] === "chrome"
25
+ if (sourcemapsSourcesProtocol === undefined) {
26
+ sourcemapsSourcesProtocol = "file:///"
27
+ }
28
+ if (sourcemapsSourcesContent === undefined) {
29
+ if (chromeAsSingleRuntime && sourcemapsSourcesProtocol === "file:///") {
30
+ // chrome is able to fetch source when referenced with "file:"
31
+ sourcemapsSourcesContent = false
32
+ } else {
33
+ sourcemapsSourcesContent = true
34
+ }
35
+ }
36
+
20
37
  const sourcemapsEnabled =
21
38
  sourcemaps === "inline" ||
22
39
  sourcemaps === "file" ||
@@ -172,6 +189,16 @@ export const createUrlInfoTransformer = ({
172
189
  return sourceRelative || "."
173
190
  })
174
191
  }
192
+ if (sourcemapsSourcesProtocol !== "file:///") {
193
+ sourcemap.sources = sourcemap.sources.map((source) => {
194
+ if (source.startsWith("file:///")) {
195
+ return `${sourcemapsSourcesProtocol}${source.slice(
196
+ "file:///".length,
197
+ )}`
198
+ }
199
+ return source
200
+ })
201
+ }
175
202
  sourcemapUrlInfo.content = JSON.stringify(sourcemap, null, " ")
176
203
  if (!urlInfo.sourcemapIsWrong) {
177
204
  if (sourcemaps === "inline") {
@@ -218,6 +218,7 @@ export const createUrlGraph = ({
218
218
 
219
219
  const createUrlInfo = (url) => {
220
220
  return {
221
+ error: null,
221
222
  modifiedTimestamp: 0,
222
223
  contentEtag: null,
223
224
  dependsOnPackageJson: false,
@@ -15,6 +15,7 @@ const reloader = {
15
15
  isAutoreloadEnabled,
16
16
  setAutoreloadPreference,
17
17
  status: "idle",
18
+ currentExecution: null,
18
19
  onstatuschange: () => {},
19
20
  setStatus: (status) => {
20
21
  reloader.status = status
@@ -49,6 +50,7 @@ const reloader = {
49
50
  promise.then(
50
51
  () => {
51
52
  onApplied(reloadMessage)
53
+ reloader.currentExecution = null
52
54
  },
53
55
  (e) => {
54
56
  reloader.setStatus("failed")
@@ -61,6 +63,7 @@ const reloader = {
61
63
  `[jsenv] Hot reload failed after ${reloadMessage.reason}.
62
64
  This could be due to syntax errors or importing non-existent modules (see errors in console)`,
63
65
  )
66
+ reloader.currentExecution = null
64
67
  },
65
68
  )
66
69
  }
@@ -146,6 +149,10 @@ const applyHotReload = async ({ hotInstructions }) => {
146
149
  await urlHotMeta.disposeCallback()
147
150
  }
148
151
  console.log(`importing js module`)
152
+ reloader.currentExecution = {
153
+ type: "dynamic_import",
154
+ url: urlToFetch,
155
+ }
149
156
  const namespace = await reloadJsImport(urlToFetch)
150
157
  if (urlHotMeta.acceptCallback) {
151
158
  await urlHotMeta.acceptCallback(namespace)
@@ -1,33 +1,62 @@
1
1
  export const formatError = (
2
2
  error,
3
- {
4
- rootDirectoryUrl,
5
- errorBaseUrl,
6
- openInEditor,
7
- url,
8
- line,
9
- column,
10
- codeFrame,
11
- requestedRessource,
12
- reportedBy,
13
- },
3
+ { rootDirectoryUrl, errorBaseUrl, openInEditor, url, line, column },
14
4
  ) => {
15
5
  let { message, stack } = normalizeErrorParts(error)
16
- let codeFramePromiseReference = { current: null }
17
- let tip = formatTip({ reportedBy, requestedRessource })
6
+ let errorDetailsPromiseReference = { current: null }
7
+ let tip = `Reported by the browser while executing <code>${window.location.pathname}${window.location.search}</code>.`
18
8
  let errorUrlSite
19
9
 
10
+ const errorMeta = extractErrorMeta(error, { url, line, column })
11
+
20
12
  const resolveUrlSite = ({ url, line, column }) => {
21
- const inlineUrlMatch = url.match(/@L([0-9]+)\-L([0-9]+)\.[\w]+$/)
13
+ if (typeof line === "string") line = parseInt(line)
14
+ if (typeof column === "string") column = parseInt(column)
15
+
16
+ const inlineUrlMatch = url.match(
17
+ /@L([0-9]+)C([0-9]+)\-L([0-9]+)C([0-9]+)(\.[\w]+)$/,
18
+ )
22
19
  if (inlineUrlMatch) {
23
20
  const htmlUrl = url.slice(0, inlineUrlMatch.index)
24
- const tagLine = parseInt(inlineUrlMatch[1])
25
- const tagColumn = parseInt(inlineUrlMatch[2])
21
+ const tagLineStart = parseInt(inlineUrlMatch[1])
22
+ const tagColumnStart = parseInt(inlineUrlMatch[2])
23
+ const tagLineEnd = parseInt(inlineUrlMatch[3])
24
+ const tagColumnEnd = parseInt(inlineUrlMatch[4])
25
+ const extension = inlineUrlMatch[5]
26
26
  url = htmlUrl
27
- line = tagLine + parseInt(line) - 1
28
- column = tagColumn + parseInt(column)
27
+ line = tagLineStart + (typeof line === "number" ? line : 0)
28
+ // stackTrace formatted by V8 (chrome)
29
+ if (Error.captureStackTrace) {
30
+ line--
31
+ }
32
+ if (errorMeta.type === "dynamic_import_syntax_error") {
33
+ // syntax error on inline script need line-1 for some reason
34
+ if (Error.captureStackTrace) {
35
+ line--
36
+ } else {
37
+ // firefox and safari need line-2
38
+ line -= 2
39
+ }
40
+ }
41
+ column = tagColumnStart + (typeof column === "number" ? column : 0)
42
+ const fileUrl = resolveFileUrl(url)
43
+ return {
44
+ isInline: true,
45
+ originalUrl: `${fileUrl}@L${tagLineStart}C${tagColumnStart}-L${tagLineEnd}C${tagColumnEnd}${extension}`,
46
+ url: fileUrl,
47
+ line,
48
+ column,
49
+ }
29
50
  }
51
+ return {
52
+ isInline: false,
53
+ url: resolveFileUrl(url),
54
+ line,
55
+ column,
56
+ }
57
+ }
30
58
 
59
+ const resolveFileUrl = (url) => {
31
60
  let urlObject = new URL(url)
32
61
  if (urlObject.origin === window.origin) {
33
62
  urlObject = new URL(
@@ -39,19 +68,10 @@ export const formatError = (
39
68
  const atFsIndex = urlObject.pathname.indexOf("/@fs/")
40
69
  if (atFsIndex > -1) {
41
70
  const afterAtFs = urlObject.pathname.slice(atFsIndex + "/@fs/".length)
42
- url = new URL(afterAtFs, "file:///").href
43
- } else {
44
- url = urlObject.href
71
+ return new URL(afterAtFs, "file:///").href
45
72
  }
46
- } else {
47
- url = urlObject.href
48
- }
49
-
50
- return {
51
- url,
52
- line,
53
- column,
54
73
  }
74
+ return urlObject.href
55
75
  }
56
76
 
57
77
  const generateClickableText = (text) => {
@@ -59,7 +79,7 @@ export const formatError = (
59
79
  createLink: (url, { line, column }) => {
60
80
  const urlSite = resolveUrlSite({ url, line, column })
61
81
  if (!errorUrlSite && text === stack) {
62
- onErrorLocated(urlSite)
82
+ onErrorLocated(urlSite, "error.stack")
63
83
  }
64
84
  if (errorBaseUrl) {
65
85
  if (urlSite.url.startsWith(rootDirectoryUrl)) {
@@ -83,42 +103,82 @@ export const formatError = (
83
103
  return textWithHtmlLinks
84
104
  }
85
105
 
86
- const onErrorLocated = (urlSite) => {
87
- errorUrlSite = urlSite
88
- if (codeFrame) {
89
- return
106
+ const formatErrorText = ({ message, stack, codeFrame }) => {
107
+ let text
108
+ if (message && stack) {
109
+ text = `${generateClickableText(message)}\n${generateClickableText(
110
+ stack,
111
+ )}`
112
+ } else if (stack) {
113
+ text = generateClickableText(stack)
114
+ } else {
115
+ text = generateClickableText(message)
90
116
  }
91
- if (reportedBy !== "browser") {
92
- return
117
+ if (codeFrame) {
118
+ text += `\n\n${generateClickableText(codeFrame)}`
93
119
  }
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
120
+ return text
121
+ }
122
+
123
+ const onErrorLocated = (urlSite) => {
124
+ errorUrlSite = urlSite
125
+ errorDetailsPromiseReference.current = (async () => {
126
+ if (errorMeta.type === "dynamic_import_fetch_error") {
127
+ const response = await window.fetch(
128
+ `/__get_error_cause__/${
129
+ urlSite.isInline ? urlSite.originalUrl : urlSite.url
130
+ }`,
131
+ )
132
+ if (response.status !== 200) {
133
+ return null
134
+ }
135
+ const causeInfo = await response.json()
136
+ if (!causeInfo) {
137
+ return null
138
+ }
139
+
140
+ const causeText =
141
+ causeInfo.code === "NOT_FOUND"
142
+ ? formatErrorText({
143
+ message: causeInfo.reason,
144
+ stack: causeInfo.codeFrame,
145
+ })
146
+ : formatErrorText({
147
+ message: causeInfo.stack,
148
+ stack: causeInfo.codeFrame,
149
+ })
150
+ return {
151
+ cause: causeText,
152
+ }
153
+ }
154
+ if (urlSite.line !== undefined) {
155
+ let ressourceToFetch = `/__get_code_frame__/${formatUrlWithLineAndColumn(
156
+ urlSite,
157
+ )}`
158
+ if (!Error.captureStackTrace) {
159
+ ressourceToFetch += `?remap`
160
+ }
161
+ const response = await window.fetch(ressourceToFetch)
162
+ const codeFrame = await response.text()
163
+ return {
164
+ codeFrame: formatErrorText({ message: codeFrame }),
165
+ }
166
+ }
167
+ return null
101
168
  })()
102
169
  }
103
170
 
104
171
  // error.stack is more reliable than url/line/column reported on window error events
105
172
  // so use it only when error.stack is not available
106
- if (url && !stack) {
173
+ if (
174
+ url &&
175
+ !stack &&
176
+ // ignore window.reportError() it gives no valuable info
177
+ !url.endsWith("html_supervisor_installer.js")
178
+ ) {
107
179
  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)}`
180
+ } else if (errorMeta.url) {
181
+ onErrorLocated(resolveUrlSite(errorMeta))
122
182
  }
123
183
 
124
184
  return {
@@ -127,12 +187,79 @@ export const formatError = (
127
187
  ? "light"
128
188
  : "dark",
129
189
  title: "An error occured",
130
- text,
131
- codeFramePromise: codeFramePromiseReference.current,
190
+ text: formatErrorText({ message, stack }),
132
191
  tip: `${tip}
133
192
  <br />
134
193
  Click outside to close.`,
194
+ errorDetailsPromise: errorDetailsPromiseReference.current,
195
+ }
196
+ }
197
+
198
+ const extractErrorMeta = (error, { line }) => {
199
+ if (!error) {
200
+ return {}
201
+ }
202
+ const { message } = error
203
+ if (!message) {
204
+ return {}
205
+ }
206
+
207
+ export_missing: {
208
+ // chrome
209
+ if (message.includes("does not provide an export named")) {
210
+ return {
211
+ type: "dynamic_import_export_missing",
212
+ }
213
+ }
214
+ // firefox
215
+ if (message.startsWith("import not found:")) {
216
+ return {
217
+ type: "dynamic_import_export_missing",
218
+ browser: "firefox",
219
+ }
220
+ }
221
+ // safari
222
+ if (message.startsWith("import binding name")) {
223
+ return {
224
+ type: "dynamic_import_export_missing",
225
+ }
226
+ }
227
+ }
228
+
229
+ js_syntax_error: {
230
+ if (error.name === "SyntaxError" && typeof line === "number") {
231
+ return {
232
+ type: "dynamic_import_syntax_error",
233
+ }
234
+ }
235
+ }
236
+
237
+ fetch_error: {
238
+ // chrome
239
+ if (message.startsWith("Failed to fetch dynamically imported module: ")) {
240
+ const url = error.message.slice(
241
+ "Failed to fetch dynamically imported module: ".length,
242
+ )
243
+ return {
244
+ type: "dynamic_import_fetch_error",
245
+ url,
246
+ }
247
+ }
248
+ // firefox
249
+ if (message === "error loading dynamically imported module") {
250
+ return {
251
+ type: "dynamic_import_fetch_error",
252
+ }
253
+ }
254
+ // safari
255
+ if (message === "Importing a module script failed.") {
256
+ return {
257
+ type: "dynamic_import_fetch_error",
258
+ }
259
+ }
135
260
  }
261
+
262
+ return {}
136
263
  }
137
264
 
138
265
  const formatUrlWithLineAndColumn = ({ url, line, column }) => {
@@ -208,13 +335,6 @@ const getErrorStackWithoutErrorMessage = (error) => {
208
335
  return stack
209
336
  }
210
337
 
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
338
  const makeLinksClickable = (string, { createLink = (url) => url }) => {
219
339
  // normalize line breaks
220
340
  string = string.replace(/\n/g, "\n")
@@ -12,11 +12,9 @@ export const displayErrorInDocument = (
12
12
  line,
13
13
  column,
14
14
  codeFrame,
15
- reportedBy,
16
- requestedRessource,
17
15
  },
18
16
  ) => {
19
- const { theme, title, text, codeFramePromise, tip } = formatError(error, {
17
+ const { theme, title, text, tip, errorDetailsPromise } = formatError(error, {
20
18
  rootDirectoryUrl,
21
19
  errorBaseUrl,
22
20
  openInEditor,
@@ -24,16 +22,14 @@ export const displayErrorInDocument = (
24
22
  line,
25
23
  column,
26
24
  codeFrame,
27
- reportedBy,
28
- requestedRessource,
29
25
  })
30
26
 
31
27
  let jsenvErrorOverlay = new JsenvErrorOverlay({
32
28
  theme,
33
29
  title,
34
30
  text,
35
- codeFramePromise,
36
31
  tip,
32
+ errorDetailsPromise,
37
33
  })
38
34
  document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
39
35
  node.parentNode.removeChild(node)
@@ -56,7 +52,7 @@ export const displayErrorInDocument = (
56
52
  }
57
53
 
58
54
  class JsenvErrorOverlay extends HTMLElement {
59
- constructor({ theme, title, text, codeFramePromise, tip }) {
55
+ constructor({ theme, title, text, tip, errorDetailsPromise }) {
60
56
  super()
61
57
  this.root = this.attachShadow({ mode: "open" })
62
58
  this.root.innerHTML = `
@@ -81,16 +77,39 @@ class JsenvErrorOverlay extends HTMLElement {
81
77
  this.root.querySelector(".backdrop").onclick = null
82
78
  this.parentNode.removeChild(this)
83
79
  }
84
- if (codeFramePromise) {
85
- codeFramePromise.then((codeFrame) => {
86
- if (this.parentNode) {
80
+ if (errorDetailsPromise) {
81
+ errorDetailsPromise.then((errorDetails) => {
82
+ if (!errorDetails || !this.parentNode) {
83
+ return
84
+ }
85
+ const { codeFrame, cause } = errorDetails
86
+ if (codeFrame) {
87
87
  this.root.querySelector(".text").innerHTML += `\n\n${codeFrame}`
88
88
  }
89
+ if (cause) {
90
+ const causeIndented = prefixRemainingLines(cause, " ")
91
+ this.root.querySelector(
92
+ ".text",
93
+ ).innerHTML += `\n [cause]: ${causeIndented}`
94
+ }
89
95
  })
90
96
  }
91
97
  }
92
98
  }
93
99
 
100
+ const prefixRemainingLines = (text, prefix) => {
101
+ const lines = text.split(/\r?\n/)
102
+ const firstLine = lines.shift()
103
+ let result = firstLine
104
+ let i = 0
105
+ while (i < lines.length) {
106
+ const line = lines[i]
107
+ i++
108
+ result += line.length ? `\n${prefix}${line}` : `\n`
109
+ }
110
+ return result
111
+ }
112
+
94
113
  if (customElements && !customElements.get(JSENV_ERROR_OVERLAY_TAGNAME)) {
95
114
  customElements.define(JSENV_ERROR_OVERLAY_TAGNAME, JsenvErrorOverlay)
96
115
  }