11import path from 'node:path'
2- import { fsCopy } from '../utils/fs.ts'
2+ import { glob , isDynamicPattern } from 'tinyglobby'
3+ import { fsCopy , fsStat } from '../utils/fs.ts'
34import { toArray } from '../utils/general.ts'
45import type { ResolvedConfig } from '../config/index.ts'
56import type { Arrayable , Awaitable } from '../utils/types.ts'
67
78export interface CopyEntry {
8- from : string
9- to : string
9+ /**
10+ * Source path or glob pattern.
11+ */
12+ from : string | string [ ]
13+ /**
14+ * Destination path.
15+ * If not specified, defaults to the output directory ("outDir").
16+ */
17+ to ?: string
18+ /**
19+ * Whether to flatten the copied files (not preserving directory structure).
20+ *
21+ * @default false
22+ */
23+ flatten ?: boolean
1024}
1125export type CopyOptions = Arrayable < string | CopyEntry >
1226export type CopyOptionsFn = ( options : ResolvedConfig ) => Awaitable < CopyOptions >
@@ -19,17 +33,29 @@ export async function copy(options: ResolvedConfig): Promise<void> {
1933 ? await options . copy ( options )
2034 : options . copy
2135
22- const resolved : [ from : string , to : string ] [ ] = toArray ( copy ) . map ( ( dir ) => {
23- const from = path . resolve (
24- options . cwd ,
25- typeof dir === 'string' ? dir : dir . from ,
36+ const resolved = (
37+ await Promise . all (
38+ toArray ( copy ) . map ( async ( entry ) => {
39+ if ( typeof entry === 'string' ) {
40+ entry = { from : [ entry ] }
41+ }
42+ let from = toArray ( entry . from )
43+
44+ const isGlob = from . some ( ( f ) => isDynamicPattern ( f ) )
45+ if ( isGlob ) {
46+ from = await glob ( from , {
47+ cwd : options . cwd ,
48+ onlyFiles : true ,
49+ expandDirectories : false ,
50+ } )
51+ }
52+
53+ return Promise . all (
54+ from . map ( ( file ) => resolveCopyEntry ( { ...entry , from : file } ) ) ,
55+ )
56+ } ) ,
2657 )
27- const to =
28- typeof dir === 'string'
29- ? path . resolve ( options . outDir , path . basename ( from ) )
30- : path . resolve ( options . cwd , dir . to )
31- return [ from , to ]
32- } )
58+ ) . flat ( )
3359
3460 await Promise . all (
3561 resolved . map ( ( [ from , to ] ) => {
@@ -43,4 +69,28 @@ export async function copy(options: ResolvedConfig): Promise<void> {
4369 return fsCopy ( from , to )
4470 } ) ,
4571 )
72+
73+ async function resolveCopyEntry (
74+ entry : CopyEntry & { from : string } ,
75+ ) : Promise < [ from : string , to : string ] > {
76+ const from = path . resolve ( options . cwd , entry . from )
77+ const parsedFrom = path . parse ( path . relative ( options . cwd , from ) )
78+ const dest = entry . to ? path . resolve ( options . cwd , entry . to ) : options . outDir
79+
80+ if ( entry . flatten || ! parsedFrom . dir ) {
81+ const isFile = ( await fsStat ( from ) ) ?. isFile ( )
82+ const to = isFile ? path . join ( dest , parsedFrom . base ) : dest
83+ return [ from , to ]
84+ }
85+
86+ const to = path . join (
87+ dest ,
88+ // Stripe off the first segment to avoid unnecessary nesting
89+ // e.g. "src/index.css" -> index.css" -> "dist/index.css"
90+ parsedFrom . dir . replace ( parsedFrom . dir . split ( path . sep ) [ 0 ] , '' ) ,
91+ parsedFrom . base ,
92+ )
93+
94+ return [ from , to ]
95+ }
4696}
0 commit comments