@jsenv/snapshot 2.6.5 → 2.6.7
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 +7 -5
- package/src/filesystem_well_known_values.js +18 -26
- package/src/get_caller_location.js +22 -0
- package/src/main.js +4 -1
- package/src/replace_fluctuating_values.js +1 -1
- package/src/side_effects/capture_side_effects.js +6 -0
- package/src/side_effects/create_capture_side_effects.js +305 -0
- package/src/side_effects/filesystem/filesystem_side_effects.js +302 -0
- package/src/side_effects/filesystem/group_file_side_effects_per_directory.js +30 -0
- package/src/{function_side_effects → side_effects/filesystem}/spy_filesystem_calls.js +48 -25
- package/src/side_effects/log/group_log_side_effects.js +29 -0
- package/src/side_effects/log/log_side_effects.js +156 -0
- package/src/side_effects/render_logs_gif.js +18 -0
- package/src/side_effects/render_side_effects.js +435 -0
- package/src/side_effects/snapshot_side_effects.js +43 -0
- package/src/side_effects/snapshot_tests.js +115 -0
- package/src/side_effects/utils/group_side_effects.js +89 -0
- package/src/function_side_effects/function_side_effects_collector.js +0 -160
- package/src/function_side_effects/function_side_effects_renderer.js +0 -29
- package/src/function_side_effects/function_side_effects_snapshot.js +0 -302
- package/src/function_side_effects/group_file_side_effects_per_directory.js +0 -114
- package/src/function_side_effects/spy_console_calls.js +0 -89
- /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.js +0 -0
- /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.test.mjs +0 -0
- /package/src/{function_side_effects → side_effects}/hook_into_method.js +0 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { setUrlBasename, urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
|
|
2
|
+
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { createWellKnown } from "../../filesystem_well_known_values.js";
|
|
5
|
+
import { renderFileContent } from "../render_side_effects.js";
|
|
6
|
+
import { groupFileSideEffectsPerDirectory } from "./group_file_side_effects_per_directory.js";
|
|
7
|
+
import { spyFilesystemCalls } from "./spy_filesystem_calls.js";
|
|
8
|
+
|
|
9
|
+
const filesystemSideEffectsOptionsDefault = {
|
|
10
|
+
include: null,
|
|
11
|
+
preserve: false,
|
|
12
|
+
baseDirectory: "",
|
|
13
|
+
textualFilesIntoDirectory: false,
|
|
14
|
+
};
|
|
15
|
+
const INLINE_MAX_LINES = 20;
|
|
16
|
+
const INLINE_MAX_LENGTH = 2000;
|
|
17
|
+
|
|
18
|
+
export const filesystemSideEffects = (
|
|
19
|
+
filesystemSideEffectsOptions,
|
|
20
|
+
{ replaceFilesystemWellKnownValues },
|
|
21
|
+
) => {
|
|
22
|
+
filesystemSideEffectsOptions = {
|
|
23
|
+
...filesystemSideEffectsOptionsDefault,
|
|
24
|
+
...filesystemSideEffectsOptions,
|
|
25
|
+
};
|
|
26
|
+
let baseDirectory;
|
|
27
|
+
let removeBaseDirectoryWellKnown = () => {};
|
|
28
|
+
const setBaseDirectory = (value) => {
|
|
29
|
+
removeBaseDirectoryWellKnown();
|
|
30
|
+
baseDirectory = value;
|
|
31
|
+
if (baseDirectory) {
|
|
32
|
+
removeBaseDirectoryWellKnown =
|
|
33
|
+
replaceFilesystemWellKnownValues.addWellKnownFileUrl(
|
|
34
|
+
baseDirectory,
|
|
35
|
+
createWellKnown("base"),
|
|
36
|
+
{ position: "start" },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: "filesystem",
|
|
43
|
+
setBaseDirectory,
|
|
44
|
+
install: (addSideEffect, { addSkippableHandler, addFinallyCallback }) => {
|
|
45
|
+
let { include, preserve, textualFilesIntoDirectory } =
|
|
46
|
+
filesystemSideEffectsOptions;
|
|
47
|
+
if (filesystemSideEffectsOptions.baseDirectory) {
|
|
48
|
+
setBaseDirectory(filesystemSideEffectsOptions.baseDirectory);
|
|
49
|
+
}
|
|
50
|
+
const getUrlRelativeToBase = (url) => {
|
|
51
|
+
if (baseDirectory) {
|
|
52
|
+
return urlToRelativeUrl(url, baseDirectory, {
|
|
53
|
+
preferRelativeNotation: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return url;
|
|
57
|
+
};
|
|
58
|
+
const getUrlInsideOutDirectory = (url, outDirectoryUrl) => {
|
|
59
|
+
if (baseDirectory) {
|
|
60
|
+
if (
|
|
61
|
+
url.href === baseDirectory.href ||
|
|
62
|
+
urlIsInsideOf(url, baseDirectory)
|
|
63
|
+
) {
|
|
64
|
+
const outRelativeUrl = urlToRelativeUrl(url, baseDirectory);
|
|
65
|
+
return new URL(outRelativeUrl, outDirectoryUrl);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// otherwise we replace the url with well known
|
|
69
|
+
const toRelativeUrl = replaceFilesystemWellKnownValues(url);
|
|
70
|
+
return new URL(toRelativeUrl, outDirectoryUrl);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
addSkippableHandler((sideEffect) => {
|
|
74
|
+
// if directory ends up with something inside we'll not report
|
|
75
|
+
// this side effect because:
|
|
76
|
+
// - it was likely created to write the file
|
|
77
|
+
// - the file creation will be reported and implies directory creation
|
|
78
|
+
if (sideEffect.code === "write_directory") {
|
|
79
|
+
return (nextSideEffect, { skip, stop }) => {
|
|
80
|
+
if (
|
|
81
|
+
(nextSideEffect.code === "write_file" ||
|
|
82
|
+
nextSideEffect.code === "write_directory") &&
|
|
83
|
+
urlIsInsideOf(nextSideEffect.value.url, sideEffect.value.url)
|
|
84
|
+
) {
|
|
85
|
+
skip();
|
|
86
|
+
stop();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
nextSideEffect.code === "remove_directory" &&
|
|
91
|
+
nextSideEffect.value.url === sideEffect.value.url
|
|
92
|
+
) {
|
|
93
|
+
stop();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
addFinallyCallback((sideEffects) => {
|
|
102
|
+
// gather all file side effect next to each other
|
|
103
|
+
// collapse them if they have a shared ancestor
|
|
104
|
+
groupFileSideEffectsPerDirectory(sideEffects, {
|
|
105
|
+
createWriteFileGroupSideEffect: (fileSideEffectArray, commonPath) => {
|
|
106
|
+
let commonUrl = pathToFileURL(commonPath);
|
|
107
|
+
let commonDirectoryUrl;
|
|
108
|
+
if (commonUrl.href.endsWith("/")) {
|
|
109
|
+
commonDirectoryUrl = commonUrl;
|
|
110
|
+
} else {
|
|
111
|
+
commonDirectoryUrl = new URL("./", commonUrl);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
code: "write_file_group",
|
|
116
|
+
type: `write_file_group ${commonDirectoryUrl}`,
|
|
117
|
+
value: {},
|
|
118
|
+
render: {
|
|
119
|
+
md: (options) => {
|
|
120
|
+
let allFilesInsideOutDirectory = true;
|
|
121
|
+
let groupMd = "";
|
|
122
|
+
let numberOfFiles = 0;
|
|
123
|
+
for (const fileSideEffect of fileSideEffectArray) {
|
|
124
|
+
numberOfFiles++;
|
|
125
|
+
if (groupMd) {
|
|
126
|
+
groupMd += "\n\n";
|
|
127
|
+
}
|
|
128
|
+
const { url, outDirectoryReason } = fileSideEffect.value;
|
|
129
|
+
const { text } = fileSideEffect.render.md(options);
|
|
130
|
+
const urlRelativeToCommonDirectory = urlToRelativeUrl(
|
|
131
|
+
url,
|
|
132
|
+
commonDirectoryUrl,
|
|
133
|
+
);
|
|
134
|
+
if (outDirectoryReason) {
|
|
135
|
+
const outUrlRelativeToCommonDirectory = urlToRelativeUrl(
|
|
136
|
+
text.urlInsideOutDirectory,
|
|
137
|
+
options.sideEffectFileUrl,
|
|
138
|
+
);
|
|
139
|
+
groupMd += `${"#".repeat(2)} ${urlRelativeToCommonDirectory}
|
|
140
|
+
${renderFileContent(
|
|
141
|
+
{
|
|
142
|
+
...text,
|
|
143
|
+
relativeUrl: urlRelativeToCommonDirectory,
|
|
144
|
+
outRelativeUrl: outUrlRelativeToCommonDirectory,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
...options,
|
|
148
|
+
sideEffect: fileSideEffect,
|
|
149
|
+
},
|
|
150
|
+
)}`;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
allFilesInsideOutDirectory = false;
|
|
154
|
+
groupMd += `${"#".repeat(2)} ${urlRelativeToCommonDirectory}
|
|
155
|
+
${renderFileContent(
|
|
156
|
+
{
|
|
157
|
+
...text,
|
|
158
|
+
relativeUrl: urlRelativeToCommonDirectory,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
...options,
|
|
162
|
+
sideEffect: fileSideEffect,
|
|
163
|
+
},
|
|
164
|
+
)}`;
|
|
165
|
+
}
|
|
166
|
+
const commonDirectoryRelativeUrl =
|
|
167
|
+
getUrlRelativeToBase(commonDirectoryUrl);
|
|
168
|
+
if (allFilesInsideOutDirectory) {
|
|
169
|
+
const commonDirectoryOutUrl = getUrlInsideOutDirectory(
|
|
170
|
+
commonDirectoryUrl,
|
|
171
|
+
options.outDirectoryUrl,
|
|
172
|
+
);
|
|
173
|
+
const commonDirectoryOutRelativeUrl = urlToRelativeUrl(
|
|
174
|
+
commonDirectoryOutUrl,
|
|
175
|
+
options.sideEffectFileUrl,
|
|
176
|
+
{ preferRelativeNotation: true },
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
label: `write ${numberOfFiles} files into "${commonDirectoryRelativeUrl}"`,
|
|
180
|
+
text: `see [${commonDirectoryOutRelativeUrl}](${commonDirectoryOutRelativeUrl})`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
label: `write ${numberOfFiles} files into "${commonDirectoryRelativeUrl}"`,
|
|
185
|
+
text: groupMd,
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const filesystemSpy = spyFilesystemCalls(
|
|
195
|
+
{
|
|
196
|
+
onWriteFile: (url, buffer) => {
|
|
197
|
+
const contentType = CONTENT_TYPE.fromUrlExtension(url);
|
|
198
|
+
const isTextual = CONTENT_TYPE.isTextual(contentType);
|
|
199
|
+
let outDirectoryReason;
|
|
200
|
+
if (isTextual) {
|
|
201
|
+
if (textualFilesIntoDirectory) {
|
|
202
|
+
outDirectoryReason = "textual_in_directory_option";
|
|
203
|
+
} else if (String(buffer).split("\n").length > INLINE_MAX_LINES) {
|
|
204
|
+
outDirectoryReason = "lot_of_lines";
|
|
205
|
+
} else if (buffer.size > INLINE_MAX_LENGTH) {
|
|
206
|
+
outDirectoryReason = "lot_of_chars";
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
outDirectoryReason = "binary";
|
|
210
|
+
}
|
|
211
|
+
const writeFileSideEffect = {
|
|
212
|
+
code: "write_file",
|
|
213
|
+
type: `write_file:${url}`,
|
|
214
|
+
value: {
|
|
215
|
+
url,
|
|
216
|
+
buffer,
|
|
217
|
+
contentType,
|
|
218
|
+
isTextual,
|
|
219
|
+
outDirectoryReason,
|
|
220
|
+
},
|
|
221
|
+
render: {
|
|
222
|
+
md: ({ sideEffectFileUrl, outDirectoryUrl }) => {
|
|
223
|
+
const urlRelativeToBase = getUrlRelativeToBase(url);
|
|
224
|
+
if (outDirectoryReason) {
|
|
225
|
+
const urlInsideOutDirectory = getUrlInsideOutDirectory(
|
|
226
|
+
url,
|
|
227
|
+
outDirectoryUrl,
|
|
228
|
+
);
|
|
229
|
+
if (writeFileSideEffect.counter) {
|
|
230
|
+
setUrlBasename(
|
|
231
|
+
urlInsideOutDirectory,
|
|
232
|
+
(basename) =>
|
|
233
|
+
`${basename}_${writeFileSideEffect.counter}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
let textValue;
|
|
237
|
+
if (outDirectoryReason === "lot_of_chars") {
|
|
238
|
+
textValue = String(buffer.slice(0, INLINE_MAX_LENGTH));
|
|
239
|
+
} else if (outDirectoryReason === "lot_of_lines") {
|
|
240
|
+
textValue = String(buffer)
|
|
241
|
+
.split("\n")
|
|
242
|
+
.slice(0, INLINE_MAX_LINES)
|
|
243
|
+
.join("\n");
|
|
244
|
+
} else {
|
|
245
|
+
textValue = buffer;
|
|
246
|
+
}
|
|
247
|
+
const outRelativeUrl = urlToRelativeUrl(
|
|
248
|
+
urlInsideOutDirectory,
|
|
249
|
+
sideEffectFileUrl,
|
|
250
|
+
{
|
|
251
|
+
preferRelativeNotation: true,
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
return {
|
|
255
|
+
label: `write file "${urlRelativeToBase}"`,
|
|
256
|
+
text: {
|
|
257
|
+
type: "file_content",
|
|
258
|
+
value: textValue,
|
|
259
|
+
relativeUrl: urlRelativeToBase,
|
|
260
|
+
outRelativeUrl,
|
|
261
|
+
urlInsideOutDirectory,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
label: `write file "${urlRelativeToBase}"`,
|
|
267
|
+
text: {
|
|
268
|
+
type: "file_content",
|
|
269
|
+
value: String(buffer),
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
addSideEffect(writeFileSideEffect);
|
|
276
|
+
},
|
|
277
|
+
onWriteDirectory: (url) => {
|
|
278
|
+
addSideEffect({
|
|
279
|
+
code: "write_directory",
|
|
280
|
+
type: `write_directory:${url}`,
|
|
281
|
+
value: { url },
|
|
282
|
+
render: {
|
|
283
|
+
md: () => {
|
|
284
|
+
return {
|
|
285
|
+
label: `write directory "${getUrlRelativeToBase(url)}"`,
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
include,
|
|
294
|
+
undoFilesystemSideEffects: !preserve,
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
return () => {
|
|
298
|
+
filesystemSpy.restore();
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { groupSideEffectsPer, IGNORE } from "../utils/group_side_effects.js";
|
|
2
|
+
import { findCommonAncestorPath } from "./common_ancestor_path.js";
|
|
3
|
+
|
|
4
|
+
export const groupFileSideEffectsPerDirectory = (
|
|
5
|
+
allSideEffects,
|
|
6
|
+
{ createWriteFileGroupSideEffect },
|
|
7
|
+
) => {
|
|
8
|
+
return groupSideEffectsPer(
|
|
9
|
+
allSideEffects,
|
|
10
|
+
(sideEffect) => {
|
|
11
|
+
if (sideEffect.code === "write_directory") {
|
|
12
|
+
return IGNORE;
|
|
13
|
+
}
|
|
14
|
+
return sideEffect.code === "write_file";
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
createGroupSideEffect: (sideEffects) => {
|
|
18
|
+
const commonAncestorPath = findCommonAncestorPath(
|
|
19
|
+
sideEffects,
|
|
20
|
+
convertToPathname,
|
|
21
|
+
);
|
|
22
|
+
return createWriteFileGroupSideEffect(sideEffects, commonAncestorPath);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const convertToPathname = (writeFileSideEffect) => {
|
|
29
|
+
return new URL(writeFileSideEffect.value.url).pathname;
|
|
30
|
+
};
|
|
@@ -9,20 +9,21 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "@jsenv/filesystem";
|
|
11
11
|
import { URL_META } from "@jsenv/url-meta";
|
|
12
|
+
import { yieldAncestorUrls } from "@jsenv/urls";
|
|
12
13
|
import { readFileSync, statSync } from "node:fs";
|
|
13
14
|
import { pathToFileURL } from "node:url";
|
|
14
15
|
import {
|
|
15
16
|
disableHooksWhileCalling,
|
|
16
17
|
hookIntoMethod,
|
|
17
18
|
METHOD_EXECUTION_NODE_CALLBACK,
|
|
18
|
-
} from "
|
|
19
|
+
} from "../hook_into_method.js";
|
|
19
20
|
|
|
20
21
|
export const spyFilesystemCalls = (
|
|
21
22
|
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
onReadFile = () => {}, // TODO
|
|
24
|
+
onWriteFile = () => {},
|
|
25
|
+
onWriteDirectory = () => {},
|
|
26
|
+
onRemoveFile = () => {}, // TODO
|
|
26
27
|
// removeDirectory = () => {},
|
|
27
28
|
},
|
|
28
29
|
{ include, undoFilesystemSideEffects } = {},
|
|
@@ -40,15 +41,19 @@ export const spyFilesystemCalls = (
|
|
|
40
41
|
// - writing file for the 1st time
|
|
41
42
|
// - updating file content
|
|
42
43
|
// the important part is the file content in the end of the function execution
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
const reason =
|
|
45
|
+
!stateBefore.found && stateAfter.found
|
|
46
|
+
? "created"
|
|
47
|
+
: Buffer.compare(stateBefore.buffer, stateAfter.buffer)
|
|
48
|
+
? "content_modified"
|
|
49
|
+
: stateBefore.mtimeMs === stateAfter.mtimeMs
|
|
50
|
+
? ""
|
|
51
|
+
: "mtime_modified";
|
|
52
|
+
if (reason) {
|
|
48
53
|
if (undoFilesystemSideEffects && !fileRestoreMap.has(fileUrl)) {
|
|
49
54
|
if (stateBefore.found) {
|
|
50
55
|
fileRestoreMap.set(fileUrl, () => {
|
|
51
|
-
writeFileSync(fileUrl, stateBefore.
|
|
56
|
+
writeFileSync(fileUrl, stateBefore.buffer);
|
|
52
57
|
});
|
|
53
58
|
} else {
|
|
54
59
|
fileRestoreMap.set(fileUrl, () => {
|
|
@@ -57,17 +62,14 @@ export const spyFilesystemCalls = (
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
if (shouldReport(fileUrl)) {
|
|
60
|
-
|
|
65
|
+
onWriteFile(fileUrl, stateAfter.buffer, reason);
|
|
61
66
|
return;
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
// file is exactly the same
|
|
65
70
|
// function did not have any effect on the file
|
|
66
71
|
};
|
|
67
|
-
const onWriteDirectoryDone = (directoryUrl
|
|
68
|
-
if (stateBefore.found) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
72
|
+
const onWriteDirectoryDone = (directoryUrl) => {
|
|
71
73
|
if (undoFilesystemSideEffects && !fileRestoreMap.has(directoryUrl)) {
|
|
72
74
|
fileRestoreMap.set(directoryUrl, () => {
|
|
73
75
|
removeDirectorySync(directoryUrl, {
|
|
@@ -77,7 +79,7 @@ export const spyFilesystemCalls = (
|
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
81
|
if (shouldReport(directoryUrl)) {
|
|
80
|
-
|
|
82
|
+
onWriteDirectory(directoryUrl);
|
|
81
83
|
}
|
|
82
84
|
};
|
|
83
85
|
const beforeUndoCallbackSet = new Set();
|
|
@@ -93,13 +95,34 @@ export const spyFilesystemCalls = (
|
|
|
93
95
|
const mkdirHook = hookIntoMethod(
|
|
94
96
|
_internalFs,
|
|
95
97
|
"mkdir",
|
|
96
|
-
(directoryPath) => {
|
|
98
|
+
(directoryPath, mode, recursive) => {
|
|
97
99
|
const directoryUrl = pathToFileURL(directoryPath);
|
|
98
100
|
const stateBefore = getDirectoryState(directoryPath);
|
|
101
|
+
if (!stateBefore.found && recursive) {
|
|
102
|
+
const ancestorNotFoundArray = [];
|
|
103
|
+
for (const ancestorUrl of yieldAncestorUrls(directoryUrl)) {
|
|
104
|
+
const ancestorState = getDirectoryState(new URL(ancestorUrl));
|
|
105
|
+
if (ancestorState.found) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
ancestorNotFoundArray.unshift(ancestorUrl);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
return: (fd) => {
|
|
112
|
+
fileDescriptorPathMap.set(fd, directoryPath);
|
|
113
|
+
for (const ancestorNotFoundUrl of ancestorNotFoundArray) {
|
|
114
|
+
onWriteDirectoryDone(ancestorNotFoundUrl);
|
|
115
|
+
}
|
|
116
|
+
onWriteDirectoryDone(String(directoryUrl));
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
99
120
|
return {
|
|
100
121
|
return: (fd) => {
|
|
101
122
|
fileDescriptorPathMap.set(fd, directoryPath);
|
|
102
|
-
|
|
123
|
+
if (!stateBefore.found) {
|
|
124
|
+
onWriteDirectoryDone(String(directoryUrl));
|
|
125
|
+
}
|
|
103
126
|
},
|
|
104
127
|
};
|
|
105
128
|
},
|
|
@@ -140,12 +163,12 @@ export const spyFilesystemCalls = (
|
|
|
140
163
|
}
|
|
141
164
|
const fileUrl = pathToFileURL(filePath);
|
|
142
165
|
if (buffer) {
|
|
143
|
-
|
|
166
|
+
onReadFile(String(fileUrl));
|
|
144
167
|
}
|
|
145
168
|
fileDescriptorPathMap.delete(fileDescriptor);
|
|
146
169
|
filesystemStateInfoMap.delete(filePath);
|
|
147
170
|
const stateAfter = getFileStateWithinHook(fileUrl);
|
|
148
|
-
onWriteFileDone(fileUrl, stateBefore, stateAfter);
|
|
171
|
+
onWriteFileDone(String(fileUrl), stateBefore, stateAfter);
|
|
149
172
|
},
|
|
150
173
|
};
|
|
151
174
|
},
|
|
@@ -160,7 +183,7 @@ export const spyFilesystemCalls = (
|
|
|
160
183
|
return {
|
|
161
184
|
return: () => {
|
|
162
185
|
const stateAfter = getFileStateWithinHook(fileUrl);
|
|
163
|
-
onWriteFileDone(fileUrl, stateBefore, stateAfter);
|
|
186
|
+
onWriteFileDone(String(fileUrl), stateBefore, stateAfter);
|
|
164
187
|
},
|
|
165
188
|
};
|
|
166
189
|
},
|
|
@@ -168,7 +191,7 @@ export const spyFilesystemCalls = (
|
|
|
168
191
|
const unlinkHook = hookIntoMethod(_internalFs, "unlink", (filePath) => {
|
|
169
192
|
return {
|
|
170
193
|
return: () => {
|
|
171
|
-
|
|
194
|
+
onRemoveFile(filePath); // TODO eventually split in removeFile/removeDirectory
|
|
172
195
|
},
|
|
173
196
|
};
|
|
174
197
|
});
|
|
@@ -202,12 +225,12 @@ export const spyFilesystemCalls = (
|
|
|
202
225
|
|
|
203
226
|
const getFileState = (file) => {
|
|
204
227
|
try {
|
|
205
|
-
const
|
|
228
|
+
const fileBuffer = readFileSync(file);
|
|
206
229
|
const { mtimeMs } = statSync(file);
|
|
207
230
|
return {
|
|
208
231
|
found: true,
|
|
209
232
|
mtimeMs,
|
|
210
|
-
|
|
233
|
+
buffer: fileBuffer,
|
|
211
234
|
};
|
|
212
235
|
} catch (e) {
|
|
213
236
|
if (e.code === "ENOENT") {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { groupSideEffectsPer } from "../utils/group_side_effects.js";
|
|
2
|
+
|
|
3
|
+
export const groupLogSideEffects = (
|
|
4
|
+
allSideEffects,
|
|
5
|
+
{ createLogGroupSideEffect },
|
|
6
|
+
) => {
|
|
7
|
+
return groupSideEffectsPer(
|
|
8
|
+
allSideEffects,
|
|
9
|
+
(sideEffect) => {
|
|
10
|
+
if (
|
|
11
|
+
sideEffect.code === "console.trace" ||
|
|
12
|
+
sideEffect.code === "console.log" ||
|
|
13
|
+
sideEffect.code === "console.info" ||
|
|
14
|
+
sideEffect.code === "console.warn" ||
|
|
15
|
+
sideEffect.code === "console.error" ||
|
|
16
|
+
sideEffect.code === "process.stdout" ||
|
|
17
|
+
sideEffect.code === "process.stderr"
|
|
18
|
+
) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
createGroupSideEffect: (sideEffects) => {
|
|
25
|
+
return createLogGroupSideEffect(sideEffects);
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { hookIntoMethod } from "../hook_into_method.js";
|
|
2
|
+
import { renderConsole } from "../render_side_effects.js";
|
|
3
|
+
import { groupLogSideEffects } from "./group_log_side_effects.js";
|
|
4
|
+
|
|
5
|
+
const logSideEffectsOptionsDefault = {
|
|
6
|
+
prevent: true,
|
|
7
|
+
group: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const logSideEffects = (logSideEffectsOptions) => {
|
|
11
|
+
logSideEffectsOptions = {
|
|
12
|
+
...logSideEffectsOptionsDefault,
|
|
13
|
+
...logSideEffectsOptions,
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
name: "console",
|
|
17
|
+
install: (addSideEffect, { addFinallyCallback }) => {
|
|
18
|
+
const { prevent, group } = logSideEffectsOptions;
|
|
19
|
+
if (group) {
|
|
20
|
+
addFinallyCallback((sideEffects) => {
|
|
21
|
+
groupLogSideEffects(sideEffects, {
|
|
22
|
+
createLogGroupSideEffect: (logSideEffectArray) => {
|
|
23
|
+
const logGroupSideEffect = {
|
|
24
|
+
code: "log_group",
|
|
25
|
+
type: `log_group`,
|
|
26
|
+
value: {},
|
|
27
|
+
render: {
|
|
28
|
+
md: (options) => {
|
|
29
|
+
const renderLogGroup = () => {
|
|
30
|
+
let logs = "";
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i < logSideEffectArray.length) {
|
|
33
|
+
const logSideEffect = logSideEffectArray[i];
|
|
34
|
+
i++;
|
|
35
|
+
const { text } = logSideEffect.render.md(options);
|
|
36
|
+
logs += text.value;
|
|
37
|
+
if (
|
|
38
|
+
i !== logSideEffectArray.length &&
|
|
39
|
+
logSideEffect.type.startsWith("console")
|
|
40
|
+
) {
|
|
41
|
+
logs += "\n";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const logGroupMd = renderConsole(logs, {
|
|
45
|
+
sideEffect: logGroupSideEffect,
|
|
46
|
+
...options,
|
|
47
|
+
});
|
|
48
|
+
return logGroupMd;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
label: `logs`,
|
|
52
|
+
text: renderLogGroup(),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return logGroupSideEffect;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const addLogSideEffect = (type, message) => {
|
|
63
|
+
addSideEffect({
|
|
64
|
+
code: type,
|
|
65
|
+
type,
|
|
66
|
+
value: message,
|
|
67
|
+
render: {
|
|
68
|
+
md: () => {
|
|
69
|
+
return {
|
|
70
|
+
label: type,
|
|
71
|
+
text: {
|
|
72
|
+
type: "console",
|
|
73
|
+
value: message,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const errorHook = hookIntoMethod(console, "error", (message) => {
|
|
82
|
+
return {
|
|
83
|
+
preventOriginalCall: prevent,
|
|
84
|
+
return: () => {
|
|
85
|
+
addLogSideEffect("console.error", message);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
const warnHook = hookIntoMethod(console, "warn", (message) => {
|
|
90
|
+
return {
|
|
91
|
+
preventOriginalCall: prevent,
|
|
92
|
+
return: () => {
|
|
93
|
+
addLogSideEffect("console.warn", message);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
const infoHook = hookIntoMethod(console, "info", (message) => {
|
|
98
|
+
return {
|
|
99
|
+
preventOriginalCall: prevent,
|
|
100
|
+
return: () => {
|
|
101
|
+
addLogSideEffect("console.info", message);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
const logHook = hookIntoMethod(console, "log", (message) => {
|
|
106
|
+
return {
|
|
107
|
+
preventOriginalCall: prevent,
|
|
108
|
+
return: () => {
|
|
109
|
+
addLogSideEffect("console.log", message);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
const traceHook = hookIntoMethod(console, "trace", (message) => {
|
|
114
|
+
return {
|
|
115
|
+
preventOriginalCall: prevent,
|
|
116
|
+
return: () => {
|
|
117
|
+
addLogSideEffect("console.trace", message);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const processStdouthook = hookIntoMethod(
|
|
122
|
+
process.stdout,
|
|
123
|
+
"write",
|
|
124
|
+
(message) => {
|
|
125
|
+
return {
|
|
126
|
+
preventOriginalCall: prevent,
|
|
127
|
+
return: () => {
|
|
128
|
+
addLogSideEffect("process.stdout", message);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
const processStderrHhook = hookIntoMethod(
|
|
134
|
+
process.stderr,
|
|
135
|
+
"write",
|
|
136
|
+
(message) => {
|
|
137
|
+
return {
|
|
138
|
+
preventOriginalCall: prevent,
|
|
139
|
+
return: () => {
|
|
140
|
+
addLogSideEffect("process.stderr", message);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
return () => {
|
|
146
|
+
errorHook.remove();
|
|
147
|
+
warnHook.remove();
|
|
148
|
+
infoHook.remove();
|
|
149
|
+
logHook.remove();
|
|
150
|
+
traceHook.remove();
|
|
151
|
+
processStdouthook.remove();
|
|
152
|
+
processStderrHhook.remove();
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { writeFileSync } from "@jsenv/filesystem";
|
|
2
|
+
import { startTerminalRecording } from "@jsenv/terminal-recorder";
|
|
3
|
+
|
|
4
|
+
export const renderLogsGif = async (sideEffects, gitFileUrl) => {
|
|
5
|
+
const terminalRecorder = await startTerminalRecording({
|
|
6
|
+
gif: true,
|
|
7
|
+
});
|
|
8
|
+
for (const sideEffect of sideEffects) {
|
|
9
|
+
if (sideEffect.type === "console.log") {
|
|
10
|
+
await terminalRecorder.write(sideEffect.value, {
|
|
11
|
+
delay: sideEffect.delay,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const result = await terminalRecorder.stop();
|
|
16
|
+
const gif = await result.gif();
|
|
17
|
+
writeFileSync(new URL(gitFileUrl), gif);
|
|
18
|
+
};
|