@@ -7,8 +7,8 @@ import type { HfsImpl } from "@humanfs/types";
77import { expect , onTestFinished } from "vitest" ;
88
99interface ScopedHfsImpl extends HfsImpl {
10- text ( file : string | URL ) : Promise < string | undefined > ;
11- json ( file : string | URL ) : Promise < unknown | undefined > ;
10+ text ( file : string | URL ) : Promise < string | undefined > ;
11+ json ( file : string | URL ) : Promise < unknown | undefined > ;
1212}
1313
1414/**
@@ -17,49 +17,49 @@ interface ScopedHfsImpl extends HfsImpl {
1717 * Includes all `hfs` methods — paths are resolved relative to the fixture root.
1818 */
1919export interface Fixture extends ScopedHfsImpl {
20- /** The fixture root as a `file://` URL. */
21- root : URL ;
22- /** Resolve a relative path within the fixture root. */
23- resolve : ( ...segments : string [ ] ) => URL ;
24- /** Delete the fixture directory. Also runs automatically via `onTestFinished`. */
25- cleanup : ( ) => Promise < void > ;
20+ /** The fixture root as a `file://` URL. */
21+ root : URL ;
22+ /** Resolve a relative path within the fixture root. */
23+ resolve : ( ...segments : string [ ] ) => URL ;
24+ /** Delete the fixture directory. Also runs automatically via `onTestFinished`. */
25+ cleanup : ( ) => Promise < void > ;
2626}
2727
2828/** Context passed to dynamic file content functions. */
2929export interface FileContext {
30- /**
31- * Metadata about the fixture root, analogous to `import.meta`.
32- *
33- * - `url` — the fixture root as a `file://` URL string
34- * - `filename` — absolute filesystem path to the fixture root
35- * - `dirname` — same as `filename` (root is a directory)
36- * - `resolve(path)` — resolve a relative path against the fixture root
37- */
38- importMeta : {
39- url : string ;
40- filename : string ;
41- dirname : string ;
42- resolve : ( path : string ) => string ;
43- } ;
44- /**
45- * Create a symbolic link to `target`.
46- *
47- * Returns a `SymlinkMarker` — the fixture will create the symlink on disk.
48- *
49- * @example
50- * ```ts
51- * { 'link.txt': ({ symlink }) => symlink('./target.txt') }
52- * ```
53- */
54- symlink : ( target : string ) => SymlinkMarker ;
30+ /**
31+ * Metadata about the fixture root, analogous to `import.meta`.
32+ *
33+ * - `url` — the fixture root as a `file://` URL string
34+ * - `filename` — absolute filesystem path to the fixture root
35+ * - `dirname` — same as `filename` (root is a directory)
36+ * - `resolve(path)` — resolve a relative path against the fixture root
37+ */
38+ importMeta : {
39+ url : string ;
40+ filename : string ;
41+ dirname : string ;
42+ resolve : ( path : string ) => string ;
43+ } ;
44+ /**
45+ * Create a symbolic link to `target`.
46+ *
47+ * Returns a `SymlinkMarker` — the fixture will create the symlink on disk.
48+ *
49+ * @example
50+ * ```ts
51+ * { 'link.txt': ({ symlink }) => symlink('./target.txt') }
52+ * ```
53+ */
54+ symlink : ( target : string ) => SymlinkMarker ;
5555}
5656
5757const SYMLINK = Symbol ( "symlink" ) ;
5858
5959/** Opaque marker returned by `ctx.symlink()`. */
6060export interface SymlinkMarker {
61- [ SYMLINK ] : true ;
62- target : string ;
61+ [ SYMLINK ] : true ;
62+ target : string ;
6363}
6464
6565/**
@@ -74,55 +74,55 @@ export interface SymlinkMarker {
7474 * | Function | `({ importMeta, symlink }) => symlink('./target')` |
7575 */
7676export type FileTreeValue =
77- | string
78- | Buffer
79- | Record < string , unknown >
80- | unknown [ ]
81- | FileTree
82- | ( ( ctx : FileContext ) => string | Buffer | SymlinkMarker ) ;
77+ | string
78+ | Buffer
79+ | Record < string , unknown >
80+ | unknown [ ]
81+ | FileTree
82+ | ( ( ctx : FileContext ) => string | Buffer | SymlinkMarker ) ;
8383
8484/** A recursive tree of files and directories. */
8585export interface FileTree {
86- [ key : string ] : FileTreeValue ;
86+ [ key : string ] : FileTreeValue ;
8787}
8888
8989function isSymlinkMarker ( value : unknown ) : value is SymlinkMarker {
90- return typeof value === "object" && value !== null && SYMLINK in value ;
90+ return typeof value === "object" && value !== null && SYMLINK in value ;
9191}
9292
9393function isFileTree ( value : unknown ) : value is FileTree {
94- return (
95- typeof value === "object" &&
96- value !== null &&
97- ! Buffer . isBuffer ( value ) &&
98- ! Array . isArray ( value ) &&
99- ! isSymlinkMarker ( value )
100- ) ;
94+ return (
95+ typeof value === "object" &&
96+ value !== null &&
97+ ! Buffer . isBuffer ( value ) &&
98+ ! Array . isArray ( value ) &&
99+ ! isSymlinkMarker ( value )
100+ ) ;
101101}
102102
103103function scopeHfs ( inner : NodeHfs , base : URL ) : ScopedHfsImpl {
104- const r = ( p : string | URL ) => new URL ( `./${ p } ` , base ) ;
105- const r2 = ( a : string | URL , b : string | URL ) => [ r ( a ) , r ( b ) ] as const ;
106-
107- return {
108- text : ( p : string | URL ) => inner . text ( r ( p ) ) ,
109- json : ( p : string | URL ) => inner . json ( r ( p ) ) ,
110- bytes : ( p ) => inner . bytes ( r ( p ) ) ,
111- write : ( p , c ) => inner . write ( r ( p ) , c ) ,
112- append : ( p , c ) => inner . append ( r ( p ) , c ) ,
113- isFile : ( p ) => inner . isFile ( r ( p ) ) ,
114- isDirectory : ( p ) => inner . isDirectory ( r ( p ) ) ,
115- createDirectory : ( p ) => inner . createDirectory ( r ( p ) ) ,
116- delete : ( p ) => inner . delete ( r ( p ) ) ,
117- deleteAll : ( p ) => inner . deleteAll ( r ( p ) ) ,
118- list : ( p ) => inner . list ( r ( p ) ) ,
119- size : ( p ) => inner . size ( r ( p ) ) ,
120- lastModified : ( p ) => inner . lastModified ( r ( p ) ) ,
121- copy : ( s , d ) => inner . copy ( ...r2 ( s , d ) ) ,
122- copyAll : ( s , d ) => inner . copyAll ( ...r2 ( s , d ) ) ,
123- move : ( s , d ) => inner . move ( ...r2 ( s , d ) ) ,
124- moveAll : ( s , d ) => inner . moveAll ( ...r2 ( s , d ) ) ,
125- } ;
104+ const r = ( p : string | URL ) => new URL ( `./${ p } ` , base ) ;
105+ const r2 = ( a : string | URL , b : string | URL ) => [ r ( a ) , r ( b ) ] as const ;
106+
107+ return {
108+ text : ( p : string | URL ) => inner . text ( r ( p ) ) ,
109+ json : ( p : string | URL ) => inner . json ( r ( p ) ) ,
110+ bytes : ( p ) => inner . bytes ( r ( p ) ) ,
111+ write : ( p , c ) => inner . write ( r ( p ) , c ) ,
112+ append : ( p , c ) => inner . append ( r ( p ) , c ) ,
113+ isFile : ( p ) => inner . isFile ( r ( p ) ) ,
114+ isDirectory : ( p ) => inner . isDirectory ( r ( p ) ) ,
115+ createDirectory : ( p ) => inner . createDirectory ( r ( p ) ) ,
116+ delete : ( p ) => inner . delete ( r ( p ) ) ,
117+ deleteAll : ( p ) => inner . deleteAll ( r ( p ) ) ,
118+ list : ( p ) => inner . list ( r ( p ) ) ,
119+ size : ( p ) => inner . size ( r ( p ) ) ,
120+ lastModified : ( p ) => inner . lastModified ( r ( p ) ) ,
121+ copy : ( s , d ) => inner . copy ( ...r2 ( s , d ) ) ,
122+ copyAll : ( s , d ) => inner . copyAll ( ...r2 ( s , d ) ) ,
123+ move : ( s , d ) => inner . move ( ...r2 ( s , d ) ) ,
124+ moveAll : ( s , d ) => inner . moveAll ( ...r2 ( s , d ) ) ,
125+ } ;
126126}
127127
128128/**
@@ -148,86 +148,86 @@ function scopeHfs(inner: NodeHfs, base: URL): ScopedHfsImpl {
148148 * ```
149149 */
150150export async function createFixture ( files : FileTree ) : Promise < Fixture > {
151- const raw = expect . getState ( ) . currentTestName ?? "bsh" ;
152- const prefix = raw
153- . toLowerCase ( )
154- . replace ( / [ ^ a - z 0 - 9 ] + / g, "-" )
155- . replace ( / ^ - | - $ / g, "" ) ;
156- const root = new URL ( `${ prefix } -` , `file://${ tmpdir ( ) } /` ) ;
157- const path = await mkdtemp ( fileURLToPath ( root ) ) ;
158- const base = pathToFileURL ( path + sep ) ;
159-
160- const inner = new NodeHfs ( ) ;
161- const scoped = scopeHfs ( inner , base ) ;
162- const resolve = ( ...segments : string [ ] ) => new URL ( `./${ segments . join ( "/" ) } ` , base ) ;
163-
164- const ctx : FileContext = {
165- importMeta : {
166- url : base . toString ( ) ,
167- filename : fileURLToPath ( base ) ,
168- dirname : fileURLToPath ( base ) ,
169- resolve : ( p : string ) => new URL ( `./${ p } ` , base ) . toString ( ) ,
170- } ,
171- symlink : ( target : string ) : SymlinkMarker => ( { [ SYMLINK ] : true , target } ) ,
172- } ;
173-
174- async function writeTree ( tree : FileTree , dir : URL ) : Promise < void > {
175- for ( const [ name , raw ] of Object . entries ( tree ) ) {
176- const url = new URL ( name , dir ) ;
177-
178- // Nested directory object (not a plain value)
179- if (
180- typeof raw !== "function" &&
181- ! Buffer . isBuffer ( raw ) &&
182- ! Array . isArray ( raw ) &&
183- isFileTree ( raw ) &&
184- ! name . includes ( "." )
185- ) {
186- await inner . createDirectory ( url ) ;
187- // Trailing slash so nested entries resolve relative to the dir
188- await writeTree ( raw , new URL ( `${ url } /` ) ) ;
189- continue ;
190- }
191-
192- // Ensure parent directory exists
193- const parent = new URL ( "./" , url ) ;
194- await inner . createDirectory ( parent ) ;
195-
196- // Resolve functions
197- const content = typeof raw === "function" ? raw ( ctx ) : raw ;
198-
199- // Symlink
200- if ( isSymlinkMarker ( content ) ) {
201- await fsSymlink ( content . target , url ) ;
202- continue ;
203- }
204-
205- // Buffer
206- if ( Buffer . isBuffer ( content ) ) {
207- await inner . write ( url , content ) ;
208- continue ;
209- }
210-
211- // JSON auto-serialization for .json files with non-string content
212- if ( name . endsWith ( ".json" ) && typeof content !== "string" ) {
213- await inner . write ( url , JSON . stringify ( content , null , 2 ) ) ;
214- continue ;
215- }
216-
217- // String content
218- await inner . write ( url , content as string ) ;
219- }
220- }
221-
222- await writeTree ( files , base ) ;
223-
224- const cleanup = ( ) => inner . deleteAll ( path ) . then ( ( ) => undefined ) ;
225- onTestFinished ( cleanup ) ;
226-
227- return {
228- root : base ,
229- resolve,
230- cleanup,
231- ...scoped ,
232- } ;
151+ const raw = expect . getState ( ) . currentTestName ?? "bsh" ;
152+ const prefix = raw
153+ . toLowerCase ( )
154+ . replace ( / [ ^ a - z 0 - 9 ] + / g, "-" )
155+ . replace ( / ^ - | - $ / g, "" ) ;
156+ const root = new URL ( `${ prefix } -` , `file://${ tmpdir ( ) } /` ) ;
157+ const path = await mkdtemp ( fileURLToPath ( root ) ) ;
158+ const base = pathToFileURL ( path + sep ) ;
159+
160+ const inner = new NodeHfs ( ) ;
161+ const scoped = scopeHfs ( inner , base ) ;
162+ const resolve = ( ...segments : string [ ] ) => new URL ( `./${ segments . join ( "/" ) } ` , base ) ;
163+
164+ const ctx : FileContext = {
165+ importMeta : {
166+ url : base . toString ( ) ,
167+ filename : fileURLToPath ( base ) ,
168+ dirname : fileURLToPath ( base ) ,
169+ resolve : ( p : string ) => new URL ( `./${ p } ` , base ) . toString ( ) ,
170+ } ,
171+ symlink : ( target : string ) : SymlinkMarker => ( { [ SYMLINK ] : true , target } ) ,
172+ } ;
173+
174+ async function writeTree ( tree : FileTree , dir : URL ) : Promise < void > {
175+ for ( const [ name , raw ] of Object . entries ( tree ) ) {
176+ const url = new URL ( name , dir ) ;
177+
178+ // Nested directory object (not a plain value)
179+ if (
180+ typeof raw !== "function" &&
181+ ! Buffer . isBuffer ( raw ) &&
182+ ! Array . isArray ( raw ) &&
183+ isFileTree ( raw ) &&
184+ ! name . includes ( "." )
185+ ) {
186+ await inner . createDirectory ( url ) ;
187+ // Trailing slash so nested entries resolve relative to the dir
188+ await writeTree ( raw , new URL ( `${ url } /` ) ) ;
189+ continue ;
190+ }
191+
192+ // Ensure parent directory exists
193+ const parent = new URL ( "./" , url ) ;
194+ await inner . createDirectory ( parent ) ;
195+
196+ // Resolve functions
197+ const content = typeof raw === "function" ? raw ( ctx ) : raw ;
198+
199+ // Symlink
200+ if ( isSymlinkMarker ( content ) ) {
201+ await fsSymlink ( content . target , url ) ;
202+ continue ;
203+ }
204+
205+ // Buffer
206+ if ( Buffer . isBuffer ( content ) ) {
207+ await inner . write ( url , content ) ;
208+ continue ;
209+ }
210+
211+ // JSON auto-serialization for .json files with non-string content
212+ if ( name . endsWith ( ".json" ) && typeof content !== "string" ) {
213+ await inner . write ( url , JSON . stringify ( content , null , 2 ) ) ;
214+ continue ;
215+ }
216+
217+ // String content
218+ await inner . write ( url , content as string ) ;
219+ }
220+ }
221+
222+ await writeTree ( files , base ) ;
223+
224+ const cleanup = ( ) => inner . deleteAll ( path ) . then ( ( ) => undefined ) ;
225+ onTestFinished ( cleanup ) ;
226+
227+ return {
228+ root : base ,
229+ resolve,
230+ cleanup,
231+ ...scoped ,
232+ } ;
233233}
0 commit comments