@jsenv/core 27.0.0-alpha.45 → 27.0.0-alpha.46

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.0.0-alpha.45",
3
+ "version": "27.0.0-alpha.46",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,7 +11,8 @@
11
11
  "node": ">=16.13.0"
12
12
  },
13
13
  "publishConfig": {
14
- "access": "public"
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org"
15
16
  },
16
17
  "type": "module",
17
18
  "imports": {},
@@ -59,15 +60,15 @@
59
60
  "@financial-times/polyfill-useragent-normaliser": "2.0.1",
60
61
  "@jsenv/abort": "4.1.2",
61
62
  "@jsenv/babel-plugins": "1.0.2",
62
- "@jsenv/filesystem": "3.1.0",
63
+ "@jsenv/filesystem": "3.2.1",
63
64
  "@jsenv/importmap": "1.2.0",
64
65
  "@jsenv/integrity": "0.0.1",
65
66
  "@jsenv/log": "1.5.2",
66
67
  "@jsenv/logger": "4.0.1",
67
68
  "@jsenv/node-esm-resolution": "0.0.6",
68
- "@jsenv/server": "12.6.1",
69
+ "@jsenv/server": "12.6.2",
69
70
  "@jsenv/uneval": "1.6.0",
70
- "@jsenv/utils": "1.6.2",
71
+ "@jsenv/utils": "1.7.0",
71
72
  "construct-style-sheets-polyfill": "3.1.0",
72
73
  "cssnano": "5.1.7",
73
74
  "cssnano-preset-default": "5.2.7",
@@ -107,4 +108,4 @@
107
108
  "redux": "4.1.2",
108
109
  "rollup": "2.70.1"
109
110
  }
110
- }
111
+ }
@@ -21,13 +21,15 @@ import {
21
21
  pluginCORS,
22
22
  fetchFileSystem,
23
23
  } from "@jsenv/server"
24
- import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
24
+ import {
25
+ assertAndNormalizeDirectoryUrl,
26
+ registerDirectoryLifecycle,
27
+ } from "@jsenv/filesystem"
25
28
  import { createLogger } from "@jsenv/logger"
26
29
  import { Abort } from "@jsenv/abort"
27
30
 
28
- import { initProcessAutorestart } from "@jsenv/utils/file_watcher/process_auto_restart.js"
31
+ import { initReloadableProcess } from "@jsenv/utils/process_reload/process_reload.js"
29
32
  import { createTaskLog } from "@jsenv/utils/logs/task_log.js"
30
- import { watchFiles } from "@jsenv/utils/file_watcher/file_watcher.js"
31
33
  import { executeCommand } from "@jsenv/utils/command/command.js"
32
34
 
33
35
  export const startBuildServer = async ({
@@ -45,43 +47,80 @@ export const startBuildServer = async ({
45
47
 
46
48
  rootDirectoryUrl,
47
49
  buildDirectoryUrl,
50
+ buildServerFiles = {
51
+ "./package.json": true,
52
+ "./jsenv.config.mjs": true,
53
+ },
54
+ buildServerMainFile,
55
+ buildServerAutoreload = false,
56
+ clientFiles = {
57
+ "./**": true,
58
+ "./**/.*/": false, // any folder starting with a dot is ignored (includes .git,.jsenv for instance)
59
+ "./dist/": false,
60
+ "./**/node_modules/": false,
61
+ },
62
+ cooldownBetweenFileEvents,
48
63
  buildCommand,
49
64
  mainBuildFileUrl = "/index.html",
50
- watchedFilePatterns,
51
- cooldownBetweenFileEvents,
52
- autorestart,
53
65
  }) => {
66
+ const logger = createLogger({ logLevel })
54
67
  rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl)
55
68
  buildDirectoryUrl = assertAndNormalizeDirectoryUrl(buildDirectoryUrl)
56
69
 
57
- const autorestartProcess = await initProcessAutorestart({
58
- signal,
70
+ const reloadableProcess = await initReloadableProcess({
59
71
  handleSIGINT,
60
- ...(autorestart
72
+ ...(buildServerAutoreload
61
73
  ? {
62
74
  enabled: true,
63
- logLevel: autorestart.logLevel,
64
- urlToRestart: autorestart.url,
65
- urlsToWatch: [
66
- ...(autorestart.urlsToWatch || []),
67
- new URL("package.json", rootDirectoryUrl),
68
- new URL("jsenv.config.mjs", rootDirectoryUrl),
69
- ],
75
+ logLevel: "info",
76
+ fileToRestart: buildServerMainFile,
70
77
  }
71
78
  : {
72
79
  enabled: false,
73
80
  }),
74
81
  })
75
- if (autorestartProcess.isPrimary) {
82
+ if (reloadableProcess.isPrimary) {
83
+ const buildServerFileChangeCallback = ({ relativeUrl, event }) => {
84
+ const url = new URL(relativeUrl, rootDirectoryUrl).href
85
+ if (buildServerAutoreload) {
86
+ logger.info(`file ${event} ${url} -> restarting server...`)
87
+ reloadableProcess.reload()
88
+ }
89
+ }
90
+ const stopWatchingBuildServerFiles = registerDirectoryLifecycle(
91
+ rootDirectoryUrl,
92
+ {
93
+ watchPatterns: {
94
+ [buildServerMainFile]: true,
95
+ ...buildServerFiles,
96
+ },
97
+ cooldownBetweenFileEvents,
98
+ keepProcessAlive: false,
99
+ recursive: true,
100
+ added: ({ relativeUrl }) => {
101
+ buildServerFileChangeCallback({ relativeUrl, event: "added" })
102
+ },
103
+ updated: ({ relativeUrl }) => {
104
+ buildServerFileChangeCallback({ relativeUrl, event: "modified" })
105
+ },
106
+ removed: ({ relativeUrl }) => {
107
+ buildServerFileChangeCallback({ relativeUrl, event: "removed" })
108
+ },
109
+ },
110
+ )
111
+ signal.addEventListener("abort", () => {
112
+ stopWatchingBuildServerFiles()
113
+ })
76
114
  return {
77
115
  origin: `${protocol}://127.0.0.1:${port}`,
78
116
  stop: () => {
79
- autorestartProcess.stop()
117
+ stopWatchingBuildServerFiles()
118
+
119
+ reloadableProcess.stop()
80
120
  },
81
121
  }
82
122
  }
83
- signal = autorestartProcess.signal
84
- const logger = createLogger({ logLevel })
123
+ signal = reloadableProcess.signal
85
124
 
86
125
  let buildPromise
87
126
  let buildAbortController
@@ -177,28 +216,39 @@ export const startBuildServer = async ({
177
216
  })
178
217
  logger.info(``)
179
218
 
180
- const unregisterDirectoryLifecyle = watchFiles({
181
- rootDirectoryUrl,
182
- watchedFilePatterns,
219
+ runBuild()
220
+ const clientFileChangeCallback = ({ relativeUrl, event }) => {
221
+ const url = new URL(relativeUrl, rootDirectoryUrl).href
222
+ buildAbortController.abort()
223
+ // setTimeout is to ensure the abortController.abort() above
224
+ // is properly taken into account so that logs about abort comes first
225
+ // then logs about re-running the build happens
226
+ setTimeout(() => {
227
+ logger.info(`${url.slice(rootDirectoryUrl.length)} ${event} -> rebuild`)
228
+ runBuild()
229
+ })
230
+ }
231
+ const stopWatchingClientFiles = registerDirectoryLifecycle(rootDirectoryUrl, {
232
+ watchPatterns: clientFiles,
183
233
  cooldownBetweenFileEvents,
184
- fileChangeCallback: ({ url, event }) => {
185
- buildAbortController.abort()
186
- // setTimeout is to ensure the abortController.abort() above
187
- // is properly taken into account so that logs about abort comes first
188
- // then logs about re-running the build happens
189
- setTimeout(() => {
190
- logger.info(`${url.slice(rootDirectoryUrl.length)} ${event} -> rebuild`)
191
- runBuild()
192
- })
234
+ keepProcessAlive: false,
235
+ recursive: true,
236
+ added: ({ relativeUrl }) => {
237
+ clientFileChangeCallback({ relativeUrl, event: "added" })
238
+ },
239
+ updated: ({ relativeUrl }) => {
240
+ clientFileChangeCallback({ relativeUrl, event: "modified" })
241
+ },
242
+ removed: ({ relativeUrl }) => {
243
+ clientFileChangeCallback({ relativeUrl, event: "removed" })
193
244
  },
194
245
  })
195
- signal.addEventListener("abort", () => {
196
- unregisterDirectoryLifecyle()
197
- })
198
- runBuild()
199
246
  return {
200
247
  origin: server.origin,
201
- stop: () => server.stop(),
248
+ stop: () => {
249
+ stopWatchingClientFiles()
250
+ server.stop()
251
+ },
202
252
  }
203
253
  }
204
254
 
@@ -1,7 +1,10 @@
1
- import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"
1
+ import {
2
+ assertAndNormalizeDirectoryUrl,
3
+ registerDirectoryLifecycle,
4
+ } from "@jsenv/filesystem"
2
5
  import { createLogger } from "@jsenv/logger"
3
6
 
4
- import { initProcessAutorestart } from "@jsenv/utils/file_watcher/process_auto_restart.js"
7
+ import { initReloadableProcess } from "@jsenv/utils/process_reload/process_reload.js"
5
8
  import { createTaskLog } from "@jsenv/utils/logs/task_log.js"
6
9
  import { getCorePlugins } from "@jsenv/core/src/plugins/plugins.js"
7
10
  import { createUrlGraph } from "@jsenv/core/src/omega/url_graph.js"
@@ -14,7 +17,8 @@ import { jsenvPluginToolbar } from "./plugins/toolbar/jsenv_plugin_toolbar.js"
14
17
  export const startDevServer = async ({
15
18
  signal = new AbortController().signal,
16
19
  handleSIGINT,
17
- logLevel,
20
+ logLevel = "info",
21
+ omegaServerLogLevel = "warn",
18
22
  port = 3456,
19
23
  protocol = "http",
20
24
  listenAnyIp,
@@ -25,6 +29,20 @@ export const startDevServer = async ({
25
29
  privateKey,
26
30
  keepProcessAlive = true,
27
31
  rootDirectoryUrl,
32
+ devServerFiles = {
33
+ "./package.json": true,
34
+ "./jsenv.config.mjs": true,
35
+ },
36
+ devServerMainFile,
37
+ devServerAutoreload = false,
38
+ clientFiles = {
39
+ "./**": true,
40
+ "./**/.*/": false, // any folder starting with a dot is ignored (includes .git,.jsenv for instance)
41
+ "./dist/": false,
42
+ "./**/node_modules/": false,
43
+ },
44
+ cooldownBetweenFileEvents,
45
+ clientAutoreload = true,
28
46
 
29
47
  sourcemaps = "inline",
30
48
  plugins = [],
@@ -33,7 +51,6 @@ export const startDevServer = async ({
33
51
  nodeEsmResolution,
34
52
  fileSystemMagicResolution,
35
53
  transpilation,
36
- autoreload = true,
37
54
  explorerGroups = {
38
55
  source: {
39
56
  "./*.html": true,
@@ -44,40 +61,92 @@ export const startDevServer = async ({
44
61
  },
45
62
  },
46
63
  toolbar = false,
47
- autorestart,
48
64
  }) => {
65
+ const logger = createLogger({ logLevel })
49
66
  rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl)
50
- const autorestartProcess = await initProcessAutorestart({
67
+ const reloadableProcess = await initReloadableProcess({
51
68
  signal,
52
69
  handleSIGINT,
53
- ...(autorestart
70
+ ...(devServerAutoreload
54
71
  ? {
55
72
  enabled: true,
56
- logLevel: autorestart.logLevel,
57
- urlToRestart: autorestart.url,
58
- urlsToWatch: [
59
- ...(autorestart.urlsToWatch || []),
60
- new URL("package.json", rootDirectoryUrl),
61
- new URL("jsenv.config.mjs", rootDirectoryUrl),
62
- ],
73
+ logLevel: "warn",
74
+ fileToRestart: devServerMainFile,
63
75
  }
64
76
  : {
65
77
  enabled: false,
66
78
  }),
67
79
  })
68
- if (autorestartProcess.isPrimary) {
80
+ if (reloadableProcess.isPrimary) {
81
+ const devServerFileChangeCallback = ({ relativeUrl, event }) => {
82
+ const url = new URL(relativeUrl, rootDirectoryUrl).href
83
+ if (devServerAutoreload) {
84
+ logger.info(`file ${event} ${url} -> restarting server...`)
85
+ reloadableProcess.reload()
86
+ }
87
+ }
88
+ const unregisterDevServerFilesWatcher = registerDirectoryLifecycle(
89
+ rootDirectoryUrl,
90
+ {
91
+ watchPatterns: {
92
+ [devServerMainFile]: true,
93
+ ...devServerFiles,
94
+ },
95
+ cooldownBetweenFileEvents,
96
+ keepProcessAlive: false,
97
+ recursive: true,
98
+ added: ({ relativeUrl }) => {
99
+ devServerFileChangeCallback({ relativeUrl, event: "added" })
100
+ },
101
+ updated: ({ relativeUrl }) => {
102
+ devServerFileChangeCallback({ relativeUrl, event: "modified" })
103
+ },
104
+ removed: ({ relativeUrl }) => {
105
+ devServerFileChangeCallback({ relativeUrl, event: "removed" })
106
+ },
107
+ },
108
+ )
109
+ signal.addEventListener("abort", () => {
110
+ unregisterDevServerFilesWatcher()
111
+ })
69
112
  return {
70
113
  origin: `${protocol}://127.0.0.1:${port}`,
71
114
  stop: () => {
72
- autorestartProcess.stop()
115
+ unregisterDevServerFilesWatcher()
116
+ reloadableProcess.stop()
73
117
  },
74
118
  }
75
119
  }
76
120
 
77
- const logger = createLogger({ logLevel })
78
121
  const startServerTask = createTaskLog(logger, "start server")
79
122
 
80
- const urlGraph = createUrlGraph()
123
+ const clientFileChangeCallbackList = []
124
+ const clientFilesPruneCallbackList = []
125
+ const clientFileChangeCallback = ({ relativeUrl, event }) => {
126
+ const url = new URL(relativeUrl, rootDirectoryUrl).href
127
+ clientFileChangeCallbackList.forEach((callback) => {
128
+ callback({ url, event })
129
+ })
130
+ }
131
+ const stopWatchingClientFiles = registerDirectoryLifecycle(rootDirectoryUrl, {
132
+ watchPatterns: clientFiles,
133
+ cooldownBetweenFileEvents,
134
+ keepProcessAlive: false,
135
+ recursive: true,
136
+ added: ({ relativeUrl }) => {
137
+ clientFileChangeCallback({ event: "added", relativeUrl })
138
+ },
139
+ updated: ({ relativeUrl }) => {
140
+ clientFileChangeCallback({ event: "modified", relativeUrl })
141
+ },
142
+ removed: ({ relativeUrl }) => {
143
+ clientFileChangeCallback({ event: "removed", relativeUrl })
144
+ },
145
+ })
146
+ const urlGraph = createUrlGraph({
147
+ clientFileChangeCallbackList,
148
+ clientFilesPruneCallbackList,
149
+ })
81
150
  const kitchen = createKitchen({
82
151
  signal,
83
152
  logger,
@@ -97,7 +166,9 @@ export const startDevServer = async ({
97
166
  nodeEsmResolution,
98
167
  fileSystemMagicResolution,
99
168
  transpilation,
100
- autoreload,
169
+ clientAutoreload,
170
+ clientFileChangeCallbackList,
171
+ clientFilesPruneCallbackList,
101
172
  }),
102
173
  jsenvPluginExplorer({
103
174
  groups: explorerGroups,
@@ -106,7 +177,7 @@ export const startDevServer = async ({
106
177
  ],
107
178
  })
108
179
  const server = await startOmegaServer({
109
- logger,
180
+ logLevel: omegaServerLogLevel,
110
181
  keepProcessAlive,
111
182
  listenAnyIp,
112
183
  port,
@@ -132,6 +203,9 @@ export const startDevServer = async ({
132
203
  })
133
204
  return {
134
205
  origin: server.origin,
135
- stop: server.stop,
206
+ stop: () => {
207
+ stopWatchingClientFiles()
208
+ server.stop()
209
+ },
136
210
  }
137
211
  }
@@ -89,10 +89,9 @@ export const execute = async ({
89
89
  }),
90
90
  ],
91
91
  })
92
- const serverLogger = createLogger({ logLevel: "warn" })
93
92
  const server = await startOmegaServer({
94
93
  signal: executeOperation.signal,
95
- logger: serverLogger,
94
+ logLevel: "warn",
96
95
  rootDirectoryUrl,
97
96
  urlGraph,
98
97
  kitchen,
@@ -619,7 +619,7 @@ export const createKitchen = ({
619
619
  },
620
620
  )
621
621
  }
622
- const cook = async ({ urlInfo, outDirectoryUrl, ...rest }) => {
622
+ const cook = memoizeCook(async ({ urlInfo, outDirectoryUrl, ...rest }) => {
623
623
  outDirectoryUrl = outDirectoryUrl ? String(outDirectoryUrl) : undefined
624
624
 
625
625
  const writeFiles = ({ gotError }) => {
@@ -654,7 +654,7 @@ export const createKitchen = ({
654
654
  writeFiles({ gotError: true })
655
655
  throw e
656
656
  }
657
- }
657
+ })
658
658
 
659
659
  baseContext.cook = cook
660
660
 
@@ -684,6 +684,37 @@ export const createKitchen = ({
684
684
  }
685
685
  }
686
686
 
687
+ const memoizeCook = (cook) => {
688
+ const pendingDishes = new Map()
689
+ return async (params) => {
690
+ const { urlInfo } = params
691
+ const { url, modifiedTimestamp } = urlInfo
692
+ const pendingDish = pendingDishes.get(url)
693
+ if (pendingDish) {
694
+ if (!modifiedTimestamp) {
695
+ await pendingDish.promise
696
+ return
697
+ }
698
+ if (pendingDish.timestamp > modifiedTimestamp) {
699
+ await pendingDish.promise
700
+ return
701
+ }
702
+ pendingDishes.delete(url)
703
+ }
704
+ const timestamp = Date.now()
705
+ const promise = cook(params)
706
+ pendingDishes.set(url, {
707
+ timestamp,
708
+ promise,
709
+ })
710
+ try {
711
+ await promise
712
+ } finally {
713
+ pendingDishes.delete(url)
714
+ }
715
+ }
716
+ }
717
+
687
718
  const applyReferenceEffectsOnUrlInfo = (reference, urlInfo, context) => {
688
719
  Object.assign(urlInfo.data, reference.data)
689
720
  Object.assign(urlInfo.timing, reference.timing)
@@ -8,14 +8,13 @@ import {
8
8
  } from "@jsenv/server"
9
9
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js"
10
10
  import { createCallbackListNotifiedOnce } from "@jsenv/abort"
11
- import { loggerToLogLevel } from "@jsenv/logger"
12
11
 
13
12
  import { createFileService } from "./server/file_service.js"
14
13
 
15
14
  export const startOmegaServer = async ({
16
15
  signal,
17
16
  handleSIGINT,
18
- logger,
17
+ logLevel,
19
18
  protocol = "http",
20
19
  http2 = protocol === "https",
21
20
  privateKey,
@@ -48,7 +47,7 @@ export const startOmegaServer = async ({
48
47
  stopOnSIGINT: handleSIGINT,
49
48
  stopOnInternalError: false,
50
49
  keepProcessAlive,
51
- logLevel: loggerToLogLevel(logger),
50
+ logLevel,
52
51
  startLog: false,
53
52
 
54
53
  protocol,
@@ -47,6 +47,15 @@ export const createFileService = ({
47
47
  type: "entry_point",
48
48
  specifier: request.ressource,
49
49
  })
50
+ const ifNoneMatch = request.headers["if-none-match"]
51
+ if (ifNoneMatch && urlInfo.contentEtag === ifNoneMatch) {
52
+ return {
53
+ status: 304,
54
+ headers: {
55
+ "cache-control": `private,max-age=0,must-revalidate`,
56
+ },
57
+ }
58
+ }
50
59
  const referenceFromGraph = urlGraph.inferReference(
51
60
  reference.url,
52
61
  reference.parentUrl,
@@ -60,7 +69,7 @@ export const createFileService = ({
60
69
  [runtimeName]: runtimeVersion,
61
70
  },
62
71
  })
63
- let { response, contentType, content } = urlInfo
72
+ let { response, contentType, content, contentEtag } = urlInfo
64
73
  if (response) {
65
74
  return response
66
75
  }
@@ -71,6 +80,7 @@ export const createFileService = ({
71
80
  "content-type": contentType,
72
81
  "content-length": Buffer.byteLength(content),
73
82
  "cache-control": `private,max-age=0,must-revalidate`,
83
+ "eTag": contentEtag,
74
84
  },
75
85
  body: content,
76
86
  timing: urlInfo.timing,
@@ -1,4 +1,5 @@
1
- import { urlToRelativeUrl } from "@jsenv/filesystem"
1
+ import { bufferToEtag, urlToRelativeUrl } from "@jsenv/filesystem"
2
+
2
3
  import { composeTwoSourcemaps } from "@jsenv/utils/sourcemap/sourcemap_composition_v3.js"
3
4
  import {
4
5
  SOURCEMAP,
@@ -163,6 +164,7 @@ export const createUrlInfoTransformer = ({
163
164
  })
164
165
  }
165
166
  }
167
+ urlInfo.contentEtag = bufferToEtag(Buffer.from(urlInfo.content))
166
168
  }
167
169
 
168
170
  return {
@@ -1,7 +1,9 @@
1
- import { createCallbackList } from "@jsenv/abort"
2
1
  import { urlToRelativeUrl } from "@jsenv/filesystem"
3
2
 
4
- export const createUrlGraph = () => {
3
+ export const createUrlGraph = ({
4
+ clientFileChangeCallbackList,
5
+ clientFilesPruneCallbackList,
6
+ } = {}) => {
5
7
  const urlInfos = {}
6
8
  const getUrlInfo = (url) => urlInfos[url]
7
9
  const deleteUrlInfo = (url) => {
@@ -49,7 +51,6 @@ export const createUrlGraph = () => {
49
51
  return visitDependents(urlInfo)
50
52
  }
51
53
 
52
- const prunedCallbackList = createCallbackList()
53
54
  const updateReferences = (urlInfo, references) => {
54
55
  const dependencyUrls = []
55
56
  references.forEach((reference) => {
@@ -101,7 +102,47 @@ export const createUrlGraph = () => {
101
102
  if (prunedUrlInfos.length === 0) {
102
103
  return
103
104
  }
104
- prunedCallbackList.notify({ prunedUrlInfos, firstUrlInfo })
105
+ prunedUrlInfos.forEach((prunedUrlInfo) => {
106
+ prunedUrlInfo.modifiedTimestamp = Date.now()
107
+ // should we delete?
108
+ // delete urlInfos[prunedUrlInfo.url]
109
+ })
110
+ if (clientFilesPruneCallbackList) {
111
+ clientFilesPruneCallbackList.forEach((callback) => {
112
+ callback({
113
+ firstUrlInfo,
114
+ prunedUrlInfos,
115
+ })
116
+ })
117
+ }
118
+ }
119
+
120
+ if (clientFileChangeCallbackList) {
121
+ const updateModifiedTimestamp = (urlInfo, modifiedTimestamp) => {
122
+ const seen = []
123
+ const iterate = (urlInfo) => {
124
+ if (seen.includes(urlInfo.url)) {
125
+ return
126
+ }
127
+ seen.push(urlInfo.url)
128
+ urlInfo.modifiedTimestamp = modifiedTimestamp
129
+ urlInfo.dependents.forEach((dependentUrl) => {
130
+ const dependentUrlInfo = urlInfos[dependentUrl]
131
+ const { hotAcceptDependencies = [] } = dependentUrlInfo.data
132
+ if (!hotAcceptDependencies.includes(urlInfo.url)) {
133
+ iterate(dependentUrlInfo)
134
+ }
135
+ })
136
+ }
137
+ iterate(urlInfo)
138
+ }
139
+ clientFileChangeCallbackList.push(({ url }) => {
140
+ const urlInfo = urlInfos[url]
141
+ if (urlInfo) {
142
+ updateModifiedTimestamp(urlInfo, Date.now())
143
+ urlInfo.contentEtag = null
144
+ }
145
+ })
105
146
  }
106
147
 
107
148
  return {
@@ -111,8 +152,6 @@ export const createUrlGraph = () => {
111
152
  deleteUrlInfo,
112
153
  inferReference,
113
154
  findDependent,
114
-
115
- prunedCallbackList,
116
155
  updateReferences,
117
156
 
118
157
  toJSON: (rootDirectoryUrl) => {
@@ -133,6 +172,7 @@ export const createUrlGraph = () => {
133
172
 
134
173
  const createUrlInfo = (url) => {
135
174
  return {
175
+ modifiedTimestamp: 0,
136
176
  data: {}, // plugins can put whatever they want here
137
177
  references: [],
138
178
  dependencies: new Set(),
@@ -148,6 +188,7 @@ const createUrlInfo = (url) => {
148
188
  external: false,
149
189
  originalContent: undefined,
150
190
  content: undefined,
191
+ contentEtag: null,
151
192
  sourcemap: null,
152
193
  sourcemapReference: null,
153
194
  timing: {},
@@ -6,10 +6,12 @@ import { createSSEService } from "@jsenv/utils/event_source/sse_service.js"
6
6
  export const jsenvPluginDevSSEServer = ({
7
7
  rootDirectoryUrl,
8
8
  urlGraph,
9
- watchedFilePatterns,
10
- cooldownBetweenFileEvents,
9
+ clientFileChangeCallbackList,
10
+ clientFilesPruneCallbackList,
11
11
  }) => {
12
12
  const serverEventCallbackList = createCallbackList()
13
+ const sseService = createSSEService({ serverEventCallbackList })
14
+
13
15
  const notifyDeclined = ({ cause, reason, declinedBy }) => {
14
16
  serverEventCallbackList.notify({
15
17
  type: "reload",
@@ -32,25 +34,6 @@ export const jsenvPluginDevSSEServer = ({
32
34
  }),
33
35
  })
34
36
  }
35
- const updateHmrTimestamp = (urlInfo, hmrTimestamp) => {
36
- const urlInfos = urlGraph.urlInfos
37
- const seen = []
38
- const iterate = (urlInfo) => {
39
- if (seen.includes(urlInfo.url)) {
40
- return
41
- }
42
- seen.push(urlInfo.url)
43
- urlInfo.data.hmrTimestamp = hmrTimestamp
44
- urlInfo.dependents.forEach((dependentUrl) => {
45
- const dependentUrlInfo = urlInfos[dependentUrl]
46
- const { hotAcceptDependencies = [] } = dependentUrlInfo.data
47
- if (!hotAcceptDependencies.includes(urlInfo.url)) {
48
- iterate(dependentUrlInfo, hmrTimestamp)
49
- }
50
- })
51
- }
52
- iterate(urlInfo)
53
- }
54
37
  const propagateUpdate = (firstUrlInfo) => {
55
38
  const urlInfos = urlGraph.urlInfos
56
39
  const iterate = (urlInfo, trace) => {
@@ -129,41 +112,29 @@ export const jsenvPluginDevSSEServer = ({
129
112
  const trace = []
130
113
  return iterate(firstUrlInfo, trace)
131
114
  }
132
- const sseService = createSSEService({
133
- rootDirectoryUrl,
134
- watchedFilePatterns,
135
- cooldownBetweenFileEvents,
136
- serverEventCallbackList,
137
- onFileChange: ({ url, event }) => {
138
- const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl)
139
- const urlInfo = urlGraph.urlInfos[url]
140
- // file not part of dependency graph
141
- if (!urlInfo) {
142
- return
143
- }
144
- updateHmrTimestamp(urlInfo, Date.now())
145
- const hotUpdate = propagateUpdate(urlInfo)
146
- if (hotUpdate.declined) {
147
- notifyDeclined({
148
- cause: `${relativeUrl} ${event}`,
149
- reason: hotUpdate.reason,
150
- declinedBy: hotUpdate.declinedBy,
151
- })
152
- } else {
153
- notifyAccepted({
154
- cause: `${relativeUrl} ${event}`,
155
- reason: hotUpdate.reason,
156
- instructions: hotUpdate.instructions,
157
- })
158
- }
159
- },
115
+ clientFileChangeCallbackList.push(({ url, event }) => {
116
+ const urlInfo = urlGraph.urlInfos[url]
117
+ // file not part of dependency graph
118
+ if (!urlInfo) {
119
+ return
120
+ }
121
+ const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl)
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
+ }
160
136
  })
161
- urlGraph.prunedCallbackList.add(({ prunedUrlInfos, firstUrlInfo }) => {
162
- prunedUrlInfos.forEach((prunedUrlInfo) => {
163
- prunedUrlInfo.data.hmrTimestamp = Date.now()
164
- // should we delete instead?
165
- // delete urlGraph.urlInfos[prunedUrlInfo.url]
166
- })
137
+ clientFilesPruneCallbackList.push(({ prunedUrlInfos, firstUrlInfo }) => {
167
138
  const mainHotUpdate = propagateUpdate(firstUrlInfo)
168
139
  const cause = `following files are no longer referenced: ${prunedUrlInfos.map(
169
140
  (prunedUrlInfo) => urlToRelativeUrl(prunedUrlInfo.url, rootDirectoryUrl),
@@ -207,7 +178,7 @@ export const jsenvPluginDevSSEServer = ({
207
178
  return {
208
179
  name: "jsenv:sse_server",
209
180
  appliesDuring: { dev: true },
210
- serve: (request, { urlGraph, rootDirectoryUrl }) => {
181
+ serve: (request) => {
211
182
  if (request.ressource === "/__graph__") {
212
183
  const graphJson = JSON.stringify(urlGraph.toJSON(rootDirectoryUrl))
213
184
  return {
@@ -6,8 +6,8 @@ export const jsenvPluginAutoreload = ({
6
6
  rootDirectoryUrl,
7
7
  urlGraph,
8
8
  scenario,
9
- watchedFilePatterns,
10
- cooldownBetweenFileEvents,
9
+ clientFileChangeCallbackList,
10
+ clientFilesPruneCallbackList,
11
11
  }) => {
12
12
  if (scenario === "build") {
13
13
  return []
@@ -20,8 +20,8 @@ export const jsenvPluginAutoreload = ({
20
20
  jsenvPluginDevSSEServer({
21
21
  rootDirectoryUrl,
22
22
  urlGraph,
23
- watchedFilePatterns,
24
- cooldownBetweenFileEvents,
23
+ clientFileChangeCallbackList,
24
+ clientFilesPruneCallbackList,
25
25
  }),
26
26
  ]
27
27
  }
@@ -23,12 +23,12 @@ export const jsenvPluginHmr = () => {
23
23
  return null
24
24
  }
25
25
  const urlInfo = context.urlGraph.getUrlInfo(reference.url)
26
- if (!urlInfo.data.hmrTimestamp) {
26
+ if (!urlInfo.modifiedTimestamp) {
27
27
  return null
28
28
  }
29
29
  return {
30
30
  hmr: "",
31
- v: urlInfo.data.hmrTimestamp,
31
+ v: urlInfo.modifiedTimestamp,
32
32
  }
33
33
  },
34
34
  }
@@ -31,14 +31,13 @@ export const getCorePlugins = ({
31
31
  minification = false,
32
32
  bundling = false,
33
33
 
34
- autoreload = false,
34
+ clientAutoreload = false,
35
+ clientFileChangeCallbackList,
36
+ clientFilesPruneCallbackList,
35
37
  } = {}) => {
36
38
  if (htmlSupervisor === true) {
37
39
  htmlSupervisor = {}
38
40
  }
39
- if (autoreload === true) {
40
- autoreload = {}
41
- }
42
41
  if (nodeEsmResolution === true) {
43
42
  nodeEsmResolution = {}
44
43
  }
@@ -75,13 +74,14 @@ export const getCorePlugins = ({
75
74
  jsenvPluginMinification(minification),
76
75
 
77
76
  jsenvPluginImportMetaHot(),
78
- ...(autoreload
77
+ ...(clientAutoreload
79
78
  ? [
80
79
  jsenvPluginAutoreload({
81
80
  rootDirectoryUrl,
82
81
  urlGraph,
83
82
  scenario,
84
- ...autoreload,
83
+ clientFileChangeCallbackList,
84
+ clientFilesPruneCallbackList,
85
85
  }),
86
86
  ]
87
87
  : []),
@@ -11,11 +11,7 @@ import {
11
11
  urlToMeta,
12
12
  writeFileSync,
13
13
  } from "@jsenv/filesystem"
14
- import {
15
- createLogger,
16
- createDetailedMessage,
17
- loggerToLevels,
18
- } from "@jsenv/logger"
14
+ import { createDetailedMessage, loggerToLevels } from "@jsenv/logger"
19
15
  import { createLog, startSpinner } from "@jsenv/log"
20
16
  import { Abort, raceProcessTeardownEvents } from "@jsenv/abort"
21
17
 
@@ -173,10 +169,9 @@ export const executePlan = async (
173
169
  }),
174
170
  ],
175
171
  })
176
- const serverLogger = createLogger({ logLevel: "warn" })
177
172
  const server = await startOmegaServer({
178
173
  signal: multipleExecutionsOperation.signal,
179
- logger: serverLogger,
174
+ logLevel: "warn",
180
175
  rootDirectoryUrl,
181
176
  urlGraph,
182
177
  kitchen,