vite-plugin-mirrorstate 0.1.0-alpha.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 +9 -0
- package/dist/index.js +184 -0
- package/package.json +41 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
export interface MirrorStatePluginOptions {
|
|
3
|
+
path?: string;
|
|
4
|
+
filePattern?: string | string[];
|
|
5
|
+
watchOptions?: any;
|
|
6
|
+
prettyPrint?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function mirrorStatePlugin(options?: MirrorStatePluginOptions): Plugin;
|
|
9
|
+
export default mirrorStatePlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as chokidar from "chokidar";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { glob } from "glob";
|
|
6
|
+
import debug from "debug";
|
|
7
|
+
const logger = debug("mirrorstate:vite-plugin");
|
|
8
|
+
export function mirrorStatePlugin(options = {}) {
|
|
9
|
+
const opts = {
|
|
10
|
+
path: "/mirrorstate",
|
|
11
|
+
filePattern: "**/*.mirror.json",
|
|
12
|
+
prettyPrint: true,
|
|
13
|
+
...options,
|
|
14
|
+
};
|
|
15
|
+
let wss;
|
|
16
|
+
let watcher;
|
|
17
|
+
let recentWrites = new Set(); // Track recent writes to prevent echo
|
|
18
|
+
let lastMessageHash = new Map(); // Track last message hash per client to prevent duplicates
|
|
19
|
+
return {
|
|
20
|
+
name: "vite-plugin-mirrorstate",
|
|
21
|
+
configureServer(server) {
|
|
22
|
+
const wsPath = opts.path;
|
|
23
|
+
wss = new WebSocketServer({ noServer: true });
|
|
24
|
+
server.httpServer.on("upgrade", (request, socket, head) => {
|
|
25
|
+
if (request.url === wsPath) {
|
|
26
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
27
|
+
wss.emit("connection", ws, request);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
watcher = chokidar.watch(opts.filePattern, {
|
|
32
|
+
ignored: /node_modules/,
|
|
33
|
+
persistent: true,
|
|
34
|
+
...opts.watchOptions,
|
|
35
|
+
});
|
|
36
|
+
logger(`MirrorState WebSocket listening on ws://localhost:${server.config.server.port || 5173}${wsPath}`);
|
|
37
|
+
watcher.on("change", (filePath) => {
|
|
38
|
+
try {
|
|
39
|
+
// Skip if this was a recent write from WebSocket to prevent echo
|
|
40
|
+
if (recentWrites.has(filePath)) {
|
|
41
|
+
recentWrites.delete(filePath);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
45
|
+
const data = JSON.parse(content);
|
|
46
|
+
const relativePath = path.relative(server.config.root || process.cwd(), filePath);
|
|
47
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
48
|
+
// This is an external file change (from editor, etc.)
|
|
49
|
+
wss.clients.forEach((client) => {
|
|
50
|
+
if (client.readyState === client.OPEN) {
|
|
51
|
+
client.send(JSON.stringify({
|
|
52
|
+
type: "fileChange",
|
|
53
|
+
name,
|
|
54
|
+
state: data,
|
|
55
|
+
source: "external",
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
logger(`Mirror file changed externally: ${name}`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`Error reading mirror file ${filePath}:`, error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
wss.on("connection", (ws) => {
|
|
66
|
+
// Generate unique ID for this connection to prevent echo loops
|
|
67
|
+
const clientId = Math.random().toString(36).substring(7);
|
|
68
|
+
ws.clientId = clientId;
|
|
69
|
+
logger(`Client connected to MirrorState (${clientId})`);
|
|
70
|
+
const pattern = Array.isArray(opts.filePattern)
|
|
71
|
+
? opts.filePattern
|
|
72
|
+
: [opts.filePattern];
|
|
73
|
+
const mirrorFiles = pattern.flatMap((p) => glob.sync(p, { ignore: "node_modules/**" }));
|
|
74
|
+
mirrorFiles.forEach((filePath) => {
|
|
75
|
+
try {
|
|
76
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
77
|
+
const data = JSON.parse(content);
|
|
78
|
+
const relativePath = path.relative(server.config.root || process.cwd(), filePath);
|
|
79
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
80
|
+
ws.send(JSON.stringify({
|
|
81
|
+
type: "initialState",
|
|
82
|
+
name,
|
|
83
|
+
state: data,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error(`Error reading initial state from ${filePath}:`, error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
ws.on("message", (message) => {
|
|
91
|
+
try {
|
|
92
|
+
const messageStr = message.toString();
|
|
93
|
+
const data = JSON.parse(messageStr);
|
|
94
|
+
const { name, state } = data;
|
|
95
|
+
// Create a hash of the message to detect duplicates
|
|
96
|
+
const messageHash = `${name}:${JSON.stringify(state)}`;
|
|
97
|
+
const lastHash = lastMessageHash.get(clientId);
|
|
98
|
+
// Skip if this is a duplicate message from the same client
|
|
99
|
+
if (lastHash === messageHash) {
|
|
100
|
+
logger(`Skipping duplicate message from ${clientId} for ${name}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Update last message hash for this client
|
|
104
|
+
lastMessageHash.set(clientId, messageHash);
|
|
105
|
+
const filePath = `${name}.mirror.json`;
|
|
106
|
+
const jsonContent = opts.prettyPrint
|
|
107
|
+
? JSON.stringify(state, null, 2)
|
|
108
|
+
: JSON.stringify(state);
|
|
109
|
+
// Mark this as a recent write to prevent file watcher echo
|
|
110
|
+
recentWrites.add(filePath);
|
|
111
|
+
// Write state to file
|
|
112
|
+
fs.writeFileSync(filePath, jsonContent);
|
|
113
|
+
// Broadcast to other clients (exclude sender to prevent echo)
|
|
114
|
+
wss.clients.forEach((client) => {
|
|
115
|
+
if (client !== ws && client.readyState === client.OPEN) {
|
|
116
|
+
const relativePath = path.relative(server.config.root || process.cwd(), filePath);
|
|
117
|
+
const fileName = relativePath.replace(/\.mirror\.json$/, "");
|
|
118
|
+
client.send(JSON.stringify({
|
|
119
|
+
type: "fileChange",
|
|
120
|
+
name: fileName,
|
|
121
|
+
state: state,
|
|
122
|
+
source: clientId,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
logger(`Updated ${name} with state (from ${clientId}):`, state);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error("Error handling client message:", error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
ws.on("close", () => {
|
|
133
|
+
// Clean up client data on disconnect
|
|
134
|
+
lastMessageHash.delete(clientId);
|
|
135
|
+
logger(`Client ${clientId} disconnected`);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
resolveId(id) {
|
|
140
|
+
if (id === "virtual:mirrorstate/config" ||
|
|
141
|
+
id === "virtual:mirrorstate/initial-states") {
|
|
142
|
+
return id;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
load(id) {
|
|
146
|
+
if (id === "virtual:mirrorstate/config") {
|
|
147
|
+
return `export const WS_PATH = "${opts.path}";`;
|
|
148
|
+
}
|
|
149
|
+
if (id === "virtual:mirrorstate/initial-states") {
|
|
150
|
+
// During build, read all mirror files and inline them
|
|
151
|
+
const pattern = Array.isArray(opts.filePattern)
|
|
152
|
+
? opts.filePattern
|
|
153
|
+
: [opts.filePattern];
|
|
154
|
+
const mirrorFiles = pattern.flatMap((p) => glob.sync(p, { ignore: "node_modules/**" }));
|
|
155
|
+
const states = {};
|
|
156
|
+
mirrorFiles.forEach((filePath) => {
|
|
157
|
+
try {
|
|
158
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
159
|
+
const data = JSON.parse(content);
|
|
160
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
161
|
+
const name = relativePath.replace(/\.mirror\.json$/, "");
|
|
162
|
+
states[name] = data;
|
|
163
|
+
logger(`Inlined initial state for ${name}`);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error(`Error reading initial state from ${filePath}:`, error);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return `export const INITIAL_STATES = ${JSON.stringify(states, null, 2)};`;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
closeBundle() {
|
|
173
|
+
if (watcher) {
|
|
174
|
+
watcher.close();
|
|
175
|
+
logger("File watcher closed");
|
|
176
|
+
}
|
|
177
|
+
if (wss) {
|
|
178
|
+
wss.close();
|
|
179
|
+
logger("WebSocket server closed");
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export default mirrorStatePlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-mirrorstate",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Vite plugin for bidirectional state synchronization through *.mirror.json files",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"vite",
|
|
13
|
+
"plugin",
|
|
14
|
+
"state",
|
|
15
|
+
"sync"
|
|
16
|
+
],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"chokidar": "^3.5.3",
|
|
21
|
+
"debug": "^4.4.1",
|
|
22
|
+
"glob": "^10.3.10",
|
|
23
|
+
"ws": "^8.14.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/debug": "^4.1.12",
|
|
27
|
+
"@types/node": "^18.0.0",
|
|
28
|
+
"@types/ws": "^8.5.8",
|
|
29
|
+
"typescript": "^5.3.3",
|
|
30
|
+
"vite": "^7.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"vite": "^7.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|