jest-roblox-assassin 1.0.0 → 1.1.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.
@@ -0,0 +1,243 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Generates a sourcemap from a Rojo project file.
6
+ * @param {string} projectFilePath Path to the .project.json file.
7
+ * @returns {object | undefined} The generated sourcemap object.
8
+ */
9
+ export function createSourcemap(projectFilePath) {
10
+ const absoluteProjectRef = path.resolve(projectFilePath);
11
+ const projectDir = path.dirname(absoluteProjectRef);
12
+
13
+ let project;
14
+ try {
15
+ project = JSON.parse(fs.readFileSync(absoluteProjectRef, "utf8"));
16
+ } catch (err) {
17
+ console.error(`Failed to read project file: ${err.message}`);
18
+ return;
19
+ }
20
+
21
+ const rootName = project.name || path.basename(projectDir);
22
+ const rootNode = processNode(
23
+ project.tree,
24
+ rootName,
25
+ projectDir,
26
+ projectDir
27
+ );
28
+ // Add the project.json to the root filePaths
29
+ rootNode.filePaths.push(toRelativePosixPath(absoluteProjectRef, projectDir));
30
+ // Add meta.json if exists
31
+ const metaPath = absoluteProjectRef + ".meta.json";
32
+ if (fs.existsSync(metaPath)) {
33
+ rootNode.filePaths.push(toRelativePosixPath(metaPath, projectDir));
34
+ }
35
+ return filterScripts(rootNode);
36
+ }
37
+
38
+ function toRelativePosixPath(p, base) {
39
+ return path.relative(base, p).split(path.sep).join("/");
40
+ }
41
+
42
+ /**
43
+ * Processes a Rojo project node recursively.
44
+ * @param {object} node The current node in the project tree.
45
+ * @param {string} name The name of the current node.
46
+ * @param {string} currentDir The current directory for resolving relative paths.
47
+ * @param {string} projectDir The root project directory for relative paths.
48
+ * @returns {object} The processed node with className, filePaths, and children.
49
+ */
50
+ function processNode(node, name, currentDir, projectDir) {
51
+ let className = node.$className || "Folder";
52
+ let filePaths = [];
53
+ let children = [];
54
+
55
+ if (node.$path) {
56
+ const resolvedPath = path.resolve(currentDir, node.$path);
57
+ if (fs.existsSync(resolvedPath)) {
58
+ const stats = fs.statSync(resolvedPath);
59
+
60
+ if (stats.isFile()) {
61
+ const result = getScriptInfo(resolvedPath);
62
+ if (result) {
63
+ className = node.$className || result.className;
64
+ filePaths = [resolvedPath];
65
+ }
66
+ } else if (stats.isDirectory()) {
67
+ const dirResult = processDirectory(resolvedPath, projectDir);
68
+ className = node.$className || dirResult.className;
69
+ filePaths = dirResult.filePaths;
70
+ children = dirResult.children;
71
+ }
72
+ }
73
+ // Check for meta.json
74
+ const metaPath = resolvedPath + ".meta.json";
75
+ if (fs.existsSync(metaPath)) {
76
+ filePaths.push(metaPath);
77
+ }
78
+ }
79
+
80
+ // Process explicit children in the tree
81
+ for (const [childName, childNode] of Object.entries(node)) {
82
+ if (childName.startsWith("$")) continue;
83
+ const child = processNode(childNode, childName, currentDir, projectDir);
84
+ if (child) {
85
+ children.push(child);
86
+ }
87
+ }
88
+
89
+ return {
90
+ name: name,
91
+ className: className,
92
+ filePaths: filePaths.map((p) => toRelativePosixPath(p, projectDir)),
93
+ children: children,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Determines script class name based on file extension and naming convention.
99
+ * @param {string} filePath The file path to analyze.
100
+ * @returns {{ className: string, name: string } | null} The script info or null if not a script.
101
+ */
102
+ function getScriptInfo(filePath) {
103
+ const ext = path.extname(filePath);
104
+ const base = path.basename(filePath, ext);
105
+
106
+ if (ext === ".lua" || ext === ".luau") {
107
+ if (base.endsWith(".server")) {
108
+ return { className: "Script", name: base.slice(0, -7) };
109
+ } else if (base.endsWith(".client")) {
110
+ return { className: "LocalScript", name: base.slice(0, -7) };
111
+ } else {
112
+ return { className: "ModuleScript", name: base };
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Processes a directory to find scripts and subdirectories.
120
+ * @param {string} dirPath The directory path to process.
121
+ * @param {string} projectDir The root project directory for relative paths.
122
+ * @returns {{ className: string, filePaths: string[], children: object[] }} The processed directory info.
123
+ */
124
+ function processDirectory(dirPath, projectDir) {
125
+ const entries = fs.readdirSync(dirPath);
126
+ let className = "Folder";
127
+ let filePaths = [];
128
+ let children = [];
129
+
130
+ // Check for init scripts
131
+ const initFile = entries.find((e) => {
132
+ const ext = path.extname(e);
133
+ const base = path.basename(e, ext);
134
+ return base === "init" && (ext === ".lua" || ext === ".luau");
135
+ });
136
+
137
+ if (initFile) {
138
+ const initPath = path.join(dirPath, initFile);
139
+ const result = getScriptInfo(initPath);
140
+ if (result) {
141
+ className = result.className;
142
+ filePaths = [initPath];
143
+ }
144
+ }
145
+
146
+ for (const entry of entries) {
147
+ const fullPath = path.join(dirPath, entry);
148
+ const stats = fs.statSync(fullPath);
149
+
150
+ if (stats.isDirectory()) {
151
+ // Check if this directory has a default.project.json (subproject)
152
+ const projectPath = path.join(fullPath, "default.project.json");
153
+ if (fs.existsSync(projectPath)) {
154
+ let subProject;
155
+ try {
156
+ subProject = JSON.parse(fs.readFileSync(projectPath, "utf8"));
157
+ } catch (err) {
158
+ console.warn(`Failed to read subproject file: ${err.message}`);
159
+ }
160
+ if (subProject) {
161
+ const subRootNode = processNode(
162
+ subProject.tree,
163
+ subProject.name || entry,
164
+ fullPath,
165
+ projectDir
166
+ );
167
+ // Add the project.json to filePaths
168
+ subRootNode.filePaths.push(toRelativePosixPath(projectPath, projectDir));
169
+ // Add meta.json if exists
170
+ const metaPath = projectPath + ".meta.json";
171
+ if (fs.existsSync(metaPath)) {
172
+ subRootNode.filePaths.push(toRelativePosixPath(metaPath, projectDir));
173
+ }
174
+ children.push(subRootNode);
175
+ continue;
176
+ }
177
+ }
178
+ // Otherwise, process as normal directory
179
+ const childNode = processNode(
180
+ { $path: entry },
181
+ entry,
182
+ dirPath,
183
+ projectDir
184
+ );
185
+ if (childNode) {
186
+ children.push(childNode);
187
+ }
188
+ } else {
189
+ const ext = path.extname(entry);
190
+ const base = path.basename(entry, ext);
191
+
192
+ // Skip init files as they are handled by the parent directory
193
+ if (base === "init" && (ext === ".lua" || ext === ".luau"))
194
+ continue;
195
+ // Skip project files and meta files
196
+ if (
197
+ entry === "default.project.json" ||
198
+ entry.endsWith(".meta.json")
199
+ )
200
+ continue;
201
+
202
+ const result = getScriptInfo(fullPath);
203
+ if (result) {
204
+ let scriptFilePaths = [toRelativePosixPath(fullPath, projectDir)];
205
+ const metaPath = fullPath + ".meta.json";
206
+ if (fs.existsSync(metaPath)) {
207
+ scriptFilePaths.push(toRelativePosixPath(metaPath, projectDir));
208
+ }
209
+ children.push({
210
+ name: result.name,
211
+ className: result.className,
212
+ filePaths: scriptFilePaths,
213
+ children: [],
214
+ });
215
+ }
216
+ }
217
+ }
218
+
219
+ return { className, filePaths, children };
220
+ }
221
+
222
+ /**
223
+ * Filters the tree to only include scripts or nodes with script descendants.
224
+ * @param {object} node The current node in the tree.
225
+ * @returns {object | null} The filtered node or null if it has no scripts.
226
+ */
227
+ function filterScripts(node) {
228
+ const isScript = ["Script", "LocalScript", "ModuleScript"].includes(
229
+ node.className
230
+ );
231
+ const filteredChildren = node.children
232
+ .map((child) => filterScripts(child))
233
+ .filter(Boolean);
234
+
235
+ if (isScript || filteredChildren.length > 0) {
236
+ return {
237
+ name: node.name,
238
+ className: node.className,
239
+ filePaths: node.filePaths,
240
+ children: filteredChildren,
241
+ };
242
+ }
243
+ }