@jsenv/core 27.5.3 → 27.6.0

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.
@@ -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,59 @@
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
+ const inlineUrlMatch = url.match(
14
+ /@L([0-9]+)C([0-9]+)\-L([0-9]+)C([0-9]+)(\.[\w]+)$/,
15
+ )
22
16
  if (inlineUrlMatch) {
23
17
  const htmlUrl = url.slice(0, inlineUrlMatch.index)
24
- const tagLine = parseInt(inlineUrlMatch[1])
25
- const tagColumn = parseInt(inlineUrlMatch[2])
18
+ const tagLineStart = parseInt(inlineUrlMatch[1])
19
+ const tagColumnStart = parseInt(inlineUrlMatch[2])
20
+ const tagLineEnd = parseInt(inlineUrlMatch[3])
21
+ const tagColumnEnd = parseInt(inlineUrlMatch[4])
22
+ const extension = inlineUrlMatch[5]
26
23
  url = htmlUrl
27
- line = tagLine + parseInt(line) - 1
28
- column = tagColumn + parseInt(column)
24
+ line = tagLineStart + (line === undefined ? 0 : parseInt(line))
25
+ // stackTrace formatted by V8 (chrome)
26
+ if (Error.captureStackTrace) {
27
+ line--
28
+ }
29
+ if (errorMeta.type === "dynamic_import_syntax_error") {
30
+ // syntax error on inline script need line-1 for some reason
31
+ if (Error.captureStackTrace) {
32
+ line--
33
+ } else {
34
+ // firefox and safari need line-2
35
+ line -= 2
36
+ }
37
+ }
38
+ column = tagColumnStart + (column === undefined ? 0 : parseInt(column))
39
+ const fileUrl = resolveFileUrl(url)
40
+ return {
41
+ isInline: true,
42
+ originalUrl: `${fileUrl}@L${tagLineStart}C${tagColumnStart}-L${tagLineEnd}C${tagColumnEnd}${extension}`,
43
+ url: fileUrl,
44
+ line,
45
+ column,
46
+ }
47
+ }
48
+ return {
49
+ isInline: false,
50
+ url: resolveFileUrl(url),
51
+ line,
52
+ column,
29
53
  }
54
+ }
30
55
 
56
+ const resolveFileUrl = (url) => {
31
57
  let urlObject = new URL(url)
32
58
  if (urlObject.origin === window.origin) {
33
59
  urlObject = new URL(
@@ -39,19 +65,10 @@ export const formatError = (
39
65
  const atFsIndex = urlObject.pathname.indexOf("/@fs/")
40
66
  if (atFsIndex > -1) {
41
67
  const afterAtFs = urlObject.pathname.slice(atFsIndex + "/@fs/".length)
42
- url = new URL(afterAtFs, "file:///").href
43
- } else {
44
- url = urlObject.href
68
+ return new URL(afterAtFs, "file:///").href
45
69
  }
46
- } else {
47
- url = urlObject.href
48
- }
49
-
50
- return {
51
- url,
52
- line,
53
- column,
54
70
  }
71
+ return urlObject.href
55
72
  }
56
73
 
57
74
  const generateClickableText = (text) => {
@@ -59,7 +76,7 @@ export const formatError = (
59
76
  createLink: (url, { line, column }) => {
60
77
  const urlSite = resolveUrlSite({ url, line, column })
61
78
  if (!errorUrlSite && text === stack) {
62
- onErrorLocated(urlSite)
79
+ onErrorLocated(urlSite, "error.stack")
63
80
  }
64
81
  if (errorBaseUrl) {
65
82
  if (urlSite.url.startsWith(rootDirectoryUrl)) {
@@ -83,21 +100,64 @@ export const formatError = (
83
100
  return textWithHtmlLinks
84
101
  }
85
102
 
86
- const onErrorLocated = (urlSite) => {
87
- errorUrlSite = urlSite
88
- if (codeFrame) {
89
- return
103
+ const formatErrorText = ({ message, stack, codeFrame }) => {
104
+ let text
105
+ if (message && stack) {
106
+ text = `${generateClickableText(message)}\n${generateClickableText(
107
+ stack,
108
+ )}`
109
+ } else if (stack) {
110
+ text = generateClickableText(stack)
111
+ } else {
112
+ text = generateClickableText(message)
90
113
  }
91
- if (reportedBy !== "browser") {
92
- return
114
+ if (codeFrame) {
115
+ text += `\n\n${generateClickableText(codeFrame)}`
93
116
  }
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
117
+ return text
118
+ }
119
+
120
+ const onErrorLocated = (urlSite) => {
121
+ errorUrlSite = urlSite
122
+ errorDetailsPromiseReference.current = (async () => {
123
+ if (errorMeta.type === "dynamic_import_fetch_error") {
124
+ const response = await window.fetch(
125
+ `/__get_error_cause__/${
126
+ urlSite.isInline ? urlSite.originalUrl : urlSite.url
127
+ }`,
128
+ )
129
+ if (response.status !== 200) {
130
+ return null
131
+ }
132
+ const causeInfo = await response.json()
133
+ if (!causeInfo) {
134
+ return null
135
+ }
136
+
137
+ const causeText =
138
+ causeInfo.code === "NOT_FOUND"
139
+ ? formatErrorText({
140
+ message: causeInfo.reason,
141
+ stack: causeInfo.codeFrame,
142
+ })
143
+ : formatErrorText({
144
+ message: causeInfo.stack,
145
+ stack: causeInfo.codeFrame,
146
+ })
147
+ return {
148
+ cause: causeText,
149
+ }
150
+ }
151
+ if (urlSite.line !== undefined) {
152
+ const response = await window.fetch(
153
+ `/__get_code_frame__/${formatUrlWithLineAndColumn(urlSite)}`,
154
+ )
155
+ const codeFrame = await response.text()
156
+ return {
157
+ codeFrame: formatErrorText({ message: codeFrame }),
158
+ }
159
+ }
160
+ return null
101
161
  })()
102
162
  }
103
163
 
@@ -110,20 +170,8 @@ export const formatError = (
110
170
  !url.endsWith("html_supervisor_installer.js")
111
171
  ) {
112
172
  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)}`
173
+ } else if (errorMeta.url) {
174
+ onErrorLocated(resolveUrlSite(errorMeta))
127
175
  }
128
176
 
129
177
  return {
@@ -132,14 +180,81 @@ export const formatError = (
132
180
  ? "light"
133
181
  : "dark",
134
182
  title: "An error occured",
135
- text,
136
- codeFramePromise: codeFramePromiseReference.current,
183
+ text: formatErrorText({ message, stack }),
137
184
  tip: `${tip}
138
185
  <br />
139
186
  Click outside to close.`,
187
+ errorDetailsPromise: errorDetailsPromiseReference.current,
140
188
  }
141
189
  }
142
190
 
191
+ const extractErrorMeta = (error, { line }) => {
192
+ if (!error) {
193
+ return {}
194
+ }
195
+ const { message } = error
196
+ if (!message) {
197
+ return {}
198
+ }
199
+
200
+ export_missing: {
201
+ // chrome
202
+ if (message.includes("does not provide an export named")) {
203
+ return {
204
+ type: "dynamic_import_export_missing",
205
+ }
206
+ }
207
+ // firefox
208
+ if (message.startsWith("import not found:")) {
209
+ return {
210
+ type: "dynamic_import_export_missing",
211
+ browser: "firefox",
212
+ }
213
+ }
214
+ // safari
215
+ if (message.startsWith("import binding name")) {
216
+ return {
217
+ type: "dynamic_import_export_missing",
218
+ }
219
+ }
220
+ }
221
+
222
+ js_syntax_error: {
223
+ if (error.name === "SyntaxError" && typeof line === "number") {
224
+ return {
225
+ type: "dynamic_import_syntax_error",
226
+ }
227
+ }
228
+ }
229
+
230
+ fetch_error: {
231
+ // chrome
232
+ if (message.startsWith("Failed to fetch dynamically imported module: ")) {
233
+ const url = error.message.slice(
234
+ "Failed to fetch dynamically imported module: ".length,
235
+ )
236
+ return {
237
+ type: "dynamic_import_fetch_error",
238
+ url,
239
+ }
240
+ }
241
+ // firefox
242
+ if (message === "error loading dynamically imported module") {
243
+ return {
244
+ type: "dynamic_import_fetch_error",
245
+ }
246
+ }
247
+ // safari
248
+ if (message === "Importing a module script failed.") {
249
+ return {
250
+ type: "dynamic_import_fetch_error",
251
+ }
252
+ }
253
+ }
254
+
255
+ return {}
256
+ }
257
+
143
258
  const formatUrlWithLineAndColumn = ({ url, line, column }) => {
144
259
  return line === undefined && column === undefined
145
260
  ? url
@@ -213,13 +328,6 @@ const getErrorStackWithoutErrorMessage = (error) => {
213
328
  return stack
214
329
  }
215
330
 
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
331
  const makeLinksClickable = (string, { createLink = (url) => url }) => {
224
332
  // normalize line breaks
225
333
  string = string.replace(/\n/g, "\n")
@@ -2,8 +2,6 @@ import { formatError } from "./error_formatter.js"
2
2
 
3
3
  const JSENV_ERROR_OVERLAY_TAGNAME = "jsenv-error-overlay"
4
4
 
5
- let previousErrorInfo = null
6
-
7
5
  export const displayErrorInDocument = (
8
6
  error,
9
7
  {
@@ -14,30 +12,9 @@ export const displayErrorInDocument = (
14
12
  line,
15
13
  column,
16
14
  codeFrame,
17
- reportedBy,
18
- requestedRessource,
19
15
  },
20
16
  ) => {
21
- const nowMs = Date.now()
22
- // ensure error dispatched on window by browser is displayed first
23
- // then the server error replaces it (because it contains more information)
24
- if (previousErrorInfo) {
25
- const previousErrorReportedBy = previousErrorInfo.reportedBy
26
- const msEllapsedSincePreviousError = nowMs - previousErrorInfo.ms
27
- if (
28
- previousErrorReportedBy === "server" &&
29
- reportedBy === "browser" &&
30
- msEllapsedSincePreviousError < 50
31
- ) {
32
- return () => {}
33
- }
34
- }
35
- previousErrorInfo = {
36
- ms: nowMs,
37
- reportedBy,
38
- }
39
-
40
- const { theme, title, text, codeFramePromise, tip } = formatError(error, {
17
+ const { theme, title, text, tip, errorDetailsPromise } = formatError(error, {
41
18
  rootDirectoryUrl,
42
19
  errorBaseUrl,
43
20
  openInEditor,
@@ -45,16 +22,14 @@ export const displayErrorInDocument = (
45
22
  line,
46
23
  column,
47
24
  codeFrame,
48
- reportedBy,
49
- requestedRessource,
50
25
  })
51
26
 
52
27
  let jsenvErrorOverlay = new JsenvErrorOverlay({
53
28
  theme,
54
29
  title,
55
30
  text,
56
- codeFramePromise,
57
31
  tip,
32
+ errorDetailsPromise,
58
33
  })
59
34
  document.querySelectorAll(JSENV_ERROR_OVERLAY_TAGNAME).forEach((node) => {
60
35
  node.parentNode.removeChild(node)
@@ -77,7 +52,7 @@ export const displayErrorInDocument = (
77
52
  }
78
53
 
79
54
  class JsenvErrorOverlay extends HTMLElement {
80
- constructor({ theme, title, text, codeFramePromise, tip }) {
55
+ constructor({ theme, title, text, tip, errorDetailsPromise }) {
81
56
  super()
82
57
  this.root = this.attachShadow({ mode: "open" })
83
58
  this.root.innerHTML = `
@@ -102,16 +77,39 @@ class JsenvErrorOverlay extends HTMLElement {
102
77
  this.root.querySelector(".backdrop").onclick = null
103
78
  this.parentNode.removeChild(this)
104
79
  }
105
- if (codeFramePromise) {
106
- codeFramePromise.then((codeFrame) => {
107
- 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) {
108
87
  this.root.querySelector(".text").innerHTML += `\n\n${codeFrame}`
109
88
  }
89
+ if (cause) {
90
+ const causeIndented = prefixRemainingLines(cause, " ")
91
+ this.root.querySelector(
92
+ ".text",
93
+ ).innerHTML += `\n [cause]: ${causeIndented}`
94
+ }
110
95
  })
111
96
  }
112
97
  }
113
98
  }
114
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
+
115
113
  if (customElements && !customElements.get(JSENV_ERROR_OVERLAY_TAGNAME)) {
116
114
  customElements.define(JSENV_ERROR_OVERLAY_TAGNAME, JsenvErrorOverlay)
117
115
  }
@@ -0,0 +1,85 @@
1
+ import { memoize } from "@jsenv/utils/src/memoize/memoize.js"
2
+ import { remapSourcePosition } from "@jsenv/sourcemap/src/error_stack_remap/remap_source_position.js"
3
+ import { SOURCEMAP } from "@jsenv/sourcemap/src/sourcemap_comment.js"
4
+ import { DATA_URL } from "@jsenv/urls/src/data_url.js"
5
+
6
+ const loadSourceMapConsumer = memoize(async () => {
7
+ const script = document.createElement("script")
8
+ script.src = "https://unpkg.com/source-map@0.7.3/dist/source-map.js"
9
+
10
+ const scriptLoadedPromise = new Promise((resolve) => {
11
+ script.onload = resolve
12
+ })
13
+ document.head.appendChild(script)
14
+ await scriptLoadedPromise
15
+ const { SourceMapConsumer } = window.sourceMap
16
+ await SourceMapConsumer.initialize({
17
+ "lib/mappings.wasm": "https://unpkg.com/source-map@0.7.3/lib/mappings.wasm",
18
+ })
19
+ return SourceMapConsumer
20
+ })
21
+
22
+ export const remapErrorSite = async ({ url, line, column }) => {
23
+ const asServerUrl = (url) => {
24
+ if (url.startsWith("file:///")) {
25
+ url = `${window.origin}/@fs/${url.slice("file:///".length)}`
26
+ }
27
+ return url
28
+ }
29
+
30
+ const SourceMapConsumer = await loadSourceMapConsumer()
31
+ const original = await remapSourcePosition({
32
+ source: url,
33
+ line,
34
+ column,
35
+ resolveFile: (specifier) => new URL(specifier, `${window.origin}/`).href,
36
+ urlToSourcemapConsumer: async (url) => {
37
+ const serverUrl = asServerUrl(url)
38
+ const fileResponse = await window.fetch(serverUrl)
39
+ const text = await fileResponse.text()
40
+ const jsSourcemapComment = SOURCEMAP.readComment({
41
+ contentType: "text/javascript",
42
+ content: text,
43
+ })
44
+
45
+ const jsSourcemapUrl = jsSourcemapComment.specifier
46
+ let sourcemapUrl
47
+ let sourcemapUrlContent
48
+ if (jsSourcemapUrl.startsWith("data:")) {
49
+ sourcemapUrl = url
50
+ sourcemapUrlContent = window.atob(DATA_URL.parse(jsSourcemapUrl).data)
51
+ } else {
52
+ sourcemapUrl = new URL(jsSourcemapUrl, url).href
53
+ const sourcemapResponse = await window.fetch(sourcemapUrl)
54
+ sourcemapUrlContent = await sourcemapResponse.text()
55
+ }
56
+
57
+ const sourceMap = JSON.parse(sourcemapUrlContent)
58
+ let { sourcesContent } = sourceMap
59
+ if (!sourcesContent) {
60
+ sourcesContent = []
61
+ sourceMap.sourcesContent = sourcesContent
62
+ }
63
+ let firstSourceMapSourceFailure = null
64
+ await Promise.all(
65
+ sourceMap.sources.map(async (source, index) => {
66
+ if (index in sourcesContent) return
67
+ let sourceUrl = new URL(source, sourcemapUrl).href
68
+ sourceUrl = asServerUrl(sourceUrl)
69
+ try {
70
+ const sourceResponse = await window.fetch(sourceUrl)
71
+ const sourceContent = await sourceResponse.text()
72
+ sourcesContent[index] = sourceContent
73
+ } catch (e) {
74
+ firstSourceMapSourceFailure = e
75
+ }
76
+ }),
77
+ )
78
+ if (firstSourceMapSourceFailure) {
79
+ return null
80
+ }
81
+ return new SourceMapConsumer(sourceMap)
82
+ },
83
+ })
84
+ return original
85
+ }