11import assert from 'node:assert/strict'
2- import { readFile } from 'node:fs/promises'
2+ import { readFile , readdir } from 'node:fs/promises'
33import { existsSync } from 'node:fs'
44import { fileURLToPath , pathToFileURL } from 'node:url'
55import { basename , dirname , extname , resolve , join , relative } from 'node:path'
@@ -175,11 +175,15 @@ export const build = async (...files) => {
175175
176176 const fsfiles = await getPackageFiles ( filename ? dirname ( resolve ( filename ) ) : process . cwd ( ) )
177177 const fsFilesContents = new Map ( )
178+ const fsFilesDirs = new Map ( )
178179 const cwd = process . cwd ( )
179- const fsFilesAdd = async ( fileRelative ) => {
180- if ( ! fileRelative || ! / ^ [ a - z 0 - 9 @ _ . / - ] + $ / iu. test ( fileRelative ) ) return
181- const file = resolve ( fileRelative )
182- if ( ! file . startsWith ( `${ cwd } /` ) ) return
180+ const fixturesRegex = / f i x t u r e s / u
181+ const aggressiveExtensions = / \. ( j s o n | t x t | h e x ) $ / u // These are bundled when just used in path.join and by wildcard from fixtures/
182+ const fileAllowed = ( f ) =>
183+ f && f . startsWith ( `${ cwd } /` ) && resolve ( f ) === f && / ^ [ a - z 0 - 9 @ _ . / - ] + $ / iu. test ( relative ( cwd , f ) )
184+
185+ const fsFilesAdd = async ( file ) => {
186+ if ( ! fileAllowed ( file ) ) return
183187 try {
184188 const data = await readFile ( file , 'base64' )
185189 if ( fsFilesContents . has ( file ) ) {
@@ -192,19 +196,46 @@ export const build = async (...files) => {
192196 }
193197 }
194198
199+ const fixturesSeen = { fs : false , fixtures : false , bundled : false }
200+ const fsFilesBundleFixtures = async ( reason ) => {
201+ if ( fixturesSeen . bundled || ! filename ) return
202+ if ( reason === 'fs' || reason === 'fixtures' ) fixturesSeen [ reason ] = true
203+ if ( ! fixturesSeen . fs || ! fixturesSeen . fixtures ) return
204+ fixturesSeen . bundled = true
205+ const dir = dirname ( resolve ( filename ) )
206+ for ( const name of await readdir ( dir , { recursive : true } ) ) {
207+ const parent = dirname ( name )
208+ if ( ! fixturesRegex . test ( parent ) ) continue // relative dir path should look like a fixtures dir
209+ // Save as directiry entry into parent dir
210+ const subdir = resolve ( dir , parent )
211+ if ( fileAllowed ( subdir ) ) {
212+ if ( ! fsFilesDirs . has ( subdir ) ) fsFilesDirs . set ( subdir , [ ] )
213+ fsFilesDirs . get ( subdir ) . push ( basename ( name ) )
214+ }
215+
216+ // Save to files
217+ const file = resolve ( dir , name )
218+ if ( aggressiveExtensions . test ( file ) ) await fsFilesAdd ( file )
219+ }
220+ }
221+
195222 specificLoadPipeline . push ( async ( source , filepath ) => {
196223 for ( const m of source . matchAll ( / r e a d F i l e S y n c \( \s * (?: " ( [ ^ " \\ ] + ) " | ' ( [ ^ ' \\ ] + ) ' ) [ ) , ] / gu) ) {
197- await fsFilesAdd ( m [ 1 ] || m [ 2 ] )
224+ await fsFilesAdd ( resolve ( m [ 1 ] || m [ 2 ] ) ) // resolves from cwd
198225 }
199226
200227 // E.g. path.join(import.meta.dirname, './fixtures/data.json'), dirname is inlined by loadPipeline already
201228 const dir = dirname ( filepath )
202229 for ( const m of source . matchAll ( / j o i n \( \s * ( " [ ^ " \\ ] + " ) , \s * (?: " ( [ ^ " \\ ] + ) " | ' ( [ ^ ' \\ ] + ) ' ) \s * \) / gu) ) {
203230 if ( m [ 1 ] !== JSON . stringify ( dir ) ) continue // only allow files relative to dirname, from loadPipeline
204- const file = relative ( cwd , join ( dir , m [ 2 ] || m [ 3 ] ) )
205- if ( / \. ( j s o n | t x t | h e x ) $ / u . test ( file ) ) await fsFilesAdd ( file ) // only allow specific extensions used as test fixtures
231+ const file = resolve ( dir , m [ 2 ] || m [ 3 ] )
232+ if ( aggressiveExtensions . test ( file ) ) await fsFilesAdd ( file ) // only bundle path.join for specific extensions used as test fixtures
206233 }
207234
235+ // Both conditions should happen for deep fixtures inclusion
236+ if ( / ( r e a d d i r | r e a d F i l e | e x i s t s ) S y n c / u. test ( source ) ) await fsFilesBundleFixtures ( 'fs' )
237+ if ( fixturesRegex . test ( source ) ) await fsFilesBundleFixtures ( 'fixtures' )
238+
208239 return source
209240 } )
210241
@@ -284,6 +315,7 @@ export const build = async (...files) => {
284315 EXODUS_TEST_RECORDINGS : stringify ( EXODUS_TEST_RECORDINGS ) ,
285316 EXODUS_TEST_FSFILES : stringify ( fsfiles ) , // TODO: can we safely use relative paths?
286317 EXODUS_TEST_FSFILES_CONTENTS : stringify ( [ ...fsFilesContents . entries ( ) ] ) ,
318+ EXODUS_TEST_FSDIRS : stringify ( [ ...fsFilesDirs . entries ( ) ] ) ,
287319 } ,
288320 alias : {
289321 // Jest, tape and node:test
@@ -352,9 +384,10 @@ export const build = async (...files) => {
352384 let res = await buildWrap ( config )
353385 assert . equal ( res instanceof Error , res . errors . length > 0 )
354386
355- if ( fsFilesContents . size > 0 ) {
387+ if ( fsFilesContents . size > 0 || fsFilesDirs . size > 0 ) {
356388 // re-run as we detected that tests depend on fsReadFileSync contents
357389 config . define . EXODUS_TEST_FSFILES_CONTENTS = stringify ( [ ...fsFilesContents . entries ( ) ] )
390+ config . define . EXODUS_TEST_FSDIRS = stringify ( [ ...fsFilesDirs . entries ( ) ] )
358391 res = await buildWrap ( config )
359392 assert . equal ( res instanceof Error , res . errors . length > 0 )
360393 }
0 commit comments