@rpgjs/vite 5.0.0-alpha.4 → 5.0.0-alpha.41
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/entry-point-plugin.d.ts +48 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1062 -468
- package/dist/index.js.map +1 -1
- package/dist/replace-config-import.d.ts +4 -0
- package/dist/rpgjs-plugin.d.ts +23 -0
- package/dist/server-plugin.d.ts +285 -0
- package/package.json +16 -12
- package/src/entry-point-plugin.ts +99 -0
- package/src/index.ts +4 -0
- package/src/module-config.ts +13 -0
- package/src/replace-config-import.ts +24 -0
- package/src/rpgjs-plugin.ts +29 -0
- package/src/server-plugin.ts +977 -0
- package/src/types/rpgjs__server.d.ts +11 -0
- package/tests/entry-point-plugin.spec.ts +172 -0
- package/tests/latency-simulation.spec.ts +209 -0
- package/vite.config.ts +2 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare module '@rpgjs/server' {
|
|
2
|
+
export class RpgServerEngine {
|
|
3
|
+
onStart?(): void | Promise<void>;
|
|
4
|
+
onRequest?(req: any): any | Promise<any>;
|
|
5
|
+
onMessage?(message: string, connection: any): void | Promise<void>;
|
|
6
|
+
onClose?(connection: any): void | Promise<void>;
|
|
7
|
+
onConnect?(connection: any, context: any): void | Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { entryPointPlugin } from '../src/entry-point-plugin';
|
|
3
|
+
|
|
4
|
+
describe('entryPointPlugin', () => {
|
|
5
|
+
let originalEnv: string | undefined;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
originalEnv = process.env.RPG_TYPE;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (originalEnv !== undefined) {
|
|
13
|
+
process.env.RPG_TYPE = originalEnv;
|
|
14
|
+
} else {
|
|
15
|
+
delete process.env.RPG_TYPE;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should replace script tag with rpg entry point when RPG_TYPE is rpg', () => {
|
|
20
|
+
process.env.RPG_TYPE = 'rpg';
|
|
21
|
+
|
|
22
|
+
const plugin = entryPointPlugin();
|
|
23
|
+
const html = `
|
|
24
|
+
<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<script type="module" src="./src/client.ts"></script>
|
|
28
|
+
</head>
|
|
29
|
+
<body></body>
|
|
30
|
+
</html>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const transform = plugin.transformIndexHtml as any;
|
|
34
|
+
const result = transform.handler(html);
|
|
35
|
+
|
|
36
|
+
expect(result).toContain('<script type="module" src="./src/standalone.ts"></script>');
|
|
37
|
+
expect(result).not.toContain('./src/client.ts');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should replace script tag with mmorpg entry point when RPG_TYPE is mmorpg', () => {
|
|
41
|
+
process.env.RPG_TYPE = 'mmorpg';
|
|
42
|
+
|
|
43
|
+
const plugin = entryPointPlugin();
|
|
44
|
+
const html = `
|
|
45
|
+
<!DOCTYPE html>
|
|
46
|
+
<html>
|
|
47
|
+
<head>
|
|
48
|
+
<script type="module" src="./src/standalone.ts"></script>
|
|
49
|
+
</head>
|
|
50
|
+
<body></body>
|
|
51
|
+
</html>
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const transform = plugin.transformIndexHtml as any;
|
|
55
|
+
const result = transform.handler(html);
|
|
56
|
+
|
|
57
|
+
expect(result).toContain('<script type="module" src="./src/client.ts"></script>');
|
|
58
|
+
expect(result).not.toContain('./src/standalone.ts');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should use custom entry points when provided', () => {
|
|
62
|
+
process.env.RPG_TYPE = 'rpg';
|
|
63
|
+
|
|
64
|
+
const plugin = entryPointPlugin({
|
|
65
|
+
entryPoints: {
|
|
66
|
+
rpg: './src/custom-standalone.ts',
|
|
67
|
+
mmorpg: './src/custom-client.ts'
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const html = `
|
|
72
|
+
<!DOCTYPE html>
|
|
73
|
+
<html>
|
|
74
|
+
<head>
|
|
75
|
+
<script type="module" src="./src/client.ts"></script>
|
|
76
|
+
</head>
|
|
77
|
+
<body></body>
|
|
78
|
+
</html>
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const transform = plugin.transformIndexHtml as any;
|
|
82
|
+
const result = transform.handler(html);
|
|
83
|
+
|
|
84
|
+
expect(result).toContain('<script type="module" src="./src/custom-standalone.ts"></script>');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should add script tag if none exists', () => {
|
|
88
|
+
process.env.RPG_TYPE = 'rpg';
|
|
89
|
+
|
|
90
|
+
const plugin = entryPointPlugin();
|
|
91
|
+
const html = `
|
|
92
|
+
<!DOCTYPE html>
|
|
93
|
+
<html>
|
|
94
|
+
<head>
|
|
95
|
+
<title>Test</title>
|
|
96
|
+
</head>
|
|
97
|
+
<body></body>
|
|
98
|
+
</html>
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const transform = plugin.transformIndexHtml as any;
|
|
102
|
+
const result = transform.handler(html);
|
|
103
|
+
|
|
104
|
+
expect(result).toContain('<script type="module" src="./src/standalone.ts"></script>');
|
|
105
|
+
expect(result).toContain('</head>');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should default to rpg when RPG_TYPE is not set', () => {
|
|
109
|
+
delete process.env.RPG_TYPE;
|
|
110
|
+
|
|
111
|
+
const plugin = entryPointPlugin();
|
|
112
|
+
const html = `
|
|
113
|
+
<!DOCTYPE html>
|
|
114
|
+
<html>
|
|
115
|
+
<head>
|
|
116
|
+
<script type="module" src="./src/client.ts"></script>
|
|
117
|
+
</head>
|
|
118
|
+
<body></body>
|
|
119
|
+
</html>
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const transform = plugin.transformIndexHtml as any;
|
|
123
|
+
const result = transform.handler(html);
|
|
124
|
+
|
|
125
|
+
expect(result).toContain('<script type="module" src="./src/standalone.ts"></script>');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle unknown RPG_TYPE gracefully', () => {
|
|
129
|
+
process.env.RPG_TYPE = 'unknown';
|
|
130
|
+
|
|
131
|
+
const plugin = entryPointPlugin();
|
|
132
|
+
const html = `
|
|
133
|
+
<!DOCTYPE html>
|
|
134
|
+
<html>
|
|
135
|
+
<head>
|
|
136
|
+
<script type="module" src="./src/client.ts"></script>
|
|
137
|
+
</head>
|
|
138
|
+
<body></body>
|
|
139
|
+
</html>
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const transform = plugin.transformIndexHtml as any;
|
|
143
|
+
const result = transform.handler(html);
|
|
144
|
+
|
|
145
|
+
// Should fallback to rpg entry point
|
|
146
|
+
expect(result).toContain('<script type="module" src="./src/standalone.ts"></script>');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle multiple script tags', () => {
|
|
150
|
+
process.env.RPG_TYPE = 'mmorpg';
|
|
151
|
+
|
|
152
|
+
const plugin = entryPointPlugin();
|
|
153
|
+
const html = `
|
|
154
|
+
<!DOCTYPE html>
|
|
155
|
+
<html>
|
|
156
|
+
<head>
|
|
157
|
+
<script type="module" src="./src/client.ts"></script>
|
|
158
|
+
<script type="module" src="./src/another.ts"></script>
|
|
159
|
+
</head>
|
|
160
|
+
<body></body>
|
|
161
|
+
</html>
|
|
162
|
+
`;
|
|
163
|
+
|
|
164
|
+
const transform = plugin.transformIndexHtml as any;
|
|
165
|
+
const result = transform.handler(html);
|
|
166
|
+
|
|
167
|
+
// Both should be replaced
|
|
168
|
+
expect(result).toContain('<script type="module" src="./src/client.ts"></script>');
|
|
169
|
+
expect(result).not.toContain('./src/another.ts');
|
|
170
|
+
expect((result.match(/<script type="module" src="\.\/src\/client\.ts"><\/script>/g) || []).length).toBe(2);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock the PartyConnection class for testing
|
|
4
|
+
class MockPartyConnection {
|
|
5
|
+
public static latencyEnabled: boolean = false;
|
|
6
|
+
public static latencyMinMs: number = 50;
|
|
7
|
+
public static latencyMaxMs: number = 200;
|
|
8
|
+
public static latencyFilter: string = '';
|
|
9
|
+
|
|
10
|
+
public id: string;
|
|
11
|
+
private ws: any;
|
|
12
|
+
|
|
13
|
+
constructor(ws: any, id?: string) {
|
|
14
|
+
this.ws = ws;
|
|
15
|
+
this.id = id || 'test-connection';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async send(data: any): Promise<void> {
|
|
19
|
+
if (this.ws.readyState === 1) {
|
|
20
|
+
const message = typeof data === "string" ? data : JSON.stringify(data);
|
|
21
|
+
|
|
22
|
+
// Check if latency simulation is enabled
|
|
23
|
+
if (MockPartyConnection.latencyEnabled && MockPartyConnection.latencyMaxMs > 0) {
|
|
24
|
+
// Apply filter if specified
|
|
25
|
+
if (MockPartyConnection.latencyFilter && !message.includes(MockPartyConnection.latencyFilter)) {
|
|
26
|
+
this.ws.send(message);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Calculate random latency between min and max
|
|
31
|
+
const latencyMs = Math.random() * (MockPartyConnection.latencyMaxMs - MockPartyConnection.latencyMinMs) + MockPartyConnection.latencyMinMs;
|
|
32
|
+
|
|
33
|
+
// Delay the message
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, latencyMs));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.ws.send(message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static configureLatency(enabled: boolean, minMs: number, maxMs: number, filter?: string): void {
|
|
42
|
+
MockPartyConnection.latencyEnabled = enabled;
|
|
43
|
+
MockPartyConnection.latencyMinMs = Math.max(0, minMs);
|
|
44
|
+
MockPartyConnection.latencyMaxMs = Math.max(MockPartyConnection.latencyMinMs, maxMs);
|
|
45
|
+
MockPartyConnection.latencyFilter = filter || '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static getLatencyStatus(): { enabled: boolean; minMs: number; maxMs: number; filter: string } {
|
|
49
|
+
return {
|
|
50
|
+
enabled: MockPartyConnection.latencyEnabled,
|
|
51
|
+
minMs: MockPartyConnection.latencyMinMs,
|
|
52
|
+
maxMs: MockPartyConnection.latencyMaxMs,
|
|
53
|
+
filter: MockPartyConnection.latencyFilter
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('Latency Simulation', () => {
|
|
59
|
+
let mockWs: any;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
// Reset latency settings before each test
|
|
63
|
+
MockPartyConnection.configureLatency(false, 0, 0);
|
|
64
|
+
|
|
65
|
+
// Create mock WebSocket
|
|
66
|
+
mockWs = {
|
|
67
|
+
readyState: 1, // WebSocket.OPEN
|
|
68
|
+
send: vi.fn(),
|
|
69
|
+
close: vi.fn()
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Configuration', () => {
|
|
78
|
+
it('should configure latency settings correctly', () => {
|
|
79
|
+
MockPartyConnection.configureLatency(true, 100, 300, 'sync');
|
|
80
|
+
|
|
81
|
+
const status = MockPartyConnection.getLatencyStatus();
|
|
82
|
+
expect(status.enabled).toBe(true);
|
|
83
|
+
expect(status.minMs).toBe(100);
|
|
84
|
+
expect(status.maxMs).toBe(300);
|
|
85
|
+
expect(status.filter).toBe('sync');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should clamp minMs to 0', () => {
|
|
89
|
+
MockPartyConnection.configureLatency(true, -50, 200);
|
|
90
|
+
|
|
91
|
+
const status = MockPartyConnection.getLatencyStatus();
|
|
92
|
+
expect(status.minMs).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should ensure maxMs is at least minMs', () => {
|
|
96
|
+
MockPartyConnection.configureLatency(true, 200, 100);
|
|
97
|
+
|
|
98
|
+
const status = MockPartyConnection.getLatencyStatus();
|
|
99
|
+
expect(status.maxMs).toBe(200); // Should be set to minMs value
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should disable latency when enabled is false', () => {
|
|
103
|
+
MockPartyConnection.configureLatency(false, 100, 300);
|
|
104
|
+
|
|
105
|
+
const status = MockPartyConnection.getLatencyStatus();
|
|
106
|
+
expect(status.enabled).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Message Sending', () => {
|
|
111
|
+
it('should send message immediately when latency is disabled', async () => {
|
|
112
|
+
const connection = new MockPartyConnection(mockWs);
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
|
|
115
|
+
await connection.send('test message');
|
|
116
|
+
|
|
117
|
+
const endTime = Date.now();
|
|
118
|
+
expect(mockWs.send).toHaveBeenCalledWith('test message');
|
|
119
|
+
expect(endTime - startTime).toBeLessThan(10); // Should be almost immediate
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should delay message when latency is enabled', async () => {
|
|
123
|
+
MockPartyConnection.configureLatency(true, 50, 100);
|
|
124
|
+
const connection = new MockPartyConnection(mockWs);
|
|
125
|
+
const startTime = Date.now();
|
|
126
|
+
|
|
127
|
+
await connection.send('test message');
|
|
128
|
+
|
|
129
|
+
const endTime = Date.now();
|
|
130
|
+
expect(mockWs.send).toHaveBeenCalledWith('test message');
|
|
131
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(50);
|
|
132
|
+
expect(endTime - startTime).toBeLessThanOrEqual(150); // Allow some buffer
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should apply filter correctly', async () => {
|
|
136
|
+
MockPartyConnection.configureLatency(true, 50, 100, 'sync');
|
|
137
|
+
const connection = new MockPartyConnection(mockWs);
|
|
138
|
+
|
|
139
|
+
// Message with filter should be delayed
|
|
140
|
+
const startTime1 = Date.now();
|
|
141
|
+
await connection.send('sync message');
|
|
142
|
+
const endTime1 = Date.now();
|
|
143
|
+
expect(endTime1 - startTime1).toBeGreaterThanOrEqual(50);
|
|
144
|
+
|
|
145
|
+
// Message without filter should be sent immediately
|
|
146
|
+
const startTime2 = Date.now();
|
|
147
|
+
await connection.send('normal message');
|
|
148
|
+
const endTime2 = Date.now();
|
|
149
|
+
expect(endTime2 - startTime2).toBeLessThan(10);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should not send when WebSocket is not open', async () => {
|
|
153
|
+
mockWs.readyState = 3; // WebSocket.CLOSED
|
|
154
|
+
const connection = new MockPartyConnection(mockWs);
|
|
155
|
+
|
|
156
|
+
await connection.send('test message');
|
|
157
|
+
|
|
158
|
+
expect(mockWs.send).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle JSON data correctly', async () => {
|
|
162
|
+
const connection = new MockPartyConnection(mockWs);
|
|
163
|
+
const testData = { type: 'test', value: 123 };
|
|
164
|
+
|
|
165
|
+
await connection.send(testData);
|
|
166
|
+
|
|
167
|
+
expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify(testData));
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('Edge Cases', () => {
|
|
172
|
+
it('should handle zero latency range', async () => {
|
|
173
|
+
MockPartyConnection.configureLatency(true, 0, 0);
|
|
174
|
+
const connection = new MockPartyConnection(mockWs);
|
|
175
|
+
const startTime = Date.now();
|
|
176
|
+
|
|
177
|
+
await connection.send('test message');
|
|
178
|
+
|
|
179
|
+
const endTime = Date.now();
|
|
180
|
+
expect(mockWs.send).toHaveBeenCalledWith('test message');
|
|
181
|
+
expect(endTime - startTime).toBeLessThan(10); // Should be immediate
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle very high latency', async () => {
|
|
185
|
+
MockPartyConnection.configureLatency(true, 1000, 2000);
|
|
186
|
+
const connection = new MockPartyConnection(mockWs);
|
|
187
|
+
const startTime = Date.now();
|
|
188
|
+
|
|
189
|
+
await connection.send('test message');
|
|
190
|
+
|
|
191
|
+
const endTime = Date.now();
|
|
192
|
+
expect(mockWs.send).toHaveBeenCalledWith('test message');
|
|
193
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(1000);
|
|
194
|
+
expect(endTime - startTime).toBeLessThanOrEqual(2100); // Allow buffer
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle empty filter string', async () => {
|
|
198
|
+
MockPartyConnection.configureLatency(true, 50, 100, '');
|
|
199
|
+
const connection = new MockPartyConnection(mockWs);
|
|
200
|
+
const startTime = Date.now();
|
|
201
|
+
|
|
202
|
+
await connection.send('test message');
|
|
203
|
+
|
|
204
|
+
const endTime = Date.now();
|
|
205
|
+
expect(mockWs.send).toHaveBeenCalledWith('test message');
|
|
206
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(50); // Should still be delayed
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|