@gregorlohaus/tdir 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -208,8 +208,31 @@ tdir reverse ./output ./templates --map meta/reverse-map.json
208
208
  bunx @gregorlohaus/tdir reverse ./output ./templates --map meta/reverse-map.json
209
209
  ```
210
210
 
211
+ New files created in the rendered directory are ignored by default. Include them explicitly with one or more glob patterns:
212
+
213
+ ```sh
214
+ tdir reverse ./output ./templates --include "components/**"
215
+ tdir reverse ./output ./templates --include "components/**/*.ts" --include "pages/*.html"
216
+ ```
217
+
218
+ Programmatically, pass the same globs to `reverseDir`:
219
+
220
+ ```ts
221
+ reverseDir("./output", "./templates", {
222
+ include: ["components/**/*.ts", "pages/*.html"]
223
+ })
224
+ ```
225
+
211
226
  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.
212
227
 
228
+ The template output directory may be inside the rendered directory, for example:
229
+
230
+ ```sh
231
+ tdir reverse ./ ./reversed --include "components/**"
232
+ ```
233
+
234
+ 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.
235
+
213
236
  ## Unmatched directives
214
237
 
215
238
  A `<@if>` without a matching `<@endif>` throws when the renderer is initialized:
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  existsSync,
7
7
  mkdirSync,
8
8
  readFileSync,
9
+ readdirSync,
9
10
  statSync,
10
11
  writeFileSync
11
12
  } from "node:fs";
@@ -39,6 +40,45 @@ function readManifest(mapPath) {
39
40
  }
40
41
  return manifest;
41
42
  }
43
+ function normalizePath(path) {
44
+ return path.split("\\").join("/");
45
+ }
46
+ function escapeRegExp(text) {
47
+ return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
48
+ }
49
+ function globToRegExp(glob) {
50
+ let source = "^";
51
+ const pattern = normalizePath(glob);
52
+ for (let i = 0;i < pattern.length; i++) {
53
+ const char = pattern[i];
54
+ const next = pattern[i + 1];
55
+ if (char === "*" && next === "*") {
56
+ if (pattern[i + 2] === "/") {
57
+ source += "(?:.*/)?";
58
+ i += 2;
59
+ } else {
60
+ source += ".*";
61
+ i += 1;
62
+ }
63
+ } else if (char === "*") {
64
+ source += "[^/]*";
65
+ } else if (char === "?") {
66
+ source += "[^/]";
67
+ } else {
68
+ source += escapeRegExp(char);
69
+ }
70
+ }
71
+ return new RegExp(`${source}$`);
72
+ }
73
+ function getIncludeMatchers(include) {
74
+ if (!include)
75
+ return [];
76
+ return (Array.isArray(include) ? include : [include]).map(globToRegExp);
77
+ }
78
+ function matchesAny(path, matchers) {
79
+ const normalized = normalizePath(path);
80
+ return matchers.some((matcher) => matcher.test(normalized));
81
+ }
42
82
  function replaceAtRange(content, token) {
43
83
  if (!token.range)
44
84
  return null;
@@ -159,11 +199,106 @@ function writeSkippedTemplate(templateRoot, skipped) {
159
199
  writeFileSync(templatePath, content);
160
200
  return 1;
161
201
  }
202
+ function dirnamePath(path) {
203
+ const normalized = normalizePath(path);
204
+ const index = normalized.lastIndexOf("/");
205
+ return index === -1 ? "" : normalized.slice(0, index);
206
+ }
207
+ function basenamePath(path) {
208
+ const normalized = normalizePath(path);
209
+ const index = normalized.lastIndexOf("/");
210
+ return index === -1 ? normalized : normalized.slice(index + 1);
211
+ }
212
+ function joinPath(...parts) {
213
+ return parts.filter((part) => part !== "").join("/");
214
+ }
215
+ function buildDirectoryMap(manifest) {
216
+ const mappings = new Map([["", ""]]);
217
+ function addMapping(outputDir, templateDir) {
218
+ const outputParts = normalizePath(outputDir).split("/").filter(Boolean);
219
+ const templateParts = normalizePath(templateDir).split("/").filter(Boolean);
220
+ for (let i = 1;i <= outputParts.length; i++) {
221
+ if (i <= templateParts.length) {
222
+ mappings.set(outputParts.slice(0, i).join("/"), templateParts.slice(0, i).join("/"));
223
+ }
224
+ }
225
+ mappings.set(normalizePath(outputDir), normalizePath(templateDir));
226
+ }
227
+ for (const file of manifest.files) {
228
+ const output = normalizePath(file.outputPath);
229
+ const template = normalizePath(file.templatePath);
230
+ addMapping(dirnamePath(output), dirnamePath(template));
231
+ if (file.tokens.some((token) => token.kind === "path"))
232
+ addMapping(output, template);
233
+ }
234
+ for (const skipped of manifest.skipped ?? []) {
235
+ if (skipped.kind === "directory")
236
+ continue;
237
+ addMapping(dirnamePath(skipped.templatePath), dirnamePath(skipped.templatePath));
238
+ }
239
+ return mappings;
240
+ }
241
+ function inferTemplatePath(outputPath, directoryMap) {
242
+ const normalized = normalizePath(outputPath);
243
+ const outputDir = dirnamePath(normalized);
244
+ let bestOutputDir = "";
245
+ let bestTemplateDir = "";
246
+ for (const [mappedOutputDir, mappedTemplateDir] of directoryMap) {
247
+ if (mappedOutputDir.length >= bestOutputDir.length && (outputDir === mappedOutputDir || outputDir.startsWith(`${mappedOutputDir}/`))) {
248
+ bestOutputDir = mappedOutputDir;
249
+ bestTemplateDir = mappedTemplateDir;
250
+ }
251
+ }
252
+ const suffix = bestOutputDir === "" ? outputDir : outputDir.slice(bestOutputDir.length).replace(/^\//, "");
253
+ return joinPath(bestTemplateDir, suffix, basenamePath(normalized));
254
+ }
255
+ function walkFiles(root, excludedRoots = []) {
256
+ const files = [];
257
+ const pending = [root];
258
+ while (pending.length > 0) {
259
+ const current = pending.pop();
260
+ for (const entry of readdirSync(current).sort()) {
261
+ const path = resolvePath(current, entry);
262
+ if (excludedRoots.some((excluded) => isInsidePath(excluded, path)))
263
+ continue;
264
+ const stat = statSync(path);
265
+ if (stat.isDirectory()) {
266
+ pending.push(path);
267
+ } else if (stat.isFile()) {
268
+ files.push(normalizePath(relative(root, path)));
269
+ }
270
+ }
271
+ }
272
+ return files;
273
+ }
274
+ function copyIncludedRenderedFiles(renderedRoot, templateRoot, includedOutputPaths, directoryMap) {
275
+ let filesWritten = 0;
276
+ for (const outputPath of includedOutputPaths) {
277
+ const renderedPath = resolveInside(renderedRoot, outputPath);
278
+ const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap));
279
+ mkdirSync(dirname(templatePath), { recursive: true });
280
+ copyFileSync(renderedPath, templatePath);
281
+ filesWritten += 1;
282
+ }
283
+ return filesWritten;
284
+ }
285
+ function getIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, include) {
286
+ const matchers = getIncludeMatchers(include);
287
+ if (matchers.length === 0)
288
+ return [];
289
+ const mappedOutputPaths = new Set(manifest.files.map((file) => normalizePath(file.outputPath)));
290
+ const mapRelativePath = normalizePath(relative(renderedRoot, mapPath));
291
+ return walkFiles(renderedRoot, [templateRoot]).filter((outputPath) => {
292
+ return outputPath !== mapRelativePath && !mappedOutputPaths.has(outputPath) && matchesAny(outputPath, matchers);
293
+ });
294
+ }
162
295
  function reverseDir(renderedDir, templateDir, options = {}) {
163
296
  const renderedRoot = resolvePath(renderedDir);
164
297
  const templateRoot = resolvePath(templateDir);
165
298
  const mapPath = options.mapPath ? resolvePath(renderedRoot, options.mapPath) : resolvePath(renderedRoot, ".tdir-map.json");
166
299
  const manifest = readManifest(mapPath);
300
+ const directoryMap = buildDirectoryMap(manifest);
301
+ const includedOutputPaths = getIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, options.include);
167
302
  const warnings = [];
168
303
  let filesWritten = 0;
169
304
  for (const file of manifest.files) {
@@ -198,6 +333,7 @@ function reverseDir(renderedDir, templateDir, options = {}) {
198
333
  for (const skipped of manifest.skipped ?? []) {
199
334
  filesWritten += writeSkippedTemplate(templateRoot, skipped);
200
335
  }
336
+ filesWritten += copyIncludedRenderedFiles(renderedRoot, templateRoot, includedOutputPaths, directoryMap);
201
337
  return { filesWritten, warnings };
202
338
  }
203
339
 
@@ -206,7 +342,7 @@ function printHelp() {
206
342
  console.log(`tdir
207
343
 
208
344
  Usage:
209
- tdir reverse <rendered-dir> <template-dir> [--map <path>]
345
+ tdir reverse <rendered-dir> <template-dir> [--map <path>] [--include <glob>...]
210
346
 
211
347
  Commands:
212
348
  reverse Rebuild template files from a rendered directory and reverse map
@@ -214,16 +350,18 @@ Commands:
214
350
  Options:
215
351
  --map Reverse map path. Defaults to <rendered-dir>/.tdir-map.json.
216
352
  Relative paths are resolved from <rendered-dir>.
353
+ --include Include new rendered files matching a glob. Can be repeated.
217
354
  --help Show this help message.
218
355
  `);
219
356
  }
220
357
  function parseReverseArgs(args) {
221
358
  const positional = [];
359
+ const include = [];
222
360
  let mapPath;
223
361
  for (let i = 0;i < args.length; i++) {
224
362
  const arg = args[i];
225
363
  if (arg === "--help" || arg === "-h") {
226
- return { help: true, positional, mapPath };
364
+ return { help: true, positional, mapPath, include };
227
365
  }
228
366
  if (arg === "--map") {
229
367
  const value = args[++i];
@@ -232,9 +370,16 @@ function parseReverseArgs(args) {
232
370
  mapPath = value;
233
371
  continue;
234
372
  }
373
+ if (arg === "--include") {
374
+ const value = args[++i];
375
+ if (!value)
376
+ throw new Error("Missing value for --include");
377
+ include.push(value);
378
+ continue;
379
+ }
235
380
  positional.push(arg);
236
381
  }
237
- return { help: false, positional, mapPath };
382
+ return { help: false, positional, mapPath, include };
238
383
  }
239
384
  function main(argv) {
240
385
  const [command, ...args] = argv;
@@ -252,9 +397,12 @@ function main(argv) {
252
397
  }
253
398
  const [renderedDir, templateDir] = parsed.positional;
254
399
  if (!renderedDir || !templateDir || parsed.positional.length > 2) {
255
- throw new Error("Usage: tdir reverse <rendered-dir> <template-dir> [--map <path>]");
400
+ throw new Error("Usage: tdir reverse <rendered-dir> <template-dir> [--map <path>] [--include <glob>...]");
256
401
  }
257
- const result = reverseDir(renderedDir, templateDir, { mapPath: parsed.mapPath });
402
+ const result = reverseDir(renderedDir, templateDir, {
403
+ mapPath: parsed.mapPath,
404
+ include: parsed.include
405
+ });
258
406
  console.log(`Wrote ${result.filesWritten} file${result.filesWritten === 1 ? "" : "s"}`);
259
407
  for (const warning of result.warnings) {
260
408
  console.warn(`Warning: ${warning.outputPath}: ${warning.message}`);
package/dist/index.js CHANGED
@@ -492,6 +492,7 @@ import {
492
492
  existsSync,
493
493
  mkdirSync as mkdirSync2,
494
494
  readFileSync as readFileSync3,
495
+ readdirSync as readdirSync3,
495
496
  statSync as statSync3,
496
497
  writeFileSync as writeFileSync2
497
498
  } from "node:fs";
@@ -525,6 +526,45 @@ function readManifest(mapPath) {
525
526
  }
526
527
  return manifest;
527
528
  }
529
+ function normalizePath(path) {
530
+ return path.split("\\").join("/");
531
+ }
532
+ function escapeRegExp(text) {
533
+ return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
534
+ }
535
+ function globToRegExp(glob) {
536
+ let source = "^";
537
+ const pattern = normalizePath(glob);
538
+ for (let i = 0;i < pattern.length; i++) {
539
+ const char = pattern[i];
540
+ const next = pattern[i + 1];
541
+ if (char === "*" && next === "*") {
542
+ if (pattern[i + 2] === "/") {
543
+ source += "(?:.*/)?";
544
+ i += 2;
545
+ } else {
546
+ source += ".*";
547
+ i += 1;
548
+ }
549
+ } else if (char === "*") {
550
+ source += "[^/]*";
551
+ } else if (char === "?") {
552
+ source += "[^/]";
553
+ } else {
554
+ source += escapeRegExp(char);
555
+ }
556
+ }
557
+ return new RegExp(`${source}$`);
558
+ }
559
+ function getIncludeMatchers(include) {
560
+ if (!include)
561
+ return [];
562
+ return (Array.isArray(include) ? include : [include]).map(globToRegExp);
563
+ }
564
+ function matchesAny(path, matchers) {
565
+ const normalized = normalizePath(path);
566
+ return matchers.some((matcher) => matcher.test(normalized));
567
+ }
528
568
  function replaceAtRange(content, token) {
529
569
  if (!token.range)
530
570
  return null;
@@ -645,11 +685,106 @@ function writeSkippedTemplate(templateRoot, skipped) {
645
685
  writeFileSync2(templatePath, content);
646
686
  return 1;
647
687
  }
688
+ function dirnamePath(path) {
689
+ const normalized = normalizePath(path);
690
+ const index = normalized.lastIndexOf("/");
691
+ return index === -1 ? "" : normalized.slice(0, index);
692
+ }
693
+ function basenamePath(path) {
694
+ const normalized = normalizePath(path);
695
+ const index = normalized.lastIndexOf("/");
696
+ return index === -1 ? normalized : normalized.slice(index + 1);
697
+ }
698
+ function joinPath(...parts) {
699
+ return parts.filter((part) => part !== "").join("/");
700
+ }
701
+ function buildDirectoryMap(manifest) {
702
+ const mappings = new Map([["", ""]]);
703
+ function addMapping(outputDir, templateDir) {
704
+ const outputParts = normalizePath(outputDir).split("/").filter(Boolean);
705
+ const templateParts = normalizePath(templateDir).split("/").filter(Boolean);
706
+ for (let i = 1;i <= outputParts.length; i++) {
707
+ if (i <= templateParts.length) {
708
+ mappings.set(outputParts.slice(0, i).join("/"), templateParts.slice(0, i).join("/"));
709
+ }
710
+ }
711
+ mappings.set(normalizePath(outputDir), normalizePath(templateDir));
712
+ }
713
+ for (const file of manifest.files) {
714
+ const output = normalizePath(file.outputPath);
715
+ const template = normalizePath(file.templatePath);
716
+ addMapping(dirnamePath(output), dirnamePath(template));
717
+ if (file.tokens.some((token) => token.kind === "path"))
718
+ addMapping(output, template);
719
+ }
720
+ for (const skipped of manifest.skipped ?? []) {
721
+ if (skipped.kind === "directory")
722
+ continue;
723
+ addMapping(dirnamePath(skipped.templatePath), dirnamePath(skipped.templatePath));
724
+ }
725
+ return mappings;
726
+ }
727
+ function inferTemplatePath(outputPath, directoryMap) {
728
+ const normalized = normalizePath(outputPath);
729
+ const outputDir = dirnamePath(normalized);
730
+ let bestOutputDir = "";
731
+ let bestTemplateDir = "";
732
+ for (const [mappedOutputDir, mappedTemplateDir] of directoryMap) {
733
+ if (mappedOutputDir.length >= bestOutputDir.length && (outputDir === mappedOutputDir || outputDir.startsWith(`${mappedOutputDir}/`))) {
734
+ bestOutputDir = mappedOutputDir;
735
+ bestTemplateDir = mappedTemplateDir;
736
+ }
737
+ }
738
+ const suffix = bestOutputDir === "" ? outputDir : outputDir.slice(bestOutputDir.length).replace(/^\//, "");
739
+ return joinPath(bestTemplateDir, suffix, basenamePath(normalized));
740
+ }
741
+ function walkFiles(root, excludedRoots = []) {
742
+ const files = [];
743
+ const pending = [root];
744
+ while (pending.length > 0) {
745
+ const current = pending.pop();
746
+ for (const entry of readdirSync3(current).sort()) {
747
+ const path = resolvePath2(current, entry);
748
+ if (excludedRoots.some((excluded) => isInsidePath2(excluded, path)))
749
+ continue;
750
+ const stat = statSync3(path);
751
+ if (stat.isDirectory()) {
752
+ pending.push(path);
753
+ } else if (stat.isFile()) {
754
+ files.push(normalizePath(relative2(root, path)));
755
+ }
756
+ }
757
+ }
758
+ return files;
759
+ }
760
+ function copyIncludedRenderedFiles(renderedRoot, templateRoot, includedOutputPaths, directoryMap) {
761
+ let filesWritten = 0;
762
+ for (const outputPath of includedOutputPaths) {
763
+ const renderedPath = resolveInside(renderedRoot, outputPath);
764
+ const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap));
765
+ mkdirSync2(dirname2(templatePath), { recursive: true });
766
+ copyFileSync2(renderedPath, templatePath);
767
+ filesWritten += 1;
768
+ }
769
+ return filesWritten;
770
+ }
771
+ function getIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, include) {
772
+ const matchers = getIncludeMatchers(include);
773
+ if (matchers.length === 0)
774
+ return [];
775
+ const mappedOutputPaths = new Set(manifest.files.map((file) => normalizePath(file.outputPath)));
776
+ const mapRelativePath = normalizePath(relative2(renderedRoot, mapPath));
777
+ return walkFiles(renderedRoot, [templateRoot]).filter((outputPath) => {
778
+ return outputPath !== mapRelativePath && !mappedOutputPaths.has(outputPath) && matchesAny(outputPath, matchers);
779
+ });
780
+ }
648
781
  function reverseDir(renderedDir, templateDir, options = {}) {
649
782
  const renderedRoot = resolvePath2(renderedDir);
650
783
  const templateRoot = resolvePath2(templateDir);
651
784
  const mapPath = options.mapPath ? resolvePath2(renderedRoot, options.mapPath) : resolvePath2(renderedRoot, ".tdir-map.json");
652
785
  const manifest = readManifest(mapPath);
786
+ const directoryMap = buildDirectoryMap(manifest);
787
+ const includedOutputPaths = getIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, options.include);
653
788
  const warnings = [];
654
789
  let filesWritten = 0;
655
790
  for (const file of manifest.files) {
@@ -684,6 +819,7 @@ function reverseDir(renderedDir, templateDir, options = {}) {
684
819
  for (const skipped of manifest.skipped ?? []) {
685
820
  filesWritten += writeSkippedTemplate(templateRoot, skipped);
686
821
  }
822
+ filesWritten += copyIncludedRenderedFiles(renderedRoot, templateRoot, includedOutputPaths, directoryMap);
687
823
  return { filesWritten, warnings };
688
824
  }
689
825
 
package/dist/reverse.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type ReverseOptions = {
2
2
  mapPath?: string;
3
+ include?: string | string[];
3
4
  };
4
5
  export type ReverseWarning = {
5
6
  outputPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gregorlohaus/tdir",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",