@jsenv/snapshot 2.6.0 → 2.6.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/package.json +6 -6
- package/src/filesystem_well_known_values.js +249 -0
- package/src/function_side_effects/common_ancestor_path.js +13 -0
- package/src/function_side_effects/common_ancestor_path.test.mjs +14 -0
- package/src/function_side_effects/function_side_effects_collector.js +4 -1
- package/src/function_side_effects/function_side_effects_renderer.js +4 -1
- package/src/function_side_effects/function_side_effects_snapshot.js +134 -28
- package/src/function_side_effects/group_file_side_effects_per_directory.js +114 -0
- package/src/function_side_effects/spy_console_calls.js +27 -1
- package/src/function_side_effects/spy_filesystem_calls.js +27 -6
- package/src/main.js +1 -0
- package/src/replace_fluctuating_values.js +29 -162
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/snapshot",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Snapshot testing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"test": "node --conditions=development ./scripts/test.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@jsenv/assert": "4.1.
|
|
38
|
-
"@jsenv/ast": "6.2.
|
|
39
|
-
"@jsenv/exception": "1.0.
|
|
40
|
-
"@jsenv/filesystem": "4.9.
|
|
41
|
-
"@jsenv/urls": "2.4.
|
|
37
|
+
"@jsenv/assert": "4.1.7",
|
|
38
|
+
"@jsenv/ast": "6.2.9",
|
|
39
|
+
"@jsenv/exception": "1.0.1",
|
|
40
|
+
"@jsenv/filesystem": "4.9.5",
|
|
41
|
+
"@jsenv/urls": "2.4.1",
|
|
42
42
|
"@jsenv/utils": "2.1.2",
|
|
43
43
|
"pixelmatch": "6.0.0",
|
|
44
44
|
"prettier": "3.3.3",
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensurePathnameTrailingSlash,
|
|
3
|
+
removePathnameTrailingSlash,
|
|
4
|
+
} from "@jsenv/urls";
|
|
5
|
+
import { escapeRegexpSpecialChars } from "@jsenv/utils/src/string/escape_regexp_special_chars.js";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
|
|
10
|
+
// remember this: https://stackoverflow.com/a/31976060/24573072
|
|
11
|
+
// when deciding which replacement to use and willBeWrittenOnFilesystem is true
|
|
12
|
+
const WELL_KNOWN_ROOT = {
|
|
13
|
+
name: "root",
|
|
14
|
+
getReplacement: ({ preferFileUrl }) => {
|
|
15
|
+
if (preferFileUrl) {
|
|
16
|
+
return "file:///[root]";
|
|
17
|
+
}
|
|
18
|
+
return "[root]";
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const WELL_KNOWN_HOMEDIR = {
|
|
22
|
+
name: "homedir",
|
|
23
|
+
getReplacement: ({ willBeWrittenOnFilesystem, preferFileUrl }) => {
|
|
24
|
+
if (willBeWrittenOnFilesystem) {
|
|
25
|
+
if (preferFileUrl) {
|
|
26
|
+
return "file:///[homedir]";
|
|
27
|
+
}
|
|
28
|
+
return "[homedir]";
|
|
29
|
+
}
|
|
30
|
+
if (preferFileUrl) {
|
|
31
|
+
return "file:///~";
|
|
32
|
+
}
|
|
33
|
+
return "~";
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const WELL_KNOWN_CWD = {
|
|
37
|
+
name: "cwd",
|
|
38
|
+
getReplacement: ({ preferFileUrl }) => {
|
|
39
|
+
if (preferFileUrl) {
|
|
40
|
+
return "file:///cwd()";
|
|
41
|
+
}
|
|
42
|
+
return "cwd()";
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const createWellKnownPackage = (name) => {
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
getReplacement: () => name,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export const createWellKnown = (name, replacement = name) => {
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
getReplacement: () => replacement,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const createReplaceFilesystemWellKnownValues = ({
|
|
59
|
+
rootDirectoryUrl,
|
|
60
|
+
// for unit tests
|
|
61
|
+
isWindows = process.platform === "win32",
|
|
62
|
+
ancestorPackagesDisabled,
|
|
63
|
+
ancestorPackagesRootDirectoryUrl = "file:///",
|
|
64
|
+
homedirDisabled,
|
|
65
|
+
cwdDisabled,
|
|
66
|
+
cwdUrl,
|
|
67
|
+
cwdPath = process.cwd(),
|
|
68
|
+
} = {}) => {
|
|
69
|
+
const wellKownUrlArray = [];
|
|
70
|
+
const wellKnownPathArray = [];
|
|
71
|
+
const addWellKnownFileUrl = (url, wellKnown, { position = "end" } = {}) => {
|
|
72
|
+
url = new URL(url);
|
|
73
|
+
const urlWithoutTrailingSlash = String(removePathnameTrailingSlash(url));
|
|
74
|
+
const wellKnownUrl = {
|
|
75
|
+
url: urlWithoutTrailingSlash,
|
|
76
|
+
replace: (string, { willBeWrittenOnFilesystem }) => {
|
|
77
|
+
const replacement = wellKnown.getReplacement({
|
|
78
|
+
preferFileUrl: true,
|
|
79
|
+
willBeWrittenOnFilesystem,
|
|
80
|
+
});
|
|
81
|
+
return string.replaceAll(urlWithoutTrailingSlash, replacement);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const path =
|
|
85
|
+
String(url) === String(cwdUrl)
|
|
86
|
+
? cwdPath
|
|
87
|
+
: fileURLToPath(urlWithoutTrailingSlash);
|
|
88
|
+
const windowPathRegex = new RegExp(
|
|
89
|
+
`${escapeRegexpSpecialChars(path)}(((?:\\\\(?:[\\w !#()-]+|[.]{1,2})+)*)(?:\\\\)?)`,
|
|
90
|
+
"gm",
|
|
91
|
+
);
|
|
92
|
+
const wellKnownPath = {
|
|
93
|
+
path,
|
|
94
|
+
replace: isWindows
|
|
95
|
+
? (string, { willBeWrittenOnFilesystem }) => {
|
|
96
|
+
const replacement = wellKnown.getReplacement({
|
|
97
|
+
willBeWrittenOnFilesystem,
|
|
98
|
+
});
|
|
99
|
+
return string.replaceAll(windowPathRegex, (match, after) => {
|
|
100
|
+
return `${replacement}${after.replaceAll("\\", "/")}`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
: (string, { willBeWrittenOnFilesystem }) => {
|
|
104
|
+
const replacement = wellKnown.getReplacement({
|
|
105
|
+
willBeWrittenOnFilesystem,
|
|
106
|
+
});
|
|
107
|
+
return string.replaceAll(path, replacement);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
if (position === "start") {
|
|
111
|
+
wellKownUrlArray.unshift(wellKnownUrl);
|
|
112
|
+
wellKnownPathArray.unshift(wellKnownPath);
|
|
113
|
+
} else {
|
|
114
|
+
wellKownUrlArray.push(wellKnownUrl);
|
|
115
|
+
wellKnownPathArray.push(wellKnownPath);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
if (rootDirectoryUrl) {
|
|
119
|
+
addWellKnownFileUrl(rootDirectoryUrl, WELL_KNOWN_ROOT);
|
|
120
|
+
}
|
|
121
|
+
/*
|
|
122
|
+
* When running code inside a node project ancestor packages
|
|
123
|
+
* should make things super predictible because
|
|
124
|
+
* it will use a package.json name field
|
|
125
|
+
* to replace files urls
|
|
126
|
+
* And uses the highest ancestor package so that even if the file
|
|
127
|
+
* is executed once within a package then outside that package
|
|
128
|
+
* the replace value remains predictible as the highest package is used
|
|
129
|
+
* The highest package is used because it's pushed first by
|
|
130
|
+
* addWellKnownFileUrl
|
|
131
|
+
*/
|
|
132
|
+
ancestor_packages: {
|
|
133
|
+
if (ancestorPackagesDisabled) {
|
|
134
|
+
break ancestor_packages;
|
|
135
|
+
}
|
|
136
|
+
const ancestorPackages = [];
|
|
137
|
+
const cwd = cwdPath || process.cwd();
|
|
138
|
+
const cwdUrl = ensurePathnameTrailingSlash(pathToFileURL(cwd));
|
|
139
|
+
let currentUrl = cwdUrl;
|
|
140
|
+
while (currentUrl.href !== ancestorPackagesRootDirectoryUrl) {
|
|
141
|
+
const packageFileUrl = new URL("package.json", currentUrl);
|
|
142
|
+
const packageDirectoryUrl = currentUrl;
|
|
143
|
+
currentUrl = new URL(getParentUrl(currentUrl));
|
|
144
|
+
let packageFileContent;
|
|
145
|
+
try {
|
|
146
|
+
packageFileContent = readFileSync(packageFileUrl);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
if (e.code === "ENOENT") {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
let packageObject;
|
|
154
|
+
try {
|
|
155
|
+
packageObject = JSON.parse(packageFileContent);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const packageName = packageObject.name;
|
|
160
|
+
ancestorPackages.unshift({
|
|
161
|
+
packageDirectoryUrl,
|
|
162
|
+
packageName,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const ancestorPackage of ancestorPackages) {
|
|
166
|
+
addWellKnownFileUrl(
|
|
167
|
+
ancestorPackage.packageDirectoryUrl,
|
|
168
|
+
createWellKnownPackage(ancestorPackage.packageName),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
home_dir: {
|
|
173
|
+
if (homedirDisabled) {
|
|
174
|
+
break home_dir;
|
|
175
|
+
}
|
|
176
|
+
const homedirPath = homedir();
|
|
177
|
+
const homedirUrl = pathToFileURL(homedirPath);
|
|
178
|
+
addWellKnownFileUrl(homedirUrl, WELL_KNOWN_HOMEDIR);
|
|
179
|
+
}
|
|
180
|
+
process_cwd: {
|
|
181
|
+
if (cwdDisabled) {
|
|
182
|
+
break process_cwd;
|
|
183
|
+
}
|
|
184
|
+
// we fallback on process.cwd()
|
|
185
|
+
// but it's brittle because a file might be execute from anywhere
|
|
186
|
+
// so it should be the last resort
|
|
187
|
+
cwdUrl = cwdUrl || pathToFileURL(cwdPath);
|
|
188
|
+
addWellKnownFileUrl(cwdUrl, WELL_KNOWN_CWD);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const replaceFileUrls = (string, { willBeWrittenOnFilesystem }) => {
|
|
192
|
+
for (const wellKownUrl of wellKownUrlArray) {
|
|
193
|
+
const replaceResult = wellKownUrl.replace(string, {
|
|
194
|
+
willBeWrittenOnFilesystem,
|
|
195
|
+
});
|
|
196
|
+
if (replaceResult !== string) {
|
|
197
|
+
return replaceResult;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return string;
|
|
201
|
+
};
|
|
202
|
+
const replaceFilePaths = (string, { willBeWrittenOnFilesystem }) => {
|
|
203
|
+
for (const wellKownPath of wellKnownPathArray) {
|
|
204
|
+
const replaceResult = wellKownPath.replace(string, {
|
|
205
|
+
willBeWrittenOnFilesystem,
|
|
206
|
+
});
|
|
207
|
+
if (replaceResult !== string) {
|
|
208
|
+
return replaceResult;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return string;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const replaceFilesystemWellKnownValues = (
|
|
215
|
+
string,
|
|
216
|
+
{ willBeWrittenOnFilesystem = true } = {},
|
|
217
|
+
) => {
|
|
218
|
+
const isUrl = typeof string === "object" && typeof string.href === "string";
|
|
219
|
+
if (isUrl) {
|
|
220
|
+
string = string.href;
|
|
221
|
+
}
|
|
222
|
+
string = replaceFileUrls(string, { willBeWrittenOnFilesystem });
|
|
223
|
+
string = replaceFilePaths(string, { willBeWrittenOnFilesystem });
|
|
224
|
+
return string;
|
|
225
|
+
};
|
|
226
|
+
replaceFilesystemWellKnownValues.addWellKnownFileUrl = addWellKnownFileUrl;
|
|
227
|
+
return replaceFilesystemWellKnownValues;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const getParentUrl = (url) => {
|
|
231
|
+
url = String(url);
|
|
232
|
+
// With node.js new URL('../', 'file:///C:/').href
|
|
233
|
+
// returns "file:///C:/" instead of "file:///"
|
|
234
|
+
const resource = url.slice("file://".length);
|
|
235
|
+
const slashLastIndex = resource.lastIndexOf("/");
|
|
236
|
+
if (slashLastIndex === -1) {
|
|
237
|
+
return url;
|
|
238
|
+
}
|
|
239
|
+
const lastCharIndex = resource.length - 1;
|
|
240
|
+
if (slashLastIndex === lastCharIndex) {
|
|
241
|
+
const slashBeforeLastIndex = resource.lastIndexOf("/", slashLastIndex - 1);
|
|
242
|
+
if (slashBeforeLastIndex === -1) {
|
|
243
|
+
return url;
|
|
244
|
+
}
|
|
245
|
+
return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return `file://${resource.slice(0, slashLastIndex + 1)}`;
|
|
249
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getCommonPathname } from "@jsenv/urls";
|
|
2
|
+
|
|
3
|
+
export const findCommonAncestorPath = (paths, castAsPath) => {
|
|
4
|
+
return paths.reduce((a, b) => {
|
|
5
|
+
if (typeof a !== "string" && castAsPath) a = castAsPath(a);
|
|
6
|
+
if (typeof b !== "string" && castAsPath) b = castAsPath(b);
|
|
7
|
+
if (a === b) {
|
|
8
|
+
return a;
|
|
9
|
+
}
|
|
10
|
+
const common = getCommonPathname(a, b);
|
|
11
|
+
return common;
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { assert } from "@jsenv/assert";
|
|
2
|
+
import { findCommonAncestorPath } from "./common_ancestor_path.js";
|
|
3
|
+
|
|
4
|
+
const test = (paths, expect) => {
|
|
5
|
+
const actual = findCommonAncestorPath(paths);
|
|
6
|
+
assert({ actual, expect });
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
test(["/a/b/c/d", "/a/b/c/d/"], "/a/b/c/d");
|
|
10
|
+
test(["/a/b/c", "/a/b/d", "/a/x/y"], "/a/");
|
|
11
|
+
test(["/a/b/", "/a/b/"], "/a/b/");
|
|
12
|
+
test(["/a", "/a/b/c"], "/a");
|
|
13
|
+
test(["/a/b/c", "/a"], "/a/");
|
|
14
|
+
test(["/a/b/c"], "/a/b/c");
|
|
@@ -14,6 +14,7 @@ export const collectFunctionSideEffects = (
|
|
|
14
14
|
const sideEffects = [];
|
|
15
15
|
const addSideEffect = (sideEffect) => {
|
|
16
16
|
sideEffects.push(sideEffect);
|
|
17
|
+
return sideEffect;
|
|
17
18
|
};
|
|
18
19
|
const finallyCallbackSet = new Set();
|
|
19
20
|
for (const sideEffectDetector of sideEffectDetectors) {
|
|
@@ -36,7 +37,6 @@ export const collectFunctionSideEffects = (
|
|
|
36
37
|
`collectFunctionSideEffects called while other function(s) side effects are collected`,
|
|
37
38
|
);
|
|
38
39
|
}
|
|
39
|
-
functionExecutingCount++;
|
|
40
40
|
|
|
41
41
|
const onCatch = (valueThrow) => {
|
|
42
42
|
sideEffects.push({
|
|
@@ -98,6 +98,7 @@ export const collectFunctionSideEffects = (
|
|
|
98
98
|
});
|
|
99
99
|
};
|
|
100
100
|
const onFinally = () => {
|
|
101
|
+
delete process.env.SNAPSHOTING_FUNCTION_SIDE_EFFECTS;
|
|
101
102
|
functionExecutingCount--;
|
|
102
103
|
for (const finallyCallback of finallyCallbackSet) {
|
|
103
104
|
finallyCallback();
|
|
@@ -105,6 +106,8 @@ export const collectFunctionSideEffects = (
|
|
|
105
106
|
finallyCallbackSet.clear();
|
|
106
107
|
};
|
|
107
108
|
|
|
109
|
+
process.env.SNAPSHOTING_FUNCTION_SIDE_EFFECTS = "1";
|
|
110
|
+
functionExecutingCount++;
|
|
108
111
|
let returnedPromise = false;
|
|
109
112
|
try {
|
|
110
113
|
const valueReturned = fn();
|
|
@@ -2,6 +2,9 @@ export const renderSideEffects = (sideEffects) => {
|
|
|
2
2
|
let string = "";
|
|
3
3
|
let index = 0;
|
|
4
4
|
for (const sideEffect of sideEffects) {
|
|
5
|
+
if (sideEffect.skippable) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
5
8
|
if (string) {
|
|
6
9
|
string += "\n\n";
|
|
7
10
|
}
|
|
@@ -17,7 +20,7 @@ export const renderSideEffects = (sideEffects) => {
|
|
|
17
20
|
return string;
|
|
18
21
|
};
|
|
19
22
|
|
|
20
|
-
export const wrapIntoMarkdownBlock = (value, blockName) => {
|
|
23
|
+
export const wrapIntoMarkdownBlock = (value, blockName = "") => {
|
|
21
24
|
const start = "```";
|
|
22
25
|
const end = "```";
|
|
23
26
|
return `${start}${blockName}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { writeFileSync } from "@jsenv/filesystem";
|
|
1
|
+
import { readDirectorySync, writeFileSync } from "@jsenv/filesystem";
|
|
2
2
|
import {
|
|
3
3
|
ensurePathnameTrailingSlash,
|
|
4
|
+
urlIsInsideOf,
|
|
4
5
|
urlToExtension,
|
|
5
6
|
urlToRelativeUrl,
|
|
6
7
|
} from "@jsenv/urls";
|
|
@@ -8,12 +9,17 @@ import {
|
|
|
8
9
|
takeDirectorySnapshot,
|
|
9
10
|
takeFileSnapshot,
|
|
10
11
|
} from "../filesystem_snapshot.js";
|
|
12
|
+
import {
|
|
13
|
+
createReplaceFilesystemWellKnownValues,
|
|
14
|
+
createWellKnown,
|
|
15
|
+
} from "../filesystem_well_known_values.js";
|
|
11
16
|
import { replaceFluctuatingValues } from "../replace_fluctuating_values.js";
|
|
12
17
|
import { collectFunctionSideEffects } from "./function_side_effects_collector.js";
|
|
13
18
|
import {
|
|
14
19
|
renderSideEffects,
|
|
15
20
|
wrapIntoMarkdownBlock,
|
|
16
21
|
} from "./function_side_effects_renderer.js";
|
|
22
|
+
import { groupFileSideEffectsPerDirectory } from "./group_file_side_effects_per_directory.js";
|
|
17
23
|
import { spyConsoleCalls } from "./spy_console_calls.js";
|
|
18
24
|
import { spyFilesystemCalls } from "./spy_filesystem_calls.js";
|
|
19
25
|
|
|
@@ -27,13 +33,8 @@ const consoleEffectsDefault = {
|
|
|
27
33
|
|
|
28
34
|
export const snapshotFunctionSideEffects = (
|
|
29
35
|
fn,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
rootDirectoryUrl = new URL("./", fnFileUrl),
|
|
34
|
-
consoleEffects = true,
|
|
35
|
-
filesystemEffects = true,
|
|
36
|
-
} = {},
|
|
36
|
+
sideEffectFileUrl,
|
|
37
|
+
{ consoleEffects = true, filesystemEffects = true, rootDirectoryUrl } = {},
|
|
37
38
|
) => {
|
|
38
39
|
if (consoleEffects === true) {
|
|
39
40
|
consoleEffects = {};
|
|
@@ -41,7 +42,10 @@ export const snapshotFunctionSideEffects = (
|
|
|
41
42
|
if (filesystemEffects === true) {
|
|
42
43
|
filesystemEffects = {};
|
|
43
44
|
}
|
|
44
|
-
const
|
|
45
|
+
const replaceFilesystemWellKnownValues =
|
|
46
|
+
createReplaceFilesystemWellKnownValues({
|
|
47
|
+
rootDirectoryUrl,
|
|
48
|
+
});
|
|
45
49
|
const sideEffectFileSnapshot = takeFileSnapshot(sideEffectFileUrl);
|
|
46
50
|
const callbackSet = new Set();
|
|
47
51
|
const sideEffectDetectors = [
|
|
@@ -60,7 +64,7 @@ export const snapshotFunctionSideEffects = (
|
|
|
60
64
|
text: wrapIntoMarkdownBlock(
|
|
61
65
|
replaceFluctuatingValues(message, {
|
|
62
66
|
stringType: "console",
|
|
63
|
-
|
|
67
|
+
replaceFilesystemWellKnownValues,
|
|
64
68
|
}),
|
|
65
69
|
"console",
|
|
66
70
|
),
|
|
@@ -80,6 +84,34 @@ export const snapshotFunctionSideEffects = (
|
|
|
80
84
|
log: (message) => {
|
|
81
85
|
onConsole("log", message);
|
|
82
86
|
},
|
|
87
|
+
stdout: (message) => {
|
|
88
|
+
addSideEffect({
|
|
89
|
+
type: `process:stdout`,
|
|
90
|
+
value: message,
|
|
91
|
+
label: `process.stdout`,
|
|
92
|
+
text: wrapIntoMarkdownBlock(
|
|
93
|
+
replaceFluctuatingValues(message, {
|
|
94
|
+
stringType: "console",
|
|
95
|
+
replaceFilesystemWellKnownValues,
|
|
96
|
+
}),
|
|
97
|
+
"console",
|
|
98
|
+
),
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
stderr: (message) => {
|
|
102
|
+
addSideEffect({
|
|
103
|
+
type: `process:stderr`,
|
|
104
|
+
value: message,
|
|
105
|
+
label: `process.stderr`,
|
|
106
|
+
text: wrapIntoMarkdownBlock(
|
|
107
|
+
replaceFluctuatingValues(message, {
|
|
108
|
+
stringType: "console",
|
|
109
|
+
replaceFilesystemWellKnownValues,
|
|
110
|
+
}),
|
|
111
|
+
"console",
|
|
112
|
+
),
|
|
113
|
+
});
|
|
114
|
+
},
|
|
83
115
|
},
|
|
84
116
|
{
|
|
85
117
|
preventConsoleSideEffects: prevent,
|
|
@@ -102,7 +134,21 @@ export const snapshotFunctionSideEffects = (
|
|
|
102
134
|
...filesystemEffects,
|
|
103
135
|
};
|
|
104
136
|
let writeFile;
|
|
105
|
-
const { preserve, outDirectory } =
|
|
137
|
+
const { include, preserve, baseDirectory, outDirectory } =
|
|
138
|
+
filesystemEffects;
|
|
139
|
+
if (baseDirectory) {
|
|
140
|
+
replaceFilesystemWellKnownValues.addWellKnownFileUrl(
|
|
141
|
+
baseDirectory,
|
|
142
|
+
createWellKnown("base"),
|
|
143
|
+
{ position: "start" },
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const renderLabel = (label) => {
|
|
147
|
+
return replaceFluctuatingValues(label, {
|
|
148
|
+
replaceFilesystemWellKnownValues,
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
106
152
|
if (outDirectory) {
|
|
107
153
|
const fsEffectsOutDirectoryUrl = ensurePathnameTrailingSlash(
|
|
108
154
|
new URL(outDirectory, sideEffectFileUrl),
|
|
@@ -110,12 +156,53 @@ export const snapshotFunctionSideEffects = (
|
|
|
110
156
|
const fsEffectsOutDirectorySnapshot = takeDirectorySnapshot(
|
|
111
157
|
fsEffectsOutDirectoryUrl,
|
|
112
158
|
);
|
|
113
|
-
const fsEffectsOutDirectoryRelativeUrl = urlToRelativeUrl(
|
|
114
|
-
fsEffectsOutDirectoryUrl,
|
|
115
|
-
sideEffectFileUrl,
|
|
116
|
-
);
|
|
117
159
|
const writeFileCallbackSet = new Set();
|
|
118
|
-
|
|
160
|
+
const getFilesystemActionInfo = (action, url) => {
|
|
161
|
+
let toUrl;
|
|
162
|
+
let urlDisplayed = url;
|
|
163
|
+
if (baseDirectory) {
|
|
164
|
+
urlDisplayed = urlToRelativeUrl(url, baseDirectory, {
|
|
165
|
+
preferRelativeNotation: true,
|
|
166
|
+
});
|
|
167
|
+
if (
|
|
168
|
+
url.href === baseDirectory.href ||
|
|
169
|
+
urlIsInsideOf(url, baseDirectory)
|
|
170
|
+
) {
|
|
171
|
+
const toRelativeUrl = urlToRelativeUrl(
|
|
172
|
+
url,
|
|
173
|
+
baseDirectory,
|
|
174
|
+
);
|
|
175
|
+
toUrl = new URL(toRelativeUrl, fsEffectsOutDirectoryUrl);
|
|
176
|
+
} else {
|
|
177
|
+
const toRelativeUrl =
|
|
178
|
+
replaceFilesystemWellKnownValues(url);
|
|
179
|
+
toUrl = new URL(toRelativeUrl, fsEffectsOutDirectoryUrl);
|
|
180
|
+
}
|
|
181
|
+
// otherwise we need to replace the url with well known
|
|
182
|
+
} else {
|
|
183
|
+
const toRelativeUrl = replaceFilesystemWellKnownValues(url);
|
|
184
|
+
toUrl = new URL(toRelativeUrl, fsEffectsOutDirectoryUrl);
|
|
185
|
+
}
|
|
186
|
+
const toUrlDisplayed = urlToRelativeUrl(
|
|
187
|
+
toUrl,
|
|
188
|
+
sideEffectFileUrl,
|
|
189
|
+
{ preferRelativeNotation: true },
|
|
190
|
+
);
|
|
191
|
+
return {
|
|
192
|
+
toUrl,
|
|
193
|
+
label: renderLabel(
|
|
194
|
+
`${action} "${urlDisplayed}" (see ${toUrlDisplayed})`,
|
|
195
|
+
),
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
callbackSet.add((sideEffects) => {
|
|
200
|
+
// gather all file side effect next to each other
|
|
201
|
+
// collapse them if they have a shared ancestor
|
|
202
|
+
groupFileSideEffectsPerDirectory(sideEffects, {
|
|
203
|
+
baseDirectory,
|
|
204
|
+
getFilesystemActionInfo,
|
|
205
|
+
});
|
|
119
206
|
for (const writeFileCallback of writeFileCallbackSet) {
|
|
120
207
|
writeFileCallback();
|
|
121
208
|
}
|
|
@@ -123,25 +210,32 @@ export const snapshotFunctionSideEffects = (
|
|
|
123
210
|
fsEffectsOutDirectorySnapshot.compare();
|
|
124
211
|
});
|
|
125
212
|
writeFile = (url, content) => {
|
|
126
|
-
const
|
|
127
|
-
|
|
213
|
+
const { toUrl, label } = getFilesystemActionInfo(
|
|
214
|
+
"write file",
|
|
215
|
+
url,
|
|
216
|
+
);
|
|
128
217
|
writeFileCallbackSet.add(() => {
|
|
129
218
|
writeFileSync(toUrl, content);
|
|
130
219
|
});
|
|
131
220
|
addSideEffect({
|
|
132
221
|
type: "fs:write_file",
|
|
133
|
-
value: {
|
|
134
|
-
label
|
|
222
|
+
value: { url: String(url), content },
|
|
223
|
+
label,
|
|
135
224
|
text: null,
|
|
136
225
|
});
|
|
137
226
|
};
|
|
138
227
|
} else {
|
|
139
228
|
writeFile = (url, content) => {
|
|
140
|
-
|
|
229
|
+
let urlDisplayed = url;
|
|
230
|
+
if (baseDirectory) {
|
|
231
|
+
urlDisplayed = urlToRelativeUrl(url, baseDirectory, {
|
|
232
|
+
preferRelativeNotation: true,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
141
235
|
addSideEffect({
|
|
142
236
|
type: "fs:write_file",
|
|
143
|
-
value: {
|
|
144
|
-
label: `write file "${
|
|
237
|
+
value: { url: String(url), content },
|
|
238
|
+
label: renderLabel(`write file "${urlDisplayed}"`),
|
|
145
239
|
text: wrapIntoMarkdownBlock(
|
|
146
240
|
content,
|
|
147
241
|
urlToExtension(url).slice(1),
|
|
@@ -153,16 +247,28 @@ export const snapshotFunctionSideEffects = (
|
|
|
153
247
|
{
|
|
154
248
|
writeFile,
|
|
155
249
|
writeDirectory: (url) => {
|
|
156
|
-
const
|
|
157
|
-
addSideEffect({
|
|
250
|
+
const writeDirectorySideEffect = addSideEffect({
|
|
158
251
|
type: "fs:write_directory",
|
|
159
|
-
value: {
|
|
160
|
-
label: `write directory "${
|
|
252
|
+
value: { url: String(url) },
|
|
253
|
+
label: renderLabel(`write directory "${url}"`),
|
|
161
254
|
text: null,
|
|
162
255
|
});
|
|
256
|
+
// if directory ends up with something inside we'll not report
|
|
257
|
+
// this side effect because:
|
|
258
|
+
// - it was likely created to write the file
|
|
259
|
+
// - the file creation will be reported and implies directory creation
|
|
260
|
+
filesystemSpy.addBeforeUndoCallback(() => {
|
|
261
|
+
try {
|
|
262
|
+
const dirContent = readDirectorySync(url);
|
|
263
|
+
if (dirContent.length) {
|
|
264
|
+
writeDirectorySideEffect.skippable = true;
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {}
|
|
267
|
+
});
|
|
163
268
|
},
|
|
164
269
|
},
|
|
165
270
|
{
|
|
271
|
+
include,
|
|
166
272
|
undoFilesystemSideEffects: !preserve,
|
|
167
273
|
},
|
|
168
274
|
);
|
|
@@ -176,7 +282,7 @@ export const snapshotFunctionSideEffects = (
|
|
|
176
282
|
];
|
|
177
283
|
const onSideEffectsCollected = (sideEffects) => {
|
|
178
284
|
for (const callback of callbackSet) {
|
|
179
|
-
callback();
|
|
285
|
+
callback(sideEffects);
|
|
180
286
|
}
|
|
181
287
|
callbackSet.clear();
|
|
182
288
|
sideEffectFileSnapshot.update(renderSideEffects(sideEffects), {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { findCommonAncestorPath } from "./common_ancestor_path.js";
|
|
3
|
+
|
|
4
|
+
export const groupFileSideEffectsPerDirectory = (
|
|
5
|
+
sideEffects,
|
|
6
|
+
{ getFilesystemActionInfo },
|
|
7
|
+
) => {
|
|
8
|
+
const groupArray = groupFileTogether(sideEffects);
|
|
9
|
+
|
|
10
|
+
const convertToPathname = (writeFileSideEffect) => {
|
|
11
|
+
return new URL(writeFileSideEffect.value.url).pathname;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
for (const group of groupArray) {
|
|
15
|
+
if (group.id !== "file") {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const fileEffectArray = group.values;
|
|
19
|
+
if (fileEffectArray.length < 2) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const commonAncestorPath = findCommonAncestorPath(
|
|
23
|
+
fileEffectArray,
|
|
24
|
+
convertToPathname,
|
|
25
|
+
);
|
|
26
|
+
const firstEffect = fileEffectArray[0];
|
|
27
|
+
const firstEffectIndex = sideEffects.indexOf(firstEffect);
|
|
28
|
+
const commonAncestorUrl = pathToFileURL(commonAncestorPath);
|
|
29
|
+
const numberOfFiles = fileEffectArray.length;
|
|
30
|
+
const { label } = getFilesystemActionInfo(
|
|
31
|
+
`write ${numberOfFiles} files into`,
|
|
32
|
+
commonAncestorUrl,
|
|
33
|
+
);
|
|
34
|
+
for (const fileEffect of fileEffectArray) {
|
|
35
|
+
sideEffects.splice(sideEffects.indexOf(fileEffect), 1);
|
|
36
|
+
}
|
|
37
|
+
sideEffects.splice(firstEffectIndex, 0, {
|
|
38
|
+
type: "fs:write_file",
|
|
39
|
+
label,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const groupBy = (array, groupCallback) => {
|
|
45
|
+
let i = 0;
|
|
46
|
+
const groupArray = [];
|
|
47
|
+
let currentGroup = null;
|
|
48
|
+
while (i < array.length) {
|
|
49
|
+
const value = array[i];
|
|
50
|
+
i++;
|
|
51
|
+
let ignoreCalled = false;
|
|
52
|
+
let ignore = () => {
|
|
53
|
+
ignoreCalled = true;
|
|
54
|
+
};
|
|
55
|
+
const groupId = groupCallback(value, { ignore });
|
|
56
|
+
if (ignoreCalled) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (currentGroup === null) {
|
|
60
|
+
currentGroup = {
|
|
61
|
+
id: groupId,
|
|
62
|
+
values: [value],
|
|
63
|
+
};
|
|
64
|
+
groupArray.push(currentGroup);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (groupId === currentGroup.id) {
|
|
68
|
+
currentGroup.values.push(value);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
currentGroup = {
|
|
72
|
+
id: groupId,
|
|
73
|
+
values: [value],
|
|
74
|
+
};
|
|
75
|
+
groupArray.push(currentGroup);
|
|
76
|
+
}
|
|
77
|
+
return groupArray;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const groupFileTogether = (sideEffects) =>
|
|
81
|
+
groupBy(sideEffects, (sideEffect, { ignore }) => {
|
|
82
|
+
if (sideEffect.type === "fs:write_directory") {
|
|
83
|
+
ignore();
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (sideEffect.type === "fs:write_file") {
|
|
87
|
+
return "file";
|
|
88
|
+
}
|
|
89
|
+
return "other";
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// const groups = groupFileTogether([
|
|
93
|
+
// {
|
|
94
|
+
// name: "a",
|
|
95
|
+
// type: "fs:write_file",
|
|
96
|
+
// },
|
|
97
|
+
// {
|
|
98
|
+
// name: "b",
|
|
99
|
+
// type: "fs:write_directory",
|
|
100
|
+
// },
|
|
101
|
+
// {
|
|
102
|
+
// name: "c",
|
|
103
|
+
// type: "fs:write_file",
|
|
104
|
+
// },
|
|
105
|
+
// {
|
|
106
|
+
// name: "d",
|
|
107
|
+
// type: "other",
|
|
108
|
+
// },
|
|
109
|
+
// {
|
|
110
|
+
// name: "e",
|
|
111
|
+
// type: "fs:write_file",
|
|
112
|
+
// },
|
|
113
|
+
// ]);
|
|
114
|
+
// debugger;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hookIntoMethod } from "./hook_into_method.js";
|
|
2
2
|
|
|
3
3
|
export const spyConsoleCalls = (
|
|
4
|
-
{ error, warn, info, log, trace },
|
|
4
|
+
{ error, warn, info, log, trace, stdout, stderr },
|
|
5
5
|
{ preventConsoleSideEffects },
|
|
6
6
|
) => {
|
|
7
7
|
const restoreCallbackSet = new Set();
|
|
@@ -45,12 +45,38 @@ export const spyConsoleCalls = (
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
});
|
|
48
|
+
const processStdouthook = hookIntoMethod(
|
|
49
|
+
process.stdout,
|
|
50
|
+
"write",
|
|
51
|
+
(message) => {
|
|
52
|
+
return {
|
|
53
|
+
preventOriginalCall: preventConsoleSideEffects,
|
|
54
|
+
return: () => {
|
|
55
|
+
stdout(message);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
const processStderrHhook = hookIntoMethod(
|
|
61
|
+
process.stderr,
|
|
62
|
+
"write",
|
|
63
|
+
(message) => {
|
|
64
|
+
return {
|
|
65
|
+
preventOriginalCall: preventConsoleSideEffects,
|
|
66
|
+
return: () => {
|
|
67
|
+
stderr(message);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
);
|
|
48
72
|
restoreCallbackSet.add(() => {
|
|
49
73
|
errorHook.remove();
|
|
50
74
|
warnHook.remove();
|
|
51
75
|
infoHook.remove();
|
|
52
76
|
logHook.remove();
|
|
53
77
|
traceHook.remove();
|
|
78
|
+
processStdouthook.remove();
|
|
79
|
+
processStderrHhook.remove();
|
|
54
80
|
});
|
|
55
81
|
return {
|
|
56
82
|
restore: () => {
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
// https://github.com/tschaub/mock-fs/blob/6e84d5bb320022624c7d770432e3322323ce043e/lib/binding.js#L353
|
|
4
4
|
// https://github.com/tschaub/mock-fs/issues/348
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
removeDirectorySync,
|
|
8
|
+
removeFileSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "@jsenv/filesystem";
|
|
11
|
+
import { URL_META } from "@jsenv/url-meta";
|
|
12
|
+
import { readFileSync, statSync } from "node:fs";
|
|
8
13
|
import { pathToFileURL } from "node:url";
|
|
9
14
|
import {
|
|
10
15
|
disableHooksWhileCalling,
|
|
@@ -20,8 +25,12 @@ export const spyFilesystemCalls = (
|
|
|
20
25
|
removeFile = () => {}, // TODO
|
|
21
26
|
// removeDirectory = () => {},
|
|
22
27
|
},
|
|
23
|
-
{ undoFilesystemSideEffects } = {},
|
|
28
|
+
{ include, undoFilesystemSideEffects } = {},
|
|
24
29
|
) => {
|
|
30
|
+
const shouldReport = include
|
|
31
|
+
? URL_META.createFilter(include, "file:///")
|
|
32
|
+
: () => true;
|
|
33
|
+
|
|
25
34
|
const _internalFs = process.binding("fs");
|
|
26
35
|
const filesystemStateInfoMap = new Map();
|
|
27
36
|
const fileDescriptorPathMap = new Map();
|
|
@@ -47,8 +56,10 @@ export const spyFilesystemCalls = (
|
|
|
47
56
|
});
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
if (shouldReport(fileUrl)) {
|
|
60
|
+
writeFile(fileUrl, stateAfter.content);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
52
63
|
}
|
|
53
64
|
// file is exactly the same
|
|
54
65
|
// function did not have any effect on the file
|
|
@@ -65,8 +76,11 @@ export const spyFilesystemCalls = (
|
|
|
65
76
|
});
|
|
66
77
|
});
|
|
67
78
|
}
|
|
68
|
-
|
|
79
|
+
if (shouldReport(directoryUrl)) {
|
|
80
|
+
writeDirectory(directoryUrl);
|
|
81
|
+
}
|
|
69
82
|
};
|
|
83
|
+
const beforeUndoCallbackSet = new Set();
|
|
70
84
|
const restoreCallbackSet = new Set();
|
|
71
85
|
|
|
72
86
|
const getFileStateWithinHook = (fileUrl) => {
|
|
@@ -166,11 +180,18 @@ export const spyFilesystemCalls = (
|
|
|
166
180
|
unlinkHook.remove();
|
|
167
181
|
});
|
|
168
182
|
return {
|
|
183
|
+
addBeforeUndoCallback: (callback) => {
|
|
184
|
+
beforeUndoCallbackSet.add(callback);
|
|
185
|
+
},
|
|
169
186
|
restore: () => {
|
|
170
187
|
for (const restoreCallback of restoreCallbackSet) {
|
|
171
188
|
restoreCallback();
|
|
172
189
|
}
|
|
173
190
|
restoreCallbackSet.clear();
|
|
191
|
+
for (const beforeUndoCallback of beforeUndoCallbackSet) {
|
|
192
|
+
beforeUndoCallback();
|
|
193
|
+
}
|
|
194
|
+
beforeUndoCallbackSet.clear();
|
|
174
195
|
for (const [, restore] of fileRestoreMap) {
|
|
175
196
|
restore();
|
|
176
197
|
}
|
package/src/main.js
CHANGED
|
@@ -2,5 +2,6 @@ export {
|
|
|
2
2
|
takeDirectorySnapshot,
|
|
3
3
|
takeFileSnapshot,
|
|
4
4
|
} from "./filesystem_snapshot.js";
|
|
5
|
+
export { createReplaceFilesystemWellKnownValues } from "./filesystem_well_known_values.js";
|
|
5
6
|
export { snapshotFunctionSideEffects } from "./function_side_effects/function_side_effects_snapshot.js";
|
|
6
7
|
export { replaceFluctuatingValues } from "./replace_fluctuating_values.js";
|
|
@@ -11,168 +11,56 @@ import {
|
|
|
11
11
|
stringifyHtmlAst,
|
|
12
12
|
visitHtmlNodes,
|
|
13
13
|
} from "@jsenv/ast";
|
|
14
|
-
import {
|
|
15
|
-
ensurePathnameTrailingSlash,
|
|
16
|
-
removePathnameTrailingSlash,
|
|
17
|
-
urlToExtension,
|
|
18
|
-
} from "@jsenv/urls";
|
|
19
|
-
import { escapeRegexpSpecialChars } from "@jsenv/utils/src/string/escape_regexp_special_chars.js";
|
|
20
|
-
import { readFileSync } from "node:fs";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
+
import { urlToExtension } from "@jsenv/urls";
|
|
23
15
|
import stripAnsi from "strip-ansi";
|
|
16
|
+
import { createReplaceFilesystemWellKnownValues } from "./filesystem_well_known_values.js";
|
|
24
17
|
|
|
25
18
|
export const replaceFluctuatingValues = (
|
|
26
19
|
string,
|
|
27
20
|
{
|
|
28
21
|
stringType,
|
|
22
|
+
rootDirectoryUrl,
|
|
29
23
|
fileUrl,
|
|
30
24
|
removeAnsi = true,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
homedirDisabled,
|
|
36
|
-
cwdPath = process.cwd(),
|
|
37
|
-
cwdUrl,
|
|
38
|
-
isWindows = process.platform === "win32",
|
|
25
|
+
// for unit test
|
|
26
|
+
replaceFilesystemWellKnownValues = createReplaceFilesystemWellKnownValues({
|
|
27
|
+
rootDirectoryUrl,
|
|
28
|
+
}),
|
|
39
29
|
} = {},
|
|
40
30
|
) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
replacement,
|
|
48
|
-
replace: (string) =>
|
|
49
|
-
string.replaceAll(urlWithoutTrailingSlash, replacement),
|
|
50
|
-
});
|
|
51
|
-
const path =
|
|
52
|
-
url === cwdUrl ? cwdPath : fileURLToPath(urlWithoutTrailingSlash);
|
|
53
|
-
const windowPathRegex = new RegExp(
|
|
54
|
-
`${escapeRegexpSpecialChars(path)}(((?:\\\\(?:[\\w !#()-]+|[.]{1,2})+)*)(?:\\\\)?)`,
|
|
55
|
-
"gm",
|
|
56
|
-
);
|
|
57
|
-
const pathReplacement = replacement.startsWith("file:///")
|
|
58
|
-
? replacement.slice("file:///".length)
|
|
59
|
-
: replacement;
|
|
60
|
-
wellKnownPathArray.push({
|
|
61
|
-
path,
|
|
62
|
-
replacement: pathReplacement,
|
|
63
|
-
replace: isWindows
|
|
64
|
-
? (string) => {
|
|
65
|
-
return string.replaceAll(windowPathRegex, (match, after) => {
|
|
66
|
-
return `${pathReplacement}${after.replaceAll("\\", "/")}`;
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
: (string) => string.replaceAll(path, pathReplacement),
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
if (rootDirectoryUrl) {
|
|
73
|
-
addWellKnownFileUrl(rootDirectoryUrl, "file:///<root>");
|
|
74
|
-
}
|
|
75
|
-
/*
|
|
76
|
-
* When running code inside a node project ancestor packages
|
|
77
|
-
* should make things super predictible because
|
|
78
|
-
* it will use a package.json name field
|
|
79
|
-
* to replace files urls
|
|
80
|
-
* And uses the highest ancestor package so that even if the file
|
|
81
|
-
* is executed once within a package then outside that package
|
|
82
|
-
* the replace value remains predictible as the highest package is used
|
|
83
|
-
* The highest package is used because it's pushed first by
|
|
84
|
-
* addWellKnownFileUrl
|
|
85
|
-
*/
|
|
86
|
-
ancestor_packages: {
|
|
87
|
-
if (ancestorPackagesDisabled) {
|
|
88
|
-
break ancestor_packages;
|
|
89
|
-
}
|
|
90
|
-
const ancestorPackages = [];
|
|
91
|
-
const cwd = cwdPath || process.cwd();
|
|
92
|
-
const cwdUrl = ensurePathnameTrailingSlash(pathToFileURL(cwd));
|
|
93
|
-
let currentUrl = cwdUrl;
|
|
94
|
-
while (currentUrl.href !== ancestorPackagesRootDirectoryUrl) {
|
|
95
|
-
const packageFileUrl = new URL("package.json", currentUrl);
|
|
96
|
-
const packageDirectoryUrl = currentUrl;
|
|
97
|
-
currentUrl = new URL(getParentUrl(currentUrl));
|
|
98
|
-
let packageFileContent;
|
|
99
|
-
try {
|
|
100
|
-
packageFileContent = readFileSync(packageFileUrl);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
if (e.code === "ENOENT") {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
throw e;
|
|
106
|
-
}
|
|
107
|
-
let packageObject;
|
|
108
|
-
try {
|
|
109
|
-
packageObject = JSON.parse(packageFileContent);
|
|
110
|
-
} catch (e) {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const packageName = packageObject.name;
|
|
114
|
-
ancestorPackages.unshift({
|
|
115
|
-
packageDirectoryUrl,
|
|
116
|
-
packageName,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
for (const ancestorPackage of ancestorPackages) {
|
|
120
|
-
addWellKnownFileUrl(
|
|
121
|
-
ancestorPackage.packageDirectoryUrl,
|
|
122
|
-
ancestorPackage.packageName,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
home_dir: {
|
|
127
|
-
if (!homedirDisabled) {
|
|
128
|
-
const homedirPath = homedir();
|
|
129
|
-
const homedirUrl = pathToFileURL(homedirPath);
|
|
130
|
-
addWellKnownFileUrl(homedirUrl, "file:///~");
|
|
31
|
+
if (fileUrl && stringType === undefined) {
|
|
32
|
+
const extension = urlToExtension(fileUrl);
|
|
33
|
+
if (extension === ".html") {
|
|
34
|
+
stringType = "html";
|
|
35
|
+
} else if (extension === ".svg") {
|
|
36
|
+
stringType = "svg";
|
|
131
37
|
}
|
|
132
38
|
}
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const replaceResult = wellKownUrl.replace(value);
|
|
143
|
-
if (replaceResult !== value) {
|
|
144
|
-
return replaceResult;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return value;
|
|
148
|
-
};
|
|
149
|
-
const replaceFilePaths = (value) => {
|
|
150
|
-
for (const wellKownPath of wellKnownPathArray) {
|
|
151
|
-
const replaceResult = wellKownPath.replace(value);
|
|
152
|
-
if (replaceResult !== value) {
|
|
153
|
-
return replaceResult;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
39
|
+
const replaceDurations = (value) => {
|
|
40
|
+
// https://stackoverflow.com/a/59202307/24573072
|
|
41
|
+
value = value.replace(
|
|
42
|
+
/(?<!\d|\.)\d+(?:\.\d+)?(\s*)(seconds|second|s)\b/g,
|
|
43
|
+
(match, space, unit) => {
|
|
44
|
+
if (unit === "seconds") unit = "second";
|
|
45
|
+
return `<X>${space}${unit}`;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
156
48
|
return value;
|
|
157
49
|
};
|
|
158
50
|
const replaceThings = (value) => {
|
|
51
|
+
if (stringType === "filesystem") {
|
|
52
|
+
return replaceFilesystemWellKnownValues(value);
|
|
53
|
+
}
|
|
159
54
|
if (removeAnsi) {
|
|
160
55
|
value = stripAnsi(value);
|
|
161
56
|
}
|
|
162
|
-
value =
|
|
163
|
-
|
|
57
|
+
value = replaceFilesystemWellKnownValues(value, {
|
|
58
|
+
willBeWrittenOnFilesystem: false,
|
|
59
|
+
});
|
|
164
60
|
value = replaceHttpUrls(value);
|
|
61
|
+
value = replaceDurations(value);
|
|
165
62
|
return value;
|
|
166
63
|
};
|
|
167
|
-
|
|
168
|
-
if (fileUrl && stringType === undefined) {
|
|
169
|
-
const extension = urlToExtension(fileUrl);
|
|
170
|
-
if (extension === ".html") {
|
|
171
|
-
stringType = "html";
|
|
172
|
-
} else if (extension === ".svg") {
|
|
173
|
-
stringType = "svg";
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
64
|
if (stringType === "html") {
|
|
177
65
|
// do parse html
|
|
178
66
|
const htmlAst =
|
|
@@ -204,27 +92,6 @@ export const replaceFluctuatingValues = (
|
|
|
204
92
|
return replaceThings(string);
|
|
205
93
|
};
|
|
206
94
|
|
|
207
|
-
const getParentUrl = (url) => {
|
|
208
|
-
url = String(url);
|
|
209
|
-
// With node.js new URL('../', 'file:///C:/').href
|
|
210
|
-
// returns "file:///C:/" instead of "file:///"
|
|
211
|
-
const resource = url.slice("file://".length);
|
|
212
|
-
const slashLastIndex = resource.lastIndexOf("/");
|
|
213
|
-
if (slashLastIndex === -1) {
|
|
214
|
-
return url;
|
|
215
|
-
}
|
|
216
|
-
const lastCharIndex = resource.length - 1;
|
|
217
|
-
if (slashLastIndex === lastCharIndex) {
|
|
218
|
-
const slashBeforeLastIndex = resource.lastIndexOf("/", slashLastIndex - 1);
|
|
219
|
-
if (slashBeforeLastIndex === -1) {
|
|
220
|
-
return url;
|
|
221
|
-
}
|
|
222
|
-
return `file://${resource.slice(0, slashBeforeLastIndex + 1)}`;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return `file://${resource.slice(0, slashLastIndex + 1)}`;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
95
|
const replaceHttpUrls = (source) => {
|
|
229
96
|
return source.replace(/(?:https?|ftp):\/\/\S+[\w/]/g, (match) => {
|
|
230
97
|
const lastChar = match[match.length - 1];
|