@jsenv/core 27.3.4 → 27.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "27.3.4",
3
+ "version": "27.4.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -43,7 +43,6 @@
43
43
  "eslint": "npx eslint . --ext=.js,.mjs,.cjs,.html",
44
44
  "dev": "node --conditions=development ./scripts/dev/dev.mjs",
45
45
  "test": "node --conditions=development ./scripts/test/test.mjs",
46
- "test:coverage": "cross-env NODE_V8_COVERAGE=.coverage/node npm run test -- --coverage",
47
46
  "test:workspace": "npm run test --workspaces --if-present -- --workspace",
48
47
  "build": "node --conditions=development ./scripts/build/build.mjs",
49
48
  "workspace:versions": "node ./scripts/publish/workspace_versions.mjs",
@@ -68,16 +67,16 @@
68
67
  "@jsenv/abort": "4.2.3",
69
68
  "@jsenv/ast": "1.1.2",
70
69
  "@jsenv/babel-plugins": "1.0.6",
71
- "@jsenv/filesystem": "4.1.0",
70
+ "@jsenv/filesystem": "4.1.1",
72
71
  "@jsenv/importmap": "1.2.1",
73
72
  "@jsenv/integrity": "0.0.1",
74
73
  "@jsenv/log": "3.1.0",
75
74
  "@jsenv/node-esm-resolution": "0.1.0",
76
75
  "@jsenv/server": "12.8.0",
77
- "@jsenv/sourcemap": "1.0.1",
76
+ "@jsenv/sourcemap": "1.0.2",
78
77
  "@jsenv/uneval": "1.6.0",
79
78
  "@jsenv/url-meta": "7.0.0",
80
- "@jsenv/urls": "1.2.6",
79
+ "@jsenv/urls": "1.2.7",
81
80
  "@jsenv/utils": "2.0.1",
82
81
  "acorn-import-assertions": "1.8.0",
83
82
  "cuid": "2.1.8",
@@ -86,6 +85,7 @@
86
85
  "istanbul-lib-instrument": "5.2.0",
87
86
  "istanbul-lib-report": "3.0.0",
88
87
  "istanbul-reports": "3.1.4",
88
+ "launch-editor": "2.4.0",
89
89
  "pidtree": "0.6.0",
90
90
  "rollup": "2.76.0",
91
91
  "string-width": "5.1.2",
@@ -103,7 +103,6 @@
103
103
  "@jsenv/https-local": "2.1.0",
104
104
  "@jsenv/package-workspace": "0.4.1",
105
105
  "@jsenv/performance-impact": "3.0.1",
106
- "cross-env": "7.0.3",
107
106
  "eslint": "8.19.0",
108
107
  "eslint-plugin-html": "6.2.0",
109
108
  "eslint-plugin-import": "2.26.0",
@@ -228,7 +228,9 @@ build ${entryPointKeys.length} entry points`)
228
228
  startLoading: (cookEntryFile) => {
229
229
  Object.keys(entryPoints).forEach((key) => {
230
230
  const [, entryUrlInfo] = cookEntryFile({
231
- trace: `"${key}" in entryPoints parameter`,
231
+ trace: {
232
+ message: `"${key}" in entryPoints parameter`,
233
+ },
232
234
  type: "entry_point",
233
235
  specifier: key,
234
236
  })
@@ -722,7 +724,7 @@ build ${entryPointKeys.length} entry points`)
722
724
  startLoading: (cookEntryFile) => {
723
725
  entryUrls.forEach((entryUrl) => {
724
726
  const [, postBuildEntryUrlInfo] = cookEntryFile({
725
- trace: `entryPoint`,
727
+ trace: { message: `entryPoint` },
726
728
  type: "entry_point",
727
729
  specifier: entryUrl,
728
730
  })
@@ -1,8 +1,13 @@
1
1
  /* eslint-env browser */
2
2
 
3
+ const STATUSES = {
4
+ CONNECTING: "connecting",
5
+ CONNECTED: "connected",
6
+ DISCONNECTED: "disconnected",
7
+ }
8
+
3
9
  export const createEventSourceConnection = (
4
10
  eventSourceUrl,
5
- events = {},
6
11
  { retryMaxAttempt = Infinity, retryAllocatedMs = Infinity, lastEventId } = {},
7
12
  ) => {
8
13
  const { EventSource } = window
@@ -10,18 +15,26 @@ export const createEventSourceConnection = (
10
15
  return () => {}
11
16
  }
12
17
 
18
+ let eventSource
19
+ const events = {}
13
20
  const eventSourceOrigin = new URL(eventSourceUrl).origin
14
- Object.keys(events).forEach((eventName) => {
15
- const eventCallback = events[eventName]
16
- events[eventName] = (e) => {
17
- if (e.origin === eventSourceOrigin) {
18
- if (e.lastEventId) {
19
- lastEventId = e.lastEventId
21
+ const addEventCallbacks = (eventCallbacks) => {
22
+ Object.keys(eventCallbacks).forEach((eventName) => {
23
+ const eventCallback = eventCallbacks[eventName]
24
+ events[eventName] = (e) => {
25
+ if (e.origin === eventSourceOrigin) {
26
+ if (e.lastEventId) {
27
+ lastEventId = e.lastEventId
28
+ }
29
+ eventCallback(e)
20
30
  }
21
- eventCallback(e)
22
31
  }
23
- }
24
- })
32
+ if (eventSource) {
33
+ eventSource.addEventListener(eventName, events[eventName])
34
+ }
35
+ })
36
+ }
37
+ addEventCallbacks(events)
25
38
 
26
39
  const status = {
27
40
  value: "default",
@@ -37,11 +50,14 @@ export const createEventSourceConnection = (
37
50
  let _disconnect = () => {}
38
51
 
39
52
  const attemptConnection = (url) => {
40
- const eventSource = new EventSource(url, {
53
+ eventSource = new EventSource(url, {
41
54
  withCredentials: true,
42
55
  })
43
56
  _disconnect = () => {
44
- if (status.value !== "connecting" && status.value !== "connected") {
57
+ if (
58
+ status.value !== STATUSES.CONNECTING &&
59
+ status.value !== STATUSES.CONNECTED
60
+ ) {
45
61
  console.warn(
46
62
  `disconnect() ignored because connection is ${status.value}`,
47
63
  )
@@ -52,7 +68,8 @@ export const createEventSourceConnection = (
52
68
  Object.keys(events).forEach((eventName) => {
53
69
  eventSource.removeEventListener(eventName, events[eventName])
54
70
  })
55
- status.goTo("disconnected")
71
+ eventSource = null
72
+ status.goTo(STATUSES.DISCONNECTED)
56
73
  }
57
74
  let retryCount = 0
58
75
  let firstRetryMs = Date.now()
@@ -78,7 +95,7 @@ export const createEventSourceConnection = (
78
95
  }
79
96
 
80
97
  retryCount++
81
- status.goTo("connecting")
98
+ status.goTo(STATUSES.CONNECTING)
82
99
  return
83
100
  }
84
101
 
@@ -88,7 +105,7 @@ export const createEventSourceConnection = (
88
105
  }
89
106
  }
90
107
  eventSource.onopen = () => {
91
- status.goTo("connected")
108
+ status.goTo(STATUSES.CONNECTED)
92
109
  }
93
110
  Object.keys(events).forEach((eventName) => {
94
111
  eventSource.addEventListener(eventName, events[eventName])
@@ -100,7 +117,7 @@ export const createEventSourceConnection = (
100
117
  }
101
118
  })
102
119
  }
103
- status.goTo("connecting")
120
+ status.goTo(STATUSES.CONNECTING)
104
121
  }
105
122
 
106
123
  let connect = () => {
@@ -115,7 +132,10 @@ export const createEventSourceConnection = (
115
132
  }
116
133
 
117
134
  const removePageUnloadListener = listenPageUnload(() => {
118
- if (status.value === "connecting" || status.value === "connected") {
135
+ if (
136
+ status.value === STATUSES.CONNECTING ||
137
+ status.value === STATUSES.CONNECTED
138
+ ) {
119
139
  _disconnect()
120
140
  }
121
141
  })
@@ -128,6 +148,7 @@ export const createEventSourceConnection = (
128
148
  return {
129
149
  status,
130
150
  connect,
151
+ addEventCallbacks,
131
152
  disconnect: () => _disconnect(),
132
153
  destroy,
133
154
  }
@@ -1,4 +1,5 @@
1
1
  import { createDetailedMessage } from "@jsenv/log"
2
+ import { stringifyUrlSite } from "@jsenv/urls"
2
3
 
3
4
  export const createResolveUrlError = ({
4
5
  pluginController,
@@ -15,7 +16,7 @@ export const createResolveUrlError = ({
15
16
  reason,
16
17
  ...details,
17
18
  "specifier": `"${reference.specifier}"`,
18
- "specifier trace": reference.trace,
19
+ "specifier trace": reference.trace.message,
19
20
  ...detailsFromPluginController(pluginController),
20
21
  }),
21
22
  )
@@ -46,19 +47,23 @@ export const createFetchUrlContentError = ({
46
47
  reason,
47
48
  ...details
48
49
  }) => {
49
- const fetchContentError = new Error(
50
+ const fetchError = new Error(
50
51
  createDetailedMessage(`Failed to fetch url content`, {
51
52
  reason,
52
53
  ...details,
53
54
  "url": urlInfo.url,
54
- "url reference trace": reference.trace,
55
+ "url reference trace": reference.trace.message,
55
56
  ...detailsFromPluginController(pluginController),
56
57
  }),
57
58
  )
58
- fetchContentError.name = "FETCH_URL_CONTENT_ERROR"
59
- fetchContentError.code = code
60
- fetchContentError.reason = reason
61
- return fetchContentError
59
+ fetchError.name = "FETCH_URL_CONTENT_ERROR"
60
+ fetchError.code = code
61
+ fetchError.reason = reason
62
+ fetchError.url = reference.trace.url
63
+ fetchError.line = reference.trace.line
64
+ fetchError.column = reference.trace.column
65
+ fetchError.contentFrame = reference.trace.message
66
+ return fetchError
62
67
  }
63
68
 
64
69
  if (error.code === "EPERM") {
@@ -103,7 +108,7 @@ export const createTransformUrlContentError = ({
103
108
  reason,
104
109
  ...details,
105
110
  "url": urlInfo.url,
106
- "url reference trace": reference.trace,
111
+ "url reference trace": reference.trace.message,
107
112
  ...detailsFromPluginController(pluginController),
108
113
  },
109
114
  ),
@@ -111,6 +116,33 @@ export const createTransformUrlContentError = ({
111
116
  transformError.name = "TRANSFORM_URL_CONTENT_ERROR"
112
117
  transformError.code = code
113
118
  transformError.reason = reason
119
+ transformError.url = reference.trace.url
120
+ transformError.line = reference.trace.line
121
+ transformError.column = reference.trace.column
122
+ transformError.stack = error.stack
123
+ transformError.contentFrame = reference.trace.message
124
+ if (code === "PARSE_ERROR") {
125
+ transformError.reason = error.message
126
+ if (urlInfo.isInline) {
127
+ transformError.line = reference.trace.line + error.line - 1
128
+ transformError.column = reference.trace.column + error.column
129
+ transformError.contentFrame = stringifyUrlSite({
130
+ url: urlInfo.inlineUrlSite.url,
131
+ line: transformError.line,
132
+ column: transformError.column,
133
+ content: urlInfo.inlineUrlSite.content,
134
+ })
135
+ } else {
136
+ transformError.line = error.line
137
+ transformError.column = error.column
138
+ transformError.contentFrame = stringifyUrlSite({
139
+ url: urlInfo.url,
140
+ line: transformError.line,
141
+ column: transformError.column,
142
+ content: urlInfo.content,
143
+ })
144
+ }
145
+ }
114
146
  return transformError
115
147
  }
116
148
  return createFailedToTransformError({
@@ -130,7 +162,7 @@ export const createFinalizeUrlContentError = ({
130
162
  "reason": `An error occured during "finalizeUrlContent"`,
131
163
  ...detailsFromValueThrown(error),
132
164
  "url": urlInfo.url,
133
- "url reference trace": reference.trace,
165
+ "url reference trace": reference.trace.message,
134
166
  ...detailsFromPluginController(pluginController),
135
167
  }),
136
168
  )
@@ -221,7 +221,10 @@ export const createKitchen = ({
221
221
  sourcemapsRelativeSources,
222
222
  injectSourcemapPlaceholder: ({ urlInfo, specifier }) => {
223
223
  const sourcemapReference = createReference({
224
- trace: `sourcemap comment placeholder for ${urlInfo.url}`,
224
+ trace: {
225
+ message: `sourcemap comment placeholder`,
226
+ url: urlInfo.url,
227
+ },
225
228
  type: "sourcemap_comment",
226
229
  subtype: urlInfo.contentType === "text/javascript" ? "js" : "css",
227
230
  parentUrl: urlInfo.url,
@@ -238,15 +241,14 @@ export const createKitchen = ({
238
241
  specifierLine,
239
242
  specifierColumn,
240
243
  }) => {
244
+ const sourcemapUrlSite = adjustUrlSite(urlInfo, {
245
+ urlGraph,
246
+ url: urlInfo.url,
247
+ line: specifierLine,
248
+ column: specifierColumn,
249
+ })
241
250
  const sourcemapReference = createReference({
242
- trace: stringifyUrlSite(
243
- adjustUrlSite(urlInfo, {
244
- urlGraph,
245
- url: urlInfo.url,
246
- line: specifierLine,
247
- column: specifierColumn,
248
- }),
249
- ),
251
+ trace: traceFromUrlSite(sourcemapUrlSite),
250
252
  type,
251
253
  parentUrl: urlInfo.url,
252
254
  specifier,
@@ -273,7 +275,7 @@ export const createKitchen = ({
273
275
  `no plugin has handled url during "fetchUrlContent" hook -> url will be ignored`,
274
276
  {
275
277
  "url": urlInfo.url,
276
- "url reference trace": reference.trace,
278
+ "url reference trace": reference.trace.message,
277
279
  },
278
280
  ),
279
281
  )
@@ -408,7 +410,7 @@ export const createKitchen = ({
408
410
  },
409
411
  found: ({ trace, ...rest }) => {
410
412
  if (trace === undefined) {
411
- trace = stringifyUrlSite(
413
+ trace = traceFromUrlSite(
412
414
  adjustUrlSite(urlInfo, {
413
415
  urlGraph,
414
416
  url: urlInfo.url,
@@ -423,7 +425,12 @@ export const createKitchen = ({
423
425
  ...rest,
424
426
  })
425
427
  },
426
- foundInline: ({ isOriginalPosition, line, column, ...rest }) => {
428
+ foundInline: ({
429
+ isOriginalPosition,
430
+ specifierLine,
431
+ specifierColumn,
432
+ ...rest
433
+ }) => {
427
434
  const parentUrl = isOriginalPosition
428
435
  ? urlInfo.url
429
436
  : urlInfo.generatedUrl
@@ -431,15 +438,15 @@ export const createKitchen = ({
431
438
  ? urlInfo.originalContent
432
439
  : urlInfo.content
433
440
  return addReference({
434
- trace: stringifyUrlSite({
441
+ trace: traceFromUrlSite({
435
442
  url: parentUrl,
436
443
  content: parentContent,
437
- line,
438
- column,
444
+ line: specifierLine,
445
+ column: specifierColumn,
439
446
  }),
440
447
  isOriginalPosition,
441
- line,
442
- column,
448
+ specifierLine,
449
+ specifierColumn,
443
450
  isInline: true,
444
451
  ...rest,
445
452
  })
@@ -487,7 +494,7 @@ export const createKitchen = ({
487
494
  ? urlInfo.originalContent
488
495
  : urlInfo.content
489
496
  return referenceUtils.update(reference, {
490
- trace: stringifyUrlSite({
497
+ trace: traceFromUrlSite({
491
498
  url: parentUrl,
492
499
  content: parentContent,
493
500
  line: specifierLine,
@@ -505,7 +512,7 @@ export const createKitchen = ({
505
512
  inject: ({ trace, ...rest }) => {
506
513
  if (trace === undefined) {
507
514
  const { url, line, column } = getCallerPosition()
508
- trace = stringifyUrlSite({
515
+ trace = traceFromUrlSite({
509
516
  url,
510
517
  line,
511
518
  column,
@@ -722,6 +729,15 @@ const memoizeCook = (cook) => {
722
729
  }
723
730
  }
724
731
 
732
+ const traceFromUrlSite = (urlSite) => {
733
+ return {
734
+ message: stringifyUrlSite(urlSite),
735
+ url: urlSite.url,
736
+ line: urlSite.line,
737
+ column: urlSite.column,
738
+ }
739
+ }
740
+
725
741
  const applyReferenceEffectsOnUrlInfo = (reference, urlInfo, context) => {
726
742
  if (reference.shouldHandle) {
727
743
  urlInfo.shouldHandle = true
@@ -7,8 +7,12 @@ import {
7
7
  pluginCORS,
8
8
  } from "@jsenv/server"
9
9
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js"
10
- import { createCallbackListNotifiedOnce } from "@jsenv/abort"
10
+ import {
11
+ createCallbackListNotifiedOnce,
12
+ createCallbackList,
13
+ } from "@jsenv/abort"
11
14
 
15
+ import { createSSEService } from "@jsenv/core/src/helpers/event_source/sse_service.js"
12
16
  import { createFileService } from "./server/file_service.js"
13
17
 
14
18
  export const startOmegaServer = async ({
@@ -33,12 +37,60 @@ export const startOmegaServer = async ({
33
37
  kitchen,
34
38
  }) => {
35
39
  const serverStopCallbackList = createCallbackListNotifiedOnce()
40
+
41
+ const serverEventCallbackList = createCallbackList()
42
+ const sseService = createSSEService({ serverEventCallbackList })
43
+ const sendServerEvent = ({ type, data }) => {
44
+ serverEventCallbackList.notify({
45
+ type,
46
+ data: JSON.stringify(data),
47
+ })
48
+ }
49
+
50
+ kitchen.pluginController.addHook("registerServerEvents")
51
+ kitchen.pluginController.callHooks(
52
+ "registerServerEvents",
53
+ { sendServerEvent },
54
+ {
55
+ rootDirectoryUrl,
56
+ urlGraph,
57
+ scenario,
58
+ },
59
+ () => {},
60
+ )
61
+
62
+ const sendServerErrorEvent = (event) => {
63
+ // setTimeout display first the error
64
+ // dispatched on window by browser
65
+ // then display the jsenv error
66
+ setTimeout(() => {
67
+ sendServerEvent(event)
68
+ }, 10)
69
+ }
70
+
36
71
  const coreServices = {
72
+ "service:server_events": (request) => {
73
+ const { accept } = request.headers
74
+ if (accept && accept.includes("text/event-stream")) {
75
+ const room = sseService.getOrCreateSSERoom(request)
76
+ return room.join(request)
77
+ }
78
+ return null
79
+ },
37
80
  "service:file": createFileService({
38
81
  rootDirectoryUrl,
39
82
  urlGraph,
40
83
  kitchen,
41
84
  scenario,
85
+ onFileNotFound: (data) => {
86
+ sendServerErrorEvent({ type: "file_not_found", data })
87
+ },
88
+ onParseError: (data) => {
89
+ sendServerErrorEvent({ type: "parse_error", data })
90
+ },
91
+ onUnexpectedError: (data) => {
92
+ sendServerErrorEvent({ type: "unexpected_error", data })
93
+ },
42
94
  }),
43
95
  }
44
96
  const server = await startServer({
@@ -119,6 +171,7 @@ export const startOmegaServer = async ({
119
171
  }),
120
172
  onStop: (reason) => {
121
173
  onStop()
174
+ sseService.destroy()
122
175
  serverStopCallbackList.notify(reason)
123
176
  },
124
177
  })
@@ -12,6 +12,9 @@ export const createFileService = ({
12
12
  urlGraph,
13
13
  kitchen,
14
14
  scenario,
15
+ onParseError,
16
+ onFileNotFound,
17
+ onUnexpectedError,
15
18
  }) => {
16
19
  kitchen.pluginController.addHook("serve")
17
20
  kitchen.pluginController.addHook("augmentResponse")
@@ -51,7 +54,7 @@ export const createFileService = ({
51
54
  }
52
55
  if (!reference) {
53
56
  const entryPoint = kitchen.injectReference({
54
- trace: parentUrl || rootDirectoryUrl,
57
+ trace: { message: parentUrl || rootDirectoryUrl },
55
58
  parentUrl: parentUrl || rootDirectoryUrl,
56
59
  type: "http_request",
57
60
  specifier: request.ressource,
@@ -135,10 +138,17 @@ export const createFileService = ({
135
138
  } catch (e) {
136
139
  const code = e.code
137
140
  if (code === "PARSE_ERROR") {
138
- // let the browser re-throw the syntax error
141
+ onParseError({
142
+ reason: e.reason,
143
+ message: e.message,
144
+ url: e.url,
145
+ line: e.line,
146
+ column: e.column,
147
+ contentFrame: e.contentFrame,
148
+ })
139
149
  return {
140
150
  url: reference.url,
141
- status: 200,
151
+ status: 200, // let the browser re-throw the syntax error
142
152
  statusText: e.reason,
143
153
  statusMessage: e.message,
144
154
  headers: {
@@ -166,6 +176,14 @@ export const createFileService = ({
166
176
  }
167
177
  }
168
178
  if (code === "NOT_FOUND") {
179
+ onFileNotFound({
180
+ reason: e.reason,
181
+ message: e.message,
182
+ url: e.url,
183
+ line: e.line,
184
+ column: e.column,
185
+ contentFrame: e.contentFrame,
186
+ })
169
187
  return {
170
188
  url: reference.url,
171
189
  status: 404,
@@ -173,6 +191,15 @@ export const createFileService = ({
173
191
  statusMessage: e.message,
174
192
  }
175
193
  }
194
+ onUnexpectedError({
195
+ reason: e.reason,
196
+ message: e.message,
197
+ stack: e.stack,
198
+ url: e.url,
199
+ line: e.line,
200
+ column: e.column,
201
+ contentFrame: e.contentFrame,
202
+ })
176
203
  return {
177
204
  url: reference.url,
178
205
  status: 500,
@@ -157,21 +157,21 @@ const addReloadMessage = (reloadMessage) => {
157
157
 
158
158
  const eventsourceConnection = createEventSourceConnection(
159
159
  document.location.href,
160
- {
161
- reload: ({ data }) => {
162
- const reloadMessage = JSON.parse(data)
163
- addReloadMessage(reloadMessage)
164
- },
165
- },
166
160
  {
167
161
  retryMaxAttempt: Infinity,
168
162
  retryAllocatedMs: 20 * 1000,
169
163
  },
170
164
  )
171
-
172
- const { status, connect, disconnect } = eventsourceConnection
165
+ const { status, connect, addEventCallbacks, disconnect } = eventsourceConnection
166
+ eventsourceConnection.addEventCallbacks({
167
+ reload: ({ data }) => {
168
+ const reloadMessage = JSON.parse(data)
169
+ addReloadMessage(reloadMessage)
170
+ },
171
+ })
173
172
  connect()
174
173
  window.__jsenv_event_source_client__ = {
174
+ addEventCallbacks,
175
175
  status,
176
176
  connect,
177
177
  disconnect,