/
node.ts
84 lines (80 loc) · 2.53 KB
/
node.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import path from 'path';
import assert from 'assert';
import { TreeNode } from './types';
/** Visit the tree node and its children in DFS. */
export const visitTree = (
source: TreeNode,
handler: (src: TreeNode, parent?: TreeNode) => void,
parent?: TreeNode,
) => {
source.children?.forEach(child => visitTree(child, handler, source));
handler(source, parent);
};
export interface CreateTreeFromFilesOptions {
cwd?: string;
processor?: (node: TreeNode, filename: string, parent?: TreeNode) => void;
absolute?: boolean;
combine?: boolean;
}
/** Create a tree from a list of files. */
export const createTreeFromFiles = (
files: readonly string[],
options: CreateTreeFromFilesOptions = {},
) => {
assert(files.length, 'files should not be empty');
const cwd = options.cwd || process.cwd();
const mapping: Record<string, TreeNode> = {};
// Create path parts map.
const partsMap = new Map<string[], true>();
for (const p of files) {
const parts = path.resolve(cwd, p).split(path.sep);
parts.length > 1 && partsMap.set(parts, true);
}
// Consume path parts map to create tree.
let depth = 1;
while (partsMap.size) {
for (const [parts] of partsMap) {
const p = parts.slice(0, depth).join(path.sep) || path.sep;
if (!mapping[p]) {
mapping[p] = {
name: p,
children: [],
};
const dir = path.dirname(p);
dir !== p && mapping[dir].children?.push(mapping[p]);
}
parts.length <= depth && partsMap.delete(parts);
}
depth++;
}
const root = mapping[path.sep];
// Organize tree structure.
visitTree(root, (src, parent) => {
// Remove empty children.
src.children?.length || delete src.children;
// Combine directories with only one child.
if (options.combine !== false && parent && parent.children?.length === 1) {
parent.name = src.name;
parent.children = src.children;
}
});
if (options.combine === false) {
while (root.children?.length === 1) {
const child = root.children[0];
root.name = path.resolve(root.name, child.name);
root.children = child.children;
}
}
// Resolve name to relative path and run custom node processor.
const needVisit = !options.absolute || Boolean(options.processor);
needVisit &&
visitTree(root, (src, parent) => {
const filename = src.name;
if (!options.absolute) {
src.name = path.relative(parent?.name || cwd, src.name);
src.name ||= '.';
}
options.processor?.(src, filename, parent);
});
return root;
};