Skip to content
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

feat(copy): Add capability to transform copied files on the way #114

Open
wants to merge 1 commit into
base: main
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
37 changes: 37 additions & 0 deletions packages/esbuild-plugin-copy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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> | string | Buffer;
}

export interface Options {
Expand Down
63 changes: 39 additions & 24 deletions packages/esbuild-plugin-copy/src/lib/esbuild-plugin-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -113,7 +114,12 @@ export const copy = (options: Partial<Options> = {}): Plugin => {
globalWatchControl = false;
}

for (const { from, to, watch: localWatchControl } of formattedAssets) {
for (const {
from,
to,
watch: localWatchControl,
transform,
} of formattedAssets) {
const useWatchModeForCurrentAssetPair =
globalWatchControl || localWatchControl;

Expand All @@ -138,20 +144,26 @@ export const copy = (options: Partial<Options> = {}): Plugin => {
);
}

const executor = () => {
const executor = async () => {
const copyPromises: Promise<void>[] = [];

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';
};

Expand All @@ -163,7 +175,7 @@ export const copy = (options: Partial<Options> = {}): Plugin => {
verbose
);

executor();
await executor();

const watcher = chokidar.watch(from, {
disableGlobbing: false,
Expand All @@ -174,27 +186,30 @@ export const copy = (options: Partial<Options> = {}): Plugin => {
: {}),
});

watcher.on('change', (fromPath) => {
watcher.on('change', async (fromPath) => {
verboseLog(
`[File Changed] File ${chalk.white(
fromPath
)} changed, copy operation triggered.`,
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();
}
}
});
Expand Down
74 changes: 61 additions & 13 deletions packages/esbuild-plugin-copy/src/lib/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'fs-extra';
import chalk from 'chalk';

import { verboseLog } from './utils';
import { AssetPair } from './typings';

/**
*
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}
11 changes: 11 additions & 0 deletions packages/esbuild-plugin-copy/src/lib/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> | string | Buffer;
}

export interface Options {
Expand Down
3 changes: 2 additions & 1 deletion packages/esbuild-plugin-copy/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ export function verboseLog(msg: string, verbose: boolean, lineBefore = false) {
export function formatAssets(assets: MaybeArray<AssetPair>) {
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,
}));
}

Expand Down
39 changes: 34 additions & 5 deletions packages/esbuild-plugin-copy/tests/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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(
Expand All @@ -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 () => {
Expand Down