@jaeymo/toybox 1.0.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 +1 -0
- package/default.project.json +34 -0
- package/package.json +39 -0
- package/src/cli.ts +20 -0
- package/src/generator.ts +123 -0
- package/tsconfig.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# toybox
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sometest",
|
|
3
|
+
"tree": {
|
|
4
|
+
"$className": "DataModel",
|
|
5
|
+
"ReplicatedStorage": {
|
|
6
|
+
"FilesHere": {
|
|
7
|
+
"$className": "Folder"
|
|
8
|
+
},
|
|
9
|
+
"Asset": {
|
|
10
|
+
"$className": "Folder",
|
|
11
|
+
"Models": {
|
|
12
|
+
"$className": "Folder",
|
|
13
|
+
"AnotherModel": {
|
|
14
|
+
"$className": "Model",
|
|
15
|
+
"SomePartName": {
|
|
16
|
+
"$className": "Part"
|
|
17
|
+
},
|
|
18
|
+
"PrettyPrincess": {
|
|
19
|
+
"$className": "Model"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"SomeModelName": {
|
|
24
|
+
"$className": "Model"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"ServerScriptService": {
|
|
29
|
+
"files": {
|
|
30
|
+
"$className": "Folder"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jaeymo/toybox",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Toybox scans a Roblox hierarchy and produces a Rojo mapping so all of your assets are statically represented",
|
|
8
|
+
"homepage": "https://github.com/jaeymo/toybox#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/jaeymo/toybox/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/jaeymo/toybox.git"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Jaeymo",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "index.js",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "ts-node src/cli.ts",
|
|
23
|
+
"clean": "rimraf dist"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^14.0.3",
|
|
27
|
+
"fast-xml-parser": "^5.3.6",
|
|
28
|
+
"ts-node": "^10.9.2",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@eslint/js": "^10.0.1",
|
|
33
|
+
"@types/node": "^25.2.3",
|
|
34
|
+
"eslint": "^10.0.0",
|
|
35
|
+
"globals": "^17.3.0",
|
|
36
|
+
"rimraf": "^6.1.3",
|
|
37
|
+
"typescript-eslint": "^8.56.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { generateMapping } from "./generator.js";
|
|
3
|
+
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name("toybox")
|
|
8
|
+
.description("Generate rojo mappings from a Roblox directory")
|
|
9
|
+
.version("1.0.0");
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command("generate")
|
|
13
|
+
.requiredOption("-i, --input <path>", "Input roblox directory")
|
|
14
|
+
.option("-f, --folder <name>", "Folder inside Roblox to map", "Asset")
|
|
15
|
+
.requiredOption("-o, --output <path>", "Path to .project.json")
|
|
16
|
+
.action((options) => {
|
|
17
|
+
generateMapping(options.input, options.output, options.folder);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
program.parse(process.argv);
|
package/src/generator.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { XMLParser } from "fast-xml-parser";
|
|
3
|
+
|
|
4
|
+
interface AssetNode {
|
|
5
|
+
name: string;
|
|
6
|
+
className: string;
|
|
7
|
+
children?: Record<string, AssetNode>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getItemName(item: any): string | null {
|
|
11
|
+
if (!item.Properties || !item.Properties.string) return null;
|
|
12
|
+
|
|
13
|
+
const strings = Array.isArray(item.Properties.string)
|
|
14
|
+
? item.Properties.string
|
|
15
|
+
: [item.Properties.string];
|
|
16
|
+
|
|
17
|
+
for (const str of strings) {
|
|
18
|
+
if (str.name === "Name") {
|
|
19
|
+
if (typeof str === "string") return str;
|
|
20
|
+
if ("#text" in str) return str["#text"];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findFolderByName(item: any, name: string): any | null {
|
|
28
|
+
const itemName = getItemName(item);
|
|
29
|
+
if (itemName === name) return item;
|
|
30
|
+
|
|
31
|
+
const children = item.Item
|
|
32
|
+
? Array.isArray(item.Item)
|
|
33
|
+
? item.Item
|
|
34
|
+
: [item.Item]
|
|
35
|
+
: [];
|
|
36
|
+
for (const child of children) {
|
|
37
|
+
const found = findFolderByName(child, name);
|
|
38
|
+
if (found) return found;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function traverse(item: any): AssetNode {
|
|
45
|
+
const name = getItemName(item) || "Unnamed";
|
|
46
|
+
const node: AssetNode = {
|
|
47
|
+
name,
|
|
48
|
+
className: item.class,
|
|
49
|
+
children: {},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const children = item.Item
|
|
53
|
+
? Array.isArray(item.Item)
|
|
54
|
+
? item.Item
|
|
55
|
+
: [item.Item]
|
|
56
|
+
: [];
|
|
57
|
+
for (const child of children) {
|
|
58
|
+
const childNode = traverse(child);
|
|
59
|
+
node.children![childNode.name] = childNode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return node;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toRojo(node: AssetNode): Record<string, any> {
|
|
66
|
+
const result: Record<string, any> = { $className: node.className };
|
|
67
|
+
if (node.children && Object.keys(node.children).length > 0) {
|
|
68
|
+
for (const [name, child] of Object.entries(node.children)) {
|
|
69
|
+
result[name] = toRojo(child as AssetNode);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildTreeFromRbxlx(
|
|
76
|
+
filePath: string,
|
|
77
|
+
rootName: string,
|
|
78
|
+
): Record<string, any> {
|
|
79
|
+
const xmlData = fs.readFileSync(filePath, "utf-8");
|
|
80
|
+
const parser = new XMLParser({
|
|
81
|
+
ignoreAttributes: false,
|
|
82
|
+
attributeNamePrefix: "",
|
|
83
|
+
});
|
|
84
|
+
const jsonObj = parser.parse(xmlData);
|
|
85
|
+
|
|
86
|
+
const items = Array.isArray(jsonObj.roblox.Item)
|
|
87
|
+
? jsonObj.roblox.Item
|
|
88
|
+
: [jsonObj.roblox.Item];
|
|
89
|
+
const rootItem = findFolderByName({ Item: items }, rootName);
|
|
90
|
+
if (!rootItem) throw new Error(`Folder '${rootName}' not found in .rbxlx`);
|
|
91
|
+
|
|
92
|
+
const rootNode = traverse(rootItem);
|
|
93
|
+
return { [rootNode.name]: toRojo(rootNode) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function generateMapping(
|
|
97
|
+
input: string,
|
|
98
|
+
rojoPath: string,
|
|
99
|
+
folderName: string,
|
|
100
|
+
) {
|
|
101
|
+
if (!fs.existsSync(input)) {
|
|
102
|
+
console.error("Input file does not exist");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(rojoPath)) {
|
|
107
|
+
console.error("Rojo project file does not exist");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const project = JSON.parse(fs.readFileSync(rojoPath, "utf-8"));
|
|
112
|
+
if (!project.tree || !project.tree.ReplicatedStorage) {
|
|
113
|
+
console.error(
|
|
114
|
+
"Invalid Rojo project structure (is there a ReplicatedStorage?)",
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const mapping = buildTreeFromRbxlx(input, folderName);
|
|
120
|
+
project.tree.ReplicatedStorage[folderName] = mapping[folderName];
|
|
121
|
+
fs.writeFileSync(rojoPath, JSON.stringify(project, null, 4));
|
|
122
|
+
console.log(`Mapping generated for '${folderName}'`);
|
|
123
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
// File Layout
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
|
|
8
|
+
// Environment Settings
|
|
9
|
+
// See also https://aka.ms/tsconfig/module
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"moduleResolution": "nodenext",
|
|
12
|
+
"target": "esnext",
|
|
13
|
+
"types": ["node"],
|
|
14
|
+
// For nodejs:
|
|
15
|
+
// "lib": ["esnext"],
|
|
16
|
+
// "types": ["node"],
|
|
17
|
+
// and npm install -D @types/node
|
|
18
|
+
|
|
19
|
+
// Other Outputs
|
|
20
|
+
"sourceMap": true,
|
|
21
|
+
"declaration": true,
|
|
22
|
+
"declarationMap": true,
|
|
23
|
+
|
|
24
|
+
// Stricter Typechecking Options
|
|
25
|
+
"noUncheckedIndexedAccess": true,
|
|
26
|
+
"exactOptionalPropertyTypes": true,
|
|
27
|
+
|
|
28
|
+
// Style Options
|
|
29
|
+
// "noImplicitReturns": true,
|
|
30
|
+
// "noImplicitOverride": true,
|
|
31
|
+
// "noUnusedLocals": true,
|
|
32
|
+
// "noUnusedParameters": true,
|
|
33
|
+
// "noFallthroughCasesInSwitch": true,
|
|
34
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
35
|
+
|
|
36
|
+
// Recommended Options
|
|
37
|
+
"strict": true,
|
|
38
|
+
"jsx": "react-jsx",
|
|
39
|
+
"verbatimModuleSyntax": true,
|
|
40
|
+
"isolatedModules": true,
|
|
41
|
+
"noUncheckedSideEffectImports": true,
|
|
42
|
+
"moduleDetection": "force",
|
|
43
|
+
"skipLibCheck": true
|
|
44
|
+
}
|
|
45
|
+
}
|