@cronn/lib-file-snapshots 0.10.0 → 0.12.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/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  interface SnapshotSerializer {
2
+ /**
3
+ * The file extension associated with the serialized value
4
+ */
5
+ readonly fileExtension: string;
2
6
  /**
3
7
  * Returns true when value can be serialized
4
8
  *
@@ -9,19 +13,10 @@ interface SnapshotSerializer {
9
13
  * Serializes value
10
14
  *
11
15
  * @param value The value to be serialized
16
+ * @return {string} The serialized value
12
17
  * @throws {Error} Will throw an error if value cannot be serialized.
13
18
  */
14
- serialize(value: unknown): SnapshotSerializerResult;
15
- }
16
- interface SnapshotSerializerResult {
17
- /**
18
- * The serialized value
19
- */
20
- serializedValue: string;
21
- /**
22
- * The file extension associated with the serialized value
23
- */
24
- fileExtension: string;
19
+ serialize(value: unknown): string;
25
20
  }
26
21
 
27
22
  interface JsonSerializerOptions {
@@ -42,11 +37,12 @@ interface JsonNormalizerContext {
42
37
  index?: number;
43
38
  }
44
39
  declare class JsonSerializer implements SnapshotSerializer {
40
+ readonly fileExtension = "json";
45
41
  private readonly includeUndefinedObjectProperties;
46
42
  private readonly normalizers;
47
43
  constructor(options?: JsonSerializerOptions);
48
44
  canSerialize(_value: unknown): boolean;
49
- serialize(value: unknown): SnapshotSerializerResult;
45
+ serialize(value: unknown): string;
50
46
  private normalizeValueRecursive;
51
47
  private normalizeNumber;
52
48
  private normalizeArray;
@@ -67,10 +63,11 @@ interface TextSerializerOptions {
67
63
  }
68
64
  type TextNormalizer = (value: string) => string;
69
65
  declare class TextSerializer implements SnapshotSerializer {
66
+ readonly fileExtension = "txt";
70
67
  private readonly normalizers;
71
68
  constructor(options?: TextSerializerOptions);
72
69
  canSerialize(value: unknown): value is string;
73
- serialize(value: unknown): SnapshotSerializerResult;
70
+ serialize(value: unknown): string;
74
71
  private normalizeValue;
75
72
  }
76
73
 
@@ -87,8 +84,6 @@ interface ValidationFileMatcherConfig {
87
84
  * @default "data/test/output"
88
85
  */
89
86
  outputDir?: string;
90
- }
91
- interface MatchValidationFileOptions {
92
87
  /**
93
88
  * The full path to the current test file
94
89
  *
@@ -122,18 +117,23 @@ type SnapshotNamingStrategy = "file" | "fileSuffix";
122
117
  interface ValidationFileMatcherResult {
123
118
  actual: string;
124
119
  expected: string;
125
- actualFile: string;
126
- validationFile: string;
120
+ outputFilePath: string;
121
+ validationFilePath: string;
127
122
  message: () => string;
123
+ writeFileSnapshots: () => void;
128
124
  }
129
125
 
130
126
  declare class ValidationFileMatcher {
131
- private readonly validationDir;
132
- private readonly outputDir;
133
- constructor(config?: ValidationFileMatcherConfig);
134
- matchFileSnapshot(actual: unknown, options: MatchValidationFileOptions): ValidationFileMatcherResult;
135
- private buildValidationFilePath;
127
+ private readonly serializer;
128
+ private readonly filePaths;
129
+ private readonly validationFile;
130
+ constructor(config: ValidationFileMatcherConfig);
131
+ get isValidationFileMissing(): boolean;
132
+ matchFileSnapshot(actual: unknown): ValidationFileMatcherResult;
133
+ private buildFilePaths;
136
134
  private applyNamingStrategy;
135
+ private readValidationFile;
136
+ private createMatcherResult;
137
137
  private writeFileSnapshots;
138
138
  }
139
139
 
@@ -142,4 +142,4 @@ declare function isArray(value: unknown): value is Array<unknown>;
142
142
  type PlainObject = Record<PropertyKey, unknown>;
143
143
  declare function isPlainObject(value: unknown): value is PlainObject;
144
144
 
145
- export { type JsonNormalizer, type JsonNormalizerContext, JsonSerializer, type PlainObject, type SnapshotNamingStrategy, type SnapshotSerializer, type TextNormalizer, TextSerializer, ValidationFileMatcher, isArray, isPlainObject, isString };
145
+ export { type JsonNormalizer, type JsonNormalizerContext, JsonSerializer, type PlainObject, type SnapshotNamingStrategy, type SnapshotSerializer, type TextNormalizer, TextSerializer, ValidationFileMatcher, type ValidationFileMatcherResult, isArray, isPlainObject, isString };
package/dist/index.js CHANGED
@@ -1,3 +1,28 @@
1
+ // src/utils/file.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ var NEW_LINE_SEPARATOR = "\n";
5
+ var EMOJI_REGEXP = /(?!(\*|#|\d))[\p{Extended_Pictographic}\p{Emoji_Component}]|[\u0030-\u0039]\ufe0f?[\u20e3]|[\u002A\u0023]?\ufe0f?[\u20e3]/gu;
6
+ function normalizeFileName(testName) {
7
+ return testName.replaceAll(EMOJI_REGEXP, "").replaceAll(/[+*%~<>?!$#'"`|\\/()[\]{}]/g, "").replaceAll(/[\s.:,;]+/g, "_").replaceAll(/_{2,}/g, "_");
8
+ }
9
+ function readSnapshotFile(path3) {
10
+ return fs.readFileSync(path3, { encoding: "utf8" });
11
+ }
12
+ function writeSnapshotFile(file, data) {
13
+ mkdirRecursive(path.dirname(file));
14
+ fs.writeFileSync(file, data, { encoding: "utf8" });
15
+ }
16
+ function mkdirRecursive(path3) {
17
+ fs.mkdirSync(path3, { recursive: true });
18
+ }
19
+ function addTrailingNewLine(data) {
20
+ return `${data}${NEW_LINE_SEPARATOR}`;
21
+ }
22
+ function addMissingFileMarker(data) {
23
+ return `===== missing file =====${NEW_LINE_SEPARATOR}${data}`;
24
+ }
25
+
1
26
  // src/utils/guards.ts
2
27
  function isString(value) {
3
28
  return typeof value === "string";
@@ -18,6 +43,7 @@ function isPlainObject(value) {
18
43
 
19
44
  // src/serializers/json-serializer.ts
20
45
  var JsonSerializer = class {
46
+ fileExtension = "json";
21
47
  includeUndefinedObjectProperties;
22
48
  normalizers;
23
49
  constructor(options = {}) {
@@ -29,11 +55,8 @@ var JsonSerializer = class {
29
55
  }
30
56
  serialize(value) {
31
57
  const jsonValue = this.normalizeValueRecursive(value);
32
- const serializedValue = JSON.stringify(jsonValue, void 0, 2);
33
- return {
34
- serializedValue,
35
- fileExtension: "json"
36
- };
58
+ const jsonString = JSON.stringify(jsonValue, void 0, 2);
59
+ return addTrailingNewLine(jsonString);
37
60
  }
38
61
  normalizeValueRecursive(value, context) {
39
62
  const customValue = this.applyCustomNormalizers(value, context);
@@ -179,6 +202,7 @@ var JsonSerializer = class {
179
202
 
180
203
  // src/serializers/text-serializer.ts
181
204
  var TextSerializer = class {
205
+ fileExtension = "txt";
182
206
  normalizers;
183
207
  constructor(options = {}) {
184
208
  this.normalizers = options.normalizers ?? [];
@@ -192,10 +216,8 @@ var TextSerializer = class {
192
216
  `Missing text serialization for value of type ${typeof value}.`
193
217
  );
194
218
  }
195
- return {
196
- serializedValue: this.normalizeValue(value),
197
- fileExtension: "txt"
198
- };
219
+ const normalizedValue = this.normalizeValue(value);
220
+ return addTrailingNewLine(normalizedValue);
199
221
  }
200
222
  normalizeValue(value) {
201
223
  let normalizedValue = value;
@@ -208,73 +230,55 @@ var TextSerializer = class {
208
230
 
209
231
  // src/matcher/validation-file-matcher.ts
210
232
  import * as fs2 from "fs";
211
- import * as path from "path";
212
-
213
- // src/utils/file.ts
214
- import fs from "fs";
215
- var NEW_LINE_SEPARATOR = "\n";
216
- var EMOJI_REGEXP = /(?!(\*|#|\d))[\p{Extended_Pictographic}\p{Emoji_Component}]|[\u0030-\u0039]\ufe0f?[\u20e3]|[\u002A\u0023]?\ufe0f?[\u20e3]/gu;
217
- function normalizeFileName(testName) {
218
- return testName.replaceAll(EMOJI_REGEXP, "").replaceAll(/[+*%~<>?!$#'"`|\\/()[\]{}]/g, "").replaceAll(/[\s.:,;]+/g, "_").replaceAll(/_{2,}/g, "_");
219
- }
220
- function mkdirRecursive(path2) {
221
- fs.mkdirSync(path2, { recursive: true });
222
- }
223
- function readSnapshotFile(path2) {
224
- return fs.readFileSync(path2, { encoding: "utf8" });
225
- }
226
- function writeSnapshotFile(file, data, markAsMissing = false) {
227
- let finalizedData = addTrailingNewLine(data);
228
- if (markAsMissing) {
229
- finalizedData = addMissingFileMarker(finalizedData);
230
- }
231
- fs.writeFileSync(file, finalizedData, { encoding: "utf8" });
232
- }
233
- function addTrailingNewLine(data) {
234
- return `${data}${NEW_LINE_SEPARATOR}`;
235
- }
236
- function addMissingFileMarker(data) {
237
- return `===== missing file =====${NEW_LINE_SEPARATOR}${data}`;
238
- }
239
-
240
- // src/matcher/validation-file-matcher.ts
233
+ import * as path2 from "path";
241
234
  var ValidationFileMatcher = class {
242
- validationDir;
243
- outputDir;
244
- constructor(config = {}) {
245
- this.validationDir = config.validationDir ?? "data/test/validation";
246
- this.outputDir = config.outputDir ?? "data/test/output";
235
+ serializer;
236
+ filePaths;
237
+ validationFile;
238
+ constructor(config) {
239
+ this.serializer = config.serializer;
240
+ this.filePaths = this.buildFilePaths(config);
241
+ this.validationFile = this.readValidationFile();
247
242
  }
248
- matchFileSnapshot(actual, options) {
249
- const { testPath, titlePath, name, namingStrategy, serializer } = options;
250
- if (!serializer.canSerialize(actual)) {
243
+ get isValidationFileMissing() {
244
+ return this.validationFile === void 0;
245
+ }
246
+ matchFileSnapshot(actual) {
247
+ if (!this.serializer.canSerialize(actual)) {
251
248
  throw new Error(`Cannot serialize value of type ${typeof actual}`);
252
249
  }
253
- const serializerResult = serializer.serialize(actual);
254
- const validationFilePath = this.buildValidationFilePath({
255
- titlePath,
256
- testPath,
257
- name,
258
- namingStrategy,
259
- fileExtension: serializerResult.fileExtension
250
+ const serializedActual = this.serializer.serialize(actual);
251
+ if (this.validationFile === void 0) {
252
+ return this.createMatcherResult({
253
+ actual: serializedActual,
254
+ expected: addMissingFileMarker(serializedActual)
255
+ });
256
+ }
257
+ return this.createMatcherResult({
258
+ actual: serializedActual,
259
+ expected: this.validationFile
260
260
  });
261
- return this.writeFileSnapshots(
262
- serializerResult.serializedValue,
263
- validationFilePath
264
- );
265
261
  }
266
- buildValidationFilePath(options) {
267
- const { testPath, titlePath, name, namingStrategy, fileExtension } = options;
268
- const normalizedTitlePath = titlePath.map(normalizeFileName);
269
- const filePath = path.join(testPath, ...normalizedTitlePath);
270
- const fullFilePath = this.applyNamingStrategy(
271
- filePath,
272
- name,
273
- namingStrategy
262
+ buildFilePaths(config) {
263
+ const validationDir = config.validationDir ?? "data/test/validation";
264
+ const outputDir = config.outputDir ?? "data/test/output";
265
+ const normalizedTitlePath = config.titlePath.map(normalizeFileName);
266
+ const relativeFilePath = path2.join(config.testPath, ...normalizedTitlePath);
267
+ const relativeFilePathWithName = this.applyNamingStrategy(
268
+ relativeFilePath,
269
+ config.name,
270
+ config.namingStrategy
274
271
  );
275
- return `${fullFilePath}.${fileExtension}`;
272
+ const relativeFilePathWithExtension = `${relativeFilePathWithName}.${this.serializer.fileExtension}`;
273
+ return {
274
+ outputFilePath: path2.join(outputDir, relativeFilePathWithExtension),
275
+ validationFilePath: path2.join(
276
+ validationDir,
277
+ relativeFilePathWithExtension
278
+ )
279
+ };
276
280
  }
277
- applyNamingStrategy(filePath, snapshotName, namingStrategy) {
281
+ applyNamingStrategy(filePath, snapshotName, namingStrategy = "file") {
278
282
  if (snapshotName === void 0) {
279
283
  return filePath;
280
284
  }
@@ -282,29 +286,36 @@ var ValidationFileMatcher = class {
282
286
  if (namingStrategy === "fileSuffix") {
283
287
  return `${filePath}_${normalizedSnapshotName}`;
284
288
  }
285
- return path.join(filePath, normalizedSnapshotName);
289
+ return path2.join(filePath, normalizedSnapshotName);
286
290
  }
287
- writeFileSnapshots(actual, validationFilePath) {
288
- const validationFileDir = path.dirname(validationFilePath);
289
- const testOutputDir = `${this.outputDir}/${validationFileDir}`;
290
- const actualFile = `${this.outputDir}/${validationFilePath}`;
291
- const testValidationDir = `${this.validationDir}/${validationFileDir}`;
292
- const validationFile = `${this.validationDir}/${validationFilePath}`;
293
- mkdirRecursive(testOutputDir);
294
- mkdirRecursive(testValidationDir);
295
- if (!fs2.existsSync(validationFile)) {
296
- writeSnapshotFile(validationFile, actual, true);
291
+ readValidationFile() {
292
+ const { validationFilePath } = this.filePaths;
293
+ if (!fs2.existsSync(validationFilePath)) {
294
+ return void 0;
297
295
  }
298
- writeSnapshotFile(actualFile, actual);
296
+ return readSnapshotFile(validationFilePath);
297
+ }
298
+ createMatcherResult(params) {
299
+ const { actual, expected } = params;
300
+ const { outputFilePath, validationFilePath } = this.filePaths;
299
301
  return {
300
- actual: readSnapshotFile(actualFile),
301
- expected: readSnapshotFile(validationFile),
302
- actualFile,
303
- validationFile,
304
- message: () => `Actual file '${actualFile}'
305
- does not match validation file '${validationFile}'`
302
+ actual,
303
+ expected,
304
+ outputFilePath,
305
+ validationFilePath,
306
+ message: () => this.isValidationFileMissing ? `Missing validation file '${validationFilePath}'` : `Output file '${outputFilePath}'
307
+ does not match validation file '${validationFilePath}'`,
308
+ writeFileSnapshots: () => this.writeFileSnapshots(params)
306
309
  };
307
310
  }
311
+ writeFileSnapshots(matcherResult) {
312
+ const { actual, expected } = matcherResult;
313
+ const { outputFilePath, validationFilePath } = this.filePaths;
314
+ writeSnapshotFile(outputFilePath, actual);
315
+ if (this.isValidationFileMissing) {
316
+ writeSnapshotFile(validationFilePath, expected);
317
+ }
318
+ }
308
319
  };
309
320
  export {
310
321
  JsonSerializer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/lib-file-snapshots",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "The library agnostic core for testing file snapshots",
5
5
  "keywords": [
6
6
  "file snapshots"
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "homepage": "https://github.com/cronn/file-snapshots",
19
19
  "engines": {
20
- "node": ">=22",
20
+ "node": ">=22.18",
21
21
  "pnpm": ">=10"
22
22
  },
23
23
  "type": "module",
@@ -33,28 +33,24 @@
33
33
  ],
34
34
  "devDependencies": {
35
35
  "@trivago/prettier-plugin-sort-imports": "5.2.2",
36
- "@types/node": "22.15.30",
36
+ "@types/node": "22.16.5",
37
37
  "@vitest/coverage-istanbul": "3.2.4",
38
- "eslint": "9.30.1",
39
- "eslint-config-prettier": "10.1.5",
38
+ "eslint": "9.32.0",
39
+ "eslint-config-prettier": "10.1.8",
40
40
  "eslint-plugin-unused-imports": "4.1.4",
41
- "jiti": "2.4.2",
42
41
  "prettier": "3.6.2",
43
42
  "tsup": "8.5.0",
44
43
  "typescript": "5.8.3",
45
- "typescript-eslint": "8.36.0",
44
+ "typescript-eslint": "8.38.0",
46
45
  "vitest": "3.2.4",
47
46
  "@cronn/shared-configs": "0.0.0"
48
47
  },
49
48
  "scripts": {
50
49
  "lint:check": "eslint src/ --max-warnings=0",
51
50
  "lint:fix": "eslint src/ --max-warnings=0 --fix",
52
- "check": "pnpm run compile && pnpm run lint:check",
53
- "fix": "pnpm run lint:fix",
54
51
  "test": "vitest run",
55
52
  "test:coverage": "vitest run --coverage",
56
53
  "compile": "tsc",
57
- "build": "tsup",
58
- "ci": "pnpm run check && pnpm run test:coverage && pnpm run build"
54
+ "build": "tsup"
59
55
  }
60
56
  }