@rollup-extras/plugin-copy
Version:
Rollup plugin to copy assets during build.
273 lines (243 loc) • 10.1 kB
JavaScript
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;
}