flatten-tool 1.4.0 → 1.5.0
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 +4 -0
- package/index.ts +58 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,10 @@ This project uses Bun for runtime, TypeScript for type safety, and follows the g
|
|
|
113
113
|
|
|
114
114
|
## Changelog
|
|
115
115
|
|
|
116
|
+
### v1.5.0
|
|
117
|
+
- Improved navigation: project file tree is now a clickable nested Markdown list with links to each file's content section using standard markdown anchors.
|
|
118
|
+
- Simplified file headers: removed custom anchors from section headers.
|
|
119
|
+
|
|
116
120
|
### v1.4.0
|
|
117
121
|
- Added project file tree to the beginning of merged Markdown output for better navigation.
|
|
118
122
|
|
package/index.ts
CHANGED
|
@@ -7,63 +7,65 @@ import yargs from 'yargs';
|
|
|
7
7
|
import { hideBin } from 'yargs/helpers';
|
|
8
8
|
import { globby } from 'globby';
|
|
9
9
|
import pkg from './package.json' assert { type: 'json' };
|
|
10
|
-
import treeify from 'treeify';
|
|
11
10
|
|
|
12
11
|
function escapePathComponent(component: string): string {
|
|
13
12
|
return component.replace(/_/g, '__');
|
|
14
13
|
}
|
|
15
14
|
|
|
15
|
+
function generateMarkdownAnchor(text: string): string {
|
|
16
|
+
return text
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^\w\s-]/g, '') // Remove punctuation except hyphens and underscores
|
|
19
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
20
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
function buildTreeObject(relPaths: string[]): any {
|
|
17
24
|
const tree: any = {};
|
|
18
|
-
|
|
19
|
-
relPaths.forEach((path) => {
|
|
20
|
-
// Normalize paths
|
|
21
|
-
path = path.replace(/\\/g, '/');
|
|
22
|
-
if (path.startsWith('./')) path = path.slice(2);
|
|
23
|
-
|
|
25
|
+
for (const path of relPaths) {
|
|
24
26
|
const parts = path.split('/');
|
|
25
27
|
let node = tree;
|
|
26
|
-
|
|
27
|
-
parts.
|
|
28
|
+
const currentParts: string[] = [];
|
|
29
|
+
for (const [index, part] of parts.entries()) {
|
|
30
|
+
currentParts.push(part);
|
|
28
31
|
const isDir = index < parts.length - 1;
|
|
29
32
|
const key = isDir ? `${part}/` : part;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
node[key] = isDir ? {} : null;
|
|
33
|
+
if (node[key] === undefined) {
|
|
34
|
+
node[key] = isDir ? {} : currentParts.join('/');
|
|
33
35
|
}
|
|
34
|
-
|
|
35
36
|
if (isDir) {
|
|
36
37
|
node = node[key];
|
|
37
38
|
}
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Recursively sort: directories first, then files, case-insensitive
|
|
42
|
-
function sortNode(node: any): void {
|
|
43
|
-
if (node === null || typeof node !== 'object') return;
|
|
44
|
-
|
|
45
|
-
const entries = Object.entries(node);
|
|
46
|
-
entries.sort(([a], [b]) => {
|
|
47
|
-
const aIsDir = a.endsWith('/');
|
|
48
|
-
const bIsDir = b.endsWith('/');
|
|
49
|
-
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1; // dirs before files
|
|
50
|
-
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Rebuild node in sorted order (preserves insertion order for rendering)
|
|
54
|
-
const sorted: any = {};
|
|
55
|
-
entries.forEach(([key, value]) => {
|
|
56
|
-
sorted[key] = value;
|
|
57
|
-
sortNode(value);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
Object.keys(node).forEach((k) => delete node[k]);
|
|
61
|
-
Object.assign(node, sorted);
|
|
39
|
+
}
|
|
62
40
|
}
|
|
41
|
+
return tree;
|
|
42
|
+
}
|
|
63
43
|
|
|
64
|
-
|
|
44
|
+
function renderMarkdownTree(node: any, depth: number): string {
|
|
45
|
+
let result = '';
|
|
46
|
+
const indent = ' '.repeat(depth);
|
|
47
|
+
const entries: [string, any][] = Object.entries(node);
|
|
65
48
|
|
|
66
|
-
|
|
49
|
+
entries.sort(([a], [b]) => {
|
|
50
|
+
const aDir = a.endsWith('/');
|
|
51
|
+
const bDir = b.endsWith('/');
|
|
52
|
+
if (aDir !== bDir) return aDir ? -1 : 1;
|
|
53
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of entries) {
|
|
57
|
+
const isDir = key.endsWith('/');
|
|
58
|
+
const display = isDir ? key.slice(0, -1) + '/' : key;
|
|
59
|
+
if (isDir) {
|
|
60
|
+
result += `${indent}- ${display}\n`;
|
|
61
|
+
result += renderMarkdownTree(value, depth + 1);
|
|
62
|
+
} else {
|
|
63
|
+
const fullPath = value as string;
|
|
64
|
+
const anchor = generateMarkdownAnchor(fullPath);
|
|
65
|
+
result += `${indent}- [${display}](#${anchor})\n`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
async function removeEmptyDirs(dir: string, root?: string): Promise<void> {
|
|
@@ -127,42 +129,40 @@ export async function flattenDirectory(
|
|
|
127
129
|
if (err.code !== 'ENOENT') throw err;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
// Sort files for consistent content order
|
|
133
|
+
const fileEntries = files.map(srcPath => ({
|
|
134
|
+
srcPath,
|
|
135
|
+
relPath: relative(absSource, srcPath).replace(/\\/g, '/')
|
|
136
|
+
}));
|
|
137
|
+
fileEntries.sort((a, b) => a.relPath.toLowerCase().localeCompare(b.relPath.toLowerCase()));
|
|
133
138
|
|
|
134
|
-
|
|
135
|
-
relPaths.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
139
|
+
const relPaths = fileEntries.map(e => e.relPath);
|
|
136
140
|
|
|
141
|
+
// Build tree structure
|
|
137
142
|
const treeObj = buildTreeObject(relPaths);
|
|
138
143
|
|
|
139
|
-
//
|
|
140
|
-
|
|
144
|
+
// Render as clickable nested Markdown list
|
|
145
|
+
let treeMarkdown = "# Project File Tree\n\n- .\n";
|
|
146
|
+
treeMarkdown += renderMarkdownTree(treeObj, 1);
|
|
147
|
+
treeMarkdown += "\n";
|
|
141
148
|
|
|
142
149
|
const writeStream = createWriteStream(absTarget);
|
|
143
150
|
writeStream.setMaxListeners(0);
|
|
144
151
|
|
|
145
|
-
writeStream.write(
|
|
146
|
-
writeStream.write("```\n");
|
|
147
|
-
writeStream.write(treeify.asTree(rootTree));
|
|
148
|
-
writeStream.write("```\n\n");
|
|
152
|
+
writeStream.write(treeMarkdown);
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
// Write file contents (now in sorted order)
|
|
155
|
+
for (const { srcPath, relPath } of fileEntries) {
|
|
152
156
|
let ext = extname(srcPath).slice(1) || 'text';
|
|
157
|
+
const lang = ext;
|
|
153
158
|
const isMd = ['md', 'markdown'].includes(ext.toLowerCase());
|
|
154
159
|
const ticks = isMd ? '````' : '```';
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
writeStream.write(`# ${relPath}\n\n${ticks}${ext}\n`);
|
|
161
|
+
writeStream.write(`## ${relPath}\n\n${ticks}${lang}\n`);
|
|
158
162
|
|
|
159
|
-
// Stream file content (preserve original UTF-8 text behavior)
|
|
160
163
|
const readStream = createReadStream(srcPath, { encoding: 'utf8' });
|
|
161
|
-
|
|
162
|
-
// Pipe with { end: false } so writeStream stays open for next files
|
|
163
164
|
await pipeline(readStream, writeStream, { end: false });
|
|
164
165
|
|
|
165
|
-
// Write closing fence
|
|
166
166
|
writeStream.write(`\n${ticks}\n\n`);
|
|
167
167
|
}
|
|
168
168
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flatten-tool",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "CLI tool to flatten directory structures: merge files into a single Markdown file (default) or copy/move to a flat directory with escaped filenames. Respects .gitignore, supports move/overwrite, and more.",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|