vite-plugin-mirrorstate 0.3.0 → 0.3.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/dist/index.d.ts +1 -7
- package/dist/index.js +114 -196
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
|
-
export
|
|
3
|
-
path?: string;
|
|
4
|
-
filePattern?: string | string[];
|
|
5
|
-
watchOptions?: any;
|
|
6
|
-
prettyPrint?: boolean;
|
|
7
|
-
}
|
|
8
|
-
export declare function mirrorStatePlugin(options?: MirrorStatePluginOptions): Plugin;
|
|
2
|
+
export declare function mirrorStatePlugin(): Plugin;
|
|
9
3
|
export default mirrorStatePlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,267 +1,185 @@
|
|
|
1
1
|
import * as chokidar from "chokidar";
|
|
2
2
|
import { WebSocketServer } from "ws";
|
|
3
3
|
import * as fs from "fs";
|
|
4
|
+
import { promises as fsPromises } from "fs";
|
|
4
5
|
import * as path from "path";
|
|
5
6
|
import { glob } from "glob";
|
|
6
7
|
import debug from "debug";
|
|
7
8
|
const logger = debug("mirrorstate:vite-plugin");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
path: "/mirrorstate",
|
|
11
|
-
filePattern: ["*.mirror.json", "**/*.mirror.json"],
|
|
12
|
-
prettyPrint: true,
|
|
13
|
-
...options,
|
|
14
|
-
};
|
|
9
|
+
const WS_PATH = "/mirrorstate";
|
|
10
|
+
export function mirrorStatePlugin() {
|
|
15
11
|
let wss;
|
|
16
12
|
let watcher;
|
|
13
|
+
let viteRoot;
|
|
17
14
|
let fileSequences = new Map(); // Track sequence number per file
|
|
18
|
-
let
|
|
19
|
-
let lastMessageHash = new Map(); // Track last message hash per client to prevent duplicates
|
|
20
|
-
let watcherReady = false; // Track if watcher has finished initial scan
|
|
21
|
-
let viteRoot; // Captured vite root directory
|
|
15
|
+
let ignoreNextChange = new Map(); // Track our own writes to ignore echo from file watcher
|
|
22
16
|
return {
|
|
23
17
|
name: "vite-plugin-mirrorstate",
|
|
24
18
|
configResolved(config) {
|
|
25
|
-
// Capture vite root for use in other hooks
|
|
26
19
|
viteRoot = config.root || process.cwd();
|
|
27
20
|
},
|
|
28
21
|
configureServer(server) {
|
|
29
|
-
const
|
|
22
|
+
const invalidateVirtualModule = () => {
|
|
23
|
+
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
24
|
+
if (mod) {
|
|
25
|
+
server.moduleGraph.invalidateModule(mod);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
30
28
|
wss = new WebSocketServer({ noServer: true });
|
|
31
29
|
server.httpServer.on("upgrade", (request, socket, head) => {
|
|
32
|
-
if (request.url ===
|
|
30
|
+
if (request.url === WS_PATH) {
|
|
33
31
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
34
32
|
wss.emit("connection", ws, request);
|
|
35
33
|
});
|
|
36
34
|
}
|
|
37
35
|
});
|
|
36
|
+
logger(`MirrorState WebSocket listening on ws://localhost:${server.config.server.port || 5173}${WS_PATH}`);
|
|
38
37
|
const baseDir = server.config.root || process.cwd();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
logger(`Setting up file watcher for ${baseDir}`);
|
|
39
|
+
watcher = chokidar.watch(baseDir, {
|
|
40
|
+
ignored: (path, stats) => {
|
|
41
|
+
// Ignore hidden files, node_modules
|
|
42
|
+
if (path.includes("node_modules") || path.includes("/.")) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Only watch .mirror.json files (and directories to traverse)
|
|
46
|
+
if (stats?.isFile() && !path.endsWith(".mirror.json")) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
},
|
|
51
51
|
persistent: true,
|
|
52
|
-
|
|
52
|
+
ignoreInitial: true,
|
|
53
53
|
});
|
|
54
54
|
watcher.on("add", (filePath) => {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
56
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
57
|
+
invalidateVirtualModule();
|
|
58
|
+
logger(`New mirror file added: ${name}`);
|
|
59
|
+
});
|
|
60
|
+
watcher.on("unlink", (filePath) => {
|
|
61
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
62
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
63
|
+
invalidateVirtualModule();
|
|
64
|
+
logger(`Mirror file deleted: ${name}`);
|
|
65
|
+
});
|
|
66
|
+
watcher.on("change", (filePath) => {
|
|
67
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
68
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
69
|
+
// Skip if this is our own write (echo from file watcher)
|
|
70
|
+
if (ignoreNextChange.get(name)) {
|
|
71
|
+
ignoreNextChange.delete(name);
|
|
72
|
+
logger(`Skipping file watcher echo for ${name}`);
|
|
57
73
|
return;
|
|
58
74
|
}
|
|
75
|
+
let data;
|
|
59
76
|
try {
|
|
60
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
61
77
|
const content = fs.readFileSync(filePath, "utf8");
|
|
62
|
-
|
|
63
|
-
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
64
|
-
// Send new state to all connected clients
|
|
65
|
-
wss.clients.forEach((client) => {
|
|
66
|
-
if (client.readyState === client.OPEN) {
|
|
67
|
-
client.send(JSON.stringify({
|
|
68
|
-
type: "initialState",
|
|
69
|
-
name,
|
|
70
|
-
state: data,
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
// Invalidate the virtual module for HMR
|
|
75
|
-
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
76
|
-
if (mod) {
|
|
77
|
-
server.moduleGraph.invalidateModule(mod);
|
|
78
|
-
}
|
|
79
|
-
logger(`New mirror file added: ${name}`);
|
|
78
|
+
data = JSON.parse(content);
|
|
80
79
|
}
|
|
81
80
|
catch (error) {
|
|
82
|
-
console.error(`Error reading
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
watcher.on("unlink", (filePath) => {
|
|
86
|
-
// Only process .mirror.json files
|
|
87
|
-
if (!filePath.endsWith(".mirror.json")) {
|
|
81
|
+
console.error(`Error reading mirror file ${filePath}:`, error);
|
|
88
82
|
return;
|
|
89
83
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
84
|
+
const currentSeq = fileSequences.get(name) ?? 0;
|
|
85
|
+
const seq = currentSeq + 1;
|
|
86
|
+
fileSequences.set(name, seq);
|
|
87
|
+
// Broadcast file change with sequence number
|
|
88
|
+
wss.clients.forEach((client) => {
|
|
89
|
+
if (client.readyState === client.OPEN) {
|
|
90
|
+
client.send(JSON.stringify({
|
|
91
|
+
type: "fileChange",
|
|
92
|
+
name,
|
|
93
|
+
state: data,
|
|
94
|
+
seq,
|
|
95
|
+
}));
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.error(`Error handling mirror file deletion ${filePath}:`, error);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
watcher.on("ready", () => {
|
|
105
|
-
watcherReady = true;
|
|
106
|
-
logger("File watcher is ready");
|
|
97
|
+
});
|
|
98
|
+
invalidateVirtualModule();
|
|
99
|
+
logger(`Mirror file changed externally: ${name} (seq: ${seq})`);
|
|
107
100
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const lastHash = lastWrittenState.get(name);
|
|
122
|
-
// Skip if this matches what we just wrote (echo from our own write)
|
|
123
|
-
if (lastHash === stateHash) {
|
|
124
|
-
logger(`Skipping file watcher echo for ${name}`);
|
|
101
|
+
wss.on("connection", (ws) => {
|
|
102
|
+
// Generate unique ID for this connection for logging purposes
|
|
103
|
+
const clientId = Math.random().toString(36).substring(8);
|
|
104
|
+
ws.clientId = clientId;
|
|
105
|
+
logger(`Client connected to MirrorState [${clientId}]`);
|
|
106
|
+
ws.on("message", (message) => {
|
|
107
|
+
const messageStr = message.toString();
|
|
108
|
+
let data;
|
|
109
|
+
try {
|
|
110
|
+
data = JSON.parse(messageStr);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.error("Error handling client message:", error);
|
|
125
114
|
return;
|
|
126
115
|
}
|
|
127
|
-
|
|
116
|
+
const { name, state } = data;
|
|
117
|
+
const baseDir = server.config.root || process.cwd();
|
|
118
|
+
const relativeFilePath = `${name}.mirror.json`;
|
|
119
|
+
const filePath = path.join(baseDir, relativeFilePath);
|
|
120
|
+
const jsonContent = JSON.stringify(state, null, 2);
|
|
121
|
+
// Increment sequence number for this file
|
|
128
122
|
const currentSeq = fileSequences.get(name) ?? 0;
|
|
129
123
|
const seq = currentSeq + 1;
|
|
130
124
|
fileSequences.set(name, seq);
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
//
|
|
125
|
+
// Set flag to ignore the next file change event (our own write)
|
|
126
|
+
ignoreNextChange.set(name, true);
|
|
127
|
+
// Write state to file
|
|
128
|
+
fs.writeFileSync(filePath, jsonContent);
|
|
129
|
+
// Broadcast to all OTHER clients (skip sender - they already applied optimistically)
|
|
134
130
|
wss.clients.forEach((client) => {
|
|
135
|
-
if (client.readyState === client.OPEN) {
|
|
131
|
+
if (client !== ws && client.readyState === client.OPEN) {
|
|
136
132
|
client.send(JSON.stringify({
|
|
137
133
|
type: "fileChange",
|
|
138
|
-
name,
|
|
139
|
-
state: data,
|
|
140
134
|
seq,
|
|
141
|
-
|
|
135
|
+
name,
|
|
136
|
+
state,
|
|
142
137
|
}));
|
|
143
138
|
}
|
|
144
139
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (mod) {
|
|
148
|
-
server.moduleGraph.invalidateModule(mod);
|
|
149
|
-
}
|
|
150
|
-
logger(`Mirror file changed externally: ${name} (seq: ${seq})`);
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
console.error(`Error reading mirror file ${filePath}:`, error);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
wss.on("connection", (ws) => {
|
|
157
|
-
// Generate unique ID for this connection to prevent echo loops
|
|
158
|
-
const clientId = Math.random().toString(36).substring(7);
|
|
159
|
-
ws.clientId = clientId;
|
|
160
|
-
logger(`Client connected to MirrorState (${clientId})`);
|
|
161
|
-
// Send clientId to the client
|
|
162
|
-
const connectedMessage = JSON.stringify({
|
|
163
|
-
type: "connected",
|
|
164
|
-
clientId,
|
|
165
|
-
});
|
|
166
|
-
logger(`Sending connected message: ${connectedMessage}`);
|
|
167
|
-
ws.send(connectedMessage);
|
|
168
|
-
ws.on("message", (message) => {
|
|
169
|
-
try {
|
|
170
|
-
const messageStr = message.toString();
|
|
171
|
-
const data = JSON.parse(messageStr);
|
|
172
|
-
const { clientId: msgClientId, name, state } = data;
|
|
173
|
-
// Create a hash of the message to detect duplicates
|
|
174
|
-
const messageHash = `${name}:${JSON.stringify(state)}`;
|
|
175
|
-
const lastHash = lastMessageHash.get(clientId);
|
|
176
|
-
// Skip if this is a duplicate message from the same client
|
|
177
|
-
if (lastHash === messageHash) {
|
|
178
|
-
logger(`Skipping duplicate message from ${clientId} for ${name}`);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
// Update last message hash for this client
|
|
182
|
-
lastMessageHash.set(clientId, messageHash);
|
|
183
|
-
const baseDir = server.config.root || process.cwd();
|
|
184
|
-
const relativeFilePath = `${name}.mirror.json`;
|
|
185
|
-
const filePath = path.join(baseDir, relativeFilePath);
|
|
186
|
-
const jsonContent = opts.prettyPrint
|
|
187
|
-
? JSON.stringify(state, null, 2)
|
|
188
|
-
: JSON.stringify(state);
|
|
189
|
-
// Increment sequence number for this file BEFORE writing
|
|
190
|
-
const currentSeq = fileSequences.get(name) ?? 0;
|
|
191
|
-
const seq = currentSeq + 1;
|
|
192
|
-
fileSequences.set(name, seq);
|
|
193
|
-
// Record state hash to detect our own write in file watcher
|
|
194
|
-
lastWrittenState.set(name, JSON.stringify(state));
|
|
195
|
-
// Write state to file
|
|
196
|
-
fs.writeFileSync(filePath, jsonContent);
|
|
197
|
-
// Invalidate the virtual module for HMR
|
|
198
|
-
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
199
|
-
if (mod) {
|
|
200
|
-
server.moduleGraph.invalidateModule(mod);
|
|
201
|
-
}
|
|
202
|
-
// Broadcast to all OTHER clients (skip sender - they already applied optimistically)
|
|
203
|
-
wss.clients.forEach((client) => {
|
|
204
|
-
if (client !== ws && client.readyState === client.OPEN) {
|
|
205
|
-
client.send(JSON.stringify({
|
|
206
|
-
type: "fileChange",
|
|
207
|
-
clientId: msgClientId,
|
|
208
|
-
seq,
|
|
209
|
-
name,
|
|
210
|
-
state: state,
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
logger(`Updated ${name} (seq: ${seq}) from ${clientId}:`, state);
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
console.error("Error handling client message:", error);
|
|
218
|
-
}
|
|
140
|
+
invalidateVirtualModule();
|
|
141
|
+
logger(`Updated ${name} (seq: ${seq}) from [${clientId}]:`, state);
|
|
219
142
|
});
|
|
220
143
|
ws.on("close", () => {
|
|
221
|
-
|
|
222
|
-
lastMessageHash.delete(clientId);
|
|
223
|
-
logger(`Client ${clientId} disconnected`);
|
|
144
|
+
logger(`Client [${clientId}] disconnected`);
|
|
224
145
|
});
|
|
225
146
|
});
|
|
226
147
|
},
|
|
227
148
|
resolveId(id) {
|
|
228
|
-
if (id === "virtual:mirrorstate/config"
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (id === "virtual:mirrorstate/initial-states") {
|
|
149
|
+
if (id === "virtual:mirrorstate/config" ||
|
|
150
|
+
id === "virtual:mirrorstate/initial-states") {
|
|
232
151
|
return "\0" + id;
|
|
233
152
|
}
|
|
234
153
|
},
|
|
235
|
-
load(id) {
|
|
154
|
+
async load(id) {
|
|
236
155
|
if (id === "\0virtual:mirrorstate/config") {
|
|
237
|
-
return `export const WS_PATH = "${
|
|
156
|
+
return `export const WS_PATH = "${WS_PATH}";`;
|
|
238
157
|
}
|
|
158
|
+
// During build, read all mirror files and inline them
|
|
239
159
|
if (id === "\0virtual:mirrorstate/initial-states") {
|
|
240
|
-
// During build, read all mirror files and inline them
|
|
241
|
-
// Use vite root instead of process.cwd() to handle monorepos correctly
|
|
242
160
|
const baseDir = viteRoot || process.cwd();
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
: [path.join(baseDir, opts.filePattern)];
|
|
246
|
-
const mirrorFiles = pattern.flatMap((p) => glob.sync(p, {
|
|
161
|
+
const mirrorFiles = glob.sync("**/*.mirror.json", {
|
|
162
|
+
cwd: baseDir,
|
|
247
163
|
ignore: "node_modules/**",
|
|
248
|
-
})
|
|
249
|
-
const
|
|
250
|
-
|
|
164
|
+
});
|
|
165
|
+
const filePromises = mirrorFiles.map(async (relativePath) => {
|
|
166
|
+
const absolutePath = path.join(baseDir, relativePath);
|
|
167
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
168
|
+
let data;
|
|
251
169
|
try {
|
|
252
|
-
const content =
|
|
253
|
-
|
|
254
|
-
// Use baseDir (vite root) for relative path calculation
|
|
255
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
256
|
-
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
257
|
-
states[name] = data;
|
|
258
|
-
logger(`Inlined initial state for ${name}`);
|
|
170
|
+
const content = await fsPromises.readFile(absolutePath, "utf8");
|
|
171
|
+
data = JSON.parse(content);
|
|
259
172
|
}
|
|
260
173
|
catch (error) {
|
|
261
|
-
console.error(`Error reading initial state from ${
|
|
174
|
+
console.error(`Error reading initial state from ${relativePath}:`, error);
|
|
175
|
+
return null;
|
|
262
176
|
}
|
|
177
|
+
logger(`Inlined initial state for ${name}`);
|
|
178
|
+
return [name, data];
|
|
263
179
|
});
|
|
264
|
-
|
|
180
|
+
const results = await Promise.all(filePromises);
|
|
181
|
+
const states = Object.fromEntries(results.filter((x) => x != null));
|
|
182
|
+
return `export const INITIAL_STATES = ${JSON.stringify(states)};`;
|
|
265
183
|
}
|
|
266
184
|
},
|
|
267
185
|
closeBundle() {
|
package/package.json
CHANGED