Skip to content

Commit 66d9c70

Browse files
authored
chore: implement an unified fs-tree for front & back (#14)
1 parent 783d6a9 commit 66d9c70

27 files changed

+1658
-154
lines changed

package-lock.json

Lines changed: 415 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"ci:version": "changeset version"
1212
},
1313
"workspaces": [
14-
"packages/tree-view",
1514
"packages/project",
1615
"packages/server",
17-
"packages/client"
16+
"packages/client",
17+
"packages/fs-tree"
1818
],
1919
"repository": {
2020
"type": "git",

packages/fs-tree/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<p align="center"><h1 align="center">
2+
fs-tree
3+
</h1>
4+
5+
<p align="center">
6+
A front-end and back-end implementation of FS / Assets tree
7+
</p>
8+
9+
## Getting Started
10+
11+
This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).
12+
13+
```bash
14+
$ npm i @jolly-pixel/fs-tree
15+
# or
16+
$ yarn add @jolly-pixel/fs-tree
17+
```
18+
19+
## Usage example
20+
21+
TBC

packages/fs-tree/docs/FSTree.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# FSTree
2+
3+
Represents a file system tree, extending `EventEmitter` to emit events on file system changes.
4+
5+
## Signature
6+
7+
```typescript
8+
class FSTree<T extends FSTreeFile = FSTreeFile> extends EventEmitter {
9+
constructor(dir: string | URL, options?: FSTreeOptions<T>);
10+
11+
static loadFromPath<T extends FSTreeFile = FSTreeFile>(
12+
location: string | URL,
13+
fileMapFn?: FSTreeFileMap<T>
14+
): Promise<FSTree<T>>;
15+
16+
get root(): string;
17+
18+
prevent(tagOrOmitNumber: number | NonNullable<FSTreeTag>): this;
19+
20+
mkdir(location: string | FSTreeOptionalDirent): boolean;
21+
rmdir(location: string | FSTreeOptionalDirent): boolean;
22+
mvdir(
23+
location: string | FSTreeOptionalDirent,
24+
newLocation: string | FSTreeOptionalDirent
25+
): boolean;
26+
27+
append(dirent: FSTreeOptionalDirent, file?: T | null): boolean;
28+
update(
29+
dirent: FSTreeOptionalDirent,
30+
fileUpdateMapFn: FSTreeFileUpdateMap<T>
31+
): boolean;
32+
unlink(dirent: FSTreeOptionalDirent): T | null;
33+
copy(
34+
dirent: FSTreeOptionalDirent,
35+
newLocation: string | FSTreeOptionalDirent
36+
): boolean;
37+
38+
readdir(
39+
location: string,
40+
options?: { recursive?: boolean; absolutePath?: boolean }
41+
): Iterable<T>;
42+
}
43+
```
44+
45+
## Usage example
46+
47+
```typescript
48+
import { FSTree } from "./FSTree.class.js";
49+
50+
const tree = await FSTree.loadFromPath("/path/to/another/directory");
51+
52+
// Example: Creating a directory
53+
tree.mkdir("new-directory");
54+
55+
// Example: Appending a file
56+
tree.append({ name: "new-file.txt", parentPath: "new-directory/" });
57+
```
58+
59+
## Methods
60+
61+
### constructor(dir: string | URL, options: FSTreeOptions<T> = {})
62+
63+
Creates a new `FSTree` instance, initializing the tree with the given root directory.
64+
65+
### static async loadFromPath<T extends FSTreeFile = FSTreeFile>(location: string | URL, fileMapFn?: FSTreeFileMap<T>): Promise<FSTree<T>>
66+
67+
Asynchronously loads a file tree from a specified path. This static method is useful for initializing an `FSTree` instance from an existing directory structure.
68+
69+
### get root(): string
70+
71+
Returns the absolute path of the root directory managed by this `FSTree` instance.
72+
73+
### prevent(tagOrOmitNumber: number | NonNullable<FSTreeTag>): this
74+
75+
Prevents the emission of events for a specified number of operations or for operations associated with a given tag. This can be useful for batch operations where you only want to emit events once at the end.
76+
77+
### mkdir(location: string | FSTreeOptionalDirent): boolean
78+
79+
Creates a new directory within the tree. Returns `true` if the directory was created, `false` otherwise (e.g., if it already exists).
80+
81+
### rmdir(location: string | FSTreeOptionalDirent): boolean
82+
83+
Deletes an existing directory from the tree. Returns `true` if the directory was deleted, `false` otherwise.
84+
85+
### mvdir(location: string | FSTreeOptionalDirent, newLocation: string | FSTreeOptionalDirent): boolean
86+
87+
Moves a directory from one location to another within the tree. Returns `true` if the move was successful, `false` otherwise.
88+
89+
### append(dirent: FSTreeOptionalDirent, file?: T | null): boolean
90+
91+
Adds a file or directory entry (`dirent`) to the tree. If `file` is provided, it will be associated with the `dirent`. Returns `true` if the entry was added, `false` otherwise.
92+
93+
### update(dirent: FSTreeOptionalDirent, fileUpdateMapFn: FSTreeFileUpdateMap<T>): boolean
94+
95+
Updates an existing file in the tree using a provided `fileUpdateMapFn`. This function receives the current file object and should return the updated file object. Returns `true` if the file was updated, `false` otherwise.
96+
97+
### unlink(dirent: FSTreeOptionalDirent): T | null
98+
99+
Removes a file from the tree. Returns the removed file object if successful, `null` otherwise.
100+
101+
### copy(dirent: FSTreeOptionalDirent, newLocation: string | FSTreeOptionalDirent): boolean
102+
103+
Copies a file from its current location to a new location within the tree. Returns `true` if the copy was successful, `false` otherwise.
104+
105+
### * readdir(location: string, options: { recursive?: boolean; absolutePath?: boolean; } = {}): Iterable<T>
106+
107+
Reads the contents of a directory within the tree. Can optionally read recursively and return absolute paths. Returns an iterable of `FSTreeFile` objects.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# FSTreeSynchronizer
2+
3+
Synchronizes an `FSTree` instance with the actual file system, ensuring consistency between the in-memory tree and disk.
4+
5+
## Signature
6+
7+
```typescript
8+
class FSTreeSynchronizer {
9+
constructor(tree: FSTree, options?: FSTreeSynchronizerOptions);
10+
11+
async synchronize(): Promise<void>;
12+
async close(): Promise<void>;
13+
}
14+
```
15+
16+
## Usage example
17+
18+
```typescript
19+
import { FSTree, FSTreeSynchronizer } from "@jolly-pixel/fs-tree";
20+
21+
const tree = await FSTree.loadFromPath(process.cwd());
22+
{
23+
await using synchronizer = new FSTreeSynchronizer(tree);
24+
25+
// Perform some operations on the tree (e.g., delete a directory)
26+
tree.rmdir("/foobar");
27+
}
28+
29+
// directory is deleted from FS
30+
```
31+
32+
## Methods
33+
34+
### constructor(tree: FSTree, options: FSTreeSynchronizerOptions = {})
35+
36+
Creates a new `FSTreeSynchronizer` instance, associating it with a specific `FSTree` to monitor and synchronize its changes.
37+
38+
### async synchronize(): Promise<void>
39+
40+
Asynchronously synchronizes all pending operations (e.g., file creations, deletions, moves) from the `FSTree` instance to the underlying file system. This method ensures that the disk state reflects the in-memory tree state.
41+
42+
### async close(): Promise<void>
43+
44+
Asynchronously stops the synchronization process and releases any resources held by the synchronizer, such as file handles or watchers. It's important to call this method when the synchronizer is no longer needed to prevent resource leaks.
Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
1-
<p align="center"><h1 align="center">
2-
Tree-View
3-
</h1>
1+
# TreeView
42

5-
<p align="center">
6-
A modern fork/re-implementation of <a href="https://github.com/sparklinlabs/dnd-tree-view">dnd-tree-view</a> from Sparklin Labs, rewritten to be fully compatible with the Web Platform using <code>EventTarget</code> and native <code>ES2022</code> features.
7-
</p>
8-
9-
## Getting Started
10-
11-
This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).
12-
13-
```bash
14-
$ npm i @jolly-pixel/tree-view
15-
# or
16-
$ yarn add @jolly-pixel/tree-view
17-
```
3+
A modern fork/re-implementation of <a href="https://github.com/sparklinlabs/dnd-tree-view">dnd-tree-view</a> from Sparklin Labs, rewritten to be fully compatible with the Web Platform using <code>EventTarget</code> and native <code>ES2022</code> features.
184

195
## Usage example
206

packages/fs-tree/index.html

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Tree-View</title>
8+
<link rel="stylesheet" href="./public/main.css">
9+
</head>
10+
11+
<body>
12+
<nav>
13+
<button class="create-item" data-type="item">Create item</button>
14+
<button class="create-group" data-type="group">Create group</button>
15+
<button class="remove-selected">Remove Selected</button>
16+
<div class="selected-nodes">No items selected</div>
17+
</nav>
18+
<main>
19+
</main>
20+
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
21+
<script type="module">
22+
import { TreeView } from "./src/frontend/TreeView.class.ts";
23+
24+
const socket = io("http://localhost:3000");
25+
26+
socket.on("connect", () => {
27+
console.log("Connected to WebSocket server!");
28+
});
29+
30+
const treeView = new TreeView(
31+
document.querySelector("main"),
32+
{
33+
dragStartCallback(event, node) {
34+
event.dataTransfer.setData("text/plain", node.textContent);
35+
36+
return true;
37+
},
38+
dropCallback(event, dropLocation) {
39+
console.log(
40+
event.dataTransfer.getData("text/plain") +
41+
" was dropped " +
42+
dropLocation.where +
43+
" " +
44+
dropLocation.target.textContent
45+
);
46+
47+
return true;
48+
}
49+
}
50+
);
51+
52+
socket.on("initialFSTree", (initialTree) => {
53+
console.log("Received initialFSTree:", initialTree);
54+
55+
// for (const dirent of initialTree) {
56+
// const node = dirent.kind === "directory" ?
57+
// createGroup(dirent.name) :
58+
// createItem(dirent.name);
59+
// treeView.append(node, dirent.kind === "directory" ? "group" : "item");
60+
// }
61+
});
62+
63+
socket.on("fsTreeChange", (event) => {
64+
console.log("Received fsTreeChange event:", event);
65+
// TODO: Handle FSTree changes to update the TreeView
66+
});
67+
68+
function onClickCreate(event) {
69+
const type = event.target.dataset.type;
70+
const label = prompt("Enter a name", "");
71+
if (label.length === 0) {
72+
return;
73+
}
74+
75+
const parentNode = treeView.selector.nodes[0];
76+
const parentPath = parentNode ? (parentNode.dataset.path || "") : "";
77+
const operation = {
78+
type: type === "item" ? "mkfile" : "mkdir",
79+
path: parentPath + "/" + label
80+
};
81+
socket.emit("fsTreeOperation", operation);
82+
}
83+
84+
function createItem(label) {
85+
const itemElt = document.createElement("li");
86+
87+
const iconElt = document.createElement("i");
88+
iconElt.classList.add("icon");
89+
itemElt.appendChild(iconElt);
90+
91+
const spanElt = document.createElement("span");
92+
spanElt.textContent = label;
93+
itemElt.appendChild(spanElt);
94+
95+
return itemElt;
96+
}
97+
98+
function createGroup(label) {
99+
const groupElt = document.createElement("li");
100+
101+
const spanElt = document.createElement("span");
102+
spanElt.textContent = label;
103+
groupElt.appendChild(spanElt);
104+
105+
return groupElt;
106+
}
107+
108+
document.querySelector("nav .create-item").addEventListener("click", onClickCreate);
109+
document.querySelector("nav .create-group").addEventListener("click", onClickCreate);
110+
document.querySelector("nav .remove-selected").addEventListener("click", function() {
111+
while (treeView.selector.size > 0) {
112+
const node = treeView.selector.nodes.at(-1);
113+
const operation = {
114+
type: node.classList.contains("group") ? "rmdir" : "unlink",
115+
path: node.dataset.path
116+
};
117+
socket.emit("fsTreeOperation", operation);
118+
treeView.remove(node);
119+
}
120+
});
121+
122+
treeView.addEventListener("selectionChange", function () {
123+
let text;
124+
if (treeView.selector.nodes.length > 1) {
125+
text = "" + treeView.selector.nodes.length + " items selected";
126+
}
127+
else if (treeView.selector.nodes.length === 1) {
128+
text = "1 item selected";
129+
}
130+
else {
131+
text = "No items selected";
132+
}
133+
134+
document.querySelector("nav .selected-nodes").textContent = text;
135+
});
136+
137+
treeView.addEventListener("activate", function () {
138+
alert("Activated " + treeView.selector.nodes[0].querySelector("span").textContent);
139+
});
140+
</script>
141+
</body>
142+
143+
</html>

packages/fs-tree/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@jolly-pixel/fs-tree",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"main": "dist/index.class.js",
6+
"types": "dist/index.class.d.ts",
7+
"scripts": {
8+
"prepublish": "rimraf ./dist && tsc -b",
9+
"test": "tsx --test test/**/*.test.ts",
10+
"build:ts": "tsc",
11+
"dev": "vite",
12+
"build": "vite build",
13+
"preview": "vite preview",
14+
"start-server": "tsx ./scripts/socket-server.js"
15+
},
16+
"keywords": [],
17+
"files": [
18+
"dist"
19+
],
20+
"dependencies": {
21+
"chokidar": "^4.0.3",
22+
"digraph-js": "^2.2.3",
23+
"lodash.set": "^4.3.2",
24+
"ts-pattern": "^5.7.1"
25+
},
26+
"directories": {
27+
"doc": "docs",
28+
"test": "test"
29+
},
30+
"author": "GENTILHOMME Thomas <gentilhomme.thomas@gmail.com>",
31+
"license": "MIT",
32+
"devDependencies": {
33+
"@types/lodash.set": "^4.3.9",
34+
"socket.io": "^4.8.1"
35+
}
36+
}
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)