syncrbx 1.1.0 → 1.1.1
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 -1
- package/converters.js +33 -10
- package/local-sync.js +82 -21
- package/package.json +1 -1
package/README.md
CHANGED
package/converters.js
CHANGED
|
@@ -382,24 +382,44 @@ function filePathToInstancePath(filePath, projectRoot) {
|
|
|
382
382
|
// ---------------------------------------------------------------------------
|
|
383
383
|
// Phase 2 - #4: Project config loader
|
|
384
384
|
// ---------------------------------------------------------------------------
|
|
385
|
+
const PROJECT_CONFIG_FILE = 'syncrbx.project.json';
|
|
386
|
+
|
|
387
|
+
// Keeps only known Roblox services, without duplicates. Returns null when the
|
|
388
|
+
// value is not a list, meaning "not chosen yet".
|
|
389
|
+
function sanitizeServices(services) {
|
|
390
|
+
if (!Array.isArray(services)) return null;
|
|
391
|
+
return [...new Set(services.filter(s => typeof s === 'string' && ROOT_SERVICES.includes(s)))];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readProjectConfigFile(projectRoot) {
|
|
395
|
+
const configPath = path.join(projectRoot, PROJECT_CONFIG_FILE);
|
|
396
|
+
if (!fs.existsSync(configPath)) return {};
|
|
397
|
+
try {
|
|
398
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
399
|
+
} catch (e) {
|
|
400
|
+
console.error(`[WARN] Error parsing ${PROJECT_CONFIG_FILE}: ${e.message}`);
|
|
401
|
+
return {};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
385
405
|
function loadProjectConfig(projectRoot) {
|
|
386
|
-
const configPath = path.join(projectRoot, 'syncrbx.project.json');
|
|
387
406
|
const defaults = {
|
|
388
407
|
name: path.basename(projectRoot),
|
|
389
408
|
port: 34872,
|
|
390
409
|
ignore: ['*.conflict.bak', 'node_modules', '.git', '.vscode'],
|
|
391
410
|
tree: null, // null = auto-detect from folder names
|
|
411
|
+
services: null, // null = the user has not chosen which services to sync
|
|
392
412
|
};
|
|
413
|
+
const config = { ...defaults, ...readProjectConfigFile(projectRoot) };
|
|
414
|
+
config.services = sanitizeServices(config.services);
|
|
415
|
+
return config;
|
|
416
|
+
}
|
|
393
417
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
console.error(`[WARN] Error parsing syncrbx.project.json: ${e.message}`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return defaults;
|
|
418
|
+
// Saves the chosen services, keeping any other settings already in the file.
|
|
419
|
+
function saveProjectServices(projectRoot, services) {
|
|
420
|
+
const config = readProjectConfigFile(projectRoot);
|
|
421
|
+
config.services = services;
|
|
422
|
+
fs.writeFileSync(path.join(projectRoot, PROJECT_CONFIG_FILE), JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
403
423
|
}
|
|
404
424
|
|
|
405
425
|
module.exports = {
|
|
@@ -420,5 +440,8 @@ module.exports = {
|
|
|
420
440
|
filePathToInstancePath,
|
|
421
441
|
getMetaProperties,
|
|
422
442
|
loadProjectConfig,
|
|
443
|
+
saveProjectServices,
|
|
444
|
+
sanitizeServices,
|
|
445
|
+
PROJECT_CONFIG_FILE,
|
|
423
446
|
fileChecksum,
|
|
424
447
|
};
|
package/local-sync.js
CHANGED
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
processDirectory, convertFile, instanceToFile,
|
|
7
7
|
filePathToInstancePath, stripExtension, ROOT_SERVICES,
|
|
8
8
|
buildChecksums, loadProjectConfig, classFromFileName, isInitFile, ANY_CLASS,
|
|
9
|
+
saveProjectServices, sanitizeServices, STARTER_PLAYER_CHILDREN,
|
|
9
10
|
} = require('./converters');
|
|
10
11
|
|
|
11
12
|
const app = express();
|
|
@@ -17,6 +18,45 @@ const PROJECT_ROOT = process.cwd();
|
|
|
17
18
|
|
|
18
19
|
const projectConfig = loadProjectConfig(PROJECT_ROOT);
|
|
19
20
|
const PORT = projectConfig.port || 34872;
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Synced services — the user picks them in the Studio plugin the first time a
|
|
24
|
+
// project connects; they are saved in syncrbx.project.json. Nothing outside
|
|
25
|
+
// them is read, written or deleted, in either direction.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function isServiceSynced(serviceName) {
|
|
28
|
+
// Projects that have not chosen yet keep the previous behaviour (every
|
|
29
|
+
// known service), so older plugins still work.
|
|
30
|
+
const services = projectConfig.services || ROOT_SERVICES;
|
|
31
|
+
return services.includes(serviceName);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// "Workspace/Map/Door" → is Workspace synced?
|
|
35
|
+
function isInstancePathSynced(instancePath) {
|
|
36
|
+
return isServiceSynced(String(instancePath || '').split('/')[0]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isDiskPathSynced(filePath) {
|
|
40
|
+
const relative = path.relative(PROJECT_ROOT, filePath);
|
|
41
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
|
|
42
|
+
return isServiceSynced(relative.split(path.sep)[0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Creates the folder of each synced service that does not exist yet.
|
|
46
|
+
function createServiceFolders(services) {
|
|
47
|
+
const folders = [...services];
|
|
48
|
+
if (services.includes('StarterPlayer')) {
|
|
49
|
+
folders.push(...STARTER_PLAYER_CHILDREN.map(child => `StarterPlayer/${child}`));
|
|
50
|
+
}
|
|
51
|
+
for (const folder of folders) {
|
|
52
|
+
const folderPath = path.join(PROJECT_ROOT, folder);
|
|
53
|
+
if (!fs.existsSync(folderPath)) {
|
|
54
|
+
markIgnored(folderPath);
|
|
55
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
|
56
|
+
logDisk(`Folder created: ${folder}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
20
60
|
const HOST = '127.0.0.1';
|
|
21
61
|
const ALLOWED_HOSTS = new Set([`localhost:${PORT}`, `127.0.0.1:${PORT}`, `[::1]:${PORT}`]);
|
|
22
62
|
|
|
@@ -257,30 +297,30 @@ const watcher = chokidar.watch(PROJECT_ROOT, {
|
|
|
257
297
|
|
|
258
298
|
watcher
|
|
259
299
|
.on('add', filePath => {
|
|
260
|
-
if (isIgnored(filePath)) return;
|
|
300
|
+
if (isIgnored(filePath) || !isDiskPathSynced(filePath)) return;
|
|
261
301
|
logDisk(`File created: ${path.relative(PROJECT_ROOT, filePath)}`);
|
|
262
302
|
getTimestamps(filePath).disk = Date.now();
|
|
263
303
|
pushChange(buildChangePayload('Added', filePath));
|
|
264
304
|
})
|
|
265
305
|
.on('change', filePath => {
|
|
266
|
-
if (isIgnored(filePath)) return;
|
|
306
|
+
if (isIgnored(filePath) || !isDiskPathSynced(filePath)) return;
|
|
267
307
|
logDisk(`File modified: ${path.relative(PROJECT_ROOT, filePath)}`);
|
|
268
308
|
getTimestamps(filePath).disk = Date.now();
|
|
269
309
|
pushChange(buildChangePayload('Changed', filePath));
|
|
270
310
|
})
|
|
271
311
|
.on('unlink', filePath => {
|
|
272
|
-
if (isIgnored(filePath)) return;
|
|
312
|
+
if (isIgnored(filePath) || !isDiskPathSynced(filePath)) return;
|
|
273
313
|
logDisk(`File deleted: ${path.relative(PROJECT_ROOT, filePath)}`);
|
|
274
314
|
pushChange(buildChangePayload('Removed', filePath));
|
|
275
315
|
lastWriteTimestamps.delete(path.resolve(filePath));
|
|
276
316
|
})
|
|
277
317
|
.on('addDir', dirPath => {
|
|
278
|
-
if (isIgnored(dirPath)) return;
|
|
318
|
+
if (isIgnored(dirPath) || !isDiskPathSynced(dirPath)) return;
|
|
279
319
|
logDisk(`Folder created: ${path.relative(PROJECT_ROOT, dirPath)}`);
|
|
280
320
|
pushChange(buildChangePayload('Added', dirPath, true));
|
|
281
321
|
})
|
|
282
322
|
.on('unlinkDir', dirPath => {
|
|
283
|
-
if (isIgnored(dirPath)) return;
|
|
323
|
+
if (isIgnored(dirPath) || !isDiskPathSynced(dirPath)) return;
|
|
284
324
|
logDisk(`Folder deleted: ${path.relative(PROJECT_ROOT, dirPath)}`);
|
|
285
325
|
pushChange(buildChangePayload('Removed', dirPath, true));
|
|
286
326
|
});
|
|
@@ -294,9 +334,32 @@ app.get('/ping', (req, res) => {
|
|
|
294
334
|
status: 'ok',
|
|
295
335
|
project: projectConfig.name || path.basename(PROJECT_ROOT),
|
|
296
336
|
version: require('./package.json').version,
|
|
337
|
+
// false until the user picks the services to sync in the plugin
|
|
338
|
+
configured: Array.isArray(projectConfig.services),
|
|
339
|
+
services: projectConfig.services,
|
|
340
|
+
availableServices: ROOT_SERVICES,
|
|
297
341
|
});
|
|
298
342
|
});
|
|
299
343
|
|
|
344
|
+
// POST /config — saves the services chosen in the plugin
|
|
345
|
+
app.post('/config', (req, res) => {
|
|
346
|
+
const requested = req.body && req.body.services;
|
|
347
|
+
const services = sanitizeServices(requested);
|
|
348
|
+
if (!services || services.length === 0 || services.length !== requested.length) {
|
|
349
|
+
return res.status(400).json({ error: 'services must be a non-empty list of Roblox services' });
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
saveProjectServices(PROJECT_ROOT, services);
|
|
353
|
+
projectConfig.services = services;
|
|
354
|
+
createServiceFolders(services);
|
|
355
|
+
logServer(`Synced services: ${services.join(', ')}`);
|
|
356
|
+
res.json({ success: true, services });
|
|
357
|
+
} catch (e) {
|
|
358
|
+
logError(`Could not save the project config: ${e.message}`);
|
|
359
|
+
res.status(500).json({ error: e.message });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
300
363
|
app.post('/shutdown', (req, res) => {
|
|
301
364
|
res.json({ success: true });
|
|
302
365
|
logServer('Shutdown requested by VS Code extension.');
|
|
@@ -310,7 +373,7 @@ app.get('/tree', (req, res) => {
|
|
|
310
373
|
const rootFolders = fs.readdirSync(PROJECT_ROOT, { withFileTypes: true });
|
|
311
374
|
for (const folder of rootFolders) {
|
|
312
375
|
// Only service folders are synced; skips node_modules, .git...
|
|
313
|
-
if (folder.isDirectory() && ROOT_SERVICES.includes(folder.name)) {
|
|
376
|
+
if (folder.isDirectory() && ROOT_SERVICES.includes(folder.name) && isServiceSynced(folder.name)) {
|
|
314
377
|
const subTree = processDirectory(path.join(PROJECT_ROOT, folder.name), true);
|
|
315
378
|
tree.push(subTree);
|
|
316
379
|
}
|
|
@@ -400,6 +463,11 @@ app.post('/studio-change', (req, res) => {
|
|
|
400
463
|
continue;
|
|
401
464
|
}
|
|
402
465
|
|
|
466
|
+
// Changes outside the synced services never touch the disk
|
|
467
|
+
if (!isInstancePathSynced(change.path) || (change.oldPath && !isInstancePathSynced(change.oldPath))) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
403
471
|
logStudio(`${change.type} → ${change.path} (${change.className || '?'})`);
|
|
404
472
|
|
|
405
473
|
const fileInfo = instanceToFile(change);
|
|
@@ -526,31 +594,24 @@ process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
|
526
594
|
// Start
|
|
527
595
|
// ---------------------------------------------------------------------------
|
|
528
596
|
function startServer() {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
'StarterPlayer/StarterPlayerScripts',
|
|
532
|
-
'StarterPlayer/StarterCharacterScripts'
|
|
533
|
-
];
|
|
534
|
-
for (const folder of defaultFolders) {
|
|
535
|
-
const folderPath = path.join(PROJECT_ROOT, folder);
|
|
536
|
-
if (!fs.existsSync(folderPath)) {
|
|
537
|
-
try {
|
|
538
|
-
fs.mkdirSync(folderPath, { recursive: true });
|
|
539
|
-
} catch (e) {
|
|
540
|
-
// Ignore permissions/nested errors
|
|
541
|
-
}
|
|
542
|
-
}
|
|
597
|
+
if (projectConfig.services) {
|
|
598
|
+
createServiceFolders(projectConfig.services);
|
|
543
599
|
}
|
|
544
600
|
|
|
545
601
|
const server = app.listen(PORT, HOST, () => {
|
|
546
602
|
console.log('');
|
|
547
603
|
console.log(c.bold(c.green(' ╔══════════════════════════════════════════╗')));
|
|
548
|
-
console.log(c.bold(c.green(' ║
|
|
604
|
+
console.log(c.bold(c.green(' ║ 🌿 SYNCRBX v' + require('./package.json').version + ' 🌿 ║')));
|
|
549
605
|
console.log(c.bold(c.green(' ╚══════════════════════════════════════════╝')));
|
|
550
606
|
console.log('');
|
|
551
607
|
logServer(`Server at ${c.bold(`${HOST}:${PORT}`)}`);
|
|
552
608
|
logServer(`Project: ${c.cyan(projectConfig.name)}`);
|
|
553
609
|
logServer(`Monitoring: ${c.cyan(PROJECT_ROOT)}`);
|
|
610
|
+
if (projectConfig.services) {
|
|
611
|
+
logServer(`Syncing: ${c.cyan(projectConfig.services.join(', '))}`);
|
|
612
|
+
} else {
|
|
613
|
+
logServer(`No services chosen yet: pick them in the Studio plugin when you connect.`);
|
|
614
|
+
}
|
|
554
615
|
logServer(`Waiting for connection from Roblox Studio...`);
|
|
555
616
|
console.log('');
|
|
556
617
|
});
|