From eaac1b7548473749c6a6c20b0a6154b37cd531b5 Mon Sep 17 00:00:00 2001 From: lemon-clown Date: Mon, 27 Apr 2020 19:39:22 +0800 Subject: [PATCH 1/4] Add comment to the codes --- src/index.js | 60 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/index.js b/src/index.js index a720841..f31dcf1 100644 --- a/src/index.js +++ b/src/index.js @@ -6,25 +6,39 @@ import isObject from 'is-plain-object' import globby from 'globby' import { bold, green, yellow } from 'colorette' + function stringify(value) { return util.inspect(value, { breakLength: Infinity }) } + async function isFile(filePath) { const fileStats = await fs.stat(filePath) - return fileStats.isFile() } -function renameTarget(target, rename) { - const parsedPath = path.parse(target) - return typeof rename === 'string' - ? rename - : rename(parsedPath.name, parsedPath.ext.replace('.', '')) +/** + * @param {string} targetFilePath + * @param {string|(fileName: string, fileExt: string): string} rename + */ + +function renameTarget(targetFilePath, rename) { + const parsedPath = path.parse(targetFilePath) + if (typeof rename === 'string') return rename + return rename(parsedPath.name, parsedPath.ext.replace(/^(\.)?/, '')) } -async function generateCopyTarget(src, dest, { flatten, rename, transform }) { + +/** + * @param {string} src + * @param {string} dest + * @param {boolean} options.flatten + * @param {string|(fileName: string, fileExt: string): string} options.rename + * @param {(content: string|ArrayBuffer): string|ArrayBuffer} options.transform + */ +async function generateCopyTarget(src, dest, options) { + const { flatten, rename, transform } = options if (transform && !await isFile(src)) { throw new Error(`"transform" option works only on files: '${src}' must be a file`) } @@ -43,18 +57,34 @@ async function generateCopyTarget(src, dest, { flatten, rename, transform }) { } } + export default function copy(options = {}) { const { copyOnce = false, flatten = true, hook = 'buildEnd', targets = [], - verbose = false, + verbose: shouldBeVerbose = false, ...restPluginOptions } = options let copied = false + const log = { + /** + * print verbose messages + * @param {string|() => string} message + */ + verbose(message) { + if (!shouldBeVerbose) return + if (typeof message === 'function') { + // eslint-disable-next-line no-param-reassign + message = message() + } + console.log(message) + } + } + return { name: 'copy', [hook]: async () => { @@ -104,9 +134,7 @@ export default function copy(options = {}) { } if (copyTargets.length) { - if (verbose) { - console.log(green('copied:')) - } + log.verbose(green('copied:')) for (const copyTarget of copyTargets) { const { contents, dest, src, transformed } = copyTarget @@ -117,7 +145,7 @@ export default function copy(options = {}) { await fs.copy(src, dest, restPluginOptions) } - if (verbose) { + log.verbose(() => { let message = green(` ${bold(src)} → ${bold(dest)}`) const flags = Object.entries(copyTarget) .filter(([key, value]) => ['renamed', 'transformed'].includes(key) && value) @@ -127,11 +155,11 @@ export default function copy(options = {}) { message = `${message} ${yellow(`[${flags.join(', ')}]`)}` } - console.log(message) - } + return message + }) } - } else if (verbose) { - console.log(yellow('no items to copy')) + } else { + log.verbose(yellow('no items to copy')) } copied = true From b0c540cabf0a423ad7c9829297f47491abbaea2c Mon Sep 17 00:00:00 2001 From: lemon-clown Date: Mon, 27 Apr 2020 19:50:28 +0800 Subject: [PATCH 2/4] Pass additional filePath info into transform func & update README & update typescript types --- .eslintrc.js | 3 ++ index.d.ts | 116 +++++++++++++++++++++++++++------------------------ readme.md | 7 +++- src/index.js | 21 +++++++--- 4 files changed, 85 insertions(+), 62 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 3268c29..d7b488a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,9 @@ module.exports = { 'object-curly-newline': ['error', { multiline: true, consistent: true }], semi: ['error', 'never'] }, + ignorePatterns: [ + 'index.d.ts' + ], env: { jest: true } diff --git a/index.d.ts b/index.d.ts index eaff965..b35b765 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,62 +1,68 @@ -import rollup from 'rollup'; -import fs from 'fs-extra'; -import globby from 'globby'; - -interface Target extends globby.GlobbyOptions { - /** - * Path or glob of what to copy. - */ - readonly src: string | readonly string[]; - - /** - * One or more destinations where to copy. - */ - readonly dest: string | readonly string[]; - - /** - * Change destination file or folder name. - */ - readonly rename?: string | Function; - - /** - * Modify file contents. - */ - readonly transform?: Function; +import rollup from 'rollup' +import fs from 'fs-extra' +import globby from 'globby' + + +export interface Target extends globby.GlobbyOptions { + /** + * Path or glob of what to copy. + */ + readonly src: string | readonly string[] + + /** + * One or more destinations where to copy. + */ + readonly dest: string | readonly string[] + + /** + * Change destination file or folder name. + */ + readonly rename?: string | ((fileName: string, fileExt: string) => string) + /** + * Modify file contents. + */ + readonly transform?: ( + content: string | ArrayBuffer, + srcPath: string, + destPath: string + ) => string | ArrayBuffer } -interface CopyOptions extends globby.GlobbyOptions, fs.CopyOptions { - /** - * Copy items once. Useful in watch mode. - * @default false - */ - readonly copyOnce?: boolean; - - /** - * Remove the directory structure of copied files. - * @default true - */ - readonly flatten?: boolean; - - /** - * Rollup hook the plugin should use. - * @default 'buildEnd' - */ - readonly hook?: string; - - /** - * Array of targets to copy. - * @default [] - */ - readonly targets?: readonly Target[]; - - /** - * Output copied items to console. - * @default false - */ - readonly verbose?: boolean; + +export interface CopyOptions extends globby.GlobbyOptions, fs.CopyOptions { + /** + * Copy items once. Useful in watch mode. + * @default false + */ + readonly copyOnce?: boolean + + /** + * Remove the directory structure of copied files. + * @default true + */ + readonly flatten?: boolean + + /** + * Rollup hook the plugin should use. + * @default 'buildEnd' + */ + readonly hook?: string + + /** + * Array of targets to copy. + * @default [] + */ + readonly targets?: readonly Target[] + + /** + * Output copied items to console. + * @default false + */ + readonly verbose?: boolean } + /** * Copy files and folders using Rollup */ -export default function copy(options?: CopyOptions): rollup.Plugin; +export default function copy(options?: CopyOptions): rollup.Plugin diff --git a/readme.md b/readme.md index 9bf965b..a7d5acd 100644 --- a/readme.md +++ b/readme.md @@ -142,7 +142,12 @@ copy({ targets: [{ src: 'src/index.html', dest: 'dist/public', - transform: (contents) => contents.toString().replace('__SCRIPT__', 'app.js') + transform: (contents, srcPath, destPath) => ( + contents.toString() + .replace('__SCRIPT__', 'app.js') + .replace('__SOURCE_FILE_PATH__', srcPath) + .replace('__TARGET_FILE_NAME__', path.basename(srcPath)) + ) }] }) ``` diff --git a/src/index.js b/src/index.js index f31dcf1..d66ee05 100644 --- a/src/index.js +++ b/src/index.js @@ -34,8 +34,12 @@ function renameTarget(targetFilePath, rename) { * @param {string} src * @param {string} dest * @param {boolean} options.flatten - * @param {string|(fileName: string, fileExt: string): string} options.rename - * @param {(content: string|ArrayBuffer): string|ArrayBuffer} options.transform + * @param {string|((fileName: string, fileExt: string) => string)} options.rename + * @param {( + * content: string|ArrayBuffer, + * srcPath: string, + * destPath: string + * ): string|ArrayBuffer} options.transform */ async function generateCopyTarget(src, dest, options) { const { flatten, rename, transform } = options @@ -48,13 +52,18 @@ async function generateCopyTarget(src, dest, options) { ? dest : dir.replace(dir.split('/')[0], dest) - return { + const result = { src, dest: path.join(destinationFolder, rename ? renameTarget(base, rename) : base), - ...(transform && { contents: await transform(await fs.readFile(src)) }), - renamed: rename, - transformed: transform + renamed: Boolean(rename), + transformed: false + } + + if (transform) { + result.contents = await transform(await fs.readFile(src), src, dest) + result.transformed = true } + return result } From a19ede99ba0286aaba7834e226b1f8fa636d7777 Mon Sep 17 00:00:00 2001 From: lemon-clown Date: Mon, 27 Apr 2020 20:04:56 +0800 Subject: [PATCH 3/4] Add watchStart feature, trigger recopy when files changed --- index.d.ts | 8 +++ readme.md | 15 +++++ src/index.js | 167 +++++++++++++++++++++++++++++++-------------------- 3 files changed, 125 insertions(+), 65 deletions(-) diff --git a/index.d.ts b/index.d.ts index b35b765..092e946 100644 --- a/index.d.ts +++ b/index.d.ts @@ -48,6 +48,14 @@ export interface CopyOptions extends globby.GlobbyOptions, fs.CopyOptions { */ readonly hook?: string + /** + * Rollup hook the `this.addWatchFile` should call, only be used in hooks + * during the build phase, and must be processed earlier than hook + * @default 'buildStart' + * @see https://rollupjs.org/guide/en/#thisaddwatchfileid-string--void + */ + readonly watchStart?: 'buildStart' | 'load' | 'resolveId' | 'transform' | string + /** * Array of targets to copy. * @default [] diff --git a/readme.md b/readme.md index a7d5acd..7928f62 100644 --- a/readme.md +++ b/readme.md @@ -178,6 +178,21 @@ copy({ }) ``` +#### watchHook + +Type: `string` | Default: `buildStart` + +[Rollup hook](https://rollupjs.org/guide/en/#hooks) the [this.addWatchFile](https://rollupjs.org/guide/en/#thisaddwatchfileid-string--void) should call. By default, `addWatchFile` called on each rollup.rollup build. +Only be used in hooks during the build phase, i.e. in `buildStart`, `load`, `resolveId`, and `transform`. + +```js +copy({ + targets: [{ src: 'assets/*', dest: 'dist/public' }], + watchHook: 'resolveId' +}) +``` + + #### copyOnce Type: `boolean` | Default: `false` diff --git a/src/index.js b/src/index.js index d66ee05..6c26750 100644 --- a/src/index.js +++ b/src/index.js @@ -72,106 +72,143 @@ export default function copy(options = {}) { copyOnce = false, flatten = true, hook = 'buildEnd', + watchHook = 'buildStart', targets = [], verbose: shouldBeVerbose = false, ...restPluginOptions } = options - let copied = false - const log = { /** * print verbose messages - * @param {string|() => string} message + * @param {string|() => string} message */ verbose(message) { if (!shouldBeVerbose) return if (typeof message === 'function') { - // eslint-disable-next-line no-param-reassign + // eslint-disable-next-line no-param-reassign message = message() } console.log(message) } } - return { - name: 'copy', - [hook]: async () => { - if (copyOnce && copied) { - return - } + let copied = false + let copyTargets = [] - const copyTargets = [] + async function collectAndWatchingTargets() { + const self = this + if (copyOnce && copied) { + return + } - if (Array.isArray(targets) && targets.length) { - for (const target of targets) { - if (!isObject(target)) { - throw new Error(`${stringify(target)} target must be an object`) - } + // Recollect copyTargets + 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`) + } - const { dest, rename, src, transform, ...restTargetOptions } = target + const { dest, rename, src, transform, ...restTargetOptions } = target - if (!src || !dest) { - throw new Error(`${stringify(target)} target must have "src" and "dest" properties`) - } + 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`) - } + 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) - } + const matchedPaths = await globby(src, { + expandDirectories: false, + onlyFiles: false, + ...restPluginOptions, + ...restTargetOptions + }) + + if (matchedPaths.length) { + for (const matchedPath of matchedPaths) { + const destinations = Array.isArray(dest) ? dest : [dest] + const generatedCopyTargets = await Promise.all( + destinations.map((destination) => generateCopyTarget( + matchedPath, + destination, + { flatten, rename, transform } + )) + ) + copyTargets.push(...generatedCopyTargets) } } } + } - if (copyTargets.length) { - log.verbose(green('copied:')) - - for (const copyTarget of copyTargets) { - const { contents, dest, src, transformed } = copyTarget + /** + * Watching source files + */ + for (const target of copyTargets) { + const srcPath = path.resolve(target.src) + self.addWatchFile(srcPath) + } + } - if (transformed) { - await fs.outputFile(dest, contents, restPluginOptions) - } else { - await fs.copy(src, dest, restPluginOptions) - } + /** + * Do copy operation + */ + async function handleCopy() { + if (copyOnce && copied) { + return + } - log.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 (copyTargets.length) { + log.verbose(green('copied:')) - if (flags.length) { - message = `${message} ${yellow(`[${flags.join(', ')}]`)}` - } + for (const copyTarget of copyTargets) { + const { contents, dest, src, transformed } = copyTarget - return message - }) + if (transformed) { + await fs.outputFile(dest, contents, restPluginOptions) + } else { + await fs.copy(src, dest, restPluginOptions) } - } else { - log.verbose(yellow('no items to copy')) + + log.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 (flags.length) { + message = `${message} ${yellow(`[${flags.join(', ')}]`)}` + } + + return message + }) } + } else { + log.verbose(yellow('no items to copy')) + } + + copied = true + } - copied = true + const plugin = { + name: 'copy', + async [watchHook](...args) { + const self = this + await collectAndWatchingTargets.call(self, ...args) + + /** + * Merge handleCopy and collectAndWatchingTargets + */ + if (hook === watchHook) { + await handleCopy.call(self, ...args) + } } } + + if (hook !== watchHook) { + plugin[hook] = handleCopy + } + return plugin } From 7c258e37c436deb37a9345d7b160d220cfb1cc6c Mon Sep 17 00:00:00 2001 From: lemon-clown Date: Thu, 30 Apr 2020 17:52:42 +0800 Subject: [PATCH 4/4] Fix wrong destFilePath passed into optiions.transform --- src/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 6c26750..6f0c463 100644 --- a/src/index.js +++ b/src/index.js @@ -52,15 +52,16 @@ async function generateCopyTarget(src, dest, options) { ? dest : dir.replace(dir.split('/')[0], dest) + const destFilePath = path.join(destinationFolder, rename ? renameTarget(base, rename) : base) const result = { src, - dest: path.join(destinationFolder, rename ? renameTarget(base, rename) : base), + dest: destFilePath, renamed: Boolean(rename), transformed: false } if (transform) { - result.contents = await transform(await fs.readFile(src), src, dest) + result.contents = await transform(await fs.readFile(src), src, destFilePath) result.transformed = true } return result