@cronn/lib-file-snapshots 0.19.0 → 1.0.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.
@@ -0,0 +1,163 @@
1
+ //#region src/types/serializer.d.ts
2
+ interface SnapshotSerializer {
3
+ /**
4
+ * The file extension associated with the serialized value
5
+ */
6
+ readonly fileExtension: string;
7
+ /**
8
+ * Returns true when value can be serialized
9
+ *
10
+ * @param value The value to be serialized
11
+ */
12
+ canSerialize(value: unknown): boolean;
13
+ /**
14
+ * Serializes value
15
+ *
16
+ * @param value The value to be serialized
17
+ * @return {string} The serialized value
18
+ * @throws {Error} Will throw an error if value cannot be serialized.
19
+ */
20
+ serialize(value: unknown): string;
21
+ }
22
+ //#endregion
23
+ //#region src/serializers/json-serializer.d.ts
24
+ interface JsonSerializerOptions {
25
+ /**
26
+ * Serializes `undefined` properties in objects. By default, they are omitted.
27
+ *
28
+ * @default false
29
+ */
30
+ includeUndefinedObjectProperties?: boolean;
31
+ /**
32
+ * Custom normalizers to apply before serialization
33
+ */
34
+ normalizers?: Array<JsonNormalizer>;
35
+ /**
36
+ * Indentation size in spaces
37
+ *
38
+ * @default 2
39
+ */
40
+ indentSize?: number;
41
+ }
42
+ type JsonNormalizer = (value: unknown, context: JsonNormalizerContext) => unknown;
43
+ interface JsonNormalizerContext {
44
+ key?: string;
45
+ index?: number;
46
+ }
47
+ declare class JsonSerializer implements SnapshotSerializer {
48
+ readonly fileExtension = "json";
49
+ private readonly includeUndefinedObjectProperties;
50
+ private readonly normalizers;
51
+ private readonly indentSize;
52
+ constructor(options?: JsonSerializerOptions);
53
+ canSerialize(_value: unknown): boolean;
54
+ serialize(value: unknown): string;
55
+ private normalizeValueRecursive;
56
+ private normalizeNumber;
57
+ private normalizeArray;
58
+ private normalizeObject;
59
+ private normalizeDate;
60
+ private normalizePlainObject;
61
+ private normalizeMap;
62
+ private serializedValue;
63
+ private assertKeyType;
64
+ private applyCustomNormalizers;
65
+ }
66
+ //#endregion
67
+ //#region src/serializers/text-serializer.d.ts
68
+ interface TextSerializerOptions {
69
+ /**
70
+ * Custom normalizers to apply before serialization
71
+ */
72
+ normalizers?: Array<TextNormalizer>;
73
+ /**
74
+ * File extension used for storing the text file
75
+ *
76
+ * @default "txt"
77
+ */
78
+ fileExtension?: string;
79
+ }
80
+ type TextNormalizer = (value: string) => string;
81
+ declare class TextSerializer implements SnapshotSerializer {
82
+ readonly fileExtension: string;
83
+ private readonly normalizers;
84
+ constructor(options?: TextSerializerOptions);
85
+ canSerialize(value: unknown): value is string;
86
+ serialize(value: unknown): string;
87
+ private normalizeValue;
88
+ }
89
+ //#endregion
90
+ //#region src/types/matcher.d.ts
91
+ interface ValidationFileMatcherConfig {
92
+ /**
93
+ * Directory in which golden masters are stored
94
+ */
95
+ validationDir: string;
96
+ /**
97
+ * Directory in which file snapshots from test runs are stored
98
+ */
99
+ outputDir: string;
100
+ /**
101
+ * The full path to the snapshot file
102
+ *
103
+ * @example "src/tests/feature/test/when_x_then_y"
104
+ */
105
+ filePath: string;
106
+ /**
107
+ * The serializer to use for the snapshot
108
+ */
109
+ serializer: SnapshotSerializer;
110
+ /**
111
+ * Whether to update golden masters with the actual result.
112
+ *
113
+ * @default "missing"
114
+ */
115
+ updateSnapshots?: UpdateSnapshotsType;
116
+ }
117
+ type UpdateSnapshotsType = "all" | "missing" | "none";
118
+ interface ValidationFileMatcherResult {
119
+ actual: string;
120
+ expected: string;
121
+ outputFilePath: string;
122
+ validationFilePath: string;
123
+ message: () => string;
124
+ writeFileSnapshots: () => void;
125
+ }
126
+ type FilePathResolver = (params: FilePathResolverParams) => string;
127
+ interface FilePathResolverParams {
128
+ testPath: string;
129
+ titlePath: Array<string>;
130
+ name?: string;
131
+ }
132
+ //#endregion
133
+ //#region src/matcher/validation-file-matcher.d.ts
134
+ declare class ValidationFileMatcher {
135
+ private readonly updateSnapshots;
136
+ private readonly serializer;
137
+ private readonly filePaths;
138
+ private validationFile;
139
+ constructor(config: ValidationFileMatcherConfig);
140
+ get isValidationFileMissing(): boolean;
141
+ get isUpdate(): boolean;
142
+ matchFileSnapshot(actual: unknown): ValidationFileMatcherResult;
143
+ private buildFilePaths;
144
+ private readValidationFile;
145
+ private createMatcherResult;
146
+ private writeFileSnapshots;
147
+ private resolveExpected;
148
+ }
149
+ //#endregion
150
+ //#region src/matcher/file-path-resolver.d.ts
151
+ declare function resolveNameAsFile(params: FilePathResolverParams): string;
152
+ declare function resolveNameAsFileSuffix(params: FilePathResolverParams): string;
153
+ //#endregion
154
+ //#region src/utils/file.d.ts
155
+ declare function normalizeFileName(testName: string): string;
156
+ //#endregion
157
+ //#region src/utils/guards.d.ts
158
+ declare function isString(value: unknown): value is string;
159
+ declare function isArray(value: unknown): value is Array<unknown>;
160
+ type PlainObject = Record<PropertyKey, unknown>;
161
+ declare function isPlainObject(value: unknown): value is PlainObject;
162
+ //#endregion
163
+ export { type FilePathResolver, type FilePathResolverParams, type JsonNormalizer, type JsonNormalizerContext, JsonSerializer, type PlainObject, type SnapshotSerializer, type TextNormalizer, TextSerializer, type UpdateSnapshotsType, ValidationFileMatcher, type ValidationFileMatcherResult, isArray, isPlainObject, isString, normalizeFileName, resolveNameAsFile, resolveNameAsFileSuffix };
package/dist/index.mjs ADDED
@@ -0,0 +1,266 @@
1
+ import * as fs$1 from "node:fs";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import * as path$1 from "node:path";
5
+ import path from "node:path";
6
+
7
+ //#region src/utils/file.ts
8
+ const NEW_LINE_SEPARATOR = "\n";
9
+ const EMOJI_REGEXP = /(?!(\*|#|\d))[\p{Extended_Pictographic}\p{Emoji_Component}]|[\u0030-\u0039]\ufe0f?[\u20e3]|[\u002A\u0023]?\ufe0f?[\u20e3]/gu;
10
+ function normalizeFileName(testName) {
11
+ return testName.replaceAll(EMOJI_REGEXP, "").replaceAll(/[+*%~<>?!$#'"`|\\/()[\]{}]/g, "").replaceAll(/[\s.:,;]+/g, "_").replaceAll(/_{2,}/g, "_");
12
+ }
13
+ function readSnapshotFile(filePath) {
14
+ const contents = fs.readFileSync(filePath, { encoding: "utf8" });
15
+ if (os.EOL === NEW_LINE_SEPARATOR) return contents;
16
+ return contents.replace(new RegExp(os.EOL, "g"), NEW_LINE_SEPARATOR);
17
+ }
18
+ function writeSnapshotFile(filePath, data) {
19
+ mkdirRecursive(path.dirname(filePath));
20
+ fs.writeFileSync(filePath, data, { encoding: "utf8" });
21
+ }
22
+ function mkdirRecursive(dirPath) {
23
+ fs.mkdirSync(dirPath, { recursive: true });
24
+ }
25
+ function addTrailingNewLine(data) {
26
+ return `${data}${NEW_LINE_SEPARATOR}`;
27
+ }
28
+ function addMissingFileMarker(data) {
29
+ return `===== missing file =====${NEW_LINE_SEPARATOR}${data}`;
30
+ }
31
+
32
+ //#endregion
33
+ //#region src/utils/guards.ts
34
+ function isString(value) {
35
+ return typeof value === "string";
36
+ }
37
+ function isArray(value) {
38
+ return Array.isArray(value);
39
+ }
40
+ function isObject(value) {
41
+ return typeof value === "object" && value !== null && !isArray(value);
42
+ }
43
+ function isPlainObject(value) {
44
+ if (!isObject(value)) return false;
45
+ const proto = Object.getPrototypeOf(value);
46
+ return proto === null || proto === Object.prototype;
47
+ }
48
+
49
+ //#endregion
50
+ //#region src/utils/validators.ts
51
+ function isPositiveInteger(value) {
52
+ return Number.isInteger(value) && value > 0;
53
+ }
54
+
55
+ //#endregion
56
+ //#region src/serializers/json-serializer.ts
57
+ var JsonSerializer = class {
58
+ fileExtension = "json";
59
+ includeUndefinedObjectProperties;
60
+ normalizers;
61
+ indentSize;
62
+ constructor(options = {}) {
63
+ if (options.indentSize !== void 0 && !isPositiveInteger(options.indentSize)) throw new Error("Invalid option indentSize: value must be a positive integer.");
64
+ this.includeUndefinedObjectProperties = options.includeUndefinedObjectProperties ?? false;
65
+ this.normalizers = options.normalizers ?? [];
66
+ this.indentSize = options.indentSize ?? 2;
67
+ }
68
+ canSerialize(_value) {
69
+ return true;
70
+ }
71
+ serialize(value) {
72
+ const jsonValue = this.normalizeValueRecursive(value);
73
+ return addTrailingNewLine(JSON.stringify(jsonValue, void 0, this.indentSize));
74
+ }
75
+ normalizeValueRecursive(value, context) {
76
+ const customValue = this.applyCustomNormalizers(value, context);
77
+ if (customValue === void 0) return this.serializedValue("undefined");
78
+ if (customValue === null || isString(customValue) || typeof customValue === "boolean") return context === void 0 ? this.serializedValue(typeof customValue, { value: customValue }) : customValue;
79
+ if (typeof customValue === "number") return this.normalizeNumber(customValue);
80
+ if (typeof customValue === "bigint") return this.serializedValue("bigint", { value: customValue.toString() });
81
+ if (typeof customValue === "symbol") return this.serializedValue("symbol", { description: customValue.description });
82
+ if (typeof customValue === "function") return this.serializedValue("function", { name: customValue.name });
83
+ if (typeof customValue === "object") return this.normalizeObject(customValue);
84
+ throw new Error(`Missing JSON normalization for value of type ${typeof customValue}.`);
85
+ }
86
+ normalizeNumber(value) {
87
+ if (Number.isNaN(value)) return this.serializedValue("number", { value: "Number.NaN" });
88
+ switch (value) {
89
+ case Number.MIN_VALUE: return this.serializedValue("number", { value: "Number.MIN_VALUE" });
90
+ case Number.MAX_VALUE: return this.serializedValue("number", { value: "Number.MAX_VALUE" });
91
+ case Number.MIN_SAFE_INTEGER: return this.serializedValue("number", { value: "Number.MIN_SAFE_INTEGER" });
92
+ case Number.MAX_SAFE_INTEGER: return this.serializedValue("number", { value: "Number.MAX_SAFE_INTEGER" });
93
+ case Number.NEGATIVE_INFINITY: return this.serializedValue("number", { value: "Number.NEGATIVE_INFINITY" });
94
+ case Number.POSITIVE_INFINITY: return this.serializedValue("number", { value: "Number.POSITIVE_INFINITY" });
95
+ default: return value;
96
+ }
97
+ }
98
+ normalizeArray(value) {
99
+ return value.map((item, index) => this.normalizeValueRecursive(item, { index }));
100
+ }
101
+ normalizeObject(value) {
102
+ if (isArray(value)) return this.normalizeArray(value);
103
+ if (isPlainObject(value)) return this.normalizePlainObject(value);
104
+ if (value instanceof Date) return this.normalizeDate(value);
105
+ if (value instanceof Promise) return this.serializedValue("Promise");
106
+ if (value instanceof Set) return this.serializedValue("Set", { values: this.normalizeArray(Array.from(value.values())) });
107
+ if (value instanceof Map) {
108
+ const mapAsObject = this.normalizeMap(value);
109
+ return this.serializedValue("Map", { values: this.normalizePlainObject(mapAsObject) });
110
+ }
111
+ throw new Error(`Missing JSON normalization for object of type ${Object.getPrototypeOf(value)}`);
112
+ }
113
+ normalizeDate(value) {
114
+ if (Number.isNaN(value.getTime())) return this.serializedValue("Date", { value: "Invalid date" });
115
+ return this.serializedValue("Date", { value: value.toISOString() });
116
+ }
117
+ normalizePlainObject(value) {
118
+ const normalizedObject = {};
119
+ for (const [key, propertyValue] of Object.entries(value)) {
120
+ if (propertyValue === void 0 && !this.includeUndefinedObjectProperties) continue;
121
+ this.assertKeyType(key);
122
+ normalizedObject[key] = this.normalizeValueRecursive(propertyValue, { key });
123
+ }
124
+ return normalizedObject;
125
+ }
126
+ normalizeMap(value) {
127
+ return Array.from(value.entries()).reduce((object, [key, propertyValue]) => {
128
+ this.assertKeyType(key);
129
+ object[key] = propertyValue;
130
+ return object;
131
+ }, {});
132
+ }
133
+ serializedValue(type, additionalProps = {}) {
134
+ return {
135
+ $type: type,
136
+ ...additionalProps
137
+ };
138
+ }
139
+ assertKeyType(key) {
140
+ if (!isString(key)) throw new Error(`Key of type ${typeof key} cannot be normalized.`);
141
+ }
142
+ applyCustomNormalizers(value, context = {}) {
143
+ let normalizedValue = value;
144
+ for (const normalizer of this.normalizers) normalizedValue = normalizer(normalizedValue, context);
145
+ return normalizedValue;
146
+ }
147
+ };
148
+
149
+ //#endregion
150
+ //#region src/serializers/text-serializer.ts
151
+ var TextSerializer = class {
152
+ fileExtension;
153
+ normalizers;
154
+ constructor(options = {}) {
155
+ this.normalizers = options.normalizers ?? [];
156
+ this.fileExtension = options.fileExtension ?? "txt";
157
+ }
158
+ canSerialize(value) {
159
+ return isString(value);
160
+ }
161
+ serialize(value) {
162
+ if (!this.canSerialize(value)) throw new Error(`Missing text serialization for value of type ${typeof value}.`);
163
+ return addTrailingNewLine(this.normalizeValue(value));
164
+ }
165
+ normalizeValue(value) {
166
+ let normalizedValue = value;
167
+ for (const normalizer of this.normalizers) normalizedValue = normalizer(normalizedValue);
168
+ return normalizedValue.trim();
169
+ }
170
+ };
171
+
172
+ //#endregion
173
+ //#region src/matcher/validation-file-matcher.ts
174
+ var ValidationFileMatcher = class {
175
+ updateSnapshots;
176
+ serializer;
177
+ filePaths;
178
+ validationFile;
179
+ constructor(config) {
180
+ this.updateSnapshots = config.updateSnapshots ?? "missing";
181
+ this.serializer = config.serializer;
182
+ this.filePaths = this.buildFilePaths(config);
183
+ this.validationFile = this.readValidationFile();
184
+ }
185
+ get isValidationFileMissing() {
186
+ return this.validationFile === void 0;
187
+ }
188
+ get isUpdate() {
189
+ return this.updateSnapshots === "all" || this.isValidationFileMissing && this.updateSnapshots === "missing";
190
+ }
191
+ matchFileSnapshot(actual) {
192
+ if (!this.serializer.canSerialize(actual)) throw new Error(`Cannot serialize value of type ${typeof actual}`);
193
+ const serializedActual = this.serializer.serialize(actual);
194
+ const expected = this.resolveExpected(serializedActual);
195
+ return this.createMatcherResult({
196
+ actual: serializedActual,
197
+ expected
198
+ });
199
+ }
200
+ buildFilePaths(config) {
201
+ const { validationDir, outputDir, filePath, serializer } = config;
202
+ const filePathWithExtension = `${filePath}.${serializer.fileExtension}`;
203
+ return {
204
+ outputFilePath: path$1.join(outputDir, filePathWithExtension),
205
+ validationFilePath: path$1.join(validationDir, filePathWithExtension)
206
+ };
207
+ }
208
+ readValidationFile() {
209
+ const { validationFilePath } = this.filePaths;
210
+ if (!fs$1.existsSync(validationFilePath)) return;
211
+ return readSnapshotFile(validationFilePath);
212
+ }
213
+ createMatcherResult(params) {
214
+ const { actual, expected } = params;
215
+ const { outputFilePath, validationFilePath } = this.filePaths;
216
+ const isValidationFileMissing = this.isValidationFileMissing;
217
+ return {
218
+ actual,
219
+ expected,
220
+ outputFilePath,
221
+ validationFilePath,
222
+ message: () => isValidationFileMissing ? `Missing validation file '${validationFilePath}'` : `Output file '${outputFilePath}'\ndoes not match validation file '${validationFilePath}'`,
223
+ writeFileSnapshots: () => this.writeFileSnapshots(params)
224
+ };
225
+ }
226
+ writeFileSnapshots(matcherResult) {
227
+ const { actual, expected } = matcherResult;
228
+ const { outputFilePath, validationFilePath } = this.filePaths;
229
+ writeSnapshotFile(outputFilePath, actual);
230
+ if (this.isUpdate) {
231
+ const validationFileData = this.isValidationFileMissing ? expected : actual;
232
+ writeSnapshotFile(validationFilePath, validationFileData);
233
+ this.validationFile = validationFileData;
234
+ }
235
+ }
236
+ resolveExpected(actual) {
237
+ if (this.validationFile === void 0) return addMissingFileMarker(actual);
238
+ return this.validationFile;
239
+ }
240
+ };
241
+
242
+ //#endregion
243
+ //#region src/matcher/file-path-resolver.ts
244
+ function resolveNameAsFile(params) {
245
+ return resolveFilePath(params, ({ basePath, normalizedName }) => path.join(basePath, normalizedName));
246
+ }
247
+ function resolveNameAsFileSuffix(params) {
248
+ return resolveFilePath(params, ({ basePath, normalizedName }) => `${basePath}_${normalizedName}`);
249
+ }
250
+ function resolveBasePath(params) {
251
+ const { testPath, titlePath } = params;
252
+ const normalizedTitlePath = titlePath.map(normalizeFileName);
253
+ return path.join(testPath, ...normalizedTitlePath);
254
+ }
255
+ function resolveFilePath(params, resolveNamedFilePath) {
256
+ const { name, ...baseParams } = params;
257
+ const basePath = resolveBasePath(baseParams);
258
+ if (name === void 0) return basePath;
259
+ return resolveNamedFilePath({
260
+ basePath,
261
+ normalizedName: normalizeFileName(name)
262
+ });
263
+ }
264
+
265
+ //#endregion
266
+ export { JsonSerializer, TextSerializer, ValidationFileMatcher, isArray, isPlainObject, isString, normalizeFileName, resolveNameAsFile, resolveNameAsFileSuffix };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cronn/lib-file-snapshots",
3
- "version": "0.19.0",
3
+ "version": "1.0.0",
4
4
  "description": "The library agnostic core for testing file snapshots",
5
5
  "keywords": [
6
6
  "file snapshots"
@@ -12,7 +12,7 @@
12
12
  "license": "Apache-2.0",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "https://github.com/cronn/file-snapshots.git",
15
+ "url": "git+https://github.com/cronn/file-snapshots.git",
16
16
  "directory": "packages/lib-file-snapshots"
17
17
  },
18
18
  "homepage": "https://github.com/cronn/file-snapshots",
@@ -35,25 +35,27 @@
35
35
  "exports": {
36
36
  "./package.json": "./package.json",
37
37
  ".": {
38
- "types": "./dist/index.d.ts",
39
- "default": "./dist/index.js"
38
+ "types": "./dist/index.d.mts",
39
+ "default": "./dist/index.mjs"
40
40
  }
41
41
  },
42
42
  "files": [
43
43
  "dist"
44
44
  ],
45
45
  "devDependencies": {
46
+ "@arethetypeswrong/core": "0.18.2",
46
47
  "@trivago/prettier-plugin-sort-imports": "6.0.2",
47
- "@types/node": "24.10.9",
48
- "@vitest/coverage-v8": "4.0.17",
49
- "eslint": "9.39.2",
48
+ "@typescript/native-preview": "7.0.0-dev.20260218.1",
49
+ "@types/node": "24.10.13",
50
+ "@vitest/coverage-v8": "4.0.18",
51
+ "eslint": "10.0.1",
50
52
  "eslint-config-prettier": "10.1.8",
51
- "eslint-plugin-unused-imports": "4.3.0",
52
- "prettier": "3.8.0",
53
- "tsup": "8.5.1",
54
- "typescript": "5.9.3",
55
- "typescript-eslint": "8.53.1",
56
- "vitest": "4.0.17",
53
+ "eslint-plugin-unused-imports": "4.4.1",
54
+ "prettier": "3.8.1",
55
+ "publint": "0.3.17",
56
+ "tsdown": "0.20.3",
57
+ "typescript-eslint": "8.56.0",
58
+ "vitest": "4.0.18",
57
59
  "@cronn/shared-configs": "0.0.0"
58
60
  },
59
61
  "scripts": {
@@ -61,7 +63,7 @@
61
63
  "lint:fix": "eslint src/ --max-warnings=0 --fix",
62
64
  "test": "vitest run",
63
65
  "test:coverage": "vitest run --coverage",
64
- "compile": "tsc",
65
- "build": "tsup"
66
+ "compile": "tsgo",
67
+ "build": "tsdown"
66
68
  }
67
69
  }
package/dist/index.d.ts DELETED
@@ -1,155 +0,0 @@
1
- interface SnapshotSerializer {
2
- /**
3
- * The file extension associated with the serialized value
4
- */
5
- readonly fileExtension: string;
6
- /**
7
- * Returns true when value can be serialized
8
- *
9
- * @param value The value to be serialized
10
- */
11
- canSerialize(value: unknown): boolean;
12
- /**
13
- * Serializes value
14
- *
15
- * @param value The value to be serialized
16
- * @return {string} The serialized value
17
- * @throws {Error} Will throw an error if value cannot be serialized.
18
- */
19
- serialize(value: unknown): string;
20
- }
21
-
22
- interface JsonSerializerOptions {
23
- /**
24
- * Serializes `undefined` properties in objects. By default, they are omitted.
25
- *
26
- * @default false
27
- */
28
- includeUndefinedObjectProperties?: boolean;
29
- /**
30
- * Custom normalizers to apply before serialization
31
- */
32
- normalizers?: Array<JsonNormalizer>;
33
- /**
34
- * Indentation size in spaces
35
- *
36
- * @default 2
37
- */
38
- indentSize?: number;
39
- }
40
- type JsonNormalizer = (value: unknown, context: JsonNormalizerContext) => unknown;
41
- interface JsonNormalizerContext {
42
- key?: string;
43
- index?: number;
44
- }
45
- declare class JsonSerializer implements SnapshotSerializer {
46
- readonly fileExtension = "json";
47
- private readonly includeUndefinedObjectProperties;
48
- private readonly normalizers;
49
- private readonly indentSize;
50
- constructor(options?: JsonSerializerOptions);
51
- canSerialize(_value: unknown): boolean;
52
- serialize(value: unknown): string;
53
- private normalizeValueRecursive;
54
- private normalizeNumber;
55
- private normalizeArray;
56
- private normalizeObject;
57
- private normalizeDate;
58
- private normalizePlainObject;
59
- private normalizeMap;
60
- private serializedValue;
61
- private assertKeyType;
62
- private applyCustomNormalizers;
63
- }
64
-
65
- interface TextSerializerOptions {
66
- /**
67
- * Custom normalizers to apply before serialization
68
- */
69
- normalizers?: Array<TextNormalizer>;
70
- /**
71
- * File extension used for storing the text file
72
- *
73
- * @default "txt"
74
- */
75
- fileExtension?: string;
76
- }
77
- type TextNormalizer = (value: string) => string;
78
- declare class TextSerializer implements SnapshotSerializer {
79
- readonly fileExtension: string;
80
- private readonly normalizers;
81
- constructor(options?: TextSerializerOptions);
82
- canSerialize(value: unknown): value is string;
83
- serialize(value: unknown): string;
84
- private normalizeValue;
85
- }
86
-
87
- interface ValidationFileMatcherConfig {
88
- /**
89
- * Directory in which golden masters are stored
90
- */
91
- validationDir: string;
92
- /**
93
- * Directory in which file snapshots from test runs are stored
94
- */
95
- outputDir: string;
96
- /**
97
- * The full path to the snapshot file
98
- *
99
- * @example "src/tests/feature/test/when_x_then_y"
100
- */
101
- filePath: string;
102
- /**
103
- * The serializer to use for the snapshot
104
- */
105
- serializer: SnapshotSerializer;
106
- /**
107
- * Whether to update golden masters with the actual result.
108
- *
109
- * @default "missing"
110
- */
111
- updateSnapshots?: UpdateSnapshotsType;
112
- }
113
- type UpdateSnapshotsType = "all" | "missing" | "none";
114
- interface ValidationFileMatcherResult {
115
- actual: string;
116
- expected: string;
117
- outputFilePath: string;
118
- validationFilePath: string;
119
- message: () => string;
120
- writeFileSnapshots: () => void;
121
- }
122
- type FilePathResolver = (params: FilePathResolverParams) => string;
123
- interface FilePathResolverParams {
124
- testPath: string;
125
- titlePath: Array<string>;
126
- name?: string;
127
- }
128
-
129
- declare class ValidationFileMatcher {
130
- private readonly updateSnapshots;
131
- private readonly serializer;
132
- private readonly filePaths;
133
- private validationFile;
134
- constructor(config: ValidationFileMatcherConfig);
135
- get isValidationFileMissing(): boolean;
136
- get isUpdate(): boolean;
137
- matchFileSnapshot(actual: unknown): ValidationFileMatcherResult;
138
- private buildFilePaths;
139
- private readValidationFile;
140
- private createMatcherResult;
141
- private writeFileSnapshots;
142
- private resolveExpected;
143
- }
144
-
145
- declare function resolveNameAsFile(params: FilePathResolverParams): string;
146
- declare function resolveNameAsFileSuffix(params: FilePathResolverParams): string;
147
-
148
- declare function normalizeFileName(testName: string): string;
149
-
150
- declare function isString(value: unknown): value is string;
151
- declare function isArray(value: unknown): value is Array<unknown>;
152
- type PlainObject = Record<PropertyKey, unknown>;
153
- declare function isPlainObject(value: unknown): value is PlainObject;
154
-
155
- export { type FilePathResolver, type FilePathResolverParams, type JsonNormalizer, type JsonNormalizerContext, JsonSerializer, type PlainObject, type SnapshotSerializer, type TextNormalizer, TextSerializer, type UpdateSnapshotsType, ValidationFileMatcher, type ValidationFileMatcherResult, isArray, isPlainObject, isString, normalizeFileName, resolveNameAsFile, resolveNameAsFileSuffix };
package/dist/index.js DELETED
@@ -1,365 +0,0 @@
1
- // src/utils/file.ts
2
- import fs from "fs";
3
- import os from "os";
4
- import path from "path";
5
- var NEW_LINE_SEPARATOR = "\n";
6
- var EMOJI_REGEXP = /(?!(\*|#|\d))[\p{Extended_Pictographic}\p{Emoji_Component}]|[\u0030-\u0039]\ufe0f?[\u20e3]|[\u002A\u0023]?\ufe0f?[\u20e3]/gu;
7
- function normalizeFileName(testName) {
8
- return testName.replaceAll(EMOJI_REGEXP, "").replaceAll(/[+*%~<>?!$#'"`|\\/()[\]{}]/g, "").replaceAll(/[\s.:,;]+/g, "_").replaceAll(/_{2,}/g, "_");
9
- }
10
- function readSnapshotFile(path4) {
11
- const contents = fs.readFileSync(path4, { encoding: "utf8" });
12
- if (os.EOL === NEW_LINE_SEPARATOR) {
13
- return contents;
14
- }
15
- return contents.replace(new RegExp(os.EOL, "g"), NEW_LINE_SEPARATOR);
16
- }
17
- function writeSnapshotFile(file, data) {
18
- mkdirRecursive(path.dirname(file));
19
- fs.writeFileSync(file, data, { encoding: "utf8" });
20
- }
21
- function mkdirRecursive(path4) {
22
- fs.mkdirSync(path4, { recursive: true });
23
- }
24
- function addTrailingNewLine(data) {
25
- return `${data}${NEW_LINE_SEPARATOR}`;
26
- }
27
- function addMissingFileMarker(data) {
28
- return `===== missing file =====${NEW_LINE_SEPARATOR}${data}`;
29
- }
30
-
31
- // src/utils/guards.ts
32
- function isString(value) {
33
- return typeof value === "string";
34
- }
35
- function isArray(value) {
36
- return Array.isArray(value);
37
- }
38
- function isObject(value) {
39
- return typeof value === "object" && value !== null && !isArray(value);
40
- }
41
- function isPlainObject(value) {
42
- if (!isObject(value)) {
43
- return false;
44
- }
45
- const proto = Object.getPrototypeOf(value);
46
- return proto === null || proto === Object.prototype;
47
- }
48
-
49
- // src/utils/validators.ts
50
- function isPositiveInteger(value) {
51
- return Number.isInteger(value) && value > 0;
52
- }
53
-
54
- // src/serializers/json-serializer.ts
55
- var JsonSerializer = class {
56
- fileExtension = "json";
57
- includeUndefinedObjectProperties;
58
- normalizers;
59
- indentSize;
60
- constructor(options = {}) {
61
- if (options.indentSize !== void 0 && !isPositiveInteger(options.indentSize)) {
62
- throw new Error(
63
- "Invalid option indentSize: value must be a positive integer."
64
- );
65
- }
66
- this.includeUndefinedObjectProperties = options.includeUndefinedObjectProperties ?? false;
67
- this.normalizers = options.normalizers ?? [];
68
- this.indentSize = options.indentSize ?? 2;
69
- }
70
- canSerialize(_value) {
71
- return true;
72
- }
73
- serialize(value) {
74
- const jsonValue = this.normalizeValueRecursive(value);
75
- const jsonString = JSON.stringify(jsonValue, void 0, this.indentSize);
76
- return addTrailingNewLine(jsonString);
77
- }
78
- normalizeValueRecursive(value, context) {
79
- const customValue = this.applyCustomNormalizers(value, context);
80
- if (customValue === void 0) {
81
- return this.serializedValue("undefined");
82
- }
83
- if (customValue === null || isString(customValue) || typeof customValue === "boolean") {
84
- const isRoot = context === void 0;
85
- return isRoot ? this.serializedValue(typeof customValue, {
86
- value: customValue
87
- }) : customValue;
88
- }
89
- if (typeof customValue === "number") {
90
- return this.normalizeNumber(customValue);
91
- }
92
- if (typeof customValue === "bigint") {
93
- return this.serializedValue("bigint", {
94
- value: customValue.toString()
95
- });
96
- }
97
- if (typeof customValue === "symbol") {
98
- return this.serializedValue("symbol", {
99
- description: customValue.description
100
- });
101
- }
102
- if (typeof customValue === "function") {
103
- return this.serializedValue("function", { name: customValue.name });
104
- }
105
- if (typeof customValue === "object") {
106
- return this.normalizeObject(customValue);
107
- }
108
- throw new Error(
109
- `Missing JSON normalization for value of type ${typeof customValue}.`
110
- );
111
- }
112
- normalizeNumber(value) {
113
- if (Number.isNaN(value)) {
114
- return this.serializedValue("number", { value: "Number.NaN" });
115
- }
116
- switch (value) {
117
- case Number.MIN_VALUE:
118
- return this.serializedValue("number", { value: "Number.MIN_VALUE" });
119
- case Number.MAX_VALUE:
120
- return this.serializedValue("number", { value: "Number.MAX_VALUE" });
121
- case Number.MIN_SAFE_INTEGER:
122
- return this.serializedValue("number", {
123
- value: "Number.MIN_SAFE_INTEGER"
124
- });
125
- case Number.MAX_SAFE_INTEGER:
126
- return this.serializedValue("number", {
127
- value: "Number.MAX_SAFE_INTEGER"
128
- });
129
- case Number.NEGATIVE_INFINITY:
130
- return this.serializedValue("number", {
131
- value: "Number.NEGATIVE_INFINITY"
132
- });
133
- case Number.POSITIVE_INFINITY:
134
- return this.serializedValue("number", {
135
- value: "Number.POSITIVE_INFINITY"
136
- });
137
- default:
138
- return value;
139
- }
140
- }
141
- normalizeArray(value) {
142
- return value.map(
143
- (item, index) => this.normalizeValueRecursive(item, { index })
144
- );
145
- }
146
- normalizeObject(value) {
147
- if (isArray(value)) {
148
- return this.normalizeArray(value);
149
- }
150
- if (isPlainObject(value)) {
151
- return this.normalizePlainObject(value);
152
- }
153
- if (value instanceof Date) {
154
- return this.normalizeDate(value);
155
- }
156
- if (value instanceof Promise) {
157
- return this.serializedValue("Promise");
158
- }
159
- if (value instanceof Set) {
160
- return this.serializedValue("Set", {
161
- values: this.normalizeArray(Array.from(value.values()))
162
- });
163
- }
164
- if (value instanceof Map) {
165
- const mapAsObject = this.normalizeMap(value);
166
- return this.serializedValue("Map", {
167
- values: this.normalizePlainObject(mapAsObject)
168
- });
169
- }
170
- throw new Error(
171
- `Missing JSON normalization for object of type ${Object.getPrototypeOf(value)}`
172
- );
173
- }
174
- normalizeDate(value) {
175
- if (Number.isNaN(value.getTime())) {
176
- return this.serializedValue("Date", { value: "Invalid date" });
177
- }
178
- return this.serializedValue("Date", { value: value.toISOString() });
179
- }
180
- normalizePlainObject(value) {
181
- const normalizedObject = {};
182
- for (const [key, propertyValue] of Object.entries(value)) {
183
- if (propertyValue === void 0 && !this.includeUndefinedObjectProperties) {
184
- continue;
185
- }
186
- this.assertKeyType(key);
187
- normalizedObject[key] = this.normalizeValueRecursive(propertyValue, {
188
- key
189
- });
190
- }
191
- return normalizedObject;
192
- }
193
- normalizeMap(value) {
194
- return Array.from(value.entries()).reduce(
195
- (object, [key, value2]) => {
196
- this.assertKeyType(key);
197
- object[key] = value2;
198
- return object;
199
- },
200
- {}
201
- );
202
- }
203
- serializedValue(type, additionalProps = {}) {
204
- return { $type: type, ...additionalProps };
205
- }
206
- assertKeyType(key) {
207
- if (!isString(key)) {
208
- throw new Error(`Key of type ${typeof key} cannot be normalized.`);
209
- }
210
- }
211
- applyCustomNormalizers(value, context = {}) {
212
- let normalizedValue = value;
213
- for (const normalizer of this.normalizers) {
214
- normalizedValue = normalizer(normalizedValue, context);
215
- }
216
- return normalizedValue;
217
- }
218
- };
219
-
220
- // src/serializers/text-serializer.ts
221
- var TextSerializer = class {
222
- fileExtension;
223
- normalizers;
224
- constructor(options = {}) {
225
- this.normalizers = options.normalizers ?? [];
226
- this.fileExtension = options.fileExtension ?? "txt";
227
- }
228
- canSerialize(value) {
229
- return isString(value);
230
- }
231
- serialize(value) {
232
- if (!this.canSerialize(value)) {
233
- throw new Error(
234
- `Missing text serialization for value of type ${typeof value}.`
235
- );
236
- }
237
- const normalizedValue = this.normalizeValue(value);
238
- return addTrailingNewLine(normalizedValue);
239
- }
240
- normalizeValue(value) {
241
- let normalizedValue = value;
242
- for (const normalizer of this.normalizers) {
243
- normalizedValue = normalizer(normalizedValue);
244
- }
245
- return normalizedValue.trim();
246
- }
247
- };
248
-
249
- // src/matcher/validation-file-matcher.ts
250
- import * as fs2 from "fs";
251
- import * as path2 from "path";
252
- var ValidationFileMatcher = class {
253
- updateSnapshots;
254
- serializer;
255
- filePaths;
256
- validationFile;
257
- constructor(config) {
258
- this.updateSnapshots = config.updateSnapshots ?? "missing";
259
- this.serializer = config.serializer;
260
- this.filePaths = this.buildFilePaths(config);
261
- this.validationFile = this.readValidationFile();
262
- }
263
- get isValidationFileMissing() {
264
- return this.validationFile === void 0;
265
- }
266
- get isUpdate() {
267
- return this.updateSnapshots === "all" || this.isValidationFileMissing && this.updateSnapshots === "missing";
268
- }
269
- matchFileSnapshot(actual) {
270
- if (!this.serializer.canSerialize(actual)) {
271
- throw new Error(`Cannot serialize value of type ${typeof actual}`);
272
- }
273
- const serializedActual = this.serializer.serialize(actual);
274
- const expected = this.resolveExpected(serializedActual);
275
- return this.createMatcherResult({
276
- actual: serializedActual,
277
- expected
278
- });
279
- }
280
- buildFilePaths(config) {
281
- const { validationDir, outputDir, filePath, serializer } = config;
282
- const filePathWithExtension = `${filePath}.${serializer.fileExtension}`;
283
- return {
284
- outputFilePath: path2.join(outputDir, filePathWithExtension),
285
- validationFilePath: path2.join(validationDir, filePathWithExtension)
286
- };
287
- }
288
- readValidationFile() {
289
- const { validationFilePath } = this.filePaths;
290
- if (!fs2.existsSync(validationFilePath)) {
291
- return void 0;
292
- }
293
- return readSnapshotFile(validationFilePath);
294
- }
295
- createMatcherResult(params) {
296
- const { actual, expected } = params;
297
- const { outputFilePath, validationFilePath } = this.filePaths;
298
- const isValidationFileMissing = this.isValidationFileMissing;
299
- return {
300
- actual,
301
- expected,
302
- outputFilePath,
303
- validationFilePath,
304
- message: () => isValidationFileMissing ? `Missing validation file '${validationFilePath}'` : `Output file '${outputFilePath}'
305
- does not match validation file '${validationFilePath}'`,
306
- writeFileSnapshots: () => this.writeFileSnapshots(params)
307
- };
308
- }
309
- writeFileSnapshots(matcherResult) {
310
- const { actual, expected } = matcherResult;
311
- const { outputFilePath, validationFilePath } = this.filePaths;
312
- writeSnapshotFile(outputFilePath, actual);
313
- if (this.isUpdate) {
314
- const validationFileData = this.isValidationFileMissing ? expected : actual;
315
- writeSnapshotFile(validationFilePath, validationFileData);
316
- this.validationFile = validationFileData;
317
- }
318
- }
319
- resolveExpected(actual) {
320
- if (this.validationFile === void 0) {
321
- return addMissingFileMarker(actual);
322
- }
323
- return this.validationFile;
324
- }
325
- };
326
-
327
- // src/matcher/file-path-resolver.ts
328
- import path3 from "path";
329
- function resolveNameAsFile(params) {
330
- return resolveFilePath(
331
- params,
332
- ({ basePath, normalizedName }) => path3.join(basePath, normalizedName)
333
- );
334
- }
335
- function resolveNameAsFileSuffix(params) {
336
- return resolveFilePath(
337
- params,
338
- ({ basePath, normalizedName }) => `${basePath}_${normalizedName}`
339
- );
340
- }
341
- function resolveBasePath(params) {
342
- const { testPath, titlePath } = params;
343
- const normalizedTitlePath = titlePath.map(normalizeFileName);
344
- return path3.join(testPath, ...normalizedTitlePath);
345
- }
346
- function resolveFilePath(params, resolveNamedFilePath) {
347
- const { name, ...baseParams } = params;
348
- const basePath = resolveBasePath(baseParams);
349
- if (name === void 0) {
350
- return basePath;
351
- }
352
- const normalizedName = normalizeFileName(name);
353
- return resolveNamedFilePath({ basePath, normalizedName });
354
- }
355
- export {
356
- JsonSerializer,
357
- TextSerializer,
358
- ValidationFileMatcher,
359
- isArray,
360
- isPlainObject,
361
- isString,
362
- normalizeFileName,
363
- resolveNameAsFile,
364
- resolveNameAsFileSuffix
365
- };