vite-plugin-mirrorstate 0.2.2 → 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.
- package/dist/index.js +129 -46
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,16 +8,23 @@ const logger = debug("mirrorstate:vite-plugin");
|
|
|
8
8
|
export function mirrorStatePlugin(options = {}) {
|
|
9
9
|
const opts = {
|
|
10
10
|
path: "/mirrorstate",
|
|
11
|
-
filePattern: "**/*.mirror.json",
|
|
11
|
+
filePattern: ["*.mirror.json", "**/*.mirror.json"],
|
|
12
12
|
prettyPrint: true,
|
|
13
13
|
...options,
|
|
14
14
|
};
|
|
15
15
|
let wss;
|
|
16
16
|
let watcher;
|
|
17
|
-
let
|
|
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
|
|
20
|
+
let watcherReady = false; // Track if watcher has finished initial scan
|
|
21
|
+
let viteRoot; // Captured vite root directory
|
|
19
22
|
return {
|
|
20
23
|
name: "vite-plugin-mirrorstate",
|
|
24
|
+
configResolved(config) {
|
|
25
|
+
// Capture vite root for use in other hooks
|
|
26
|
+
viteRoot = config.root || process.cwd();
|
|
27
|
+
},
|
|
21
28
|
configureServer(server) {
|
|
22
29
|
const wsPath = opts.path;
|
|
23
30
|
wss = new WebSocketServer({ noServer: true });
|
|
@@ -28,40 +35,119 @@ export function mirrorStatePlugin(options = {}) {
|
|
|
28
35
|
});
|
|
29
36
|
}
|
|
30
37
|
});
|
|
31
|
-
|
|
38
|
+
const baseDir = server.config.root || process.cwd();
|
|
39
|
+
const pattern = Array.isArray(opts.filePattern)
|
|
40
|
+
? opts.filePattern.map((p) => path.join(baseDir, p))
|
|
41
|
+
: [path.join(baseDir, opts.filePattern)];
|
|
42
|
+
// Find all existing files matching the pattern
|
|
43
|
+
const existingFiles = pattern.flatMap((p) => glob.sync(p, {
|
|
44
|
+
ignore: "node_modules/**",
|
|
45
|
+
}));
|
|
46
|
+
logger(`Setting up file watcher for ${existingFiles.length} files: ${JSON.stringify(existingFiles)}`);
|
|
47
|
+
// Watch both existing files AND the directory for new files
|
|
48
|
+
const watchTargets = [...existingFiles, baseDir];
|
|
49
|
+
watcher = chokidar.watch(watchTargets, {
|
|
32
50
|
ignored: /node_modules/,
|
|
33
51
|
persistent: true,
|
|
34
52
|
...opts.watchOptions,
|
|
35
53
|
});
|
|
54
|
+
watcher.on("add", (filePath) => {
|
|
55
|
+
// Only process .mirror.json files added after initial scan
|
|
56
|
+
if (!watcherReady || !filePath.endsWith(".mirror.json")) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
61
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
62
|
+
const data = JSON.parse(content);
|
|
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}`);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.error(`Error reading new mirror file ${filePath}:`, error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
watcher.on("unlink", (filePath) => {
|
|
86
|
+
// Only process .mirror.json files
|
|
87
|
+
if (!filePath.endsWith(".mirror.json")) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
92
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
93
|
+
// Invalidate the virtual module for HMR
|
|
94
|
+
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
95
|
+
if (mod) {
|
|
96
|
+
server.moduleGraph.invalidateModule(mod);
|
|
97
|
+
}
|
|
98
|
+
logger(`Mirror file deleted: ${name}`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
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");
|
|
107
|
+
});
|
|
36
108
|
logger(`MirrorState WebSocket listening on ws://localhost:${server.config.server.port || 5173}${wsPath}`);
|
|
37
109
|
watcher.on("change", (filePath) => {
|
|
110
|
+
// Only watch .mirror.json files
|
|
111
|
+
if (!filePath.endsWith(".mirror.json")) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
38
114
|
try {
|
|
39
|
-
|
|
40
|
-
if (recentWrites.has(filePath)) {
|
|
41
|
-
recentWrites.delete(filePath);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
115
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
44
116
|
const content = fs.readFileSync(filePath, "utf8");
|
|
45
117
|
const data = JSON.parse(content);
|
|
46
|
-
const relativePath = path.relative(server.config.root || process.cwd(), filePath);
|
|
47
118
|
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
48
|
-
//
|
|
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
|
|
49
134
|
wss.clients.forEach((client) => {
|
|
50
135
|
if (client.readyState === client.OPEN) {
|
|
51
136
|
client.send(JSON.stringify({
|
|
52
137
|
type: "fileChange",
|
|
53
138
|
name,
|
|
54
139
|
state: data,
|
|
140
|
+
seq,
|
|
55
141
|
source: "external",
|
|
56
142
|
}));
|
|
57
143
|
}
|
|
58
144
|
});
|
|
59
145
|
// Invalidate the virtual module for HMR
|
|
60
|
-
const mod = server.moduleGraph.getModuleById("
|
|
146
|
+
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
61
147
|
if (mod) {
|
|
62
148
|
server.moduleGraph.invalidateModule(mod);
|
|
63
149
|
}
|
|
64
|
-
logger(`Mirror file changed externally: ${name}`);
|
|
150
|
+
logger(`Mirror file changed externally: ${name} (seq: ${seq})`);
|
|
65
151
|
}
|
|
66
152
|
catch (error) {
|
|
67
153
|
console.error(`Error reading mirror file ${filePath}:`, error);
|
|
@@ -72,31 +158,18 @@ export function mirrorStatePlugin(options = {}) {
|
|
|
72
158
|
const clientId = Math.random().toString(36).substring(7);
|
|
73
159
|
ws.clientId = clientId;
|
|
74
160
|
logger(`Client connected to MirrorState (${clientId})`);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
:
|
|
78
|
-
|
|
79
|
-
mirrorFiles.forEach((filePath) => {
|
|
80
|
-
try {
|
|
81
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
82
|
-
const data = JSON.parse(content);
|
|
83
|
-
const relativePath = path.relative(server.config.root || process.cwd(), filePath);
|
|
84
|
-
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
85
|
-
ws.send(JSON.stringify({
|
|
86
|
-
type: "initialState",
|
|
87
|
-
name,
|
|
88
|
-
state: data,
|
|
89
|
-
}));
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
console.error(`Error reading initial state from ${filePath}:`, error);
|
|
93
|
-
}
|
|
161
|
+
// Send clientId to the client
|
|
162
|
+
const connectedMessage = JSON.stringify({
|
|
163
|
+
type: "connected",
|
|
164
|
+
clientId,
|
|
94
165
|
});
|
|
166
|
+
logger(`Sending connected message: ${connectedMessage}`);
|
|
167
|
+
ws.send(connectedMessage);
|
|
95
168
|
ws.on("message", (message) => {
|
|
96
169
|
try {
|
|
97
170
|
const messageStr = message.toString();
|
|
98
171
|
const data = JSON.parse(messageStr);
|
|
99
|
-
const { name, state } = data;
|
|
172
|
+
const { clientId: msgClientId, name, state } = data;
|
|
100
173
|
// Create a hash of the message to detect duplicates
|
|
101
174
|
const messageHash = `${name}:${JSON.stringify(state)}`;
|
|
102
175
|
const lastHash = lastMessageHash.get(clientId);
|
|
@@ -107,33 +180,38 @@ export function mirrorStatePlugin(options = {}) {
|
|
|
107
180
|
}
|
|
108
181
|
// Update last message hash for this client
|
|
109
182
|
lastMessageHash.set(clientId, messageHash);
|
|
110
|
-
const
|
|
183
|
+
const baseDir = server.config.root || process.cwd();
|
|
184
|
+
const relativeFilePath = `${name}.mirror.json`;
|
|
185
|
+
const filePath = path.join(baseDir, relativeFilePath);
|
|
111
186
|
const jsonContent = opts.prettyPrint
|
|
112
187
|
? JSON.stringify(state, null, 2)
|
|
113
188
|
: JSON.stringify(state);
|
|
114
|
-
//
|
|
115
|
-
|
|
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));
|
|
116
195
|
// Write state to file
|
|
117
196
|
fs.writeFileSync(filePath, jsonContent);
|
|
118
197
|
// Invalidate the virtual module for HMR
|
|
119
|
-
const mod = server.moduleGraph.getModuleById("
|
|
198
|
+
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
120
199
|
if (mod) {
|
|
121
200
|
server.moduleGraph.invalidateModule(mod);
|
|
122
201
|
}
|
|
123
|
-
// Broadcast to
|
|
202
|
+
// Broadcast to all OTHER clients (skip sender - they already applied optimistically)
|
|
124
203
|
wss.clients.forEach((client) => {
|
|
125
204
|
if (client !== ws && client.readyState === client.OPEN) {
|
|
126
|
-
const relativePath = path.relative(server.config.root || process.cwd(), filePath);
|
|
127
|
-
const fileName = relativePath.replace(/\.mirror\.json$/, "");
|
|
128
205
|
client.send(JSON.stringify({
|
|
129
206
|
type: "fileChange",
|
|
130
|
-
|
|
207
|
+
clientId: msgClientId,
|
|
208
|
+
seq,
|
|
209
|
+
name,
|
|
131
210
|
state: state,
|
|
132
|
-
source: clientId,
|
|
133
211
|
}));
|
|
134
212
|
}
|
|
135
213
|
});
|
|
136
|
-
logger(`Updated ${name}
|
|
214
|
+
logger(`Updated ${name} (seq: ${seq}) from ${clientId}:`, state);
|
|
137
215
|
}
|
|
138
216
|
catch (error) {
|
|
139
217
|
console.error("Error handling client message:", error);
|
|
@@ -160,16 +238,21 @@ export function mirrorStatePlugin(options = {}) {
|
|
|
160
238
|
}
|
|
161
239
|
if (id === "\0virtual:mirrorstate/initial-states") {
|
|
162
240
|
// During build, read all mirror files and inline them
|
|
241
|
+
// Use vite root instead of process.cwd() to handle monorepos correctly
|
|
242
|
+
const baseDir = viteRoot || process.cwd();
|
|
163
243
|
const pattern = Array.isArray(opts.filePattern)
|
|
164
|
-
? opts.filePattern
|
|
165
|
-
: [opts.filePattern];
|
|
166
|
-
const mirrorFiles = pattern.flatMap((p) => glob.sync(p, {
|
|
244
|
+
? opts.filePattern.map((p) => path.join(baseDir, p))
|
|
245
|
+
: [path.join(baseDir, opts.filePattern)];
|
|
246
|
+
const mirrorFiles = pattern.flatMap((p) => glob.sync(p, {
|
|
247
|
+
ignore: "node_modules/**",
|
|
248
|
+
}));
|
|
167
249
|
const states = {};
|
|
168
250
|
mirrorFiles.forEach((filePath) => {
|
|
169
251
|
try {
|
|
170
252
|
const content = fs.readFileSync(filePath, "utf8");
|
|
171
253
|
const data = JSON.parse(content);
|
|
172
|
-
|
|
254
|
+
// Use baseDir (vite root) for relative path calculation
|
|
255
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
173
256
|
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
174
257
|
states[name] = data;
|
|
175
258
|
logger(`Inlined initial state for ${name}`);
|
package/package.json
CHANGED