azul-sync 1.3.0 → 1.3.2

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.
Files changed (76) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +139 -142
  3. package/dist/cli.js +43 -43
  4. package/dist/ipc/messages.d.ts +1 -0
  5. package/dist/ipc/messages.d.ts.map +1 -1
  6. package/dist/pack.d.ts.map +1 -1
  7. package/dist/pack.js +8 -1
  8. package/dist/pack.js.map +1 -1
  9. package/dist/push.d.ts +2 -0
  10. package/dist/push.d.ts.map +1 -1
  11. package/dist/push.js +46 -1
  12. package/dist/push.js.map +1 -1
  13. package/dist/sourcemap/propertyLoader.d.ts +1 -0
  14. package/dist/sourcemap/propertyLoader.d.ts.map +1 -1
  15. package/dist/sourcemap/propertyLoader.js +7 -1
  16. package/dist/sourcemap/propertyLoader.js.map +1 -1
  17. package/package.json +45 -41
  18. package/.gitattributes +0 -1
  19. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  20. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  21. package/dist/rojo.d.ts +0 -9
  22. package/dist/rojo.d.ts.map +0 -1
  23. package/dist/rojo.js +0 -114
  24. package/dist/rojo.js.map +0 -1
  25. package/docs/assets/azul-logo.pdn +0 -0
  26. package/docs/assets/logo-200px.png +0 -0
  27. package/docs/assets/logo.png +0 -0
  28. package/docs/assets/plugin/toolbox.png +0 -0
  29. package/docs/assets/synced.png +0 -0
  30. package/plugin/README.md +0 -54
  31. package/plugin/sourcemap.json +0 -264
  32. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Actor/AzulSync.server.luau +0 -905
  33. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/AzulService.luau +0 -1010
  34. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Config.luau +0 -29
  35. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Enums.luau +0 -11
  36. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CollapsibleTitledSection.luau +0 -214
  37. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ColorPicker.luau +0 -360
  38. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CustomTextButton.luau +0 -170
  39. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/DropdownMenu.luau +0 -363
  40. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/HorizontalLine.luau +0 -43
  41. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ImageButtonWithText.luau +0 -181
  42. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledCheckbox.luau +0 -295
  43. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledColorInputPicker.luau +0 -294
  44. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledMultiChoice.luau +0 -163
  45. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledNumberInput.luau +0 -312
  46. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledRadioButton.luau +0 -55
  47. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledSlider.luau +0 -151
  48. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledTextInput.luau +0 -222
  49. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledToggleButton.luau +0 -73
  50. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/StatefulImageButton.luau +0 -125
  51. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalScrollingFrame.luau +0 -100
  52. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalSpacer.luau +0 -35
  53. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticallyScalingListFrame.luau +0 -107
  54. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/GuiUtilities.luau +0 -429
  55. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/RbxGui.luau +0 -4363
  56. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/UI.luau +0 -425
  57. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/WebSocketClient.luau +0 -161
  58. package/src/build.ts +0 -120
  59. package/src/cli.ts +0 -496
  60. package/src/config.ts +0 -170
  61. package/src/fs/fileWriter.ts +0 -414
  62. package/src/fs/treeManager.ts +0 -458
  63. package/src/fs/watcher.ts +0 -142
  64. package/src/index.ts +0 -450
  65. package/src/ipc/httpPolling.ts +0 -214
  66. package/src/ipc/messages.ts +0 -159
  67. package/src/ipc/server.ts +0 -196
  68. package/src/pack.ts +0 -309
  69. package/src/push.ts +0 -726
  70. package/src/snapshot/rojo.ts +0 -467
  71. package/src/snapshot.ts +0 -161
  72. package/src/sourcemap/generator.ts +0 -504
  73. package/src/sourcemap/propertyLoader.ts +0 -195
  74. package/src/util/id.ts +0 -15
  75. package/src/util/log.ts +0 -94
  76. package/tsconfig.json +0 -24
@@ -1,159 +0,0 @@
1
- /**
2
- * Types for communication protocol between Studio and Daemon
3
- */
4
-
5
- export type InstanceClassName =
6
- | "Script"
7
- | "LocalScript"
8
- | "ModuleScript"
9
- | "Folder"
10
- | "Model"
11
- | "Part"
12
- | "MeshPart"
13
- | "Tool"
14
- | "Configuration"
15
- | string; // Allow any Roblox class
16
-
17
- /**
18
- * Represents a single instance in the DataModel
19
- */
20
- export interface InstanceData {
21
- guid: string;
22
- className: InstanceClassName;
23
- name: string;
24
- path: string[]; // ["ReplicatedStorage", "Modules", "Foo"]
25
- parentGuid?: string | null; // parent instance GUID
26
- source?: string; // Only present for Script/LocalScript/ModuleScript
27
- properties?: Record<string, unknown>;
28
- attributes?: Record<string, unknown>;
29
- }
30
-
31
- export interface SnapshotRequestOptions {
32
- includeProperties?: boolean;
33
- scriptsAndDescendantsOnly?: boolean;
34
- }
35
-
36
- /**
37
- * Messages from Studio → Daemon
38
- */
39
- export type StudioPayloadMessage =
40
- | FullSnapshotMessage
41
- | InstanceUpdatedMessage
42
- | ScriptChangedMessage
43
- | DeletedMessage
44
- | PingMessage
45
- | ClientDisconnect
46
- | PushConfigMessage;
47
-
48
- export interface BatchMessage {
49
- type: "batch";
50
- messages: StudioPayloadMessage[];
51
- }
52
-
53
- export type StudioMessage = StudioPayloadMessage | BatchMessage;
54
-
55
- export interface FullSnapshotMessage {
56
- type: "fullSnapshot";
57
- data: InstanceData[];
58
- }
59
-
60
- export interface InstanceUpdatedMessage {
61
- type: "instanceUpdated";
62
- data: InstanceData;
63
- }
64
-
65
- export interface ScriptChangedMessage {
66
- type: "scriptChanged";
67
- data: {
68
- guid: string;
69
- path: string[];
70
- className: InstanceClassName;
71
- source: string;
72
- };
73
- }
74
-
75
- export interface DeletedMessage {
76
- type: "deleted";
77
- data: {
78
- guid: string;
79
- };
80
- }
81
-
82
- export interface PingMessage {
83
- type: "ping";
84
- }
85
-
86
- export interface ClientDisconnect {
87
- type: "clientDisconnect";
88
- }
89
-
90
- export interface PushConfigMessage {
91
- type: "pushConfig";
92
- config: PushConfig;
93
- }
94
-
95
- /**
96
- * Messages from Daemon → Studio
97
- */
98
- export type DaemonMessage =
99
- | PatchScriptMessage
100
- | RequestSnapshotMessage
101
- | PongMessage
102
- | ErrorMessage
103
- | BuildSnapshotMessage
104
- | RequestPushConfigMessage
105
- | PushSnapshotMessage;
106
-
107
- export interface PatchScriptMessage {
108
- type: "patchScript";
109
- guid: string;
110
- source: string;
111
- }
112
-
113
- export interface RequestSnapshotMessage {
114
- type: "requestSnapshot";
115
- options?: SnapshotRequestOptions;
116
- }
117
-
118
- export interface PongMessage {
119
- type: "pong";
120
- }
121
-
122
- export interface ErrorMessage {
123
- type: "error";
124
- message: string;
125
- }
126
-
127
- export interface BuildSnapshotMessage {
128
- type: "buildSnapshot";
129
- data: InstanceData[];
130
- }
131
-
132
- export interface RequestPushConfigMessage {
133
- type: "requestPushConfig";
134
- }
135
-
136
- export interface PushSnapshotMessage {
137
- type: "pushSnapshot";
138
- mappings: PushSnapshotMapping[];
139
- }
140
-
141
- export interface PushSnapshotMapping {
142
- destination: string[];
143
- destructive?: boolean;
144
- instances: InstanceData[];
145
- }
146
-
147
- export interface PushConfig {
148
- mappings: PushConfigMapping[];
149
- port?: number;
150
- debugMode?: boolean;
151
- deleteOrphansOnConnect?: boolean;
152
- }
153
-
154
- export interface PushConfigMapping {
155
- source: string;
156
- destination: string[];
157
- destructive?: boolean;
158
- rojoMode?: boolean;
159
- }
package/src/ipc/server.ts DELETED
@@ -1,196 +0,0 @@
1
- import { WebSocketServer, WebSocket } from "ws";
2
- import { log } from "../util/log.js";
3
- import type { StudioMessage, DaemonMessage } from "./messages.js";
4
- import type { SnapshotRequestOptions } from "./messages.js";
5
- import type { Server as HttpServer } from "http";
6
-
7
- export type MessageHandler = (message: StudioMessage) => void;
8
-
9
- interface IPCServerOptions {
10
- requestSnapshotOnConnect?: boolean;
11
- }
12
-
13
- export class IPCServer {
14
- private wss: WebSocketServer;
15
- private client: WebSocket | null = null;
16
- private messageHandler: MessageHandler | null = null;
17
- private connectionHandler: (() => void) | null = null;
18
- private requestSnapshotOnConnect: boolean;
19
-
20
- constructor(port?: number, server?: HttpServer, options?: IPCServerOptions) {
21
- this.requestSnapshotOnConnect = options?.requestSnapshotOnConnect !== false;
22
- if (server) {
23
- // Use existing HTTP server
24
- this.wss = new WebSocketServer({
25
- server,
26
- // perMessageDeflate: false, // Roblox WebSocket client does not negotiate RSV2/RSV3 extensions
27
- maxPayload: 256 * 1024 * 1024, // 256 MB
28
- });
29
- } else {
30
- // Create standalone WebSocket server
31
- this.wss = new WebSocketServer({
32
- port: port || 8080,
33
- // perMessageDeflate: false, // avoid RSV2/RSV3 bits from compression
34
- maxPayload: 256 * 1024 * 1024, // 256 MB
35
- });
36
- }
37
- this.setupServer();
38
- }
39
-
40
- private setupServer(): void {
41
- this.wss.on("connection", (ws) => {
42
- log.info("Studio client connected");
43
- log.info("Waiting for Studio messages...");
44
-
45
- // Disconnect previous client if exists
46
- if (this.client) {
47
- log.warn("Disconnecting previous client");
48
- this.client.close();
49
- }
50
-
51
- this.client = ws;
52
-
53
- if (this.connectionHandler) {
54
- this.connectionHandler();
55
- }
56
-
57
- ws.on("message", (data) => {
58
- try {
59
- const message: StudioMessage = JSON.parse(data.toString());
60
- log.debug(`Received: ${message.type}`);
61
-
62
- if (this.messageHandler) {
63
- this.messageHandler(message);
64
- }
65
- } catch (error) {
66
- log.error("Failed to parse message:", error);
67
- this.sendError("Invalid JSON message");
68
- }
69
- });
70
-
71
- ws.on("close", () => {
72
- log.info("Studio client disconnected");
73
- this.client = null;
74
- });
75
-
76
- ws.on("error", (error) => {
77
- log.error("WebSocket error:", error);
78
- });
79
-
80
- // Set up ping/pong to keep connection alive
81
- ws.on("pong", () => {
82
- log.debug("Received pong from client");
83
- });
84
-
85
- // Send ping every 30 seconds
86
- const pingInterval = setInterval(() => {
87
- if (this.client === ws && ws.readyState === WebSocket.OPEN) {
88
- ws.ping();
89
- } else {
90
- clearInterval(pingInterval);
91
- }
92
- }, 30000);
93
-
94
- // Request initial snapshot after a brief delay
95
- if (this.requestSnapshotOnConnect) {
96
- setTimeout(() => {
97
- if (this.client === ws) {
98
- this.send({ type: "requestSnapshot" });
99
- }
100
- }, 100);
101
- }
102
- });
103
-
104
- this.wss.on("listening", () => {
105
- log.success("WebSocket server ready");
106
- });
107
-
108
- this.wss.on("error", (error) => {
109
- log.error("WebSocket server error:", error);
110
- });
111
- }
112
-
113
- /**
114
- * Register a handler for incoming Studio messages
115
- */
116
- public onMessage(handler: MessageHandler): void {
117
- this.messageHandler = handler;
118
- }
119
-
120
- /**
121
- * Register a handler that fires when a Studio client connects
122
- */
123
- public onConnection(handler: () => void): void {
124
- this.connectionHandler = handler;
125
- }
126
-
127
- /**
128
- * Send a message to the connected Studio client
129
- */
130
- public send(message: DaemonMessage): boolean {
131
- if (!this.client || this.client.readyState !== WebSocket.OPEN) {
132
- log.warn("Cannot send message: no connected client");
133
- return false;
134
- }
135
-
136
- try {
137
- this.client.send(JSON.stringify(message));
138
- log.debug(`Sent: ${message.type}`);
139
- return true;
140
- } catch (error) {
141
- log.error("Failed to send message:", error);
142
- return false;
143
- }
144
- }
145
-
146
- /**
147
- * Send a patch to update a script's source in Studio
148
- */
149
- public patchScript(guid: string, source: string): boolean {
150
- return this.send({
151
- type: "patchScript",
152
- guid,
153
- source,
154
- });
155
- }
156
-
157
- /**
158
- * Send an error message to Studio
159
- */
160
- public sendError(message: string): boolean {
161
- return this.send({
162
- type: "error",
163
- message,
164
- });
165
- }
166
-
167
- /**
168
- * Request a full snapshot from Studio
169
- */
170
- public requestSnapshot(options?: SnapshotRequestOptions): boolean {
171
- return this.send({
172
- type: "requestSnapshot",
173
- options,
174
- });
175
- }
176
-
177
- /**
178
- * Check if a client is connected
179
- */
180
- public isConnected(): boolean {
181
- return this.client !== null && this.client.readyState === WebSocket.OPEN;
182
- }
183
-
184
- /**
185
- * Close the server
186
- */
187
- public close(): void {
188
- if (this.client) {
189
- this.client.close();
190
- }
191
- this.wss.close();
192
- log.info("WebSocket server closed.");
193
- log.info("Exiting...");
194
- process.exit(0);
195
- }
196
- }
package/src/pack.ts DELETED
@@ -1,309 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { IPCServer } from "./ipc/server.js";
4
- import { config } from "./config.js";
5
- import { log } from "./util/log.js";
6
- import type {
7
- InstanceData,
8
- SnapshotRequestOptions,
9
- StudioMessage,
10
- } from "./ipc/messages.js";
11
-
12
- interface PackOptions {
13
- outputPath?: string;
14
- scriptsAndDescendantsOnly?: boolean;
15
- }
16
-
17
- interface SourcemapNode {
18
- name: string;
19
- className: string;
20
- guid?: string;
21
- filePaths?: string[];
22
- children?: SourcemapNode[];
23
- properties?: Record<string, unknown>;
24
- attributes?: Record<string, unknown>;
25
- }
26
-
27
- interface SourcemapRoot {
28
- name: string;
29
- className: string;
30
- children: SourcemapNode[];
31
- _azul?: {
32
- packVersion?: number;
33
- packedAt?: string;
34
- mode?: "all" | "scripts-and-descendants";
35
- };
36
- }
37
-
38
- export class PackCommand {
39
- private ipc: IPCServer;
40
- private outputPath: string;
41
- private scriptsAndDescendantsOnly: boolean;
42
-
43
- constructor(options: PackOptions = {}) {
44
- this.outputPath = path.resolve(options.outputPath ?? config.sourcemapPath);
45
- this.scriptsAndDescendantsOnly = Boolean(options.scriptsAndDescendantsOnly);
46
- this.ipc = new IPCServer(config.port, undefined, {
47
- requestSnapshotOnConnect: false,
48
- });
49
- }
50
-
51
- public async run(): Promise<void> {
52
- log.info(`Waiting for Studio to connect on port ${config.port}...`);
53
- const snapshot = await this.requestSnapshot({
54
- includeProperties: true,
55
- scriptsAndDescendantsOnly: this.scriptsAndDescendantsOnly,
56
- });
57
-
58
- if (!snapshot) {
59
- log.error("Failed to receive snapshot from Studio for packing.");
60
- return;
61
- }
62
-
63
- const existing = this.readExistingSourcemap();
64
- const regenerated = this.regenerateSourcemap(snapshot, existing);
65
- const packedCount = this.packIntoSourcemap(snapshot, regenerated);
66
-
67
- this.writeSourcemap(regenerated, this.outputPath);
68
- log.success(`Packed ${packedCount} node(s) into ${this.outputPath}`);
69
- }
70
-
71
- private async requestSnapshot(
72
- options: SnapshotRequestOptions,
73
- ): Promise<InstanceData[] | null> {
74
- return new Promise<InstanceData[] | null>((resolve) => {
75
- let timeoutHandle: NodeJS.Timeout | null = null;
76
- let resolved = false;
77
-
78
- const finalize = (result: InstanceData[] | null): void => {
79
- if (resolved) return;
80
- resolved = true;
81
-
82
- if (timeoutHandle) {
83
- clearTimeout(timeoutHandle);
84
- timeoutHandle = null;
85
- }
86
-
87
- setTimeout(() => {
88
- this.ipc.close();
89
- }, 200);
90
-
91
- resolve(result);
92
- };
93
-
94
- this.ipc.onMessage((message: StudioMessage) => {
95
- if (message.type !== "fullSnapshot") return;
96
- finalize(message.data);
97
- });
98
-
99
- this.ipc.onConnection(() => {
100
- log.info("Studio connected. Requesting snapshot...");
101
- this.ipc.requestSnapshot(options);
102
- });
103
-
104
- timeoutHandle = setTimeout(() => {
105
- log.error("Timed out waiting for Studio snapshot.");
106
- finalize(null);
107
- }, 30000);
108
- });
109
- }
110
-
111
- private readExistingSourcemap(): SourcemapRoot | null {
112
- if (!fs.existsSync(this.outputPath)) {
113
- return null;
114
- }
115
-
116
- try {
117
- const raw = fs.readFileSync(this.outputPath, "utf8");
118
- return JSON.parse(raw) as SourcemapRoot;
119
- } catch (error) {
120
- log.warn(
121
- `Failed to read existing sourcemap at ${this.outputPath}: ${error}`,
122
- );
123
- return null;
124
- }
125
- }
126
-
127
- private regenerateSourcemap(
128
- snapshot: InstanceData[],
129
- existing: SourcemapRoot | null,
130
- ): SourcemapRoot {
131
- const root: SourcemapRoot = {
132
- name: "Game",
133
- className: "DataModel",
134
- children: [],
135
- };
136
-
137
- const guidFilePaths = new Map<string, string[]>();
138
- const pathClassFilePaths = new Map<string, string[][]>();
139
- const pathClassCursor = new Map<string, number>();
140
-
141
- const indexExisting = (
142
- node: SourcemapNode,
143
- currentPath: string[],
144
- ): void => {
145
- const nodePath = [...currentPath, node.name];
146
- if (node.filePaths && node.filePaths.length > 0) {
147
- if (node.guid) {
148
- guidFilePaths.set(node.guid, node.filePaths);
149
- }
150
-
151
- const key = this.pathClassKey(nodePath, node.className);
152
- const bucket = pathClassFilePaths.get(key) ?? [];
153
- bucket.push(node.filePaths);
154
- pathClassFilePaths.set(key, bucket);
155
- }
156
-
157
- for (const child of node.children ?? []) {
158
- indexExisting(child, nodePath);
159
- }
160
- };
161
-
162
- for (const child of existing?.children ?? []) {
163
- indexExisting(child, []);
164
- }
165
-
166
- const byGuid = new Map<string, SourcemapNode>();
167
- byGuid.set("root", root as unknown as SourcemapNode);
168
-
169
- const sorted = [...snapshot].sort((a, b) => {
170
- if (a.path.length !== b.path.length) {
171
- return a.path.length - b.path.length;
172
- }
173
- return a.path.join("/").localeCompare(b.path.join("/"));
174
- });
175
-
176
- for (const item of sorted) {
177
- const node: SourcemapNode = {
178
- name: item.name,
179
- className: item.className,
180
- guid: item.guid,
181
- };
182
-
183
- const directFilePaths = guidFilePaths.get(item.guid);
184
- if (directFilePaths && directFilePaths.length > 0) {
185
- node.filePaths = directFilePaths;
186
- } else {
187
- const key = this.pathClassKey(item.path, item.className);
188
- const bucket = pathClassFilePaths.get(key);
189
- if (bucket && bucket.length > 0) {
190
- const cursor = pathClassCursor.get(key) ?? 0;
191
- const candidate = bucket[cursor];
192
- if (candidate && candidate.length > 0) {
193
- node.filePaths = candidate;
194
- pathClassCursor.set(key, cursor + 1);
195
- }
196
- }
197
- }
198
-
199
- let parentNode = root as SourcemapNode;
200
- if (item.parentGuid && item.parentGuid !== "root") {
201
- parentNode = byGuid.get(item.parentGuid) ?? root;
202
- }
203
-
204
- if (!parentNode.children) {
205
- parentNode.children = [];
206
- }
207
- parentNode.children.push(node);
208
- byGuid.set(item.guid, node);
209
- }
210
-
211
- return root;
212
- }
213
-
214
- private packIntoSourcemap(
215
- snapshot: InstanceData[],
216
- sourcemap: SourcemapRoot,
217
- ): number {
218
- const byGuid = new Map<string, InstanceData>();
219
- const byPathClass = new Map<string, InstanceData[]>();
220
- for (const item of snapshot) {
221
- byGuid.set(item.guid, item);
222
- const key = this.pathClassKey(item.path, item.className);
223
- const bucket = byPathClass.get(key) ?? [];
224
- bucket.push(item);
225
- byPathClass.set(key, bucket);
226
- }
227
-
228
- const usedGuids = new Set<string>();
229
- let packed = 0;
230
-
231
- const visit = (node: SourcemapNode, currentPath: string[]): void => {
232
- const nodePath = [...currentPath, node.name];
233
- let match: InstanceData | undefined;
234
-
235
- if (node.guid) {
236
- const direct = byGuid.get(node.guid);
237
- if (direct) {
238
- match = direct;
239
- usedGuids.add(direct.guid);
240
- }
241
- }
242
-
243
- if (!match) {
244
- const key = this.pathClassKey(nodePath, node.className);
245
- const bucket = byPathClass.get(key);
246
- if (bucket && bucket.length > 0) {
247
- match = bucket.find((candidate) => !usedGuids.has(candidate.guid));
248
- if (match) {
249
- usedGuids.add(match.guid);
250
- }
251
- }
252
- }
253
-
254
- if (match) {
255
- if (match.properties && Object.keys(match.properties).length > 0) {
256
- node.properties = match.properties;
257
- } else if (!this.scriptsAndDescendantsOnly) {
258
- delete node.properties;
259
- }
260
-
261
- if (match.attributes && Object.keys(match.attributes).length > 0) {
262
- node.attributes = match.attributes;
263
- } else if (!this.scriptsAndDescendantsOnly) {
264
- delete node.attributes;
265
- }
266
-
267
- if (match.properties || match.attributes) {
268
- packed += 1;
269
- }
270
- } else if (!this.scriptsAndDescendantsOnly) {
271
- delete node.properties;
272
- delete node.attributes;
273
- }
274
-
275
- for (const child of node.children ?? []) {
276
- visit(child, nodePath);
277
- }
278
- };
279
-
280
- for (const child of sourcemap.children ?? []) {
281
- visit(child, []);
282
- }
283
-
284
- sourcemap._azul = {
285
- packVersion: 1,
286
- packedAt: new Date().toISOString(),
287
- mode: this.scriptsAndDescendantsOnly ? "scripts-and-descendants" : "all",
288
- };
289
-
290
- return packed;
291
- }
292
-
293
- private writeSourcemap(sourcemap: SourcemapRoot, outputPath: string): void {
294
- const dir = path.dirname(outputPath);
295
- if (dir && dir !== "." && !fs.existsSync(dir)) {
296
- fs.mkdirSync(dir, { recursive: true });
297
- }
298
-
299
- fs.writeFileSync(
300
- outputPath,
301
- `${JSON.stringify(sourcemap, null, 2)}\n`,
302
- "utf8",
303
- );
304
- }
305
-
306
- private pathClassKey(pathSegments: string[], className: string): string {
307
- return `${pathSegments.join("\u0001")}::${className}`;
308
- }
309
- }