3 Commits

Author SHA1 Message Date
Gregor Lohaus
e16fc8b482 verison bump, new files need to include var tokens
All checks were successful
Publish npm package / publish (push) Successful in 26s
2026-05-24 15:05:22 +02:00
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
5 changed files with 110 additions and 27 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 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 ## Unmatched directives
A `<@if>` without a matching `<@endif>` throws when the renderer is initialized: A `<@if>` without a matching `<@endif>` throws when the renderer is initialized:

View File

@@ -229,22 +229,38 @@ test("reverseDir only includes new rendered files matching include globs", () =>
dir: "components" dir: "components"
}, },
header: { header: {
render: false, render: true,
text: "test" text: "test"
} }
}, { reverseMap: true }) }, { reverseMap: true })
writeFileSync(join(renderedOut, "components", "new.ts"), "export const value = 1\n") writeFileSync(join(renderedOut, "components", "new.ts"), "export const value = 1\n")
writeFileSync(join(renderedOut, "components", "title.ts"), "export const title = 'test'\n")
writeFileSync(join(renderedOut, "components", "debug.tmp"), "debug\n") writeFileSync(join(renderedOut, "components", "debug.tmp"), "debug\n")
reverseDir(renderedOut, ignoredOut) reverseDir(renderedOut, ignoredOut)
expect(existsSync(join(ignoredOut, "<@if(context.web.create)><@var(context.web.dir)>", "new.ts"))).toBe(false) expect(existsSync(join(ignoredOut, "<@if(context.web.create)><@var(context.web.dir)>", "new.ts"))).toBe(false)
const result = reverseDir(renderedOut, templateOut, { include: ["components/**/*.ts"] }) const result = reverseDir(renderedOut, templateOut, { include: ["components/**/*.ts"] })
expect(result.filesWritten).toBe(2) expect(result.filesWritten).toBe(3)
expect(existsSync(join(templateOut, "<@if(context.web.create)><@var(context.web.dir)>", "new.ts"))).toBe(true) expect(existsSync(join(templateOut, "<@if(context.web.create)><@var(context.web.dir)>", "new.ts"))).toBe(true)
expect(readFileSync(join(templateOut, "<@if(context.web.create)><@var(context.web.dir)>", "title.ts"), "utf-8")).toContain("<@var(context.header.text)>")
expect(existsSync(join(templateOut, "<@if(context.web.create)><@var(context.web.dir)>", "debug.tmp"))).toBe(false) 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",() => { test("wrong schema throws error",() => {
const createRenderer = initRenderer("./testdata/if_example") const createRenderer = initRenderer("./testdata/if_example")
expect(() => createRenderer(z.object({ expect(() => createRenderer(z.object({

View File

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

View File

@@ -139,6 +139,17 @@ function addReverseMapToken(
state.manifest.tokens[token.result] = tokens state.manifest.tokens[token.result] = tokens
} }
function addFlatToken(
state: RenderState | undefined,
result: string,
token: string,
) {
if (!state?.manifest) return
const tokens = state.manifest.tokens[result] ?? []
if (!tokens.includes(token)) tokens.push(token)
state.manifest.tokens[result] = tokens
}
function getReverseMapPath(destRoot: string, reverseMap: true | string): string { function getReverseMapPath(destRoot: string, reverseMap: true | string): string {
if (reverseMap === true) return resolveOutputPath(destRoot, ".tdir-map.json") if (reverseMap === true) return resolveOutputPath(destRoot, ".tdir-map.json")
return resolveOutputPath(destRoot, reverseMap) return resolveOutputPath(destRoot, reverseMap)
@@ -384,6 +395,12 @@ function renderContentWithMap(
state?: RenderState, state?: RenderState,
file?: ReverseMapFile, file?: ReverseMapFile,
): string { ): string {
for (const match of content.matchAll(VAR_RE)) {
const token = match[0]
const path = match[1]!
addFlatToken(state, String(resolveContext(context, path) ?? ""), token)
}
const processedResult = processIfBlocksWithMap(content, context) const processedResult = processIfBlocksWithMap(content, context)
const processed = processedResult.content const processed = processedResult.content
for (const token of processedResult.conditionalTokens) { for (const token of processedResult.conditionalTokens) {

View File

@@ -245,6 +245,19 @@ function writeSkippedTemplate(
return 1 return 1
} }
function replaceFlatTokens(content: string, manifest: ReverseMapManifest): string {
const entries = Object.entries(manifest.tokens)
.filter(([, tokens]) => tokens.length > 0)
.sort(([a], [b]) => b.length - a.length)
let result = content
for (const [rendered, tokens] of entries) {
if (rendered === "") continue
result = result.split(rendered).join(tokens[0]!)
}
return result
}
function dirnamePath(path: string): string { function dirnamePath(path: string): string {
const normalized = normalizePath(path) const normalized = normalizePath(path)
const index = normalized.lastIndexOf("/") const index = normalized.lastIndexOf("/")
@@ -314,50 +327,71 @@ function inferTemplatePath(outputPath: string, directoryMap: Map<string, string>
return joinPath(bestTemplateDir, suffix, basenamePath(normalized)) return joinPath(bestTemplateDir, suffix, basenamePath(normalized))
} }
function walkFiles(root: string, current = root): string[] { function walkFiles(root: string, excludedRoots: string[] = []): string[] {
const files: string[] = [] const files: string[] = []
for (const entry of readdirSync(current).sort()) { const pending = [root]
const path = resolvePath(current, entry)
const stat = statSync(path) while (pending.length > 0) {
if (stat.isDirectory()) { const current = pending.pop()!
files.push(...walkFiles(root, path)) for (const entry of readdirSync(current).sort()) {
} else if (stat.isFile()) { const path = resolvePath(current, entry)
files.push(normalizePath(relative(root, path))) 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 return files
} }
function copyIncludedRenderedFiles( function copyIncludedRenderedFiles(
renderedRoot: string, renderedRoot: string,
templateRoot: string, templateRoot: string,
mapPath: string, includedOutputPaths: string[],
directoryMap: Map<string, string>,
manifest: ReverseMapManifest, manifest: ReverseMapManifest,
include: ReverseOptions["include"],
): number { ): 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 let filesWritten = 0
for (const outputPath of walkFiles(renderedRoot)) { for (const outputPath of includedOutputPaths) {
if (outputPath === mapRelativePath) continue
if (mappedOutputPaths.has(outputPath)) continue
if (!matchesAny(outputPath, matchers)) continue
const renderedPath = resolveInside(renderedRoot, outputPath) const renderedPath = resolveInside(renderedRoot, outputPath)
const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap)) const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap))
mkdirSync(dirname(templatePath), { recursive: true }) mkdirSync(dirname(templatePath), { recursive: true })
copyFileSync(renderedPath, templatePath) const content = readFileSync(renderedPath)
if (isUtf8Text(content)) {
writeFileSync(templatePath, replaceFlatTokens(content.toString("utf-8"), manifest))
} else {
copyFileSync(renderedPath, templatePath)
}
filesWritten += 1 filesWritten += 1
} }
return filesWritten 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( export function reverseDir(
renderedDir: string, renderedDir: string,
templateDir: string, templateDir: string,
@@ -369,6 +403,14 @@ export function reverseDir(
? resolvePath(renderedRoot, options.mapPath) ? resolvePath(renderedRoot, options.mapPath)
: resolvePath(renderedRoot, ".tdir-map.json") : resolvePath(renderedRoot, ".tdir-map.json")
const manifest = readManifest(mapPath) const manifest = readManifest(mapPath)
const directoryMap = buildDirectoryMap(manifest)
const includedOutputPaths = getIncludedRenderedFiles(
renderedRoot,
templateRoot,
mapPath,
manifest,
options.include,
)
const warnings: ReverseWarning[] = [] const warnings: ReverseWarning[] = []
let filesWritten = 0 let filesWritten = 0
@@ -415,9 +457,9 @@ export function reverseDir(
filesWritten += copyIncludedRenderedFiles( filesWritten += copyIncludedRenderedFiles(
renderedRoot, renderedRoot,
templateRoot, templateRoot,
mapPath, includedOutputPaths,
directoryMap,
manifest, manifest,
options.include,
) )
return { filesWritten, warnings } return { filesWritten, warnings }