@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 +15 -0
- package/dist/cli.js +147 -5
- package/dist/index.js +130 -0
- package/dist/reverse.d.ts +1 -0
- package/package.json +1 -1
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, {
|
|
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