UNPKG

@rollup-extras/plugin-copy

Version:
273 lines (243 loc) 10.1 kB
import { constants } from 'node:fs'; import { copyFile, lstat, mkdir, readFile, readlink, stat, symlink } from 'node:fs/promises'; import { basename, dirname, join, relative, resolve } from 'node:path'; import globParent from 'glob-parent'; import { globFiles } from './glob.js'; import { createLogger, LogLevel } from '@niceties/logger'; import logger from '@rollup-extras/utils/logger'; import { getOptions } from '@rollup-extras/utils/options'; import statistics from '@rollup-extras/utils/statistics'; const factories = { targets, logger: ( (logger) ), }; const listFilenames = 'list-filenames'; export default function (options) { const files = new Map(); const normalizedOptions = getOptions( options, { pluginName: '@rollup-extras/plugin-copy', copyOnce: true, flatten: false, verbose: (false), exactFileNames: true, watch: true, emitFiles: true, outputPlugin: false, emitOriginalFileName: ('absolute'), preserveSymlinks: false, }, 'targets', factories ); const { pluginName, copyOnce, verbose, exactFileNames, targets, outputPlugin, flatten, emitFiles, logger, emitOriginalFileName, preserveSymlinks, } = normalizedOptions; let { watch } = normalizedOptions; const hookName = outputPlugin ? 'generateBundle' : emitFiles ? 'buildStart' : 'buildEnd'; if (!outputPlugin && !emitFiles && watch) { watch = false; logger("can't use watch with emitFiles = false and outputPlugin = false", LogLevel.verbose); } if (outputPlugin && watch) { watch = false; logger("can't use watch with outputPlugin = true", LogLevel.verbose); } if (preserveSymlinks && emitFiles) { logger('preserveSymlinks has no effect with emitFiles = true, symlinks will be dereferenced', LogLevel.verbose); } return ({ name: pluginName, async [hookName]() { const results = await Promise.all( (targets) .flatMap(target => Array.isArray(target.src) ? target.src.map(itemSrc => ({ ...target, src: itemSrc, })) : target ) .map(target => globFiles( (target.src), target.exclude).then(result => ({ src: result, dest: target.dest ? target.dest : '', parent: globParent( (target.src)), })) ) ); if (preserveSymlinks && !emitFiles) { const symlinkDirCache = new Map(); for (const result of results) { const symlinkDirs = new Set(); const filesToRemove = new Set(); for (const file of result.src) { const rel = relative(result.parent, file); const parts = rel.split(/[/\\]/); let current = result.parent; for (let i = 0; i < parts.length - 1; i++) { current = join(current, parts[i]); if (symlinkDirCache.has(current)) { if (symlinkDirCache.get(current)) { symlinkDirs.add(current); filesToRemove.add(file); break; } continue; } const s = await lstat(current); symlinkDirCache.set(current, s.isSymbolicLink()); if (s.isSymbolicLink()) { symlinkDirs.add(current); filesToRemove.add(file); break; } } } result.src = result.src.filter(( f) => !filesToRemove.has(f)); for (const dir of symlinkDirs) { result.src.push(dir); } } } for (const result of results) { for (const file of result.src) { let fileDesc; if (files.has(file)) { fileDesc = (files.get(file)); } else { fileDesc = { dest: [], copied: [], timestamp: 0, }; files.set(file, fileDesc); } const dest = flatten ? normalizeSlash(result.dest) : join(result.dest, relative(result.parent, dirname(file))); if (!fileDesc.dest.includes(dest)) { fileDesc.dest.push(dest); } if (watch) { ( (this)).addWatchFile(file); } } } const statisticsCollector = statistics( verbose === listFilenames, ( result) => `copied ${typeof result === 'number' ? `${result} files` : result.join(', ')}` ); logger.start('copying files', verbose ? LogLevel.info : LogLevel.verbose); await Promise.all( [...files].map(async ([fileName, fileDesc]) => { let source; let linkTarget; try { const fileStat = preserveSymlinks && !emitFiles ? await lstat(fileName) : await stat(fileName); if (preserveSymlinks && fileStat.isSymbolicLink()) { linkTarget = await readlink(fileName); } else if (!fileStat.isFile()) { return; } const timestamp = fileStat.mtime.getTime(); if (timestamp > fileDesc.timestamp) { fileDesc.timestamp = timestamp; fileDesc.copied = []; } if (!linkTarget && emitFiles) { source = await readFile(fileName); } } catch ( e) { const loglevel = (e).code === 'ENOENT' ? undefined : LogLevel.warn; logger(`error reading file ${fileName}`, loglevel, (e)); return; } for (const dest of fileDesc.dest) { if (copyOnce && fileDesc.copied.includes(dest)) { continue; } const baseName = basename(fileName); const destFileName = join(dest, baseName); try { if (linkTarget) { await mkdir(dirname(destFileName), { recursive: true }); await symlink(linkTarget, destFileName); } else if (emitFiles) { ( (this)).emitFile( ({ type: 'asset', [exactFileNames ? 'fileName' : 'name']: destFileName, source: (source), originalFileName: getOriginalFileName(fileName, emitOriginalFileName), }) ); } else { await mkdir(dirname(destFileName), { recursive: true }); await copyFile(fileName, destFileName, constants.COPYFILE_FICLONE); } if (verbose === listFilenames) { logger(`\t${fileName}${destFileName}`, LogLevel.info); } statisticsCollector(baseName); fileDesc.copied.push(dest); } catch (e) { logger(`error copying file ${fileName}${destFileName}`, LogLevel.warn, e); } } }) ); logger.finish( (statisticsCollector())); }, }); } function normalizeSlash(dir) { if (dir.endsWith('/')) { return `${dir.substring(0, dir.length - 1)}`; } return dir; } function targets(options, field) { const optionsRecord = ( (options)); const rawTargets = (optionsRecord[field]); const normalizedTargets = rawTargets ?? [ ( (options))]; return ( normalizedTargets .map(( item) => { if (item) { if (typeof item === 'string') { return { src: item }; } if (typeof item === 'object' && 'src' in item) { return item; } } return undefined; }) .filter(Boolean) ); } function getOriginalFileName(fileName, emitOriginalFileName) { if (emitOriginalFileName === 'relative') { return fileName; } if (emitOriginalFileName === 'absolute') { return resolve(fileName); } if (typeof emitOriginalFileName === 'function') { return emitOriginalFileName(fileName); } return undefined; }