@jsenv/filesystem 3.1.0 → 3.2.2
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/README.md +1 -1
- package/package.json +1 -1
- package/src/fileSystemRootUrl.js +1 -1
- package/src/internal/createWatcher.js +1 -1
- package/src/internal/guard_second_call.js +20 -0
- package/src/internal/read_entry_info.js +20 -0
- package/src/registerDirectoryLifecycle.js +201 -151
- package/src/registerFileLifecycle.js +36 -3
- package/src/internal/entryToTypeOrNull.js +0 -18
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ import { registerDirectoryLifecycle } from "@jsenv/filesystem"
|
|
|
64
64
|
|
|
65
65
|
const directoryContentDescription = {}
|
|
66
66
|
const unregister = registerDirectoryLifecycle("file:///directory/", {
|
|
67
|
-
|
|
67
|
+
watchPatterns: {
|
|
68
68
|
"./**/*": true,
|
|
69
69
|
"./node_modules/": false,
|
|
70
70
|
},
|
package/package.json
CHANGED
package/src/fileSystemRootUrl.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const fileSystemRootUrl =
|
|
2
|
-
process.platform === "win32" ? `file
|
|
2
|
+
process.platform === "win32" ? `file:///${process.cwd()[0]}:/` : "file:///"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const guardTooFastSecondCall = (
|
|
2
|
+
callback,
|
|
3
|
+
cooldownBetweenFileEvents = 40,
|
|
4
|
+
) => {
|
|
5
|
+
const previousCallMsMap = new Map()
|
|
6
|
+
return (fileEvent) => {
|
|
7
|
+
const { relativeUrl } = fileEvent
|
|
8
|
+
const previousCallMs = previousCallMsMap.get(relativeUrl)
|
|
9
|
+
const nowMs = Date.now()
|
|
10
|
+
if (previousCallMs) {
|
|
11
|
+
const msEllapsed = nowMs - previousCallMs
|
|
12
|
+
if (msEllapsed < cooldownBetweenFileEvents) {
|
|
13
|
+
previousCallMsMap.delete(relativeUrl)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
previousCallMsMap.set(relativeUrl, nowMs)
|
|
18
|
+
callback(fileEvent)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { statSync } from "node:fs"
|
|
2
|
+
|
|
3
|
+
import { statsToType } from "./statsToType.js"
|
|
4
|
+
|
|
5
|
+
export const readEntryInfo = (url) => {
|
|
6
|
+
try {
|
|
7
|
+
const stats = statSync(new URL(url))
|
|
8
|
+
const type = statsToType(stats)
|
|
9
|
+
return {
|
|
10
|
+
type,
|
|
11
|
+
atimeMs: stats.atimeMs,
|
|
12
|
+
mtimeMs: stats.mtimeMs,
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if (e.code === "ENOENT") {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
throw e
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
import { readdirSync } from "fs"
|
|
1
|
+
import { readdirSync, statSync } from "node:fs"
|
|
3
2
|
import {
|
|
4
3
|
normalizeStructuredMetaMap,
|
|
5
4
|
urlCanContainsMetaMatching,
|
|
6
5
|
urlToMeta,
|
|
7
6
|
} from "@jsenv/url-meta"
|
|
7
|
+
|
|
8
|
+
import { assertAndNormalizeDirectoryUrl } from "./assertAndNormalizeDirectoryUrl.js"
|
|
9
|
+
import { statsToType } from "./internal/statsToType.js"
|
|
10
|
+
import { guardTooFastSecondCall } from "./internal/guard_second_call.js"
|
|
8
11
|
import { replaceBackSlashesWithSlashes } from "./internal/replaceBackSlashesWithSlashes.js"
|
|
9
|
-
import { entryToTypeOrNull } from "./internal/entryToTypeOrNull.js"
|
|
10
12
|
import { createWatcher } from "./internal/createWatcher.js"
|
|
11
13
|
import { trackRessources } from "./internal/trackRessources.js"
|
|
12
|
-
import { ensureUrlTrailingSlash } from "./internal/ensureUrlTrailingSlash.js"
|
|
13
|
-
import { resolveUrl } from "./resolveUrl.js"
|
|
14
|
-
import { assertAndNormalizeFileUrl } from "./assertAndNormalizeFileUrl.js"
|
|
15
14
|
import { urlToFileSystemPath } from "./urlToFileSystemPath.js"
|
|
16
15
|
import { urlToRelativeUrl } from "./urlToRelativeUrl.js"
|
|
17
16
|
|
|
@@ -22,18 +21,25 @@ const fsWatchSupportsRecursive = !isLinux
|
|
|
22
21
|
export const registerDirectoryLifecycle = (
|
|
23
22
|
source,
|
|
24
23
|
{
|
|
24
|
+
debug = false,
|
|
25
25
|
added,
|
|
26
26
|
updated,
|
|
27
27
|
removed,
|
|
28
|
-
|
|
28
|
+
watchPatterns = {
|
|
29
29
|
"./**/*": true,
|
|
30
30
|
},
|
|
31
31
|
notifyExistent = false,
|
|
32
32
|
keepProcessAlive = true,
|
|
33
33
|
recursive = false,
|
|
34
|
+
// filesystem might dispatch more events than expected
|
|
35
|
+
// Code can use "cooldownBetweenFileEvents" to prevent that
|
|
36
|
+
// BUT it is UNADVISED to rely on this as explained later (search for "is lying" in this file)
|
|
37
|
+
// For this reason"cooldownBetweenFileEvents" should be reserved to scenarios
|
|
38
|
+
// like unit tests
|
|
39
|
+
cooldownBetweenFileEvents = 0,
|
|
34
40
|
},
|
|
35
41
|
) => {
|
|
36
|
-
const sourceUrl =
|
|
42
|
+
const sourceUrl = assertAndNormalizeDirectoryUrl(source)
|
|
37
43
|
if (!undefinedOrFunction(added)) {
|
|
38
44
|
throw new TypeError(`added must be a function or undefined, got ${added}`)
|
|
39
45
|
}
|
|
@@ -47,34 +53,70 @@ export const registerDirectoryLifecycle = (
|
|
|
47
53
|
`removed must be a function or undefined, got ${removed}`,
|
|
48
54
|
)
|
|
49
55
|
}
|
|
56
|
+
if (cooldownBetweenFileEvents) {
|
|
57
|
+
if (added) {
|
|
58
|
+
added = guardTooFastSecondCall(added, cooldownBetweenFileEvents)
|
|
59
|
+
}
|
|
60
|
+
if (updated) {
|
|
61
|
+
updated = guardTooFastSecondCall(updated, cooldownBetweenFileEvents)
|
|
62
|
+
}
|
|
63
|
+
if (removed) {
|
|
64
|
+
removed = guardTooFastSecondCall(removed, cooldownBetweenFileEvents)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
50
67
|
|
|
51
68
|
const structuredMetaMap = normalizeStructuredMetaMap(
|
|
52
|
-
{ watch:
|
|
69
|
+
{ watch: watchPatterns },
|
|
53
70
|
sourceUrl,
|
|
54
71
|
)
|
|
55
|
-
const
|
|
56
|
-
const entryUrl = resolveUrl(relativeUrl, sourceUrl)
|
|
57
|
-
|
|
72
|
+
const getWatchPatternValue = ({ url, type }) => {
|
|
58
73
|
if (type === "directory") {
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
let firstMeta = false
|
|
75
|
+
urlCanContainsMetaMatching({
|
|
76
|
+
url: `${url}/`,
|
|
61
77
|
structuredMetaMap,
|
|
62
|
-
predicate: ({ watch }) =>
|
|
78
|
+
predicate: ({ watch }) => {
|
|
79
|
+
if (watch) {
|
|
80
|
+
firstMeta = watch
|
|
81
|
+
}
|
|
82
|
+
return watch
|
|
83
|
+
},
|
|
63
84
|
})
|
|
64
|
-
return
|
|
85
|
+
return firstMeta
|
|
65
86
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
url: entryUrl,
|
|
87
|
+
const filesystemEntryMeta = urlToMeta({
|
|
88
|
+
url,
|
|
69
89
|
structuredMetaMap,
|
|
70
90
|
})
|
|
71
|
-
|
|
72
|
-
return entryMeta.watch
|
|
91
|
+
return filesystemEntryMeta.watch
|
|
73
92
|
}
|
|
74
|
-
|
|
75
93
|
const tracker = trackRessources()
|
|
76
|
-
|
|
77
|
-
const
|
|
94
|
+
const infoMap = new Map()
|
|
95
|
+
const readEntryInfo = (url) => {
|
|
96
|
+
try {
|
|
97
|
+
const relativeUrl = urlToRelativeUrl(url, source)
|
|
98
|
+
const previousInfo = infoMap.get(relativeUrl)
|
|
99
|
+
const stats = statSync(new URL(url))
|
|
100
|
+
const type = statsToType(stats)
|
|
101
|
+
const patternValue = previousInfo
|
|
102
|
+
? previousInfo.patternValue
|
|
103
|
+
: getWatchPatternValue({ url, type })
|
|
104
|
+
return {
|
|
105
|
+
previousInfo,
|
|
106
|
+
url,
|
|
107
|
+
relativeUrl,
|
|
108
|
+
type,
|
|
109
|
+
atimeMs: stats.atimeMs,
|
|
110
|
+
mtimeMs: stats.mtimeMs,
|
|
111
|
+
patternValue,
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e.code === "ENOENT") {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
throw e
|
|
118
|
+
}
|
|
119
|
+
}
|
|
78
120
|
|
|
79
121
|
const handleDirectoryEvent = ({
|
|
80
122
|
directoryRelativeUrl,
|
|
@@ -84,98 +126,103 @@ export const registerDirectoryLifecycle = (
|
|
|
84
126
|
if (filename) {
|
|
85
127
|
if (directoryRelativeUrl) {
|
|
86
128
|
handleChange(`${directoryRelativeUrl}/${filename}`)
|
|
87
|
-
|
|
88
|
-
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
handleChange(`${filename}`)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (eventType === "rename") {
|
|
135
|
+
if (!removed && !added) {
|
|
136
|
+
return
|
|
89
137
|
}
|
|
90
|
-
} else if ((removed || added) && eventType === "rename") {
|
|
91
138
|
// we might receive `rename` without filename
|
|
92
139
|
// in that case we try to find ourselves which file was removed.
|
|
93
|
-
|
|
94
|
-
let relativeUrlCandidateArray = Array.from(contentMap.keys())
|
|
95
|
-
|
|
140
|
+
let relativeUrlCandidateArray = Array.from(infoMap.keys())
|
|
96
141
|
if (recursive && !fsWatchSupportsRecursive) {
|
|
97
142
|
relativeUrlCandidateArray = relativeUrlCandidateArray.filter(
|
|
98
143
|
(relativeUrlCandidate) => {
|
|
99
144
|
if (!directoryRelativeUrl) {
|
|
100
145
|
// ensure entry is top level
|
|
101
|
-
if (relativeUrlCandidate.includes("/"))
|
|
146
|
+
if (relativeUrlCandidate.includes("/")) {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
102
149
|
return true
|
|
103
150
|
}
|
|
104
|
-
|
|
105
151
|
// entry not inside this directory
|
|
106
|
-
if (!relativeUrlCandidate.startsWith(directoryRelativeUrl))
|
|
152
|
+
if (!relativeUrlCandidate.startsWith(directoryRelativeUrl)) {
|
|
107
153
|
return false
|
|
108
|
-
|
|
154
|
+
}
|
|
109
155
|
const afterDirectory = relativeUrlCandidate.slice(
|
|
110
156
|
directoryRelativeUrl.length + 1,
|
|
111
157
|
)
|
|
112
158
|
// deep inside this directory
|
|
113
|
-
if (afterDirectory.includes("/"))
|
|
114
|
-
|
|
159
|
+
if (afterDirectory.includes("/")) {
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
115
162
|
return true
|
|
116
163
|
},
|
|
117
164
|
)
|
|
118
165
|
}
|
|
119
|
-
|
|
120
166
|
const removedEntryRelativeUrl = relativeUrlCandidateArray.find(
|
|
121
167
|
(relativeUrlCandidate) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
try {
|
|
169
|
+
statSync(new URL(relativeUrlCandidate, sourceUrl))
|
|
170
|
+
return false
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (e.code === "ENOENT") {
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
throw e
|
|
176
|
+
}
|
|
125
177
|
},
|
|
126
178
|
)
|
|
127
|
-
|
|
128
179
|
if (removedEntryRelativeUrl) {
|
|
129
|
-
handleEntryLost(
|
|
130
|
-
relativeUrl: removedEntryRelativeUrl,
|
|
131
|
-
type: contentMap.get(removedEntryRelativeUrl),
|
|
132
|
-
})
|
|
180
|
+
handleEntryLost(infoMap.get(removedEntryRelativeUrl))
|
|
133
181
|
}
|
|
134
182
|
}
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
const handleChange = (relativeUrl) => {
|
|
138
|
-
const entryUrl =
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
const entryUrl = new URL(relativeUrl, sourceUrl).href
|
|
187
|
+
const entryInfo = readEntryInfo(entryUrl)
|
|
188
|
+
if (!entryInfo) {
|
|
189
|
+
const previousEntryInfo = infoMap.get(relativeUrl)
|
|
190
|
+
if (!previousEntryInfo) {
|
|
191
|
+
// on MacOS it's possible to receive a "rename" event for
|
|
192
|
+
// a file that does not exists...
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
if (debug) {
|
|
196
|
+
console.debug(`"${relativeUrl}" removed`)
|
|
197
|
+
}
|
|
198
|
+
handleEntryLost(previousEntryInfo)
|
|
143
199
|
return
|
|
144
200
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
handleEntryFound({ relativeUrl, type, existent: false })
|
|
201
|
+
const { previousInfo } = entryInfo
|
|
202
|
+
if (!previousInfo) {
|
|
203
|
+
if (debug) {
|
|
204
|
+
console.debug(`"${relativeUrl}" added`)
|
|
150
205
|
}
|
|
206
|
+
handleEntryFound(entryInfo)
|
|
151
207
|
return
|
|
152
208
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
if (entryInfo.type !== previousInfo.type) {
|
|
210
|
+
// it existed and was replaced by something else
|
|
211
|
+
// we don't handle this as an update. We rather say the ressource
|
|
212
|
+
// is lost and something else is found (call removed() then added())
|
|
213
|
+
handleEntryLost(previousInfo)
|
|
214
|
+
handleEntryFound(entryInfo)
|
|
157
215
|
return
|
|
158
216
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
handleEntryLost({ relativeUrl, type: previousType })
|
|
165
|
-
handleEntryFound({ relativeUrl, type })
|
|
217
|
+
if (entryInfo.type === "directory") {
|
|
218
|
+
// a directory cannot really be updated in way that matters for us
|
|
219
|
+
// filesystem is trying to tell us the directory content have changed
|
|
220
|
+
// but we don't care about that
|
|
221
|
+
// we'll already be notified about what has changed
|
|
166
222
|
return
|
|
167
223
|
}
|
|
168
|
-
|
|
169
|
-
// a directory cannot really be updated in way that matters for us
|
|
170
|
-
// filesystem is trying to tell us the directory content have changed
|
|
171
|
-
// but we don't care about that
|
|
172
|
-
// we'll already be notified about what has changed
|
|
173
|
-
if (type === "directory") {
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
|
|
177
224
|
// something has changed at this relativeUrl (the file existed and was not deleted)
|
|
178
|
-
// it's possible to get there
|
|
225
|
+
// it's possible to get there without a real update
|
|
179
226
|
// (file content is the same and file mtime is the same).
|
|
180
227
|
// In short filesystem is sometimes "lying"
|
|
181
228
|
// Not trying to guard against that because:
|
|
@@ -183,76 +230,95 @@ export const registerDirectoryLifecycle = (
|
|
|
183
230
|
// - it happens very rarely
|
|
184
231
|
// - it's not really a concern in practice
|
|
185
232
|
// - filesystem did not send an event out of nowhere:
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
233
|
+
// something occured but we don't know exactly what
|
|
234
|
+
// maybe we should exclude some stuff as done in
|
|
235
|
+
// https://github.com/paulmillr/chokidar/blob/b2c4f249b6cfa98c703f0066fb4a56ccd83128b5/lib/nodefs-handler.js#L366
|
|
236
|
+
if (debug) {
|
|
237
|
+
console.debug(`"${relativeUrl}" modified`)
|
|
189
238
|
}
|
|
239
|
+
handleEntryUpdated(entryInfo)
|
|
190
240
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (type === "directory") {
|
|
202
|
-
visitDirectory({
|
|
203
|
-
directoryUrl: `${entryUrl}/`,
|
|
204
|
-
entryFound: (entry) => {
|
|
205
|
-
handleEntryFound({
|
|
206
|
-
relativeUrl: `${relativeUrl}/${entry.relativeUrl}`,
|
|
207
|
-
type: entry.type,
|
|
208
|
-
existent,
|
|
209
|
-
})
|
|
210
|
-
},
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (added) {
|
|
215
|
-
if (existent) {
|
|
216
|
-
if (notifyExistent) {
|
|
217
|
-
added({ relativeUrl, type, existent: true })
|
|
241
|
+
const handleEntryFound = (entryInfo, { notify = true } = {}) => {
|
|
242
|
+
infoMap.set(entryInfo.relativeUrl, entryInfo)
|
|
243
|
+
if (entryInfo.type === "directory") {
|
|
244
|
+
const directoryUrl = `${entryInfo.url}/`
|
|
245
|
+
readdirSync(new URL(directoryUrl)).forEach((entryName) => {
|
|
246
|
+
const childEntryUrl = new URL(entryName, directoryUrl).href
|
|
247
|
+
const childEntryInfo = readEntryInfo(childEntryUrl)
|
|
248
|
+
if (childEntryInfo && childEntryInfo.patternValue) {
|
|
249
|
+
handleEntryFound(childEntryInfo, { notify })
|
|
218
250
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
251
|
+
})
|
|
252
|
+
// we must watch manually every directory we find
|
|
253
|
+
if (!fsWatchSupportsRecursive) {
|
|
254
|
+
const watcher = createWatcher(urlToFileSystemPath(entryInfo.url), {
|
|
255
|
+
persistent: keepProcessAlive,
|
|
256
|
+
})
|
|
257
|
+
tracker.registerCleanupCallback(() => {
|
|
258
|
+
watcher.close()
|
|
259
|
+
})
|
|
260
|
+
watcher.on("change", (eventType, filename) => {
|
|
261
|
+
handleDirectoryEvent({
|
|
262
|
+
directoryRelativeUrl: entryInfo.relativeUrl,
|
|
263
|
+
filename: filename ? replaceBackSlashesWithSlashes(filename) : "",
|
|
264
|
+
eventType,
|
|
265
|
+
})
|
|
266
|
+
})
|
|
221
267
|
}
|
|
222
268
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
tracker.registerCleanupCallback(() => {
|
|
230
|
-
watcher.close()
|
|
269
|
+
if (added && entryInfo.patternValue && notify) {
|
|
270
|
+
added({
|
|
271
|
+
relativeUrl: entryInfo.relativeUrl,
|
|
272
|
+
type: entryInfo.type,
|
|
273
|
+
patternValue: entryInfo.patternValue,
|
|
274
|
+
mtime: entryInfo.mtimeMs,
|
|
231
275
|
})
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const handleEntryLost = (entryInfo) => {
|
|
279
|
+
infoMap.delete(entryInfo.relativeUrl)
|
|
280
|
+
if (removed && entryInfo.patternValue) {
|
|
281
|
+
removed({
|
|
282
|
+
relativeUrl: entryInfo.relativeUrl,
|
|
283
|
+
type: entryInfo.type,
|
|
284
|
+
patternValue: entryInfo.patternValue,
|
|
285
|
+
mtime: entryInfo.mtimeMs,
|
|
238
286
|
})
|
|
239
287
|
}
|
|
240
288
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
289
|
+
const handleEntryUpdated = (entryInfo) => {
|
|
290
|
+
infoMap.set(entryInfo.relativeUrl, entryInfo)
|
|
291
|
+
if (updated && entryInfo.patternValue) {
|
|
292
|
+
updated({
|
|
293
|
+
relativeUrl: entryInfo.relativeUrl,
|
|
294
|
+
type: entryInfo.type,
|
|
295
|
+
patternValue: entryInfo.patternValue,
|
|
296
|
+
mtime: entryInfo.mtimeMs,
|
|
297
|
+
previousMtime: entryInfo.previousInfo.mtimeMs,
|
|
298
|
+
})
|
|
246
299
|
}
|
|
247
300
|
}
|
|
248
301
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
302
|
+
readdirSync(new URL(sourceUrl)).forEach((entry) => {
|
|
303
|
+
const entryUrl = new URL(entry, sourceUrl).href
|
|
304
|
+
const entryInfo = readEntryInfo(entryUrl)
|
|
305
|
+
if (entryInfo && entryInfo.patternValue) {
|
|
306
|
+
handleEntryFound(entryInfo, {
|
|
307
|
+
notify: notifyExistent,
|
|
308
|
+
})
|
|
309
|
+
}
|
|
254
310
|
})
|
|
255
|
-
|
|
311
|
+
if (debug) {
|
|
312
|
+
const relativeUrls = Array.from(infoMap.keys())
|
|
313
|
+
if (relativeUrls.length === 0) {
|
|
314
|
+
console.debug(`No file found`)
|
|
315
|
+
} else {
|
|
316
|
+
console.debug(
|
|
317
|
+
`${relativeUrls.length} file found:
|
|
318
|
+
${relativeUrls.join("\n")}`,
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
256
322
|
const watcher = createWatcher(urlToFileSystemPath(sourceUrl), {
|
|
257
323
|
recursive: recursive && fsWatchSupportsRecursive,
|
|
258
324
|
persistent: keepProcessAlive,
|
|
@@ -270,24 +336,8 @@ export const registerDirectoryLifecycle = (
|
|
|
270
336
|
return tracker.cleanup
|
|
271
337
|
}
|
|
272
338
|
|
|
273
|
-
const undefinedOrFunction = (value) =>
|
|
274
|
-
typeof value === "undefined" || typeof value === "function"
|
|
275
|
-
|
|
276
|
-
const visitDirectory = ({ directoryUrl, entryFound }) => {
|
|
277
|
-
const directoryPath = urlToFileSystemPath(directoryUrl)
|
|
278
|
-
readdirSync(directoryPath).forEach((entry) => {
|
|
279
|
-
const entryUrl = resolveUrl(entry, directoryUrl)
|
|
280
|
-
const type = entryToTypeOrNull(entryUrl)
|
|
281
|
-
if (type === null) {
|
|
282
|
-
return
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const relativeUrl = urlToRelativeUrl(entryUrl, directoryUrl)
|
|
286
|
-
entryFound({
|
|
287
|
-
relativeUrl,
|
|
288
|
-
type,
|
|
289
|
-
})
|
|
290
|
-
})
|
|
339
|
+
const undefinedOrFunction = (value) => {
|
|
340
|
+
return typeof value === "undefined" || typeof value === "function"
|
|
291
341
|
}
|
|
292
342
|
|
|
293
343
|
const fileSystemPathToDirectoryRelativeUrlAndFilename = (path) => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { statSync } from "node:fs"
|
|
2
|
+
import { dirname, basename } from "node:path"
|
|
3
|
+
|
|
4
|
+
import { guardTooFastSecondCall } from "./internal/guard_second_call.js"
|
|
5
|
+
import { statsToType } from "./internal/statsToType.js"
|
|
3
6
|
import { createWatcher } from "./internal/createWatcher.js"
|
|
4
7
|
import { trackRessources } from "./internal/trackRessources.js"
|
|
5
8
|
import { assertAndNormalizeFileUrl } from "./assertAndNormalizeFileUrl.js"
|
|
@@ -7,7 +10,14 @@ import { urlToFileSystemPath } from "./urlToFileSystemPath.js"
|
|
|
7
10
|
|
|
8
11
|
export const registerFileLifecycle = (
|
|
9
12
|
source,
|
|
10
|
-
{
|
|
13
|
+
{
|
|
14
|
+
added,
|
|
15
|
+
updated,
|
|
16
|
+
removed,
|
|
17
|
+
notifyExistent = false,
|
|
18
|
+
keepProcessAlive = true,
|
|
19
|
+
cooldownBetweenFileEvents = 0,
|
|
20
|
+
},
|
|
11
21
|
) => {
|
|
12
22
|
const sourceUrl = assertAndNormalizeFileUrl(source)
|
|
13
23
|
if (!undefinedOrFunction(added)) {
|
|
@@ -23,6 +33,17 @@ export const registerFileLifecycle = (
|
|
|
23
33
|
`removed must be a function or undefined, got ${removed}`,
|
|
24
34
|
)
|
|
25
35
|
}
|
|
36
|
+
if (cooldownBetweenFileEvents) {
|
|
37
|
+
if (added) {
|
|
38
|
+
added = guardTooFastSecondCall(added, cooldownBetweenFileEvents)
|
|
39
|
+
}
|
|
40
|
+
if (updated) {
|
|
41
|
+
updated = guardTooFastSecondCall(updated, cooldownBetweenFileEvents)
|
|
42
|
+
}
|
|
43
|
+
if (removed) {
|
|
44
|
+
removed = guardTooFastSecondCall(removed, cooldownBetweenFileEvents)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
26
47
|
|
|
27
48
|
const tracker = trackRessources()
|
|
28
49
|
|
|
@@ -89,6 +110,18 @@ export const registerFileLifecycle = (
|
|
|
89
110
|
return tracker.cleanup
|
|
90
111
|
}
|
|
91
112
|
|
|
113
|
+
const entryToTypeOrNull = (url) => {
|
|
114
|
+
try {
|
|
115
|
+
const stats = statSync(new URL(url))
|
|
116
|
+
return statsToType(stats)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
if (e.code === "ENOENT") {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
throw e
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
const undefinedOrFunction = (value) =>
|
|
93
126
|
typeof value === "undefined" || typeof value === "function"
|
|
94
127
|
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { statSync } from "node:fs"
|
|
2
|
-
|
|
3
|
-
import { urlToFileSystemPath } from "../urlToFileSystemPath.js"
|
|
4
|
-
|
|
5
|
-
import { statsToType } from "./statsToType.js"
|
|
6
|
-
|
|
7
|
-
export const entryToTypeOrNull = (url) => {
|
|
8
|
-
const path = urlToFileSystemPath(url)
|
|
9
|
-
try {
|
|
10
|
-
const stats = statSync(path)
|
|
11
|
-
return statsToType(stats)
|
|
12
|
-
} catch (e) {
|
|
13
|
-
if (e.code === "ENOENT") {
|
|
14
|
-
return null
|
|
15
|
-
}
|
|
16
|
-
throw e
|
|
17
|
-
}
|
|
18
|
-
}
|