@@ -79,6 +79,13 @@ test('throws on invalid concurrency value', async () => {
7979 await assert . rejects ( cpy ( [ 'license' , 'package.json' ] , context . tmp , { concurrency : 'foo' } ) ) ;
8080} ) ;
8181
82+ test ( 'throws on invalid base option' , async ( ) => {
83+ await assert . rejects (
84+ cpy ( [ 'license' ] , context . tmp , { base : 'invalid' } ) ,
85+ expectError ( TypeError , / ` b a s e ` m u s t b e / ) ,
86+ ) ;
87+ } ) ;
88+
8289test ( 'copy array of files with filter' , async ( ) => {
8390 await cpy ( [ 'license' , 'package.json' ] , context . tmp , {
8491 filter ( file ) {
@@ -238,6 +245,219 @@ test('path structure', async () => {
238245 ) ;
239246} ) ;
240247
248+ test ( 'base option: pattern aligns explicit paths with globs' , async ( ) => {
249+ fs . mkdirSync ( path . join ( context . tmp , 'src' ) , { recursive : true } ) ;
250+ fs . writeFileSync ( path . join ( context . tmp , 'src/hello-world.js' ) , 'console.log("hello");' ) ;
251+ fs . writeFileSync ( path . join ( context . tmp , 'src/README.md' ) , 'readme' ) ;
252+
253+ await cpy ( [ 'src/*.md' , 'src/hello-world.js' ] , 'dist' , {
254+ cwd : context . tmp ,
255+ base : 'pattern' ,
256+ } ) ;
257+
258+ assert . strictEqual (
259+ read ( context . tmp , 'src/README.md' ) ,
260+ read ( context . tmp , 'dist/README.md' ) ,
261+ ) ;
262+ assert . strictEqual (
263+ read ( context . tmp , 'src/hello-world.js' ) ,
264+ read ( context . tmp , 'dist/hello-world.js' ) ,
265+ ) ;
266+ } ) ;
267+
268+ test ( 'base option: cwd preserves nested glob structure from cwd' , async ( ) => {
269+ writeFiles ( context . tmp , {
270+ 'resources/views/app.edge' : 'app' ,
271+ 'resources/views/nested/baz.edge' : 'baz' ,
272+ } ) ;
273+
274+ const buildDirectory = path . join ( context . tmp , 'build' ) ;
275+
276+ await cpy ( [ 'resources/views/**/*.edge' ] , buildDirectory , {
277+ cwd : context . tmp ,
278+ base : 'cwd' ,
279+ } ) ;
280+
281+ assert . strictEqual (
282+ read ( context . tmp , 'resources' , 'views' , 'app.edge' ) ,
283+ read ( buildDirectory , 'resources' , 'views' , 'app.edge' ) ,
284+ ) ;
285+ assert . strictEqual (
286+ read ( context . tmp , 'resources' , 'views' , 'nested' , 'baz.edge' ) ,
287+ read ( buildDirectory , 'resources' , 'views' , 'nested' , 'baz.edge' ) ,
288+ ) ;
289+ } ) ;
290+
291+ test ( 'base option: cwd aligns globs outside cwd with explicit paths' , async ( ) => {
292+ const root = context . dir ;
293+ const cwd = path . join ( root , 'cwd' ) ;
294+ const src = path . join ( root , 'src' ) ;
295+ const out = path . join ( cwd , 'out' ) ;
296+ fs . mkdirSync ( cwd , { recursive : true } ) ;
297+ fs . mkdirSync ( src , { recursive : true } ) ;
298+ writeFiles ( root , {
299+ 'src/root.js' : 'root' ,
300+ 'src/nested/inner.js' : 'inner' ,
301+ 'src/nested/README.md' : 'readme' ,
302+ } ) ;
303+
304+ await cpy ( [ '../src/**/*.js' ] , 'out' , {
305+ cwd,
306+ base : 'cwd' ,
307+ } ) ;
308+
309+ assert . strictEqual ( read ( src , 'root.js' ) , read ( out , 'src/root.js' ) ) ;
310+ assert . strictEqual ( read ( src , 'nested/inner.js' ) , read ( out , 'src/nested/inner.js' ) ) ;
311+ assert . ok ( ! fs . existsSync ( path . join ( out , 'src/nested/README.md' ) ) ) ;
312+ } ) ;
313+
314+ test ( 'base option: cwd aligns absolute globs outside cwd' , async ( ) => {
315+ const root = context . dir ;
316+ const cwd = path . join ( root , 'cwd' ) ;
317+ const src = path . join ( root , 'src' ) ;
318+ const out = path . join ( cwd , 'out' ) ;
319+ fs . mkdirSync ( cwd , { recursive : true } ) ;
320+ fs . mkdirSync ( path . join ( src , 'nested' ) , { recursive : true } ) ;
321+ fs . writeFileSync ( path . join ( src , 'nested/inner.js' ) , 'inner' ) ;
322+
323+ const pattern = path . join ( src , '**/*.js' ) ;
324+ await cpy ( [ pattern ] , 'out' , {
325+ cwd,
326+ base : 'cwd' ,
327+ } ) ;
328+
329+ assert . strictEqual ( read ( src , 'nested/inner.js' ) , read ( out , 'src/nested/inner.js' ) ) ;
330+ } ) ;
331+
332+ test ( 'base option: cwd aligns outside globs with absolute destination' , async ( ) => {
333+ const root = context . dir ;
334+ const cwd = path . join ( root , 'cwd' ) ;
335+ const src = path . join ( root , 'src' ) ;
336+ const out = path . join ( root , 'out' ) ;
337+ fs . mkdirSync ( cwd , { recursive : true } ) ;
338+ fs . mkdirSync ( path . join ( src , 'nested' ) , { recursive : true } ) ;
339+ fs . writeFileSync ( path . join ( src , 'nested/inner.js' ) , 'inner' ) ;
340+
341+ await cpy ( [ '../src/**/*.js' ] , out , {
342+ cwd,
343+ base : 'cwd' ,
344+ } ) ;
345+
346+ assert . strictEqual ( read ( src , 'nested/inner.js' ) , read ( out , 'src/nested/inner.js' ) ) ;
347+ } ) ;
348+
349+ test ( 'base option: cwd aligns outside globs with symlinked parent' , async ( ) => {
350+ const root = context . dir ;
351+ const cwd = path . join ( root , 'cwd' ) ;
352+ const src = path . join ( root , 'src' ) ;
353+ const alias = path . join ( root , 'alias' ) ;
354+ const out = path . join ( cwd , 'out' ) ;
355+ fs . mkdirSync ( cwd , { recursive : true } ) ;
356+ fs . mkdirSync ( path . join ( src , 'nested' ) , { recursive : true } ) ;
357+ fs . writeFileSync ( path . join ( src , 'nested/inner.js' ) , 'inner' ) ;
358+ fs . symlinkSync ( src , alias , 'dir' ) ;
359+
360+ await cpy ( [ '../alias/**/*.js' ] , 'out' , {
361+ cwd,
362+ base : 'cwd' ,
363+ } ) ;
364+
365+ assert . strictEqual ( read ( src , 'nested/inner.js' ) , read ( out , 'alias/nested/inner.js' ) ) ;
366+ } ) ;
367+
368+ test ( 'base option: cwd does not escape destination for dynamic absolute globs' , { skip : process . platform === 'win32' } , async ( ) => {
369+ const root = context . dir ;
370+ const cwd = path . join ( root , 'nested' , 'cwd' ) ;
371+ const source = path . join ( root , 'source' ) ;
372+ const out = path . join ( root , 'dest' , 'nested' ) ;
373+ fs . mkdirSync ( cwd , { recursive : true } ) ;
374+ fs . mkdirSync ( path . join ( source , 'nested' ) , { recursive : true } ) ;
375+ fs . writeFileSync ( path . join ( source , 'nested' , 'file.js' ) , 'content' ) ;
376+
377+ const rootSegments = root . split ( path . sep ) . filter ( Boolean ) ;
378+ if ( rootSegments . length < 2 ) {
379+ return ;
380+ }
381+
382+ const pattern = path . posix . join ( '/' , '*' , ...rootSegments . slice ( 1 ) , 'source' , '**/*.js' ) ;
383+ await cpy ( [ pattern ] , out , {
384+ cwd,
385+ base : 'cwd' ,
386+ } ) ;
387+
388+ assert . strictEqual ( read ( source , 'nested/file.js' ) , read ( out , 'file.js' ) ) ;
389+ assert . ok ( ! fs . existsSync ( path . join ( root , 'dest' , 'source' , 'nested' , 'file.js' ) ) ) ;
390+ } ) ;
391+
392+ test ( 'base option: cwd aligns parent directory globs outside cwd' , async ( ) => {
393+ const root = context . dir ;
394+ const cwd = path . join ( root , 'cwd' ) ;
395+ const out = path . join ( cwd , 'out' ) ;
396+ fs . mkdirSync ( cwd , { recursive : true } ) ;
397+ writeFiles ( root , {
398+ 'root.js' : 'root' ,
399+ 'nested/inner.js' : 'inner' ,
400+ } ) ;
401+
402+ await cpy ( [ '../**/*.js' ] , 'out' , {
403+ cwd,
404+ base : 'cwd' ,
405+ } ) ;
406+
407+ const rootName = path . basename ( root ) ;
408+ assert . strictEqual ( read ( root , 'root.js' ) , read ( out , rootName , 'root.js' ) ) ;
409+ assert . strictEqual ( read ( root , 'nested/inner.js' ) , read ( out , rootName , 'nested/inner.js' ) ) ;
410+ } ) ;
411+
412+ test ( 'base option: cwd keeps distinct outside glob parents' , async ( ) => {
413+ const root = context . dir ;
414+ const cwd = path . join ( root , 'cwd' ) ;
415+ const first = path . join ( root , 'first' ) ;
416+ const second = path . join ( root , 'second' ) ;
417+ const out = path . join ( cwd , 'out' ) ;
418+ fs . mkdirSync ( cwd , { recursive : true } ) ;
419+ fs . mkdirSync ( first , { recursive : true } ) ;
420+ fs . mkdirSync ( second , { recursive : true } ) ;
421+ writeFiles ( root , {
422+ 'first/index.js' : 'first' ,
423+ 'first/nested/shared.js' : 'shared-1' ,
424+ 'second/index.js' : 'second' ,
425+ 'second/nested/shared.js' : 'shared-2' ,
426+ } ) ;
427+
428+ await cpy ( [ '../first/**/*.js' , '../second/**/*.js' ] , 'out' , {
429+ cwd,
430+ base : 'cwd' ,
431+ } ) ;
432+
433+ assert . strictEqual ( read ( first , 'index.js' ) , read ( out , 'first/index.js' ) ) ;
434+ assert . strictEqual ( read ( first , 'nested/shared.js' ) , read ( out , 'first/nested/shared.js' ) ) ;
435+ assert . strictEqual ( read ( second , 'index.js' ) , read ( out , 'second/index.js' ) ) ;
436+ assert . strictEqual ( read ( second , 'nested/shared.js' ) , read ( out , 'second/nested/shared.js' ) ) ;
437+ } ) ;
438+
439+ test ( 'base option: pattern with directory source copies contents without top-level directory' , async ( ) => {
440+ writeFiles ( context . tmp , {
441+ 'source/file.txt' : 'content' ,
442+ 'source/nested/inner.txt' : 'inner' ,
443+ } ) ;
444+
445+ await cpy ( [ 'source' ] , 'destination' , {
446+ cwd : context . tmp ,
447+ base : 'pattern' ,
448+ } ) ;
449+
450+ assert . strictEqual (
451+ read ( context . tmp , 'source/file.txt' ) ,
452+ read ( context . tmp , 'destination/file.txt' ) ,
453+ ) ;
454+ assert . strictEqual (
455+ read ( context . tmp , 'source/nested/inner.txt' ) ,
456+ read ( context . tmp , 'destination/nested/inner.txt' ) ,
457+ ) ;
458+ assert . ok ( ! fs . existsSync ( path . join ( context . tmp , 'destination/source' ) ) ) ;
459+ } ) ;
460+
241461test ( 'directory source outside cwd preserves structure under destination' , async ( ) => {
242462 const root = temporaryDirectory ( ) ;
243463 const cwd = path . join ( root , 'cwd' ) ;
0 commit comments