fs-fixture 2.8.1 โ†’ 2.10.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
@@ -7,23 +7,51 @@
7
7
  <a href="https://npm.im/fs-fixture"><img src="https://badgen.net/npm/v/fs-fixture"></a> <a href="https://npm.im/fs-fixture"><img src="https://badgen.net/npm/dm/fs-fixture"></a>
8
8
  </h1>
9
9
 
10
- Simple API to create disposable test fixtures on disk.
10
+ Simple API to create disposable test fixtures on disk. Tiny (`1.1 kB` gzipped) with zero dependencies!
11
11
 
12
- Tiny (`560 B` gzipped) and no dependencies!
12
+ ### Features
13
+ - ๐Ÿ“ Create files & directories from simple objects
14
+ - ๐Ÿงน Automatic cleanup with `using` keyword
15
+ - ๐Ÿ“ Built-in JSON read/write support
16
+ - ๐Ÿ”— Symlink support
17
+ - ๐Ÿ’พ Binary file support with Buffers
18
+ - ๐ŸŽฏ TypeScript-first with full type safety
19
+ - ๐Ÿ”„ File methods inherit types directly from Node.js `fs` module
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ npm install fs-fixture
25
+ ```
26
+
27
+ ## Quick start
13
28
 
14
- ### Example
15
29
  ```ts
16
- import fs from 'node:fs/promises'
17
30
  import { createFixture } from 'fs-fixture'
18
31
 
32
+ // Create a temporary fixture
19
33
  const fixture = await createFixture({
20
- 'dir-a': {
21
- 'file-b': 'hello world'
22
- }
34
+ 'package.json': JSON.stringify({ name: 'my-app' }),
35
+ 'src/index.js': 'console.log("Hello world")'
36
+ })
37
+
38
+ // Read files
39
+ const content = await fixture.readFile('src/index.js', 'utf8')
40
+
41
+ // Cleanup when done
42
+ await fixture.rm()
43
+ ```
44
+
45
+ ### Auto cleanup with `using` keyword
46
+
47
+ Uses TypeScript 5.2+ [Explicit Resource Management](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html) for automatic cleanup:
48
+
49
+ ```ts
50
+ await using fixture = await createFixture({
51
+ 'config.json': '{ "setting": true }'
23
52
  })
24
53
 
25
- const content = await fs.readFile(fixture.getPath('dir-a/file-b'))
26
- console.log(content)
54
+ // Fixture is automatically cleaned up when exiting scope
27
55
  ```
28
56
 
29
57
  <p align="center">
@@ -34,163 +62,166 @@ console.log(content)
34
62
 
35
63
  ## Usage
36
64
 
37
- Pass in an object representing the file structure:
65
+ ### Creating fixtures
38
66
 
67
+ **From an object:**
39
68
  ```ts
40
- import { createFixture } from 'fs-fixture'
41
-
42
69
  const fixture = await createFixture({
43
- // Nested directory syntax
44
- 'dir-a': {
45
- 'file-a.txt': 'hello world',
46
- 'dir-b': {
47
- 'file-b.txt': ({ fixturePath }) => `Fixture path: ${fixturePath}`,
48
- 'symlink-c': ({ symlink }) => symlink('../file-a.txt')
49
- }
50
- },
51
-
52
- // Alternatively, use the directory path syntax - Same as above
53
- 'dir-a/dir-b/file-b.txt': 'goodbye world'
70
+ 'package.json': '{ "name": "test" }',
71
+ 'src/index.js': 'export default () => {}',
72
+ 'src/utils': {
73
+ 'helper.js': 'export const help = () => {}'
74
+ }
54
75
  })
76
+ ```
55
77
 
56
- // Interact with the fixture
57
- console.log(fixture.path)
78
+ **From a template directory:**
79
+ ```ts
80
+ // Copies an existing directory structure
81
+ const fixture = await createFixture('./test-templates/basic')
82
+ ```
58
83
 
59
- // Cleanup fixture
60
- await fixture.rm()
84
+ **Empty fixture:**
85
+ ```ts
86
+ // Create an empty temporary directory
87
+ const fixture = await createFixture()
61
88
  ```
62
89
 
63
- ### Template path input
90
+ ### Working with files
64
91
 
65
- Pass in a path to a test fixture template directory to make a copy of it.
92
+ File methods (`readFile`, `writeFile`, `readdir`) inherit their type signatures directly from Node.js `fs/promises`, preserving all overloads and type narrowing behavior.
66
93
 
94
+ **Read files:**
67
95
  ```ts
68
- // Pass in a path to a fixture template path, and it will make a copy of it
69
- const fixture = await createFixture('./fixtures/template-a')
70
-
71
- /* Your test code here... */
96
+ // Read as string (type: Promise<string>)
97
+ const text = await fixture.readFile('config.txt', 'utf8')
72
98
 
73
- // Cleanup fixture
74
- await fixture.rm()
99
+ // Read as buffer (type: Promise<Buffer>)
100
+ const binary = await fixture.readFile('image.png')
75
101
  ```
76
102
 
77
- ### `using` keyword (Explicit Resource Management)
78
-
79
- [TypeScript 5.2](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html) supports the [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) feature, which allows you to instantiate the fixture via `using`. When the fixture is declared this way, it gets automatically cleaned up when exiting the scope.
103
+ **Write files:**
104
+ ```ts
105
+ await fixture.writeFile('output.txt', 'Hello world')
106
+ await fixture.writeFile('data.bin', Buffer.from([0x89, 0x50]))
107
+ ```
80
108
 
109
+ **JSON operations:**
81
110
  ```ts
82
- await using fixture = await createFixture({ file: 'hello' })
111
+ // Write JSON with formatting
112
+ await fixture.writeJson('config.json', { port: 3000 })
83
113
 
84
- // No need to run fixture.rm()
114
+ // Read and parse JSON with type safety
115
+ type Config = { port: number }
116
+ const config = await fixture.readJson<Config>('config.json')
85
117
  ```
86
118
 
87
- ## API
88
-
89
- ### createFixture(source, options)
119
+ ### Working with directories
90
120
 
91
- An async function that creates a fixture from the `source` you pass in, and returns a `FsFixture` instance.
121
+ ```ts
122
+ // Create directories
123
+ await fixture.mkdir('nested/folders')
92
124
 
93
- #### source
94
- Type: `string | FileTree`
125
+ // List directory contents
126
+ const files = await fixture.readdir('src')
95
127
 
96
- Path to a template fixture path, or a `FileTree` object that represents the fixture content.
128
+ // Copy files into fixture
129
+ await fixture.cp('/path/to/file.txt', 'copied-file.txt')
97
130
 
131
+ // Check if path exists
132
+ if (await fixture.exists('optional-file.txt')) {
133
+ // ...
134
+ }
135
+ ```
98
136
 
99
- #### options
137
+ ### Advanced features
100
138
 
101
- ##### tempDir
139
+ **Dynamic content with functions:**
140
+ ```ts
141
+ const fixture = await createFixture({
142
+ 'target.txt': 'original file',
143
+ 'info.txt': ({ fixturePath }) => `Created at: ${fixturePath}`,
144
+ 'link.txt': ({ symlink }) => symlink('./target.txt')
145
+ })
146
+ ```
102
147
 
103
- Type: `string`
148
+ **Binary files:**
149
+ ```ts
150
+ const fixture = await createFixture({
151
+ 'image.png': Buffer.from(imageData),
152
+ 'generated.bin': () => Buffer.from('dynamic binary content')
153
+ })
154
+ ```
104
155
 
105
- Default: `os.tmpdir()`
156
+ **Path syntax:**
157
+ ```ts
158
+ const fixture = await createFixture({
159
+ // Nested object syntax
160
+ src: {
161
+ utils: {
162
+ 'helper.js': 'export const help = () => {}'
163
+ }
164
+ },
106
165
 
107
- The directory where the fixture will be created.
166
+ // Or path syntax (creates same structure)
167
+ 'src/utils/helper.js': 'export const help = () => {}'
168
+ })
169
+ ```
108
170
 
171
+ ## API
109
172
 
110
- ##### templateFilter
173
+ ### `createFixture(source?, options?)`
111
174
 
112
- Type: `(source: string, destination: string) => boolean | Promise<boolean>`
175
+ Creates a temporary fixture directory and returns a `FsFixture` instance.
113
176
 
114
- Function to filter files to copy when using a template path. Return `true` to copy the item, `false` to ignore it.
177
+ **Parameters:**
178
+ - `source` (optional): String path to template directory, or `FileTree` object defining the structure
179
+ - `options.tempDir` (optional): Custom temp directory. Defaults to `os.tmpdir()`
180
+ - `options.templateFilter` (optional): Filter function when copying from template directory
115
181
 
116
- ### Types
117
- #### FileTree
182
+ **Returns:** `Promise<FsFixture>`
118
183
 
119
184
  ```ts
120
- type FileTree = {
121
- [path: string]: string | FileTree | ((api: Api) => string)
122
- }
185
+ const fixture = await createFixture()
186
+ const fixture = await createFixture({ 'file.txt': 'content' })
187
+ const fixture = await createFixture('./template-dir')
188
+ const fixture = await createFixture({}, { tempDir: './custom-temp' })
189
+ ```
123
190
 
124
- type Api = {
125
- // Fixture root path
126
- fixturePath: string
191
+ ### `FsFixture` Methods
192
+
193
+ | Method | Description |
194
+ |--------|-------------|
195
+ | `fixture.path` | Absolute path to the fixture directory |
196
+ | `getPath(...paths)` | Get absolute path to file/directory in fixture |
197
+ | `exists(path?)` | Check if file/directory exists |
198
+ | `rm(path?)` | Delete file/directory (or entire fixture if no path) |
199
+ | `readFile(path, encoding?)` | Read file as string or Buffer |
200
+ | `writeFile(path, content)` | Write string or Buffer to file |
201
+ | `readJson<T>(path)` | Read and parse JSON file |
202
+ | `writeJson(path, data, space?)` | Write JSON with optional formatting |
203
+ | `readdir(path, options?)` | List directory contents |
204
+ | `mkdir(path)` | Create directory (recursive) |
205
+ | `cp(source, dest?)` | Copy file/directory into fixture |
127
206
 
128
- // Current file path
129
- filePath: string
207
+ ### Types
130
208
 
131
- // Get path from the root of the fixture
132
- getPath: (...subpaths: string[]) => string
209
+ <details>
210
+ <summary><strong>FileTree</strong></summary>
133
211
 
134
- // Create a symlink
135
- symlink: (target: string) => Symlink
212
+ ```ts
213
+ type FileTree = {
214
+ [path: string]: string | Buffer | FileTree | ((api: Api) => string | Buffer | Symlink)
136
215
  }
137
- ```
138
216
 
139
- #### FsFixture
140
-
141
- ```ts
142
- class FsFixture {
143
- /**
144
- Path to the fixture directory.
145
- */
146
- readonly path: string
147
-
148
- /**
149
- Create a Fixture instance from a path. Does not create the fixture directory.
150
- */
151
- constructor(fixturePath: string)
152
-
153
- /**
154
- Get the full path to a subpath in the fixture directory.
155
- */
156
- getPath(...subpaths: string[]): string
157
-
158
- /**
159
- Check if the fixture exists. Pass in a subpath to check if it exists.
160
- */
161
- exists(subpath?: string): Promise<boolean>
162
-
163
- /**
164
- Delete the fixture directory. Pass in a subpath to delete it.
165
- */
166
- rm(subpath?: string): Promise<void>
167
-
168
- /**
169
- Copy a path into the fixture directory.
170
- */
171
- cp(sourcePath: string, destinationSubpath?: string): Promise<void>
172
-
173
- /**
174
- Create a new folder in the fixture directory.
175
- */
176
- mkdir(folderPath: string): Promise<void>
177
-
178
- /**
179
- Create a file in the fixture directory.
180
- */
181
- writeFile(filePath: string, content: string): Promise<void>
182
-
183
- /**
184
- Create a JSON file in the fixture directory.
185
- */
186
- writeJson(filePath: string, json: unknown): Promise<void>
187
-
188
- /**
189
- Read a file from the fixture directory.
190
- */
191
- readFile(filePath: string, encoding?: BufferEncoding): Promise<string | Buffer>
217
+ type Api = {
218
+ fixturePath: string // Fixture root path
219
+ filePath: string // Current file path
220
+ getPath: (...paths: string[]) => string // Get path from fixture root
221
+ symlink: (target: string) => Symlink // Create a symlink
192
222
  }
193
223
  ```
224
+ </details>
194
225
 
195
226
  ## Related
196
227
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var d=Object.defineProperty;var n=(s,t)=>d(s,"name",{value:t,configurable:!0});var a=require("node:fs/promises"),c=require("node:path"),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{n(this,"FsFixture")}path;constructor(t){this.path=t}getPath(...t){return c.join(this.path,...t)}exists(t=""){return a.access(this.getPath(t)).then(()=>!0,()=>!1)}rm(t=""){return a.rm(this.getPath(t),{recursive:!0,force:!0})}cp(t,r,i){return r?r.endsWith(c.sep)&&(r+=c.basename(t)):r=c.basename(t),a.cp(t,this.getPath(r),i)}mkdir(t){return a.mkdir(this.getPath(t),{recursive:!0})}writeFile(t,r){return a.writeFile(this.getPath(t),r)}writeJson(t,r){return this.writeFile(t,JSON.stringify(r,null,2))}readFile(t,r){return a.readFile(this.getPath(t),r)}async[Symbol.asyncDispose](){await this.rm()}}const P=v.realpathSync(F.tmpdir()),D=`fs-fixture-${Date.now()}-${process.pid}`;let p=0;const j=n(()=>(p+=1,p),"getId");class y{static{n(this,"Path")}path;constructor(t){this.path=t}}class f extends y{static{n(this,"Directory")}}class m extends y{static{n(this,"File")}content;constructor(t,r){super(t),this.content=r}}class l{static{n(this,"Symlink")}target;type;path;constructor(t,r){this.target=t,this.type=r}}const w=n((s,t,r)=>{const i=[];for(const u in s){if(!Object.hasOwn(s,u))continue;const e=c.join(t,u);let o=s[u];if(typeof o=="function"){const g=Object.assign(Object.create(r),{filePath:e}),h=o(g);if(h instanceof l){h.path=e,i.push(h);continue}else o=h}typeof o=="string"?i.push(new m(e,o)):i.push(new f(e),...w(o,e,r))}return i},"flattenFileTree"),k=n(async(s,t)=>{const r=t?.tempDir?c.resolve(t.tempDir):P,i=c.join(r,`${D}-${j()}/`);if(await a.mkdir(i,{recursive:!0}),s){if(typeof s=="string")await a.cp(s,i,{recursive:!0,filter:t?.templateFilter});else if(typeof s=="object"){const u={fixturePath:i,getPath:n((...e)=>c.join(i,...e),"getPath"),symlink:n((e,o)=>new l(e,o),"symlink")};await Promise.all(w(s,i,u).map(async e=>{e instanceof f?await a.mkdir(e.path,{recursive:!0}):e instanceof l?(await a.mkdir(c.dirname(e.path),{recursive:!0}),await a.symlink(e.target,e.path,e.type)):e instanceof m&&(await a.mkdir(c.dirname(e.path),{recursive:!0}),await a.writeFile(e.path,e.content))}))}}return new b(i)},"createFixture");exports.createFixture=k;
1
+ "use strict";var g=Object.defineProperty;var a=(s,e)=>g(s,"name",{value:e,configurable:!0});var n=require("node:fs/promises"),o=require("node:path"),F=require("node:url"),v=require("node:fs"),P=require("node:os");typeof Symbol.asyncDispose!="symbol"&&Object.defineProperty(Symbol,"asyncDispose",{configurable:!1,enumerable:!1,writable:!1,value:Symbol.for("asyncDispose")});class b{static{a(this,"FsFixture")}path;constructor(e){this.path=e}getPath(...e){return o.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,r,i){return r?r.endsWith(o.sep)&&(r+=o.basename(e)):r=o.basename(e),n.cp(e,this.getPath(r),i)}mkdir(e){return n.mkdir(this.getPath(e),{recursive:!0})}readFile=a(((e,r)=>n.readFile(this.getPath(e),r)),"readFile");readdir=a(((e,r)=>n.readdir(this.getPath(e||""),r)),"readdir");writeFile=a(((e,r,...i)=>n.writeFile(this.getPath(e),r,...i)),"writeFile");async readJson(e){const r=await this.readFile(e,"utf8");return JSON.parse(r)}writeJson(e,r,i=2){return this.writeFile(e,JSON.stringify(r,null,i))}async[Symbol.asyncDispose](){await this.rm()}}const D=v.realpathSync(P.tmpdir());class p{static{a(this,"PathBase")}constructor(e){this.path=e}}class y extends p{static{a(this,"Directory")}}class m extends p{static{a(this,"File")}constructor(e,r){super(e),this.content=r}}class u extends p{static{a(this,"Symlink")}constructor(e,r,i){super(i??""),this.target=e,this.type=r}}const w=a((s,e,r)=>{const i=[];for(const l in s){if(!Object.hasOwn(s,l))continue;const c=o.join(e,l);let t=s[l];if(typeof t=="function"){const h=Object.assign(Object.create(r),{filePath:c}),f=t(h);if(f instanceof u){const d=new u(f.target,f.type,c);i.push(d);continue}else t=f}if(typeof t=="string"||Buffer.isBuffer(t))i.push(new m(c,t));else if(t&&typeof t=="object"&&!Array.isArray(t))i.push(new y(c),...w(t,c,r));else throw new TypeError(`Invalid file content for path "${c}". Functions must return a string, Buffer, Symlink, or a nested FileTree object. Received: ${String(t)}`)}return i},"flattenFileTree"),j=a(async(s,e)=>{const r=e?.tempDir?o.resolve(typeof e.tempDir=="string"?e.tempDir:F.fileURLToPath(e.tempDir)):D;e?.tempDir&&await n.mkdir(r,{recursive:!0});const i=await n.mkdtemp(o.join(r,"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=w(s,i,{fixturePath:i,getPath:a((...t)=>o.join(i,...t),"getPath"),symlink:a((t,h)=>new u(t,h),"symlink")});await Promise.all(c.filter(t=>t instanceof y).map(t=>n.mkdir(t.path,{recursive:!0}))),await Promise.all(c.map(async t=>{t instanceof u?await n.symlink(t.target,t.path,t.type):t instanceof m&&await n.writeFile(t.path,t.content)}))}}return new b(i)},"createFixture");exports.createFixture=j;
package/dist/index.d.cts CHANGED
@@ -1,47 +1,128 @@
1
1
  import { CopyOptions } from 'node:fs';
2
+ import fs from 'node:fs/promises';
2
3
 
3
4
  declare class FsFixture {
4
5
  /**
5
- Path to the fixture directory.
6
- */
6
+ * Path to the fixture directory.
7
+ */
7
8
  readonly path: string;
8
9
  /**
9
- Create a Fixture instance from a path. Does not create the fixture directory.
10
- */
10
+ * Create a Fixture instance from a path. Does not create the fixture directory.
11
+ *
12
+ * @param fixturePath - The path to the fixture directory
13
+ */
11
14
  constructor(fixturePath: string);
12
15
  /**
13
- Get the full path to a subpath in the fixture directory.
14
- */
16
+ * Get the full path to a subpath in the fixture directory.
17
+ *
18
+ * @param subpaths - Path segments to join with the fixture directory
19
+ * @returns The absolute path to the subpath
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * fixture.getPath('dir', 'file.txt')
24
+ * // => '/tmp/fs-fixture-123/dir/file.txt'
25
+ * ```
26
+ */
15
27
  getPath(...subpaths: string[]): string;
16
28
  /**
17
- Check if the fixture exists. Pass in a subpath to check if it exists.
18
- */
29
+ * Check if the fixture exists. Pass in a subpath to check if it exists.
30
+ *
31
+ * @param subpath - Optional subpath to check within the fixture directory
32
+ * @returns Promise resolving to true if the path exists, false otherwise
33
+ */
19
34
  exists(subpath?: string): Promise<boolean>;
20
35
  /**
21
- Delete the fixture directory. Pass in a subpath to delete it.
22
- */
36
+ * Delete the fixture directory or a subpath within it.
37
+ *
38
+ * @param subpath - Optional subpath to delete within the fixture directory.
39
+ * Defaults to deleting the entire fixture.
40
+ * @returns Promise that resolves when deletion is complete
41
+ */
23
42
  rm(subpath?: string): Promise<void>;
24
43
  /**
25
- Copy a path into the fixture directory.
26
- */
44
+ * Copy a file or directory into the fixture directory.
45
+ *
46
+ * @param sourcePath - The source path to copy from
47
+ * @param destinationSubpath - Optional destination path within the fixture.
48
+ * If omitted, uses the basename of sourcePath.
49
+ * If ends with path separator, appends basename of sourcePath.
50
+ * @param options - Copy options (e.g., recursive, filter)
51
+ * @returns Promise that resolves when copy is complete
52
+ */
27
53
  cp(sourcePath: string, destinationSubpath?: string, options?: CopyOptions): Promise<void>;
28
54
  /**
29
- Create a new folder in the fixture directory.
30
- */
55
+ * Create a new folder in the fixture directory (including parent directories).
56
+ *
57
+ * @param folderPath - The folder path to create within the fixture
58
+ * @returns Promise that resolves when directory is created
59
+ */
31
60
  mkdir(folderPath: string): Promise<string | undefined>;
32
61
  /**
33
- Create a file in the fixture directory.
34
- */
35
- writeFile(filePath: string, content: string): Promise<void>;
62
+ * Read a file from the fixture directory.
63
+ *
64
+ * @param filePath - The file path within the fixture to read
65
+ * @param options - Optional encoding or read options.
66
+ * When encoding is specified, returns a string; otherwise returns a Buffer.
67
+ * @returns Promise resolving to file contents as string or Buffer
68
+ */
69
+ readFile: typeof fs.readFile;
36
70
  /**
37
- Create a JSON file in the fixture directory.
38
- */
39
- writeJson(filePath: string, json: unknown): Promise<void>;
71
+ * Read the contents of a directory in the fixture.
72
+ *
73
+ * @param directoryPath - The directory path within the fixture to read.
74
+ * Defaults to the fixture root when empty string is passed.
75
+ * @param options - Optional read directory options.
76
+ * Use `{ withFileTypes: true }` to get Dirent objects.
77
+ * @returns Promise resolving to array of file/directory names or Dirent objects
78
+ */
79
+ readdir: typeof fs.readdir;
40
80
  /**
41
- Read a file from the fixture directory.
42
- */
43
- readFile(filePath: string, encoding?: null): Promise<Buffer>;
44
- readFile(filePath: string, encoding: BufferEncoding): Promise<string>;
81
+ * Create or overwrite a file in the fixture directory.
82
+ *
83
+ * @param filePath - The file path within the fixture to write
84
+ * @param data - The content to write (string or Buffer)
85
+ * @param options - Optional encoding or write options
86
+ * @returns Promise that resolves when file is written
87
+ */
88
+ writeFile: typeof fs.writeFile;
89
+ /**
90
+ * Read and parse a JSON file from the fixture directory.
91
+ *
92
+ * @param filePath - The JSON file path within the fixture to read
93
+ * @returns Promise resolving to the parsed JSON content
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const data = await fixture.readJson<{ name: string }>('config.json')
98
+ * console.log(data.name) // Typed as string
99
+ * ```
100
+ */
101
+ readJson<T = unknown>(filePath: string): Promise<T>;
102
+ /**
103
+ * Create or overwrite a JSON file in the fixture directory.
104
+ *
105
+ * @param filePath - The JSON file path within the fixture to write
106
+ * @param json - The data to serialize as JSON
107
+ * @param space - Number of spaces or string to use for indentation. Defaults to 2.
108
+ * @returns Promise that resolves when file is written
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Default 2-space indentation
113
+ * await fixture.writeJson('config.json', { key: 'value' })
114
+ *
115
+ * // 4-space indentation
116
+ * await fixture.writeJson('config.json', { key: 'value' }, 4)
117
+ *
118
+ * // Tab indentation
119
+ * await fixture.writeJson('config.json', { key: 'value' }, '\t')
120
+ *
121
+ * // Minified (no formatting)
122
+ * await fixture.writeJson('config.json', { key: 'value' }, 0)
123
+ * ```
124
+ */
125
+ writeJson(filePath: string, json: unknown, space?: string | number): Promise<void>;
45
126
  /**
46
127
  * Resource management cleanup
47
128
  * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html
@@ -50,27 +131,32 @@ declare class FsFixture {
50
131
  }
51
132
  type FsFixtureType = FsFixture;
52
133
 
134
+ declare class PathBase {
135
+ readonly path: string;
136
+ constructor(path: string);
137
+ }
138
+
53
139
  type SymlinkType = 'file' | 'dir' | 'junction';
54
- declare class Symlink {
55
- target: string;
56
- type?: SymlinkType;
57
- path?: string;
58
- constructor(target: string, type?: SymlinkType);
140
+ declare class Symlink extends PathBase {
141
+ readonly target: string;
142
+ readonly type?: SymlinkType | undefined;
143
+ constructor(target: string, type?: SymlinkType | undefined, filePath?: string);
59
144
  }
145
+
60
146
  type ApiBase = {
61
147
  fixturePath: string;
62
148
  getPath(...subpaths: string[]): string;
63
149
  symlink(targetPath: string,
64
150
  /**
65
- * Symlink type for Windows. Defaults to auto-detect by Node.
66
- */
151
+ * Symlink type for Windows. Defaults to auto-detect by Node.
152
+ */
67
153
  type?: SymlinkType): Symlink;
68
154
  };
69
155
  type Api = ApiBase & {
70
156
  filePath: string;
71
157
  };
72
158
  type FileTree = {
73
- [path: string]: string | FileTree | ((api: Api) => string | Symlink);
159
+ [path: string]: string | Buffer | FileTree | ((api: Api) => string | Buffer | Symlink);
74
160
  };
75
161
 
76
162
  type FilterFunction = CopyOptions['filter'];
@@ -78,14 +164,48 @@ type CreateFixtureOptions = {
78
164
  /**
79
165
  * The temporary directory to create the fixtures in.
80
166
  * Defaults to `os.tmpdir()`.
167
+ *
168
+ * Accepts either a string path or a URL object.
169
+ *
170
+ * Tip: use `new URL('.', import.meta.url)` to the get the file's directory (not the file).
81
171
  */
82
- tempDir?: string;
172
+ tempDir?: string | URL;
83
173
  /**
84
174
  * Function to filter files to copy when using a template path.
85
175
  * Return `true` to copy the item, `false` to ignore it.
86
176
  */
87
177
  templateFilter?: FilterFunction;
88
178
  };
179
+ /**
180
+ * Create a temporary test fixture directory.
181
+ *
182
+ * @param source - Optional source to create the fixture from:
183
+ * - If omitted, creates an empty fixture directory
184
+ * - If a string, copies the directory at that path to the fixture
185
+ * - If a FileTree object, creates files and directories from the object structure
186
+ * @param options - Optional configuration for fixture creation
187
+ * @returns Promise resolving to an FsFixture instance
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * // Create empty fixture
192
+ * const fixture = await createFixture()
193
+ *
194
+ * // Create from object
195
+ * const fixture = await createFixture({
196
+ * 'file.txt': 'content',
197
+ * 'dir/nested.txt': 'nested content',
198
+ * 'binary.bin': Buffer.from('binary'),
199
+ * })
200
+ *
201
+ * // Create from template directory
202
+ * const fixture = await createFixture('./my-template')
203
+ *
204
+ * // Cleanup
205
+ * await fixture.rm()
206
+ * ```
207
+ */
89
208
  declare const createFixture: (source?: string | FileTree, options?: CreateFixtureOptions) => Promise<FsFixture>;
90
209
 
91
- export { type CreateFixtureOptions, type FileTree, type FsFixtureType as FsFixture, createFixture };
210
+ export { createFixture };
211
+ export type { CreateFixtureOptions, FileTree, FsFixtureType as FsFixture };
package/dist/index.d.mts CHANGED
@@ -1,47 +1,128 @@
1
1
  import { CopyOptions } from 'node:fs';
2
+ import fs from 'node:fs/promises';
2
3
 
3
4
  declare class FsFixture {
4
5
  /**
5
- Path to the fixture directory.
6
- */
6
+ * Path to the fixture directory.
7
+ */
7
8
  readonly path: string;
8
9
  /**
9
- Create a Fixture instance from a path. Does not create the fixture directory.
10
- */
10
+ * Create a Fixture instance from a path. Does not create the fixture directory.
11
+ *
12
+ * @param fixturePath - The path to the fixture directory
13
+ */
11
14
  constructor(fixturePath: string);
12
15
  /**
13
- Get the full path to a subpath in the fixture directory.
14
- */
16
+ * Get the full path to a subpath in the fixture directory.
17
+ *
18
+ * @param subpaths - Path segments to join with the fixture directory
19
+ * @returns The absolute path to the subpath
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * fixture.getPath('dir', 'file.txt')
24
+ * // => '/tmp/fs-fixture-123/dir/file.txt'
25
+ * ```
26
+ */
15
27
  getPath(...subpaths: string[]): string;
16
28
  /**
17
- Check if the fixture exists. Pass in a subpath to check if it exists.
18
- */
29
+ * Check if the fixture exists. Pass in a subpath to check if it exists.
30
+ *
31
+ * @param subpath - Optional subpath to check within the fixture directory
32
+ * @returns Promise resolving to true if the path exists, false otherwise
33
+ */
19
34
  exists(subpath?: string): Promise<boolean>;
20
35
  /**
21
- Delete the fixture directory. Pass in a subpath to delete it.
22
- */
36
+ * Delete the fixture directory or a subpath within it.
37
+ *
38
+ * @param subpath - Optional subpath to delete within the fixture directory.
39
+ * Defaults to deleting the entire fixture.
40
+ * @returns Promise that resolves when deletion is complete
41
+ */
23
42
  rm(subpath?: string): Promise<void>;
24
43
  /**
25
- Copy a path into the fixture directory.
26
- */
44
+ * Copy a file or directory into the fixture directory.
45
+ *
46
+ * @param sourcePath - The source path to copy from
47
+ * @param destinationSubpath - Optional destination path within the fixture.
48
+ * If omitted, uses the basename of sourcePath.
49
+ * If ends with path separator, appends basename of sourcePath.
50
+ * @param options - Copy options (e.g., recursive, filter)
51
+ * @returns Promise that resolves when copy is complete
52
+ */
27
53
  cp(sourcePath: string, destinationSubpath?: string, options?: CopyOptions): Promise<void>;
28
54
  /**
29
- Create a new folder in the fixture directory.
30
- */
55
+ * Create a new folder in the fixture directory (including parent directories).
56
+ *
57
+ * @param folderPath - The folder path to create within the fixture
58
+ * @returns Promise that resolves when directory is created
59
+ */
31
60
  mkdir(folderPath: string): Promise<string | undefined>;
32
61
  /**
33
- Create a file in the fixture directory.
34
- */
35
- writeFile(filePath: string, content: string): Promise<void>;
62
+ * Read a file from the fixture directory.
63
+ *
64
+ * @param filePath - The file path within the fixture to read
65
+ * @param options - Optional encoding or read options.
66
+ * When encoding is specified, returns a string; otherwise returns a Buffer.
67
+ * @returns Promise resolving to file contents as string or Buffer
68
+ */
69
+ readFile: typeof fs.readFile;
36
70
  /**
37
- Create a JSON file in the fixture directory.
38
- */
39
- writeJson(filePath: string, json: unknown): Promise<void>;
71
+ * Read the contents of a directory in the fixture.
72
+ *
73
+ * @param directoryPath - The directory path within the fixture to read.
74
+ * Defaults to the fixture root when empty string is passed.
75
+ * @param options - Optional read directory options.
76
+ * Use `{ withFileTypes: true }` to get Dirent objects.
77
+ * @returns Promise resolving to array of file/directory names or Dirent objects
78
+ */
79
+ readdir: typeof fs.readdir;
40
80
  /**
41
- Read a file from the fixture directory.
42
- */
43
- readFile(filePath: string, encoding?: null): Promise<Buffer>;
44
- readFile(filePath: string, encoding: BufferEncoding): Promise<string>;
81
+ * Create or overwrite a file in the fixture directory.
82
+ *
83
+ * @param filePath - The file path within the fixture to write
84
+ * @param data - The content to write (string or Buffer)
85
+ * @param options - Optional encoding or write options
86
+ * @returns Promise that resolves when file is written
87
+ */
88
+ writeFile: typeof fs.writeFile;
89
+ /**
90
+ * Read and parse a JSON file from the fixture directory.
91
+ *
92
+ * @param filePath - The JSON file path within the fixture to read
93
+ * @returns Promise resolving to the parsed JSON content
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const data = await fixture.readJson<{ name: string }>('config.json')
98
+ * console.log(data.name) // Typed as string
99
+ * ```
100
+ */
101
+ readJson<T = unknown>(filePath: string): Promise<T>;
102
+ /**
103
+ * Create or overwrite a JSON file in the fixture directory.
104
+ *
105
+ * @param filePath - The JSON file path within the fixture to write
106
+ * @param json - The data to serialize as JSON
107
+ * @param space - Number of spaces or string to use for indentation. Defaults to 2.
108
+ * @returns Promise that resolves when file is written
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Default 2-space indentation
113
+ * await fixture.writeJson('config.json', { key: 'value' })
114
+ *
115
+ * // 4-space indentation
116
+ * await fixture.writeJson('config.json', { key: 'value' }, 4)
117
+ *
118
+ * // Tab indentation
119
+ * await fixture.writeJson('config.json', { key: 'value' }, '\t')
120
+ *
121
+ * // Minified (no formatting)
122
+ * await fixture.writeJson('config.json', { key: 'value' }, 0)
123
+ * ```
124
+ */
125
+ writeJson(filePath: string, json: unknown, space?: string | number): Promise<void>;
45
126
  /**
46
127
  * Resource management cleanup
47
128
  * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html
@@ -50,27 +131,32 @@ declare class FsFixture {
50
131
  }
51
132
  type FsFixtureType = FsFixture;
52
133
 
134
+ declare class PathBase {
135
+ readonly path: string;
136
+ constructor(path: string);
137
+ }
138
+
53
139
  type SymlinkType = 'file' | 'dir' | 'junction';
54
- declare class Symlink {
55
- target: string;
56
- type?: SymlinkType;
57
- path?: string;
58
- constructor(target: string, type?: SymlinkType);
140
+ declare class Symlink extends PathBase {
141
+ readonly target: string;
142
+ readonly type?: SymlinkType | undefined;
143
+ constructor(target: string, type?: SymlinkType | undefined, filePath?: string);
59
144
  }
145
+
60
146
  type ApiBase = {
61
147
  fixturePath: string;
62
148
  getPath(...subpaths: string[]): string;
63
149
  symlink(targetPath: string,
64
150
  /**
65
- * Symlink type for Windows. Defaults to auto-detect by Node.
66
- */
151
+ * Symlink type for Windows. Defaults to auto-detect by Node.
152
+ */
67
153
  type?: SymlinkType): Symlink;
68
154
  };
69
155
  type Api = ApiBase & {
70
156
  filePath: string;
71
157
  };
72
158
  type FileTree = {
73
- [path: string]: string | FileTree | ((api: Api) => string | Symlink);
159
+ [path: string]: string | Buffer | FileTree | ((api: Api) => string | Buffer | Symlink);
74
160
  };
75
161
 
76
162
  type FilterFunction = CopyOptions['filter'];
@@ -78,14 +164,48 @@ type CreateFixtureOptions = {
78
164
  /**
79
165
  * The temporary directory to create the fixtures in.
80
166
  * Defaults to `os.tmpdir()`.
167
+ *
168
+ * Accepts either a string path or a URL object.
169
+ *
170
+ * Tip: use `new URL('.', import.meta.url)` to the get the file's directory (not the file).
81
171
  */
82
- tempDir?: string;
172
+ tempDir?: string | URL;
83
173
  /**
84
174
  * Function to filter files to copy when using a template path.
85
175
  * Return `true` to copy the item, `false` to ignore it.
86
176
  */
87
177
  templateFilter?: FilterFunction;
88
178
  };
179
+ /**
180
+ * Create a temporary test fixture directory.
181
+ *
182
+ * @param source - Optional source to create the fixture from:
183
+ * - If omitted, creates an empty fixture directory
184
+ * - If a string, copies the directory at that path to the fixture
185
+ * - If a FileTree object, creates files and directories from the object structure
186
+ * @param options - Optional configuration for fixture creation
187
+ * @returns Promise resolving to an FsFixture instance
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * // Create empty fixture
192
+ * const fixture = await createFixture()
193
+ *
194
+ * // Create from object
195
+ * const fixture = await createFixture({
196
+ * 'file.txt': 'content',
197
+ * 'dir/nested.txt': 'nested content',
198
+ * 'binary.bin': Buffer.from('binary'),
199
+ * })
200
+ *
201
+ * // Create from template directory
202
+ * const fixture = await createFixture('./my-template')
203
+ *
204
+ * // Cleanup
205
+ * await fixture.rm()
206
+ * ```
207
+ */
89
208
  declare const createFixture: (source?: string | FileTree, options?: CreateFixtureOptions) => Promise<FsFixture>;
90
209
 
91
- export { type CreateFixtureOptions, type FileTree, type FsFixtureType as FsFixture, createFixture };
210
+ export { createFixture };
211
+ export type { CreateFixtureOptions, FileTree, FsFixtureType as FsFixture };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- var d=Object.defineProperty;var n=(s,t)=>d(s,"name",{value:t,configurable:!0});import a from"node:fs/promises";import c from"node:path";import b from"node:fs";import F from"node:os";typeof Symbol.asyncDispose!="symbol"&&Object.defineProperty(Symbol,"asyncDispose",{configurable:!1,enumerable:!1,writable:!1,value:Symbol.for("asyncDispose")});class P{static{n(this,"FsFixture")}path;constructor(t){this.path=t}getPath(...t){return c.join(this.path,...t)}exists(t=""){return a.access(this.getPath(t)).then(()=>!0,()=>!1)}rm(t=""){return a.rm(this.getPath(t),{recursive:!0,force:!0})}cp(t,r,i){return r?r.endsWith(c.sep)&&(r+=c.basename(t)):r=c.basename(t),a.cp(t,this.getPath(r),i)}mkdir(t){return a.mkdir(this.getPath(t),{recursive:!0})}writeFile(t,r){return a.writeFile(this.getPath(t),r)}writeJson(t,r){return this.writeFile(t,JSON.stringify(r,null,2))}readFile(t,r){return a.readFile(this.getPath(t),r)}async[Symbol.asyncDispose](){await this.rm()}}const v=b.realpathSync(F.tmpdir()),D=`fs-fixture-${Date.now()}-${process.pid}`;let m=0;const j=n(()=>(m+=1,m),"getId");class u{static{n(this,"Path")}path;constructor(t){this.path=t}}class f extends u{static{n(this,"Directory")}}class y extends u{static{n(this,"File")}content;constructor(t,r){super(t),this.content=r}}class l{static{n(this,"Symlink")}target;type;path;constructor(t,r){this.target=t,this.type=r}}const w=n((s,t,r)=>{const i=[];for(const p in s){if(!Object.hasOwn(s,p))continue;const e=c.join(t,p);let o=s[p];if(typeof o=="function"){const g=Object.assign(Object.create(r),{filePath:e}),h=o(g);if(h instanceof l){h.path=e,i.push(h);continue}else o=h}typeof o=="string"?i.push(new y(e,o)):i.push(new f(e),...w(o,e,r))}return i},"flattenFileTree"),k=n(async(s,t)=>{const r=t?.tempDir?c.resolve(t.tempDir):v,i=c.join(r,`${D}-${j()}/`);if(await a.mkdir(i,{recursive:!0}),s){if(typeof s=="string")await a.cp(s,i,{recursive:!0,filter:t?.templateFilter});else if(typeof s=="object"){const p={fixturePath:i,getPath:n((...e)=>c.join(i,...e),"getPath"),symlink:n((e,o)=>new l(e,o),"symlink")};await Promise.all(w(s,i,p).map(async e=>{e instanceof f?await a.mkdir(e.path,{recursive:!0}):e instanceof l?(await a.mkdir(c.dirname(e.path),{recursive:!0}),await a.symlink(e.target,e.path,e.type)):e instanceof y&&(await a.mkdir(c.dirname(e.path),{recursive:!0}),await a.writeFile(e.path,e.content))}))}}return new P(i)},"createFixture");export{k as createFixture};
1
+ var d=Object.defineProperty;var a=(s,e)=>d(s,"name",{value:e,configurable:!0});import n from"node:fs/promises";import c from"node:path";import{fileURLToPath as F}from"node:url";import P 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{a(this,"FsFixture")}path;constructor(e){this.path=e}getPath(...e){return c.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,r,i){return r?r.endsWith(c.sep)&&(r+=c.basename(e)):r=c.basename(e),n.cp(e,this.getPath(r),i)}mkdir(e){return n.mkdir(this.getPath(e),{recursive:!0})}readFile=a(((e,r)=>n.readFile(this.getPath(e),r)),"readFile");readdir=a(((e,r)=>n.readdir(this.getPath(e||""),r)),"readdir");writeFile=a(((e,r,...i)=>n.writeFile(this.getPath(e),r,...i)),"writeFile");async readJson(e){const r=await this.readFile(e,"utf8");return JSON.parse(r)}writeJson(e,r,i=2){return this.writeFile(e,JSON.stringify(r,null,i))}async[Symbol.asyncDispose](){await this.rm()}}const j=P.realpathSync(b.tmpdir());class h{static{a(this,"PathBase")}constructor(e){this.path=e}}class u extends h{static{a(this,"Directory")}}class y extends h{static{a(this,"File")}constructor(e,r){super(e),this.content=r}}class p extends h{static{a(this,"Symlink")}constructor(e,r,i){super(i??""),this.target=e,this.type=r}}const w=a((s,e,r)=>{const i=[];for(const f in s){if(!Object.hasOwn(s,f))continue;const o=c.join(e,f);let t=s[f];if(typeof t=="function"){const m=Object.assign(Object.create(r),{filePath:o}),l=t(m);if(l instanceof p){const g=new p(l.target,l.type,o);i.push(g);continue}else t=l}if(typeof t=="string"||Buffer.isBuffer(t))i.push(new y(o,t));else if(t&&typeof t=="object"&&!Array.isArray(t))i.push(new u(o),...w(t,o,r));else throw new TypeError(`Invalid file content for path "${o}". Functions must return a string, Buffer, Symlink, or a nested FileTree object. Received: ${String(t)}`)}return i},"flattenFileTree"),k=a(async(s,e)=>{const r=e?.tempDir?c.resolve(typeof e.tempDir=="string"?e.tempDir:F(e.tempDir)):j;e?.tempDir&&await n.mkdir(r,{recursive:!0});const i=await n.mkdtemp(c.join(r,"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=w(s,i,{fixturePath:i,getPath:a((...t)=>c.join(i,...t),"getPath"),symlink:a((t,m)=>new p(t,m),"symlink")});await Promise.all(o.filter(t=>t instanceof u).map(t=>n.mkdir(t.path,{recursive:!0}))),await Promise.all(o.map(async t=>{t instanceof p?await n.symlink(t.target,t.path,t.type):t instanceof y&&await n.writeFile(t.path,t.content)}))}}return new D(i)},"createFixture");export{k as createFixture};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fs-fixture",
3
- "version": "2.8.1",
3
+ "version": "2.10.0",
4
4
  "description": "Easily create test fixtures at a temporary file-system path",
5
5
  "keywords": [
6
6
  "test",