roport 1.4.0 → 2.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/dist/index.js +933 -0
- package/package.json +25 -32
- package/README.md +0 -52
- package/assets/RoportSyncPlugin.rbxmx +0 -1864
- package/bin/roport.js +0 -131
- package/src/builder.js +0 -563
- package/src/context.js +0 -76
- package/src/ignore.js +0 -120
- package/src/server.js +0 -393
- package/templates/default/README.md +0 -28
- package/templates/default/default.project.json +0 -23
- package/templates/default/src/client/init.client.lua +0 -1
- package/templates/default/src/server/init.server.lua +0 -1
- package/templates/default/src/shared/Hello.lua +0 -7
package/src/builder.js
DELETED
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const IgnoreManager = require('./ignore');
|
|
5
|
-
|
|
6
|
-
class RbxmxBuilder {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.referentCount = 0;
|
|
9
|
-
this.ignoreManager = null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
generateReferent() {
|
|
13
|
-
return `RBX${this.referentCount++}`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async writeProperties(write, properties) {
|
|
17
|
-
await write(' <Properties>\n');
|
|
18
|
-
for (const [name, value] of Object.entries(properties)) {
|
|
19
|
-
if (name === 'Source') {
|
|
20
|
-
await write(` <ProtectedString name="Source"><![CDATA[${value}]]></ProtectedString>\n`);
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (name === 'Name') {
|
|
25
|
-
await write(` <string name="Name">${this.escapeXml(value)}</string>\n`);
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (typeof value === 'boolean') {
|
|
30
|
-
await write(` <bool name="${name}">${value}</bool>\n`);
|
|
31
|
-
} else if (typeof value === 'number') {
|
|
32
|
-
await write(` <double name="${name}">${value}</double>\n`);
|
|
33
|
-
} else if (typeof value === 'string') {
|
|
34
|
-
await write(` <string name="${name}">${this.escapeXml(value)}</string>\n`);
|
|
35
|
-
} else if (typeof value === 'object' && value !== null && value.$type) {
|
|
36
|
-
switch (value.$type) {
|
|
37
|
-
case 'Vector3':
|
|
38
|
-
await write(` <Vector3 name="${name}">
|
|
39
|
-
<X>${value.value[0]}</X>
|
|
40
|
-
<Y>${value.value[1]}</Y>
|
|
41
|
-
<Z>${value.value[2]}</Z>
|
|
42
|
-
</Vector3>\n`);
|
|
43
|
-
break;
|
|
44
|
-
case 'Vector2':
|
|
45
|
-
await write(` <Vector2 name="${name}">
|
|
46
|
-
<X>${value.value[0]}</X>
|
|
47
|
-
<Y>${value.value[1]}</Y>
|
|
48
|
-
</Vector2>\n`);
|
|
49
|
-
break;
|
|
50
|
-
case 'Color3':
|
|
51
|
-
await write(` <Color3 name="${name}">
|
|
52
|
-
<R>${value.value[0]}</R>
|
|
53
|
-
<G>${value.value[1]}</G>
|
|
54
|
-
<B>${value.value[2]}</B>
|
|
55
|
-
</Color3>\n`);
|
|
56
|
-
break;
|
|
57
|
-
case 'UDim2':
|
|
58
|
-
await write(` <UDim2 name="${name}">
|
|
59
|
-
<XS>${value.value[0]}</XS>
|
|
60
|
-
<XO>${value.value[1]}</XO>
|
|
61
|
-
<YS>${value.value[2]}</YS>
|
|
62
|
-
<YO>${value.value[3]}</YO>
|
|
63
|
-
</UDim2>\n`);
|
|
64
|
-
break;
|
|
65
|
-
case 'Enum':
|
|
66
|
-
await write(` <token name="${name}">${value.value}</token>\n`);
|
|
67
|
-
break;
|
|
68
|
-
case 'CFrame':
|
|
69
|
-
// value: [x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22]
|
|
70
|
-
const cf = value.value;
|
|
71
|
-
await write(` <CoordinateFrame name="${name}">
|
|
72
|
-
<X>${cf[0]}</X><Y>${cf[1]}</Y><Z>${cf[2]}</Z>
|
|
73
|
-
<R00>${cf[3]}</R00><R01>${cf[4]}</R01><R02>${cf[5]}</R02>
|
|
74
|
-
<R10>${cf[6]}</R10><R11>${cf[7]}</R11><R12>${cf[8]}</R12>
|
|
75
|
-
<R20>${cf[9]}</R20><R21>${cf[10]}</R21><R22>${cf[11]}</R22>
|
|
76
|
-
</CoordinateFrame>\n`);
|
|
77
|
-
break;
|
|
78
|
-
case 'BrickColor':
|
|
79
|
-
await write(` <int name="${name}">${value.value}</int>\n`);
|
|
80
|
-
break;
|
|
81
|
-
case 'Rect':
|
|
82
|
-
// value: [minX, minY, maxX, maxY]
|
|
83
|
-
await write(` <Rect2D name="${name}">
|
|
84
|
-
<min><X>${value.value[0]}</X><Y>${value.value[1]}</Y></min>
|
|
85
|
-
<max><X>${value.value[2]}</X><Y>${value.value[3]}</Y></max>
|
|
86
|
-
</Rect2D>\n`);
|
|
87
|
-
break;
|
|
88
|
-
case 'NumberRange':
|
|
89
|
-
// value: [min, max]
|
|
90
|
-
await write(` <NumberRange name="${name}">${value.value[0]} ${value.value[1]}</NumberRange>\n`);
|
|
91
|
-
break;
|
|
92
|
-
case 'NumberSequence':
|
|
93
|
-
// value: string "0 0.5 0 1 1 0" or array
|
|
94
|
-
const nsVal = Array.isArray(value.value) ? value.value.join(' ') : value.value;
|
|
95
|
-
await write(` <NumberSequence name="${name}">${nsVal}</NumberSequence>\n`);
|
|
96
|
-
break;
|
|
97
|
-
case 'ColorSequence':
|
|
98
|
-
// value: string or array
|
|
99
|
-
const csVal = Array.isArray(value.value) ? value.value.join(' ') : value.value;
|
|
100
|
-
await write(` <ColorSequence name="${name}">${csVal}</ColorSequence>\n`);
|
|
101
|
-
break;
|
|
102
|
-
case 'PhysicalProperties':
|
|
103
|
-
const pp = value.value;
|
|
104
|
-
await write(` <PhysicalProperties name="${name}">
|
|
105
|
-
<CustomPhysics>true</CustomPhysics>
|
|
106
|
-
<Density>${pp.Density}</Density>
|
|
107
|
-
<Friction>${pp.Friction}</Friction>
|
|
108
|
-
<Elasticity>${pp.Elasticity}</Elasticity>
|
|
109
|
-
<FrictionWeight>${pp.FrictionWeight}</FrictionWeight>
|
|
110
|
-
<ElasticityWeight>${pp.ElasticityWeight}</ElasticityWeight>
|
|
111
|
-
</PhysicalProperties>\n`);
|
|
112
|
-
break;
|
|
113
|
-
case 'Font':
|
|
114
|
-
const font = value.value;
|
|
115
|
-
await write(` <Font name="${name}">
|
|
116
|
-
<Family><url>${font.family}</url></Family>
|
|
117
|
-
<Weight>${font.weight}</Weight>
|
|
118
|
-
<Style>${font.style}</Style>
|
|
119
|
-
</Font>\n`);
|
|
120
|
-
break;
|
|
121
|
-
case 'Ray':
|
|
122
|
-
const ray = value.value; // [ox, oy, oz, dx, dy, dz]
|
|
123
|
-
await write(` <Ray name="${name}">
|
|
124
|
-
<origin><X>${ray[0]}</X><Y>${ray[1]}</Y><Z>${ray[2]}</Z></origin>
|
|
125
|
-
<direction><X>${ray[3]}</X><Y>${ray[4]}</Y><Z>${ray[5]}</Z></direction>
|
|
126
|
-
</Ray>\n`);
|
|
127
|
-
break;
|
|
128
|
-
case 'Axes':
|
|
129
|
-
await write(` <Axes name="${name}"><axes>${value.value}</axes></Axes>\n`);
|
|
130
|
-
break;
|
|
131
|
-
case 'Faces':
|
|
132
|
-
await write(` <Faces name="${name}"><faces>${value.value}</faces></Faces>\n`);
|
|
133
|
-
break;
|
|
134
|
-
case 'Ref':
|
|
135
|
-
// value: string (referent ID)
|
|
136
|
-
const refVal = value.value || 'null';
|
|
137
|
-
await write(` <Ref name="${name}">${refVal}</Ref>\n`);
|
|
138
|
-
break;
|
|
139
|
-
case 'BinaryString':
|
|
140
|
-
// value: base64 string
|
|
141
|
-
await write(` <BinaryString name="${name}"><![CDATA[${value.value}]]></BinaryString>\n`);
|
|
142
|
-
break;
|
|
143
|
-
case 'UDim':
|
|
144
|
-
// value: [scale, offset]
|
|
145
|
-
await write(` <UDim name="${name}">
|
|
146
|
-
<S>${value.value[0]}</S>
|
|
147
|
-
<O>${value.value[1]}</O>
|
|
148
|
-
</UDim>\n`);
|
|
149
|
-
break;
|
|
150
|
-
case 'Vector3int16':
|
|
151
|
-
// value: [x, y, z]
|
|
152
|
-
await write(` <Vector3int16 name="${name}">
|
|
153
|
-
<X>${value.value[0]}</X>
|
|
154
|
-
<Y>${value.value[1]}</Y>
|
|
155
|
-
<Z>${value.value[2]}</Z>
|
|
156
|
-
</Vector3int16>\n`);
|
|
157
|
-
break;
|
|
158
|
-
case 'SharedString':
|
|
159
|
-
// value: base64 string
|
|
160
|
-
await write(` <SharedString name="${name}">${value.value}</SharedString>\n`);
|
|
161
|
-
break;
|
|
162
|
-
case 'ProtectedString':
|
|
163
|
-
await write(` <ProtectedString name="${name}"><![CDATA[${value.value}]]></ProtectedString>\n`);
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
await write(' </Properties>\n');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
escapeXml(unsafe) {
|
|
172
|
-
return String(unsafe).replace(/[<>&'"]/g, function (c) {
|
|
173
|
-
switch (c) {
|
|
174
|
-
case '<': return '<';
|
|
175
|
-
case '>': return '>';
|
|
176
|
-
case '&': return '&';
|
|
177
|
-
case '\'': return ''';
|
|
178
|
-
case '"': return '"';
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
buildProperties(props) {
|
|
184
|
-
let xml = ' <Properties>\n';
|
|
185
|
-
for (const [name, value] of Object.entries(props)) {
|
|
186
|
-
if (name === 'Name') {
|
|
187
|
-
xml += ` <string name="Name">${this.escapeXml(value)}</string>\n`;
|
|
188
|
-
} else if (typeof value === 'boolean') {
|
|
189
|
-
xml += ` <bool name="${name}">${value}</bool>\n`;
|
|
190
|
-
} else if (typeof value === 'number') {
|
|
191
|
-
xml += ` <double name="${name}">${value}</double>\n`;
|
|
192
|
-
} else if (typeof value === 'string') {
|
|
193
|
-
xml += ` <string name="${name}">${this.escapeXml(value)}</string>\n`;
|
|
194
|
-
}
|
|
195
|
-
// TODO: Add more types (Vector3, Color3, etc) as needed
|
|
196
|
-
}
|
|
197
|
-
xml += ' </Properties>\n';
|
|
198
|
-
return xml;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async processNode(node, name, currentPath, write, initOverrideFile = null) {
|
|
202
|
-
let className = node.$className || node.type || 'Folder';
|
|
203
|
-
// Allow manual referent override for linking
|
|
204
|
-
const referent = node.$referent || this.generateReferent();
|
|
205
|
-
|
|
206
|
-
// Determine properties
|
|
207
|
-
const properties = node.$properties || {};
|
|
208
|
-
properties.Name = name;
|
|
209
|
-
|
|
210
|
-
// Handle $path
|
|
211
|
-
let childrenProcessor = async () => {}; // Deferred children processing
|
|
212
|
-
|
|
213
|
-
if (node.$path) {
|
|
214
|
-
const filePath = path.resolve(currentPath, node.$path);
|
|
215
|
-
if (await fs.pathExists(filePath)) {
|
|
216
|
-
const stat = await fs.stat(filePath);
|
|
217
|
-
|
|
218
|
-
if (stat.isDirectory()) {
|
|
219
|
-
// Check for init scripts in this directory to transform the container
|
|
220
|
-
const initServer = path.join(filePath, 'init.server.lua');
|
|
221
|
-
const initClient = path.join(filePath, 'init.client.lua');
|
|
222
|
-
const initModule = path.join(filePath, 'init.lua');
|
|
223
|
-
const initMeta = path.join(filePath, 'init.meta.json');
|
|
224
|
-
|
|
225
|
-
let initContent = null;
|
|
226
|
-
|
|
227
|
-
if (initOverrideFile) {
|
|
228
|
-
// Use the provided file as the source of truth for this container
|
|
229
|
-
if (initOverrideFile.endsWith('.server.lua') || initOverrideFile.endsWith('.server.luau')) {
|
|
230
|
-
className = 'Script';
|
|
231
|
-
initContent = await fs.readFile(initOverrideFile, 'utf8');
|
|
232
|
-
} else if (initOverrideFile.endsWith('.client.lua') || initOverrideFile.endsWith('.client.luau')) {
|
|
233
|
-
className = 'LocalScript';
|
|
234
|
-
initContent = await fs.readFile(initOverrideFile, 'utf8');
|
|
235
|
-
} else if (initOverrideFile.endsWith('.lua') || initOverrideFile.endsWith('.luau')) {
|
|
236
|
-
className = 'ModuleScript';
|
|
237
|
-
initContent = await fs.readFile(initOverrideFile, 'utf8');
|
|
238
|
-
} else if (initOverrideFile.endsWith('.model.json')) {
|
|
239
|
-
try {
|
|
240
|
-
const modelData = await fs.readJson(initOverrideFile);
|
|
241
|
-
className = modelData.className || modelData.ClassName || modelData.type || className;
|
|
242
|
-
if (modelData.properties) Object.assign(properties, modelData.properties);
|
|
243
|
-
} catch (e) {}
|
|
244
|
-
}
|
|
245
|
-
} else {
|
|
246
|
-
// Standard init file check
|
|
247
|
-
if (await fs.pathExists(initServer)) {
|
|
248
|
-
className = 'Script';
|
|
249
|
-
initContent = await fs.readFile(initServer, 'utf8');
|
|
250
|
-
} else if (await fs.pathExists(initClient)) {
|
|
251
|
-
className = 'LocalScript';
|
|
252
|
-
initContent = await fs.readFile(initClient, 'utf8');
|
|
253
|
-
} else if (await fs.pathExists(initModule)) {
|
|
254
|
-
className = 'ModuleScript';
|
|
255
|
-
initContent = await fs.readFile(initModule, 'utf8');
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (await fs.pathExists(initMeta)) {
|
|
259
|
-
try {
|
|
260
|
-
const meta = await fs.readJson(initMeta);
|
|
261
|
-
if (meta.className) className = meta.className;
|
|
262
|
-
if (meta.properties) Object.assign(properties, meta.properties);
|
|
263
|
-
} catch (e) {}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (initContent !== null) {
|
|
268
|
-
properties.Source = initContent;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Process directory children
|
|
272
|
-
childrenProcessor = async () => {
|
|
273
|
-
const files = await fs.readdir(filePath);
|
|
274
|
-
const entries = {};
|
|
275
|
-
|
|
276
|
-
// Group files by name to handle File+Folder merging
|
|
277
|
-
for (const file of files) {
|
|
278
|
-
if (file.startsWith('.')) continue;
|
|
279
|
-
|
|
280
|
-
if (this.ignoreManager && this.ignoreManager.isIgnored(path.join(filePath, file))) continue;
|
|
281
|
-
|
|
282
|
-
// Determine base name
|
|
283
|
-
let baseName = file;
|
|
284
|
-
if (file.endsWith('.server.lua')) baseName = file.slice(0, -11);
|
|
285
|
-
else if (file.endsWith('.client.lua')) baseName = file.slice(0, -11);
|
|
286
|
-
else if (file.endsWith('.lua')) baseName = file.slice(0, -4);
|
|
287
|
-
else if (file.endsWith('.server.luau')) baseName = file.slice(0, -12);
|
|
288
|
-
else if (file.endsWith('.client.luau')) baseName = file.slice(0, -12);
|
|
289
|
-
else if (file.endsWith('.luau')) baseName = file.slice(0, -5);
|
|
290
|
-
else if (file.endsWith('.model.json')) baseName = file.slice(0, -11);
|
|
291
|
-
else if (file.endsWith('.json')) baseName = file.slice(0, -5);
|
|
292
|
-
else if (file.endsWith('.txt')) baseName = file.slice(0, -4);
|
|
293
|
-
|
|
294
|
-
// Skip init files as they are handled above
|
|
295
|
-
if (baseName === 'init' || file === 'init.meta.json') continue;
|
|
296
|
-
|
|
297
|
-
if (!entries[baseName]) entries[baseName] = {};
|
|
298
|
-
|
|
299
|
-
const childPath = path.join(filePath, file);
|
|
300
|
-
const childStat = await fs.stat(childPath);
|
|
301
|
-
|
|
302
|
-
if (childStat.isDirectory()) {
|
|
303
|
-
entries[baseName].folder = file;
|
|
304
|
-
} else {
|
|
305
|
-
entries[baseName].file = file;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
for (const [childName, entry] of Object.entries(entries)) {
|
|
310
|
-
if (entry.folder && entry.file) {
|
|
311
|
-
// Merge: Process folder, but use file as init override
|
|
312
|
-
await this.processNode({ $path: entry.folder }, childName, filePath, write, path.join(filePath, entry.file));
|
|
313
|
-
} else if (entry.folder) {
|
|
314
|
-
// Just a folder
|
|
315
|
-
await this.processNode({ $path: entry.folder }, childName, filePath, write);
|
|
316
|
-
} else if (entry.file) {
|
|
317
|
-
// Just a file - process inline
|
|
318
|
-
const file = entry.file;
|
|
319
|
-
const childPath = path.join(filePath, file);
|
|
320
|
-
|
|
321
|
-
let childClass = 'Folder';
|
|
322
|
-
let content = '';
|
|
323
|
-
let childProps = {};
|
|
324
|
-
|
|
325
|
-
if (file.endsWith('.server.lua') || file.endsWith('.server.luau')) childClass = 'Script';
|
|
326
|
-
else if (file.endsWith('.client.lua') || file.endsWith('.client.luau')) childClass = 'LocalScript';
|
|
327
|
-
else if (file.endsWith('.lua') || file.endsWith('.luau')) childClass = 'ModuleScript';
|
|
328
|
-
else if (file.endsWith('.txt')) childClass = 'StringValue';
|
|
329
|
-
else if (file.endsWith('.model.json')) {
|
|
330
|
-
try {
|
|
331
|
-
const modelData = await fs.readJson(childPath);
|
|
332
|
-
childClass = modelData.className || modelData.ClassName || modelData.type || 'Folder';
|
|
333
|
-
childProps = modelData.properties || {};
|
|
334
|
-
} catch (e) {}
|
|
335
|
-
}
|
|
336
|
-
else if (file.endsWith('.json')) childClass = 'ModuleScript';
|
|
337
|
-
|
|
338
|
-
if (childClass !== 'Folder' || file.endsWith('.model.json')) {
|
|
339
|
-
if (file.endsWith('.json') && !file.endsWith('.model.json')) {
|
|
340
|
-
const jsonContent = await fs.readFile(childPath, 'utf8');
|
|
341
|
-
content = `return game:GetService("HttpService"):JSONDecode([[${jsonContent}]])`;
|
|
342
|
-
} else if (!file.endsWith('.model.json')) {
|
|
343
|
-
content = await fs.readFile(childPath, 'utf8');
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const childRef = childProps.$referent || this.generateReferent();
|
|
347
|
-
await write(` <Item class="${childClass}" referent="${childRef}">\n`);
|
|
348
|
-
|
|
349
|
-
// Prepare properties
|
|
350
|
-
childProps.Name = childName;
|
|
351
|
-
if (childClass.includes('Script')) {
|
|
352
|
-
childProps.Source = content;
|
|
353
|
-
} else if (childClass === 'StringValue') {
|
|
354
|
-
childProps.Value = content;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
await this.writeProperties(write, childProps);
|
|
358
|
-
await write(` </Item>\n`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
} else {
|
|
364
|
-
// It's a file mapped directly (e.g. a script)
|
|
365
|
-
const ext = path.extname(filePath);
|
|
366
|
-
if (ext === '.lua' || ext === '.luau') {
|
|
367
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
368
|
-
properties.Source = content;
|
|
369
|
-
if (className === 'Folder') {
|
|
370
|
-
if (filePath.endsWith('.server.lua')) className = 'Script';
|
|
371
|
-
else if (filePath.endsWith('.client.lua')) className = 'LocalScript';
|
|
372
|
-
else className = 'ModuleScript';
|
|
373
|
-
}
|
|
374
|
-
} else if (filePath.endsWith('.model.json')) {
|
|
375
|
-
try {
|
|
376
|
-
const modelData = await fs.readJson(filePath);
|
|
377
|
-
className = modelData.className || modelData.ClassName || modelData.type || className;
|
|
378
|
-
if (modelData.properties) Object.assign(properties, modelData.properties);
|
|
379
|
-
} catch (e) {}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
await write(` <Item class="${className}" referent="${referent}">\n`);
|
|
386
|
-
|
|
387
|
-
await this.writeProperties(write, properties);
|
|
388
|
-
|
|
389
|
-
// Process children
|
|
390
|
-
await childrenProcessor();
|
|
391
|
-
|
|
392
|
-
// Handle nested tree nodes
|
|
393
|
-
for (const key in node) {
|
|
394
|
-
if (!key.startsWith('$') && typeof node[key] === 'object') {
|
|
395
|
-
await this.processNode(node[key], key, currentPath, write);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
await write(` </Item>\n`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async build(projectPath, outputPath) {
|
|
403
|
-
console.log(chalk.blue(`Building project from ${projectPath}...`));
|
|
404
|
-
|
|
405
|
-
this.ignoreManager = new IgnoreManager(projectPath);
|
|
406
|
-
|
|
407
|
-
const configPath = path.join(projectPath, 'default.project.json');
|
|
408
|
-
if (!await fs.pathExists(configPath)) {
|
|
409
|
-
throw new Error('default.project.json not found');
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const config = await fs.readJson(configPath);
|
|
413
|
-
const rootName = config.name || 'Project';
|
|
414
|
-
const tree = config.tree;
|
|
415
|
-
|
|
416
|
-
const stream = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
|
417
|
-
|
|
418
|
-
// Helper to handle backpressure
|
|
419
|
-
const write = (chunk) => {
|
|
420
|
-
return new Promise((resolve, reject) => {
|
|
421
|
-
if (!stream.write(chunk)) {
|
|
422
|
-
stream.once('drain', resolve);
|
|
423
|
-
} else {
|
|
424
|
-
resolve();
|
|
425
|
-
}
|
|
426
|
-
stream.once('error', reject);
|
|
427
|
-
});
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
await write('<roblox version="4">\n');
|
|
432
|
-
await this.processNode(tree, rootName, projectPath, write);
|
|
433
|
-
await write('</roblox>');
|
|
434
|
-
} finally {
|
|
435
|
-
stream.end();
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
console.log(chalk.green(`Build complete: ${outputPath}`));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async processNodeSourcemap(node, name, currentPath) {
|
|
442
|
-
let className = node.$className || 'Folder';
|
|
443
|
-
const children = [];
|
|
444
|
-
const filePaths = [];
|
|
445
|
-
|
|
446
|
-
// Handle $path
|
|
447
|
-
if (node.$path) {
|
|
448
|
-
const filePath = path.resolve(currentPath, node.$path);
|
|
449
|
-
if (await fs.pathExists(filePath)) {
|
|
450
|
-
filePaths.push(filePath);
|
|
451
|
-
const stat = await fs.stat(filePath);
|
|
452
|
-
|
|
453
|
-
if (stat.isDirectory()) {
|
|
454
|
-
// Check for init scripts
|
|
455
|
-
const initServer = path.join(filePath, 'init.server.lua');
|
|
456
|
-
const initClient = path.join(filePath, 'init.client.lua');
|
|
457
|
-
const initModule = path.join(filePath, 'init.lua');
|
|
458
|
-
|
|
459
|
-
if (await fs.pathExists(initServer)) {
|
|
460
|
-
className = 'Script';
|
|
461
|
-
filePaths.push(initServer);
|
|
462
|
-
} else if (await fs.pathExists(initClient)) {
|
|
463
|
-
className = 'LocalScript';
|
|
464
|
-
filePaths.push(initClient);
|
|
465
|
-
} else if (await fs.pathExists(initModule)) {
|
|
466
|
-
className = 'ModuleScript';
|
|
467
|
-
filePaths.push(initModule);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Process directory children
|
|
471
|
-
const files = await fs.readdir(filePath);
|
|
472
|
-
for (const file of files) {
|
|
473
|
-
if (file.startsWith('.')) continue;
|
|
474
|
-
|
|
475
|
-
const childPath = path.join(filePath, file);
|
|
476
|
-
if (this.ignoreManager && this.ignoreManager.isIgnored(childPath)) continue;
|
|
477
|
-
|
|
478
|
-
const childStat = await fs.stat(childPath);
|
|
479
|
-
|
|
480
|
-
if (childStat.isDirectory()) {
|
|
481
|
-
children.push(await this.processNodeSourcemap({ $path: file }, file, filePath));
|
|
482
|
-
} else {
|
|
483
|
-
const ext = path.extname(file);
|
|
484
|
-
const basename = path.basename(file, ext);
|
|
485
|
-
|
|
486
|
-
if (basename.startsWith('init') && (ext === '.lua' || ext === '.luau')) continue;
|
|
487
|
-
|
|
488
|
-
let childClass = 'Folder';
|
|
489
|
-
if (file.endsWith('.server.lua') || file.endsWith('.server.luau')) childClass = 'Script';
|
|
490
|
-
else if (file.endsWith('.client.lua') || file.endsWith('.client.luau')) childClass = 'LocalScript';
|
|
491
|
-
else if (file.endsWith('.lua') || file.endsWith('.luau')) childClass = 'ModuleScript';
|
|
492
|
-
else if (file.endsWith('.txt')) childClass = 'StringValue';
|
|
493
|
-
else if (file.endsWith('.json')) childClass = 'ModuleScript';
|
|
494
|
-
|
|
495
|
-
if (childClass !== 'Folder') {
|
|
496
|
-
children.push({
|
|
497
|
-
name: basename.replace('.server', '').replace('.client', ''),
|
|
498
|
-
className: childClass,
|
|
499
|
-
filePaths: [childPath]
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
} else {
|
|
505
|
-
// File mapped directly
|
|
506
|
-
const ext = path.extname(filePath);
|
|
507
|
-
if (ext === '.lua' || ext === '.luau') {
|
|
508
|
-
if (className === 'Folder') {
|
|
509
|
-
if (filePath.endsWith('.server.lua')) className = 'Script';
|
|
510
|
-
else if (filePath.endsWith('.client.lua')) className = 'LocalScript';
|
|
511
|
-
else className = 'ModuleScript';
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Handle nested tree nodes
|
|
519
|
-
for (const key in node) {
|
|
520
|
-
if (!key.startsWith('$') && typeof node[key] === 'object') {
|
|
521
|
-
children.push(await this.processNodeSourcemap(node[key], key, currentPath));
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const result = {
|
|
526
|
-
name: name,
|
|
527
|
-
className: className,
|
|
528
|
-
filePaths: filePaths
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
if (children.length > 0) {
|
|
532
|
-
result.children = children;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return result;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async sourcemap(projectPath, outputPath) {
|
|
539
|
-
console.log(chalk.blue(`Generating sourcemap from ${projectPath}...`));
|
|
540
|
-
|
|
541
|
-
this.ignoreManager = new IgnoreManager(projectPath);
|
|
542
|
-
|
|
543
|
-
const configPath = path.join(projectPath, 'default.project.json');
|
|
544
|
-
if (!await fs.pathExists(configPath)) {
|
|
545
|
-
throw new Error('default.project.json not found');
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const config = await fs.readJson(configPath);
|
|
549
|
-
const rootName = config.name || 'Project';
|
|
550
|
-
const tree = config.tree;
|
|
551
|
-
|
|
552
|
-
const map = await this.processNodeSourcemap(tree, rootName, projectPath);
|
|
553
|
-
|
|
554
|
-
if (outputPath) {
|
|
555
|
-
await fs.outputJson(outputPath, map, { spaces: 2 });
|
|
556
|
-
console.log(chalk.green(`Sourcemap generated: ${outputPath}`));
|
|
557
|
-
} else {
|
|
558
|
-
console.log(JSON.stringify(map, null, 2));
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
module.exports = new RbxmxBuilder();
|
package/src/context.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
const fs = require('fs-extra');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const IgnoreManager = require('./ignore');
|
|
5
|
-
|
|
6
|
-
class ContextGenerator {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.ignoreManager = null;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async generate(dir, options = {}) {
|
|
12
|
-
const { maxDepth = 5, includeSource = false } = options;
|
|
13
|
-
this.ignoreManager = new IgnoreManager(dir);
|
|
14
|
-
let output = `# Project Context: ${path.basename(dir)}\n\n`;
|
|
15
|
-
|
|
16
|
-
output += await this.traverse(dir, 0, maxDepth, includeSource);
|
|
17
|
-
|
|
18
|
-
return output;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async traverse(currentPath, depth, maxDepth, includeSource) {
|
|
22
|
-
if (depth > maxDepth) return '';
|
|
23
|
-
|
|
24
|
-
let output = '';
|
|
25
|
-
const items = await fs.readdir(currentPath);
|
|
26
|
-
|
|
27
|
-
// Sort: Directories first, then files
|
|
28
|
-
items.sort((a, b) => {
|
|
29
|
-
const aPath = path.join(currentPath, a);
|
|
30
|
-
const bPath = path.join(currentPath, b);
|
|
31
|
-
const aStat = fs.statSync(aPath);
|
|
32
|
-
const bStat = fs.statSync(bPath);
|
|
33
|
-
if (aStat.isDirectory() && !bStat.isDirectory()) return -1;
|
|
34
|
-
if (!aStat.isDirectory() && bStat.isDirectory()) return 1;
|
|
35
|
-
return a.localeCompare(b);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
for (const item of items) {
|
|
39
|
-
if (this.ignoreManager.isIgnored(path.join(currentPath, item))) continue;
|
|
40
|
-
|
|
41
|
-
const fullPath = path.join(currentPath, item);
|
|
42
|
-
const stat = await fs.stat(fullPath);
|
|
43
|
-
const indent = ' '.repeat(depth);
|
|
44
|
-
|
|
45
|
-
if (stat.isDirectory()) {
|
|
46
|
-
output += `${indent}- 📂 **${item}/**\n`;
|
|
47
|
-
output += await this.traverse(fullPath, depth + 1, maxDepth, includeSource);
|
|
48
|
-
} else {
|
|
49
|
-
const ext = path.extname(item);
|
|
50
|
-
let icon = '📄';
|
|
51
|
-
if (ext === '.lua' || ext === '.luau') icon = '📜';
|
|
52
|
-
if (ext === '.json') icon = '⚙️';
|
|
53
|
-
if (ext === '.model.json') icon = '📦';
|
|
54
|
-
|
|
55
|
-
output += `${indent}- ${icon} ${item}\n`;
|
|
56
|
-
|
|
57
|
-
if (includeSource && (ext === '.lua' || ext === '.luau' || ext === '.model.json' || item === 'default.project.json')) {
|
|
58
|
-
try {
|
|
59
|
-
const content = await fs.readFile(fullPath, 'utf8');
|
|
60
|
-
// Truncate if too long
|
|
61
|
-
if (content.length > 2000) {
|
|
62
|
-
output += `${indent} \`\`\`${ext.slice(1)}\n${content.slice(0, 2000)}\n... (truncated)\n \`\`\`\n`;
|
|
63
|
-
} else {
|
|
64
|
-
output += `${indent} \`\`\`${ext.slice(1)}\n${content}\n \`\`\`\n`;
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
output += `${indent} *(Error reading file)*\n`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return output;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
module.exports = new ContextGenerator();
|