azul-sync 1.3.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/.gitattributes +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/README.md +142 -0
- package/dist/build.d.ts +19 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +92 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +397 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +105 -0
- package/dist/config.js.map +1 -0
- package/dist/fs/fileWriter.d.ts +100 -0
- package/dist/fs/fileWriter.d.ts.map +1 -0
- package/dist/fs/fileWriter.js +342 -0
- package/dist/fs/fileWriter.js.map +1 -0
- package/dist/fs/treeManager.d.ts +84 -0
- package/dist/fs/treeManager.d.ts.map +1 -0
- package/dist/fs/treeManager.js +365 -0
- package/dist/fs/treeManager.js.map +1 -0
- package/dist/fs/watcher.d.ts +39 -0
- package/dist/fs/watcher.d.ts.map +1 -0
- package/dist/fs/watcher.js +120 -0
- package/dist/fs/watcher.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +349 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/httpPolling.d.ts +56 -0
- package/dist/ipc/httpPolling.d.ts.map +1 -0
- package/dist/ipc/httpPolling.js +171 -0
- package/dist/ipc/httpPolling.js.map +1 -0
- package/dist/ipc/messages.d.ts +112 -0
- package/dist/ipc/messages.d.ts.map +1 -0
- package/dist/ipc/messages.js +5 -0
- package/dist/ipc/messages.js.map +1 -0
- package/dist/ipc/server.d.ts +50 -0
- package/dist/ipc/server.d.ts.map +1 -0
- package/dist/ipc/server.js +168 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/pack.d.ts +19 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +225 -0
- package/dist/pack.js.map +1 -0
- package/dist/push.d.ts +43 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +532 -0
- package/dist/push.js.map +1 -0
- package/dist/rojo.d.ts +9 -0
- package/dist/rojo.d.ts.map +1 -0
- package/dist/rojo.js +114 -0
- package/dist/rojo.js.map +1 -0
- package/dist/snapshot/rojo.d.ts +39 -0
- package/dist/snapshot/rojo.d.ts.map +1 -0
- package/dist/snapshot/rojo.js +364 -0
- package/dist/snapshot/rojo.js.map +1 -0
- package/dist/snapshot.d.ts +23 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +132 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/sourcemap/generator.d.ts +78 -0
- package/dist/sourcemap/generator.d.ts.map +1 -0
- package/dist/sourcemap/generator.js +351 -0
- package/dist/sourcemap/generator.js.map +1 -0
- package/dist/sourcemap/propertyLoader.d.ts +19 -0
- package/dist/sourcemap/propertyLoader.d.ts.map +1 -0
- package/dist/sourcemap/propertyLoader.js +131 -0
- package/dist/sourcemap/propertyLoader.js.map +1 -0
- package/dist/util/id.d.ts +9 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +14 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/log.d.ts +13 -0
- package/dist/util/log.d.ts.map +1 -0
- package/dist/util/log.js +51 -0
- package/dist/util/log.js.map +1 -0
- package/docs/assets/azul-logo.pdn +0 -0
- package/docs/assets/logo-200px.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/assets/plugin/toolbox.png +0 -0
- package/docs/assets/synced.png +0 -0
- package/package.json +41 -0
- package/plugin/README.md +54 -0
- package/plugin/sourcemap.json +264 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Actor/AzulSync.server.luau +905 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/AzulService.luau +1010 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Config.luau +29 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Enums.luau +11 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CollapsibleTitledSection.luau +214 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ColorPicker.luau +360 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CustomTextButton.luau +170 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/DropdownMenu.luau +363 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/HorizontalLine.luau +43 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ImageButtonWithText.luau +181 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledCheckbox.luau +295 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledColorInputPicker.luau +294 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledMultiChoice.luau +163 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledNumberInput.luau +312 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledRadioButton.luau +55 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledSlider.luau +151 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledTextInput.luau +222 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledToggleButton.luau +73 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/StatefulImageButton.luau +125 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalScrollingFrame.luau +100 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalSpacer.luau +35 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticallyScalingListFrame.luau +107 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/GuiUtilities.luau +429 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/RbxGui.luau +4363 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/UI.luau +425 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/WebSocketClient.luau +161 -0
- package/src/build.ts +120 -0
- package/src/cli.ts +496 -0
- package/src/config.ts +170 -0
- package/src/fs/fileWriter.ts +414 -0
- package/src/fs/treeManager.ts +458 -0
- package/src/fs/watcher.ts +142 -0
- package/src/index.ts +450 -0
- package/src/ipc/httpPolling.ts +214 -0
- package/src/ipc/messages.ts +159 -0
- package/src/ipc/server.ts +196 -0
- package/src/pack.ts +309 -0
- package/src/push.ts +726 -0
- package/src/snapshot/rojo.ts +467 -0
- package/src/snapshot.ts +161 -0
- package/src/sourcemap/generator.ts +504 -0
- package/src/sourcemap/propertyLoader.ts +195 -0
- package/src/util/id.ts +15 -0
- package/src/util/log.ts +94 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { TreeNode } from "./treeManager.js";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
import { log } from "../util/log.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mapping of GUID to file path
|
|
9
|
+
*/
|
|
10
|
+
export interface FileMapping {
|
|
11
|
+
guid: string;
|
|
12
|
+
filePath: string;
|
|
13
|
+
className: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handles writing the virtual tree to the filesystem
|
|
18
|
+
*/
|
|
19
|
+
export class FileWriter {
|
|
20
|
+
private baseDir: string;
|
|
21
|
+
private fileMappings: Map<string, FileMapping> = new Map();
|
|
22
|
+
private pathToGuid: Map<string, string> = new Map(); // Reverse index for O(1) path lookups
|
|
23
|
+
|
|
24
|
+
constructor(baseDir: string = config.syncDir) {
|
|
25
|
+
this.baseDir = path.resolve(baseDir);
|
|
26
|
+
this.ensureDirectory(this.baseDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Write all script nodes to the filesystem
|
|
31
|
+
*/
|
|
32
|
+
public writeTree(nodes: Map<string, TreeNode>): void {
|
|
33
|
+
log.info("Writing tree to filesystem...");
|
|
34
|
+
|
|
35
|
+
// Clear existing mappings
|
|
36
|
+
this.fileMappings.clear();
|
|
37
|
+
this.pathToGuid.clear();
|
|
38
|
+
|
|
39
|
+
// Collect all script nodes for batch writing
|
|
40
|
+
const scriptNodes: TreeNode[] = [];
|
|
41
|
+
for (const node of nodes.values()) {
|
|
42
|
+
if (this.isScriptNode(node)) {
|
|
43
|
+
scriptNodes.push(node);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.writeBatch(scriptNodes);
|
|
48
|
+
|
|
49
|
+
log.success(`Wrote ${this.fileMappings.size} scripts to filesystem`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write multiple scripts in a batch for improved I/O efficiency
|
|
54
|
+
*/
|
|
55
|
+
public writeBatch(nodes: TreeNode[]): void {
|
|
56
|
+
// Pre-compute all file paths and collect writes
|
|
57
|
+
const writes: { node: TreeNode; filePath: string; dirPath: string }[] = [];
|
|
58
|
+
const dirsToCreate = new Set<string>();
|
|
59
|
+
const batchPathToGuid = new Map<string, string>();
|
|
60
|
+
|
|
61
|
+
for (const node of nodes) {
|
|
62
|
+
if (!this.isScriptNode(node) || node.source === undefined) continue;
|
|
63
|
+
const filePath = this.getFilePathWithCollisionMap(node, batchPathToGuid);
|
|
64
|
+
const dirPath = path.dirname(filePath);
|
|
65
|
+
writes.push({ node, filePath, dirPath });
|
|
66
|
+
dirsToCreate.add(dirPath);
|
|
67
|
+
batchPathToGuid.set(path.resolve(filePath), node.guid);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Batch create all directories first (sorted by depth to ensure parents exist)
|
|
71
|
+
const sortedDirs = Array.from(dirsToCreate).sort(
|
|
72
|
+
(a, b) => a.length - b.length,
|
|
73
|
+
);
|
|
74
|
+
for (const dir of sortedDirs) {
|
|
75
|
+
this.ensureDirectory(dir);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const { node, filePath } of writes) {
|
|
79
|
+
try {
|
|
80
|
+
fs.writeFileSync(filePath, node.source!, "utf-8");
|
|
81
|
+
|
|
82
|
+
this.fileMappings.set(node.guid, {
|
|
83
|
+
guid: node.guid,
|
|
84
|
+
filePath: filePath,
|
|
85
|
+
className: node.className,
|
|
86
|
+
});
|
|
87
|
+
this.pathToGuid.set(path.resolve(filePath), node.guid);
|
|
88
|
+
|
|
89
|
+
log.script(this.getRelativePath(filePath), "updated");
|
|
90
|
+
} catch (error) {
|
|
91
|
+
log.error(`Failed to write script ${filePath}:`, error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write or update a single script
|
|
98
|
+
*/
|
|
99
|
+
public writeScript(node: TreeNode): string | null {
|
|
100
|
+
if (!this.isScriptNode(node)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Allow empty-string sources on new scripts; only skip if source is truly undefined
|
|
105
|
+
if (node.source === undefined) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const existingMapping = this.fileMappings.get(node.guid);
|
|
110
|
+
const filePath = this.getFilePath(node);
|
|
111
|
+
const dirPath = path.dirname(filePath);
|
|
112
|
+
const previousPath = existingMapping?.filePath;
|
|
113
|
+
const pathChanged = previousPath && previousPath !== filePath;
|
|
114
|
+
|
|
115
|
+
// Ensure directory exists
|
|
116
|
+
this.ensureDirectory(dirPath);
|
|
117
|
+
|
|
118
|
+
// Write file
|
|
119
|
+
try {
|
|
120
|
+
fs.writeFileSync(filePath, node.source, "utf-8");
|
|
121
|
+
|
|
122
|
+
// If the target path changed for this guid, remove the old file to avoid stale copies
|
|
123
|
+
if (pathChanged && previousPath && fs.existsSync(previousPath)) {
|
|
124
|
+
fs.unlinkSync(previousPath);
|
|
125
|
+
this.pathToGuid.delete(path.resolve(previousPath));
|
|
126
|
+
this.cleanupParentsIfEmpty(path.dirname(previousPath));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Update mapping and reverse index
|
|
130
|
+
this.fileMappings.set(node.guid, {
|
|
131
|
+
guid: node.guid,
|
|
132
|
+
filePath: filePath,
|
|
133
|
+
className: node.className,
|
|
134
|
+
});
|
|
135
|
+
this.pathToGuid.set(path.resolve(filePath), node.guid);
|
|
136
|
+
|
|
137
|
+
log.script(this.getRelativePath(filePath), "updated");
|
|
138
|
+
return filePath;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
log.error(`Failed to write script ${filePath}:`, error);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delete a script file
|
|
147
|
+
*/
|
|
148
|
+
public deleteScript(guid: string): boolean {
|
|
149
|
+
const mapping = this.fileMappings.get(guid);
|
|
150
|
+
if (!mapping) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const deleted = this.deleteFilePathInternal(mapping.filePath);
|
|
156
|
+
this.fileMappings.delete(guid);
|
|
157
|
+
this.pathToGuid.delete(path.resolve(mapping.filePath));
|
|
158
|
+
return deleted;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
log.error(`Failed to delete script ${mapping.filePath}:`, error);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Delete a script file by path even if the mapping is missing
|
|
167
|
+
*/
|
|
168
|
+
public deleteFilePath(filePath: string): boolean {
|
|
169
|
+
try {
|
|
170
|
+
return this.deleteFilePathInternal(filePath);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
log.error(`Failed to delete script ${filePath}:`, error);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get the filesystem path for a node
|
|
179
|
+
*/
|
|
180
|
+
public getFilePath(node: TreeNode): string {
|
|
181
|
+
return this.getFilePathWithCollisionMap(node);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the filesystem path for a node, with optional collision map for batch operations
|
|
186
|
+
*/
|
|
187
|
+
private getFilePathWithCollisionMap(
|
|
188
|
+
node: TreeNode,
|
|
189
|
+
batchCollisionMap?: Map<string, string>,
|
|
190
|
+
): string {
|
|
191
|
+
// Build the path from the node's hierarchy. For scripts, we only use the parent path
|
|
192
|
+
// as directories, then add the script file name. This prevents creating an extra
|
|
193
|
+
// folder named after the script itself.
|
|
194
|
+
const parts: string[] = [];
|
|
195
|
+
|
|
196
|
+
const dirSegments = this.isScriptNode(node)
|
|
197
|
+
? node.path.slice(0, Math.max(0, node.path.length - 1))
|
|
198
|
+
: node.path;
|
|
199
|
+
|
|
200
|
+
for (const segment of dirSegments) {
|
|
201
|
+
parts.push(this.sanitizeName(segment));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If this is a script, add the script name as a file
|
|
205
|
+
if (this.isScriptNode(node)) {
|
|
206
|
+
const scriptName = this.getScriptFileName(node);
|
|
207
|
+
parts.push(scriptName);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const desiredPath = path.join(this.baseDir, ...parts);
|
|
211
|
+
const normalizedDesiredPath = path.resolve(desiredPath);
|
|
212
|
+
|
|
213
|
+
// Check for collisions in both the persistent mappings and the batch collision map
|
|
214
|
+
const existingGuid = this.findGuidByFilePath(desiredPath);
|
|
215
|
+
const batchGuid = batchCollisionMap?.get(normalizedDesiredPath);
|
|
216
|
+
const collision = existingGuid || batchGuid;
|
|
217
|
+
|
|
218
|
+
// If another GUID already owns this path, disambiguate using a stable suffix
|
|
219
|
+
if (collision && collision !== node.guid) {
|
|
220
|
+
const ext = config.scriptExtension;
|
|
221
|
+
const uniqueName = `${this.sanitizeName(node.name)}__${node.guid.slice(
|
|
222
|
+
0,
|
|
223
|
+
8,
|
|
224
|
+
)}${ext}`;
|
|
225
|
+
const uniqueParts = [...parts.slice(0, -1), uniqueName];
|
|
226
|
+
return path.join(this.baseDir, ...uniqueParts);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return desiredPath;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get the appropriate filename for a script node
|
|
234
|
+
*/
|
|
235
|
+
private getScriptFileName(node: TreeNode): string {
|
|
236
|
+
const ext = config.scriptExtension;
|
|
237
|
+
|
|
238
|
+
// If the script has the same name as its parent, use init pattern
|
|
239
|
+
// const parentName = node.path[node.path.length - 2]; // 2 to get parent
|
|
240
|
+
// if (node.name === parentName) {
|
|
241
|
+
// log.info(
|
|
242
|
+
// `Using init file pattern for script ${node.name} because it matches its parent directory name (${parentName}).`
|
|
243
|
+
// );
|
|
244
|
+
// return `init${ext}`;
|
|
245
|
+
// }
|
|
246
|
+
|
|
247
|
+
let name = this.sanitizeName(node.name);
|
|
248
|
+
|
|
249
|
+
if (node.className === "Script") {
|
|
250
|
+
name = `${name}.server`;
|
|
251
|
+
} else if (node.className === "LocalScript") {
|
|
252
|
+
name = `${name}.client`;
|
|
253
|
+
} else if (node.className === "ModuleScript") {
|
|
254
|
+
if (config.suffixModuleScripts) {
|
|
255
|
+
name = `${name}.module`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return `${name}${ext}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Sanitize a name for use in filesystem
|
|
264
|
+
*/
|
|
265
|
+
private sanitizeName(name: string): string {
|
|
266
|
+
// Replace invalid filesystem characters
|
|
267
|
+
return name.replace(/[<>:"|?*]/g, "_");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if a node is a script
|
|
272
|
+
*/
|
|
273
|
+
private isScriptNode(node: TreeNode): boolean {
|
|
274
|
+
return (
|
|
275
|
+
node.className === "Script" ||
|
|
276
|
+
node.className === "LocalScript" ||
|
|
277
|
+
node.className === "ModuleScript"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Ensure a directory exists
|
|
283
|
+
*/
|
|
284
|
+
private ensureDirectory(dirPath: string): void {
|
|
285
|
+
if (!fs.existsSync(dirPath)) {
|
|
286
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Internal helper to remove a file and clean mapping
|
|
292
|
+
*/
|
|
293
|
+
private deleteFilePathInternal(filePath: string): boolean {
|
|
294
|
+
const normalized = path.resolve(filePath);
|
|
295
|
+
|
|
296
|
+
if (fs.existsSync(normalized)) {
|
|
297
|
+
fs.unlinkSync(normalized);
|
|
298
|
+
log.script(this.getRelativePath(normalized), "deleted");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const guid = this.pathToGuid.get(normalized);
|
|
302
|
+
if (guid) {
|
|
303
|
+
this.fileMappings.delete(guid);
|
|
304
|
+
this.pathToGuid.delete(normalized);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Find the GUID that currently owns a file path, if any
|
|
312
|
+
*/
|
|
313
|
+
private findGuidByFilePath(filePath: string): string | undefined {
|
|
314
|
+
const normalized = path.resolve(filePath);
|
|
315
|
+
for (const [guid, mapping] of this.fileMappings) {
|
|
316
|
+
if (path.resolve(mapping.filePath) === normalized) {
|
|
317
|
+
return guid;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get path relative to base directory
|
|
325
|
+
*/
|
|
326
|
+
private getRelativePath(filePath: string): string {
|
|
327
|
+
return path.relative(this.baseDir, filePath);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get file mapping by GUID
|
|
332
|
+
*/
|
|
333
|
+
public getMapping(guid: string): FileMapping | undefined {
|
|
334
|
+
return this.fileMappings.get(guid);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get GUID by file path
|
|
339
|
+
*/
|
|
340
|
+
public getGuidByPath(filePath: string): string | undefined {
|
|
341
|
+
return this.pathToGuid.get(path.resolve(filePath));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get all file mappings
|
|
346
|
+
*/
|
|
347
|
+
public getAllMappings(): Map<string, FileMapping> {
|
|
348
|
+
return this.fileMappings;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get the base directory
|
|
353
|
+
*/
|
|
354
|
+
public getBaseDir(): string {
|
|
355
|
+
return this.baseDir;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Clean up empty directories
|
|
360
|
+
*/
|
|
361
|
+
public cleanupEmptyDirectories(): void {
|
|
362
|
+
this.cleanupEmptyDirsRecursive(this.baseDir);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private cleanupEmptyDirsRecursive(dirPath: string): boolean {
|
|
366
|
+
if (!fs.existsSync(dirPath)) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
371
|
+
|
|
372
|
+
// Recursively check subdirectories
|
|
373
|
+
for (const entry of entries) {
|
|
374
|
+
if (entry.isDirectory()) {
|
|
375
|
+
const subPath = path.join(dirPath, entry.name);
|
|
376
|
+
this.cleanupEmptyDirsRecursive(subPath);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check if directory is now empty
|
|
381
|
+
const updatedEntries = fs.readdirSync(dirPath);
|
|
382
|
+
if (updatedEntries.length === 0 && dirPath !== this.baseDir) {
|
|
383
|
+
fs.rmdirSync(dirPath);
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Walk up from a directory and remove empty parents until baseDir is reached.
|
|
392
|
+
*/
|
|
393
|
+
private cleanupParentsIfEmpty(startDir: string): void {
|
|
394
|
+
let current = path.resolve(startDir);
|
|
395
|
+
const root = this.baseDir;
|
|
396
|
+
|
|
397
|
+
while (current.startsWith(root)) {
|
|
398
|
+
if (current === root) {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const entries = fs.existsSync(current)
|
|
403
|
+
? fs.readdirSync(current, { withFileTypes: true })
|
|
404
|
+
: [];
|
|
405
|
+
|
|
406
|
+
if (entries.length === 0) {
|
|
407
|
+
fs.rmdirSync(current);
|
|
408
|
+
current = path.dirname(current);
|
|
409
|
+
} else {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|