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 }