2 Commits

Author SHA1 Message Date
Gregor Lohaus
7d01b2f7c9 version bump
All checks were successful
Publish npm package / publish (push) Successful in 22s
2026-05-24 14:58:04 +02:00
Gregor Lohaus
0412cea241 ignore output in include globs 2026-05-24 14:57:44 +02:00
4 changed files with 70 additions and 26 deletions

View File

@@ -225,6 +225,14 @@ reverseDir("./output", "./templates", {
The command writes files at their original template paths, restores recorded `<@var(...)>` tokens, wraps edited inline conditional output back in the original conditional block, and restores template files that were skipped by path conditionals.
The template output directory may be inside the rendered directory, for example:
```sh
tdir reverse ./ ./reversed --include "components/**"
```
When the template output directory is inside the rendered directory, reverse snapshots included files before writing and excludes the output directory from include glob matching.
## Unmatched directives
A `<@if>` without a matching `<@endif>` throws when the renderer is initialized:

View File

@@ -245,6 +245,20 @@ test("reverseDir only includes new rendered files matching include globs", () =>
expect(existsSync(join(templateOut, "<@if(context.web.create)><@var(context.web.dir)>", "debug.tmp"))).toBe(false)
})
test("reverseDir can write templates inside rendered output without including its own writes", () => {
const createRenderer = initRenderer("./testdata/if_example")
const render = createRenderer(ifExampleSchema)
const renderedOut = join(tmp, "rendered")
render(renderedOut, { web: true, header: { render: true, text: "My Title" } }, { reverseMap: true })
writeFileSync(join(renderedOut, "new.html"), "<main>new</main>\n")
const result = reverseDir(renderedOut, join(renderedOut, "reversed"), { include: ["**/*"] })
expect(result.warnings).toEqual([])
expect(existsSync(join(renderedOut, "reversed", "<@if(context.web)>web", "if_example.html"))).toBe(true)
expect(existsSync(join(renderedOut, "reversed", "new.html"))).toBe(true)
expect(existsSync(join(renderedOut, "reversed", "reversed", "new.html"))).toBe(false)
})
test("wrong schema throws error",() => {
const createRenderer = initRenderer("./testdata/if_example")
expect(() => createRenderer(z.object({

View File

@@ -1,6 +1,6 @@
{
"name": "@gregorlohaus/tdir",
"version": "0.1.5",
"version": "0.1.6",
"license": "MIT",
"type": "module",
"main": "./dist/index.js",

View File

@@ -314,40 +314,36 @@ function inferTemplatePath(outputPath: string, directoryMap: Map<string, string>
return joinPath(bestTemplateDir, suffix, basenamePath(normalized))
}
function walkFiles(root: string, current = root): string[] {
function walkFiles(root: string, excludedRoots: string[] = []): string[] {
const files: string[] = []
const pending = [root]
while (pending.length > 0) {
const current = pending.pop()!
for (const entry of readdirSync(current).sort()) {
const path = resolvePath(current, entry)
if (excludedRoots.some(excluded => isInsidePath(excluded, path))) continue
const stat = statSync(path)
if (stat.isDirectory()) {
files.push(...walkFiles(root, path))
pending.push(path)
} else if (stat.isFile()) {
files.push(normalizePath(relative(root, path)))
}
}
}
return files
}
function copyIncludedRenderedFiles(
renderedRoot: string,
templateRoot: string,
mapPath: string,
manifest: ReverseMapManifest,
include: ReverseOptions["include"],
includedOutputPaths: string[],
directoryMap: Map<string, string>,
): number {
const matchers = getIncludeMatchers(include)
if (matchers.length === 0) return 0
const mappedOutputPaths = new Set(manifest.files.map(file => normalizePath(file.outputPath)))
const mapRelativePath = normalizePath(relative(renderedRoot, mapPath))
const directoryMap = buildDirectoryMap(manifest)
let filesWritten = 0
for (const outputPath of walkFiles(renderedRoot)) {
if (outputPath === mapRelativePath) continue
if (mappedOutputPaths.has(outputPath)) continue
if (!matchesAny(outputPath, matchers)) continue
for (const outputPath of includedOutputPaths) {
const renderedPath = resolveInside(renderedRoot, outputPath)
const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap))
mkdirSync(dirname(templatePath), { recursive: true })
@@ -358,6 +354,25 @@ function copyIncludedRenderedFiles(
return filesWritten
}
function getIncludedRenderedFiles(
renderedRoot: string,
templateRoot: string,
mapPath: string,
manifest: ReverseMapManifest,
include: ReverseOptions["include"],
): string[] {
const matchers = getIncludeMatchers(include)
if (matchers.length === 0) return []
const mappedOutputPaths = new Set(manifest.files.map(file => normalizePath(file.outputPath)))
const mapRelativePath = normalizePath(relative(renderedRoot, mapPath))
return walkFiles(renderedRoot, [templateRoot]).filter(outputPath => {
return outputPath !== mapRelativePath
&& !mappedOutputPaths.has(outputPath)
&& matchesAny(outputPath, matchers)
})
}
export function reverseDir(
renderedDir: string,
templateDir: string,
@@ -369,6 +384,14 @@ export function reverseDir(
? resolvePath(renderedRoot, options.mapPath)
: resolvePath(renderedRoot, ".tdir-map.json")
const manifest = readManifest(mapPath)
const directoryMap = buildDirectoryMap(manifest)
const includedOutputPaths = getIncludedRenderedFiles(
renderedRoot,
templateRoot,
mapPath,
manifest,
options.include,
)
const warnings: ReverseWarning[] = []
let filesWritten = 0
@@ -415,9 +438,8 @@ export function reverseDir(
filesWritten += copyIncludedRenderedFiles(
renderedRoot,
templateRoot,
mapPath,
manifest,
options.include,
includedOutputPaths,
directoryMap,
)
return { filesWritten, warnings }