vite-plugin-mirrorstate 0.2.3 → 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 +118 -193
- 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,260 +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;
|
|
17
|
-
let
|
|
18
|
-
let
|
|
19
|
-
let
|
|
13
|
+
let viteRoot;
|
|
14
|
+
let fileSequences = new Map(); // Track sequence number per file
|
|
15
|
+
let ignoreNextChange = new Map(); // Track our own writes to ignore echo from file watcher
|
|
20
16
|
return {
|
|
21
17
|
name: "vite-plugin-mirrorstate",
|
|
18
|
+
configResolved(config) {
|
|
19
|
+
viteRoot = config.root || process.cwd();
|
|
20
|
+
},
|
|
22
21
|
configureServer(server) {
|
|
23
|
-
const
|
|
22
|
+
const invalidateVirtualModule = () => {
|
|
23
|
+
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
24
|
+
if (mod) {
|
|
25
|
+
server.moduleGraph.invalidateModule(mod);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
24
28
|
wss = new WebSocketServer({ noServer: true });
|
|
25
29
|
server.httpServer.on("upgrade", (request, socket, head) => {
|
|
26
|
-
if (request.url ===
|
|
30
|
+
if (request.url === WS_PATH) {
|
|
27
31
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
28
32
|
wss.emit("connection", ws, request);
|
|
29
33
|
});
|
|
30
34
|
}
|
|
31
35
|
});
|
|
36
|
+
logger(`MirrorState WebSocket listening on ws://localhost:${server.config.server.port || 5173}${WS_PATH}`);
|
|
32
37
|
const baseDir = server.config.root || process.cwd();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
},
|
|
45
51
|
persistent: true,
|
|
46
|
-
|
|
52
|
+
ignoreInitial: true,
|
|
47
53
|
});
|
|
48
54
|
watcher.on("add", (filePath) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
55
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
56
|
-
const data = JSON.parse(content);
|
|
57
|
-
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
58
|
-
// Send new state to all connected clients
|
|
59
|
-
wss.clients.forEach((client) => {
|
|
60
|
-
if (client.readyState === client.OPEN) {
|
|
61
|
-
client.send(JSON.stringify({
|
|
62
|
-
type: "initialState",
|
|
63
|
-
name,
|
|
64
|
-
state: data,
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
// Invalidate the virtual module for HMR
|
|
69
|
-
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
70
|
-
if (mod) {
|
|
71
|
-
server.moduleGraph.invalidateModule(mod);
|
|
72
|
-
}
|
|
73
|
-
logger(`New mirror file added: ${name}`);
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
console.error(`Error reading new mirror file ${filePath}:`, error);
|
|
77
|
-
}
|
|
55
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
56
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
57
|
+
invalidateVirtualModule();
|
|
58
|
+
logger(`New mirror file added: ${name}`);
|
|
78
59
|
});
|
|
79
60
|
watcher.on("unlink", (filePath) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
const relativePath = path.relative(baseDir, filePath);
|
|
86
|
-
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
87
|
-
// Invalidate the virtual module for HMR
|
|
88
|
-
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
89
|
-
if (mod) {
|
|
90
|
-
server.moduleGraph.invalidateModule(mod);
|
|
91
|
-
}
|
|
92
|
-
logger(`Mirror file deleted: ${name}`);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
console.error(`Error handling mirror file deletion ${filePath}:`, error);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
watcher.on("ready", () => {
|
|
99
|
-
watcherReady = true;
|
|
100
|
-
logger("File watcher is ready");
|
|
61
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
62
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
63
|
+
invalidateVirtualModule();
|
|
64
|
+
logger(`Mirror file deleted: ${name}`);
|
|
101
65
|
});
|
|
102
|
-
logger(`MirrorState WebSocket listening on ws://localhost:${server.config.server.port || 5173}${wsPath}`);
|
|
103
66
|
watcher.on("change", (filePath) => {
|
|
104
|
-
|
|
105
|
-
|
|
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}`);
|
|
106
73
|
return;
|
|
107
74
|
}
|
|
75
|
+
let data;
|
|
108
76
|
try {
|
|
109
|
-
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
77
|
const content = fs.readFileSync(filePath, "utf8");
|
|
116
|
-
|
|
117
|
-
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
118
|
-
// This is an external file change (from editor, etc.)
|
|
119
|
-
wss.clients.forEach((client) => {
|
|
120
|
-
if (client.readyState === client.OPEN) {
|
|
121
|
-
client.send(JSON.stringify({
|
|
122
|
-
type: "fileChange",
|
|
123
|
-
name,
|
|
124
|
-
state: data,
|
|
125
|
-
source: "external",
|
|
126
|
-
}));
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
// Invalidate the virtual module for HMR
|
|
130
|
-
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
131
|
-
if (mod) {
|
|
132
|
-
server.moduleGraph.invalidateModule(mod);
|
|
133
|
-
}
|
|
134
|
-
logger(`Mirror file changed externally: ${name}`);
|
|
78
|
+
data = JSON.parse(content);
|
|
135
79
|
}
|
|
136
80
|
catch (error) {
|
|
137
81
|
console.error(`Error reading mirror file ${filePath}:`, error);
|
|
82
|
+
return;
|
|
138
83
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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",
|
|
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",
|
|
159
92
|
name,
|
|
160
93
|
state: data,
|
|
94
|
+
seq,
|
|
161
95
|
}));
|
|
162
96
|
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
console.error(`Error reading initial state from ${filePath}:`, error);
|
|
165
|
-
}
|
|
166
97
|
});
|
|
98
|
+
invalidateVirtualModule();
|
|
99
|
+
logger(`Mirror file changed externally: ${name} (seq: ${seq})`);
|
|
100
|
+
});
|
|
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}]`);
|
|
167
106
|
ws.on("message", (message) => {
|
|
107
|
+
const messageStr = message.toString();
|
|
108
|
+
let data;
|
|
168
109
|
try {
|
|
169
|
-
|
|
170
|
-
const data = JSON.parse(messageStr);
|
|
171
|
-
const { name, state } = data;
|
|
172
|
-
// Create a hash of the message to detect duplicates
|
|
173
|
-
const messageHash = `${name}:${JSON.stringify(state)}`;
|
|
174
|
-
const lastHash = lastMessageHash.get(clientId);
|
|
175
|
-
// Skip if this is a duplicate message from the same client
|
|
176
|
-
if (lastHash === messageHash) {
|
|
177
|
-
logger(`Skipping duplicate message from ${clientId} for ${name}`);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
// Update last message hash for this client
|
|
181
|
-
lastMessageHash.set(clientId, messageHash);
|
|
182
|
-
const baseDir = server.config.root || process.cwd();
|
|
183
|
-
const relativeFilePath = `${name}.mirror.json`;
|
|
184
|
-
const filePath = path.join(baseDir, relativeFilePath);
|
|
185
|
-
const jsonContent = opts.prettyPrint
|
|
186
|
-
? JSON.stringify(state, null, 2)
|
|
187
|
-
: JSON.stringify(state);
|
|
188
|
-
// Write state to file
|
|
189
|
-
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
|
-
// Invalidate the virtual module for HMR
|
|
194
|
-
const mod = server.moduleGraph.getModuleById("\0virtual:mirrorstate/initial-states");
|
|
195
|
-
if (mod) {
|
|
196
|
-
server.moduleGraph.invalidateModule(mod);
|
|
197
|
-
}
|
|
198
|
-
// Broadcast to other clients (exclude sender to prevent echo)
|
|
199
|
-
wss.clients.forEach((client) => {
|
|
200
|
-
if (client !== ws && client.readyState === client.OPEN) {
|
|
201
|
-
client.send(JSON.stringify({
|
|
202
|
-
type: "fileChange",
|
|
203
|
-
name,
|
|
204
|
-
state: state,
|
|
205
|
-
source: clientId,
|
|
206
|
-
}));
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
logger(`Updated ${name} with state (from ${clientId}):`, state);
|
|
110
|
+
data = JSON.parse(messageStr);
|
|
210
111
|
}
|
|
211
112
|
catch (error) {
|
|
212
113
|
console.error("Error handling client message:", error);
|
|
114
|
+
return;
|
|
213
115
|
}
|
|
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
|
|
122
|
+
const currentSeq = fileSequences.get(name) ?? 0;
|
|
123
|
+
const seq = currentSeq + 1;
|
|
124
|
+
fileSequences.set(name, seq);
|
|
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)
|
|
130
|
+
wss.clients.forEach((client) => {
|
|
131
|
+
if (client !== ws && client.readyState === client.OPEN) {
|
|
132
|
+
client.send(JSON.stringify({
|
|
133
|
+
type: "fileChange",
|
|
134
|
+
seq,
|
|
135
|
+
name,
|
|
136
|
+
state,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
invalidateVirtualModule();
|
|
141
|
+
logger(`Updated ${name} (seq: ${seq}) from [${clientId}]:`, state);
|
|
214
142
|
});
|
|
215
143
|
ws.on("close", () => {
|
|
216
|
-
|
|
217
|
-
lastMessageHash.delete(clientId);
|
|
218
|
-
logger(`Client ${clientId} disconnected`);
|
|
144
|
+
logger(`Client [${clientId}] disconnected`);
|
|
219
145
|
});
|
|
220
146
|
});
|
|
221
147
|
},
|
|
222
148
|
resolveId(id) {
|
|
223
|
-
if (id === "virtual:mirrorstate/config"
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
if (id === "virtual:mirrorstate/initial-states") {
|
|
149
|
+
if (id === "virtual:mirrorstate/config" ||
|
|
150
|
+
id === "virtual:mirrorstate/initial-states") {
|
|
227
151
|
return "\0" + id;
|
|
228
152
|
}
|
|
229
153
|
},
|
|
230
|
-
load(id) {
|
|
154
|
+
async load(id) {
|
|
231
155
|
if (id === "\0virtual:mirrorstate/config") {
|
|
232
|
-
return `export const WS_PATH = "${
|
|
156
|
+
return `export const WS_PATH = "${WS_PATH}";`;
|
|
233
157
|
}
|
|
158
|
+
// During build, read all mirror files and inline them
|
|
234
159
|
if (id === "\0virtual:mirrorstate/initial-states") {
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
? opts.filePattern.map((p) => path.join(baseDir, p))
|
|
239
|
-
: [path.join(baseDir, opts.filePattern)];
|
|
240
|
-
const mirrorFiles = pattern.flatMap((p) => glob.sync(p, {
|
|
160
|
+
const baseDir = viteRoot || process.cwd();
|
|
161
|
+
const mirrorFiles = glob.sync("**/*.mirror.json", {
|
|
162
|
+
cwd: baseDir,
|
|
241
163
|
ignore: "node_modules/**",
|
|
242
|
-
})
|
|
243
|
-
const
|
|
244
|
-
|
|
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;
|
|
245
169
|
try {
|
|
246
|
-
const content =
|
|
247
|
-
|
|
248
|
-
const relativePath = path.relative(process.cwd(), filePath);
|
|
249
|
-
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
250
|
-
states[name] = data;
|
|
251
|
-
logger(`Inlined initial state for ${name}`);
|
|
170
|
+
const content = await fsPromises.readFile(absolutePath, "utf8");
|
|
171
|
+
data = JSON.parse(content);
|
|
252
172
|
}
|
|
253
173
|
catch (error) {
|
|
254
|
-
console.error(`Error reading initial state from ${
|
|
174
|
+
console.error(`Error reading initial state from ${relativePath}:`, error);
|
|
175
|
+
return null;
|
|
255
176
|
}
|
|
177
|
+
logger(`Inlined initial state for ${name}`);
|
|
178
|
+
return [name, data];
|
|
256
179
|
});
|
|
257
|
-
|
|
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)};`;
|
|
258
183
|
}
|
|
259
184
|
},
|
|
260
185
|
closeBundle() {
|
package/package.json
CHANGED