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.
- package/README.md +23 -13
- package/package.json +16 -12
- package/src/cache.js +1 -0
- package/src/cli.js +160 -774
- package/src/discovery.js +355 -0
- package/src/rewriter.js +454 -257
- package/src/runJestRoblox.js +838 -0
- package/src/sourcemap.js +243 -0
package/src/sourcemap.js
ADDED
|
@@ -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
|
+
}
|