Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
101769a4b2 | ||
|
|
ed5a666ad3 | ||
|
|
452d872954 | ||
|
|
e900729c8f |
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: x86
|
runs-on: local
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -21,6 +21,10 @@ jobs:
|
|||||||
node-version: 24
|
node-version: 24
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
|
# Stage-only tokens require `npm stage publish` (npm >= 11.15.0).
|
||||||
|
- name: Update npm
|
||||||
|
run: npm install -g npm@^11.15.0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
@@ -30,10 +34,13 @@ jobs:
|
|||||||
TAG_NAME="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}"
|
TAG_NAME="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}"
|
||||||
test "$PACKAGE_VERSION" = "$TAG_NAME"
|
test "$PACKAGE_VERSION" = "$TAG_NAME"
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: bun test
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: bun run build
|
||||||
|
|
||||||
- name: Publish
|
- name: Stage publish
|
||||||
run: npm publish --access public
|
run: npm stage publish --access public
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -131,6 +131,31 @@ render("./output", { web: "not a boolean", header: { show: true, title: "Hi" } }
|
|||||||
|
|
||||||
For safety, tdir refuses to render into the filesystem root, the current working directory, the home directory, or any directory that overlaps the template source. Dynamic file and directory names are also resolved against the output directory and cannot write outside it.
|
For safety, tdir refuses to render into the filesystem root, the current working directory, the home directory, or any directory that overlaps the template source. Dynamic file and directory names are also resolved against the output directory and cannot write outside it.
|
||||||
|
|
||||||
|
Pass `{ overwrite: false }` to keep existing work safe, e.g. in a scaffolding CLI. tdir then throws `TargetExistsError` instead of clearing a target that exists and is not empty (or is a file); a missing or empty target is rendered as usual:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TargetExistsError } from "@gregorlohaus/tdir"
|
||||||
|
|
||||||
|
try {
|
||||||
|
render("./my-app", context, { overwrite: false })
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof TargetExistsError) console.error("./my-app already exists")
|
||||||
|
else throw error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Renaming output files
|
||||||
|
|
||||||
|
Some files can't be stored in a template under their real name, e.g. npm never publishes `.gitignore` or `.npmrc` files. Store them under another name and map it back with `rename`, which applies to rendered file and directory names:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
render("./output", context, { rename: { _gitignore: ".gitignore", _npmrc: ".npmrc" } })
|
||||||
|
// templates/_gitignore -> output/.gitignore
|
||||||
|
// templates/<@if(context.web)>web/_gitignore -> output/web/.gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse maps record the renamed output path, so `tdir reverse` writes edits back to `_gitignore`.
|
||||||
|
|
||||||
## Reverse maps
|
## Reverse maps
|
||||||
|
|
||||||
Pass `{ reverseMap: true }` as the third render argument to write `.tdir-map.json` into the output directory:
|
Pass `{ reverseMap: true }` as the third render argument to write `.tdir-map.json` into the output directory:
|
||||||
|
|||||||
+9
-9
@@ -3,11 +3,11 @@
|
|||||||
"devenv": {
|
"devenv": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"dir": "src/modules",
|
"dir": "src/modules",
|
||||||
"lastModified": 1776271913,
|
"lastModified": 1790701375,
|
||||||
"narHash": "sha256-j/1hNdZSci/jrYEHj3/F24EI/YE8DL0OzfMWZUgpMig=",
|
"narHash": "sha256-0FpLHIRDYiyo3EMTnMQSobZQknZaa3JuWo6vmMCK76o=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv",
|
"repo": "devenv",
|
||||||
"rev": "2012662a89ff2ce92044151d7bbf3894eec5620a",
|
"rev": "fe20b5cba7ab5e93ae73f956a8d3efc50e1753f4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
"nixpkgs-src": "nixpkgs-src"
|
"nixpkgs-src": "nixpkgs-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776097194,
|
"lastModified": 1789506231,
|
||||||
"narHash": "sha256-XD4DsgNcfXC5nlCxlAcCP5hSjTYlgLXEIoTj7fKkQg4=",
|
"narHash": "sha256-vNkJbtmqyBfFhn/HnzQo4ROuoyYWuy/allqTfJb4kMY=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv-nixpkgs",
|
"repo": "devenv-nixpkgs",
|
||||||
"rev": "6e8a07b02f6f8557ffab71274feac9827bcc2532",
|
"rev": "c2f38fe7f9e04d9aadd354d380f2bd40531d9737",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
"nixpkgs-src": {
|
"nixpkgs-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775888245,
|
"lastModified": 1789370336,
|
||||||
"narHash": "sha256-nwASzrRDD1JBEu/o8ekKYEXm/oJW6EMCzCRdrwcLe90=",
|
"narHash": "sha256-6RSEDHIWQtesQKWSu5qRai8L2h4KgCgMEfJHstW99G4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "13043924aaa7375ce482ebe2494338e058282925",
|
"rev": "c7def046b9a883d46974757852106483d741586f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+71
-2
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test, beforeEach, afterEach } from 'bun:test'
|
import { expect, test, beforeEach, afterEach } from 'bun:test'
|
||||||
import { initRenderer, reverseDir, SchemaMismatchError } from "./index";
|
import { initRenderer, reverseDir, SchemaMismatchError, TargetExistsError } from "./index";
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
|
||||||
import { basename, join } from "node:path"
|
import { basename, join } from "node:path"
|
||||||
@@ -451,7 +451,7 @@ test("file if",() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("no endif should throw",() => {
|
test("no endif should throw",() => {
|
||||||
expect(() => initRenderer("./testdata/no_end_if")).toThrow()
|
expect(() => initRenderer("./testdata/no_end_if")).toThrow("test.txt")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("path vars cannot write outside target",() => {
|
test("path vars cannot write outside target",() => {
|
||||||
@@ -655,3 +655,72 @@ test("and or in path", () => {
|
|||||||
render(tmp,{enabled:false, kind:"docs"})
|
render(tmp,{enabled:false, kind:"docs"})
|
||||||
expect(existsSync(join(tmp,"match"))).toBe(false)
|
expect(existsSync(join(tmp,"match"))).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("rename maps output names and records them in the reverse map", () => {
|
||||||
|
const templateDir = join(tmp, "template")
|
||||||
|
mkdirSync(join(templateDir, "<@if(context.web)>web"), { recursive: true })
|
||||||
|
writeFileSync(join(templateDir, "_gitignore"), "node_modules\n")
|
||||||
|
writeFileSync(join(templateDir, "<@if(context.web)>web", "_gitignore"), "dist\n")
|
||||||
|
const render = initRenderer(templateDir)(z.object({ web: z.boolean() }))
|
||||||
|
const out = join(tmp, "out")
|
||||||
|
|
||||||
|
render(out, { web: true }, { reverseMap: true, rename: { _gitignore: ".gitignore" } })
|
||||||
|
|
||||||
|
expect(readFileSync(join(out, ".gitignore"), "utf-8")).toBe("node_modules\n")
|
||||||
|
expect(readFileSync(join(out, "web", ".gitignore"), "utf-8")).toBe("dist\n")
|
||||||
|
expect(existsSync(join(out, "_gitignore"))).toBe(false)
|
||||||
|
const manifest = JSON.parse(readFileSync(join(out, ".tdir-map.json"), "utf-8"))
|
||||||
|
expect(manifest.files).toContainEqual(expect.objectContaining({ outputPath: ".gitignore", templatePath: "_gitignore" }))
|
||||||
|
expect(manifest.files).toContainEqual(expect.objectContaining({
|
||||||
|
outputPath: join("web", ".gitignore"),
|
||||||
|
templatePath: join("<@if(context.web)>web", "_gitignore"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const reversedDir = join(tmp, "reversed")
|
||||||
|
reverseDir(out, reversedDir)
|
||||||
|
expect(readFileSync(join(reversedDir, "_gitignore"), "utf-8")).toBe("node_modules\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rename applies to directories", () => {
|
||||||
|
const templateDir = join(tmp, "template")
|
||||||
|
mkdirSync(join(templateDir, "_github"), { recursive: true })
|
||||||
|
writeFileSync(join(templateDir, "_github", "ci.yml"), "on: push\n")
|
||||||
|
const render = initRenderer(templateDir)(z.object({}))
|
||||||
|
const out = join(tmp, "out")
|
||||||
|
|
||||||
|
render(out, {}, { rename: { _github: ".github" } })
|
||||||
|
|
||||||
|
expect(readFileSync(join(out, ".github", "ci.yml"), "utf-8")).toBe("on: push\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("overwrite false refuses a non-empty target and leaves it untouched", () => {
|
||||||
|
const render = initRenderer("./testdata/if_example")(ifExampleSchema)
|
||||||
|
const out = join(tmp, "out")
|
||||||
|
mkdirSync(out)
|
||||||
|
writeFileSync(join(out, "keep.txt"), "important")
|
||||||
|
|
||||||
|
expect(() => render(out, { web: true, header: { render: true, text: "T" } }, { overwrite: false })).toThrow(TargetExistsError)
|
||||||
|
expect(readFileSync(join(out, "keep.txt"), "utf-8")).toBe("important")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("overwrite false renders into a missing or empty target", () => {
|
||||||
|
const render = initRenderer("./testdata/if_example")(ifExampleSchema)
|
||||||
|
const missing = join(tmp, "missing")
|
||||||
|
const empty = join(tmp, "empty")
|
||||||
|
mkdirSync(empty)
|
||||||
|
|
||||||
|
render(missing, { web: true, header: { render: true, text: "T" } }, { overwrite: false })
|
||||||
|
render(empty, { web: true, header: { render: true, text: "T" } }, { overwrite: false })
|
||||||
|
|
||||||
|
expect(existsSync(join(missing, "web", "if_example.html"))).toBe(true)
|
||||||
|
expect(existsSync(join(empty, "web", "if_example.html"))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("overwrite false refuses when the target is a file", () => {
|
||||||
|
const render = initRenderer("./testdata/if_example")(ifExampleSchema)
|
||||||
|
const out = join(tmp, "out")
|
||||||
|
writeFileSync(out, "not a directory")
|
||||||
|
|
||||||
|
expect(() => render(out, { web: true, header: { render: true, text: "T" } }, { overwrite: false })).toThrow(TargetExistsError)
|
||||||
|
expect(readFileSync(out, "utf-8")).toBe("not a directory")
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod"
|
|||||||
import { parse, type TemplateVariable } from "./parser"
|
import { parse, type TemplateVariable } from "./parser"
|
||||||
import { renderDir, type RenderOptions } from "./render"
|
import { renderDir, type RenderOptions } from "./render"
|
||||||
|
|
||||||
|
export { TargetExistsError } from "./render"
|
||||||
export type { RenderOptions, ReverseMapFile, ReverseMapManifest, ReverseMapStoredTemplate, ReverseMapToken } from "./render"
|
export type { RenderOptions, ReverseMapFile, ReverseMapManifest, ReverseMapStoredTemplate, ReverseMapToken } from "./render"
|
||||||
export { reverseDir, type ReverseOptions, type ReverseResult, type ReverseWarning } from "./reverse"
|
export { reverseDir, type ReverseOptions, type ReverseResult, type ReverseWarning } from "./reverse"
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@gregorlohaus/tdir",
|
"name": "@gregorlohaus/tdir",
|
||||||
"version": "0.1.9",
|
"version": "0.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tdir": "./dist/cli.js"
|
"tdir": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
@@ -25,5 +25,9 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^4"
|
"zod": "^4"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.gregorlohaus.com/gregor/tdir"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readdirSync, statSync, readFileSync } from "node:fs"
|
import { readdirSync, statSync, readFileSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
import { join, relative } from "node:path"
|
||||||
import { TextDecoder } from "node:util"
|
import { TextDecoder } from "node:util"
|
||||||
import { getDirectiveTokens, splitArgs } from "./scanner"
|
import { getDirectiveTokens, splitArgs } from "./scanner"
|
||||||
|
|
||||||
@@ -36,10 +36,15 @@ function extractCondition(expr: string | undefined, vars: TemplateVariable[]) {
|
|||||||
throw new Error(`Invalid condition expression: ${expr}`)
|
throw new Error(`Invalid condition expression: ${expr}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFromString(text: string, vars: TemplateVariable[]) {
|
function extractFromString(text: string, vars: TemplateVariable[], source = "template") {
|
||||||
for (const token of getDirectiveTokens(text)) {
|
for (const token of getDirectiveTokens(text)) {
|
||||||
if (token.type === "if" || token.type === "elseif") {
|
if (token.type === "if" || token.type === "elseif") {
|
||||||
extractCondition(token.condition, vars)
|
try {
|
||||||
|
extractCondition(token.condition, vars)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) throw new Error(`${source}: ${error.message}`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const match of text.matchAll(VAR_RE)) {
|
for (const match of text.matchAll(VAR_RE)) {
|
||||||
@@ -47,7 +52,7 @@ function extractFromString(text: string, vars: TemplateVariable[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateIfBlocks(content: string, vars: TemplateVariable[]) {
|
function validateIfBlocks(content: string, vars: TemplateVariable[], source: string) {
|
||||||
const stack: { sawElse: boolean }[] = []
|
const stack: { sawElse: boolean }[] = []
|
||||||
|
|
||||||
for (const token of getDirectiveTokens(content)) {
|
for (const token of getDirectiveTokens(content)) {
|
||||||
@@ -59,22 +64,22 @@ function validateIfBlocks(content: string, vars: TemplateVariable[]) {
|
|||||||
stack.push({ sawElse: false })
|
stack.push({ sawElse: false })
|
||||||
} else if (directive === "elseif") {
|
} else if (directive === "elseif") {
|
||||||
const frame = stack[stack.length - 1]
|
const frame = stack[stack.length - 1]
|
||||||
if (!frame) throw new Error("Unexpected <@elseif> without <@if>")
|
if (!frame) throw new Error(`${source}: Unexpected <@elseif> without <@if>`)
|
||||||
if (frame.sawElse) throw new Error("Unexpected <@elseif> after <@else>")
|
if (frame.sawElse) throw new Error(`${source}: Unexpected <@elseif> after <@else>`)
|
||||||
extractCondition(condition!, vars)
|
extractCondition(condition!, vars)
|
||||||
} else if (directive === "else") {
|
} else if (directive === "else") {
|
||||||
const frame = stack[stack.length - 1]
|
const frame = stack[stack.length - 1]
|
||||||
if (!frame) throw new Error("Unexpected <@else> without <@if>")
|
if (!frame) throw new Error(`${source}: Unexpected <@else> without <@if>`)
|
||||||
if (frame.sawElse) throw new Error("Unexpected duplicate <@else>")
|
if (frame.sawElse) throw new Error(`${source}: Unexpected duplicate <@else>`)
|
||||||
frame.sawElse = true
|
frame.sawElse = true
|
||||||
} else if (directive === "endif") {
|
} else if (directive === "endif") {
|
||||||
if (stack.length === 0) throw new Error("Unexpected <@endif> without <@if>")
|
if (stack.length === 0) throw new Error(`${source}: Unexpected <@endif> without <@if>`)
|
||||||
stack.pop()
|
stack.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stack.length > 0) {
|
if (stack.length > 0) {
|
||||||
throw new Error("Unmatched <@if> without <@endif>")
|
throw new Error(`${source}: Unmatched <@if> without <@endif>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,21 +93,22 @@ function isUtf8Text(buffer: Buffer): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function walkDir(dirPath: string, vars: TemplateVariable[]) {
|
function walkDir(dirPath: string, vars: TemplateVariable[], rootPath: string) {
|
||||||
const entries = readdirSync(dirPath).sort()
|
const entries = readdirSync(dirPath).sort()
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const fullPath = join(dirPath, entry)
|
const fullPath = join(dirPath, entry)
|
||||||
extractFromString(entry, vars)
|
const relativePath = relative(rootPath, fullPath)
|
||||||
|
extractFromString(entry, vars, relativePath || entry)
|
||||||
|
|
||||||
const stat = statSync(fullPath)
|
const stat = statSync(fullPath)
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
walkDir(fullPath, vars)
|
walkDir(fullPath, vars, rootPath)
|
||||||
} else if (stat.isFile()) {
|
} else if (stat.isFile()) {
|
||||||
const content = readFileSync(fullPath)
|
const content = readFileSync(fullPath)
|
||||||
if (isUtf8Text(content)) {
|
if (isUtf8Text(content)) {
|
||||||
const text = content.toString("utf-8")
|
const text = content.toString("utf-8")
|
||||||
extractFromString(text, vars)
|
extractFromString(text, vars, relativePath || fullPath)
|
||||||
validateIfBlocks(text, vars)
|
validateIfBlocks(text, vars, relativePath || fullPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +116,7 @@ function walkDir(dirPath: string, vars: TemplateVariable[]) {
|
|||||||
|
|
||||||
export function parse(dirPath: string): TemplateVariable[] {
|
export function parse(dirPath: string): TemplateVariable[] {
|
||||||
const vars: TemplateVariable[] = []
|
const vars: TemplateVariable[] = []
|
||||||
walkDir(dirPath, vars)
|
walkDir(dirPath, vars, dirPath)
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
return vars.filter(v => {
|
return vars.filter(v => {
|
||||||
const key = `${v.path}:${v.type}`
|
const key = `${v.path}:${v.type}`
|
||||||
|
|||||||
@@ -58,12 +58,24 @@ export type ReverseMapManifest = {
|
|||||||
|
|
||||||
export type RenderOptions = {
|
export type RenderOptions = {
|
||||||
reverseMap?: boolean | string
|
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 = {
|
type RenderState = {
|
||||||
sourceRoot: string
|
sourceRoot: string
|
||||||
destRoot: string
|
destRoot: string
|
||||||
manifest?: ReverseMapManifest
|
manifest?: ReverseMapManifest
|
||||||
|
rename: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
function evalCondition(expr: string | undefined, context: Record<string, unknown>): boolean {
|
function evalCondition(expr: string | undefined, context: Record<string, unknown>): boolean {
|
||||||
@@ -169,6 +181,7 @@ function getReverseMapPath(destRoot: string, reverseMap: true | string): string
|
|||||||
function getOutputName(
|
function getOutputName(
|
||||||
entry: string,
|
entry: string,
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
|
rename: Record<string, string>,
|
||||||
state?: RenderState,
|
state?: RenderState,
|
||||||
file?: ReverseMapFile,
|
file?: ReverseMapFile,
|
||||||
): string | null {
|
): 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) ?? "")
|
const result = String(resolveContext(context, path) ?? "")
|
||||||
addReverseMapToken(state, file, {
|
addReverseMapToken(state, file, {
|
||||||
kind: "path",
|
kind: "path",
|
||||||
@@ -200,6 +213,15 @@ function getOutputName(
|
|||||||
})
|
})
|
||||||
return result
|
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 {
|
function isUtf8Text(buffer: Buffer): boolean {
|
||||||
@@ -450,10 +472,14 @@ function renderDir(
|
|||||||
sourceRoot,
|
sourceRoot,
|
||||||
destRoot,
|
destRoot,
|
||||||
manifest: options.reverseMap ? createReverseMapManifest() : undefined,
|
manifest: options.reverseMap ? createReverseMapManifest() : undefined,
|
||||||
|
rename: options.rename ?? {},
|
||||||
}
|
}
|
||||||
const mapPath = options.reverseMap ? getReverseMapPath(destRoot, options.reverseMap) : undefined
|
const mapPath = options.reverseMap ? getReverseMapPath(destRoot, options.reverseMap) : undefined
|
||||||
assertSafeRenderTarget(srcDir, destDir)
|
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 })
|
rmSync(destDir, { recursive: true, force: true })
|
||||||
renderDirInner(srcDir, destRoot, context, state)
|
renderDirInner(srcDir, destRoot, context, state)
|
||||||
if (state.manifest && mapPath) {
|
if (state.manifest && mapPath) {
|
||||||
@@ -466,18 +492,19 @@ function validateOutputPaths(
|
|||||||
srcDir: string,
|
srcDir: string,
|
||||||
destDir: string,
|
destDir: string,
|
||||||
context: Record<string, unknown>,
|
context: Record<string, unknown>,
|
||||||
|
rename: Record<string, string>,
|
||||||
) {
|
) {
|
||||||
const entries = readdirSync(srcDir).sort()
|
const entries = readdirSync(srcDir).sort()
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const srcPath = resolvePath(srcDir, entry)
|
const srcPath = resolvePath(srcDir, entry)
|
||||||
const stat = statSync(srcPath)
|
const stat = statSync(srcPath)
|
||||||
const outputName = getOutputName(entry, context)
|
const outputName = getOutputName(entry, context, rename)
|
||||||
if (outputName === null) continue
|
if (outputName === null) continue
|
||||||
|
|
||||||
const destPath = resolveOutputPath(destDir, outputName)
|
const destPath = resolveOutputPath(destDir, outputName)
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
validateOutputPaths(srcPath, destPath, context)
|
validateOutputPaths(srcPath, destPath, context, rename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,7 +560,7 @@ function renderDirInner(
|
|||||||
templatePath,
|
templatePath,
|
||||||
tokens: [],
|
tokens: [],
|
||||||
}
|
}
|
||||||
const outputName = getOutputName(entry, context, state, tempFile)
|
const outputName = getOutputName(entry, context, state.rename, state, tempFile)
|
||||||
if (outputName === null) {
|
if (outputName === null) {
|
||||||
storeSkippedTemplate(srcPath, state)
|
storeSkippedTemplate(srcPath, state)
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user