-
Notifications
You must be signed in to change notification settings - Fork 58
Watch target files with Chokidar #30
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
base: master
Are you sure you want to change the base?
Changes from all commits
21ef49e
53e7c6f
709edab
89a6b88
cdfda05
1755733
d416b01
4d6431c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ import fs from 'fs-extra' | |
import isObject from 'is-plain-object' | ||
import globby from 'globby' | ||
import { bold, green, yellow } from 'colorette' | ||
import chokidar from 'chokidar' | ||
|
||
function stringify(value) { | ||
return util.inspect(value, { breakLength: Infinity }) | ||
|
@@ -27,6 +28,63 @@ function generateCopyTarget(src, dest, rename) { | |
} | ||
} | ||
|
||
function generateCopyTargets(src, dest, rename) { | ||
return Array.isArray(dest) | ||
? dest.map(destination => generateCopyTarget(src, destination, rename)) | ||
: [generateCopyTarget(src, dest, rename)] | ||
} | ||
|
||
async function copyFiles(copyTargets, verbose, copyOptions) { | ||
if (Array.isArray(copyTargets) && copyTargets.length) { | ||
if (verbose) { | ||
console.log(green('copied:')) | ||
} | ||
|
||
for (const { src, dest } of copyTargets) { | ||
await fs.copy(src, dest, copyOptions) | ||
|
||
if (verbose) { | ||
console.log(green(` ${bold(src)} → ${bold(dest)}`)) | ||
} | ||
} | ||
} else if (verbose) { | ||
console.log(yellow('no items to copy')) | ||
} | ||
} | ||
|
||
function watchFiles(targets, verbose, copyOptions) { | ||
return targets.map(({ src, dest, rename }) => { | ||
async function onChange(matchedPath) { | ||
const copyTargets = generateCopyTargets(matchedPath, dest, rename) | ||
await copyFiles(copyTargets, verbose, copyOptions) | ||
} | ||
|
||
return chokidar.watch(src, { ignoreInitial: true }) | ||
vladshcherbin marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately, we can't pass
This package uses globby inside for glob support. Globby uses fast-glob inside, which uses micromatch:
So, chokidar v3 misses micromatch package which adds support for some glob features on top of picomatch. Globs will have different features for copy and watch. Chokidar used micromatch in v2, but switched in v3. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well that's unfortunate... I'll think about it a bit and do some research into options. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've opened an issue in chokidar paulmillr/chokidar#956, maybe only braces extension is missing and we can live with that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems to be compatible. I'll add few tests this week and hopefully release a new version after merging this. |
||
.on('change', onChange) | ||
.on('add', onChange) | ||
}) | ||
} | ||
|
||
function verifyTargets(targets) { | ||
if (Array.isArray(targets) && targets.length) { | ||
for (const target of targets) { | ||
if (!isObject(target)) { | ||
throw new Error(`${stringify(target)} target must be an object`) | ||
} | ||
|
||
const { src, dest, rename } = 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`) | ||
} | ||
} | ||
} | ||
} | ||
|
||
export default function copy(options = {}) { | ||
const { | ||
copyOnce = false, | ||
|
@@ -37,6 +95,9 @@ export default function copy(options = {}) { | |
} = options | ||
|
||
let copied = false | ||
let watchers = [] | ||
|
||
verifyTargets(targets) | ||
|
||
return { | ||
name: 'copy', | ||
|
@@ -49,20 +110,8 @@ export default function copy(options = {}) { | |
|
||
if (Array.isArray(targets) && targets.length) { | ||
for (const target of targets) { | ||
if (!isObject(target)) { | ||
throw new Error(`${stringify(target)} target must be an object`) | ||
} | ||
|
||
const { src, dest, rename, ...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, | ||
|
@@ -72,33 +121,22 @@ export default function copy(options = {}) { | |
|
||
if (matchedPaths.length) { | ||
matchedPaths.forEach((matchedPath) => { | ||
const generatedCopyTargets = Array.isArray(dest) | ||
? dest.map(destination => generateCopyTarget(matchedPath, destination, rename)) | ||
: [generateCopyTarget(matchedPath, dest, rename)] | ||
|
||
copyTargets.push(...generatedCopyTargets) | ||
copyTargets.push(...generateCopyTargets(matchedPath, dest, rename)) | ||
}) | ||
} | ||
} | ||
} | ||
|
||
if (copyTargets.length) { | ||
if (verbose) { | ||
console.log(green('copied:')) | ||
} | ||
|
||
for (const { src, dest } of copyTargets) { | ||
await fs.copy(src, dest, restPluginOptions) | ||
await copyFiles(copyTargets, verbose, restPluginOptions) | ||
|
||
if (verbose) { | ||
console.log(green(` ${bold(src)} → ${bold(dest)}`)) | ||
} | ||
} | ||
} else if (verbose) { | ||
console.log(yellow('no items to copy')) | ||
if (!copied && !copyOnce && process.env.ROLLUP_WATCH === 'true') { | ||
watchers = watchFiles(targets, verbose, restPluginOptions) | ||
} | ||
|
||
copied = true | ||
}, | ||
_closeWatchers: async () => { // For unit tests | ||
await Promise.all(watchers.map(watcher => watcher.close())) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ import { rollup, watch } from 'rollup' | |
import fs from 'fs-extra' | ||
import replace from 'replace-in-file' | ||
import { bold, yellow, green } from 'colorette' | ||
import { join } from 'path' | ||
import copy from '../src' | ||
|
||
process.chdir(`${__dirname}/fixtures`) | ||
|
@@ -16,12 +17,14 @@ afterEach(async () => { | |
}) | ||
|
||
async function build(options) { | ||
const copyPlugin = copy(options) | ||
await rollup({ | ||
input: 'src/index.js', | ||
plugins: [ | ||
copy(options) | ||
copyPlugin | ||
] | ||
}) | ||
return copyPlugin | ||
} | ||
|
||
describe('Copy', () => { | ||
|
@@ -230,6 +233,137 @@ describe('Copy', () => { | |
}) | ||
}) | ||
|
||
describe('Watching', () => { | ||
test('Does not watch target files when watch mode disabled', async () => { | ||
await build({ | ||
targets: [ | ||
{ src: 'src/assets/asset-1.js', dest: 'dist' } | ||
] | ||
}) | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(true) | ||
await fs.remove('dist') | ||
expect(await fs.pathExists('dist/asset-1.js')).toBe(false) | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'asset1', | ||
to: 'assetX' | ||
}) | ||
|
||
await sleep(1000) | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(false) | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'assetX', | ||
to: 'asset1' | ||
}) | ||
}) | ||
|
||
test('Does not watch target files when watch mode and copyOnce enabled', async () => { | ||
process.env.ROLLUP_WATCH = 'true' | ||
await build({ | ||
targets: [ | ||
{ src: 'src/assets/asset-1.js', dest: 'dist' } | ||
], | ||
copyOnce: true | ||
}) | ||
delete process.env.ROLLUP_WATCH | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(true) | ||
await fs.remove('dist') | ||
expect(await fs.pathExists('dist/asset-1.js')).toBe(false) | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'asset1', | ||
to: 'assetX' | ||
}) | ||
|
||
await sleep(1000) | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(false) | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'assetX', | ||
to: 'asset1' | ||
}) | ||
}) | ||
|
||
test('Watches target files when watch mode enabled', async () => { | ||
process.env.ROLLUP_WATCH = 'true' | ||
const copyPlugin = await build({ | ||
targets: [ | ||
{ src: 'src/assets/asset-1.js', dest: 'dist' } | ||
] | ||
}) | ||
delete process.env.ROLLUP_WATCH | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(true) | ||
await fs.remove('dist') | ||
expect(await fs.pathExists('dist/asset-1.js')).toBe(false) | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'asset1', | ||
to: 'assetX' | ||
}) | ||
|
||
await sleep(1000) | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(true) | ||
|
||
// eslint-disable-next-line no-underscore-dangle | ||
await copyPlugin._closeWatchers() | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'assetX', | ||
to: 'asset1' | ||
}) | ||
}) | ||
|
||
test('Watches and copies multiple targets from same file', async () => { | ||
process.env.ROLLUP_WATCH = 'true' | ||
const copyPlugin = await build({ | ||
targets: [ | ||
{ src: 'src/assets/asset-1.js', dest: 'dist' }, | ||
{ src: 'src/assets/asset-1.js', dest: 'dist/2' } | ||
] | ||
}) | ||
delete process.env.ROLLUP_WATCH | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(true) | ||
expect(await fs.pathExists('dist/2/asset-1.js')).toBe(true) | ||
await fs.remove('dist') | ||
expect(await fs.pathExists('dist/asset-1.js')).toBe(false) | ||
expect(await fs.pathExists('dist/2/asset-1.js')).toBe(false) | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'asset1', | ||
to: 'assetX' | ||
}) | ||
|
||
await sleep(1000) | ||
|
||
expect(await fs.pathExists('dist/asset-1.js')).toBe(true) | ||
expect(await fs.pathExists('dist/2/asset-1.js')).toBe(true) | ||
|
||
// eslint-disable-next-line no-underscore-dangle | ||
await copyPlugin._closeWatchers() | ||
|
||
await replace({ | ||
files: 'src/assets/asset-1.js', | ||
from: 'assetX', | ||
to: 'asset1' | ||
}) | ||
}) | ||
}) | ||
|
||
describe('Options', () => { | ||
/* eslint-disable no-console */ | ||
test('Verbose', async () => { | ||
|
@@ -251,16 +385,16 @@ describe('Options', () => { | |
expect(console.log).toHaveBeenCalledTimes(5) | ||
expect(console.log).toHaveBeenCalledWith(green('copied:')) | ||
expect(console.log).toHaveBeenCalledWith( | ||
green(` ${bold('src/assets/asset-1.js')} → ${bold('dist/asset-1.js')}`) | ||
green(` ${bold('src/assets/asset-1.js')} → ${bold(join('dist', 'asset-1.js'))}`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test was broken on Windows because it was getting 'dist\asset-1.js' instead of 'dist/asset-1.js'
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, so it outputs backslashes on windows 🤔 I'll try to resolve this in |
||
) | ||
expect(console.log).toHaveBeenCalledWith( | ||
green(` ${bold('src/assets/css/css-1.css')} → ${bold('dist/css-1.css')}`) | ||
green(` ${bold('src/assets/css/css-1.css')} → ${bold(join('dist', 'css-1.css'))}`) | ||
) | ||
expect(console.log).toHaveBeenCalledWith( | ||
green(` ${bold('src/assets/css/css-2.css')} → ${bold('dist/css-2.css')}`) | ||
green(` ${bold('src/assets/css/css-2.css')} → ${bold(join('dist', 'css-2.css'))}`) | ||
) | ||
expect(console.log).toHaveBeenCalledWith( | ||
green(` ${bold('src/assets/scss')} → ${bold('dist/scss')}`) | ||
green(` ${bold('src/assets/scss')} → ${bold(join('dist', 'scss'))}`) | ||
) | ||
}) | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.