azul-sync 1.3.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/.gitattributes +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/README.md +142 -0
- package/dist/build.d.ts +19 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +92 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +397 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +105 -0
- package/dist/config.js.map +1 -0
- package/dist/fs/fileWriter.d.ts +100 -0
- package/dist/fs/fileWriter.d.ts.map +1 -0
- package/dist/fs/fileWriter.js +342 -0
- package/dist/fs/fileWriter.js.map +1 -0
- package/dist/fs/treeManager.d.ts +84 -0
- package/dist/fs/treeManager.d.ts.map +1 -0
- package/dist/fs/treeManager.js +365 -0
- package/dist/fs/treeManager.js.map +1 -0
- package/dist/fs/watcher.d.ts +39 -0
- package/dist/fs/watcher.d.ts.map +1 -0
- package/dist/fs/watcher.js +120 -0
- package/dist/fs/watcher.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +349 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/httpPolling.d.ts +56 -0
- package/dist/ipc/httpPolling.d.ts.map +1 -0
- package/dist/ipc/httpPolling.js +171 -0
- package/dist/ipc/httpPolling.js.map +1 -0
- package/dist/ipc/messages.d.ts +112 -0
- package/dist/ipc/messages.d.ts.map +1 -0
- package/dist/ipc/messages.js +5 -0
- package/dist/ipc/messages.js.map +1 -0
- package/dist/ipc/server.d.ts +50 -0
- package/dist/ipc/server.d.ts.map +1 -0
- package/dist/ipc/server.js +168 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/pack.d.ts +19 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/pack.js +225 -0
- package/dist/pack.js.map +1 -0
- package/dist/push.d.ts +43 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +532 -0
- package/dist/push.js.map +1 -0
- package/dist/rojo.d.ts +9 -0
- package/dist/rojo.d.ts.map +1 -0
- package/dist/rojo.js +114 -0
- package/dist/rojo.js.map +1 -0
- package/dist/snapshot/rojo.d.ts +39 -0
- package/dist/snapshot/rojo.d.ts.map +1 -0
- package/dist/snapshot/rojo.js +364 -0
- package/dist/snapshot/rojo.js.map +1 -0
- package/dist/snapshot.d.ts +23 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +132 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/sourcemap/generator.d.ts +78 -0
- package/dist/sourcemap/generator.d.ts.map +1 -0
- package/dist/sourcemap/generator.js +351 -0
- package/dist/sourcemap/generator.js.map +1 -0
- package/dist/sourcemap/propertyLoader.d.ts +19 -0
- package/dist/sourcemap/propertyLoader.d.ts.map +1 -0
- package/dist/sourcemap/propertyLoader.js +131 -0
- package/dist/sourcemap/propertyLoader.js.map +1 -0
- package/dist/util/id.d.ts +9 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +14 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/log.d.ts +13 -0
- package/dist/util/log.d.ts.map +1 -0
- package/dist/util/log.js +51 -0
- package/dist/util/log.js.map +1 -0
- package/docs/assets/azul-logo.pdn +0 -0
- package/docs/assets/logo-200px.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/assets/plugin/toolbox.png +0 -0
- package/docs/assets/synced.png +0 -0
- package/package.json +41 -0
- package/plugin/README.md +54 -0
- package/plugin/sourcemap.json +264 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Actor/AzulSync.server.luau +905 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/AzulService.luau +1010 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Config.luau +29 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Enums.luau +11 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CollapsibleTitledSection.luau +214 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ColorPicker.luau +360 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CustomTextButton.luau +170 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/DropdownMenu.luau +363 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/HorizontalLine.luau +43 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ImageButtonWithText.luau +181 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledCheckbox.luau +295 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledColorInputPicker.luau +294 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledMultiChoice.luau +163 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledNumberInput.luau +312 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledRadioButton.luau +55 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledSlider.luau +151 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledTextInput.luau +222 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledToggleButton.luau +73 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/StatefulImageButton.luau +125 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalScrollingFrame.luau +100 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalSpacer.luau +35 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticallyScalingListFrame.luau +107 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/GuiUtilities.luau +429 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/RbxGui.luau +4363 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/UI.luau +425 -0
- package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/WebSocketClient.luau +161 -0
- package/src/build.ts +120 -0
- package/src/cli.ts +496 -0
- package/src/config.ts +170 -0
- package/src/fs/fileWriter.ts +414 -0
- package/src/fs/treeManager.ts +458 -0
- package/src/fs/watcher.ts +142 -0
- package/src/index.ts +450 -0
- package/src/ipc/httpPolling.ts +214 -0
- package/src/ipc/messages.ts +159 -0
- package/src/ipc/server.ts +196 -0
- package/src/pack.ts +309 -0
- package/src/push.ts +726 -0
- package/src/snapshot/rojo.ts +467 -0
- package/src/snapshot.ts +161 -0
- package/src/sourcemap/generator.ts +504 -0
- package/src/sourcemap/propertyLoader.ts +195 -0
- package/src/util/id.ts +15 -0
- package/src/util/log.ts +94 -0
- package/tsconfig.json +24 -0
package/src/push.ts
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fsp } from "node:fs";
|
|
4
|
+
import { IPCServer } from "./ipc/server.js";
|
|
5
|
+
import { config } from "./config.js";
|
|
6
|
+
import { log } from "./util/log.js";
|
|
7
|
+
import { SnapshotBuilder } from "./snapshot.js";
|
|
8
|
+
import { RojoSnapshotBuilder } from "./snapshot/rojo.js";
|
|
9
|
+
import { generateGUID } from "./util/id.js";
|
|
10
|
+
import {
|
|
11
|
+
applySourcemapProperties,
|
|
12
|
+
buildInstancesFromSourcemap,
|
|
13
|
+
loadSourcemapPropertyIndex,
|
|
14
|
+
} from "./sourcemap/propertyLoader.js";
|
|
15
|
+
import type {
|
|
16
|
+
InstanceData,
|
|
17
|
+
PushConfig,
|
|
18
|
+
PushConfigMessage,
|
|
19
|
+
PushSnapshotMapping,
|
|
20
|
+
RequestPushConfigMessage,
|
|
21
|
+
StudioMessage,
|
|
22
|
+
} from "./ipc/messages.js";
|
|
23
|
+
|
|
24
|
+
interface PushOptions {
|
|
25
|
+
source?: string;
|
|
26
|
+
destination?: string;
|
|
27
|
+
destructive?: boolean;
|
|
28
|
+
usePlaceConfig?: boolean;
|
|
29
|
+
rojoMode?: boolean;
|
|
30
|
+
rojoProjectFile?: string;
|
|
31
|
+
applySourcemap?: boolean;
|
|
32
|
+
fromSourcemap?: boolean;
|
|
33
|
+
sourcemapPath?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class PushCommand {
|
|
37
|
+
private ipc: IPCServer;
|
|
38
|
+
private options: PushOptions;
|
|
39
|
+
private sourcemapPath: string;
|
|
40
|
+
private sourcemapIndex: ReturnType<typeof loadSourcemapPropertyIndex>;
|
|
41
|
+
|
|
42
|
+
constructor(options: PushOptions = {}) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.sourcemapPath = path.resolve(
|
|
45
|
+
options.sourcemapPath ?? config.sourcemapPath,
|
|
46
|
+
);
|
|
47
|
+
this.sourcemapIndex = loadSourcemapPropertyIndex(this.sourcemapPath);
|
|
48
|
+
this.ipc = new IPCServer(config.port, undefined, {
|
|
49
|
+
requestSnapshotOnConnect: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async run(): Promise<void> {
|
|
54
|
+
if (this.options.rojoMode) {
|
|
55
|
+
log.info(
|
|
56
|
+
"Rojo compatibility mode: ignoring place config; destination becomes a prefix.",
|
|
57
|
+
);
|
|
58
|
+
const destSegments = this.options.destination
|
|
59
|
+
? this.parseDestination(this.options.destination)
|
|
60
|
+
: [];
|
|
61
|
+
const instances = await this.buildRojoInstances(
|
|
62
|
+
destSegments,
|
|
63
|
+
this.options.source,
|
|
64
|
+
);
|
|
65
|
+
if (!instances) return;
|
|
66
|
+
|
|
67
|
+
const snapshotMappings: PushSnapshotMapping[] = [
|
|
68
|
+
{
|
|
69
|
+
destination: destSegments,
|
|
70
|
+
destructive: Boolean(this.options.destructive),
|
|
71
|
+
instances,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
await new Promise<void>((resolve) => {
|
|
76
|
+
const sendSnapshot = () => {
|
|
77
|
+
log.info("Studio connected. Sending Rojo compatibility push...");
|
|
78
|
+
this.ipc.send({ type: "pushSnapshot", mappings: snapshotMappings });
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
this.ipc.close();
|
|
81
|
+
resolve();
|
|
82
|
+
}, 200);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (this.ipc.isConnected()) {
|
|
86
|
+
sendSnapshot();
|
|
87
|
+
} else {
|
|
88
|
+
this.ipc.onConnection(sendSnapshot);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mappings = await this.collectMappings();
|
|
95
|
+
if (!mappings || mappings.length === 0) {
|
|
96
|
+
log.error(
|
|
97
|
+
"No push mappings available. Provide '--source' / '--destination' or place config.",
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
log.info(`Building ${mappings.length} mapping(s) for push...`);
|
|
103
|
+
|
|
104
|
+
const snapshotMappings: PushSnapshotMapping[] = [];
|
|
105
|
+
|
|
106
|
+
for (const mapping of mappings) {
|
|
107
|
+
const destSegments = mapping.destination;
|
|
108
|
+
|
|
109
|
+
if (mapping.rojoMode) {
|
|
110
|
+
log.info(
|
|
111
|
+
`Mapping source ${mapping.source} in Rojo compatibility mode.`,
|
|
112
|
+
);
|
|
113
|
+
const instances = await this.buildRojoInstances(
|
|
114
|
+
destSegments,
|
|
115
|
+
mapping.source,
|
|
116
|
+
);
|
|
117
|
+
if (!instances) continue;
|
|
118
|
+
|
|
119
|
+
snapshotMappings.push({
|
|
120
|
+
destination: destSegments,
|
|
121
|
+
destructive: Boolean(mapping.destructive),
|
|
122
|
+
instances,
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sourceCandidates = this.expandSourceCandidates(mapping.source);
|
|
128
|
+
const sourceDir = sourceCandidates.find((candidate) =>
|
|
129
|
+
fs.existsSync(candidate),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!sourceDir) {
|
|
133
|
+
log.error(
|
|
134
|
+
`Source path not found for push mapping. Tried: ${sourceCandidates.join(
|
|
135
|
+
", ",
|
|
136
|
+
)}`,
|
|
137
|
+
);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const builder = new SnapshotBuilder({
|
|
142
|
+
sourceDir,
|
|
143
|
+
destPrefix: destSegments,
|
|
144
|
+
skipSymlinks: true,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const instances = this.options.fromSourcemap
|
|
148
|
+
? this.buildPushInstancesFromSourcemap(sourceDir, destSegments)
|
|
149
|
+
: await builder.build();
|
|
150
|
+
|
|
151
|
+
if (this.options.fromSourcemap && !instances) {
|
|
152
|
+
log.warn(
|
|
153
|
+
`Could not derive sourcemap subtree for ${sourceDir}; falling back to filesystem snapshot.`,
|
|
154
|
+
);
|
|
155
|
+
const fallback = await builder.build();
|
|
156
|
+
snapshotMappings.push({
|
|
157
|
+
destination: destSegments,
|
|
158
|
+
destructive: Boolean(mapping.destructive),
|
|
159
|
+
instances: fallback,
|
|
160
|
+
});
|
|
161
|
+
log.success(
|
|
162
|
+
`Prepared ${fallback.length} instances from ${sourceDir} -> ${destSegments.join("/")}`,
|
|
163
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!instances) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
!this.options.fromSourcemap &&
|
|
173
|
+
this.options.applySourcemap !== false
|
|
174
|
+
) {
|
|
175
|
+
const applied = applySourcemapProperties(
|
|
176
|
+
instances,
|
|
177
|
+
this.sourcemapIndex,
|
|
178
|
+
);
|
|
179
|
+
if (applied > 0) {
|
|
180
|
+
log.success(
|
|
181
|
+
`Applied properties/attributes from sourcemap to ${applied} instance(s) for ${destSegments.join("/")}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
log.success(
|
|
187
|
+
`Prepared ${
|
|
188
|
+
instances.length
|
|
189
|
+
} instances from ${sourceDir} -> ${destSegments.join("/")}`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
snapshotMappings.push({
|
|
193
|
+
destination: destSegments,
|
|
194
|
+
destructive: Boolean(mapping.destructive),
|
|
195
|
+
instances,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (snapshotMappings.length === 0) {
|
|
200
|
+
log.error("No push mappings could be prepared (missing source paths).");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await new Promise<void>((resolve) => {
|
|
205
|
+
const sendSnapshot = () => {
|
|
206
|
+
log.info("Studio connected. Sending push snapshot...");
|
|
207
|
+
this.ipc.send({ type: "pushSnapshot", mappings: snapshotMappings });
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
this.ipc.close();
|
|
210
|
+
resolve();
|
|
211
|
+
}, 200);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (this.ipc.isConnected()) {
|
|
215
|
+
sendSnapshot();
|
|
216
|
+
} else {
|
|
217
|
+
this.ipc.onConnection(sendSnapshot);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async buildRojoInstances(
|
|
223
|
+
destSegments: string[],
|
|
224
|
+
sourceOverride?: string,
|
|
225
|
+
): Promise<InstanceData[] | null> {
|
|
226
|
+
const sourceRootOpt = sourceOverride ?? this.options.source;
|
|
227
|
+
|
|
228
|
+
const projectFiles = await this.resolveRojoProjectFiles(sourceRootOpt);
|
|
229
|
+
if (projectFiles.length === 0) {
|
|
230
|
+
log.error(
|
|
231
|
+
"Rojo compatibility mode could not find default.project.json. Provide --rojo-project or point --source to a folder that contains one.",
|
|
232
|
+
);
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const allInstances: InstanceData[] = [];
|
|
237
|
+
const projectDirs = new Set<string>();
|
|
238
|
+
|
|
239
|
+
for (const projectFile of projectFiles) {
|
|
240
|
+
// If a source root was provided, include the relative path from that root to the project file's folder
|
|
241
|
+
let relativeSegments: string[] = [];
|
|
242
|
+
if (sourceRootOpt) {
|
|
243
|
+
const sourceRoot = path.resolve(process.cwd(), sourceRootOpt);
|
|
244
|
+
const projectDir = path.dirname(projectFile);
|
|
245
|
+
projectDirs.add(projectDir);
|
|
246
|
+
const rel = path.relative(sourceRoot, projectDir).replace(/\\/g, "/");
|
|
247
|
+
if (rel && !rel.startsWith("..")) {
|
|
248
|
+
relativeSegments = rel.split("/").filter(Boolean);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const effectivePrefix = [...destSegments, ...relativeSegments];
|
|
253
|
+
|
|
254
|
+
const builder = new RojoSnapshotBuilder({
|
|
255
|
+
projectFile,
|
|
256
|
+
cwd: process.cwd(),
|
|
257
|
+
destPrefix: effectivePrefix,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
log.info(`Preparing Rojo compatibility push from ${projectFile}`);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const built = await builder.build();
|
|
264
|
+
allInstances.push(...built);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
log.error(`${error}`);
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Emit loose scripts not covered by a Rojo project (e.g., cmdr.lua, janitor.lua, Promise.lua)
|
|
272
|
+
if (sourceRootOpt) {
|
|
273
|
+
const sourceRoot = path.resolve(process.cwd(), sourceRootOpt);
|
|
274
|
+
const existingFolders = new Set(
|
|
275
|
+
allInstances
|
|
276
|
+
.filter((i) => i.className === "Folder")
|
|
277
|
+
.map((i) => i.path.join("/")),
|
|
278
|
+
);
|
|
279
|
+
const existingPaths = new Set(allInstances.map((i) => i.path.join("/")));
|
|
280
|
+
|
|
281
|
+
const loose = await this.collectLooseScripts(
|
|
282
|
+
sourceRoot,
|
|
283
|
+
destSegments,
|
|
284
|
+
projectDirs,
|
|
285
|
+
existingFolders,
|
|
286
|
+
existingPaths,
|
|
287
|
+
);
|
|
288
|
+
allInstances.push(...loose);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (allInstances.length === 0) {
|
|
292
|
+
log.warn(
|
|
293
|
+
"Rojo compatibility build produced 0 instances. Check project paths and ignores.",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return allInstances;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async collectMappings(): Promise<PushConfig["mappings"] | null> {
|
|
301
|
+
// CLI-provided mapping takes priority
|
|
302
|
+
if (this.options.source && this.options.destination) {
|
|
303
|
+
const destSegments = this.parseDestination(this.options.destination);
|
|
304
|
+
if (destSegments.length === 0) {
|
|
305
|
+
log.error(
|
|
306
|
+
"Destination must be a dot-separated path (e.g., ReplicatedStorage.Packages)",
|
|
307
|
+
);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return [
|
|
311
|
+
{
|
|
312
|
+
source: this.options.source,
|
|
313
|
+
destination: destSegments,
|
|
314
|
+
destructive: Boolean(this.options.destructive),
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (this.options.usePlaceConfig === false) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
log.info(
|
|
324
|
+
"No source/destination provided. Requesting push config from Studio... (ServerStorage.Azul.Config)",
|
|
325
|
+
);
|
|
326
|
+
const config = await this.waitForPushConfig();
|
|
327
|
+
if (!config) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
log.debug("Received push config from Studio.", config);
|
|
332
|
+
|
|
333
|
+
const sanitized = config.mappings?.filter((m) =>
|
|
334
|
+
Boolean(m && m.source && m.destination && m.destination.length > 0),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (!sanitized || sanitized.length === 0) {
|
|
338
|
+
log.error("Received push config, but no valid mappings were found.");
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return sanitized.map((m) => ({
|
|
343
|
+
source: m.source,
|
|
344
|
+
destination: m.destination,
|
|
345
|
+
destructive: Boolean(m.destructive),
|
|
346
|
+
rojoMode: Boolean(m.rojoMode),
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private parseDestination(input: string): string[] {
|
|
351
|
+
return input
|
|
352
|
+
.split(/[./\\]+/)
|
|
353
|
+
.map((segment) => segment.trim())
|
|
354
|
+
.filter(Boolean);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private buildPushInstancesFromSourcemap(
|
|
358
|
+
sourceDir: string,
|
|
359
|
+
destSegments: string[],
|
|
360
|
+
): InstanceData[] | null {
|
|
361
|
+
const all = buildInstancesFromSourcemap(this.sourcemapPath);
|
|
362
|
+
if (!all || all.length === 0) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const sourcePrefix = this.inferSourcePrefixFromPath(sourceDir, all);
|
|
367
|
+
if (!sourcePrefix || sourcePrefix.length === 0) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const selected = all.filter((instance) =>
|
|
372
|
+
this.pathStartsWith(instance.path, sourcePrefix),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const rebased = selected
|
|
376
|
+
.filter((instance) => instance.path.length > sourcePrefix.length)
|
|
377
|
+
.map((instance) => ({
|
|
378
|
+
...instance,
|
|
379
|
+
path: [...destSegments, ...instance.path.slice(sourcePrefix.length)],
|
|
380
|
+
}));
|
|
381
|
+
|
|
382
|
+
rebased.sort((a, b) => a.path.length - b.path.length);
|
|
383
|
+
return rebased;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private inferSourcePrefixFromPath(
|
|
387
|
+
sourceDir: string,
|
|
388
|
+
instances: InstanceData[],
|
|
389
|
+
): string[] | null {
|
|
390
|
+
const normalized = path
|
|
391
|
+
.resolve(sourceDir)
|
|
392
|
+
.replace(/\\/g, "/")
|
|
393
|
+
.split("/")
|
|
394
|
+
.filter(Boolean);
|
|
395
|
+
|
|
396
|
+
let best: string[] | null = null;
|
|
397
|
+
for (let start = 0; start < normalized.length; start++) {
|
|
398
|
+
const candidate = normalized.slice(start);
|
|
399
|
+
if (candidate.length === 0) continue;
|
|
400
|
+
|
|
401
|
+
const matches = instances.some((instance) =>
|
|
402
|
+
this.pathStartsWith(instance.path, candidate),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
if (!matches) continue;
|
|
406
|
+
|
|
407
|
+
if (!best || candidate.length > best.length) {
|
|
408
|
+
best = candidate;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return best;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private pathStartsWith(pathSegments: string[], prefix: string[]): boolean {
|
|
416
|
+
if (prefix.length > pathSegments.length) return false;
|
|
417
|
+
|
|
418
|
+
for (let index = 0; index < prefix.length; index++) {
|
|
419
|
+
if (pathSegments[index] !== prefix[index]) return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private async resolveRojoProjectFiles(
|
|
426
|
+
sourceOverride?: string,
|
|
427
|
+
): Promise<string[]> {
|
|
428
|
+
const cwd = process.cwd();
|
|
429
|
+
const results = new Set<string>();
|
|
430
|
+
|
|
431
|
+
const add = (p: string) => {
|
|
432
|
+
const abs = path.resolve(cwd, p);
|
|
433
|
+
if (fs.existsSync(abs)) {
|
|
434
|
+
results.add(abs);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (this.options.rojoProjectFile) {
|
|
439
|
+
add(this.options.rojoProjectFile);
|
|
440
|
+
return [...results];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const sourceRootOpt = sourceOverride ?? this.options.source;
|
|
444
|
+
|
|
445
|
+
// If a source root is provided, only search within it (and its nested projects).
|
|
446
|
+
if (sourceRootOpt) {
|
|
447
|
+
const srcRoot = path.resolve(cwd, sourceRootOpt);
|
|
448
|
+
if (!fs.existsSync(srcRoot)) {
|
|
449
|
+
log.warn(`--source path does not exist: ${srcRoot}`);
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const direct = path.join(srcRoot, "default.project.json");
|
|
454
|
+
if (fs.existsSync(direct)) {
|
|
455
|
+
results.add(direct);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const foundInSource = await this.findProjectJsons(srcRoot, 6);
|
|
459
|
+
for (const f of foundInSource) {
|
|
460
|
+
results.add(f);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return [...results];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// No source root: search at workspace root (previous behavior).
|
|
467
|
+
const rootDirect = path.join(cwd, "default.project.json");
|
|
468
|
+
if (fs.existsSync(rootDirect)) {
|
|
469
|
+
results.add(rootDirect);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const found = await this.findProjectJsons(cwd, 3);
|
|
473
|
+
for (const f of found) {
|
|
474
|
+
results.add(f);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return [...results];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Breadth-first search for all default.project.json under a root.
|
|
482
|
+
* Skips common vendor/ignored folders.
|
|
483
|
+
*/
|
|
484
|
+
private async findProjectJsons(
|
|
485
|
+
root: string,
|
|
486
|
+
maxDepth: number,
|
|
487
|
+
): Promise<string[]> {
|
|
488
|
+
const queue: { dir: string; depth: number }[] = [{ dir: root, depth: 0 }];
|
|
489
|
+
const found: string[] = [];
|
|
490
|
+
const skip = new Set(["node_modules", ".git", "dist", "sync"]);
|
|
491
|
+
|
|
492
|
+
while (queue.length > 0) {
|
|
493
|
+
const { dir, depth } = queue.shift()!;
|
|
494
|
+
if (depth > maxDepth) continue;
|
|
495
|
+
|
|
496
|
+
let entries: fs.Dirent[];
|
|
497
|
+
try {
|
|
498
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
499
|
+
} catch {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Deterministic order
|
|
504
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
505
|
+
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
if (entry.isFile() && entry.name === "default.project.json") {
|
|
508
|
+
found.push(path.join(dir, entry.name));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const entry of entries) {
|
|
513
|
+
if (!entry.isDirectory()) continue;
|
|
514
|
+
if (skip.has(entry.name)) continue;
|
|
515
|
+
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return found;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private async collectLooseScripts(
|
|
523
|
+
root: string,
|
|
524
|
+
destSegments: string[],
|
|
525
|
+
projectDirs: Set<string>,
|
|
526
|
+
emittedFolders: Set<string>,
|
|
527
|
+
emittedPaths: Set<string>,
|
|
528
|
+
): Promise<InstanceData[]> {
|
|
529
|
+
const results: InstanceData[] = [];
|
|
530
|
+
|
|
531
|
+
const walk = async (dir: string, relSegments: string[]) => {
|
|
532
|
+
// Skip directories already handled by a Rojo project
|
|
533
|
+
for (const proj of projectDirs) {
|
|
534
|
+
if (dir === proj || dir.startsWith(proj + path.sep)) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let entries: fs.Dirent[];
|
|
540
|
+
try {
|
|
541
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
542
|
+
} catch {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
547
|
+
|
|
548
|
+
// If this directory has an init-like file, treat the directory itself as that script
|
|
549
|
+
const initCandidates = [
|
|
550
|
+
"init.lua",
|
|
551
|
+
"init.luau",
|
|
552
|
+
"init.server.lua",
|
|
553
|
+
"init.server.luau",
|
|
554
|
+
"init.client.lua",
|
|
555
|
+
"init.client.luau",
|
|
556
|
+
"init.module.lua",
|
|
557
|
+
"init.module.luau",
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
const initEntry = entries.find(
|
|
561
|
+
(e) => e.isFile() && initCandidates.includes(e.name),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
if (initEntry) {
|
|
565
|
+
const full = path.join(dir, initEntry.name);
|
|
566
|
+
const { className } = this.classifyScript(initEntry.name);
|
|
567
|
+
const destPath = [...destSegments, ...relSegments];
|
|
568
|
+
const key = destPath.join("/");
|
|
569
|
+
if (!emittedPaths.has(key)) {
|
|
570
|
+
this.ensureFolder(destPath.slice(0, -1), results, emittedFolders);
|
|
571
|
+
emittedPaths.add(key);
|
|
572
|
+
emittedFolders.add(key); // prevent folder emission at this path
|
|
573
|
+
results.push({
|
|
574
|
+
guid: generateGUID(),
|
|
575
|
+
className,
|
|
576
|
+
name: destPath[destPath.length - 1] ?? path.basename(dir),
|
|
577
|
+
path: destPath,
|
|
578
|
+
source: await fsp.readFile(full, "utf-8"),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
for (const entry of entries) {
|
|
584
|
+
const full = path.join(dir, entry.name);
|
|
585
|
+
if (entry.isDirectory()) {
|
|
586
|
+
await walk(full, [...relSegments, entry.name]);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (initEntry && initEntry.name === entry.name) {
|
|
591
|
+
continue; // already emitted as the container
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (!this.isScriptFile(entry.name)) continue;
|
|
595
|
+
|
|
596
|
+
const { className, scriptName } = this.classifyScript(entry.name);
|
|
597
|
+
const destPath = [...destSegments, ...relSegments, scriptName];
|
|
598
|
+
const key = destPath.join("/");
|
|
599
|
+
if (emittedPaths.has(key)) continue;
|
|
600
|
+
|
|
601
|
+
this.ensureFolder(destPath.slice(0, -1), results, emittedFolders);
|
|
602
|
+
emittedPaths.add(key);
|
|
603
|
+
results.push({
|
|
604
|
+
guid: generateGUID(),
|
|
605
|
+
className,
|
|
606
|
+
name: scriptName,
|
|
607
|
+
path: destPath,
|
|
608
|
+
source: await fsp.readFile(full, "utf-8"),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
await walk(root, []);
|
|
614
|
+
return results;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private ensureFolder(
|
|
618
|
+
pathSegments: string[],
|
|
619
|
+
results: InstanceData[],
|
|
620
|
+
emittedFolders: Set<string>,
|
|
621
|
+
): void {
|
|
622
|
+
if (pathSegments.length === 0) return;
|
|
623
|
+
const key = pathSegments.join("/");
|
|
624
|
+
if (emittedFolders.has(key)) return;
|
|
625
|
+
this.ensureFolder(pathSegments.slice(0, -1), results, emittedFolders);
|
|
626
|
+
emittedFolders.add(key);
|
|
627
|
+
results.push({
|
|
628
|
+
guid: generateGUID(),
|
|
629
|
+
className: "Folder",
|
|
630
|
+
name: pathSegments[pathSegments.length - 1],
|
|
631
|
+
path: [...pathSegments],
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private isScriptFile(fileName: string): boolean {
|
|
636
|
+
return fileName.endsWith(".lua") || fileName.endsWith(".luau");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private classifyScript(fileName: string): {
|
|
640
|
+
className: "Script" | "LocalScript" | "ModuleScript";
|
|
641
|
+
scriptName: string;
|
|
642
|
+
} {
|
|
643
|
+
const normalized = fileName.replace(/\.lua$/i, ".luau");
|
|
644
|
+
const base = normalized.replace(/\.luau$/i, "");
|
|
645
|
+
|
|
646
|
+
if (base.endsWith(".server")) {
|
|
647
|
+
return { className: "Script", scriptName: base.replace(/\.server$/, "") };
|
|
648
|
+
}
|
|
649
|
+
if (base.endsWith(".client")) {
|
|
650
|
+
return {
|
|
651
|
+
className: "LocalScript",
|
|
652
|
+
scriptName: base.replace(/\.client$/, ""),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (base.endsWith(".module")) {
|
|
656
|
+
return {
|
|
657
|
+
className: "ModuleScript",
|
|
658
|
+
scriptName: base.replace(/\.module$/, ""),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
return { className: "ModuleScript", scriptName: base };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Normalize source path strings from config, preferring the raw value but
|
|
666
|
+
* attempting obvious fixes (e.g., accidental leading '.' before a folder).
|
|
667
|
+
*/
|
|
668
|
+
private expandSourceCandidates(source: string): string[] {
|
|
669
|
+
const candidates: string[] = [];
|
|
670
|
+
const cwd = process.cwd();
|
|
671
|
+
|
|
672
|
+
const add = (p: string) => {
|
|
673
|
+
const abs = path.resolve(cwd, p);
|
|
674
|
+
if (!candidates.includes(abs)) {
|
|
675
|
+
candidates.push(abs);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
add(source);
|
|
680
|
+
|
|
681
|
+
// If someone wrote ".Packages" by mistake, try "Packages"
|
|
682
|
+
if (source.startsWith(".")) {
|
|
683
|
+
const trimmedDot = source.replace(/^\.*/, "");
|
|
684
|
+
if (trimmedDot) add(trimmedDot);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// If someone prefixed with ./ or .\, resolve both forms
|
|
688
|
+
if (source.startsWith("./") || source.startsWith(".\\")) {
|
|
689
|
+
add(source.slice(2));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return candidates;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private async waitForPushConfig(): Promise<PushConfig | null> {
|
|
696
|
+
return new Promise<PushConfig | null>((resolve) => {
|
|
697
|
+
let resolved = false;
|
|
698
|
+
|
|
699
|
+
const timeout = setTimeout(() => {
|
|
700
|
+
if (!resolved) {
|
|
701
|
+
log.warn("Timed out waiting for push config from Studio.");
|
|
702
|
+
resolved = true;
|
|
703
|
+
resolve(null);
|
|
704
|
+
}
|
|
705
|
+
}, 8000);
|
|
706
|
+
|
|
707
|
+
this.ipc.onMessage((message: StudioMessage) => {
|
|
708
|
+
if (message.type === "pushConfig") {
|
|
709
|
+
const pushConfig = (message as PushConfigMessage).config;
|
|
710
|
+
|
|
711
|
+
clearTimeout(timeout);
|
|
712
|
+
if (!resolved) {
|
|
713
|
+
resolved = true;
|
|
714
|
+
resolve(pushConfig);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Ask the plugin to send config after connection
|
|
720
|
+
this.ipc.onConnection(() => {
|
|
721
|
+
const request: RequestPushConfigMessage = { type: "requestPushConfig" };
|
|
722
|
+
this.ipc.send(request);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|