Skip to content

Commit 7672842

Browse files
authored
feat: add name property to RenameObject and allow RenameFunc to return RenameObject (#251)
1 parent 7ae0134 commit 7672842

File tree

5 files changed

+114
-23
lines changed

5 files changed

+114
-23
lines changed

.changeset/large-kings-show.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'vite-plugin-static-copy': minor
3+
---
4+
5+
Add `name` property to the `rename` object form and allow rename functions to return a `RenameObject`. The `name` property replaces the file's basename (filename + extension), and can be combined with `stripBase` to both flatten directory structure and rename the file in one step. Rename functions can now return `{ name, stripBase }` objects instead of only strings, making it easier to declaratively control output paths from dynamic rename logic.
6+
7+
```js
8+
// node_modules/lib/dist/index.js → vendor/lib.js
9+
{ src: 'node_modules/lib/dist/index.js', dest: 'vendor', rename: { name: 'lib.js', stripBase: true } }
10+
11+
// src/pages/events/test.html → dist/events/index.html
12+
{ src: 'src/pages/**/*.html', dest: 'dist/', rename: { stripBase: 2, name: 'index.html' } }
13+
```

src/options.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import type { WatchOptions } from 'chokidar'
22

33
type MaybePromise<T> = T | Promise<T>
4+
type WithRequiredSingleKey<T, K extends keyof T> = {
5+
[P in K]-?: Required<T[P]>
6+
} & T
7+
8+
type OptionalRenameObject = { stripBase?: number | true; name?: string }
9+
export type RenameObject =
10+
| WithRequiredSingleKey<OptionalRenameObject, 'stripBase'>
11+
| WithRequiredSingleKey<OptionalRenameObject, 'name'>
412

513
export type RenameFunc = (
614
fileName: string,
715
fileExtension: string,
816
fullPath: string,
9-
) => MaybePromise<string>
10-
11-
export type RenameObject = { stripBase: number | true }
17+
) => MaybePromise<string | RenameObject>
1218

1319
/**
1420
* @param content content of file
@@ -46,18 +52,23 @@ export type Target = {
4652
/**
4753
* Rename the output file.
4854
*
49-
* When a string is provided, the matched file is renamed to that string.
55+
* When a string is provided, it is a shorthand for `{ name: string }`.
5056
*
51-
* When an object `{ stripBase: number | true }` is provided, the given number
52-
* of leading directory segments from the matched path are stripped from the
53-
* destination. When `true`, all directory segments are stripped (equivalent to
54-
* flat copy).
57+
* When an object is provided:
58+
* - `name`: replaces the file's basename (filename + extension).
59+
* - `stripBase`: the given number of leading directory segments from the matched
60+
* path are stripped from the destination. When `true`, all directory segments
61+
* are stripped (equivalent to flat copy).
62+
* - When both are provided, `stripBase` is applied first, then `name` replaces
63+
* the basename.
5564
*
5665
* When a function is provided, it receives `(fileName, fileExtension, fullPath)`
57-
* and should return the new file name.
58-
* The returned value is joined with the resolved `dest` directory using
59-
* `path.join`, so it can include path segments (e.g. `subdir/file.txt`) or
60-
* `../` traversals to restructure the output.
66+
* and should return the new file name or a `RenameObject`.
67+
* When a string is returned, it is joined with the resolved `dest` directory
68+
* using `path.join`, so it can include path segments (e.g. `subdir/file.txt`)
69+
* or `../` traversals to restructure the output.
70+
* When a `RenameObject` is returned, it is applied the same way as the object
71+
* form above.
6172
*
6273
* @example
6374
* ```js
@@ -69,6 +80,11 @@ export type Target = {
6980
* { src: 'src/pages/**\/*.html', dest: 'dist/', rename: { stripBase: true } }
7081
* // Copies ../../src/pages/events/test.html to dist/test.html
7182
* { src: '../../src/pages/**\/*.html', dest: 'dist/', rename: { stripBase: true } }
83+
* // Copies foo.txt to dist/newname.txt (name only)
84+
* { src: 'foo.txt', dest: 'dist/', rename: { name: 'newname.txt' } }
85+
* // Copies src/pages/events/test.html to dist/events/newname.txt (stripBase + name)
86+
* { src: 'src/pages/**\/*.html', dest: 'dist/', rename: { stripBase: 2, name: 'newname.txt' } }
87+
*
7288
* // Copies src/pages/events/test.html to dist/src/pages/events/test2.html
7389
* { src: 'src/pages/**\/*.html', dest: 'dist/', rename: (name, ext) => `${name}2.${ext}` }
7490
* // Copies src/pages/events/test.html to dist/pages/events/test.html

src/utils.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,28 +184,50 @@ export const groupTargetsByDirectoryTree = <T extends { resolvedDest: string }>(
184184
return groups
185185
}
186186

187+
function applyRenameObject(
188+
renameObj: RenameObject,
189+
target: string,
190+
dir: string,
191+
): string {
192+
let result = target
193+
if ('stripBase' in renameObj && renameObj.stripBase !== undefined) {
194+
const dirSegments = dir ? dir.split('/') : []
195+
const goUp = '../'.repeat(dirSegments.length)
196+
const stripCount =
197+
renameObj.stripBase === true ? dirSegments.length : renameObj.stripBase
198+
const remaining = dirSegments.slice(stripCount).join('/')
199+
result = remaining ? `${goUp}${remaining}/${target}` : `${goUp}${target}`
200+
}
201+
if ('name' in renameObj && renameObj.name !== undefined) {
202+
const parsed = path.parse(result)
203+
result = path.join(parsed.dir, renameObj.name)
204+
}
205+
return result
206+
}
207+
187208
async function renameTarget(
188209
target: string,
189210
rename: string | RenameObject | RenameFunc,
190211
src: string,
191212
dir: string,
192213
): Promise<string> {
193-
const parsedPath = path.parse(target)
194-
195214
if (typeof rename === 'string') {
196215
return rename
197216
}
198-
199-
if (typeof rename === 'object' && 'stripBase' in rename) {
200-
const dirSegments = dir ? dir.split('/') : []
201-
const goUp = '../'.repeat(dirSegments.length)
202-
const stripCount =
203-
rename.stripBase === true ? dirSegments.length : rename.stripBase
204-
const remaining = dirSegments.slice(stripCount).join('/')
205-
return remaining ? `${goUp}${remaining}/${target}` : `${goUp}${target}`
217+
if (typeof rename === 'object') {
218+
return applyRenameObject(rename, target, dir)
206219
}
207220

208-
return rename(parsedPath.name, parsedPath.ext.replace('.', ''), src)
221+
const parsedPath = path.parse(target)
222+
const result = await rename(
223+
parsedPath.name,
224+
parsedPath.ext.replace('.', ''),
225+
src,
226+
)
227+
if (typeof result === 'object') {
228+
return applyRenameObject(result, target, dir)
229+
}
230+
return result
209231
}
210232

211233
export const collectCopyTargets = async (

test/fixtures/vite.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,26 @@ export default defineConfig({
223223
dest: 'fixture23',
224224
rename: { stripBase: true },
225225
},
226+
{
227+
src: 'foo.txt',
228+
dest: 'fixture24',
229+
rename: { name: 'newname.txt' },
230+
},
231+
{
232+
src: 'dir/bar.txt',
233+
dest: 'fixture24',
234+
rename: { stripBase: 1, name: 'newname2.txt' },
235+
},
236+
{
237+
src: 'foo.txt',
238+
dest: 'fixture24',
239+
rename: () => ({ name: 'fn-renamed.txt' }),
240+
},
241+
{
242+
src: 'dir/bar.txt',
243+
dest: 'fixture24',
244+
rename: () => ({ stripBase: 1 }),
245+
},
226246
],
227247
}),
228248
viteStaticCopy({

test/testcases.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,26 @@ export const testcases: Record<string, Testcase[]> = {
247247
src: './dir/deep/bar.txt',
248248
dest: '/fixture23/bar.txt',
249249
},
250+
{
251+
name: 'rename object with name only',
252+
src: './foo.txt',
253+
dest: '/fixture24/newname.txt',
254+
},
255+
{
256+
name: 'rename object with stripBase and name',
257+
src: './dir/bar.txt',
258+
dest: '/fixture24/newname2.txt',
259+
},
260+
{
261+
name: 'rename function returning object with name',
262+
src: './foo.txt',
263+
dest: '/fixture24/fn-renamed.txt',
264+
},
265+
{
266+
name: 'rename function returning object with stripBase',
267+
src: './dir/bar.txt',
268+
dest: '/fixture24/bar.txt',
269+
},
250270
{
251271
name: 'parallel copy to same dir (1)',
252272
src: './eexist/a/1.txt',

0 commit comments

Comments
 (0)