@jsenv/core 27.3.3 → 27.5.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.
Files changed (89) hide show
  1. package/dist/js/autoreload.js +359 -0
  2. package/dist/js/execute_using_dynamic_import.js +1 -1
  3. package/dist/js/html_supervisor_installer.js +524 -147
  4. package/dist/js/html_supervisor_setup.js +3 -4
  5. package/dist/js/new_stylesheet.js +26 -58
  6. package/dist/js/server_events_client.js +307 -0
  7. package/dist/main.js +7699 -7307
  8. package/package.json +15 -15
  9. package/{README.md → readme.md} +18 -7
  10. package/src/build/build.js +16 -18
  11. package/src/build/start_build_server.js +24 -28
  12. package/src/dev/start_dev_server.js +30 -94
  13. package/src/execute/execute.js +17 -35
  14. package/src/execute/run.js +2 -0
  15. package/src/omega/errors.js +43 -9
  16. package/src/omega/kitchen.js +42 -25
  17. package/src/omega/omega_server.js +96 -74
  18. package/src/omega/server/file_service.js +256 -28
  19. package/src/omega/url_graph.js +39 -20
  20. package/src/plugins/autoreload/client/autoreload.js +201 -0
  21. package/src/plugins/autoreload/{dev_sse/client → client}/autoreload_preference.js +0 -0
  22. package/src/plugins/autoreload/{dev_sse/client → client}/reload.js +29 -10
  23. package/src/plugins/autoreload/{dev_sse/client → client}/url_helpers.js +0 -0
  24. package/src/plugins/autoreload/jsenv_plugin_autoreload.js +4 -8
  25. package/src/plugins/autoreload/{dev_sse/jsenv_plugin_dev_sse_client.js → jsenv_plugin_autoreload_client.js} +8 -8
  26. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +196 -0
  27. package/src/{dev/plugins → plugins}/explorer/client/explorer.html +0 -0
  28. package/src/{dev/plugins → plugins}/explorer/client/jsenv.png +0 -0
  29. package/src/{dev/plugins → plugins}/explorer/jsenv_plugin_explorer.js +1 -3
  30. package/src/plugins/html_supervisor/client/error_overlay.js +401 -0
  31. package/src/plugins/html_supervisor/client/html_supervisor_installer.js +138 -23
  32. package/src/plugins/html_supervisor/client/html_supervisor_setup.js +3 -4
  33. package/src/plugins/html_supervisor/jsenv_plugin_html_supervisor.js +55 -23
  34. package/src/plugins/inline/jsenv_plugin_html_inline_content.js +97 -117
  35. package/src/plugins/node_esm_resolution/jsenv_plugin_node_esm_resolution.js +66 -58
  36. package/src/plugins/plugin_controller.js +102 -67
  37. package/src/plugins/plugins.js +10 -10
  38. package/src/{helpers/event_source/event_source.js → plugins/server_events/client/event_source_connection.js} +125 -33
  39. package/src/plugins/server_events/client/server_events_client.js +17 -0
  40. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +48 -0
  41. package/src/plugins/server_events/server_events_dispatcher.js +69 -0
  42. package/src/{dev/plugins → plugins}/toolbar/client/animation/toolbar_animation.js +0 -0
  43. package/src/{dev/plugins → plugins}/toolbar/client/eventsource/eventsource.css +0 -0
  44. package/src/{dev/plugins → plugins}/toolbar/client/eventsource/toolbar_eventsource.js +0 -0
  45. package/src/{dev/plugins → plugins}/toolbar/client/execution/execution.css +0 -0
  46. package/src/{dev/plugins → plugins}/toolbar/client/execution/toolbar_execution.js +0 -0
  47. package/src/{dev/plugins → plugins}/toolbar/client/focus/focus.css +0 -0
  48. package/src/{dev/plugins → plugins}/toolbar/client/focus/toolbar_focus.js +0 -0
  49. package/src/{dev/plugins → plugins}/toolbar/client/jsenv_logo.svg +0 -0
  50. package/src/{dev/plugins → plugins}/toolbar/client/notification/toolbar_notification.js +0 -0
  51. package/src/{dev/plugins → plugins}/toolbar/client/responsive/overflow_menu.css +0 -0
  52. package/src/{dev/plugins → plugins}/toolbar/client/responsive/toolbar_responsive.js +0 -0
  53. package/src/{dev/plugins → plugins}/toolbar/client/settings/settings.css +0 -0
  54. package/src/{dev/plugins → plugins}/toolbar/client/settings/toolbar_settings.js +0 -0
  55. package/src/{dev/plugins → plugins}/toolbar/client/theme/jsenv_theme.css +0 -0
  56. package/src/{dev/plugins → plugins}/toolbar/client/theme/light_theme.css +0 -0
  57. package/src/{dev/plugins → plugins}/toolbar/client/theme/toolbar_theme.js +0 -0
  58. package/src/{dev/plugins → plugins}/toolbar/client/toolbar.html +0 -0
  59. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_injector.js +0 -0
  60. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_main.css +0 -0
  61. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_main.js +0 -0
  62. package/src/{dev/plugins → plugins}/toolbar/client/tooltip/tooltip.css +0 -0
  63. package/src/{dev/plugins → plugins}/toolbar/client/tooltip/tooltip.js +0 -0
  64. package/src/{dev/plugins → plugins}/toolbar/client/util/animation.js +0 -0
  65. package/src/{dev/plugins → plugins}/toolbar/client/util/dom.js +0 -0
  66. package/src/{dev/plugins → plugins}/toolbar/client/util/fetch_using_xhr.js +0 -0
  67. package/src/{dev/plugins → plugins}/toolbar/client/util/fetching.js +0 -0
  68. package/src/{dev/plugins → plugins}/toolbar/client/util/iframe_to_parent_href.js +0 -0
  69. package/src/{dev/plugins → plugins}/toolbar/client/util/jsenv_logger.js +0 -0
  70. package/src/{dev/plugins → plugins}/toolbar/client/util/preferences.js +0 -0
  71. package/src/{dev/plugins → plugins}/toolbar/client/util/responsive.js +0 -0
  72. package/src/{dev/plugins → plugins}/toolbar/client/util/util.js +0 -0
  73. package/src/{dev/plugins → plugins}/toolbar/client/variant/variant.js +0 -0
  74. package/src/{dev/plugins → plugins}/toolbar/jsenv_plugin_toolbar.js +0 -0
  75. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +4 -3
  76. package/src/plugins/transpilation/babel/new_stylesheet/client/new_stylesheet.js +25 -55
  77. package/src/plugins/transpilation/import_assertions/jsenv_plugin_import_assertions.js +44 -24
  78. package/src/plugins/transpilation/jsenv_plugin_transpilation.js +6 -1
  79. package/src/plugins/url_analysis/html/html_urls.js +8 -8
  80. package/src/plugins/url_analysis/jsenv_plugin_url_analysis.js +3 -1
  81. package/src/test/execute_plan.js +36 -54
  82. package/src/test/execute_test_plan.js +2 -2
  83. package/src/test/logs_file_execution.js +60 -27
  84. package/src/test/logs_file_execution.test.mjs +41 -0
  85. package/dist/js/event_source_client.js +0 -528
  86. package/src/helpers/event_source/sse_service.js +0 -53
  87. package/src/plugins/autoreload/dev_sse/client/event_source_client.js +0 -193
  88. package/src/plugins/autoreload/dev_sse/jsenv_plugin_dev_sse_server.js +0 -203
  89. package/src/plugins/html_supervisor/client/error_in_document.js +0 -198
@@ -3,43 +3,242 @@ import {
3
3
  serveDirectory,
4
4
  composeTwoResponses,
5
5
  } from "@jsenv/server"
6
+ import { registerDirectoryLifecycle } from "@jsenv/filesystem"
6
7
  import { urlIsInsideOf, moveUrl } from "@jsenv/urls"
8
+ import { URL_META } from "@jsenv/url-meta"
7
9
 
10
+ import { getCorePlugins } from "@jsenv/core/src/plugins/plugins.js"
11
+ import { createUrlGraph } from "@jsenv/core/src/omega/url_graph.js"
12
+ import { createKitchen } from "@jsenv/core/src/omega/kitchen.js"
13
+ import { createServerEventsDispatcher } from "@jsenv/core/src/plugins/server_events/server_events_dispatcher.js"
14
+ import { jsenvPluginServerEventsClientInjection } from "@jsenv/core/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js"
8
15
  import { parseUserAgentHeader } from "./user_agent.js"
9
16
 
10
17
  export const createFileService = ({
18
+ signal,
19
+ logLevel,
20
+ serverStopCallbacks,
21
+
11
22
  rootDirectoryUrl,
12
- urlGraph,
13
- kitchen,
14
23
  scenario,
24
+ runtimeCompat,
25
+
26
+ plugins,
27
+ urlAnalysis,
28
+ htmlSupervisor,
29
+ nodeEsmResolution,
30
+ fileSystemMagicResolution,
31
+ transpilation,
32
+ clientAutoreload,
33
+ clientFiles,
34
+ cooldownBetweenFileEvents,
35
+ explorer,
36
+ sourcemaps,
37
+ writeGeneratedFiles,
15
38
  }) => {
16
- kitchen.pluginController.addHook("serve")
17
- kitchen.pluginController.addHook("augmentResponse")
18
- const serveContext = {
19
- rootDirectoryUrl,
20
- urlGraph,
21
- scenario,
39
+ 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
+
49
+ const clientFileChangeCallbackList = []
50
+ const clientFilesPruneCallbackList = []
51
+ const clientFileChangeCallback = ({ relativeUrl, event }) => {
52
+ const url = new URL(relativeUrl, rootDirectoryUrl).href
53
+ clientFileChangeCallbackList.forEach((callback) => {
54
+ callback({ url, event })
55
+ })
56
+ }
57
+ const clientFilePatterns = {
58
+ ...clientFiles,
59
+ ".jsenv/": false,
22
60
  }
23
- const augmentResponseContext = {
24
- rootDirectoryUrl,
25
- urlGraph,
26
- scenario,
61
+
62
+ if (scenario === "dev") {
63
+ const stopWatchingClientFiles = registerDirectoryLifecycle(
64
+ rootDirectoryUrl,
65
+ {
66
+ watchPatterns: clientFilePatterns,
67
+ cooldownBetweenFileEvents,
68
+ keepProcessAlive: false,
69
+ recursive: true,
70
+ added: ({ relativeUrl }) => {
71
+ clientFileChangeCallback({ event: "added", relativeUrl })
72
+ },
73
+ updated: ({ relativeUrl }) => {
74
+ clientFileChangeCallback({ event: "modified", relativeUrl })
75
+ },
76
+ removed: ({ relativeUrl }) => {
77
+ clientFileChangeCallback({ event: "removed", relativeUrl })
78
+ },
79
+ },
80
+ )
81
+ serverStopCallbacks.push(stopWatchingClientFiles)
82
+ }
83
+
84
+ const contextCache = new Map()
85
+ const getOrCreateContext = (request) => {
86
+ const { runtimeName, runtimeVersion } = parseUserAgentHeader(
87
+ request.headers["user-agent"],
88
+ )
89
+ const runtimeId = `${runtimeName}@${runtimeVersion}`
90
+ const existingContext = contextCache.get(runtimeId)
91
+ if (existingContext) {
92
+ return existingContext
93
+ }
94
+ const watchAssociations = URL_META.resolveAssociations(
95
+ { watch: clientFilePatterns },
96
+ rootDirectoryUrl,
97
+ )
98
+ const urlGraph = createUrlGraph({
99
+ clientFileChangeCallbackList,
100
+ clientFilesPruneCallbackList,
101
+ onCreateUrlInfo: (urlInfo) => {
102
+ const { watch } = URL_META.applyAssociations({
103
+ url: urlInfo.url,
104
+ associations: watchAssociations,
105
+ })
106
+ urlInfo.isValid = () => watch
107
+ },
108
+ includeOriginalUrls: scenario === "dev",
109
+ })
110
+ const kitchen = createKitchen({
111
+ signal,
112
+ logLevel,
113
+ rootDirectoryUrl,
114
+ scenario,
115
+ runtimeCompat,
116
+ urlGraph,
117
+ plugins: [
118
+ ...plugins,
119
+ ...getCorePlugins({
120
+ rootDirectoryUrl,
121
+ scenario,
122
+ runtimeCompat,
123
+
124
+ urlAnalysis,
125
+ htmlSupervisor,
126
+ nodeEsmResolution,
127
+ fileSystemMagicResolution,
128
+ transpilation,
129
+
130
+ clientAutoreload,
131
+ clientFileChangeCallbackList,
132
+ clientFilesPruneCallbackList,
133
+ explorer,
134
+ }),
135
+ ],
136
+ sourcemaps,
137
+ writeGeneratedFiles,
138
+ })
139
+ serverStopCallbacks.push(() => {
140
+ kitchen.pluginController.callHooks("destroy", kitchen.kitchenContext)
141
+ })
142
+ server_events: {
143
+ const allServerEvents = {}
144
+ kitchen.pluginController.plugins.forEach((plugin) => {
145
+ const { serverEvents } = plugin
146
+ if (serverEvents) {
147
+ Object.keys(serverEvents).forEach((serverEventName) => {
148
+ // we could throw on serverEvent name conflict
149
+ // we could throw if serverEvents[serverEventName] is not a function
150
+ allServerEvents[serverEventName] = serverEvents[serverEventName]
151
+ })
152
+ }
153
+ })
154
+ const serverEventNames = Object.keys(allServerEvents)
155
+ if (serverEventNames.length > 0) {
156
+ const serverEventsDispatcher = createServerEventsDispatcher()
157
+ serverStopCallbacks.push(() => {
158
+ serverEventsDispatcher.destroy()
159
+ })
160
+ Object.keys(allServerEvents).forEach((serverEventName) => {
161
+ allServerEvents[serverEventName]({
162
+ rootDirectoryUrl,
163
+ urlGraph,
164
+ scenario,
165
+ sendServerEvent: (data) => {
166
+ serverEventsDispatcher.dispatch({
167
+ type: serverEventName,
168
+ data,
169
+ })
170
+ },
171
+ })
172
+ })
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
+ // "unshift" because serve must come first to catch event source client request
196
+ kitchen.pluginController.unshiftPlugin({
197
+ name: "jsenv:provide_server_events",
198
+ serve: (request) => {
199
+ const { accept } = request.headers
200
+ if (accept && accept.includes("text/event-stream")) {
201
+ const room = serverEventsDispatcher.addRoom(request)
202
+ return room.join(request)
203
+ }
204
+ return null
205
+ },
206
+ })
207
+ // "push" so that event source client connection can be put as early as possible in html
208
+ kitchen.pluginController.pushPlugin(
209
+ jsenvPluginServerEventsClientInjection(),
210
+ )
211
+ }
212
+ }
213
+
214
+ const context = {
215
+ rootDirectoryUrl,
216
+ scenario,
217
+ runtimeName,
218
+ runtimeVersion,
219
+ urlGraph,
220
+ kitchen,
221
+ }
222
+ contextCache.set(runtimeId, context)
223
+ return context
27
224
  }
28
225
 
29
- const getResponse = async (request) => {
226
+ return async (request) => {
30
227
  // serve file inside ".jsenv" directory
31
228
  const requestFileUrl = new URL(request.ressource.slice(1), rootDirectoryUrl)
32
229
  .href
33
- if (urlIsInsideOf(requestFileUrl, kitchen.jsenvDirectoryUrl)) {
230
+ if (urlIsInsideOf(requestFileUrl, jsenvDirectoryUrl)) {
34
231
  return fetchFileSystem(requestFileUrl, {
35
232
  headers: request.headers,
36
233
  })
37
234
  }
235
+ const { runtimeName, runtimeVersion, urlGraph, kitchen } =
236
+ getOrCreateContext(request)
38
237
  const responseFromPlugin =
39
238
  await kitchen.pluginController.callAsyncHooksUntil(
40
239
  "serve",
41
240
  request,
42
- serveContext,
241
+ kitchen.kitchenContext,
43
242
  )
44
243
  if (responseFromPlugin) {
45
244
  return responseFromPlugin
@@ -51,7 +250,7 @@ export const createFileService = ({
51
250
  }
52
251
  if (!reference) {
53
252
  const entryPoint = kitchen.injectReference({
54
- trace: parentUrl || rootDirectoryUrl,
253
+ trace: { message: parentUrl || rootDirectoryUrl },
55
254
  parentUrl: parentUrl || rootDirectoryUrl,
56
255
  type: "http_request",
57
256
  specifier: request.ressource,
@@ -64,8 +263,12 @@ export const createFileService = ({
64
263
  if (
65
264
  ifNoneMatch &&
66
265
  urlInfo.contentEtag === ifNoneMatch &&
67
- // - isValid is true by default
68
- // - isValid can be overriden by plugins such as cjs_to_esm
266
+ // urlInfo.isValid
267
+ // - is false by default because there must be some logic capable
268
+ // to invalidate the url (otherwise server would return 304 forever)
269
+ // - is set to a function returning true if the file is watched
270
+ // in start_dev_server.js
271
+ // - is set to a custom function by cjs_to_esm in compiled_file_cache.js
69
272
  urlInfo.isValid()
70
273
  ) {
71
274
  return {
@@ -92,9 +295,6 @@ export const createFileService = ({
92
295
  urlInfo.dependsOnPackageJson = false
93
296
  urlInfo.timing = {}
94
297
  }
95
- const { runtimeName, runtimeVersion } = parseUserAgentHeader(
96
- request.headers["user-agent"],
97
- )
98
298
  await kitchen.cook(urlInfo, {
99
299
  request,
100
300
  reference,
@@ -126,7 +326,7 @@ export const createFileService = ({
126
326
  kitchen.pluginController.callHooks(
127
327
  "augmentResponse",
128
328
  { reference, urlInfo },
129
- augmentResponseContext,
329
+ kitchen.kitchenContext,
130
330
  (returnValue) => {
131
331
  response = composeTwoResponses(response, returnValue)
132
332
  },
@@ -135,10 +335,19 @@ export const createFileService = ({
135
335
  } catch (e) {
136
336
  const code = e.code
137
337
  if (code === "PARSE_ERROR") {
138
- // let the browser re-throw the syntax 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
+ })
139
348
  return {
140
349
  url: reference.url,
141
- status: 200,
350
+ status: 200, // let the browser re-throw the syntax error
142
351
  statusText: e.reason,
143
352
  statusMessage: e.message,
144
353
  headers: {
@@ -166,6 +375,19 @@ export const createFileService = ({
166
375
  }
167
376
  }
168
377
  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
+ })
169
391
  return {
170
392
  url: reference.url,
171
393
  status: 404,
@@ -173,6 +395,16 @@ export const createFileService = ({
173
395
  statusMessage: e.message,
174
396
  }
175
397
  }
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
+ })
176
408
  return {
177
409
  url: reference.url,
178
410
  status: 500,
@@ -181,10 +413,6 @@ export const createFileService = ({
181
413
  }
182
414
  }
183
415
  }
184
- return async (request) => {
185
- let response = await getResponse(request)
186
- return response
187
- }
188
416
  }
189
417
 
190
418
  const inferParentFromRequest = (request, rootDirectoryUrl) => {
@@ -4,6 +4,8 @@ import { urlSpecifierEncoding } from "./url_specifier_encoding.js"
4
4
  export const createUrlGraph = ({
5
5
  clientFileChangeCallbackList,
6
6
  clientFilesPruneCallbackList,
7
+ onCreateUrlInfo = () => {},
8
+ includeOriginalUrls,
7
9
  } = {}) => {
8
10
  const urlInfoMap = new Map()
9
11
  const getUrlInfo = (url) => urlInfoMap.get(url)
@@ -22,6 +24,7 @@ export const createUrlGraph = ({
22
24
  if (existingUrlInfo) return existingUrlInfo
23
25
  const urlInfo = createUrlInfo(url)
24
26
  urlInfoMap.set(url, urlInfo)
27
+ onCreateUrlInfo(urlInfo)
25
28
  return urlInfo
26
29
  }
27
30
  const inferReference = (specifier, parentUrl) => {
@@ -55,7 +58,17 @@ export const createUrlGraph = ({
55
58
  }
56
59
 
57
60
  const updateReferences = (urlInfo, references) => {
58
- const dependencyUrls = []
61
+ const setOfDependencyUrls = new Set()
62
+
63
+ // for import assertion "file.css?as_css_module" depends on "file.css"
64
+ // this is enabled only for dev where there is autoreload
65
+ // during build the css file must be considered as not referenced
66
+ // (except if referenced explicitely by something else) so that
67
+ // the css file does not appear in the build directory
68
+ if (includeOriginalUrls && urlInfo.originalUrl !== urlInfo.url) {
69
+ setOfDependencyUrls.add(urlInfo.originalUrl)
70
+ }
71
+
59
72
  references.forEach((reference) => {
60
73
  if (reference.isRessourceHint) {
61
74
  // ressource hint are a special kind of reference.
@@ -66,19 +79,14 @@ export const createUrlGraph = ({
66
79
  // by <link> as dependency and it's fine
67
80
  return
68
81
  }
69
- if (dependencyUrls.includes(reference.url)) {
70
- return
71
- }
72
- dependencyUrls.push(reference.url)
82
+ setOfDependencyUrls.add(reference.url)
73
83
  })
74
- pruneDependencies(
75
- urlInfo,
76
- Array.from(urlInfo.dependencies).filter(
77
- (dep) => !dependencyUrls.includes(dep),
78
- ),
84
+ const urlsToRemove = Array.from(urlInfo.dependencies).filter(
85
+ (dep) => !setOfDependencyUrls.has(dep),
79
86
  )
87
+ pruneDependencies(urlInfo, urlsToRemove)
80
88
  urlInfo.references = references
81
- dependencyUrls.forEach((dependencyUrl) => {
89
+ setOfDependencyUrls.forEach((dependencyUrl) => {
82
90
  const dependencyUrlInfo = reuseOrCreateUrlInfo(dependencyUrl)
83
91
  urlInfo.dependencies.add(dependencyUrl)
84
92
  dependencyUrlInfo.dependents.add(urlInfo.url)
@@ -90,14 +98,17 @@ export const createUrlGraph = ({
90
98
  const removeDependencies = (urlInfo, urlsToPrune) => {
91
99
  urlsToPrune.forEach((urlToPrune) => {
92
100
  urlInfo.dependencies.delete(urlToPrune)
93
- const dependency = getUrlInfo(urlToPrune)
94
- if (!dependency) {
101
+ const dependencyUrlInfo = getUrlInfo(urlToPrune)
102
+ if (!dependencyUrlInfo) {
95
103
  return
96
104
  }
97
- dependency.dependents.delete(urlInfo.url)
98
- if (dependency.dependents.size === 0) {
99
- removeDependencies(dependency, Array.from(dependency.dependencies))
100
- prunedUrlInfos.push(dependency)
105
+ dependencyUrlInfo.dependents.delete(urlInfo.url)
106
+ if (dependencyUrlInfo.dependents.size === 0) {
107
+ removeDependencies(
108
+ dependencyUrlInfo,
109
+ Array.from(dependencyUrlInfo.dependencies),
110
+ )
111
+ prunedUrlInfos.push(dependencyUrlInfo)
101
112
  }
102
113
  })
103
114
  }
@@ -107,8 +118,10 @@ export const createUrlGraph = ({
107
118
  }
108
119
  prunedUrlInfos.forEach((prunedUrlInfo) => {
109
120
  prunedUrlInfo.modifiedTimestamp = Date.now()
110
- // should we delete?
111
- // delete urlInfos[prunedUrlInfo.url]
121
+ if (prunedUrlInfo.isInline) {
122
+ // should we always delete?
123
+ deleteUrlInfo(prunedUrlInfo.url)
124
+ }
112
125
  })
113
126
  if (clientFilesPruneCallbackList) {
114
127
  clientFilesPruneCallbackList.forEach((callback) => {
@@ -145,6 +158,12 @@ export const createUrlGraph = ({
145
158
  iterate(dependentUrlInfo)
146
159
  }
147
160
  })
161
+ urlInfo.dependencies.forEach((dependencyUrl) => {
162
+ const dependencyUrlInfo = getUrlInfo(dependencyUrl)
163
+ if (dependencyUrlInfo.isInline) {
164
+ iterate(dependencyUrlInfo)
165
+ }
166
+ })
148
167
  }
149
168
  iterate(urlInfo)
150
169
  }
@@ -229,4 +248,4 @@ const createUrlInfo = (url) => {
229
248
  }
230
249
  }
231
250
 
232
- const isValid = () => true
251
+ const isValid = () => false
@@ -0,0 +1,201 @@
1
+ import { urlHotMetas } from "../../import_meta_hot/client/import_meta_hot.js"
2
+ import {
3
+ isAutoreloadEnabled,
4
+ setAutoreloadPreference,
5
+ } from "./autoreload_preference.js"
6
+ import { compareTwoUrlPaths } from "./url_helpers.js"
7
+ import {
8
+ reloadHtmlPage,
9
+ reloadJsImport,
10
+ getDOMNodesUsingUrl,
11
+ } from "./reload.js"
12
+
13
+ const reloader = {
14
+ urlHotMetas,
15
+ isAutoreloadEnabled,
16
+ setAutoreloadPreference,
17
+ status: "idle",
18
+ onstatuschange: () => {},
19
+ setStatus: (status) => {
20
+ reloader.status = status
21
+ reloader.onstatuschange()
22
+ },
23
+ messages: [],
24
+ addMessage: (reloadMessage) => {
25
+ reloader.messages.push(reloadMessage)
26
+ if (isAutoreloadEnabled()) {
27
+ reloader.reload()
28
+ } else {
29
+ reloader.setStatus("can_reload")
30
+ }
31
+ },
32
+ reload: () => {
33
+ const someEffectIsFullReload = reloader.messages.some(
34
+ (reloadMessage) => reloadMessage.type === "full",
35
+ )
36
+ if (someEffectIsFullReload) {
37
+ reloadHtmlPage()
38
+ return
39
+ }
40
+ reloader.setStatus("reloading")
41
+ const onApplied = (reloadMessage) => {
42
+ const index = reloader.messages.indexOf(reloadMessage)
43
+ reloader.messages.splice(index, 1)
44
+ if (reloader.messages.length === 0) {
45
+ reloader.setStatus("idle")
46
+ }
47
+ }
48
+ const setReloadMessagePromise = (reloadMessage, promise) => {
49
+ promise.then(
50
+ () => {
51
+ onApplied(reloadMessage)
52
+ },
53
+ (e) => {
54
+ reloader.setStatus("failed")
55
+ if (typeof window.reportError === "function") {
56
+ window.reportError(e)
57
+ } else {
58
+ console.error(e)
59
+ }
60
+ console.error(
61
+ `[jsenv] Hot reload failed after ${reloadMessage.reason}.
62
+ This could be due to syntax errors or importing non-existent modules (see errors in console)`,
63
+ )
64
+ },
65
+ )
66
+ }
67
+ reloader.messages.forEach((reloadMessage) => {
68
+ if (reloadMessage.type === "hot") {
69
+ const promise = addToHotQueue(() => {
70
+ return applyHotReload(reloadMessage)
71
+ })
72
+ setReloadMessagePromise(reloadMessage, promise)
73
+ } else {
74
+ setReloadMessagePromise(reloadMessage, Promise.resolve())
75
+ }
76
+ })
77
+ },
78
+ }
79
+
80
+ let pendingCallbacks = []
81
+ let running = false
82
+ const addToHotQueue = async (callback) => {
83
+ pendingCallbacks.push(callback)
84
+ dequeue()
85
+ }
86
+ const dequeue = async () => {
87
+ if (running) {
88
+ return
89
+ }
90
+ const callbacks = pendingCallbacks.slice()
91
+ pendingCallbacks = []
92
+ running = true
93
+ try {
94
+ await callbacks.reduce(async (previous, callback) => {
95
+ await previous
96
+ await callback()
97
+ }, Promise.resolve())
98
+ } finally {
99
+ running = false
100
+ if (pendingCallbacks.length) {
101
+ dequeue()
102
+ }
103
+ }
104
+ }
105
+
106
+ const applyHotReload = async ({ hotInstructions }) => {
107
+ await hotInstructions.reduce(
108
+ async (previous, { type, boundary, acceptedBy }) => {
109
+ await previous
110
+
111
+ const urlToFetch = new URL(boundary, `${window.location.origin}/`).href
112
+ const urlHotMeta = urlHotMetas[urlToFetch]
113
+ // there is no url hot meta when:
114
+ // - code was not executed (code splitting with dynamic import)
115
+ // - import.meta.hot.accept() is not called (happens for HTML and CSS)
116
+
117
+ if (type === "prune") {
118
+ if (urlHotMeta) {
119
+ delete urlHotMetas[urlToFetch]
120
+ if (urlHotMeta.disposeCallback) {
121
+ console.groupCollapsed(
122
+ `[jsenv] cleanup ${boundary} (previously used in ${acceptedBy})`,
123
+ )
124
+ console.log(`call dispose callback`)
125
+ await urlHotMeta.disposeCallback()
126
+ console.groupEnd()
127
+ }
128
+ }
129
+ return null
130
+ }
131
+
132
+ if (acceptedBy === boundary) {
133
+ console.groupCollapsed(`[jsenv] hot reloading ${boundary}`)
134
+ } else {
135
+ console.groupCollapsed(
136
+ `[jsenv] hot reloading ${acceptedBy} usage in ${boundary}`,
137
+ )
138
+ }
139
+ if (type === "js_module") {
140
+ if (!urlHotMeta) {
141
+ // code was not executed, no need to re-execute it
142
+ return null
143
+ }
144
+ if (urlHotMeta.disposeCallback) {
145
+ console.log(`call dispose callback`)
146
+ await urlHotMeta.disposeCallback()
147
+ }
148
+ console.log(`importing js module`)
149
+ const namespace = await reloadJsImport(urlToFetch)
150
+ if (urlHotMeta.acceptCallback) {
151
+ await urlHotMeta.acceptCallback(namespace)
152
+ }
153
+ console.log(`js module import done`)
154
+ console.groupEnd()
155
+ return namespace
156
+ }
157
+ if (type === "html") {
158
+ if (!compareTwoUrlPaths(urlToFetch, window.location.href)) {
159
+ // we are not in that HTML page
160
+ return null
161
+ }
162
+ const urlToReload = new URL(acceptedBy, `${window.location.origin}/`)
163
+ .href
164
+ const domNodesUsingUrl = getDOMNodesUsingUrl(urlToReload)
165
+ const domNodesCount = domNodesUsingUrl.length
166
+ if (domNodesCount === 0) {
167
+ console.log(`no dom node using ${acceptedBy}`)
168
+ } else if (domNodesCount === 1) {
169
+ console.log(`reloading`, domNodesUsingUrl[0].node)
170
+ domNodesUsingUrl[0].reload()
171
+ } else {
172
+ console.log(`reloading ${domNodesCount} nodes using ${acceptedBy}`)
173
+ domNodesUsingUrl.forEach((domNodesUsingUrl) => {
174
+ domNodesUsingUrl.reload()
175
+ })
176
+ }
177
+ console.groupEnd()
178
+ return null
179
+ }
180
+ console.warn(`unknown update type: "${type}"`)
181
+ return null
182
+ },
183
+ Promise.resolve(),
184
+ )
185
+ }
186
+
187
+ window.__reloader__ = reloader
188
+ window.__server_events__.addEventCallbacks({
189
+ reload: ({ data }) => {
190
+ const reloadMessage = JSON.parse(data)
191
+ reloader.addMessage(reloadMessage)
192
+ },
193
+ })
194
+
195
+ // const findHotMetaUrl = (originalFileRelativeUrl) => {
196
+ // return Object.keys(urlHotMetas).find((compileUrl) => {
197
+ // return (
198
+ // parseCompiledUrl(compileUrl).fileRelativeUrl === originalFileRelativeUrl
199
+ // )
200
+ // })
201
+ // }