fs-fixture 2.11.0 → 2.13.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/README.md CHANGED
@@ -17,6 +17,7 @@ Simple API to create disposable test fixtures on disk. Tiny (`1.1 kB` gzipped) w
17
17
  - 💾 Binary file support with Buffers
18
18
  - 🎯 TypeScript-first with full type safety
19
19
  - 🔄 File methods inherit types directly from Node.js `fs` module
20
+ - 🔌 Pluggable filesystem — use with @platformatic/vfs, memfs, or any `fs/promises`-compatible API
20
21
 
21
22
  ## Installation
22
23
 
@@ -149,6 +150,19 @@ const fixture = await createFixture({
149
150
  })
150
151
  ```
151
152
 
153
+ **Symlinks:**
154
+ ```ts
155
+ const fixture = await createFixture({
156
+ 'index.js': 'import pkg from \'pkg\'',
157
+
158
+ // Symlink individual file or directory
159
+ 'node_modules/pkg': ({ symlink }) => symlink(process.cwd()),
160
+
161
+ // Symlink entire directory (useful for sharing node_modules)
162
+ node_modules: ({ symlink }) => symlink(path.resolve('node_modules'))
163
+ })
164
+ ```
165
+
152
166
  **Binary files:**
153
167
  ```ts
154
168
  const fixture = await createFixture({
@@ -172,6 +186,31 @@ const fixture = await createFixture({
172
186
  })
173
187
  ```
174
188
 
189
+ ### Custom filesystem
190
+
191
+ Pass any `fs/promises`-compatible API via the `fs` option to use a virtual filesystem instead of disk:
192
+
193
+ ```ts
194
+ import { create, MemoryProvider } from '@platformatic/vfs'
195
+ import { createFixture } from 'fs-fixture'
196
+
197
+ const fs = create(new MemoryProvider()).promises
198
+ const fixture = await createFixture({
199
+ 'package.json': JSON.stringify({ name: 'test' }),
200
+ 'src/index.js': 'export default 42'
201
+ }, { fs })
202
+
203
+ await fixture.readFile('src/index.js', 'utf8') // 'export default 42'
204
+ ```
205
+
206
+ Works with any library that implements the `fs/promises` API shape, including [@platformatic/vfs](https://github.com/platformatic/vfs), the future [`node:vfs`](https://github.com/nodejs/node/pull/61478), and [memfs](https://github.com/streamich/memfs).
207
+
208
+ > [!NOTE]
209
+ > With a custom fs, files only exist in that fs instance. Use `fixture.readFile()` or `fixture.fs` to access them — `fixture.path` is a virtual path that doesn't exist on the real disk.
210
+
211
+ > [!NOTE]
212
+ > Template directory sources (string paths) are not supported with custom filesystems because most virtual fs implementations lack recursive `cp`. Use a `FileTree` object instead.
213
+
175
214
  ## API
176
215
 
177
216
  ### `createFixture(source?, options?)`
@@ -182,6 +221,7 @@ Creates a temporary fixture directory and returns a `FsFixture` instance.
182
221
  - `source` (optional): String path to template directory, or `FileTree` object defining the structure
183
222
  - `options.tempDir` (optional): Custom temp directory. Defaults to `os.tmpdir()`
184
223
  - `options.templateFilter` (optional): Filter function when copying from template directory
224
+ - `options.fs` (optional): Custom `fs/promises`-compatible API for virtual filesystem support
185
225
 
186
226
  **Returns:** `Promise<FsFixture>`
187
227
 
@@ -197,6 +237,7 @@ const fixture = await createFixture({}, { tempDir: './custom-temp' })
197
237
  | Method | Description |
198
238
  |--------|-------------|
199
239
  | `fixture.path` | Absolute path to the fixture directory |
240
+ | `fixture.fs` | The underlying `fs/promises` API used by the fixture |
200
241
  | `getPath(...paths)` | Get absolute path to file/directory in fixture |
201
242
  | `exists(path?)` | Check if file/directory exists |
202
243
  | `rm(path?)` | Delete file/directory (or entire fixture if no path) |
@@ -228,6 +269,34 @@ type Api = {
228
269
  ```
229
270
  </details>
230
271
 
272
+ <details>
273
+ <summary><strong>FsPromises</strong></summary>
274
+
275
+ The subset of `fs/promises` methods that custom filesystem implementations must provide:
276
+
277
+ ```ts
278
+ type FsPromises = {
279
+ // Required
280
+ readFile(path: string, options?): Promise<Buffer | string>
281
+ writeFile(path: string, data: string | Buffer, options?): Promise<void>
282
+ readdir(path: string, options?): Promise<string[] | Dirent[]>
283
+ mkdir(path: string, options?): Promise<string | undefined>
284
+ rename(oldPath: string, newPath: string): Promise<void>
285
+ access(path: string, mode?: number): Promise<void>
286
+
287
+ // Optional
288
+ rm?(path: string, options?): Promise<void>
289
+ unlink?(path: string): Promise<void>
290
+ rmdir?(path: string): Promise<void>
291
+ symlink?(target: string, path: string, type?: string): Promise<void>
292
+ cp?(source: string, destination: string, options?): Promise<void>
293
+ mkdtemp?(prefix: string): Promise<string>
294
+ }
295
+ ```
296
+
297
+ If `rm` is not available, fs-fixture falls back to recursive removal using `readdir({ withFileTypes })` + `unlink` + `rmdir`. If `mkdtemp` is not available, fixture paths are generated with a counter.
298
+ </details>
299
+
231
300
  ## Related
232
301
 
233
302
  ### [manten](https://github.com/privatenumber/manten)
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var g=Object.defineProperty;var c=(s,e)=>g(s,"name",{value:e,configurable:!0});var n=require("node:fs/promises"),f=require("node:path"),P=require("node:url"),v=require("node:fs"),F=require("node:os");typeof Symbol.asyncDispose!="symbol"&&Object.defineProperty(Symbol,"asyncDispose",{configurable:!1,enumerable:!1,writable:!1,value:Symbol.for("asyncDispose")});class b{static{c(this,"FsFixture")}path;constructor(e){this.path=e}getPath(...e){return f.join(this.path,...e)}exists(e=""){return n.access(this.getPath(e)).then(()=>!0,()=>!1)}rm(e=""){return n.rm(this.getPath(e),{recursive:!0,force:!0})}cp(e,t,i){return t?t.endsWith(f.sep)&&(t+=f.basename(e)):t=f.basename(e),n.cp(e,this.getPath(t),i)}mkdir(e){return n.mkdir(this.getPath(e),{recursive:!0})}mv(e,t){return n.rename(this.getPath(e),this.getPath(t))}readFile=c(((e,t)=>n.readFile(this.getPath(e),t)),"readFile");readdir=c(((e,t)=>n.readdir(this.getPath(e||""),t)),"readdir");writeFile=c(((e,t,...i)=>n.writeFile(this.getPath(e),t,...i)),"writeFile");async readJson(e){const t=await this.readFile(e,"utf8");return JSON.parse(t)}writeJson(e,t,i=2){return this.writeFile(e,JSON.stringify(t,null,i))}async[Symbol.asyncDispose](){await this.rm()}}const D=v.realpathSync(F.tmpdir());class p{static{c(this,"PathBase")}constructor(e){this.path=e}}class y extends p{static{c(this,"Directory")}}class m extends p{static{c(this,"File")}constructor(e,t){super(e),this.content=t}}class u extends p{static{c(this,"Symlink")}constructor(e,t,i){super(i??""),this.target=e,this.type=t}}const d=c((s,e,t)=>{const i=[];for(const h in s){if(!Object.hasOwn(s,h))continue;const o=f.join(e,h);let a=s[h];if(typeof a=="function"){const r=Object.assign(Object.create(t),{filePath:o}),l=a(r);if(l instanceof u){const w=new u(l.target,l.type,o);i.push(w);continue}else a=l}if(typeof a=="string"||Buffer.isBuffer(a))i.push(new m(o,a));else if(a&&typeof a=="object"&&!Array.isArray(a))i.push(new y(o),...d(a,o,t));else throw new TypeError(`Invalid file content for path "${o}". Functions must return a string, Buffer, Symlink, or a nested FileTree object. Received: ${String(a)}`)}return i},"flattenFileTree"),j=c(async(s,e)=>{const t=e?.tempDir?f.resolve(typeof e.tempDir=="string"?e.tempDir:P.fileURLToPath(e.tempDir)):D;e?.tempDir&&await n.mkdir(t,{recursive:!0});const i=await n.mkdtemp(f.join(t,"fs-fixture-"));if(s){if(typeof s=="string")await n.cp(s,i,{recursive:!0,filter:e?.templateFilter});else if(typeof s=="object"){const o=d(s,i,{fixturePath:i,getPath:c((...r)=>f.join(i,...r),"getPath"),symlink:c((r,l)=>new u(r,l),"symlink")}),a=new Set;for(const r of o)r instanceof y?a.add(r.path):(r instanceof m||r instanceof u)&&a.add(f.dirname(r.path));await Promise.all(Array.from(a).map(r=>n.mkdir(r,{recursive:!0}))),await Promise.all(o.map(async r=>{r instanceof u?await n.symlink(r.target,r.path,r.type):r instanceof m&&await n.writeFile(r.path,r.content)}))}}return new b(i)},"createFixture");exports.createFixture=j;
1
+ "use strict";var v=Object.defineProperty;var o=(r,e)=>v(r,"name",{value:e,configurable:!0});var y=require("node:fs/promises"),c=require("node:path"),F=require("node:url"),b=require("node:fs"),j=require("node:os");const w=o(async(r,e)=>{try{await r.unlink(e);return}catch(i){if(D(i))return}const t=await r.readdir(e,{withFileTypes:!0});await Promise.all(t.map(i=>{const n=c.join(e,i.name);return i.isDirectory()?w(r,n):r.unlink(n)})),await r.rmdir(e)},"recursiveRm"),D=o(r=>r instanceof Error&&"code"in r&&r.code==="ENOENT","isEnoent");typeof Symbol.asyncDispose!="symbol"&&Object.defineProperty(Symbol,"asyncDispose",{configurable:!1,enumerable:!1,writable:!1,value:Symbol.for("asyncDispose")});class x{static{o(this,"FsFixture")}path;fs;constructor(e,t){this.path=e,this.fs=t??y}getPath(...e){return c.join(this.path,...e)}exists(e=""){return this.fs.access(this.getPath(e)).then(()=>!0,()=>!1)}rm(e=""){const t=this.getPath(e);if(this.fs.rm)return this.fs.rm(t,{recursive:!0,force:!0});if(!this.fs.unlink||!this.fs.rmdir)throw new Error("rm() requires the fs API to support rm(), or unlink() + rmdir()");return w(this.fs,t)}cp(e,t,i){if(!this.fs.cp)throw new Error("cp() requires the fs API to support cp()");return t?(t.endsWith("/")||t.endsWith(c.sep))&&(t+=c.basename(e)):t=c.basename(e),this.fs.cp(e,this.getPath(t),i)}mkdir(e){return this.fs.mkdir(this.getPath(e),{recursive:!0})}mv(e,t){return this.fs.rename(this.getPath(e),this.getPath(t))}readFile=o(((e,t)=>this.fs.readFile(this.getPath(e),t)),"readFile");readdir=o(((e,t)=>this.fs.readdir(this.getPath(e||""),t)),"readdir");writeFile=o(((e,t,...i)=>this.fs.writeFile(this.getPath(e),t,...i)),"writeFile");async readJson(e){const t=await this.readFile(e,"utf8");return JSON.parse(t)}writeJson(e,t,i=2){return this.writeFile(e,JSON.stringify(t,null,i))}async[Symbol.asyncDispose](){await this.rm()}}const T=b.realpathSync(j.tmpdir());class m{static{o(this,"PathBase")}path;constructor(e){this.path=e}}class d extends m{static{o(this,"Directory")}}class p extends m{static{o(this,"File")}content;constructor(e,t){super(e),this.content=t}}class h extends m{static{o(this,"Symlink")}target;type;constructor(e,t,i){super(i??""),this.target=e,this.type=t}}const g=o((r,e,t)=>{const i=[];for(const n in r){if(!Object.hasOwn(r,n))continue;const f=c.join(e,n);let a=r[n];if(typeof a=="function"){const l=Object.assign(Object.create(t),{filePath:f}),u=a(l);if(u instanceof h){const s=new h(u.target,u.type,f);i.push(s);continue}else a=u}if(typeof a=="string"||Buffer.isBuffer(a))i.push(new p(f,a));else if(a&&typeof a=="object"&&!Array.isArray(a))i.push(new d(f),...g(a,f,t));else throw new TypeError(`Invalid file content for path "${f}". Functions must return a string, Buffer, Symlink, or a nested FileTree object. Received: ${String(a)}`)}return i},"flattenFileTree");let k=0;const q=o(async(r,e)=>{const t=e?.fs??y,i=e?.tempDir?c.resolve(typeof e.tempDir=="string"?e.tempDir:F.fileURLToPath(e.tempDir)):T;e?.tempDir&&await t.mkdir(i,{recursive:!0});let n;if(t.mkdtemp?n=await t.mkdtemp(c.join(i,"fs-fixture-")):(k+=1,n=c.join(i,`fs-fixture-${process.pid}-${k}`),await t.mkdir(n,{recursive:!0})),r){if(typeof r=="string"){if(!t.cp)throw new TypeError("Template directory sources require the fs API to support cp()");await t.cp(r,n,{recursive:!0,filter:e?.templateFilter})}else if(typeof r=="object"){const a=g(r,n,{fixturePath:n,getPath:o((...s)=>c.join(n,...s),"getPath"),symlink:o((s,P)=>new h(s,P),"symlink")}),l=new Set;for(const s of a)s instanceof d?l.add(s.path):(s instanceof p||s instanceof h)&&l.add(c.dirname(s.path));if(await Promise.all(Array.from(l).map(s=>t.mkdir(s,{recursive:!0}))),a.some(s=>s instanceof h)&&!t.symlink)throw new TypeError("Symlinks require the fs API to support symlink()");await Promise.all(a.map(async s=>{s instanceof h?await t.symlink(s.target,s.path,s.type):s instanceof p&&await t.writeFile(s.path,s.content)}))}}return new x(n,e?.fs)},"createFixture");exports.createFixture=q;
package/dist/index.d.cts CHANGED
@@ -1,17 +1,69 @@
1
1
  import { CopyOptions } from 'node:fs';
2
2
  import fs from 'node:fs/promises';
3
3
 
4
+ /**
5
+ * A subset of `fs/promises` methods used by FsFixture.
6
+ * Compatible with Node.js `fs/promises`, `@platformatic/vfs`,
7
+ * `memfs`, and other fs-compatible implementations.
8
+ *
9
+ * Pass a custom implementation to `createFixture({ fs })`
10
+ * to use a virtual filesystem.
11
+ */
12
+ type FsPromises = {
13
+ readFile: {
14
+ (path: string, options?: {
15
+ encoding?: null;
16
+ } | null): Promise<Buffer>;
17
+ (path: string, options: BufferEncoding | {
18
+ encoding: BufferEncoding;
19
+ }): Promise<string>;
20
+ };
21
+ writeFile(path: string, data: string | Buffer, options?: BufferEncoding | {
22
+ encoding?: BufferEncoding;
23
+ } | null): Promise<void>;
24
+ readdir: {
25
+ (path: string, options?: {
26
+ withFileTypes?: false;
27
+ }): Promise<string[]>;
28
+ (path: string, options: {
29
+ withFileTypes: true;
30
+ }): Promise<Array<{
31
+ name: string;
32
+ isFile(): boolean;
33
+ isDirectory(): boolean;
34
+ }>>;
35
+ };
36
+ mkdir(path: string, options?: {
37
+ recursive?: boolean;
38
+ }): Promise<string | undefined>;
39
+ rename(oldPath: string, newPath: string): Promise<void>;
40
+ access(path: string, mode?: number): Promise<void>;
41
+ rm?(path: string, options?: {
42
+ recursive?: boolean;
43
+ force?: boolean;
44
+ }): Promise<void>;
45
+ unlink?(path: string): Promise<void>;
46
+ rmdir?(path: string): Promise<void>;
47
+ symlink?(target: string, path: string, type?: string | null): Promise<void>;
48
+ cp?(source: string, destination: string, options?: {
49
+ recursive?: boolean;
50
+ }): Promise<void>;
51
+ mkdtemp?(prefix: string): Promise<string>;
52
+ };
53
+
4
54
  declare class FsFixture {
5
55
  /**
6
56
  * Path to the fixture directory.
7
57
  */
8
58
  readonly path: string;
59
+ readonly fs: FsPromises;
9
60
  /**
10
61
  * Create a Fixture instance from a path. Does not create the fixture directory.
11
62
  *
12
63
  * @param fixturePath - The path to the fixture directory
64
+ * @param fsApi - Optional fs/promises-compatible API. Defaults to real node:fs/promises.
13
65
  */
14
- constructor(fixturePath: string);
66
+ constructor(fixturePath: string, fsApi?: FsPromises);
15
67
  /**
16
68
  * Get the full path to a subpath in the fixture directory.
17
69
  *
@@ -160,8 +212,8 @@ declare class PathBase {
160
212
  type SymlinkType = 'file' | 'dir' | 'junction';
161
213
  declare class Symlink extends PathBase {
162
214
  readonly target: string;
163
- readonly type?: SymlinkType | undefined;
164
- constructor(target: string, type?: SymlinkType | undefined, filePath?: string);
215
+ readonly type?: SymlinkType;
216
+ constructor(target: string, type?: SymlinkType, filePath?: string);
165
217
  }
166
218
 
167
219
  type ApiBase = {
@@ -196,6 +248,22 @@ type CreateFixtureOptions = {
196
248
  * Return `true` to copy the item, `false` to ignore it.
197
249
  */
198
250
  templateFilter?: FilterFunction;
251
+ /**
252
+ * Custom fs/promises-compatible API for fixture operations.
253
+ * Use this to create fixtures in a virtual filesystem instead of on disk.
254
+ *
255
+ * Required: readFile, writeFile, readdir (with withFileTypes),
256
+ * mkdir, rename, access.
257
+ * Optional: rm (or unlink + rmdir as fallback), symlink, cp, mkdtemp.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * import { create, MemoryProvider } from '@platformatic/vfs'
262
+ * const vfs = create(new MemoryProvider())
263
+ * const fixture = await createFixture({ 'file.txt': 'hi' }, { fs: vfs.promises })
264
+ * ```
265
+ */
266
+ fs?: FsPromises;
199
267
  };
200
268
  /**
201
269
  * Create a temporary test fixture directory.
@@ -229,4 +297,4 @@ type CreateFixtureOptions = {
229
297
  declare const createFixture: (source?: string | FileTree, options?: CreateFixtureOptions) => Promise<FsFixture>;
230
298
 
231
299
  export { createFixture };
232
- export type { CreateFixtureOptions, FileTree, FsFixtureType as FsFixture };
300
+ export type { CreateFixtureOptions, FileTree, FsFixtureType as FsFixture, FsPromises };
package/dist/index.d.mts CHANGED
@@ -1,17 +1,69 @@
1
1
  import { CopyOptions } from 'node:fs';
2
2
  import fs from 'node:fs/promises';
3
3
 
4
+ /**
5
+ * A subset of `fs/promises` methods used by FsFixture.
6
+ * Compatible with Node.js `fs/promises`, `@platformatic/vfs`,
7
+ * `memfs`, and other fs-compatible implementations.
8
+ *
9
+ * Pass a custom implementation to `createFixture({ fs })`
10
+ * to use a virtual filesystem.
11
+ */
12
+ type FsPromises = {
13
+ readFile: {
14
+ (path: string, options?: {
15
+ encoding?: null;
16
+ } | null): Promise<Buffer>;
17
+ (path: string, options: BufferEncoding | {
18
+ encoding: BufferEncoding;
19
+ }): Promise<string>;
20
+ };
21
+ writeFile(path: string, data: string | Buffer, options?: BufferEncoding | {
22
+ encoding?: BufferEncoding;
23
+ } | null): Promise<void>;
24
+ readdir: {
25
+ (path: string, options?: {
26
+ withFileTypes?: false;
27
+ }): Promise<string[]>;
28
+ (path: string, options: {
29
+ withFileTypes: true;
30
+ }): Promise<Array<{
31
+ name: string;
32
+ isFile(): boolean;
33
+ isDirectory(): boolean;
34
+ }>>;
35
+ };
36
+ mkdir(path: string, options?: {
37
+ recursive?: boolean;
38
+ }): Promise<string | undefined>;
39
+ rename(oldPath: string, newPath: string): Promise<void>;
40
+ access(path: string, mode?: number): Promise<void>;
41
+ rm?(path: string, options?: {
42
+ recursive?: boolean;
43
+ force?: boolean;
44
+ }): Promise<void>;
45
+ unlink?(path: string): Promise<void>;
46
+ rmdir?(path: string): Promise<void>;
47
+ symlink?(target: string, path: string, type?: string | null): Promise<void>;
48
+ cp?(source: string, destination: string, options?: {
49
+ recursive?: boolean;
50
+ }): Promise<void>;
51
+ mkdtemp?(prefix: string): Promise<string>;
52
+ };
53
+
4
54
  declare class FsFixture {
5
55
  /**
6
56
  * Path to the fixture directory.
7
57
  */
8
58
  readonly path: string;
59
+ readonly fs: FsPromises;
9
60
  /**
10
61
  * Create a Fixture instance from a path. Does not create the fixture directory.
11
62
  *
12
63
  * @param fixturePath - The path to the fixture directory
64
+ * @param fsApi - Optional fs/promises-compatible API. Defaults to real node:fs/promises.
13
65
  */
14
- constructor(fixturePath: string);
66
+ constructor(fixturePath: string, fsApi?: FsPromises);
15
67
  /**
16
68
  * Get the full path to a subpath in the fixture directory.
17
69
  *
@@ -160,8 +212,8 @@ declare class PathBase {
160
212
  type SymlinkType = 'file' | 'dir' | 'junction';
161
213
  declare class Symlink extends PathBase {
162
214
  readonly target: string;
163
- readonly type?: SymlinkType | undefined;
164
- constructor(target: string, type?: SymlinkType | undefined, filePath?: string);
215
+ readonly type?: SymlinkType;
216
+ constructor(target: string, type?: SymlinkType, filePath?: string);
165
217
  }
166
218
 
167
219
  type ApiBase = {
@@ -196,6 +248,22 @@ type CreateFixtureOptions = {
196
248
  * Return `true` to copy the item, `false` to ignore it.
197
249
  */
198
250
  templateFilter?: FilterFunction;
251
+ /**
252
+ * Custom fs/promises-compatible API for fixture operations.
253
+ * Use this to create fixtures in a virtual filesystem instead of on disk.
254
+ *
255
+ * Required: readFile, writeFile, readdir (with withFileTypes),
256
+ * mkdir, rename, access.
257
+ * Optional: rm (or unlink + rmdir as fallback), symlink, cp, mkdtemp.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * import { create, MemoryProvider } from '@platformatic/vfs'
262
+ * const vfs = create(new MemoryProvider())
263
+ * const fixture = await createFixture({ 'file.txt': 'hi' }, { fs: vfs.promises })
264
+ * ```
265
+ */
266
+ fs?: FsPromises;
199
267
  };
200
268
  /**
201
269
  * Create a temporary test fixture directory.
@@ -229,4 +297,4 @@ type CreateFixtureOptions = {
229
297
  declare const createFixture: (source?: string | FileTree, options?: CreateFixtureOptions) => Promise<FsFixture>;
230
298
 
231
299
  export { createFixture };
232
- export type { CreateFixtureOptions, FileTree, FsFixtureType as FsFixture };
300
+ export type { CreateFixtureOptions, FileTree, FsFixtureType as FsFixture, FsPromises };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- var g=Object.defineProperty;var o=(s,e)=>g(s,"name",{value:e,configurable:!0});import n from"node:fs/promises";import f from"node:path";import{fileURLToPath as P}from"node:url";import F from"node:fs";import b from"node:os";typeof Symbol.asyncDispose!="symbol"&&Object.defineProperty(Symbol,"asyncDispose",{configurable:!1,enumerable:!1,writable:!1,value:Symbol.for("asyncDispose")});class D{static{o(this,"FsFixture")}path;constructor(e){this.path=e}getPath(...e){return f.join(this.path,...e)}exists(e=""){return n.access(this.getPath(e)).then(()=>!0,()=>!1)}rm(e=""){return n.rm(this.getPath(e),{recursive:!0,force:!0})}cp(e,t,i){return t?t.endsWith(f.sep)&&(t+=f.basename(e)):t=f.basename(e),n.cp(e,this.getPath(t),i)}mkdir(e){return n.mkdir(this.getPath(e),{recursive:!0})}mv(e,t){return n.rename(this.getPath(e),this.getPath(t))}readFile=o(((e,t)=>n.readFile(this.getPath(e),t)),"readFile");readdir=o(((e,t)=>n.readdir(this.getPath(e||""),t)),"readdir");writeFile=o(((e,t,...i)=>n.writeFile(this.getPath(e),t,...i)),"writeFile");async readJson(e){const t=await this.readFile(e,"utf8");return JSON.parse(t)}writeJson(e,t,i=2){return this.writeFile(e,JSON.stringify(t,null,i))}async[Symbol.asyncDispose](){await this.rm()}}const j=F.realpathSync(b.tmpdir());class h{static{o(this,"PathBase")}constructor(e){this.path=e}}class y extends h{static{o(this,"Directory")}}class u extends h{static{o(this,"File")}constructor(e,t){super(e),this.content=t}}class m extends h{static{o(this,"Symlink")}constructor(e,t,i){super(i??""),this.target=e,this.type=t}}const d=o((s,e,t)=>{const i=[];for(const p in s){if(!Object.hasOwn(s,p))continue;const c=f.join(e,p);let a=s[p];if(typeof a=="function"){const r=Object.assign(Object.create(t),{filePath:c}),l=a(r);if(l instanceof m){const w=new m(l.target,l.type,c);i.push(w);continue}else a=l}if(typeof a=="string"||Buffer.isBuffer(a))i.push(new u(c,a));else if(a&&typeof a=="object"&&!Array.isArray(a))i.push(new y(c),...d(a,c,t));else throw new TypeError(`Invalid file content for path "${c}". Functions must return a string, Buffer, Symlink, or a nested FileTree object. Received: ${String(a)}`)}return i},"flattenFileTree"),v=o(async(s,e)=>{const t=e?.tempDir?f.resolve(typeof e.tempDir=="string"?e.tempDir:P(e.tempDir)):j;e?.tempDir&&await n.mkdir(t,{recursive:!0});const i=await n.mkdtemp(f.join(t,"fs-fixture-"));if(s){if(typeof s=="string")await n.cp(s,i,{recursive:!0,filter:e?.templateFilter});else if(typeof s=="object"){const c=d(s,i,{fixturePath:i,getPath:o((...r)=>f.join(i,...r),"getPath"),symlink:o((r,l)=>new m(r,l),"symlink")}),a=new Set;for(const r of c)r instanceof y?a.add(r.path):(r instanceof u||r instanceof m)&&a.add(f.dirname(r.path));await Promise.all(Array.from(a).map(r=>n.mkdir(r,{recursive:!0}))),await Promise.all(c.map(async r=>{r instanceof m?await n.symlink(r.target,r.path,r.type):r instanceof u&&await n.writeFile(r.path,r.content)}))}}return new D(i)},"createFixture");export{v as createFixture};
1
+ var F=Object.defineProperty;var o=(r,t)=>F(r,"name",{value:t,configurable:!0});import y from"node:fs/promises";import c from"node:path";import{fileURLToPath as b}from"node:url";import j from"node:fs";import v from"node:os";const w=o(async(r,t)=>{try{await r.unlink(t);return}catch(i){if(D(i))return}const e=await r.readdir(t,{withFileTypes:!0});await Promise.all(e.map(i=>{const n=c.join(t,i.name);return i.isDirectory()?w(r,n):r.unlink(n)})),await r.rmdir(t)},"recursiveRm"),D=o(r=>r instanceof Error&&"code"in r&&r.code==="ENOENT","isEnoent");typeof Symbol.asyncDispose!="symbol"&&Object.defineProperty(Symbol,"asyncDispose",{configurable:!1,enumerable:!1,writable:!1,value:Symbol.for("asyncDispose")});class x{static{o(this,"FsFixture")}path;fs;constructor(t,e){this.path=t,this.fs=e??y}getPath(...t){return c.join(this.path,...t)}exists(t=""){return this.fs.access(this.getPath(t)).then(()=>!0,()=>!1)}rm(t=""){const e=this.getPath(t);if(this.fs.rm)return this.fs.rm(e,{recursive:!0,force:!0});if(!this.fs.unlink||!this.fs.rmdir)throw new Error("rm() requires the fs API to support rm(), or unlink() + rmdir()");return w(this.fs,e)}cp(t,e,i){if(!this.fs.cp)throw new Error("cp() requires the fs API to support cp()");return e?(e.endsWith("/")||e.endsWith(c.sep))&&(e+=c.basename(t)):e=c.basename(t),this.fs.cp(t,this.getPath(e),i)}mkdir(t){return this.fs.mkdir(this.getPath(t),{recursive:!0})}mv(t,e){return this.fs.rename(this.getPath(t),this.getPath(e))}readFile=o(((t,e)=>this.fs.readFile(this.getPath(t),e)),"readFile");readdir=o(((t,e)=>this.fs.readdir(this.getPath(t||""),e)),"readdir");writeFile=o(((t,e,...i)=>this.fs.writeFile(this.getPath(t),e,...i)),"writeFile");async readJson(t){const e=await this.readFile(t,"utf8");return JSON.parse(e)}writeJson(t,e,i=2){return this.writeFile(t,JSON.stringify(e,null,i))}async[Symbol.asyncDispose](){await this.rm()}}const T=j.realpathSync(v.tmpdir());class p{static{o(this,"PathBase")}path;constructor(t){this.path=t}}class d extends p{static{o(this,"Directory")}}class u extends p{static{o(this,"File")}content;constructor(t,e){super(t),this.content=e}}class h extends p{static{o(this,"Symlink")}target;type;constructor(t,e,i){super(i??""),this.target=t,this.type=e}}const g=o((r,t,e)=>{const i=[];for(const n in r){if(!Object.hasOwn(r,n))continue;const f=c.join(t,n);let a=r[n];if(typeof a=="function"){const l=Object.assign(Object.create(e),{filePath:f}),m=a(l);if(m instanceof h){const s=new h(m.target,m.type,f);i.push(s);continue}else a=m}if(typeof a=="string"||Buffer.isBuffer(a))i.push(new u(f,a));else if(a&&typeof a=="object"&&!Array.isArray(a))i.push(new d(f),...g(a,f,e));else throw new TypeError(`Invalid file content for path "${f}". Functions must return a string, Buffer, Symlink, or a nested FileTree object. Received: ${String(a)}`)}return i},"flattenFileTree");let k=0;const E=o(async(r,t)=>{const e=t?.fs??y,i=t?.tempDir?c.resolve(typeof t.tempDir=="string"?t.tempDir:b(t.tempDir)):T;t?.tempDir&&await e.mkdir(i,{recursive:!0});let n;if(e.mkdtemp?n=await e.mkdtemp(c.join(i,"fs-fixture-")):(k+=1,n=c.join(i,`fs-fixture-${process.pid}-${k}`),await e.mkdir(n,{recursive:!0})),r){if(typeof r=="string"){if(!e.cp)throw new TypeError("Template directory sources require the fs API to support cp()");await e.cp(r,n,{recursive:!0,filter:t?.templateFilter})}else if(typeof r=="object"){const a=g(r,n,{fixturePath:n,getPath:o((...s)=>c.join(n,...s),"getPath"),symlink:o((s,P)=>new h(s,P),"symlink")}),l=new Set;for(const s of a)s instanceof d?l.add(s.path):(s instanceof u||s instanceof h)&&l.add(c.dirname(s.path));if(await Promise.all(Array.from(l).map(s=>e.mkdir(s,{recursive:!0}))),a.some(s=>s instanceof h)&&!e.symlink)throw new TypeError("Symlinks require the fs API to support symlink()");await Promise.all(a.map(async s=>{s instanceof h?await e.symlink(s.target,s.path,s.type):s instanceof u&&await e.writeFile(s.path,s.content)}))}}return new x(n,t?.fs)},"createFixture");export{E as createFixture};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fs-fixture",
3
- "version": "2.11.0",
3
+ "version": "2.13.0",
4
4
  "description": "Easily create test fixtures at a temporary file-system path",
5
5
  "keywords": [
6
6
  "test",
@@ -20,8 +20,10 @@
20
20
  "email": "hiroki.osame@gmail.com"
21
21
  },
22
22
  "files": [
23
- "dist"
23
+ "dist",
24
+ "skills"
24
25
  ],
26
+ "type": "module",
25
27
  "main": "./dist/index.cjs",
26
28
  "module": "./dist/index.mjs",
27
29
  "types": "./dist/index.d.cts",
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: fs-fixture
3
+ description: Create disposable file system test fixtures from objects, templates, or empty directories with automatic cleanup. Use when writing tests that need temporary files, directories, symlinks, or JSON fixtures on disk. Keywords - fs, tmp, temp, fixture, testing utility, cleanup, dispose.
4
+ ---
5
+
6
+ ## Quick Start
7
+
8
+ ```ts
9
+ import { createFixture } from 'fs-fixture'
10
+
11
+ // From object — keys are paths, values are content
12
+ const fixture = await createFixture({
13
+ 'package.json': JSON.stringify({ name: 'test' }),
14
+ 'src/index.js': 'export default 42',
15
+ })
16
+
17
+ // Use fixture.path, fixture.readFile(), etc.
18
+ await fixture.rm() // Cleanup when done
19
+ ```
20
+
21
+ Auto-cleanup with `using` (TypeScript 5.2+):
22
+ ```ts
23
+ await using fixture = await createFixture({ 'file.txt': 'content' })
24
+ // Automatically cleaned up when scope exits
25
+ ```
26
+
27
+ ## createFixture(source?, options?)
28
+
29
+ | Source | Behavior |
30
+ |--------|----------|
31
+ | `{ path: content }` | Create files from object. Nested objects create directories. |
32
+ | `'./path'` | Copy template directory into fixture |
33
+ | _(omitted)_ | Empty temporary directory |
34
+
35
+ | Option | Type | Default | Description |
36
+ |--------|------|---------|-------------|
37
+ | `tempDir` | `string \| URL` | `os.tmpdir()` | Custom parent directory |
38
+ | `templateFilter` | `(src, dest) => boolean` | — | Filter files when copying template |
39
+
40
+ ## FsFixture Methods
41
+
42
+ | Method | Description |
43
+ |--------|-------------|
44
+ | `fixture.path` | Absolute path to fixture root |
45
+ | `getPath(...paths)` | Resolve path relative to fixture root |
46
+ | `exists(path?)` | Check existence (returns `boolean`) |
47
+ | `readFile(path, encoding?)` | Read file — `'utf8'` returns `string`, omit for `Buffer` |
48
+ | `writeFile(path, content)` | Write string or Buffer |
49
+ | `readJson<T>(path)` | Read and parse JSON with type parameter |
50
+ | `writeJson(path, data, space?)` | Write JSON (default: 2-space indent, `0` for minified) |
51
+ | `readdir(path, options?)` | List directory (`{ withFileTypes: true }` for Dirent[]) |
52
+ | `mkdir(path)` | Create directory recursively |
53
+ | `cp(source, dest?)` | Copy external file/directory into fixture |
54
+ | `mv(source, dest)` | Move or rename within fixture |
55
+ | `rm(path?)` | Delete path, or entire fixture if omitted |
56
+
57
+ ## FileTree Values
58
+
59
+ | Type | Example |
60
+ |------|---------|
61
+ | `string` | `'file content'` |
62
+ | `Buffer` | `Buffer.from([0x89, 0x50])` |
63
+ | Nested object | `{ dir: { 'file.txt': 'content' } }` |
64
+ | Function | `({ fixturePath, filePath, getPath, symlink }) => symlink('./target')` |
65
+
66
+ Path syntax — these are equivalent:
67
+ ```ts
68
+ // Nested objects
69
+ { src: { utils: { 'helper.js': 'code' } } }
70
+
71
+ // Slash-separated keys
72
+ { 'src/utils/helper.js': 'code' }
73
+ ```
74
+
75
+ ## Patterns
76
+
77
+ ### Symlinks
78
+ ```ts
79
+ const fixture = await createFixture({
80
+ 'target.txt': 'real file',
81
+ 'link.txt': ({ symlink }) => symlink('./target.txt'),
82
+ 'node_modules/pkg': ({ symlink }) => symlink(process.cwd()),
83
+ })
84
+ ```
85
+
86
+ ### Dynamic content
87
+ ```ts
88
+ const fixture = await createFixture({
89
+ 'info.txt': ({ fixturePath }) => `Root: ${fixturePath}`,
90
+ })
91
+ ```
92
+
93
+ ## Related
94
+
95
+ Commonly used with `manten` test framework.