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.
@@ -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
+ }