vite-plugin-mirrorstate 0.2.3 → 0.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.
Files changed (2) hide show
  1. package/dist/index.js +45 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,11 +14,17 @@ export function mirrorStatePlugin(options = {}) {
14
14
  };
15
15
  let wss;
16
16
  let watcher;
17
- let recentWrites = new Set(); // Track recent writes to prevent echo
17
+ let fileSequences = new Map(); // Track sequence number per file
18
+ let lastWrittenState = new Map(); // Track last written state hash per file to detect external changes
18
19
  let lastMessageHash = new Map(); // Track last message hash per client to prevent duplicates
19
20
  let watcherReady = false; // Track if watcher has finished initial scan
21
+ let viteRoot; // Captured vite root directory
20
22
  return {
21
23
  name: "vite-plugin-mirrorstate",
24
+ configResolved(config) {
25
+ // Capture vite root for use in other hooks
26
+ viteRoot = config.root || process.cwd();
27
+ },
22
28
  configureServer(server) {
23
29
  const wsPath = opts.path;
24
30
  wss = new WebSocketServer({ noServer: true });
@@ -107,21 +113,31 @@ export function mirrorStatePlugin(options = {}) {
107
113
  }
108
114
  try {
109
115
  const relativePath = path.relative(baseDir, filePath);
110
- // Skip if this was a recent write from WebSocket to prevent echo
111
- if (recentWrites.has(relativePath)) {
112
- recentWrites.delete(relativePath);
113
- return;
114
- }
115
116
  const content = fs.readFileSync(filePath, "utf8");
116
117
  const data = JSON.parse(content);
117
118
  const name = relativePath.replace(/\.mirror\.json$/, "");
118
- // This is an external file change (from editor, etc.)
119
+ // Create hash of the state to detect if this is our own write
120
+ const stateHash = JSON.stringify(data);
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}`);
125
+ return;
126
+ }
127
+ // This is an external change - increment sequence number
128
+ const currentSeq = fileSequences.get(name) ?? 0;
129
+ const seq = currentSeq + 1;
130
+ fileSequences.set(name, seq);
131
+ // Update our record of the last written state
132
+ lastWrittenState.set(name, stateHash);
133
+ // Broadcast file change with sequence number
119
134
  wss.clients.forEach((client) => {
120
135
  if (client.readyState === client.OPEN) {
121
136
  client.send(JSON.stringify({
122
137
  type: "fileChange",
123
138
  name,
124
139
  state: data,
140
+ seq,
125
141
  source: "external",
126
142
  }));
127
143
  }
@@ -131,7 +147,7 @@ export function mirrorStatePlugin(options = {}) {
131
147
  if (mod) {
132
148
  server.moduleGraph.invalidateModule(mod);
133
149
  }
134
- logger(`Mirror file changed externally: ${name}`);
150
+ logger(`Mirror file changed externally: ${name} (seq: ${seq})`);
135
151
  }
136
152
  catch (error) {
137
153
  console.error(`Error reading mirror file ${filePath}:`, error);
@@ -142,33 +158,18 @@ export function mirrorStatePlugin(options = {}) {
142
158
  const clientId = Math.random().toString(36).substring(7);
143
159
  ws.clientId = clientId;
144
160
  logger(`Client connected to MirrorState (${clientId})`);
145
- const pattern = Array.isArray(opts.filePattern)
146
- ? opts.filePattern.map((p) => path.join(baseDir, p))
147
- : [path.join(baseDir, opts.filePattern)];
148
- const mirrorFiles = pattern.flatMap((p) => glob.sync(p, {
149
- ignore: "node_modules/**",
150
- }));
151
- mirrorFiles.forEach((filePath) => {
152
- try {
153
- const content = fs.readFileSync(filePath, "utf8");
154
- const data = JSON.parse(content);
155
- const relativePath = path.relative(server.config.root || process.cwd(), filePath);
156
- const name = relativePath.replace(/\.mirror\.json$/, "");
157
- ws.send(JSON.stringify({
158
- type: "initialState",
159
- name,
160
- state: data,
161
- }));
162
- }
163
- catch (error) {
164
- console.error(`Error reading initial state from ${filePath}:`, error);
165
- }
161
+ // Send clientId to the client
162
+ const connectedMessage = JSON.stringify({
163
+ type: "connected",
164
+ clientId,
166
165
  });
166
+ logger(`Sending connected message: ${connectedMessage}`);
167
+ ws.send(connectedMessage);
167
168
  ws.on("message", (message) => {
168
169
  try {
169
170
  const messageStr = message.toString();
170
171
  const data = JSON.parse(messageStr);
171
- const { name, state } = data;
172
+ const { clientId: msgClientId, name, state } = data;
172
173
  // Create a hash of the message to detect duplicates
173
174
  const messageHash = `${name}:${JSON.stringify(state)}`;
174
175
  const lastHash = lastMessageHash.get(clientId);
@@ -185,28 +186,32 @@ export function mirrorStatePlugin(options = {}) {
185
186
  const jsonContent = opts.prettyPrint
186
187
  ? JSON.stringify(state, null, 2)
187
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));
188
195
  // Write state to file
189
196
  fs.writeFileSync(filePath, jsonContent);
190
- // Mark this as a recent write to prevent file watcher echo
191
- // (only after successful write)
192
- recentWrites.add(relativeFilePath);
193
197
  // Invalidate the virtual module for HMR
194
198
  const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
195
199
  if (mod) {
196
200
  server.moduleGraph.invalidateModule(mod);
197
201
  }
198
- // Broadcast to other clients (exclude sender to prevent echo)
202
+ // Broadcast to all OTHER clients (skip sender - they already applied optimistically)
199
203
  wss.clients.forEach((client) => {
200
204
  if (client !== ws && client.readyState === client.OPEN) {
201
205
  client.send(JSON.stringify({
202
206
  type: "fileChange",
207
+ clientId: msgClientId,
208
+ seq,
203
209
  name,
204
210
  state: state,
205
- source: clientId,
206
211
  }));
207
212
  }
208
213
  });
209
- logger(`Updated ${name} with state (from ${clientId}):`, state);
214
+ logger(`Updated ${name} (seq: ${seq}) from ${clientId}:`, state);
210
215
  }
211
216
  catch (error) {
212
217
  console.error("Error handling client message:", error);
@@ -233,7 +238,8 @@ export function mirrorStatePlugin(options = {}) {
233
238
  }
234
239
  if (id === "\0virtual:mirrorstate/initial-states") {
235
240
  // During build, read all mirror files and inline them
236
- const baseDir = process.cwd();
241
+ // Use vite root instead of process.cwd() to handle monorepos correctly
242
+ const baseDir = viteRoot || process.cwd();
237
243
  const pattern = Array.isArray(opts.filePattern)
238
244
  ? opts.filePattern.map((p) => path.join(baseDir, p))
239
245
  : [path.join(baseDir, opts.filePattern)];
@@ -245,7 +251,8 @@ export function mirrorStatePlugin(options = {}) {
245
251
  try {
246
252
  const content = fs.readFileSync(filePath, "utf8");
247
253
  const data = JSON.parse(content);
248
- const relativePath = path.relative(process.cwd(), filePath);
254
+ // Use baseDir (vite root) for relative path calculation
255
+ const relativePath = path.relative(baseDir, filePath);
249
256
  const name = relativePath.replace(/\.mirror\.json$/, "");
250
257
  states[name] = data;
251
258
  logger(`Inlined initial state for ${name}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-mirrorstate",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Vite plugin for bidirectional state synchronization through *.mirror.json files",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",