vite-plugin-mirrorstate 0.3.0 → 0.4.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.d.ts +1 -7
- package/dist/index.js +118 -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,189 @@
|
|
|
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
|
+
awaitWriteFinish: {
|
|
54
|
+
stabilityThreshold: 50,
|
|
55
|
+
pollInterval: 10,
|
|
56
|
+
},
|
|
53
57
|
});
|
|
54
58
|
watcher.on("add", (filePath) => {
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
60
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
61
|
+
invalidateVirtualModule();
|
|
62
|
+
logger(`New mirror file added: ${name}`);
|
|
63
|
+
});
|
|
64
|
+
watcher.on("unlink", (filePath) => {
|
|
65
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
66
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
67
|
+
invalidateVirtualModule();
|
|
68
|
+
logger(`Mirror file deleted: ${name}`);
|
|
69
|
+
});
|
|
70
|
+
watcher.on("change", (filePath) => {
|
|
71
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
72
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
73
|
+
// Skip if this is our own write (echo from file watcher)
|
|
74
|
+
if (ignoreNextChange.get(name)) {
|
|
75
|
+
ignoreNextChange.delete(name);
|
|
76
|
+
logger(`Skipping file watcher echo for ${name}`);
|
|
57
77
|
return;
|
|
58
78
|
}
|
|
79
|
+
let data;
|
|
59
80
|
try {
|
|
60
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
61
81
|
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}`);
|
|
82
|
+
data = JSON.parse(content);
|
|
80
83
|
}
|
|
81
84
|
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")) {
|
|
85
|
+
console.error(`Error reading mirror file ${filePath}:`, error);
|
|
88
86
|
return;
|
|
89
87
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
88
|
+
const currentSeq = fileSequences.get(name) ?? 0;
|
|
89
|
+
const seq = currentSeq + 1;
|
|
90
|
+
fileSequences.set(name, seq);
|
|
91
|
+
// Broadcast file change with sequence number
|
|
92
|
+
wss.clients.forEach((client) => {
|
|
93
|
+
if (client.readyState === client.OPEN) {
|
|
94
|
+
client.send(JSON.stringify({
|
|
95
|
+
type: "fileChange",
|
|
96
|
+
name,
|
|
97
|
+
state: data,
|
|
98
|
+
seq,
|
|
99
|
+
}));
|
|
97
100
|
}
|
|
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");
|
|
101
|
+
});
|
|
102
|
+
invalidateVirtualModule();
|
|
103
|
+
logger(`Mirror file changed externally: ${name} (seq: ${seq})`);
|
|
107
104
|
});
|
|
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}`);
|
|
105
|
+
wss.on("connection", (ws) => {
|
|
106
|
+
// Generate unique ID for this connection for logging purposes
|
|
107
|
+
const clientId = Math.random().toString(36).substring(8);
|
|
108
|
+
ws.clientId = clientId;
|
|
109
|
+
logger(`Client connected to MirrorState [${clientId}]`);
|
|
110
|
+
ws.on("message", (message) => {
|
|
111
|
+
const messageStr = message.toString();
|
|
112
|
+
let data;
|
|
113
|
+
try {
|
|
114
|
+
data = JSON.parse(messageStr);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error("Error handling client message:", error);
|
|
125
118
|
return;
|
|
126
119
|
}
|
|
127
|
-
|
|
120
|
+
const { name, state } = data;
|
|
121
|
+
const baseDir = server.config.root || process.cwd();
|
|
122
|
+
const relativeFilePath = `${name}.mirror.json`;
|
|
123
|
+
const filePath = path.join(baseDir, relativeFilePath);
|
|
124
|
+
const jsonContent = JSON.stringify(state, null, 2);
|
|
125
|
+
// Increment sequence number for this file
|
|
128
126
|
const currentSeq = fileSequences.get(name) ?? 0;
|
|
129
127
|
const seq = currentSeq + 1;
|
|
130
128
|
fileSequences.set(name, seq);
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
//
|
|
129
|
+
// Set flag to ignore the next file change event (our own write)
|
|
130
|
+
ignoreNextChange.set(name, true);
|
|
131
|
+
// Write state to file
|
|
132
|
+
fs.writeFileSync(filePath, jsonContent);
|
|
133
|
+
// Broadcast to all OTHER clients (skip sender - they already applied optimistically)
|
|
134
134
|
wss.clients.forEach((client) => {
|
|
135
|
-
if (client.readyState === client.OPEN) {
|
|
135
|
+
if (client !== ws && client.readyState === client.OPEN) {
|
|
136
136
|
client.send(JSON.stringify({
|
|
137
137
|
type: "fileChange",
|
|
138
|
-
name,
|
|
139
|
-
state: data,
|
|
140
138
|
seq,
|
|
141
|
-
|
|
139
|
+
name,
|
|
140
|
+
state,
|
|
142
141
|
}));
|
|
143
142
|
}
|
|
144
143
|
});
|
|
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
|
-
}
|
|
144
|
+
invalidateVirtualModule();
|
|
145
|
+
logger(`Updated ${name} (seq: ${seq}) from [${clientId}]:`, state);
|
|
219
146
|
});
|
|
220
147
|
ws.on("close", () => {
|
|
221
|
-
|
|
222
|
-
lastMessageHash.delete(clientId);
|
|
223
|
-
logger(`Client ${clientId} disconnected`);
|
|
148
|
+
logger(`Client [${clientId}] disconnected`);
|
|
224
149
|
});
|
|
225
150
|
});
|
|
226
151
|
},
|
|
227
152
|
resolveId(id) {
|
|
228
|
-
if (id === "virtual:mirrorstate/config"
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (id === "virtual:mirrorstate/initial-states") {
|
|
153
|
+
if (id === "virtual:mirrorstate/config" ||
|
|
154
|
+
id === "virtual:mirrorstate/initial-states") {
|
|
232
155
|
return "\0" + id;
|
|
233
156
|
}
|
|
234
157
|
},
|
|
235
|
-
load(id) {
|
|
158
|
+
async load(id) {
|
|
236
159
|
if (id === "\0virtual:mirrorstate/config") {
|
|
237
|
-
return `export const WS_PATH = "${
|
|
160
|
+
return `export const WS_PATH = "${WS_PATH}";`;
|
|
238
161
|
}
|
|
162
|
+
// During build, read all mirror files and inline them
|
|
239
163
|
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
164
|
const baseDir = viteRoot || process.cwd();
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
: [path.join(baseDir, opts.filePattern)];
|
|
246
|
-
const mirrorFiles = pattern.flatMap((p) => glob.sync(p, {
|
|
165
|
+
const mirrorFiles = glob.sync("**/*.mirror.json", {
|
|
166
|
+
cwd: baseDir,
|
|
247
167
|
ignore: "node_modules/**",
|
|
248
|
-
})
|
|
249
|
-
const
|
|
250
|
-
|
|
168
|
+
});
|
|
169
|
+
const filePromises = mirrorFiles.map(async (relativePath) => {
|
|
170
|
+
const absolutePath = path.join(baseDir, relativePath);
|
|
171
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
172
|
+
let data;
|
|
251
173
|
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}`);
|
|
174
|
+
const content = await fsPromises.readFile(absolutePath, "utf8");
|
|
175
|
+
data = JSON.parse(content);
|
|
259
176
|
}
|
|
260
177
|
catch (error) {
|
|
261
|
-
console.error(`Error reading initial state from ${
|
|
178
|
+
console.error(`Error reading initial state from ${relativePath}:`, error);
|
|
179
|
+
return null;
|
|
262
180
|
}
|
|
181
|
+
logger(`Inlined initial state for ${name}`);
|
|
182
|
+
return [name, data];
|
|
263
183
|
});
|
|
264
|
-
|
|
184
|
+
const results = await Promise.all(filePromises);
|
|
185
|
+
const states = Object.fromEntries(results.filter((x) => x != null));
|
|
186
|
+
return `export const INITIAL_STATES = ${JSON.stringify(states)};`;
|
|
265
187
|
}
|
|
266
188
|
},
|
|
267
189
|
closeBundle() {
|
package/package.json
CHANGED