@jsenv/snapshot 1.3.8 → 1.4.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 +4 -3
- package/src/compare_png_files.js +49 -0
- package/src/errors.js +6 -0
- package/src/file_snapshots.js +212 -166
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/snapshot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Snapshot testing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -35,10 +35,11 @@
|
|
|
35
35
|
"test": "node --conditions=development ./scripts/test.mjs"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jsenv/
|
|
38
|
+
"@jsenv/assert": "4.0.10",
|
|
39
|
+
"@jsenv/filesystem": "4.7.1",
|
|
39
40
|
"@jsenv/urls": "2.2.7",
|
|
40
41
|
"@jsenv/utils": "2.1.1",
|
|
41
|
-
"
|
|
42
|
+
"pixelmatch": "6.0.0",
|
|
42
43
|
"prettier": "3.3.2"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// https://github.com/image-size/image-size/blob/main/lib/types/png.ts
|
|
2
|
+
|
|
3
|
+
import pixelmatch from "pixelmatch";
|
|
4
|
+
import { PNG } from "pngjs";
|
|
5
|
+
|
|
6
|
+
export const comparePngFiles = (actualData, expectData) => {
|
|
7
|
+
const { width, height } = getPngDimensions(actualData);
|
|
8
|
+
const actualPng = PNG.sync.read(actualData);
|
|
9
|
+
const expectPng = PNG.sync.read(expectData);
|
|
10
|
+
const numberOfPixels = width * height;
|
|
11
|
+
const numberOfPixelsConsideredAsDiff = pixelmatch(
|
|
12
|
+
actualPng.data,
|
|
13
|
+
expectPng.data,
|
|
14
|
+
null,
|
|
15
|
+
width,
|
|
16
|
+
height,
|
|
17
|
+
{
|
|
18
|
+
threshold: 0.1,
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
const diffRatio = numberOfPixelsConsideredAsDiff / numberOfPixels;
|
|
22
|
+
const diffPercentage = diffRatio * 100;
|
|
23
|
+
return diffPercentage <= 1;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getPngDimensions = (buffer) => {
|
|
27
|
+
if (toUTF8String(buffer, 12, 16) === pngFriedChunkName) {
|
|
28
|
+
return {
|
|
29
|
+
height: readUInt32BE(buffer, 36),
|
|
30
|
+
width: readUInt32BE(buffer, 32),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
height: readUInt32BE(buffer, 20),
|
|
35
|
+
width: readUInt32BE(buffer, 16),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const pngFriedChunkName = "CgBI";
|
|
40
|
+
|
|
41
|
+
const decoder = new TextDecoder();
|
|
42
|
+
const toUTF8String = (input, start = 0, end = input.length) =>
|
|
43
|
+
decoder.decode(input.slice(start, end));
|
|
44
|
+
|
|
45
|
+
const readUInt32BE = (input, offset = 0) =>
|
|
46
|
+
input[offset] * 2 ** 24 +
|
|
47
|
+
input[offset + 1] * 2 ** 16 +
|
|
48
|
+
input[offset + 2] * 2 ** 8 +
|
|
49
|
+
input[offset + 3];
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { assert } from "@jsenv/assert";
|
|
2
|
+
|
|
3
|
+
export class FileContentNotFoundAssertionError extends assert.AssertionError {}
|
|
4
|
+
export class FileMissingAssertionError extends assert.AssertionError {}
|
|
5
|
+
export class ExtraFileAssertionError extends assert.AssertionError {}
|
|
6
|
+
export class FileContentAssertionError extends assert.AssertionError {}
|
package/src/file_snapshots.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readdirSync, statSync, readFileSync } from "node:fs";
|
|
2
|
+
import { URL_META } from "@jsenv/url-meta";
|
|
2
3
|
import {
|
|
3
4
|
assertAndNormalizeDirectoryUrl,
|
|
4
5
|
assertAndNormalizeFileUrl,
|
|
@@ -7,31 +8,39 @@ import {
|
|
|
7
8
|
removeFileSync,
|
|
8
9
|
writeFileSync,
|
|
9
10
|
removeDirectorySync,
|
|
10
|
-
writeFileStructureSync,
|
|
11
11
|
} from "@jsenv/filesystem";
|
|
12
|
-
import { urlToFilename, urlToRelativeUrl } from "@jsenv/urls";
|
|
13
12
|
import { CONTENT_TYPE } from "@jsenv/utils/src/content_type/content_type.js";
|
|
13
|
+
import { urlToRelativeUrl, urlToFilename } from "@jsenv/urls";
|
|
14
14
|
|
|
15
15
|
import { assert } from "@jsenv/assert";
|
|
16
|
+
import {
|
|
17
|
+
FileContentNotFoundAssertionError,
|
|
18
|
+
FileMissingAssertionError,
|
|
19
|
+
ExtraFileAssertionError,
|
|
20
|
+
FileContentAssertionError,
|
|
21
|
+
} from "./errors.js";
|
|
22
|
+
import { comparePngFiles } from "./compare_png_files.js";
|
|
16
23
|
|
|
17
24
|
export const takeFileSnapshot = (fileUrl) => {
|
|
18
25
|
fileUrl = assertAndNormalizeFileUrl(fileUrl);
|
|
19
|
-
const
|
|
26
|
+
const fileSnapshot = createFileSnapshot(fileUrl);
|
|
20
27
|
removeFileSync(fileUrl, { allowUseless: true });
|
|
21
|
-
|
|
22
28
|
return {
|
|
23
|
-
compare: () => {
|
|
24
|
-
|
|
29
|
+
compare: (doIt = process.env.CI) => {
|
|
30
|
+
if (!doIt) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
fileSnapshot.compare(createFileSnapshot(fileUrl));
|
|
25
34
|
},
|
|
26
35
|
writeContent: (content) => {
|
|
27
36
|
writeFileSync(fileUrl, content);
|
|
28
37
|
},
|
|
29
38
|
restore: () => {
|
|
30
|
-
if (
|
|
39
|
+
if (fileSnapshot.empty) {
|
|
31
40
|
removeFileSync(fileUrl, { allowUseless: true });
|
|
32
41
|
return;
|
|
33
42
|
}
|
|
34
|
-
writeFileSync(fileUrl,
|
|
43
|
+
writeFileSync(fileUrl, fileSnapshot.content);
|
|
35
44
|
},
|
|
36
45
|
};
|
|
37
46
|
};
|
|
@@ -40,7 +49,54 @@ const createFileSnapshot = (fileUrl) => {
|
|
|
40
49
|
type: "file",
|
|
41
50
|
url: fileUrl,
|
|
42
51
|
stat: null,
|
|
52
|
+
contentType: CONTENT_TYPE.fromUrlExtension(fileUrl),
|
|
43
53
|
content: "",
|
|
54
|
+
compare: (nextFileSnapshot) => {
|
|
55
|
+
const filename = urlToFilename(fileUrl);
|
|
56
|
+
const failureMessage = `snapshot comparison failed for "${filename}"`;
|
|
57
|
+
|
|
58
|
+
if (!nextFileSnapshot.stat) {
|
|
59
|
+
const fileNotFoundAssertionError =
|
|
60
|
+
new FileContentNotFoundAssertionError(`${failureMessage}
|
|
61
|
+
--- reason ---
|
|
62
|
+
file not found
|
|
63
|
+
--- file ---
|
|
64
|
+
${fileUrl}`);
|
|
65
|
+
throw fileNotFoundAssertionError;
|
|
66
|
+
}
|
|
67
|
+
if (!fileSnapshot.stat) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const fileContent = fileSnapshot.content;
|
|
71
|
+
const nextFileContent = nextFileSnapshot.content;
|
|
72
|
+
if (Buffer.isBuffer(nextFileContent)) {
|
|
73
|
+
if (nextFileContent.equals(fileContent)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (nextFileContent.contentType === "image/png") {
|
|
77
|
+
if (comparePngFiles(fileContent, nextFileContent)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const fileContentAssertionError =
|
|
82
|
+
new FileContentAssertionError(`${failureMessage}
|
|
83
|
+
--- reason ---
|
|
84
|
+
content has changed
|
|
85
|
+
--- file ---
|
|
86
|
+
${fileUrl}`);
|
|
87
|
+
throw fileContentAssertionError;
|
|
88
|
+
}
|
|
89
|
+
if (nextFileContent === fileContent) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
assert({
|
|
93
|
+
message: failureMessage,
|
|
94
|
+
actual: nextFileContent,
|
|
95
|
+
expect: fileContent,
|
|
96
|
+
details: fileUrl,
|
|
97
|
+
forceMultilineDiff: true,
|
|
98
|
+
});
|
|
99
|
+
},
|
|
44
100
|
};
|
|
45
101
|
|
|
46
102
|
try {
|
|
@@ -55,9 +111,7 @@ const createFileSnapshot = (fileUrl) => {
|
|
|
55
111
|
throw new Error(`file expect at ${fileUrl}`);
|
|
56
112
|
}
|
|
57
113
|
|
|
58
|
-
const isTextual = CONTENT_TYPE.isTextual(
|
|
59
|
-
CONTENT_TYPE.fromUrlExtension(fileUrl),
|
|
60
|
-
);
|
|
114
|
+
const isTextual = CONTENT_TYPE.isTextual(fileSnapshot.contentType);
|
|
61
115
|
if (isTextual) {
|
|
62
116
|
const contentAsString = readFileSync(new URL(fileUrl), "utf8");
|
|
63
117
|
if (process.platform === "win32") {
|
|
@@ -76,63 +130,86 @@ const createFileSnapshot = (fileUrl) => {
|
|
|
76
130
|
}
|
|
77
131
|
return fileSnapshot;
|
|
78
132
|
};
|
|
79
|
-
const compareFileSnapshots = (actualFileSnapshot, expectedFileSnapshot) => {
|
|
80
|
-
const fileUrl = actualFileSnapshot.url;
|
|
81
|
-
const filename = urlToFilename(fileUrl);
|
|
82
|
-
const failureMessage = `snapshot comparison failed for "${filename}"`;
|
|
83
133
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
file not found
|
|
89
|
-
--- file ---
|
|
90
|
-
${fileUrl}`);
|
|
91
|
-
fileNotFoundAssertionError.name = "FileNotFoundAssertionError";
|
|
92
|
-
throw fileNotFoundAssertionError;
|
|
93
|
-
}
|
|
94
|
-
if (!expectedFileSnapshot.stat) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const actualFileContent = actualFileSnapshot.content;
|
|
98
|
-
const expectedFileContent = expectedFileSnapshot.content;
|
|
99
|
-
if (Buffer.isBuffer(actualFileContent)) {
|
|
100
|
-
if (actualFileContent.equals(expectedFileContent)) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const fileContentAssertionError =
|
|
104
|
-
assert.createAssertionError(`${failureMessage}
|
|
105
|
-
--- reason ---
|
|
106
|
-
content has changed
|
|
107
|
-
--- file ---
|
|
108
|
-
${fileUrl}`);
|
|
109
|
-
fileContentAssertionError.name = "FileContentAssertionError";
|
|
110
|
-
throw fileContentAssertionError;
|
|
111
|
-
}
|
|
112
|
-
if (actualFileContent === expectedFileContent) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
assert({
|
|
116
|
-
message: failureMessage,
|
|
117
|
-
details: fileUrl,
|
|
118
|
-
actual: actualFileContent,
|
|
119
|
-
expect: expectedFileContent,
|
|
120
|
-
forceMultilineDiff: true,
|
|
121
|
-
});
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export const takeDirectorySnapshot = (directoryUrl) => {
|
|
134
|
+
export const takeDirectorySnapshot = (
|
|
135
|
+
directoryUrl,
|
|
136
|
+
pattern = { "**/*": true },
|
|
137
|
+
) => {
|
|
125
138
|
directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl);
|
|
126
139
|
directoryUrl = new URL(directoryUrl);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
const associations = URL_META.resolveAssociations(
|
|
141
|
+
{
|
|
142
|
+
included: pattern,
|
|
143
|
+
},
|
|
144
|
+
directoryUrl,
|
|
145
|
+
);
|
|
146
|
+
const predicate = ({ included }) => included;
|
|
147
|
+
const shouldVisitDirectory = (url) =>
|
|
148
|
+
URL_META.urlChildMayMatch({
|
|
149
|
+
url,
|
|
150
|
+
associations,
|
|
151
|
+
predicate,
|
|
152
|
+
});
|
|
153
|
+
const shouldIncludeFile = (url) =>
|
|
154
|
+
predicate(
|
|
155
|
+
URL_META.applyAssociations({
|
|
156
|
+
url,
|
|
157
|
+
associations,
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
const directorySnapshot = createDirectorySnapshot(directoryUrl, {
|
|
161
|
+
shouldVisitDirectory,
|
|
162
|
+
shouldIncludeFile,
|
|
163
|
+
clean: true,
|
|
164
|
+
});
|
|
130
165
|
return {
|
|
131
|
-
|
|
166
|
+
__snapshot: directorySnapshot,
|
|
167
|
+
compare: (doIt = process.env.CI) => {
|
|
168
|
+
if (!doIt) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const nextDirectorySnapshot = createDirectorySnapshot(directoryUrl, {
|
|
172
|
+
shouldVisitDirectory,
|
|
173
|
+
shouldIncludeFile,
|
|
174
|
+
});
|
|
175
|
+
directorySnapshot.compare(nextDirectorySnapshot);
|
|
176
|
+
},
|
|
177
|
+
addFile: (relativeUrl, content) => {
|
|
178
|
+
writeFileSync(new URL(relativeUrl, directoryUrl), content);
|
|
179
|
+
},
|
|
180
|
+
restore: () => {
|
|
181
|
+
if (directorySnapshot.notFound) {
|
|
182
|
+
removeDirectorySync(directoryUrl, {
|
|
183
|
+
recursive: true,
|
|
184
|
+
allowUseless: true,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (directorySnapshot.empty) {
|
|
189
|
+
ensureEmptyDirectorySync(directoryUrl);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const relativeUrl of directorySnapshot.contentSnapshot) {
|
|
193
|
+
const snapshot = directorySnapshot.contentSnapshot[relativeUrl];
|
|
194
|
+
snapshot.restore();
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
const createDirectorySnapshot = (
|
|
200
|
+
directoryUrl,
|
|
201
|
+
{ shouldVisitDirectory, shouldIncludeFile, clean },
|
|
202
|
+
) => {
|
|
203
|
+
const directorySnapshot = {
|
|
204
|
+
type: "directory",
|
|
205
|
+
url: directoryUrl.href,
|
|
206
|
+
stat: null,
|
|
207
|
+
empty: false,
|
|
208
|
+
contentSnapshot: {},
|
|
209
|
+
compare: (nextDirectorySnapshot) => {
|
|
132
210
|
const dirname = `${urlToFilename(directoryUrl)}/`;
|
|
133
211
|
const failureMessage = `snapshot comparison failed for "${dirname}"`;
|
|
134
|
-
|
|
135
|
-
if (!expectedDirectorySnapshot.stat || expectedDirectorySnapshot.empty) {
|
|
212
|
+
if (!directorySnapshot.stat || directorySnapshot.empty) {
|
|
136
213
|
// the snapshot taken for directory/file/whatever is empty:
|
|
137
214
|
// - first time code executes:
|
|
138
215
|
// it defines snapshot that will be used for comparison by future runs
|
|
@@ -142,17 +219,15 @@ export const takeDirectorySnapshot = (directoryUrl) => {
|
|
|
142
219
|
// to review them using git diff)
|
|
143
220
|
return;
|
|
144
221
|
}
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
// missing_files
|
|
222
|
+
const directoryContentSnapshot = directorySnapshot.contentSnapshot;
|
|
223
|
+
const relativeUrls = Object.keys(directoryContentSnapshot);
|
|
224
|
+
const nextDirectoryContentSnapshot =
|
|
225
|
+
nextDirectorySnapshot.contentSnapshot;
|
|
226
|
+
const nextRelativeUrls = Object.keys(nextDirectoryContentSnapshot);
|
|
227
|
+
// missing content
|
|
152
228
|
{
|
|
153
|
-
const missingRelativeUrls =
|
|
154
|
-
(
|
|
155
|
-
!actualRelativeUrls.includes(expectedRelativeUrl),
|
|
229
|
+
const missingRelativeUrls = relativeUrls.filter(
|
|
230
|
+
(relativeUrl) => !nextRelativeUrls.includes(relativeUrl),
|
|
156
231
|
);
|
|
157
232
|
const missingFileCount = missingRelativeUrls.length;
|
|
158
233
|
if (missingFileCount > 0) {
|
|
@@ -161,30 +236,26 @@ export const takeDirectorySnapshot = (directoryUrl) => {
|
|
|
161
236
|
);
|
|
162
237
|
if (missingFileCount === 1) {
|
|
163
238
|
const fileMissingAssertionError =
|
|
164
|
-
|
|
239
|
+
new FileMissingAssertionError(`${failureMessage}
|
|
165
240
|
--- reason ---
|
|
166
|
-
"${missingRelativeUrls[0]}" is missing
|
|
167
|
-
---
|
|
241
|
+
"${missingRelativeUrls[0]}" directory entry is missing
|
|
242
|
+
--- missing entry ---
|
|
168
243
|
${missingUrls[0]}`);
|
|
169
|
-
fileMissingAssertionError.name = "FileMissingAssertionError";
|
|
170
244
|
throw fileMissingAssertionError;
|
|
171
245
|
}
|
|
172
246
|
const fileMissingAssertionError =
|
|
173
|
-
|
|
247
|
+
new FileMissingAssertionError(`${failureMessage}
|
|
174
248
|
--- reason ---
|
|
175
|
-
${missingFileCount}
|
|
176
|
-
---
|
|
249
|
+
${missingFileCount} directory entries are missing
|
|
250
|
+
--- missing entries ---
|
|
177
251
|
${missingUrls.join("\n")}`);
|
|
178
|
-
fileMissingAssertionError.name = "FileMissingAssertionError";
|
|
179
252
|
throw fileMissingAssertionError;
|
|
180
253
|
}
|
|
181
254
|
}
|
|
182
|
-
|
|
183
|
-
// unexpected files
|
|
255
|
+
// unexpected content
|
|
184
256
|
{
|
|
185
|
-
const extraRelativeUrls =
|
|
186
|
-
(
|
|
187
|
-
!expectedRelativeUrls.includes(actualRelativeUrl),
|
|
257
|
+
const extraRelativeUrls = nextRelativeUrls.filter(
|
|
258
|
+
(nextRelativeUrl) => !relativeUrls.includes(nextRelativeUrl),
|
|
188
259
|
);
|
|
189
260
|
const extraFileCount = extraRelativeUrls.length;
|
|
190
261
|
if (extraFileCount > 0) {
|
|
@@ -193,78 +264,34 @@ ${missingUrls.join("\n")}`);
|
|
|
193
264
|
);
|
|
194
265
|
if (extraFileCount === 1) {
|
|
195
266
|
const extraFileAssertionError =
|
|
196
|
-
|
|
267
|
+
new ExtraFileAssertionError(`${failureMessage}
|
|
197
268
|
--- reason ---
|
|
198
|
-
"${extraRelativeUrls[0]}" is unexpected
|
|
199
|
-
---
|
|
269
|
+
"${extraRelativeUrls[0]}" directory entry is unexpected
|
|
270
|
+
--- unexpected entry ---
|
|
200
271
|
${extraUrls[0]}`);
|
|
201
|
-
extraFileAssertionError.name = "ExtraFileAssertionError";
|
|
202
272
|
throw extraFileAssertionError;
|
|
203
273
|
}
|
|
204
274
|
const extraFileAssertionError =
|
|
205
|
-
|
|
275
|
+
new ExtraFileAssertionError(`${failureMessage}
|
|
206
276
|
--- reason ---
|
|
207
|
-
${extraFileCount}
|
|
208
|
-
---
|
|
277
|
+
${extraFileCount} directory entries are unexpected
|
|
278
|
+
--- unexpected entries ---
|
|
209
279
|
${extraUrls.join("\n")}`);
|
|
210
|
-
extraFileAssertionError.name = "ExtraFileAssertionError";
|
|
211
280
|
throw extraFileAssertionError;
|
|
212
281
|
}
|
|
213
282
|
}
|
|
214
|
-
|
|
215
|
-
// file contents
|
|
283
|
+
// content
|
|
216
284
|
{
|
|
217
|
-
for (const relativeUrl of
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
285
|
+
for (const relativeUrl of nextRelativeUrls) {
|
|
286
|
+
const snapshot = directoryContentSnapshot[relativeUrl];
|
|
287
|
+
const nextSnapshot = nextDirectoryContentSnapshot[relativeUrl];
|
|
288
|
+
snapshot.compare(nextSnapshot);
|
|
221
289
|
}
|
|
222
290
|
}
|
|
223
291
|
},
|
|
224
|
-
addFile: (relativeUrl, content) => {
|
|
225
|
-
writeFileSync(new URL(relativeUrl, directoryUrl), content);
|
|
226
|
-
},
|
|
227
|
-
restore: () => {
|
|
228
|
-
if (expectedDirectorySnapshot.notFound) {
|
|
229
|
-
removeDirectorySync(directoryUrl, {
|
|
230
|
-
recursive: true,
|
|
231
|
-
allowUseless: true,
|
|
232
|
-
});
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
if (expectedDirectorySnapshot.empty) {
|
|
236
|
-
ensureEmptyDirectorySync(directoryUrl);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const fileStructure = {};
|
|
241
|
-
Object.keys(expectedDirectorySnapshot.fileSnapshots).forEach(
|
|
242
|
-
(relativeUrl) => {
|
|
243
|
-
const fileSnapshot =
|
|
244
|
-
expectedDirectorySnapshot.fileSnapshots[relativeUrl];
|
|
245
|
-
if (!fileSnapshot.empty) {
|
|
246
|
-
fileStructure[relativeUrl] = fileSnapshot.content;
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
);
|
|
250
|
-
writeFileStructureSync(
|
|
251
|
-
directoryUrl,
|
|
252
|
-
expectedDirectorySnapshot.fileStructure,
|
|
253
|
-
);
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
};
|
|
257
|
-
const createDirectorySnapshot = (directoryUrl) => {
|
|
258
|
-
const directorySnapshot = {
|
|
259
|
-
type: "directory",
|
|
260
|
-
url: directoryUrl.href,
|
|
261
|
-
stat: null,
|
|
262
|
-
empty: false,
|
|
263
|
-
fileSnapshots: {},
|
|
264
292
|
};
|
|
265
|
-
|
|
266
293
|
try {
|
|
267
|
-
directorySnapshot.stat = statSync(directoryUrl);
|
|
294
|
+
directorySnapshot.stat = statSync(new URL(directoryUrl));
|
|
268
295
|
} catch (e) {
|
|
269
296
|
if (e.code === "ENOENT") {
|
|
270
297
|
return directorySnapshot;
|
|
@@ -272,48 +299,67 @@ const createDirectorySnapshot = (directoryUrl) => {
|
|
|
272
299
|
if (e.code === "ENOTDIR") {
|
|
273
300
|
// trailing slash is forced on directoryUrl
|
|
274
301
|
// as a result Node.js throw ENOTDIR when doing "stat" operation
|
|
275
|
-
throw new Error(`directory
|
|
302
|
+
throw new Error(`directory expected at ${directoryUrl}`);
|
|
276
303
|
}
|
|
277
304
|
throw e;
|
|
278
305
|
}
|
|
279
306
|
if (!directorySnapshot.stat.isDirectory()) {
|
|
280
|
-
throw new Error(`directory
|
|
307
|
+
throw new Error(`directory expected at ${directoryUrl}`);
|
|
281
308
|
}
|
|
282
|
-
|
|
283
309
|
const entryNames = readdirSync(directoryUrl);
|
|
284
310
|
if (entryNames.length === 0) {
|
|
285
311
|
directorySnapshot.empty = true;
|
|
286
312
|
return directorySnapshot;
|
|
287
313
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
314
|
+
const contentSnapshotNaturalOrder = {};
|
|
315
|
+
try {
|
|
316
|
+
const directoryItemArray = readdirSync(directoryUrl);
|
|
317
|
+
for (const directoryItem of directoryItemArray) {
|
|
318
|
+
const directoryItemUrl = new URL(directoryItem, directoryUrl);
|
|
319
|
+
let directoryItemStat;
|
|
320
|
+
try {
|
|
321
|
+
directoryItemStat = statSync(directoryItemUrl);
|
|
322
|
+
} catch (e) {
|
|
323
|
+
if (e.code === "ENOENT") {
|
|
324
|
+
continue;
|
|
299
325
|
}
|
|
300
|
-
|
|
301
|
-
fileSnapshotsNaturalOrder[relativeUrl] = createFileSnapshot(contentUrl);
|
|
326
|
+
throw e;
|
|
302
327
|
}
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
328
|
+
const relativeUrl = urlToRelativeUrl(directoryItemUrl, directoryUrl);
|
|
329
|
+
if (directoryItemStat.isDirectory()) {
|
|
330
|
+
if (!shouldVisitDirectory(directoryUrl)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
contentSnapshotNaturalOrder[relativeUrl] = createDirectorySnapshot(
|
|
334
|
+
new URL(`${directoryItemUrl}/`),
|
|
335
|
+
{
|
|
336
|
+
shouldVisitDirectory,
|
|
337
|
+
shouldIncludeFile,
|
|
338
|
+
clean,
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!shouldIncludeFile(directoryItemUrl)) {
|
|
344
|
+
continue;
|
|
306
345
|
}
|
|
346
|
+
contentSnapshotNaturalOrder[relativeUrl] =
|
|
347
|
+
createFileSnapshot(directoryItemUrl);
|
|
348
|
+
if (clean) {
|
|
349
|
+
removeFileSync(directoryItemUrl, { allowUseless: true });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (e) {
|
|
353
|
+
if (e && e.code === "ENOENT") {
|
|
354
|
+
} else {
|
|
307
355
|
throw e;
|
|
308
356
|
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const relativeUrls = Object.keys(fileSnapshotsNaturalOrder);
|
|
357
|
+
}
|
|
358
|
+
const relativeUrls = Object.keys(contentSnapshotNaturalOrder);
|
|
313
359
|
relativeUrls.sort(comparePathnames);
|
|
314
360
|
relativeUrls.forEach((relativeUrl) => {
|
|
315
|
-
directorySnapshot.
|
|
316
|
-
|
|
361
|
+
directorySnapshot.contentSnapshot[relativeUrl] =
|
|
362
|
+
contentSnapshotNaturalOrder[relativeUrl];
|
|
317
363
|
});
|
|
318
364
|
return directorySnapshot;
|
|
319
365
|
};
|