1 | import {getPathNodes, normalizePathNodes} from "./utils";
|
2 |
|
3 | export const pathSeparator = '/';
|
4 |
|
5 |
|
6 | export type DirectoryContent = { [key: string]: string | DirectoryContent }
|
7 |
|
8 |
|
9 | export interface FileSystemNode {
|
10 | type: 'dir' | 'file';
|
11 | name: string,
|
12 | fullPath: string,
|
13 | }
|
14 |
|
15 | export class ShallowDirectory implements FileSystemNode {
|
16 | public type: 'dir' = 'dir';
|
17 |
|
18 | constructor(public name: string,
|
19 | public fullPath: string,) {
|
20 | }
|
21 | }
|
22 |
|
23 | export class Directory implements FileSystemNode {
|
24 | public type: 'dir' = 'dir';
|
25 |
|
26 | constructor(public name: string,
|
27 | public fullPath: string,
|
28 | public children: Array<File | Directory> = []) {
|
29 | }
|
30 |
|
31 | static getSubDir(directory: Directory, path: string | string[]): Directory | null {
|
32 | const pathArr = typeof path === 'string' ? getPathNodes(path) : path;
|
33 | while (pathArr.length) {
|
34 | const targetName = pathArr.shift();
|
35 | if (targetName && directory.children) {
|
36 | const node = directory.children.find(({name}) => name === targetName);
|
37 | if (isDir(node)) {
|
38 | directory = node;
|
39 | } else {
|
40 | return null;
|
41 | }
|
42 | } else {
|
43 | return null;
|
44 | }
|
45 | }
|
46 | return directory;
|
47 | }
|
48 |
|
49 | static clone(node: Directory, path: string | string[] = []): Directory {
|
50 | const pathArr = typeof path === 'string' ? getPathNodes(path) : path;
|
51 | if (!pathArr.length && node.name) {
|
52 | pathArr.push(node.name);
|
53 | }
|
54 | return new Directory(
|
55 | pathArr.length ? pathArr[pathArr.length - 1] : node.name,
|
56 | normalizePathNodes(pathArr),
|
57 | node.children.map(child => {
|
58 | let childPath = pathArr.concat([child.name]);
|
59 | if (isDir(child)) {
|
60 | return this.clone(child, childPath);
|
61 | } else {
|
62 | return new File(child.name, normalizePathNodes(childPath), child.content);
|
63 | }
|
64 | })
|
65 | )
|
66 | }
|
67 |
|
68 | static cloneStructure(node: Directory): Directory {
|
69 | return new Directory(
|
70 | node.name,
|
71 | node.fullPath,
|
72 | node.children.map(child => isDir(child) ? this.cloneStructure(child) : new File(child.name, child.fullPath))
|
73 | )
|
74 | }
|
75 |
|
76 | static fromContent(content: DirectoryContent, name: string = '', location: string = ''): Directory {
|
77 | if (location === pathSeparator) {
|
78 | location = '';
|
79 | } else if (location.length && !location.endsWith(pathSeparator)) {
|
80 | location = location + pathSeparator;
|
81 | }
|
82 | let path = location.length ? location + name : name;
|
83 | let childLocation = path.length ? path + pathSeparator : name;
|
84 | return new Directory(name, path,
|
85 | Object.keys(content).map(contentPartPath => {
|
86 | const fileContent = content[contentPartPath];
|
87 | const fullPath = childLocation + contentPartPath;
|
88 | if (typeof fileContent === 'string') {
|
89 | return new File(contentPartPath, fullPath, fileContent);
|
90 | } else {
|
91 | return this.fromContent(fileContent, contentPartPath, childLocation);
|
92 | }
|
93 | }));
|
94 | }
|
95 |
|
96 | static toContent(directory: Directory): DirectoryContent {
|
97 | const result: DirectoryContent = {};
|
98 | directory.children.forEach(child => {
|
99 | if (child.type === 'file') {
|
100 | if (child.content === undefined) {
|
101 | throw new Error('file not loaded : ' + child.fullPath);
|
102 | }
|
103 | result[child.name] = child.content;
|
104 | } else if (child.type === 'dir') {
|
105 | result[child.name] = this.toContent(child);
|
106 | }
|
107 | });
|
108 | return result;
|
109 | }
|
110 |
|
111 | |
112 |
|
113 |
|
114 |
|
115 | static mix(subject: Directory, mixin: Directory): Directory {
|
116 | mixin.children.forEach(mixChild => {
|
117 | const subjChild = subject.children.find(({name}) => name === mixChild.name);
|
118 | if (isFile(mixChild)) {
|
119 | if (mixChild.content === undefined) {
|
120 | throw new Error('file not loaded : ' + mixChild.fullPath);
|
121 | }
|
122 | if (subjChild) {
|
123 | if (isFile(subjChild)) {
|
124 | subjChild.content = mixChild.content;
|
125 | } else if (isDir(subjChild)) {
|
126 | throw new Error(`can't override directory with file : ${mixChild.fullPath} , ${subjChild.fullPath}`);
|
127 | }
|
128 | } else {
|
129 | subject.children.push(new File(mixChild.name, normalizePathNodes([subject.fullPath, mixChild.name]), mixChild.content));
|
130 | }
|
131 | } else if (isDir(mixChild)) {
|
132 | if (subjChild) {
|
133 | if (isFile(subjChild)) {
|
134 | throw new Error(`can't file directory with directory : ${mixChild.fullPath} , ${subjChild.fullPath}`);
|
135 | } else if (isDir(subjChild)) {
|
136 | this.mix(subjChild, mixChild);
|
137 | }
|
138 | } else {
|
139 | subject.children.push(Directory.clone(mixChild, [subject.fullPath, mixChild.name]));
|
140 | }
|
141 | }
|
142 | });
|
143 |
|
144 | return subject;
|
145 | }
|
146 | }
|
147 |
|
148 | export class File implements FileSystemNode {
|
149 | public type: 'file' = 'file';
|
150 | public content?: string;
|
151 |
|
152 | constructor(public name: string,
|
153 | public fullPath: string,
|
154 | content?: string) {
|
155 | if (content) this.content = content;
|
156 | }
|
157 | }
|
158 |
|
159 | export function isFile(node?: FileSystemNode | null): node is File {
|
160 | if (!node) return false;
|
161 | return node.type === 'file';
|
162 | }
|
163 |
|
164 | export function isDir(node?: FileSystemNode | null): node is Directory {
|
165 | if (!node) return false;
|
166 | return node.type === 'dir';
|
167 | }
|