Skip to content

Commit c1fd4f3

Browse files
kricsleosxzz
andauthored
feat(copy): support glob in copy (#637)
Co-authored-by: Kevin Deng <sxzz@sxzz.moe>
1 parent 36540d2 commit c1fd4f3

File tree

2 files changed

+66
-13
lines changed

2 files changed

+66
-13
lines changed

src/config/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,10 @@ export interface UserConfig {
477477
* ```ts
478478
* [
479479
* 'src/assets',
480+
* 'src/env.d.ts',
481+
* 'src/styles/**\/*.css',
480482
* { from: 'src/assets', to: 'dist/assets' },
483+
* { from: 'src/styles/**\/*.css', to: 'dist', flatten: true },
481484
* ]
482485
* ```
483486
*/

src/features/copy.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
import 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'
34
import { toArray } from '../utils/general.ts'
45
import type { ResolvedConfig } from '../config/index.ts'
56
import type { Arrayable, Awaitable } from '../utils/types.ts'
67

78
export 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
}
1125
export type CopyOptions = Arrayable<string | CopyEntry>
1226
export 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

Comments
 (0)