@u-devtools/core 0.1.5 → 0.2.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/LICENSE +1 -2
- package/README.md +119 -0
- package/dist/index.cjs.js +46 -1
- package/dist/index.d.ts +871 -64
- package/dist/index.es.js +3113 -56
- package/dist/vite/vite.config.base.cjs.js +1 -0
- package/dist/vite/vite.config.base.d.ts +59 -0
- package/dist/vite/vite.config.base.js +91 -0
- package/dist/vite.config.base.d.ts +42 -0
- package/package.json +26 -18
- package/src/bridge-app.ts +198 -51
- package/src/control.ts +23 -52
- package/src/event-bus.ts +126 -0
- package/src/index.ts +476 -44
- package/src/schemas/rpc.ts +36 -0
- package/src/schemas/settings.ts +125 -0
- package/src/transport.ts +172 -0
- package/src/transports/broadcast-transport.ts +174 -0
- package/src/transports/hmr-transport.ts +82 -0
- package/src/transports/websocket-transport.ts +158 -0
- package/vite/vite.config.base.ts +81 -14
- package/vite/clean-timestamp-plugin.ts +0 -28
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Transport } from '../transport';
|
|
2
|
+
import type { RpcMessage } from '../index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transport based on WebSocket
|
|
6
|
+
* Used for communication between client and server when HMR is unavailable
|
|
7
|
+
* (e.g., for remote debugging or production builds)
|
|
8
|
+
*/
|
|
9
|
+
export class WebSocketTransport extends Transport {
|
|
10
|
+
private ws: WebSocket | null = null;
|
|
11
|
+
|
|
12
|
+
// Reconnection
|
|
13
|
+
private reconnectAttempts = 0;
|
|
14
|
+
private maxReconnectAttempts = 5;
|
|
15
|
+
private reconnectDelay = 1000;
|
|
16
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null; // For cleanup
|
|
17
|
+
private isManualClose = false;
|
|
18
|
+
|
|
19
|
+
// Message queue (buffer)
|
|
20
|
+
private messageQueue: string[] = [];
|
|
21
|
+
|
|
22
|
+
constructor(private url: string) {
|
|
23
|
+
super();
|
|
24
|
+
this.connect();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private connect() {
|
|
28
|
+
// Clear old socket if exists
|
|
29
|
+
if (this.ws) {
|
|
30
|
+
this.ws.close();
|
|
31
|
+
this.ws = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
this.ws = new WebSocket(this.url);
|
|
36
|
+
|
|
37
|
+
this.ws.onopen = () => {
|
|
38
|
+
this.reconnectAttempts = 0;
|
|
39
|
+
console.log('[WebSocketTransport] Connected');
|
|
40
|
+
this.flushQueue(); // Send queued messages
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.ws.onmessage = (e) => {
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(e.data);
|
|
46
|
+
// Call base class method for message routing
|
|
47
|
+
this.handleMessage(data);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('[WebSocketTransport] Failed to parse message:', err);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.ws.onerror = (error) => {
|
|
54
|
+
// Connection errors often lead to onclose, so just log here
|
|
55
|
+
console.error('[WebSocketTransport] Socket error:', error);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
this.ws.onclose = (e) => {
|
|
59
|
+
if (this.isManualClose) return;
|
|
60
|
+
|
|
61
|
+
console.warn(`[WebSocketTransport] Closed (code: ${e.code}). Attempting reconnect...`);
|
|
62
|
+
this.scheduleReconnect();
|
|
63
|
+
};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('[WebSocketTransport] Failed to create socket:', err);
|
|
66
|
+
this.scheduleReconnect();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private scheduleReconnect() {
|
|
71
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
72
|
+
|
|
73
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
74
|
+
this.reconnectAttempts++;
|
|
75
|
+
const delay = this.reconnectDelay * this.reconnectAttempts; // Exponential backoff (simple)
|
|
76
|
+
|
|
77
|
+
console.log(`[WebSocketTransport] Reconnecting in ${delay}ms...`);
|
|
78
|
+
|
|
79
|
+
this.reconnectTimer = setTimeout(() => {
|
|
80
|
+
this.connect();
|
|
81
|
+
}, delay);
|
|
82
|
+
} else {
|
|
83
|
+
console.error('[WebSocketTransport] Max reconnect attempts reached');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected sendMessage(type: string, data: unknown): void {
|
|
88
|
+
// Improvement 3: Form message more safely.
|
|
89
|
+
// Assume Base Transport passes payload that needs to be wrapped,
|
|
90
|
+
// OR Base Transport already passes a ready structure.
|
|
91
|
+
// Better to standardize: Transport should pass a ready object for sending.
|
|
92
|
+
|
|
93
|
+
// Let's form the packet here, as in original, but safer:
|
|
94
|
+
const rawData = data as any;
|
|
95
|
+
|
|
96
|
+
const message: RpcMessage = {
|
|
97
|
+
id: rawData.id || '', // Fallback
|
|
98
|
+
type: type as 'request' | 'event' | 'response', // Cast type safely
|
|
99
|
+
method: rawData.method,
|
|
100
|
+
payload: type === 'request' ? rawData.payload : rawData,
|
|
101
|
+
error: rawData.error, // Don't forget errors
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const serialized = JSON.stringify(message);
|
|
105
|
+
|
|
106
|
+
if (this.isConnected()) {
|
|
107
|
+
this.ws!.send(serialized);
|
|
108
|
+
} else {
|
|
109
|
+
// Improvement 2: Buffering
|
|
110
|
+
this.messageQueue.push(serialized);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private flushQueue() {
|
|
115
|
+
if (!this.isConnected()) return;
|
|
116
|
+
|
|
117
|
+
while (this.messageQueue.length > 0) {
|
|
118
|
+
const msg = this.messageQueue.shift();
|
|
119
|
+
if (msg) this.ws!.send(msg);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Subscription methods not needed, as WS is a "pipe".
|
|
124
|
+
// Base Transport class manages listener map itself,
|
|
125
|
+
// and handleMessage distributes data.
|
|
126
|
+
protected subscribe(_type: string, _handler: (data: unknown) => void): () => void {
|
|
127
|
+
return () => {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected unsubscribe(_type: string, _handler: (data: unknown) => void): void {
|
|
131
|
+
// No-op
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
override dispose() {
|
|
135
|
+
this.isManualClose = true;
|
|
136
|
+
|
|
137
|
+
// Improvement 1: Clear timer
|
|
138
|
+
if (this.reconnectTimer) {
|
|
139
|
+
clearTimeout(this.reconnectTimer);
|
|
140
|
+
this.reconnectTimer = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.ws) {
|
|
144
|
+
this.ws.close();
|
|
145
|
+
this.ws = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.messageQueue = []; // Clear memory
|
|
149
|
+
super.dispose();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check connection status
|
|
154
|
+
*/
|
|
155
|
+
isConnected(): boolean {
|
|
156
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/vite/vite.config.base.ts
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
import { defineConfig, type PluginOption } from 'vite';
|
|
1
|
+
import { defineConfig, type Plugin, type PluginOption } from 'vite';
|
|
2
2
|
import vue from '@vitejs/plugin-vue';
|
|
3
3
|
import dts from 'vite-plugin-dts';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { readdirSync, unlinkSync } from 'node:fs';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function cleanTimestampFiles(dir: string): Plugin {
|
|
8
|
+
return {
|
|
9
|
+
name: 'clean-timestamp-files',
|
|
10
|
+
buildStart() {
|
|
11
|
+
try {
|
|
12
|
+
const files = readdirSync(dir);
|
|
13
|
+
files.forEach((file) => {
|
|
14
|
+
if (file.includes('.timestamp-') && file.endsWith('.mjs')) {
|
|
15
|
+
try {
|
|
16
|
+
unlinkSync(join(dir, file));
|
|
17
|
+
} catch (_e) {
|
|
18
|
+
// Ignore deletion errors
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
} catch (_e) {
|
|
23
|
+
// Ignore directory read errors
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration options for createViteConfig
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export interface ConfigOptions {
|
|
8
34
|
entry: string | Record<string, string>;
|
|
9
35
|
name: string;
|
|
10
36
|
dir: string;
|
|
@@ -24,6 +50,40 @@ interface ConfigOptions {
|
|
|
24
50
|
cssCodeSplit?: boolean;
|
|
25
51
|
}
|
|
26
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Creates a Vite configuration optimized for building DevTools packages.
|
|
55
|
+
*
|
|
56
|
+
* This function generates a Vite config with proper TypeScript declaration file generation,
|
|
57
|
+
* preserving JSDoc comments in .d.ts files for better IDE autocomplete.
|
|
58
|
+
*
|
|
59
|
+
* @param options - Configuration options for the Vite build
|
|
60
|
+
* @param options.entry - Entry point(s) for the library (string or record of entry points)
|
|
61
|
+
* @param options.name - Library name (used for UMD builds)
|
|
62
|
+
* @param options.dir - Package directory (usually __dirname)
|
|
63
|
+
* @param options.external - Array of module IDs to externalize
|
|
64
|
+
* @param options.clearScreen - Whether to clear screen on build (default: false)
|
|
65
|
+
* @param options.useVue - Whether to include Vue plugin (default: true)
|
|
66
|
+
* @param options.formats - Output formats: 'es' and/or 'cjs' (default: ['es', 'cjs'])
|
|
67
|
+
* @param options.fileName - Custom file naming function or string
|
|
68
|
+
* @param options.dtsOptions - Options for TypeScript declaration file generation
|
|
69
|
+
* @param options.additionalPlugins - Additional Vite plugins to include
|
|
70
|
+
* @param options.resolveAlias - Path aliases for module resolution
|
|
71
|
+
* @param options.cssCodeSplit - Whether to enable CSS code splitting
|
|
72
|
+
* @returns Vite configuration object
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { createViteConfig } from '@u-devtools/core/vite.config.base';
|
|
77
|
+
*
|
|
78
|
+
* export default createViteConfig({
|
|
79
|
+
* name: 'MyPackage',
|
|
80
|
+
* entry: 'src/index.ts',
|
|
81
|
+
* dir: __dirname,
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
27
87
|
export function createViteConfig({
|
|
28
88
|
entry,
|
|
29
89
|
name,
|
|
@@ -51,6 +111,10 @@ export function createViteConfig({
|
|
|
51
111
|
exclude: dtsOptions.exclude,
|
|
52
112
|
copyDtsFiles: dtsOptions.copyDtsFiles,
|
|
53
113
|
tsconfigPath: resolve(dir, 'tsconfig.json'),
|
|
114
|
+
outDir: resolve(dir, 'dist'),
|
|
115
|
+
compilerOptions: {
|
|
116
|
+
removeComments: false, // Explicitly preserve JSDoc comments
|
|
117
|
+
},
|
|
54
118
|
})
|
|
55
119
|
);
|
|
56
120
|
|
|
@@ -72,19 +136,22 @@ export function createViteConfig({
|
|
|
72
136
|
return defineConfig({
|
|
73
137
|
clearScreen,
|
|
74
138
|
plugins,
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
139
|
+
// IMPORTANT: This prevents replacing import.meta.hot with false during build
|
|
140
|
+
// Now code in dist will contain check if (import.meta.hot)
|
|
141
|
+
// and HMR will work even in built version
|
|
78
142
|
define: {
|
|
79
143
|
'import.meta.hot': 'import.meta.hot',
|
|
80
144
|
},
|
|
81
|
-
resolve:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
resolve: {
|
|
146
|
+
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
|
147
|
+
...(resolveAlias
|
|
148
|
+
? {
|
|
149
|
+
alias: Object.fromEntries(
|
|
150
|
+
Object.entries(resolveAlias).map(([key, value]) => [key, resolve(dir, value)])
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
},
|
|
88
155
|
build: {
|
|
89
156
|
lib: {
|
|
90
157
|
entry: entryResolved,
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
2
|
-
import { readdirSync, unlinkSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Плагин для очистки временных timestamp файлов, создаваемых Vite
|
|
7
|
-
*/
|
|
8
|
-
export function cleanTimestampFiles(dir: string): Plugin {
|
|
9
|
-
return {
|
|
10
|
-
name: 'clean-timestamp-files',
|
|
11
|
-
buildStart() {
|
|
12
|
-
try {
|
|
13
|
-
const files = readdirSync(dir);
|
|
14
|
-
files.forEach((file) => {
|
|
15
|
-
if (file.includes('.timestamp-') && file.endsWith('.mjs')) {
|
|
16
|
-
try {
|
|
17
|
-
unlinkSync(join(dir, file));
|
|
18
|
-
} catch (_e) {
|
|
19
|
-
// Игнорируем ошибки удаления
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
} catch (_e) {
|
|
24
|
-
// Игнорируем ошибки чтения директории
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|