@jsenv/snapshot 2.2.8 → 2.3.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.
- package/package.json +3 -2
- package/src/compare_png_files.js +1 -1
- package/src/function_side_effects/function_side_effects_collector.js +132 -0
- package/src/function_side_effects/function_side_effects_renderer.js +18 -0
- package/src/function_side_effects/function_side_effects_snapshot.js +160 -0
- package/src/function_side_effects/spy_console_calls.js +53 -0
- package/src/function_side_effects/spy_filesystem_calls.js +202 -0
- package/src/function_side_effects/spy_method.js +111 -0
- package/src/main.js +1 -1
- package/src/function_snapshot.js +0 -256
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/snapshot",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Snapshot testing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
"test": "node --conditions=development ./scripts/test.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@jsenv/assert": "4.1.
|
|
37
|
+
"@jsenv/assert": "4.1.6",
|
|
38
38
|
"@jsenv/ast": "6.2.6",
|
|
39
|
+
"@jsenv/exception": "1.0.0",
|
|
39
40
|
"@jsenv/filesystem": "4.9.2",
|
|
40
41
|
"@jsenv/urls": "2.3.2",
|
|
41
42
|
"@jsenv/utils": "2.1.1",
|
package/src/compare_png_files.js
CHANGED
|
@@ -20,7 +20,7 @@ export const comparePngFiles = (actualData, expectData) => {
|
|
|
20
20
|
);
|
|
21
21
|
const diffRatio = numberOfPixelsConsideredAsDiff / numberOfPixels;
|
|
22
22
|
const diffPercentage = diffRatio * 100;
|
|
23
|
-
return diffPercentage <=
|
|
23
|
+
return diffPercentage <= 1;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const getPngDimensions = (buffer) => {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createException } from "@jsenv/exception";
|
|
2
|
+
import { replaceFluctuatingValues } from "../replace_fluctuating_values.js";
|
|
3
|
+
|
|
4
|
+
const RETURN_PROMISE = {};
|
|
5
|
+
|
|
6
|
+
let executing = false;
|
|
7
|
+
export const collectFunctionSideEffects = (
|
|
8
|
+
fn,
|
|
9
|
+
sideEffectDetectors,
|
|
10
|
+
{ rootDirectoryUrl },
|
|
11
|
+
) => {
|
|
12
|
+
if (executing) {
|
|
13
|
+
throw new Error("collectFunctionSideEffects already running");
|
|
14
|
+
}
|
|
15
|
+
const sideEffects = [];
|
|
16
|
+
const addSideEffect = (sideEffect) => {
|
|
17
|
+
sideEffects.push(sideEffect);
|
|
18
|
+
};
|
|
19
|
+
const finallyCallbackSet = new Set();
|
|
20
|
+
for (const sideEffectDetector of sideEffectDetectors) {
|
|
21
|
+
const uninstall = sideEffectDetector.install(addSideEffect);
|
|
22
|
+
finallyCallbackSet.add(() => {
|
|
23
|
+
uninstall();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const onCatch = (valueThrow) => {
|
|
27
|
+
sideEffects.push({
|
|
28
|
+
type: "throw",
|
|
29
|
+
value: valueThrow,
|
|
30
|
+
label: "throw",
|
|
31
|
+
text: renderValueThrownOrRejected(
|
|
32
|
+
createException(valueThrow, { rootDirectoryUrl }),
|
|
33
|
+
{ rootDirectoryUrl },
|
|
34
|
+
),
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const onReturn = (valueReturned) => {
|
|
38
|
+
if (valueReturned === RETURN_PROMISE) {
|
|
39
|
+
sideEffects.push({
|
|
40
|
+
type: "return",
|
|
41
|
+
value: valueReturned,
|
|
42
|
+
label: "return promise",
|
|
43
|
+
text: null,
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
sideEffects.push({
|
|
47
|
+
type: "return",
|
|
48
|
+
value: valueReturned,
|
|
49
|
+
label: "return",
|
|
50
|
+
text: renderReturnValueOrResolveValue(valueReturned, {
|
|
51
|
+
rootDirectoryUrl,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const onResolve = (value) => {
|
|
57
|
+
sideEffects.push({
|
|
58
|
+
type: "resolve",
|
|
59
|
+
value,
|
|
60
|
+
label: "resolve",
|
|
61
|
+
text: renderReturnValueOrResolveValue(value, { rootDirectoryUrl }),
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const onReject = (reason) => {
|
|
65
|
+
sideEffects.push({
|
|
66
|
+
type: "reject",
|
|
67
|
+
value: reason,
|
|
68
|
+
label: "reject",
|
|
69
|
+
text: renderValueThrownOrRejected(
|
|
70
|
+
createException(reason, { rootDirectoryUrl }),
|
|
71
|
+
{ rootDirectoryUrl },
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const onFinally = () => {
|
|
76
|
+
executing = false;
|
|
77
|
+
for (const finallyCallback of finallyCallbackSet) {
|
|
78
|
+
finallyCallback();
|
|
79
|
+
}
|
|
80
|
+
finallyCallbackSet.clear();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let returnedPromise = false;
|
|
84
|
+
try {
|
|
85
|
+
const valueReturned = fn();
|
|
86
|
+
if (valueReturned && typeof valueReturned.then === "function") {
|
|
87
|
+
onReturn(RETURN_PROMISE);
|
|
88
|
+
returnedPromise = valueReturned.then(
|
|
89
|
+
(value) => {
|
|
90
|
+
onResolve(value);
|
|
91
|
+
onFinally();
|
|
92
|
+
return sideEffects;
|
|
93
|
+
},
|
|
94
|
+
(e) => {
|
|
95
|
+
onReject(e);
|
|
96
|
+
onFinally();
|
|
97
|
+
return sideEffects;
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
return returnedPromise;
|
|
101
|
+
}
|
|
102
|
+
onReturn(valueReturned);
|
|
103
|
+
return sideEffects;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
onCatch(e);
|
|
106
|
+
return sideEffects;
|
|
107
|
+
} finally {
|
|
108
|
+
if (!returnedPromise) {
|
|
109
|
+
onFinally();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const renderReturnValueOrResolveValue = (value, { rootDirectoryUrl }) => {
|
|
115
|
+
if (value === undefined) {
|
|
116
|
+
return "undefined";
|
|
117
|
+
}
|
|
118
|
+
return replaceFluctuatingValues(JSON.stringify(value, null, " "), {
|
|
119
|
+
stringType: "json",
|
|
120
|
+
rootDirectoryUrl,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const renderValueThrownOrRejected = (value, { rootDirectoryUrl }) => {
|
|
125
|
+
return replaceFluctuatingValues(
|
|
126
|
+
value ? value.stack || value.message || value : String(value),
|
|
127
|
+
{
|
|
128
|
+
stringType: "error",
|
|
129
|
+
rootDirectoryUrl,
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const renderSideEffects = (sideEffects) => {
|
|
2
|
+
let string = "";
|
|
3
|
+
let index = 0;
|
|
4
|
+
for (const sideEffect of sideEffects) {
|
|
5
|
+
if (string) {
|
|
6
|
+
string += "\n\n";
|
|
7
|
+
}
|
|
8
|
+
let label = `${index + 1}. ${sideEffect.label}`;
|
|
9
|
+
let text = sideEffect.text;
|
|
10
|
+
string += label;
|
|
11
|
+
if (text) {
|
|
12
|
+
string += "\n";
|
|
13
|
+
string += text;
|
|
14
|
+
}
|
|
15
|
+
index++;
|
|
16
|
+
}
|
|
17
|
+
return string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { writeFileSync } from "@jsenv/filesystem";
|
|
2
|
+
import {
|
|
3
|
+
ensurePathnameTrailingSlash,
|
|
4
|
+
urlToFilename,
|
|
5
|
+
urlToRelativeUrl,
|
|
6
|
+
} from "@jsenv/urls";
|
|
7
|
+
import { takeDirectorySnapshot } from "../filesystem_snapshot.js";
|
|
8
|
+
import { replaceFluctuatingValues } from "../replace_fluctuating_values.js";
|
|
9
|
+
import { collectFunctionSideEffects } from "./function_side_effects_collector.js";
|
|
10
|
+
import { renderSideEffects } from "./function_side_effects_renderer.js";
|
|
11
|
+
import { spyConsoleCalls } from "./spy_console_calls.js";
|
|
12
|
+
import { spyFilesystemCalls } from "./spy_filesystem_calls.js";
|
|
13
|
+
|
|
14
|
+
let filesystemSideEffectInstalled;
|
|
15
|
+
|
|
16
|
+
export const snapshotFunctionSideEffects = (
|
|
17
|
+
fn,
|
|
18
|
+
fnFileUrl,
|
|
19
|
+
sideEffectDirectoryRelativeUrl = "./",
|
|
20
|
+
{
|
|
21
|
+
rootDirectoryUrl = new URL("./", fnFileUrl),
|
|
22
|
+
filesystemEffectsDirectory,
|
|
23
|
+
preventConsoleSideEffects = true,
|
|
24
|
+
undoFilesystemSideEffects = true,
|
|
25
|
+
} = {},
|
|
26
|
+
) => {
|
|
27
|
+
if (filesystemEffectsDirectory === true) {
|
|
28
|
+
filesystemEffectsDirectory = "./fs/";
|
|
29
|
+
}
|
|
30
|
+
const sideEffectDirectoryUrl = new URL(
|
|
31
|
+
sideEffectDirectoryRelativeUrl,
|
|
32
|
+
fnFileUrl,
|
|
33
|
+
);
|
|
34
|
+
const sideEffectDirectorySnapshot = takeDirectorySnapshot(
|
|
35
|
+
sideEffectDirectoryUrl,
|
|
36
|
+
);
|
|
37
|
+
const sideEffectFilename = `${urlToFilename(sideEffectDirectoryUrl)}_side_effects.txt`;
|
|
38
|
+
const sideEffectFileUrl = new URL(sideEffectFilename, sideEffectDirectoryUrl);
|
|
39
|
+
const callbackSet = new Set();
|
|
40
|
+
const sideEffectDetectors = [
|
|
41
|
+
{
|
|
42
|
+
name: "console",
|
|
43
|
+
install: (addSideEffect) => {
|
|
44
|
+
const onConsole = (methodName, message) => {
|
|
45
|
+
addSideEffect({
|
|
46
|
+
type: `console:${methodName}`,
|
|
47
|
+
value: message,
|
|
48
|
+
label: `console.${methodName}`,
|
|
49
|
+
text: replaceFluctuatingValues(message, {
|
|
50
|
+
stringType: "console",
|
|
51
|
+
rootDirectoryUrl,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const consoleSpy = spyConsoleCalls(
|
|
56
|
+
{
|
|
57
|
+
error: (message) => {
|
|
58
|
+
onConsole("error", message);
|
|
59
|
+
},
|
|
60
|
+
warn: (message) => {
|
|
61
|
+
onConsole("warn", message);
|
|
62
|
+
},
|
|
63
|
+
info: (message) => {
|
|
64
|
+
onConsole("info", message);
|
|
65
|
+
},
|
|
66
|
+
log: (message) => {
|
|
67
|
+
onConsole("log", message);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
preventConsoleSideEffects,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
return () => {
|
|
75
|
+
consoleSpy.restore();
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "filesystem",
|
|
81
|
+
install: (addSideEffect) => {
|
|
82
|
+
if (filesystemSideEffectInstalled) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"cannot collect filesystem side effects concurrently (already collecting side effect for an other function)",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
filesystemSideEffectInstalled = true;
|
|
88
|
+
const fsSideEffectDirectoryUrl = ensurePathnameTrailingSlash(
|
|
89
|
+
new URL(filesystemEffectsDirectory, sideEffectDirectoryUrl),
|
|
90
|
+
);
|
|
91
|
+
const fsSideEffectsDirectoryRelativeUrl = urlToRelativeUrl(
|
|
92
|
+
fsSideEffectDirectoryUrl,
|
|
93
|
+
sideEffectFileUrl,
|
|
94
|
+
);
|
|
95
|
+
const filesystemSpy = spyFilesystemCalls(
|
|
96
|
+
{
|
|
97
|
+
writeFile: (url, content) => {
|
|
98
|
+
const relativeUrl = urlToRelativeUrl(url, fnFileUrl);
|
|
99
|
+
const toUrl = new URL(relativeUrl, fsSideEffectDirectoryUrl);
|
|
100
|
+
if (filesystemEffectsDirectory) {
|
|
101
|
+
callbackSet.add(() => {
|
|
102
|
+
writeFileSync(toUrl, content);
|
|
103
|
+
});
|
|
104
|
+
addSideEffect({
|
|
105
|
+
type: "fs:write_file",
|
|
106
|
+
value: { relativeUrl, content },
|
|
107
|
+
label: `write file "${relativeUrl}" (see ./${fsSideEffectsDirectoryRelativeUrl}${relativeUrl})`,
|
|
108
|
+
text: null,
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
addSideEffect({
|
|
112
|
+
type: "fs:write_file",
|
|
113
|
+
value: { relativeUrl, content },
|
|
114
|
+
label: `write file "${relativeUrl}"`,
|
|
115
|
+
text: content,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
writeDirectory: (url) => {
|
|
120
|
+
const relativeUrl = urlToRelativeUrl(url, fnFileUrl);
|
|
121
|
+
addSideEffect({
|
|
122
|
+
type: "fs:write_directory",
|
|
123
|
+
value: { relativeUrl },
|
|
124
|
+
label: `write directory "${relativeUrl}"`,
|
|
125
|
+
text: null,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
undoFilesystemSideEffects,
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
return () => {
|
|
134
|
+
filesystemSpy.restore();
|
|
135
|
+
filesystemSideEffectInstalled = false;
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const onSideEffectsCollected = (sideEffects) => {
|
|
142
|
+
for (const callback of callbackSet) {
|
|
143
|
+
callback();
|
|
144
|
+
}
|
|
145
|
+
callbackSet.clear();
|
|
146
|
+
writeFileSync(sideEffectFileUrl, renderSideEffects(sideEffects));
|
|
147
|
+
sideEffectDirectorySnapshot.compare();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const returnValue = collectFunctionSideEffects(fn, sideEffectDetectors, {
|
|
151
|
+
rootDirectoryUrl,
|
|
152
|
+
});
|
|
153
|
+
if (returnValue && typeof returnValue.then === "function") {
|
|
154
|
+
return returnValue.then((sideEffects) => {
|
|
155
|
+
onSideEffectsCollected(sideEffects);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
onSideEffectsCollected(returnValue);
|
|
159
|
+
return undefined;
|
|
160
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spyMethod } from "./spy_method.js";
|
|
2
|
+
|
|
3
|
+
export const spyConsoleCalls = (
|
|
4
|
+
{ error, warn, info, log, trace },
|
|
5
|
+
{ preventConsoleSideEffects },
|
|
6
|
+
) => {
|
|
7
|
+
const restoreCallbackSet = new Set();
|
|
8
|
+
const errorSpy = spyMethod(console, "error", (message) => {
|
|
9
|
+
if (preventConsoleSideEffects) {
|
|
10
|
+
errorSpy.preventOriginalCall();
|
|
11
|
+
}
|
|
12
|
+
error(message);
|
|
13
|
+
});
|
|
14
|
+
const warnSpy = spyMethod(console, "warn", (message) => {
|
|
15
|
+
if (preventConsoleSideEffects) {
|
|
16
|
+
warnSpy.preventOriginalCall();
|
|
17
|
+
}
|
|
18
|
+
warn(message);
|
|
19
|
+
});
|
|
20
|
+
const infoSpy = spyMethod(console, "info", (message) => {
|
|
21
|
+
if (preventConsoleSideEffects) {
|
|
22
|
+
infoSpy.preventOriginalCall();
|
|
23
|
+
}
|
|
24
|
+
info(message);
|
|
25
|
+
});
|
|
26
|
+
const logSpy = spyMethod(console, "log", (message) => {
|
|
27
|
+
if (preventConsoleSideEffects) {
|
|
28
|
+
logSpy.preventOriginalCall();
|
|
29
|
+
}
|
|
30
|
+
log(message);
|
|
31
|
+
});
|
|
32
|
+
const traceSpy = spyMethod(console, "trace", (message) => {
|
|
33
|
+
if (preventConsoleSideEffects) {
|
|
34
|
+
traceSpy.preventOriginalCall();
|
|
35
|
+
}
|
|
36
|
+
trace(message);
|
|
37
|
+
});
|
|
38
|
+
restoreCallbackSet.add(() => {
|
|
39
|
+
errorSpy.remove();
|
|
40
|
+
warnSpy.remove();
|
|
41
|
+
infoSpy.remove();
|
|
42
|
+
logSpy.remove();
|
|
43
|
+
traceSpy.remove();
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
restore: () => {
|
|
47
|
+
for (const restoreCallback of restoreCallbackSet) {
|
|
48
|
+
restoreCallback();
|
|
49
|
+
}
|
|
50
|
+
restoreCallbackSet.clear();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// https://github.com/antfu/fs-spy/blob/main/src/index.ts
|
|
2
|
+
// https://github.com/tschaub/mock-fs/tree/main
|
|
3
|
+
|
|
4
|
+
import { removeDirectorySync, removeFileSync } from "@jsenv/filesystem";
|
|
5
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { spyMethod } from "./spy_method.js";
|
|
8
|
+
|
|
9
|
+
export const spyFilesystemCalls = (
|
|
10
|
+
{
|
|
11
|
+
writeFile = () => {},
|
|
12
|
+
writeDirectory = () => {},
|
|
13
|
+
removeFile = () => {},
|
|
14
|
+
// removeDirectory = () => {},
|
|
15
|
+
},
|
|
16
|
+
{ undoFilesystemSideEffects } = {},
|
|
17
|
+
) => {
|
|
18
|
+
const _internalFs = process.binding("fs");
|
|
19
|
+
const filesystemStateInfoMap = new Map();
|
|
20
|
+
const fileDescriptorPathMap = new Map();
|
|
21
|
+
const fileRestoreMap = new Map();
|
|
22
|
+
const onWriteFileDone = (fileUrl, stateBefore, stateAfter) => {
|
|
23
|
+
// we use same type because we don't want to differentiate between
|
|
24
|
+
// - writing file for the 1st time
|
|
25
|
+
// - updating file content
|
|
26
|
+
// the important part is the file content in the end of the function execution
|
|
27
|
+
if (
|
|
28
|
+
(!stateBefore.found && stateAfter.found) ||
|
|
29
|
+
stateBefore.content !== stateAfter.content ||
|
|
30
|
+
stateBefore.mtimeMs !== stateAfter.mtimeMs
|
|
31
|
+
) {
|
|
32
|
+
if (undoFilesystemSideEffects && !fileRestoreMap.has(fileUrl)) {
|
|
33
|
+
if (stateBefore.found) {
|
|
34
|
+
fileRestoreMap.set(fileUrl, () => {
|
|
35
|
+
writeFileSync(fileUrl, stateBefore.content);
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
fileRestoreMap.set(fileUrl, () => {
|
|
39
|
+
removeFileSync(fileUrl, { allowUseless: true });
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
writeFile(fileUrl, stateAfter.content);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// file is exactly the same
|
|
47
|
+
// function did not have any effect on the file
|
|
48
|
+
};
|
|
49
|
+
const onWriteDirectoryDone = (directoryUrl, stateBefore) => {
|
|
50
|
+
if (stateBefore.found) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (undoFilesystemSideEffects && !fileRestoreMap.has(directoryUrl)) {
|
|
54
|
+
fileRestoreMap.set(directoryUrl, () => {
|
|
55
|
+
removeDirectorySync(directoryUrl, {
|
|
56
|
+
allowUseless: true,
|
|
57
|
+
recursive: true,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
writeDirectory(directoryUrl);
|
|
62
|
+
};
|
|
63
|
+
const restoreCallbackSet = new Set();
|
|
64
|
+
const mkdirSpy = spyMethod(
|
|
65
|
+
_internalFs,
|
|
66
|
+
"mkdir",
|
|
67
|
+
(directoryPath, mode, recursive, callback) => {
|
|
68
|
+
const directoryUrl = pathToFileURL(directoryPath);
|
|
69
|
+
if (callback) {
|
|
70
|
+
const stateBefore = getDirectoryState(directoryPath);
|
|
71
|
+
const oncomplete = callback.oncomplete;
|
|
72
|
+
callback.oncomplete = (error, fd) => {
|
|
73
|
+
if (error) {
|
|
74
|
+
oncomplete(error);
|
|
75
|
+
} else {
|
|
76
|
+
fileDescriptorPathMap.set(fd, directoryPath);
|
|
77
|
+
oncomplete();
|
|
78
|
+
onWriteDirectoryDone(directoryUrl, stateBefore, { found: true });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return mkdirSpy.callOriginal();
|
|
82
|
+
}
|
|
83
|
+
const stateBefore = getDirectoryState(directoryPath);
|
|
84
|
+
mkdirSpy.callOriginal();
|
|
85
|
+
onWriteDirectoryDone(directoryUrl, stateBefore, { found: true });
|
|
86
|
+
return undefined;
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
const openSpy = spyMethod(
|
|
90
|
+
_internalFs,
|
|
91
|
+
"open",
|
|
92
|
+
(filePath, flags, mode, callback) => {
|
|
93
|
+
if (callback) {
|
|
94
|
+
const stateBefore = getFileState(filePath);
|
|
95
|
+
filesystemStateInfoMap.set(filePath, stateBefore);
|
|
96
|
+
const oncomplete = callback.oncomplete;
|
|
97
|
+
callback.oncomplete = (error, fd) => {
|
|
98
|
+
if (error) {
|
|
99
|
+
oncomplete(error);
|
|
100
|
+
} else {
|
|
101
|
+
fileDescriptorPathMap.set(fd, filePath);
|
|
102
|
+
oncomplete(error, fd);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return openSpy.callOriginal();
|
|
106
|
+
}
|
|
107
|
+
const stateBefore = getFileState(filePath);
|
|
108
|
+
filesystemStateInfoMap.set(filePath, stateBefore);
|
|
109
|
+
const fd = openSpy.callOriginal();
|
|
110
|
+
fileDescriptorPathMap.set(fd, filePath);
|
|
111
|
+
return fd;
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
const closeSpy = spyMethod(_internalFs, "close", (fileDescriptor) => {
|
|
115
|
+
const filePath = fileDescriptorPathMap.get(fileDescriptor);
|
|
116
|
+
if (!filePath) {
|
|
117
|
+
return closeSpy.callOriginal();
|
|
118
|
+
}
|
|
119
|
+
const openInfo = filesystemStateInfoMap.get(filePath);
|
|
120
|
+
if (!openInfo) {
|
|
121
|
+
const returnValue = closeSpy.callOriginal();
|
|
122
|
+
fileDescriptorPathMap.delete(fileDescriptor);
|
|
123
|
+
return returnValue;
|
|
124
|
+
}
|
|
125
|
+
const fileUrl = pathToFileURL(filePath);
|
|
126
|
+
const nowState = getFileState(fileUrl);
|
|
127
|
+
onWriteFileDone(fileUrl, openInfo, nowState);
|
|
128
|
+
const returnValue = closeSpy.callOriginal();
|
|
129
|
+
fileDescriptorPathMap.delete(fileDescriptor);
|
|
130
|
+
filesystemStateInfoMap.delete(filePath);
|
|
131
|
+
return returnValue;
|
|
132
|
+
});
|
|
133
|
+
const writeFileUtf8Spy = spyMethod(
|
|
134
|
+
_internalFs,
|
|
135
|
+
"writeFileUtf8",
|
|
136
|
+
(filePath) => {
|
|
137
|
+
const fileUrl = pathToFileURL(filePath);
|
|
138
|
+
const stateBefore = getFileState(fileUrl);
|
|
139
|
+
writeFileUtf8Spy.callOriginal();
|
|
140
|
+
const stateAfter = getFileState(fileUrl);
|
|
141
|
+
onWriteFileDone(fileUrl, stateBefore, stateAfter);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
const unlinkSpy = spyMethod(_internalFs, "unlink", (filePath) => {
|
|
145
|
+
unlinkSpy.callOriginal();
|
|
146
|
+
removeFile(filePath); // TODO eventually split in removeFile/removeDirectory
|
|
147
|
+
});
|
|
148
|
+
restoreCallbackSet.add(() => {
|
|
149
|
+
mkdirSpy.remove();
|
|
150
|
+
openSpy.remove();
|
|
151
|
+
closeSpy.remove();
|
|
152
|
+
writeFileUtf8Spy.remove();
|
|
153
|
+
unlinkSpy.remove();
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
restore: () => {
|
|
157
|
+
for (const restoreCallback of restoreCallbackSet) {
|
|
158
|
+
restoreCallback();
|
|
159
|
+
}
|
|
160
|
+
restoreCallbackSet.clear();
|
|
161
|
+
for (const [, restore] of fileRestoreMap) {
|
|
162
|
+
restore();
|
|
163
|
+
}
|
|
164
|
+
fileRestoreMap.clear();
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const getFileState = (file) => {
|
|
170
|
+
try {
|
|
171
|
+
const fileContent = readFileSync(file);
|
|
172
|
+
const { mtimeMs } = statSync(file);
|
|
173
|
+
return {
|
|
174
|
+
found: true,
|
|
175
|
+
mtimeMs,
|
|
176
|
+
content: String(fileContent),
|
|
177
|
+
};
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (e.code === "ENOENT") {
|
|
180
|
+
return {
|
|
181
|
+
found: false,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const getDirectoryState = (directory) => {
|
|
189
|
+
try {
|
|
190
|
+
statSync(directory);
|
|
191
|
+
return {
|
|
192
|
+
found: true,
|
|
193
|
+
};
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (e.code === "ENOENT") {
|
|
196
|
+
return {
|
|
197
|
+
found: false,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
throw e;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const jsenvSpySymbol = Symbol.for("jsenv_spy");
|
|
2
|
+
|
|
3
|
+
export const spyMethod = (object, method, spyCallback) => {
|
|
4
|
+
const current = object[method];
|
|
5
|
+
const jsenvSpySymbolValue = current[jsenvSpySymbol];
|
|
6
|
+
let addCallback;
|
|
7
|
+
let removeCallback;
|
|
8
|
+
let callOriginal;
|
|
9
|
+
let onOriginalCall;
|
|
10
|
+
let preventOriginalCall;
|
|
11
|
+
if (jsenvSpySymbolValue) {
|
|
12
|
+
addCallback = jsenvSpySymbolValue.addCallback;
|
|
13
|
+
removeCallback = jsenvSpySymbolValue.removeCallback;
|
|
14
|
+
callOriginal = jsenvSpySymbolValue.callOriginal;
|
|
15
|
+
onOriginalCall = jsenvSpySymbolValue.onOriginalCall;
|
|
16
|
+
preventOriginalCall = jsenvSpySymbolValue.preventOriginalCall;
|
|
17
|
+
} else {
|
|
18
|
+
const original = current;
|
|
19
|
+
let currentArgs;
|
|
20
|
+
let originalCalled = false;
|
|
21
|
+
let originalReturnValue;
|
|
22
|
+
let preventOriginalCallCalled;
|
|
23
|
+
let spyExecuting;
|
|
24
|
+
let someSpyUsedCallOriginal;
|
|
25
|
+
let allSpyUsedPreventOriginalCall;
|
|
26
|
+
onOriginalCall = (returnValue) => {
|
|
27
|
+
originalCalled = true;
|
|
28
|
+
originalReturnValue = returnValue;
|
|
29
|
+
};
|
|
30
|
+
callOriginal = () => {
|
|
31
|
+
if (someSpyUsedCallOriginal) {
|
|
32
|
+
return originalReturnValue;
|
|
33
|
+
}
|
|
34
|
+
someSpyUsedCallOriginal = true;
|
|
35
|
+
onOriginalCall(original(...currentArgs));
|
|
36
|
+
return originalReturnValue;
|
|
37
|
+
};
|
|
38
|
+
preventOriginalCall = () => {
|
|
39
|
+
preventOriginalCallCalled = true;
|
|
40
|
+
};
|
|
41
|
+
const spyCallbackSet = new Set();
|
|
42
|
+
const spy = (...args) => {
|
|
43
|
+
if (spyExecuting) {
|
|
44
|
+
// when a spy is executing
|
|
45
|
+
// if it calls the method himself
|
|
46
|
+
// then we want this call to go trough
|
|
47
|
+
// and others spy should not know about it
|
|
48
|
+
onOriginalCall(original(...args));
|
|
49
|
+
return originalReturnValue;
|
|
50
|
+
}
|
|
51
|
+
spyExecuting = true;
|
|
52
|
+
originalCalled = false;
|
|
53
|
+
currentArgs = args;
|
|
54
|
+
someSpyUsedCallOriginal = false;
|
|
55
|
+
allSpyUsedPreventOriginalCall = true;
|
|
56
|
+
for (const spyCallback of spyCallbackSet) {
|
|
57
|
+
try {
|
|
58
|
+
spyCallback(...args);
|
|
59
|
+
} finally {
|
|
60
|
+
if (preventOriginalCallCalled) {
|
|
61
|
+
preventOriginalCallCalled = false;
|
|
62
|
+
} else {
|
|
63
|
+
allSpyUsedPreventOriginalCall = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
spyExecuting = false;
|
|
68
|
+
if (!someSpyUsedCallOriginal && !allSpyUsedPreventOriginalCall) {
|
|
69
|
+
callOriginal();
|
|
70
|
+
}
|
|
71
|
+
currentArgs = null;
|
|
72
|
+
if (originalCalled) {
|
|
73
|
+
originalCalled = false;
|
|
74
|
+
const value = originalReturnValue;
|
|
75
|
+
originalReturnValue = undefined;
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
addCallback = (spyCallback) => {
|
|
81
|
+
if (spyCallbackSet.size === 0) {
|
|
82
|
+
object[method] = spy;
|
|
83
|
+
}
|
|
84
|
+
spyCallbackSet.add(spyCallback);
|
|
85
|
+
};
|
|
86
|
+
removeCallback = (spyCallback) => {
|
|
87
|
+
spyCallbackSet.delete(spyCallback);
|
|
88
|
+
if (spyCallbackSet.size === 0) {
|
|
89
|
+
object[method] = original;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
spy[jsenvSpySymbol] = {
|
|
93
|
+
addCallback,
|
|
94
|
+
removeCallback,
|
|
95
|
+
original,
|
|
96
|
+
callOriginal,
|
|
97
|
+
onOriginalCall,
|
|
98
|
+
preventOriginalCall,
|
|
99
|
+
};
|
|
100
|
+
object[method] = spy;
|
|
101
|
+
}
|
|
102
|
+
addCallback(spyCallback);
|
|
103
|
+
const spyHooks = {
|
|
104
|
+
callOriginal,
|
|
105
|
+
preventOriginalCall,
|
|
106
|
+
remove: () => {
|
|
107
|
+
removeCallback(spyCallback);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
return spyHooks;
|
|
111
|
+
};
|
package/src/main.js
CHANGED
|
@@ -2,5 +2,5 @@ export {
|
|
|
2
2
|
takeDirectorySnapshot,
|
|
3
3
|
takeFileSnapshot,
|
|
4
4
|
} from "./filesystem_snapshot.js";
|
|
5
|
-
export { snapshotFunctionSideEffects } from "./
|
|
5
|
+
export { snapshotFunctionSideEffects } from "./function_side_effects/function_side_effects_snapshot.js";
|
|
6
6
|
export { replaceFluctuatingValues } from "./replace_fluctuating_values.js";
|
package/src/function_snapshot.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
readEntryStatSync,
|
|
3
|
-
readFileSync,
|
|
4
|
-
removeFileSync,
|
|
5
|
-
writeFileSync,
|
|
6
|
-
} from "@jsenv/filesystem";
|
|
7
|
-
import {
|
|
8
|
-
ensurePathnameTrailingSlash,
|
|
9
|
-
urlToFilename,
|
|
10
|
-
urlToRelativeUrl,
|
|
11
|
-
} from "@jsenv/urls";
|
|
12
|
-
import { takeDirectorySnapshot } from "./filesystem_snapshot.js";
|
|
13
|
-
import { replaceFluctuatingValues } from "./replace_fluctuating_values.js";
|
|
14
|
-
|
|
15
|
-
const consoleSpySymbol = Symbol.for("console_spy_for_jsenv_snapshot");
|
|
16
|
-
|
|
17
|
-
export const snapshotFunctionSideEffects = (
|
|
18
|
-
fn,
|
|
19
|
-
fnFileUrl,
|
|
20
|
-
sideEffectDirectoryRelativeUrl = "./",
|
|
21
|
-
{
|
|
22
|
-
rootDirectoryUrl = new URL("./", fnFileUrl),
|
|
23
|
-
captureConsole = true,
|
|
24
|
-
filesystemEffects,
|
|
25
|
-
filesystemEffectsDirectory,
|
|
26
|
-
restoreFilesystem = true,
|
|
27
|
-
} = {},
|
|
28
|
-
) => {
|
|
29
|
-
if (filesystemEffectsDirectory === true) {
|
|
30
|
-
filesystemEffectsDirectory = "./fs/";
|
|
31
|
-
}
|
|
32
|
-
const sideEffectDirectoryUrl = new URL(
|
|
33
|
-
sideEffectDirectoryRelativeUrl,
|
|
34
|
-
fnFileUrl,
|
|
35
|
-
);
|
|
36
|
-
const sideEffectDirectorySnapshot = takeDirectorySnapshot(
|
|
37
|
-
sideEffectDirectoryUrl,
|
|
38
|
-
);
|
|
39
|
-
const sideEffectFilename = `${urlToFilename(sideEffectDirectoryUrl)}_side_effects.txt`;
|
|
40
|
-
const sideEffectFileUrl = new URL(sideEffectFilename, sideEffectDirectoryUrl);
|
|
41
|
-
const sideEffects = [];
|
|
42
|
-
const finallyCallbackSet = new Set();
|
|
43
|
-
const onError = (e, isAsync) => {
|
|
44
|
-
sideEffects.push({
|
|
45
|
-
type: isAsync ? "reject" : "throw",
|
|
46
|
-
value: e,
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
const onResult = (result, isAsync) => {
|
|
50
|
-
sideEffects.push({
|
|
51
|
-
type: isAsync ? "resolve" : "return",
|
|
52
|
-
value: result,
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
const onFinally = () => {
|
|
56
|
-
for (const finallyCallback of finallyCallbackSet) {
|
|
57
|
-
finallyCallback();
|
|
58
|
-
}
|
|
59
|
-
writeFileSync(
|
|
60
|
-
sideEffectFileUrl,
|
|
61
|
-
stringifySideEffects(sideEffects, {
|
|
62
|
-
rootDirectoryUrl,
|
|
63
|
-
filesystemEffectsDirectory,
|
|
64
|
-
}),
|
|
65
|
-
);
|
|
66
|
-
sideEffectDirectorySnapshot.compare();
|
|
67
|
-
};
|
|
68
|
-
if (captureConsole) {
|
|
69
|
-
const installConsoleSpy = (methodName) => {
|
|
70
|
-
const methodSpied = console[methodName];
|
|
71
|
-
if (consoleSpySymbol in methodSpied) {
|
|
72
|
-
throw new Error("snapshotFunctionSideEffects already running");
|
|
73
|
-
}
|
|
74
|
-
const methodSpy = (message) => {
|
|
75
|
-
sideEffects.push({
|
|
76
|
-
type: `console.${methodName}`,
|
|
77
|
-
value: message,
|
|
78
|
-
});
|
|
79
|
-
};
|
|
80
|
-
methodSpy[consoleSpySymbol] = true;
|
|
81
|
-
console[methodName] = methodSpy;
|
|
82
|
-
finallyCallbackSet.add(() => {
|
|
83
|
-
console[methodName] = methodSpied;
|
|
84
|
-
});
|
|
85
|
-
};
|
|
86
|
-
installConsoleSpy("error");
|
|
87
|
-
installConsoleSpy("warn");
|
|
88
|
-
installConsoleSpy("info");
|
|
89
|
-
installConsoleSpy("log");
|
|
90
|
-
}
|
|
91
|
-
if (filesystemEffects) {
|
|
92
|
-
const fsSideEffectDirectoryUrl = ensurePathnameTrailingSlash(
|
|
93
|
-
new URL(filesystemEffectsDirectory, sideEffectDirectoryUrl),
|
|
94
|
-
);
|
|
95
|
-
const fsSideEffectsDirectoryRelativeUrl = urlToRelativeUrl(
|
|
96
|
-
fsSideEffectDirectoryUrl,
|
|
97
|
-
sideEffectFileUrl,
|
|
98
|
-
);
|
|
99
|
-
for (const filesystemEffect of filesystemEffects) {
|
|
100
|
-
const from = new URL(filesystemEffect, fnFileUrl);
|
|
101
|
-
const relativeUrl = urlToRelativeUrl(from, fnFileUrl);
|
|
102
|
-
const toUrl = new URL(relativeUrl, fsSideEffectDirectoryUrl);
|
|
103
|
-
const atStartState = getFileState(from);
|
|
104
|
-
const onFileSystemSideEffect = (fsSideEffect) => {
|
|
105
|
-
const last = sideEffects.pop();
|
|
106
|
-
sideEffects.push(fsSideEffect);
|
|
107
|
-
sideEffects.push(last);
|
|
108
|
-
};
|
|
109
|
-
finallyCallbackSet.add(() => {
|
|
110
|
-
const nowState = getFileState(from);
|
|
111
|
-
if (atStartState.found && !nowState.found) {
|
|
112
|
-
onFileSystemSideEffect({
|
|
113
|
-
type: `remove file "${relativeUrl}"`,
|
|
114
|
-
value: atStartState.content,
|
|
115
|
-
});
|
|
116
|
-
if (restoreFilesystem) {
|
|
117
|
-
writeFileSync(from, atStartState.content);
|
|
118
|
-
}
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
// we use same type because we don't want to differentiate between
|
|
122
|
-
// - writing file for the 1st time
|
|
123
|
-
// - updating file content
|
|
124
|
-
// the important part is the file content in the end of the function execution
|
|
125
|
-
if (
|
|
126
|
-
(!atStartState.found && nowState.found) ||
|
|
127
|
-
atStartState.content !== nowState.content ||
|
|
128
|
-
atStartState.mtimeMs !== nowState.mtimeMs
|
|
129
|
-
) {
|
|
130
|
-
if (filesystemEffectsDirectory) {
|
|
131
|
-
writeFileSync(toUrl, nowState.content);
|
|
132
|
-
onFileSystemSideEffect({
|
|
133
|
-
type: `write file "${relativeUrl}" (see ./${fsSideEffectsDirectoryRelativeUrl}${relativeUrl})`,
|
|
134
|
-
value: nowState.content,
|
|
135
|
-
});
|
|
136
|
-
} else {
|
|
137
|
-
onFileSystemSideEffect({
|
|
138
|
-
type: `write file "${relativeUrl}"`,
|
|
139
|
-
value: nowState.content,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
if (restoreFilesystem) {
|
|
143
|
-
if (atStartState.found) {
|
|
144
|
-
if (atStartState.content !== nowState.content) {
|
|
145
|
-
writeFileSync(from, atStartState.content);
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
removeFileSync(from);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
// file is exactly the same
|
|
154
|
-
// function did not have any effect on the file
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
let returnedPromise = false;
|
|
159
|
-
try {
|
|
160
|
-
const returnValue = fn();
|
|
161
|
-
if (returnValue && returnValue.then) {
|
|
162
|
-
returnedPromise = returnValue.then(
|
|
163
|
-
(value) => {
|
|
164
|
-
onResult(value, true);
|
|
165
|
-
onFinally();
|
|
166
|
-
},
|
|
167
|
-
(e) => {
|
|
168
|
-
onError(e, true);
|
|
169
|
-
onFinally();
|
|
170
|
-
},
|
|
171
|
-
);
|
|
172
|
-
return returnedPromise;
|
|
173
|
-
}
|
|
174
|
-
onResult(returnValue);
|
|
175
|
-
return null;
|
|
176
|
-
} catch (e) {
|
|
177
|
-
onError(e);
|
|
178
|
-
return null;
|
|
179
|
-
} finally {
|
|
180
|
-
if (returnedPromise) {
|
|
181
|
-
return returnedPromise;
|
|
182
|
-
}
|
|
183
|
-
onFinally();
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const stringifySideEffects = (
|
|
188
|
-
sideEffects,
|
|
189
|
-
{ rootDirectoryUrl, filesystemEffectsDirectory },
|
|
190
|
-
) => {
|
|
191
|
-
let string = "";
|
|
192
|
-
let index = 0;
|
|
193
|
-
for (const sideEffect of sideEffects) {
|
|
194
|
-
if (string) {
|
|
195
|
-
string += "\n\n";
|
|
196
|
-
}
|
|
197
|
-
string += `${index + 1}. ${sideEffect.type}`;
|
|
198
|
-
let value = sideEffect.value;
|
|
199
|
-
if (sideEffect.type.startsWith("console.")) {
|
|
200
|
-
value = replaceFluctuatingValues(value, {
|
|
201
|
-
stringType: "console",
|
|
202
|
-
rootDirectoryUrl,
|
|
203
|
-
});
|
|
204
|
-
string += "\n";
|
|
205
|
-
string += value;
|
|
206
|
-
} else if (
|
|
207
|
-
sideEffect.type.startsWith("remove file") ||
|
|
208
|
-
sideEffect.type.startsWith("write file")
|
|
209
|
-
) {
|
|
210
|
-
if (!filesystemEffectsDirectory) {
|
|
211
|
-
string += "\n";
|
|
212
|
-
string += value;
|
|
213
|
-
}
|
|
214
|
-
} else if (sideEffect.type === "throw" || sideEffect.type === "reject") {
|
|
215
|
-
value = replaceFluctuatingValues(value.stack, {
|
|
216
|
-
stringType: "error",
|
|
217
|
-
});
|
|
218
|
-
string += "\n";
|
|
219
|
-
string += value;
|
|
220
|
-
} else if (sideEffect.type === "return" || sideEffect.type === "resolve") {
|
|
221
|
-
value =
|
|
222
|
-
value === undefined
|
|
223
|
-
? undefined
|
|
224
|
-
: replaceFluctuatingValues(JSON.stringify(value, null, " "), {
|
|
225
|
-
stringType: "json",
|
|
226
|
-
rootDirectoryUrl,
|
|
227
|
-
});
|
|
228
|
-
string += "\n";
|
|
229
|
-
string += value;
|
|
230
|
-
} else {
|
|
231
|
-
string += "\n";
|
|
232
|
-
string += value;
|
|
233
|
-
}
|
|
234
|
-
index++;
|
|
235
|
-
}
|
|
236
|
-
return string;
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const getFileState = (fileUrl) => {
|
|
240
|
-
try {
|
|
241
|
-
const fileContent = readFileSync(fileUrl);
|
|
242
|
-
const { mtimeMs } = readEntryStatSync(fileUrl);
|
|
243
|
-
return {
|
|
244
|
-
found: true,
|
|
245
|
-
mtimeMs,
|
|
246
|
-
content: String(fileContent),
|
|
247
|
-
};
|
|
248
|
-
} catch (e) {
|
|
249
|
-
if (e.code === "ENOENT") {
|
|
250
|
-
return {
|
|
251
|
-
found: false,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
throw e;
|
|
255
|
-
}
|
|
256
|
-
};
|