@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 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);
@@ -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
+ }