@jsenv/core 27.5.1 → 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.
@@ -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)
@@ -0,0 +1,413 @@
1
+ export const formatError = (
2
+ error,
3
+ { rootDirectoryUrl, errorBaseUrl, openInEditor, url, line, column },
4
+ ) => {
5
+ let { message, stack } = normalizeErrorParts(error)
6
+ let errorDetailsPromiseReference = { current: null }
7
+ let tip = `Reported by the browser while executing <code>${window.location.pathname}${window.location.search}</code>.`
8
+ let errorUrlSite
9
+
10
+ const errorMeta = extractErrorMeta(error, { url, line, column })
11
+
12
+ const resolveUrlSite = ({ url, line, column }) => {
13
+ const inlineUrlMatch = url.match(
14
+ /@L([0-9]+)C([0-9]+)\-L([0-9]+)C([0-9]+)(\.[\w]+)$/,
15
+ )
16
+ if (inlineUrlMatch) {
17
+ const htmlUrl = url.slice(0, inlineUrlMatch.index)
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]
23
+ url = htmlUrl
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,
53
+ }
54
+ }
55
+
56
+ const resolveFileUrl = (url) => {
57
+ let urlObject = new URL(url)
58
+ if (urlObject.origin === window.origin) {
59
+ urlObject = new URL(
60
+ `${urlObject.pathname.slice(1)}${urlObject.search}`,
61
+ rootDirectoryUrl,
62
+ )
63
+ }
64
+ if (urlObject.href.startsWith("file:")) {
65
+ const atFsIndex = urlObject.pathname.indexOf("/@fs/")
66
+ if (atFsIndex > -1) {
67
+ const afterAtFs = urlObject.pathname.slice(atFsIndex + "/@fs/".length)
68
+ return new URL(afterAtFs, "file:///").href
69
+ }
70
+ }
71
+ return urlObject.href
72
+ }
73
+
74
+ const generateClickableText = (text) => {
75
+ const textWithHtmlLinks = makeLinksClickable(text, {
76
+ createLink: (url, { line, column }) => {
77
+ const urlSite = resolveUrlSite({ url, line, column })
78
+ if (!errorUrlSite && text === stack) {
79
+ onErrorLocated(urlSite, "error.stack")
80
+ }
81
+ if (errorBaseUrl) {
82
+ if (urlSite.url.startsWith(rootDirectoryUrl)) {
83
+ urlSite.url = `${errorBaseUrl}${urlSite.url.slice(
84
+ rootDirectoryUrl.length,
85
+ )}`
86
+ } else {
87
+ urlSite.url = "file:///mocked_for_snapshots"
88
+ }
89
+ }
90
+ const urlWithLineAndColumn = formatUrlWithLineAndColumn(urlSite)
91
+ return {
92
+ href:
93
+ url.startsWith("file:") && openInEditor
94
+ ? `javascript:window.fetch('/__open_in_editor__/${urlWithLineAndColumn}')`
95
+ : urlSite.url,
96
+ text: urlWithLineAndColumn,
97
+ }
98
+ },
99
+ })
100
+ return textWithHtmlLinks
101
+ }
102
+
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)
113
+ }
114
+ if (codeFrame) {
115
+ text += `\n\n${generateClickableText(codeFrame)}`
116
+ }
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
161
+ })()
162
+ }
163
+
164
+ // error.stack is more reliable than url/line/column reported on window error events
165
+ // so use it only when error.stack is not available
166
+ if (
167
+ url &&
168
+ !stack &&
169
+ // ignore window.reportError() it gives no valuable info
170
+ !url.endsWith("html_supervisor_installer.js")
171
+ ) {
172
+ onErrorLocated(resolveUrlSite({ url, line, column }))
173
+ } else if (errorMeta.url) {
174
+ onErrorLocated(resolveUrlSite(errorMeta))
175
+ }
176
+
177
+ return {
178
+ theme:
179
+ error && error.cause && error.cause.code === "PARSE_ERROR"
180
+ ? "light"
181
+ : "dark",
182
+ title: "An error occured",
183
+ text: formatErrorText({ message, stack }),
184
+ tip: `${tip}
185
+ <br />
186
+ Click outside to close.`,
187
+ errorDetailsPromise: errorDetailsPromiseReference.current,
188
+ }
189
+ }
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
+
258
+ const formatUrlWithLineAndColumn = ({ url, line, column }) => {
259
+ return line === undefined && column === undefined
260
+ ? url
261
+ : column === undefined
262
+ ? `${url}:${line}`
263
+ : `${url}:${line}:${column}`
264
+ }
265
+
266
+ const normalizeErrorParts = (error) => {
267
+ if (error === undefined) {
268
+ return {
269
+ message: "undefined",
270
+ }
271
+ }
272
+ if (error === null) {
273
+ return {
274
+ message: "null",
275
+ }
276
+ }
277
+ if (typeof error === "string") {
278
+ return {
279
+ message: error,
280
+ }
281
+ }
282
+ if (error instanceof Error) {
283
+ if (error.name === "SyntaxError") {
284
+ return {
285
+ message: error.message,
286
+ }
287
+ }
288
+ if (error.cause && error.cause.code === "PARSE_ERROR") {
289
+ if (error.messageHTML) {
290
+ return {
291
+ message: error.messageHTML,
292
+ }
293
+ }
294
+ return {
295
+ message: error.message,
296
+ }
297
+ }
298
+ // stackTrace formatted by V8
299
+ if (Error.captureStackTrace) {
300
+ return {
301
+ message: error.message,
302
+ stack: getErrorStackWithoutErrorMessage(error),
303
+ }
304
+ }
305
+ return {
306
+ message: error.message,
307
+ stack: error.stack ? ` ${error.stack}` : null,
308
+ }
309
+ }
310
+ if (typeof error === "object") {
311
+ return error
312
+ }
313
+ return {
314
+ message: JSON.stringify(error),
315
+ }
316
+ }
317
+
318
+ const getErrorStackWithoutErrorMessage = (error) => {
319
+ let stack = error.stack
320
+ const messageInStack = `${error.name}: ${error.message}`
321
+ if (stack.startsWith(messageInStack)) {
322
+ stack = stack.slice(messageInStack.length)
323
+ }
324
+ const nextLineIndex = stack.indexOf("\n")
325
+ if (nextLineIndex > -1) {
326
+ stack = stack.slice(nextLineIndex + 1)
327
+ }
328
+ return stack
329
+ }
330
+
331
+ const makeLinksClickable = (string, { createLink = (url) => url }) => {
332
+ // normalize line breaks
333
+ string = string.replace(/\n/g, "\n")
334
+ string = escapeHtml(string)
335
+ // render links
336
+ string = stringToStringWithLink(string, {
337
+ transform: (url, { line, column }) => {
338
+ const { href, text } = createLink(url, { line, column })
339
+ return link({ href, text })
340
+ },
341
+ })
342
+ return string
343
+ }
344
+
345
+ const escapeHtml = (string) => {
346
+ return string
347
+ .replace(/&/g, "&amp;")
348
+ .replace(/</g, "&lt;")
349
+ .replace(/>/g, "&gt;")
350
+ .replace(/"/g, "&quot;")
351
+ .replace(/'/g, "&#039;")
352
+ }
353
+
354
+ // `Error: yo
355
+ // at Object.execute (http://127.0.0.1:57300/build/src/__test__/file-throw.js:9:13)
356
+ // at doExec (http://127.0.0.1:3000/src/__test__/file-throw.js:452:38)
357
+ // at postOrderExec (http://127.0.0.1:3000/src/__test__/file-throw.js:448:16)
358
+ // at http://127.0.0.1:3000/src/__test__/file-throw.js:399:18`.replace(/(?:https?|ftp|file):\/\/(.*+)$/gm, (...args) => {
359
+ // debugger
360
+ // })
361
+ const stringToStringWithLink = (
362
+ source,
363
+ {
364
+ transform = (url) => {
365
+ return {
366
+ href: url,
367
+ text: url,
368
+ }
369
+ },
370
+ } = {},
371
+ ) => {
372
+ return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
373
+ let linkHTML = ""
374
+
375
+ const lastChar = match[match.length - 1]
376
+
377
+ // hotfix because our url regex sucks a bit
378
+ const endsWithSeparationChar = lastChar === ")" || lastChar === ":"
379
+ if (endsWithSeparationChar) {
380
+ match = match.slice(0, -1)
381
+ }
382
+
383
+ const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/
384
+ const lineAndColumMatch = match.match(lineAndColumnPattern)
385
+ if (lineAndColumMatch) {
386
+ const lineAndColumnString = lineAndColumMatch[0]
387
+ const lineNumber = lineAndColumMatch[1]
388
+ const columnNumber = lineAndColumMatch[2]
389
+ linkHTML = transform(match.slice(0, -lineAndColumnString.length), {
390
+ line: lineNumber,
391
+ column: columnNumber,
392
+ })
393
+ } else {
394
+ const linePattern = /:([0-9]+)$/
395
+ const lineMatch = match.match(linePattern)
396
+ if (lineMatch) {
397
+ const lineString = lineMatch[0]
398
+ const lineNumber = lineMatch[1]
399
+ linkHTML = transform(match.slice(0, -lineString.length), {
400
+ line: lineNumber,
401
+ })
402
+ } else {
403
+ linkHTML = transform(match, {})
404
+ }
405
+ }
406
+ if (endsWithSeparationChar) {
407
+ return `${linkHTML}${lastChar}`
408
+ }
409
+ return linkHTML
410
+ })
411
+ }
412
+
413
+ const link = ({ href, text = href }) => `<a href="${href}">${text}</a>`