diff --git a/README.md b/README.md
index 4be4ac4..7dd51eb 100644
--- a/README.md
+++ b/README.md
@@ -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:
diff --git a/index.test.ts b/index.test.ts
index c9c334c..9f8132d 100644
--- a/index.test.ts
+++ b/index.test.ts
@@ -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"), "new\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({
diff --git a/reverse.ts b/reverse.ts
index 2599eed..8e2ed0e 100644
--- a/reverse.ts
+++ b/reverse.ts
@@ -314,40 +314,36 @@ function inferTemplatePath(outputPath: string, directoryMap: Map
return joinPath(bestTemplateDir, suffix, basenamePath(normalized))
}
-function walkFiles(root: string, current = root): string[] {
+function walkFiles(root: string, excludedRoots: string[] = []): string[] {
const files: string[] = []
- for (const entry of readdirSync(current).sort()) {
- const path = resolvePath(current, entry)
- const stat = statSync(path)
- if (stat.isDirectory()) {
- files.push(...walkFiles(root, path))
- } else if (stat.isFile()) {
- files.push(normalizePath(relative(root, path)))
+ 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()) {
+ 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,
): 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 }