Skip to content

Add watch mode support for target files #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ interface CopyOptions extends globby.GlobbyOptions, fs.WriteFileOptions, fs.Copy
* @default false
*/
readonly verbose?: boolean;

/**
* Watch targets for changes.
* @default false
*/
readonly watchTargets?: boolean;
}

/**
Expand Down
13 changes: 13 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ copy({
})
```

#### watchTargets

Type: `boolean` | Default: `false`

Add the files specified in `targets` to the watch list

```js
copy({
targets: [{ src: "assets/**/*", dest: "dist/public" }],
watchTargets: true,
})
```

All other options are passed to packages, used inside:
- [globby](https://github.com/sindresorhus/globby)
- [fs-extra copy function](https://github.com/jprichardson/node-fs-extra/blob/7.0.0/docs/copy.md)
Expand Down
165 changes: 102 additions & 63 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ async function generateCopyTarget(src, dest, { flatten, rename, transform }) {
}
}

async function getMatchedPaths(target, restPluginOptions) {
if (!isObject(target)) {
throw new Error(`${stringify(target)} target must be an object`)
}

const { dest, rename, src, transform, ...restTargetOptions } = target

if (!src || !dest) {
throw new Error(`${stringify(target)} target must have "src" and "dest" properties`)
}

if (rename && typeof rename !== 'string' && typeof rename !== 'function') {
throw new Error(`${stringify(target)} target's "rename" property must be a string or a function`)
}

return globby(src, {
expandDirectories: false,
onlyFiles: false,
...restPluginOptions,
...restTargetOptions
})
}

export default function copy(options = {}) {
const {
copyOnce = false,
Expand All @@ -51,93 +74,109 @@ export default function copy(options = {}) {
hook = 'buildEnd',
targets = [],
verbose = false,
watchTargets = false,
...restPluginOptions
} = options

let copied = false

return {
name: 'copy',
[hook]: async () => {
if (copyOnce && copied) {
return
}
async function processCopyTargets() {
if (copyOnce && copied) {
return
}

const copyTargets = []
const copyTargets = []

if (Array.isArray(targets) && targets.length) {
for (const target of targets) {
if (!isObject(target)) {
throw new Error(`${stringify(target)} target must be an object`)
}
if (Array.isArray(targets) && targets.length) {
for (const target of targets) {
const matchedPaths = await getMatchedPaths(target, restPluginOptions)

if (matchedPaths.length) {
// eslint-disable-next-line no-unused-vars
const { dest, rename, src, transform, ...restTargetOptions } = target

if (!src || !dest) {
throw new Error(`${stringify(target)} target must have "src" and "dest" properties`)
}

if (rename && typeof rename !== 'string' && typeof rename !== 'function') {
throw new Error(`${stringify(target)} target's "rename" property must be a string or a function`)
}

const matchedPaths = await globby(src, {
expandDirectories: false,
onlyFiles: false,
...restPluginOptions,
...restTargetOptions
})

if (matchedPaths.length) {
for (const matchedPath of matchedPaths) {
const generatedCopyTargets = Array.isArray(dest)
? await Promise.all(dest.map((destination) => generateCopyTarget(
matchedPath,
destination,
{ flatten, rename, transform }
)))
: [await generateCopyTarget(matchedPath, dest, { flatten, rename, transform })]

copyTargets.push(...generatedCopyTargets)
}
for (const matchedPath of matchedPaths) {
const generatedCopyTargets = Array.isArray(dest)
? await Promise.all(dest.map((destination) => generateCopyTarget(
matchedPath,
destination,
{ flatten, rename, transform }
)))
: [await generateCopyTarget(matchedPath, dest, { flatten, rename, transform })]

copyTargets.push(...generatedCopyTargets)
}
}
}
}

if (copyTargets.length) {
if (verbose) {
console.log(green('copied:'))
if (copyTargets.length) {
if (verbose) {
console.log(green('copied:'))
}

for (const copyTarget of copyTargets) {
const { contents, dest, src, transformed } = copyTarget

if (transformed) {
await fs.outputFile(dest, contents, restPluginOptions)
} else if (!copySync) {
await fs.copy(src, dest, restPluginOptions)
} else {
fs.copySync(src, dest, restPluginOptions)
}

for (const copyTarget of copyTargets) {
const { contents, dest, src, transformed } = copyTarget
if (verbose) {
let message = green(` ${bold(src)} → ${bold(dest)}`)
const flags = Object.entries(copyTarget)
.filter(([key, value]) => ['renamed', 'transformed'].includes(key) && value)
.map(([key]) => key.charAt(0).toUpperCase())

if (transformed) {
await fs.outputFile(dest, contents, restPluginOptions)
} else if (!copySync) {
await fs.copy(src, dest, restPluginOptions)
} else {
fs.copySync(src, dest, restPluginOptions)
if (flags.length) {
message = `${message} ${yellow(`[${flags.join(', ')}]`)}`
}

if (verbose) {
let message = green(` ${bold(src)} → ${bold(dest)}`)
const flags = Object.entries(copyTarget)
.filter(([key, value]) => ['renamed', 'transformed'].includes(key) && value)
.map(([key]) => key.charAt(0).toUpperCase())
console.log(message)
}
}
} else if (verbose) {
console.log(yellow('no items to copy'))
}

copied = true
}

if (flags.length) {
message = `${message} ${yellow(`[${flags.join(', ')}]`)}`
}
return {
name: 'copy',
[hook]: async () => {
await processCopyTargets()
},
// overwrites the preceding `buildStart` function if `hook` parameter is `buildStart`
async buildStart() {
if (watchTargets) {
if (verbose) {
console.log(green('extra watch targets:'))
}

console.log(message)
if (Array.isArray(targets) && targets.length) {
for (const target of targets) {
const matchedPaths = await getMatchedPaths(target, restPluginOptions)

if (matchedPaths.length) {
for (const matchedPath of matchedPaths) {
if (verbose) {
const message = green(` ${bold(matchedPath)}`)
console.log(message)
}
this.addWatchFile(matchedPath)
}
}
}
}
} else if (verbose) {
console.log(yellow('no items to copy'))
}

copied = true
if (hook === 'buildStart') {
await processCopyTargets()
}
}
}
}
52 changes: 52 additions & 0 deletions tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,4 +497,56 @@ describe('Options', () => {

expect(await fs.pathExists('dist/asset-1.js')).toBe(false)
})

/* eslint-disable no-console */
test('Watch target files', async () => {
console.log = jest.fn()

const watcher = watch({
input: 'src/index.js',
output: {
dir: 'build',
format: 'esm'
},
plugins: [
copy({
targets: [{
src: 'src/assets/css/css-*.css',
dest: 'dist'
}],
verbose: true,
watchTargets: true
})
]
})

await sleep(1000)

expect(await fs.pathExists('dist/css-1.css')).toBe(true)
expect(await readFile('src/assets/css/css-1.css')).toEqual(expect.stringContaining('blue'))
expect(await readFile('dist/css-1.css')).toEqual(expect.stringContaining('blue'))
expect(await fs.pathExists('dist/css-2.css')).toBe(true)

await replace({
files: 'src/assets/css/css-1.css',
from: 'blue',
to: 'red'
})

await sleep(1000)

expect(await readFile('dist/css-1.css')).toEqual(expect.stringContaining('red'))

watcher.close()

await replace({
files: 'src/assets/css/css-1.css',
from: 'red',
to: 'blue'
})

expect(console.log).toHaveBeenCalledWith(green('extra watch targets:'))
expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/css/css-1.css')}`)}`)
expect(console.log).toHaveBeenCalledWith(`${green(` ${bold('src/assets/css/css-2.css')}`)}`)
})
})