@gregorlohaus/tdir 0.1.4 → 0.1.5

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,6 +208,21 @@ 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
 
213
228
  ## Unmatched directives
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,6 +199,95 @@ 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, current = root) {
256
+ const files = [];
257
+ for (const entry of readdirSync(current).sort()) {
258
+ const path = resolvePath(current, entry);
259
+ const stat = statSync(path);
260
+ if (stat.isDirectory()) {
261
+ files.push(...walkFiles(root, path));
262
+ } else if (stat.isFile()) {
263
+ files.push(normalizePath(relative(root, path)));
264
+ }
265
+ }
266
+ return files;
267
+ }
268
+ function copyIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, include) {
269
+ const matchers = getIncludeMatchers(include);
270
+ if (matchers.length === 0)
271
+ return 0;
272
+ const mappedOutputPaths = new Set(manifest.files.map((file) => normalizePath(file.outputPath)));
273
+ const mapRelativePath = normalizePath(relative(renderedRoot, mapPath));
274
+ const directoryMap = buildDirectoryMap(manifest);
275
+ let filesWritten = 0;
276
+ for (const outputPath of walkFiles(renderedRoot)) {
277
+ if (outputPath === mapRelativePath)
278
+ continue;
279
+ if (mappedOutputPaths.has(outputPath))
280
+ continue;
281
+ if (!matchesAny(outputPath, matchers))
282
+ continue;
283
+ const renderedPath = resolveInside(renderedRoot, outputPath);
284
+ const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap));
285
+ mkdirSync(dirname(templatePath), { recursive: true });
286
+ copyFileSync(renderedPath, templatePath);
287
+ filesWritten += 1;
288
+ }
289
+ return filesWritten;
290
+ }
162
291
  function reverseDir(renderedDir, templateDir, options = {}) {
163
292
  const renderedRoot = resolvePath(renderedDir);
164
293
  const templateRoot = resolvePath(templateDir);
@@ -198,6 +327,7 @@ function reverseDir(renderedDir, templateDir, options = {}) {
198
327
  for (const skipped of manifest.skipped ?? []) {
199
328
  filesWritten += writeSkippedTemplate(templateRoot, skipped);
200
329
  }
330
+ filesWritten += copyIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, options.include);
201
331
  return { filesWritten, warnings };
202
332
  }
203
333
 
@@ -206,7 +336,7 @@ function printHelp() {
206
336
  console.log(`tdir
207
337
 
208
338
  Usage:
209
- tdir reverse <rendered-dir> <template-dir> [--map <path>]
339
+ tdir reverse <rendered-dir> <template-dir> [--map <path>] [--include <glob>...]
210
340
 
211
341
  Commands:
212
342
  reverse Rebuild template files from a rendered directory and reverse map
@@ -214,16 +344,18 @@ Commands:
214
344
  Options:
215
345
  --map Reverse map path. Defaults to <rendered-dir>/.tdir-map.json.
216
346
  Relative paths are resolved from <rendered-dir>.
347
+ --include Include new rendered files matching a glob. Can be repeated.
217
348
  --help Show this help message.
218
349
  `);
219
350
  }
220
351
  function parseReverseArgs(args) {
221
352
  const positional = [];
353
+ const include = [];
222
354
  let mapPath;
223
355
  for (let i = 0;i < args.length; i++) {
224
356
  const arg = args[i];
225
357
  if (arg === "--help" || arg === "-h") {
226
- return { help: true, positional, mapPath };
358
+ return { help: true, positional, mapPath, include };
227
359
  }
228
360
  if (arg === "--map") {
229
361
  const value = args[++i];
@@ -232,9 +364,16 @@ function parseReverseArgs(args) {
232
364
  mapPath = value;
233
365
  continue;
234
366
  }
367
+ if (arg === "--include") {
368
+ const value = args[++i];
369
+ if (!value)
370
+ throw new Error("Missing value for --include");
371
+ include.push(value);
372
+ continue;
373
+ }
235
374
  positional.push(arg);
236
375
  }
237
- return { help: false, positional, mapPath };
376
+ return { help: false, positional, mapPath, include };
238
377
  }
239
378
  function main(argv) {
240
379
  const [command, ...args] = argv;
@@ -252,9 +391,12 @@ function main(argv) {
252
391
  }
253
392
  const [renderedDir, templateDir] = parsed.positional;
254
393
  if (!renderedDir || !templateDir || parsed.positional.length > 2) {
255
- throw new Error("Usage: tdir reverse <rendered-dir> <template-dir> [--map <path>]");
394
+ throw new Error("Usage: tdir reverse <rendered-dir> <template-dir> [--map <path>] [--include <glob>...]");
256
395
  }
257
- const result = reverseDir(renderedDir, templateDir, { mapPath: parsed.mapPath });
396
+ const result = reverseDir(renderedDir, templateDir, {
397
+ mapPath: parsed.mapPath,
398
+ include: parsed.include
399
+ });
258
400
  console.log(`Wrote ${result.filesWritten} file${result.filesWritten === 1 ? "" : "s"}`);
259
401
  for (const warning of result.warnings) {
260
402
  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,6 +685,95 @@ 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, current = root) {
742
+ const files = [];
743
+ for (const entry of readdirSync3(current).sort()) {
744
+ const path = resolvePath2(current, entry);
745
+ const stat = statSync3(path);
746
+ if (stat.isDirectory()) {
747
+ files.push(...walkFiles(root, path));
748
+ } else if (stat.isFile()) {
749
+ files.push(normalizePath(relative2(root, path)));
750
+ }
751
+ }
752
+ return files;
753
+ }
754
+ function copyIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, include) {
755
+ const matchers = getIncludeMatchers(include);
756
+ if (matchers.length === 0)
757
+ return 0;
758
+ const mappedOutputPaths = new Set(manifest.files.map((file) => normalizePath(file.outputPath)));
759
+ const mapRelativePath = normalizePath(relative2(renderedRoot, mapPath));
760
+ const directoryMap = buildDirectoryMap(manifest);
761
+ let filesWritten = 0;
762
+ for (const outputPath of walkFiles(renderedRoot)) {
763
+ if (outputPath === mapRelativePath)
764
+ continue;
765
+ if (mappedOutputPaths.has(outputPath))
766
+ continue;
767
+ if (!matchesAny(outputPath, matchers))
768
+ continue;
769
+ const renderedPath = resolveInside(renderedRoot, outputPath);
770
+ const templatePath = resolveInside(templateRoot, inferTemplatePath(outputPath, directoryMap));
771
+ mkdirSync2(dirname2(templatePath), { recursive: true });
772
+ copyFileSync2(renderedPath, templatePath);
773
+ filesWritten += 1;
774
+ }
775
+ return filesWritten;
776
+ }
648
777
  function reverseDir(renderedDir, templateDir, options = {}) {
649
778
  const renderedRoot = resolvePath2(renderedDir);
650
779
  const templateRoot = resolvePath2(templateDir);
@@ -684,6 +813,7 @@ function reverseDir(renderedDir, templateDir, options = {}) {
684
813
  for (const skipped of manifest.skipped ?? []) {
685
814
  filesWritten += writeSkippedTemplate(templateRoot, skipped);
686
815
  }
816
+ filesWritten += copyIncludedRenderedFiles(renderedRoot, templateRoot, mapPath, manifest, options.include);
687
817
  return { filesWritten, warnings };
688
818
  }
689
819
 
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.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",