@@ -58,12 +58,24 @@ export type ReverseMapManifest = {
|
||||
|
||||
export type RenderOptions = {
|
||||
reverseMap?: boolean | string
|
||||
/** When false, throw TargetExistsError instead of clearing a non-empty target. Defaults to true. */
|
||||
overwrite?: boolean
|
||||
/** Renames rendered file and directory names, e.g. `{ _gitignore: ".gitignore" }`. */
|
||||
rename?: Record<string, string>
|
||||
}
|
||||
|
||||
export class TargetExistsError extends Error {
|
||||
constructor(target: string) {
|
||||
super(`Refusing to overwrite non-empty target directory: ${target}`)
|
||||
this.name = "TargetExistsError"
|
||||
}
|
||||
}
|
||||
|
||||
type RenderState = {
|
||||
sourceRoot: string
|
||||
destRoot: string
|
||||
manifest?: ReverseMapManifest
|
||||
rename: Record<string, string>
|
||||
}
|
||||
|
||||
function evalCondition(expr: string | undefined, context: Record<string, unknown>): boolean {
|
||||
@@ -169,6 +181,7 @@ function getReverseMapPath(destRoot: string, reverseMap: true | string): string
|
||||
function getOutputName(
|
||||
entry: string,
|
||||
context: Record<string, unknown>,
|
||||
rename: Record<string, string>,
|
||||
state?: RenderState,
|
||||
file?: ReverseMapFile,
|
||||
): string | null {
|
||||
@@ -188,7 +201,7 @@ function getOutputName(
|
||||
}
|
||||
}
|
||||
|
||||
return outputName.replace(VAR_RE, (_match, path: string) => {
|
||||
const rendered = outputName.replace(VAR_RE, (_match, path: string) => {
|
||||
const result = String(resolveContext(context, path) ?? "")
|
||||
addReverseMapToken(state, file, {
|
||||
kind: "path",
|
||||
@@ -200,6 +213,15 @@ function getOutputName(
|
||||
})
|
||||
return result
|
||||
})
|
||||
return Object.hasOwn(rename, rendered) ? rename[rendered]! : rendered
|
||||
}
|
||||
|
||||
function isNonEmptyTarget(destRoot: string): boolean {
|
||||
try {
|
||||
return !statSync(destRoot).isDirectory() || readdirSync(destRoot).length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isUtf8Text(buffer: Buffer): boolean {
|
||||
@@ -450,10 +472,14 @@ function renderDir(
|
||||
sourceRoot,
|
||||
destRoot,
|
||||
manifest: options.reverseMap ? createReverseMapManifest() : undefined,
|
||||
rename: options.rename ?? {},
|
||||
}
|
||||
const mapPath = options.reverseMap ? getReverseMapPath(destRoot, options.reverseMap) : undefined
|
||||
assertSafeRenderTarget(srcDir, destDir)
|
||||
validateOutputPaths(srcDir, destRoot, context)
|
||||
validateOutputPaths(srcDir, destRoot, context, state.rename)
|
||||
if (options.overwrite === false && isNonEmptyTarget(destRoot)) {
|
||||
throw new TargetExistsError(destDir)
|
||||
}
|
||||
rmSync(destDir, { recursive: true, force: true })
|
||||
renderDirInner(srcDir, destRoot, context, state)
|
||||
if (state.manifest && mapPath) {
|
||||
@@ -466,18 +492,19 @@ function validateOutputPaths(
|
||||
srcDir: string,
|
||||
destDir: string,
|
||||
context: Record<string, unknown>,
|
||||
rename: Record<string, string>,
|
||||
) {
|
||||
const entries = readdirSync(srcDir).sort()
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = resolvePath(srcDir, entry)
|
||||
const stat = statSync(srcPath)
|
||||
const outputName = getOutputName(entry, context)
|
||||
const outputName = getOutputName(entry, context, rename)
|
||||
if (outputName === null) continue
|
||||
|
||||
const destPath = resolveOutputPath(destDir, outputName)
|
||||
if (stat.isDirectory()) {
|
||||
validateOutputPaths(srcPath, destPath, context)
|
||||
validateOutputPaths(srcPath, destPath, context, rename)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -533,7 +560,7 @@ function renderDirInner(
|
||||
templatePath,
|
||||
tokens: [],
|
||||
}
|
||||
const outputName = getOutputName(entry, context, state, tempFile)
|
||||
const outputName = getOutputName(entry, context, state.rename, state, tempFile)
|
||||
if (outputName === null) {
|
||||
storeSkippedTemplate(srcPath, state)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user