@jsenv/core 27.4.0 → 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 (84) 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 +221 -73
  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 +7483 -7281
  8. package/package.json +11 -10
  9. package/{README.md → readme.md} +8 -9
  10. package/src/build/build.js +12 -16
  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/omega/errors.js +20 -18
  15. package/src/omega/kitchen.js +7 -6
  16. package/src/omega/omega_server.js +96 -127
  17. package/src/omega/server/file_service.js +247 -46
  18. package/src/omega/url_graph.js +33 -20
  19. package/src/plugins/autoreload/client/autoreload.js +201 -0
  20. package/src/plugins/autoreload/{dev_sse/client → client}/autoreload_preference.js +0 -0
  21. package/src/plugins/autoreload/{dev_sse/client → client}/reload.js +29 -10
  22. package/src/plugins/autoreload/{dev_sse/client → client}/url_helpers.js +0 -0
  23. package/src/plugins/autoreload/jsenv_plugin_autoreload.js +4 -4
  24. package/src/plugins/autoreload/{dev_sse/jsenv_plugin_dev_sse_client.js → jsenv_plugin_autoreload_client.js} +8 -8
  25. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +196 -0
  26. package/src/{dev/plugins → plugins}/explorer/client/explorer.html +0 -0
  27. package/src/{dev/plugins → plugins}/explorer/client/jsenv.png +0 -0
  28. package/src/{dev/plugins → plugins}/explorer/jsenv_plugin_explorer.js +1 -3
  29. package/src/plugins/html_supervisor/client/{error_in_document.js → error_overlay.js} +73 -17
  30. package/src/plugins/html_supervisor/client/html_supervisor_installer.js +127 -54
  31. package/src/plugins/html_supervisor/client/html_supervisor_setup.js +3 -4
  32. package/src/plugins/html_supervisor/jsenv_plugin_html_supervisor.js +19 -12
  33. package/src/plugins/inline/jsenv_plugin_html_inline_content.js +97 -117
  34. package/src/plugins/node_esm_resolution/jsenv_plugin_node_esm_resolution.js +66 -58
  35. package/src/plugins/plugin_controller.js +102 -67
  36. package/src/plugins/plugins.js +10 -8
  37. package/src/{helpers/event_source/event_source.js → plugins/server_events/client/event_source_connection.js} +102 -31
  38. package/src/plugins/server_events/client/server_events_client.js +17 -0
  39. package/src/plugins/server_events/jsenv_plugin_server_events_client_injection.js +48 -0
  40. package/src/plugins/server_events/server_events_dispatcher.js +69 -0
  41. package/src/{dev/plugins → plugins}/toolbar/client/animation/toolbar_animation.js +0 -0
  42. package/src/{dev/plugins → plugins}/toolbar/client/eventsource/eventsource.css +0 -0
  43. package/src/{dev/plugins → plugins}/toolbar/client/eventsource/toolbar_eventsource.js +0 -0
  44. package/src/{dev/plugins → plugins}/toolbar/client/execution/execution.css +0 -0
  45. package/src/{dev/plugins → plugins}/toolbar/client/execution/toolbar_execution.js +0 -0
  46. package/src/{dev/plugins → plugins}/toolbar/client/focus/focus.css +0 -0
  47. package/src/{dev/plugins → plugins}/toolbar/client/focus/toolbar_focus.js +0 -0
  48. package/src/{dev/plugins → plugins}/toolbar/client/jsenv_logo.svg +0 -0
  49. package/src/{dev/plugins → plugins}/toolbar/client/notification/toolbar_notification.js +0 -0
  50. package/src/{dev/plugins → plugins}/toolbar/client/responsive/overflow_menu.css +0 -0
  51. package/src/{dev/plugins → plugins}/toolbar/client/responsive/toolbar_responsive.js +0 -0
  52. package/src/{dev/plugins → plugins}/toolbar/client/settings/settings.css +0 -0
  53. package/src/{dev/plugins → plugins}/toolbar/client/settings/toolbar_settings.js +0 -0
  54. package/src/{dev/plugins → plugins}/toolbar/client/theme/jsenv_theme.css +0 -0
  55. package/src/{dev/plugins → plugins}/toolbar/client/theme/light_theme.css +0 -0
  56. package/src/{dev/plugins → plugins}/toolbar/client/theme/toolbar_theme.js +0 -0
  57. package/src/{dev/plugins → plugins}/toolbar/client/toolbar.html +0 -0
  58. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_injector.js +0 -0
  59. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_main.css +0 -0
  60. package/src/{dev/plugins → plugins}/toolbar/client/toolbar_main.js +0 -0
  61. package/src/{dev/plugins → plugins}/toolbar/client/tooltip/tooltip.css +0 -0
  62. package/src/{dev/plugins → plugins}/toolbar/client/tooltip/tooltip.js +0 -0
  63. package/src/{dev/plugins → plugins}/toolbar/client/util/animation.js +0 -0
  64. package/src/{dev/plugins → plugins}/toolbar/client/util/dom.js +0 -0
  65. package/src/{dev/plugins → plugins}/toolbar/client/util/fetch_using_xhr.js +0 -0
  66. package/src/{dev/plugins → plugins}/toolbar/client/util/fetching.js +0 -0
  67. package/src/{dev/plugins → plugins}/toolbar/client/util/iframe_to_parent_href.js +0 -0
  68. package/src/{dev/plugins → plugins}/toolbar/client/util/jsenv_logger.js +0 -0
  69. package/src/{dev/plugins → plugins}/toolbar/client/util/preferences.js +0 -0
  70. package/src/{dev/plugins → plugins}/toolbar/client/util/responsive.js +0 -0
  71. package/src/{dev/plugins → plugins}/toolbar/client/util/util.js +0 -0
  72. package/src/{dev/plugins → plugins}/toolbar/client/variant/variant.js +0 -0
  73. package/src/{dev/plugins → plugins}/toolbar/jsenv_plugin_toolbar.js +0 -0
  74. package/src/plugins/transpilation/as_js_classic/jsenv_plugin_as_js_classic_html.js +4 -3
  75. package/src/plugins/transpilation/babel/new_stylesheet/client/new_stylesheet.js +25 -55
  76. package/src/plugins/transpilation/import_assertions/jsenv_plugin_import_assertions.js +44 -24
  77. package/src/plugins/transpilation/jsenv_plugin_transpilation.js +6 -1
  78. package/src/plugins/url_analysis/html/html_urls.js +8 -8
  79. package/src/test/execute_plan.js +36 -54
  80. package/src/test/execute_test_plan.js +2 -2
  81. package/dist/js/event_source_client.js +0 -549
  82. package/src/helpers/event_source/sse_service.js +0 -53
  83. package/src/plugins/autoreload/dev_sse/client/event_source_client.js +0 -193
  84. package/src/plugins/autoreload/dev_sse/jsenv_plugin_dev_sse_server.js +0 -192
@@ -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) => {
@@ -235,4 +248,4 @@ const createUrlInfo = (url) => {
235
248
  }
236
249
  }
237
250
 
238
- 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
+ // }
@@ -13,8 +13,8 @@ export const reloadHtmlPage = () => {
13
13
  // - no need to check [hot-accept]and [hot-decline] attributes for instance
14
14
  // This is because if something should full reload, we receive "full_reload"
15
15
  // from server and this function is not called
16
- export const reloadDOMNodesUsingUrl = (urlToReload) => {
17
- const mutations = []
16
+ export const getDOMNodesUsingUrl = (urlToReload) => {
17
+ const nodes = []
18
18
  const shouldReloadUrl = (urlCandidate) => {
19
19
  return compareTwoUrlPaths(urlCandidate, urlToReload)
20
20
  }
@@ -29,8 +29,11 @@ export const reloadDOMNodesUsingUrl = (urlToReload) => {
29
29
  if (!shouldReloadUrl(attribute)) {
30
30
  return
31
31
  }
32
- mutations.push(() => {
33
- node[attributeName] = injectQuery(attribute, { hmr: Date.now() })
32
+ nodes.push({
33
+ node,
34
+ reload: () => {
35
+ node[attributeName] = injectQuery(attribute, { hmr: Date.now() })
36
+ },
34
37
  })
35
38
  }
36
39
  Array.from(document.querySelectorAll(`link[rel="stylesheet"]`)).forEach(
@@ -41,8 +44,23 @@ export const reloadDOMNodesUsingUrl = (urlToReload) => {
41
44
  Array.from(document.querySelectorAll(`link[rel="icon"]`)).forEach((link) => {
42
45
  visitNodeAttributeAsUrl(link, "href")
43
46
  })
44
- Array.from(document.querySelector("script")).forEach((script) => {
47
+ Array.from(document.querySelectorAll("script")).forEach((script) => {
45
48
  visitNodeAttributeAsUrl(script, "src")
49
+ const generatedFromSrc = script.getAttribute("generated-from-src")
50
+ if (generatedFromSrc) {
51
+ const generatedFromUrl = new URL(generatedFromSrc, window.location.origin)
52
+ .href
53
+ if (shouldReloadUrl(generatedFromUrl)) {
54
+ nodes.push({
55
+ node: script,
56
+ reload: () =>
57
+ window.__html_supervisor__.reloadSupervisedScript({
58
+ type: script.type,
59
+ src: generatedFromSrc,
60
+ }),
61
+ })
62
+ }
63
+ }
46
64
  })
47
65
  // There is no real need to update a.href because the ressource will be fetched when clicked.
48
66
  // But in a scenario where the ressource was already visited and is in browser cache, adding
@@ -67,8 +85,11 @@ export const reloadDOMNodesUsingUrl = (urlToReload) => {
67
85
  srcCandidate.specifier = injectQuery(url, { hmr: Date.now() })
68
86
  }
69
87
  })
70
- mutations.push(() => {
71
- img.srcset = stringifySrcSet(srcCandidates)
88
+ nodes.push({
89
+ node: img,
90
+ reload: () => {
91
+ img.srcset = stringifySrcSet(srcCandidates)
92
+ },
72
93
  })
73
94
  }
74
95
  })
@@ -83,9 +104,7 @@ export const reloadDOMNodesUsingUrl = (urlToReload) => {
83
104
  Array.from(document.querySelectorAll("use")).forEach((use) => {
84
105
  visitNodeAttributeAsUrl(use, "href")
85
106
  })
86
- mutations.forEach((mutation) => {
87
- mutation()
88
- })
107
+ return nodes
89
108
  }
90
109
 
91
110
  export const reloadJsImport = async (url) => {
@@ -1,6 +1,6 @@
1
1
  import { jsenvPluginHmr } from "./jsenv_plugin_hmr.js"
2
- import { jsenvPluginDevSSEClient } from "./dev_sse/jsenv_plugin_dev_sse_client.js"
3
- import { jsenvPluginDevSSEServer } from "./dev_sse/jsenv_plugin_dev_sse_server.js"
2
+ import { jsenvPluginAutoreloadClient } from "./jsenv_plugin_autoreload_client.js"
3
+ import { jsenvPluginAutoreloadServer } from "./jsenv_plugin_autoreload_server.js"
4
4
 
5
5
  export const jsenvPluginAutoreload = ({
6
6
  scenario,
@@ -12,8 +12,8 @@ export const jsenvPluginAutoreload = ({
12
12
  }
13
13
  return [
14
14
  jsenvPluginHmr(),
15
- jsenvPluginDevSSEClient(),
16
- jsenvPluginDevSSEServer({
15
+ jsenvPluginAutoreloadClient(),
16
+ jsenvPluginAutoreloadServer({
17
17
  clientFileChangeCallbackList,
18
18
  clientFilesPruneCallbackList,
19
19
  }),
@@ -5,30 +5,30 @@ import {
5
5
  createHtmlNode,
6
6
  } from "@jsenv/ast"
7
7
 
8
- export const jsenvPluginDevSSEClient = () => {
9
- const eventSourceClientFileUrl = new URL(
10
- "./client/event_source_client.js",
8
+ export const jsenvPluginAutoreloadClient = () => {
9
+ const autoreloadClientFileUrl = new URL(
10
+ "./client/autoreload.js",
11
11
  import.meta.url,
12
12
  ).href
13
13
 
14
14
  return {
15
- name: "jsenv:dev_sse_client",
15
+ name: "jsenv:autoreload_client",
16
16
  appliesDuring: { dev: true },
17
17
  transformUrlContent: {
18
18
  html: (htmlUrlInfo, context) => {
19
19
  const htmlAst = parseHtmlString(htmlUrlInfo.content)
20
- const [eventSourceClientReference] = context.referenceUtils.inject({
20
+ const [autoreloadClientReference] = context.referenceUtils.inject({
21
21
  type: "script_src",
22
22
  expectedType: "js_module",
23
- specifier: eventSourceClientFileUrl,
23
+ specifier: autoreloadClientFileUrl,
24
24
  })
25
25
  injectScriptNodeAsEarlyAsPossible(
26
26
  htmlAst,
27
27
  createHtmlNode({
28
28
  "tagName": "script",
29
29
  "type": "module",
30
- "src": eventSourceClientReference.generatedSpecifier,
31
- "injected-by": "jsenv:dev_sse_client",
30
+ "src": autoreloadClientReference.generatedSpecifier,
31
+ "injected-by": "jsenv:autoreload_client",
32
32
  }),
33
33
  )
34
34
  const htmlModified = stringifyHtmlAst(htmlAst)
@@ -0,0 +1,196 @@
1
+ import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls"
2
+
3
+ export const jsenvPluginAutoreloadServer = ({
4
+ clientFileChangeCallbackList,
5
+ clientFilesPruneCallbackList,
6
+ }) => {
7
+ return {
8
+ name: "jsenv:autoreload_server",
9
+ appliesDuring: { dev: true },
10
+ serverEvents: {
11
+ reload: ({ sendServerEvent, rootDirectoryUrl, urlGraph }) => {
12
+ const formatUrlForClient = (url) => {
13
+ if (urlIsInsideOf(url, rootDirectoryUrl)) {
14
+ return urlToRelativeUrl(url, rootDirectoryUrl)
15
+ }
16
+ if (url.startsWith("file:")) {
17
+ return `/@fs/${url.slice("file:///".length)}`
18
+ }
19
+ return url
20
+ }
21
+
22
+ const notifyDeclined = ({ cause, reason, declinedBy }) => {
23
+ sendServerEvent({
24
+ cause,
25
+ type: "full",
26
+ typeReason: reason,
27
+ declinedBy,
28
+ })
29
+ }
30
+ const notifyAccepted = ({ cause, reason, instructions }) => {
31
+ sendServerEvent({
32
+ cause,
33
+ type: "hot",
34
+ typeReason: reason,
35
+ hotInstructions: instructions,
36
+ })
37
+ }
38
+ const propagateUpdate = (firstUrlInfo) => {
39
+ const iterate = (urlInfo, seen) => {
40
+ if (urlInfo.data.hotAcceptSelf) {
41
+ return {
42
+ accepted: true,
43
+ reason:
44
+ urlInfo === firstUrlInfo
45
+ ? `file accepts hot reload`
46
+ : `a dependent file accepts hot reload`,
47
+ instructions: [
48
+ {
49
+ type: urlInfo.type,
50
+ boundary: formatUrlForClient(urlInfo.url),
51
+ acceptedBy: formatUrlForClient(urlInfo.url),
52
+ },
53
+ ],
54
+ }
55
+ }
56
+ const { dependents } = urlInfo
57
+ const instructions = []
58
+ for (const dependentUrl of dependents) {
59
+ const dependentUrlInfo = urlGraph.getUrlInfo(dependentUrl)
60
+ if (dependentUrlInfo.data.hotDecline) {
61
+ return {
62
+ declined: true,
63
+ reason: `a dependent file declines hot reload`,
64
+ declinedBy: dependentUrl,
65
+ }
66
+ }
67
+ const { hotAcceptDependencies = [] } = dependentUrlInfo.data
68
+ if (hotAcceptDependencies.includes(urlInfo.url)) {
69
+ instructions.push({
70
+ type: dependentUrlInfo.type,
71
+ boundary: formatUrlForClient(dependentUrl),
72
+ acceptedBy: formatUrlForClient(urlInfo.url),
73
+ })
74
+ continue
75
+ }
76
+ if (seen.includes(dependentUrl)) {
77
+ return {
78
+ declined: true,
79
+ reason: "circular dependency",
80
+ declinedBy: formatUrlForClient(dependentUrl),
81
+ }
82
+ }
83
+ const dependentPropagationResult = iterate(dependentUrlInfo, [
84
+ ...seen,
85
+ dependentUrl,
86
+ ])
87
+ if (dependentPropagationResult.accepted) {
88
+ instructions.push(...dependentPropagationResult.instructions)
89
+ continue
90
+ }
91
+ if (
92
+ // declined explicitely by an other file, it must decline the whole update
93
+ dependentPropagationResult.declinedBy
94
+ ) {
95
+ return dependentPropagationResult
96
+ }
97
+ // declined by absence of boundary, we can keep searching
98
+ continue
99
+ }
100
+ if (instructions.length === 0) {
101
+ return {
102
+ declined: true,
103
+ reason: `there is no file accepting hot reload while propagating update`,
104
+ }
105
+ }
106
+ return {
107
+ accepted: true,
108
+ reason: `${instructions.length} dependent file(s) accepts hot reload`,
109
+ instructions,
110
+ }
111
+ }
112
+ const seen = []
113
+ return iterate(firstUrlInfo, seen)
114
+ }
115
+ clientFileChangeCallbackList.push(({ url, event }) => {
116
+ const urlInfo = urlGraph.getUrlInfo(url)
117
+ // file not part of dependency graph
118
+ if (!urlInfo) {
119
+ return
120
+ }
121
+ const relativeUrl = formatUrlForClient(url)
122
+ const hotUpdate = propagateUpdate(urlInfo)
123
+ if (hotUpdate.declined) {
124
+ notifyDeclined({
125
+ cause: `${relativeUrl} ${event}`,
126
+ reason: hotUpdate.reason,
127
+ declinedBy: hotUpdate.declinedBy,
128
+ })
129
+ } else {
130
+ notifyAccepted({
131
+ cause: `${relativeUrl} ${event}`,
132
+ reason: hotUpdate.reason,
133
+ instructions: hotUpdate.instructions,
134
+ })
135
+ }
136
+ })
137
+ clientFilesPruneCallbackList.push(
138
+ ({ prunedUrlInfos, firstUrlInfo }) => {
139
+ const mainHotUpdate = propagateUpdate(firstUrlInfo)
140
+ const cause = `following files are no longer referenced: ${prunedUrlInfos.map(
141
+ (prunedUrlInfo) => formatUrlForClient(prunedUrlInfo.url),
142
+ )}`
143
+ // now check if we can hot update the main ressource
144
+ // then if we can hot update all dependencies
145
+ if (mainHotUpdate.declined) {
146
+ notifyDeclined({
147
+ cause,
148
+ reason: mainHotUpdate.reason,
149
+ declinedBy: mainHotUpdate.declinedBy,
150
+ })
151
+ return
152
+ }
153
+ // main can hot update
154
+ let i = 0
155
+ const instructions = []
156
+ while (i < prunedUrlInfos.length) {
157
+ const prunedUrlInfo = prunedUrlInfos[i++]
158
+ if (prunedUrlInfo.data.hotDecline) {
159
+ notifyDeclined({
160
+ cause,
161
+ reason: `a pruned file declines hot reload`,
162
+ declinedBy: formatUrlForClient(prunedUrlInfo.url),
163
+ })
164
+ return
165
+ }
166
+ instructions.push({
167
+ type: "prune",
168
+ boundary: formatUrlForClient(prunedUrlInfo.url),
169
+ acceptedBy: formatUrlForClient(firstUrlInfo.url),
170
+ })
171
+ }
172
+ notifyAccepted({
173
+ cause,
174
+ reason: mainHotUpdate.reason,
175
+ instructions,
176
+ })
177
+ },
178
+ )
179
+ },
180
+ },
181
+ serve: (request, { rootDirectoryUrl, urlGraph }) => {
182
+ if (request.ressource === "/__graph__") {
183
+ const graphJson = JSON.stringify(urlGraph.toJSON(rootDirectoryUrl))
184
+ return {
185
+ status: 200,
186
+ headers: {
187
+ "content-type": "application/json",
188
+ "content-length": Buffer.byteLength(graphJson),
189
+ },
190
+ body: graphJson,
191
+ }
192
+ }
193
+ return null
194
+ },
195
+ }
196
+ }
@@ -9,9 +9,7 @@ export const jsenvPluginExplorer = ({ groups }) => {
9
9
 
10
10
  return {
11
11
  name: "jsenv:explorer",
12
- appliesDuring: {
13
- dev: true,
14
- },
12
+ appliesDuring: "dev",
15
13
  serve: async (request, { rootDirectoryUrl }) => {
16
14
  if (request.ressource !== "/") {
17
15
  return null