Skip to content

folder-clone ​

Copies every file of the stream into one or more extra folders, relative to file.base, so a single dest writes them all. The originals are kept unless you turn them off, and a hook can change, replace or skip each copy. It replaces gulp-clone and the second dest that goes with it.

sh
npm install --save-dev @zilero/gulp-folder-clone

Usage ​

ts
import { folderClone } from '@zilero/gulp-folder-clone';
import { dest, src } from 'gulp';
import { pipeline } from 'node:stream/promises';

// dist/images/logo.png, dist/backup/images/logo.png and dist/v2/images/logo.png
export const images = (): Promise<void> =>
  pipeline(
    src('src/images/**', { base: 'src', encoding: false }),
    folderClone({ dirs: ['backup', 'v2'] }),
    dest('dist')
  );

onClone runs for every copy:

ts
folderClone({
  dirs: ['legacy', 'drafts'],
  onClone: ({ file, dir, original }) => {
    if (dir === 'drafts') {
      return null; // skip this copy
    }

    file.stem = `${file.stem}-${dir}`; // change it in place
  }
});

Options ​

OptionTypeDefaultDescription
dirsstring | string[]—One or more folders inside file.base that receive a copy of every file. Required.
keepOriginalbooleantrueEmit the original file as well as the copies.
onClone({ file, dir, original }) => Vinyl | null | void | Promise<Vinyl | null | void>—Runs for every copy, may be async: change it in place, return another file, or null to skip it.

onClone gets:

KeyDescription
fileThe copy, already moved into dir; change it in place.
dirThe folder this copy goes to, as given in dirs.
originalThe file the copy was made from.

Return nothing to keep the copy, null to skip it, or another Vinyl file to emit instead. The source map of the emitted file is pointed at its path after the hook, so a copy the hook renames or moves keeps a correct map.

Each folder in dirs must be a relative path inside file.base: an absolute path, '.' (the base itself), a path that leaves the base such as '../out', and an empty string are errors, and so is an empty dirs list (Add at least one folder). A folder listed twice, also written differently ('backup' and './backup/'), is an error (Each folder may be listed only once): both copies would be written to the same path. Unknown keys are an error too. Options are checked when the plugin is created.

The package also exports the types FolderCloneOptions, CloneHook and CloneInput.

Notes ​

  • A copy keeps the original's base and gets the path base/dir/relative: img/logo.png with dirs: 'backup' becomes backup/img/logo.png.
  • Copies are deep clones: each has its own contents and its own source map object, so changing a copy never touches the original or another copy.
  • With src(…, { sourcemaps: true }), each copy's sourceMap.file points at the copy's relative path, after onClone; the original's map is left as it was.
  • For each file the original comes first, then one copy per folder in the order of dirs. With keepOriginal: false and every copy skipped, nothing is emitted for that file.
  • Every file is cloned, including files without contents (read: false) and directories.
  • Read images and other binary files with src(…, { encoding: false }); gulp 5 reads files as UTF-8 by default.
  • An error thrown by onClone is emitted as a PluginError with the file's path.

Recipes ​

Mirror the build into dist/latest:

ts
pipeline(src('dist/**', { encoding: false }), folderClone({ dirs: 'latest' }), dest('dist'));

Publish into two release folders, without the original:

ts
folderClone({ dirs: ['releases/stable', 'releases/next'], keepOriginal: false });

Leave source maps out of one folder:

ts
folderClone({
  dirs: 'public',
  onClone: ({ file }) => (file.extname === '.map' ? null : undefined)
});

Migrating ​

From gulp-clone ​

gulp-clonefolder-clone
clone() / clone.sink(), a second branch with its own destfolderClone({ dirs }) and one dest
change the cloned files in the second branchonClone({ file, dir, original })
drop the originals by not writing the first branchkeepOriginal: false

From 1.x ​

  • 1.x only had an onBeforeCopy(file) hook and passed files through unchanged. 2.0 really copies each file into the folders in dirs.
  • keepOriginal (default true) is new.
  • onBeforeCopy is replaced by onClone({ file, dir, original }), which can change, replace or skip (null) each copy.

See migrating from 1.x for every package.

Released under the MIT License.