@plugjs/plug
Version:
PlugJS Build System ===================
133 lines (114 loc) • 5.03 kB
text/typescript
import { assert } from '../asserts'
import { Files } from '../files'
import { chmod, copyFile, fsConstants, mkdir } from '../fs'
import { $p } from '../logging'
import { assertAbsolutePath, getAbsoluteParent, resolveAbsolutePath } from '../paths'
import { install } from '../pipe'
import type { Context, PipeParameters, Plug } from '../pipe'
/** Options for copying files */
export interface CopyOptions {
/** Whether to allow overwriting or not (default `fail`). */
overwrite?: 'overwrite' | 'fail' | 'skip',
/** If specified, use this `mode` (octal string) when creating files. */
mode?: string | number,
/** If specified, use this `mode` (octal string) when creating directories. */
dirMode?: string | number,
/** If specified, this function will be invoked to rename files. */
rename?: (relative: string) => string
}
declare module '../index' {
export interface Pipe {
/**
* Copy the curent {@link Files} to a different directory
*
* @param directory The target directory where files will be copied to
*/
copy(directory: string): Pipe
/**
* Copy the curent {@link Files} to a different directory
*
* @param directory The target directory where files will be copied to
* @param options Extra {@link CopyOptions | options} for the copy operation
*/
copy(directory: string, options: CopyOptions): Pipe
}
}
/* ========================================================================== *
* INSTALLATION / IMPLEMENTATION *
* ========================================================================== */
/** Copy the curent {@link Files} to a different directory */
install('copy', class Copy implements Plug<Files> {
constructor(...args: PipeParameters<'copy'>)
constructor(
private readonly _directory: string,
private readonly _options: CopyOptions = {},
) {}
async pipe(files: Files, context: Context): Promise<Files> {
/* Destructure our options with some defaults and compute write flags */
const { mode, dirMode, overwrite = 'fail', rename = (s): string => s } = this._options
const flags = overwrite === 'overwrite' ? 0 : fsConstants.COPYFILE_EXCL
const dmode = parseMode(dirMode)
const fmode = parseMode(mode)
/* Our files builder for all written files */
const directory = context.resolve(this._directory)
const builder = Files.builder(directory)
/* Iterate through all the mappings of the source files */
for (const [ relative, absolute ] of files.pathMappings()) {
/* The target absolute is the (possibly) renamed relative source file
* relocated to the the target directory */
const target = resolveAbsolutePath(builder.directory, rename(relative))
/* We never copy a file onto itself, but not fail either */
if (target === absolute) {
context.log.warn('Cowardly refusing to copy same file', $p(absolute))
continue
}
/* Create the parent directory, recursively */
const directory = getAbsoluteParent(target)
const firstParent = await mkdir(directory, { recursive: true })
/* Set the mode for all created directories */
if (firstParent && (dmode !== undefined)) {
assertAbsolutePath(firstParent)
for (let dir = directory; ; dir = getAbsoluteParent(dir)) {
context.log.trace(`Setting mode ${stringifyMode(dmode)} for directory`, $p(dir))
await chmod(dir, dmode)
if (dir === firstParent) break
}
}
context.log.trace(`Copying "${$p(absolute)}" to "${$p(target)}"`)
try {
/* Actually _copy_ the file */
await copyFile(absolute, target, flags)
/* Set the mode, if we need to */
if (fmode !== undefined) {
context.log.trace(`Setting mode ${stringifyMode(fmode)} for file`, $p(target))
await chmod(target, fmode)
}
/* Record this file */
builder.add(target)
} catch (error: any) {
/* Just log that we skipped the file if we have to */
if ((error.code === 'EEXIST') && (overwrite === 'skip')) {
context.log.warn(`Not overwriting existing file ${$p(target)}`)
} else {
throw error
}
}
}
const result = builder.build()
context.log.info('Copied', result.length, 'files to', $p(builder.directory))
return result
}
})
/* ========================================================================== *
* INTERNALS *
* ========================================================================== */
function parseMode(mode: string | number | undefined): number | undefined {
if (mode === undefined) return undefined
if (typeof mode === 'number') return mode
const parsed = parseInt(mode, 8)
assert(! isNaN(parsed), `Invalid mode "${mode}"`)
return parsed
}
function stringifyMode(mode: number): string {
return mode.toString(8).padStart(4, '0')
}