@ucdjs/test-utils 1.0.1-beta.1

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,84 @@
1
+ import * as _ucdjs_fs_bridge0 from "@ucdjs/fs-bridge";
2
+ import { FSEntry, FileSystemBridge } from "@ucdjs/fs-bridge";
3
+ import { z } from "zod";
4
+
5
+ //#region src/fs-bridges/memory-fs-bridge.d.ts
6
+ declare const createMemoryMockFS: _ucdjs_fs_bridge0.FileSystemBridgeFactory<z.ZodOptional<z.ZodObject<{
7
+ basePath: z.ZodOptional<z.ZodString>;
8
+ initialFiles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
9
+ functions: z.ZodOptional<z.ZodObject<{
10
+ read: z.ZodOptional<z.ZodOptional<z.ZodXor<readonly [z.ZodFunction<z.ZodTuple<readonly [z.ZodString], null>, z.ZodPromise<z.ZodString>>, z.ZodLiteral<false>]>>>;
11
+ exists: z.ZodOptional<z.ZodOptional<z.ZodXor<readonly [z.ZodFunction<z.ZodTuple<readonly [z.ZodString], null>, z.ZodPromise<z.ZodBoolean>>, z.ZodLiteral<false>]>>>;
12
+ listdir: z.ZodOptional<z.ZodOptional<z.ZodXor<readonly [z.ZodFunction<z.ZodTuple<readonly [z.ZodString, z.ZodOptional<z.ZodBoolean>], null>, z.ZodPromise<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
13
+ name: z.ZodString;
14
+ path: z.ZodString;
15
+ lastModified: z.ZodUnion<[z.ZodNumber, z.ZodNull]>;
16
+ type: z.ZodLiteral<"directory">;
17
+ }, z.core.$strip>, z.ZodObject<{
18
+ name: z.ZodString;
19
+ path: z.ZodString;
20
+ lastModified: z.ZodUnion<[z.ZodNumber, z.ZodNull]>;
21
+ type: z.ZodLiteral<"file">;
22
+ }, z.core.$strip>]>>>>, z.ZodLiteral<false>]>>>;
23
+ write: z.ZodOptional<z.ZodOptional<z.ZodXor<readonly [z.ZodFunction<z.ZodTuple<readonly [z.ZodString, z.ZodXor<readonly [z.ZodString, z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>]>, z.ZodOptional<z.ZodString>], null>, z.ZodPromise<z.ZodVoid>>, z.ZodLiteral<false>]>>>;
24
+ mkdir: z.ZodOptional<z.ZodOptional<z.ZodXor<readonly [z.ZodFunction<z.ZodTuple<readonly [z.ZodString, z.ZodOptional<z.ZodObject<{
25
+ recursive: z.ZodOptional<z.ZodBoolean>;
26
+ }, z.core.$strip>>], null>, z.ZodPromise<z.ZodVoid>>, z.ZodLiteral<false>]>>>;
27
+ rm: z.ZodOptional<z.ZodOptional<z.ZodXor<readonly [z.ZodFunction<z.ZodTuple<readonly [z.ZodString, z.ZodOptional<z.ZodObject<{
28
+ recursive: z.ZodOptional<z.ZodBoolean>;
29
+ force: z.ZodOptional<z.ZodBoolean>;
30
+ }, z.core.$strip>>], null>, z.ZodPromise<z.ZodVoid>>, z.ZodLiteral<false>]>>>;
31
+ }, z.core.$strip>>;
32
+ }, z.core.$strip>>>;
33
+ //#endregion
34
+ //#region src/fs-bridges/read-only-bridge.d.ts
35
+ interface CreateReadOnlyBridgeOptions {
36
+ /**
37
+ * Mock function for reading files
38
+ * @default vi.fn().mockResolvedValue("content")
39
+ */
40
+ read?: (path: string) => Promise<string>;
41
+ /**
42
+ * Mock function for checking file existence
43
+ * @default vi.fn().mockResolvedValue(true)
44
+ */
45
+ exists?: (path: string) => Promise<boolean>;
46
+ /**
47
+ * Mock function for listing directory contents
48
+ * @default vi.fn().mockResolvedValue([])
49
+ */
50
+ listdir?: (path: string, recursive?: boolean) => Promise<FSEntry[]>;
51
+ }
52
+ /**
53
+ * Creates a read-only filesystem bridge for testing.
54
+ *
55
+ * Useful for testing operations that should skip when write capability is unavailable.
56
+ * All functions are optional and will use sensible defaults if not provided.
57
+ *
58
+ * @param options - Optional mock functions for read, exists, and listdir
59
+ * @returns A read-only FileSystemBridge instance
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * import { createReadOnlyBridge } from "#test-utils/fs-bridges";
64
+ * import { vi } from "vitest";
65
+ *
66
+ * // Use defaults
67
+ * const bridge = createReadOnlyBridge();
68
+ *
69
+ * // Custom read function
70
+ * const bridge = createReadOnlyBridge({
71
+ * read: vi.fn().mockResolvedValue("custom content"),
72
+ * });
73
+ *
74
+ * // All custom functions
75
+ * const bridge = createReadOnlyBridge({
76
+ * read: vi.fn().mockResolvedValue("file content"),
77
+ * exists: vi.fn().mockResolvedValue(false),
78
+ * listdir: vi.fn().mockResolvedValue([...]),
79
+ * });
80
+ * ```
81
+ */
82
+ declare function createReadOnlyBridge(options?: CreateReadOnlyBridgeOptions): FileSystemBridge;
83
+ //#endregion
84
+ export { type CreateReadOnlyBridgeOptions, createMemoryMockFS, createReadOnlyBridge };
@@ -0,0 +1,303 @@
1
+ import { vi } from "vitest";
2
+ import { Buffer } from "node:buffer";
3
+ import { appendTrailingSlash, prependLeadingSlash } from "@luxass/utils/path";
4
+ import { defineFileSystemBridge } from "@ucdjs/fs-bridge";
5
+ import { FileEntrySchema } from "@ucdjs/schemas";
6
+ import { z } from "zod";
7
+
8
+ //#region src/fs-bridges/memory-fs-bridge.ts
9
+ /**
10
+ * Normalizes root path inputs to an empty string.
11
+ * Treats "", ".", "/" and undefined as the empty root.
12
+ */
13
+ function normalizeRootPath(path) {
14
+ return !path || path === "." || path === "/" ? "" : path;
15
+ }
16
+ /**
17
+ * Formats a relative path to match FSEntry schema requirements (parity with node/http bridges):
18
+ * - Leading slash required for all paths
19
+ * - Trailing slash required for directories
20
+ */
21
+ function formatEntryPath(relativePath, isDirectory) {
22
+ const withLeadingSlash = prependLeadingSlash(relativePath);
23
+ return isDirectory ? appendTrailingSlash(withLeadingSlash) : withLeadingSlash;
24
+ }
25
+ /**
26
+ * Marker value for explicit directories in the flat Map storage.
27
+ * Directories are stored as "path/" -> DIR_MARKER
28
+ */
29
+ const DIR_MARKER = Symbol("directory");
30
+ /**
31
+ * Checks if a path represents an explicit directory marker.
32
+ */
33
+ function isDirMarkerKey(path) {
34
+ return path.endsWith("/");
35
+ }
36
+ /**
37
+ * Gets the directory marker key for a given path.
38
+ */
39
+ function getDirMarkerKey(path) {
40
+ const normalized = normalizeRootPath(path);
41
+ if (normalized === "") return "";
42
+ return normalized.endsWith("/") ? normalized : `${normalized}/`;
43
+ }
44
+ const createMemoryMockFS = defineFileSystemBridge({
45
+ meta: {
46
+ name: "In-Memory File System Bridge",
47
+ description: "A simple in-memory file system bridge using a flat Map for storage, perfect for testing."
48
+ },
49
+ optionsSchema: z.object({
50
+ basePath: z.string().optional(),
51
+ initialFiles: z.record(z.string(), z.string()).optional(),
52
+ functions: z.object({
53
+ read: z.xor([z.function({
54
+ input: [z.string()],
55
+ output: z.promise(z.string())
56
+ }), z.literal(false)]).optional(),
57
+ exists: z.xor([z.function({
58
+ input: [z.string()],
59
+ output: z.promise(z.boolean())
60
+ }), z.literal(false)]).optional(),
61
+ listdir: z.xor([z.function({
62
+ input: [z.string(), z.boolean().optional()],
63
+ output: z.promise(z.array(FileEntrySchema))
64
+ }), z.literal(false)]).optional(),
65
+ write: z.xor([z.function({
66
+ input: [
67
+ z.string(),
68
+ z.xor([z.string(), z.instanceof(Uint8Array)]),
69
+ z.string().optional()
70
+ ],
71
+ output: z.promise(z.void())
72
+ }), z.literal(false)]).optional(),
73
+ mkdir: z.xor([z.function({
74
+ input: [z.string(), z.object({ recursive: z.boolean().optional() }).optional()],
75
+ output: z.promise(z.void())
76
+ }), z.literal(false)]).optional(),
77
+ rm: z.xor([z.function({
78
+ input: [z.string(), z.object({
79
+ recursive: z.boolean().optional(),
80
+ force: z.boolean().optional()
81
+ }).optional()],
82
+ output: z.promise(z.void())
83
+ }), z.literal(false)]).optional()
84
+ }).partial().optional()
85
+ }).optional(),
86
+ state: { files: /* @__PURE__ */ new Map() },
87
+ setup({ options, state, resolveSafePath }) {
88
+ const basePath = options?.basePath ?? "/";
89
+ const resolve = (path) => {
90
+ return resolveSafePath(basePath, path);
91
+ };
92
+ if (options?.initialFiles) for (const [path, content] of Object.entries(options.initialFiles)) state.files.set(resolve(path), content);
93
+ const operations = {
94
+ read: async (path) => {
95
+ const resolvedPath = resolve(path);
96
+ const content = state.files.get(resolvedPath);
97
+ if (content === void 0) throw new Error(`ENOENT: no such file or directory, open '${resolvedPath}'`);
98
+ if (content === DIR_MARKER) throw new Error(`EISDIR: illegal operation on a directory, read '${resolvedPath}'`);
99
+ return content;
100
+ },
101
+ exists: async (path) => {
102
+ const resolvedPath = resolve(path);
103
+ if (state.files.has(resolvedPath)) return true;
104
+ const dirMarkerKey = getDirMarkerKey(resolvedPath);
105
+ if (dirMarkerKey && state.files.has(dirMarkerKey)) return true;
106
+ const normalizedPath = normalizeRootPath(resolvedPath);
107
+ const pathWithSlash = normalizedPath === "" ? "" : normalizedPath.endsWith("/") ? normalizedPath : `${normalizedPath}/`;
108
+ for (const filePath of state.files.keys()) if (filePath.startsWith(pathWithSlash)) return true;
109
+ return false;
110
+ },
111
+ listdir: async (path, recursive = false) => {
112
+ const resolvedPath = resolve(path);
113
+ const entries = [];
114
+ const normalizedPath = normalizeRootPath(resolvedPath);
115
+ const pathPrefix = normalizedPath === "" ? "" : normalizedPath.endsWith("/") ? normalizedPath : `${normalizedPath}/`;
116
+ const requestedPath = normalizeRootPath(path).replace(/^\/+/, "").replace(/\/+$/, "");
117
+ const prefixToRoot = (relative) => {
118
+ if (!requestedPath) return relative;
119
+ if (!relative) return requestedPath;
120
+ return `${requestedPath}/${relative}`;
121
+ };
122
+ const seenDirs = /* @__PURE__ */ new Set();
123
+ for (const [filePath, value] of state.files.entries()) {
124
+ if (!filePath.startsWith(pathPrefix)) continue;
125
+ const relativePath = filePath.slice(pathPrefix.length);
126
+ if (isDirMarkerKey(filePath) && value === DIR_MARKER) {
127
+ const parts = relativePath.slice(0, -1).split("/");
128
+ if (!recursive) {
129
+ if (parts.length === 1 && parts[0]) {
130
+ const dirName = parts[0];
131
+ if (!seenDirs.has(dirName)) {
132
+ seenDirs.add(dirName);
133
+ entries.push({
134
+ type: "directory",
135
+ name: dirName,
136
+ path: formatEntryPath(prefixToRoot(dirName), true),
137
+ children: []
138
+ });
139
+ }
140
+ }
141
+ } else {
142
+ let currentLevel = entries;
143
+ for (let i = 0; i < parts.length; i++) {
144
+ const part = parts[i];
145
+ if (!part) continue;
146
+ const partPath = parts.slice(0, i + 1).join("/");
147
+ let dirEntry = currentLevel.find((e) => e.type === "directory" && e.name === part);
148
+ if (!dirEntry) {
149
+ dirEntry = {
150
+ type: "directory",
151
+ name: part,
152
+ path: formatEntryPath(prefixToRoot(partPath), true),
153
+ children: []
154
+ };
155
+ currentLevel.push(dirEntry);
156
+ }
157
+ currentLevel = dirEntry.children;
158
+ }
159
+ }
160
+ continue;
161
+ }
162
+ const parts = relativePath.split("/");
163
+ if (!recursive) {
164
+ if (parts.length === 1 && parts[0]) entries.push({
165
+ type: "file",
166
+ name: parts[0],
167
+ path: formatEntryPath(prefixToRoot(relativePath), false)
168
+ });
169
+ else if (parts.length > 1 && parts[0]) {
170
+ const dirName = parts[0];
171
+ if (!seenDirs.has(dirName)) {
172
+ seenDirs.add(dirName);
173
+ entries.push({
174
+ type: "directory",
175
+ name: dirName,
176
+ path: formatEntryPath(prefixToRoot(dirName), true),
177
+ children: []
178
+ });
179
+ }
180
+ }
181
+ } else {
182
+ let currentLevel = entries;
183
+ for (let i = 0; i < parts.length; i++) {
184
+ const part = parts[i];
185
+ if (!part) continue;
186
+ const isLastPart = i === parts.length - 1;
187
+ const partPath = parts.slice(0, i + 1).join("/");
188
+ if (isLastPart) currentLevel.push({
189
+ type: "file",
190
+ name: part,
191
+ path: formatEntryPath(prefixToRoot(partPath), false)
192
+ });
193
+ else {
194
+ let dirEntry = currentLevel.find((e) => e.type === "directory" && e.name === part);
195
+ if (!dirEntry) {
196
+ dirEntry = {
197
+ type: "directory",
198
+ name: part,
199
+ path: formatEntryPath(prefixToRoot(partPath), true),
200
+ children: []
201
+ };
202
+ currentLevel.push(dirEntry);
203
+ }
204
+ currentLevel = dirEntry.children;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return entries;
210
+ },
211
+ write: async (path, data, encoding = "utf8") => {
212
+ const resolvedPath = resolve(path);
213
+ const content = typeof data === "string" ? data : Buffer.from(data).toString(encoding);
214
+ state.files.set(resolvedPath, content);
215
+ },
216
+ mkdir: async (path) => {
217
+ const normalizedPath = normalizeRootPath(resolve(path));
218
+ if (normalizedPath === "") return;
219
+ const dirMarkerKey = getDirMarkerKey(normalizedPath);
220
+ state.files.set(dirMarkerKey, DIR_MARKER);
221
+ const parts = normalizedPath.split("/");
222
+ for (let i = 1; i < parts.length; i++) {
223
+ const parentMarkerKey = getDirMarkerKey(parts.slice(0, i).join("/"));
224
+ if (parentMarkerKey && !state.files.has(parentMarkerKey)) state.files.set(parentMarkerKey, DIR_MARKER);
225
+ }
226
+ },
227
+ rm: async (path, options) => {
228
+ const resolvedPath = resolve(path);
229
+ if (state.files.has(resolvedPath)) {
230
+ state.files.delete(resolvedPath);
231
+ return;
232
+ }
233
+ const dirMarkerKey = getDirMarkerKey(resolvedPath);
234
+ if (dirMarkerKey && state.files.has(dirMarkerKey)) state.files.delete(dirMarkerKey);
235
+ if (options?.recursive) {
236
+ const normalizedPath = normalizeRootPath(resolvedPath);
237
+ const pathPrefix = normalizedPath === "" ? "" : normalizedPath.endsWith("/") ? normalizedPath : `${normalizedPath}/`;
238
+ const keysToDelete = [];
239
+ for (const filePath of state.files.keys()) if (filePath.startsWith(pathPrefix)) keysToDelete.push(filePath);
240
+ for (const key of keysToDelete) state.files.delete(key);
241
+ }
242
+ }
243
+ };
244
+ if (options?.functions) {
245
+ const fns = options.functions;
246
+ for (const key of Object.keys(fns)) {
247
+ const val = fns[key];
248
+ if (val === false) delete operations[key];
249
+ else operations[key] = val;
250
+ }
251
+ }
252
+ return operations;
253
+ }
254
+ });
255
+
256
+ //#endregion
257
+ //#region src/fs-bridges/read-only-bridge.ts
258
+ /**
259
+ * Creates a read-only filesystem bridge for testing.
260
+ *
261
+ * Useful for testing operations that should skip when write capability is unavailable.
262
+ * All functions are optional and will use sensible defaults if not provided.
263
+ *
264
+ * @param options - Optional mock functions for read, exists, and listdir
265
+ * @returns A read-only FileSystemBridge instance
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * import { createReadOnlyBridge } from "#test-utils/fs-bridges";
270
+ * import { vi } from "vitest";
271
+ *
272
+ * // Use defaults
273
+ * const bridge = createReadOnlyBridge();
274
+ *
275
+ * // Custom read function
276
+ * const bridge = createReadOnlyBridge({
277
+ * read: vi.fn().mockResolvedValue("custom content"),
278
+ * });
279
+ *
280
+ * // All custom functions
281
+ * const bridge = createReadOnlyBridge({
282
+ * read: vi.fn().mockResolvedValue("file content"),
283
+ * exists: vi.fn().mockResolvedValue(false),
284
+ * listdir: vi.fn().mockResolvedValue([...]),
285
+ * });
286
+ * ```
287
+ */
288
+ function createReadOnlyBridge(options = {}) {
289
+ return defineFileSystemBridge({
290
+ meta: {
291
+ name: "Read-Only Test Bridge",
292
+ description: "A read-only bridge for testing"
293
+ },
294
+ setup: () => ({
295
+ read: options.read ?? vi.fn().mockResolvedValue("content"),
296
+ exists: options.exists ?? vi.fn().mockResolvedValue(true),
297
+ listdir: options.listdir ?? vi.fn().mockResolvedValue([])
298
+ })
299
+ })();
300
+ }
301
+
302
+ //#endregion
303
+ export { createMemoryMockFS, createReadOnlyBridge };