diff --git a/packages/esbuild-plugin-copy/README.md b/packages/esbuild-plugin-copy/README.md index 12f645d..01d6c6d 100644 --- a/packages/esbuild-plugin-copy/README.md +++ b/packages/esbuild-plugin-copy/README.md @@ -12,6 +12,7 @@ ESBuild plugin for assets copy. - Control assets destination path freely - Support verbose output log - Run only once or only when assets changed +- Transform files along the way ## Usage @@ -164,6 +165,31 @@ Watching Mode of this plugin is implemented using polling for being consistent w })(); ``` +## Transform + +You can provide a transform function to make changes to files along the way. The function will be passed the absolute path where the file is being copied from as well as the original content of the file as a `Buffer`. The transform function should return the final content of the file as a string or buffer. + +```typescript +const res = await build({ + plugins: [ + copy({ + assets: [ + { + from: 'src/**/*', + to: 'dist', + transform: (fromPath, content) => { + if (fromPath.endsWith('.js')) { + content = `"use string"\n${content}`; + } + return content; + }, + }, + ], + }), + ], +}); +``` + ## Configurations ```typescript @@ -191,6 +217,17 @@ export interface AssetPair { * @default false */ watch?: boolean | WatchOptions; + + /** + * transforms files before copying them to the destination path + * `from` is he resolved source path of the current file + * + * @default false + */ + transform?: ( + from: string, + content: Buffer + ) => Promise | string | Buffer; } export interface Options { diff --git a/packages/esbuild-plugin-copy/src/lib/esbuild-plugin-copy.ts b/packages/esbuild-plugin-copy/src/lib/esbuild-plugin-copy.ts index 53384cf..e4d0bd5 100644 --- a/packages/esbuild-plugin-copy/src/lib/esbuild-plugin-copy.ts +++ b/packages/esbuild-plugin-copy/src/lib/esbuild-plugin-copy.ts @@ -2,6 +2,7 @@ import path from 'path'; import chalk from 'chalk'; import globby from 'globby'; import chokidar from 'chokidar'; +import fs from 'fs'; import { copyOperationHandler } from './handler'; @@ -113,7 +114,12 @@ export const copy = (options: Partial = {}): Plugin => { globalWatchControl = false; } - for (const { from, to, watch: localWatchControl } of formattedAssets) { + for (const { + from, + to, + watch: localWatchControl, + transform, + } of formattedAssets) { const useWatchModeForCurrentAssetPair = globalWatchControl || localWatchControl; @@ -138,20 +144,26 @@ export const copy = (options: Partial = {}): Plugin => { ); } - const executor = () => { + const executor = async () => { + const copyPromises: Promise[] = []; + for (const fromPath of deduplicatedPaths) { - to.forEach((toPath) => { - copyOperationHandler( - outDirResolveFrom, - from, - fromPath, - toPath, - verbose, - dryRun + for (const toPath of to) { + copyPromises.push( + copyOperationHandler( + outDirResolveFrom, + from, + fromPath, + toPath, + verbose, + dryRun, + transform + ) ); - }); + } } + await Promise.all(copyPromises); process.env[PLUGIN_EXECUTED_FLAG] = 'true'; }; @@ -163,7 +175,7 @@ export const copy = (options: Partial = {}): Plugin => { verbose ); - executor(); + await executor(); const watcher = chokidar.watch(from, { disableGlobbing: false, @@ -174,7 +186,7 @@ export const copy = (options: Partial = {}): Plugin => { : {}), }); - watcher.on('change', (fromPath) => { + watcher.on('change', async (fromPath) => { verboseLog( `[File Changed] File ${chalk.white( fromPath @@ -182,19 +194,22 @@ export const copy = (options: Partial = {}): Plugin => { verbose ); - to.forEach((toPath) => { - copyOperationHandler( - outDirResolveFrom, - from, - fromPath, - toPath, - verbose, - dryRun - ); - }); + await Promise.all( + to.map((toPath) => + copyOperationHandler( + outDirResolveFrom, + from, + fromPath, + toPath, + verbose, + dryRun, + transform + ) + ) + ); }); } else { - executor(); + await executor(); } } }); diff --git a/packages/esbuild-plugin-copy/src/lib/handler.ts b/packages/esbuild-plugin-copy/src/lib/handler.ts index 53ede14..c8ba28b 100644 --- a/packages/esbuild-plugin-copy/src/lib/handler.ts +++ b/packages/esbuild-plugin-copy/src/lib/handler.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import { verboseLog } from './utils'; +import { AssetPair } from './typings'; /** * @@ -11,19 +12,16 @@ import { verboseLog } from './utils'; * @param globbedFromPath the globbed file from path, which are globbed from rawFromPath * @param baseToPath the original asset.to value from user config, which will be resolved with outDirResolveFrom option * @param verbose verbose logging - * @param dryRun dry run mode * @returns */ -export function copyOperationHandler( +function resolvePaths( outDirResolveFrom: string, rawFromPath: string[], globbedFromPath: string, baseToPath: string, - - verbose = false, - dryRun = false -) { - for (const rawFrom of rawFromPath) { + verbose = false +): [src: string, dest: string][] { + return rawFromPath.map((rawFrom) => { // only support from dir like: /**/*(.ext) const { dir } = path.parse(rawFrom); @@ -73,15 +71,65 @@ export function copyOperationHandler( baseToPath ); - dryRun ? void 0 : fs.ensureDirSync(path.dirname(composedDistDirPath)); + return [sourcePath, composedDistDirPath]; + }); +} - dryRun ? void 0 : fs.copyFileSync(sourcePath, composedDistDirPath); +/** + * + * @param outDirResolveFrom the base destination dir that will resolve with asset.to value + * @param rawFromPath the original asset.from value from user config + * @param globbedFromPath the globbed file from path, which are globbed from rawFromPath + * @param baseToPath the original asset.to value from user config, which will be resolved with outDirResolveFrom option + * @param verbose verbose logging + * @param dryRun dry run mode + * @param transform middleman transform function + * @returns + */ +export async function copyOperationHandler( + outDirResolveFrom: string, + rawFromPath: string[], + globbedFromPath: string, + baseToPath: string, + + verbose = false, + dryRun = false, + transform: AssetPair['transform'] = null +) { + const resolvedPaths = resolvePaths( + outDirResolveFrom, + rawFromPath, + globbedFromPath, + baseToPath, + verbose + ); + + const copyPromises = resolvedPaths.map(async ([src, dest]) => { + if (dryRun) { + verboseLog( + `${chalk.white('[DryRun] ')}File copied: ${chalk.white( + src + )} -> ${chalk.white(dest)}`, + verbose + ); + return; + } + + await fs.ensureDir(path.dirname(dest)); + + if (transform) { + const sourceContent = await fs.readFile(src); + const finalContent = await transform(src, sourceContent); + await fs.writeFile(dest, finalContent); + } else { + await fs.copyFile(src, dest); + } verboseLog( - `${dryRun ? chalk.white('[DryRun] ') : ''}File copied: ${chalk.white( - sourcePath - )} -> ${chalk.white(composedDistDirPath)}`, + `File copied: ${chalk.white(src)} -> ${chalk.white(dest)}`, verbose ); - } + }); + + await Promise.all(copyPromises); } diff --git a/packages/esbuild-plugin-copy/src/lib/typings.ts b/packages/esbuild-plugin-copy/src/lib/typings.ts index c5f4640..de8c9ee 100644 --- a/packages/esbuild-plugin-copy/src/lib/typings.ts +++ b/packages/esbuild-plugin-copy/src/lib/typings.ts @@ -22,6 +22,17 @@ export interface AssetPair { * @default false */ watch?: boolean | WatchOptions; + + /** + * transforms files before copying them to the destination path + * `from` is he resolved source path of the current file + * + * @default false + */ + transform?: ( + from: string, + content: Buffer + ) => Promise | string | Buffer; } export interface Options { diff --git a/packages/esbuild-plugin-copy/src/lib/utils.ts b/packages/esbuild-plugin-copy/src/lib/utils.ts index d383b80..80c82b4 100644 --- a/packages/esbuild-plugin-copy/src/lib/utils.ts +++ b/packages/esbuild-plugin-copy/src/lib/utils.ts @@ -15,10 +15,11 @@ export function verboseLog(msg: string, verbose: boolean, lineBefore = false) { export function formatAssets(assets: MaybeArray) { return ensureArray(assets) .filter((asset) => asset.from && asset.to) - .map(({ from, to, watch }) => ({ + .map(({ from, to, watch, transform }) => ({ from: ensureArray(from), to: ensureArray(to), watch: watch ?? false, + transform, })); } diff --git a/packages/esbuild-plugin-copy/tests/plugin.spec.ts b/packages/esbuild-plugin-copy/tests/plugin.spec.ts index 8fe9cbb..3dcacd3 100644 --- a/packages/esbuild-plugin-copy/tests/plugin.spec.ts +++ b/packages/esbuild-plugin-copy/tests/plugin.spec.ts @@ -11,7 +11,7 @@ import { PLUGIN_EXECUTED_FLAG, } from '../src/lib/utils'; -import type { Options } from '../src/lib/typings'; +import type { AssetPair, Options } from '../src/lib/typings'; const FixtureDir = path.resolve(__dirname, './fixtures'); @@ -46,7 +46,7 @@ describe('CopyPlugin:Core', async () => { afterEach(() => { delete process.env[PLUGIN_EXECUTED_FLAG]; }); - it('should works for from path: /**<1>', async () => { + it('should work for from path: /**<1>', async () => { const outDir = tmp.dirSync().name; const outAssetsDir = tmp.dirSync().name; @@ -78,7 +78,7 @@ describe('CopyPlugin:Core', async () => { expect(d3).toEqual(['deep.txt']); }); - it('should works for from path: /**<2>', async () => { + it('should work for from path: /**<2>', async () => { const outDir = tmp.dirSync().name; const outAssetsDir = tmp.dirSync().name; @@ -105,7 +105,7 @@ describe('CopyPlugin:Core', async () => { expect(d2).toEqual(['content.js']); }); - it('should works for from path: /**<3>', async () => { + it('should work for from path: /**<3>', async () => { const outDir = tmp.dirSync().name; const outAssetsDir = tmp.dirSync().name; @@ -372,7 +372,8 @@ describe('CopyPlugin:Core', async () => { expect(d1).toEqual(['hello.txt', 'index.js']); }); - it.only('should copy from file to file with nested dest dir', async () => { + + it('should copy from file to file with nested dest dir', async () => { const outDir = tmp.dirSync().name; await builder( @@ -393,6 +394,34 @@ describe('CopyPlugin:Core', async () => { expect(d1).toEqual(['hello.txt']); }); + + it('should transform file when provided a transform function', async () => { + const outDir = tmp.dirSync().name; + + const suffix = 'wondeful'; + + const transform: AssetPair['transform'] = (_, content) => { + return content.toString() + suffix; + }; + + await builder( + outDir, + { outdir: outDir }, + { + assets: { + from: path.resolve(__dirname, './fixtures/assets/note.txt'), + to: 'hello.txt', + transform, + }, + resolveFrom: outDir, + verbose: false, + dryRun: false, + } + ); + + const result = fs.readFileSync(path.join(outDir, 'hello.txt'), 'utf8'); + expect(result.endsWith(suffix)).to.be.true; + }); }); describe('CopyPlugin:Utils', async () => {